assistme 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -63,14 +63,15 @@ function getNextRunTime(cronExpr, timezone, fromDate) {
63
63
  const daysOfMonth = parseField(domExpr, 1, 31);
64
64
  const months = parseField(monExpr, 1, 12);
65
65
  const daysOfWeek = parseField(dowExpr, 0, 6);
66
+ const useUTC = timezone === "UTC";
66
67
  const candidate = new Date(now.getTime() + 6e4);
67
68
  candidate.setSeconds(0, 0);
68
69
  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();
70
+ const m = useUTC ? candidate.getUTCMinutes() : candidate.getMinutes();
71
+ const h = useUTC ? candidate.getUTCHours() : candidate.getHours();
72
+ const dom = useUTC ? candidate.getUTCDate() : candidate.getDate();
73
+ const mon = (useUTC ? candidate.getUTCMonth() : candidate.getMonth()) + 1;
74
+ const dow = useUTC ? candidate.getUTCDay() : candidate.getDay();
74
75
  if (minutes.includes(m) && hours.includes(h) && daysOfMonth.includes(dom) && months.includes(mon) && (dowExpr === "*" || daysOfWeek.includes(dow))) {
75
76
  return candidate;
76
77
  }
@@ -393,8 +394,9 @@ import {
393
394
  // src/tools/browser.ts
394
395
  import { WebSocket } from "ws";
395
396
  import { execSync, spawn } from "child_process";
396
- import { platform } from "os";
397
- import { existsSync, unlinkSync } from "fs";
397
+ import { platform, homedir } from "os";
398
+ import { existsSync, unlinkSync, mkdirSync, cpSync } from "fs";
399
+ import { join } from "path";
398
400
  var BrowserController = class {
399
401
  ws = null;
400
402
  debugPort;
@@ -848,6 +850,76 @@ URL: ${info.url}`;
848
850
  isConnected() {
849
851
  return this.connected && this.ws?.readyState === WebSocket.OPEN;
850
852
  }
853
+ // ── Login Detection ────────────────────────────────────────────
854
+ /**
855
+ * Detect if the current page appears to be a login/authentication page.
856
+ * Checks URL patterns, password input fields, and login form actions.
857
+ */
858
+ async detectLoginPage() {
859
+ try {
860
+ const result = await this.send("Runtime.evaluate", {
861
+ expression: `
862
+ (function() {
863
+ var url = window.location.href.toLowerCase();
864
+
865
+ // URL-based detection
866
+ var loginPatterns = [
867
+ '/login', '/signin', '/sign-in', '/sign_in',
868
+ '/auth/', '/sso/', '/oauth/', '/session/new',
869
+ '/accounts/login', '/users/sign_in',
870
+ 'accounts.google.com', 'login.microsoftonline.com',
871
+ 'github.com/login', 'github.com/session',
872
+ 'login.live.com', 'appleid.apple.com'
873
+ ];
874
+ for (var i = 0; i < loginPatterns.length; i++) {
875
+ if (url.indexOf(loginPatterns[i]) !== -1) {
876
+ return JSON.stringify({
877
+ isLoginPage: true,
878
+ reason: 'URL contains login pattern: ' + loginPatterns[i]
879
+ });
880
+ }
881
+ }
882
+
883
+ // Password input detection (visible only)
884
+ var passwordInputs = document.querySelectorAll('input[type="password"]');
885
+ for (var j = 0; j < passwordInputs.length; j++) {
886
+ var input = passwordInputs[j];
887
+ var rect = input.getBoundingClientRect();
888
+ var style = window.getComputedStyle(input);
889
+ if (rect.width > 0 && rect.height > 0 &&
890
+ style.display !== 'none' && style.visibility !== 'hidden') {
891
+ return JSON.stringify({
892
+ isLoginPage: true,
893
+ reason: 'Page contains visible password input field'
894
+ });
895
+ }
896
+ }
897
+
898
+ // Login form action detection
899
+ var formSelectors = [
900
+ 'form[action*="login"]', 'form[action*="signin"]',
901
+ 'form[action*="session"]', 'form[action*="auth"]',
902
+ 'form[action*="authenticate"]'
903
+ ];
904
+ var loginForms = document.querySelectorAll(formSelectors.join(','));
905
+ if (loginForms.length > 0) {
906
+ return JSON.stringify({
907
+ isLoginPage: true,
908
+ reason: 'Page contains login form'
909
+ });
910
+ }
911
+
912
+ return JSON.stringify({ isLoginPage: false, reason: '' });
913
+ })()
914
+ `,
915
+ returnByValue: true
916
+ });
917
+ const value = result.result?.value;
918
+ return JSON.parse(value || '{"isLoginPage":false,"reason":""}');
919
+ } catch {
920
+ return { isLoginPage: false, reason: "" };
921
+ }
922
+ }
851
923
  };
852
924
  function findChromePath() {
853
925
  const os = platform();
@@ -903,148 +975,97 @@ function findChromePath() {
903
975
  }
904
976
  return null;
905
977
  }
906
- function isChromeRunning(chromePath) {
907
- try {
908
- if (platform() === "win32") {
909
- const out2 = execSync(
910
- 'tasklist /FI "IMAGENAME eq chrome.exe" /FI "IMAGENAME eq msedge.exe" /FI "IMAGENAME eq brave.exe" /NH',
911
- { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
912
- );
913
- return /chrome\.exe|msedge\.exe|brave\.exe/i.test(out2);
914
- }
915
- if (platform() === "darwin") {
916
- if (chromePath) {
917
- const appDir = chromePath.replace(/\/Contents\/MacOS\/.*$/, "");
918
- const out3 = execSync(`pgrep -f ${JSON.stringify(appDir)}`, {
919
- encoding: "utf-8",
920
- stdio: ["pipe", "pipe", "pipe"]
921
- });
922
- return out3.trim().length > 0;
923
- }
924
- const out2 = execSync(
925
- 'pgrep -f "(Google Chrome|Microsoft Edge|Brave Browser|Chromium).app/Contents/MacOS/"',
926
- { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
927
- );
928
- return out2.trim().length > 0;
929
- }
930
- if (chromePath) {
931
- const out2 = execSync(`pgrep -f ${JSON.stringify(chromePath)} 2>/dev/null || true`, {
932
- encoding: "utf-8",
933
- stdio: ["pipe", "pipe", "pipe"]
934
- });
935
- return out2.trim().length > 0;
936
- }
937
- const out = execSync("pgrep -f '(chrome|chromium|msedge|brave)' 2>/dev/null || true", {
938
- encoding: "utf-8",
939
- stdio: ["pipe", "pipe", "pipe"]
940
- });
941
- return out.trim().length > 0;
942
- } catch {
943
- return false;
978
+ function getDefaultProfileDir(chromePath) {
979
+ const home = homedir();
980
+ const os = platform();
981
+ if (os === "darwin") {
982
+ if (chromePath.includes("Brave Browser"))
983
+ return join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser");
984
+ if (chromePath.includes("Microsoft Edge"))
985
+ return join(home, "Library", "Application Support", "Microsoft Edge");
986
+ if (chromePath.includes("Chromium"))
987
+ return join(home, "Library", "Application Support", "Chromium");
988
+ if (chromePath.includes("Canary"))
989
+ return join(home, "Library", "Application Support", "Google", "Chrome Canary");
990
+ return join(home, "Library", "Application Support", "Google", "Chrome");
944
991
  }
992
+ if (os === "win32") {
993
+ const appData = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
994
+ if (chromePath.includes("brave"))
995
+ return join(appData, "BraveSoftware", "Brave-Browser", "User Data");
996
+ if (chromePath.includes("msedge")) return join(appData, "Microsoft", "Edge", "User Data");
997
+ return join(appData, "Google", "Chrome", "User Data");
998
+ }
999
+ if (chromePath.includes("brave")) return join(home, ".config", "BraveSoftware", "Brave-Browser");
1000
+ if (chromePath.includes("microsoft-edge")) return join(home, ".config", "microsoft-edge");
1001
+ if (chromePath.includes("chromium")) return join(home, ".config", "chromium");
1002
+ return join(home, ".config", "google-chrome");
945
1003
  }
946
- function macAppName(chromePath) {
947
- if (chromePath.includes("Brave Browser")) return "Brave Browser";
948
- if (chromePath.includes("Microsoft Edge")) return "Microsoft Edge";
949
- if (chromePath.includes("Chromium")) return "Chromium";
950
- if (chromePath.includes("Canary")) return "Google Chrome Canary";
951
- return "Google Chrome";
1004
+ function getDebugProfileDir(chromePath) {
1005
+ const home = homedir();
1006
+ const debugDir = join(home, ".assistme", "browser-profile");
1007
+ if (!existsSync(debugDir)) {
1008
+ mkdirSync(debugDir, { recursive: true });
1009
+ log.debug(`Created debug profile directory: ${debugDir}`);
1010
+ const realDir = getDefaultProfileDir(chromePath);
1011
+ if (existsSync(realDir)) {
1012
+ seedDebugProfile(realDir, debugDir);
1013
+ }
1014
+ }
1015
+ return debugDir;
952
1016
  }
953
- async function killChromeGracefully(chromePath) {
954
- const os = platform();
955
- try {
956
- if (os === "darwin") {
957
- const app = macAppName(chromePath);
958
- execSync(`osascript -e 'quit app "${app}"'`, {
959
- timeout: 5e3,
960
- stdio: ["pipe", "pipe", "pipe"]
961
- });
962
- } else if (os === "linux") {
963
- execSync(`pkill -TERM -f ${JSON.stringify(chromePath)}`, {
964
- timeout: 5e3,
965
- stdio: ["pipe", "pipe", "pipe"]
966
- });
967
- } else if (os === "win32") {
968
- const exe = chromePath.split("\\").pop() || "chrome.exe";
969
- execSync(`taskkill /IM "${exe}"`, {
970
- timeout: 5e3,
971
- stdio: ["pipe", "pipe", "pipe"]
972
- });
973
- }
974
- } catch {
975
- }
976
- const start = Date.now();
977
- while (Date.now() - start < 8e3) {
978
- if (!isChromeRunning(chromePath)) {
979
- log.debug(`Browser exited after ${Date.now() - start}ms`);
980
- return;
981
- }
982
- await new Promise((r) => setTimeout(r, 500));
983
- }
984
- log.debug("Browser still running after graceful quit, force-killing...");
985
- try {
986
- if (os === "win32") {
987
- const exe = chromePath.split("\\").pop() || "chrome.exe";
988
- execSync(`taskkill /F /IM "${exe}"`, {
989
- stdio: ["pipe", "pipe", "pipe"]
990
- });
991
- } else {
992
- execSync(`pkill -9 -f ${JSON.stringify(chromePath)}`, {
993
- stdio: ["pipe", "pipe", "pipe"]
994
- });
1017
+ function seedDebugProfile(realDir, debugDir) {
1018
+ const rootFiles = ["Local State"];
1019
+ const profileFiles = ["Bookmarks", "Preferences", "Favicons", "Top Sites", "Shortcuts"];
1020
+ for (const file of rootFiles) {
1021
+ const src = join(realDir, file);
1022
+ const dest = join(debugDir, file);
1023
+ try {
1024
+ if (existsSync(src)) {
1025
+ cpSync(src, dest, { force: true });
1026
+ log.debug(`Seeded: ${file}`);
1027
+ }
1028
+ } catch {
995
1029
  }
996
- } catch {
997
1030
  }
998
- await new Promise((r) => setTimeout(r, 1e3));
999
- if (os !== "win32") {
1000
- const home = process.env.HOME;
1001
- if (home) {
1002
- const lockSuffixes = ["SingletonLock", "SingletonSocket", "SingletonCookie"];
1003
- const profileDirs = os === "darwin" ? [
1004
- `${home}/Library/Application Support/Google/Chrome`,
1005
- `${home}/Library/Application Support/Microsoft Edge`,
1006
- `${home}/Library/Application Support/BraveSoftware/Brave-Browser`
1007
- ] : [
1008
- `${home}/.config/google-chrome`,
1009
- `${home}/.config/chromium`,
1010
- `${home}/.config/microsoft-edge`,
1011
- `${home}/.config/BraveSoftware/Brave-Browser`
1012
- ];
1013
- for (const dir of profileDirs) {
1014
- for (const suffix of lockSuffixes) {
1015
- const lockPath = `${dir}/${suffix}`;
1016
- try {
1017
- if (existsSync(lockPath)) {
1018
- unlinkSync(lockPath);
1019
- log.debug(`Removed stale lock: ${lockPath}`);
1020
- }
1021
- } catch {
1022
- }
1031
+ const srcProfile = join(realDir, "Default");
1032
+ const destProfile = join(debugDir, "Default");
1033
+ if (existsSync(srcProfile)) {
1034
+ mkdirSync(destProfile, { recursive: true });
1035
+ for (const file of profileFiles) {
1036
+ const src = join(srcProfile, file);
1037
+ const dest = join(destProfile, file);
1038
+ try {
1039
+ if (existsSync(src)) {
1040
+ cpSync(src, dest, { force: true });
1041
+ log.debug(`Seeded: Default/${file}`);
1023
1042
  }
1043
+ } catch {
1024
1044
  }
1025
1045
  }
1046
+ const srcExt = join(srcProfile, "Extensions");
1047
+ const destExt = join(destProfile, "Extensions");
1048
+ try {
1049
+ if (existsSync(srcExt)) {
1050
+ cpSync(srcExt, destExt, { recursive: true, force: true });
1051
+ log.debug("Seeded: Default/Extensions");
1052
+ }
1053
+ } catch {
1054
+ }
1026
1055
  }
1027
1056
  }
1028
1057
  function spawnChrome(chromePath, port) {
1029
- const cdpFlag = `--remote-debugging-port=${port}`;
1030
- const os = platform();
1031
- let child;
1032
- if (os === "darwin") {
1033
- const appName = macAppName(chromePath);
1034
- log.debug(
1035
- `Spawning browser via: open -a "${appName}" --args ${cdpFlag} --restore-last-session`
1036
- );
1037
- child = spawn("open", ["-a", appName, "--args", cdpFlag, "--restore-last-session"], {
1038
- detached: true,
1039
- stdio: ["ignore", "pipe", "pipe"]
1040
- });
1041
- } else {
1042
- log.debug(`Spawning Chrome: ${chromePath} ${cdpFlag} --restore-last-session`);
1043
- child = spawn(chromePath, [cdpFlag, "--restore-last-session"], {
1044
- detached: true,
1045
- stdio: ["ignore", "pipe", "pipe"]
1046
- });
1047
- }
1058
+ const profileDir = getDebugProfileDir(chromePath);
1059
+ const flags = [
1060
+ `--remote-debugging-port=${port}`,
1061
+ `--user-data-dir=${profileDir}`,
1062
+ "--restore-last-session"
1063
+ ];
1064
+ log.debug(`Spawning browser: ${chromePath} ${flags.join(" ")}`);
1065
+ const child = spawn(chromePath, flags, {
1066
+ detached: true,
1067
+ stdio: ["ignore", "pipe", "pipe"]
1068
+ });
1048
1069
  let stderr = "";
1049
1070
  child.stderr?.on("data", (chunk) => {
1050
1071
  stderr += chunk.toString();
@@ -1086,7 +1107,7 @@ async function isPortInUse(port) {
1086
1107
  signal: AbortSignal.timeout(1e3)
1087
1108
  });
1088
1109
  const body = await res.text();
1089
- return !body.includes("Chrome");
1110
+ return !body.includes("webSocketDebuggerUrl");
1090
1111
  } catch {
1091
1112
  return false;
1092
1113
  }
@@ -1111,71 +1132,34 @@ async function ensureBrowserAvailable(port = 9222) {
1111
1132
  return { success: false, action: "chrome_not_found" };
1112
1133
  }
1113
1134
  log.debug(`Found Chrome at: ${chromePath}`);
1114
- const running = isChromeRunning(chromePath);
1115
- log.debug(`Browser currently running: ${running}`);
1116
- if (running) {
1117
- log.debug("Killing browser gracefully for restart with CDP...");
1118
- await killChromeGracefully(chromePath);
1119
- if (isChromeRunning(chromePath)) {
1120
- log.debug("Browser still running after kill attempt \u2014 cannot restart with CDP");
1121
- return {
1122
- success: false,
1123
- action: "launch_failed",
1124
- chromePath,
1125
- detail: "Could not stop the existing browser process. Please quit the browser manually and run assistme again."
1126
- };
1127
- }
1128
- await new Promise((r) => setTimeout(r, 2e3));
1129
- const child2 = spawnChrome(chromePath, port);
1130
- if (await waitForCDP(browser)) {
1131
- return { success: true, action: "restarted", chromePath };
1132
- }
1133
- if (child2.exitCode !== null) {
1134
- log.debug(
1135
- `Browser process already exited (code ${child2.exitCode}) \u2014 may have crashed or profile is locked`
1136
- );
1137
- return {
1138
- success: false,
1139
- action: "launch_failed",
1140
- chromePath,
1141
- detail: `Browser exited immediately (code ${child2.exitCode}). The profile may be locked. Try closing all browser windows first, then run assistme again.`
1142
- };
1143
- }
1144
- log.debug("First CDP wait timed out after restart, retrying...");
1145
- if (await waitForCDP(browser, 15e3)) {
1146
- return { success: true, action: "restarted", chromePath };
1147
- }
1148
- const stillRunning2 = isChromeRunning(chromePath);
1149
- return {
1150
- success: false,
1151
- action: "launch_failed",
1152
- chromePath,
1153
- detail: stillRunning2 ? "Browser is running but CDP port is not responding. Try: 1) Quit the browser completely, 2) Run assistme again." : "Browser was restarted but exited unexpectedly. Try launching it manually to check for errors."
1154
- };
1155
- }
1156
- const child = spawnChrome(chromePath, port);
1135
+ spawnChrome(chromePath, port);
1157
1136
  if (await waitForCDP(browser)) {
1158
1137
  return { success: true, action: "launched", chromePath };
1159
1138
  }
1160
- if (child.exitCode !== null) {
1161
- log.debug(`Browser process already exited (code ${child.exitCode})`);
1162
- return {
1163
- success: false,
1164
- action: "launch_failed",
1165
- chromePath,
1166
- detail: `Browser exited immediately (code ${child.exitCode}). Try launching it manually to see any error dialogs.`
1167
- };
1168
- }
1169
- log.debug("First CDP wait timed out after launch, retrying...");
1170
- if (await waitForCDP(browser, 15e3)) {
1171
- return { success: true, action: "launched", chromePath };
1139
+ const debugDir = getDebugProfileDir(chromePath);
1140
+ const lockPath = join(debugDir, "SingletonLock");
1141
+ if (existsSync(lockPath)) {
1142
+ log.debug("Found stale SingletonLock in debug profile \u2014 removing and retrying");
1143
+ try {
1144
+ unlinkSync(lockPath);
1145
+ for (const f of ["SingletonSocket", "SingletonCookie"]) {
1146
+ try {
1147
+ unlinkSync(join(debugDir, f));
1148
+ } catch {
1149
+ }
1150
+ }
1151
+ } catch {
1152
+ }
1153
+ spawnChrome(chromePath, port);
1154
+ if (await waitForCDP(browser, 15e3)) {
1155
+ return { success: true, action: "launched", chromePath };
1156
+ }
1172
1157
  }
1173
- const stillRunning = isChromeRunning(chromePath);
1174
1158
  return {
1175
1159
  success: false,
1176
1160
  action: "launch_failed",
1177
1161
  chromePath,
1178
- detail: stillRunning ? "Chrome is running but CDP port is not responding. Try quitting Chrome completely and running assistme again." : "Chrome exited unexpectedly after launch."
1162
+ detail: "Could not start browser with remote debugging. Possible causes:\n 1) Another assistme debug browser is already using port " + port + "\n 2) The browser crashed on startup\nTry: rm -rf ~/.assistme/browser-profile && assistme"
1179
1163
  };
1180
1164
  }
1181
1165
  var browserInstance = null;
@@ -1326,16 +1310,16 @@ var MemoryManager = class {
1326
1310
  // src/agent/skills.ts
1327
1311
  import {
1328
1312
  existsSync as existsSync2,
1329
- mkdirSync,
1330
- readdirSync,
1313
+ mkdirSync as mkdirSync2,
1314
+ readdirSync as readdirSync2,
1331
1315
  readFileSync,
1332
1316
  writeFileSync,
1333
1317
  statSync,
1334
1318
  unlinkSync as unlinkSync2,
1335
1319
  rmSync
1336
1320
  } from "fs";
1337
- import { join, basename, dirname } from "path";
1338
- import { homedir } from "os";
1321
+ import { join as join2, basename, dirname } from "path";
1322
+ import { homedir as homedir2 } from "os";
1339
1323
  var STOP_WORDS = /* @__PURE__ */ new Set([
1340
1324
  "the",
1341
1325
  "a",
@@ -1455,8 +1439,8 @@ function bigrams(tokens) {
1455
1439
  }
1456
1440
  return result;
1457
1441
  }
1458
- var SKILLS_DIR = join(homedir(), ".config", "assistme", "skills");
1459
- var BUNDLED_SKILLS_DIR = join(
1442
+ var SKILLS_DIR = join2(homedir2(), ".config", "assistme", "skills");
1443
+ var BUNDLED_SKILLS_DIR = join2(
1460
1444
  new URL(".", import.meta.url).pathname,
1461
1445
  "..",
1462
1446
  "..",
@@ -1464,7 +1448,7 @@ var BUNDLED_SKILLS_DIR = join(
1464
1448
  );
1465
1449
  function ensureSkillsDir() {
1466
1450
  if (!existsSync2(SKILLS_DIR)) {
1467
- mkdirSync(SKILLS_DIR, { recursive: true });
1451
+ mkdirSync2(SKILLS_DIR, { recursive: true });
1468
1452
  }
1469
1453
  }
1470
1454
  function parseSkillFile(filePath, source = "user") {
@@ -1540,13 +1524,13 @@ var SkillManager = class {
1540
1524
  }
1541
1525
  loadFromDir(dir, source) {
1542
1526
  try {
1543
- const entries = readdirSync(dir);
1527
+ const entries = readdirSync2(dir);
1544
1528
  for (const entry of entries) {
1545
- const fullPath = join(dir, entry);
1529
+ const fullPath = join2(dir, entry);
1546
1530
  const stat2 = statSync(fullPath);
1547
1531
  if (stat2.isDirectory()) {
1548
- const skillMd = join(fullPath, "SKILL.md");
1549
- const skillMdLower = join(fullPath, "skill.md");
1532
+ const skillMd = join2(fullPath, "SKILL.md");
1533
+ const skillMdLower = join2(fullPath, "skill.md");
1550
1534
  const mdPath = existsSync2(skillMd) ? skillMd : existsSync2(skillMdLower) ? skillMdLower : null;
1551
1535
  if (mdPath) {
1552
1536
  const skill = parseSkillFile(mdPath, source);
@@ -1655,9 +1639,9 @@ var SkillManager = class {
1655
1639
  */
1656
1640
  create(name, description, content) {
1657
1641
  ensureSkillsDir();
1658
- const skillDir = join(SKILLS_DIR, name);
1659
- mkdirSync(skillDir, { recursive: true });
1660
- const filePath = join(skillDir, "SKILL.md");
1642
+ const skillDir = join2(SKILLS_DIR, name);
1643
+ mkdirSync2(skillDir, { recursive: true });
1644
+ const filePath = join2(skillDir, "SKILL.md");
1661
1645
  const fileContent = `---
1662
1646
  name: ${name}
1663
1647
  description: ${description}
@@ -1696,7 +1680,7 @@ ${content}
1696
1680
  gitUrl += ".git";
1697
1681
  }
1698
1682
  const name = basename(gitUrl, ".git");
1699
- const targetDir = join(SKILLS_DIR, name);
1683
+ const targetDir = join2(SKILLS_DIR, name);
1700
1684
  if (existsSync2(targetDir)) {
1701
1685
  throw new Error(`Skill "${name}" already exists. Remove it first.`);
1702
1686
  }
@@ -1713,8 +1697,8 @@ ${content}
1713
1697
  { cause: err }
1714
1698
  );
1715
1699
  }
1716
- const skillMd = join(targetDir, "SKILL.md");
1717
- const skillMdLower = join(targetDir, "skill.md");
1700
+ const skillMd = join2(targetDir, "SKILL.md");
1701
+ const skillMdLower = join2(targetDir, "skill.md");
1718
1702
  if (!existsSync2(skillMd) && !existsSync2(skillMdLower)) {
1719
1703
  rmSync(targetDir, { recursive: true, force: true });
1720
1704
  throw new Error(
@@ -1740,9 +1724,9 @@ ${content}
1740
1724
  throw new Error(`HTTP ${dlResp.status}`);
1741
1725
  }
1742
1726
  const buffer = Buffer.from(await dlResp.arrayBuffer());
1743
- const skillDir = join(SKILLS_DIR, name);
1744
- mkdirSync(skillDir, { recursive: true });
1745
- const zipPath = join(skillDir, "_download.zip");
1727
+ const skillDir = join2(SKILLS_DIR, name);
1728
+ mkdirSync2(skillDir, { recursive: true });
1729
+ const zipPath = join2(skillDir, "_download.zip");
1746
1730
  writeFileSync(zipPath, buffer);
1747
1731
  const { exec: execCb } = await import("child_process");
1748
1732
  const { promisify: promisifyUtil } = await import("util");
@@ -1761,12 +1745,12 @@ ${content}
1761
1745
  throw new Error(`Could not fetch SKILL.md`);
1762
1746
  }
1763
1747
  writeFileSync(
1764
- join(skillDir, "SKILL.md"),
1748
+ join2(skillDir, "SKILL.md"),
1765
1749
  await fileResp.text(),
1766
1750
  "utf-8"
1767
1751
  );
1768
1752
  }
1769
- const skillMd = join(skillDir, "SKILL.md");
1753
+ const skillMd = join2(skillDir, "SKILL.md");
1770
1754
  if (!existsSync2(skillMd)) {
1771
1755
  rmSync(skillDir, { recursive: true, force: true });
1772
1756
  throw new Error("No SKILL.md in downloaded package");
@@ -2204,7 +2188,7 @@ import { z } from "zod/v4";
2204
2188
 
2205
2189
  // src/tools/filesystem.ts
2206
2190
  import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
2207
- import { resolve, relative, join as join2 } from "path";
2191
+ import { resolve, relative, join as join3 } from "path";
2208
2192
  import { glob } from "glob";
2209
2193
  function assertWithinWorkspace(filePath) {
2210
2194
  const config = getConfig();
@@ -2241,7 +2225,7 @@ async function searchFiles(pattern, directory) {
2241
2225
  ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
2242
2226
  });
2243
2227
  if (matches.length === 0) return "No files found matching the pattern.";
2244
- return matches.slice(0, 50).map((m) => relative(config.workspacePath, join2(cwd, m))).join("\n");
2228
+ return matches.slice(0, 50).map((m) => relative(config.workspacePath, join3(cwd, m))).join("\n");
2245
2229
  }
2246
2230
  async function listDirectory(path) {
2247
2231
  const config = getConfig();
@@ -2251,7 +2235,7 @@ async function listDirectory(path) {
2251
2235
  for (const entry of entries) {
2252
2236
  if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
2253
2237
  const icon = entry.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}";
2254
- const info = entry.isFile() ? await stat(join2(resolved, entry.name)).then(
2238
+ const info = entry.isFile() ? await stat(join3(resolved, entry.name)).then(
2255
2239
  (s) => ` (${formatSize(s.size)})`
2256
2240
  ) : "";
2257
2241
  results.push(`${icon} ${entry.name}${info}`);
@@ -2275,11 +2259,11 @@ async function searchContent(pattern, fileGlob, directory) {
2275
2259
  const results = [];
2276
2260
  for (const file of files.slice(0, 200)) {
2277
2261
  try {
2278
- const content = await readFile(join2(cwd, file), "utf-8");
2262
+ const content = await readFile(join3(cwd, file), "utf-8");
2279
2263
  const lines = content.split("\n");
2280
2264
  for (let i = 0; i < lines.length; i++) {
2281
2265
  if (regex.test(lines[i])) {
2282
- const relPath = relative(config.workspacePath, join2(cwd, file));
2266
+ const relPath = relative(config.workspacePath, join3(cwd, file));
2283
2267
  results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
2284
2268
  regex.lastIndex = 0;
2285
2269
  if (results.length >= 30) break;
@@ -2362,6 +2346,60 @@ ${stderr}` : "";
2362
2346
  }
2363
2347
 
2364
2348
  // src/tools/index.ts
2349
+ async function detectAndHandleLogin(browser) {
2350
+ const detection = await browser.detectLoginPage();
2351
+ if (!detection.isLoginPage) return null;
2352
+ const pageInfo = await browser.getPageInfo();
2353
+ let siteName;
2354
+ try {
2355
+ siteName = new URL(pageInfo.url).hostname;
2356
+ } catch {
2357
+ siteName = pageInfo.url;
2358
+ }
2359
+ console.log("\n");
2360
+ console.log("\u2501".repeat(60));
2361
+ console.log(" \u{1F510} LOGIN REQUIRED");
2362
+ console.log("\u2501".repeat(60));
2363
+ console.log(` ${detection.reason}`);
2364
+ console.log(` Site: ${siteName}`);
2365
+ console.log(` Please log in using the browser window.`);
2366
+ console.log(` Your session will be saved for future use.`);
2367
+ console.log(` (Waiting up to 120s for you to complete login)`);
2368
+ console.log("\u2501".repeat(60));
2369
+ console.log("\n");
2370
+ const loginUrl = pageInfo.url;
2371
+ const deadline = Date.now() + 12e4;
2372
+ while (Date.now() < deadline) {
2373
+ await new Promise((r) => setTimeout(r, 3e3));
2374
+ try {
2375
+ const currentInfo = await browser.getPageInfo();
2376
+ if (currentInfo.url !== loginUrl) {
2377
+ await new Promise((r) => setTimeout(r, 1500));
2378
+ const newPage = await browser.readPage();
2379
+ return `Login completed. Redirected to: ${currentInfo.url}
2380
+ Session saved in assistme browser profile for future use.
2381
+
2382
+ Current page:
2383
+ ${newPage.slice(0, 3e3)}`;
2384
+ }
2385
+ const stillLogin = await browser.detectLoginPage();
2386
+ if (!stillLogin.isLoginPage) {
2387
+ const newPage = await browser.readPage();
2388
+ return `Login completed on ${siteName}.
2389
+ Session saved in assistme browser profile for future use.
2390
+
2391
+ Current page:
2392
+ ${newPage.slice(0, 3e3)}`;
2393
+ }
2394
+ } catch {
2395
+ try {
2396
+ await browser.connect();
2397
+ } catch {
2398
+ }
2399
+ }
2400
+ }
2401
+ return `Login wait timed out after 120s. The user may still need to log in at ${siteName}.`;
2402
+ }
2365
2403
  async function ensureConnected(browser, tabIndex) {
2366
2404
  if (browser.isConnected() && tabIndex === void 0) return;
2367
2405
  if (!await browser.isAvailable()) {
@@ -2403,9 +2441,15 @@ async function executeTool(name, input) {
2403
2441
  await ensureConnected(browser, input.tab_index);
2404
2442
  return browser.isConnected() ? "Connected to browser." : "Failed to connect.";
2405
2443
  }
2406
- case "browser_navigate":
2444
+ case "browser_navigate": {
2407
2445
  await ensureConnected(browser);
2408
- return browser.navigate(input.url);
2446
+ const navResult = await browser.navigate(input.url);
2447
+ const loginResult = await detectAndHandleLogin(browser);
2448
+ if (loginResult) {
2449
+ return navResult + "\n\n" + loginResult;
2450
+ }
2451
+ return navResult;
2452
+ }
2409
2453
  case "browser_read_page":
2410
2454
  await ensureConnected(browser);
2411
2455
  return browser.readPage();
@@ -2919,7 +2963,9 @@ var BASE_SYSTEM_PROMPT = `You are AssistMe, an AI assistant that operates like a
2919
2963
  KEY PRINCIPLE: You operate the user's real browser, not a headless sandbox. This means:
2920
2964
  - The browser has the user's real cookies, logins, and sessions
2921
2965
  - When you navigate to amazon.com, you see the user's logged-in Amazon
2922
- - If a site needs login, ask the user to log in using browser_request_user_action
2966
+ - If a site needs login, the browser will auto-detect the login page and prompt the user
2967
+ - After the user logs in, their session is saved in the persistent browser profile (~/.assistme/browser-profile)
2968
+ - Saved sessions persist across assistme restarts \u2014 the user only needs to log in once per site
2923
2969
  - You are like a human assistant sitting at the user's computer
2924
2970
  - Chrome is automatically managed \u2014 just call browser_connect and it will auto-launch if needed
2925
2971
  - NEVER ask the user to manually start Chrome or run any terminal commands for browser setup
@@ -2946,9 +2992,9 @@ Available capabilities:
2946
2992
  Workflow for web tasks (e.g. "\u67E5\u4E00\u4E0B kindle \u6700\u65B0\u6B3E\u4EF7\u683C"):
2947
2993
  1. browser_connect \u2192 connect to user's Chrome
2948
2994
  2. browser_new_tab \u2192 open a new tab
2949
- 3. browser_navigate \u2192 go to the website
2995
+ 3. browser_navigate \u2192 go to the website (login pages are auto-detected \u2014 the user will be prompted and their session saved)
2950
2996
  4. browser_read_page or browser_screenshot \u2192 read the content
2951
- 5. If login required \u2192 browser_request_user_action \u2192 wait \u2192 continue
2997
+ 5. If login is needed but not auto-detected \u2192 use browser_request_user_action to ask the user
2952
2998
  6. Repeat across multiple sites as needed
2953
2999
  7. Summarize findings
2954
3000
 
@@ -2956,7 +3002,8 @@ Guidelines:
2956
3002
  - Always use the real browser for web tasks, never try to fetch URLs programmatically
2957
3003
  - Use browser_screenshot when you need to see the visual layout
2958
3004
  - Use browser_get_elements to find clickable elements before clicking
2959
- - If a page needs authentication, use browser_request_user_action immediately
3005
+ - Login pages are auto-detected after navigation \u2014 the user is prompted and sessions are saved automatically
3006
+ - If auto-detection misses a login page, use browser_request_user_action manually
2960
3007
  - Be thorough: check multiple sources when comparing prices/products
2961
3008
  - Summarize results clearly at the end
2962
3009
  - When you learn something about the user (preferences, habits), use memory_store to remember it
@@ -3331,13 +3378,10 @@ browserCmd.command("setup").description("Set up Chrome for AI control").option("
3331
3378
  if (result.success) {
3332
3379
  switch (result.action) {
3333
3380
  case "already_available":
3334
- spinner.succeed("Chrome is already running with remote debugging enabled");
3381
+ spinner.succeed("Browser is already running with remote debugging enabled");
3335
3382
  break;
3336
3383
  case "launched":
3337
- spinner.succeed("Chrome launched with remote debugging enabled");
3338
- break;
3339
- case "restarted":
3340
- spinner.succeed("Chrome restarted with remote debugging enabled (tabs restored)");
3384
+ spinner.succeed("Browser launched with remote debugging (debug profile)");
3341
3385
  break;
3342
3386
  }
3343
3387
  console.log(chalk.dim("\n You can now run: assistme start\n"));
@@ -3430,13 +3474,10 @@ program.command("start", { isDefault: true }).description("Start the agent and l
3430
3474
  if (launchResult.success) {
3431
3475
  switch (launchResult.action) {
3432
3476
  case "already_available":
3433
- launchSpinner.succeed("Chrome browser detected (CDP port 9222)");
3477
+ launchSpinner.succeed("Browser detected (CDP port 9222)");
3434
3478
  break;
3435
3479
  case "launched":
3436
- launchSpinner.succeed("Chrome launched with remote debugging enabled");
3437
- break;
3438
- case "restarted":
3439
- launchSpinner.succeed("Chrome restarted with remote debugging enabled (tabs restored)");
3480
+ launchSpinner.succeed("Browser launched with remote debugging (debug profile)");
3440
3481
  break;
3441
3482
  }
3442
3483
  } else {