@love-moon/chat-web 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +142 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/commands/doctor.d.ts +27 -0
  6. package/dist/commands/doctor.js +116 -0
  7. package/dist/commands/doctor.js.map +1 -0
  8. package/dist/commands/index.d.ts +3 -0
  9. package/dist/commands/index.js +4 -0
  10. package/dist/commands/index.js.map +1 -0
  11. package/dist/commands/info.d.ts +32 -0
  12. package/dist/commands/info.js +81 -0
  13. package/dist/commands/info.js.map +1 -0
  14. package/dist/commands/login.d.ts +19 -0
  15. package/dist/commands/login.js +61 -0
  16. package/dist/commands/login.js.map +1 -0
  17. package/dist/core/browser.d.ts +70 -0
  18. package/dist/core/browser.js +96 -0
  19. package/dist/core/browser.js.map +1 -0
  20. package/dist/core/errors.d.ts +60 -0
  21. package/dist/core/errors.js +153 -0
  22. package/dist/core/errors.js.map +1 -0
  23. package/dist/core/install-chromium.d.ts +55 -0
  24. package/dist/core/install-chromium.js +156 -0
  25. package/dist/core/install-chromium.js.map +1 -0
  26. package/dist/core/keyboard.d.ts +39 -0
  27. package/dist/core/keyboard.js +54 -0
  28. package/dist/core/keyboard.js.map +1 -0
  29. package/dist/core/locator-score.d.ts +41 -0
  30. package/dist/core/locator-score.js +101 -0
  31. package/dist/core/locator-score.js.map +1 -0
  32. package/dist/core/logger.d.ts +10 -0
  33. package/dist/core/logger.js +38 -0
  34. package/dist/core/logger.js.map +1 -0
  35. package/dist/core/navigate.d.ts +52 -0
  36. package/dist/core/navigate.js +102 -0
  37. package/dist/core/navigate.js.map +1 -0
  38. package/dist/core/paths.d.ts +12 -0
  39. package/dist/core/paths.js +30 -0
  40. package/dist/core/paths.js.map +1 -0
  41. package/dist/core/profile-manager.d.ts +13 -0
  42. package/dist/core/profile-manager.js +44 -0
  43. package/dist/core/profile-manager.js.map +1 -0
  44. package/dist/core/provider.d.ts +64 -0
  45. package/dist/core/provider.js +31 -0
  46. package/dist/core/provider.js.map +1 -0
  47. package/dist/core/response-watcher.d.ts +35 -0
  48. package/dist/core/response-watcher.js +70 -0
  49. package/dist/core/response-watcher.js.map +1 -0
  50. package/dist/core/snapshot.d.ts +38 -0
  51. package/dist/core/snapshot.js +137 -0
  52. package/dist/core/snapshot.js.map +1 -0
  53. package/dist/core/sse-parser.d.ts +20 -0
  54. package/dist/core/sse-parser.js +49 -0
  55. package/dist/core/sse-parser.js.map +1 -0
  56. package/dist/index.d.ts +33 -0
  57. package/dist/index.js +40 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/providers/chatgpt-sse-collector.d.ts +76 -0
  60. package/dist/providers/chatgpt-sse-collector.js +298 -0
  61. package/dist/providers/chatgpt-sse-collector.js.map +1 -0
  62. package/dist/providers/chatgpt.d.ts +56 -0
  63. package/dist/providers/chatgpt.js +357 -0
  64. package/dist/providers/chatgpt.js.map +1 -0
  65. package/dist/providers/deepseek.d.ts +22 -0
  66. package/dist/providers/deepseek.js +153 -0
  67. package/dist/providers/deepseek.js.map +1 -0
  68. package/dist/providers/gemini.d.ts +102 -0
  69. package/dist/providers/gemini.js +480 -0
  70. package/dist/providers/gemini.js.map +1 -0
  71. package/dist/providers/index.d.ts +8 -0
  72. package/dist/providers/index.js +17 -0
  73. package/dist/providers/index.js.map +1 -0
  74. package/dist/session.d.ts +121 -0
  75. package/dist/session.js +242 -0
  76. package/dist/session.js.map +1 -0
  77. package/package.json +47 -0
@@ -0,0 +1,96 @@
1
+ import { chromium } from "playwright";
2
+ import { BrowserLaunchError } from "./errors.js";
3
+ import { autoInstallDisabled, hasAttemptedAutoInstall, installChromium, isBrowserMissingError, markAutoInstallAttempted, } from "./install-chromium.js";
4
+ import { defaultLogger } from "./logger.js";
5
+ import { createProfileManager } from "./profile-manager.js";
6
+ /**
7
+ * Launch (or reattach to) a persistent Chromium context for a provider.
8
+ *
9
+ * Important: we use `launchPersistentContext`, NOT `browser.newContext`,
10
+ * because ChatGPT / DeepSeek depend on more than just cookies (see RFC §19.1).
11
+ *
12
+ * First-run UX: if Playwright reports that the Chromium binary is not
13
+ * installed (a very common state under pnpm 10+, which silently blocks
14
+ * the `playwright` postinstall script), we run
15
+ * `npx playwright install chromium` once and retry the launch. Disable
16
+ * via `CHAT_WEB_NO_AUTO_INSTALL=1` (or `noAutoInstall: true` on the call).
17
+ */
18
+ export async function launchProviderBrowser(provider, options = {}) {
19
+ const profileManager = options.profileManager ?? createProfileManager();
20
+ const userDataDir = await profileManager.ensureProfile(provider);
21
+ const logger = options.logger ?? defaultLogger;
22
+ const launchArgs = {
23
+ headless: resolveHeadless(options.headless),
24
+ viewport: options.viewport ?? { width: 1280, height: 900 },
25
+ args: [
26
+ // Hide the obvious "I'm an automated browser" signal that several
27
+ // chat sites probe via navigator.webdriver. RFC §19.3.
28
+ "--disable-blink-features=AutomationControlled",
29
+ ...(options.args ?? []),
30
+ ],
31
+ ...resolveBrowserBinary(options),
32
+ };
33
+ try {
34
+ return await launch(userDataDir, launchArgs);
35
+ }
36
+ catch (err) {
37
+ // First-failure recovery path: if Chromium isn't installed, try to
38
+ // install it once and retry the launch.
39
+ const shouldAutoInstall = isBrowserMissingError(err) &&
40
+ !hasAttemptedAutoInstall() &&
41
+ !(options.noAutoInstall || autoInstallDisabled());
42
+ if (!shouldAutoInstall) {
43
+ throw new BrowserLaunchError(provider, `Failed to launch Chromium for "${provider}" at ${userDataDir}: ${err.message}`, err);
44
+ }
45
+ markAutoInstallAttempted();
46
+ logger.warn(`chat-web: Chromium binary not found; attempting one-time install (\`npx playwright install chromium\`).`);
47
+ try {
48
+ await installChromium({ logger });
49
+ }
50
+ catch (installErr) {
51
+ throw new BrowserLaunchError(provider, `Auto-install of Chromium failed for "${provider}". Run manually: \`npx playwright install chromium\` (cause: ${installErr.message})`, installErr);
52
+ }
53
+ try {
54
+ return await launch(userDataDir, launchArgs);
55
+ }
56
+ catch (retryErr) {
57
+ throw new BrowserLaunchError(provider, `Failed to launch Chromium for "${provider}" at ${userDataDir} even after auto-install: ${retryErr.message}`, retryErr);
58
+ }
59
+ }
60
+ }
61
+ async function launch(userDataDir, launchArgs) {
62
+ const context = await chromium.launchPersistentContext(userDataDir, launchArgs);
63
+ const existing = context.pages();
64
+ const page = existing.length > 0 ? existing[0] : await context.newPage();
65
+ return { context, page, userDataDir };
66
+ }
67
+ function resolveHeadless(explicit) {
68
+ if (typeof explicit === "boolean")
69
+ return explicit;
70
+ const env = process.env.CHAT_WEB_HEADLESS;
71
+ if (env === "1" || env === "true")
72
+ return true;
73
+ // Default to headed, per RFC §19.3 (better against anti-bot heuristics).
74
+ return false;
75
+ }
76
+ /**
77
+ * Decide which Chromium-family binary to launch:
78
+ *
79
+ * - Explicit `options.executablePath` (or env `CHAT_WEB_BROWSER_EXECUTABLE`)
80
+ * wins outright.
81
+ * - Otherwise `options.channel` (or env `CHAT_WEB_BROWSER_CHANNEL`)
82
+ * selects a Playwright "channel" — `"chrome"` for system Google
83
+ * Chrome, `"msedge"` for system Edge, etc.
84
+ * - Otherwise undefined → Playwright defaults to its bundled
85
+ * `chrome-headless-shell` build.
86
+ */
87
+ function resolveBrowserBinary(options) {
88
+ const explicitExec = options.executablePath?.trim() || process.env.CHAT_WEB_BROWSER_EXECUTABLE?.trim();
89
+ if (explicitExec)
90
+ return { executablePath: explicitExec };
91
+ const explicitChannel = options.channel?.trim() || process.env.CHAT_WEB_BROWSER_CHANNEL?.trim();
92
+ if (explicitChannel)
93
+ return { channel: explicitChannel };
94
+ return {};
95
+ }
96
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/core/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAkC,MAAM,YAAY,CAAC;AAEtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,eAAe,EACf,qBAAqB,EACrB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAe,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAA8B,MAAM,sBAAsB,CAAC;AA+DxF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,QAAgB,EAChB,UAAyB,EAAE;IAE3B,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,oBAAoB,EAAE,CAAC;IACxE,MAAM,WAAW,GAAG,MAAM,cAAc,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IACjE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,aAAa,CAAC;IAE/C,MAAM,UAAU,GAAyB;QACvC,QAAQ,EAAE,eAAe,CAAC,OAAO,CAAC,QAAQ,CAAC;QAC3C,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;QAC1D,IAAI,EAAE;YACJ,kEAAkE;YAClE,uDAAuD;YACvD,+CAA+C;YAC/C,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;SACxB;QACD,GAAG,oBAAoB,CAAC,OAAO,CAAC;KACjC,CAAC;IAEF,IAAI,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,mEAAmE;QACnE,wCAAwC;QACxC,MAAM,iBAAiB,GACrB,qBAAqB,CAAC,GAAG,CAAC;YAC1B,CAAC,uBAAuB,EAAE;YAC1B,CAAC,CAAC,OAAO,CAAC,aAAa,IAAI,mBAAmB,EAAE,CAAC,CAAC;QAEpD,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACvB,MAAM,IAAI,kBAAkB,CAC1B,QAAQ,EACR,kCAAkC,QAAQ,QAAQ,WAAW,KAAM,GAAa,CAAC,OAAO,EAAE,EAC1F,GAAG,CACJ,CAAC;QACJ,CAAC;QAED,wBAAwB,EAAE,CAAC;QAC3B,MAAM,CAAC,IAAI,CACT,yGAAyG,CAC1G,CAAC;QACF,IAAI,CAAC;YACH,MAAM,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,UAAU,EAAE,CAAC;YACpB,MAAM,IAAI,kBAAkB,CAC1B,QAAQ,EACR,wCAAwC,QAAQ,gEAAiE,UAAoB,CAAC,OAAO,GAAG,EAChJ,UAAU,CACX,CAAC;QACJ,CAAC;QAED,IAAI,CAAC;YACH,OAAO,MAAM,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QAC/C,CAAC;QAAC,OAAO,QAAQ,EAAE,CAAC;YAClB,MAAM,IAAI,kBAAkB,CAC1B,QAAQ,EACR,kCAAkC,QAAQ,QAAQ,WAAW,6BAA8B,QAAkB,CAAC,OAAO,EAAE,EACvH,QAAQ,CACT,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,WAAmB,EAAE,UAAgC;IACzE,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,uBAAuB,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IAChF,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;IAC1E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;AACxC,CAAC;AAED,SAAS,eAAe,CAAC,QAA6B;IACpD,IAAI,OAAO,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACnD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;IAC1C,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IAC/C,yEAAyE;IACzE,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,oBAAoB,CAAC,OAAsB;IAIlD,MAAM,YAAY,GAAG,OAAO,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,IAAI,EAAE,CAAC;IACvG,IAAI,YAAY;QAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC;IAC1D,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,IAAI,EAAE,CAAC;IAChG,IAAI,eAAe;QAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC;IACzD,OAAO,EAAE,CAAC;AACZ,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Typed error classes used across chat-web.
3
+ *
4
+ * Each subclass carries a `code` field (machine-friendly) and a `hint`
5
+ * field (human-friendly next action), so CLI / daemon callers can both
6
+ * branch on the code and surface a useful remediation step.
7
+ */
8
+ export type ChatWebErrorCode = "NOT_LOGGED_IN" | "INPUT_NOT_FOUND" | "SEND_BUTTON_NOT_FOUND" | "RESPONSE_TIMEOUT" | "RESPONSE_EXTRACTION" | "PROVIDER_RATE_LIMITED" | "PROVIDER_CAPTCHA" | "PROVIDER_API_KEY_REQUIRED" | "PROVIDER_PERMISSION_DENIED" | "PROVIDER_AUTOMATION_BLOCKED" | "SELECTOR_VERIFICATION" | "UNKNOWN_PROVIDER" | "PROFILE_ERROR" | "BROWSER_LAUNCH_FAILED";
9
+ export declare class ChatWebError extends Error {
10
+ readonly code: ChatWebErrorCode;
11
+ readonly hint?: string;
12
+ readonly provider?: string;
13
+ constructor(code: ChatWebErrorCode, message: string, options?: {
14
+ hint?: string;
15
+ provider?: string;
16
+ cause?: unknown;
17
+ });
18
+ }
19
+ export declare class NotLoggedInError extends ChatWebError {
20
+ constructor(provider: string);
21
+ }
22
+ export declare class InputNotFoundError extends ChatWebError {
23
+ constructor(provider: string);
24
+ }
25
+ export declare class SendButtonNotFoundError extends ChatWebError {
26
+ constructor(provider: string);
27
+ }
28
+ export declare class ResponseTimeoutError extends ChatWebError {
29
+ constructor(provider: string, timeoutMs: number);
30
+ }
31
+ export declare class ResponseExtractionError extends ChatWebError {
32
+ constructor(provider: string, message?: string);
33
+ }
34
+ export declare class ProviderRateLimitedError extends ChatWebError {
35
+ constructor(provider: string);
36
+ }
37
+ export declare class ProviderCaptchaError extends ChatWebError {
38
+ constructor(provider: string);
39
+ }
40
+ export declare class ProviderApiKeyRequiredError extends ChatWebError {
41
+ constructor(provider: string, detail?: string);
42
+ }
43
+ export declare class ProviderAutomationBlockedError extends ChatWebError {
44
+ constructor(provider: string, detail?: string);
45
+ }
46
+ export declare class ProviderPermissionDeniedError extends ChatWebError {
47
+ constructor(provider: string, detail?: string);
48
+ }
49
+ export declare class SelectorVerificationError extends ChatWebError {
50
+ constructor(provider: string, what: string);
51
+ }
52
+ export declare class UnknownProviderError extends ChatWebError {
53
+ constructor(provider: string, known: readonly string[]);
54
+ }
55
+ export declare class ProfileError extends ChatWebError {
56
+ constructor(provider: string, message: string, cause?: unknown);
57
+ }
58
+ export declare class BrowserLaunchError extends ChatWebError {
59
+ constructor(provider: string, message: string, cause?: unknown);
60
+ }
@@ -0,0 +1,153 @@
1
+ export class ChatWebError extends Error {
2
+ code;
3
+ hint;
4
+ provider;
5
+ constructor(code, message, options = {}) {
6
+ super(message);
7
+ this.name = "ChatWebError";
8
+ this.code = code;
9
+ this.hint = options.hint;
10
+ this.provider = options.provider;
11
+ if (options.cause !== undefined) {
12
+ this.cause = options.cause;
13
+ }
14
+ }
15
+ }
16
+ export class NotLoggedInError extends ChatWebError {
17
+ constructor(provider) {
18
+ super("NOT_LOGGED_IN", `Provider "${provider}" is not logged in.`, {
19
+ provider,
20
+ hint: `Run: chat-web login ${provider}`,
21
+ });
22
+ this.name = "NotLoggedInError";
23
+ }
24
+ }
25
+ export class InputNotFoundError extends ChatWebError {
26
+ constructor(provider) {
27
+ super("INPUT_NOT_FOUND", `Could not locate the input box for "${provider}".`, {
28
+ provider,
29
+ hint: `Run: chat-web doctor ${provider} --snapshot`,
30
+ });
31
+ this.name = "InputNotFoundError";
32
+ }
33
+ }
34
+ export class SendButtonNotFoundError extends ChatWebError {
35
+ constructor(provider) {
36
+ super("SEND_BUTTON_NOT_FOUND", `Could not locate the send button for "${provider}".`, {
37
+ provider,
38
+ hint: `Run: chat-web doctor ${provider} --snapshot`,
39
+ });
40
+ this.name = "SendButtonNotFoundError";
41
+ }
42
+ }
43
+ export class ResponseTimeoutError extends ChatWebError {
44
+ constructor(provider, timeoutMs) {
45
+ super("RESPONSE_TIMEOUT", `Timed out after ${timeoutMs}ms waiting for "${provider}" response.`, {
46
+ provider,
47
+ hint: `Try: chat-web ask ${provider} --timeout ${timeoutMs * 2} "..."`,
48
+ });
49
+ this.name = "ResponseTimeoutError";
50
+ }
51
+ }
52
+ export class ResponseExtractionError extends ChatWebError {
53
+ constructor(provider, message = "Failed to extract assistant message.") {
54
+ super("RESPONSE_EXTRACTION", message, {
55
+ provider,
56
+ hint: `Run: chat-web doctor ${provider} --snapshot`,
57
+ });
58
+ this.name = "ResponseExtractionError";
59
+ }
60
+ }
61
+ export class ProviderRateLimitedError extends ChatWebError {
62
+ constructor(provider) {
63
+ super("PROVIDER_RATE_LIMITED", `Provider "${provider}" appears to be rate limiting us.`, {
64
+ provider,
65
+ hint: "Slow down request rate, or wait a few minutes before retrying.",
66
+ });
67
+ this.name = "ProviderRateLimitedError";
68
+ }
69
+ }
70
+ export class ProviderCaptchaError extends ChatWebError {
71
+ constructor(provider) {
72
+ super("PROVIDER_CAPTCHA", `Provider "${provider}" is requesting human verification.`, {
73
+ provider,
74
+ hint: `Run: chat-web login ${provider} and pass the verification flow in the headed browser.`,
75
+ });
76
+ this.name = "ProviderCaptchaError";
77
+ }
78
+ }
79
+ export class ProviderApiKeyRequiredError extends ChatWebError {
80
+ constructor(provider, detail) {
81
+ const base = `Provider "${provider}" needs an API key configured in its web console.`;
82
+ super("PROVIDER_API_KEY_REQUIRED", detail ? `${base} ${detail}` : base, {
83
+ provider,
84
+ hint: provider === "gemini"
85
+ ? "Two common causes: (a) you've hit AI Studio's free-tier daily quota " +
86
+ "— wait a day for the quota to reset, OR (b) no API key is configured. " +
87
+ "If you need uninterrupted access, open https://aistudio.google.com/app/apikey, " +
88
+ "create a key, then in AI Studio click the 'Get API key' / 'No API key selected' " +
89
+ "button and select that key."
90
+ : `Configure an API key for ${provider} in its web console.`,
91
+ });
92
+ this.name = "ProviderApiKeyRequiredError";
93
+ }
94
+ }
95
+ export class ProviderAutomationBlockedError extends ChatWebError {
96
+ constructor(provider, detail) {
97
+ const base = `Provider "${provider}" appears to be blocking automated access ` +
98
+ "(model invocation never started after the input was submitted; the page sits on 'Thinking' indefinitely).";
99
+ super("PROVIDER_AUTOMATION_BLOCKED", detail ? `${base} ${detail}` : base, {
100
+ provider,
101
+ hint: provider === "gemini"
102
+ ? "Google AI Studio runs an anti-abuse challenge (WAA) before invoking the model. " +
103
+ "Headless / scripted browsers routinely fail this challenge silently. " +
104
+ "Workarounds: (1) use chat-web's `chatgpt` provider instead — ChatGPT is far more permissive of automation; " +
105
+ "(2) call the Gemini API directly with an API key from https://aistudio.google.com/app/apikey (outside chat-web); " +
106
+ "(3) try again later — the challenge sometimes passes."
107
+ : "The provider's anti-bot system is silently blocking model invocation. Try a different provider.",
108
+ });
109
+ this.name = "ProviderAutomationBlockedError";
110
+ }
111
+ }
112
+ export class ProviderPermissionDeniedError extends ChatWebError {
113
+ constructor(provider, detail) {
114
+ const base = `Provider "${provider}" denied the request (permission denied).`;
115
+ super("PROVIDER_PERMISSION_DENIED", detail ? `${base} ${detail}` : base, {
116
+ provider,
117
+ hint: "Common causes: missing/invalid API key, model not enabled on the account, region restriction, quota exhausted. Open the provider's web console to inspect.",
118
+ });
119
+ this.name = "ProviderPermissionDeniedError";
120
+ }
121
+ }
122
+ export class SelectorVerificationError extends ChatWebError {
123
+ constructor(provider, what) {
124
+ super("SELECTOR_VERIFICATION", `Selector verification failed for "${provider}" while probing ${what}.`, {
125
+ provider,
126
+ hint: `Run: chat-web doctor ${provider} --snapshot`,
127
+ });
128
+ this.name = "SelectorVerificationError";
129
+ }
130
+ }
131
+ export class UnknownProviderError extends ChatWebError {
132
+ constructor(provider, known) {
133
+ super("UNKNOWN_PROVIDER", `Unknown provider "${provider}". Known providers: ${known.join(", ")}`, { provider, hint: `Use one of: ${known.join(", ")}` });
134
+ this.name = "UnknownProviderError";
135
+ }
136
+ }
137
+ export class ProfileError extends ChatWebError {
138
+ constructor(provider, message, cause) {
139
+ super("PROFILE_ERROR", message, { provider, cause });
140
+ this.name = "ProfileError";
141
+ }
142
+ }
143
+ export class BrowserLaunchError extends ChatWebError {
144
+ constructor(provider, message, cause) {
145
+ super("BROWSER_LAUNCH_FAILED", message, {
146
+ provider,
147
+ cause,
148
+ hint: "Check Playwright install: `npx playwright install chromium`",
149
+ });
150
+ this.name = "BrowserLaunchError";
151
+ }
152
+ }
153
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import { type ChildProcess, type SpawnOptions } from "node:child_process";
2
+ import { type Logger } from "./logger.js";
3
+ /**
4
+ * Returns true when an error thrown by Playwright's `launchPersistentContext`
5
+ * / `launch` is the well-known "Chromium binary not installed" condition.
6
+ *
7
+ * We match on multiple hint substrings rather than a single regex because
8
+ * Playwright has historically rephrased this error every couple of
9
+ * minor releases; missing only the "browsertype.launch" prefix would break
10
+ * detection on first contact with a new wording.
11
+ */
12
+ export declare function isBrowserMissingError(err: unknown): boolean;
13
+ /**
14
+ * Honour `CHAT_WEB_NO_AUTO_INSTALL` / `CHAT_WEB_SKIP_BROWSER_INSTALL` to
15
+ * disable the auto-install path entirely. Useful in CI / Docker where the
16
+ * operator wants explicit control over browser provisioning.
17
+ */
18
+ export declare function autoInstallDisabled(env?: NodeJS.ProcessEnv): boolean;
19
+ export interface InstallChromiumOptions {
20
+ logger?: Logger;
21
+ /** Override the command (mostly for tests). Default: "npx". */
22
+ command?: string;
23
+ /** Override the args (mostly for tests). Default: ["--yes", "playwright", "install", "chromium"]. */
24
+ args?: string[];
25
+ /** Custom spawn function (for tests). Defaults to node:child_process.spawn. */
26
+ spawnFn?: (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
27
+ /** Override environment passed to the child. Default: process.env. */
28
+ env?: NodeJS.ProcessEnv;
29
+ /** Cancel the install. */
30
+ signal?: AbortSignal;
31
+ /** Hard cap on the install duration. Default: 5 minutes. */
32
+ timeoutMs?: number;
33
+ }
34
+ export interface InstallChromiumResult {
35
+ command: string;
36
+ args: readonly string[];
37
+ exitCode: number;
38
+ durationMs: number;
39
+ stdoutTail: string;
40
+ stderrTail: string;
41
+ }
42
+ /**
43
+ * Runs `npx --yes playwright install chromium` to its conclusion.
44
+ *
45
+ * Resolves with the exit info on success (exitCode === 0), rejects with
46
+ * an `Error` whose message includes the stderr tail otherwise. We don't
47
+ * stream the binary download to stdout — that's noisy — but we keep the
48
+ * last ~2KB of each stream so callers can attach useful diagnostics if
49
+ * the install fails.
50
+ */
51
+ export declare function installChromium(options?: InstallChromiumOptions): Promise<InstallChromiumResult>;
52
+ export declare function hasAttemptedAutoInstall(): boolean;
53
+ export declare function markAutoInstallAttempted(): void;
54
+ /** Test-only: reset the once-per-process guard. */
55
+ export declare function _resetAutoInstallGuardForTests(): void;
@@ -0,0 +1,156 @@
1
+ import { spawn, } from "node:child_process";
2
+ import { defaultLogger } from "./logger.js";
3
+ /**
4
+ * Substrings (lower-cased) that identify Playwright's "browser binary is
5
+ * missing" error. Playwright's exact wording has shifted across versions
6
+ * but the core sentinels stay stable.
7
+ */
8
+ const BROWSER_MISSING_HINTS = [
9
+ "executable doesn't exist",
10
+ "please run the following command to download new browsers",
11
+ "browsertype.launch", // every variant starts with this when the binary is missing
12
+ "npx playwright install",
13
+ ];
14
+ /**
15
+ * Returns true when an error thrown by Playwright's `launchPersistentContext`
16
+ * / `launch` is the well-known "Chromium binary not installed" condition.
17
+ *
18
+ * We match on multiple hint substrings rather than a single regex because
19
+ * Playwright has historically rephrased this error every couple of
20
+ * minor releases; missing only the "browsertype.launch" prefix would break
21
+ * detection on first contact with a new wording.
22
+ */
23
+ export function isBrowserMissingError(err) {
24
+ const msg = String(err?.message ?? "").toLowerCase();
25
+ if (!msg)
26
+ return false;
27
+ return BROWSER_MISSING_HINTS.some((hint) => msg.includes(hint));
28
+ }
29
+ /**
30
+ * Honour `CHAT_WEB_NO_AUTO_INSTALL` / `CHAT_WEB_SKIP_BROWSER_INSTALL` to
31
+ * disable the auto-install path entirely. Useful in CI / Docker where the
32
+ * operator wants explicit control over browser provisioning.
33
+ */
34
+ export function autoInstallDisabled(env = process.env) {
35
+ for (const key of ["CHAT_WEB_NO_AUTO_INSTALL", "CHAT_WEB_SKIP_BROWSER_INSTALL"]) {
36
+ const v = env[key];
37
+ if (v === "1" || v === "true" || v === "yes")
38
+ return true;
39
+ }
40
+ return false;
41
+ }
42
+ /**
43
+ * Runs `npx --yes playwright install chromium` to its conclusion.
44
+ *
45
+ * Resolves with the exit info on success (exitCode === 0), rejects with
46
+ * an `Error` whose message includes the stderr tail otherwise. We don't
47
+ * stream the binary download to stdout — that's noisy — but we keep the
48
+ * last ~2KB of each stream so callers can attach useful diagnostics if
49
+ * the install fails.
50
+ */
51
+ export function installChromium(options = {}) {
52
+ const command = options.command ?? "npx";
53
+ const args = options.args ?? ["--yes", "playwright", "install", "chromium"];
54
+ const spawnFn = options.spawnFn ?? spawn;
55
+ const env = options.env ?? process.env;
56
+ const logger = options.logger ?? defaultLogger;
57
+ const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
58
+ const start = Date.now();
59
+ logger.info(`chat-web: installing Chromium via \`${command} ${args.join(" ")}\` (first-run only) …`);
60
+ return new Promise((resolve, reject) => {
61
+ let settled = false;
62
+ let stdoutTail = "";
63
+ let stderrTail = "";
64
+ const child = spawnFn(command, args, {
65
+ stdio: ["ignore", "pipe", "pipe"],
66
+ env,
67
+ });
68
+ const onAbort = () => {
69
+ if (settled)
70
+ return;
71
+ try {
72
+ child.kill("SIGTERM");
73
+ }
74
+ catch {
75
+ // best effort
76
+ }
77
+ };
78
+ options.signal?.addEventListener("abort", onAbort);
79
+ const timeoutTimer = setTimeout(() => {
80
+ if (settled)
81
+ return;
82
+ try {
83
+ child.kill("SIGTERM");
84
+ }
85
+ catch {
86
+ // best effort
87
+ }
88
+ finalize(new Error(`chat-web: Chromium install timed out after ${timeoutMs}ms. Run manually: \`${command} ${args.join(" ")}\``));
89
+ }, timeoutMs);
90
+ if (typeof timeoutTimer.unref === "function")
91
+ timeoutTimer.unref();
92
+ const appendTail = (current, chunk) => {
93
+ const combined = current + chunk;
94
+ return combined.length > 2048 ? combined.slice(-2048) : combined;
95
+ };
96
+ child.stdout?.on("data", (chunk) => {
97
+ const s = chunk.toString("utf8");
98
+ stdoutTail = appendTail(stdoutTail, s);
99
+ // Surface progress lines at debug level — full output is noisy.
100
+ logger.debug(s.replace(/\n+$/, ""));
101
+ });
102
+ child.stderr?.on("data", (chunk) => {
103
+ const s = chunk.toString("utf8");
104
+ stderrTail = appendTail(stderrTail, s);
105
+ logger.debug(s.replace(/\n+$/, ""));
106
+ });
107
+ child.on("error", (err) => finalize(err));
108
+ child.on("exit", (code, signal) => {
109
+ if (code === 0) {
110
+ finalize(null);
111
+ }
112
+ else {
113
+ const reason = code !== null ? `exit code ${code}` : `signal ${signal}`;
114
+ finalize(new Error(`chat-web: \`${command} ${args.join(" ")}\` failed (${reason}). Last stderr:\n${stderrTail.trim() || "(empty)"}`));
115
+ }
116
+ });
117
+ function finalize(err) {
118
+ if (settled)
119
+ return;
120
+ settled = true;
121
+ clearTimeout(timeoutTimer);
122
+ options.signal?.removeEventListener("abort", onAbort);
123
+ if (err) {
124
+ reject(err);
125
+ return;
126
+ }
127
+ const durationMs = Date.now() - start;
128
+ logger.info(`chat-web: Chromium install finished in ${Math.round(durationMs / 1000)}s.`);
129
+ resolve({
130
+ command,
131
+ args,
132
+ exitCode: 0,
133
+ durationMs,
134
+ stdoutTail,
135
+ stderrTail,
136
+ });
137
+ }
138
+ });
139
+ }
140
+ /**
141
+ * Module-private flag so we attempt auto-install at most once per process.
142
+ * Subsequent failures fall through to the original BrowserLaunchError so
143
+ * we don't loop on a genuinely broken environment.
144
+ */
145
+ let attempted = false;
146
+ export function hasAttemptedAutoInstall() {
147
+ return attempted;
148
+ }
149
+ export function markAutoInstallAttempted() {
150
+ attempted = true;
151
+ }
152
+ /** Test-only: reset the once-per-process guard. */
153
+ export function _resetAutoInstallGuardForTests() {
154
+ attempted = false;
155
+ }
156
+ //# sourceMappingURL=install-chromium.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-chromium.js","sourceRoot":"","sources":["../../src/core/install-chromium.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { Page } from "playwright";
2
+ /**
3
+ * A single instruction in the typing plan: either type literal text, or
4
+ * press a key sequence. Exposed separately from the executor so we can
5
+ * unit-test the *plan* without a real Playwright Page.
6
+ */
7
+ export type TypingAction = {
8
+ type: "type";
9
+ text: string;
10
+ } | {
11
+ type: "press";
12
+ key: string;
13
+ };
14
+ /**
15
+ * Plan how to type a (potentially multi-line) string into a chat composer.
16
+ *
17
+ * Why this exists:
18
+ * ChatGPT's ProseMirror and Google AI Studio's Angular textarea both
19
+ * bind `Enter` to "submit prompt" and `Shift+Enter` to "soft line
20
+ * break". `page.keyboard.type(text)` literally emits an `Enter` key
21
+ * event for every `\n` in `text`, which causes a multi-line prompt to
22
+ * be submitted line-by-line — exactly the bug we saw on the ATLAS
23
+ * arXiv prompt (task 09b34cf4-…), where one logical message landed on
24
+ * the assistant as 5 fragments.
25
+ *
26
+ * The fix: split the text on newlines, type each segment as literal
27
+ * characters, and emit `Shift+Enter` between segments. The composer ends
28
+ * up with the same visual content but is never prematurely submitted.
29
+ */
30
+ export declare function planMultilineTyping(text: string): TypingAction[];
31
+ /**
32
+ * Execute a typing plan against a Playwright Page.
33
+ *
34
+ * Kept as a thin wrapper over `planMultilineTyping` so the interesting
35
+ * logic stays in the pure function. Adapters call this from
36
+ * `sendMessage`; they MUST NOT fall back to `page.keyboard.type(message)`
37
+ * directly on user-supplied prompts.
38
+ */
39
+ export declare function typeMultiline(page: Page, text: string): Promise<void>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Plan how to type a (potentially multi-line) string into a chat composer.
3
+ *
4
+ * Why this exists:
5
+ * ChatGPT's ProseMirror and Google AI Studio's Angular textarea both
6
+ * bind `Enter` to "submit prompt" and `Shift+Enter` to "soft line
7
+ * break". `page.keyboard.type(text)` literally emits an `Enter` key
8
+ * event for every `\n` in `text`, which causes a multi-line prompt to
9
+ * be submitted line-by-line — exactly the bug we saw on the ATLAS
10
+ * arXiv prompt (task 09b34cf4-…), where one logical message landed on
11
+ * the assistant as 5 fragments.
12
+ *
13
+ * The fix: split the text on newlines, type each segment as literal
14
+ * characters, and emit `Shift+Enter` between segments. The composer ends
15
+ * up with the same visual content but is never prematurely submitted.
16
+ */
17
+ export function planMultilineTyping(text) {
18
+ if (!text)
19
+ return [];
20
+ // Normalise line endings so \r\n and \r both collapse to \n splitting.
21
+ const normalised = text.replace(/\r\n?/g, "\n");
22
+ const segments = normalised.split("\n");
23
+ const actions = [];
24
+ for (let i = 0; i < segments.length; i++) {
25
+ const segment = segments[i];
26
+ if (segment) {
27
+ actions.push({ type: "type", text: segment });
28
+ }
29
+ if (i < segments.length - 1) {
30
+ actions.push({ type: "press", key: "Shift+Enter" });
31
+ }
32
+ }
33
+ return actions;
34
+ }
35
+ /**
36
+ * Execute a typing plan against a Playwright Page.
37
+ *
38
+ * Kept as a thin wrapper over `planMultilineTyping` so the interesting
39
+ * logic stays in the pure function. Adapters call this from
40
+ * `sendMessage`; they MUST NOT fall back to `page.keyboard.type(message)`
41
+ * directly on user-supplied prompts.
42
+ */
43
+ export async function typeMultiline(page, text) {
44
+ const actions = planMultilineTyping(text);
45
+ for (const action of actions) {
46
+ if (action.type === "type") {
47
+ await page.keyboard.type(action.text);
48
+ }
49
+ else {
50
+ await page.keyboard.press(action.key);
51
+ }
52
+ }
53
+ }
54
+ //# sourceMappingURL=keyboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard.js","sourceRoot":"","sources":["../../src/core/keyboard.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,uEAAuE;IACvE,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,OAAO,GAAmB,EAAE,CAAC;IACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAC;QAC7B,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAU,EAAE,IAAY;IAC1D,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAC1C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC3B,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACxC,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;AACH,CAAC"}