assistme 0.1.11 → 0.1.13

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
@@ -393,8 +393,9 @@ import {
393
393
  // src/tools/browser.ts
394
394
  import { WebSocket } from "ws";
395
395
  import { execSync, spawn } from "child_process";
396
- import { platform } from "os";
397
- import { existsSync, unlinkSync } from "fs";
396
+ import { platform, homedir } from "os";
397
+ import { existsSync, unlinkSync, mkdirSync, cpSync } from "fs";
398
+ import { join } from "path";
398
399
  var BrowserController = class {
399
400
  ws = null;
400
401
  debugPort;
@@ -903,148 +904,97 @@ function findChromePath() {
903
904
  }
904
905
  return null;
905
906
  }
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;
907
+ function getDefaultProfileDir(chromePath) {
908
+ const home = homedir();
909
+ const os = platform();
910
+ if (os === "darwin") {
911
+ if (chromePath.includes("Brave Browser"))
912
+ return join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser");
913
+ if (chromePath.includes("Microsoft Edge"))
914
+ return join(home, "Library", "Application Support", "Microsoft Edge");
915
+ if (chromePath.includes("Chromium"))
916
+ return join(home, "Library", "Application Support", "Chromium");
917
+ if (chromePath.includes("Canary"))
918
+ return join(home, "Library", "Application Support", "Google", "Chrome Canary");
919
+ return join(home, "Library", "Application Support", "Google", "Chrome");
944
920
  }
921
+ if (os === "win32") {
922
+ const appData = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
923
+ if (chromePath.includes("brave"))
924
+ return join(appData, "BraveSoftware", "Brave-Browser", "User Data");
925
+ if (chromePath.includes("msedge")) return join(appData, "Microsoft", "Edge", "User Data");
926
+ return join(appData, "Google", "Chrome", "User Data");
927
+ }
928
+ if (chromePath.includes("brave")) return join(home, ".config", "BraveSoftware", "Brave-Browser");
929
+ if (chromePath.includes("microsoft-edge")) return join(home, ".config", "microsoft-edge");
930
+ if (chromePath.includes("chromium")) return join(home, ".config", "chromium");
931
+ return join(home, ".config", "google-chrome");
945
932
  }
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";
933
+ function getDebugProfileDir(chromePath) {
934
+ const home = homedir();
935
+ const debugDir = join(home, ".assistme", "browser-profile");
936
+ if (!existsSync(debugDir)) {
937
+ mkdirSync(debugDir, { recursive: true });
938
+ log.debug(`Created debug profile directory: ${debugDir}`);
939
+ const realDir = getDefaultProfileDir(chromePath);
940
+ if (existsSync(realDir)) {
941
+ seedDebugProfile(realDir, debugDir);
942
+ }
943
+ }
944
+ return debugDir;
952
945
  }
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
- });
946
+ function seedDebugProfile(realDir, debugDir) {
947
+ const rootFiles = ["Local State"];
948
+ const profileFiles = ["Bookmarks", "Preferences", "Favicons", "Top Sites", "Shortcuts"];
949
+ for (const file of rootFiles) {
950
+ const src = join(realDir, file);
951
+ const dest = join(debugDir, file);
952
+ try {
953
+ if (existsSync(src)) {
954
+ cpSync(src, dest, { force: true });
955
+ log.debug(`Seeded: ${file}`);
956
+ }
957
+ } catch {
995
958
  }
996
- } catch {
997
959
  }
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
- }
960
+ const srcProfile = join(realDir, "Default");
961
+ const destProfile = join(debugDir, "Default");
962
+ if (existsSync(srcProfile)) {
963
+ mkdirSync(destProfile, { recursive: true });
964
+ for (const file of profileFiles) {
965
+ const src = join(srcProfile, file);
966
+ const dest = join(destProfile, file);
967
+ try {
968
+ if (existsSync(src)) {
969
+ cpSync(src, dest, { force: true });
970
+ log.debug(`Seeded: Default/${file}`);
1023
971
  }
972
+ } catch {
1024
973
  }
1025
974
  }
975
+ const srcExt = join(srcProfile, "Extensions");
976
+ const destExt = join(destProfile, "Extensions");
977
+ try {
978
+ if (existsSync(srcExt)) {
979
+ cpSync(srcExt, destExt, { recursive: true, force: true });
980
+ log.debug("Seeded: Default/Extensions");
981
+ }
982
+ } catch {
983
+ }
1026
984
  }
1027
985
  }
1028
986
  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
- }
987
+ const profileDir = getDebugProfileDir(chromePath);
988
+ const flags = [
989
+ `--remote-debugging-port=${port}`,
990
+ `--user-data-dir=${profileDir}`,
991
+ "--restore-last-session"
992
+ ];
993
+ log.debug(`Spawning browser: ${chromePath} ${flags.join(" ")}`);
994
+ const child = spawn(chromePath, flags, {
995
+ detached: true,
996
+ stdio: ["ignore", "pipe", "pipe"]
997
+ });
1048
998
  let stderr = "";
1049
999
  child.stderr?.on("data", (chunk) => {
1050
1000
  stderr += chunk.toString();
@@ -1086,7 +1036,7 @@ async function isPortInUse(port) {
1086
1036
  signal: AbortSignal.timeout(1e3)
1087
1037
  });
1088
1038
  const body = await res.text();
1089
- return !body.includes("Chrome");
1039
+ return !body.includes("webSocketDebuggerUrl");
1090
1040
  } catch {
1091
1041
  return false;
1092
1042
  }
@@ -1111,71 +1061,34 @@ async function ensureBrowserAvailable(port = 9222) {
1111
1061
  return { success: false, action: "chrome_not_found" };
1112
1062
  }
1113
1063
  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);
1064
+ spawnChrome(chromePath, port);
1157
1065
  if (await waitForCDP(browser)) {
1158
1066
  return { success: true, action: "launched", chromePath };
1159
1067
  }
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 };
1068
+ const debugDir = getDebugProfileDir(chromePath);
1069
+ const lockPath = join(debugDir, "SingletonLock");
1070
+ if (existsSync(lockPath)) {
1071
+ log.debug("Found stale SingletonLock in debug profile \u2014 removing and retrying");
1072
+ try {
1073
+ unlinkSync(lockPath);
1074
+ for (const f of ["SingletonSocket", "SingletonCookie"]) {
1075
+ try {
1076
+ unlinkSync(join(debugDir, f));
1077
+ } catch {
1078
+ }
1079
+ }
1080
+ } catch {
1081
+ }
1082
+ spawnChrome(chromePath, port);
1083
+ if (await waitForCDP(browser, 15e3)) {
1084
+ return { success: true, action: "launched", chromePath };
1085
+ }
1172
1086
  }
1173
- const stillRunning = isChromeRunning(chromePath);
1174
1087
  return {
1175
1088
  success: false,
1176
1089
  action: "launch_failed",
1177
1090
  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."
1091
+ 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
1092
  };
1180
1093
  }
1181
1094
  var browserInstance = null;
@@ -1326,16 +1239,16 @@ var MemoryManager = class {
1326
1239
  // src/agent/skills.ts
1327
1240
  import {
1328
1241
  existsSync as existsSync2,
1329
- mkdirSync,
1330
- readdirSync,
1242
+ mkdirSync as mkdirSync2,
1243
+ readdirSync as readdirSync2,
1331
1244
  readFileSync,
1332
1245
  writeFileSync,
1333
1246
  statSync,
1334
1247
  unlinkSync as unlinkSync2,
1335
1248
  rmSync
1336
1249
  } from "fs";
1337
- import { join, basename, dirname } from "path";
1338
- import { homedir } from "os";
1250
+ import { join as join2, basename, dirname } from "path";
1251
+ import { homedir as homedir2 } from "os";
1339
1252
  var STOP_WORDS = /* @__PURE__ */ new Set([
1340
1253
  "the",
1341
1254
  "a",
@@ -1455,8 +1368,8 @@ function bigrams(tokens) {
1455
1368
  }
1456
1369
  return result;
1457
1370
  }
1458
- var SKILLS_DIR = join(homedir(), ".config", "assistme", "skills");
1459
- var BUNDLED_SKILLS_DIR = join(
1371
+ var SKILLS_DIR = join2(homedir2(), ".config", "assistme", "skills");
1372
+ var BUNDLED_SKILLS_DIR = join2(
1460
1373
  new URL(".", import.meta.url).pathname,
1461
1374
  "..",
1462
1375
  "..",
@@ -1464,7 +1377,7 @@ var BUNDLED_SKILLS_DIR = join(
1464
1377
  );
1465
1378
  function ensureSkillsDir() {
1466
1379
  if (!existsSync2(SKILLS_DIR)) {
1467
- mkdirSync(SKILLS_DIR, { recursive: true });
1380
+ mkdirSync2(SKILLS_DIR, { recursive: true });
1468
1381
  }
1469
1382
  }
1470
1383
  function parseSkillFile(filePath, source = "user") {
@@ -1540,13 +1453,13 @@ var SkillManager = class {
1540
1453
  }
1541
1454
  loadFromDir(dir, source) {
1542
1455
  try {
1543
- const entries = readdirSync(dir);
1456
+ const entries = readdirSync2(dir);
1544
1457
  for (const entry of entries) {
1545
- const fullPath = join(dir, entry);
1458
+ const fullPath = join2(dir, entry);
1546
1459
  const stat2 = statSync(fullPath);
1547
1460
  if (stat2.isDirectory()) {
1548
- const skillMd = join(fullPath, "SKILL.md");
1549
- const skillMdLower = join(fullPath, "skill.md");
1461
+ const skillMd = join2(fullPath, "SKILL.md");
1462
+ const skillMdLower = join2(fullPath, "skill.md");
1550
1463
  const mdPath = existsSync2(skillMd) ? skillMd : existsSync2(skillMdLower) ? skillMdLower : null;
1551
1464
  if (mdPath) {
1552
1465
  const skill = parseSkillFile(mdPath, source);
@@ -1655,9 +1568,9 @@ var SkillManager = class {
1655
1568
  */
1656
1569
  create(name, description, content) {
1657
1570
  ensureSkillsDir();
1658
- const skillDir = join(SKILLS_DIR, name);
1659
- mkdirSync(skillDir, { recursive: true });
1660
- const filePath = join(skillDir, "SKILL.md");
1571
+ const skillDir = join2(SKILLS_DIR, name);
1572
+ mkdirSync2(skillDir, { recursive: true });
1573
+ const filePath = join2(skillDir, "SKILL.md");
1661
1574
  const fileContent = `---
1662
1575
  name: ${name}
1663
1576
  description: ${description}
@@ -1696,7 +1609,7 @@ ${content}
1696
1609
  gitUrl += ".git";
1697
1610
  }
1698
1611
  const name = basename(gitUrl, ".git");
1699
- const targetDir = join(SKILLS_DIR, name);
1612
+ const targetDir = join2(SKILLS_DIR, name);
1700
1613
  if (existsSync2(targetDir)) {
1701
1614
  throw new Error(`Skill "${name}" already exists. Remove it first.`);
1702
1615
  }
@@ -1713,8 +1626,8 @@ ${content}
1713
1626
  { cause: err }
1714
1627
  );
1715
1628
  }
1716
- const skillMd = join(targetDir, "SKILL.md");
1717
- const skillMdLower = join(targetDir, "skill.md");
1629
+ const skillMd = join2(targetDir, "SKILL.md");
1630
+ const skillMdLower = join2(targetDir, "skill.md");
1718
1631
  if (!existsSync2(skillMd) && !existsSync2(skillMdLower)) {
1719
1632
  rmSync(targetDir, { recursive: true, force: true });
1720
1633
  throw new Error(
@@ -1740,9 +1653,9 @@ ${content}
1740
1653
  throw new Error(`HTTP ${dlResp.status}`);
1741
1654
  }
1742
1655
  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");
1656
+ const skillDir = join2(SKILLS_DIR, name);
1657
+ mkdirSync2(skillDir, { recursive: true });
1658
+ const zipPath = join2(skillDir, "_download.zip");
1746
1659
  writeFileSync(zipPath, buffer);
1747
1660
  const { exec: execCb } = await import("child_process");
1748
1661
  const { promisify: promisifyUtil } = await import("util");
@@ -1761,12 +1674,12 @@ ${content}
1761
1674
  throw new Error(`Could not fetch SKILL.md`);
1762
1675
  }
1763
1676
  writeFileSync(
1764
- join(skillDir, "SKILL.md"),
1677
+ join2(skillDir, "SKILL.md"),
1765
1678
  await fileResp.text(),
1766
1679
  "utf-8"
1767
1680
  );
1768
1681
  }
1769
- const skillMd = join(skillDir, "SKILL.md");
1682
+ const skillMd = join2(skillDir, "SKILL.md");
1770
1683
  if (!existsSync2(skillMd)) {
1771
1684
  rmSync(skillDir, { recursive: true, force: true });
1772
1685
  throw new Error("No SKILL.md in downloaded package");
@@ -2204,7 +2117,7 @@ import { z } from "zod/v4";
2204
2117
 
2205
2118
  // src/tools/filesystem.ts
2206
2119
  import { readFile, writeFile, readdir, stat, mkdir } from "fs/promises";
2207
- import { resolve, relative, join as join2 } from "path";
2120
+ import { resolve, relative, join as join3 } from "path";
2208
2121
  import { glob } from "glob";
2209
2122
  function assertWithinWorkspace(filePath) {
2210
2123
  const config = getConfig();
@@ -2241,7 +2154,7 @@ async function searchFiles(pattern, directory) {
2241
2154
  ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**"]
2242
2155
  });
2243
2156
  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");
2157
+ return matches.slice(0, 50).map((m) => relative(config.workspacePath, join3(cwd, m))).join("\n");
2245
2158
  }
2246
2159
  async function listDirectory(path) {
2247
2160
  const config = getConfig();
@@ -2251,7 +2164,7 @@ async function listDirectory(path) {
2251
2164
  for (const entry of entries) {
2252
2165
  if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
2253
2166
  const icon = entry.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}";
2254
- const info = entry.isFile() ? await stat(join2(resolved, entry.name)).then(
2167
+ const info = entry.isFile() ? await stat(join3(resolved, entry.name)).then(
2255
2168
  (s) => ` (${formatSize(s.size)})`
2256
2169
  ) : "";
2257
2170
  results.push(`${icon} ${entry.name}${info}`);
@@ -2275,11 +2188,11 @@ async function searchContent(pattern, fileGlob, directory) {
2275
2188
  const results = [];
2276
2189
  for (const file of files.slice(0, 200)) {
2277
2190
  try {
2278
- const content = await readFile(join2(cwd, file), "utf-8");
2191
+ const content = await readFile(join3(cwd, file), "utf-8");
2279
2192
  const lines = content.split("\n");
2280
2193
  for (let i = 0; i < lines.length; i++) {
2281
2194
  if (regex.test(lines[i])) {
2282
- const relPath = relative(config.workspacePath, join2(cwd, file));
2195
+ const relPath = relative(config.workspacePath, join3(cwd, file));
2283
2196
  results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
2284
2197
  regex.lastIndex = 0;
2285
2198
  if (results.length >= 30) break;
@@ -3331,13 +3244,10 @@ browserCmd.command("setup").description("Set up Chrome for AI control").option("
3331
3244
  if (result.success) {
3332
3245
  switch (result.action) {
3333
3246
  case "already_available":
3334
- spinner.succeed("Chrome is already running with remote debugging enabled");
3247
+ spinner.succeed("Browser is already running with remote debugging enabled");
3335
3248
  break;
3336
3249
  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)");
3250
+ spinner.succeed("Browser launched with remote debugging (debug profile)");
3341
3251
  break;
3342
3252
  }
3343
3253
  console.log(chalk.dim("\n You can now run: assistme start\n"));
@@ -3430,13 +3340,10 @@ program.command("start", { isDefault: true }).description("Start the agent and l
3430
3340
  if (launchResult.success) {
3431
3341
  switch (launchResult.action) {
3432
3342
  case "already_available":
3433
- launchSpinner.succeed("Chrome browser detected (CDP port 9222)");
3343
+ launchSpinner.succeed("Browser detected (CDP port 9222)");
3434
3344
  break;
3435
3345
  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)");
3346
+ launchSpinner.succeed("Browser launched with remote debugging (debug profile)");
3440
3347
  break;
3441
3348
  }
3442
3349
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistme",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "AssistMe CLI Agent - AI-powered assistant that controls your real browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -191,13 +191,10 @@ browserCmd
191
191
  if (result.success) {
192
192
  switch (result.action) {
193
193
  case "already_available":
194
- spinner.succeed("Chrome is already running with remote debugging enabled");
194
+ spinner.succeed("Browser is already running with remote debugging enabled");
195
195
  break;
196
196
  case "launched":
197
- spinner.succeed("Chrome launched with remote debugging enabled");
198
- break;
199
- case "restarted":
200
- spinner.succeed("Chrome restarted with remote debugging enabled (tabs restored)");
197
+ spinner.succeed("Browser launched with remote debugging (debug profile)");
201
198
  break;
202
199
  }
203
200
  console.log(chalk.dim("\n You can now run: assistme start\n"));
@@ -318,13 +315,10 @@ program
318
315
  if (launchResult.success) {
319
316
  switch (launchResult.action) {
320
317
  case "already_available":
321
- launchSpinner.succeed("Chrome browser detected (CDP port 9222)");
318
+ launchSpinner.succeed("Browser detected (CDP port 9222)");
322
319
  break;
323
320
  case "launched":
324
- launchSpinner.succeed("Chrome launched with remote debugging enabled");
325
- break;
326
- case "restarted":
327
- launchSpinner.succeed("Chrome restarted with remote debugging enabled (tabs restored)");
321
+ launchSpinner.succeed("Browser launched with remote debugging (debug profile)");
328
322
  break;
329
323
  }
330
324
  } else {
@@ -16,8 +16,9 @@
16
16
 
17
17
  import { WebSocket } from "ws";
18
18
  import { execSync, spawn, type ChildProcess } from "node:child_process";
19
- import { platform } from "node:os";
20
- import { existsSync, unlinkSync } from "node:fs";
19
+ import { platform, homedir } from "node:os";
20
+ import { existsSync, unlinkSync, mkdirSync, cpSync, readdirSync } from "node:fs";
21
+ import { join } from "node:path";
21
22
  import { log } from "../utils/logger.js";
22
23
 
23
24
  interface CDPTab {
@@ -807,40 +808,160 @@ async function killChromeGracefully(chromePath: string): Promise<void> {
807
808
  }
808
809
 
809
810
  /**
810
- * Spawn Chrome with the remote-debugging-port flag.
811
- * Returns the child process so callers can detect early failures.
811
+ * Return the browser's default profile directory.
812
+ */
813
+ function getDefaultProfileDir(chromePath: string): string {
814
+ const home = homedir();
815
+ const os = platform();
816
+
817
+ if (os === "darwin") {
818
+ if (chromePath.includes("Brave Browser"))
819
+ return join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser");
820
+ if (chromePath.includes("Microsoft Edge"))
821
+ return join(home, "Library", "Application Support", "Microsoft Edge");
822
+ if (chromePath.includes("Chromium"))
823
+ return join(home, "Library", "Application Support", "Chromium");
824
+ if (chromePath.includes("Canary"))
825
+ return join(home, "Library", "Application Support", "Google", "Chrome Canary");
826
+ return join(home, "Library", "Application Support", "Google", "Chrome");
827
+ }
828
+
829
+ if (os === "win32") {
830
+ const appData = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
831
+ if (chromePath.includes("brave"))
832
+ return join(appData, "BraveSoftware", "Brave-Browser", "User Data");
833
+ if (chromePath.includes("msedge")) return join(appData, "Microsoft", "Edge", "User Data");
834
+ return join(appData, "Google", "Chrome", "User Data");
835
+ }
836
+
837
+ // Linux
838
+ if (chromePath.includes("brave")) return join(home, ".config", "BraveSoftware", "Brave-Browser");
839
+ if (chromePath.includes("microsoft-edge")) return join(home, ".config", "microsoft-edge");
840
+ if (chromePath.includes("chromium")) return join(home, ".config", "chromium");
841
+ return join(home, ".config", "google-chrome");
842
+ }
843
+
844
+ /**
845
+ * Return a dedicated debug profile directory for assistme.
846
+ *
847
+ * Chrome 136+ silently ignores --remote-debugging-port when launched with the
848
+ * DEFAULT user-data-dir (security hardening against cookie-stealing malware).
849
+ * It also ignores --user-data-dir pointing to the default path.
850
+ * The flag ONLY works with a NON-DEFAULT --user-data-dir.
812
851
  *
813
- * On macOS, uses `open -a` which goes through Launch Services — the correct
814
- * way to launch a .app bundle and ensure flags reach the Chrome process.
815
- * Direct binary invocation on macOS can fail because Chrome's Mach-O binary
816
- * goes through framework wrappers that may drop command-line flags.
852
+ * Strategy: use ~/.assistme/browser-profile as a dedicated debug profile.
853
+ * On first use, copy key files from the real profile (bookmarks, cookies,
854
+ * login data, preferences) so the user doesn't start completely fresh.
855
+ * Sessions accumulate in the debug profile from then on.
817
856
  *
818
- * IMPORTANT: On macOS, `open -a` ignores --args if Chrome is ALREADY running.
819
- * Callers must ensure Chrome is fully quit before calling this function.
857
+ * See: https://developer.chrome.com/blog/remote-debugging-port
820
858
  */
821
- function spawnChrome(chromePath: string, port: number): ChildProcess {
822
- const cdpFlag = `--remote-debugging-port=${port}`;
823
- const os = platform();
859
+ function getDebugProfileDir(chromePath: string): string {
860
+ const home = homedir();
861
+ const debugDir = join(home, ".assistme", "browser-profile");
862
+
863
+ if (!existsSync(debugDir)) {
864
+ mkdirSync(debugDir, { recursive: true });
865
+ log.debug(`Created debug profile directory: ${debugDir}`);
866
+
867
+ // Seed from the real profile — copy lightweight files, skip caches
868
+ const realDir = getDefaultProfileDir(chromePath);
869
+ if (existsSync(realDir)) {
870
+ seedDebugProfile(realDir, debugDir);
871
+ }
872
+ }
824
873
 
825
- let child: ChildProcess;
874
+ return debugDir;
875
+ }
826
876
 
827
- if (os === "darwin") {
828
- const appName = macAppName(chromePath);
829
- log.debug(
830
- `Spawning browser via: open -a "${appName}" --args ${cdpFlag} --restore-last-session`
831
- );
832
- child = spawn("open", ["-a", appName, "--args", cdpFlag, "--restore-last-session"], {
833
- detached: true,
834
- stdio: ["ignore", "pipe", "pipe"],
835
- });
836
- } else {
837
- log.debug(`Spawning Chrome: ${chromePath} ${cdpFlag} --restore-last-session`);
838
- child = spawn(chromePath, [cdpFlag, "--restore-last-session"], {
839
- detached: true,
840
- stdio: ["ignore", "pipe", "pipe"],
841
- });
877
+ /**
878
+ * Copy essential profile data from the user's real Chrome profile to the
879
+ * debug profile. This preserves bookmarks, preferences, and (where possible)
880
+ * login state without copying multi-GB caches.
881
+ *
882
+ * Note: cookies/login data are encrypted with a key tied to the user-data-dir
883
+ * on Chrome 136+, so they won't decrypt in the debug profile. The user will
884
+ * need to log in once in the debug browser. After that, sessions persist.
885
+ */
886
+ function seedDebugProfile(realDir: string, debugDir: string): void {
887
+ // Files to copy from the profile root
888
+ const rootFiles = ["Local State"];
889
+ // Files to copy from the "Default" sub-profile
890
+ const profileFiles = ["Bookmarks", "Preferences", "Favicons", "Top Sites", "Shortcuts"];
891
+
892
+ for (const file of rootFiles) {
893
+ const src = join(realDir, file);
894
+ const dest = join(debugDir, file);
895
+ try {
896
+ if (existsSync(src)) {
897
+ cpSync(src, dest, { force: true });
898
+ log.debug(`Seeded: ${file}`);
899
+ }
900
+ } catch {
901
+ /* best effort */
902
+ }
842
903
  }
843
904
 
905
+ // Copy the Default profile sub-directory essentials
906
+ const srcProfile = join(realDir, "Default");
907
+ const destProfile = join(debugDir, "Default");
908
+ if (existsSync(srcProfile)) {
909
+ mkdirSync(destProfile, { recursive: true });
910
+ for (const file of profileFiles) {
911
+ const src = join(srcProfile, file);
912
+ const dest = join(destProfile, file);
913
+ try {
914
+ if (existsSync(src)) {
915
+ cpSync(src, dest, { force: true });
916
+ log.debug(`Seeded: Default/${file}`);
917
+ }
918
+ } catch {
919
+ /* best effort */
920
+ }
921
+ }
922
+
923
+ // Copy Extensions directory if it exists (preserves user's extensions)
924
+ const srcExt = join(srcProfile, "Extensions");
925
+ const destExt = join(destProfile, "Extensions");
926
+ try {
927
+ if (existsSync(srcExt)) {
928
+ cpSync(srcExt, destExt, { recursive: true, force: true });
929
+ log.debug("Seeded: Default/Extensions");
930
+ }
931
+ } catch {
932
+ /* best effort — extensions can be large */
933
+ }
934
+ }
935
+ }
936
+
937
+ /**
938
+ * Spawn a Chromium-based browser with CDP enabled.
939
+ * Returns the child process for exit-code monitoring.
940
+ *
941
+ * Key design decisions:
942
+ * - Launches the binary directly (not via macOS `open -a`) so flags are
943
+ * guaranteed to reach the process and the child stays alive.
944
+ * - Uses a dedicated debug profile (not the default profile) so that:
945
+ * (a) Chrome 136+ allows --remote-debugging-port
946
+ * (b) Can run alongside the user's regular Chrome (different singleton)
947
+ * - Callers should ensure no OTHER debug-profile Chrome is running, but
948
+ * the user's regular Chrome can stay open.
949
+ */
950
+ function spawnChrome(chromePath: string, port: number): ChildProcess {
951
+ const profileDir = getDebugProfileDir(chromePath);
952
+ const flags = [
953
+ `--remote-debugging-port=${port}`,
954
+ `--user-data-dir=${profileDir}`,
955
+ "--restore-last-session",
956
+ ];
957
+
958
+ log.debug(`Spawning browser: ${chromePath} ${flags.join(" ")}`);
959
+
960
+ const child = spawn(chromePath, flags, {
961
+ detached: true,
962
+ stdio: ["ignore", "pipe", "pipe"],
963
+ });
964
+
844
965
  // Capture stderr for diagnostics — Chrome prints errors here
845
966
  let stderr = "";
846
967
  child.stderr?.on("data", (chunk: Buffer) => {
@@ -894,9 +1015,10 @@ async function isPortInUse(port: number): Promise<boolean> {
894
1015
  const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
895
1016
  signal: AbortSignal.timeout(1000),
896
1017
  });
897
- // If we get a response but it's not Chrome, the port is occupied
1018
+ // CDP /json/version returns a JSON object with "Browser" and "webSocketDebuggerUrl" keys.
1019
+ // All Chromium-based browsers (Chrome, Edge, Brave) include these.
898
1020
  const body = await res.text();
899
- return !body.includes("Chrome");
1021
+ return !body.includes("webSocketDebuggerUrl");
900
1022
  } catch {
901
1023
  // Connection refused → port is free
902
1024
  return false;
@@ -908,27 +1030,23 @@ async function isPortInUse(port: number): Promise<boolean> {
908
1030
  */
909
1031
  export interface AutoLaunchResult {
910
1032
  success: boolean;
911
- action:
912
- | "already_available"
913
- | "launched"
914
- | "restarted"
915
- | "chrome_not_found"
916
- | "launch_failed"
917
- | "port_conflict";
1033
+ action: "already_available" | "launched" | "chrome_not_found" | "launch_failed" | "port_conflict";
918
1034
  chromePath?: string;
919
1035
  detail?: string;
920
1036
  }
921
1037
 
922
1038
  /**
923
- * Ensure Chrome is running with CDP enabled.
1039
+ * Ensure a Chromium browser is running with CDP enabled.
924
1040
  *
925
- * 1. Already listening on the port return immediately.
926
- * 2. Port occupied by non-Chrome process report conflict.
927
- * 3. Chrome not running → launch with --remote-debugging-port.
928
- * 4. Chrome running without CDP graceful quit, then relaunch with CDP.
929
- * Chrome's session restore brings back all tabs.
1041
+ * Uses a SEPARATE debug profile (~/.assistme/browser-profile) so that:
1042
+ * - The user's regular Chrome can stay open — no killing required
1043
+ * - Chrome 136+ enables --remote-debugging-port (requires non-default dir)
1044
+ * - The debug browser has its own singleton no conflicts
930
1045
  *
931
- * On launch failure, retries once with a longer wait.
1046
+ * Flow:
1047
+ * 1. CDP already reachable on the port → return immediately.
1048
+ * 2. Port occupied by a non-Chromium process → report conflict.
1049
+ * 3. Launch a new browser instance with the debug profile + CDP flag.
932
1050
  */
933
1051
  export async function ensureBrowserAvailable(port = 9222): Promise<AutoLaunchResult> {
934
1052
  const browser = getBrowser(port);
@@ -958,97 +1076,51 @@ export async function ensureBrowserAvailable(port = 9222): Promise<AutoLaunchRes
958
1076
 
959
1077
  log.debug(`Found Chrome at: ${chromePath}`);
960
1078
 
961
- const running = isChromeRunning(chromePath);
962
- log.debug(`Browser currently running: ${running}`);
963
-
964
- // Case 3: Browser running without CDP → restart
965
- if (running) {
966
- log.debug("Killing browser gracefully for restart with CDP...");
967
- await killChromeGracefully(chromePath);
968
-
969
- // Verify browser is fully dead — critical on macOS where `open -a`
970
- // ignores --args if the app is still alive.
971
- if (isChromeRunning(chromePath)) {
972
- log.debug("Browser still running after kill attempt — cannot restart with CDP");
973
- return {
974
- success: false,
975
- action: "launch_failed",
976
- chromePath,
977
- detail:
978
- "Could not stop the existing browser process. Please quit the browser manually and run assistme again.",
979
- };
980
- }
981
-
982
- // Extra wait for profile lock release after kill
983
- await new Promise((r) => setTimeout(r, 2000));
984
-
985
- const child = spawnChrome(chromePath, port);
986
-
987
- if (await waitForCDP(browser)) {
988
- return { success: true, action: "restarted", chromePath };
989
- }
990
-
991
- // Check if browser process exited immediately
992
- if (child.exitCode !== null) {
993
- log.debug(
994
- `Browser process already exited (code ${child.exitCode}) — may have crashed or profile is locked`
995
- );
996
- return {
997
- success: false,
998
- action: "launch_failed",
999
- chromePath,
1000
- detail: `Browser exited immediately (code ${child.exitCode}). The profile may be locked. Try closing all browser windows first, then run assistme again.`,
1001
- };
1002
- }
1003
-
1004
- // Retry once — browser can be slow to start (extensions, session restore)
1005
- log.debug("First CDP wait timed out after restart, retrying...");
1006
- if (await waitForCDP(browser, 15000)) {
1007
- return { success: true, action: "restarted", chromePath };
1008
- }
1009
-
1010
- const stillRunning = isChromeRunning(chromePath);
1011
- return {
1012
- success: false,
1013
- action: "launch_failed",
1014
- chromePath,
1015
- detail: stillRunning
1016
- ? "Browser is running but CDP port is not responding. Try: 1) Quit the browser completely, 2) Run assistme again."
1017
- : "Browser was restarted but exited unexpectedly. Try launching it manually to check for errors.",
1018
- };
1019
- }
1020
-
1021
- // Case 4: Browser not running → launch
1022
- const child = spawnChrome(chromePath, port);
1079
+ // Launch a debug Chrome instance (separate profile — no need to kill the user's Chrome)
1080
+ spawnChrome(chromePath, port);
1023
1081
 
1024
1082
  if (await waitForCDP(browser)) {
1025
1083
  return { success: true, action: "launched", chromePath };
1026
1084
  }
1027
1085
 
1028
- if (child.exitCode !== null) {
1029
- log.debug(`Browser process already exited (code ${child.exitCode})`);
1030
- return {
1031
- success: false,
1032
- action: "launch_failed",
1033
- chromePath,
1034
- detail: `Browser exited immediately (code ${child.exitCode}). Try launching it manually to see any error dialogs.`,
1035
- };
1036
- }
1086
+ // CDP didn't come up — check if the debug profile is locked by a previous
1087
+ // crashed assistme session (stale SingletonLock)
1088
+ const debugDir = getDebugProfileDir(chromePath);
1089
+ const lockPath = join(debugDir, "SingletonLock");
1090
+ if (existsSync(lockPath)) {
1091
+ log.debug("Found stale SingletonLock in debug profile — removing and retrying");
1092
+ try {
1093
+ unlinkSync(lockPath);
1094
+ // Also clean SingletonSocket/Cookie
1095
+ for (const f of ["SingletonSocket", "SingletonCookie"]) {
1096
+ try {
1097
+ unlinkSync(join(debugDir, f));
1098
+ } catch {
1099
+ /* ok */
1100
+ }
1101
+ }
1102
+ } catch {
1103
+ /* best effort */
1104
+ }
1037
1105
 
1038
- // Retry once
1039
- log.debug("First CDP wait timed out after launch, retrying...");
1040
- if (await waitForCDP(browser, 15000)) {
1041
- return { success: true, action: "launched", chromePath };
1106
+ // Retry spawn
1107
+ spawnChrome(chromePath, port);
1108
+ if (await waitForCDP(browser, 15000)) {
1109
+ return { success: true, action: "launched", chromePath };
1110
+ }
1042
1111
  }
1043
1112
 
1044
- const stillRunning = isChromeRunning(chromePath);
1045
1113
  return {
1046
1114
  success: false,
1047
1115
  action: "launch_failed",
1048
1116
  chromePath,
1049
- detail: stillRunning
1050
- ? "Chrome is running but CDP port is not responding. Try quitting Chrome completely and running assistme again."
1051
- : "Chrome exited unexpectedly after launch.",
1117
+ detail:
1118
+ "Could not start browser with remote debugging. Possible causes:\n" +
1119
+ " 1) Another assistme debug browser is already using port " +
1120
+ port +
1121
+ "\n" +
1122
+ " 2) The browser crashed on startup\n" +
1123
+ "Try: rm -rf ~/.assistme/browser-profile && assistme",
1052
1124
  };
1053
1125
  }
1054
1126