assistme 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +1791 -572
  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 -1020
  8. package/src/agent/memory.ts +2 -11
  9. package/src/agent/processor.ts +18 -108
  10. package/src/agent/scheduler.ts +2 -3
  11. package/src/agent/session.ts +20 -36
  12. package/src/agent/skills.ts +167 -61
  13. package/src/agent/system-prompt.ts +126 -0
  14. package/src/browser/chrome-launcher.ts +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
  },
@@ -3408,17 +4434,17 @@ ${content}`;
3408
4434
  response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills.
3409
4435
 
3410
4436
  `;
3411
- response += `**IMPORTANT \u2014 You MUST use request_user_confirmation before creating skills:**
4437
+ response += `**IMPORTANT \u2014 You MUST use ask_user before creating skills:**
3412
4438
  `;
3413
4439
  response += `1. Analyze the job and draft a list of proposed skills (name, emoji, one-line description for each).
3414
4440
  `;
3415
- response += `2. Call \`request_user_confirmation\` with the formatted skill list as "message" and these options:
4441
+ response += `2. Call \`ask_user\` with the formatted skill list as "question" and these options:
3416
4442
  `;
3417
4443
  response += ` - options: [{label: "Approve All", action_key: "approve_all", description: "Create all proposed skills"}, {label: "Cancel", action_key: "cancel", description: "Do not create any skills"}]
3418
4444
  `;
3419
4445
  response += `3. WAIT for the response. If action_key is "approve_all", create all skills using \`skill_create\`. If "cancel", stop.
3420
4446
  `;
3421
- response += `4. Do NOT ask for confirmation in text. Do NOT create skills without calling request_user_confirmation first.
4447
+ response += `4. Do NOT ask for confirmation in text. Do NOT create skills without calling ask_user first.
3422
4448
 
3423
4449
  `;
3424
4450
  response += `For each skill, call \`skill_create\` with:
@@ -3453,47 +4479,48 @@ ${content}`;
3453
4479
  return { content: [{ type: "text", text: response }] };
3454
4480
  }
3455
4481
  ),
3456
- 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,163 +4607,124 @@ ${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
- // ── User Interaction Tools ──────────────────────────────────
3589
- tool(
3590
- "request_user_input",
3591
- "Ask the user a clarifying question and wait for their free-text response. Use this when you need information that cannot be inferred from context, memory, or the workspace \u2014 e.g. which account to use, specific preferences, ambiguous instructions, or missing parameters for a skill. Do NOT use this for information you can discover yourself (git remote, file contents, etc.).",
4628
+ // ── User Interaction Tool ───────────────────────────────────
4629
+ tool2(
4630
+ "ask_user",
4631
+ "Ask the user a question via the web UI and wait for their response. Shows a message with optional predefined option buttons PLUS a free-text input field \u2014 the user can either click a suggested option or type a custom answer. ALWAYS provide options when you can suggest likely answers. Do NOT use this for information you can discover yourself (git remote, file contents, etc.).",
3592
4632
  {
3593
- question: z.string().describe("The question to ask the user (supports markdown). Be specific about what you need and why."),
3594
- placeholder: z.string().optional().describe("Placeholder text for the input field (e.g. 'https://github.com/owner/repo')"),
3595
- 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)")
3596
4647
  },
3597
4648
  async (args) => {
3598
- const actionId = `input_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4649
+ const actionId = `ask_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
3599
4650
  const timeout = (args.timeout_seconds || 300) * 1e3;
3600
4651
  const actionData = {
3601
4652
  id: actionId,
3602
- type: "input",
4653
+ type: "ask_user",
3603
4654
  message: args.question,
4655
+ options: args.options || [],
3604
4656
  placeholder: args.placeholder || "",
3605
4657
  created_at: (/* @__PURE__ */ new Date()).toISOString()
3606
4658
  };
3607
4659
  try {
3608
4660
  await setActionRequest(taskId, actionData);
3609
- log.info(`Input request ${actionId}: "${args.question.slice(0, 80)}..."`);
4661
+ log.info(`Ask user ${actionId}: "${args.question.slice(0, 80)}..."`);
3610
4662
  emitEvent(taskId, "user_action_request", actionData).catch(() => {
3611
4663
  });
3612
- const startTime = Date.now();
3613
- const pollInterval = 2e3;
3614
- while (Date.now() - startTime < timeout) {
3615
- const response = await pollActionResponse(taskId);
3616
- if (response && (!response.action_id || response.action_id === actionId)) {
3617
- const text = response.text || response.value || "";
3618
- log.info(`User input received: "${text.slice(0, 80)}"`);
3619
- return {
3620
- content: [{
3621
- type: "text",
3622
- text: JSON.stringify({ status: "responded", text })
3623
- }]
3624
- };
3625
- }
3626
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
3627
- }
3628
- log.warn(`Input request ${actionId} timed out`);
3629
- return {
3630
- content: [{
3631
- type: "text",
3632
- text: JSON.stringify({
3633
- status: "timeout",
3634
- message: "User did not respond within the timeout period."
3635
- })
3636
- }]
3637
- };
3638
- } catch (err) {
3639
- log.error(`request_user_input failed: ${err}`);
3640
- return {
3641
- content: [{
3642
- type: "text",
3643
- text: `Failed to request user input: ${err instanceof Error ? err.message : err}`
3644
- }]
3645
- };
3646
- }
3647
- }
3648
- ),
3649
- tool(
3650
- "request_user_confirmation",
3651
- "Pause and ask the user for approval or input via the web UI. Returns the user's chosen action_key. Use this BEFORE creating skills, making irreversible changes, etc. The agent will block until the user responds or the timeout expires.",
3652
- {
3653
- message: z.string().describe("What to show the user (supports markdown)"),
3654
- options: z.array(z.object({
3655
- label: z.string().describe("Button label shown to user"),
3656
- action_key: z.string().describe("Machine-readable key returned when selected"),
3657
- description: z.string().optional().describe("Tooltip/description for this option")
3658
- })).describe("Buttons/options to show the user"),
3659
- timeout_seconds: z.number().optional().describe("How long to wait for response (default: 300)")
3660
- },
3661
- async (args) => {
3662
- const actionId = `action_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
3663
- const timeout = (args.timeout_seconds || 300) * 1e3;
3664
- const actionData = {
3665
- id: actionId,
3666
- type: "confirmation",
3667
- message: args.message,
3668
- options: args.options,
3669
- created_at: (/* @__PURE__ */ new Date()).toISOString()
3670
- };
3671
- try {
3672
- await setActionRequest(taskId, actionData);
3673
- log.info(`Action request ${actionId} stored in metadata, waiting for user response...`);
3674
- emitEvent(taskId, "user_action_request", actionData).catch(() => {
4664
+ emitEvent(taskId, "status_change", {
4665
+ status: "waiting_for_user",
4666
+ message: args.question
4667
+ }).catch(() => {
3675
4668
  });
3676
4669
  const startTime = Date.now();
3677
4670
  const pollInterval = 2e3;
3678
4671
  while (Date.now() - startTime < timeout) {
3679
4672
  const response = await pollActionResponse(taskId);
3680
4673
  if (response && (!response.action_id || response.action_id === actionId)) {
3681
- const actionKey = response.action_key || response.action || "";
3682
- const label = response.label || actionKey;
3683
- log.info(`User responded: ${label} (${actionKey})`);
4674
+ const actionKey = response.action_key || "";
4675
+ const text = response.text || "";
4676
+ const label = response.label || actionKey || text;
4677
+ log.info(`User responded: "${label}"`);
3684
4678
  return {
3685
- content: [{
3686
- type: "text",
3687
- text: JSON.stringify({
3688
- status: "responded",
3689
- action_key: actionKey,
3690
- label
3691
- })
3692
- }]
4679
+ content: [
4680
+ {
4681
+ type: "text",
4682
+ text: JSON.stringify({
4683
+ status: "responded",
4684
+ action_key: actionKey || "custom_input",
4685
+ label,
4686
+ text: text || label
4687
+ })
4688
+ }
4689
+ ]
3693
4690
  };
3694
4691
  }
3695
4692
  await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
3696
4693
  }
3697
- log.warn(`Action request ${actionId} timed out after ${args.timeout_seconds || 300}s`);
4694
+ log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
3698
4695
  return {
3699
- content: [{
3700
- type: "text",
3701
- text: JSON.stringify({
3702
- status: "timeout",
3703
- message: "User did not respond within the timeout period."
3704
- })
3705
- }]
4696
+ content: [
4697
+ {
4698
+ type: "text",
4699
+ text: JSON.stringify({
4700
+ status: "timeout",
4701
+ message: "User did not respond within the timeout period."
4702
+ })
4703
+ }
4704
+ ]
3706
4705
  };
3707
4706
  } catch (err) {
3708
- log.error(`request_user_confirmation failed: ${err}`);
4707
+ log.error(`ask_user failed: ${err}`);
3709
4708
  return {
3710
- content: [{
3711
- type: "text",
3712
- text: `Failed to request user confirmation: ${err instanceof Error ? err.message : err}`
3713
- }]
4709
+ content: [
4710
+ {
4711
+ type: "text",
4712
+ text: `Failed to ask user: ${err instanceof Error ? err.message : err}`
4713
+ }
4714
+ ]
3714
4715
  };
3715
4716
  }
3716
4717
  }
3717
4718
  ),
3718
4719
  // ── Job Automation Tools ──────────────────────────────────────
3719
- tool(
4720
+ tool2(
3720
4721
  "job_run",
3721
4722
  "Run a job by loading its goal and available skills as capabilities. You then decide dynamically which skills to use, in what order, and how to chain them based on what you discover. Use this when the user asks to run their job, or when a scheduled job fires.",
3722
4723
  {
3723
- job_name: z.string().describe(
3724
- "Name of the job to run (e.g. 'software-engineer', 'Frontend Dev')"
3725
- )
4724
+ job_name: z2.string().describe("Name of the job to run (e.g. 'software-engineer', 'Frontend Dev')")
3726
4725
  },
3727
4726
  async (args) => {
3728
- if (!userId) {
3729
- return {
3730
- content: [{ type: "text", text: "Not authenticated. Cannot run job." }]
3731
- };
3732
- }
3733
- const runner = new JobRunner(userId);
4727
+ const runner = new JobRunner();
3734
4728
  const job = await runner.loadJob(args.job_name);
3735
4729
  if (!job) {
3736
4730
  const jobs = await runner.listJobs();
@@ -3741,10 +4735,12 @@ ${content}`;
3741
4735
  }
3742
4736
  if (job.skills.length === 0) {
3743
4737
  return {
3744
- content: [{
3745
- type: "text",
3746
- text: `Job "${args.job_name}" has no linked skills. Use skill_generate to add skills to this job.`
3747
- }]
4738
+ content: [
4739
+ {
4740
+ type: "text",
4741
+ text: `Job "${args.job_name}" has no linked skills. Use skill_generate to add skills to this job.`
4742
+ }
4743
+ ]
3748
4744
  };
3749
4745
  }
3750
4746
  const runId = await runner.createRun(job.jobId, {
@@ -3756,51 +4752,55 @@ ${content}`;
3756
4752
  log.debug("Failed to create job run record, proceeding without tracking");
3757
4753
  }
3758
4754
  const prompt = runner.buildJobPrompt(job, runId || "untracked");
3759
- log.info(`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
+ );
3760
4758
  return {
3761
4759
  content: [{ type: "text", text: prompt }]
3762
4760
  };
3763
4761
  }
3764
4762
  ),
3765
- tool(
4763
+ tool2(
3766
4764
  "job_schedule",
3767
4765
  "Schedule a job to run automatically on a recurring basis using a cron expression. For example, schedule your 'software-engineer' job to run every morning at 9am.",
3768
4766
  {
3769
- job_name: z.string().describe("Name of the job to schedule"),
3770
- cron: z.string().describe(
4767
+ job_name: z2.string().describe("Name of the job to schedule"),
4768
+ cron: z2.string().describe(
3771
4769
  "Cron expression: 'minute hour day-of-month month day-of-week'. Examples: '0 9 * * *' (daily 9am), '0 9 * * 1-5' (weekdays 9am), '0 */2 * * *' (every 2 hours)"
3772
4770
  ),
3773
- timezone: z.string().optional().describe("Timezone (default: UTC). Examples: 'UTC', 'America/New_York'"),
3774
- 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>')")
3775
4773
  },
3776
4774
  async (args) => {
3777
- if (!userId) {
3778
- return {
3779
- content: [{ type: "text", text: "Not authenticated. Cannot schedule job." }]
3780
- };
3781
- }
3782
- const runner = new JobRunner(userId);
4775
+ const runner = new JobRunner();
3783
4776
  const job = await runner.loadJob(args.job_name);
3784
4777
  if (!job) {
3785
4778
  return {
3786
- content: [{ 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
+ ]
3787
4785
  };
3788
4786
  }
3789
4787
  try {
3790
4788
  getNextRunTime(args.cron, args.timezone || "UTC");
3791
4789
  } catch {
3792
4790
  return {
3793
- content: [{
3794
- type: "text",
3795
- text: `Invalid cron expression: "${args.cron}". Use format: "minute hour day-of-month month day-of-week"`
3796
- }]
4791
+ content: [
4792
+ {
4793
+ type: "text",
4794
+ text: `Invalid cron expression: "${args.cron}". Use format: "minute hour day-of-month month day-of-week"`
4795
+ }
4796
+ ]
3797
4797
  };
3798
4798
  }
3799
4799
  const name = args.schedule_name || `Job: ${args.job_name}`;
3800
4800
  const prompt = `[JobRun: ${args.job_name}] Run the "${args.job_name}" job. Use job_run to execute it.`;
3801
4801
  const tz = args.timezone || "UTC";
3802
4802
  try {
3803
- const task = await createScheduledTask(userId, name, prompt, args.cron, tz);
4803
+ const task = await createScheduledTask(name, prompt, args.cron, tz);
3804
4804
  await callMcpHandler("schedule.link_job", {
3805
4805
  task_id: task.id,
3806
4806
  job_id: job.jobId
@@ -3828,36 +4828,35 @@ ${content}`;
3828
4828
  return { content: [{ type: "text", text: response }] };
3829
4829
  } catch (err) {
3830
4830
  return {
3831
- content: [{
3832
- type: "text",
3833
- text: `Failed to schedule job: ${err instanceof Error ? err.message : err}`
3834
- }]
4831
+ content: [
4832
+ {
4833
+ type: "text",
4834
+ text: `Failed to schedule job: ${err instanceof Error ? err.message : err}`
4835
+ }
4836
+ ]
3835
4837
  };
3836
4838
  }
3837
4839
  }
3838
4840
  ),
3839
- tool(
4841
+ tool2(
3840
4842
  "job_status",
3841
4843
  "Check the status and run history of a job. Shows recent executions, success rates, and details.",
3842
4844
  {
3843
- job_name: z.string().optional().describe("Job name to check (omit for all jobs)"),
3844
- 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)")
3845
4847
  },
3846
4848
  async (args) => {
3847
- if (!userId) {
3848
- return {
3849
- content: [{ type: "text", text: "Not authenticated." }]
3850
- };
3851
- }
3852
- const runner = new JobRunner(userId);
4849
+ const runner = new JobRunner();
3853
4850
  if (!args.job_name) {
3854
4851
  const jobs = await runner.listJobs();
3855
4852
  if (jobs.length === 0) {
3856
4853
  return {
3857
- content: [{
3858
- type: "text",
3859
- text: "No jobs defined. Use skill_generate to create a job from your job description."
3860
- }]
4854
+ content: [
4855
+ {
4856
+ type: "text",
4857
+ text: "No jobs defined. Use skill_generate to create a job from your job description."
4858
+ }
4859
+ ]
3861
4860
  };
3862
4861
  }
3863
4862
  let response2 = "## Your Jobs\n\n";
@@ -3871,10 +4870,12 @@ ${content}`;
3871
4870
  const runs = await runner.getRunHistory(args.job_name, args.limit || 5);
3872
4871
  if (runs.length === 0) {
3873
4872
  return {
3874
- content: [{
3875
- type: "text",
3876
- text: `No runs found for job "${args.job_name}". Use job_run to execute it.`
3877
- }]
4873
+ content: [
4874
+ {
4875
+ type: "text",
4876
+ text: `No runs found for job "${args.job_name}". Use job_run to execute it.`
4877
+ }
4878
+ ]
3878
4879
  };
3879
4880
  }
3880
4881
  let response = `## Job Status: ${args.job_name}
@@ -3903,18 +4904,142 @@ ${content}`;
3903
4904
  response += "\n";
3904
4905
  return { content: [{ type: "text", text: response }] };
3905
4906
  }
4907
+ ),
4908
+ // ── Credential Tools ──────────────────────────────────────────
4909
+ tool2(
4910
+ "credential_get",
4911
+ "Retrieve a locally stored credential by name. Returns the secret data (API keys, tokens, etc.) stored on the user's machine. Use this when a skill needs authentication or API access.",
4912
+ {
4913
+ name: z2.string().describe("Credential name (e.g. 'amazon-login', 'openai-api-key')")
4914
+ },
4915
+ async (args) => {
4916
+ const store = getCredentialStore();
4917
+ const credential = store.getByName(args.name);
4918
+ if (!credential) {
4919
+ const all = store.list();
4920
+ const available = all.length > 0 ? `Available credentials: ${all.map((m) => m.name).join(", ")}` : "No credentials stored yet.";
4921
+ return {
4922
+ content: [
4923
+ {
4924
+ type: "text",
4925
+ text: `Credential "${args.name}" not found. ${available}
4926
+ Use ask_user to request it from the user, or create it yourself (e.g. register an account), then store with credential_set.`
4927
+ }
4928
+ ]
4929
+ };
4930
+ }
4931
+ log.info(`Credential accessed: "${args.name}" (${credential.meta.type})`);
4932
+ return {
4933
+ content: [
4934
+ {
4935
+ type: "text",
4936
+ text: JSON.stringify({
4937
+ name: credential.meta.name,
4938
+ type: credential.meta.type,
4939
+ data: credential.data,
4940
+ skill: credential.meta.skillName || null
4941
+ })
4942
+ }
4943
+ ]
4944
+ };
4945
+ }
4946
+ ),
4947
+ tool2(
4948
+ "credential_set",
4949
+ "Store a credential locally on the user's machine. The credential is encrypted at rest and never sent to any remote server. IMPORTANT: Always use this to persist credentials \u2014 both when receiving them from the user via ask_user AND when you generate new credentials yourself (e.g. registering an account, creating an API key, generating a token). This ensures credentials survive across sessions and don't need to be recreated.",
4950
+ {
4951
+ name: z2.string().describe("Credential name (lowercase kebab-case, e.g. 'amazon-login')"),
4952
+ type: z2.enum(["api_key", "oauth_token", "login", "secret", "custom"]).describe("Credential type"),
4953
+ data: z2.record(z2.string(), z2.string()).describe(
4954
+ 'Key-value pairs (e.g. { "username": "...", "password": "..." } or { "api_key": "..." })'
4955
+ ),
4956
+ skill_name: z2.string().optional().describe("Associate with a specific skill"),
4957
+ tags: z2.array(z2.string()).optional().describe("Tags for searchability")
4958
+ },
4959
+ async (args) => {
4960
+ const store = getCredentialStore();
4961
+ const meta = store.save(args.name, args.type, args.data, {
4962
+ skillName: args.skill_name,
4963
+ tags: args.tags
4964
+ });
4965
+ log.info(`Credential stored: "${args.name}" (${args.type})`);
4966
+ return {
4967
+ content: [
4968
+ {
4969
+ type: "text",
4970
+ text: `Credential "${meta.name}" stored locally (type: ${meta.type}, id: ${meta.id}). It is encrypted and will persist across app restarts.`
4971
+ }
4972
+ ]
4973
+ };
4974
+ }
4975
+ ),
4976
+ tool2(
4977
+ "credential_list",
4978
+ "List all locally stored credentials (metadata only, no secrets). Use this to check what credentials are available before executing a skill.",
4979
+ {
4980
+ skill_name: z2.string().optional().describe("Filter by skill name"),
4981
+ type: z2.string().optional().describe("Filter by credential type")
4982
+ },
4983
+ async (args) => {
4984
+ const store = getCredentialStore();
4985
+ let results = store.list();
4986
+ if (args.skill_name) {
4987
+ results = results.filter((m) => m.skillName === args.skill_name);
4988
+ }
4989
+ if (args.type) {
4990
+ results = results.filter((m) => m.type === args.type);
4991
+ }
4992
+ if (results.length === 0) {
4993
+ const filter = args.skill_name ? ` for skill "${args.skill_name}"` : "";
4994
+ return {
4995
+ content: [{ type: "text", text: `No credentials found${filter}.` }]
4996
+ };
4997
+ }
4998
+ let response = "## Stored Credentials\n\n";
4999
+ for (const m of results) {
5000
+ const skill = m.skillName ? ` [${m.skillName}]` : "";
5001
+ const tags = m.tags.length > 0 ? ` (${m.tags.join(", ")})` : "";
5002
+ response += `- **${m.name}** (${m.type})${skill}${tags}
5003
+ `;
5004
+ }
5005
+ return { content: [{ type: "text", text: response }] };
5006
+ }
5007
+ ),
5008
+ tool2(
5009
+ "credential_remove",
5010
+ "Remove a locally stored credential by name.",
5011
+ {
5012
+ name: z2.string().describe("Credential name to remove")
5013
+ },
5014
+ async (args) => {
5015
+ const store = getCredentialStore();
5016
+ const removed = store.removeByName(args.name);
5017
+ if (!removed) {
5018
+ return {
5019
+ content: [{ type: "text", text: `Credential "${args.name}" not found.` }]
5020
+ };
5021
+ }
5022
+ log.info(`Credential removed: "${args.name}"`);
5023
+ return {
5024
+ content: [
5025
+ { type: "text", text: `Credential "${args.name}" removed from local storage.` }
5026
+ ]
5027
+ };
5028
+ }
3906
5029
  )
3907
5030
  ]
3908
5031
  });
3909
5032
  }
3910
- async function saveJobToDb(_userId, jobName, jobDescription, createdSkillNames) {
5033
+ async function saveJobToDb(jobName, jobDescription, createdSkillNames) {
3911
5034
  try {
3912
5035
  const data = await callMcpHandler("job.save_with_skills", {
3913
5036
  job_name: jobName,
3914
5037
  job_description: jobDescription,
3915
5038
  skill_names: createdSkillNames
3916
5039
  });
3917
- log.debug(`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
+ );
3918
5043
  } catch (err) {
3919
5044
  log.debug(`saveJobToDb error: ${err}`);
3920
5045
  }
@@ -3969,7 +5094,7 @@ function createEventHooks(taskId, toolCallRecords) {
3969
5094
  };
3970
5095
  }
3971
5096
 
3972
- // src/agent/processor.ts
5097
+ // src/agent/system-prompt.ts
3973
5098
  var BASE_SYSTEM_PROMPT = `You are AssistMe, an AI assistant that operates like a real human on the user's computer. You control the user's actual Chrome browser and work with their real files.
3974
5099
 
3975
5100
  KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This means:
@@ -3984,7 +5109,28 @@ KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This
3984
5109
 
3985
5110
  Available capabilities:
3986
5111
  1. BROWSER CONTROL (user's real Chrome via CDP):
3987
- - 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
3988
5134
  - If auth is needed: use browser_request_user_action to ask the user to log in
3989
5135
 
3990
5136
  2. FILE OPERATIONS & SHELL:
@@ -4021,7 +5167,7 @@ Available capabilities:
4021
5167
 
4022
5168
  5. JOB AUTOMATION:
4023
5169
  - When the user describes their job/role/daily work, use skill_generate to decompose it into automatable skills
4024
- - ALWAYS use request_user_confirmation to get user approval before creating skills \u2014 never create skills without approval
5170
+ - ALWAYS use ask_user to get user approval before creating skills \u2014 never create skills without approval
4025
5171
  - Use job_run to start a job \u2014 it gives you the job's goal and available skills as capabilities
4026
5172
  - When running a job, be AGENTIC: decide dynamically what to do based on what you discover
4027
5173
  - Do NOT follow a fixed sequence \u2014 if checking Slack reveals a task that needs GitHub, go do GitHub immediately
@@ -4038,46 +5184,58 @@ Available capabilities:
4038
5184
  Workflow for web tasks (e.g. "\u67E5\u4E00\u4E0B kindle \u6700\u65B0\u6B3E\u4EF7\u683C"):
4039
5185
  1. browser_connect \u2192 connect to user's Chrome
4040
5186
  2. browser_new_tab \u2192 open a new tab
4041
- 3. browser_navigate \u2192 go to the website (login pages are auto-detected \u2014 the user will be prompted and their session saved)
4042
- 4. browser_read_page or browser_screenshot \u2192 read the content
4043
- 5. If login is needed but not auto-detected \u2192 use browser_request_user_action to ask the user
4044
- 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)
4045
5191
  7. Summarize findings
4046
5192
 
5193
+ Workflow for form filling (e.g. "\u6CE8\u518C\u4E00\u4E2A Gmail \u8D26\u53F7"):
5194
+ 1. browser_connect + browser_navigate \u2192 go to the form page
5195
+ 2. browser_snapshot \u2192 see all form fields with ref numbers
5196
+ 3. browser_act \u2192 batch fill multiple fields + click submit in ONE call:
5197
+ actions=[{action:"type", ref:1, text:"John"}, {action:"type", ref:2, text:"Doe"}, {action:"select", ref:3, option:"March"}, {action:"click", ref:7}] screenshot=true
5198
+ 4. Check the screenshot \u2014 if validation errors appear, re-snapshot and fix
5199
+ 5. When a username/email is taken, append a random 4-digit suffix and retry
5200
+
4047
5201
  Guidelines:
4048
5202
  - Always use the real browser for web tasks, never try to fetch URLs programmatically
4049
- - Use browser_screenshot when you need to see the visual layout
4050
- - 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
4051
5207
  - Login pages are auto-detected after navigation \u2014 the user is prompted and sessions are saved automatically
4052
5208
  - If auto-detection misses a login page, use browser_request_user_action manually
5209
+ - Fall back to legacy tools (browser_click, browser_type, browser_evaluate) only when refs don't work
4053
5210
  - Be thorough: check multiple sources when comparing prices/products
4054
5211
  - Summarize results clearly at the end
4055
5212
  - When you learn something about the user (preferences, habits), use memory_store to remember it
4056
5213
 
4057
5214
  CRITICAL \u2014 Ask before you guess:
4058
- - Before executing a task, verify you have all required information. If anything is ambiguous or missing, use request_user_input to ask.
5215
+ - Before executing a task, verify you have all required information. If anything is ambiguous or missing, use ask_user to ask.
4059
5216
  - First try to resolve unknowns yourself: check memories, read workspace files (e.g. git remote, config files), or infer from conversation history.
4060
- - If you still lack a critical piece of information after self-resolution, ASK the user via request_user_input. Do NOT guess, assume defaults, or proceed with incomplete information.
5217
+ - If you still lack a critical piece of information after self-resolution, ASK the user via ask_user. Do NOT guess, assume defaults, or proceed with incomplete information.
5218
+ - When asking, provide suggested options as buttons whenever possible \u2014 the user can always type a custom answer instead.
4061
5219
  - Examples of when to ask: which account/repo/project to target, what format the user wants, which of multiple options to choose, credentials or URLs that cannot be inferred.
4062
5220
  - Keep questions specific and actionable. Explain what you already know and what exactly you need.
4063
5221
  - After receiving the answer, store it with memory_store if it is likely to be useful in future conversations.
4064
5222
 
4065
5223
  Workspace path: {workspace_path}`;
5224
+
5225
+ // src/agent/processor.ts
4066
5226
  var MAX_HISTORY_ENTRIES = 10;
4067
5227
  var MAX_RESPONSE_LENGTH = 1500;
4068
5228
  var TaskProcessor = class {
4069
5229
  memoryManager = null;
4070
5230
  skillManager;
4071
- userId = null;
4072
5231
  sessionId = null;
4073
5232
  /** In-memory conversation history, keyed by conversation_id */
4074
5233
  historyCache = /* @__PURE__ */ new Map();
4075
5234
  constructor() {
4076
5235
  this.skillManager = new SkillManager();
4077
5236
  }
4078
- setUserId(userId) {
4079
- this.userId = userId;
4080
- this.memoryManager = new MemoryManager(userId);
5237
+ init(userId) {
5238
+ this.memoryManager = new MemoryManager();
4081
5239
  this.skillManager.setUserId(userId);
4082
5240
  this.skillManager.loadFromDb().catch((err) => {
4083
5241
  log.debug(`DB skill load deferred: ${err}`);
@@ -4152,8 +5310,7 @@ var TaskProcessor = class {
4152
5310
  memoryManager: this.memoryManager,
4153
5311
  skillManager: this.skillManager,
4154
5312
  taskId: task.id,
4155
- sessionId: this.sessionId || void 0,
4156
- userId: this.userId || void 0
5313
+ sessionId: this.sessionId || void 0
4157
5314
  });
4158
5315
  const eventHooks = createEventHooks(task.id, toolCallRecords);
4159
5316
  const allowedTools = [
@@ -4178,12 +5335,16 @@ var TaskProcessor = class {
4178
5335
  "mcp__assistme-agent__skill_add",
4179
5336
  "mcp__assistme-agent__skill_publish",
4180
5337
  // User interaction
4181
- "mcp__assistme-agent__request_user_input",
4182
- "mcp__assistme-agent__request_user_confirmation",
5338
+ "mcp__assistme-agent__ask_user",
4183
5339
  // Job automation tools
4184
5340
  "mcp__assistme-agent__job_run",
4185
5341
  "mcp__assistme-agent__job_schedule",
4186
- "mcp__assistme-agent__job_status"
5342
+ "mcp__assistme-agent__job_status",
5343
+ // Credential tools (local storage)
5344
+ "mcp__assistme-agent__credential_get",
5345
+ "mcp__assistme-agent__credential_set",
5346
+ "mcp__assistme-agent__credential_list",
5347
+ "mcp__assistme-agent__credential_remove"
4187
5348
  ];
4188
5349
  async function* promptMessages() {
4189
5350
  yield {
@@ -4294,7 +5455,9 @@ var TaskProcessor = class {
4294
5455
  }
4295
5456
  this.historyCache.set(task.conversation_id, convHistory);
4296
5457
  if (agentSessionId) {
4297
- this.evaluateSkillPostTask(agentSessionId, config.model).catch((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
+ );
4298
5461
  }
4299
5462
  } catch (err) {
4300
5463
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -4317,10 +5480,7 @@ var TaskProcessor = class {
4317
5480
 
4318
5481
  // src/commands/start.ts
4319
5482
  function registerStartCommand(program2) {
4320
- program2.command("start", { isDefault: true, hidden: true }).description("Start the agent (default command)").option(
4321
- "-w, --workspace <path>",
4322
- "Workspace path (default: current directory)"
4323
- ).option("-n, --name <name>", "Session name").option("-v, --verbose", "Enable verbose/debug logging").action(runAgent);
5483
+ program2.command("start", { isDefault: true, hidden: true }).description("Start the agent (default command)").option("-w, --workspace <path>", "Workspace path (default: current directory)").option("-n, --name <name>", "Session name").option("-v, --verbose", "Enable verbose/debug logging").action(runAgent);
4324
5484
  }
4325
5485
  async function runAgent(opts) {
4326
5486
  if (opts.verbose) {
@@ -4333,26 +5493,10 @@ async function runAgent(opts) {
4333
5493
  setConfig("sessionName", opts.name);
4334
5494
  }
4335
5495
  console.log();
4336
- console.log(
4337
- chalk4.bold.cyan(
4338
- " \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"
4339
- )
4340
- );
4341
- console.log(
4342
- chalk4.bold.cyan(
4343
- " \u2551 AssistMe CLI Agent \u2551"
4344
- )
4345
- );
4346
- console.log(
4347
- chalk4.bold.cyan(
4348
- " \u2551 AI that controls your real browser \u2551"
4349
- )
4350
- );
4351
- console.log(
4352
- chalk4.bold.cyan(
4353
- " \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
4354
- )
4355
- );
5496
+ console.log(chalk4.bold.cyan(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
5497
+ console.log(chalk4.bold.cyan(" \u2551 AssistMe CLI Agent \u2551"));
5498
+ console.log(chalk4.bold.cyan(" \u2551 AI that controls your real browser \u2551"));
5499
+ console.log(chalk4.bold.cyan(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
4356
5500
  console.log();
4357
5501
  let userId;
4358
5502
  try {
@@ -4369,9 +5513,7 @@ async function runAgent(opts) {
4369
5513
  launchSpinner.succeed("Browser detected (CDP port 9222)");
4370
5514
  break;
4371
5515
  case "launched":
4372
- launchSpinner.succeed(
4373
- "Browser launched with remote debugging (debug profile)"
4374
- );
5516
+ launchSpinner.succeed("Browser launched with remote debugging (debug profile)");
4375
5517
  break;
4376
5518
  }
4377
5519
  } else {
@@ -4382,9 +5524,7 @@ async function runAgent(opts) {
4382
5524
  break;
4383
5525
  case "port_conflict":
4384
5526
  launchSpinner.fail("Port 9222 is in use by another process");
4385
- log.info(
4386
- launchResult.detail ?? "Stop the conflicting process or use a different port."
4387
- );
5527
+ log.info(launchResult.detail ?? "Stop the conflicting process or use a different port.");
4388
5528
  break;
4389
5529
  default:
4390
5530
  launchSpinner.fail("Failed to start Chrome with remote debugging");
@@ -4394,14 +5534,12 @@ async function runAgent(opts) {
4394
5534
  if (launchResult.chromePath) {
4395
5535
  log.info(`Chrome binary: ${launchResult.chromePath}`);
4396
5536
  }
4397
- log.info(
4398
- "Browser will be auto-launched when the first task needs it."
4399
- );
5537
+ log.info("Browser will be auto-launched when the first task needs it.");
4400
5538
  break;
4401
5539
  }
4402
5540
  }
4403
5541
  const processor = new TaskProcessor();
4404
- processor.setUserId(userId);
5542
+ processor.init(userId);
4405
5543
  const sessionManager = new SessionManager();
4406
5544
  const browserRef = getBrowser();
4407
5545
  const shutdown = async () => {
@@ -4463,9 +5601,7 @@ async function runAgent(opts) {
4463
5601
  });
4464
5602
  rl.on("close", shutdown);
4465
5603
  } catch (err) {
4466
- log.error(
4467
- `Failed to start: ${err instanceof Error ? err.message : err}`
4468
- );
5604
+ log.error(`Failed to start: ${err instanceof Error ? err.message : err}`);
4469
5605
  process.exit(1);
4470
5606
  }
4471
5607
  }
@@ -4509,13 +5645,7 @@ function registerStatusCommand(program2) {
4509
5645
  import chalk6 from "chalk";
4510
5646
  function registerScheduleCommands(program2) {
4511
5647
  const scheduleCmd = program2.command("schedule").description("Manage scheduled (cron) tasks");
4512
- scheduleCmd.command("add").description("Add a scheduled task").requiredOption("-n, --name <name>", "Task name").requiredOption(
4513
- "-p, --prompt <prompt>",
4514
- "Task prompt (what the AI should do)"
4515
- ).requiredOption(
4516
- "-c, --cron <expression>",
4517
- "Cron expression (e.g. '0 8 * * *' for daily 8am)"
4518
- ).option("-t, --timezone <tz>", "Timezone (default: UTC)").action(async (opts) => {
5648
+ scheduleCmd.command("add").description("Add a scheduled task").requiredOption("-n, --name <name>", "Task name").requiredOption("-p, --prompt <prompt>", "Task prompt (what the AI should do)").requiredOption("-c, --cron <expression>", "Cron expression (e.g. '0 8 * * *' for daily 8am)").option("-t, --timezone <tz>", "Timezone (default: UTC)").action(async (opts) => {
4519
5649
  try {
4520
5650
  const cronParts = opts.cron.trim().split(/\s+/);
4521
5651
  if (cronParts.length !== 5) {
@@ -4525,14 +5655,8 @@ function registerScheduleCommands(program2) {
4525
5655
  console.log(' Examples: "0 9 * * *" (daily 9am), "*/15 * * * *" (every 15 min)');
4526
5656
  process.exit(1);
4527
5657
  }
4528
- const userId = await getCurrentUserId();
4529
- const task = await createScheduledTask(
4530
- userId,
4531
- opts.name,
4532
- opts.prompt,
4533
- opts.cron,
4534
- opts.timezone
4535
- );
5658
+ await getCurrentUserId();
5659
+ const task = await createScheduledTask(opts.name, opts.prompt, opts.cron, opts.timezone);
4536
5660
  log.success(`Scheduled task created: ${task.name}`);
4537
5661
  console.log(` ID: ${task.id.slice(0, 8)}...`);
4538
5662
  console.log(` Cron: ${task.cron_expression}`);
@@ -4546,8 +5670,8 @@ function registerScheduleCommands(program2) {
4546
5670
  });
4547
5671
  scheduleCmd.command("list").description("List all scheduled tasks").action(async () => {
4548
5672
  try {
4549
- const userId = await getCurrentUserId();
4550
- const tasks = await listScheduledTasks(userId);
5673
+ await getCurrentUserId();
5674
+ const tasks = await listScheduledTasks();
4551
5675
  if (tasks.length === 0) {
4552
5676
  console.log(chalk6.yellow("No scheduled tasks."));
4553
5677
  console.log('Run "assistme schedule add" to create one.');
@@ -4557,22 +5681,14 @@ function registerScheduleCommands(program2) {
4557
5681
  for (const t of tasks) {
4558
5682
  const icon = t.enabled ? chalk6.green("\u25CF") : chalk6.dim("\u25CB");
4559
5683
  console.log(` ${icon} ${t.name} (${t.id.slice(0, 8)}...)`);
4560
- console.log(
4561
- ` Cron: ${t.cron_expression} (${t.timezone})`
4562
- );
4563
- console.log(
4564
- ` Prompt: ${t.prompt.slice(0, 60)}${t.prompt.length > 60 ? "..." : ""}`
4565
- );
5684
+ console.log(` Cron: ${t.cron_expression} (${t.timezone})`);
5685
+ console.log(` Prompt: ${t.prompt.slice(0, 60)}${t.prompt.length > 60 ? "..." : ""}`);
4566
5686
  console.log(` Runs: ${t.run_count}`);
4567
5687
  if (t.next_run_at) {
4568
- console.log(
4569
- ` Next run: ${new Date(t.next_run_at).toLocaleString()}`
4570
- );
5688
+ console.log(` Next run: ${new Date(t.next_run_at).toLocaleString()}`);
4571
5689
  }
4572
5690
  if (t.last_error) {
4573
- console.log(
4574
- chalk6.red(` Error: ${t.last_error.slice(0, 80)}`)
4575
- );
5691
+ console.log(chalk6.red(` Error: ${t.last_error.slice(0, 80)}`));
4576
5692
  }
4577
5693
  console.log();
4578
5694
  }
@@ -4583,8 +5699,8 @@ function registerScheduleCommands(program2) {
4583
5699
  });
4584
5700
  scheduleCmd.command("toggle <id>").description("Enable/disable a scheduled task").action(async (id) => {
4585
5701
  try {
4586
- const userId = await getCurrentUserId();
4587
- const tasks = await listScheduledTasks(userId);
5702
+ await getCurrentUserId();
5703
+ const tasks = await listScheduledTasks();
4588
5704
  const task = tasks.find((t) => t.id.startsWith(id));
4589
5705
  if (!task) {
4590
5706
  log.error(`Task not found: ${id}`);
@@ -4599,8 +5715,8 @@ function registerScheduleCommands(program2) {
4599
5715
  });
4600
5716
  scheduleCmd.command("remove <id>").description("Delete a scheduled task").action(async (id) => {
4601
5717
  try {
4602
- const userId = await getCurrentUserId();
4603
- const tasks = await listScheduledTasks(userId);
5718
+ await getCurrentUserId();
5719
+ const tasks = await listScheduledTasks();
4604
5720
  const task = tasks.find((t) => t.id.startsWith(id));
4605
5721
  if (!task) {
4606
5722
  log.error(`Task not found: ${id}`);
@@ -4621,17 +5737,12 @@ function registerMemoryCommands(program2) {
4621
5737
  const memoryCmd = program2.command("memory").description("Manage the agent's memory about you");
4622
5738
  memoryCmd.command("list").description("List stored memories").option("-c, --category <category>", "Filter by category").option("-l, --limit <number>", "Max items (default: 20)").action(async (opts) => {
4623
5739
  try {
4624
- const userId = await getCurrentUserId();
4625
- const mm = new MemoryManager(userId);
4626
- const memories = await mm.list(
4627
- opts.category,
4628
- parseInt(opts.limit || "20")
4629
- );
5740
+ await getCurrentUserId();
5741
+ const mm = new MemoryManager();
5742
+ const memories = await mm.list(opts.category, parseInt(opts.limit || "20"));
4630
5743
  if (memories.length === 0) {
4631
5744
  console.log(chalk7.yellow("No memories stored yet."));
4632
- console.log(
4633
- "The agent will automatically remember things as you interact with it."
4634
- );
5745
+ console.log("The agent will automatically remember things as you interact with it.");
4635
5746
  return;
4636
5747
  }
4637
5748
  console.log(chalk7.bold(`
@@ -4658,8 +5769,8 @@ Memories (${memories.length}):`));
4658
5769
  });
4659
5770
  memoryCmd.command("add <content>").description("Manually add a memory").option("-c, --category <category>", "Category (default: general)").option("-i, --importance <number>", "Importance 1-10 (default: 5)").option("-t, --tags <tags>", "Comma-separated tags").action(async (content, opts) => {
4660
5771
  try {
4661
- const userId = await getCurrentUserId();
4662
- const mm = new MemoryManager(userId);
5772
+ await getCurrentUserId();
5773
+ const mm = new MemoryManager();
4663
5774
  const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [];
4664
5775
  const mem = await mm.add(
4665
5776
  content,
@@ -4667,9 +5778,7 @@ Memories (${memories.length}):`));
4667
5778
  parseInt(opts.importance || "5"),
4668
5779
  tags
4669
5780
  );
4670
- log.success(
4671
- `Memory stored: "${mem.content.slice(0, 60)}..." [${mem.category}]`
4672
- );
5781
+ log.success(`Memory stored: "${mem.content.slice(0, 60)}..." [${mem.category}]`);
4673
5782
  } catch (err) {
4674
5783
  log.error(`${err instanceof Error ? err.message : err}`);
4675
5784
  process.exit(1);
@@ -4677,8 +5786,8 @@ Memories (${memories.length}):`));
4677
5786
  });
4678
5787
  memoryCmd.command("search <query>").description("Search memories").action(async (query3) => {
4679
5788
  try {
4680
- const userId = await getCurrentUserId();
4681
- const mm = new MemoryManager(userId);
5789
+ await getCurrentUserId();
5790
+ const mm = new MemoryManager();
4682
5791
  const results = await mm.search(query3);
4683
5792
  if (results.length === 0) {
4684
5793
  console.log(chalk7.yellow(`No memories matching "${query3}"`));
@@ -4697,8 +5806,8 @@ Search results for "${query3}":`));
4697
5806
  });
4698
5807
  memoryCmd.command("remove <id>").description("Delete a specific memory").action(async (id) => {
4699
5808
  try {
4700
- const userId = await getCurrentUserId();
4701
- const mm = new MemoryManager(userId);
5809
+ await getCurrentUserId();
5810
+ const mm = new MemoryManager();
4702
5811
  const memories = await mm.list();
4703
5812
  const mem = memories.find((m) => m.id.startsWith(id));
4704
5813
  if (!mem) {
@@ -4714,12 +5823,10 @@ Search results for "${query3}":`));
4714
5823
  });
4715
5824
  memoryCmd.command("clear").description("Clear all memories").option("-c, --category <category>", "Only clear specific category").action(async (opts) => {
4716
5825
  try {
4717
- const userId = await getCurrentUserId();
4718
- const mm = new MemoryManager(userId);
5826
+ await getCurrentUserId();
5827
+ const mm = new MemoryManager();
4719
5828
  await mm.clear(opts.category);
4720
- log.success(
4721
- `Memories cleared${opts.category ? ` (${opts.category})` : ""}`
4722
- );
5829
+ log.success(`Memories cleared${opts.category ? ` (${opts.category})` : ""}`);
4723
5830
  } catch (err) {
4724
5831
  log.error(`${err instanceof Error ? err.message : err}`);
4725
5832
  process.exit(1);
@@ -4857,21 +5964,17 @@ function registerJobCommands(program2) {
4857
5964
  jobCmd.command("list").description("List your defined jobs").action(async () => {
4858
5965
  try {
4859
5966
  const userId = await getCurrentUserId();
4860
- const { JobRunner: JobRunner2 } = await import("./job-runner-N4XAAWLJ.js");
4861
- const runner = new JobRunner2(userId);
5967
+ const { JobRunner: JobRunner2 } = await import("./job-runner-P2L6MOOX.js");
5968
+ const runner = new JobRunner2();
4862
5969
  const jobs = await runner.listJobs();
4863
5970
  if (jobs.length === 0) {
4864
5971
  console.log(chalk9.yellow("No jobs defined."));
4865
- console.log(
4866
- 'Use "assistme" and tell the agent about your job to generate skills.'
4867
- );
5972
+ console.log('Use "assistme" and tell the agent about your job to generate skills.');
4868
5973
  return;
4869
5974
  }
4870
5975
  console.log(chalk9.bold("\nYour Jobs:"));
4871
5976
  for (const job of jobs) {
4872
- console.log(
4873
- ` ${chalk9.cyan(job.name)} (${job.skillCount} skills)`
4874
- );
5977
+ console.log(` ${chalk9.cyan(job.name)} (${job.skillCount} skills)`);
4875
5978
  console.log(
4876
5979
  ` ${job.description.slice(0, 80)}${job.description.length > 80 ? "..." : ""}`
4877
5980
  );
@@ -4885,38 +5988,23 @@ function registerJobCommands(program2) {
4885
5988
  jobCmd.command("status [name]").description("Show run history for a job (or all jobs)").option("-l, --limit <number>", "Max runs to show (default: 5)").action(async (name, opts) => {
4886
5989
  try {
4887
5990
  const userId = await getCurrentUserId();
4888
- const { JobRunner: JobRunner2 } = await import("./job-runner-N4XAAWLJ.js");
4889
- const runner = new JobRunner2(userId);
4890
- const runs = await runner.getRunHistory(
4891
- name,
4892
- parseInt(opts.limit || "5")
4893
- );
5991
+ const { JobRunner: JobRunner2 } = await import("./job-runner-P2L6MOOX.js");
5992
+ const runner = new JobRunner2();
5993
+ const runs = await runner.getRunHistory(name, parseInt(opts.limit || "5"));
4894
5994
  if (runs.length === 0) {
4895
- console.log(
4896
- chalk9.yellow(
4897
- name ? `No runs found for "${name}".` : "No job runs yet."
4898
- )
4899
- );
5995
+ console.log(chalk9.yellow(name ? `No runs found for "${name}".` : "No job runs yet."));
4900
5996
  return;
4901
5997
  }
4902
- console.log(
4903
- chalk9.bold(
4904
- `
4905
- Job Run History${name ? ` \u2014 ${name}` : ""}:`
4906
- )
4907
- );
5998
+ console.log(chalk9.bold(`
5999
+ Job Run History${name ? ` \u2014 ${name}` : ""}:`));
4908
6000
  for (const run of runs) {
4909
6001
  const icon = run.status === "completed" ? chalk9.green("\u25CF") : run.status === "failed" ? chalk9.red("\u25CF") : run.status === "running" ? chalk9.yellow("\u25CF") : chalk9.dim("\u25CB");
4910
6002
  const date = new Date(run.startedAt).toLocaleString();
4911
6003
  const duration = run.completedAt ? `${Math.round((new Date(run.completedAt).getTime() - new Date(run.startedAt).getTime()) / 1e3)}s` : "in progress";
4912
- console.log(
4913
- ` ${icon} ${chalk9.bold(run.jobName)} | ${run.triggerType} | ${date}`
4914
- );
6004
+ console.log(` ${icon} ${chalk9.bold(run.jobName)} | ${run.triggerType} | ${date}`);
4915
6005
  console.log(` Duration: ${duration}`);
4916
6006
  if (run.summary) {
4917
- console.log(
4918
- ` ${chalk9.dim(run.summary.slice(0, 100))}`
4919
- );
6007
+ console.log(` ${chalk9.dim(run.summary.slice(0, 100))}`);
4920
6008
  }
4921
6009
  console.log();
4922
6010
  }
@@ -4939,28 +6027,20 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
4939
6027
  process.exit(1);
4940
6028
  }
4941
6029
  const userId = await getCurrentUserId();
4942
- const { JobRunner: JobRunner2 } = await import("./job-runner-N4XAAWLJ.js");
4943
- const runner = new JobRunner2(userId);
6030
+ const { JobRunner: JobRunner2 } = await import("./job-runner-P2L6MOOX.js");
6031
+ const runner = new JobRunner2();
4944
6032
  const job = await runner.loadJob(name);
4945
6033
  if (!job) {
4946
6034
  log.error(`Job "${name}" not found.`);
4947
6035
  const jobs = await runner.listJobs();
4948
6036
  if (jobs.length > 0) {
4949
- console.log(
4950
- `Available: ${jobs.map((j) => j.name).join(", ")}`
4951
- );
6037
+ console.log(`Available: ${jobs.map((j) => j.name).join(", ")}`);
4952
6038
  }
4953
6039
  process.exit(1);
4954
6040
  }
4955
6041
  const tz = opts.timezone || "UTC";
4956
6042
  const prompt = `[JobRun: ${name}] Run the "${name}" job. Use job_run to execute it.`;
4957
- const task = await createScheduledTask(
4958
- userId,
4959
- `Job: ${name}`,
4960
- prompt,
4961
- opts.cron,
4962
- tz
4963
- );
6043
+ const task = await createScheduledTask(`Job: ${name}`, prompt, opts.cron, tz);
4964
6044
  await callMcpHandler("schedule.link_job", {
4965
6045
  task_id: task.id,
4966
6046
  job_id: job.jobId
@@ -4977,6 +6057,144 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
4977
6057
  });
4978
6058
  }
4979
6059
 
6060
+ // src/commands/credential.ts
6061
+ import chalk10 from "chalk";
6062
+ import { createInterface as createInterface3 } from "readline";
6063
+ var VALID_TYPES = ["api_key", "oauth_token", "login", "secret", "custom"];
6064
+ function registerCredentialCommands(program2) {
6065
+ const credCmd = program2.command("credential").alias("cred").description("Manage locally stored credentials (encrypted, never sent to server)");
6066
+ credCmd.command("list").description("List all stored credentials (metadata only, no secrets)").option("-s, --skill <name>", "Filter by skill name").option("-t, --type <type>", "Filter by credential type").action(async (opts) => {
6067
+ const store = getCredentialStore();
6068
+ let results = store.list();
6069
+ if (opts.skill) {
6070
+ results = results.filter((m) => m.skillName === opts.skill);
6071
+ }
6072
+ if (opts.type) {
6073
+ results = results.filter((m) => m.type === opts.type);
6074
+ }
6075
+ if (results.length === 0) {
6076
+ console.log(chalk10.yellow(" No credentials stored."));
6077
+ console.log(chalk10.dim(" Use `assistme credential set <name>` to add one."));
6078
+ return;
6079
+ }
6080
+ console.log(chalk10.bold("\n Stored Credentials:\n"));
6081
+ for (const m of results) {
6082
+ const skill = m.skillName ? chalk10.dim(` [${m.skillName}]`) : "";
6083
+ const tags = m.tags.length > 0 ? chalk10.dim(` (${m.tags.join(", ")})`) : "";
6084
+ console.log(` ${chalk10.cyan(m.name)} ${chalk10.gray(`(${m.type})`)}${skill}${tags}`);
6085
+ console.log(chalk10.dim(` ID: ${m.id} Created: ${m.createdAt.slice(0, 10)}`));
6086
+ }
6087
+ console.log();
6088
+ });
6089
+ credCmd.command("set <name>").description("Store or update a credential interactively").option("-t, --type <type>", `Credential type: ${VALID_TYPES.join(", ")}`, "secret").option("-s, --skill <name>", "Associate with a skill").option("--tags <tags>", "Comma-separated tags").action(async (name, opts) => {
6090
+ if (!VALID_TYPES.includes(opts.type)) {
6091
+ log.error(`Invalid type "${opts.type}". Must be one of: ${VALID_TYPES.join(", ")}`);
6092
+ process.exit(1);
6093
+ }
6094
+ const rl = createInterface3({
6095
+ input: process.stdin,
6096
+ output: process.stdout
6097
+ });
6098
+ const ask = (q) => new Promise((resolve2) => {
6099
+ rl.question(q, (answer) => resolve2(answer.trim()));
6100
+ });
6101
+ console.log(chalk10.bold(`
6102
+ Set credential: ${name}`));
6103
+ console.log(chalk10.dim(" Enter key-value pairs. Empty key to finish.\n"));
6104
+ const data = {};
6105
+ if (opts.type === "login") {
6106
+ data.username = await ask(chalk10.cyan(" Username: "));
6107
+ data.password = await ask(chalk10.cyan(" Password: "));
6108
+ } else if (opts.type === "api_key") {
6109
+ data.api_key = await ask(chalk10.cyan(" API Key: "));
6110
+ } else {
6111
+ while (true) {
6112
+ const key = await ask(chalk10.cyan(" Key (empty to finish): "));
6113
+ if (!key) break;
6114
+ const value = await ask(chalk10.cyan(` Value for "${key}": `));
6115
+ data[key] = value;
6116
+ }
6117
+ }
6118
+ rl.close();
6119
+ if (Object.keys(data).length === 0) {
6120
+ console.log(chalk10.yellow(" No data provided. Credential not saved."));
6121
+ return;
6122
+ }
6123
+ const store = getCredentialStore();
6124
+ const tags = opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [];
6125
+ const meta = store.save(name, opts.type, data, {
6126
+ skillName: opts.skill,
6127
+ tags
6128
+ });
6129
+ log.success(`Credential "${meta.name}" saved (ID: ${meta.id})`);
6130
+ console.log(chalk10.dim(" Encrypted and stored at ~/.config/assistme/credentials/"));
6131
+ });
6132
+ credCmd.command("get <name>").description("Show credential metadata (use --reveal to show secrets)").option("-r, --reveal", "Reveal secret values").action(async (name, opts) => {
6133
+ const store = getCredentialStore();
6134
+ const credential = store.getByName(name);
6135
+ if (!credential) {
6136
+ log.error(`Credential not found: ${name}`);
6137
+ process.exit(1);
6138
+ }
6139
+ const m = credential.meta;
6140
+ console.log(chalk10.bold(`
6141
+ ${m.name} (${m.type})`));
6142
+ if (m.skillName) console.log(` Skill: ${m.skillName}`);
6143
+ if (m.tags.length > 0) console.log(` Tags: ${m.tags.join(", ")}`);
6144
+ console.log(` Created: ${m.createdAt}`);
6145
+ console.log(` Updated: ${m.updatedAt}`);
6146
+ if (opts.reveal) {
6147
+ console.log(chalk10.bold("\n Data:"));
6148
+ for (const [key, value] of Object.entries(credential.data)) {
6149
+ console.log(` ${chalk10.cyan(key)}: ${value}`);
6150
+ }
6151
+ } else {
6152
+ console.log(chalk10.bold("\n Data keys:"));
6153
+ for (const key of Object.keys(credential.data)) {
6154
+ console.log(` ${chalk10.cyan(key)}: ${"*".repeat(8)}`);
6155
+ }
6156
+ console.log(chalk10.dim("\n Use --reveal to show secret values."));
6157
+ }
6158
+ console.log();
6159
+ });
6160
+ credCmd.command("remove <name>").description("Remove a stored credential").action(async (name) => {
6161
+ const store = getCredentialStore();
6162
+ const removed = store.removeByName(name);
6163
+ if (removed) {
6164
+ log.success(`Credential "${name}" removed.`);
6165
+ } else {
6166
+ log.error(`Credential not found: ${name}`);
6167
+ }
6168
+ });
6169
+ credCmd.command("clear").description("Remove ALL stored credentials").action(async () => {
6170
+ const store = getCredentialStore();
6171
+ const count = store.list().length;
6172
+ if (count === 0) {
6173
+ console.log(chalk10.yellow(" No credentials to clear."));
6174
+ return;
6175
+ }
6176
+ const rl = createInterface3({
6177
+ input: process.stdin,
6178
+ output: process.stdout
6179
+ });
6180
+ const answer = await new Promise((resolve2) => {
6181
+ rl.question(
6182
+ chalk10.red(` Remove ALL ${count} credential(s)? This cannot be undone. (yes/no): `),
6183
+ (ans) => {
6184
+ rl.close();
6185
+ resolve2(ans.trim().toLowerCase());
6186
+ }
6187
+ );
6188
+ });
6189
+ if (answer === "yes" || answer === "y") {
6190
+ store.clear();
6191
+ log.success(`All ${count} credential(s) removed.`);
6192
+ } else {
6193
+ console.log(chalk10.dim(" Cancelled."));
6194
+ }
6195
+ });
6196
+ }
6197
+
4980
6198
  // src/index.ts
4981
6199
  loadEnv();
4982
6200
  var require2 = createRequire(import.meta.url);
@@ -4992,4 +6210,5 @@ registerScheduleCommands(program);
4992
6210
  registerMemoryCommands(program);
4993
6211
  registerSkillCommands(program);
4994
6212
  registerJobCommands(program);
6213
+ registerCredentialCommands(program);
4995
6214
  program.parse();