assistme 0.3.1 → 0.3.3
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/PLAN.md +14 -3
- package/dist/{chunk-UWE5WVQI.js → chunk-KX7ITO55.js} +20 -11
- package/dist/index.js +1889 -583
- package/dist/{job-runner-N4XAAWLJ.js → job-runner-P2L6MOOX.js} +1 -1
- package/package.json +5 -3
- package/src/agent/job-runner.ts +9 -13
- package/src/agent/mcp-servers.ts +6 -952
- package/src/agent/memory.ts +2 -11
- package/src/agent/processor.ts +25 -108
- package/src/agent/scheduler.ts +2 -3
- package/src/agent/session.ts +20 -36
- package/src/agent/skills.ts +167 -61
- package/src/agent/system-prompt.ts +126 -0
- package/src/browser/chrome-launcher.ts +557 -0
- package/src/browser/controller.ts +1448 -0
- package/src/browser/types.ts +76 -0
- package/src/commands/credential.ts +190 -0
- package/src/commands/job.ts +14 -45
- package/src/commands/memory.ts +16 -29
- package/src/commands/schedule.ts +15 -37
- package/src/commands/start.ts +11 -43
- package/src/credentials/credential-store.test.ts +162 -0
- package/src/credentials/credential-store.ts +266 -0
- package/src/credentials/encryption.test.ts +98 -0
- package/src/credentials/encryption.ts +82 -0
- package/src/credentials/index.ts +15 -0
- package/src/credentials/local-store.ts +89 -0
- package/src/db/action.ts +19 -0
- package/src/db/api-client.ts +3 -32
- package/src/db/auth-store.ts +41 -0
- package/src/db/auth.ts +38 -0
- package/src/db/conversation.ts +39 -0
- package/src/db/event.ts +52 -0
- package/src/db/job-poll.ts +18 -0
- package/src/db/session.ts +60 -0
- package/src/db/supabase.ts +40 -383
- package/src/db/task.ts +69 -0
- package/src/db/types.ts +54 -0
- package/src/index.ts +2 -0
- package/src/mcp/agent-tools-server.ts +1047 -0
- package/src/mcp/browser-server.ts +241 -0
- package/src/tools/browser.ts +29 -1208
- package/src/tools/index.ts +31 -265
- package/src/tools/web.ts +0 -73
package/dist/index.js
CHANGED
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
callMcpHandler,
|
|
5
5
|
log,
|
|
6
6
|
newCorrelationId,
|
|
7
|
+
readAuthStore,
|
|
7
8
|
setCorrelationId,
|
|
8
|
-
setLogLevel
|
|
9
|
-
|
|
9
|
+
setLogLevel,
|
|
10
|
+
writeAuthStore
|
|
11
|
+
} from "./chunk-KX7ITO55.js";
|
|
10
12
|
import {
|
|
11
13
|
clearConfig,
|
|
12
14
|
getConfig,
|
|
@@ -24,35 +26,10 @@ import chalk from "chalk";
|
|
|
24
26
|
import ora from "ora";
|
|
25
27
|
import { createInterface } from "readline";
|
|
26
28
|
|
|
27
|
-
// src/db/
|
|
28
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
29
|
-
import { join } from "path";
|
|
30
|
-
import { homedir } from "os";
|
|
31
|
-
var AUTH_DIR = join(homedir(), ".config", "assistme");
|
|
32
|
-
var AUTH_FILE = join(AUTH_DIR, "auth.json");
|
|
33
|
-
function ensureAuthDir() {
|
|
34
|
-
if (!existsSync(AUTH_DIR)) {
|
|
35
|
-
mkdirSync(AUTH_DIR, { recursive: true, mode: 448 });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
function readAuthStore() {
|
|
39
|
-
try {
|
|
40
|
-
if (existsSync(AUTH_FILE)) {
|
|
41
|
-
return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
|
|
42
|
-
}
|
|
43
|
-
} catch {
|
|
44
|
-
}
|
|
45
|
-
return {};
|
|
46
|
-
}
|
|
47
|
-
function writeAuthStore(data) {
|
|
48
|
-
ensureAuthDir();
|
|
49
|
-
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
50
|
-
}
|
|
29
|
+
// src/db/auth.ts
|
|
51
30
|
async function loginWithToken(mcpToken) {
|
|
52
31
|
if (!mcpToken.startsWith("am_")) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
"Invalid token format. Use an am_ token from the web page."
|
|
55
|
-
);
|
|
32
|
+
throw new Error("Invalid token format. Use an am_ token from the web page.");
|
|
56
33
|
}
|
|
57
34
|
const result = await callMcpHandler(
|
|
58
35
|
"auth.validate_token",
|
|
@@ -65,9 +42,7 @@ async function loginWithToken(mcpToken) {
|
|
|
65
42
|
return result.user_id;
|
|
66
43
|
}
|
|
67
44
|
async function getCurrentUserId() {
|
|
68
|
-
const result = await callMcpHandler(
|
|
69
|
-
"auth.validate_token"
|
|
70
|
-
);
|
|
45
|
+
const result = await callMcpHandler("auth.validate_token");
|
|
71
46
|
return result.user_id;
|
|
72
47
|
}
|
|
73
48
|
async function logout() {
|
|
@@ -76,7 +51,9 @@ async function logout() {
|
|
|
76
51
|
} catch {
|
|
77
52
|
}
|
|
78
53
|
}
|
|
79
|
-
|
|
54
|
+
|
|
55
|
+
// src/db/session.ts
|
|
56
|
+
async function createSession(sessionName, workspacePath, version2) {
|
|
80
57
|
const { getConfig: getConfig2 } = await import("./config-PUIS2TQL.js");
|
|
81
58
|
const data = await callMcpHandler("session.create", {
|
|
82
59
|
session_name: sessionName,
|
|
@@ -108,10 +85,10 @@ async function setSessionBusy(sessionId, busy) {
|
|
|
108
85
|
}
|
|
109
86
|
async function cleanupStaleSessions(currentSessionId, thresholdMs = 12e4) {
|
|
110
87
|
try {
|
|
111
|
-
const result = await callMcpHandler(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
88
|
+
const result = await callMcpHandler("session.cleanup_stale", {
|
|
89
|
+
current_session_id: currentSessionId,
|
|
90
|
+
threshold_ms: thresholdMs
|
|
91
|
+
});
|
|
115
92
|
return result.cleaned;
|
|
116
93
|
} catch {
|
|
117
94
|
return 0;
|
|
@@ -120,11 +97,9 @@ async function cleanupStaleSessions(currentSessionId, thresholdMs = 12e4) {
|
|
|
120
97
|
async function getActiveSessions(limit = 5) {
|
|
121
98
|
return callMcpHandler("session.get_active", { limit });
|
|
122
99
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
async function createTask(conversationId, _userId, sessionId, prompt) {
|
|
100
|
+
|
|
101
|
+
// src/db/task.ts
|
|
102
|
+
async function createTask(conversationId, sessionId, prompt) {
|
|
128
103
|
const data = await callMcpHandler("task.create", {
|
|
129
104
|
conversation_id: conversationId,
|
|
130
105
|
session_id: sessionId,
|
|
@@ -134,10 +109,9 @@ async function createTask(conversationId, _userId, sessionId, prompt) {
|
|
|
134
109
|
}
|
|
135
110
|
async function pollAndClaimTask(sessionId) {
|
|
136
111
|
try {
|
|
137
|
-
const data = await callMcpHandler(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
);
|
|
112
|
+
const data = await callMcpHandler("task.poll_and_claim", {
|
|
113
|
+
session_id: sessionId
|
|
114
|
+
});
|
|
141
115
|
if (!data) return null;
|
|
142
116
|
return {
|
|
143
117
|
...data,
|
|
@@ -171,27 +145,19 @@ async function failTask(messageId, errorMessage) {
|
|
|
171
145
|
log.error(`Failed to update task status: ${err instanceof Error ? err.message : err}`);
|
|
172
146
|
}
|
|
173
147
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return data;
|
|
180
|
-
} catch (err) {
|
|
181
|
-
log.debug(`Job run poll failed: ${err instanceof Error ? err.message : err}`);
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
148
|
+
|
|
149
|
+
// src/db/conversation.ts
|
|
150
|
+
async function getOrCreateCliConversation() {
|
|
151
|
+
const data = await callMcpHandler("conversation.get_or_create");
|
|
152
|
+
return data;
|
|
184
153
|
}
|
|
185
154
|
async function getConversationHistory(conversationId, excludeMessageId, limit = 20) {
|
|
186
155
|
try {
|
|
187
|
-
const rows = await callMcpHandler(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
limit
|
|
193
|
-
}
|
|
194
|
-
);
|
|
156
|
+
const rows = await callMcpHandler("conversation.get_history", {
|
|
157
|
+
conversation_id: conversationId,
|
|
158
|
+
exclude_message_id: excludeMessageId,
|
|
159
|
+
limit
|
|
160
|
+
});
|
|
195
161
|
return (rows || []).reverse().map((row) => {
|
|
196
162
|
const prompt = row.metadata?.prompt || "";
|
|
197
163
|
const content = row.content || "";
|
|
@@ -203,6 +169,8 @@ async function getConversationHistory(conversationId, excludeMessageId, limit =
|
|
|
203
169
|
return [];
|
|
204
170
|
}
|
|
205
171
|
}
|
|
172
|
+
|
|
173
|
+
// src/db/event.ts
|
|
206
174
|
var eventSequence = 0;
|
|
207
175
|
function resetEventSequence() {
|
|
208
176
|
eventSequence = 0;
|
|
@@ -220,6 +188,8 @@ async function emitEvent(messageId, eventType, eventData) {
|
|
|
220
188
|
log.warn(`Failed to emit event: ${err instanceof Error ? err.message : err}`);
|
|
221
189
|
}
|
|
222
190
|
}
|
|
191
|
+
|
|
192
|
+
// src/db/action.ts
|
|
223
193
|
async function setActionRequest(messageId, actionData) {
|
|
224
194
|
await callMcpHandler("action.set_request", {
|
|
225
195
|
message_id: messageId,
|
|
@@ -227,10 +197,20 @@ async function setActionRequest(messageId, actionData) {
|
|
|
227
197
|
});
|
|
228
198
|
}
|
|
229
199
|
async function pollActionResponse(messageId) {
|
|
230
|
-
return callMcpHandler(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
200
|
+
return callMcpHandler("action.poll_response", {
|
|
201
|
+
message_id: messageId
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/db/job-poll.ts
|
|
206
|
+
async function pollAndClaimJobRun() {
|
|
207
|
+
try {
|
|
208
|
+
const data = await callMcpHandler("job.claim_pending_run");
|
|
209
|
+
return data;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
log.debug(`Job run poll failed: ${err instanceof Error ? err.message : err}`);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
234
214
|
}
|
|
235
215
|
|
|
236
216
|
// src/commands/auth.ts
|
|
@@ -248,8 +228,8 @@ function registerAuthCommands(program2) {
|
|
|
248
228
|
console.log();
|
|
249
229
|
try {
|
|
250
230
|
const { exec: exec2 } = await import("child_process");
|
|
251
|
-
const
|
|
252
|
-
const openCmd =
|
|
231
|
+
const platform3 = process.platform;
|
|
232
|
+
const openCmd = platform3 === "darwin" ? `open "${tokenPageUrl}"` : platform3 === "win32" ? `start "${tokenPageUrl}"` : `xdg-open "${tokenPageUrl}" 2>/dev/null || echo ""`;
|
|
253
233
|
exec2(openCmd);
|
|
254
234
|
console.log(chalk.dim(" (Browser opened automatically)"));
|
|
255
235
|
console.log();
|
|
@@ -355,12 +335,9 @@ function registerConfigCommands(program2) {
|
|
|
355
335
|
import chalk3 from "chalk";
|
|
356
336
|
import ora2 from "ora";
|
|
357
337
|
|
|
358
|
-
// src/
|
|
338
|
+
// src/browser/controller.ts
|
|
359
339
|
import { WebSocket } from "ws";
|
|
360
|
-
import {
|
|
361
|
-
import { platform, homedir as homedir2 } from "os";
|
|
362
|
-
import { existsSync as existsSync2, unlinkSync, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
363
|
-
import { join as join2 } from "path";
|
|
340
|
+
import { platform } from "os";
|
|
364
341
|
var BrowserController = class {
|
|
365
342
|
ws = null;
|
|
366
343
|
debugPort;
|
|
@@ -368,6 +345,7 @@ var BrowserController = class {
|
|
|
368
345
|
callbacks = /* @__PURE__ */ new Map();
|
|
369
346
|
connected = false;
|
|
370
347
|
currentTabId = null;
|
|
348
|
+
refCache = /* @__PURE__ */ new Map();
|
|
371
349
|
constructor(port = 9222) {
|
|
372
350
|
this.debugPort = port;
|
|
373
351
|
}
|
|
@@ -515,11 +493,20 @@ URL: ${info.url}`;
|
|
|
515
493
|
}
|
|
516
494
|
async goBack() {
|
|
517
495
|
this.ensureConnected();
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
496
|
+
try {
|
|
497
|
+
const history = await this.send("Page.getNavigationHistory");
|
|
498
|
+
const idx = history.currentIndex ?? 0;
|
|
499
|
+
const entries = history.entries ?? [];
|
|
500
|
+
if (idx > 0 && entries[idx - 1]) {
|
|
501
|
+
await this.send("Page.navigateToHistoryEntry", {
|
|
502
|
+
entryId: entries[idx - 1].id
|
|
503
|
+
});
|
|
504
|
+
} else {
|
|
505
|
+
await this.evaluate("window.history.back()");
|
|
506
|
+
}
|
|
507
|
+
} catch {
|
|
508
|
+
await this.evaluate("window.history.back()");
|
|
509
|
+
}
|
|
523
510
|
await this.waitForLoad();
|
|
524
511
|
const info = await this.getPageInfo();
|
|
525
512
|
return `Went back to: ${info.title}`;
|
|
@@ -602,8 +589,35 @@ URL: ${info.url}`;
|
|
|
602
589
|
const result = await this.send("Runtime.evaluate", {
|
|
603
590
|
expression: `
|
|
604
591
|
(function() {
|
|
605
|
-
|
|
606
|
-
|
|
592
|
+
var sel = ${selectorJS};
|
|
593
|
+
|
|
594
|
+
// Support :contains('text') pseudo-selector (not native CSS)
|
|
595
|
+
var containsMatch = sel.match(/^(.+?)?:contains\\(['"](.+?)['"]\\)$/);
|
|
596
|
+
if (containsMatch) {
|
|
597
|
+
var baseTag = (containsMatch[1] || '*').toLowerCase();
|
|
598
|
+
var searchText = containsMatch[2];
|
|
599
|
+
var candidates = document.querySelectorAll(baseTag === '*' ? '*' : baseTag);
|
|
600
|
+
var found = null;
|
|
601
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
602
|
+
var c = candidates[i];
|
|
603
|
+
// Prefer exact text match on direct text content (not children)
|
|
604
|
+
var directText = Array.from(c.childNodes)
|
|
605
|
+
.filter(function(n) { return n.nodeType === 3; })
|
|
606
|
+
.map(function(n) { return n.textContent.trim(); })
|
|
607
|
+
.join(' ');
|
|
608
|
+
if (directText === searchText || c.textContent.trim() === searchText) {
|
|
609
|
+
// Prefer the deepest (most specific) matching element
|
|
610
|
+
if (!found || found.contains(c)) found = c;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (!found) return 'Element not found: ' + sel;
|
|
614
|
+
found.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
615
|
+
found.click();
|
|
616
|
+
return 'Clicked: ' + (found.tagName || '') + ' ' + (found.textContent || '').slice(0, 50).trim();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
var el = document.querySelector(sel);
|
|
620
|
+
if (!el) return 'Element not found: ' + sel;
|
|
607
621
|
|
|
608
622
|
// Scroll into view
|
|
609
623
|
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
@@ -629,9 +643,23 @@ URL: ${info.url}`;
|
|
|
629
643
|
if (!el) return 'Element not found: ' + ${selectorJS};
|
|
630
644
|
|
|
631
645
|
el.focus();
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
646
|
+
|
|
647
|
+
// Clear existing value
|
|
648
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
649
|
+
window.HTMLInputElement.prototype, 'value'
|
|
650
|
+
)?.set || Object.getOwnPropertyDescriptor(
|
|
651
|
+
window.HTMLTextAreaElement.prototype, 'value'
|
|
652
|
+
)?.set;
|
|
653
|
+
if (nativeInputValueSetter) {
|
|
654
|
+
nativeInputValueSetter.call(el, ${textJS});
|
|
655
|
+
} else {
|
|
656
|
+
el.value = ${textJS};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Dispatch events that frameworks (React, Angular, Material) listen to
|
|
660
|
+
el.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
|
|
661
|
+
el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
|
|
662
|
+
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: ${textJS} }));
|
|
635
663
|
return 'Typed into: ' + (el.tagName || '') + ' [' + (el.name || el.id || '') + ']';
|
|
636
664
|
})()
|
|
637
665
|
`,
|
|
@@ -646,29 +674,80 @@ URL: ${info.url}`;
|
|
|
646
674
|
Tab: { keyCode: 9, code: "Tab" },
|
|
647
675
|
Escape: { keyCode: 27, code: "Escape" },
|
|
648
676
|
Backspace: { keyCode: 8, code: "Backspace" },
|
|
677
|
+
Delete: { keyCode: 46, code: "Delete" },
|
|
649
678
|
ArrowDown: { keyCode: 40, code: "ArrowDown" },
|
|
650
|
-
ArrowUp: { keyCode: 38, code: "ArrowUp" }
|
|
679
|
+
ArrowUp: { keyCode: 38, code: "ArrowUp" },
|
|
680
|
+
ArrowLeft: { keyCode: 37, code: "ArrowLeft" },
|
|
681
|
+
ArrowRight: { keyCode: 39, code: "ArrowRight" },
|
|
682
|
+
Home: { keyCode: 36, code: "Home" },
|
|
683
|
+
End: { keyCode: 35, code: "End" },
|
|
684
|
+
Space: { keyCode: 32, code: "Space" }
|
|
685
|
+
};
|
|
686
|
+
const modifierMap = {
|
|
687
|
+
Alt: 1,
|
|
688
|
+
Control: 2,
|
|
689
|
+
Meta: 4,
|
|
690
|
+
Shift: 8
|
|
651
691
|
};
|
|
652
|
-
const
|
|
692
|
+
const parts = key.split("+");
|
|
693
|
+
let modifiers = 0;
|
|
694
|
+
let actualKey = parts[parts.length - 1];
|
|
695
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
696
|
+
const mod = modifierMap[parts[i]];
|
|
697
|
+
if (mod) modifiers |= mod;
|
|
698
|
+
}
|
|
699
|
+
const mapped = keyMap[actualKey];
|
|
653
700
|
if (mapped) {
|
|
654
701
|
await this.send("Input.dispatchKeyEvent", {
|
|
655
702
|
type: "keyDown",
|
|
656
|
-
key,
|
|
703
|
+
key: actualKey,
|
|
657
704
|
code: mapped.code,
|
|
658
705
|
windowsVirtualKeyCode: mapped.keyCode,
|
|
659
|
-
nativeVirtualKeyCode: mapped.keyCode
|
|
706
|
+
nativeVirtualKeyCode: mapped.keyCode,
|
|
707
|
+
modifiers
|
|
660
708
|
});
|
|
661
709
|
await this.send("Input.dispatchKeyEvent", {
|
|
662
710
|
type: "keyUp",
|
|
663
|
-
key,
|
|
711
|
+
key: actualKey,
|
|
664
712
|
code: mapped.code,
|
|
665
713
|
windowsVirtualKeyCode: mapped.keyCode,
|
|
666
|
-
nativeVirtualKeyCode: mapped.keyCode
|
|
714
|
+
nativeVirtualKeyCode: mapped.keyCode,
|
|
715
|
+
modifiers
|
|
716
|
+
});
|
|
717
|
+
} else if (actualKey.length === 1) {
|
|
718
|
+
const code = `Key${actualKey.toUpperCase()}`;
|
|
719
|
+
const keyCode = actualKey.toUpperCase().charCodeAt(0);
|
|
720
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
721
|
+
type: "keyDown",
|
|
722
|
+
key: actualKey,
|
|
723
|
+
code,
|
|
724
|
+
windowsVirtualKeyCode: keyCode,
|
|
725
|
+
nativeVirtualKeyCode: keyCode,
|
|
726
|
+
modifiers
|
|
727
|
+
});
|
|
728
|
+
if (!modifiers) {
|
|
729
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
730
|
+
type: "char",
|
|
731
|
+
text: actualKey,
|
|
732
|
+
modifiers
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
736
|
+
type: "keyUp",
|
|
737
|
+
key: actualKey,
|
|
738
|
+
code,
|
|
739
|
+
modifiers
|
|
667
740
|
});
|
|
668
741
|
} else {
|
|
669
742
|
await this.send("Input.dispatchKeyEvent", {
|
|
670
|
-
type: "
|
|
671
|
-
|
|
743
|
+
type: "keyDown",
|
|
744
|
+
key: actualKey,
|
|
745
|
+
modifiers
|
|
746
|
+
});
|
|
747
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
748
|
+
type: "keyUp",
|
|
749
|
+
key: actualKey,
|
|
750
|
+
modifiers
|
|
672
751
|
});
|
|
673
752
|
}
|
|
674
753
|
return `Pressed key: ${key}`;
|
|
@@ -689,6 +768,670 @@ URL: ${info.url}`;
|
|
|
689
768
|
await new Promise((r) => setTimeout(r, 300));
|
|
690
769
|
return "Scrolled up.";
|
|
691
770
|
}
|
|
771
|
+
// ── Annotated Snapshot (ref system) ─────────────────────────────
|
|
772
|
+
/**
|
|
773
|
+
* Take a snapshot of all interactive elements on the page.
|
|
774
|
+
*
|
|
775
|
+
* Strategy (informed by research — arxiv:2511.19477):
|
|
776
|
+
* - **Text ref table is ALWAYS returned** — compact, low-token, works for
|
|
777
|
+
* all page complexities including dense layouts (date pickers, tables).
|
|
778
|
+
* - **Annotated screenshot is OPTIONAL** (annotate parameter):
|
|
779
|
+
* - true: overlay ref badges on screenshot (best for simple pages with
|
|
780
|
+
* few interactive elements — gives visual context)
|
|
781
|
+
* - false: plain screenshot without overlays (default — avoids label
|
|
782
|
+
* clutter on dense pages; model still sees the page visually)
|
|
783
|
+
* - Research shows text-based grounding outperforms visual annotations
|
|
784
|
+
* on complex pages, and the hybrid approach (a11y text primary +
|
|
785
|
+
* selective vision) achieves ~85% vs ~50% for pure vision.
|
|
786
|
+
*/
|
|
787
|
+
async snapshot(annotate = false) {
|
|
788
|
+
this.ensureConnected();
|
|
789
|
+
await this.waitForLoad(5e3);
|
|
790
|
+
const findResult = await this.send("Runtime.evaluate", {
|
|
791
|
+
expression: `
|
|
792
|
+
(function() {
|
|
793
|
+
// Clean up previous refs
|
|
794
|
+
document.querySelectorAll('[data-assistme-ref]').forEach(function(el) {
|
|
795
|
+
el.removeAttribute('data-assistme-ref');
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
var selectors = [
|
|
799
|
+
'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
|
|
800
|
+
'[role="button"]', '[role="link"]', '[role="checkbox"]', '[role="radio"]',
|
|
801
|
+
'[role="combobox"]', '[role="listbox"]', '[role="menuitem"]', '[role="tab"]',
|
|
802
|
+
'[role="switch"]', '[role="slider"]', '[role="option"]', '[role="searchbox"]',
|
|
803
|
+
'[onclick]', '[tabindex]:not([tabindex="-1"])',
|
|
804
|
+
'[contenteditable="true"]'
|
|
805
|
+
].join(', ');
|
|
806
|
+
|
|
807
|
+
// Collect elements from main document AND same-origin iframes
|
|
808
|
+
var all = Array.from(document.querySelectorAll(selectors));
|
|
809
|
+
try {
|
|
810
|
+
var iframes = document.querySelectorAll('iframe');
|
|
811
|
+
for (var fi = 0; fi < iframes.length; fi++) {
|
|
812
|
+
try {
|
|
813
|
+
var iframeDoc = iframes[fi].contentDocument;
|
|
814
|
+
if (iframeDoc) {
|
|
815
|
+
var iframeRect = iframes[fi].getBoundingClientRect();
|
|
816
|
+
var iframeEls = iframeDoc.querySelectorAll(selectors);
|
|
817
|
+
for (var fe = 0; fe < iframeEls.length; fe++) {
|
|
818
|
+
// Tag iframe elements with offset for coordinate correction
|
|
819
|
+
iframeEls[fe].__iframeOffset = { x: iframeRect.x, y: iframeRect.y };
|
|
820
|
+
all.push(iframeEls[fe]);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
} catch(e) { /* cross-origin iframe, skip */ }
|
|
824
|
+
}
|
|
825
|
+
} catch(e) { /* iframe enumeration failed, continue */ }
|
|
826
|
+
|
|
827
|
+
var refs = [];
|
|
828
|
+
var vh = window.innerHeight;
|
|
829
|
+
var vw = window.innerWidth;
|
|
830
|
+
|
|
831
|
+
for (var i = 0; i < all.length && refs.length < 80; i++) {
|
|
832
|
+
var el = all[i];
|
|
833
|
+
var rect = el.getBoundingClientRect();
|
|
834
|
+
|
|
835
|
+
// Skip invisible / tiny elements
|
|
836
|
+
if (rect.width < 5 || rect.height < 5) continue;
|
|
837
|
+
var style = window.getComputedStyle(el);
|
|
838
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
|
|
839
|
+
|
|
840
|
+
// Skip elements far outside viewport
|
|
841
|
+
if (rect.bottom < -50 || rect.top > vh + 50) continue;
|
|
842
|
+
if (rect.right < -50 || rect.left > vw + 50) continue;
|
|
843
|
+
|
|
844
|
+
// Determine role
|
|
845
|
+
var role = el.getAttribute('role') || '';
|
|
846
|
+
if (!role) {
|
|
847
|
+
var tag = el.tagName.toLowerCase();
|
|
848
|
+
if (tag === 'a') role = 'link';
|
|
849
|
+
else if (tag === 'button') role = 'button';
|
|
850
|
+
else if (tag === 'input') {
|
|
851
|
+
var t = (el.type || 'text').toLowerCase();
|
|
852
|
+
if (t === 'checkbox') role = 'checkbox';
|
|
853
|
+
else if (t === 'radio') role = 'radio';
|
|
854
|
+
else if (t === 'submit' || t === 'button') role = 'button';
|
|
855
|
+
else role = 'textbox';
|
|
856
|
+
}
|
|
857
|
+
else if (tag === 'select') role = 'combobox';
|
|
858
|
+
else if (tag === 'textarea') role = 'textbox';
|
|
859
|
+
else role = tag;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Determine accessible name
|
|
863
|
+
var name = '';
|
|
864
|
+
var ariaLabel = el.getAttribute('aria-label');
|
|
865
|
+
var ariaLabelledBy = el.getAttribute('aria-labelledby');
|
|
866
|
+
if (ariaLabel) {
|
|
867
|
+
name = ariaLabel;
|
|
868
|
+
} else if (ariaLabelledBy) {
|
|
869
|
+
var labelEl = document.getElementById(ariaLabelledBy);
|
|
870
|
+
if (labelEl) name = labelEl.textContent.trim();
|
|
871
|
+
} else if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
|
872
|
+
if (el.id) {
|
|
873
|
+
var lbl = document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
|
|
874
|
+
if (lbl) name = lbl.textContent.trim();
|
|
875
|
+
}
|
|
876
|
+
if (!name) name = el.getAttribute('placeholder') || el.getAttribute('name') || '';
|
|
877
|
+
} else {
|
|
878
|
+
name = (el.textContent || '').trim().slice(0, 60);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
var refId = refs.length + 1;
|
|
882
|
+
el.setAttribute('data-assistme-ref', String(refId));
|
|
883
|
+
|
|
884
|
+
// Correct coordinates for elements inside iframes
|
|
885
|
+
var offsetX = el.__iframeOffset ? el.__iframeOffset.x : 0;
|
|
886
|
+
var offsetY = el.__iframeOffset ? el.__iframeOffset.y : 0;
|
|
887
|
+
|
|
888
|
+
refs.push({
|
|
889
|
+
id: refId,
|
|
890
|
+
role: role,
|
|
891
|
+
name: name,
|
|
892
|
+
tag: el.tagName.toLowerCase(),
|
|
893
|
+
type: el.getAttribute('type') || '',
|
|
894
|
+
box: {
|
|
895
|
+
x: Math.round(rect.x + offsetX),
|
|
896
|
+
y: Math.round(rect.y + offsetY),
|
|
897
|
+
width: Math.round(rect.width),
|
|
898
|
+
height: Math.round(rect.height)
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return JSON.stringify(refs);
|
|
904
|
+
})()
|
|
905
|
+
`,
|
|
906
|
+
returnByValue: true
|
|
907
|
+
});
|
|
908
|
+
const refs = JSON.parse(
|
|
909
|
+
findResult.result?.value || "[]"
|
|
910
|
+
).map((r) => ({
|
|
911
|
+
id: r.id,
|
|
912
|
+
role: r.role,
|
|
913
|
+
name: r.name,
|
|
914
|
+
tag: r.tag,
|
|
915
|
+
inputType: r.type || "",
|
|
916
|
+
box: r.box
|
|
917
|
+
}));
|
|
918
|
+
if (annotate && refs.length <= 40) {
|
|
919
|
+
const refsJson = JSON.stringify(refs);
|
|
920
|
+
await this.send("Runtime.evaluate", {
|
|
921
|
+
expression: `
|
|
922
|
+
(function() {
|
|
923
|
+
var old = document.getElementById('__assistme_refs__');
|
|
924
|
+
if (old) old.remove();
|
|
925
|
+
|
|
926
|
+
var overlay = document.createElement('div');
|
|
927
|
+
overlay.id = '__assistme_refs__';
|
|
928
|
+
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;';
|
|
929
|
+
|
|
930
|
+
var refs = ${refsJson};
|
|
931
|
+
var vh = window.innerHeight;
|
|
932
|
+
var vw = window.innerWidth;
|
|
933
|
+
|
|
934
|
+
for (var i = 0; i < refs.length; i++) {
|
|
935
|
+
var b = refs[i].box;
|
|
936
|
+
if (b.y + b.height < 0 || b.y > vh || b.x + b.width < 0 || b.x > vw) continue;
|
|
937
|
+
|
|
938
|
+
// Red badge with ref number
|
|
939
|
+
var badge = document.createElement('div');
|
|
940
|
+
var badgeTop = Math.max(0, b.y - 14);
|
|
941
|
+
var badgeLeft = Math.max(0, b.x);
|
|
942
|
+
badge.style.cssText = 'position:fixed;background:#e8384f;color:#fff;font:bold 10px/1.2 monospace;padding:1px 3px;border-radius:2px;white-space:nowrap;'
|
|
943
|
+
+ 'left:' + badgeLeft + 'px;top:' + badgeTop + 'px;';
|
|
944
|
+
badge.textContent = String(refs[i].id);
|
|
945
|
+
overlay.appendChild(badge);
|
|
946
|
+
|
|
947
|
+
// Border around element
|
|
948
|
+
var border = document.createElement('div');
|
|
949
|
+
border.style.cssText = 'position:fixed;border:1.5px solid #e8384f;border-radius:2px;'
|
|
950
|
+
+ 'left:' + b.x + 'px;top:' + b.y + 'px;width:' + b.width + 'px;height:' + b.height + 'px;';
|
|
951
|
+
overlay.appendChild(border);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
document.documentElement.appendChild(overlay);
|
|
955
|
+
})()
|
|
956
|
+
`
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
const image = await this.screenshot();
|
|
960
|
+
if (annotate) {
|
|
961
|
+
await this.send("Runtime.evaluate", {
|
|
962
|
+
expression: `(function() { var el = document.getElementById('__assistme_refs__'); if (el) el.remove(); })()`
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
this.refCache.clear();
|
|
966
|
+
for (const ref of refs) {
|
|
967
|
+
this.refCache.set(ref.id, ref);
|
|
968
|
+
}
|
|
969
|
+
const pageInfo = await this.getPageInfo();
|
|
970
|
+
return { image, refs, url: pageInfo.url, title: pageInfo.title };
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Build a compact text table of refs for the model.
|
|
974
|
+
*/
|
|
975
|
+
static formatRefTable(result) {
|
|
976
|
+
let table = `Page: ${result.title}
|
|
977
|
+
URL: ${result.url}
|
|
978
|
+
|
|
979
|
+
Refs:
|
|
980
|
+
`;
|
|
981
|
+
for (const ref of result.refs) {
|
|
982
|
+
const extra = ref.inputType ? ` (${ref.inputType})` : "";
|
|
983
|
+
const nameStr = ref.name ? ` "${ref.name}"` : "";
|
|
984
|
+
table += `[${ref.id}] ${ref.role}${nameStr}${extra}
|
|
985
|
+
`;
|
|
986
|
+
}
|
|
987
|
+
if (result.refs.length === 0) {
|
|
988
|
+
table += "(no interactive elements found)\n";
|
|
989
|
+
}
|
|
990
|
+
return table;
|
|
991
|
+
}
|
|
992
|
+
// ── Ref Resolution ────────────────────────────────────────────────
|
|
993
|
+
/**
|
|
994
|
+
* Resolve a ref ID to its current center coordinates in the viewport.
|
|
995
|
+
* Uses two strategies:
|
|
996
|
+
* 1. Fast: find by data-assistme-ref attribute (set during snapshot)
|
|
997
|
+
* 2. Stable: search by role + accessible name (survives DOM changes)
|
|
998
|
+
*
|
|
999
|
+
* Includes actionability checks (like Playwright):
|
|
1000
|
+
* - Element must be visible (not display:none, not zero-size)
|
|
1001
|
+
* - Element must be in viewport (scrolls into view if needed)
|
|
1002
|
+
* - Element must not be covered by another element (checks elementFromPoint)
|
|
1003
|
+
*
|
|
1004
|
+
* Returns null if the element cannot be found or is not actionable.
|
|
1005
|
+
* Returns { error: string } if found but not actionable (for diagnostics).
|
|
1006
|
+
*/
|
|
1007
|
+
async resolveRef(refId) {
|
|
1008
|
+
const cached = this.refCache.get(refId);
|
|
1009
|
+
const role = cached?.role || "";
|
|
1010
|
+
const name = cached?.name || "";
|
|
1011
|
+
const roleJS = JSON.stringify(role);
|
|
1012
|
+
const nameJS = JSON.stringify(name);
|
|
1013
|
+
const result = await this.send("Runtime.evaluate", {
|
|
1014
|
+
expression: `
|
|
1015
|
+
(function() {
|
|
1016
|
+
var refId = ${refId};
|
|
1017
|
+
var role = ${roleJS};
|
|
1018
|
+
var name = ${nameJS};
|
|
1019
|
+
|
|
1020
|
+
// Strategy 1: data attribute (fast, from last snapshot)
|
|
1021
|
+
var el = document.querySelector('[data-assistme-ref="' + refId + '"]');
|
|
1022
|
+
|
|
1023
|
+
// Strategy 2: role + name search (stable, survives DOM changes)
|
|
1024
|
+
if (!el && role && name) {
|
|
1025
|
+
var selectorMap = {
|
|
1026
|
+
textbox: 'input, textarea, [role="textbox"], [role="searchbox"]',
|
|
1027
|
+
button: 'button, [role="button"], input[type="submit"], input[type="button"]',
|
|
1028
|
+
link: 'a[href], [role="link"]',
|
|
1029
|
+
combobox: 'select, [role="combobox"]',
|
|
1030
|
+
checkbox: 'input[type="checkbox"], [role="checkbox"]',
|
|
1031
|
+
radio: 'input[type="radio"], [role="radio"]',
|
|
1032
|
+
tab: '[role="tab"]',
|
|
1033
|
+
menuitem: '[role="menuitem"]',
|
|
1034
|
+
option: '[role="option"], option',
|
|
1035
|
+
};
|
|
1036
|
+
var sel = selectorMap[role] || '*[role="' + role + '"]';
|
|
1037
|
+
var candidates = document.querySelectorAll(sel);
|
|
1038
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
1039
|
+
var c = candidates[i];
|
|
1040
|
+
var cName = c.getAttribute('aria-label')
|
|
1041
|
+
|| c.getAttribute('placeholder')
|
|
1042
|
+
|| (c.textContent || '').trim().slice(0, 60);
|
|
1043
|
+
if (cName === name) { el = c; break; }
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (!el) return 'null';
|
|
1048
|
+
|
|
1049
|
+
// \u2500\u2500 Actionability checks (Playwright-style) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1050
|
+
|
|
1051
|
+
// Check visibility
|
|
1052
|
+
var style = window.getComputedStyle(el);
|
|
1053
|
+
if (style.display === 'none')
|
|
1054
|
+
return JSON.stringify({ error: 'Element is hidden (display:none)' });
|
|
1055
|
+
if (style.visibility === 'hidden')
|
|
1056
|
+
return JSON.stringify({ error: 'Element is hidden (visibility:hidden)' });
|
|
1057
|
+
if (parseFloat(style.opacity) < 0.05)
|
|
1058
|
+
return JSON.stringify({ error: 'Element is hidden (opacity:0)' });
|
|
1059
|
+
|
|
1060
|
+
// Check disabled
|
|
1061
|
+
if (el.disabled || el.getAttribute('aria-disabled') === 'true')
|
|
1062
|
+
return JSON.stringify({ error: 'Element is disabled' });
|
|
1063
|
+
|
|
1064
|
+
// Scroll into view
|
|
1065
|
+
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
1066
|
+
var r = el.getBoundingClientRect();
|
|
1067
|
+
|
|
1068
|
+
// Check non-zero size
|
|
1069
|
+
if (r.width < 1 || r.height < 1)
|
|
1070
|
+
return JSON.stringify({ error: 'Element has zero size (' + r.width + 'x' + r.height + ')' });
|
|
1071
|
+
|
|
1072
|
+
// Check element is in viewport
|
|
1073
|
+
if (r.bottom < 0 || r.top > window.innerHeight || r.right < 0 || r.left > window.innerWidth)
|
|
1074
|
+
return JSON.stringify({ error: 'Element is outside viewport after scroll' });
|
|
1075
|
+
|
|
1076
|
+
var cx = r.x + r.width / 2;
|
|
1077
|
+
var cy = r.y + r.height / 2;
|
|
1078
|
+
|
|
1079
|
+
// Check not covered by another element (hit test)
|
|
1080
|
+
var topEl = document.elementFromPoint(cx, cy);
|
|
1081
|
+
if (topEl && topEl !== el && !el.contains(topEl) && !topEl.closest('[data-assistme-ref="' + refId + '"]')) {
|
|
1082
|
+
// Check if the covering element is the overlay (ignore it)
|
|
1083
|
+
if (!topEl.closest('#__assistme_refs__')) {
|
|
1084
|
+
var coverTag = topEl.tagName.toLowerCase();
|
|
1085
|
+
var coverText = (topEl.textContent || '').trim().slice(0, 30);
|
|
1086
|
+
return JSON.stringify({
|
|
1087
|
+
error: 'Element is covered by <' + coverTag + '>' + (coverText ? ' "' + coverText + '"' : ''),
|
|
1088
|
+
x: cx, y: cy, width: r.width, height: r.height
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return JSON.stringify({
|
|
1094
|
+
x: cx,
|
|
1095
|
+
y: cy,
|
|
1096
|
+
width: r.width,
|
|
1097
|
+
height: r.height
|
|
1098
|
+
});
|
|
1099
|
+
})()
|
|
1100
|
+
`,
|
|
1101
|
+
returnByValue: true
|
|
1102
|
+
});
|
|
1103
|
+
const value = result.result?.value;
|
|
1104
|
+
if (!value || value === "null") return null;
|
|
1105
|
+
try {
|
|
1106
|
+
return JSON.parse(value);
|
|
1107
|
+
} catch {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
// ── Ref-based Interactions (CDP Input Events) ─────────────────────
|
|
1112
|
+
/**
|
|
1113
|
+
* Click an element by ref using CDP Input.dispatchMouseEvent.
|
|
1114
|
+
* This simulates a real mouse click through the browser's input pipeline,
|
|
1115
|
+
* triggering hover states, focus management, and all native browser events
|
|
1116
|
+
* — more reliable than el.click() for framework components.
|
|
1117
|
+
*
|
|
1118
|
+
* Includes auto-wait: retries up to 3 times (with 500ms intervals) if the
|
|
1119
|
+
* element is not yet actionable (e.g., covered by a loading overlay, still
|
|
1120
|
+
* animating into view). This matches Playwright's auto-waiting behavior.
|
|
1121
|
+
*/
|
|
1122
|
+
async clickRef(refId) {
|
|
1123
|
+
this.ensureConnected();
|
|
1124
|
+
const ref = this.refCache.get(refId);
|
|
1125
|
+
const refLabel = `[${refId}] ${ref?.role || ""} "${ref?.name || ""}"`;
|
|
1126
|
+
const maxRetries = 3;
|
|
1127
|
+
let lastError = "";
|
|
1128
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1129
|
+
const resolved = await this.resolveRef(refId);
|
|
1130
|
+
if (!resolved) {
|
|
1131
|
+
return {
|
|
1132
|
+
success: false,
|
|
1133
|
+
message: `Ref ${refLabel} not found. Take a new snapshot with browser_snapshot.`
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
if (resolved.error) {
|
|
1137
|
+
lastError = resolved.error;
|
|
1138
|
+
if (attempt < maxRetries - 1) {
|
|
1139
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
return { success: false, message: `Cannot click ${refLabel}: ${lastError}` };
|
|
1143
|
+
}
|
|
1144
|
+
if (attempt === 0) {
|
|
1145
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1146
|
+
const settled = await this.resolveRef(refId);
|
|
1147
|
+
if (settled && !settled.error) {
|
|
1148
|
+
resolved.x = settled.x;
|
|
1149
|
+
resolved.y = settled.y;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
1153
|
+
type: "mouseMoved",
|
|
1154
|
+
x: resolved.x,
|
|
1155
|
+
y: resolved.y
|
|
1156
|
+
});
|
|
1157
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
1158
|
+
type: "mousePressed",
|
|
1159
|
+
x: resolved.x,
|
|
1160
|
+
y: resolved.y,
|
|
1161
|
+
button: "left",
|
|
1162
|
+
clickCount: 1
|
|
1163
|
+
});
|
|
1164
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
1165
|
+
type: "mouseReleased",
|
|
1166
|
+
x: resolved.x,
|
|
1167
|
+
y: resolved.y,
|
|
1168
|
+
button: "left",
|
|
1169
|
+
clickCount: 1
|
|
1170
|
+
});
|
|
1171
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1172
|
+
return { success: true, message: `Clicked ${refLabel}` };
|
|
1173
|
+
}
|
|
1174
|
+
return { success: false, message: `Cannot click ${refLabel}: ${lastError}` };
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Type text into an element by ref using CDP Input events.
|
|
1178
|
+
* Clicks to focus, selects all existing text (Ctrl/Cmd+A), then uses
|
|
1179
|
+
* Input.insertText for reliable text insertion across all frameworks.
|
|
1180
|
+
*/
|
|
1181
|
+
async typeRef(refId, text) {
|
|
1182
|
+
this.ensureConnected();
|
|
1183
|
+
const ref = this.refCache.get(refId);
|
|
1184
|
+
const refLabel = `[${refId}] ${ref?.role || ""} "${ref?.name || ""}"`;
|
|
1185
|
+
const clickResult = await this.clickRef(refId);
|
|
1186
|
+
if (!clickResult.success) return clickResult;
|
|
1187
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1188
|
+
const selectAllKey = platform() === "darwin" ? "Meta+a" : "Control+a";
|
|
1189
|
+
await this.pressKey(selectAllKey);
|
|
1190
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1191
|
+
await this.pressKey("Backspace");
|
|
1192
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1193
|
+
const cleared = await this.send("Runtime.evaluate", {
|
|
1194
|
+
expression: `
|
|
1195
|
+
(function() {
|
|
1196
|
+
var el = document.querySelector('[data-assistme-ref="${refId}"]');
|
|
1197
|
+
if (!el) return 'no_element';
|
|
1198
|
+
if (el.value !== undefined && el.value !== '') {
|
|
1199
|
+
// Ctrl+A didn't work (some frameworks intercept it) \u2014 clear via JS
|
|
1200
|
+
var setter = Object.getOwnPropertyDescriptor(
|
|
1201
|
+
window.HTMLInputElement.prototype, 'value'
|
|
1202
|
+
)?.set || Object.getOwnPropertyDescriptor(
|
|
1203
|
+
window.HTMLTextAreaElement.prototype, 'value'
|
|
1204
|
+
)?.set;
|
|
1205
|
+
if (setter) setter.call(el, '');
|
|
1206
|
+
else el.value = '';
|
|
1207
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1208
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1209
|
+
return 'js_cleared';
|
|
1210
|
+
}
|
|
1211
|
+
return 'ok';
|
|
1212
|
+
})()
|
|
1213
|
+
`,
|
|
1214
|
+
returnByValue: true
|
|
1215
|
+
});
|
|
1216
|
+
const clearStatus = cleared.result?.value || "ok";
|
|
1217
|
+
if (clearStatus === "no_element") {
|
|
1218
|
+
return {
|
|
1219
|
+
success: false,
|
|
1220
|
+
message: `Ref ${refLabel} not found after click. Take a new snapshot.`
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
await this.send("Input.insertText", { text });
|
|
1224
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1225
|
+
return { success: true, message: `Typed "${text}" into ${refLabel}` };
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Select a dropdown option by ref. Delegates to selectOption with the
|
|
1229
|
+
* ref's data attribute as selector, handling both native <select> and
|
|
1230
|
+
* custom dropdown components.
|
|
1231
|
+
*/
|
|
1232
|
+
async selectRef(refId, option) {
|
|
1233
|
+
this.ensureConnected();
|
|
1234
|
+
const cached = this.refCache.get(refId);
|
|
1235
|
+
if (!cached) {
|
|
1236
|
+
return {
|
|
1237
|
+
success: false,
|
|
1238
|
+
message: `Ref [${refId}] not found. Take a new snapshot with browser_snapshot.`
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
const refLabel = `[${refId}] ${cached.role} "${cached.name}"`;
|
|
1242
|
+
const result = await this.selectOption(`[data-assistme-ref="${refId}"]`, option);
|
|
1243
|
+
const message = result.replace(/\[data-assistme-ref="\d+"\]/, refLabel);
|
|
1244
|
+
const success = !result.includes("not found");
|
|
1245
|
+
return { success, message };
|
|
1246
|
+
}
|
|
1247
|
+
// ── Action Pipeline ───────────────────────────────────────────────
|
|
1248
|
+
/**
|
|
1249
|
+
* Execute a batch of actions sequentially using refs.
|
|
1250
|
+
* Reduces round-trips: instead of one tool call per action, the model
|
|
1251
|
+
* can specify a sequence of actions that execute atomically.
|
|
1252
|
+
*
|
|
1253
|
+
* Optionally takes a screenshot after all actions complete.
|
|
1254
|
+
*/
|
|
1255
|
+
async act(actions, takeScreenshot = false) {
|
|
1256
|
+
this.ensureConnected();
|
|
1257
|
+
const results = [];
|
|
1258
|
+
for (const spec of actions) {
|
|
1259
|
+
let result;
|
|
1260
|
+
let success = true;
|
|
1261
|
+
try {
|
|
1262
|
+
switch (spec.action) {
|
|
1263
|
+
case "click": {
|
|
1264
|
+
const r = await this.clickRef(spec.ref);
|
|
1265
|
+
result = r.message;
|
|
1266
|
+
success = r.success;
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
case "type": {
|
|
1270
|
+
const r = await this.typeRef(spec.ref, spec.text);
|
|
1271
|
+
result = r.message;
|
|
1272
|
+
success = r.success;
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
case "select": {
|
|
1276
|
+
const r = await this.selectRef(spec.ref, spec.option);
|
|
1277
|
+
result = r.message;
|
|
1278
|
+
success = r.success;
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
case "press":
|
|
1282
|
+
result = await this.pressKey(spec.key);
|
|
1283
|
+
break;
|
|
1284
|
+
case "scroll":
|
|
1285
|
+
result = spec.direction === "up" ? await this.scrollUp() : await this.scrollDown();
|
|
1286
|
+
break;
|
|
1287
|
+
case "wait":
|
|
1288
|
+
await new Promise((r) => setTimeout(r, Math.min(spec.ms, 5e3)));
|
|
1289
|
+
result = `Waited ${spec.ms}ms`;
|
|
1290
|
+
break;
|
|
1291
|
+
default:
|
|
1292
|
+
result = `Unknown action: ${spec.action}`;
|
|
1293
|
+
success = false;
|
|
1294
|
+
}
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
result = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1297
|
+
success = false;
|
|
1298
|
+
}
|
|
1299
|
+
results.push({
|
|
1300
|
+
action: spec.action,
|
|
1301
|
+
ref: "ref" in spec ? spec.ref : void 0,
|
|
1302
|
+
result,
|
|
1303
|
+
success
|
|
1304
|
+
});
|
|
1305
|
+
if (!success) break;
|
|
1306
|
+
if (spec.action !== "wait") {
|
|
1307
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
let screenshot;
|
|
1311
|
+
if (takeScreenshot) {
|
|
1312
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1313
|
+
screenshot = await this.screenshot();
|
|
1314
|
+
}
|
|
1315
|
+
return { results, screenshot };
|
|
1316
|
+
}
|
|
1317
|
+
// ── Dropdown/Select ─────────────────────────────────────────────
|
|
1318
|
+
/**
|
|
1319
|
+
* Select an option from a dropdown — handles both native <select> elements
|
|
1320
|
+
* and custom Material Design / React / Angular dropdown components.
|
|
1321
|
+
*
|
|
1322
|
+
* Strategy:
|
|
1323
|
+
* 1. Try native <select> first (by selector or label text)
|
|
1324
|
+
* 2. Fall back to custom dropdown: click to open, then click the option by text
|
|
1325
|
+
*/
|
|
1326
|
+
async selectOption(selector, optionText) {
|
|
1327
|
+
this.ensureConnected();
|
|
1328
|
+
const selectorJS = JSON.stringify(selector);
|
|
1329
|
+
const optionJS = JSON.stringify(optionText);
|
|
1330
|
+
const result = await this.send("Runtime.evaluate", {
|
|
1331
|
+
expression: `
|
|
1332
|
+
(function() {
|
|
1333
|
+
var sel = ${selectorJS};
|
|
1334
|
+
var optText = ${optionJS};
|
|
1335
|
+
|
|
1336
|
+
// Strategy 1: Native <select> element
|
|
1337
|
+
var selectEl = document.querySelector(sel);
|
|
1338
|
+
if (selectEl && selectEl.tagName === 'SELECT') {
|
|
1339
|
+
var options = selectEl.querySelectorAll('option');
|
|
1340
|
+
for (var i = 0; i < options.length; i++) {
|
|
1341
|
+
if (options[i].textContent.trim() === optText) {
|
|
1342
|
+
selectEl.value = options[i].value;
|
|
1343
|
+
selectEl.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1344
|
+
selectEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1345
|
+
return 'Selected "' + optText + '" in native select';
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return 'Option "' + optText + '" not found in select. Available: ' +
|
|
1349
|
+
Array.from(options).map(function(o) { return o.textContent.trim(); }).join(', ');
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// Strategy 2: Custom dropdown \u2014 find the trigger element
|
|
1353
|
+
var trigger = selectEl;
|
|
1354
|
+
if (!trigger) {
|
|
1355
|
+
// Try finding by aria-label first (fast, indexed)
|
|
1356
|
+
trigger = document.querySelector('[aria-label="' + sel.replace(/"/g, '\\"') + '"]');
|
|
1357
|
+
}
|
|
1358
|
+
if (!trigger) {
|
|
1359
|
+
// Try finding by label/placeholder text in likely dropdown elements
|
|
1360
|
+
var dropdownCandidates = document.querySelectorAll(
|
|
1361
|
+
'button, [role="combobox"], [role="listbox"], [role="button"], ' +
|
|
1362
|
+
'select, input, .MuiSelect-root, .MuiInput-root, ' +
|
|
1363
|
+
'[class*="select"], [class*="dropdown"], [class*="picker"]'
|
|
1364
|
+
);
|
|
1365
|
+
for (var j = 0; j < dropdownCandidates.length; j++) {
|
|
1366
|
+
var el = dropdownCandidates[j];
|
|
1367
|
+
var ownText = Array.from(el.childNodes)
|
|
1368
|
+
.filter(function(n) { return n.nodeType === 3; })
|
|
1369
|
+
.map(function(n) { return n.textContent.trim(); })
|
|
1370
|
+
.join('');
|
|
1371
|
+
if (ownText === sel || el.getAttribute('aria-label') === sel ||
|
|
1372
|
+
el.getAttribute('placeholder') === sel) {
|
|
1373
|
+
trigger = el;
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (!trigger) return 'Dropdown not found: ' + sel;
|
|
1380
|
+
|
|
1381
|
+
// Click to open the dropdown
|
|
1382
|
+
trigger.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
1383
|
+
trigger.click();
|
|
1384
|
+
|
|
1385
|
+
// Wait a frame for the dropdown menu to render, then select the option
|
|
1386
|
+
return new Promise(function(resolve) {
|
|
1387
|
+
setTimeout(function() {
|
|
1388
|
+
// Look for the option in listbox/menu/dropdown overlays
|
|
1389
|
+
var optionContainers = document.querySelectorAll(
|
|
1390
|
+
'[role="listbox"], [role="menu"], [role="presentation"], .MuiMenu-list, .MuiList-root, ul.mdc-list, .VfPpkd-xl07Ob'
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
// Also check all visible elements as fallback
|
|
1394
|
+
var searchIn = optionContainers.length > 0
|
|
1395
|
+
? Array.from(optionContainers).flatMap(function(c) { return Array.from(c.querySelectorAll('*')); })
|
|
1396
|
+
: Array.from(document.querySelectorAll('li, [role="option"], [role="menuitem"], div[data-value]'));
|
|
1397
|
+
|
|
1398
|
+
for (var k = 0; k < searchIn.length; k++) {
|
|
1399
|
+
var opt = searchIn[k];
|
|
1400
|
+
var txt = opt.textContent ? opt.textContent.trim() : '';
|
|
1401
|
+
if (txt === optText) {
|
|
1402
|
+
opt.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
1403
|
+
opt.click();
|
|
1404
|
+
resolve('Selected "' + optText + '" from custom dropdown');
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Broader search: visible leaf elements in interactive containers
|
|
1410
|
+
var broadCandidates = document.querySelectorAll(
|
|
1411
|
+
'li, span, div, a, button, label, [role="option"], [role="menuitem"], ' +
|
|
1412
|
+
'[role="menuitemradio"], [role="menuitemcheckbox"], [data-value]'
|
|
1413
|
+
);
|
|
1414
|
+
for (var m = 0; m < broadCandidates.length; m++) {
|
|
1415
|
+
var candidate = broadCandidates[m];
|
|
1416
|
+
if (candidate.textContent && candidate.textContent.trim() === optText &&
|
|
1417
|
+
candidate.offsetParent !== null && candidate.children.length === 0) {
|
|
1418
|
+
candidate.click();
|
|
1419
|
+
resolve('Selected "' + optText + '" (broad match)');
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
resolve('Option "' + optText + '" not found in dropdown');
|
|
1425
|
+
}, 300);
|
|
1426
|
+
});
|
|
1427
|
+
})()
|
|
1428
|
+
`,
|
|
1429
|
+
returnByValue: true,
|
|
1430
|
+
awaitPromise: true
|
|
1431
|
+
});
|
|
1432
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1433
|
+
return result.result?.value || "Selection attempted.";
|
|
1434
|
+
}
|
|
692
1435
|
// ── JavaScript Evaluation ───────────────────────────────────────
|
|
693
1436
|
async evaluate(expression) {
|
|
694
1437
|
this.ensureConnected();
|
|
@@ -742,6 +1485,7 @@ URL: ${info.url}`;
|
|
|
742
1485
|
// ── Helpers ─────────────────────────────────────────────────────
|
|
743
1486
|
async waitForLoad(timeoutMs = 8e3) {
|
|
744
1487
|
const start = Date.now();
|
|
1488
|
+
let sawInteractive = false;
|
|
745
1489
|
while (Date.now() - start < timeoutMs) {
|
|
746
1490
|
try {
|
|
747
1491
|
const result = await this.send("Runtime.evaluate", {
|
|
@@ -749,67 +1493,22 @@ URL: ${info.url}`;
|
|
|
749
1493
|
returnByValue: true
|
|
750
1494
|
});
|
|
751
1495
|
const state = result.result?.value;
|
|
752
|
-
if (state === "complete"
|
|
753
|
-
await new Promise((r) => setTimeout(r,
|
|
1496
|
+
if (state === "complete") {
|
|
1497
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
754
1498
|
return;
|
|
755
1499
|
}
|
|
1500
|
+
if (state === "interactive") {
|
|
1501
|
+
if (!sawInteractive) {
|
|
1502
|
+
sawInteractive = true;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
756
1505
|
} catch {
|
|
757
1506
|
}
|
|
758
1507
|
await new Promise((r) => setTimeout(r, 300));
|
|
759
1508
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
*/
|
|
764
|
-
async getInteractiveElements() {
|
|
765
|
-
this.ensureConnected();
|
|
766
|
-
const result = await this.send("Runtime.evaluate", {
|
|
767
|
-
expression: `
|
|
768
|
-
(function() {
|
|
769
|
-
const elements = [];
|
|
770
|
-
const selectors = 'a, button, input, select, textarea, [role="button"], [onclick]';
|
|
771
|
-
const all = document.querySelectorAll(selectors);
|
|
772
|
-
for (let i = 0; i < all.length && elements.length < 50; i++) {
|
|
773
|
-
const el = all[i];
|
|
774
|
-
const rect = el.getBoundingClientRect();
|
|
775
|
-
if (rect.width === 0 || rect.height === 0) continue; // Skip hidden
|
|
776
|
-
|
|
777
|
-
// Build a reliable CSS selector
|
|
778
|
-
let selector;
|
|
779
|
-
if (el.id) {
|
|
780
|
-
selector = '#' + CSS.escape(el.id);
|
|
781
|
-
} else if (el.getAttribute('data-testid')) {
|
|
782
|
-
selector = '[data-testid="' + el.getAttribute('data-testid') + '"]';
|
|
783
|
-
} else {
|
|
784
|
-
// Build a path-based selector: find nth-of-type among siblings
|
|
785
|
-
const tag = el.tagName.toLowerCase();
|
|
786
|
-
const parent = el.parentElement;
|
|
787
|
-
if (parent) {
|
|
788
|
-
const siblings = parent.querySelectorAll(':scope > ' + tag);
|
|
789
|
-
const idx = Array.from(siblings).indexOf(el) + 1;
|
|
790
|
-
selector = tag + ':nth-of-type(' + idx + ')';
|
|
791
|
-
} else {
|
|
792
|
-
selector = tag;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
elements.push({
|
|
797
|
-
tag: el.tagName.toLowerCase(),
|
|
798
|
-
text: (el.textContent || '').trim().slice(0, 80),
|
|
799
|
-
type: el.getAttribute('type') || '',
|
|
800
|
-
name: el.getAttribute('name') || '',
|
|
801
|
-
id: el.id || '',
|
|
802
|
-
href: el.getAttribute('href') || '',
|
|
803
|
-
placeholder: el.getAttribute('placeholder') || '',
|
|
804
|
-
selector: selector,
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
return JSON.stringify(elements, null, 2);
|
|
808
|
-
})()
|
|
809
|
-
`,
|
|
810
|
-
returnByValue: true
|
|
811
|
-
});
|
|
812
|
-
return result.result?.value || "[]";
|
|
1509
|
+
if (sawInteractive) {
|
|
1510
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1511
|
+
}
|
|
813
1512
|
}
|
|
814
1513
|
isConnected() {
|
|
815
1514
|
return this.connected && this.ws?.readyState === WebSocket.OPEN;
|
|
@@ -826,12 +1525,28 @@ URL: ${info.url}`;
|
|
|
826
1525
|
(function() {
|
|
827
1526
|
var url = window.location.href.toLowerCase();
|
|
828
1527
|
|
|
1528
|
+
// Exclude signup/registration pages \u2014 these are NOT login pages
|
|
1529
|
+
var signupPatterns = [
|
|
1530
|
+
'/signup', '/sign-up', '/sign_up', '/register',
|
|
1531
|
+
'/registration', '/create-account', '/create_account',
|
|
1532
|
+
'/join', '/enroll',
|
|
1533
|
+
'accounts.google.com/lifecycle/steps/signup',
|
|
1534
|
+
'signup.live.com',
|
|
1535
|
+
];
|
|
1536
|
+
for (var s = 0; s < signupPatterns.length; s++) {
|
|
1537
|
+
if (url.indexOf(signupPatterns[s]) !== -1) {
|
|
1538
|
+
return JSON.stringify({ isLoginPage: false, reason: '' });
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
829
1542
|
// URL-based detection
|
|
830
1543
|
var loginPatterns = [
|
|
831
1544
|
'/login', '/signin', '/sign-in', '/sign_in',
|
|
832
1545
|
'/auth/', '/sso/', '/oauth/', '/session/new',
|
|
833
1546
|
'/accounts/login', '/users/sign_in',
|
|
834
|
-
'accounts.google.com',
|
|
1547
|
+
'accounts.google.com/v3/signin',
|
|
1548
|
+
'accounts.google.com/servicelogin',
|
|
1549
|
+
'login.microsoftonline.com',
|
|
835
1550
|
'github.com/login', 'github.com/session',
|
|
836
1551
|
'login.live.com', 'appleid.apple.com'
|
|
837
1552
|
];
|
|
@@ -885,8 +1600,14 @@ URL: ${info.url}`;
|
|
|
885
1600
|
}
|
|
886
1601
|
}
|
|
887
1602
|
};
|
|
1603
|
+
|
|
1604
|
+
// src/browser/chrome-launcher.ts
|
|
1605
|
+
import { execSync, spawn } from "child_process";
|
|
1606
|
+
import { platform as platform2, homedir } from "os";
|
|
1607
|
+
import { existsSync, unlinkSync, mkdirSync, cpSync } from "fs";
|
|
1608
|
+
import { join } from "path";
|
|
888
1609
|
function findChromePath() {
|
|
889
|
-
const os =
|
|
1610
|
+
const os = platform2();
|
|
890
1611
|
if (os === "darwin") {
|
|
891
1612
|
const paths = [
|
|
892
1613
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
@@ -895,7 +1616,7 @@ function findChromePath() {
|
|
|
895
1616
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
896
1617
|
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
897
1618
|
];
|
|
898
|
-
return paths.find((p) =>
|
|
1619
|
+
return paths.find((p) => existsSync(p)) ?? null;
|
|
899
1620
|
}
|
|
900
1621
|
if (os === "linux") {
|
|
901
1622
|
const names = [
|
|
@@ -932,7 +1653,7 @@ function findChromePath() {
|
|
|
932
1653
|
for (const prefix of prefixes) {
|
|
933
1654
|
for (const sub of subPaths) {
|
|
934
1655
|
const p = `${prefix}\\${sub}`;
|
|
935
|
-
if (
|
|
1656
|
+
if (existsSync(p)) return p;
|
|
936
1657
|
}
|
|
937
1658
|
}
|
|
938
1659
|
return null;
|
|
@@ -940,39 +1661,39 @@ function findChromePath() {
|
|
|
940
1661
|
return null;
|
|
941
1662
|
}
|
|
942
1663
|
function getDefaultProfileDir(chromePath) {
|
|
943
|
-
const home =
|
|
944
|
-
const os =
|
|
1664
|
+
const home = homedir();
|
|
1665
|
+
const os = platform2();
|
|
945
1666
|
if (os === "darwin") {
|
|
946
1667
|
if (chromePath.includes("Brave Browser"))
|
|
947
|
-
return
|
|
1668
|
+
return join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser");
|
|
948
1669
|
if (chromePath.includes("Microsoft Edge"))
|
|
949
|
-
return
|
|
1670
|
+
return join(home, "Library", "Application Support", "Microsoft Edge");
|
|
950
1671
|
if (chromePath.includes("Chromium"))
|
|
951
|
-
return
|
|
1672
|
+
return join(home, "Library", "Application Support", "Chromium");
|
|
952
1673
|
if (chromePath.includes("Canary"))
|
|
953
|
-
return
|
|
954
|
-
return
|
|
1674
|
+
return join(home, "Library", "Application Support", "Google", "Chrome Canary");
|
|
1675
|
+
return join(home, "Library", "Application Support", "Google", "Chrome");
|
|
955
1676
|
}
|
|
956
1677
|
if (os === "win32") {
|
|
957
|
-
const appData = process.env.LOCALAPPDATA ||
|
|
1678
|
+
const appData = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
|
|
958
1679
|
if (chromePath.includes("brave"))
|
|
959
|
-
return
|
|
960
|
-
if (chromePath.includes("msedge")) return
|
|
961
|
-
return
|
|
962
|
-
}
|
|
963
|
-
if (chromePath.includes("brave")) return
|
|
964
|
-
if (chromePath.includes("microsoft-edge")) return
|
|
965
|
-
if (chromePath.includes("chromium")) return
|
|
966
|
-
return
|
|
1680
|
+
return join(appData, "BraveSoftware", "Brave-Browser", "User Data");
|
|
1681
|
+
if (chromePath.includes("msedge")) return join(appData, "Microsoft", "Edge", "User Data");
|
|
1682
|
+
return join(appData, "Google", "Chrome", "User Data");
|
|
1683
|
+
}
|
|
1684
|
+
if (chromePath.includes("brave")) return join(home, ".config", "BraveSoftware", "Brave-Browser");
|
|
1685
|
+
if (chromePath.includes("microsoft-edge")) return join(home, ".config", "microsoft-edge");
|
|
1686
|
+
if (chromePath.includes("chromium")) return join(home, ".config", "chromium");
|
|
1687
|
+
return join(home, ".config", "google-chrome");
|
|
967
1688
|
}
|
|
968
1689
|
function getDebugProfileDir(chromePath) {
|
|
969
|
-
const home =
|
|
970
|
-
const debugDir =
|
|
971
|
-
if (!
|
|
972
|
-
|
|
1690
|
+
const home = homedir();
|
|
1691
|
+
const debugDir = join(home, ".assistme", "browser-profile");
|
|
1692
|
+
if (!existsSync(debugDir)) {
|
|
1693
|
+
mkdirSync(debugDir, { recursive: true });
|
|
973
1694
|
log.debug(`Created debug profile directory: ${debugDir}`);
|
|
974
1695
|
const realDir = getDefaultProfileDir(chromePath);
|
|
975
|
-
if (
|
|
1696
|
+
if (existsSync(realDir)) {
|
|
976
1697
|
seedDebugProfile(realDir, debugDir);
|
|
977
1698
|
}
|
|
978
1699
|
}
|
|
@@ -982,35 +1703,35 @@ function seedDebugProfile(realDir, debugDir) {
|
|
|
982
1703
|
const rootFiles = ["Local State"];
|
|
983
1704
|
const profileFiles = ["Bookmarks", "Preferences", "Favicons", "Top Sites", "Shortcuts"];
|
|
984
1705
|
for (const file of rootFiles) {
|
|
985
|
-
const src =
|
|
986
|
-
const dest =
|
|
1706
|
+
const src = join(realDir, file);
|
|
1707
|
+
const dest = join(debugDir, file);
|
|
987
1708
|
try {
|
|
988
|
-
if (
|
|
1709
|
+
if (existsSync(src)) {
|
|
989
1710
|
cpSync(src, dest, { force: true });
|
|
990
1711
|
log.debug(`Seeded: ${file}`);
|
|
991
1712
|
}
|
|
992
1713
|
} catch {
|
|
993
1714
|
}
|
|
994
1715
|
}
|
|
995
|
-
const srcProfile =
|
|
996
|
-
const destProfile =
|
|
997
|
-
if (
|
|
998
|
-
|
|
1716
|
+
const srcProfile = join(realDir, "Default");
|
|
1717
|
+
const destProfile = join(debugDir, "Default");
|
|
1718
|
+
if (existsSync(srcProfile)) {
|
|
1719
|
+
mkdirSync(destProfile, { recursive: true });
|
|
999
1720
|
for (const file of profileFiles) {
|
|
1000
|
-
const src =
|
|
1001
|
-
const dest =
|
|
1721
|
+
const src = join(srcProfile, file);
|
|
1722
|
+
const dest = join(destProfile, file);
|
|
1002
1723
|
try {
|
|
1003
|
-
if (
|
|
1724
|
+
if (existsSync(src)) {
|
|
1004
1725
|
cpSync(src, dest, { force: true });
|
|
1005
1726
|
log.debug(`Seeded: Default/${file}`);
|
|
1006
1727
|
}
|
|
1007
1728
|
} catch {
|
|
1008
1729
|
}
|
|
1009
1730
|
}
|
|
1010
|
-
const srcExt =
|
|
1011
|
-
const destExt =
|
|
1731
|
+
const srcExt = join(srcProfile, "Extensions");
|
|
1732
|
+
const destExt = join(destProfile, "Extensions");
|
|
1012
1733
|
try {
|
|
1013
|
-
if (
|
|
1734
|
+
if (existsSync(srcExt)) {
|
|
1014
1735
|
cpSync(srcExt, destExt, { recursive: true, force: true });
|
|
1015
1736
|
log.debug("Seeded: Default/Extensions");
|
|
1016
1737
|
}
|
|
@@ -1101,14 +1822,14 @@ async function ensureBrowserAvailable(port = 9222) {
|
|
|
1101
1822
|
return { success: true, action: "launched", chromePath };
|
|
1102
1823
|
}
|
|
1103
1824
|
const debugDir = getDebugProfileDir(chromePath);
|
|
1104
|
-
const lockPath =
|
|
1105
|
-
if (
|
|
1825
|
+
const lockPath = join(debugDir, "SingletonLock");
|
|
1826
|
+
if (existsSync(lockPath)) {
|
|
1106
1827
|
log.debug("Found stale SingletonLock in debug profile \u2014 removing and retrying");
|
|
1107
1828
|
try {
|
|
1108
1829
|
unlinkSync(lockPath);
|
|
1109
1830
|
for (const f of ["SingletonSocket", "SingletonCookie"]) {
|
|
1110
1831
|
try {
|
|
1111
|
-
unlinkSync(
|
|
1832
|
+
unlinkSync(join(debugDir, f));
|
|
1112
1833
|
} catch {
|
|
1113
1834
|
}
|
|
1114
1835
|
}
|
|
@@ -1126,12 +1847,14 @@ async function ensureBrowserAvailable(port = 9222) {
|
|
|
1126
1847
|
detail: "Could not start browser with remote debugging. Possible causes:\n 1) Another assistme debug browser is already using port " + port + "\n 2) The browser crashed on startup\nTry: rm -rf ~/.assistme/browser-profile && assistme"
|
|
1127
1848
|
};
|
|
1128
1849
|
}
|
|
1129
|
-
var
|
|
1850
|
+
var browserInstances = /* @__PURE__ */ new Map();
|
|
1130
1851
|
function getBrowser(port = 9222) {
|
|
1131
|
-
|
|
1132
|
-
|
|
1852
|
+
let instance = browserInstances.get(port);
|
|
1853
|
+
if (!instance) {
|
|
1854
|
+
instance = new BrowserController(port);
|
|
1855
|
+
browserInstances.set(port, instance);
|
|
1133
1856
|
}
|
|
1134
|
-
return
|
|
1857
|
+
return instance;
|
|
1135
1858
|
}
|
|
1136
1859
|
|
|
1137
1860
|
// src/commands/browser.ts
|
|
@@ -1365,7 +2088,7 @@ var Scheduler = class {
|
|
|
1365
2088
|
}
|
|
1366
2089
|
}
|
|
1367
2090
|
};
|
|
1368
|
-
async function createScheduledTask(
|
|
2091
|
+
async function createScheduledTask(name, prompt, cronExpression, timezone = "UTC") {
|
|
1369
2092
|
const nextRun = getNextRunTime(cronExpression, timezone);
|
|
1370
2093
|
return callMcpHandler("schedule.create", {
|
|
1371
2094
|
name,
|
|
@@ -1375,7 +2098,7 @@ async function createScheduledTask(_userId, name, prompt, cronExpression, timezo
|
|
|
1375
2098
|
next_run_at: nextRun.toISOString()
|
|
1376
2099
|
});
|
|
1377
2100
|
}
|
|
1378
|
-
async function listScheduledTasks(
|
|
2101
|
+
async function listScheduledTasks() {
|
|
1379
2102
|
return callMcpHandler("schedule.list");
|
|
1380
2103
|
}
|
|
1381
2104
|
async function toggleScheduledTask(taskId, enabled) {
|
|
@@ -1423,20 +2146,10 @@ var SessionManager = class {
|
|
|
1423
2146
|
const config = getConfig();
|
|
1424
2147
|
this.onTask = onTask;
|
|
1425
2148
|
this.userId = userId;
|
|
1426
|
-
this.session = await createSession(
|
|
1427
|
-
|
|
1428
|
-
config.sessionName,
|
|
1429
|
-
config.workspacePath,
|
|
1430
|
-
"0.1.0"
|
|
1431
|
-
);
|
|
1432
|
-
this.conversationId = await getOrCreateCliConversation(
|
|
1433
|
-
userId,
|
|
1434
|
-
this.session.id
|
|
1435
|
-
);
|
|
2149
|
+
this.session = await createSession(config.sessionName, config.workspacePath, "0.1.0");
|
|
2150
|
+
this.conversationId = await getOrCreateCliConversation();
|
|
1436
2151
|
this.running = true;
|
|
1437
|
-
log.success(
|
|
1438
|
-
`Session started: ${this.session.id} (${config.sessionName})`
|
|
1439
|
-
);
|
|
2152
|
+
log.success(`Session started: ${this.session.id} (${config.sessionName})`);
|
|
1440
2153
|
log.info(`Workspace: ${config.workspacePath}`);
|
|
1441
2154
|
this.heartbeatTimer = setInterval(async () => {
|
|
1442
2155
|
if (this.session) {
|
|
@@ -1464,20 +2177,15 @@ var SessionManager = class {
|
|
|
1464
2177
|
if (!this.session || !this.userId || !this.conversationId) return;
|
|
1465
2178
|
log.info(`Running scheduled task: "${scheduledTask.name}"`);
|
|
1466
2179
|
try {
|
|
1467
|
-
await this.submitTask(
|
|
1468
|
-
`[Scheduled: ${scheduledTask.name}] ${scheduledTask.prompt}`
|
|
1469
|
-
);
|
|
2180
|
+
await this.submitTask(`[Scheduled: ${scheduledTask.name}] ${scheduledTask.prompt}`);
|
|
1470
2181
|
} catch (err) {
|
|
1471
2182
|
log.error(`Scheduled task error: ${err}`);
|
|
1472
2183
|
}
|
|
1473
2184
|
}
|
|
1474
2185
|
async executeJobRun(jobRun) {
|
|
1475
|
-
if (!this.session || !this.userId || !this.conversationId || !this.onTask)
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
`Executing job run: ${jobRun.job_name} (${jobRun.id.slice(0, 8)}...)`
|
|
1479
|
-
);
|
|
1480
|
-
const runner = new JobRunner(this.userId);
|
|
2186
|
+
if (!this.session || !this.userId || !this.conversationId || !this.onTask) return;
|
|
2187
|
+
log.info(`Executing job run: ${jobRun.job_name} (${jobRun.id.slice(0, 8)}...)`);
|
|
2188
|
+
const runner = new JobRunner();
|
|
1481
2189
|
const job = await runner.loadJob(jobRun.job_name);
|
|
1482
2190
|
if (!job) {
|
|
1483
2191
|
log.error(`Job "${jobRun.job_name}" not found, marking run as failed`);
|
|
@@ -1549,7 +2257,7 @@ var SessionManager = class {
|
|
|
1549
2257
|
}
|
|
1550
2258
|
}
|
|
1551
2259
|
} else if (this.userId) {
|
|
1552
|
-
const jobRun = await pollAndClaimJobRun(
|
|
2260
|
+
const jobRun = await pollAndClaimJobRun();
|
|
1553
2261
|
if (jobRun) {
|
|
1554
2262
|
this.processingDepth++;
|
|
1555
2263
|
await setSessionBusy(this.session.id, true);
|
|
@@ -1593,12 +2301,7 @@ var SessionManager = class {
|
|
|
1593
2301
|
this.processingDepth++;
|
|
1594
2302
|
await setSessionBusy(this.session.id, true);
|
|
1595
2303
|
try {
|
|
1596
|
-
const task = await createTask(
|
|
1597
|
-
this.conversationId,
|
|
1598
|
-
this.userId,
|
|
1599
|
-
this.session.id,
|
|
1600
|
-
prompt
|
|
1601
|
-
);
|
|
2304
|
+
const task = await createTask(this.conversationId, this.session.id, prompt);
|
|
1602
2305
|
await claimTask(task.id);
|
|
1603
2306
|
await this.onTask(task);
|
|
1604
2307
|
} catch (err) {
|
|
@@ -1656,16 +2359,12 @@ import {
|
|
|
1656
2359
|
|
|
1657
2360
|
// src/agent/memory.ts
|
|
1658
2361
|
var MemoryManager = class {
|
|
1659
|
-
constructor(_userId) {
|
|
1660
|
-
}
|
|
1661
2362
|
/**
|
|
1662
2363
|
* Store a new memory. Called by the agent after completing tasks
|
|
1663
2364
|
* to remember important facts about the user.
|
|
1664
2365
|
*/
|
|
1665
2366
|
async remember(content, category = "general", options) {
|
|
1666
|
-
const expiresAt = options?.expiresInDays ? new Date(
|
|
1667
|
-
Date.now() + options.expiresInDays * 864e5
|
|
1668
|
-
).toISOString() : null;
|
|
2367
|
+
const expiresAt = options?.expiresInDays ? new Date(Date.now() + options.expiresInDays * 864e5).toISOString() : null;
|
|
1669
2368
|
const data = await callMcpHandler("memory.store", {
|
|
1670
2369
|
category,
|
|
1671
2370
|
content,
|
|
@@ -1895,7 +2594,8 @@ function parseDbMetadata(raw) {
|
|
|
1895
2594
|
primaryEnv: openclaw.primaryEnv,
|
|
1896
2595
|
os: openclaw.os,
|
|
1897
2596
|
always: openclaw.always,
|
|
1898
|
-
skillKey: openclaw.skillKey
|
|
2597
|
+
skillKey: openclaw.skillKey,
|
|
2598
|
+
credentials: openclaw.credentials
|
|
1899
2599
|
};
|
|
1900
2600
|
}
|
|
1901
2601
|
var SkillManager = class {
|
|
@@ -2055,23 +2755,6 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
2055
2755
|
}
|
|
2056
2756
|
return prompt;
|
|
2057
2757
|
}
|
|
2058
|
-
/** @deprecated Use buildSkillDescriptions() + skill_invoke tool instead. */
|
|
2059
|
-
buildSkillPrompt(taskPrompt) {
|
|
2060
|
-
const relevant = this.findRelevant(taskPrompt);
|
|
2061
|
-
if (relevant.length === 0) return "";
|
|
2062
|
-
let prompt = "\n\n## Available Skills\n";
|
|
2063
|
-
prompt += "The following skills provide detailed instructions for this type of task:\n\n";
|
|
2064
|
-
for (const skill of relevant) {
|
|
2065
|
-
const emoji = skill.metadata.emoji || "";
|
|
2066
|
-
prompt += `### ${emoji ? emoji + " " : ""}${skill.name}
|
|
2067
|
-
`;
|
|
2068
|
-
prompt += `*${skill.description}*
|
|
2069
|
-
|
|
2070
|
-
`;
|
|
2071
|
-
prompt += skill.content + "\n\n";
|
|
2072
|
-
}
|
|
2073
|
-
return prompt;
|
|
2074
|
-
}
|
|
2075
2758
|
async create(name, description, content, options) {
|
|
2076
2759
|
if (!this.userId) return null;
|
|
2077
2760
|
try {
|
|
@@ -2259,7 +2942,10 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
2259
2942
|
async searchDb(query3, limit = 10) {
|
|
2260
2943
|
if (this.userId) {
|
|
2261
2944
|
try {
|
|
2262
|
-
const data = await callMcpHandler("skill.search", {
|
|
2945
|
+
const data = await callMcpHandler("skill.search", {
|
|
2946
|
+
query: query3,
|
|
2947
|
+
limit
|
|
2948
|
+
});
|
|
2263
2949
|
if (data) {
|
|
2264
2950
|
return data.map((row) => ({
|
|
2265
2951
|
name: row.name,
|
|
@@ -2586,7 +3272,7 @@ async function withRetry(fn, opts = {}) {
|
|
|
2586
3272
|
throw lastError;
|
|
2587
3273
|
}
|
|
2588
3274
|
|
|
2589
|
-
// src/
|
|
3275
|
+
// src/mcp/browser-server.ts
|
|
2590
3276
|
import {
|
|
2591
3277
|
createSdkMcpServer,
|
|
2592
3278
|
tool
|
|
@@ -2595,7 +3281,7 @@ import { z } from "zod/v4";
|
|
|
2595
3281
|
|
|
2596
3282
|
// src/tools/filesystem.ts
|
|
2597
3283
|
import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
|
|
2598
|
-
import { resolve, relative, join as
|
|
3284
|
+
import { resolve, relative, join as join2 } from "path";
|
|
2599
3285
|
import { glob } from "glob";
|
|
2600
3286
|
function assertWithinWorkspace(filePath) {
|
|
2601
3287
|
const config = getConfig();
|
|
@@ -2632,7 +3318,7 @@ async function searchFiles(pattern, directory) {
|
|
|
2632
3318
|
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
|
|
2633
3319
|
});
|
|
2634
3320
|
if (matches.length === 0) return "No files found matching the pattern.";
|
|
2635
|
-
return matches.slice(0, 50).map((m) => relative(config.workspacePath,
|
|
3321
|
+
return matches.slice(0, 50).map((m) => relative(config.workspacePath, join2(cwd, m))).join("\n");
|
|
2636
3322
|
}
|
|
2637
3323
|
async function listDirectory(path) {
|
|
2638
3324
|
const config = getConfig();
|
|
@@ -2642,7 +3328,7 @@ async function listDirectory(path) {
|
|
|
2642
3328
|
for (const entry of entries) {
|
|
2643
3329
|
if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
|
|
2644
3330
|
const icon = entry.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}";
|
|
2645
|
-
const info = entry.isFile() ? await stat(
|
|
3331
|
+
const info = entry.isFile() ? await stat(join2(resolved, entry.name)).then(
|
|
2646
3332
|
(s) => ` (${formatSize(s.size)})`
|
|
2647
3333
|
) : "";
|
|
2648
3334
|
results.push(`${icon} ${entry.name}${info}`);
|
|
@@ -2666,11 +3352,11 @@ async function searchContent(pattern, fileGlob, directory) {
|
|
|
2666
3352
|
const results = [];
|
|
2667
3353
|
for (const file of files.slice(0, 200)) {
|
|
2668
3354
|
try {
|
|
2669
|
-
const content = await readFile(
|
|
3355
|
+
const content = await readFile(join2(cwd, file), "utf-8");
|
|
2670
3356
|
const lines = content.split("\n");
|
|
2671
3357
|
for (let i = 0; i < lines.length; i++) {
|
|
2672
3358
|
if (regex.test(lines[i])) {
|
|
2673
|
-
const relPath = relative(config.workspacePath,
|
|
3359
|
+
const relPath = relative(config.workspacePath, join2(cwd, file));
|
|
2674
3360
|
results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
|
|
2675
3361
|
regex.lastIndex = 0;
|
|
2676
3362
|
if (results.length >= 30) break;
|
|
@@ -2875,12 +3561,28 @@ async function executeTool(name, input) {
|
|
|
2875
3561
|
case "browser_scroll":
|
|
2876
3562
|
await ensureConnected(browser);
|
|
2877
3563
|
return input.direction === "up" ? browser.scrollUp() : browser.scrollDown();
|
|
2878
|
-
case "
|
|
3564
|
+
case "browser_select":
|
|
2879
3565
|
await ensureConnected(browser);
|
|
2880
|
-
return browser.
|
|
3566
|
+
return browser.selectOption(input.selector, input.option);
|
|
2881
3567
|
case "browser_evaluate":
|
|
2882
3568
|
await ensureConnected(browser);
|
|
2883
3569
|
return browser.evaluate(input.expression);
|
|
3570
|
+
case "browser_snapshot": {
|
|
3571
|
+
await ensureConnected(browser);
|
|
3572
|
+
const snap = await browser.snapshot(input.annotate);
|
|
3573
|
+
return BrowserController.formatRefTable(snap) + "\n__SNAPSHOT_IMAGE__:" + snap.image;
|
|
3574
|
+
}
|
|
3575
|
+
case "browser_act": {
|
|
3576
|
+
await ensureConnected(browser);
|
|
3577
|
+
const actions = input.actions;
|
|
3578
|
+
const wantScreenshot = input.screenshot || false;
|
|
3579
|
+
const actResult = await browser.act(actions, wantScreenshot);
|
|
3580
|
+
let response = actResult.results.map((r) => `${r.success ? "OK" : "FAIL"}: ${r.result}`).join("\n");
|
|
3581
|
+
if (actResult.screenshot) {
|
|
3582
|
+
response += "\n__ACT_SCREENSHOT__:" + actResult.screenshot;
|
|
3583
|
+
}
|
|
3584
|
+
return response;
|
|
3585
|
+
}
|
|
2884
3586
|
case "browser_list_tabs":
|
|
2885
3587
|
return browser.listTabs();
|
|
2886
3588
|
case "browser_switch_tab":
|
|
@@ -3023,7 +3725,7 @@ function getLimiterForTool(toolName) {
|
|
|
3023
3725
|
return null;
|
|
3024
3726
|
}
|
|
3025
3727
|
|
|
3026
|
-
// src/
|
|
3728
|
+
// src/mcp/browser-server.ts
|
|
3027
3729
|
async function callTool(name, input) {
|
|
3028
3730
|
const limiter = getLimiterForTool(name);
|
|
3029
3731
|
if (limiter) await limiter.acquire();
|
|
@@ -3039,7 +3741,9 @@ var BROWSER_TOOL_NAMES = [
|
|
|
3039
3741
|
"browser_type",
|
|
3040
3742
|
"browser_press_key",
|
|
3041
3743
|
"browser_scroll",
|
|
3042
|
-
"
|
|
3744
|
+
"browser_select",
|
|
3745
|
+
"browser_snapshot",
|
|
3746
|
+
"browser_act",
|
|
3043
3747
|
"browser_evaluate",
|
|
3044
3748
|
"browser_list_tabs",
|
|
3045
3749
|
"browser_switch_tab",
|
|
@@ -3079,13 +3783,7 @@ function createBrowserMcpServer() {
|
|
|
3079
3783
|
const base64 = await executeTool("browser_screenshot", {});
|
|
3080
3784
|
if (base64.length > 100) {
|
|
3081
3785
|
return {
|
|
3082
|
-
content: [
|
|
3083
|
-
{
|
|
3084
|
-
type: "image",
|
|
3085
|
-
data: base64,
|
|
3086
|
-
mimeType: "image/png"
|
|
3087
|
-
}
|
|
3088
|
-
]
|
|
3786
|
+
content: [{ type: "image", data: base64, mimeType: "image/png" }]
|
|
3089
3787
|
};
|
|
3090
3788
|
}
|
|
3091
3789
|
return { content: [{ type: "text", text: base64 }] };
|
|
@@ -3119,14 +3817,77 @@ function createBrowserMcpServer() {
|
|
|
3119
3817
|
async (args) => callTool("browser_scroll", args)
|
|
3120
3818
|
),
|
|
3121
3819
|
tool(
|
|
3122
|
-
"
|
|
3123
|
-
"
|
|
3124
|
-
{
|
|
3125
|
-
|
|
3820
|
+
"browser_select",
|
|
3821
|
+
"Select an option from a dropdown menu. Handles both native <select> elements and custom dropdowns (Material Design, React, Angular). Use this instead of manually clicking dropdown items.",
|
|
3822
|
+
{
|
|
3823
|
+
selector: z.string().describe(
|
|
3824
|
+
"CSS selector of the dropdown, or its label/placeholder text (e.g. 'Month', 'Gender', '#country')"
|
|
3825
|
+
),
|
|
3826
|
+
option: z.string().describe("Visible text of the option to select (e.g. 'March', 'Male')")
|
|
3827
|
+
},
|
|
3828
|
+
async (args) => callTool("browser_select", args)
|
|
3829
|
+
),
|
|
3830
|
+
tool(
|
|
3831
|
+
"browser_snapshot",
|
|
3832
|
+
"Take a screenshot and discover all interactive elements with numbered refs. Returns a screenshot + a compact ref table. PREFERRED way to understand a page. Set annotate=true to overlay red ref badges (useful for simple pages). Use the ref numbers with browser_act to interact with elements.",
|
|
3833
|
+
{
|
|
3834
|
+
annotate: z.boolean().optional().describe(
|
|
3835
|
+
"Overlay ref badges on the screenshot. Default false. Use true for simple pages where visual context helps."
|
|
3836
|
+
)
|
|
3837
|
+
},
|
|
3838
|
+
async (args) => {
|
|
3839
|
+
const limiter = getLimiterForTool("browser_snapshot");
|
|
3840
|
+
if (limiter) await limiter.acquire();
|
|
3841
|
+
const result = await executeTool("browser_snapshot", args);
|
|
3842
|
+
const parts = result.split("\n__SNAPSHOT_IMAGE__:");
|
|
3843
|
+
const refTable = parts[0];
|
|
3844
|
+
const imageData = parts[1] || "";
|
|
3845
|
+
const content = [];
|
|
3846
|
+
if (imageData.length > 100) {
|
|
3847
|
+
content.push({ type: "image", data: imageData, mimeType: "image/png" });
|
|
3848
|
+
}
|
|
3849
|
+
content.push({ type: "text", text: refTable });
|
|
3850
|
+
return { content };
|
|
3851
|
+
}
|
|
3852
|
+
),
|
|
3853
|
+
tool(
|
|
3854
|
+
"browser_act",
|
|
3855
|
+
"Execute actions using ref numbers from browser_snapshot. Supports: click, type, select, press, scroll, wait. Batch multiple actions in one call to reduce round-trips. Set screenshot=true to see the result.",
|
|
3856
|
+
{
|
|
3857
|
+
actions: z.array(
|
|
3858
|
+
z.object({
|
|
3859
|
+
action: z.enum(["click", "type", "select", "press", "scroll", "wait"]).describe("Action type"),
|
|
3860
|
+
ref: z.number().optional().describe("Ref number from browser_snapshot"),
|
|
3861
|
+
text: z.string().optional().describe("Text to type (for 'type' action)"),
|
|
3862
|
+
option: z.string().optional().describe("Option to select (for 'select' action)"),
|
|
3863
|
+
key: z.string().optional().describe("Key to press (for 'press' action)"),
|
|
3864
|
+
direction: z.string().optional().describe("'up' or 'down' (for 'scroll')"),
|
|
3865
|
+
ms: z.number().optional().describe("Wait duration in ms (for 'wait', max 5000)")
|
|
3866
|
+
})
|
|
3867
|
+
).describe("Actions to execute sequentially"),
|
|
3868
|
+
screenshot: z.boolean().optional().describe("Take screenshot after actions (default: false)")
|
|
3869
|
+
},
|
|
3870
|
+
async (args) => {
|
|
3871
|
+
const limiter = getLimiterForTool("browser_act");
|
|
3872
|
+
if (limiter) await limiter.acquire();
|
|
3873
|
+
const result = await executeTool("browser_act", {
|
|
3874
|
+
actions: args.actions,
|
|
3875
|
+
screenshot: args.screenshot
|
|
3876
|
+
});
|
|
3877
|
+
const parts = result.split("\n__ACT_SCREENSHOT__:");
|
|
3878
|
+
const actionText = parts[0];
|
|
3879
|
+
const screenshotData = parts[1] || "";
|
|
3880
|
+
const content = [];
|
|
3881
|
+
content.push({ type: "text", text: actionText });
|
|
3882
|
+
if (screenshotData.length > 100) {
|
|
3883
|
+
content.push({ type: "image", data: screenshotData, mimeType: "image/png" });
|
|
3884
|
+
}
|
|
3885
|
+
return { content };
|
|
3886
|
+
}
|
|
3126
3887
|
),
|
|
3127
3888
|
tool(
|
|
3128
3889
|
"browser_evaluate",
|
|
3129
|
-
"Execute JavaScript in the browser page context.",
|
|
3890
|
+
"Execute JavaScript in the browser page context. Use as a last resort when browser_snapshot + browser_act cannot handle the interaction.",
|
|
3130
3891
|
{ expression: z.string().describe("JavaScript expression to evaluate") },
|
|
3131
3892
|
async (args) => callTool("browser_evaluate", args)
|
|
3132
3893
|
),
|
|
@@ -3160,20 +3921,281 @@ function createBrowserMcpServer() {
|
|
|
3160
3921
|
]
|
|
3161
3922
|
});
|
|
3162
3923
|
}
|
|
3924
|
+
|
|
3925
|
+
// src/mcp/agent-tools-server.ts
|
|
3926
|
+
import {
|
|
3927
|
+
createSdkMcpServer as createSdkMcpServer2,
|
|
3928
|
+
tool as tool2
|
|
3929
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
3930
|
+
import { z as z2 } from "zod/v4";
|
|
3931
|
+
|
|
3932
|
+
// src/credentials/credential-store.ts
|
|
3933
|
+
import { randomUUID } from "crypto";
|
|
3934
|
+
import { dirname } from "path";
|
|
3935
|
+
|
|
3936
|
+
// src/credentials/encryption.ts
|
|
3937
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash } from "crypto";
|
|
3938
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
3939
|
+
import { join as join3 } from "path";
|
|
3940
|
+
import { homedir as homedir2, hostname, userInfo } from "os";
|
|
3941
|
+
var ALGORITHM = "aes-256-gcm";
|
|
3942
|
+
var KEY_LENGTH = 32;
|
|
3943
|
+
var IV_LENGTH = 12;
|
|
3944
|
+
var AUTH_TAG_LENGTH = 16;
|
|
3945
|
+
var SALT_FILE = "encryption.salt";
|
|
3946
|
+
function deriveKey(basePath) {
|
|
3947
|
+
const saltPath = join3(basePath, SALT_FILE);
|
|
3948
|
+
let salt;
|
|
3949
|
+
if (existsSync2(saltPath)) {
|
|
3950
|
+
salt = readFileSync(saltPath);
|
|
3951
|
+
} else {
|
|
3952
|
+
salt = randomBytes(32);
|
|
3953
|
+
if (!existsSync2(basePath)) {
|
|
3954
|
+
mkdirSync2(basePath, { recursive: true, mode: 448 });
|
|
3955
|
+
}
|
|
3956
|
+
writeFileSync(saltPath, salt, { mode: 384 });
|
|
3957
|
+
}
|
|
3958
|
+
const machineId = createHash("sha256").update(hostname()).update(userInfo().username).update(homedir2()).digest();
|
|
3959
|
+
return scryptSync(machineId, salt, KEY_LENGTH);
|
|
3960
|
+
}
|
|
3961
|
+
function encrypt(plaintext, key) {
|
|
3962
|
+
const iv = randomBytes(IV_LENGTH);
|
|
3963
|
+
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
3964
|
+
const encrypted = Buffer.concat([
|
|
3965
|
+
cipher.update(plaintext, "utf-8"),
|
|
3966
|
+
cipher.final()
|
|
3967
|
+
]);
|
|
3968
|
+
return {
|
|
3969
|
+
iv: iv.toString("base64"),
|
|
3970
|
+
data: encrypted.toString("base64"),
|
|
3971
|
+
tag: cipher.getAuthTag().toString("base64")
|
|
3972
|
+
};
|
|
3973
|
+
}
|
|
3974
|
+
function decrypt(payload, key) {
|
|
3975
|
+
const iv = Buffer.from(payload.iv, "base64");
|
|
3976
|
+
const data = Buffer.from(payload.data, "base64");
|
|
3977
|
+
const tag = Buffer.from(payload.tag, "base64");
|
|
3978
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
3979
|
+
decipher.setAuthTag(tag);
|
|
3980
|
+
return Buffer.concat([
|
|
3981
|
+
decipher.update(data),
|
|
3982
|
+
decipher.final()
|
|
3983
|
+
]).toString("utf-8");
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
// src/credentials/local-store.ts
|
|
3987
|
+
import Database from "better-sqlite3";
|
|
3988
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
3989
|
+
import { join as join4 } from "path";
|
|
3990
|
+
import { homedir as homedir3 } from "os";
|
|
3991
|
+
var DEFAULT_DB_DIR = join4(homedir3(), ".config", "assistme");
|
|
3992
|
+
var DEFAULT_DB_NAME = "local.db";
|
|
3993
|
+
var LocalStore = class {
|
|
3994
|
+
db;
|
|
3995
|
+
dbPath;
|
|
3996
|
+
constructor(dbPath) {
|
|
3997
|
+
const dir = dbPath ? dbPath : DEFAULT_DB_DIR;
|
|
3998
|
+
if (!existsSync3(dir)) {
|
|
3999
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
4000
|
+
}
|
|
4001
|
+
this.dbPath = dbPath ? join4(dbPath, DEFAULT_DB_NAME) : join4(DEFAULT_DB_DIR, DEFAULT_DB_NAME);
|
|
4002
|
+
this.db = new Database(this.dbPath);
|
|
4003
|
+
this.db.pragma("journal_mode = WAL");
|
|
4004
|
+
this.db.pragma("foreign_keys = ON");
|
|
4005
|
+
this.migrate();
|
|
4006
|
+
}
|
|
4007
|
+
/** Run schema migrations. Idempotent — safe to call on every startup. */
|
|
4008
|
+
migrate() {
|
|
4009
|
+
this.db.exec(`
|
|
4010
|
+
CREATE TABLE IF NOT EXISTS credentials (
|
|
4011
|
+
id TEXT PRIMARY KEY,
|
|
4012
|
+
name TEXT NOT NULL UNIQUE,
|
|
4013
|
+
type TEXT NOT NULL DEFAULT 'secret',
|
|
4014
|
+
skill_name TEXT,
|
|
4015
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
4016
|
+
encrypted_data TEXT NOT NULL,
|
|
4017
|
+
created_at TEXT NOT NULL,
|
|
4018
|
+
updated_at TEXT NOT NULL
|
|
4019
|
+
);
|
|
4020
|
+
|
|
4021
|
+
CREATE INDEX IF NOT EXISTS idx_credentials_name ON credentials(name);
|
|
4022
|
+
CREATE INDEX IF NOT EXISTS idx_credentials_skill ON credentials(skill_name);
|
|
4023
|
+
CREATE INDEX IF NOT EXISTS idx_credentials_type ON credentials(type);
|
|
4024
|
+
`);
|
|
4025
|
+
}
|
|
4026
|
+
/** Get the raw database handle for direct queries. */
|
|
4027
|
+
getDb() {
|
|
4028
|
+
return this.db;
|
|
4029
|
+
}
|
|
4030
|
+
/** Close the database connection. */
|
|
4031
|
+
close() {
|
|
4032
|
+
this.db.close();
|
|
4033
|
+
}
|
|
4034
|
+
};
|
|
4035
|
+
var _instance = null;
|
|
4036
|
+
function getLocalStore(dbPath) {
|
|
4037
|
+
if (!_instance) {
|
|
4038
|
+
_instance = new LocalStore(dbPath);
|
|
4039
|
+
}
|
|
4040
|
+
return _instance;
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
// src/credentials/credential-store.ts
|
|
4044
|
+
var CredentialStore = class {
|
|
4045
|
+
store;
|
|
4046
|
+
encryptionKey;
|
|
4047
|
+
constructor(dbPath) {
|
|
4048
|
+
this.store = getLocalStore(dbPath);
|
|
4049
|
+
this.encryptionKey = deriveKey(dirname(this.store.dbPath));
|
|
4050
|
+
}
|
|
4051
|
+
// ── CRUD ────────────────────────────────────────────────────────
|
|
4052
|
+
save(name, type, data, opts) {
|
|
4053
|
+
const db = this.store.getDb();
|
|
4054
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4055
|
+
const encryptedData = this.encryptData(data);
|
|
4056
|
+
const tags = JSON.stringify(opts?.tags || []);
|
|
4057
|
+
const existing = db.prepare("SELECT id FROM credentials WHERE name = ?").get(name);
|
|
4058
|
+
if (existing) {
|
|
4059
|
+
db.prepare(`
|
|
4060
|
+
UPDATE credentials
|
|
4061
|
+
SET type = ?, skill_name = ?, tags = ?, encrypted_data = ?, updated_at = ?
|
|
4062
|
+
WHERE id = ?
|
|
4063
|
+
`).run(type, opts?.skillName ?? null, tags, encryptedData, now, existing.id);
|
|
4064
|
+
log.debug(`Credential "${name}" updated (${existing.id})`);
|
|
4065
|
+
return this.toMeta({ id: existing.id, name, type, skill_name: opts?.skillName ?? null, tags, encrypted_data: encryptedData, created_at: now, updated_at: now });
|
|
4066
|
+
}
|
|
4067
|
+
const id = randomUUID();
|
|
4068
|
+
db.prepare(`
|
|
4069
|
+
INSERT INTO credentials (id, name, type, skill_name, tags, encrypted_data, created_at, updated_at)
|
|
4070
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
4071
|
+
`).run(id, name, type, opts?.skillName ?? null, tags, encryptedData, now, now);
|
|
4072
|
+
log.debug(`Credential "${name}" saved (${id})`);
|
|
4073
|
+
return { id, name, type, skillName: opts?.skillName, tags: opts?.tags || [], createdAt: now, updatedAt: now };
|
|
4074
|
+
}
|
|
4075
|
+
get(id) {
|
|
4076
|
+
const row = this.store.getDb().prepare("SELECT * FROM credentials WHERE id = ?").get(id);
|
|
4077
|
+
return row ? this.toCredential(row) : null;
|
|
4078
|
+
}
|
|
4079
|
+
getByName(name) {
|
|
4080
|
+
const row = this.store.getDb().prepare("SELECT * FROM credentials WHERE name = ?").get(name);
|
|
4081
|
+
return row ? this.toCredential(row) : null;
|
|
4082
|
+
}
|
|
4083
|
+
update(id, data) {
|
|
4084
|
+
const row = this.store.getDb().prepare("SELECT * FROM credentials WHERE id = ?").get(id);
|
|
4085
|
+
if (!row) return null;
|
|
4086
|
+
const existing = this.decryptData(row.encrypted_data);
|
|
4087
|
+
const merged = { ...existing };
|
|
4088
|
+
for (const [key, value] of Object.entries(data)) {
|
|
4089
|
+
if (value !== void 0) {
|
|
4090
|
+
merged[key] = value;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4094
|
+
const encryptedData = this.encryptData(merged);
|
|
4095
|
+
this.store.getDb().prepare("UPDATE credentials SET encrypted_data = ?, updated_at = ? WHERE id = ?").run(encryptedData, now, id);
|
|
4096
|
+
log.debug(`Credential "${row.name}" updated`);
|
|
4097
|
+
return this.toMeta({ ...row, encrypted_data: encryptedData, updated_at: now });
|
|
4098
|
+
}
|
|
4099
|
+
remove(id) {
|
|
4100
|
+
const result = this.store.getDb().prepare("DELETE FROM credentials WHERE id = ?").run(id);
|
|
4101
|
+
if (result.changes > 0) {
|
|
4102
|
+
log.debug(`Credential ${id} removed`);
|
|
4103
|
+
return true;
|
|
4104
|
+
}
|
|
4105
|
+
return false;
|
|
4106
|
+
}
|
|
4107
|
+
removeByName(name) {
|
|
4108
|
+
const result = this.store.getDb().prepare("DELETE FROM credentials WHERE name = ?").run(name);
|
|
4109
|
+
if (result.changes > 0) {
|
|
4110
|
+
log.debug(`Credential "${name}" removed`);
|
|
4111
|
+
return true;
|
|
4112
|
+
}
|
|
4113
|
+
return false;
|
|
4114
|
+
}
|
|
4115
|
+
// ── Query ───────────────────────────────────────────────────────
|
|
4116
|
+
list() {
|
|
4117
|
+
const rows = this.store.getDb().prepare("SELECT * FROM credentials ORDER BY updated_at DESC").all();
|
|
4118
|
+
return rows.map((r) => this.toMeta(r));
|
|
4119
|
+
}
|
|
4120
|
+
findBySkill(skillName) {
|
|
4121
|
+
const rows = this.store.getDb().prepare("SELECT * FROM credentials WHERE skill_name = ? ORDER BY name").all(skillName);
|
|
4122
|
+
return rows.map((r) => this.toMeta(r));
|
|
4123
|
+
}
|
|
4124
|
+
findByTag(tag) {
|
|
4125
|
+
const rows = this.store.getDb().prepare("SELECT * FROM credentials WHERE tags LIKE ? ORDER BY name").all(`%${tag.toLowerCase()}%`);
|
|
4126
|
+
return rows.filter((r) => {
|
|
4127
|
+
const tags = JSON.parse(r.tags);
|
|
4128
|
+
return tags.some((t) => t.toLowerCase() === tag.toLowerCase());
|
|
4129
|
+
}).map((r) => this.toMeta(r));
|
|
4130
|
+
}
|
|
4131
|
+
findByType(type) {
|
|
4132
|
+
const rows = this.store.getDb().prepare("SELECT * FROM credentials WHERE type = ? ORDER BY name").all(type);
|
|
4133
|
+
return rows.map((r) => this.toMeta(r));
|
|
4134
|
+
}
|
|
4135
|
+
// ── Bulk ────────────────────────────────────────────────────────
|
|
4136
|
+
removeBySkill(skillName) {
|
|
4137
|
+
const result = this.store.getDb().prepare("DELETE FROM credentials WHERE skill_name = ?").run(skillName);
|
|
4138
|
+
return result.changes;
|
|
4139
|
+
}
|
|
4140
|
+
clear() {
|
|
4141
|
+
this.store.getDb().prepare("DELETE FROM credentials").run();
|
|
4142
|
+
}
|
|
4143
|
+
// ── Internal ────────────────────────────────────────────────────
|
|
4144
|
+
encryptData(data) {
|
|
4145
|
+
const payload = encrypt(JSON.stringify(data), this.encryptionKey);
|
|
4146
|
+
return JSON.stringify(payload);
|
|
4147
|
+
}
|
|
4148
|
+
decryptData(encrypted) {
|
|
4149
|
+
const payload = JSON.parse(encrypted);
|
|
4150
|
+
const decrypted = decrypt(payload, this.encryptionKey);
|
|
4151
|
+
return JSON.parse(decrypted);
|
|
4152
|
+
}
|
|
4153
|
+
toMeta(row) {
|
|
4154
|
+
return {
|
|
4155
|
+
id: row.id,
|
|
4156
|
+
name: row.name,
|
|
4157
|
+
type: row.type,
|
|
4158
|
+
skillName: row.skill_name || void 0,
|
|
4159
|
+
tags: JSON.parse(row.tags),
|
|
4160
|
+
createdAt: row.created_at,
|
|
4161
|
+
updatedAt: row.updated_at
|
|
4162
|
+
};
|
|
4163
|
+
}
|
|
4164
|
+
toCredential(row) {
|
|
4165
|
+
try {
|
|
4166
|
+
return {
|
|
4167
|
+
meta: this.toMeta(row),
|
|
4168
|
+
data: this.decryptData(row.encrypted_data)
|
|
4169
|
+
};
|
|
4170
|
+
} catch (err) {
|
|
4171
|
+
log.debug(`Failed to decrypt credential ${row.id}: ${err}`);
|
|
4172
|
+
return null;
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
};
|
|
4176
|
+
var _instance2 = null;
|
|
4177
|
+
function getCredentialStore() {
|
|
4178
|
+
if (!_instance2) {
|
|
4179
|
+
_instance2 = new CredentialStore();
|
|
4180
|
+
}
|
|
4181
|
+
return _instance2;
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
// src/mcp/agent-tools-server.ts
|
|
3163
4185
|
function createAgentToolsServer(deps) {
|
|
3164
|
-
const { memoryManager, skillManager, taskId, sessionId
|
|
3165
|
-
return
|
|
4186
|
+
const { memoryManager, skillManager, taskId, sessionId } = deps;
|
|
4187
|
+
return createSdkMcpServer2({
|
|
3166
4188
|
name: "assistme-agent",
|
|
3167
4189
|
version: "1.0.0",
|
|
3168
4190
|
tools: [
|
|
3169
|
-
|
|
4191
|
+
tool2(
|
|
3170
4192
|
"memory_store",
|
|
3171
4193
|
"Store a memory about the user that persists across conversations. Use when you learn preferences, habits, or standing instructions.",
|
|
3172
4194
|
{
|
|
3173
|
-
content:
|
|
3174
|
-
category:
|
|
3175
|
-
importance:
|
|
3176
|
-
tags:
|
|
4195
|
+
content: z2.string().describe("What to remember (concise, factual statement)"),
|
|
4196
|
+
category: z2.string().optional().describe("Category: general, preference, instruction, context, skill_learned, fact"),
|
|
4197
|
+
importance: z2.number().optional().describe("Importance 1-10 (default: 5). Use 8+ for instructions"),
|
|
4198
|
+
tags: z2.array(z2.string()).optional().describe("Optional tags for searchability")
|
|
3177
4199
|
},
|
|
3178
4200
|
async (args) => {
|
|
3179
4201
|
if (!memoryManager) {
|
|
@@ -3194,23 +4216,25 @@ function createAgentToolsServer(deps) {
|
|
|
3194
4216
|
return { content: [{ type: "text", text: result }] };
|
|
3195
4217
|
}
|
|
3196
4218
|
),
|
|
3197
|
-
|
|
4219
|
+
tool2(
|
|
3198
4220
|
"skill_create",
|
|
3199
4221
|
"Create a new skill and add it to the user's collection. Returns the skill ID on success.",
|
|
3200
4222
|
{
|
|
3201
|
-
name:
|
|
3202
|
-
description:
|
|
3203
|
-
instructions:
|
|
3204
|
-
emoji:
|
|
4223
|
+
name: z2.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
|
|
4224
|
+
description: z2.string().describe("One-line description of what this skill does"),
|
|
4225
|
+
instructions: z2.string().describe("Markdown step-by-step instructions"),
|
|
4226
|
+
emoji: z2.string().optional().describe("Single emoji representing this skill")
|
|
3205
4227
|
},
|
|
3206
4228
|
async (args) => {
|
|
3207
4229
|
const nameError = validateSkillName(args.name);
|
|
3208
4230
|
if (nameError) {
|
|
3209
4231
|
return {
|
|
3210
|
-
content: [
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
4232
|
+
content: [
|
|
4233
|
+
{
|
|
4234
|
+
type: "text",
|
|
4235
|
+
text: `Invalid skill name: ${nameError}. Use lowercase kebab-case like "flight-booking".`
|
|
4236
|
+
}
|
|
4237
|
+
]
|
|
3214
4238
|
};
|
|
3215
4239
|
}
|
|
3216
4240
|
const existing = skillManager.findSimilar(args.name);
|
|
@@ -3224,12 +4248,10 @@ function createAgentToolsServer(deps) {
|
|
|
3224
4248
|
]
|
|
3225
4249
|
};
|
|
3226
4250
|
}
|
|
3227
|
-
const result = await skillManager.create(
|
|
3228
|
-
|
|
3229
|
-
args.
|
|
3230
|
-
|
|
3231
|
-
{ source: "manual", emoji: args.emoji }
|
|
3232
|
-
);
|
|
4251
|
+
const result = await skillManager.create(args.name, args.description, args.instructions, {
|
|
4252
|
+
source: "manual",
|
|
4253
|
+
emoji: args.emoji
|
|
4254
|
+
});
|
|
3233
4255
|
if (!result) {
|
|
3234
4256
|
return {
|
|
3235
4257
|
content: [{ type: "text", text: `Failed to create skill "${args.name}".` }]
|
|
@@ -3257,13 +4279,13 @@ function createAgentToolsServer(deps) {
|
|
|
3257
4279
|
};
|
|
3258
4280
|
}
|
|
3259
4281
|
),
|
|
3260
|
-
|
|
4282
|
+
tool2(
|
|
3261
4283
|
"skill_improve",
|
|
3262
4284
|
"Improve an existing skill with better instructions based on what you just learned. Version auto-bumped.",
|
|
3263
4285
|
{
|
|
3264
|
-
name:
|
|
3265
|
-
improved_instructions:
|
|
3266
|
-
description:
|
|
4286
|
+
name: z2.string().describe("Name of the existing skill to improve"),
|
|
4287
|
+
improved_instructions: z2.string().describe("Full updated markdown instructions (not a diff)"),
|
|
4288
|
+
description: z2.string().optional().describe("Updated description (optional)")
|
|
3267
4289
|
},
|
|
3268
4290
|
async (args) => {
|
|
3269
4291
|
const existing = skillManager.get(args.name);
|
|
@@ -3304,12 +4326,12 @@ function createAgentToolsServer(deps) {
|
|
|
3304
4326
|
};
|
|
3305
4327
|
}
|
|
3306
4328
|
),
|
|
3307
|
-
|
|
4329
|
+
tool2(
|
|
3308
4330
|
"skill_invoke",
|
|
3309
4331
|
"Load a skill's full instructions when relevant to the current task. Call this when you determine a skill from the Available Skills list matches the user's request.",
|
|
3310
4332
|
{
|
|
3311
|
-
name:
|
|
3312
|
-
arguments:
|
|
4333
|
+
name: z2.string().describe("Skill name from the Available Skills list"),
|
|
4334
|
+
arguments: z2.string().optional().describe("Arguments to pass to the skill (replaces $ARGUMENTS placeholders)")
|
|
3313
4335
|
},
|
|
3314
4336
|
async (args) => {
|
|
3315
4337
|
const skill = skillManager.get(args.name);
|
|
@@ -3343,6 +4365,39 @@ ${content}`;
|
|
|
3343
4365
|
**Allowed tools for this skill:** ${skill.allowedTools.join(", ")}
|
|
3344
4366
|
`;
|
|
3345
4367
|
}
|
|
4368
|
+
const credReqs = skill.metadata.credentials;
|
|
4369
|
+
if (credReqs && credReqs.length > 0) {
|
|
4370
|
+
const store = getCredentialStore();
|
|
4371
|
+
const missing = [];
|
|
4372
|
+
for (const req of credReqs) {
|
|
4373
|
+
const cred = store.getByName(req.name);
|
|
4374
|
+
if (cred) {
|
|
4375
|
+
response += `
|
|
4376
|
+
|
|
4377
|
+
**Credential: ${req.name}** (${req.type})
|
|
4378
|
+
`;
|
|
4379
|
+
response += `\`\`\`json
|
|
4380
|
+
${JSON.stringify(cred.data, null, 2)}
|
|
4381
|
+
\`\`\`
|
|
4382
|
+
`;
|
|
4383
|
+
} else if (req.required) {
|
|
4384
|
+
missing.push(`${req.name} (${req.description})`);
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
if (missing.length > 0) {
|
|
4388
|
+
response += `
|
|
4389
|
+
|
|
4390
|
+
**Missing required credentials:**
|
|
4391
|
+
`;
|
|
4392
|
+
for (const m of missing) {
|
|
4393
|
+
response += `- ${m}
|
|
4394
|
+
`;
|
|
4395
|
+
}
|
|
4396
|
+
response += `
|
|
4397
|
+
Use \`ask_user\` to request these from the user, or create them yourself (e.g. register an account), then store with \`credential_set\`.
|
|
4398
|
+
`;
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
3346
4401
|
log.info(`Skill invoked: "${args.name}"`);
|
|
3347
4402
|
skillManager.logInvocation(args.name, {
|
|
3348
4403
|
messageId: taskId,
|
|
@@ -3355,12 +4410,12 @@ ${content}`;
|
|
|
3355
4410
|
};
|
|
3356
4411
|
}
|
|
3357
4412
|
),
|
|
3358
|
-
|
|
4413
|
+
tool2(
|
|
3359
4414
|
"skill_search",
|
|
3360
4415
|
"Search for skills by keyword. Uses full-text search across skill names, descriptions, and content. Use this to discover relevant skills when the Available Skills list doesn't have an obvious match.",
|
|
3361
4416
|
{
|
|
3362
|
-
query:
|
|
3363
|
-
limit:
|
|
4417
|
+
query: z2.string().describe("Search query (keywords, topic, or task description)"),
|
|
4418
|
+
limit: z2.number().optional().describe("Max results (default: 5)")
|
|
3364
4419
|
},
|
|
3365
4420
|
async (args) => {
|
|
3366
4421
|
const results = await skillManager.searchDb(args.query, args.limit || 5);
|
|
@@ -3382,14 +4437,14 @@ ${content}`;
|
|
|
3382
4437
|
return { content: [{ type: "text", text: response }] };
|
|
3383
4438
|
}
|
|
3384
4439
|
),
|
|
3385
|
-
|
|
4440
|
+
tool2(
|
|
3386
4441
|
"skill_generate",
|
|
3387
4442
|
"Prepare context for generating skills from a job description. Returns existing skills and job info so you can analyze the job and create skills using skill_create (which auto-adds to user's collection). After creating all skills, call skill_link_job to link them to the job and mark it as analyzed.",
|
|
3388
4443
|
{
|
|
3389
|
-
job_name:
|
|
4444
|
+
job_name: z2.string().describe(
|
|
3390
4445
|
"Short name for this job/role. Example: '\u7535\u5546\u8FD0\u8425', 'Frontend Dev', 'Data Analyst'"
|
|
3391
4446
|
),
|
|
3392
|
-
job_description:
|
|
4447
|
+
job_description: z2.string().describe(
|
|
3393
4448
|
"Description of the user's job, role, and daily tasks. Can be in any language. Example: '\u6211\u662F\u7535\u5546\u8FD0\u8425\uFF0C\u6BCF\u5929\u8981\u770B\u7ADE\u54C1\u4EF7\u683C\u3001\u5199\u5546\u54C1\u6587\u6848\u3001\u56DE\u590D\u5BA2\u6237\u8BC4\u8BBA'"
|
|
3394
4449
|
)
|
|
3395
4450
|
},
|
|
@@ -3453,47 +4508,48 @@ ${content}`;
|
|
|
3453
4508
|
return { content: [{ type: "text", text: response }] };
|
|
3454
4509
|
}
|
|
3455
4510
|
),
|
|
3456
|
-
|
|
4511
|
+
tool2(
|
|
3457
4512
|
"skill_link_job",
|
|
3458
4513
|
"Link created skills to a job and mark it as analyzed. Call this after creating all skills for a job via skill_create + skill_add.",
|
|
3459
4514
|
{
|
|
3460
|
-
job_name:
|
|
3461
|
-
job_description:
|
|
3462
|
-
skill_names:
|
|
4515
|
+
job_name: z2.string().describe("Name of the job to link skills to"),
|
|
4516
|
+
job_description: z2.string().describe("Job description (used if job doesn't exist yet)"),
|
|
4517
|
+
skill_names: z2.array(z2.string()).describe("Names of skills to link to this job")
|
|
3463
4518
|
},
|
|
3464
4519
|
async (args) => {
|
|
3465
|
-
if (!userId) {
|
|
3466
|
-
return {
|
|
3467
|
-
content: [{ type: "text", text: "Not authenticated. Cannot link job." }]
|
|
3468
|
-
};
|
|
3469
|
-
}
|
|
3470
4520
|
try {
|
|
3471
|
-
await saveJobToDb(
|
|
3472
|
-
log.success(
|
|
4521
|
+
await saveJobToDb(args.job_name, args.job_description, args.skill_names);
|
|
4522
|
+
log.success(
|
|
4523
|
+
`Job "${args.job_name}": linked ${args.skill_names.length} skills and marked as analyzed`
|
|
4524
|
+
);
|
|
3473
4525
|
return {
|
|
3474
|
-
content: [
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
4526
|
+
content: [
|
|
4527
|
+
{
|
|
4528
|
+
type: "text",
|
|
4529
|
+
text: `Job "${args.job_name}" linked with ${args.skill_names.length} skills and marked as analyzed.`
|
|
4530
|
+
}
|
|
4531
|
+
]
|
|
3478
4532
|
};
|
|
3479
4533
|
} catch (err) {
|
|
3480
4534
|
return {
|
|
3481
|
-
content: [
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
4535
|
+
content: [
|
|
4536
|
+
{
|
|
4537
|
+
type: "text",
|
|
4538
|
+
text: `Failed to link job: ${err instanceof Error ? err.message : err}`
|
|
4539
|
+
}
|
|
4540
|
+
]
|
|
3485
4541
|
};
|
|
3486
4542
|
}
|
|
3487
4543
|
}
|
|
3488
4544
|
),
|
|
3489
|
-
|
|
4545
|
+
tool2(
|
|
3490
4546
|
"skill_browse",
|
|
3491
4547
|
"Browse the skill marketplace to discover skills published by the community. Search by keyword, filter by category, and sort by popularity or rating.",
|
|
3492
4548
|
{
|
|
3493
|
-
query:
|
|
3494
|
-
category:
|
|
3495
|
-
sort:
|
|
3496
|
-
limit:
|
|
4549
|
+
query: z2.string().optional().describe("Search keywords"),
|
|
4550
|
+
category: z2.string().optional().describe("Filter by category (e.g. 'productivity', 'ecommerce', 'dev-tools')"),
|
|
4551
|
+
sort: z2.enum(["popular", "recent", "rating"]).optional().describe("Sort order (default: popular)"),
|
|
4552
|
+
limit: z2.number().optional().describe("Max results (default: 10)")
|
|
3497
4553
|
},
|
|
3498
4554
|
async (args) => {
|
|
3499
4555
|
const results = await skillManager.browse({
|
|
@@ -3526,41 +4582,47 @@ ${content}`;
|
|
|
3526
4582
|
return { content: [{ type: "text", text: response }] };
|
|
3527
4583
|
}
|
|
3528
4584
|
),
|
|
3529
|
-
|
|
4585
|
+
tool2(
|
|
3530
4586
|
"skill_add",
|
|
3531
4587
|
"Add a skill to your personal collection. Works for both marketplace skills and newly created drafts. This is the approval step \u2014 after adding, the skill becomes available for use via skill_invoke.",
|
|
3532
4588
|
{
|
|
3533
|
-
skill_id:
|
|
4589
|
+
skill_id: z2.string().describe("The skill UUID (from skill_browse or skill_create results)")
|
|
3534
4590
|
},
|
|
3535
4591
|
async (args) => {
|
|
3536
4592
|
const added = await skillManager.addSkill(args.skill_id);
|
|
3537
4593
|
if (!added) {
|
|
3538
4594
|
return {
|
|
3539
|
-
content: [
|
|
4595
|
+
content: [
|
|
4596
|
+
{ type: "text", text: `Failed to add skill. Check that the ID is correct.` }
|
|
4597
|
+
]
|
|
3540
4598
|
};
|
|
3541
4599
|
}
|
|
3542
4600
|
const emoji = added.metadata.emoji ? `${added.metadata.emoji} ` : "";
|
|
3543
4601
|
return {
|
|
3544
|
-
content: [
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
4602
|
+
content: [
|
|
4603
|
+
{
|
|
4604
|
+
type: "text",
|
|
4605
|
+
text: `Added **${emoji}${added.name}** v${added.version} to your collection. It's now available for use via skill_invoke.`
|
|
4606
|
+
}
|
|
4607
|
+
]
|
|
3548
4608
|
};
|
|
3549
4609
|
}
|
|
3550
4610
|
),
|
|
3551
|
-
|
|
4611
|
+
tool2(
|
|
3552
4612
|
"skill_publish",
|
|
3553
4613
|
"Publish one of your skills to the marketplace so others can discover and install it.",
|
|
3554
4614
|
{
|
|
3555
|
-
name:
|
|
3556
|
-
category:
|
|
3557
|
-
author_name:
|
|
4615
|
+
name: z2.string().describe("Name of your skill to publish"),
|
|
4616
|
+
category: z2.string().optional().describe("Category (e.g. 'productivity', 'ecommerce', 'dev-tools')"),
|
|
4617
|
+
author_name: z2.string().optional().describe("Your display name as the author")
|
|
3558
4618
|
},
|
|
3559
4619
|
async (args) => {
|
|
3560
4620
|
const skill = skillManager.get(args.name);
|
|
3561
4621
|
if (!skill) {
|
|
3562
4622
|
return {
|
|
3563
|
-
content: [
|
|
4623
|
+
content: [
|
|
4624
|
+
{ type: "text", text: `Skill "${args.name}" not found in your collection.` }
|
|
4625
|
+
]
|
|
3564
4626
|
};
|
|
3565
4627
|
}
|
|
3566
4628
|
if (skill.source === "external") {
|
|
@@ -3574,30 +4636,43 @@ ${content}`;
|
|
|
3574
4636
|
});
|
|
3575
4637
|
if (!result) {
|
|
3576
4638
|
return {
|
|
3577
|
-
content: [
|
|
4639
|
+
content: [
|
|
4640
|
+
{
|
|
4641
|
+
type: "text",
|
|
4642
|
+
text: `Failed to publish "${args.name}". The name may already be taken by another author.`
|
|
4643
|
+
}
|
|
4644
|
+
]
|
|
3578
4645
|
};
|
|
3579
4646
|
}
|
|
3580
4647
|
return {
|
|
3581
|
-
content: [
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
4648
|
+
content: [
|
|
4649
|
+
{
|
|
4650
|
+
type: "text",
|
|
4651
|
+
text: `Skill "${args.name}" published to the marketplace! Others can now find and install it.`
|
|
4652
|
+
}
|
|
4653
|
+
]
|
|
3585
4654
|
};
|
|
3586
4655
|
}
|
|
3587
4656
|
),
|
|
3588
4657
|
// ── User Interaction Tool ───────────────────────────────────
|
|
3589
|
-
|
|
4658
|
+
tool2(
|
|
3590
4659
|
"ask_user",
|
|
3591
4660
|
"Ask the user a question via the web UI and wait for their response. Shows a message with optional predefined option buttons PLUS a free-text input field \u2014 the user can either click a suggested option or type a custom answer. ALWAYS provide options when you can suggest likely answers. Do NOT use this for information you can discover yourself (git remote, file contents, etc.).",
|
|
3592
4661
|
{
|
|
3593
|
-
question:
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
4662
|
+
question: z2.string().describe(
|
|
4663
|
+
"The question to ask (supports markdown). Be specific about what you need and why."
|
|
4664
|
+
),
|
|
4665
|
+
options: z2.array(
|
|
4666
|
+
z2.object({
|
|
4667
|
+
label: z2.string().describe("Button label shown to user"),
|
|
4668
|
+
action_key: z2.string().describe("Machine-readable key returned when selected"),
|
|
4669
|
+
description: z2.string().optional().describe("Tooltip/description for this option")
|
|
4670
|
+
})
|
|
4671
|
+
).optional().describe(
|
|
4672
|
+
"Suggested options shown as buttons. The user can always type a custom answer instead."
|
|
4673
|
+
),
|
|
4674
|
+
placeholder: z2.string().optional().describe("Placeholder text for the free-text input field"),
|
|
4675
|
+
timeout_seconds: z2.number().optional().describe("How long to wait for response (default: 300)")
|
|
3601
4676
|
},
|
|
3602
4677
|
async (args) => {
|
|
3603
4678
|
const actionId = `ask_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -3615,6 +4690,11 @@ ${content}`;
|
|
|
3615
4690
|
log.info(`Ask user ${actionId}: "${args.question.slice(0, 80)}..."`);
|
|
3616
4691
|
emitEvent(taskId, "user_action_request", actionData).catch(() => {
|
|
3617
4692
|
});
|
|
4693
|
+
emitEvent(taskId, "status_change", {
|
|
4694
|
+
status: "waiting_for_user",
|
|
4695
|
+
message: args.question
|
|
4696
|
+
}).catch(() => {
|
|
4697
|
+
});
|
|
3618
4698
|
const startTime = Date.now();
|
|
3619
4699
|
const pollInterval = 2e3;
|
|
3620
4700
|
while (Date.now() - startTime < timeout) {
|
|
@@ -3625,56 +4705,55 @@ ${content}`;
|
|
|
3625
4705
|
const label = response.label || actionKey || text;
|
|
3626
4706
|
log.info(`User responded: "${label}"`);
|
|
3627
4707
|
return {
|
|
3628
|
-
content: [
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
4708
|
+
content: [
|
|
4709
|
+
{
|
|
4710
|
+
type: "text",
|
|
4711
|
+
text: JSON.stringify({
|
|
4712
|
+
status: "responded",
|
|
4713
|
+
action_key: actionKey || "custom_input",
|
|
4714
|
+
label,
|
|
4715
|
+
text: text || label
|
|
4716
|
+
})
|
|
4717
|
+
}
|
|
4718
|
+
]
|
|
3637
4719
|
};
|
|
3638
4720
|
}
|
|
3639
4721
|
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
3640
4722
|
}
|
|
3641
4723
|
log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
|
|
3642
4724
|
return {
|
|
3643
|
-
content: [
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
4725
|
+
content: [
|
|
4726
|
+
{
|
|
4727
|
+
type: "text",
|
|
4728
|
+
text: JSON.stringify({
|
|
4729
|
+
status: "timeout",
|
|
4730
|
+
message: "User did not respond within the timeout period."
|
|
4731
|
+
})
|
|
4732
|
+
}
|
|
4733
|
+
]
|
|
3650
4734
|
};
|
|
3651
4735
|
} catch (err) {
|
|
3652
4736
|
log.error(`ask_user failed: ${err}`);
|
|
3653
4737
|
return {
|
|
3654
|
-
content: [
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
4738
|
+
content: [
|
|
4739
|
+
{
|
|
4740
|
+
type: "text",
|
|
4741
|
+
text: `Failed to ask user: ${err instanceof Error ? err.message : err}`
|
|
4742
|
+
}
|
|
4743
|
+
]
|
|
3658
4744
|
};
|
|
3659
4745
|
}
|
|
3660
4746
|
}
|
|
3661
4747
|
),
|
|
3662
4748
|
// ── Job Automation Tools ──────────────────────────────────────
|
|
3663
|
-
|
|
4749
|
+
tool2(
|
|
3664
4750
|
"job_run",
|
|
3665
4751
|
"Run a job by loading its goal and available skills as capabilities. You then decide dynamically which skills to use, in what order, and how to chain them based on what you discover. Use this when the user asks to run their job, or when a scheduled job fires.",
|
|
3666
4752
|
{
|
|
3667
|
-
job_name:
|
|
3668
|
-
"Name of the job to run (e.g. 'software-engineer', 'Frontend Dev')"
|
|
3669
|
-
)
|
|
4753
|
+
job_name: z2.string().describe("Name of the job to run (e.g. 'software-engineer', 'Frontend Dev')")
|
|
3670
4754
|
},
|
|
3671
4755
|
async (args) => {
|
|
3672
|
-
|
|
3673
|
-
return {
|
|
3674
|
-
content: [{ type: "text", text: "Not authenticated. Cannot run job." }]
|
|
3675
|
-
};
|
|
3676
|
-
}
|
|
3677
|
-
const runner = new JobRunner(userId);
|
|
4756
|
+
const runner = new JobRunner();
|
|
3678
4757
|
const job = await runner.loadJob(args.job_name);
|
|
3679
4758
|
if (!job) {
|
|
3680
4759
|
const jobs = await runner.listJobs();
|
|
@@ -3685,10 +4764,12 @@ ${content}`;
|
|
|
3685
4764
|
}
|
|
3686
4765
|
if (job.skills.length === 0) {
|
|
3687
4766
|
return {
|
|
3688
|
-
content: [
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
4767
|
+
content: [
|
|
4768
|
+
{
|
|
4769
|
+
type: "text",
|
|
4770
|
+
text: `Job "${args.job_name}" has no linked skills. Use skill_generate to add skills to this job.`
|
|
4771
|
+
}
|
|
4772
|
+
]
|
|
3692
4773
|
};
|
|
3693
4774
|
}
|
|
3694
4775
|
const runId = await runner.createRun(job.jobId, {
|
|
@@ -3700,51 +4781,55 @@ ${content}`;
|
|
|
3700
4781
|
log.debug("Failed to create job run record, proceeding without tracking");
|
|
3701
4782
|
}
|
|
3702
4783
|
const prompt = runner.buildJobPrompt(job, runId || "untracked");
|
|
3703
|
-
log.info(
|
|
4784
|
+
log.info(
|
|
4785
|
+
`Job "${args.job_name}" started with ${job.skills.length} skills (run: ${runId?.slice(0, 8) || "untracked"})`
|
|
4786
|
+
);
|
|
3704
4787
|
return {
|
|
3705
4788
|
content: [{ type: "text", text: prompt }]
|
|
3706
4789
|
};
|
|
3707
4790
|
}
|
|
3708
4791
|
),
|
|
3709
|
-
|
|
4792
|
+
tool2(
|
|
3710
4793
|
"job_schedule",
|
|
3711
4794
|
"Schedule a job to run automatically on a recurring basis using a cron expression. For example, schedule your 'software-engineer' job to run every morning at 9am.",
|
|
3712
4795
|
{
|
|
3713
|
-
job_name:
|
|
3714
|
-
cron:
|
|
4796
|
+
job_name: z2.string().describe("Name of the job to schedule"),
|
|
4797
|
+
cron: z2.string().describe(
|
|
3715
4798
|
"Cron expression: 'minute hour day-of-month month day-of-week'. Examples: '0 9 * * *' (daily 9am), '0 9 * * 1-5' (weekdays 9am), '0 */2 * * *' (every 2 hours)"
|
|
3716
4799
|
),
|
|
3717
|
-
timezone:
|
|
3718
|
-
schedule_name:
|
|
4800
|
+
timezone: z2.string().optional().describe("Timezone (default: UTC). Examples: 'UTC', 'America/New_York'"),
|
|
4801
|
+
schedule_name: z2.string().optional().describe("Custom name for this schedule (default: 'Job: <job_name>')")
|
|
3719
4802
|
},
|
|
3720
4803
|
async (args) => {
|
|
3721
|
-
|
|
3722
|
-
return {
|
|
3723
|
-
content: [{ type: "text", text: "Not authenticated. Cannot schedule job." }]
|
|
3724
|
-
};
|
|
3725
|
-
}
|
|
3726
|
-
const runner = new JobRunner(userId);
|
|
4804
|
+
const runner = new JobRunner();
|
|
3727
4805
|
const job = await runner.loadJob(args.job_name);
|
|
3728
4806
|
if (!job) {
|
|
3729
4807
|
return {
|
|
3730
|
-
content: [
|
|
4808
|
+
content: [
|
|
4809
|
+
{
|
|
4810
|
+
type: "text",
|
|
4811
|
+
text: `Job "${args.job_name}" not found. Create it first with skill_generate.`
|
|
4812
|
+
}
|
|
4813
|
+
]
|
|
3731
4814
|
};
|
|
3732
4815
|
}
|
|
3733
4816
|
try {
|
|
3734
4817
|
getNextRunTime(args.cron, args.timezone || "UTC");
|
|
3735
4818
|
} catch {
|
|
3736
4819
|
return {
|
|
3737
|
-
content: [
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
4820
|
+
content: [
|
|
4821
|
+
{
|
|
4822
|
+
type: "text",
|
|
4823
|
+
text: `Invalid cron expression: "${args.cron}". Use format: "minute hour day-of-month month day-of-week"`
|
|
4824
|
+
}
|
|
4825
|
+
]
|
|
3741
4826
|
};
|
|
3742
4827
|
}
|
|
3743
4828
|
const name = args.schedule_name || `Job: ${args.job_name}`;
|
|
3744
4829
|
const prompt = `[JobRun: ${args.job_name}] Run the "${args.job_name}" job. Use job_run to execute it.`;
|
|
3745
4830
|
const tz = args.timezone || "UTC";
|
|
3746
4831
|
try {
|
|
3747
|
-
const task = await createScheduledTask(
|
|
4832
|
+
const task = await createScheduledTask(name, prompt, args.cron, tz);
|
|
3748
4833
|
await callMcpHandler("schedule.link_job", {
|
|
3749
4834
|
task_id: task.id,
|
|
3750
4835
|
job_id: job.jobId
|
|
@@ -3772,36 +4857,35 @@ ${content}`;
|
|
|
3772
4857
|
return { content: [{ type: "text", text: response }] };
|
|
3773
4858
|
} catch (err) {
|
|
3774
4859
|
return {
|
|
3775
|
-
content: [
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
4860
|
+
content: [
|
|
4861
|
+
{
|
|
4862
|
+
type: "text",
|
|
4863
|
+
text: `Failed to schedule job: ${err instanceof Error ? err.message : err}`
|
|
4864
|
+
}
|
|
4865
|
+
]
|
|
3779
4866
|
};
|
|
3780
4867
|
}
|
|
3781
4868
|
}
|
|
3782
4869
|
),
|
|
3783
|
-
|
|
4870
|
+
tool2(
|
|
3784
4871
|
"job_status",
|
|
3785
4872
|
"Check the status and run history of a job. Shows recent executions, success rates, and details.",
|
|
3786
4873
|
{
|
|
3787
|
-
job_name:
|
|
3788
|
-
limit:
|
|
4874
|
+
job_name: z2.string().optional().describe("Job name to check (omit for all jobs)"),
|
|
4875
|
+
limit: z2.number().optional().describe("Max number of runs to show (default: 5)")
|
|
3789
4876
|
},
|
|
3790
4877
|
async (args) => {
|
|
3791
|
-
|
|
3792
|
-
return {
|
|
3793
|
-
content: [{ type: "text", text: "Not authenticated." }]
|
|
3794
|
-
};
|
|
3795
|
-
}
|
|
3796
|
-
const runner = new JobRunner(userId);
|
|
4878
|
+
const runner = new JobRunner();
|
|
3797
4879
|
if (!args.job_name) {
|
|
3798
4880
|
const jobs = await runner.listJobs();
|
|
3799
4881
|
if (jobs.length === 0) {
|
|
3800
4882
|
return {
|
|
3801
|
-
content: [
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
4883
|
+
content: [
|
|
4884
|
+
{
|
|
4885
|
+
type: "text",
|
|
4886
|
+
text: "No jobs defined. Use skill_generate to create a job from your job description."
|
|
4887
|
+
}
|
|
4888
|
+
]
|
|
3805
4889
|
};
|
|
3806
4890
|
}
|
|
3807
4891
|
let response2 = "## Your Jobs\n\n";
|
|
@@ -3815,10 +4899,12 @@ ${content}`;
|
|
|
3815
4899
|
const runs = await runner.getRunHistory(args.job_name, args.limit || 5);
|
|
3816
4900
|
if (runs.length === 0) {
|
|
3817
4901
|
return {
|
|
3818
|
-
content: [
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
4902
|
+
content: [
|
|
4903
|
+
{
|
|
4904
|
+
type: "text",
|
|
4905
|
+
text: `No runs found for job "${args.job_name}". Use job_run to execute it.`
|
|
4906
|
+
}
|
|
4907
|
+
]
|
|
3822
4908
|
};
|
|
3823
4909
|
}
|
|
3824
4910
|
let response = `## Job Status: ${args.job_name}
|
|
@@ -3847,18 +4933,142 @@ ${content}`;
|
|
|
3847
4933
|
response += "\n";
|
|
3848
4934
|
return { content: [{ type: "text", text: response }] };
|
|
3849
4935
|
}
|
|
4936
|
+
),
|
|
4937
|
+
// ── Credential Tools ──────────────────────────────────────────
|
|
4938
|
+
tool2(
|
|
4939
|
+
"credential_get",
|
|
4940
|
+
"Retrieve a locally stored credential by name. Returns the secret data (API keys, tokens, etc.) stored on the user's machine. Use this when a skill needs authentication or API access.",
|
|
4941
|
+
{
|
|
4942
|
+
name: z2.string().describe("Credential name (e.g. 'amazon-login', 'openai-api-key')")
|
|
4943
|
+
},
|
|
4944
|
+
async (args) => {
|
|
4945
|
+
const store = getCredentialStore();
|
|
4946
|
+
const credential = store.getByName(args.name);
|
|
4947
|
+
if (!credential) {
|
|
4948
|
+
const all = store.list();
|
|
4949
|
+
const available = all.length > 0 ? `Available credentials: ${all.map((m) => m.name).join(", ")}` : "No credentials stored yet.";
|
|
4950
|
+
return {
|
|
4951
|
+
content: [
|
|
4952
|
+
{
|
|
4953
|
+
type: "text",
|
|
4954
|
+
text: `Credential "${args.name}" not found. ${available}
|
|
4955
|
+
Use ask_user to request it from the user, or create it yourself (e.g. register an account), then store with credential_set.`
|
|
4956
|
+
}
|
|
4957
|
+
]
|
|
4958
|
+
};
|
|
4959
|
+
}
|
|
4960
|
+
log.info(`Credential accessed: "${args.name}" (${credential.meta.type})`);
|
|
4961
|
+
return {
|
|
4962
|
+
content: [
|
|
4963
|
+
{
|
|
4964
|
+
type: "text",
|
|
4965
|
+
text: JSON.stringify({
|
|
4966
|
+
name: credential.meta.name,
|
|
4967
|
+
type: credential.meta.type,
|
|
4968
|
+
data: credential.data,
|
|
4969
|
+
skill: credential.meta.skillName || null
|
|
4970
|
+
})
|
|
4971
|
+
}
|
|
4972
|
+
]
|
|
4973
|
+
};
|
|
4974
|
+
}
|
|
4975
|
+
),
|
|
4976
|
+
tool2(
|
|
4977
|
+
"credential_set",
|
|
4978
|
+
"Store a credential locally on the user's machine. The credential is encrypted at rest and never sent to any remote server. IMPORTANT: Always use this to persist credentials \u2014 both when receiving them from the user via ask_user AND when you generate new credentials yourself (e.g. registering an account, creating an API key, generating a token). This ensures credentials survive across sessions and don't need to be recreated.",
|
|
4979
|
+
{
|
|
4980
|
+
name: z2.string().describe("Credential name (lowercase kebab-case, e.g. 'amazon-login')"),
|
|
4981
|
+
type: z2.enum(["api_key", "oauth_token", "login", "secret", "custom"]).describe("Credential type"),
|
|
4982
|
+
data: z2.record(z2.string(), z2.string()).describe(
|
|
4983
|
+
'Key-value pairs (e.g. { "username": "...", "password": "..." } or { "api_key": "..." })'
|
|
4984
|
+
),
|
|
4985
|
+
skill_name: z2.string().optional().describe("Associate with a specific skill"),
|
|
4986
|
+
tags: z2.array(z2.string()).optional().describe("Tags for searchability")
|
|
4987
|
+
},
|
|
4988
|
+
async (args) => {
|
|
4989
|
+
const store = getCredentialStore();
|
|
4990
|
+
const meta = store.save(args.name, args.type, args.data, {
|
|
4991
|
+
skillName: args.skill_name,
|
|
4992
|
+
tags: args.tags
|
|
4993
|
+
});
|
|
4994
|
+
log.info(`Credential stored: "${args.name}" (${args.type})`);
|
|
4995
|
+
return {
|
|
4996
|
+
content: [
|
|
4997
|
+
{
|
|
4998
|
+
type: "text",
|
|
4999
|
+
text: `Credential "${meta.name}" stored locally (type: ${meta.type}, id: ${meta.id}). It is encrypted and will persist across app restarts.`
|
|
5000
|
+
}
|
|
5001
|
+
]
|
|
5002
|
+
};
|
|
5003
|
+
}
|
|
5004
|
+
),
|
|
5005
|
+
tool2(
|
|
5006
|
+
"credential_list",
|
|
5007
|
+
"List all locally stored credentials (metadata only, no secrets). Use this to check what credentials are available before executing a skill.",
|
|
5008
|
+
{
|
|
5009
|
+
skill_name: z2.string().optional().describe("Filter by skill name"),
|
|
5010
|
+
type: z2.string().optional().describe("Filter by credential type")
|
|
5011
|
+
},
|
|
5012
|
+
async (args) => {
|
|
5013
|
+
const store = getCredentialStore();
|
|
5014
|
+
let results = store.list();
|
|
5015
|
+
if (args.skill_name) {
|
|
5016
|
+
results = results.filter((m) => m.skillName === args.skill_name);
|
|
5017
|
+
}
|
|
5018
|
+
if (args.type) {
|
|
5019
|
+
results = results.filter((m) => m.type === args.type);
|
|
5020
|
+
}
|
|
5021
|
+
if (results.length === 0) {
|
|
5022
|
+
const filter = args.skill_name ? ` for skill "${args.skill_name}"` : "";
|
|
5023
|
+
return {
|
|
5024
|
+
content: [{ type: "text", text: `No credentials found${filter}.` }]
|
|
5025
|
+
};
|
|
5026
|
+
}
|
|
5027
|
+
let response = "## Stored Credentials\n\n";
|
|
5028
|
+
for (const m of results) {
|
|
5029
|
+
const skill = m.skillName ? ` [${m.skillName}]` : "";
|
|
5030
|
+
const tags = m.tags.length > 0 ? ` (${m.tags.join(", ")})` : "";
|
|
5031
|
+
response += `- **${m.name}** (${m.type})${skill}${tags}
|
|
5032
|
+
`;
|
|
5033
|
+
}
|
|
5034
|
+
return { content: [{ type: "text", text: response }] };
|
|
5035
|
+
}
|
|
5036
|
+
),
|
|
5037
|
+
tool2(
|
|
5038
|
+
"credential_remove",
|
|
5039
|
+
"Remove a locally stored credential by name.",
|
|
5040
|
+
{
|
|
5041
|
+
name: z2.string().describe("Credential name to remove")
|
|
5042
|
+
},
|
|
5043
|
+
async (args) => {
|
|
5044
|
+
const store = getCredentialStore();
|
|
5045
|
+
const removed = store.removeByName(args.name);
|
|
5046
|
+
if (!removed) {
|
|
5047
|
+
return {
|
|
5048
|
+
content: [{ type: "text", text: `Credential "${args.name}" not found.` }]
|
|
5049
|
+
};
|
|
5050
|
+
}
|
|
5051
|
+
log.info(`Credential removed: "${args.name}"`);
|
|
5052
|
+
return {
|
|
5053
|
+
content: [
|
|
5054
|
+
{ type: "text", text: `Credential "${args.name}" removed from local storage.` }
|
|
5055
|
+
]
|
|
5056
|
+
};
|
|
5057
|
+
}
|
|
3850
5058
|
)
|
|
3851
5059
|
]
|
|
3852
5060
|
});
|
|
3853
5061
|
}
|
|
3854
|
-
async function saveJobToDb(
|
|
5062
|
+
async function saveJobToDb(jobName, jobDescription, createdSkillNames) {
|
|
3855
5063
|
try {
|
|
3856
5064
|
const data = await callMcpHandler("job.save_with_skills", {
|
|
3857
5065
|
job_name: jobName,
|
|
3858
5066
|
job_description: jobDescription,
|
|
3859
5067
|
skill_names: createdSkillNames
|
|
3860
5068
|
});
|
|
3861
|
-
log.debug(
|
|
5069
|
+
log.debug(
|
|
5070
|
+
`Job "${jobName}" saved via edge function (id: ${data}), ${createdSkillNames.length} skill(s) linked`
|
|
5071
|
+
);
|
|
3862
5072
|
} catch (err) {
|
|
3863
5073
|
log.debug(`saveJobToDb error: ${err}`);
|
|
3864
5074
|
}
|
|
@@ -3913,7 +5123,7 @@ function createEventHooks(taskId, toolCallRecords) {
|
|
|
3913
5123
|
};
|
|
3914
5124
|
}
|
|
3915
5125
|
|
|
3916
|
-
// src/agent/
|
|
5126
|
+
// src/agent/system-prompt.ts
|
|
3917
5127
|
var BASE_SYSTEM_PROMPT = `You are AssistMe, an AI assistant that operates like a real human on the user's computer. You control the user's actual Chrome browser and work with their real files.
|
|
3918
5128
|
|
|
3919
5129
|
KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This means:
|
|
@@ -3928,7 +5138,28 @@ KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This
|
|
|
3928
5138
|
|
|
3929
5139
|
Available capabilities:
|
|
3930
5140
|
1. BROWSER CONTROL (user's real Chrome via CDP):
|
|
3931
|
-
|
|
5141
|
+
**PREFERRED workflow \u2014 Snapshot + Act (ref-based):**
|
|
5142
|
+
- browser_snapshot \u2192 takes a screenshot and discovers all interactive elements with numbered refs
|
|
5143
|
+
Returns a ref table (text) + screenshot (image). The ref table is your PRIMARY context for element identification.
|
|
5144
|
+
Use annotate=true only on simple pages (few elements) where visual badge overlay helps.
|
|
5145
|
+
- browser_act \u2192 execute actions using ref numbers: click, type, select, press, scroll, wait
|
|
5146
|
+
- This is MORE RELIABLE than CSS selectors because:
|
|
5147
|
+
(a) The ref table gives you role, name, and type for every interactive element \u2014 no guessing
|
|
5148
|
+
(b) Refs use stable semantic resolution (role + accessible name) that survives DOM changes
|
|
5149
|
+
(c) Actions use CDP Input events (real mouse/keyboard) instead of JavaScript \u2014 works with all frameworks
|
|
5150
|
+
(d) You can batch multiple actions in one call \u2014 fewer round-trips
|
|
5151
|
+
- Example workflow:
|
|
5152
|
+
1. browser_snapshot \u2192 ref table shows [1] button "Next", [2] textbox "Email", [3] combobox "Month"
|
|
5153
|
+
2. browser_act actions=[{action:"type", ref:2, text:"user@example.com"}, {action:"select", ref:3, option:"March"}, {action:"click", ref:1}] screenshot=true
|
|
5154
|
+
- Refs persist across actions unless the page navigates. Re-snapshot after navigation or major DOM changes.
|
|
5155
|
+
|
|
5156
|
+
**Legacy tools (still available, use when refs don't work):**
|
|
5157
|
+
- browser_click, browser_type, browser_select, browser_screenshot, browser_evaluate
|
|
5158
|
+
- browser_click supports :contains('text') pseudo-selectors
|
|
5159
|
+
- browser_select handles native and custom dropdowns
|
|
5160
|
+
|
|
5161
|
+
**Other browser tools:**
|
|
5162
|
+
- browser_connect, browser_navigate, browser_read_page, browser_list_tabs, browser_switch_tab, browser_new_tab
|
|
3932
5163
|
- If auth is needed: use browser_request_user_action to ask the user to log in
|
|
3933
5164
|
|
|
3934
5165
|
2. FILE OPERATIONS & SHELL:
|
|
@@ -3982,18 +5213,29 @@ Available capabilities:
|
|
|
3982
5213
|
Workflow for web tasks (e.g. "\u67E5\u4E00\u4E0B kindle \u6700\u65B0\u6B3E\u4EF7\u683C"):
|
|
3983
5214
|
1. browser_connect \u2192 connect to user's Chrome
|
|
3984
5215
|
2. browser_new_tab \u2192 open a new tab
|
|
3985
|
-
3. browser_navigate \u2192 go to the website (login pages are auto-detected
|
|
3986
|
-
4.
|
|
3987
|
-
5.
|
|
3988
|
-
6. Repeat
|
|
5216
|
+
3. browser_navigate \u2192 go to the website (login pages are auto-detected)
|
|
5217
|
+
4. browser_snapshot \u2192 get ref table + screenshot (use annotate=true for simple pages)
|
|
5218
|
+
5. browser_act \u2192 interact using refs (type, click, select, etc.), set screenshot=true to see result
|
|
5219
|
+
6. Repeat 4-5 as needed (re-snapshot after navigation or major page changes)
|
|
3989
5220
|
7. Summarize findings
|
|
3990
5221
|
|
|
5222
|
+
Workflow for form filling (e.g. "\u6CE8\u518C\u4E00\u4E2A Gmail \u8D26\u53F7"):
|
|
5223
|
+
1. browser_connect + browser_navigate \u2192 go to the form page
|
|
5224
|
+
2. browser_snapshot \u2192 see all form fields with ref numbers
|
|
5225
|
+
3. browser_act \u2192 batch fill multiple fields + click submit in ONE call:
|
|
5226
|
+
actions=[{action:"type", ref:1, text:"John"}, {action:"type", ref:2, text:"Doe"}, {action:"select", ref:3, option:"March"}, {action:"click", ref:7}] screenshot=true
|
|
5227
|
+
4. Check the screenshot \u2014 if validation errors appear, re-snapshot and fix
|
|
5228
|
+
5. When a username/email is taken, append a random 4-digit suffix and retry
|
|
5229
|
+
|
|
3991
5230
|
Guidelines:
|
|
3992
5231
|
- Always use the real browser for web tasks, never try to fetch URLs programmatically
|
|
3993
|
-
-
|
|
3994
|
-
- Use
|
|
5232
|
+
- ALWAYS use browser_snapshot as your primary way to understand a page \u2014 the ref table gives actionable refs, the screenshot gives visual context
|
|
5233
|
+
- Use browser_act to batch multiple actions \u2014 fill an entire form in one call instead of individual clicks/types
|
|
5234
|
+
- Only re-snapshot when: (a) the page navigated, (b) significant DOM changes occurred, (c) an action failed with "ref not found"
|
|
5235
|
+
- Refs are semantically stable (resolved by role + name), so they often survive minor DOM updates
|
|
3995
5236
|
- Login pages are auto-detected after navigation \u2014 the user is prompted and sessions are saved automatically
|
|
3996
5237
|
- If auto-detection misses a login page, use browser_request_user_action manually
|
|
5238
|
+
- Fall back to legacy tools (browser_click, browser_type, browser_evaluate) only when refs don't work
|
|
3997
5239
|
- Be thorough: check multiple sources when comparing prices/products
|
|
3998
5240
|
- Summarize results clearly at the end
|
|
3999
5241
|
- When you learn something about the user (preferences, habits), use memory_store to remember it
|
|
@@ -4008,21 +5250,21 @@ CRITICAL \u2014 Ask before you guess:
|
|
|
4008
5250
|
- After receiving the answer, store it with memory_store if it is likely to be useful in future conversations.
|
|
4009
5251
|
|
|
4010
5252
|
Workspace path: {workspace_path}`;
|
|
5253
|
+
|
|
5254
|
+
// src/agent/processor.ts
|
|
4011
5255
|
var MAX_HISTORY_ENTRIES = 10;
|
|
4012
5256
|
var MAX_RESPONSE_LENGTH = 1500;
|
|
4013
5257
|
var TaskProcessor = class {
|
|
4014
5258
|
memoryManager = null;
|
|
4015
5259
|
skillManager;
|
|
4016
|
-
userId = null;
|
|
4017
5260
|
sessionId = null;
|
|
4018
5261
|
/** In-memory conversation history, keyed by conversation_id */
|
|
4019
5262
|
historyCache = /* @__PURE__ */ new Map();
|
|
4020
5263
|
constructor() {
|
|
4021
5264
|
this.skillManager = new SkillManager();
|
|
4022
5265
|
}
|
|
4023
|
-
|
|
4024
|
-
this.
|
|
4025
|
-
this.memoryManager = new MemoryManager(userId);
|
|
5266
|
+
init(userId) {
|
|
5267
|
+
this.memoryManager = new MemoryManager();
|
|
4026
5268
|
this.skillManager.setUserId(userId);
|
|
4027
5269
|
this.skillManager.loadFromDb().catch((err) => {
|
|
4028
5270
|
log.debug(`DB skill load deferred: ${err}`);
|
|
@@ -4097,8 +5339,7 @@ var TaskProcessor = class {
|
|
|
4097
5339
|
memoryManager: this.memoryManager,
|
|
4098
5340
|
skillManager: this.skillManager,
|
|
4099
5341
|
taskId: task.id,
|
|
4100
|
-
sessionId: this.sessionId || void 0
|
|
4101
|
-
userId: this.userId || void 0
|
|
5342
|
+
sessionId: this.sessionId || void 0
|
|
4102
5343
|
});
|
|
4103
5344
|
const eventHooks = createEventHooks(task.id, toolCallRecords);
|
|
4104
5345
|
const allowedTools = [
|
|
@@ -4127,7 +5368,12 @@ var TaskProcessor = class {
|
|
|
4127
5368
|
// Job automation tools
|
|
4128
5369
|
"mcp__assistme-agent__job_run",
|
|
4129
5370
|
"mcp__assistme-agent__job_schedule",
|
|
4130
|
-
"mcp__assistme-agent__job_status"
|
|
5371
|
+
"mcp__assistme-agent__job_status",
|
|
5372
|
+
// Credential tools (local storage)
|
|
5373
|
+
"mcp__assistme-agent__credential_get",
|
|
5374
|
+
"mcp__assistme-agent__credential_set",
|
|
5375
|
+
"mcp__assistme-agent__credential_list",
|
|
5376
|
+
"mcp__assistme-agent__credential_remove"
|
|
4131
5377
|
];
|
|
4132
5378
|
async function* promptMessages() {
|
|
4133
5379
|
yield {
|
|
@@ -4224,7 +5470,9 @@ var TaskProcessor = class {
|
|
|
4224
5470
|
} finally {
|
|
4225
5471
|
clearTimeout(timeoutId);
|
|
4226
5472
|
}
|
|
4227
|
-
|
|
5473
|
+
const MAX_CONTENT_LENGTH = 5e4;
|
|
5474
|
+
const truncatedResponse = finalResponse.length > MAX_CONTENT_LENGTH ? finalResponse.slice(0, MAX_CONTENT_LENGTH) + "\n\n[Response truncated]" : finalResponse;
|
|
5475
|
+
await withRetry(() => completeTask(task.id, truncatedResponse, tokenUsage), {
|
|
4228
5476
|
maxRetries: 2,
|
|
4229
5477
|
baseDelayMs: 300,
|
|
4230
5478
|
label: "completeTask"
|
|
@@ -4238,7 +5486,9 @@ var TaskProcessor = class {
|
|
|
4238
5486
|
}
|
|
4239
5487
|
this.historyCache.set(task.conversation_id, convHistory);
|
|
4240
5488
|
if (agentSessionId) {
|
|
4241
|
-
this.evaluateSkillPostTask(agentSessionId, config.model).catch(
|
|
5489
|
+
this.evaluateSkillPostTask(agentSessionId, config.model).catch(
|
|
5490
|
+
(err) => log.debug(`Post-task skill evaluation skipped: ${err}`)
|
|
5491
|
+
);
|
|
4242
5492
|
}
|
|
4243
5493
|
} catch (err) {
|
|
4244
5494
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -4261,10 +5511,7 @@ var TaskProcessor = class {
|
|
|
4261
5511
|
|
|
4262
5512
|
// src/commands/start.ts
|
|
4263
5513
|
function registerStartCommand(program2) {
|
|
4264
|
-
program2.command("start", { isDefault: true, hidden: true }).description("Start the agent (default command)").option(
|
|
4265
|
-
"-w, --workspace <path>",
|
|
4266
|
-
"Workspace path (default: current directory)"
|
|
4267
|
-
).option("-n, --name <name>", "Session name").option("-v, --verbose", "Enable verbose/debug logging").action(runAgent);
|
|
5514
|
+
program2.command("start", { isDefault: true, hidden: true }).description("Start the agent (default command)").option("-w, --workspace <path>", "Workspace path (default: current directory)").option("-n, --name <name>", "Session name").option("-v, --verbose", "Enable verbose/debug logging").action(runAgent);
|
|
4268
5515
|
}
|
|
4269
5516
|
async function runAgent(opts) {
|
|
4270
5517
|
if (opts.verbose) {
|
|
@@ -4277,26 +5524,10 @@ async function runAgent(opts) {
|
|
|
4277
5524
|
setConfig("sessionName", opts.name);
|
|
4278
5525
|
}
|
|
4279
5526
|
console.log();
|
|
4280
|
-
console.log(
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
);
|
|
4285
|
-
console.log(
|
|
4286
|
-
chalk4.bold.cyan(
|
|
4287
|
-
" \u2551 AssistMe CLI Agent \u2551"
|
|
4288
|
-
)
|
|
4289
|
-
);
|
|
4290
|
-
console.log(
|
|
4291
|
-
chalk4.bold.cyan(
|
|
4292
|
-
" \u2551 AI that controls your real browser \u2551"
|
|
4293
|
-
)
|
|
4294
|
-
);
|
|
4295
|
-
console.log(
|
|
4296
|
-
chalk4.bold.cyan(
|
|
4297
|
-
" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
|
|
4298
|
-
)
|
|
4299
|
-
);
|
|
5527
|
+
console.log(chalk4.bold.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
5528
|
+
console.log(chalk4.bold.cyan(" \u2551 AssistMe CLI Agent \u2551"));
|
|
5529
|
+
console.log(chalk4.bold.cyan(" \u2551 AI that controls your real browser \u2551"));
|
|
5530
|
+
console.log(chalk4.bold.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
4300
5531
|
console.log();
|
|
4301
5532
|
let userId;
|
|
4302
5533
|
try {
|
|
@@ -4313,9 +5544,7 @@ async function runAgent(opts) {
|
|
|
4313
5544
|
launchSpinner.succeed("Browser detected (CDP port 9222)");
|
|
4314
5545
|
break;
|
|
4315
5546
|
case "launched":
|
|
4316
|
-
launchSpinner.succeed(
|
|
4317
|
-
"Browser launched with remote debugging (debug profile)"
|
|
4318
|
-
);
|
|
5547
|
+
launchSpinner.succeed("Browser launched with remote debugging (debug profile)");
|
|
4319
5548
|
break;
|
|
4320
5549
|
}
|
|
4321
5550
|
} else {
|
|
@@ -4326,9 +5555,7 @@ async function runAgent(opts) {
|
|
|
4326
5555
|
break;
|
|
4327
5556
|
case "port_conflict":
|
|
4328
5557
|
launchSpinner.fail("Port 9222 is in use by another process");
|
|
4329
|
-
log.info(
|
|
4330
|
-
launchResult.detail ?? "Stop the conflicting process or use a different port."
|
|
4331
|
-
);
|
|
5558
|
+
log.info(launchResult.detail ?? "Stop the conflicting process or use a different port.");
|
|
4332
5559
|
break;
|
|
4333
5560
|
default:
|
|
4334
5561
|
launchSpinner.fail("Failed to start Chrome with remote debugging");
|
|
@@ -4338,14 +5565,12 @@ async function runAgent(opts) {
|
|
|
4338
5565
|
if (launchResult.chromePath) {
|
|
4339
5566
|
log.info(`Chrome binary: ${launchResult.chromePath}`);
|
|
4340
5567
|
}
|
|
4341
|
-
log.info(
|
|
4342
|
-
"Browser will be auto-launched when the first task needs it."
|
|
4343
|
-
);
|
|
5568
|
+
log.info("Browser will be auto-launched when the first task needs it.");
|
|
4344
5569
|
break;
|
|
4345
5570
|
}
|
|
4346
5571
|
}
|
|
4347
5572
|
const processor = new TaskProcessor();
|
|
4348
|
-
processor.
|
|
5573
|
+
processor.init(userId);
|
|
4349
5574
|
const sessionManager = new SessionManager();
|
|
4350
5575
|
const browserRef = getBrowser();
|
|
4351
5576
|
const shutdown = async () => {
|
|
@@ -4407,9 +5632,7 @@ async function runAgent(opts) {
|
|
|
4407
5632
|
});
|
|
4408
5633
|
rl.on("close", shutdown);
|
|
4409
5634
|
} catch (err) {
|
|
4410
|
-
log.error(
|
|
4411
|
-
`Failed to start: ${err instanceof Error ? err.message : err}`
|
|
4412
|
-
);
|
|
5635
|
+
log.error(`Failed to start: ${err instanceof Error ? err.message : err}`);
|
|
4413
5636
|
process.exit(1);
|
|
4414
5637
|
}
|
|
4415
5638
|
}
|
|
@@ -4453,13 +5676,7 @@ function registerStatusCommand(program2) {
|
|
|
4453
5676
|
import chalk6 from "chalk";
|
|
4454
5677
|
function registerScheduleCommands(program2) {
|
|
4455
5678
|
const scheduleCmd = program2.command("schedule").description("Manage scheduled (cron) tasks");
|
|
4456
|
-
scheduleCmd.command("add").description("Add a scheduled task").requiredOption("-n, --name <name>", "Task name").requiredOption(
|
|
4457
|
-
"-p, --prompt <prompt>",
|
|
4458
|
-
"Task prompt (what the AI should do)"
|
|
4459
|
-
).requiredOption(
|
|
4460
|
-
"-c, --cron <expression>",
|
|
4461
|
-
"Cron expression (e.g. '0 8 * * *' for daily 8am)"
|
|
4462
|
-
).option("-t, --timezone <tz>", "Timezone (default: UTC)").action(async (opts) => {
|
|
5679
|
+
scheduleCmd.command("add").description("Add a scheduled task").requiredOption("-n, --name <name>", "Task name").requiredOption("-p, --prompt <prompt>", "Task prompt (what the AI should do)").requiredOption("-c, --cron <expression>", "Cron expression (e.g. '0 8 * * *' for daily 8am)").option("-t, --timezone <tz>", "Timezone (default: UTC)").action(async (opts) => {
|
|
4463
5680
|
try {
|
|
4464
5681
|
const cronParts = opts.cron.trim().split(/\s+/);
|
|
4465
5682
|
if (cronParts.length !== 5) {
|
|
@@ -4469,14 +5686,8 @@ function registerScheduleCommands(program2) {
|
|
|
4469
5686
|
console.log(' Examples: "0 9 * * *" (daily 9am), "*/15 * * * *" (every 15 min)');
|
|
4470
5687
|
process.exit(1);
|
|
4471
5688
|
}
|
|
4472
|
-
|
|
4473
|
-
const task = await createScheduledTask(
|
|
4474
|
-
userId,
|
|
4475
|
-
opts.name,
|
|
4476
|
-
opts.prompt,
|
|
4477
|
-
opts.cron,
|
|
4478
|
-
opts.timezone
|
|
4479
|
-
);
|
|
5689
|
+
await getCurrentUserId();
|
|
5690
|
+
const task = await createScheduledTask(opts.name, opts.prompt, opts.cron, opts.timezone);
|
|
4480
5691
|
log.success(`Scheduled task created: ${task.name}`);
|
|
4481
5692
|
console.log(` ID: ${task.id.slice(0, 8)}...`);
|
|
4482
5693
|
console.log(` Cron: ${task.cron_expression}`);
|
|
@@ -4490,8 +5701,8 @@ function registerScheduleCommands(program2) {
|
|
|
4490
5701
|
});
|
|
4491
5702
|
scheduleCmd.command("list").description("List all scheduled tasks").action(async () => {
|
|
4492
5703
|
try {
|
|
4493
|
-
|
|
4494
|
-
const tasks = await listScheduledTasks(
|
|
5704
|
+
await getCurrentUserId();
|
|
5705
|
+
const tasks = await listScheduledTasks();
|
|
4495
5706
|
if (tasks.length === 0) {
|
|
4496
5707
|
console.log(chalk6.yellow("No scheduled tasks."));
|
|
4497
5708
|
console.log('Run "assistme schedule add" to create one.');
|
|
@@ -4501,22 +5712,14 @@ function registerScheduleCommands(program2) {
|
|
|
4501
5712
|
for (const t of tasks) {
|
|
4502
5713
|
const icon = t.enabled ? chalk6.green("\u25CF") : chalk6.dim("\u25CB");
|
|
4503
5714
|
console.log(` ${icon} ${t.name} (${t.id.slice(0, 8)}...)`);
|
|
4504
|
-
console.log(
|
|
4505
|
-
|
|
4506
|
-
);
|
|
4507
|
-
console.log(
|
|
4508
|
-
` Prompt: ${t.prompt.slice(0, 60)}${t.prompt.length > 60 ? "..." : ""}`
|
|
4509
|
-
);
|
|
5715
|
+
console.log(` Cron: ${t.cron_expression} (${t.timezone})`);
|
|
5716
|
+
console.log(` Prompt: ${t.prompt.slice(0, 60)}${t.prompt.length > 60 ? "..." : ""}`);
|
|
4510
5717
|
console.log(` Runs: ${t.run_count}`);
|
|
4511
5718
|
if (t.next_run_at) {
|
|
4512
|
-
console.log(
|
|
4513
|
-
` Next run: ${new Date(t.next_run_at).toLocaleString()}`
|
|
4514
|
-
);
|
|
5719
|
+
console.log(` Next run: ${new Date(t.next_run_at).toLocaleString()}`);
|
|
4515
5720
|
}
|
|
4516
5721
|
if (t.last_error) {
|
|
4517
|
-
console.log(
|
|
4518
|
-
chalk6.red(` Error: ${t.last_error.slice(0, 80)}`)
|
|
4519
|
-
);
|
|
5722
|
+
console.log(chalk6.red(` Error: ${t.last_error.slice(0, 80)}`));
|
|
4520
5723
|
}
|
|
4521
5724
|
console.log();
|
|
4522
5725
|
}
|
|
@@ -4527,8 +5730,8 @@ function registerScheduleCommands(program2) {
|
|
|
4527
5730
|
});
|
|
4528
5731
|
scheduleCmd.command("toggle <id>").description("Enable/disable a scheduled task").action(async (id) => {
|
|
4529
5732
|
try {
|
|
4530
|
-
|
|
4531
|
-
const tasks = await listScheduledTasks(
|
|
5733
|
+
await getCurrentUserId();
|
|
5734
|
+
const tasks = await listScheduledTasks();
|
|
4532
5735
|
const task = tasks.find((t) => t.id.startsWith(id));
|
|
4533
5736
|
if (!task) {
|
|
4534
5737
|
log.error(`Task not found: ${id}`);
|
|
@@ -4543,8 +5746,8 @@ function registerScheduleCommands(program2) {
|
|
|
4543
5746
|
});
|
|
4544
5747
|
scheduleCmd.command("remove <id>").description("Delete a scheduled task").action(async (id) => {
|
|
4545
5748
|
try {
|
|
4546
|
-
|
|
4547
|
-
const tasks = await listScheduledTasks(
|
|
5749
|
+
await getCurrentUserId();
|
|
5750
|
+
const tasks = await listScheduledTasks();
|
|
4548
5751
|
const task = tasks.find((t) => t.id.startsWith(id));
|
|
4549
5752
|
if (!task) {
|
|
4550
5753
|
log.error(`Task not found: ${id}`);
|
|
@@ -4565,17 +5768,12 @@ function registerMemoryCommands(program2) {
|
|
|
4565
5768
|
const memoryCmd = program2.command("memory").description("Manage the agent's memory about you");
|
|
4566
5769
|
memoryCmd.command("list").description("List stored memories").option("-c, --category <category>", "Filter by category").option("-l, --limit <number>", "Max items (default: 20)").action(async (opts) => {
|
|
4567
5770
|
try {
|
|
4568
|
-
|
|
4569
|
-
const mm = new MemoryManager(
|
|
4570
|
-
const memories = await mm.list(
|
|
4571
|
-
opts.category,
|
|
4572
|
-
parseInt(opts.limit || "20")
|
|
4573
|
-
);
|
|
5771
|
+
await getCurrentUserId();
|
|
5772
|
+
const mm = new MemoryManager();
|
|
5773
|
+
const memories = await mm.list(opts.category, parseInt(opts.limit || "20"));
|
|
4574
5774
|
if (memories.length === 0) {
|
|
4575
5775
|
console.log(chalk7.yellow("No memories stored yet."));
|
|
4576
|
-
console.log(
|
|
4577
|
-
"The agent will automatically remember things as you interact with it."
|
|
4578
|
-
);
|
|
5776
|
+
console.log("The agent will automatically remember things as you interact with it.");
|
|
4579
5777
|
return;
|
|
4580
5778
|
}
|
|
4581
5779
|
console.log(chalk7.bold(`
|
|
@@ -4602,8 +5800,8 @@ Memories (${memories.length}):`));
|
|
|
4602
5800
|
});
|
|
4603
5801
|
memoryCmd.command("add <content>").description("Manually add a memory").option("-c, --category <category>", "Category (default: general)").option("-i, --importance <number>", "Importance 1-10 (default: 5)").option("-t, --tags <tags>", "Comma-separated tags").action(async (content, opts) => {
|
|
4604
5802
|
try {
|
|
4605
|
-
|
|
4606
|
-
const mm = new MemoryManager(
|
|
5803
|
+
await getCurrentUserId();
|
|
5804
|
+
const mm = new MemoryManager();
|
|
4607
5805
|
const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [];
|
|
4608
5806
|
const mem = await mm.add(
|
|
4609
5807
|
content,
|
|
@@ -4611,9 +5809,7 @@ Memories (${memories.length}):`));
|
|
|
4611
5809
|
parseInt(opts.importance || "5"),
|
|
4612
5810
|
tags
|
|
4613
5811
|
);
|
|
4614
|
-
log.success(
|
|
4615
|
-
`Memory stored: "${mem.content.slice(0, 60)}..." [${mem.category}]`
|
|
4616
|
-
);
|
|
5812
|
+
log.success(`Memory stored: "${mem.content.slice(0, 60)}..." [${mem.category}]`);
|
|
4617
5813
|
} catch (err) {
|
|
4618
5814
|
log.error(`${err instanceof Error ? err.message : err}`);
|
|
4619
5815
|
process.exit(1);
|
|
@@ -4621,8 +5817,8 @@ Memories (${memories.length}):`));
|
|
|
4621
5817
|
});
|
|
4622
5818
|
memoryCmd.command("search <query>").description("Search memories").action(async (query3) => {
|
|
4623
5819
|
try {
|
|
4624
|
-
|
|
4625
|
-
const mm = new MemoryManager(
|
|
5820
|
+
await getCurrentUserId();
|
|
5821
|
+
const mm = new MemoryManager();
|
|
4626
5822
|
const results = await mm.search(query3);
|
|
4627
5823
|
if (results.length === 0) {
|
|
4628
5824
|
console.log(chalk7.yellow(`No memories matching "${query3}"`));
|
|
@@ -4641,8 +5837,8 @@ Search results for "${query3}":`));
|
|
|
4641
5837
|
});
|
|
4642
5838
|
memoryCmd.command("remove <id>").description("Delete a specific memory").action(async (id) => {
|
|
4643
5839
|
try {
|
|
4644
|
-
|
|
4645
|
-
const mm = new MemoryManager(
|
|
5840
|
+
await getCurrentUserId();
|
|
5841
|
+
const mm = new MemoryManager();
|
|
4646
5842
|
const memories = await mm.list();
|
|
4647
5843
|
const mem = memories.find((m) => m.id.startsWith(id));
|
|
4648
5844
|
if (!mem) {
|
|
@@ -4658,12 +5854,10 @@ Search results for "${query3}":`));
|
|
|
4658
5854
|
});
|
|
4659
5855
|
memoryCmd.command("clear").description("Clear all memories").option("-c, --category <category>", "Only clear specific category").action(async (opts) => {
|
|
4660
5856
|
try {
|
|
4661
|
-
|
|
4662
|
-
const mm = new MemoryManager(
|
|
5857
|
+
await getCurrentUserId();
|
|
5858
|
+
const mm = new MemoryManager();
|
|
4663
5859
|
await mm.clear(opts.category);
|
|
4664
|
-
log.success(
|
|
4665
|
-
`Memories cleared${opts.category ? ` (${opts.category})` : ""}`
|
|
4666
|
-
);
|
|
5860
|
+
log.success(`Memories cleared${opts.category ? ` (${opts.category})` : ""}`);
|
|
4667
5861
|
} catch (err) {
|
|
4668
5862
|
log.error(`${err instanceof Error ? err.message : err}`);
|
|
4669
5863
|
process.exit(1);
|
|
@@ -4801,21 +5995,17 @@ function registerJobCommands(program2) {
|
|
|
4801
5995
|
jobCmd.command("list").description("List your defined jobs").action(async () => {
|
|
4802
5996
|
try {
|
|
4803
5997
|
const userId = await getCurrentUserId();
|
|
4804
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
4805
|
-
const runner = new JobRunner2(
|
|
5998
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-P2L6MOOX.js");
|
|
5999
|
+
const runner = new JobRunner2();
|
|
4806
6000
|
const jobs = await runner.listJobs();
|
|
4807
6001
|
if (jobs.length === 0) {
|
|
4808
6002
|
console.log(chalk9.yellow("No jobs defined."));
|
|
4809
|
-
console.log(
|
|
4810
|
-
'Use "assistme" and tell the agent about your job to generate skills.'
|
|
4811
|
-
);
|
|
6003
|
+
console.log('Use "assistme" and tell the agent about your job to generate skills.');
|
|
4812
6004
|
return;
|
|
4813
6005
|
}
|
|
4814
6006
|
console.log(chalk9.bold("\nYour Jobs:"));
|
|
4815
6007
|
for (const job of jobs) {
|
|
4816
|
-
console.log(
|
|
4817
|
-
` ${chalk9.cyan(job.name)} (${job.skillCount} skills)`
|
|
4818
|
-
);
|
|
6008
|
+
console.log(` ${chalk9.cyan(job.name)} (${job.skillCount} skills)`);
|
|
4819
6009
|
console.log(
|
|
4820
6010
|
` ${job.description.slice(0, 80)}${job.description.length > 80 ? "..." : ""}`
|
|
4821
6011
|
);
|
|
@@ -4829,38 +6019,23 @@ function registerJobCommands(program2) {
|
|
|
4829
6019
|
jobCmd.command("status [name]").description("Show run history for a job (or all jobs)").option("-l, --limit <number>", "Max runs to show (default: 5)").action(async (name, opts) => {
|
|
4830
6020
|
try {
|
|
4831
6021
|
const userId = await getCurrentUserId();
|
|
4832
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
4833
|
-
const runner = new JobRunner2(
|
|
4834
|
-
const runs = await runner.getRunHistory(
|
|
4835
|
-
name,
|
|
4836
|
-
parseInt(opts.limit || "5")
|
|
4837
|
-
);
|
|
6022
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-P2L6MOOX.js");
|
|
6023
|
+
const runner = new JobRunner2();
|
|
6024
|
+
const runs = await runner.getRunHistory(name, parseInt(opts.limit || "5"));
|
|
4838
6025
|
if (runs.length === 0) {
|
|
4839
|
-
console.log(
|
|
4840
|
-
chalk9.yellow(
|
|
4841
|
-
name ? `No runs found for "${name}".` : "No job runs yet."
|
|
4842
|
-
)
|
|
4843
|
-
);
|
|
6026
|
+
console.log(chalk9.yellow(name ? `No runs found for "${name}".` : "No job runs yet."));
|
|
4844
6027
|
return;
|
|
4845
6028
|
}
|
|
4846
|
-
console.log(
|
|
4847
|
-
|
|
4848
|
-
`
|
|
4849
|
-
Job Run History${name ? ` \u2014 ${name}` : ""}:`
|
|
4850
|
-
)
|
|
4851
|
-
);
|
|
6029
|
+
console.log(chalk9.bold(`
|
|
6030
|
+
Job Run History${name ? ` \u2014 ${name}` : ""}:`));
|
|
4852
6031
|
for (const run of runs) {
|
|
4853
6032
|
const icon = run.status === "completed" ? chalk9.green("\u25CF") : run.status === "failed" ? chalk9.red("\u25CF") : run.status === "running" ? chalk9.yellow("\u25CF") : chalk9.dim("\u25CB");
|
|
4854
6033
|
const date = new Date(run.startedAt).toLocaleString();
|
|
4855
6034
|
const duration = run.completedAt ? `${Math.round((new Date(run.completedAt).getTime() - new Date(run.startedAt).getTime()) / 1e3)}s` : "in progress";
|
|
4856
|
-
console.log(
|
|
4857
|
-
` ${icon} ${chalk9.bold(run.jobName)} | ${run.triggerType} | ${date}`
|
|
4858
|
-
);
|
|
6035
|
+
console.log(` ${icon} ${chalk9.bold(run.jobName)} | ${run.triggerType} | ${date}`);
|
|
4859
6036
|
console.log(` Duration: ${duration}`);
|
|
4860
6037
|
if (run.summary) {
|
|
4861
|
-
console.log(
|
|
4862
|
-
` ${chalk9.dim(run.summary.slice(0, 100))}`
|
|
4863
|
-
);
|
|
6038
|
+
console.log(` ${chalk9.dim(run.summary.slice(0, 100))}`);
|
|
4864
6039
|
}
|
|
4865
6040
|
console.log();
|
|
4866
6041
|
}
|
|
@@ -4883,28 +6058,20 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
|
|
|
4883
6058
|
process.exit(1);
|
|
4884
6059
|
}
|
|
4885
6060
|
const userId = await getCurrentUserId();
|
|
4886
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
4887
|
-
const runner = new JobRunner2(
|
|
6061
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-P2L6MOOX.js");
|
|
6062
|
+
const runner = new JobRunner2();
|
|
4888
6063
|
const job = await runner.loadJob(name);
|
|
4889
6064
|
if (!job) {
|
|
4890
6065
|
log.error(`Job "${name}" not found.`);
|
|
4891
6066
|
const jobs = await runner.listJobs();
|
|
4892
6067
|
if (jobs.length > 0) {
|
|
4893
|
-
console.log(
|
|
4894
|
-
`Available: ${jobs.map((j) => j.name).join(", ")}`
|
|
4895
|
-
);
|
|
6068
|
+
console.log(`Available: ${jobs.map((j) => j.name).join(", ")}`);
|
|
4896
6069
|
}
|
|
4897
6070
|
process.exit(1);
|
|
4898
6071
|
}
|
|
4899
6072
|
const tz = opts.timezone || "UTC";
|
|
4900
6073
|
const prompt = `[JobRun: ${name}] Run the "${name}" job. Use job_run to execute it.`;
|
|
4901
|
-
const task = await createScheduledTask(
|
|
4902
|
-
userId,
|
|
4903
|
-
`Job: ${name}`,
|
|
4904
|
-
prompt,
|
|
4905
|
-
opts.cron,
|
|
4906
|
-
tz
|
|
4907
|
-
);
|
|
6074
|
+
const task = await createScheduledTask(`Job: ${name}`, prompt, opts.cron, tz);
|
|
4908
6075
|
await callMcpHandler("schedule.link_job", {
|
|
4909
6076
|
task_id: task.id,
|
|
4910
6077
|
job_id: job.jobId
|
|
@@ -4921,6 +6088,144 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
|
|
|
4921
6088
|
});
|
|
4922
6089
|
}
|
|
4923
6090
|
|
|
6091
|
+
// src/commands/credential.ts
|
|
6092
|
+
import chalk10 from "chalk";
|
|
6093
|
+
import { createInterface as createInterface3 } from "readline";
|
|
6094
|
+
var VALID_TYPES = ["api_key", "oauth_token", "login", "secret", "custom"];
|
|
6095
|
+
function registerCredentialCommands(program2) {
|
|
6096
|
+
const credCmd = program2.command("credential").alias("cred").description("Manage locally stored credentials (encrypted, never sent to server)");
|
|
6097
|
+
credCmd.command("list").description("List all stored credentials (metadata only, no secrets)").option("-s, --skill <name>", "Filter by skill name").option("-t, --type <type>", "Filter by credential type").action(async (opts) => {
|
|
6098
|
+
const store = getCredentialStore();
|
|
6099
|
+
let results = store.list();
|
|
6100
|
+
if (opts.skill) {
|
|
6101
|
+
results = results.filter((m) => m.skillName === opts.skill);
|
|
6102
|
+
}
|
|
6103
|
+
if (opts.type) {
|
|
6104
|
+
results = results.filter((m) => m.type === opts.type);
|
|
6105
|
+
}
|
|
6106
|
+
if (results.length === 0) {
|
|
6107
|
+
console.log(chalk10.yellow(" No credentials stored."));
|
|
6108
|
+
console.log(chalk10.dim(" Use `assistme credential set <name>` to add one."));
|
|
6109
|
+
return;
|
|
6110
|
+
}
|
|
6111
|
+
console.log(chalk10.bold("\n Stored Credentials:\n"));
|
|
6112
|
+
for (const m of results) {
|
|
6113
|
+
const skill = m.skillName ? chalk10.dim(` [${m.skillName}]`) : "";
|
|
6114
|
+
const tags = m.tags.length > 0 ? chalk10.dim(` (${m.tags.join(", ")})`) : "";
|
|
6115
|
+
console.log(` ${chalk10.cyan(m.name)} ${chalk10.gray(`(${m.type})`)}${skill}${tags}`);
|
|
6116
|
+
console.log(chalk10.dim(` ID: ${m.id} Created: ${m.createdAt.slice(0, 10)}`));
|
|
6117
|
+
}
|
|
6118
|
+
console.log();
|
|
6119
|
+
});
|
|
6120
|
+
credCmd.command("set <name>").description("Store or update a credential interactively").option("-t, --type <type>", `Credential type: ${VALID_TYPES.join(", ")}`, "secret").option("-s, --skill <name>", "Associate with a skill").option("--tags <tags>", "Comma-separated tags").action(async (name, opts) => {
|
|
6121
|
+
if (!VALID_TYPES.includes(opts.type)) {
|
|
6122
|
+
log.error(`Invalid type "${opts.type}". Must be one of: ${VALID_TYPES.join(", ")}`);
|
|
6123
|
+
process.exit(1);
|
|
6124
|
+
}
|
|
6125
|
+
const rl = createInterface3({
|
|
6126
|
+
input: process.stdin,
|
|
6127
|
+
output: process.stdout
|
|
6128
|
+
});
|
|
6129
|
+
const ask = (q) => new Promise((resolve2) => {
|
|
6130
|
+
rl.question(q, (answer) => resolve2(answer.trim()));
|
|
6131
|
+
});
|
|
6132
|
+
console.log(chalk10.bold(`
|
|
6133
|
+
Set credential: ${name}`));
|
|
6134
|
+
console.log(chalk10.dim(" Enter key-value pairs. Empty key to finish.\n"));
|
|
6135
|
+
const data = {};
|
|
6136
|
+
if (opts.type === "login") {
|
|
6137
|
+
data.username = await ask(chalk10.cyan(" Username: "));
|
|
6138
|
+
data.password = await ask(chalk10.cyan(" Password: "));
|
|
6139
|
+
} else if (opts.type === "api_key") {
|
|
6140
|
+
data.api_key = await ask(chalk10.cyan(" API Key: "));
|
|
6141
|
+
} else {
|
|
6142
|
+
while (true) {
|
|
6143
|
+
const key = await ask(chalk10.cyan(" Key (empty to finish): "));
|
|
6144
|
+
if (!key) break;
|
|
6145
|
+
const value = await ask(chalk10.cyan(` Value for "${key}": `));
|
|
6146
|
+
data[key] = value;
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
rl.close();
|
|
6150
|
+
if (Object.keys(data).length === 0) {
|
|
6151
|
+
console.log(chalk10.yellow(" No data provided. Credential not saved."));
|
|
6152
|
+
return;
|
|
6153
|
+
}
|
|
6154
|
+
const store = getCredentialStore();
|
|
6155
|
+
const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [];
|
|
6156
|
+
const meta = store.save(name, opts.type, data, {
|
|
6157
|
+
skillName: opts.skill,
|
|
6158
|
+
tags
|
|
6159
|
+
});
|
|
6160
|
+
log.success(`Credential "${meta.name}" saved (ID: ${meta.id})`);
|
|
6161
|
+
console.log(chalk10.dim(" Encrypted and stored at ~/.config/assistme/credentials/"));
|
|
6162
|
+
});
|
|
6163
|
+
credCmd.command("get <name>").description("Show credential metadata (use --reveal to show secrets)").option("-r, --reveal", "Reveal secret values").action(async (name, opts) => {
|
|
6164
|
+
const store = getCredentialStore();
|
|
6165
|
+
const credential = store.getByName(name);
|
|
6166
|
+
if (!credential) {
|
|
6167
|
+
log.error(`Credential not found: ${name}`);
|
|
6168
|
+
process.exit(1);
|
|
6169
|
+
}
|
|
6170
|
+
const m = credential.meta;
|
|
6171
|
+
console.log(chalk10.bold(`
|
|
6172
|
+
${m.name} (${m.type})`));
|
|
6173
|
+
if (m.skillName) console.log(` Skill: ${m.skillName}`);
|
|
6174
|
+
if (m.tags.length > 0) console.log(` Tags: ${m.tags.join(", ")}`);
|
|
6175
|
+
console.log(` Created: ${m.createdAt}`);
|
|
6176
|
+
console.log(` Updated: ${m.updatedAt}`);
|
|
6177
|
+
if (opts.reveal) {
|
|
6178
|
+
console.log(chalk10.bold("\n Data:"));
|
|
6179
|
+
for (const [key, value] of Object.entries(credential.data)) {
|
|
6180
|
+
console.log(` ${chalk10.cyan(key)}: ${value}`);
|
|
6181
|
+
}
|
|
6182
|
+
} else {
|
|
6183
|
+
console.log(chalk10.bold("\n Data keys:"));
|
|
6184
|
+
for (const key of Object.keys(credential.data)) {
|
|
6185
|
+
console.log(` ${chalk10.cyan(key)}: ${"*".repeat(8)}`);
|
|
6186
|
+
}
|
|
6187
|
+
console.log(chalk10.dim("\n Use --reveal to show secret values."));
|
|
6188
|
+
}
|
|
6189
|
+
console.log();
|
|
6190
|
+
});
|
|
6191
|
+
credCmd.command("remove <name>").description("Remove a stored credential").action(async (name) => {
|
|
6192
|
+
const store = getCredentialStore();
|
|
6193
|
+
const removed = store.removeByName(name);
|
|
6194
|
+
if (removed) {
|
|
6195
|
+
log.success(`Credential "${name}" removed.`);
|
|
6196
|
+
} else {
|
|
6197
|
+
log.error(`Credential not found: ${name}`);
|
|
6198
|
+
}
|
|
6199
|
+
});
|
|
6200
|
+
credCmd.command("clear").description("Remove ALL stored credentials").action(async () => {
|
|
6201
|
+
const store = getCredentialStore();
|
|
6202
|
+
const count = store.list().length;
|
|
6203
|
+
if (count === 0) {
|
|
6204
|
+
console.log(chalk10.yellow(" No credentials to clear."));
|
|
6205
|
+
return;
|
|
6206
|
+
}
|
|
6207
|
+
const rl = createInterface3({
|
|
6208
|
+
input: process.stdin,
|
|
6209
|
+
output: process.stdout
|
|
6210
|
+
});
|
|
6211
|
+
const answer = await new Promise((resolve2) => {
|
|
6212
|
+
rl.question(
|
|
6213
|
+
chalk10.red(` Remove ALL ${count} credential(s)? This cannot be undone. (yes/no): `),
|
|
6214
|
+
(ans) => {
|
|
6215
|
+
rl.close();
|
|
6216
|
+
resolve2(ans.trim().toLowerCase());
|
|
6217
|
+
}
|
|
6218
|
+
);
|
|
6219
|
+
});
|
|
6220
|
+
if (answer === "yes" || answer === "y") {
|
|
6221
|
+
store.clear();
|
|
6222
|
+
log.success(`All ${count} credential(s) removed.`);
|
|
6223
|
+
} else {
|
|
6224
|
+
console.log(chalk10.dim(" Cancelled."));
|
|
6225
|
+
}
|
|
6226
|
+
});
|
|
6227
|
+
}
|
|
6228
|
+
|
|
4924
6229
|
// src/index.ts
|
|
4925
6230
|
loadEnv();
|
|
4926
6231
|
var require2 = createRequire(import.meta.url);
|
|
@@ -4936,4 +6241,5 @@ registerScheduleCommands(program);
|
|
|
4936
6241
|
registerMemoryCommands(program);
|
|
4937
6242
|
registerSkillCommands(program);
|
|
4938
6243
|
registerJobCommands(program);
|
|
6244
|
+
registerCredentialCommands(program);
|
|
4939
6245
|
program.parse();
|