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 +280 -239
- package/package.json +1 -1
- package/src/agent/processor.ts +7 -4
- package/src/agent/scheduler.ts +16 -39
- package/src/db/supabase.test.ts +98 -41
- package/src/index.ts +4 -10
- package/src/tools/browser.ts +269 -124
- package/src/tools/index.ts +86 -2
- package/src/utils/config.test.ts +4 -6
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
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
return
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
if (
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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:
|
|
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 =
|
|
1459
|
-
var BUNDLED_SKILLS_DIR =
|
|
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
|
-
|
|
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 =
|
|
1527
|
+
const entries = readdirSync2(dir);
|
|
1544
1528
|
for (const entry of entries) {
|
|
1545
|
-
const fullPath =
|
|
1529
|
+
const fullPath = join2(dir, entry);
|
|
1546
1530
|
const stat2 = statSync(fullPath);
|
|
1547
1531
|
if (stat2.isDirectory()) {
|
|
1548
|
-
const skillMd =
|
|
1549
|
-
const skillMdLower =
|
|
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 =
|
|
1659
|
-
|
|
1660
|
-
const filePath =
|
|
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 =
|
|
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 =
|
|
1717
|
-
const skillMdLower =
|
|
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 =
|
|
1744
|
-
|
|
1745
|
-
const zipPath =
|
|
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
|
-
|
|
1748
|
+
join2(skillDir, "SKILL.md"),
|
|
1765
1749
|
await fileResp.text(),
|
|
1766
1750
|
"utf-8"
|
|
1767
1751
|
);
|
|
1768
1752
|
}
|
|
1769
|
-
const skillMd =
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
-
|
|
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("
|
|
3381
|
+
spinner.succeed("Browser is already running with remote debugging enabled");
|
|
3335
3382
|
break;
|
|
3336
3383
|
case "launched":
|
|
3337
|
-
spinner.succeed("
|
|
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("
|
|
3477
|
+
launchSpinner.succeed("Browser detected (CDP port 9222)");
|
|
3434
3478
|
break;
|
|
3435
3479
|
case "launched":
|
|
3436
|
-
launchSpinner.succeed("
|
|
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 {
|