assistme 0.3.0 → 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 +1791 -572
- 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 -1020
- package/src/agent/memory.ts +2 -11
- package/src/agent/processor.ts +18 -108
- package/src/agent/scheduler.ts +2 -3
- package/src/agent/session.ts +20 -36
- package/src/agent/skills.ts +167 -61
- package/src/agent/system-prompt.ts +126 -0
- package/src/browser/chrome-launcher.ts +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
|
},
|
|
@@ -3408,17 +4434,17 @@ ${content}`;
|
|
|
3408
4434
|
response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills.
|
|
3409
4435
|
|
|
3410
4436
|
`;
|
|
3411
|
-
response += `**IMPORTANT \u2014 You MUST use
|
|
4437
|
+
response += `**IMPORTANT \u2014 You MUST use ask_user before creating skills:**
|
|
3412
4438
|
`;
|
|
3413
4439
|
response += `1. Analyze the job and draft a list of proposed skills (name, emoji, one-line description for each).
|
|
3414
4440
|
`;
|
|
3415
|
-
response += `2. Call \`
|
|
4441
|
+
response += `2. Call \`ask_user\` with the formatted skill list as "question" and these options:
|
|
3416
4442
|
`;
|
|
3417
4443
|
response += ` - options: [{label: "Approve All", action_key: "approve_all", description: "Create all proposed skills"}, {label: "Cancel", action_key: "cancel", description: "Do not create any skills"}]
|
|
3418
4444
|
`;
|
|
3419
4445
|
response += `3. WAIT for the response. If action_key is "approve_all", create all skills using \`skill_create\`. If "cancel", stop.
|
|
3420
4446
|
`;
|
|
3421
|
-
response += `4. Do NOT ask for confirmation in text. Do NOT create skills without calling
|
|
4447
|
+
response += `4. Do NOT ask for confirmation in text. Do NOT create skills without calling ask_user first.
|
|
3422
4448
|
|
|
3423
4449
|
`;
|
|
3424
4450
|
response += `For each skill, call \`skill_create\` with:
|
|
@@ -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,163 +4607,124 @@ ${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
|
-
// ── User Interaction
|
|
3589
|
-
|
|
3590
|
-
"
|
|
3591
|
-
"Ask the user a
|
|
4628
|
+
// ── User Interaction Tool ───────────────────────────────────
|
|
4629
|
+
tool2(
|
|
4630
|
+
"ask_user",
|
|
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
|
-
|
|
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)")
|
|
3596
4647
|
},
|
|
3597
4648
|
async (args) => {
|
|
3598
|
-
const actionId = `
|
|
4649
|
+
const actionId = `ask_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3599
4650
|
const timeout = (args.timeout_seconds || 300) * 1e3;
|
|
3600
4651
|
const actionData = {
|
|
3601
4652
|
id: actionId,
|
|
3602
|
-
type: "
|
|
4653
|
+
type: "ask_user",
|
|
3603
4654
|
message: args.question,
|
|
4655
|
+
options: args.options || [],
|
|
3604
4656
|
placeholder: args.placeholder || "",
|
|
3605
4657
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3606
4658
|
};
|
|
3607
4659
|
try {
|
|
3608
4660
|
await setActionRequest(taskId, actionData);
|
|
3609
|
-
log.info(`
|
|
4661
|
+
log.info(`Ask user ${actionId}: "${args.question.slice(0, 80)}..."`);
|
|
3610
4662
|
emitEvent(taskId, "user_action_request", actionData).catch(() => {
|
|
3611
4663
|
});
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
if (response && (!response.action_id || response.action_id === actionId)) {
|
|
3617
|
-
const text = response.text || response.value || "";
|
|
3618
|
-
log.info(`User input received: "${text.slice(0, 80)}"`);
|
|
3619
|
-
return {
|
|
3620
|
-
content: [{
|
|
3621
|
-
type: "text",
|
|
3622
|
-
text: JSON.stringify({ status: "responded", text })
|
|
3623
|
-
}]
|
|
3624
|
-
};
|
|
3625
|
-
}
|
|
3626
|
-
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
3627
|
-
}
|
|
3628
|
-
log.warn(`Input request ${actionId} timed out`);
|
|
3629
|
-
return {
|
|
3630
|
-
content: [{
|
|
3631
|
-
type: "text",
|
|
3632
|
-
text: JSON.stringify({
|
|
3633
|
-
status: "timeout",
|
|
3634
|
-
message: "User did not respond within the timeout period."
|
|
3635
|
-
})
|
|
3636
|
-
}]
|
|
3637
|
-
};
|
|
3638
|
-
} catch (err) {
|
|
3639
|
-
log.error(`request_user_input failed: ${err}`);
|
|
3640
|
-
return {
|
|
3641
|
-
content: [{
|
|
3642
|
-
type: "text",
|
|
3643
|
-
text: `Failed to request user input: ${err instanceof Error ? err.message : err}`
|
|
3644
|
-
}]
|
|
3645
|
-
};
|
|
3646
|
-
}
|
|
3647
|
-
}
|
|
3648
|
-
),
|
|
3649
|
-
tool(
|
|
3650
|
-
"request_user_confirmation",
|
|
3651
|
-
"Pause and ask the user for approval or input via the web UI. Returns the user's chosen action_key. Use this BEFORE creating skills, making irreversible changes, etc. The agent will block until the user responds or the timeout expires.",
|
|
3652
|
-
{
|
|
3653
|
-
message: z.string().describe("What to show the user (supports markdown)"),
|
|
3654
|
-
options: z.array(z.object({
|
|
3655
|
-
label: z.string().describe("Button label shown to user"),
|
|
3656
|
-
action_key: z.string().describe("Machine-readable key returned when selected"),
|
|
3657
|
-
description: z.string().optional().describe("Tooltip/description for this option")
|
|
3658
|
-
})).describe("Buttons/options to show the user"),
|
|
3659
|
-
timeout_seconds: z.number().optional().describe("How long to wait for response (default: 300)")
|
|
3660
|
-
},
|
|
3661
|
-
async (args) => {
|
|
3662
|
-
const actionId = `action_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3663
|
-
const timeout = (args.timeout_seconds || 300) * 1e3;
|
|
3664
|
-
const actionData = {
|
|
3665
|
-
id: actionId,
|
|
3666
|
-
type: "confirmation",
|
|
3667
|
-
message: args.message,
|
|
3668
|
-
options: args.options,
|
|
3669
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3670
|
-
};
|
|
3671
|
-
try {
|
|
3672
|
-
await setActionRequest(taskId, actionData);
|
|
3673
|
-
log.info(`Action request ${actionId} stored in metadata, waiting for user response...`);
|
|
3674
|
-
emitEvent(taskId, "user_action_request", actionData).catch(() => {
|
|
4664
|
+
emitEvent(taskId, "status_change", {
|
|
4665
|
+
status: "waiting_for_user",
|
|
4666
|
+
message: args.question
|
|
4667
|
+
}).catch(() => {
|
|
3675
4668
|
});
|
|
3676
4669
|
const startTime = Date.now();
|
|
3677
4670
|
const pollInterval = 2e3;
|
|
3678
4671
|
while (Date.now() - startTime < timeout) {
|
|
3679
4672
|
const response = await pollActionResponse(taskId);
|
|
3680
4673
|
if (response && (!response.action_id || response.action_id === actionId)) {
|
|
3681
|
-
const actionKey = response.action_key ||
|
|
3682
|
-
const
|
|
3683
|
-
|
|
4674
|
+
const actionKey = response.action_key || "";
|
|
4675
|
+
const text = response.text || "";
|
|
4676
|
+
const label = response.label || actionKey || text;
|
|
4677
|
+
log.info(`User responded: "${label}"`);
|
|
3684
4678
|
return {
|
|
3685
|
-
content: [
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
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
|
+
]
|
|
3693
4690
|
};
|
|
3694
4691
|
}
|
|
3695
4692
|
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
3696
4693
|
}
|
|
3697
|
-
log.warn(`
|
|
4694
|
+
log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
|
|
3698
4695
|
return {
|
|
3699
|
-
content: [
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
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
|
+
]
|
|
3706
4705
|
};
|
|
3707
4706
|
} catch (err) {
|
|
3708
|
-
log.error(`
|
|
4707
|
+
log.error(`ask_user failed: ${err}`);
|
|
3709
4708
|
return {
|
|
3710
|
-
content: [
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
4709
|
+
content: [
|
|
4710
|
+
{
|
|
4711
|
+
type: "text",
|
|
4712
|
+
text: `Failed to ask user: ${err instanceof Error ? err.message : err}`
|
|
4713
|
+
}
|
|
4714
|
+
]
|
|
3714
4715
|
};
|
|
3715
4716
|
}
|
|
3716
4717
|
}
|
|
3717
4718
|
),
|
|
3718
4719
|
// ── Job Automation Tools ──────────────────────────────────────
|
|
3719
|
-
|
|
4720
|
+
tool2(
|
|
3720
4721
|
"job_run",
|
|
3721
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.",
|
|
3722
4723
|
{
|
|
3723
|
-
job_name:
|
|
3724
|
-
"Name of the job to run (e.g. 'software-engineer', 'Frontend Dev')"
|
|
3725
|
-
)
|
|
4724
|
+
job_name: z2.string().describe("Name of the job to run (e.g. 'software-engineer', 'Frontend Dev')")
|
|
3726
4725
|
},
|
|
3727
4726
|
async (args) => {
|
|
3728
|
-
|
|
3729
|
-
return {
|
|
3730
|
-
content: [{ type: "text", text: "Not authenticated. Cannot run job." }]
|
|
3731
|
-
};
|
|
3732
|
-
}
|
|
3733
|
-
const runner = new JobRunner(userId);
|
|
4727
|
+
const runner = new JobRunner();
|
|
3734
4728
|
const job = await runner.loadJob(args.job_name);
|
|
3735
4729
|
if (!job) {
|
|
3736
4730
|
const jobs = await runner.listJobs();
|
|
@@ -3741,10 +4735,12 @@ ${content}`;
|
|
|
3741
4735
|
}
|
|
3742
4736
|
if (job.skills.length === 0) {
|
|
3743
4737
|
return {
|
|
3744
|
-
content: [
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
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
|
+
]
|
|
3748
4744
|
};
|
|
3749
4745
|
}
|
|
3750
4746
|
const runId = await runner.createRun(job.jobId, {
|
|
@@ -3756,51 +4752,55 @@ ${content}`;
|
|
|
3756
4752
|
log.debug("Failed to create job run record, proceeding without tracking");
|
|
3757
4753
|
}
|
|
3758
4754
|
const prompt = runner.buildJobPrompt(job, runId || "untracked");
|
|
3759
|
-
log.info(
|
|
4755
|
+
log.info(
|
|
4756
|
+
`Job "${args.job_name}" started with ${job.skills.length} skills (run: ${runId?.slice(0, 8) || "untracked"})`
|
|
4757
|
+
);
|
|
3760
4758
|
return {
|
|
3761
4759
|
content: [{ type: "text", text: prompt }]
|
|
3762
4760
|
};
|
|
3763
4761
|
}
|
|
3764
4762
|
),
|
|
3765
|
-
|
|
4763
|
+
tool2(
|
|
3766
4764
|
"job_schedule",
|
|
3767
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.",
|
|
3768
4766
|
{
|
|
3769
|
-
job_name:
|
|
3770
|
-
cron:
|
|
4767
|
+
job_name: z2.string().describe("Name of the job to schedule"),
|
|
4768
|
+
cron: z2.string().describe(
|
|
3771
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)"
|
|
3772
4770
|
),
|
|
3773
|
-
timezone:
|
|
3774
|
-
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>')")
|
|
3775
4773
|
},
|
|
3776
4774
|
async (args) => {
|
|
3777
|
-
|
|
3778
|
-
return {
|
|
3779
|
-
content: [{ type: "text", text: "Not authenticated. Cannot schedule job." }]
|
|
3780
|
-
};
|
|
3781
|
-
}
|
|
3782
|
-
const runner = new JobRunner(userId);
|
|
4775
|
+
const runner = new JobRunner();
|
|
3783
4776
|
const job = await runner.loadJob(args.job_name);
|
|
3784
4777
|
if (!job) {
|
|
3785
4778
|
return {
|
|
3786
|
-
content: [
|
|
4779
|
+
content: [
|
|
4780
|
+
{
|
|
4781
|
+
type: "text",
|
|
4782
|
+
text: `Job "${args.job_name}" not found. Create it first with skill_generate.`
|
|
4783
|
+
}
|
|
4784
|
+
]
|
|
3787
4785
|
};
|
|
3788
4786
|
}
|
|
3789
4787
|
try {
|
|
3790
4788
|
getNextRunTime(args.cron, args.timezone || "UTC");
|
|
3791
4789
|
} catch {
|
|
3792
4790
|
return {
|
|
3793
|
-
content: [
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
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
|
+
]
|
|
3797
4797
|
};
|
|
3798
4798
|
}
|
|
3799
4799
|
const name = args.schedule_name || `Job: ${args.job_name}`;
|
|
3800
4800
|
const prompt = `[JobRun: ${args.job_name}] Run the "${args.job_name}" job. Use job_run to execute it.`;
|
|
3801
4801
|
const tz = args.timezone || "UTC";
|
|
3802
4802
|
try {
|
|
3803
|
-
const task = await createScheduledTask(
|
|
4803
|
+
const task = await createScheduledTask(name, prompt, args.cron, tz);
|
|
3804
4804
|
await callMcpHandler("schedule.link_job", {
|
|
3805
4805
|
task_id: task.id,
|
|
3806
4806
|
job_id: job.jobId
|
|
@@ -3828,36 +4828,35 @@ ${content}`;
|
|
|
3828
4828
|
return { content: [{ type: "text", text: response }] };
|
|
3829
4829
|
} catch (err) {
|
|
3830
4830
|
return {
|
|
3831
|
-
content: [
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
4831
|
+
content: [
|
|
4832
|
+
{
|
|
4833
|
+
type: "text",
|
|
4834
|
+
text: `Failed to schedule job: ${err instanceof Error ? err.message : err}`
|
|
4835
|
+
}
|
|
4836
|
+
]
|
|
3835
4837
|
};
|
|
3836
4838
|
}
|
|
3837
4839
|
}
|
|
3838
4840
|
),
|
|
3839
|
-
|
|
4841
|
+
tool2(
|
|
3840
4842
|
"job_status",
|
|
3841
4843
|
"Check the status and run history of a job. Shows recent executions, success rates, and details.",
|
|
3842
4844
|
{
|
|
3843
|
-
job_name:
|
|
3844
|
-
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)")
|
|
3845
4847
|
},
|
|
3846
4848
|
async (args) => {
|
|
3847
|
-
|
|
3848
|
-
return {
|
|
3849
|
-
content: [{ type: "text", text: "Not authenticated." }]
|
|
3850
|
-
};
|
|
3851
|
-
}
|
|
3852
|
-
const runner = new JobRunner(userId);
|
|
4849
|
+
const runner = new JobRunner();
|
|
3853
4850
|
if (!args.job_name) {
|
|
3854
4851
|
const jobs = await runner.listJobs();
|
|
3855
4852
|
if (jobs.length === 0) {
|
|
3856
4853
|
return {
|
|
3857
|
-
content: [
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
4854
|
+
content: [
|
|
4855
|
+
{
|
|
4856
|
+
type: "text",
|
|
4857
|
+
text: "No jobs defined. Use skill_generate to create a job from your job description."
|
|
4858
|
+
}
|
|
4859
|
+
]
|
|
3861
4860
|
};
|
|
3862
4861
|
}
|
|
3863
4862
|
let response2 = "## Your Jobs\n\n";
|
|
@@ -3871,10 +4870,12 @@ ${content}`;
|
|
|
3871
4870
|
const runs = await runner.getRunHistory(args.job_name, args.limit || 5);
|
|
3872
4871
|
if (runs.length === 0) {
|
|
3873
4872
|
return {
|
|
3874
|
-
content: [
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
4873
|
+
content: [
|
|
4874
|
+
{
|
|
4875
|
+
type: "text",
|
|
4876
|
+
text: `No runs found for job "${args.job_name}". Use job_run to execute it.`
|
|
4877
|
+
}
|
|
4878
|
+
]
|
|
3878
4879
|
};
|
|
3879
4880
|
}
|
|
3880
4881
|
let response = `## Job Status: ${args.job_name}
|
|
@@ -3903,18 +4904,142 @@ ${content}`;
|
|
|
3903
4904
|
response += "\n";
|
|
3904
4905
|
return { content: [{ type: "text", text: response }] };
|
|
3905
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
|
+
}
|
|
3906
5029
|
)
|
|
3907
5030
|
]
|
|
3908
5031
|
});
|
|
3909
5032
|
}
|
|
3910
|
-
async function saveJobToDb(
|
|
5033
|
+
async function saveJobToDb(jobName, jobDescription, createdSkillNames) {
|
|
3911
5034
|
try {
|
|
3912
5035
|
const data = await callMcpHandler("job.save_with_skills", {
|
|
3913
5036
|
job_name: jobName,
|
|
3914
5037
|
job_description: jobDescription,
|
|
3915
5038
|
skill_names: createdSkillNames
|
|
3916
5039
|
});
|
|
3917
|
-
log.debug(
|
|
5040
|
+
log.debug(
|
|
5041
|
+
`Job "${jobName}" saved via edge function (id: ${data}), ${createdSkillNames.length} skill(s) linked`
|
|
5042
|
+
);
|
|
3918
5043
|
} catch (err) {
|
|
3919
5044
|
log.debug(`saveJobToDb error: ${err}`);
|
|
3920
5045
|
}
|
|
@@ -3969,7 +5094,7 @@ function createEventHooks(taskId, toolCallRecords) {
|
|
|
3969
5094
|
};
|
|
3970
5095
|
}
|
|
3971
5096
|
|
|
3972
|
-
// src/agent/
|
|
5097
|
+
// src/agent/system-prompt.ts
|
|
3973
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.
|
|
3974
5099
|
|
|
3975
5100
|
KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This means:
|
|
@@ -3984,7 +5109,28 @@ KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This
|
|
|
3984
5109
|
|
|
3985
5110
|
Available capabilities:
|
|
3986
5111
|
1. BROWSER CONTROL (user's real Chrome via CDP):
|
|
3987
|
-
|
|
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
|
|
3988
5134
|
- If auth is needed: use browser_request_user_action to ask the user to log in
|
|
3989
5135
|
|
|
3990
5136
|
2. FILE OPERATIONS & SHELL:
|
|
@@ -4021,7 +5167,7 @@ Available capabilities:
|
|
|
4021
5167
|
|
|
4022
5168
|
5. JOB AUTOMATION:
|
|
4023
5169
|
- When the user describes their job/role/daily work, use skill_generate to decompose it into automatable skills
|
|
4024
|
-
- ALWAYS use
|
|
5170
|
+
- ALWAYS use ask_user to get user approval before creating skills \u2014 never create skills without approval
|
|
4025
5171
|
- Use job_run to start a job \u2014 it gives you the job's goal and available skills as capabilities
|
|
4026
5172
|
- When running a job, be AGENTIC: decide dynamically what to do based on what you discover
|
|
4027
5173
|
- Do NOT follow a fixed sequence \u2014 if checking Slack reveals a task that needs GitHub, go do GitHub immediately
|
|
@@ -4038,46 +5184,58 @@ Available capabilities:
|
|
|
4038
5184
|
Workflow for web tasks (e.g. "\u67E5\u4E00\u4E0B kindle \u6700\u65B0\u6B3E\u4EF7\u683C"):
|
|
4039
5185
|
1. browser_connect \u2192 connect to user's Chrome
|
|
4040
5186
|
2. browser_new_tab \u2192 open a new tab
|
|
4041
|
-
3. browser_navigate \u2192 go to the website (login pages are auto-detected
|
|
4042
|
-
4.
|
|
4043
|
-
5.
|
|
4044
|
-
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)
|
|
4045
5191
|
7. Summarize findings
|
|
4046
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
|
+
|
|
4047
5201
|
Guidelines:
|
|
4048
5202
|
- Always use the real browser for web tasks, never try to fetch URLs programmatically
|
|
4049
|
-
-
|
|
4050
|
-
- 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
|
|
4051
5207
|
- Login pages are auto-detected after navigation \u2014 the user is prompted and sessions are saved automatically
|
|
4052
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
|
|
4053
5210
|
- Be thorough: check multiple sources when comparing prices/products
|
|
4054
5211
|
- Summarize results clearly at the end
|
|
4055
5212
|
- When you learn something about the user (preferences, habits), use memory_store to remember it
|
|
4056
5213
|
|
|
4057
5214
|
CRITICAL \u2014 Ask before you guess:
|
|
4058
|
-
- Before executing a task, verify you have all required information. If anything is ambiguous or missing, use
|
|
5215
|
+
- Before executing a task, verify you have all required information. If anything is ambiguous or missing, use ask_user to ask.
|
|
4059
5216
|
- First try to resolve unknowns yourself: check memories, read workspace files (e.g. git remote, config files), or infer from conversation history.
|
|
4060
|
-
- If you still lack a critical piece of information after self-resolution, ASK the user via
|
|
5217
|
+
- If you still lack a critical piece of information after self-resolution, ASK the user via ask_user. Do NOT guess, assume defaults, or proceed with incomplete information.
|
|
5218
|
+
- When asking, provide suggested options as buttons whenever possible \u2014 the user can always type a custom answer instead.
|
|
4061
5219
|
- Examples of when to ask: which account/repo/project to target, what format the user wants, which of multiple options to choose, credentials or URLs that cannot be inferred.
|
|
4062
5220
|
- Keep questions specific and actionable. Explain what you already know and what exactly you need.
|
|
4063
5221
|
- After receiving the answer, store it with memory_store if it is likely to be useful in future conversations.
|
|
4064
5222
|
|
|
4065
5223
|
Workspace path: {workspace_path}`;
|
|
5224
|
+
|
|
5225
|
+
// src/agent/processor.ts
|
|
4066
5226
|
var MAX_HISTORY_ENTRIES = 10;
|
|
4067
5227
|
var MAX_RESPONSE_LENGTH = 1500;
|
|
4068
5228
|
var TaskProcessor = class {
|
|
4069
5229
|
memoryManager = null;
|
|
4070
5230
|
skillManager;
|
|
4071
|
-
userId = null;
|
|
4072
5231
|
sessionId = null;
|
|
4073
5232
|
/** In-memory conversation history, keyed by conversation_id */
|
|
4074
5233
|
historyCache = /* @__PURE__ */ new Map();
|
|
4075
5234
|
constructor() {
|
|
4076
5235
|
this.skillManager = new SkillManager();
|
|
4077
5236
|
}
|
|
4078
|
-
|
|
4079
|
-
this.
|
|
4080
|
-
this.memoryManager = new MemoryManager(userId);
|
|
5237
|
+
init(userId) {
|
|
5238
|
+
this.memoryManager = new MemoryManager();
|
|
4081
5239
|
this.skillManager.setUserId(userId);
|
|
4082
5240
|
this.skillManager.loadFromDb().catch((err) => {
|
|
4083
5241
|
log.debug(`DB skill load deferred: ${err}`);
|
|
@@ -4152,8 +5310,7 @@ var TaskProcessor = class {
|
|
|
4152
5310
|
memoryManager: this.memoryManager,
|
|
4153
5311
|
skillManager: this.skillManager,
|
|
4154
5312
|
taskId: task.id,
|
|
4155
|
-
sessionId: this.sessionId || void 0
|
|
4156
|
-
userId: this.userId || void 0
|
|
5313
|
+
sessionId: this.sessionId || void 0
|
|
4157
5314
|
});
|
|
4158
5315
|
const eventHooks = createEventHooks(task.id, toolCallRecords);
|
|
4159
5316
|
const allowedTools = [
|
|
@@ -4178,12 +5335,16 @@ var TaskProcessor = class {
|
|
|
4178
5335
|
"mcp__assistme-agent__skill_add",
|
|
4179
5336
|
"mcp__assistme-agent__skill_publish",
|
|
4180
5337
|
// User interaction
|
|
4181
|
-
"mcp__assistme-
|
|
4182
|
-
"mcp__assistme-agent__request_user_confirmation",
|
|
5338
|
+
"mcp__assistme-agent__ask_user",
|
|
4183
5339
|
// Job automation tools
|
|
4184
5340
|
"mcp__assistme-agent__job_run",
|
|
4185
5341
|
"mcp__assistme-agent__job_schedule",
|
|
4186
|
-
"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"
|
|
4187
5348
|
];
|
|
4188
5349
|
async function* promptMessages() {
|
|
4189
5350
|
yield {
|
|
@@ -4294,7 +5455,9 @@ var TaskProcessor = class {
|
|
|
4294
5455
|
}
|
|
4295
5456
|
this.historyCache.set(task.conversation_id, convHistory);
|
|
4296
5457
|
if (agentSessionId) {
|
|
4297
|
-
this.evaluateSkillPostTask(agentSessionId, config.model).catch(
|
|
5458
|
+
this.evaluateSkillPostTask(agentSessionId, config.model).catch(
|
|
5459
|
+
(err) => log.debug(`Post-task skill evaluation skipped: ${err}`)
|
|
5460
|
+
);
|
|
4298
5461
|
}
|
|
4299
5462
|
} catch (err) {
|
|
4300
5463
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -4317,10 +5480,7 @@ var TaskProcessor = class {
|
|
|
4317
5480
|
|
|
4318
5481
|
// src/commands/start.ts
|
|
4319
5482
|
function registerStartCommand(program2) {
|
|
4320
|
-
program2.command("start", { isDefault: true, hidden: true }).description("Start the agent (default command)").option(
|
|
4321
|
-
"-w, --workspace <path>",
|
|
4322
|
-
"Workspace path (default: current directory)"
|
|
4323
|
-
).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);
|
|
4324
5484
|
}
|
|
4325
5485
|
async function runAgent(opts) {
|
|
4326
5486
|
if (opts.verbose) {
|
|
@@ -4333,26 +5493,10 @@ async function runAgent(opts) {
|
|
|
4333
5493
|
setConfig("sessionName", opts.name);
|
|
4334
5494
|
}
|
|
4335
5495
|
console.log();
|
|
4336
|
-
console.log(
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
);
|
|
4341
|
-
console.log(
|
|
4342
|
-
chalk4.bold.cyan(
|
|
4343
|
-
" \u2551 AssistMe CLI Agent \u2551"
|
|
4344
|
-
)
|
|
4345
|
-
);
|
|
4346
|
-
console.log(
|
|
4347
|
-
chalk4.bold.cyan(
|
|
4348
|
-
" \u2551 AI that controls your real browser \u2551"
|
|
4349
|
-
)
|
|
4350
|
-
);
|
|
4351
|
-
console.log(
|
|
4352
|
-
chalk4.bold.cyan(
|
|
4353
|
-
" \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"
|
|
4354
|
-
)
|
|
4355
|
-
);
|
|
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"));
|
|
4356
5500
|
console.log();
|
|
4357
5501
|
let userId;
|
|
4358
5502
|
try {
|
|
@@ -4369,9 +5513,7 @@ async function runAgent(opts) {
|
|
|
4369
5513
|
launchSpinner.succeed("Browser detected (CDP port 9222)");
|
|
4370
5514
|
break;
|
|
4371
5515
|
case "launched":
|
|
4372
|
-
launchSpinner.succeed(
|
|
4373
|
-
"Browser launched with remote debugging (debug profile)"
|
|
4374
|
-
);
|
|
5516
|
+
launchSpinner.succeed("Browser launched with remote debugging (debug profile)");
|
|
4375
5517
|
break;
|
|
4376
5518
|
}
|
|
4377
5519
|
} else {
|
|
@@ -4382,9 +5524,7 @@ async function runAgent(opts) {
|
|
|
4382
5524
|
break;
|
|
4383
5525
|
case "port_conflict":
|
|
4384
5526
|
launchSpinner.fail("Port 9222 is in use by another process");
|
|
4385
|
-
log.info(
|
|
4386
|
-
launchResult.detail ?? "Stop the conflicting process or use a different port."
|
|
4387
|
-
);
|
|
5527
|
+
log.info(launchResult.detail ?? "Stop the conflicting process or use a different port.");
|
|
4388
5528
|
break;
|
|
4389
5529
|
default:
|
|
4390
5530
|
launchSpinner.fail("Failed to start Chrome with remote debugging");
|
|
@@ -4394,14 +5534,12 @@ async function runAgent(opts) {
|
|
|
4394
5534
|
if (launchResult.chromePath) {
|
|
4395
5535
|
log.info(`Chrome binary: ${launchResult.chromePath}`);
|
|
4396
5536
|
}
|
|
4397
|
-
log.info(
|
|
4398
|
-
"Browser will be auto-launched when the first task needs it."
|
|
4399
|
-
);
|
|
5537
|
+
log.info("Browser will be auto-launched when the first task needs it.");
|
|
4400
5538
|
break;
|
|
4401
5539
|
}
|
|
4402
5540
|
}
|
|
4403
5541
|
const processor = new TaskProcessor();
|
|
4404
|
-
processor.
|
|
5542
|
+
processor.init(userId);
|
|
4405
5543
|
const sessionManager = new SessionManager();
|
|
4406
5544
|
const browserRef = getBrowser();
|
|
4407
5545
|
const shutdown = async () => {
|
|
@@ -4463,9 +5601,7 @@ async function runAgent(opts) {
|
|
|
4463
5601
|
});
|
|
4464
5602
|
rl.on("close", shutdown);
|
|
4465
5603
|
} catch (err) {
|
|
4466
|
-
log.error(
|
|
4467
|
-
`Failed to start: ${err instanceof Error ? err.message : err}`
|
|
4468
|
-
);
|
|
5604
|
+
log.error(`Failed to start: ${err instanceof Error ? err.message : err}`);
|
|
4469
5605
|
process.exit(1);
|
|
4470
5606
|
}
|
|
4471
5607
|
}
|
|
@@ -4509,13 +5645,7 @@ function registerStatusCommand(program2) {
|
|
|
4509
5645
|
import chalk6 from "chalk";
|
|
4510
5646
|
function registerScheduleCommands(program2) {
|
|
4511
5647
|
const scheduleCmd = program2.command("schedule").description("Manage scheduled (cron) tasks");
|
|
4512
|
-
scheduleCmd.command("add").description("Add a scheduled task").requiredOption("-n, --name <name>", "Task name").requiredOption(
|
|
4513
|
-
"-p, --prompt <prompt>",
|
|
4514
|
-
"Task prompt (what the AI should do)"
|
|
4515
|
-
).requiredOption(
|
|
4516
|
-
"-c, --cron <expression>",
|
|
4517
|
-
"Cron expression (e.g. '0 8 * * *' for daily 8am)"
|
|
4518
|
-
).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) => {
|
|
4519
5649
|
try {
|
|
4520
5650
|
const cronParts = opts.cron.trim().split(/\s+/);
|
|
4521
5651
|
if (cronParts.length !== 5) {
|
|
@@ -4525,14 +5655,8 @@ function registerScheduleCommands(program2) {
|
|
|
4525
5655
|
console.log(' Examples: "0 9 * * *" (daily 9am), "*/15 * * * *" (every 15 min)');
|
|
4526
5656
|
process.exit(1);
|
|
4527
5657
|
}
|
|
4528
|
-
|
|
4529
|
-
const task = await createScheduledTask(
|
|
4530
|
-
userId,
|
|
4531
|
-
opts.name,
|
|
4532
|
-
opts.prompt,
|
|
4533
|
-
opts.cron,
|
|
4534
|
-
opts.timezone
|
|
4535
|
-
);
|
|
5658
|
+
await getCurrentUserId();
|
|
5659
|
+
const task = await createScheduledTask(opts.name, opts.prompt, opts.cron, opts.timezone);
|
|
4536
5660
|
log.success(`Scheduled task created: ${task.name}`);
|
|
4537
5661
|
console.log(` ID: ${task.id.slice(0, 8)}...`);
|
|
4538
5662
|
console.log(` Cron: ${task.cron_expression}`);
|
|
@@ -4546,8 +5670,8 @@ function registerScheduleCommands(program2) {
|
|
|
4546
5670
|
});
|
|
4547
5671
|
scheduleCmd.command("list").description("List all scheduled tasks").action(async () => {
|
|
4548
5672
|
try {
|
|
4549
|
-
|
|
4550
|
-
const tasks = await listScheduledTasks(
|
|
5673
|
+
await getCurrentUserId();
|
|
5674
|
+
const tasks = await listScheduledTasks();
|
|
4551
5675
|
if (tasks.length === 0) {
|
|
4552
5676
|
console.log(chalk6.yellow("No scheduled tasks."));
|
|
4553
5677
|
console.log('Run "assistme schedule add" to create one.');
|
|
@@ -4557,22 +5681,14 @@ function registerScheduleCommands(program2) {
|
|
|
4557
5681
|
for (const t of tasks) {
|
|
4558
5682
|
const icon = t.enabled ? chalk6.green("\u25CF") : chalk6.dim("\u25CB");
|
|
4559
5683
|
console.log(` ${icon} ${t.name} (${t.id.slice(0, 8)}...)`);
|
|
4560
|
-
console.log(
|
|
4561
|
-
|
|
4562
|
-
);
|
|
4563
|
-
console.log(
|
|
4564
|
-
` Prompt: ${t.prompt.slice(0, 60)}${t.prompt.length > 60 ? "..." : ""}`
|
|
4565
|
-
);
|
|
5684
|
+
console.log(` Cron: ${t.cron_expression} (${t.timezone})`);
|
|
5685
|
+
console.log(` Prompt: ${t.prompt.slice(0, 60)}${t.prompt.length > 60 ? "..." : ""}`);
|
|
4566
5686
|
console.log(` Runs: ${t.run_count}`);
|
|
4567
5687
|
if (t.next_run_at) {
|
|
4568
|
-
console.log(
|
|
4569
|
-
` Next run: ${new Date(t.next_run_at).toLocaleString()}`
|
|
4570
|
-
);
|
|
5688
|
+
console.log(` Next run: ${new Date(t.next_run_at).toLocaleString()}`);
|
|
4571
5689
|
}
|
|
4572
5690
|
if (t.last_error) {
|
|
4573
|
-
console.log(
|
|
4574
|
-
chalk6.red(` Error: ${t.last_error.slice(0, 80)}`)
|
|
4575
|
-
);
|
|
5691
|
+
console.log(chalk6.red(` Error: ${t.last_error.slice(0, 80)}`));
|
|
4576
5692
|
}
|
|
4577
5693
|
console.log();
|
|
4578
5694
|
}
|
|
@@ -4583,8 +5699,8 @@ function registerScheduleCommands(program2) {
|
|
|
4583
5699
|
});
|
|
4584
5700
|
scheduleCmd.command("toggle <id>").description("Enable/disable a scheduled task").action(async (id) => {
|
|
4585
5701
|
try {
|
|
4586
|
-
|
|
4587
|
-
const tasks = await listScheduledTasks(
|
|
5702
|
+
await getCurrentUserId();
|
|
5703
|
+
const tasks = await listScheduledTasks();
|
|
4588
5704
|
const task = tasks.find((t) => t.id.startsWith(id));
|
|
4589
5705
|
if (!task) {
|
|
4590
5706
|
log.error(`Task not found: ${id}`);
|
|
@@ -4599,8 +5715,8 @@ function registerScheduleCommands(program2) {
|
|
|
4599
5715
|
});
|
|
4600
5716
|
scheduleCmd.command("remove <id>").description("Delete a scheduled task").action(async (id) => {
|
|
4601
5717
|
try {
|
|
4602
|
-
|
|
4603
|
-
const tasks = await listScheduledTasks(
|
|
5718
|
+
await getCurrentUserId();
|
|
5719
|
+
const tasks = await listScheduledTasks();
|
|
4604
5720
|
const task = tasks.find((t) => t.id.startsWith(id));
|
|
4605
5721
|
if (!task) {
|
|
4606
5722
|
log.error(`Task not found: ${id}`);
|
|
@@ -4621,17 +5737,12 @@ function registerMemoryCommands(program2) {
|
|
|
4621
5737
|
const memoryCmd = program2.command("memory").description("Manage the agent's memory about you");
|
|
4622
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) => {
|
|
4623
5739
|
try {
|
|
4624
|
-
|
|
4625
|
-
const mm = new MemoryManager(
|
|
4626
|
-
const memories = await mm.list(
|
|
4627
|
-
opts.category,
|
|
4628
|
-
parseInt(opts.limit || "20")
|
|
4629
|
-
);
|
|
5740
|
+
await getCurrentUserId();
|
|
5741
|
+
const mm = new MemoryManager();
|
|
5742
|
+
const memories = await mm.list(opts.category, parseInt(opts.limit || "20"));
|
|
4630
5743
|
if (memories.length === 0) {
|
|
4631
5744
|
console.log(chalk7.yellow("No memories stored yet."));
|
|
4632
|
-
console.log(
|
|
4633
|
-
"The agent will automatically remember things as you interact with it."
|
|
4634
|
-
);
|
|
5745
|
+
console.log("The agent will automatically remember things as you interact with it.");
|
|
4635
5746
|
return;
|
|
4636
5747
|
}
|
|
4637
5748
|
console.log(chalk7.bold(`
|
|
@@ -4658,8 +5769,8 @@ Memories (${memories.length}):`));
|
|
|
4658
5769
|
});
|
|
4659
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) => {
|
|
4660
5771
|
try {
|
|
4661
|
-
|
|
4662
|
-
const mm = new MemoryManager(
|
|
5772
|
+
await getCurrentUserId();
|
|
5773
|
+
const mm = new MemoryManager();
|
|
4663
5774
|
const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [];
|
|
4664
5775
|
const mem = await mm.add(
|
|
4665
5776
|
content,
|
|
@@ -4667,9 +5778,7 @@ Memories (${memories.length}):`));
|
|
|
4667
5778
|
parseInt(opts.importance || "5"),
|
|
4668
5779
|
tags
|
|
4669
5780
|
);
|
|
4670
|
-
log.success(
|
|
4671
|
-
`Memory stored: "${mem.content.slice(0, 60)}..." [${mem.category}]`
|
|
4672
|
-
);
|
|
5781
|
+
log.success(`Memory stored: "${mem.content.slice(0, 60)}..." [${mem.category}]`);
|
|
4673
5782
|
} catch (err) {
|
|
4674
5783
|
log.error(`${err instanceof Error ? err.message : err}`);
|
|
4675
5784
|
process.exit(1);
|
|
@@ -4677,8 +5786,8 @@ Memories (${memories.length}):`));
|
|
|
4677
5786
|
});
|
|
4678
5787
|
memoryCmd.command("search <query>").description("Search memories").action(async (query3) => {
|
|
4679
5788
|
try {
|
|
4680
|
-
|
|
4681
|
-
const mm = new MemoryManager(
|
|
5789
|
+
await getCurrentUserId();
|
|
5790
|
+
const mm = new MemoryManager();
|
|
4682
5791
|
const results = await mm.search(query3);
|
|
4683
5792
|
if (results.length === 0) {
|
|
4684
5793
|
console.log(chalk7.yellow(`No memories matching "${query3}"`));
|
|
@@ -4697,8 +5806,8 @@ Search results for "${query3}":`));
|
|
|
4697
5806
|
});
|
|
4698
5807
|
memoryCmd.command("remove <id>").description("Delete a specific memory").action(async (id) => {
|
|
4699
5808
|
try {
|
|
4700
|
-
|
|
4701
|
-
const mm = new MemoryManager(
|
|
5809
|
+
await getCurrentUserId();
|
|
5810
|
+
const mm = new MemoryManager();
|
|
4702
5811
|
const memories = await mm.list();
|
|
4703
5812
|
const mem = memories.find((m) => m.id.startsWith(id));
|
|
4704
5813
|
if (!mem) {
|
|
@@ -4714,12 +5823,10 @@ Search results for "${query3}":`));
|
|
|
4714
5823
|
});
|
|
4715
5824
|
memoryCmd.command("clear").description("Clear all memories").option("-c, --category <category>", "Only clear specific category").action(async (opts) => {
|
|
4716
5825
|
try {
|
|
4717
|
-
|
|
4718
|
-
const mm = new MemoryManager(
|
|
5826
|
+
await getCurrentUserId();
|
|
5827
|
+
const mm = new MemoryManager();
|
|
4719
5828
|
await mm.clear(opts.category);
|
|
4720
|
-
log.success(
|
|
4721
|
-
`Memories cleared${opts.category ? ` (${opts.category})` : ""}`
|
|
4722
|
-
);
|
|
5829
|
+
log.success(`Memories cleared${opts.category ? ` (${opts.category})` : ""}`);
|
|
4723
5830
|
} catch (err) {
|
|
4724
5831
|
log.error(`${err instanceof Error ? err.message : err}`);
|
|
4725
5832
|
process.exit(1);
|
|
@@ -4857,21 +5964,17 @@ function registerJobCommands(program2) {
|
|
|
4857
5964
|
jobCmd.command("list").description("List your defined jobs").action(async () => {
|
|
4858
5965
|
try {
|
|
4859
5966
|
const userId = await getCurrentUserId();
|
|
4860
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
4861
|
-
const runner = new JobRunner2(
|
|
5967
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-P2L6MOOX.js");
|
|
5968
|
+
const runner = new JobRunner2();
|
|
4862
5969
|
const jobs = await runner.listJobs();
|
|
4863
5970
|
if (jobs.length === 0) {
|
|
4864
5971
|
console.log(chalk9.yellow("No jobs defined."));
|
|
4865
|
-
console.log(
|
|
4866
|
-
'Use "assistme" and tell the agent about your job to generate skills.'
|
|
4867
|
-
);
|
|
5972
|
+
console.log('Use "assistme" and tell the agent about your job to generate skills.');
|
|
4868
5973
|
return;
|
|
4869
5974
|
}
|
|
4870
5975
|
console.log(chalk9.bold("\nYour Jobs:"));
|
|
4871
5976
|
for (const job of jobs) {
|
|
4872
|
-
console.log(
|
|
4873
|
-
` ${chalk9.cyan(job.name)} (${job.skillCount} skills)`
|
|
4874
|
-
);
|
|
5977
|
+
console.log(` ${chalk9.cyan(job.name)} (${job.skillCount} skills)`);
|
|
4875
5978
|
console.log(
|
|
4876
5979
|
` ${job.description.slice(0, 80)}${job.description.length > 80 ? "..." : ""}`
|
|
4877
5980
|
);
|
|
@@ -4885,38 +5988,23 @@ function registerJobCommands(program2) {
|
|
|
4885
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) => {
|
|
4886
5989
|
try {
|
|
4887
5990
|
const userId = await getCurrentUserId();
|
|
4888
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
4889
|
-
const runner = new JobRunner2(
|
|
4890
|
-
const runs = await runner.getRunHistory(
|
|
4891
|
-
name,
|
|
4892
|
-
parseInt(opts.limit || "5")
|
|
4893
|
-
);
|
|
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"));
|
|
4894
5994
|
if (runs.length === 0) {
|
|
4895
|
-
console.log(
|
|
4896
|
-
chalk9.yellow(
|
|
4897
|
-
name ? `No runs found for "${name}".` : "No job runs yet."
|
|
4898
|
-
)
|
|
4899
|
-
);
|
|
5995
|
+
console.log(chalk9.yellow(name ? `No runs found for "${name}".` : "No job runs yet."));
|
|
4900
5996
|
return;
|
|
4901
5997
|
}
|
|
4902
|
-
console.log(
|
|
4903
|
-
|
|
4904
|
-
`
|
|
4905
|
-
Job Run History${name ? ` \u2014 ${name}` : ""}:`
|
|
4906
|
-
)
|
|
4907
|
-
);
|
|
5998
|
+
console.log(chalk9.bold(`
|
|
5999
|
+
Job Run History${name ? ` \u2014 ${name}` : ""}:`));
|
|
4908
6000
|
for (const run of runs) {
|
|
4909
6001
|
const icon = run.status === "completed" ? chalk9.green("\u25CF") : run.status === "failed" ? chalk9.red("\u25CF") : run.status === "running" ? chalk9.yellow("\u25CF") : chalk9.dim("\u25CB");
|
|
4910
6002
|
const date = new Date(run.startedAt).toLocaleString();
|
|
4911
6003
|
const duration = run.completedAt ? `${Math.round((new Date(run.completedAt).getTime() - new Date(run.startedAt).getTime()) / 1e3)}s` : "in progress";
|
|
4912
|
-
console.log(
|
|
4913
|
-
` ${icon} ${chalk9.bold(run.jobName)} | ${run.triggerType} | ${date}`
|
|
4914
|
-
);
|
|
6004
|
+
console.log(` ${icon} ${chalk9.bold(run.jobName)} | ${run.triggerType} | ${date}`);
|
|
4915
6005
|
console.log(` Duration: ${duration}`);
|
|
4916
6006
|
if (run.summary) {
|
|
4917
|
-
console.log(
|
|
4918
|
-
` ${chalk9.dim(run.summary.slice(0, 100))}`
|
|
4919
|
-
);
|
|
6007
|
+
console.log(` ${chalk9.dim(run.summary.slice(0, 100))}`);
|
|
4920
6008
|
}
|
|
4921
6009
|
console.log();
|
|
4922
6010
|
}
|
|
@@ -4939,28 +6027,20 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
|
|
|
4939
6027
|
process.exit(1);
|
|
4940
6028
|
}
|
|
4941
6029
|
const userId = await getCurrentUserId();
|
|
4942
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
4943
|
-
const runner = new JobRunner2(
|
|
6030
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-P2L6MOOX.js");
|
|
6031
|
+
const runner = new JobRunner2();
|
|
4944
6032
|
const job = await runner.loadJob(name);
|
|
4945
6033
|
if (!job) {
|
|
4946
6034
|
log.error(`Job "${name}" not found.`);
|
|
4947
6035
|
const jobs = await runner.listJobs();
|
|
4948
6036
|
if (jobs.length > 0) {
|
|
4949
|
-
console.log(
|
|
4950
|
-
`Available: ${jobs.map((j) => j.name).join(", ")}`
|
|
4951
|
-
);
|
|
6037
|
+
console.log(`Available: ${jobs.map((j) => j.name).join(", ")}`);
|
|
4952
6038
|
}
|
|
4953
6039
|
process.exit(1);
|
|
4954
6040
|
}
|
|
4955
6041
|
const tz = opts.timezone || "UTC";
|
|
4956
6042
|
const prompt = `[JobRun: ${name}] Run the "${name}" job. Use job_run to execute it.`;
|
|
4957
|
-
const task = await createScheduledTask(
|
|
4958
|
-
userId,
|
|
4959
|
-
`Job: ${name}`,
|
|
4960
|
-
prompt,
|
|
4961
|
-
opts.cron,
|
|
4962
|
-
tz
|
|
4963
|
-
);
|
|
6043
|
+
const task = await createScheduledTask(`Job: ${name}`, prompt, opts.cron, tz);
|
|
4964
6044
|
await callMcpHandler("schedule.link_job", {
|
|
4965
6045
|
task_id: task.id,
|
|
4966
6046
|
job_id: job.jobId
|
|
@@ -4977,6 +6057,144 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
|
|
|
4977
6057
|
});
|
|
4978
6058
|
}
|
|
4979
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
|
+
|
|
4980
6198
|
// src/index.ts
|
|
4981
6199
|
loadEnv();
|
|
4982
6200
|
var require2 = createRequire(import.meta.url);
|
|
@@ -4992,4 +6210,5 @@ registerScheduleCommands(program);
|
|
|
4992
6210
|
registerMemoryCommands(program);
|
|
4993
6211
|
registerSkillCommands(program);
|
|
4994
6212
|
registerJobCommands(program);
|
|
6213
|
+
registerCredentialCommands(program);
|
|
4995
6214
|
program.parse();
|