assistme 0.3.1 → 0.3.3

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