assistme 0.3.1 → 0.3.2

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