assistme 0.1.13 → 0.1.15

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.
@@ -315,6 +315,21 @@ async function failTask(messageId, errorMessage) {
315
315
  });
316
316
  if (error) log.error(`Failed to update task status: ${error.message}`);
317
317
  }
318
+ async function getConversationHistory(conversationId, excludeMessageId, limit = 20) {
319
+ const sb = getSupabase();
320
+ const { data, error } = await sb.from("conversation_messages").select("id, role, content, status, metadata, created_at").eq("conversation_id", conversationId).in("status", ["completed", "failed"]).neq("id", excludeMessageId).order("created_at", { ascending: false }).limit(limit);
321
+ if (error) {
322
+ log.debug(`Failed to fetch conversation history: ${error.message}`);
323
+ return [];
324
+ }
325
+ const rows = data || [];
326
+ return rows.reverse().map((row) => {
327
+ const prompt = row.metadata?.prompt || "";
328
+ const content = row.content || "";
329
+ const response = row.status === "failed" ? `[Task failed] ${content}` : content;
330
+ return { prompt, response };
331
+ }).filter((entry) => entry.prompt && entry.response);
332
+ }
318
333
  var eventSequence = 0;
319
334
  function resetEventSequence() {
320
335
  eventSequence = 0;
@@ -371,6 +386,7 @@ export {
371
386
  claimTask,
372
387
  completeTask,
373
388
  failTask,
389
+ getConversationHistory,
374
390
  resetEventSequence,
375
391
  emitEvent,
376
392
  emitEvents
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  failTask,
11
11
  getConfig,
12
12
  getConfigPath,
13
+ getConversationHistory,
13
14
  getCurrentUserId,
14
15
  getOrCreateCliConversation,
15
16
  getSupabase,
@@ -24,7 +25,7 @@ import {
24
25
  setLogLevel,
25
26
  setSessionBusy,
26
27
  updateHeartbeat
27
- } from "./chunk-QXT7DH44.js";
28
+ } from "./chunk-ERK6A6GH.js";
28
29
 
29
30
  // src/index.ts
30
31
  import { Command } from "commander";
@@ -63,14 +64,15 @@ function getNextRunTime(cronExpr, timezone, fromDate) {
63
64
  const daysOfMonth = parseField(domExpr, 1, 31);
64
65
  const months = parseField(monExpr, 1, 12);
65
66
  const daysOfWeek = parseField(dowExpr, 0, 6);
67
+ const useUTC = timezone === "UTC";
66
68
  const candidate = new Date(now.getTime() + 6e4);
67
69
  candidate.setSeconds(0, 0);
68
70
  for (let i = 0; i < 527040; i++) {
69
- const m = candidate.getMinutes();
70
- const h = candidate.getHours();
71
- const dom = candidate.getDate();
72
- const mon = candidate.getMonth() + 1;
73
- const dow = candidate.getDay();
71
+ const m = useUTC ? candidate.getUTCMinutes() : candidate.getMinutes();
72
+ const h = useUTC ? candidate.getUTCHours() : candidate.getHours();
73
+ const dom = useUTC ? candidate.getUTCDate() : candidate.getDate();
74
+ const mon = (useUTC ? candidate.getUTCMonth() : candidate.getMonth()) + 1;
75
+ const dow = useUTC ? candidate.getUTCDay() : candidate.getDay();
74
76
  if (minutes.includes(m) && hours.includes(h) && daysOfMonth.includes(dom) && months.includes(mon) && (dowExpr === "*" || daysOfWeek.includes(dow))) {
75
77
  return candidate;
76
78
  }
@@ -849,6 +851,76 @@ URL: ${info.url}`;
849
851
  isConnected() {
850
852
  return this.connected && this.ws?.readyState === WebSocket.OPEN;
851
853
  }
854
+ // ── Login Detection ────────────────────────────────────────────
855
+ /**
856
+ * Detect if the current page appears to be a login/authentication page.
857
+ * Checks URL patterns, password input fields, and login form actions.
858
+ */
859
+ async detectLoginPage() {
860
+ try {
861
+ const result = await this.send("Runtime.evaluate", {
862
+ expression: `
863
+ (function() {
864
+ var url = window.location.href.toLowerCase();
865
+
866
+ // URL-based detection
867
+ var loginPatterns = [
868
+ '/login', '/signin', '/sign-in', '/sign_in',
869
+ '/auth/', '/sso/', '/oauth/', '/session/new',
870
+ '/accounts/login', '/users/sign_in',
871
+ 'accounts.google.com', 'login.microsoftonline.com',
872
+ 'github.com/login', 'github.com/session',
873
+ 'login.live.com', 'appleid.apple.com'
874
+ ];
875
+ for (var i = 0; i < loginPatterns.length; i++) {
876
+ if (url.indexOf(loginPatterns[i]) !== -1) {
877
+ return JSON.stringify({
878
+ isLoginPage: true,
879
+ reason: 'URL contains login pattern: ' + loginPatterns[i]
880
+ });
881
+ }
882
+ }
883
+
884
+ // Password input detection (visible only)
885
+ var passwordInputs = document.querySelectorAll('input[type="password"]');
886
+ for (var j = 0; j < passwordInputs.length; j++) {
887
+ var input = passwordInputs[j];
888
+ var rect = input.getBoundingClientRect();
889
+ var style = window.getComputedStyle(input);
890
+ if (rect.width > 0 && rect.height > 0 &&
891
+ style.display !== 'none' && style.visibility !== 'hidden') {
892
+ return JSON.stringify({
893
+ isLoginPage: true,
894
+ reason: 'Page contains visible password input field'
895
+ });
896
+ }
897
+ }
898
+
899
+ // Login form action detection
900
+ var formSelectors = [
901
+ 'form[action*="login"]', 'form[action*="signin"]',
902
+ 'form[action*="session"]', 'form[action*="auth"]',
903
+ 'form[action*="authenticate"]'
904
+ ];
905
+ var loginForms = document.querySelectorAll(formSelectors.join(','));
906
+ if (loginForms.length > 0) {
907
+ return JSON.stringify({
908
+ isLoginPage: true,
909
+ reason: 'Page contains login form'
910
+ });
911
+ }
912
+
913
+ return JSON.stringify({ isLoginPage: false, reason: '' });
914
+ })()
915
+ `,
916
+ returnByValue: true
917
+ });
918
+ const value = result.result?.value;
919
+ return JSON.parse(value || '{"isLoginPage":false,"reason":""}');
920
+ } catch {
921
+ return { isLoginPage: false, reason: "" };
922
+ }
923
+ }
852
924
  };
853
925
  function findChromePath() {
854
926
  const os = platform();
@@ -2275,6 +2347,60 @@ ${stderr}` : "";
2275
2347
  }
2276
2348
 
2277
2349
  // src/tools/index.ts
2350
+ async function detectAndHandleLogin(browser) {
2351
+ const detection = await browser.detectLoginPage();
2352
+ if (!detection.isLoginPage) return null;
2353
+ const pageInfo = await browser.getPageInfo();
2354
+ let siteName;
2355
+ try {
2356
+ siteName = new URL(pageInfo.url).hostname;
2357
+ } catch {
2358
+ siteName = pageInfo.url;
2359
+ }
2360
+ console.log("\n");
2361
+ console.log("\u2501".repeat(60));
2362
+ console.log(" \u{1F510} LOGIN REQUIRED");
2363
+ console.log("\u2501".repeat(60));
2364
+ console.log(` ${detection.reason}`);
2365
+ console.log(` Site: ${siteName}`);
2366
+ console.log(` Please log in using the browser window.`);
2367
+ console.log(` Your session will be saved for future use.`);
2368
+ console.log(` (Waiting up to 120s for you to complete login)`);
2369
+ console.log("\u2501".repeat(60));
2370
+ console.log("\n");
2371
+ const loginUrl = pageInfo.url;
2372
+ const deadline = Date.now() + 12e4;
2373
+ while (Date.now() < deadline) {
2374
+ await new Promise((r) => setTimeout(r, 3e3));
2375
+ try {
2376
+ const currentInfo = await browser.getPageInfo();
2377
+ if (currentInfo.url !== loginUrl) {
2378
+ await new Promise((r) => setTimeout(r, 1500));
2379
+ const newPage = await browser.readPage();
2380
+ return `Login completed. Redirected to: ${currentInfo.url}
2381
+ Session saved in assistme browser profile for future use.
2382
+
2383
+ Current page:
2384
+ ${newPage.slice(0, 3e3)}`;
2385
+ }
2386
+ const stillLogin = await browser.detectLoginPage();
2387
+ if (!stillLogin.isLoginPage) {
2388
+ const newPage = await browser.readPage();
2389
+ return `Login completed on ${siteName}.
2390
+ Session saved in assistme browser profile for future use.
2391
+
2392
+ Current page:
2393
+ ${newPage.slice(0, 3e3)}`;
2394
+ }
2395
+ } catch {
2396
+ try {
2397
+ await browser.connect();
2398
+ } catch {
2399
+ }
2400
+ }
2401
+ }
2402
+ return `Login wait timed out after 120s. The user may still need to log in at ${siteName}.`;
2403
+ }
2278
2404
  async function ensureConnected(browser, tabIndex) {
2279
2405
  if (browser.isConnected() && tabIndex === void 0) return;
2280
2406
  if (!await browser.isAvailable()) {
@@ -2316,9 +2442,15 @@ async function executeTool(name, input) {
2316
2442
  await ensureConnected(browser, input.tab_index);
2317
2443
  return browser.isConnected() ? "Connected to browser." : "Failed to connect.";
2318
2444
  }
2319
- case "browser_navigate":
2445
+ case "browser_navigate": {
2320
2446
  await ensureConnected(browser);
2321
- return browser.navigate(input.url);
2447
+ const navResult = await browser.navigate(input.url);
2448
+ const loginResult = await detectAndHandleLogin(browser);
2449
+ if (loginResult) {
2450
+ return navResult + "\n\n" + loginResult;
2451
+ }
2452
+ return navResult;
2453
+ }
2322
2454
  case "browser_read_page":
2323
2455
  await ensureConnected(browser);
2324
2456
  return browser.readPage();
@@ -2832,7 +2964,9 @@ var BASE_SYSTEM_PROMPT = `You are AssistMe, an AI assistant that operates like a
2832
2964
  KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This means:
2833
2965
  - The browser has the user's real cookies, logins, and sessions
2834
2966
  - When you navigate to amazon.com, you see the user's logged-in Amazon
2835
- - If a site needs login, ask the user to log in using browser_request_user_action
2967
+ - If a site needs login, the browser will auto-detect the login page and prompt the user
2968
+ - After the user logs in, their session is saved in the persistent browser profile (~/.assistme/browser-profile)
2969
+ - Saved sessions persist across assistme restarts \u2014 the user only needs to log in once per site
2836
2970
  - You are like a human assistant sitting at the user's computer
2837
2971
  - Chrome is automatically managed \u2014 just call browser_connect and it will auto-launch if needed
2838
2972
  - NEVER ask the user to manually start Chrome or run any terminal commands for browser setup
@@ -2859,9 +2993,9 @@ Available capabilities:
2859
2993
  Workflow for web tasks (e.g. "\u67E5\u4E00\u4E0B kindle \u6700\u65B0\u6B3E\u4EF7\u683C"):
2860
2994
  1. browser_connect \u2192 connect to user's Chrome
2861
2995
  2. browser_new_tab \u2192 open a new tab
2862
- 3. browser_navigate \u2192 go to the website
2996
+ 3. browser_navigate \u2192 go to the website (login pages are auto-detected \u2014 the user will be prompted and their session saved)
2863
2997
  4. browser_read_page or browser_screenshot \u2192 read the content
2864
- 5. If login required \u2192 browser_request_user_action \u2192 wait \u2192 continue
2998
+ 5. If login is needed but not auto-detected \u2192 use browser_request_user_action to ask the user
2865
2999
  6. Repeat across multiple sites as needed
2866
3000
  7. Summarize findings
2867
3001
 
@@ -2869,15 +3003,20 @@ Guidelines:
2869
3003
  - Always use the real browser for web tasks, never try to fetch URLs programmatically
2870
3004
  - Use browser_screenshot when you need to see the visual layout
2871
3005
  - Use browser_get_elements to find clickable elements before clicking
2872
- - If a page needs authentication, use browser_request_user_action immediately
3006
+ - Login pages are auto-detected after navigation \u2014 the user is prompted and sessions are saved automatically
3007
+ - If auto-detection misses a login page, use browser_request_user_action manually
2873
3008
  - Be thorough: check multiple sources when comparing prices/products
2874
3009
  - Summarize results clearly at the end
2875
3010
  - When you learn something about the user (preferences, habits), use memory_store to remember it
2876
3011
 
2877
3012
  Workspace path: {workspace_path}`;
3013
+ var MAX_HISTORY_ENTRIES = 10;
3014
+ var MAX_RESPONSE_LENGTH = 1500;
2878
3015
  var TaskProcessor = class {
2879
3016
  memoryManager = null;
2880
3017
  skillManager;
3018
+ /** In-memory conversation history, keyed by conversation_id */
3019
+ historyCache = /* @__PURE__ */ new Map();
2881
3020
  constructor() {
2882
3021
  this.skillManager = new SkillManager();
2883
3022
  this.skillManager.load();
@@ -2916,6 +3055,28 @@ var TaskProcessor = class {
2916
3055
  for (const s of matchedSkills) {
2917
3056
  usedSkillNames.push(s.name);
2918
3057
  }
3058
+ let history = [];
3059
+ try {
3060
+ history = await getConversationHistory(task.conversation_id, task.id, MAX_HISTORY_ENTRIES);
3061
+ } catch {
3062
+ log.debug("DB conversation history unavailable, using in-memory cache");
3063
+ }
3064
+ if (history.length === 0) {
3065
+ history = this.historyCache.get(task.conversation_id) || [];
3066
+ }
3067
+ if (history.length > 0) {
3068
+ log.info(`Loaded ${history.length} message(s) from conversation history`);
3069
+ let historyPrompt = "\n\n## Conversation History\nThe following is the history of previous messages in this conversation. Use this context to maintain continuity and understand references to earlier tasks.\n\n";
3070
+ for (const entry of history) {
3071
+ historyPrompt += `User: ${entry.prompt}
3072
+ `;
3073
+ const truncated = entry.response.length > MAX_RESPONSE_LENGTH ? entry.response.slice(0, MAX_RESPONSE_LENGTH) + "\u2026" : entry.response;
3074
+ historyPrompt += `Assistant: ${truncated}
3075
+
3076
+ `;
3077
+ }
3078
+ systemPrompt += historyPrompt;
3079
+ }
2919
3080
  const browserServer = createBrowserMcpServer();
2920
3081
  const agentToolsServer = createAgentToolsServer({
2921
3082
  memoryManager: this.memoryManager,
@@ -3037,6 +3198,12 @@ var TaskProcessor = class {
3037
3198
  });
3038
3199
  await emitEvent(task.id, "status_change", { status: "completed" });
3039
3200
  log.success("Task completed.");
3201
+ const convHistory = this.historyCache.get(task.conversation_id) || [];
3202
+ convHistory.push({ prompt: task.prompt, response: finalResponse });
3203
+ if (convHistory.length > MAX_HISTORY_ENTRIES * 2) {
3204
+ convHistory.splice(0, convHistory.length - MAX_HISTORY_ENTRIES * 2);
3205
+ }
3206
+ this.historyCache.set(task.conversation_id, convHistory);
3040
3207
  if (this.memoryManager && finalResponse) {
3041
3208
  const mm = this.memoryManager;
3042
3209
  const taskIdRef = task.id;
@@ -3437,7 +3604,7 @@ program.command("start", { isDefault: true }).description("Start the agent and l
3437
3604
  program.command("status").description("Check the status of the current agent session").action(async () => {
3438
3605
  try {
3439
3606
  const userId = await getCurrentUserId();
3440
- const { getSupabase: getSupabase2 } = await import("./supabase-QU7MFNDI.js");
3607
+ const { getSupabase: getSupabase2 } = await import("./supabase-5QTM5WBI.js");
3441
3608
  const sb = getSupabase2();
3442
3609
  const { data: sessions } = await sb.from("agent_sessions").select("*").eq("user_id", userId).in("status", ["online", "busy"]).order("started_at", { ascending: false }).limit(5);
3443
3610
  if (!sessions || sessions.length === 0) {
@@ -9,6 +9,7 @@ import {
9
9
  emitEvents,
10
10
  endSession,
11
11
  failTask,
12
+ getConversationHistory,
12
13
  getCurrentUserId,
13
14
  getOrCreateCliConversation,
14
15
  getSupabase,
@@ -19,7 +20,7 @@ import {
19
20
  resetEventSequence,
20
21
  setSessionBusy,
21
22
  updateHeartbeat
22
- } from "./chunk-QXT7DH44.js";
23
+ } from "./chunk-ERK6A6GH.js";
23
24
  export {
24
25
  CLI_AGENT_ID,
25
26
  DAYBOX_AGENT_ID,
@@ -31,6 +32,7 @@ export {
31
32
  emitEvents,
32
33
  endSession,
33
34
  failTask,
35
+ getConversationHistory,
34
36
  getCurrentUserId,
35
37
  getOrCreateCliConversation,
36
38
  getSupabase,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistme",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "AssistMe CLI Agent - AI-powered assistant that controls your real browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,10 +8,12 @@ import {
8
8
  } from "@anthropic-ai/claude-agent-sdk";
9
9
  import {
10
10
  type AgentTask,
11
+ type HistoryEntry,
11
12
  completeTask,
12
13
  failTask,
13
14
  emitEvent,
14
15
  resetEventSequence,
16
+ getConversationHistory,
15
17
  } from "../db/supabase.js";
16
18
  import { getConfig } from "../utils/config.js";
17
19
  import { log, newCorrelationId, setCorrelationId } from "../utils/logger.js";
@@ -37,7 +39,9 @@ const BASE_SYSTEM_PROMPT = `You are AssistMe, an AI assistant that operates like
37
39
  KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This means:
38
40
  - The browser has the user's real cookies, logins, and sessions
39
41
  - When you navigate to amazon.com, you see the user's logged-in Amazon
40
- - If a site needs login, ask the user to log in using browser_request_user_action
42
+ - If a site needs login, the browser will auto-detect the login page and prompt the user
43
+ - After the user logs in, their session is saved in the persistent browser profile (~/.assistme/browser-profile)
44
+ - Saved sessions persist across assistme restarts — the user only needs to log in once per site
41
45
  - You are like a human assistant sitting at the user's computer
42
46
  - Chrome is automatically managed — just call browser_connect and it will auto-launch if needed
43
47
  - NEVER ask the user to manually start Chrome or run any terminal commands for browser setup
@@ -64,9 +68,9 @@ Available capabilities:
64
68
  Workflow for web tasks (e.g. "查一下 kindle 最新款价格"):
65
69
  1. browser_connect → connect to user's Chrome
66
70
  2. browser_new_tab → open a new tab
67
- 3. browser_navigate → go to the website
71
+ 3. browser_navigate → go to the website (login pages are auto-detected — the user will be prompted and their session saved)
68
72
  4. browser_read_page or browser_screenshot → read the content
69
- 5. If login required → browser_request_user_action wait continue
73
+ 5. If login is needed but not auto-detected use browser_request_user_action to ask the user
70
74
  6. Repeat across multiple sites as needed
71
75
  7. Summarize findings
72
76
 
@@ -74,16 +78,22 @@ Guidelines:
74
78
  - Always use the real browser for web tasks, never try to fetch URLs programmatically
75
79
  - Use browser_screenshot when you need to see the visual layout
76
80
  - Use browser_get_elements to find clickable elements before clicking
77
- - If a page needs authentication, use browser_request_user_action immediately
81
+ - Login pages are auto-detected after navigation the user is prompted and sessions are saved automatically
82
+ - If auto-detection misses a login page, use browser_request_user_action manually
78
83
  - Be thorough: check multiple sources when comparing prices/products
79
84
  - Summarize results clearly at the end
80
85
  - When you learn something about the user (preferences, habits), use memory_store to remember it
81
86
 
82
87
  Workspace path: {workspace_path}`;
83
88
 
89
+ const MAX_HISTORY_ENTRIES = 10;
90
+ const MAX_RESPONSE_LENGTH = 1500;
91
+
84
92
  export class TaskProcessor {
85
93
  private memoryManager: MemoryManager | null = null;
86
94
  private skillManager: SkillManager;
95
+ /** In-memory conversation history, keyed by conversation_id */
96
+ private historyCache: Map<string, HistoryEntry[]> = new Map();
87
97
 
88
98
  constructor() {
89
99
  this.skillManager = new SkillManager();
@@ -143,6 +153,34 @@ export class TaskProcessor {
143
153
  usedSkillNames.push(s.name);
144
154
  }
145
155
 
156
+ // Inject conversation history for multi-turn context
157
+ let history: HistoryEntry[] = [];
158
+ try {
159
+ history = await getConversationHistory(task.conversation_id, task.id, MAX_HISTORY_ENTRIES);
160
+ } catch {
161
+ log.debug("DB conversation history unavailable, using in-memory cache");
162
+ }
163
+
164
+ // Fall back to in-memory cache if DB returned nothing
165
+ if (history.length === 0) {
166
+ history = this.historyCache.get(task.conversation_id) || [];
167
+ }
168
+
169
+ if (history.length > 0) {
170
+ log.info(`Loaded ${history.length} message(s) from conversation history`);
171
+ let historyPrompt =
172
+ "\n\n## Conversation History\nThe following is the history of previous messages in this conversation. Use this context to maintain continuity and understand references to earlier tasks.\n\n";
173
+ for (const entry of history) {
174
+ historyPrompt += `User: ${entry.prompt}\n`;
175
+ const truncated =
176
+ entry.response.length > MAX_RESPONSE_LENGTH
177
+ ? entry.response.slice(0, MAX_RESPONSE_LENGTH) + "…"
178
+ : entry.response;
179
+ historyPrompt += `Assistant: ${truncated}\n\n`;
180
+ }
181
+ systemPrompt += historyPrompt;
182
+ }
183
+
146
184
  // Create MCP servers for custom tools
147
185
  const browserServer = createBrowserMcpServer();
148
186
  const agentToolsServer = createAgentToolsServer({
@@ -288,6 +326,15 @@ export class TaskProcessor {
288
326
  await emitEvent(task.id, "status_change", { status: "completed" });
289
327
  log.success("Task completed.");
290
328
 
329
+ // Save to in-memory conversation history cache
330
+ const convHistory = this.historyCache.get(task.conversation_id) || [];
331
+ convHistory.push({ prompt: task.prompt, response: finalResponse });
332
+ // Keep only the most recent entries
333
+ if (convHistory.length > MAX_HISTORY_ENTRIES * 2) {
334
+ convHistory.splice(0, convHistory.length - MAX_HISTORY_ENTRIES * 2);
335
+ }
336
+ this.historyCache.set(task.conversation_id, convHistory);
337
+
291
338
  // ── Post-task extraction (fire-and-forget, non-blocking) ──────
292
339
 
293
340
  // Auto-extract memories using LLM
@@ -25,11 +25,7 @@ export interface ScheduledTask {
25
25
  * Supports: minute hour day-of-month month day-of-week
26
26
  * Examples: "0 8 * * *" (daily 8am), "0,30 * * * *" (every 30min)
27
27
  */
28
- export function getNextRunTime(
29
- cronExpr: string,
30
- timezone: string,
31
- fromDate?: Date
32
- ): Date {
28
+ export function getNextRunTime(cronExpr: string, timezone: string, fromDate?: Date): Date {
33
29
  const now = fromDate || new Date();
34
30
  const parts = cronExpr.trim().split(/\s+/);
35
31
  if (parts.length !== 5) {
@@ -65,6 +61,8 @@ export function getNextRunTime(
65
61
  const months = parseField(monExpr, 1, 12);
66
62
  const daysOfWeek = parseField(dowExpr, 0, 6); // 0 = Sunday
67
63
 
64
+ const useUTC = timezone === "UTC";
65
+
68
66
  // Find the next matching time after 'now'
69
67
  const candidate = new Date(now.getTime() + 60_000); // Start from next minute
70
68
  candidate.setSeconds(0, 0);
@@ -72,11 +70,11 @@ export function getNextRunTime(
72
70
  // Search up to 366 days ahead
73
71
  for (let i = 0; i < 527040; i++) {
74
72
  // 366 * 24 * 60
75
- const m = candidate.getMinutes();
76
- const h = candidate.getHours();
77
- const dom = candidate.getDate();
78
- const mon = candidate.getMonth() + 1;
79
- const dow = candidate.getDay();
73
+ const m = useUTC ? candidate.getUTCMinutes() : candidate.getMinutes();
74
+ const h = useUTC ? candidate.getUTCHours() : candidate.getHours();
75
+ const dom = useUTC ? candidate.getUTCDate() : candidate.getDate();
76
+ const mon = (useUTC ? candidate.getUTCMonth() : candidate.getMonth()) + 1;
77
+ const dow = useUTC ? candidate.getUTCDay() : candidate.getDay();
80
78
 
81
79
  if (
82
80
  minutes.includes(m) &&
@@ -98,13 +96,9 @@ export function getNextRunTime(
98
96
  export class Scheduler {
99
97
  private timer: ReturnType<typeof setInterval> | null = null;
100
98
  private running = false;
101
- private onScheduledTask:
102
- | ((task: ScheduledTask) => Promise<void>)
103
- | null = null;
99
+ private onScheduledTask: ((task: ScheduledTask) => Promise<void>) | null = null;
104
100
 
105
- async start(
106
- onScheduledTask: (task: ScheduledTask) => Promise<void>
107
- ): Promise<void> {
101
+ async start(onScheduledTask: (task: ScheduledTask) => Promise<void>): Promise<void> {
108
102
  this.onScheduledTask = onScheduledTask;
109
103
  this.running = true;
110
104
 
@@ -187,16 +181,10 @@ export class Scheduler {
187
181
  try {
188
182
  await this.onScheduledTask(task);
189
183
 
190
- await sb
191
- .from("agent_scheduled_tasks")
192
- .update({ last_error: null })
193
- .eq("id", task.id);
184
+ await sb.from("agent_scheduled_tasks").update({ last_error: null }).eq("id", task.id);
194
185
  } catch (err) {
195
186
  const errMsg = err instanceof Error ? err.message : String(err);
196
- await sb
197
- .from("agent_scheduled_tasks")
198
- .update({ last_error: errMsg })
199
- .eq("id", task.id);
187
+ await sb.from("agent_scheduled_tasks").update({ last_error: errMsg }).eq("id", task.id);
200
188
  log.error(`Scheduled task "${task.name}" failed: ${errMsg}`);
201
189
  }
202
190
  } catch (err) {
@@ -234,9 +222,7 @@ export async function createScheduledTask(
234
222
  return data as ScheduledTask;
235
223
  }
236
224
 
237
- export async function listScheduledTasks(
238
- userId: string
239
- ): Promise<ScheduledTask[]> {
225
+ export async function listScheduledTasks(userId: string): Promise<ScheduledTask[]> {
240
226
  const sb = getSupabase();
241
227
  const { data, error } = await sb
242
228
  .from("agent_scheduled_tasks")
@@ -248,10 +234,7 @@ export async function listScheduledTasks(
248
234
  return (data || []) as ScheduledTask[];
249
235
  }
250
236
 
251
- export async function toggleScheduledTask(
252
- taskId: string,
253
- enabled: boolean
254
- ): Promise<void> {
237
+ export async function toggleScheduledTask(taskId: string, enabled: boolean): Promise<void> {
255
238
  const sb = getSupabase();
256
239
  const update: Record<string, unknown> = { enabled };
257
240
  if (enabled) {
@@ -267,20 +250,14 @@ export async function toggleScheduledTask(
267
250
  }
268
251
  }
269
252
 
270
- const { error } = await sb
271
- .from("agent_scheduled_tasks")
272
- .update(update)
273
- .eq("id", taskId);
253
+ const { error } = await sb.from("agent_scheduled_tasks").update(update).eq("id", taskId);
274
254
 
275
255
  if (error) throw new Error(`Failed to toggle schedule: ${error.message}`);
276
256
  }
277
257
 
278
258
  export async function deleteScheduledTask(taskId: string): Promise<void> {
279
259
  const sb = getSupabase();
280
- const { error } = await sb
281
- .from("agent_scheduled_tasks")
282
- .delete()
283
- .eq("id", taskId);
260
+ const { error } = await sb.from("agent_scheduled_tasks").delete().eq("id", taskId);
284
261
 
285
262
  if (error) throw new Error(`Failed to delete schedule: ${error.message}`);
286
263
  }
@@ -8,14 +8,28 @@ let finalResult: Record<string, unknown> = { data: [], error: null };
8
8
 
9
9
  const chain: Record<string, unknown> = {};
10
10
  const chainMethods = [
11
- "select", "insert", "update", "delete", "eq", "neq", "not",
12
- "or", "in", "order", "limit", "single", "from", "lt",
11
+ "select",
12
+ "insert",
13
+ "update",
14
+ "delete",
15
+ "eq",
16
+ "neq",
17
+ "not",
18
+ "or",
19
+ "in",
20
+ "order",
21
+ "limit",
22
+ "single",
23
+ "from",
24
+ "lt",
13
25
  ];
14
26
  for (const method of chainMethods) {
15
27
  chain[method] = vi.fn().mockImplementation(() => chain);
16
28
  }
17
29
  // Make chain thenable — always resolves to current finalResult
18
- chain.then = function (resolve: (value: unknown) => void) { return resolve(finalResult); };
30
+ chain.then = function (resolve: (value: unknown) => void) {
31
+ return resolve(finalResult);
32
+ };
19
33
  chain.single = vi.fn().mockImplementation(() => ({
20
34
  ...chain,
21
35
  then: (resolve: (value: unknown) => void) => resolve(finalResult),
@@ -48,7 +62,9 @@ const stableMockClient = {
48
62
 
49
63
  function setResult(result: Record<string, unknown>) {
50
64
  finalResult = result;
51
- chain.then = function (resolve: (value: unknown) => void) { return resolve(result); };
65
+ chain.then = function (resolve: (value: unknown) => void) {
66
+ return resolve(result);
67
+ };
52
68
  chain.single = vi.fn().mockImplementation(() => ({
53
69
  ...chain,
54
70
  then: (resolve: (value: unknown) => void) => resolve(result),
@@ -73,7 +89,10 @@ vi.mock("../utils/config.js", () => ({
73
89
 
74
90
  vi.mock("../utils/logger.js", () => ({
75
91
  log: {
76
- debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(),
92
+ debug: vi.fn(),
93
+ info: vi.fn(),
94
+ warn: vi.fn(),
95
+ error: vi.fn(),
77
96
  },
78
97
  }));
79
98
 
@@ -81,9 +100,9 @@ vi.mock("fs", async (importOriginal) => {
81
100
  const original = (await importOriginal()) as Record<string, unknown>;
82
101
  return {
83
102
  ...original,
84
- existsSync: vi.fn().mockReturnValue(false),
103
+ existsSync: vi.fn().mockReturnValue(true),
85
104
  mkdirSync: vi.fn(),
86
- readFileSync: vi.fn().mockReturnValue("{}"),
105
+ readFileSync: vi.fn().mockReturnValue(JSON.stringify({ mcp_token: "am_test_token_123" })),
87
106
  writeFileSync: vi.fn(),
88
107
  };
89
108
  });
@@ -131,7 +150,14 @@ describe("Supabase DB Layer", () => {
131
150
 
132
151
  const result = await createSession("user-123", "Test", "/tmp", "0.1.0");
133
152
 
134
- expect(mockFrom).toHaveBeenCalledWith("agent_sessions");
153
+ expect(mockRpc).toHaveBeenCalledWith(
154
+ "mcp_create_session",
155
+ expect.objectContaining({
156
+ p_session_name: "Test",
157
+ p_workspace_path: "/tmp",
158
+ p_version: "0.1.0",
159
+ })
160
+ );
135
161
  expect(result.id).toBe("sess-001");
136
162
  expect(result.status).toBe("online");
137
163
  });
@@ -139,9 +165,9 @@ describe("Supabase DB Layer", () => {
139
165
  it("throws on DB error", async () => {
140
166
  setResult({ data: null, error: { message: "insert failed" } });
141
167
 
142
- await expect(
143
- createSession("user-123", "Test", "/tmp", "0.1.0")
144
- ).rejects.toThrow("Failed to create session");
168
+ await expect(createSession("user-123", "Test", "/tmp", "0.1.0")).rejects.toThrow(
169
+ "Failed to create session"
170
+ );
145
171
  });
146
172
  });
147
173
 
@@ -149,8 +175,12 @@ describe("Supabase DB Layer", () => {
149
175
  it("updates the heartbeat timestamp", async () => {
150
176
  setResult({ error: null });
151
177
  await updateHeartbeat("sess-001");
152
- expect(mockFrom).toHaveBeenCalledWith("agent_sessions");
153
- expect(chain.eq).toHaveBeenCalledWith("id", "sess-001");
178
+ expect(mockRpc).toHaveBeenCalledWith(
179
+ "mcp_heartbeat",
180
+ expect.objectContaining({
181
+ p_session_id: "sess-001",
182
+ })
183
+ );
154
184
  });
155
185
  });
156
186
 
@@ -158,8 +188,12 @@ describe("Supabase DB Layer", () => {
158
188
  it("sets session to offline", async () => {
159
189
  setResult({ error: null });
160
190
  await endSession("sess-001");
161
- expect(mockFrom).toHaveBeenCalledWith("agent_sessions");
162
- expect(chain.update).toHaveBeenCalled();
191
+ expect(mockRpc).toHaveBeenCalledWith(
192
+ "mcp_end_session",
193
+ expect.objectContaining({
194
+ p_session_id: "sess-001",
195
+ })
196
+ );
163
197
  });
164
198
  });
165
199
 
@@ -167,7 +201,13 @@ describe("Supabase DB Layer", () => {
167
201
  it("sets session to busy", async () => {
168
202
  setResult({ error: null });
169
203
  await setSessionBusy("sess-001", true);
170
- expect(chain.update).toHaveBeenCalled();
204
+ expect(mockRpc).toHaveBeenCalledWith(
205
+ "mcp_set_session_busy",
206
+ expect.objectContaining({
207
+ p_session_id: "sess-001",
208
+ p_busy: true,
209
+ })
210
+ );
171
211
  });
172
212
  });
173
213
 
@@ -208,9 +248,12 @@ describe("Supabase DB Layer", () => {
208
248
  it("updates status to running", async () => {
209
249
  setResult({ error: null });
210
250
  await claimTask("msg-001");
211
- expect(mockFrom).toHaveBeenCalledWith("conversation_messages");
212
- expect(chain.eq).toHaveBeenCalledWith("id", "msg-001");
213
- expect(chain.eq).toHaveBeenCalledWith("status", "pending");
251
+ expect(mockRpc).toHaveBeenCalledWith(
252
+ "mcp_claim_task",
253
+ expect.objectContaining({
254
+ p_message_id: "msg-001",
255
+ })
256
+ );
214
257
  });
215
258
 
216
259
  it("throws on DB error", async () => {
@@ -223,7 +266,13 @@ describe("Supabase DB Layer", () => {
223
266
  it("updates status to completed with result", async () => {
224
267
  setResult({ data: { metadata: {} }, error: null });
225
268
  await completeTask("msg-001", "Task completed successfully");
226
- expect(mockFrom).toHaveBeenCalledWith("conversation_messages");
269
+ expect(mockRpc).toHaveBeenCalledWith(
270
+ "mcp_complete_task",
271
+ expect.objectContaining({
272
+ p_message_id: "msg-001",
273
+ p_result: "Task completed successfully",
274
+ })
275
+ );
227
276
  });
228
277
  });
229
278
 
@@ -231,7 +280,13 @@ describe("Supabase DB Layer", () => {
231
280
  it("updates status to failed with error", async () => {
232
281
  setResult({ data: { metadata: {} }, error: null });
233
282
  await failTask("msg-001", "Something went wrong");
234
- expect(mockFrom).toHaveBeenCalledWith("conversation_messages");
283
+ expect(mockRpc).toHaveBeenCalledWith(
284
+ "mcp_fail_task",
285
+ expect.objectContaining({
286
+ p_message_id: "msg-001",
287
+ p_error: "Something went wrong",
288
+ })
289
+ );
235
290
  });
236
291
  });
237
292
 
@@ -242,42 +297,44 @@ describe("Supabase DB Layer", () => {
242
297
  await emitEvent("msg-001", "text_delta", { text: "hello" });
243
298
  await emitEvent("msg-001", "text_delta", { text: "world" });
244
299
 
245
- expect(mockFrom).toHaveBeenCalledWith("message_events");
300
+ expect(mockRpc).toHaveBeenCalledWith(
301
+ "mcp_emit_event",
302
+ expect.objectContaining({
303
+ p_message_id: "msg-001",
304
+ p_event_type: "text_delta",
305
+ })
306
+ );
246
307
  });
247
308
  });
248
309
 
249
310
  describe("loginWithToken()", () => {
250
- it("decodes base64 token and sets session", async () => {
251
- const token = Buffer.from(
252
- JSON.stringify({
253
- access_token: "test-access",
254
- refresh_token: "test-refresh",
255
- })
256
- ).toString("base64");
311
+ it("validates am_ token via RPC and returns user ID", async () => {
312
+ setResult({ data: [{ out_user_id: "user-123" }], error: null });
257
313
 
258
- const userId = await loginWithToken(token);
314
+ const userId = await loginWithToken("am_valid_test_token");
259
315
  expect(userId).toBe("user-123");
316
+ expect(mockRpc).toHaveBeenCalledWith(
317
+ "validate_mcp_token",
318
+ expect.objectContaining({
319
+ p_token_hash: expect.any(String),
320
+ })
321
+ );
260
322
  });
261
323
 
262
- it("throws on invalid token format", async () => {
263
- await expect(loginWithToken("not-base64")).rejects.toThrow(
264
- "Invalid token format"
265
- );
324
+ it("throws on invalid token format (no am_ prefix)", async () => {
325
+ await expect(loginWithToken("not-am-token")).rejects.toThrow("Invalid token format");
266
326
  });
267
327
 
268
- it("throws on missing token fields", async () => {
269
- const token = Buffer.from(
270
- JSON.stringify({ access_token: "only-one" })
271
- ).toString("base64");
328
+ it("throws on RPC validation failure", async () => {
329
+ setResult({ data: null, error: { message: "validation failed" } });
272
330
 
273
- await expect(loginWithToken(token)).rejects.toThrow(
274
- "Invalid token format"
275
- );
331
+ await expect(loginWithToken("am_bad_token")).rejects.toThrow("Token validation failed");
276
332
  });
277
333
  });
278
334
 
279
335
  describe("getCurrentUserId()", () => {
280
336
  it("returns user ID when authenticated", async () => {
337
+ setResult({ data: [{ out_user_id: "user-123" }], error: null });
281
338
  const userId = await getCurrentUserId();
282
339
  expect(userId).toBe("user-123");
283
340
  });
@@ -329,6 +329,55 @@ export async function failTask(
329
329
  if (error) log.error(`Failed to update task status: ${error.message}`);
330
330
  }
331
331
 
332
+ // ── Conversation History ─────────────────────────────────────────────
333
+
334
+ export interface HistoryEntry {
335
+ prompt: string;
336
+ response: string;
337
+ }
338
+
339
+ /**
340
+ * Fetch completed messages from a conversation to build history context.
341
+ * Returns messages in chronological order (oldest first).
342
+ */
343
+ export async function getConversationHistory(
344
+ conversationId: string,
345
+ excludeMessageId: string,
346
+ limit: number = 20
347
+ ): Promise<HistoryEntry[]> {
348
+ const sb = getSupabase();
349
+
350
+ const { data, error } = await sb
351
+ .from("conversation_messages")
352
+ .select("id, role, content, status, metadata, created_at")
353
+ .eq("conversation_id", conversationId)
354
+ .in("status", ["completed", "failed"])
355
+ .neq("id", excludeMessageId)
356
+ .order("created_at", { ascending: false })
357
+ .limit(limit);
358
+
359
+ if (error) {
360
+ log.debug(`Failed to fetch conversation history: ${error.message}`);
361
+ return [];
362
+ }
363
+
364
+ const rows = (data || []) as Array<Record<string, unknown>>;
365
+
366
+ return rows
367
+ .reverse() // chronological order (oldest first)
368
+ .map((row) => {
369
+ const prompt =
370
+ ((row.metadata as Record<string, unknown>)?.prompt as string) || "";
371
+ const content = (row.content as string) || "";
372
+ const response =
373
+ row.status === "failed"
374
+ ? `[Task failed] ${content}`
375
+ : content;
376
+ return { prompt, response };
377
+ })
378
+ .filter((entry) => entry.prompt && entry.response);
379
+ }
380
+
332
381
  // ── Event Streaming ─────────────────────────────────────────────────
333
382
 
334
383
  export type EventType =
@@ -583,6 +583,79 @@ export class BrowserController {
583
583
  isConnected(): boolean {
584
584
  return this.connected && this.ws?.readyState === WebSocket.OPEN;
585
585
  }
586
+
587
+ // ── Login Detection ────────────────────────────────────────────
588
+
589
+ /**
590
+ * Detect if the current page appears to be a login/authentication page.
591
+ * Checks URL patterns, password input fields, and login form actions.
592
+ */
593
+ async detectLoginPage(): Promise<{ isLoginPage: boolean; reason: string }> {
594
+ try {
595
+ const result = await this.send("Runtime.evaluate", {
596
+ expression: `
597
+ (function() {
598
+ var url = window.location.href.toLowerCase();
599
+
600
+ // URL-based detection
601
+ var loginPatterns = [
602
+ '/login', '/signin', '/sign-in', '/sign_in',
603
+ '/auth/', '/sso/', '/oauth/', '/session/new',
604
+ '/accounts/login', '/users/sign_in',
605
+ 'accounts.google.com', 'login.microsoftonline.com',
606
+ 'github.com/login', 'github.com/session',
607
+ 'login.live.com', 'appleid.apple.com'
608
+ ];
609
+ for (var i = 0; i < loginPatterns.length; i++) {
610
+ if (url.indexOf(loginPatterns[i]) !== -1) {
611
+ return JSON.stringify({
612
+ isLoginPage: true,
613
+ reason: 'URL contains login pattern: ' + loginPatterns[i]
614
+ });
615
+ }
616
+ }
617
+
618
+ // Password input detection (visible only)
619
+ var passwordInputs = document.querySelectorAll('input[type="password"]');
620
+ for (var j = 0; j < passwordInputs.length; j++) {
621
+ var input = passwordInputs[j];
622
+ var rect = input.getBoundingClientRect();
623
+ var style = window.getComputedStyle(input);
624
+ if (rect.width > 0 && rect.height > 0 &&
625
+ style.display !== 'none' && style.visibility !== 'hidden') {
626
+ return JSON.stringify({
627
+ isLoginPage: true,
628
+ reason: 'Page contains visible password input field'
629
+ });
630
+ }
631
+ }
632
+
633
+ // Login form action detection
634
+ var formSelectors = [
635
+ 'form[action*="login"]', 'form[action*="signin"]',
636
+ 'form[action*="session"]', 'form[action*="auth"]',
637
+ 'form[action*="authenticate"]'
638
+ ];
639
+ var loginForms = document.querySelectorAll(formSelectors.join(','));
640
+ if (loginForms.length > 0) {
641
+ return JSON.stringify({
642
+ isLoginPage: true,
643
+ reason: 'Page contains login form'
644
+ });
645
+ }
646
+
647
+ return JSON.stringify({ isLoginPage: false, reason: '' });
648
+ })()
649
+ `,
650
+ returnByValue: true,
651
+ });
652
+
653
+ const value = (result as CDPEvalResult).result?.value as string;
654
+ return JSON.parse(value || '{"isLoginPage":false,"reason":""}');
655
+ } catch {
656
+ return { isLoginPage: false, reason: "" };
657
+ }
658
+ }
586
659
  }
587
660
 
588
661
  // ── Chrome Auto-Launch ──────────────────────────────────────────────
@@ -270,6 +270,81 @@ export function getToolDefinitions(): ToolDefinition[] {
270
270
  ];
271
271
  }
272
272
 
273
+ /**
274
+ * After navigation, detect if the page requires login and automatically prompt
275
+ * the user to log in. Waits for login completion and returns context about the
276
+ * login result. Returns null if no login was needed.
277
+ *
278
+ * The browser uses a persistent profile (~/.assistme/browser-profile), so once
279
+ * the user logs in, their session is saved and reused in future sessions.
280
+ */
281
+ async function detectAndHandleLogin(browser: BrowserController): Promise<string | null> {
282
+ const detection = await browser.detectLoginPage();
283
+ if (!detection.isLoginPage) return null;
284
+
285
+ const pageInfo = await browser.getPageInfo();
286
+ let siteName: string;
287
+ try {
288
+ siteName = new URL(pageInfo.url).hostname;
289
+ } catch {
290
+ siteName = pageInfo.url;
291
+ }
292
+
293
+ // Notify user via terminal
294
+ console.log("\n");
295
+ console.log("\u2501".repeat(60));
296
+ console.log(" \uD83D\uDD10 LOGIN REQUIRED");
297
+ console.log("\u2501".repeat(60));
298
+ console.log(` ${detection.reason}`);
299
+ console.log(` Site: ${siteName}`);
300
+ console.log(` Please log in using the browser window.`);
301
+ console.log(` Your session will be saved for future use.`);
302
+ console.log(` (Waiting up to 120s for you to complete login)`);
303
+ console.log("\u2501".repeat(60));
304
+ console.log("\n");
305
+
306
+ // Wait for user to log in — poll for URL change or login form disappearing
307
+ const loginUrl = pageInfo.url;
308
+ const deadline = Date.now() + 120_000;
309
+
310
+ while (Date.now() < deadline) {
311
+ await new Promise((r) => setTimeout(r, 3000));
312
+ try {
313
+ const currentInfo = await browser.getPageInfo();
314
+ // URL changed = user likely logged in and was redirected
315
+ if (currentInfo.url !== loginUrl) {
316
+ // Give the page a moment to settle after redirect
317
+ await new Promise((r) => setTimeout(r, 1500));
318
+ const newPage = await browser.readPage();
319
+ return (
320
+ `Login completed. Redirected to: ${currentInfo.url}\n` +
321
+ `Session saved in assistme browser profile for future use.\n\n` +
322
+ `Current page:\n${newPage.slice(0, 3000)}`
323
+ );
324
+ }
325
+ // Check if login form disappeared (user logged in on same page)
326
+ const stillLogin = await browser.detectLoginPage();
327
+ if (!stillLogin.isLoginPage) {
328
+ const newPage = await browser.readPage();
329
+ return (
330
+ `Login completed on ${siteName}.\n` +
331
+ `Session saved in assistme browser profile for future use.\n\n` +
332
+ `Current page:\n${newPage.slice(0, 3000)}`
333
+ );
334
+ }
335
+ } catch {
336
+ // Connection may drop during navigation — reconnect
337
+ try {
338
+ await browser.connect();
339
+ } catch {
340
+ /* best effort */
341
+ }
342
+ }
343
+ }
344
+
345
+ return `Login wait timed out after 120s. The user may still need to log in at ${siteName}.`;
346
+ }
347
+
273
348
  /**
274
349
  * Ensure Chrome is running with CDP and we have an active WebSocket connection.
275
350
  * Called lazily by every browser tool so the user never has to call browser_connect explicitly.
@@ -320,9 +395,18 @@ export async function executeTool(name: string, input: Record<string, unknown>):
320
395
  await ensureConnected(browser, input.tab_index as number | undefined);
321
396
  return browser.isConnected() ? "Connected to browser." : "Failed to connect.";
322
397
  }
323
- case "browser_navigate":
398
+ case "browser_navigate": {
324
399
  await ensureConnected(browser);
325
- return browser.navigate(input.url as string);
400
+ const navResult = await browser.navigate(input.url as string);
401
+
402
+ // Auto-detect login pages and prompt user if needed
403
+ const loginResult = await detectAndHandleLogin(browser);
404
+ if (loginResult) {
405
+ return navResult + "\n\n" + loginResult;
406
+ }
407
+
408
+ return navResult;
409
+ }
326
410
  case "browser_read_page":
327
411
  await ensureConnected(browser);
328
412
  return browser.readPage();
@@ -28,9 +28,7 @@ vi.mock("conf", () => {
28
28
  };
29
29
  });
30
30
 
31
- const { getConfig, setConfig, clearConfig, getConfigPath } = await import(
32
- "./config.js"
33
- );
31
+ const { getConfig, setConfig, clearConfig, getConfigPath } = await import("./config.js");
34
32
 
35
33
  describe("config module", () => {
36
34
  const originalEnv = { ...process.env };
@@ -74,10 +72,10 @@ describe("config module", () => {
74
72
  expect(config.supabaseUrl).toBe("https://config.supabase.co");
75
73
  });
76
74
 
77
- it("returns empty string for unset values", () => {
75
+ it("returns defaults for supabase and empty string for unset optional values", () => {
78
76
  const config = getConfig();
79
- expect(config.supabaseUrl).toBe("");
80
- expect(config.supabaseAnonKey).toBe("");
77
+ expect(config.supabaseUrl).toContain("supabase.co");
78
+ expect(config.supabaseAnonKey).toBeTruthy();
81
79
  expect(config.anthropicApiKey).toBe("");
82
80
  });
83
81