assistme 0.3.1 → 0.3.2
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 +1771 -496
- 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 +17 -107
- 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 +555 -0
- package/src/browser/controller.ts +1386 -0
- package/src/browser/types.ts +70 -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 +258 -0
- package/src/tools/browser.ts +28 -1208
- package/src/tools/index.ts +32 -263
- 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
|
}
|
|
@@ -602,8 +580,35 @@ URL: ${info.url}`;
|
|
|
602
580
|
const result = await this.send("Runtime.evaluate", {
|
|
603
581
|
expression: `
|
|
604
582
|
(function() {
|
|
605
|
-
|
|
606
|
-
|
|
583
|
+
var sel = ${selectorJS};
|
|
584
|
+
|
|
585
|
+
// Support :contains('text') pseudo-selector (not native CSS)
|
|
586
|
+
var containsMatch = sel.match(/^(.+?)?:contains\\(['"](.+?)['"]\\)$/);
|
|
587
|
+
if (containsMatch) {
|
|
588
|
+
var baseTag = (containsMatch[1] || '*').toLowerCase();
|
|
589
|
+
var searchText = containsMatch[2];
|
|
590
|
+
var candidates = document.querySelectorAll(baseTag === '*' ? '*' : baseTag);
|
|
591
|
+
var found = null;
|
|
592
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
593
|
+
var c = candidates[i];
|
|
594
|
+
// Prefer exact text match on direct text content (not children)
|
|
595
|
+
var directText = Array.from(c.childNodes)
|
|
596
|
+
.filter(function(n) { return n.nodeType === 3; })
|
|
597
|
+
.map(function(n) { return n.textContent.trim(); })
|
|
598
|
+
.join(' ');
|
|
599
|
+
if (directText === searchText || c.textContent.trim() === searchText) {
|
|
600
|
+
// Prefer the deepest (most specific) matching element
|
|
601
|
+
if (!found || found.contains(c)) found = c;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (!found) return 'Element not found: ' + sel;
|
|
605
|
+
found.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
606
|
+
found.click();
|
|
607
|
+
return 'Clicked: ' + (found.tagName || '') + ' ' + (found.textContent || '').slice(0, 50).trim();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
var el = document.querySelector(sel);
|
|
611
|
+
if (!el) return 'Element not found: ' + sel;
|
|
607
612
|
|
|
608
613
|
// Scroll into view
|
|
609
614
|
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
@@ -629,9 +634,23 @@ URL: ${info.url}`;
|
|
|
629
634
|
if (!el) return 'Element not found: ' + ${selectorJS};
|
|
630
635
|
|
|
631
636
|
el.focus();
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
637
|
+
|
|
638
|
+
// Clear existing value
|
|
639
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
640
|
+
window.HTMLInputElement.prototype, 'value'
|
|
641
|
+
)?.set || Object.getOwnPropertyDescriptor(
|
|
642
|
+
window.HTMLTextAreaElement.prototype, 'value'
|
|
643
|
+
)?.set;
|
|
644
|
+
if (nativeInputValueSetter) {
|
|
645
|
+
nativeInputValueSetter.call(el, ${textJS});
|
|
646
|
+
} else {
|
|
647
|
+
el.value = ${textJS};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Dispatch events that frameworks (React, Angular, Material) listen to
|
|
651
|
+
el.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
|
|
652
|
+
el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
|
|
653
|
+
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: ${textJS} }));
|
|
635
654
|
return 'Typed into: ' + (el.tagName || '') + ' [' + (el.name || el.id || '') + ']';
|
|
636
655
|
})()
|
|
637
656
|
`,
|
|
@@ -689,6 +708,635 @@ URL: ${info.url}`;
|
|
|
689
708
|
await new Promise((r) => setTimeout(r, 300));
|
|
690
709
|
return "Scrolled up.";
|
|
691
710
|
}
|
|
711
|
+
// ── Annotated Snapshot (ref system) ─────────────────────────────
|
|
712
|
+
/**
|
|
713
|
+
* Take a snapshot of all interactive elements on the page.
|
|
714
|
+
*
|
|
715
|
+
* Strategy (informed by research — arxiv:2511.19477):
|
|
716
|
+
* - **Text ref table is ALWAYS returned** — compact, low-token, works for
|
|
717
|
+
* all page complexities including dense layouts (date pickers, tables).
|
|
718
|
+
* - **Annotated screenshot is OPTIONAL** (annotate parameter):
|
|
719
|
+
* - true: overlay ref badges on screenshot (best for simple pages with
|
|
720
|
+
* few interactive elements — gives visual context)
|
|
721
|
+
* - false: plain screenshot without overlays (default — avoids label
|
|
722
|
+
* clutter on dense pages; model still sees the page visually)
|
|
723
|
+
* - Research shows text-based grounding outperforms visual annotations
|
|
724
|
+
* on complex pages, and the hybrid approach (a11y text primary +
|
|
725
|
+
* selective vision) achieves ~85% vs ~50% for pure vision.
|
|
726
|
+
*/
|
|
727
|
+
async snapshot(annotate = false) {
|
|
728
|
+
this.ensureConnected();
|
|
729
|
+
await this.waitForLoad(5e3);
|
|
730
|
+
const findResult = await this.send("Runtime.evaluate", {
|
|
731
|
+
expression: `
|
|
732
|
+
(function() {
|
|
733
|
+
// Clean up previous refs
|
|
734
|
+
document.querySelectorAll('[data-assistme-ref]').forEach(function(el) {
|
|
735
|
+
el.removeAttribute('data-assistme-ref');
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
var selectors = [
|
|
739
|
+
'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
|
|
740
|
+
'[role="button"]', '[role="link"]', '[role="checkbox"]', '[role="radio"]',
|
|
741
|
+
'[role="combobox"]', '[role="listbox"]', '[role="menuitem"]', '[role="tab"]',
|
|
742
|
+
'[role="switch"]', '[role="slider"]', '[role="option"]', '[role="searchbox"]',
|
|
743
|
+
'[onclick]', '[tabindex]:not([tabindex="-1"])',
|
|
744
|
+
'[contenteditable="true"]'
|
|
745
|
+
].join(', ');
|
|
746
|
+
|
|
747
|
+
// Collect elements from main document AND same-origin iframes
|
|
748
|
+
var all = Array.from(document.querySelectorAll(selectors));
|
|
749
|
+
try {
|
|
750
|
+
var iframes = document.querySelectorAll('iframe');
|
|
751
|
+
for (var fi = 0; fi < iframes.length; fi++) {
|
|
752
|
+
try {
|
|
753
|
+
var iframeDoc = iframes[fi].contentDocument;
|
|
754
|
+
if (iframeDoc) {
|
|
755
|
+
var iframeRect = iframes[fi].getBoundingClientRect();
|
|
756
|
+
var iframeEls = iframeDoc.querySelectorAll(selectors);
|
|
757
|
+
for (var fe = 0; fe < iframeEls.length; fe++) {
|
|
758
|
+
// Tag iframe elements with offset for coordinate correction
|
|
759
|
+
iframeEls[fe].__iframeOffset = { x: iframeRect.x, y: iframeRect.y };
|
|
760
|
+
all.push(iframeEls[fe]);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
} catch(e) { /* cross-origin iframe, skip */ }
|
|
764
|
+
}
|
|
765
|
+
} catch(e) { /* iframe enumeration failed, continue */ }
|
|
766
|
+
|
|
767
|
+
var refs = [];
|
|
768
|
+
var vh = window.innerHeight;
|
|
769
|
+
var vw = window.innerWidth;
|
|
770
|
+
|
|
771
|
+
for (var i = 0; i < all.length && refs.length < 80; i++) {
|
|
772
|
+
var el = all[i];
|
|
773
|
+
var rect = el.getBoundingClientRect();
|
|
774
|
+
|
|
775
|
+
// Skip invisible / tiny elements
|
|
776
|
+
if (rect.width < 5 || rect.height < 5) continue;
|
|
777
|
+
var style = window.getComputedStyle(el);
|
|
778
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
|
|
779
|
+
|
|
780
|
+
// Skip elements far outside viewport
|
|
781
|
+
if (rect.bottom < -50 || rect.top > vh + 50) continue;
|
|
782
|
+
if (rect.right < -50 || rect.left > vw + 50) continue;
|
|
783
|
+
|
|
784
|
+
// Determine role
|
|
785
|
+
var role = el.getAttribute('role') || '';
|
|
786
|
+
if (!role) {
|
|
787
|
+
var tag = el.tagName.toLowerCase();
|
|
788
|
+
if (tag === 'a') role = 'link';
|
|
789
|
+
else if (tag === 'button') role = 'button';
|
|
790
|
+
else if (tag === 'input') {
|
|
791
|
+
var t = (el.type || 'text').toLowerCase();
|
|
792
|
+
if (t === 'checkbox') role = 'checkbox';
|
|
793
|
+
else if (t === 'radio') role = 'radio';
|
|
794
|
+
else if (t === 'submit' || t === 'button') role = 'button';
|
|
795
|
+
else role = 'textbox';
|
|
796
|
+
}
|
|
797
|
+
else if (tag === 'select') role = 'combobox';
|
|
798
|
+
else if (tag === 'textarea') role = 'textbox';
|
|
799
|
+
else role = tag;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Determine accessible name
|
|
803
|
+
var name = '';
|
|
804
|
+
var ariaLabel = el.getAttribute('aria-label');
|
|
805
|
+
var ariaLabelledBy = el.getAttribute('aria-labelledby');
|
|
806
|
+
if (ariaLabel) {
|
|
807
|
+
name = ariaLabel;
|
|
808
|
+
} else if (ariaLabelledBy) {
|
|
809
|
+
var labelEl = document.getElementById(ariaLabelledBy);
|
|
810
|
+
if (labelEl) name = labelEl.textContent.trim();
|
|
811
|
+
} else if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
|
812
|
+
if (el.id) {
|
|
813
|
+
var lbl = document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
|
|
814
|
+
if (lbl) name = lbl.textContent.trim();
|
|
815
|
+
}
|
|
816
|
+
if (!name) name = el.getAttribute('placeholder') || el.getAttribute('name') || '';
|
|
817
|
+
} else {
|
|
818
|
+
name = (el.textContent || '').trim().slice(0, 60);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
var refId = refs.length + 1;
|
|
822
|
+
el.setAttribute('data-assistme-ref', String(refId));
|
|
823
|
+
|
|
824
|
+
// Correct coordinates for elements inside iframes
|
|
825
|
+
var offsetX = el.__iframeOffset ? el.__iframeOffset.x : 0;
|
|
826
|
+
var offsetY = el.__iframeOffset ? el.__iframeOffset.y : 0;
|
|
827
|
+
|
|
828
|
+
refs.push({
|
|
829
|
+
id: refId,
|
|
830
|
+
role: role,
|
|
831
|
+
name: name,
|
|
832
|
+
tag: el.tagName.toLowerCase(),
|
|
833
|
+
type: el.getAttribute('type') || '',
|
|
834
|
+
box: {
|
|
835
|
+
x: Math.round(rect.x + offsetX),
|
|
836
|
+
y: Math.round(rect.y + offsetY),
|
|
837
|
+
width: Math.round(rect.width),
|
|
838
|
+
height: Math.round(rect.height)
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return JSON.stringify(refs);
|
|
844
|
+
})()
|
|
845
|
+
`,
|
|
846
|
+
returnByValue: true
|
|
847
|
+
});
|
|
848
|
+
const refs = JSON.parse(
|
|
849
|
+
findResult.result?.value || "[]"
|
|
850
|
+
).map((r) => ({
|
|
851
|
+
id: r.id,
|
|
852
|
+
role: r.role,
|
|
853
|
+
name: r.name,
|
|
854
|
+
tag: r.tag,
|
|
855
|
+
inputType: r.type || "",
|
|
856
|
+
box: r.box
|
|
857
|
+
}));
|
|
858
|
+
if (annotate && refs.length <= 40) {
|
|
859
|
+
const refsJson = JSON.stringify(refs);
|
|
860
|
+
await this.send("Runtime.evaluate", {
|
|
861
|
+
expression: `
|
|
862
|
+
(function() {
|
|
863
|
+
var old = document.getElementById('__assistme_refs__');
|
|
864
|
+
if (old) old.remove();
|
|
865
|
+
|
|
866
|
+
var overlay = document.createElement('div');
|
|
867
|
+
overlay.id = '__assistme_refs__';
|
|
868
|
+
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;';
|
|
869
|
+
|
|
870
|
+
var refs = ${refsJson};
|
|
871
|
+
var vh = window.innerHeight;
|
|
872
|
+
var vw = window.innerWidth;
|
|
873
|
+
|
|
874
|
+
for (var i = 0; i < refs.length; i++) {
|
|
875
|
+
var b = refs[i].box;
|
|
876
|
+
if (b.y + b.height < 0 || b.y > vh || b.x + b.width < 0 || b.x > vw) continue;
|
|
877
|
+
|
|
878
|
+
// Red badge with ref number
|
|
879
|
+
var badge = document.createElement('div');
|
|
880
|
+
var badgeTop = Math.max(0, b.y - 14);
|
|
881
|
+
var badgeLeft = Math.max(0, b.x);
|
|
882
|
+
badge.style.cssText = 'position:fixed;background:#e8384f;color:#fff;font:bold 10px/1.2 monospace;padding:1px 3px;border-radius:2px;white-space:nowrap;'
|
|
883
|
+
+ 'left:' + badgeLeft + 'px;top:' + badgeTop + 'px;';
|
|
884
|
+
badge.textContent = String(refs[i].id);
|
|
885
|
+
overlay.appendChild(badge);
|
|
886
|
+
|
|
887
|
+
// Border around element
|
|
888
|
+
var border = document.createElement('div');
|
|
889
|
+
border.style.cssText = 'position:fixed;border:1.5px solid #e8384f;border-radius:2px;'
|
|
890
|
+
+ 'left:' + b.x + 'px;top:' + b.y + 'px;width:' + b.width + 'px;height:' + b.height + 'px;';
|
|
891
|
+
overlay.appendChild(border);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
document.documentElement.appendChild(overlay);
|
|
895
|
+
})()
|
|
896
|
+
`
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
const image = await this.screenshot();
|
|
900
|
+
if (annotate) {
|
|
901
|
+
await this.send("Runtime.evaluate", {
|
|
902
|
+
expression: `(function() { var el = document.getElementById('__assistme_refs__'); if (el) el.remove(); })()`
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
this.refCache.clear();
|
|
906
|
+
for (const ref of refs) {
|
|
907
|
+
this.refCache.set(ref.id, ref);
|
|
908
|
+
}
|
|
909
|
+
const pageInfo = await this.getPageInfo();
|
|
910
|
+
return { image, refs, url: pageInfo.url, title: pageInfo.title };
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Build a compact text table of refs for the model.
|
|
914
|
+
*/
|
|
915
|
+
static formatRefTable(result) {
|
|
916
|
+
let table = `Page: ${result.title}
|
|
917
|
+
URL: ${result.url}
|
|
918
|
+
|
|
919
|
+
Refs:
|
|
920
|
+
`;
|
|
921
|
+
for (const ref of result.refs) {
|
|
922
|
+
const extra = ref.inputType ? ` (${ref.inputType})` : "";
|
|
923
|
+
const nameStr = ref.name ? ` "${ref.name}"` : "";
|
|
924
|
+
table += `[${ref.id}] ${ref.role}${nameStr}${extra}
|
|
925
|
+
`;
|
|
926
|
+
}
|
|
927
|
+
if (result.refs.length === 0) {
|
|
928
|
+
table += "(no interactive elements found)\n";
|
|
929
|
+
}
|
|
930
|
+
return table;
|
|
931
|
+
}
|
|
932
|
+
// ── Ref Resolution ────────────────────────────────────────────────
|
|
933
|
+
/**
|
|
934
|
+
* Resolve a ref ID to its current center coordinates in the viewport.
|
|
935
|
+
* Uses two strategies:
|
|
936
|
+
* 1. Fast: find by data-assistme-ref attribute (set during snapshot)
|
|
937
|
+
* 2. Stable: search by role + accessible name (survives DOM changes)
|
|
938
|
+
*
|
|
939
|
+
* Includes actionability checks (like Playwright):
|
|
940
|
+
* - Element must be visible (not display:none, not zero-size)
|
|
941
|
+
* - Element must be in viewport (scrolls into view if needed)
|
|
942
|
+
* - Element must not be covered by another element (checks elementFromPoint)
|
|
943
|
+
*
|
|
944
|
+
* Returns null if the element cannot be found or is not actionable.
|
|
945
|
+
* Returns { error: string } if found but not actionable (for diagnostics).
|
|
946
|
+
*/
|
|
947
|
+
async resolveRef(refId) {
|
|
948
|
+
const cached = this.refCache.get(refId);
|
|
949
|
+
const role = cached?.role || "";
|
|
950
|
+
const name = cached?.name || "";
|
|
951
|
+
const roleJS = JSON.stringify(role);
|
|
952
|
+
const nameJS = JSON.stringify(name);
|
|
953
|
+
const result = await this.send("Runtime.evaluate", {
|
|
954
|
+
expression: `
|
|
955
|
+
(function() {
|
|
956
|
+
var refId = ${refId};
|
|
957
|
+
var role = ${roleJS};
|
|
958
|
+
var name = ${nameJS};
|
|
959
|
+
|
|
960
|
+
// Strategy 1: data attribute (fast, from last snapshot)
|
|
961
|
+
var el = document.querySelector('[data-assistme-ref="' + refId + '"]');
|
|
962
|
+
|
|
963
|
+
// Strategy 2: role + name search (stable, survives DOM changes)
|
|
964
|
+
if (!el && role && name) {
|
|
965
|
+
var selectorMap = {
|
|
966
|
+
textbox: 'input, textarea, [role="textbox"], [role="searchbox"]',
|
|
967
|
+
button: 'button, [role="button"], input[type="submit"], input[type="button"]',
|
|
968
|
+
link: 'a[href], [role="link"]',
|
|
969
|
+
combobox: 'select, [role="combobox"]',
|
|
970
|
+
checkbox: 'input[type="checkbox"], [role="checkbox"]',
|
|
971
|
+
radio: 'input[type="radio"], [role="radio"]',
|
|
972
|
+
tab: '[role="tab"]',
|
|
973
|
+
menuitem: '[role="menuitem"]',
|
|
974
|
+
option: '[role="option"], option',
|
|
975
|
+
};
|
|
976
|
+
var sel = selectorMap[role] || '*[role="' + role + '"]';
|
|
977
|
+
var candidates = document.querySelectorAll(sel);
|
|
978
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
979
|
+
var c = candidates[i];
|
|
980
|
+
var cName = c.getAttribute('aria-label')
|
|
981
|
+
|| c.getAttribute('placeholder')
|
|
982
|
+
|| (c.textContent || '').trim().slice(0, 60);
|
|
983
|
+
if (cName === name) { el = c; break; }
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (!el) return 'null';
|
|
988
|
+
|
|
989
|
+
// \u2500\u2500 Actionability checks (Playwright-style) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
990
|
+
|
|
991
|
+
// Check visibility
|
|
992
|
+
var style = window.getComputedStyle(el);
|
|
993
|
+
if (style.display === 'none')
|
|
994
|
+
return JSON.stringify({ error: 'Element is hidden (display:none)' });
|
|
995
|
+
if (style.visibility === 'hidden')
|
|
996
|
+
return JSON.stringify({ error: 'Element is hidden (visibility:hidden)' });
|
|
997
|
+
if (parseFloat(style.opacity) < 0.05)
|
|
998
|
+
return JSON.stringify({ error: 'Element is hidden (opacity:0)' });
|
|
999
|
+
|
|
1000
|
+
// Check disabled
|
|
1001
|
+
if (el.disabled || el.getAttribute('aria-disabled') === 'true')
|
|
1002
|
+
return JSON.stringify({ error: 'Element is disabled' });
|
|
1003
|
+
|
|
1004
|
+
// Scroll into view
|
|
1005
|
+
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
1006
|
+
var r = el.getBoundingClientRect();
|
|
1007
|
+
|
|
1008
|
+
// Check non-zero size
|
|
1009
|
+
if (r.width < 1 || r.height < 1)
|
|
1010
|
+
return JSON.stringify({ error: 'Element has zero size (' + r.width + 'x' + r.height + ')' });
|
|
1011
|
+
|
|
1012
|
+
// Check element is in viewport
|
|
1013
|
+
if (r.bottom < 0 || r.top > window.innerHeight || r.right < 0 || r.left > window.innerWidth)
|
|
1014
|
+
return JSON.stringify({ error: 'Element is outside viewport after scroll' });
|
|
1015
|
+
|
|
1016
|
+
var cx = r.x + r.width / 2;
|
|
1017
|
+
var cy = r.y + r.height / 2;
|
|
1018
|
+
|
|
1019
|
+
// Check not covered by another element (hit test)
|
|
1020
|
+
var topEl = document.elementFromPoint(cx, cy);
|
|
1021
|
+
if (topEl && topEl !== el && !el.contains(topEl) && !topEl.closest('[data-assistme-ref="' + refId + '"]')) {
|
|
1022
|
+
// Check if the covering element is the overlay (ignore it)
|
|
1023
|
+
if (!topEl.closest('#__assistme_refs__')) {
|
|
1024
|
+
var coverTag = topEl.tagName.toLowerCase();
|
|
1025
|
+
var coverText = (topEl.textContent || '').trim().slice(0, 30);
|
|
1026
|
+
return JSON.stringify({
|
|
1027
|
+
error: 'Element is covered by <' + coverTag + '>' + (coverText ? ' "' + coverText + '"' : ''),
|
|
1028
|
+
x: cx, y: cy, width: r.width, height: r.height
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return JSON.stringify({
|
|
1034
|
+
x: cx,
|
|
1035
|
+
y: cy,
|
|
1036
|
+
width: r.width,
|
|
1037
|
+
height: r.height
|
|
1038
|
+
});
|
|
1039
|
+
})()
|
|
1040
|
+
`,
|
|
1041
|
+
returnByValue: true
|
|
1042
|
+
});
|
|
1043
|
+
const value = result.result?.value;
|
|
1044
|
+
if (!value || value === "null") return null;
|
|
1045
|
+
try {
|
|
1046
|
+
return JSON.parse(value);
|
|
1047
|
+
} catch {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
// ── Ref-based Interactions (CDP Input Events) ─────────────────────
|
|
1052
|
+
/**
|
|
1053
|
+
* Click an element by ref using CDP Input.dispatchMouseEvent.
|
|
1054
|
+
* This simulates a real mouse click through the browser's input pipeline,
|
|
1055
|
+
* triggering hover states, focus management, and all native browser events
|
|
1056
|
+
* — more reliable than el.click() for framework components.
|
|
1057
|
+
*
|
|
1058
|
+
* Includes auto-wait: retries up to 3 times (with 500ms intervals) if the
|
|
1059
|
+
* element is not yet actionable (e.g., covered by a loading overlay, still
|
|
1060
|
+
* animating into view). This matches Playwright's auto-waiting behavior.
|
|
1061
|
+
*/
|
|
1062
|
+
async clickRef(refId) {
|
|
1063
|
+
this.ensureConnected();
|
|
1064
|
+
const maxRetries = 3;
|
|
1065
|
+
let lastError = "";
|
|
1066
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1067
|
+
const resolved = await this.resolveRef(refId);
|
|
1068
|
+
if (!resolved) {
|
|
1069
|
+
return `Ref [${refId}] not found. Take a new snapshot with browser_snapshot.`;
|
|
1070
|
+
}
|
|
1071
|
+
if (resolved.error) {
|
|
1072
|
+
lastError = resolved.error;
|
|
1073
|
+
if (attempt < maxRetries - 1) {
|
|
1074
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
const ref3 = this.refCache.get(refId);
|
|
1078
|
+
return `Cannot click [${refId}] ${ref3?.role || ""} "${ref3?.name || ""}": ${lastError}`;
|
|
1079
|
+
}
|
|
1080
|
+
if (attempt === 0) {
|
|
1081
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1082
|
+
const settled = await this.resolveRef(refId);
|
|
1083
|
+
if (settled && !settled.error) {
|
|
1084
|
+
resolved.x = settled.x;
|
|
1085
|
+
resolved.y = settled.y;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
1089
|
+
type: "mouseMoved",
|
|
1090
|
+
x: resolved.x,
|
|
1091
|
+
y: resolved.y
|
|
1092
|
+
});
|
|
1093
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
1094
|
+
type: "mousePressed",
|
|
1095
|
+
x: resolved.x,
|
|
1096
|
+
y: resolved.y,
|
|
1097
|
+
button: "left",
|
|
1098
|
+
clickCount: 1
|
|
1099
|
+
});
|
|
1100
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
1101
|
+
type: "mouseReleased",
|
|
1102
|
+
x: resolved.x,
|
|
1103
|
+
y: resolved.y,
|
|
1104
|
+
button: "left",
|
|
1105
|
+
clickCount: 1
|
|
1106
|
+
});
|
|
1107
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1108
|
+
const ref2 = this.refCache.get(refId);
|
|
1109
|
+
return `Clicked [${refId}] ${ref2?.role || ""} "${ref2?.name || ""}"`;
|
|
1110
|
+
}
|
|
1111
|
+
const ref = this.refCache.get(refId);
|
|
1112
|
+
return `Cannot click [${refId}] ${ref?.role || ""} "${ref?.name || ""}": ${lastError}`;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Type text into an element by ref using CDP Input events.
|
|
1116
|
+
* Clicks to focus, selects all existing text (Ctrl/Cmd+A), then uses
|
|
1117
|
+
* Input.insertText for reliable text insertion across all frameworks.
|
|
1118
|
+
*/
|
|
1119
|
+
async typeRef(refId, text) {
|
|
1120
|
+
this.ensureConnected();
|
|
1121
|
+
const clickResult = await this.clickRef(refId);
|
|
1122
|
+
if (clickResult.includes("not found")) return clickResult;
|
|
1123
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1124
|
+
const modifier = platform() === "darwin" ? 4 : 2;
|
|
1125
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
1126
|
+
type: "keyDown",
|
|
1127
|
+
modifiers: modifier,
|
|
1128
|
+
key: "a",
|
|
1129
|
+
code: "KeyA",
|
|
1130
|
+
windowsVirtualKeyCode: 65
|
|
1131
|
+
});
|
|
1132
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
1133
|
+
type: "keyUp",
|
|
1134
|
+
key: "a",
|
|
1135
|
+
code: "KeyA"
|
|
1136
|
+
});
|
|
1137
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
1138
|
+
type: "keyDown",
|
|
1139
|
+
key: "Backspace",
|
|
1140
|
+
code: "Backspace",
|
|
1141
|
+
windowsVirtualKeyCode: 8
|
|
1142
|
+
});
|
|
1143
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
1144
|
+
type: "keyUp",
|
|
1145
|
+
key: "Backspace",
|
|
1146
|
+
code: "Backspace"
|
|
1147
|
+
});
|
|
1148
|
+
await this.send("Input.insertText", { text });
|
|
1149
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1150
|
+
const ref = this.refCache.get(refId);
|
|
1151
|
+
return `Typed "${text}" into [${refId}] ${ref?.role || ""} "${ref?.name || ""}"`;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Select a dropdown option by ref. Delegates to selectOption with the
|
|
1155
|
+
* ref's data attribute as selector, handling both native <select> and
|
|
1156
|
+
* custom dropdown components.
|
|
1157
|
+
*/
|
|
1158
|
+
async selectRef(refId, option) {
|
|
1159
|
+
this.ensureConnected();
|
|
1160
|
+
const cached = this.refCache.get(refId);
|
|
1161
|
+
if (!cached) {
|
|
1162
|
+
return `Ref [${refId}] not found. Take a new snapshot with browser_snapshot.`;
|
|
1163
|
+
}
|
|
1164
|
+
const result = await this.selectOption(`[data-assistme-ref="${refId}"]`, option);
|
|
1165
|
+
return result.replace(
|
|
1166
|
+
/\[data-assistme-ref="\d+"\]/,
|
|
1167
|
+
`[${refId}] ${cached.role} "${cached.name}"`
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
// ── Action Pipeline ───────────────────────────────────────────────
|
|
1171
|
+
/**
|
|
1172
|
+
* Execute a batch of actions sequentially using refs.
|
|
1173
|
+
* Reduces round-trips: instead of one tool call per action, the model
|
|
1174
|
+
* can specify a sequence of actions that execute atomically.
|
|
1175
|
+
*
|
|
1176
|
+
* Optionally takes a screenshot after all actions complete.
|
|
1177
|
+
*/
|
|
1178
|
+
async act(actions, takeScreenshot = false) {
|
|
1179
|
+
this.ensureConnected();
|
|
1180
|
+
const results = [];
|
|
1181
|
+
for (const spec of actions) {
|
|
1182
|
+
let result;
|
|
1183
|
+
let success = true;
|
|
1184
|
+
try {
|
|
1185
|
+
switch (spec.action) {
|
|
1186
|
+
case "click":
|
|
1187
|
+
result = await this.clickRef(spec.ref);
|
|
1188
|
+
success = !result.includes("not found");
|
|
1189
|
+
break;
|
|
1190
|
+
case "type":
|
|
1191
|
+
result = await this.typeRef(spec.ref, spec.text);
|
|
1192
|
+
success = !result.includes("not found");
|
|
1193
|
+
break;
|
|
1194
|
+
case "select":
|
|
1195
|
+
result = await this.selectRef(spec.ref, spec.option);
|
|
1196
|
+
success = !result.includes("not found");
|
|
1197
|
+
break;
|
|
1198
|
+
case "press":
|
|
1199
|
+
result = await this.pressKey(spec.key);
|
|
1200
|
+
break;
|
|
1201
|
+
case "scroll":
|
|
1202
|
+
result = spec.direction === "up" ? await this.scrollUp() : await this.scrollDown();
|
|
1203
|
+
break;
|
|
1204
|
+
case "wait":
|
|
1205
|
+
await new Promise((r) => setTimeout(r, Math.min(spec.ms, 5e3)));
|
|
1206
|
+
result = `Waited ${spec.ms}ms`;
|
|
1207
|
+
break;
|
|
1208
|
+
default:
|
|
1209
|
+
result = `Unknown action: ${spec.action}`;
|
|
1210
|
+
success = false;
|
|
1211
|
+
}
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
result = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1214
|
+
success = false;
|
|
1215
|
+
}
|
|
1216
|
+
results.push({
|
|
1217
|
+
action: spec.action,
|
|
1218
|
+
ref: "ref" in spec ? spec.ref : void 0,
|
|
1219
|
+
result,
|
|
1220
|
+
success
|
|
1221
|
+
});
|
|
1222
|
+
if (!success) break;
|
|
1223
|
+
if (spec.action !== "wait") {
|
|
1224
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
let screenshot;
|
|
1228
|
+
if (takeScreenshot) {
|
|
1229
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1230
|
+
screenshot = await this.screenshot();
|
|
1231
|
+
}
|
|
1232
|
+
return { results, screenshot };
|
|
1233
|
+
}
|
|
1234
|
+
// ── Dropdown/Select ─────────────────────────────────────────────
|
|
1235
|
+
/**
|
|
1236
|
+
* Select an option from a dropdown — handles both native <select> elements
|
|
1237
|
+
* and custom Material Design / React / Angular dropdown components.
|
|
1238
|
+
*
|
|
1239
|
+
* Strategy:
|
|
1240
|
+
* 1. Try native <select> first (by selector or label text)
|
|
1241
|
+
* 2. Fall back to custom dropdown: click to open, then click the option by text
|
|
1242
|
+
*/
|
|
1243
|
+
async selectOption(selector, optionText) {
|
|
1244
|
+
this.ensureConnected();
|
|
1245
|
+
const selectorJS = JSON.stringify(selector);
|
|
1246
|
+
const optionJS = JSON.stringify(optionText);
|
|
1247
|
+
const result = await this.send("Runtime.evaluate", {
|
|
1248
|
+
expression: `
|
|
1249
|
+
(function() {
|
|
1250
|
+
var sel = ${selectorJS};
|
|
1251
|
+
var optText = ${optionJS};
|
|
1252
|
+
|
|
1253
|
+
// Strategy 1: Native <select> element
|
|
1254
|
+
var selectEl = document.querySelector(sel);
|
|
1255
|
+
if (selectEl && selectEl.tagName === 'SELECT') {
|
|
1256
|
+
var options = selectEl.querySelectorAll('option');
|
|
1257
|
+
for (var i = 0; i < options.length; i++) {
|
|
1258
|
+
if (options[i].textContent.trim() === optText) {
|
|
1259
|
+
selectEl.value = options[i].value;
|
|
1260
|
+
selectEl.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1261
|
+
selectEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1262
|
+
return 'Selected "' + optText + '" in native select';
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return 'Option "' + optText + '" not found in select. Available: ' +
|
|
1266
|
+
Array.from(options).map(function(o) { return o.textContent.trim(); }).join(', ');
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Strategy 2: Custom dropdown \u2014 find the trigger element
|
|
1270
|
+
var trigger = selectEl;
|
|
1271
|
+
if (!trigger) {
|
|
1272
|
+
// Try finding by label/placeholder text
|
|
1273
|
+
var allEls = document.querySelectorAll('*');
|
|
1274
|
+
for (var j = 0; j < allEls.length; j++) {
|
|
1275
|
+
var el = allEls[j];
|
|
1276
|
+
var ownText = Array.from(el.childNodes)
|
|
1277
|
+
.filter(function(n) { return n.nodeType === 3; })
|
|
1278
|
+
.map(function(n) { return n.textContent.trim(); })
|
|
1279
|
+
.join('');
|
|
1280
|
+
if (ownText === sel || el.getAttribute('aria-label') === sel) {
|
|
1281
|
+
trigger = el;
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (!trigger) return 'Dropdown not found: ' + sel;
|
|
1288
|
+
|
|
1289
|
+
// Click to open the dropdown
|
|
1290
|
+
trigger.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
1291
|
+
trigger.click();
|
|
1292
|
+
|
|
1293
|
+
// Wait a frame for the dropdown menu to render, then select the option
|
|
1294
|
+
return new Promise(function(resolve) {
|
|
1295
|
+
setTimeout(function() {
|
|
1296
|
+
// Look for the option in listbox/menu/dropdown overlays
|
|
1297
|
+
var optionContainers = document.querySelectorAll(
|
|
1298
|
+
'[role="listbox"], [role="menu"], [role="presentation"], .MuiMenu-list, .MuiList-root, ul.mdc-list, .VfPpkd-xl07Ob'
|
|
1299
|
+
);
|
|
1300
|
+
|
|
1301
|
+
// Also check all visible elements as fallback
|
|
1302
|
+
var searchIn = optionContainers.length > 0
|
|
1303
|
+
? Array.from(optionContainers).flatMap(function(c) { return Array.from(c.querySelectorAll('*')); })
|
|
1304
|
+
: Array.from(document.querySelectorAll('li, [role="option"], [role="menuitem"], div[data-value]'));
|
|
1305
|
+
|
|
1306
|
+
for (var k = 0; k < searchIn.length; k++) {
|
|
1307
|
+
var opt = searchIn[k];
|
|
1308
|
+
var txt = opt.textContent ? opt.textContent.trim() : '';
|
|
1309
|
+
if (txt === optText) {
|
|
1310
|
+
opt.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
1311
|
+
opt.click();
|
|
1312
|
+
resolve('Selected "' + optText + '" from custom dropdown');
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Broader search: any visible element with exact text match
|
|
1318
|
+
var everything = document.querySelectorAll('*');
|
|
1319
|
+
for (var m = 0; m < everything.length; m++) {
|
|
1320
|
+
var candidate = everything[m];
|
|
1321
|
+
if (candidate.textContent && candidate.textContent.trim() === optText &&
|
|
1322
|
+
candidate.offsetParent !== null && candidate.children.length === 0) {
|
|
1323
|
+
candidate.click();
|
|
1324
|
+
resolve('Selected "' + optText + '" (broad match)');
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
resolve('Option "' + optText + '" not found in dropdown');
|
|
1330
|
+
}, 300);
|
|
1331
|
+
});
|
|
1332
|
+
})()
|
|
1333
|
+
`,
|
|
1334
|
+
returnByValue: true,
|
|
1335
|
+
awaitPromise: true
|
|
1336
|
+
});
|
|
1337
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1338
|
+
return result.result?.value || "Selection attempted.";
|
|
1339
|
+
}
|
|
692
1340
|
// ── JavaScript Evaluation ───────────────────────────────────────
|
|
693
1341
|
async evaluate(expression) {
|
|
694
1342
|
this.ensureConnected();
|
|
@@ -826,12 +1474,28 @@ URL: ${info.url}`;
|
|
|
826
1474
|
(function() {
|
|
827
1475
|
var url = window.location.href.toLowerCase();
|
|
828
1476
|
|
|
1477
|
+
// Exclude signup/registration pages \u2014 these are NOT login pages
|
|
1478
|
+
var signupPatterns = [
|
|
1479
|
+
'/signup', '/sign-up', '/sign_up', '/register',
|
|
1480
|
+
'/registration', '/create-account', '/create_account',
|
|
1481
|
+
'/join', '/enroll',
|
|
1482
|
+
'accounts.google.com/lifecycle/steps/signup',
|
|
1483
|
+
'signup.live.com',
|
|
1484
|
+
];
|
|
1485
|
+
for (var s = 0; s < signupPatterns.length; s++) {
|
|
1486
|
+
if (url.indexOf(signupPatterns[s]) !== -1) {
|
|
1487
|
+
return JSON.stringify({ isLoginPage: false, reason: '' });
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
829
1491
|
// URL-based detection
|
|
830
1492
|
var loginPatterns = [
|
|
831
1493
|
'/login', '/signin', '/sign-in', '/sign_in',
|
|
832
1494
|
'/auth/', '/sso/', '/oauth/', '/session/new',
|
|
833
1495
|
'/accounts/login', '/users/sign_in',
|
|
834
|
-
'accounts.google.com',
|
|
1496
|
+
'accounts.google.com/v3/signin',
|
|
1497
|
+
'accounts.google.com/servicelogin',
|
|
1498
|
+
'login.microsoftonline.com',
|
|
835
1499
|
'github.com/login', 'github.com/session',
|
|
836
1500
|
'login.live.com', 'appleid.apple.com'
|
|
837
1501
|
];
|
|
@@ -885,8 +1549,14 @@ URL: ${info.url}`;
|
|
|
885
1549
|
}
|
|
886
1550
|
}
|
|
887
1551
|
};
|
|
1552
|
+
|
|
1553
|
+
// src/browser/chrome-launcher.ts
|
|
1554
|
+
import { execSync, spawn } from "child_process";
|
|
1555
|
+
import { platform as platform2, homedir } from "os";
|
|
1556
|
+
import { existsSync, unlinkSync, mkdirSync, cpSync } from "fs";
|
|
1557
|
+
import { join } from "path";
|
|
888
1558
|
function findChromePath() {
|
|
889
|
-
const os =
|
|
1559
|
+
const os = platform2();
|
|
890
1560
|
if (os === "darwin") {
|
|
891
1561
|
const paths = [
|
|
892
1562
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
@@ -895,7 +1565,7 @@ function findChromePath() {
|
|
|
895
1565
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
896
1566
|
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
897
1567
|
];
|
|
898
|
-
return paths.find((p) =>
|
|
1568
|
+
return paths.find((p) => existsSync(p)) ?? null;
|
|
899
1569
|
}
|
|
900
1570
|
if (os === "linux") {
|
|
901
1571
|
const names = [
|
|
@@ -932,7 +1602,7 @@ function findChromePath() {
|
|
|
932
1602
|
for (const prefix of prefixes) {
|
|
933
1603
|
for (const sub of subPaths) {
|
|
934
1604
|
const p = `${prefix}\\${sub}`;
|
|
935
|
-
if (
|
|
1605
|
+
if (existsSync(p)) return p;
|
|
936
1606
|
}
|
|
937
1607
|
}
|
|
938
1608
|
return null;
|
|
@@ -940,39 +1610,39 @@ function findChromePath() {
|
|
|
940
1610
|
return null;
|
|
941
1611
|
}
|
|
942
1612
|
function getDefaultProfileDir(chromePath) {
|
|
943
|
-
const home =
|
|
944
|
-
const os =
|
|
1613
|
+
const home = homedir();
|
|
1614
|
+
const os = platform2();
|
|
945
1615
|
if (os === "darwin") {
|
|
946
1616
|
if (chromePath.includes("Brave Browser"))
|
|
947
|
-
return
|
|
1617
|
+
return join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser");
|
|
948
1618
|
if (chromePath.includes("Microsoft Edge"))
|
|
949
|
-
return
|
|
1619
|
+
return join(home, "Library", "Application Support", "Microsoft Edge");
|
|
950
1620
|
if (chromePath.includes("Chromium"))
|
|
951
|
-
return
|
|
1621
|
+
return join(home, "Library", "Application Support", "Chromium");
|
|
952
1622
|
if (chromePath.includes("Canary"))
|
|
953
|
-
return
|
|
954
|
-
return
|
|
1623
|
+
return join(home, "Library", "Application Support", "Google", "Chrome Canary");
|
|
1624
|
+
return join(home, "Library", "Application Support", "Google", "Chrome");
|
|
955
1625
|
}
|
|
956
1626
|
if (os === "win32") {
|
|
957
|
-
const appData = process.env.LOCALAPPDATA ||
|
|
1627
|
+
const appData = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
|
|
958
1628
|
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
|
|
1629
|
+
return join(appData, "BraveSoftware", "Brave-Browser", "User Data");
|
|
1630
|
+
if (chromePath.includes("msedge")) return join(appData, "Microsoft", "Edge", "User Data");
|
|
1631
|
+
return join(appData, "Google", "Chrome", "User Data");
|
|
1632
|
+
}
|
|
1633
|
+
if (chromePath.includes("brave")) return join(home, ".config", "BraveSoftware", "Brave-Browser");
|
|
1634
|
+
if (chromePath.includes("microsoft-edge")) return join(home, ".config", "microsoft-edge");
|
|
1635
|
+
if (chromePath.includes("chromium")) return join(home, ".config", "chromium");
|
|
1636
|
+
return join(home, ".config", "google-chrome");
|
|
967
1637
|
}
|
|
968
1638
|
function getDebugProfileDir(chromePath) {
|
|
969
|
-
const home =
|
|
970
|
-
const debugDir =
|
|
971
|
-
if (!
|
|
972
|
-
|
|
1639
|
+
const home = homedir();
|
|
1640
|
+
const debugDir = join(home, ".assistme", "browser-profile");
|
|
1641
|
+
if (!existsSync(debugDir)) {
|
|
1642
|
+
mkdirSync(debugDir, { recursive: true });
|
|
973
1643
|
log.debug(`Created debug profile directory: ${debugDir}`);
|
|
974
1644
|
const realDir = getDefaultProfileDir(chromePath);
|
|
975
|
-
if (
|
|
1645
|
+
if (existsSync(realDir)) {
|
|
976
1646
|
seedDebugProfile(realDir, debugDir);
|
|
977
1647
|
}
|
|
978
1648
|
}
|
|
@@ -982,35 +1652,35 @@ function seedDebugProfile(realDir, debugDir) {
|
|
|
982
1652
|
const rootFiles = ["Local State"];
|
|
983
1653
|
const profileFiles = ["Bookmarks", "Preferences", "Favicons", "Top Sites", "Shortcuts"];
|
|
984
1654
|
for (const file of rootFiles) {
|
|
985
|
-
const src =
|
|
986
|
-
const dest =
|
|
1655
|
+
const src = join(realDir, file);
|
|
1656
|
+
const dest = join(debugDir, file);
|
|
987
1657
|
try {
|
|
988
|
-
if (
|
|
1658
|
+
if (existsSync(src)) {
|
|
989
1659
|
cpSync(src, dest, { force: true });
|
|
990
1660
|
log.debug(`Seeded: ${file}`);
|
|
991
1661
|
}
|
|
992
1662
|
} catch {
|
|
993
1663
|
}
|
|
994
1664
|
}
|
|
995
|
-
const srcProfile =
|
|
996
|
-
const destProfile =
|
|
997
|
-
if (
|
|
998
|
-
|
|
1665
|
+
const srcProfile = join(realDir, "Default");
|
|
1666
|
+
const destProfile = join(debugDir, "Default");
|
|
1667
|
+
if (existsSync(srcProfile)) {
|
|
1668
|
+
mkdirSync(destProfile, { recursive: true });
|
|
999
1669
|
for (const file of profileFiles) {
|
|
1000
|
-
const src =
|
|
1001
|
-
const dest =
|
|
1670
|
+
const src = join(srcProfile, file);
|
|
1671
|
+
const dest = join(destProfile, file);
|
|
1002
1672
|
try {
|
|
1003
|
-
if (
|
|
1673
|
+
if (existsSync(src)) {
|
|
1004
1674
|
cpSync(src, dest, { force: true });
|
|
1005
1675
|
log.debug(`Seeded: Default/${file}`);
|
|
1006
1676
|
}
|
|
1007
1677
|
} catch {
|
|
1008
1678
|
}
|
|
1009
1679
|
}
|
|
1010
|
-
const srcExt =
|
|
1011
|
-
const destExt =
|
|
1680
|
+
const srcExt = join(srcProfile, "Extensions");
|
|
1681
|
+
const destExt = join(destProfile, "Extensions");
|
|
1012
1682
|
try {
|
|
1013
|
-
if (
|
|
1683
|
+
if (existsSync(srcExt)) {
|
|
1014
1684
|
cpSync(srcExt, destExt, { recursive: true, force: true });
|
|
1015
1685
|
log.debug("Seeded: Default/Extensions");
|
|
1016
1686
|
}
|
|
@@ -1101,14 +1771,14 @@ async function ensureBrowserAvailable(port = 9222) {
|
|
|
1101
1771
|
return { success: true, action: "launched", chromePath };
|
|
1102
1772
|
}
|
|
1103
1773
|
const debugDir = getDebugProfileDir(chromePath);
|
|
1104
|
-
const lockPath =
|
|
1105
|
-
if (
|
|
1774
|
+
const lockPath = join(debugDir, "SingletonLock");
|
|
1775
|
+
if (existsSync(lockPath)) {
|
|
1106
1776
|
log.debug("Found stale SingletonLock in debug profile \u2014 removing and retrying");
|
|
1107
1777
|
try {
|
|
1108
1778
|
unlinkSync(lockPath);
|
|
1109
1779
|
for (const f of ["SingletonSocket", "SingletonCookie"]) {
|
|
1110
1780
|
try {
|
|
1111
|
-
unlinkSync(
|
|
1781
|
+
unlinkSync(join(debugDir, f));
|
|
1112
1782
|
} catch {
|
|
1113
1783
|
}
|
|
1114
1784
|
}
|
|
@@ -1365,7 +2035,7 @@ var Scheduler = class {
|
|
|
1365
2035
|
}
|
|
1366
2036
|
}
|
|
1367
2037
|
};
|
|
1368
|
-
async function createScheduledTask(
|
|
2038
|
+
async function createScheduledTask(name, prompt, cronExpression, timezone = "UTC") {
|
|
1369
2039
|
const nextRun = getNextRunTime(cronExpression, timezone);
|
|
1370
2040
|
return callMcpHandler("schedule.create", {
|
|
1371
2041
|
name,
|
|
@@ -1375,7 +2045,7 @@ async function createScheduledTask(_userId, name, prompt, cronExpression, timezo
|
|
|
1375
2045
|
next_run_at: nextRun.toISOString()
|
|
1376
2046
|
});
|
|
1377
2047
|
}
|
|
1378
|
-
async function listScheduledTasks(
|
|
2048
|
+
async function listScheduledTasks() {
|
|
1379
2049
|
return callMcpHandler("schedule.list");
|
|
1380
2050
|
}
|
|
1381
2051
|
async function toggleScheduledTask(taskId, enabled) {
|
|
@@ -1423,20 +2093,10 @@ var SessionManager = class {
|
|
|
1423
2093
|
const config = getConfig();
|
|
1424
2094
|
this.onTask = onTask;
|
|
1425
2095
|
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
|
-
);
|
|
2096
|
+
this.session = await createSession(config.sessionName, config.workspacePath, "0.1.0");
|
|
2097
|
+
this.conversationId = await getOrCreateCliConversation();
|
|
1436
2098
|
this.running = true;
|
|
1437
|
-
log.success(
|
|
1438
|
-
`Session started: ${this.session.id} (${config.sessionName})`
|
|
1439
|
-
);
|
|
2099
|
+
log.success(`Session started: ${this.session.id} (${config.sessionName})`);
|
|
1440
2100
|
log.info(`Workspace: ${config.workspacePath}`);
|
|
1441
2101
|
this.heartbeatTimer = setInterval(async () => {
|
|
1442
2102
|
if (this.session) {
|
|
@@ -1464,20 +2124,15 @@ var SessionManager = class {
|
|
|
1464
2124
|
if (!this.session || !this.userId || !this.conversationId) return;
|
|
1465
2125
|
log.info(`Running scheduled task: "${scheduledTask.name}"`);
|
|
1466
2126
|
try {
|
|
1467
|
-
await this.submitTask(
|
|
1468
|
-
`[Scheduled: ${scheduledTask.name}] ${scheduledTask.prompt}`
|
|
1469
|
-
);
|
|
2127
|
+
await this.submitTask(`[Scheduled: ${scheduledTask.name}] ${scheduledTask.prompt}`);
|
|
1470
2128
|
} catch (err) {
|
|
1471
2129
|
log.error(`Scheduled task error: ${err}`);
|
|
1472
2130
|
}
|
|
1473
2131
|
}
|
|
1474
2132
|
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);
|
|
2133
|
+
if (!this.session || !this.userId || !this.conversationId || !this.onTask) return;
|
|
2134
|
+
log.info(`Executing job run: ${jobRun.job_name} (${jobRun.id.slice(0, 8)}...)`);
|
|
2135
|
+
const runner = new JobRunner();
|
|
1481
2136
|
const job = await runner.loadJob(jobRun.job_name);
|
|
1482
2137
|
if (!job) {
|
|
1483
2138
|
log.error(`Job "${jobRun.job_name}" not found, marking run as failed`);
|
|
@@ -1549,7 +2204,7 @@ var SessionManager = class {
|
|
|
1549
2204
|
}
|
|
1550
2205
|
}
|
|
1551
2206
|
} else if (this.userId) {
|
|
1552
|
-
const jobRun = await pollAndClaimJobRun(
|
|
2207
|
+
const jobRun = await pollAndClaimJobRun();
|
|
1553
2208
|
if (jobRun) {
|
|
1554
2209
|
this.processingDepth++;
|
|
1555
2210
|
await setSessionBusy(this.session.id, true);
|
|
@@ -1593,12 +2248,7 @@ var SessionManager = class {
|
|
|
1593
2248
|
this.processingDepth++;
|
|
1594
2249
|
await setSessionBusy(this.session.id, true);
|
|
1595
2250
|
try {
|
|
1596
|
-
const task = await createTask(
|
|
1597
|
-
this.conversationId,
|
|
1598
|
-
this.userId,
|
|
1599
|
-
this.session.id,
|
|
1600
|
-
prompt
|
|
1601
|
-
);
|
|
2251
|
+
const task = await createTask(this.conversationId, this.session.id, prompt);
|
|
1602
2252
|
await claimTask(task.id);
|
|
1603
2253
|
await this.onTask(task);
|
|
1604
2254
|
} catch (err) {
|
|
@@ -1656,16 +2306,12 @@ import {
|
|
|
1656
2306
|
|
|
1657
2307
|
// src/agent/memory.ts
|
|
1658
2308
|
var MemoryManager = class {
|
|
1659
|
-
constructor(_userId) {
|
|
1660
|
-
}
|
|
1661
2309
|
/**
|
|
1662
2310
|
* Store a new memory. Called by the agent after completing tasks
|
|
1663
2311
|
* to remember important facts about the user.
|
|
1664
2312
|
*/
|
|
1665
2313
|
async remember(content, category = "general", options) {
|
|
1666
|
-
const expiresAt = options?.expiresInDays ? new Date(
|
|
1667
|
-
Date.now() + options.expiresInDays * 864e5
|
|
1668
|
-
).toISOString() : null;
|
|
2314
|
+
const expiresAt = options?.expiresInDays ? new Date(Date.now() + options.expiresInDays * 864e5).toISOString() : null;
|
|
1669
2315
|
const data = await callMcpHandler("memory.store", {
|
|
1670
2316
|
category,
|
|
1671
2317
|
content,
|
|
@@ -1895,7 +2541,8 @@ function parseDbMetadata(raw) {
|
|
|
1895
2541
|
primaryEnv: openclaw.primaryEnv,
|
|
1896
2542
|
os: openclaw.os,
|
|
1897
2543
|
always: openclaw.always,
|
|
1898
|
-
skillKey: openclaw.skillKey
|
|
2544
|
+
skillKey: openclaw.skillKey,
|
|
2545
|
+
credentials: openclaw.credentials
|
|
1899
2546
|
};
|
|
1900
2547
|
}
|
|
1901
2548
|
var SkillManager = class {
|
|
@@ -2055,23 +2702,6 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
2055
2702
|
}
|
|
2056
2703
|
return prompt;
|
|
2057
2704
|
}
|
|
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
2705
|
async create(name, description, content, options) {
|
|
2076
2706
|
if (!this.userId) return null;
|
|
2077
2707
|
try {
|
|
@@ -2259,7 +2889,10 @@ _(${skills.length - included} additional skills available \u2014 use skill_searc
|
|
|
2259
2889
|
async searchDb(query3, limit = 10) {
|
|
2260
2890
|
if (this.userId) {
|
|
2261
2891
|
try {
|
|
2262
|
-
const data = await callMcpHandler("skill.search", {
|
|
2892
|
+
const data = await callMcpHandler("skill.search", {
|
|
2893
|
+
query: query3,
|
|
2894
|
+
limit
|
|
2895
|
+
});
|
|
2263
2896
|
if (data) {
|
|
2264
2897
|
return data.map((row) => ({
|
|
2265
2898
|
name: row.name,
|
|
@@ -2586,7 +3219,7 @@ async function withRetry(fn, opts = {}) {
|
|
|
2586
3219
|
throw lastError;
|
|
2587
3220
|
}
|
|
2588
3221
|
|
|
2589
|
-
// src/
|
|
3222
|
+
// src/mcp/browser-server.ts
|
|
2590
3223
|
import {
|
|
2591
3224
|
createSdkMcpServer,
|
|
2592
3225
|
tool
|
|
@@ -2595,7 +3228,7 @@ import { z } from "zod/v4";
|
|
|
2595
3228
|
|
|
2596
3229
|
// src/tools/filesystem.ts
|
|
2597
3230
|
import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
|
|
2598
|
-
import { resolve, relative, join as
|
|
3231
|
+
import { resolve, relative, join as join2 } from "path";
|
|
2599
3232
|
import { glob } from "glob";
|
|
2600
3233
|
function assertWithinWorkspace(filePath) {
|
|
2601
3234
|
const config = getConfig();
|
|
@@ -2632,7 +3265,7 @@ async function searchFiles(pattern, directory) {
|
|
|
2632
3265
|
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
|
|
2633
3266
|
});
|
|
2634
3267
|
if (matches.length === 0) return "No files found matching the pattern.";
|
|
2635
|
-
return matches.slice(0, 50).map((m) => relative(config.workspacePath,
|
|
3268
|
+
return matches.slice(0, 50).map((m) => relative(config.workspacePath, join2(cwd, m))).join("\n");
|
|
2636
3269
|
}
|
|
2637
3270
|
async function listDirectory(path) {
|
|
2638
3271
|
const config = getConfig();
|
|
@@ -2642,7 +3275,7 @@ async function listDirectory(path) {
|
|
|
2642
3275
|
for (const entry of entries) {
|
|
2643
3276
|
if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
|
|
2644
3277
|
const icon = entry.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}";
|
|
2645
|
-
const info = entry.isFile() ? await stat(
|
|
3278
|
+
const info = entry.isFile() ? await stat(join2(resolved, entry.name)).then(
|
|
2646
3279
|
(s) => ` (${formatSize(s.size)})`
|
|
2647
3280
|
) : "";
|
|
2648
3281
|
results.push(`${icon} ${entry.name}${info}`);
|
|
@@ -2666,11 +3299,11 @@ async function searchContent(pattern, fileGlob, directory) {
|
|
|
2666
3299
|
const results = [];
|
|
2667
3300
|
for (const file of files.slice(0, 200)) {
|
|
2668
3301
|
try {
|
|
2669
|
-
const content = await readFile(
|
|
3302
|
+
const content = await readFile(join2(cwd, file), "utf-8");
|
|
2670
3303
|
const lines = content.split("\n");
|
|
2671
3304
|
for (let i = 0; i < lines.length; i++) {
|
|
2672
3305
|
if (regex.test(lines[i])) {
|
|
2673
|
-
const relPath = relative(config.workspacePath,
|
|
3306
|
+
const relPath = relative(config.workspacePath, join2(cwd, file));
|
|
2674
3307
|
results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
|
|
2675
3308
|
regex.lastIndex = 0;
|
|
2676
3309
|
if (results.length >= 30) break;
|
|
@@ -2878,9 +3511,28 @@ async function executeTool(name, input) {
|
|
|
2878
3511
|
case "browser_get_elements":
|
|
2879
3512
|
await ensureConnected(browser);
|
|
2880
3513
|
return browser.getInteractiveElements();
|
|
3514
|
+
case "browser_select":
|
|
3515
|
+
await ensureConnected(browser);
|
|
3516
|
+
return browser.selectOption(input.selector, input.option);
|
|
2881
3517
|
case "browser_evaluate":
|
|
2882
3518
|
await ensureConnected(browser);
|
|
2883
3519
|
return browser.evaluate(input.expression);
|
|
3520
|
+
case "browser_snapshot": {
|
|
3521
|
+
await ensureConnected(browser);
|
|
3522
|
+
const snap = await browser.snapshot(input.annotate);
|
|
3523
|
+
return BrowserController.formatRefTable(snap) + "\n__SNAPSHOT_IMAGE__:" + snap.image;
|
|
3524
|
+
}
|
|
3525
|
+
case "browser_act": {
|
|
3526
|
+
await ensureConnected(browser);
|
|
3527
|
+
const actions = input.actions;
|
|
3528
|
+
const wantScreenshot = input.screenshot || false;
|
|
3529
|
+
const actResult = await browser.act(actions, wantScreenshot);
|
|
3530
|
+
let response = actResult.results.map((r) => `${r.success ? "OK" : "FAIL"}: ${r.result}`).join("\n");
|
|
3531
|
+
if (actResult.screenshot) {
|
|
3532
|
+
response += "\n__ACT_SCREENSHOT__:" + actResult.screenshot;
|
|
3533
|
+
}
|
|
3534
|
+
return response;
|
|
3535
|
+
}
|
|
2884
3536
|
case "browser_list_tabs":
|
|
2885
3537
|
return browser.listTabs();
|
|
2886
3538
|
case "browser_switch_tab":
|
|
@@ -3023,7 +3675,7 @@ function getLimiterForTool(toolName) {
|
|
|
3023
3675
|
return null;
|
|
3024
3676
|
}
|
|
3025
3677
|
|
|
3026
|
-
// src/
|
|
3678
|
+
// src/mcp/browser-server.ts
|
|
3027
3679
|
async function callTool(name, input) {
|
|
3028
3680
|
const limiter = getLimiterForTool(name);
|
|
3029
3681
|
if (limiter) await limiter.acquire();
|
|
@@ -3040,6 +3692,9 @@ var BROWSER_TOOL_NAMES = [
|
|
|
3040
3692
|
"browser_press_key",
|
|
3041
3693
|
"browser_scroll",
|
|
3042
3694
|
"browser_get_elements",
|
|
3695
|
+
"browser_select",
|
|
3696
|
+
"browser_snapshot",
|
|
3697
|
+
"browser_act",
|
|
3043
3698
|
"browser_evaluate",
|
|
3044
3699
|
"browser_list_tabs",
|
|
3045
3700
|
"browser_switch_tab",
|
|
@@ -3124,9 +3779,86 @@ function createBrowserMcpServer() {
|
|
|
3124
3779
|
{},
|
|
3125
3780
|
async () => callTool("browser_get_elements", {})
|
|
3126
3781
|
),
|
|
3782
|
+
tool(
|
|
3783
|
+
"browser_select",
|
|
3784
|
+
"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.",
|
|
3785
|
+
{
|
|
3786
|
+
selector: z.string().describe(
|
|
3787
|
+
"CSS selector of the dropdown, or its label/placeholder text (e.g. 'Month', 'Gender', '#country')"
|
|
3788
|
+
),
|
|
3789
|
+
option: z.string().describe("Visible text of the option to select (e.g. 'March', 'Male')")
|
|
3790
|
+
},
|
|
3791
|
+
async (args) => callTool("browser_select", args)
|
|
3792
|
+
),
|
|
3793
|
+
tool(
|
|
3794
|
+
"browser_snapshot",
|
|
3795
|
+
"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.",
|
|
3796
|
+
{
|
|
3797
|
+
annotate: z.boolean().optional().describe(
|
|
3798
|
+
"Overlay ref badges on the screenshot. Default false. Use true for simple pages where visual context helps."
|
|
3799
|
+
)
|
|
3800
|
+
},
|
|
3801
|
+
async (args) => {
|
|
3802
|
+
const limiter = getLimiterForTool("browser_snapshot");
|
|
3803
|
+
if (limiter) await limiter.acquire();
|
|
3804
|
+
const result = await executeTool("browser_snapshot", args);
|
|
3805
|
+
const parts = result.split("\n__SNAPSHOT_IMAGE__:");
|
|
3806
|
+
const refTable = parts[0];
|
|
3807
|
+
const imageData = parts[1] || "";
|
|
3808
|
+
const content = [];
|
|
3809
|
+
if (imageData.length > 100) {
|
|
3810
|
+
content.push({
|
|
3811
|
+
type: "image",
|
|
3812
|
+
data: imageData,
|
|
3813
|
+
mimeType: "image/png"
|
|
3814
|
+
});
|
|
3815
|
+
}
|
|
3816
|
+
content.push({ type: "text", text: refTable });
|
|
3817
|
+
return { content };
|
|
3818
|
+
}
|
|
3819
|
+
),
|
|
3820
|
+
tool(
|
|
3821
|
+
"browser_act",
|
|
3822
|
+
"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.",
|
|
3823
|
+
{
|
|
3824
|
+
actions: z.array(
|
|
3825
|
+
z.object({
|
|
3826
|
+
action: z.enum(["click", "type", "select", "press", "scroll", "wait"]).describe("Action type"),
|
|
3827
|
+
ref: z.number().optional().describe("Ref number from browser_snapshot"),
|
|
3828
|
+
text: z.string().optional().describe("Text to type (for 'type' action)"),
|
|
3829
|
+
option: z.string().optional().describe("Option to select (for 'select' action)"),
|
|
3830
|
+
key: z.string().optional().describe("Key to press (for 'press' action)"),
|
|
3831
|
+
direction: z.string().optional().describe("'up' or 'down' (for 'scroll')"),
|
|
3832
|
+
ms: z.number().optional().describe("Wait duration in ms (for 'wait', max 5000)")
|
|
3833
|
+
})
|
|
3834
|
+
).describe("Actions to execute sequentially"),
|
|
3835
|
+
screenshot: z.boolean().optional().describe("Take screenshot after actions (default: false)")
|
|
3836
|
+
},
|
|
3837
|
+
async (args) => {
|
|
3838
|
+
const limiter = getLimiterForTool("browser_act");
|
|
3839
|
+
if (limiter) await limiter.acquire();
|
|
3840
|
+
const result = await executeTool("browser_act", {
|
|
3841
|
+
actions: args.actions,
|
|
3842
|
+
screenshot: args.screenshot
|
|
3843
|
+
});
|
|
3844
|
+
const parts = result.split("\n__ACT_SCREENSHOT__:");
|
|
3845
|
+
const actionText = parts[0];
|
|
3846
|
+
const screenshotData = parts[1] || "";
|
|
3847
|
+
const content = [];
|
|
3848
|
+
content.push({ type: "text", text: actionText });
|
|
3849
|
+
if (screenshotData.length > 100) {
|
|
3850
|
+
content.push({
|
|
3851
|
+
type: "image",
|
|
3852
|
+
data: screenshotData,
|
|
3853
|
+
mimeType: "image/png"
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
return { content };
|
|
3857
|
+
}
|
|
3858
|
+
),
|
|
3127
3859
|
tool(
|
|
3128
3860
|
"browser_evaluate",
|
|
3129
|
-
"Execute JavaScript in the browser page context.",
|
|
3861
|
+
"Execute JavaScript in the browser page context. Use as a last resort when browser_snapshot + browser_act cannot handle the interaction.",
|
|
3130
3862
|
{ expression: z.string().describe("JavaScript expression to evaluate") },
|
|
3131
3863
|
async (args) => callTool("browser_evaluate", args)
|
|
3132
3864
|
),
|
|
@@ -3160,20 +3892,281 @@ function createBrowserMcpServer() {
|
|
|
3160
3892
|
]
|
|
3161
3893
|
});
|
|
3162
3894
|
}
|
|
3895
|
+
|
|
3896
|
+
// src/mcp/agent-tools-server.ts
|
|
3897
|
+
import {
|
|
3898
|
+
createSdkMcpServer as createSdkMcpServer2,
|
|
3899
|
+
tool as tool2
|
|
3900
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
3901
|
+
import { z as z2 } from "zod/v4";
|
|
3902
|
+
|
|
3903
|
+
// src/credentials/credential-store.ts
|
|
3904
|
+
import { randomUUID } from "crypto";
|
|
3905
|
+
import { dirname } from "path";
|
|
3906
|
+
|
|
3907
|
+
// src/credentials/encryption.ts
|
|
3908
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash } from "crypto";
|
|
3909
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
3910
|
+
import { join as join3 } from "path";
|
|
3911
|
+
import { homedir as homedir2, hostname, userInfo } from "os";
|
|
3912
|
+
var ALGORITHM = "aes-256-gcm";
|
|
3913
|
+
var KEY_LENGTH = 32;
|
|
3914
|
+
var IV_LENGTH = 12;
|
|
3915
|
+
var AUTH_TAG_LENGTH = 16;
|
|
3916
|
+
var SALT_FILE = "encryption.salt";
|
|
3917
|
+
function deriveKey(basePath) {
|
|
3918
|
+
const saltPath = join3(basePath, SALT_FILE);
|
|
3919
|
+
let salt;
|
|
3920
|
+
if (existsSync2(saltPath)) {
|
|
3921
|
+
salt = readFileSync(saltPath);
|
|
3922
|
+
} else {
|
|
3923
|
+
salt = randomBytes(32);
|
|
3924
|
+
if (!existsSync2(basePath)) {
|
|
3925
|
+
mkdirSync2(basePath, { recursive: true, mode: 448 });
|
|
3926
|
+
}
|
|
3927
|
+
writeFileSync(saltPath, salt, { mode: 384 });
|
|
3928
|
+
}
|
|
3929
|
+
const machineId = createHash("sha256").update(hostname()).update(userInfo().username).update(homedir2()).digest();
|
|
3930
|
+
return scryptSync(machineId, salt, KEY_LENGTH);
|
|
3931
|
+
}
|
|
3932
|
+
function encrypt(plaintext, key) {
|
|
3933
|
+
const iv = randomBytes(IV_LENGTH);
|
|
3934
|
+
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
3935
|
+
const encrypted = Buffer.concat([
|
|
3936
|
+
cipher.update(plaintext, "utf-8"),
|
|
3937
|
+
cipher.final()
|
|
3938
|
+
]);
|
|
3939
|
+
return {
|
|
3940
|
+
iv: iv.toString("base64"),
|
|
3941
|
+
data: encrypted.toString("base64"),
|
|
3942
|
+
tag: cipher.getAuthTag().toString("base64")
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
function decrypt(payload, key) {
|
|
3946
|
+
const iv = Buffer.from(payload.iv, "base64");
|
|
3947
|
+
const data = Buffer.from(payload.data, "base64");
|
|
3948
|
+
const tag = Buffer.from(payload.tag, "base64");
|
|
3949
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
3950
|
+
decipher.setAuthTag(tag);
|
|
3951
|
+
return Buffer.concat([
|
|
3952
|
+
decipher.update(data),
|
|
3953
|
+
decipher.final()
|
|
3954
|
+
]).toString("utf-8");
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
// src/credentials/local-store.ts
|
|
3958
|
+
import Database from "better-sqlite3";
|
|
3959
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
3960
|
+
import { join as join4 } from "path";
|
|
3961
|
+
import { homedir as homedir3 } from "os";
|
|
3962
|
+
var DEFAULT_DB_DIR = join4(homedir3(), ".config", "assistme");
|
|
3963
|
+
var DEFAULT_DB_NAME = "local.db";
|
|
3964
|
+
var LocalStore = class {
|
|
3965
|
+
db;
|
|
3966
|
+
dbPath;
|
|
3967
|
+
constructor(dbPath) {
|
|
3968
|
+
const dir = dbPath ? dbPath : DEFAULT_DB_DIR;
|
|
3969
|
+
if (!existsSync3(dir)) {
|
|
3970
|
+
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
3971
|
+
}
|
|
3972
|
+
this.dbPath = dbPath ? join4(dbPath, DEFAULT_DB_NAME) : join4(DEFAULT_DB_DIR, DEFAULT_DB_NAME);
|
|
3973
|
+
this.db = new Database(this.dbPath);
|
|
3974
|
+
this.db.pragma("journal_mode = WAL");
|
|
3975
|
+
this.db.pragma("foreign_keys = ON");
|
|
3976
|
+
this.migrate();
|
|
3977
|
+
}
|
|
3978
|
+
/** Run schema migrations. Idempotent — safe to call on every startup. */
|
|
3979
|
+
migrate() {
|
|
3980
|
+
this.db.exec(`
|
|
3981
|
+
CREATE TABLE IF NOT EXISTS credentials (
|
|
3982
|
+
id TEXT PRIMARY KEY,
|
|
3983
|
+
name TEXT NOT NULL UNIQUE,
|
|
3984
|
+
type TEXT NOT NULL DEFAULT 'secret',
|
|
3985
|
+
skill_name TEXT,
|
|
3986
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
3987
|
+
encrypted_data TEXT NOT NULL,
|
|
3988
|
+
created_at TEXT NOT NULL,
|
|
3989
|
+
updated_at TEXT NOT NULL
|
|
3990
|
+
);
|
|
3991
|
+
|
|
3992
|
+
CREATE INDEX IF NOT EXISTS idx_credentials_name ON credentials(name);
|
|
3993
|
+
CREATE INDEX IF NOT EXISTS idx_credentials_skill ON credentials(skill_name);
|
|
3994
|
+
CREATE INDEX IF NOT EXISTS idx_credentials_type ON credentials(type);
|
|
3995
|
+
`);
|
|
3996
|
+
}
|
|
3997
|
+
/** Get the raw database handle for direct queries. */
|
|
3998
|
+
getDb() {
|
|
3999
|
+
return this.db;
|
|
4000
|
+
}
|
|
4001
|
+
/** Close the database connection. */
|
|
4002
|
+
close() {
|
|
4003
|
+
this.db.close();
|
|
4004
|
+
}
|
|
4005
|
+
};
|
|
4006
|
+
var _instance = null;
|
|
4007
|
+
function getLocalStore(dbPath) {
|
|
4008
|
+
if (!_instance) {
|
|
4009
|
+
_instance = new LocalStore(dbPath);
|
|
4010
|
+
}
|
|
4011
|
+
return _instance;
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
// src/credentials/credential-store.ts
|
|
4015
|
+
var CredentialStore = class {
|
|
4016
|
+
store;
|
|
4017
|
+
encryptionKey;
|
|
4018
|
+
constructor(dbPath) {
|
|
4019
|
+
this.store = getLocalStore(dbPath);
|
|
4020
|
+
this.encryptionKey = deriveKey(dirname(this.store.dbPath));
|
|
4021
|
+
}
|
|
4022
|
+
// ── CRUD ────────────────────────────────────────────────────────
|
|
4023
|
+
save(name, type, data, opts) {
|
|
4024
|
+
const db = this.store.getDb();
|
|
4025
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4026
|
+
const encryptedData = this.encryptData(data);
|
|
4027
|
+
const tags = JSON.stringify(opts?.tags || []);
|
|
4028
|
+
const existing = db.prepare("SELECT id FROM credentials WHERE name = ?").get(name);
|
|
4029
|
+
if (existing) {
|
|
4030
|
+
db.prepare(`
|
|
4031
|
+
UPDATE credentials
|
|
4032
|
+
SET type = ?, skill_name = ?, tags = ?, encrypted_data = ?, updated_at = ?
|
|
4033
|
+
WHERE id = ?
|
|
4034
|
+
`).run(type, opts?.skillName ?? null, tags, encryptedData, now, existing.id);
|
|
4035
|
+
log.debug(`Credential "${name}" updated (${existing.id})`);
|
|
4036
|
+
return this.toMeta({ id: existing.id, name, type, skill_name: opts?.skillName ?? null, tags, encrypted_data: encryptedData, created_at: now, updated_at: now });
|
|
4037
|
+
}
|
|
4038
|
+
const id = randomUUID();
|
|
4039
|
+
db.prepare(`
|
|
4040
|
+
INSERT INTO credentials (id, name, type, skill_name, tags, encrypted_data, created_at, updated_at)
|
|
4041
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
4042
|
+
`).run(id, name, type, opts?.skillName ?? null, tags, encryptedData, now, now);
|
|
4043
|
+
log.debug(`Credential "${name}" saved (${id})`);
|
|
4044
|
+
return { id, name, type, skillName: opts?.skillName, tags: opts?.tags || [], createdAt: now, updatedAt: now };
|
|
4045
|
+
}
|
|
4046
|
+
get(id) {
|
|
4047
|
+
const row = this.store.getDb().prepare("SELECT * FROM credentials WHERE id = ?").get(id);
|
|
4048
|
+
return row ? this.toCredential(row) : null;
|
|
4049
|
+
}
|
|
4050
|
+
getByName(name) {
|
|
4051
|
+
const row = this.store.getDb().prepare("SELECT * FROM credentials WHERE name = ?").get(name);
|
|
4052
|
+
return row ? this.toCredential(row) : null;
|
|
4053
|
+
}
|
|
4054
|
+
update(id, data) {
|
|
4055
|
+
const row = this.store.getDb().prepare("SELECT * FROM credentials WHERE id = ?").get(id);
|
|
4056
|
+
if (!row) return null;
|
|
4057
|
+
const existing = this.decryptData(row.encrypted_data);
|
|
4058
|
+
const merged = { ...existing };
|
|
4059
|
+
for (const [key, value] of Object.entries(data)) {
|
|
4060
|
+
if (value !== void 0) {
|
|
4061
|
+
merged[key] = value;
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4065
|
+
const encryptedData = this.encryptData(merged);
|
|
4066
|
+
this.store.getDb().prepare("UPDATE credentials SET encrypted_data = ?, updated_at = ? WHERE id = ?").run(encryptedData, now, id);
|
|
4067
|
+
log.debug(`Credential "${row.name}" updated`);
|
|
4068
|
+
return this.toMeta({ ...row, encrypted_data: encryptedData, updated_at: now });
|
|
4069
|
+
}
|
|
4070
|
+
remove(id) {
|
|
4071
|
+
const result = this.store.getDb().prepare("DELETE FROM credentials WHERE id = ?").run(id);
|
|
4072
|
+
if (result.changes > 0) {
|
|
4073
|
+
log.debug(`Credential ${id} removed`);
|
|
4074
|
+
return true;
|
|
4075
|
+
}
|
|
4076
|
+
return false;
|
|
4077
|
+
}
|
|
4078
|
+
removeByName(name) {
|
|
4079
|
+
const result = this.store.getDb().prepare("DELETE FROM credentials WHERE name = ?").run(name);
|
|
4080
|
+
if (result.changes > 0) {
|
|
4081
|
+
log.debug(`Credential "${name}" removed`);
|
|
4082
|
+
return true;
|
|
4083
|
+
}
|
|
4084
|
+
return false;
|
|
4085
|
+
}
|
|
4086
|
+
// ── Query ───────────────────────────────────────────────────────
|
|
4087
|
+
list() {
|
|
4088
|
+
const rows = this.store.getDb().prepare("SELECT * FROM credentials ORDER BY updated_at DESC").all();
|
|
4089
|
+
return rows.map((r) => this.toMeta(r));
|
|
4090
|
+
}
|
|
4091
|
+
findBySkill(skillName) {
|
|
4092
|
+
const rows = this.store.getDb().prepare("SELECT * FROM credentials WHERE skill_name = ? ORDER BY name").all(skillName);
|
|
4093
|
+
return rows.map((r) => this.toMeta(r));
|
|
4094
|
+
}
|
|
4095
|
+
findByTag(tag) {
|
|
4096
|
+
const rows = this.store.getDb().prepare("SELECT * FROM credentials WHERE tags LIKE ? ORDER BY name").all(`%${tag.toLowerCase()}%`);
|
|
4097
|
+
return rows.filter((r) => {
|
|
4098
|
+
const tags = JSON.parse(r.tags);
|
|
4099
|
+
return tags.some((t) => t.toLowerCase() === tag.toLowerCase());
|
|
4100
|
+
}).map((r) => this.toMeta(r));
|
|
4101
|
+
}
|
|
4102
|
+
findByType(type) {
|
|
4103
|
+
const rows = this.store.getDb().prepare("SELECT * FROM credentials WHERE type = ? ORDER BY name").all(type);
|
|
4104
|
+
return rows.map((r) => this.toMeta(r));
|
|
4105
|
+
}
|
|
4106
|
+
// ── Bulk ────────────────────────────────────────────────────────
|
|
4107
|
+
removeBySkill(skillName) {
|
|
4108
|
+
const result = this.store.getDb().prepare("DELETE FROM credentials WHERE skill_name = ?").run(skillName);
|
|
4109
|
+
return result.changes;
|
|
4110
|
+
}
|
|
4111
|
+
clear() {
|
|
4112
|
+
this.store.getDb().prepare("DELETE FROM credentials").run();
|
|
4113
|
+
}
|
|
4114
|
+
// ── Internal ────────────────────────────────────────────────────
|
|
4115
|
+
encryptData(data) {
|
|
4116
|
+
const payload = encrypt(JSON.stringify(data), this.encryptionKey);
|
|
4117
|
+
return JSON.stringify(payload);
|
|
4118
|
+
}
|
|
4119
|
+
decryptData(encrypted) {
|
|
4120
|
+
const payload = JSON.parse(encrypted);
|
|
4121
|
+
const decrypted = decrypt(payload, this.encryptionKey);
|
|
4122
|
+
return JSON.parse(decrypted);
|
|
4123
|
+
}
|
|
4124
|
+
toMeta(row) {
|
|
4125
|
+
return {
|
|
4126
|
+
id: row.id,
|
|
4127
|
+
name: row.name,
|
|
4128
|
+
type: row.type,
|
|
4129
|
+
skillName: row.skill_name || void 0,
|
|
4130
|
+
tags: JSON.parse(row.tags),
|
|
4131
|
+
createdAt: row.created_at,
|
|
4132
|
+
updatedAt: row.updated_at
|
|
4133
|
+
};
|
|
4134
|
+
}
|
|
4135
|
+
toCredential(row) {
|
|
4136
|
+
try {
|
|
4137
|
+
return {
|
|
4138
|
+
meta: this.toMeta(row),
|
|
4139
|
+
data: this.decryptData(row.encrypted_data)
|
|
4140
|
+
};
|
|
4141
|
+
} catch (err) {
|
|
4142
|
+
log.debug(`Failed to decrypt credential ${row.id}: ${err}`);
|
|
4143
|
+
return null;
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
};
|
|
4147
|
+
var _instance2 = null;
|
|
4148
|
+
function getCredentialStore() {
|
|
4149
|
+
if (!_instance2) {
|
|
4150
|
+
_instance2 = new CredentialStore();
|
|
4151
|
+
}
|
|
4152
|
+
return _instance2;
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
// src/mcp/agent-tools-server.ts
|
|
3163
4156
|
function createAgentToolsServer(deps) {
|
|
3164
|
-
const { memoryManager, skillManager, taskId, sessionId
|
|
3165
|
-
return
|
|
4157
|
+
const { memoryManager, skillManager, taskId, sessionId } = deps;
|
|
4158
|
+
return createSdkMcpServer2({
|
|
3166
4159
|
name: "assistme-agent",
|
|
3167
4160
|
version: "1.0.0",
|
|
3168
4161
|
tools: [
|
|
3169
|
-
|
|
4162
|
+
tool2(
|
|
3170
4163
|
"memory_store",
|
|
3171
4164
|
"Store a memory about the user that persists across conversations. Use when you learn preferences, habits, or standing instructions.",
|
|
3172
4165
|
{
|
|
3173
|
-
content:
|
|
3174
|
-
category:
|
|
3175
|
-
importance:
|
|
3176
|
-
tags:
|
|
4166
|
+
content: z2.string().describe("What to remember (concise, factual statement)"),
|
|
4167
|
+
category: z2.string().optional().describe("Category: general, preference, instruction, context, skill_learned, fact"),
|
|
4168
|
+
importance: z2.number().optional().describe("Importance 1-10 (default: 5). Use 8+ for instructions"),
|
|
4169
|
+
tags: z2.array(z2.string()).optional().describe("Optional tags for searchability")
|
|
3177
4170
|
},
|
|
3178
4171
|
async (args) => {
|
|
3179
4172
|
if (!memoryManager) {
|
|
@@ -3194,23 +4187,25 @@ function createAgentToolsServer(deps) {
|
|
|
3194
4187
|
return { content: [{ type: "text", text: result }] };
|
|
3195
4188
|
}
|
|
3196
4189
|
),
|
|
3197
|
-
|
|
4190
|
+
tool2(
|
|
3198
4191
|
"skill_create",
|
|
3199
4192
|
"Create a new skill and add it to the user's collection. Returns the skill ID on success.",
|
|
3200
4193
|
{
|
|
3201
|
-
name:
|
|
3202
|
-
description:
|
|
3203
|
-
instructions:
|
|
3204
|
-
emoji:
|
|
4194
|
+
name: z2.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
|
|
4195
|
+
description: z2.string().describe("One-line description of what this skill does"),
|
|
4196
|
+
instructions: z2.string().describe("Markdown step-by-step instructions"),
|
|
4197
|
+
emoji: z2.string().optional().describe("Single emoji representing this skill")
|
|
3205
4198
|
},
|
|
3206
4199
|
async (args) => {
|
|
3207
4200
|
const nameError = validateSkillName(args.name);
|
|
3208
4201
|
if (nameError) {
|
|
3209
4202
|
return {
|
|
3210
|
-
content: [
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
4203
|
+
content: [
|
|
4204
|
+
{
|
|
4205
|
+
type: "text",
|
|
4206
|
+
text: `Invalid skill name: ${nameError}. Use lowercase kebab-case like "flight-booking".`
|
|
4207
|
+
}
|
|
4208
|
+
]
|
|
3214
4209
|
};
|
|
3215
4210
|
}
|
|
3216
4211
|
const existing = skillManager.findSimilar(args.name);
|
|
@@ -3224,12 +4219,10 @@ function createAgentToolsServer(deps) {
|
|
|
3224
4219
|
]
|
|
3225
4220
|
};
|
|
3226
4221
|
}
|
|
3227
|
-
const result = await skillManager.create(
|
|
3228
|
-
|
|
3229
|
-
args.
|
|
3230
|
-
|
|
3231
|
-
{ source: "manual", emoji: args.emoji }
|
|
3232
|
-
);
|
|
4222
|
+
const result = await skillManager.create(args.name, args.description, args.instructions, {
|
|
4223
|
+
source: "manual",
|
|
4224
|
+
emoji: args.emoji
|
|
4225
|
+
});
|
|
3233
4226
|
if (!result) {
|
|
3234
4227
|
return {
|
|
3235
4228
|
content: [{ type: "text", text: `Failed to create skill "${args.name}".` }]
|
|
@@ -3257,13 +4250,13 @@ function createAgentToolsServer(deps) {
|
|
|
3257
4250
|
};
|
|
3258
4251
|
}
|
|
3259
4252
|
),
|
|
3260
|
-
|
|
4253
|
+
tool2(
|
|
3261
4254
|
"skill_improve",
|
|
3262
4255
|
"Improve an existing skill with better instructions based on what you just learned. Version auto-bumped.",
|
|
3263
4256
|
{
|
|
3264
|
-
name:
|
|
3265
|
-
improved_instructions:
|
|
3266
|
-
description:
|
|
4257
|
+
name: z2.string().describe("Name of the existing skill to improve"),
|
|
4258
|
+
improved_instructions: z2.string().describe("Full updated markdown instructions (not a diff)"),
|
|
4259
|
+
description: z2.string().optional().describe("Updated description (optional)")
|
|
3267
4260
|
},
|
|
3268
4261
|
async (args) => {
|
|
3269
4262
|
const existing = skillManager.get(args.name);
|
|
@@ -3304,12 +4297,12 @@ function createAgentToolsServer(deps) {
|
|
|
3304
4297
|
};
|
|
3305
4298
|
}
|
|
3306
4299
|
),
|
|
3307
|
-
|
|
4300
|
+
tool2(
|
|
3308
4301
|
"skill_invoke",
|
|
3309
4302
|
"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
4303
|
{
|
|
3311
|
-
name:
|
|
3312
|
-
arguments:
|
|
4304
|
+
name: z2.string().describe("Skill name from the Available Skills list"),
|
|
4305
|
+
arguments: z2.string().optional().describe("Arguments to pass to the skill (replaces $ARGUMENTS placeholders)")
|
|
3313
4306
|
},
|
|
3314
4307
|
async (args) => {
|
|
3315
4308
|
const skill = skillManager.get(args.name);
|
|
@@ -3343,6 +4336,39 @@ ${content}`;
|
|
|
3343
4336
|
**Allowed tools for this skill:** ${skill.allowedTools.join(", ")}
|
|
3344
4337
|
`;
|
|
3345
4338
|
}
|
|
4339
|
+
const credReqs = skill.metadata.credentials;
|
|
4340
|
+
if (credReqs && credReqs.length > 0) {
|
|
4341
|
+
const store = getCredentialStore();
|
|
4342
|
+
const missing = [];
|
|
4343
|
+
for (const req of credReqs) {
|
|
4344
|
+
const cred = store.getByName(req.name);
|
|
4345
|
+
if (cred) {
|
|
4346
|
+
response += `
|
|
4347
|
+
|
|
4348
|
+
**Credential: ${req.name}** (${req.type})
|
|
4349
|
+
`;
|
|
4350
|
+
response += `\`\`\`json
|
|
4351
|
+
${JSON.stringify(cred.data, null, 2)}
|
|
4352
|
+
\`\`\`
|
|
4353
|
+
`;
|
|
4354
|
+
} else if (req.required) {
|
|
4355
|
+
missing.push(`${req.name} (${req.description})`);
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
if (missing.length > 0) {
|
|
4359
|
+
response += `
|
|
4360
|
+
|
|
4361
|
+
**Missing required credentials:**
|
|
4362
|
+
`;
|
|
4363
|
+
for (const m of missing) {
|
|
4364
|
+
response += `- ${m}
|
|
4365
|
+
`;
|
|
4366
|
+
}
|
|
4367
|
+
response += `
|
|
4368
|
+
Use \`ask_user\` to request these from the user, or create them yourself (e.g. register an account), then store with \`credential_set\`.
|
|
4369
|
+
`;
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
3346
4372
|
log.info(`Skill invoked: "${args.name}"`);
|
|
3347
4373
|
skillManager.logInvocation(args.name, {
|
|
3348
4374
|
messageId: taskId,
|
|
@@ -3355,12 +4381,12 @@ ${content}`;
|
|
|
3355
4381
|
};
|
|
3356
4382
|
}
|
|
3357
4383
|
),
|
|
3358
|
-
|
|
4384
|
+
tool2(
|
|
3359
4385
|
"skill_search",
|
|
3360
4386
|
"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
4387
|
{
|
|
3362
|
-
query:
|
|
3363
|
-
limit:
|
|
4388
|
+
query: z2.string().describe("Search query (keywords, topic, or task description)"),
|
|
4389
|
+
limit: z2.number().optional().describe("Max results (default: 5)")
|
|
3364
4390
|
},
|
|
3365
4391
|
async (args) => {
|
|
3366
4392
|
const results = await skillManager.searchDb(args.query, args.limit || 5);
|
|
@@ -3382,14 +4408,14 @@ ${content}`;
|
|
|
3382
4408
|
return { content: [{ type: "text", text: response }] };
|
|
3383
4409
|
}
|
|
3384
4410
|
),
|
|
3385
|
-
|
|
4411
|
+
tool2(
|
|
3386
4412
|
"skill_generate",
|
|
3387
4413
|
"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
4414
|
{
|
|
3389
|
-
job_name:
|
|
4415
|
+
job_name: z2.string().describe(
|
|
3390
4416
|
"Short name for this job/role. Example: '\u7535\u5546\u8FD0\u8425', 'Frontend Dev', 'Data Analyst'"
|
|
3391
4417
|
),
|
|
3392
|
-
job_description:
|
|
4418
|
+
job_description: z2.string().describe(
|
|
3393
4419
|
"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
4420
|
)
|
|
3395
4421
|
},
|
|
@@ -3453,47 +4479,48 @@ ${content}`;
|
|
|
3453
4479
|
return { content: [{ type: "text", text: response }] };
|
|
3454
4480
|
}
|
|
3455
4481
|
),
|
|
3456
|
-
|
|
4482
|
+
tool2(
|
|
3457
4483
|
"skill_link_job",
|
|
3458
4484
|
"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
4485
|
{
|
|
3460
|
-
job_name:
|
|
3461
|
-
job_description:
|
|
3462
|
-
skill_names:
|
|
4486
|
+
job_name: z2.string().describe("Name of the job to link skills to"),
|
|
4487
|
+
job_description: z2.string().describe("Job description (used if job doesn't exist yet)"),
|
|
4488
|
+
skill_names: z2.array(z2.string()).describe("Names of skills to link to this job")
|
|
3463
4489
|
},
|
|
3464
4490
|
async (args) => {
|
|
3465
|
-
if (!userId) {
|
|
3466
|
-
return {
|
|
3467
|
-
content: [{ type: "text", text: "Not authenticated. Cannot link job." }]
|
|
3468
|
-
};
|
|
3469
|
-
}
|
|
3470
4491
|
try {
|
|
3471
|
-
await saveJobToDb(
|
|
3472
|
-
log.success(
|
|
4492
|
+
await saveJobToDb(args.job_name, args.job_description, args.skill_names);
|
|
4493
|
+
log.success(
|
|
4494
|
+
`Job "${args.job_name}": linked ${args.skill_names.length} skills and marked as analyzed`
|
|
4495
|
+
);
|
|
3473
4496
|
return {
|
|
3474
|
-
content: [
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
4497
|
+
content: [
|
|
4498
|
+
{
|
|
4499
|
+
type: "text",
|
|
4500
|
+
text: `Job "${args.job_name}" linked with ${args.skill_names.length} skills and marked as analyzed.`
|
|
4501
|
+
}
|
|
4502
|
+
]
|
|
3478
4503
|
};
|
|
3479
4504
|
} catch (err) {
|
|
3480
4505
|
return {
|
|
3481
|
-
content: [
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
4506
|
+
content: [
|
|
4507
|
+
{
|
|
4508
|
+
type: "text",
|
|
4509
|
+
text: `Failed to link job: ${err instanceof Error ? err.message : err}`
|
|
4510
|
+
}
|
|
4511
|
+
]
|
|
3485
4512
|
};
|
|
3486
4513
|
}
|
|
3487
4514
|
}
|
|
3488
4515
|
),
|
|
3489
|
-
|
|
4516
|
+
tool2(
|
|
3490
4517
|
"skill_browse",
|
|
3491
4518
|
"Browse the skill marketplace to discover skills published by the community. Search by keyword, filter by category, and sort by popularity or rating.",
|
|
3492
4519
|
{
|
|
3493
|
-
query:
|
|
3494
|
-
category:
|
|
3495
|
-
sort:
|
|
3496
|
-
limit:
|
|
4520
|
+
query: z2.string().optional().describe("Search keywords"),
|
|
4521
|
+
category: z2.string().optional().describe("Filter by category (e.g. 'productivity', 'ecommerce', 'dev-tools')"),
|
|
4522
|
+
sort: z2.enum(["popular", "recent", "rating"]).optional().describe("Sort order (default: popular)"),
|
|
4523
|
+
limit: z2.number().optional().describe("Max results (default: 10)")
|
|
3497
4524
|
},
|
|
3498
4525
|
async (args) => {
|
|
3499
4526
|
const results = await skillManager.browse({
|
|
@@ -3526,41 +4553,47 @@ ${content}`;
|
|
|
3526
4553
|
return { content: [{ type: "text", text: response }] };
|
|
3527
4554
|
}
|
|
3528
4555
|
),
|
|
3529
|
-
|
|
4556
|
+
tool2(
|
|
3530
4557
|
"skill_add",
|
|
3531
4558
|
"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
4559
|
{
|
|
3533
|
-
skill_id:
|
|
4560
|
+
skill_id: z2.string().describe("The skill UUID (from skill_browse or skill_create results)")
|
|
3534
4561
|
},
|
|
3535
4562
|
async (args) => {
|
|
3536
4563
|
const added = await skillManager.addSkill(args.skill_id);
|
|
3537
4564
|
if (!added) {
|
|
3538
4565
|
return {
|
|
3539
|
-
content: [
|
|
4566
|
+
content: [
|
|
4567
|
+
{ type: "text", text: `Failed to add skill. Check that the ID is correct.` }
|
|
4568
|
+
]
|
|
3540
4569
|
};
|
|
3541
4570
|
}
|
|
3542
4571
|
const emoji = added.metadata.emoji ? `${added.metadata.emoji} ` : "";
|
|
3543
4572
|
return {
|
|
3544
|
-
content: [
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
4573
|
+
content: [
|
|
4574
|
+
{
|
|
4575
|
+
type: "text",
|
|
4576
|
+
text: `Added **${emoji}${added.name}** v${added.version} to your collection. It's now available for use via skill_invoke.`
|
|
4577
|
+
}
|
|
4578
|
+
]
|
|
3548
4579
|
};
|
|
3549
4580
|
}
|
|
3550
4581
|
),
|
|
3551
|
-
|
|
4582
|
+
tool2(
|
|
3552
4583
|
"skill_publish",
|
|
3553
4584
|
"Publish one of your skills to the marketplace so others can discover and install it.",
|
|
3554
4585
|
{
|
|
3555
|
-
name:
|
|
3556
|
-
category:
|
|
3557
|
-
author_name:
|
|
4586
|
+
name: z2.string().describe("Name of your skill to publish"),
|
|
4587
|
+
category: z2.string().optional().describe("Category (e.g. 'productivity', 'ecommerce', 'dev-tools')"),
|
|
4588
|
+
author_name: z2.string().optional().describe("Your display name as the author")
|
|
3558
4589
|
},
|
|
3559
4590
|
async (args) => {
|
|
3560
4591
|
const skill = skillManager.get(args.name);
|
|
3561
4592
|
if (!skill) {
|
|
3562
4593
|
return {
|
|
3563
|
-
content: [
|
|
4594
|
+
content: [
|
|
4595
|
+
{ type: "text", text: `Skill "${args.name}" not found in your collection.` }
|
|
4596
|
+
]
|
|
3564
4597
|
};
|
|
3565
4598
|
}
|
|
3566
4599
|
if (skill.source === "external") {
|
|
@@ -3574,30 +4607,43 @@ ${content}`;
|
|
|
3574
4607
|
});
|
|
3575
4608
|
if (!result) {
|
|
3576
4609
|
return {
|
|
3577
|
-
content: [
|
|
4610
|
+
content: [
|
|
4611
|
+
{
|
|
4612
|
+
type: "text",
|
|
4613
|
+
text: `Failed to publish "${args.name}". The name may already be taken by another author.`
|
|
4614
|
+
}
|
|
4615
|
+
]
|
|
3578
4616
|
};
|
|
3579
4617
|
}
|
|
3580
4618
|
return {
|
|
3581
|
-
content: [
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
4619
|
+
content: [
|
|
4620
|
+
{
|
|
4621
|
+
type: "text",
|
|
4622
|
+
text: `Skill "${args.name}" published to the marketplace! Others can now find and install it.`
|
|
4623
|
+
}
|
|
4624
|
+
]
|
|
3585
4625
|
};
|
|
3586
4626
|
}
|
|
3587
4627
|
),
|
|
3588
4628
|
// ── User Interaction Tool ───────────────────────────────────
|
|
3589
|
-
|
|
4629
|
+
tool2(
|
|
3590
4630
|
"ask_user",
|
|
3591
4631
|
"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
4632
|
{
|
|
3593
|
-
question:
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
4633
|
+
question: z2.string().describe(
|
|
4634
|
+
"The question to ask (supports markdown). Be specific about what you need and why."
|
|
4635
|
+
),
|
|
4636
|
+
options: z2.array(
|
|
4637
|
+
z2.object({
|
|
4638
|
+
label: z2.string().describe("Button label shown to user"),
|
|
4639
|
+
action_key: z2.string().describe("Machine-readable key returned when selected"),
|
|
4640
|
+
description: z2.string().optional().describe("Tooltip/description for this option")
|
|
4641
|
+
})
|
|
4642
|
+
).optional().describe(
|
|
4643
|
+
"Suggested options shown as buttons. The user can always type a custom answer instead."
|
|
4644
|
+
),
|
|
4645
|
+
placeholder: z2.string().optional().describe("Placeholder text for the free-text input field"),
|
|
4646
|
+
timeout_seconds: z2.number().optional().describe("How long to wait for response (default: 300)")
|
|
3601
4647
|
},
|
|
3602
4648
|
async (args) => {
|
|
3603
4649
|
const actionId = `ask_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -3615,6 +4661,11 @@ ${content}`;
|
|
|
3615
4661
|
log.info(`Ask user ${actionId}: "${args.question.slice(0, 80)}..."`);
|
|
3616
4662
|
emitEvent(taskId, "user_action_request", actionData).catch(() => {
|
|
3617
4663
|
});
|
|
4664
|
+
emitEvent(taskId, "status_change", {
|
|
4665
|
+
status: "waiting_for_user",
|
|
4666
|
+
message: args.question
|
|
4667
|
+
}).catch(() => {
|
|
4668
|
+
});
|
|
3618
4669
|
const startTime = Date.now();
|
|
3619
4670
|
const pollInterval = 2e3;
|
|
3620
4671
|
while (Date.now() - startTime < timeout) {
|
|
@@ -3625,56 +4676,55 @@ ${content}`;
|
|
|
3625
4676
|
const label = response.label || actionKey || text;
|
|
3626
4677
|
log.info(`User responded: "${label}"`);
|
|
3627
4678
|
return {
|
|
3628
|
-
content: [
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
4679
|
+
content: [
|
|
4680
|
+
{
|
|
4681
|
+
type: "text",
|
|
4682
|
+
text: JSON.stringify({
|
|
4683
|
+
status: "responded",
|
|
4684
|
+
action_key: actionKey || "custom_input",
|
|
4685
|
+
label,
|
|
4686
|
+
text: text || label
|
|
4687
|
+
})
|
|
4688
|
+
}
|
|
4689
|
+
]
|
|
3637
4690
|
};
|
|
3638
4691
|
}
|
|
3639
4692
|
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
3640
4693
|
}
|
|
3641
4694
|
log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
|
|
3642
4695
|
return {
|
|
3643
|
-
content: [
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
4696
|
+
content: [
|
|
4697
|
+
{
|
|
4698
|
+
type: "text",
|
|
4699
|
+
text: JSON.stringify({
|
|
4700
|
+
status: "timeout",
|
|
4701
|
+
message: "User did not respond within the timeout period."
|
|
4702
|
+
})
|
|
4703
|
+
}
|
|
4704
|
+
]
|
|
3650
4705
|
};
|
|
3651
4706
|
} catch (err) {
|
|
3652
4707
|
log.error(`ask_user failed: ${err}`);
|
|
3653
4708
|
return {
|
|
3654
|
-
content: [
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
4709
|
+
content: [
|
|
4710
|
+
{
|
|
4711
|
+
type: "text",
|
|
4712
|
+
text: `Failed to ask user: ${err instanceof Error ? err.message : err}`
|
|
4713
|
+
}
|
|
4714
|
+
]
|
|
3658
4715
|
};
|
|
3659
4716
|
}
|
|
3660
4717
|
}
|
|
3661
4718
|
),
|
|
3662
4719
|
// ── Job Automation Tools ──────────────────────────────────────
|
|
3663
|
-
|
|
4720
|
+
tool2(
|
|
3664
4721
|
"job_run",
|
|
3665
4722
|
"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
4723
|
{
|
|
3667
|
-
job_name:
|
|
3668
|
-
"Name of the job to run (e.g. 'software-engineer', 'Frontend Dev')"
|
|
3669
|
-
)
|
|
4724
|
+
job_name: z2.string().describe("Name of the job to run (e.g. 'software-engineer', 'Frontend Dev')")
|
|
3670
4725
|
},
|
|
3671
4726
|
async (args) => {
|
|
3672
|
-
|
|
3673
|
-
return {
|
|
3674
|
-
content: [{ type: "text", text: "Not authenticated. Cannot run job." }]
|
|
3675
|
-
};
|
|
3676
|
-
}
|
|
3677
|
-
const runner = new JobRunner(userId);
|
|
4727
|
+
const runner = new JobRunner();
|
|
3678
4728
|
const job = await runner.loadJob(args.job_name);
|
|
3679
4729
|
if (!job) {
|
|
3680
4730
|
const jobs = await runner.listJobs();
|
|
@@ -3685,10 +4735,12 @@ ${content}`;
|
|
|
3685
4735
|
}
|
|
3686
4736
|
if (job.skills.length === 0) {
|
|
3687
4737
|
return {
|
|
3688
|
-
content: [
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
4738
|
+
content: [
|
|
4739
|
+
{
|
|
4740
|
+
type: "text",
|
|
4741
|
+
text: `Job "${args.job_name}" has no linked skills. Use skill_generate to add skills to this job.`
|
|
4742
|
+
}
|
|
4743
|
+
]
|
|
3692
4744
|
};
|
|
3693
4745
|
}
|
|
3694
4746
|
const runId = await runner.createRun(job.jobId, {
|
|
@@ -3700,51 +4752,55 @@ ${content}`;
|
|
|
3700
4752
|
log.debug("Failed to create job run record, proceeding without tracking");
|
|
3701
4753
|
}
|
|
3702
4754
|
const prompt = runner.buildJobPrompt(job, runId || "untracked");
|
|
3703
|
-
log.info(
|
|
4755
|
+
log.info(
|
|
4756
|
+
`Job "${args.job_name}" started with ${job.skills.length} skills (run: ${runId?.slice(0, 8) || "untracked"})`
|
|
4757
|
+
);
|
|
3704
4758
|
return {
|
|
3705
4759
|
content: [{ type: "text", text: prompt }]
|
|
3706
4760
|
};
|
|
3707
4761
|
}
|
|
3708
4762
|
),
|
|
3709
|
-
|
|
4763
|
+
tool2(
|
|
3710
4764
|
"job_schedule",
|
|
3711
4765
|
"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
4766
|
{
|
|
3713
|
-
job_name:
|
|
3714
|
-
cron:
|
|
4767
|
+
job_name: z2.string().describe("Name of the job to schedule"),
|
|
4768
|
+
cron: z2.string().describe(
|
|
3715
4769
|
"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
4770
|
),
|
|
3717
|
-
timezone:
|
|
3718
|
-
schedule_name:
|
|
4771
|
+
timezone: z2.string().optional().describe("Timezone (default: UTC). Examples: 'UTC', 'America/New_York'"),
|
|
4772
|
+
schedule_name: z2.string().optional().describe("Custom name for this schedule (default: 'Job: <job_name>')")
|
|
3719
4773
|
},
|
|
3720
4774
|
async (args) => {
|
|
3721
|
-
|
|
3722
|
-
return {
|
|
3723
|
-
content: [{ type: "text", text: "Not authenticated. Cannot schedule job." }]
|
|
3724
|
-
};
|
|
3725
|
-
}
|
|
3726
|
-
const runner = new JobRunner(userId);
|
|
4775
|
+
const runner = new JobRunner();
|
|
3727
4776
|
const job = await runner.loadJob(args.job_name);
|
|
3728
4777
|
if (!job) {
|
|
3729
4778
|
return {
|
|
3730
|
-
content: [
|
|
4779
|
+
content: [
|
|
4780
|
+
{
|
|
4781
|
+
type: "text",
|
|
4782
|
+
text: `Job "${args.job_name}" not found. Create it first with skill_generate.`
|
|
4783
|
+
}
|
|
4784
|
+
]
|
|
3731
4785
|
};
|
|
3732
4786
|
}
|
|
3733
4787
|
try {
|
|
3734
4788
|
getNextRunTime(args.cron, args.timezone || "UTC");
|
|
3735
4789
|
} catch {
|
|
3736
4790
|
return {
|
|
3737
|
-
content: [
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
4791
|
+
content: [
|
|
4792
|
+
{
|
|
4793
|
+
type: "text",
|
|
4794
|
+
text: `Invalid cron expression: "${args.cron}". Use format: "minute hour day-of-month month day-of-week"`
|
|
4795
|
+
}
|
|
4796
|
+
]
|
|
3741
4797
|
};
|
|
3742
4798
|
}
|
|
3743
4799
|
const name = args.schedule_name || `Job: ${args.job_name}`;
|
|
3744
4800
|
const prompt = `[JobRun: ${args.job_name}] Run the "${args.job_name}" job. Use job_run to execute it.`;
|
|
3745
4801
|
const tz = args.timezone || "UTC";
|
|
3746
4802
|
try {
|
|
3747
|
-
const task = await createScheduledTask(
|
|
4803
|
+
const task = await createScheduledTask(name, prompt, args.cron, tz);
|
|
3748
4804
|
await callMcpHandler("schedule.link_job", {
|
|
3749
4805
|
task_id: task.id,
|
|
3750
4806
|
job_id: job.jobId
|
|
@@ -3772,36 +4828,35 @@ ${content}`;
|
|
|
3772
4828
|
return { content: [{ type: "text", text: response }] };
|
|
3773
4829
|
} catch (err) {
|
|
3774
4830
|
return {
|
|
3775
|
-
content: [
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
4831
|
+
content: [
|
|
4832
|
+
{
|
|
4833
|
+
type: "text",
|
|
4834
|
+
text: `Failed to schedule job: ${err instanceof Error ? err.message : err}`
|
|
4835
|
+
}
|
|
4836
|
+
]
|
|
3779
4837
|
};
|
|
3780
4838
|
}
|
|
3781
4839
|
}
|
|
3782
4840
|
),
|
|
3783
|
-
|
|
4841
|
+
tool2(
|
|
3784
4842
|
"job_status",
|
|
3785
4843
|
"Check the status and run history of a job. Shows recent executions, success rates, and details.",
|
|
3786
4844
|
{
|
|
3787
|
-
job_name:
|
|
3788
|
-
limit:
|
|
4845
|
+
job_name: z2.string().optional().describe("Job name to check (omit for all jobs)"),
|
|
4846
|
+
limit: z2.number().optional().describe("Max number of runs to show (default: 5)")
|
|
3789
4847
|
},
|
|
3790
4848
|
async (args) => {
|
|
3791
|
-
|
|
3792
|
-
return {
|
|
3793
|
-
content: [{ type: "text", text: "Not authenticated." }]
|
|
3794
|
-
};
|
|
3795
|
-
}
|
|
3796
|
-
const runner = new JobRunner(userId);
|
|
4849
|
+
const runner = new JobRunner();
|
|
3797
4850
|
if (!args.job_name) {
|
|
3798
4851
|
const jobs = await runner.listJobs();
|
|
3799
4852
|
if (jobs.length === 0) {
|
|
3800
4853
|
return {
|
|
3801
|
-
content: [
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
4854
|
+
content: [
|
|
4855
|
+
{
|
|
4856
|
+
type: "text",
|
|
4857
|
+
text: "No jobs defined. Use skill_generate to create a job from your job description."
|
|
4858
|
+
}
|
|
4859
|
+
]
|
|
3805
4860
|
};
|
|
3806
4861
|
}
|
|
3807
4862
|
let response2 = "## Your Jobs\n\n";
|
|
@@ -3815,10 +4870,12 @@ ${content}`;
|
|
|
3815
4870
|
const runs = await runner.getRunHistory(args.job_name, args.limit || 5);
|
|
3816
4871
|
if (runs.length === 0) {
|
|
3817
4872
|
return {
|
|
3818
|
-
content: [
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
4873
|
+
content: [
|
|
4874
|
+
{
|
|
4875
|
+
type: "text",
|
|
4876
|
+
text: `No runs found for job "${args.job_name}". Use job_run to execute it.`
|
|
4877
|
+
}
|
|
4878
|
+
]
|
|
3822
4879
|
};
|
|
3823
4880
|
}
|
|
3824
4881
|
let response = `## Job Status: ${args.job_name}
|
|
@@ -3847,18 +4904,142 @@ ${content}`;
|
|
|
3847
4904
|
response += "\n";
|
|
3848
4905
|
return { content: [{ type: "text", text: response }] };
|
|
3849
4906
|
}
|
|
4907
|
+
),
|
|
4908
|
+
// ── Credential Tools ──────────────────────────────────────────
|
|
4909
|
+
tool2(
|
|
4910
|
+
"credential_get",
|
|
4911
|
+
"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.",
|
|
4912
|
+
{
|
|
4913
|
+
name: z2.string().describe("Credential name (e.g. 'amazon-login', 'openai-api-key')")
|
|
4914
|
+
},
|
|
4915
|
+
async (args) => {
|
|
4916
|
+
const store = getCredentialStore();
|
|
4917
|
+
const credential = store.getByName(args.name);
|
|
4918
|
+
if (!credential) {
|
|
4919
|
+
const all = store.list();
|
|
4920
|
+
const available = all.length > 0 ? `Available credentials: ${all.map((m) => m.name).join(", ")}` : "No credentials stored yet.";
|
|
4921
|
+
return {
|
|
4922
|
+
content: [
|
|
4923
|
+
{
|
|
4924
|
+
type: "text",
|
|
4925
|
+
text: `Credential "${args.name}" not found. ${available}
|
|
4926
|
+
Use ask_user to request it from the user, or create it yourself (e.g. register an account), then store with credential_set.`
|
|
4927
|
+
}
|
|
4928
|
+
]
|
|
4929
|
+
};
|
|
4930
|
+
}
|
|
4931
|
+
log.info(`Credential accessed: "${args.name}" (${credential.meta.type})`);
|
|
4932
|
+
return {
|
|
4933
|
+
content: [
|
|
4934
|
+
{
|
|
4935
|
+
type: "text",
|
|
4936
|
+
text: JSON.stringify({
|
|
4937
|
+
name: credential.meta.name,
|
|
4938
|
+
type: credential.meta.type,
|
|
4939
|
+
data: credential.data,
|
|
4940
|
+
skill: credential.meta.skillName || null
|
|
4941
|
+
})
|
|
4942
|
+
}
|
|
4943
|
+
]
|
|
4944
|
+
};
|
|
4945
|
+
}
|
|
4946
|
+
),
|
|
4947
|
+
tool2(
|
|
4948
|
+
"credential_set",
|
|
4949
|
+
"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.",
|
|
4950
|
+
{
|
|
4951
|
+
name: z2.string().describe("Credential name (lowercase kebab-case, e.g. 'amazon-login')"),
|
|
4952
|
+
type: z2.enum(["api_key", "oauth_token", "login", "secret", "custom"]).describe("Credential type"),
|
|
4953
|
+
data: z2.record(z2.string(), z2.string()).describe(
|
|
4954
|
+
'Key-value pairs (e.g. { "username": "...", "password": "..." } or { "api_key": "..." })'
|
|
4955
|
+
),
|
|
4956
|
+
skill_name: z2.string().optional().describe("Associate with a specific skill"),
|
|
4957
|
+
tags: z2.array(z2.string()).optional().describe("Tags for searchability")
|
|
4958
|
+
},
|
|
4959
|
+
async (args) => {
|
|
4960
|
+
const store = getCredentialStore();
|
|
4961
|
+
const meta = store.save(args.name, args.type, args.data, {
|
|
4962
|
+
skillName: args.skill_name,
|
|
4963
|
+
tags: args.tags
|
|
4964
|
+
});
|
|
4965
|
+
log.info(`Credential stored: "${args.name}" (${args.type})`);
|
|
4966
|
+
return {
|
|
4967
|
+
content: [
|
|
4968
|
+
{
|
|
4969
|
+
type: "text",
|
|
4970
|
+
text: `Credential "${meta.name}" stored locally (type: ${meta.type}, id: ${meta.id}). It is encrypted and will persist across app restarts.`
|
|
4971
|
+
}
|
|
4972
|
+
]
|
|
4973
|
+
};
|
|
4974
|
+
}
|
|
4975
|
+
),
|
|
4976
|
+
tool2(
|
|
4977
|
+
"credential_list",
|
|
4978
|
+
"List all locally stored credentials (metadata only, no secrets). Use this to check what credentials are available before executing a skill.",
|
|
4979
|
+
{
|
|
4980
|
+
skill_name: z2.string().optional().describe("Filter by skill name"),
|
|
4981
|
+
type: z2.string().optional().describe("Filter by credential type")
|
|
4982
|
+
},
|
|
4983
|
+
async (args) => {
|
|
4984
|
+
const store = getCredentialStore();
|
|
4985
|
+
let results = store.list();
|
|
4986
|
+
if (args.skill_name) {
|
|
4987
|
+
results = results.filter((m) => m.skillName === args.skill_name);
|
|
4988
|
+
}
|
|
4989
|
+
if (args.type) {
|
|
4990
|
+
results = results.filter((m) => m.type === args.type);
|
|
4991
|
+
}
|
|
4992
|
+
if (results.length === 0) {
|
|
4993
|
+
const filter = args.skill_name ? ` for skill "${args.skill_name}"` : "";
|
|
4994
|
+
return {
|
|
4995
|
+
content: [{ type: "text", text: `No credentials found${filter}.` }]
|
|
4996
|
+
};
|
|
4997
|
+
}
|
|
4998
|
+
let response = "## Stored Credentials\n\n";
|
|
4999
|
+
for (const m of results) {
|
|
5000
|
+
const skill = m.skillName ? ` [${m.skillName}]` : "";
|
|
5001
|
+
const tags = m.tags.length > 0 ? ` (${m.tags.join(", ")})` : "";
|
|
5002
|
+
response += `- **${m.name}** (${m.type})${skill}${tags}
|
|
5003
|
+
`;
|
|
5004
|
+
}
|
|
5005
|
+
return { content: [{ type: "text", text: response }] };
|
|
5006
|
+
}
|
|
5007
|
+
),
|
|
5008
|
+
tool2(
|
|
5009
|
+
"credential_remove",
|
|
5010
|
+
"Remove a locally stored credential by name.",
|
|
5011
|
+
{
|
|
5012
|
+
name: z2.string().describe("Credential name to remove")
|
|
5013
|
+
},
|
|
5014
|
+
async (args) => {
|
|
5015
|
+
const store = getCredentialStore();
|
|
5016
|
+
const removed = store.removeByName(args.name);
|
|
5017
|
+
if (!removed) {
|
|
5018
|
+
return {
|
|
5019
|
+
content: [{ type: "text", text: `Credential "${args.name}" not found.` }]
|
|
5020
|
+
};
|
|
5021
|
+
}
|
|
5022
|
+
log.info(`Credential removed: "${args.name}"`);
|
|
5023
|
+
return {
|
|
5024
|
+
content: [
|
|
5025
|
+
{ type: "text", text: `Credential "${args.name}" removed from local storage.` }
|
|
5026
|
+
]
|
|
5027
|
+
};
|
|
5028
|
+
}
|
|
3850
5029
|
)
|
|
3851
5030
|
]
|
|
3852
5031
|
});
|
|
3853
5032
|
}
|
|
3854
|
-
async function saveJobToDb(
|
|
5033
|
+
async function saveJobToDb(jobName, jobDescription, createdSkillNames) {
|
|
3855
5034
|
try {
|
|
3856
5035
|
const data = await callMcpHandler("job.save_with_skills", {
|
|
3857
5036
|
job_name: jobName,
|
|
3858
5037
|
job_description: jobDescription,
|
|
3859
5038
|
skill_names: createdSkillNames
|
|
3860
5039
|
});
|
|
3861
|
-
log.debug(
|
|
5040
|
+
log.debug(
|
|
5041
|
+
`Job "${jobName}" saved via edge function (id: ${data}), ${createdSkillNames.length} skill(s) linked`
|
|
5042
|
+
);
|
|
3862
5043
|
} catch (err) {
|
|
3863
5044
|
log.debug(`saveJobToDb error: ${err}`);
|
|
3864
5045
|
}
|
|
@@ -3913,7 +5094,7 @@ function createEventHooks(taskId, toolCallRecords) {
|
|
|
3913
5094
|
};
|
|
3914
5095
|
}
|
|
3915
5096
|
|
|
3916
|
-
// src/agent/
|
|
5097
|
+
// src/agent/system-prompt.ts
|
|
3917
5098
|
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
5099
|
|
|
3919
5100
|
KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This means:
|
|
@@ -3928,7 +5109,28 @@ KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This
|
|
|
3928
5109
|
|
|
3929
5110
|
Available capabilities:
|
|
3930
5111
|
1. BROWSER CONTROL (user's real Chrome via CDP):
|
|
3931
|
-
|
|
5112
|
+
**PREFERRED workflow \u2014 Snapshot + Act (ref-based):**
|
|
5113
|
+
- browser_snapshot \u2192 takes a screenshot and discovers all interactive elements with numbered refs
|
|
5114
|
+
Returns a ref table (text) + screenshot (image). The ref table is your PRIMARY context for element identification.
|
|
5115
|
+
Use annotate=true only on simple pages (few elements) where visual badge overlay helps.
|
|
5116
|
+
- browser_act \u2192 execute actions using ref numbers: click, type, select, press, scroll, wait
|
|
5117
|
+
- This is MORE RELIABLE than CSS selectors because:
|
|
5118
|
+
(a) The ref table gives you role, name, and type for every interactive element \u2014 no guessing
|
|
5119
|
+
(b) Refs use stable semantic resolution (role + accessible name) that survives DOM changes
|
|
5120
|
+
(c) Actions use CDP Input events (real mouse/keyboard) instead of JavaScript \u2014 works with all frameworks
|
|
5121
|
+
(d) You can batch multiple actions in one call \u2014 fewer round-trips
|
|
5122
|
+
- Example workflow:
|
|
5123
|
+
1. browser_snapshot \u2192 ref table shows [1] button "Next", [2] textbox "Email", [3] combobox "Month"
|
|
5124
|
+
2. browser_act actions=[{action:"type", ref:2, text:"user@example.com"}, {action:"select", ref:3, option:"March"}, {action:"click", ref:1}] screenshot=true
|
|
5125
|
+
- Refs persist across actions unless the page navigates. Re-snapshot after navigation or major DOM changes.
|
|
5126
|
+
|
|
5127
|
+
**Legacy tools (still available, use when refs don't work):**
|
|
5128
|
+
- browser_click, browser_type, browser_select, browser_get_elements, browser_screenshot, browser_evaluate
|
|
5129
|
+
- browser_click supports :contains('text') pseudo-selectors
|
|
5130
|
+
- browser_select handles native and custom dropdowns
|
|
5131
|
+
|
|
5132
|
+
**Other browser tools:**
|
|
5133
|
+
- browser_connect, browser_navigate, browser_read_page, browser_list_tabs, browser_switch_tab, browser_new_tab
|
|
3932
5134
|
- If auth is needed: use browser_request_user_action to ask the user to log in
|
|
3933
5135
|
|
|
3934
5136
|
2. FILE OPERATIONS & SHELL:
|
|
@@ -3982,18 +5184,29 @@ Available capabilities:
|
|
|
3982
5184
|
Workflow for web tasks (e.g. "\u67E5\u4E00\u4E0B kindle \u6700\u65B0\u6B3E\u4EF7\u683C"):
|
|
3983
5185
|
1. browser_connect \u2192 connect to user's Chrome
|
|
3984
5186
|
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
|
|
5187
|
+
3. browser_navigate \u2192 go to the website (login pages are auto-detected)
|
|
5188
|
+
4. browser_snapshot \u2192 get ref table + screenshot (use annotate=true for simple pages)
|
|
5189
|
+
5. browser_act \u2192 interact using refs (type, click, select, etc.), set screenshot=true to see result
|
|
5190
|
+
6. Repeat 4-5 as needed (re-snapshot after navigation or major page changes)
|
|
3989
5191
|
7. Summarize findings
|
|
3990
5192
|
|
|
5193
|
+
Workflow for form filling (e.g. "\u6CE8\u518C\u4E00\u4E2A Gmail \u8D26\u53F7"):
|
|
5194
|
+
1. browser_connect + browser_navigate \u2192 go to the form page
|
|
5195
|
+
2. browser_snapshot \u2192 see all form fields with ref numbers
|
|
5196
|
+
3. browser_act \u2192 batch fill multiple fields + click submit in ONE call:
|
|
5197
|
+
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
|
|
5198
|
+
4. Check the screenshot \u2014 if validation errors appear, re-snapshot and fix
|
|
5199
|
+
5. When a username/email is taken, append a random 4-digit suffix and retry
|
|
5200
|
+
|
|
3991
5201
|
Guidelines:
|
|
3992
5202
|
- Always use the real browser for web tasks, never try to fetch URLs programmatically
|
|
3993
|
-
-
|
|
3994
|
-
- Use
|
|
5203
|
+
- ALWAYS use browser_snapshot as your primary way to understand a page \u2014 the ref table gives actionable refs, the screenshot gives visual context
|
|
5204
|
+
- Use browser_act to batch multiple actions \u2014 fill an entire form in one call instead of individual clicks/types
|
|
5205
|
+
- Only re-snapshot when: (a) the page navigated, (b) significant DOM changes occurred, (c) an action failed with "ref not found"
|
|
5206
|
+
- Refs are semantically stable (resolved by role + name), so they often survive minor DOM updates
|
|
3995
5207
|
- Login pages are auto-detected after navigation \u2014 the user is prompted and sessions are saved automatically
|
|
3996
5208
|
- If auto-detection misses a login page, use browser_request_user_action manually
|
|
5209
|
+
- Fall back to legacy tools (browser_click, browser_type, browser_evaluate) only when refs don't work
|
|
3997
5210
|
- Be thorough: check multiple sources when comparing prices/products
|
|
3998
5211
|
- Summarize results clearly at the end
|
|
3999
5212
|
- When you learn something about the user (preferences, habits), use memory_store to remember it
|
|
@@ -4008,21 +5221,21 @@ CRITICAL \u2014 Ask before you guess:
|
|
|
4008
5221
|
- After receiving the answer, store it with memory_store if it is likely to be useful in future conversations.
|
|
4009
5222
|
|
|
4010
5223
|
Workspace path: {workspace_path}`;
|
|
5224
|
+
|
|
5225
|
+
// src/agent/processor.ts
|
|
4011
5226
|
var MAX_HISTORY_ENTRIES = 10;
|
|
4012
5227
|
var MAX_RESPONSE_LENGTH = 1500;
|
|
4013
5228
|
var TaskProcessor = class {
|
|
4014
5229
|
memoryManager = null;
|
|
4015
5230
|
skillManager;
|
|
4016
|
-
userId = null;
|
|
4017
5231
|
sessionId = null;
|
|
4018
5232
|
/** In-memory conversation history, keyed by conversation_id */
|
|
4019
5233
|
historyCache = /* @__PURE__ */ new Map();
|
|
4020
5234
|
constructor() {
|
|
4021
5235
|
this.skillManager = new SkillManager();
|
|
4022
5236
|
}
|
|
4023
|
-
|
|
4024
|
-
this.
|
|
4025
|
-
this.memoryManager = new MemoryManager(userId);
|
|
5237
|
+
init(userId) {
|
|
5238
|
+
this.memoryManager = new MemoryManager();
|
|
4026
5239
|
this.skillManager.setUserId(userId);
|
|
4027
5240
|
this.skillManager.loadFromDb().catch((err) => {
|
|
4028
5241
|
log.debug(`DB skill load deferred: ${err}`);
|
|
@@ -4097,8 +5310,7 @@ var TaskProcessor = class {
|
|
|
4097
5310
|
memoryManager: this.memoryManager,
|
|
4098
5311
|
skillManager: this.skillManager,
|
|
4099
5312
|
taskId: task.id,
|
|
4100
|
-
sessionId: this.sessionId || void 0
|
|
4101
|
-
userId: this.userId || void 0
|
|
5313
|
+
sessionId: this.sessionId || void 0
|
|
4102
5314
|
});
|
|
4103
5315
|
const eventHooks = createEventHooks(task.id, toolCallRecords);
|
|
4104
5316
|
const allowedTools = [
|
|
@@ -4127,7 +5339,12 @@ var TaskProcessor = class {
|
|
|
4127
5339
|
// Job automation tools
|
|
4128
5340
|
"mcp__assistme-agent__job_run",
|
|
4129
5341
|
"mcp__assistme-agent__job_schedule",
|
|
4130
|
-
"mcp__assistme-agent__job_status"
|
|
5342
|
+
"mcp__assistme-agent__job_status",
|
|
5343
|
+
// Credential tools (local storage)
|
|
5344
|
+
"mcp__assistme-agent__credential_get",
|
|
5345
|
+
"mcp__assistme-agent__credential_set",
|
|
5346
|
+
"mcp__assistme-agent__credential_list",
|
|
5347
|
+
"mcp__assistme-agent__credential_remove"
|
|
4131
5348
|
];
|
|
4132
5349
|
async function* promptMessages() {
|
|
4133
5350
|
yield {
|
|
@@ -4238,7 +5455,9 @@ var TaskProcessor = class {
|
|
|
4238
5455
|
}
|
|
4239
5456
|
this.historyCache.set(task.conversation_id, convHistory);
|
|
4240
5457
|
if (agentSessionId) {
|
|
4241
|
-
this.evaluateSkillPostTask(agentSessionId, config.model).catch(
|
|
5458
|
+
this.evaluateSkillPostTask(agentSessionId, config.model).catch(
|
|
5459
|
+
(err) => log.debug(`Post-task skill evaluation skipped: ${err}`)
|
|
5460
|
+
);
|
|
4242
5461
|
}
|
|
4243
5462
|
} catch (err) {
|
|
4244
5463
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -4261,10 +5480,7 @@ var TaskProcessor = class {
|
|
|
4261
5480
|
|
|
4262
5481
|
// src/commands/start.ts
|
|
4263
5482
|
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);
|
|
5483
|
+
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
5484
|
}
|
|
4269
5485
|
async function runAgent(opts) {
|
|
4270
5486
|
if (opts.verbose) {
|
|
@@ -4277,26 +5493,10 @@ async function runAgent(opts) {
|
|
|
4277
5493
|
setConfig("sessionName", opts.name);
|
|
4278
5494
|
}
|
|
4279
5495
|
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
|
-
);
|
|
5496
|
+
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"));
|
|
5497
|
+
console.log(chalk4.bold.cyan(" \u2551 AssistMe CLI Agent \u2551"));
|
|
5498
|
+
console.log(chalk4.bold.cyan(" \u2551 AI that controls your real browser \u2551"));
|
|
5499
|
+
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
5500
|
console.log();
|
|
4301
5501
|
let userId;
|
|
4302
5502
|
try {
|
|
@@ -4313,9 +5513,7 @@ async function runAgent(opts) {
|
|
|
4313
5513
|
launchSpinner.succeed("Browser detected (CDP port 9222)");
|
|
4314
5514
|
break;
|
|
4315
5515
|
case "launched":
|
|
4316
|
-
launchSpinner.succeed(
|
|
4317
|
-
"Browser launched with remote debugging (debug profile)"
|
|
4318
|
-
);
|
|
5516
|
+
launchSpinner.succeed("Browser launched with remote debugging (debug profile)");
|
|
4319
5517
|
break;
|
|
4320
5518
|
}
|
|
4321
5519
|
} else {
|
|
@@ -4326,9 +5524,7 @@ async function runAgent(opts) {
|
|
|
4326
5524
|
break;
|
|
4327
5525
|
case "port_conflict":
|
|
4328
5526
|
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
|
-
);
|
|
5527
|
+
log.info(launchResult.detail ?? "Stop the conflicting process or use a different port.");
|
|
4332
5528
|
break;
|
|
4333
5529
|
default:
|
|
4334
5530
|
launchSpinner.fail("Failed to start Chrome with remote debugging");
|
|
@@ -4338,14 +5534,12 @@ async function runAgent(opts) {
|
|
|
4338
5534
|
if (launchResult.chromePath) {
|
|
4339
5535
|
log.info(`Chrome binary: ${launchResult.chromePath}`);
|
|
4340
5536
|
}
|
|
4341
|
-
log.info(
|
|
4342
|
-
"Browser will be auto-launched when the first task needs it."
|
|
4343
|
-
);
|
|
5537
|
+
log.info("Browser will be auto-launched when the first task needs it.");
|
|
4344
5538
|
break;
|
|
4345
5539
|
}
|
|
4346
5540
|
}
|
|
4347
5541
|
const processor = new TaskProcessor();
|
|
4348
|
-
processor.
|
|
5542
|
+
processor.init(userId);
|
|
4349
5543
|
const sessionManager = new SessionManager();
|
|
4350
5544
|
const browserRef = getBrowser();
|
|
4351
5545
|
const shutdown = async () => {
|
|
@@ -4407,9 +5601,7 @@ async function runAgent(opts) {
|
|
|
4407
5601
|
});
|
|
4408
5602
|
rl.on("close", shutdown);
|
|
4409
5603
|
} catch (err) {
|
|
4410
|
-
log.error(
|
|
4411
|
-
`Failed to start: ${err instanceof Error ? err.message : err}`
|
|
4412
|
-
);
|
|
5604
|
+
log.error(`Failed to start: ${err instanceof Error ? err.message : err}`);
|
|
4413
5605
|
process.exit(1);
|
|
4414
5606
|
}
|
|
4415
5607
|
}
|
|
@@ -4453,13 +5645,7 @@ function registerStatusCommand(program2) {
|
|
|
4453
5645
|
import chalk6 from "chalk";
|
|
4454
5646
|
function registerScheduleCommands(program2) {
|
|
4455
5647
|
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) => {
|
|
5648
|
+
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
5649
|
try {
|
|
4464
5650
|
const cronParts = opts.cron.trim().split(/\s+/);
|
|
4465
5651
|
if (cronParts.length !== 5) {
|
|
@@ -4469,14 +5655,8 @@ function registerScheduleCommands(program2) {
|
|
|
4469
5655
|
console.log(' Examples: "0 9 * * *" (daily 9am), "*/15 * * * *" (every 15 min)');
|
|
4470
5656
|
process.exit(1);
|
|
4471
5657
|
}
|
|
4472
|
-
|
|
4473
|
-
const task = await createScheduledTask(
|
|
4474
|
-
userId,
|
|
4475
|
-
opts.name,
|
|
4476
|
-
opts.prompt,
|
|
4477
|
-
opts.cron,
|
|
4478
|
-
opts.timezone
|
|
4479
|
-
);
|
|
5658
|
+
await getCurrentUserId();
|
|
5659
|
+
const task = await createScheduledTask(opts.name, opts.prompt, opts.cron, opts.timezone);
|
|
4480
5660
|
log.success(`Scheduled task created: ${task.name}`);
|
|
4481
5661
|
console.log(` ID: ${task.id.slice(0, 8)}...`);
|
|
4482
5662
|
console.log(` Cron: ${task.cron_expression}`);
|
|
@@ -4490,8 +5670,8 @@ function registerScheduleCommands(program2) {
|
|
|
4490
5670
|
});
|
|
4491
5671
|
scheduleCmd.command("list").description("List all scheduled tasks").action(async () => {
|
|
4492
5672
|
try {
|
|
4493
|
-
|
|
4494
|
-
const tasks = await listScheduledTasks(
|
|
5673
|
+
await getCurrentUserId();
|
|
5674
|
+
const tasks = await listScheduledTasks();
|
|
4495
5675
|
if (tasks.length === 0) {
|
|
4496
5676
|
console.log(chalk6.yellow("No scheduled tasks."));
|
|
4497
5677
|
console.log('Run "assistme schedule add" to create one.');
|
|
@@ -4501,22 +5681,14 @@ function registerScheduleCommands(program2) {
|
|
|
4501
5681
|
for (const t of tasks) {
|
|
4502
5682
|
const icon = t.enabled ? chalk6.green("\u25CF") : chalk6.dim("\u25CB");
|
|
4503
5683
|
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
|
-
);
|
|
5684
|
+
console.log(` Cron: ${t.cron_expression} (${t.timezone})`);
|
|
5685
|
+
console.log(` Prompt: ${t.prompt.slice(0, 60)}${t.prompt.length > 60 ? "..." : ""}`);
|
|
4510
5686
|
console.log(` Runs: ${t.run_count}`);
|
|
4511
5687
|
if (t.next_run_at) {
|
|
4512
|
-
console.log(
|
|
4513
|
-
` Next run: ${new Date(t.next_run_at).toLocaleString()}`
|
|
4514
|
-
);
|
|
5688
|
+
console.log(` Next run: ${new Date(t.next_run_at).toLocaleString()}`);
|
|
4515
5689
|
}
|
|
4516
5690
|
if (t.last_error) {
|
|
4517
|
-
console.log(
|
|
4518
|
-
chalk6.red(` Error: ${t.last_error.slice(0, 80)}`)
|
|
4519
|
-
);
|
|
5691
|
+
console.log(chalk6.red(` Error: ${t.last_error.slice(0, 80)}`));
|
|
4520
5692
|
}
|
|
4521
5693
|
console.log();
|
|
4522
5694
|
}
|
|
@@ -4527,8 +5699,8 @@ function registerScheduleCommands(program2) {
|
|
|
4527
5699
|
});
|
|
4528
5700
|
scheduleCmd.command("toggle <id>").description("Enable/disable a scheduled task").action(async (id) => {
|
|
4529
5701
|
try {
|
|
4530
|
-
|
|
4531
|
-
const tasks = await listScheduledTasks(
|
|
5702
|
+
await getCurrentUserId();
|
|
5703
|
+
const tasks = await listScheduledTasks();
|
|
4532
5704
|
const task = tasks.find((t) => t.id.startsWith(id));
|
|
4533
5705
|
if (!task) {
|
|
4534
5706
|
log.error(`Task not found: ${id}`);
|
|
@@ -4543,8 +5715,8 @@ function registerScheduleCommands(program2) {
|
|
|
4543
5715
|
});
|
|
4544
5716
|
scheduleCmd.command("remove <id>").description("Delete a scheduled task").action(async (id) => {
|
|
4545
5717
|
try {
|
|
4546
|
-
|
|
4547
|
-
const tasks = await listScheduledTasks(
|
|
5718
|
+
await getCurrentUserId();
|
|
5719
|
+
const tasks = await listScheduledTasks();
|
|
4548
5720
|
const task = tasks.find((t) => t.id.startsWith(id));
|
|
4549
5721
|
if (!task) {
|
|
4550
5722
|
log.error(`Task not found: ${id}`);
|
|
@@ -4565,17 +5737,12 @@ function registerMemoryCommands(program2) {
|
|
|
4565
5737
|
const memoryCmd = program2.command("memory").description("Manage the agent's memory about you");
|
|
4566
5738
|
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
5739
|
try {
|
|
4568
|
-
|
|
4569
|
-
const mm = new MemoryManager(
|
|
4570
|
-
const memories = await mm.list(
|
|
4571
|
-
opts.category,
|
|
4572
|
-
parseInt(opts.limit || "20")
|
|
4573
|
-
);
|
|
5740
|
+
await getCurrentUserId();
|
|
5741
|
+
const mm = new MemoryManager();
|
|
5742
|
+
const memories = await mm.list(opts.category, parseInt(opts.limit || "20"));
|
|
4574
5743
|
if (memories.length === 0) {
|
|
4575
5744
|
console.log(chalk7.yellow("No memories stored yet."));
|
|
4576
|
-
console.log(
|
|
4577
|
-
"The agent will automatically remember things as you interact with it."
|
|
4578
|
-
);
|
|
5745
|
+
console.log("The agent will automatically remember things as you interact with it.");
|
|
4579
5746
|
return;
|
|
4580
5747
|
}
|
|
4581
5748
|
console.log(chalk7.bold(`
|
|
@@ -4602,8 +5769,8 @@ Memories (${memories.length}):`));
|
|
|
4602
5769
|
});
|
|
4603
5770
|
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
5771
|
try {
|
|
4605
|
-
|
|
4606
|
-
const mm = new MemoryManager(
|
|
5772
|
+
await getCurrentUserId();
|
|
5773
|
+
const mm = new MemoryManager();
|
|
4607
5774
|
const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [];
|
|
4608
5775
|
const mem = await mm.add(
|
|
4609
5776
|
content,
|
|
@@ -4611,9 +5778,7 @@ Memories (${memories.length}):`));
|
|
|
4611
5778
|
parseInt(opts.importance || "5"),
|
|
4612
5779
|
tags
|
|
4613
5780
|
);
|
|
4614
|
-
log.success(
|
|
4615
|
-
`Memory stored: "${mem.content.slice(0, 60)}..." [${mem.category}]`
|
|
4616
|
-
);
|
|
5781
|
+
log.success(`Memory stored: "${mem.content.slice(0, 60)}..." [${mem.category}]`);
|
|
4617
5782
|
} catch (err) {
|
|
4618
5783
|
log.error(`${err instanceof Error ? err.message : err}`);
|
|
4619
5784
|
process.exit(1);
|
|
@@ -4621,8 +5786,8 @@ Memories (${memories.length}):`));
|
|
|
4621
5786
|
});
|
|
4622
5787
|
memoryCmd.command("search <query>").description("Search memories").action(async (query3) => {
|
|
4623
5788
|
try {
|
|
4624
|
-
|
|
4625
|
-
const mm = new MemoryManager(
|
|
5789
|
+
await getCurrentUserId();
|
|
5790
|
+
const mm = new MemoryManager();
|
|
4626
5791
|
const results = await mm.search(query3);
|
|
4627
5792
|
if (results.length === 0) {
|
|
4628
5793
|
console.log(chalk7.yellow(`No memories matching "${query3}"`));
|
|
@@ -4641,8 +5806,8 @@ Search results for "${query3}":`));
|
|
|
4641
5806
|
});
|
|
4642
5807
|
memoryCmd.command("remove <id>").description("Delete a specific memory").action(async (id) => {
|
|
4643
5808
|
try {
|
|
4644
|
-
|
|
4645
|
-
const mm = new MemoryManager(
|
|
5809
|
+
await getCurrentUserId();
|
|
5810
|
+
const mm = new MemoryManager();
|
|
4646
5811
|
const memories = await mm.list();
|
|
4647
5812
|
const mem = memories.find((m) => m.id.startsWith(id));
|
|
4648
5813
|
if (!mem) {
|
|
@@ -4658,12 +5823,10 @@ Search results for "${query3}":`));
|
|
|
4658
5823
|
});
|
|
4659
5824
|
memoryCmd.command("clear").description("Clear all memories").option("-c, --category <category>", "Only clear specific category").action(async (opts) => {
|
|
4660
5825
|
try {
|
|
4661
|
-
|
|
4662
|
-
const mm = new MemoryManager(
|
|
5826
|
+
await getCurrentUserId();
|
|
5827
|
+
const mm = new MemoryManager();
|
|
4663
5828
|
await mm.clear(opts.category);
|
|
4664
|
-
log.success(
|
|
4665
|
-
`Memories cleared${opts.category ? ` (${opts.category})` : ""}`
|
|
4666
|
-
);
|
|
5829
|
+
log.success(`Memories cleared${opts.category ? ` (${opts.category})` : ""}`);
|
|
4667
5830
|
} catch (err) {
|
|
4668
5831
|
log.error(`${err instanceof Error ? err.message : err}`);
|
|
4669
5832
|
process.exit(1);
|
|
@@ -4801,21 +5964,17 @@ function registerJobCommands(program2) {
|
|
|
4801
5964
|
jobCmd.command("list").description("List your defined jobs").action(async () => {
|
|
4802
5965
|
try {
|
|
4803
5966
|
const userId = await getCurrentUserId();
|
|
4804
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
4805
|
-
const runner = new JobRunner2(
|
|
5967
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-P2L6MOOX.js");
|
|
5968
|
+
const runner = new JobRunner2();
|
|
4806
5969
|
const jobs = await runner.listJobs();
|
|
4807
5970
|
if (jobs.length === 0) {
|
|
4808
5971
|
console.log(chalk9.yellow("No jobs defined."));
|
|
4809
|
-
console.log(
|
|
4810
|
-
'Use "assistme" and tell the agent about your job to generate skills.'
|
|
4811
|
-
);
|
|
5972
|
+
console.log('Use "assistme" and tell the agent about your job to generate skills.');
|
|
4812
5973
|
return;
|
|
4813
5974
|
}
|
|
4814
5975
|
console.log(chalk9.bold("\nYour Jobs:"));
|
|
4815
5976
|
for (const job of jobs) {
|
|
4816
|
-
console.log(
|
|
4817
|
-
` ${chalk9.cyan(job.name)} (${job.skillCount} skills)`
|
|
4818
|
-
);
|
|
5977
|
+
console.log(` ${chalk9.cyan(job.name)} (${job.skillCount} skills)`);
|
|
4819
5978
|
console.log(
|
|
4820
5979
|
` ${job.description.slice(0, 80)}${job.description.length > 80 ? "..." : ""}`
|
|
4821
5980
|
);
|
|
@@ -4829,38 +5988,23 @@ function registerJobCommands(program2) {
|
|
|
4829
5988
|
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
5989
|
try {
|
|
4831
5990
|
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
|
-
);
|
|
5991
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-P2L6MOOX.js");
|
|
5992
|
+
const runner = new JobRunner2();
|
|
5993
|
+
const runs = await runner.getRunHistory(name, parseInt(opts.limit || "5"));
|
|
4838
5994
|
if (runs.length === 0) {
|
|
4839
|
-
console.log(
|
|
4840
|
-
chalk9.yellow(
|
|
4841
|
-
name ? `No runs found for "${name}".` : "No job runs yet."
|
|
4842
|
-
)
|
|
4843
|
-
);
|
|
5995
|
+
console.log(chalk9.yellow(name ? `No runs found for "${name}".` : "No job runs yet."));
|
|
4844
5996
|
return;
|
|
4845
5997
|
}
|
|
4846
|
-
console.log(
|
|
4847
|
-
|
|
4848
|
-
`
|
|
4849
|
-
Job Run History${name ? ` \u2014 ${name}` : ""}:`
|
|
4850
|
-
)
|
|
4851
|
-
);
|
|
5998
|
+
console.log(chalk9.bold(`
|
|
5999
|
+
Job Run History${name ? ` \u2014 ${name}` : ""}:`));
|
|
4852
6000
|
for (const run of runs) {
|
|
4853
6001
|
const icon = run.status === "completed" ? chalk9.green("\u25CF") : run.status === "failed" ? chalk9.red("\u25CF") : run.status === "running" ? chalk9.yellow("\u25CF") : chalk9.dim("\u25CB");
|
|
4854
6002
|
const date = new Date(run.startedAt).toLocaleString();
|
|
4855
6003
|
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
|
-
);
|
|
6004
|
+
console.log(` ${icon} ${chalk9.bold(run.jobName)} | ${run.triggerType} | ${date}`);
|
|
4859
6005
|
console.log(` Duration: ${duration}`);
|
|
4860
6006
|
if (run.summary) {
|
|
4861
|
-
console.log(
|
|
4862
|
-
` ${chalk9.dim(run.summary.slice(0, 100))}`
|
|
4863
|
-
);
|
|
6007
|
+
console.log(` ${chalk9.dim(run.summary.slice(0, 100))}`);
|
|
4864
6008
|
}
|
|
4865
6009
|
console.log();
|
|
4866
6010
|
}
|
|
@@ -4883,28 +6027,20 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
|
|
|
4883
6027
|
process.exit(1);
|
|
4884
6028
|
}
|
|
4885
6029
|
const userId = await getCurrentUserId();
|
|
4886
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
4887
|
-
const runner = new JobRunner2(
|
|
6030
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-P2L6MOOX.js");
|
|
6031
|
+
const runner = new JobRunner2();
|
|
4888
6032
|
const job = await runner.loadJob(name);
|
|
4889
6033
|
if (!job) {
|
|
4890
6034
|
log.error(`Job "${name}" not found.`);
|
|
4891
6035
|
const jobs = await runner.listJobs();
|
|
4892
6036
|
if (jobs.length > 0) {
|
|
4893
|
-
console.log(
|
|
4894
|
-
`Available: ${jobs.map((j) => j.name).join(", ")}`
|
|
4895
|
-
);
|
|
6037
|
+
console.log(`Available: ${jobs.map((j) => j.name).join(", ")}`);
|
|
4896
6038
|
}
|
|
4897
6039
|
process.exit(1);
|
|
4898
6040
|
}
|
|
4899
6041
|
const tz = opts.timezone || "UTC";
|
|
4900
6042
|
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
|
-
);
|
|
6043
|
+
const task = await createScheduledTask(`Job: ${name}`, prompt, opts.cron, tz);
|
|
4908
6044
|
await callMcpHandler("schedule.link_job", {
|
|
4909
6045
|
task_id: task.id,
|
|
4910
6046
|
job_id: job.jobId
|
|
@@ -4921,6 +6057,144 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
|
|
|
4921
6057
|
});
|
|
4922
6058
|
}
|
|
4923
6059
|
|
|
6060
|
+
// src/commands/credential.ts
|
|
6061
|
+
import chalk10 from "chalk";
|
|
6062
|
+
import { createInterface as createInterface3 } from "readline";
|
|
6063
|
+
var VALID_TYPES = ["api_key", "oauth_token", "login", "secret", "custom"];
|
|
6064
|
+
function registerCredentialCommands(program2) {
|
|
6065
|
+
const credCmd = program2.command("credential").alias("cred").description("Manage locally stored credentials (encrypted, never sent to server)");
|
|
6066
|
+
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) => {
|
|
6067
|
+
const store = getCredentialStore();
|
|
6068
|
+
let results = store.list();
|
|
6069
|
+
if (opts.skill) {
|
|
6070
|
+
results = results.filter((m) => m.skillName === opts.skill);
|
|
6071
|
+
}
|
|
6072
|
+
if (opts.type) {
|
|
6073
|
+
results = results.filter((m) => m.type === opts.type);
|
|
6074
|
+
}
|
|
6075
|
+
if (results.length === 0) {
|
|
6076
|
+
console.log(chalk10.yellow(" No credentials stored."));
|
|
6077
|
+
console.log(chalk10.dim(" Use `assistme credential set <name>` to add one."));
|
|
6078
|
+
return;
|
|
6079
|
+
}
|
|
6080
|
+
console.log(chalk10.bold("\n Stored Credentials:\n"));
|
|
6081
|
+
for (const m of results) {
|
|
6082
|
+
const skill = m.skillName ? chalk10.dim(` [${m.skillName}]`) : "";
|
|
6083
|
+
const tags = m.tags.length > 0 ? chalk10.dim(` (${m.tags.join(", ")})`) : "";
|
|
6084
|
+
console.log(` ${chalk10.cyan(m.name)} ${chalk10.gray(`(${m.type})`)}${skill}${tags}`);
|
|
6085
|
+
console.log(chalk10.dim(` ID: ${m.id} Created: ${m.createdAt.slice(0, 10)}`));
|
|
6086
|
+
}
|
|
6087
|
+
console.log();
|
|
6088
|
+
});
|
|
6089
|
+
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) => {
|
|
6090
|
+
if (!VALID_TYPES.includes(opts.type)) {
|
|
6091
|
+
log.error(`Invalid type "${opts.type}". Must be one of: ${VALID_TYPES.join(", ")}`);
|
|
6092
|
+
process.exit(1);
|
|
6093
|
+
}
|
|
6094
|
+
const rl = createInterface3({
|
|
6095
|
+
input: process.stdin,
|
|
6096
|
+
output: process.stdout
|
|
6097
|
+
});
|
|
6098
|
+
const ask = (q) => new Promise((resolve2) => {
|
|
6099
|
+
rl.question(q, (answer) => resolve2(answer.trim()));
|
|
6100
|
+
});
|
|
6101
|
+
console.log(chalk10.bold(`
|
|
6102
|
+
Set credential: ${name}`));
|
|
6103
|
+
console.log(chalk10.dim(" Enter key-value pairs. Empty key to finish.\n"));
|
|
6104
|
+
const data = {};
|
|
6105
|
+
if (opts.type === "login") {
|
|
6106
|
+
data.username = await ask(chalk10.cyan(" Username: "));
|
|
6107
|
+
data.password = await ask(chalk10.cyan(" Password: "));
|
|
6108
|
+
} else if (opts.type === "api_key") {
|
|
6109
|
+
data.api_key = await ask(chalk10.cyan(" API Key: "));
|
|
6110
|
+
} else {
|
|
6111
|
+
while (true) {
|
|
6112
|
+
const key = await ask(chalk10.cyan(" Key (empty to finish): "));
|
|
6113
|
+
if (!key) break;
|
|
6114
|
+
const value = await ask(chalk10.cyan(` Value for "${key}": `));
|
|
6115
|
+
data[key] = value;
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
rl.close();
|
|
6119
|
+
if (Object.keys(data).length === 0) {
|
|
6120
|
+
console.log(chalk10.yellow(" No data provided. Credential not saved."));
|
|
6121
|
+
return;
|
|
6122
|
+
}
|
|
6123
|
+
const store = getCredentialStore();
|
|
6124
|
+
const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [];
|
|
6125
|
+
const meta = store.save(name, opts.type, data, {
|
|
6126
|
+
skillName: opts.skill,
|
|
6127
|
+
tags
|
|
6128
|
+
});
|
|
6129
|
+
log.success(`Credential "${meta.name}" saved (ID: ${meta.id})`);
|
|
6130
|
+
console.log(chalk10.dim(" Encrypted and stored at ~/.config/assistme/credentials/"));
|
|
6131
|
+
});
|
|
6132
|
+
credCmd.command("get <name>").description("Show credential metadata (use --reveal to show secrets)").option("-r, --reveal", "Reveal secret values").action(async (name, opts) => {
|
|
6133
|
+
const store = getCredentialStore();
|
|
6134
|
+
const credential = store.getByName(name);
|
|
6135
|
+
if (!credential) {
|
|
6136
|
+
log.error(`Credential not found: ${name}`);
|
|
6137
|
+
process.exit(1);
|
|
6138
|
+
}
|
|
6139
|
+
const m = credential.meta;
|
|
6140
|
+
console.log(chalk10.bold(`
|
|
6141
|
+
${m.name} (${m.type})`));
|
|
6142
|
+
if (m.skillName) console.log(` Skill: ${m.skillName}`);
|
|
6143
|
+
if (m.tags.length > 0) console.log(` Tags: ${m.tags.join(", ")}`);
|
|
6144
|
+
console.log(` Created: ${m.createdAt}`);
|
|
6145
|
+
console.log(` Updated: ${m.updatedAt}`);
|
|
6146
|
+
if (opts.reveal) {
|
|
6147
|
+
console.log(chalk10.bold("\n Data:"));
|
|
6148
|
+
for (const [key, value] of Object.entries(credential.data)) {
|
|
6149
|
+
console.log(` ${chalk10.cyan(key)}: ${value}`);
|
|
6150
|
+
}
|
|
6151
|
+
} else {
|
|
6152
|
+
console.log(chalk10.bold("\n Data keys:"));
|
|
6153
|
+
for (const key of Object.keys(credential.data)) {
|
|
6154
|
+
console.log(` ${chalk10.cyan(key)}: ${"*".repeat(8)}`);
|
|
6155
|
+
}
|
|
6156
|
+
console.log(chalk10.dim("\n Use --reveal to show secret values."));
|
|
6157
|
+
}
|
|
6158
|
+
console.log();
|
|
6159
|
+
});
|
|
6160
|
+
credCmd.command("remove <name>").description("Remove a stored credential").action(async (name) => {
|
|
6161
|
+
const store = getCredentialStore();
|
|
6162
|
+
const removed = store.removeByName(name);
|
|
6163
|
+
if (removed) {
|
|
6164
|
+
log.success(`Credential "${name}" removed.`);
|
|
6165
|
+
} else {
|
|
6166
|
+
log.error(`Credential not found: ${name}`);
|
|
6167
|
+
}
|
|
6168
|
+
});
|
|
6169
|
+
credCmd.command("clear").description("Remove ALL stored credentials").action(async () => {
|
|
6170
|
+
const store = getCredentialStore();
|
|
6171
|
+
const count = store.list().length;
|
|
6172
|
+
if (count === 0) {
|
|
6173
|
+
console.log(chalk10.yellow(" No credentials to clear."));
|
|
6174
|
+
return;
|
|
6175
|
+
}
|
|
6176
|
+
const rl = createInterface3({
|
|
6177
|
+
input: process.stdin,
|
|
6178
|
+
output: process.stdout
|
|
6179
|
+
});
|
|
6180
|
+
const answer = await new Promise((resolve2) => {
|
|
6181
|
+
rl.question(
|
|
6182
|
+
chalk10.red(` Remove ALL ${count} credential(s)? This cannot be undone. (yes/no): `),
|
|
6183
|
+
(ans) => {
|
|
6184
|
+
rl.close();
|
|
6185
|
+
resolve2(ans.trim().toLowerCase());
|
|
6186
|
+
}
|
|
6187
|
+
);
|
|
6188
|
+
});
|
|
6189
|
+
if (answer === "yes" || answer === "y") {
|
|
6190
|
+
store.clear();
|
|
6191
|
+
log.success(`All ${count} credential(s) removed.`);
|
|
6192
|
+
} else {
|
|
6193
|
+
console.log(chalk10.dim(" Cancelled."));
|
|
6194
|
+
}
|
|
6195
|
+
});
|
|
6196
|
+
}
|
|
6197
|
+
|
|
4924
6198
|
// src/index.ts
|
|
4925
6199
|
loadEnv();
|
|
4926
6200
|
var require2 = createRequire(import.meta.url);
|
|
@@ -4936,4 +6210,5 @@ registerScheduleCommands(program);
|
|
|
4936
6210
|
registerMemoryCommands(program);
|
|
4937
6211
|
registerSkillCommands(program);
|
|
4938
6212
|
registerJobCommands(program);
|
|
6213
|
+
registerCredentialCommands(program);
|
|
4939
6214
|
program.parse();
|