@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,41 @@
1
+ /**
2
+ * Heuristic scoring for "is this DOM node our input box / send button /
3
+ * assistant message?" — implements RFC §8.
4
+ *
5
+ * The scoring functions operate on a plain-object descriptor that the
6
+ * provider adapter assembles from a Playwright Locator. This keeps the
7
+ * scoring logic pure and testable.
8
+ */
9
+ export interface ElementDescriptor {
10
+ tag: string;
11
+ role?: string | null;
12
+ ariaLabel?: string | null;
13
+ placeholder?: string | null;
14
+ type?: string | null;
15
+ contentEditable?: boolean;
16
+ visible?: boolean;
17
+ editable?: boolean;
18
+ enabled?: boolean;
19
+ text?: string | null;
20
+ /** Normalised viewport y in [0,1] where 1 == bottom. */
21
+ viewportY?: number;
22
+ /** Truthy if the element lives inside a sidebar / settings / search panel. */
23
+ inAuxiliaryRegion?: boolean;
24
+ /** Truthy if the element looks like a navigation/history item. */
25
+ inHistory?: boolean;
26
+ /** Approx distance in px to the message input. */
27
+ distanceToInput?: number;
28
+ /** Truthy if the element exposes a send/arrow-style icon. */
29
+ hasSendIcon?: boolean;
30
+ /** For assistant message scoring. */
31
+ authorRole?: string | null;
32
+ insideMainConversation?: boolean;
33
+ containsProse?: boolean;
34
+ appearedAfterUserMessage?: boolean;
35
+ textGrew?: boolean;
36
+ }
37
+ export declare function scoreInputCandidate(el: ElementDescriptor): number;
38
+ export declare function scoreSendButtonCandidate(el: ElementDescriptor): number;
39
+ export declare function scoreAssistantMessageCandidate(el: ElementDescriptor): number;
40
+ /** Pick the highest-scoring candidate; ties keep the first listed. */
41
+ export declare function pickBest<T>(items: T[], scorer: (item: T) => number): T | null;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Heuristic scoring for "is this DOM node our input box / send button /
3
+ * assistant message?" — implements RFC §8.
4
+ *
5
+ * The scoring functions operate on a plain-object descriptor that the
6
+ * provider adapter assembles from a Playwright Locator. This keeps the
7
+ * scoring logic pure and testable.
8
+ */
9
+ const INPUT_HINTS = [
10
+ "message",
11
+ "prompt",
12
+ "ask",
13
+ "ask anything", // ChatGPT 2026-era placeholder
14
+ "chat with chatgpt", // ChatGPT aria-label on the ProseMirror editor
15
+ "输入",
16
+ "提问",
17
+ "发送消息",
18
+ "给 deepseek",
19
+ ];
20
+ const SEND_HINTS = ["send", "发送"];
21
+ function containsAny(text, needles) {
22
+ if (!text)
23
+ return false;
24
+ const lower = text.toLowerCase();
25
+ return needles.some((n) => lower.includes(n.toLowerCase()));
26
+ }
27
+ export function scoreInputCandidate(el) {
28
+ let score = 0;
29
+ if (el.role === "textbox")
30
+ score += 5;
31
+ if (el.tag === "textarea" || el.contentEditable)
32
+ score += 5;
33
+ if (containsAny(el.placeholder, INPUT_HINTS))
34
+ score += 4;
35
+ if (containsAny(el.ariaLabel, INPUT_HINTS))
36
+ score += 4;
37
+ if (el.visible)
38
+ score += 3;
39
+ if (el.editable)
40
+ score += 3;
41
+ if (typeof el.viewportY === "number" && el.viewportY > 0.5)
42
+ score += 2;
43
+ if (el.inAuxiliaryRegion)
44
+ score -= 5;
45
+ if (el.visible === false)
46
+ score -= 5;
47
+ if (el.enabled === false)
48
+ score -= 5;
49
+ return score;
50
+ }
51
+ export function scoreSendButtonCandidate(el) {
52
+ let score = 0;
53
+ if (containsAny(el.ariaLabel, SEND_HINTS))
54
+ score += 5;
55
+ if (el.tag === "button")
56
+ score += 4;
57
+ if (typeof el.distanceToInput === "number" && el.distanceToInput < 200)
58
+ score += 3;
59
+ if (el.enabled)
60
+ score += 3;
61
+ if (el.hasSendIcon)
62
+ score += 2;
63
+ if (el.visible === false)
64
+ score -= 5;
65
+ if (el.enabled === false)
66
+ score -= 5;
67
+ return score;
68
+ }
69
+ export function scoreAssistantMessageCandidate(el) {
70
+ let score = 0;
71
+ if (el.authorRole === "assistant")
72
+ score += 5;
73
+ if (el.insideMainConversation)
74
+ score += 4;
75
+ if (el.containsProse)
76
+ score += 3;
77
+ if (el.appearedAfterUserMessage)
78
+ score += 2;
79
+ if (el.textGrew)
80
+ score += 2;
81
+ if (el.inAuxiliaryRegion || el.inHistory)
82
+ score -= 5;
83
+ return score;
84
+ }
85
+ /** Pick the highest-scoring candidate; ties keep the first listed. */
86
+ export function pickBest(items, scorer) {
87
+ if (items.length === 0)
88
+ return null;
89
+ let best = items[0];
90
+ let bestScore = scorer(best);
91
+ for (let i = 1; i < items.length; i++) {
92
+ const candidate = items[i];
93
+ const s = scorer(candidate);
94
+ if (s > bestScore) {
95
+ best = candidate;
96
+ bestScore = s;
97
+ }
98
+ }
99
+ return best;
100
+ }
101
+ //# sourceMappingURL=locator-score.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locator-score.js","sourceRoot":"","sources":["../../src/core/locator-score.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export type LogLevel = "silent" | "error" | "warn" | "info" | "debug";
2
+ export interface Logger {
3
+ level: LogLevel;
4
+ error(...args: unknown[]): void;
5
+ warn(...args: unknown[]): void;
6
+ info(...args: unknown[]): void;
7
+ debug(...args: unknown[]): void;
8
+ }
9
+ export declare function createLogger(level?: LogLevel): Logger;
10
+ export declare const defaultLogger: Logger;
@@ -0,0 +1,38 @@
1
+ /* eslint-disable no-console */
2
+ const LEVELS = {
3
+ silent: 0,
4
+ error: 1,
5
+ warn: 2,
6
+ info: 3,
7
+ debug: 4,
8
+ };
9
+ function envLevel() {
10
+ const raw = (process.env.CHAT_WEB_LOG ?? "info").toLowerCase();
11
+ if (raw in LEVELS)
12
+ return raw;
13
+ return "info";
14
+ }
15
+ export function createLogger(level = envLevel()) {
16
+ const should = (l) => LEVELS[l] <= LEVELS[level];
17
+ return {
18
+ level,
19
+ error(...args) {
20
+ if (should("error"))
21
+ console.error("[chat-web]", ...args);
22
+ },
23
+ warn(...args) {
24
+ if (should("warn"))
25
+ console.warn("[chat-web]", ...args);
26
+ },
27
+ info(...args) {
28
+ if (should("info"))
29
+ console.error("[chat-web]", ...args);
30
+ },
31
+ debug(...args) {
32
+ if (should("debug"))
33
+ console.error("[chat-web:debug]", ...args);
34
+ },
35
+ };
36
+ }
37
+ export const defaultLogger = createLogger();
38
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/core/logger.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAI/B,MAAM,MAAM,GAA6B;IACvC,MAAM,EAAE,CAAC;IACT,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;CACT,CAAC;AAEF,SAAS,QAAQ;IACf,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IAC/D,IAAI,GAAG,IAAI,MAAM;QAAE,OAAO,GAAe,CAAC;IAC1C,OAAO,MAAM,CAAC;AAChB,CAAC;AAUD,MAAM,UAAU,YAAY,CAAC,QAAkB,QAAQ,EAAE;IACvD,MAAM,MAAM,GAAG,CAAC,CAAW,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;IAE3D,OAAO;QACL,KAAK;QACL,KAAK,CAAC,GAAG,IAAI;YACX,IAAI,MAAM,CAAC,OAAO,CAAC;gBAAE,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,CAAC;QAC5D,CAAC;QACD,IAAI,CAAC,GAAG,IAAI;YACV,IAAI,MAAM,CAAC,MAAM,CAAC;gBAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,CAAC,GAAG,IAAI;YACV,IAAI,MAAM,CAAC,MAAM,CAAC;gBAAE,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,CAAC;QAC3D,CAAC;QACD,KAAK,CAAC,GAAG,IAAI;YACX,IAAI,MAAM,CAAC,OAAO,CAAC;gBAAE,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,IAAI,CAAC,CAAC;QAClE,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAW,YAAY,EAAE,CAAC"}
@@ -0,0 +1,52 @@
1
+ import type { Page } from "playwright";
2
+ import { type Logger } from "./logger.js";
3
+ export interface GotoWithRetryOptions {
4
+ /** Hard upper bound per attempt. Default 15_000ms. */
5
+ timeoutMs?: number;
6
+ /** Maximum number of attempts. Default 3. */
7
+ attempts?: number;
8
+ /** Base backoff between attempts, exponentially scaled. Default 1500ms. */
9
+ backoffMs?: number;
10
+ /**
11
+ * Playwright `waitUntil`. Default `"commit"` — the fastest signal that
12
+ * navigation succeeded. We deliberately do NOT wait for `load` or
13
+ * `networkidle`: chat sites pull dozens of subresources (analytics,
14
+ * fonts, CDNs) that block whole-page load events on slow / proxied
15
+ * networks. The adapter waits for the specific composer locator
16
+ * separately, which is what we actually need.
17
+ */
18
+ waitUntil?: "commit" | "domcontentloaded" | "load" | "networkidle";
19
+ logger?: Logger;
20
+ signal?: AbortSignal;
21
+ }
22
+ /**
23
+ * True when the error looks like a transient network condition worth
24
+ * retrying. Excludes DNS-not-found and TLS cert errors — those won't
25
+ * fix themselves on the next attempt.
26
+ */
27
+ export declare function isTransientNavigationError(err: unknown): boolean;
28
+ /**
29
+ * Navigate to a URL with exponential-backoff retries on transient network
30
+ * errors. Intended for adapter `open()` where flaky proxies / unstable
31
+ * networks frequently cause Chromium's first connection to time out
32
+ * even when curl from the same shell works in 1–2s (a real condition
33
+ * we see in users with DNS-rewriting boxes that DPI-fingerprint
34
+ * Chromium's TLS handshake).
35
+ *
36
+ * Behaviour:
37
+ * - Up to `attempts` tries (default 3).
38
+ * - Default per-attempt timeout 15s; the *whole* operation is capped
39
+ * at attempts × (timeoutMs + backoff*2^i).
40
+ * - Only transient errors (see `isTransientNavigationError`) trigger
41
+ * retries. DNS / TLS-cert failures bubble up immediately.
42
+ * - On final failure, the last underlying error is rethrown verbatim
43
+ * so callers can wrap it in their own typed error with context.
44
+ */
45
+ export declare function gotoWithRetry(page: Page, url: string, options?: GotoWithRetryOptions): Promise<void>;
46
+ /**
47
+ * Convenience: navigate and wrap any failure as a {@link ChatWebError}
48
+ * with a clear, user-actionable message. Adapters should use this
49
+ * inside `open()` so a connectivity problem surfaces with a useful
50
+ * hint instead of a raw `net::ERR_TIMED_OUT`.
51
+ */
52
+ export declare function gotoOrThrowNetworkError(page: Page, url: string, provider: string, options?: GotoWithRetryOptions): Promise<void>;
@@ -0,0 +1,102 @@
1
+ import { ChatWebError } from "./errors.js";
2
+ import { defaultLogger } from "./logger.js";
3
+ const TRANSIENT_NET_ERROR_HINTS = [
4
+ "net::err_timed_out",
5
+ "net::err_connection_closed",
6
+ "net::err_connection_reset",
7
+ "net::err_connection_aborted",
8
+ "net::err_network_changed",
9
+ "net::err_aborted",
10
+ "net::err_socket_not_connected",
11
+ "navigation timeout",
12
+ "timeout",
13
+ ];
14
+ /**
15
+ * True when the error looks like a transient network condition worth
16
+ * retrying. Excludes DNS-not-found and TLS cert errors — those won't
17
+ * fix themselves on the next attempt.
18
+ */
19
+ export function isTransientNavigationError(err) {
20
+ const msg = String(err?.message ?? "").toLowerCase();
21
+ if (!msg)
22
+ return false;
23
+ // Don't retry permanent failures.
24
+ if (msg.includes("net::err_name_not_resolved"))
25
+ return false;
26
+ if (msg.includes("net::err_cert_"))
27
+ return false;
28
+ if (msg.includes("net::err_blocked_by"))
29
+ return false;
30
+ return TRANSIENT_NET_ERROR_HINTS.some((hint) => msg.includes(hint));
31
+ }
32
+ /**
33
+ * Navigate to a URL with exponential-backoff retries on transient network
34
+ * errors. Intended for adapter `open()` where flaky proxies / unstable
35
+ * networks frequently cause Chromium's first connection to time out
36
+ * even when curl from the same shell works in 1–2s (a real condition
37
+ * we see in users with DNS-rewriting boxes that DPI-fingerprint
38
+ * Chromium's TLS handshake).
39
+ *
40
+ * Behaviour:
41
+ * - Up to `attempts` tries (default 3).
42
+ * - Default per-attempt timeout 15s; the *whole* operation is capped
43
+ * at attempts × (timeoutMs + backoff*2^i).
44
+ * - Only transient errors (see `isTransientNavigationError`) trigger
45
+ * retries. DNS / TLS-cert failures bubble up immediately.
46
+ * - On final failure, the last underlying error is rethrown verbatim
47
+ * so callers can wrap it in their own typed error with context.
48
+ */
49
+ export async function gotoWithRetry(page, url, options = {}) {
50
+ const timeoutMs = options.timeoutMs ?? 15_000;
51
+ const attempts = Math.max(1, options.attempts ?? 3);
52
+ const backoffMs = options.backoffMs ?? 1_500;
53
+ const waitUntil = options.waitUntil ?? "commit";
54
+ const logger = options.logger ?? defaultLogger;
55
+ let lastError;
56
+ for (let attempt = 1; attempt <= attempts; attempt++) {
57
+ if (options.signal?.aborted) {
58
+ throw new DOMException("Aborted", "AbortError");
59
+ }
60
+ try {
61
+ await page.goto(url, { waitUntil, timeout: timeoutMs });
62
+ if (attempt > 1) {
63
+ logger.info(`chat-web: navigation to ${url} succeeded on attempt ${attempt}/${attempts}`);
64
+ }
65
+ return;
66
+ }
67
+ catch (err) {
68
+ lastError = err;
69
+ if (!isTransientNavigationError(err) || attempt === attempts)
70
+ break;
71
+ const delay = backoffMs * Math.pow(2, attempt - 1);
72
+ logger.warn(`chat-web: navigation to ${url} failed (attempt ${attempt}/${attempts}): ${err.message?.split("\n")[0] ?? "(unknown)"}. Retrying in ${delay}ms…`);
73
+ await new Promise((r) => setTimeout(r, delay));
74
+ }
75
+ }
76
+ // Re-throw the last error unchanged; callers add their own context.
77
+ throw lastError;
78
+ }
79
+ /**
80
+ * Convenience: navigate and wrap any failure as a {@link ChatWebError}
81
+ * with a clear, user-actionable message. Adapters should use this
82
+ * inside `open()` so a connectivity problem surfaces with a useful
83
+ * hint instead of a raw `net::ERR_TIMED_OUT`.
84
+ */
85
+ export async function gotoOrThrowNetworkError(page, url, provider, options = {}) {
86
+ try {
87
+ await gotoWithRetry(page, url, options);
88
+ }
89
+ catch (err) {
90
+ if (isTransientNavigationError(err)) {
91
+ const summary = String(err.message ?? "").split("\n")[0];
92
+ throw new ChatWebError("BROWSER_LAUNCH_FAILED", `Chromium could not reach ${url} for "${provider}" (${summary}).`, {
93
+ provider,
94
+ cause: err,
95
+ hint: "curl from the same shell may still work — that usually means the local proxy / DNS box is DPI-fingerprinting Chromium's TLS handshake. " +
96
+ "Try a different proxy that supports browser traffic, switch networks, or wait for the route to stabilise.",
97
+ });
98
+ }
99
+ throw err;
100
+ }
101
+ }
102
+ //# sourceMappingURL=navigate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigate.js","sourceRoot":"","sources":["../../src/core/navigate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * All filesystem layout for chat-web is centralised here so the CLI,
3
+ * daemon, doctor, and tests share one view of the world.
4
+ *
5
+ * Override the root via `CHAT_WEB_HOME` (useful in tests / CI).
6
+ */
7
+ export declare function rootDir(): string;
8
+ export declare function profilesDir(): string;
9
+ export declare function profileDir(provider: string): string;
10
+ export declare function logsDir(): string;
11
+ export declare function configFile(): string;
12
+ export declare function selectorCacheFile(): string;
@@ -0,0 +1,30 @@
1
+ import { homedir } from "node:os";
2
+ import path from "node:path";
3
+ /**
4
+ * All filesystem layout for chat-web is centralised here so the CLI,
5
+ * daemon, doctor, and tests share one view of the world.
6
+ *
7
+ * Override the root via `CHAT_WEB_HOME` (useful in tests / CI).
8
+ */
9
+ export function rootDir() {
10
+ return process.env.CHAT_WEB_HOME ?? path.join(homedir(), ".chat-web");
11
+ }
12
+ export function profilesDir() {
13
+ return path.join(rootDir(), "profiles");
14
+ }
15
+ export function profileDir(provider) {
16
+ return path.join(profilesDir(), sanitize(provider));
17
+ }
18
+ export function logsDir() {
19
+ return path.join(rootDir(), "logs");
20
+ }
21
+ export function configFile() {
22
+ return path.join(rootDir(), "config.json");
23
+ }
24
+ export function selectorCacheFile() {
25
+ return path.join(rootDir(), "selector-cache.json");
26
+ }
27
+ function sanitize(name) {
28
+ return name.replace(/[^a-zA-Z0-9_\-]/g, "_");
29
+ }
30
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;GAKG;AACH,MAAM,UAAU,OAAO;IACrB,OAAO,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,aAAa,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,qBAAqB,CAAC,CAAC;AACrD,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;AAC/C,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Profile manager owns the on-disk layout for persistent browser profiles.
3
+ *
4
+ * RFC §19.1: we persist the full Chromium userDataDir per provider
5
+ * (cookies + localStorage + IndexedDB + service workers), not just cookies.
6
+ */
7
+ export interface BrowserProfileManager {
8
+ getProfileDir(provider: string): string;
9
+ ensureProfile(provider: string): Promise<string>;
10
+ clearProfile(provider: string): Promise<void>;
11
+ listProfiles(): Promise<string[]>;
12
+ }
13
+ export declare function createProfileManager(): BrowserProfileManager;
@@ -0,0 +1,44 @@
1
+ import fs from "node:fs/promises";
2
+ import { ProfileError } from "./errors.js";
3
+ import { profileDir, profilesDir, logsDir, rootDir } from "./paths.js";
4
+ export function createProfileManager() {
5
+ return {
6
+ getProfileDir(provider) {
7
+ return profileDir(provider);
8
+ },
9
+ async ensureProfile(provider) {
10
+ const dir = profileDir(provider);
11
+ try {
12
+ await fs.mkdir(dir, { recursive: true });
13
+ await fs.mkdir(logsDir(), { recursive: true });
14
+ await fs.mkdir(rootDir(), { recursive: true });
15
+ }
16
+ catch (err) {
17
+ throw new ProfileError(provider, `Failed to create profile dir at ${dir}`, err);
18
+ }
19
+ return dir;
20
+ },
21
+ async clearProfile(provider) {
22
+ const dir = profileDir(provider);
23
+ try {
24
+ await fs.rm(dir, { recursive: true, force: true });
25
+ }
26
+ catch (err) {
27
+ throw new ProfileError(provider, `Failed to clear profile dir at ${dir}`, err);
28
+ }
29
+ },
30
+ async listProfiles() {
31
+ try {
32
+ const entries = await fs.readdir(profilesDir(), { withFileTypes: true });
33
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
34
+ }
35
+ catch (err) {
36
+ const code = err.code;
37
+ if (code === "ENOENT")
38
+ return [];
39
+ throw err;
40
+ }
41
+ },
42
+ };
43
+ }
44
+ //# sourceMappingURL=profile-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile-manager.js","sourceRoot":"","sources":["../../src/core/profile-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAevE,MAAM,UAAU,oBAAoB;IAClC,OAAO;QACL,aAAa,CAAC,QAAQ;YACpB,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC9B,CAAC;QAED,KAAK,CAAC,aAAa,CAAC,QAAQ;YAC1B,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;YACjC,IAAI,CAAC;gBACH,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACzC,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC/C,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACjD,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,IAAI,YAAY,CAAC,QAAQ,EAAE,mCAAmC,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC;YAClF,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,QAAQ;YACzB,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;YACjC,IAAI,CAAC;gBACH,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACrD,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,IAAI,YAAY,CAAC,QAAQ,EAAE,kCAAkC,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC;YACjF,CAAC;QACH,CAAC;QAED,KAAK,CAAC,YAAY;YAChB,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;gBACzE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YACnE,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,IAAI,GAAI,GAA6B,CAAC,IAAI,CAAC;gBACjD,IAAI,IAAI,KAAK,QAAQ;oBAAE,OAAO,EAAE,CAAC;gBACjC,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,64 @@
1
+ import type { Locator, Page } from "playwright";
2
+ export interface WaitOptions {
3
+ /** Hard upper bound for the whole response wait. Default 120_000ms. */
4
+ timeoutMs?: number;
5
+ /** How long the text must stop changing before we call it done. Default 2000ms. */
6
+ stableMs?: number;
7
+ /** Optional cancellation. */
8
+ signal?: AbortSignal;
9
+ /** Streaming progress callback. */
10
+ onProgress?: (text: string) => void;
11
+ }
12
+ /**
13
+ * The minimum surface every provider has to implement so `ask`, `doctor`,
14
+ * `new-chat` and the daemon can be provider-agnostic.
15
+ *
16
+ * Adapters MUST NOT share logic via mixins — RFC §13.2 explicitly calls
17
+ * out that ChatGPT and DeepSeek should not be merged into one branch.
18
+ */
19
+ export interface ChatProvider {
20
+ readonly name: string;
21
+ readonly homeUrl: string;
22
+ open(page: Page): Promise<void>;
23
+ isLoggedIn(page: Page): Promise<boolean>;
24
+ findInput(page: Page): Promise<Locator>;
25
+ findSendButton(page: Page): Promise<Locator | null>;
26
+ sendMessage(page: Page, message: string): Promise<void>;
27
+ waitForResponse(page: Page, options?: WaitOptions): Promise<string>;
28
+ extractLastAssistantMessage(page: Page): Promise<string>;
29
+ newChat?(page: Page): Promise<void>;
30
+ /** For doctor: cheap summary numbers (counts + booleans). */
31
+ diagnose?(page: Page): Promise<ProviderDiagnostics>;
32
+ /**
33
+ * Provider-side conversation identifier as exposed by the web UI.
34
+ *
35
+ * For ChatGPT this is the UUID at `https://chatgpt.com/c/{uuid}`. For
36
+ * Gemini AI Studio it would be the prompts/* id segment. Implement when
37
+ * the provider exposes a stable per-conversation id in its URL/state;
38
+ * the SDK surfaces it as the canonical session id and pushes it through
39
+ * `ChatSession.conversationId` / `SendResult.conversationId` so callers
40
+ * (ai-sdk, frontend) can render real-provider-side links.
41
+ */
42
+ getConversationId?(page: Page): string | null;
43
+ }
44
+ export interface ProviderDiagnostics {
45
+ loggedIn: boolean;
46
+ inputFound: boolean;
47
+ sendButtonFound: boolean;
48
+ assistantMessageCount: number;
49
+ stopButtonFound: boolean;
50
+ lastAssistantLength: number;
51
+ pageUrl: string;
52
+ }
53
+ declare class ProviderRegistry {
54
+ private providers;
55
+ register(provider: ChatProvider): void;
56
+ get(name: string): ChatProvider;
57
+ has(name: string): boolean;
58
+ list(): string[];
59
+ }
60
+ export declare const providerRegistry: ProviderRegistry;
61
+ export declare function getProvider(name: string): ChatProvider;
62
+ export declare function registerProvider(provider: ChatProvider): void;
63
+ export declare function listProviders(): string[];
64
+ export {};
@@ -0,0 +1,31 @@
1
+ import { UnknownProviderError } from "./errors.js";
2
+ class ProviderRegistry {
3
+ providers = new Map();
4
+ register(provider) {
5
+ this.providers.set(provider.name, provider);
6
+ }
7
+ get(name) {
8
+ const provider = this.providers.get(name);
9
+ if (!provider) {
10
+ throw new UnknownProviderError(name, [...this.providers.keys()]);
11
+ }
12
+ return provider;
13
+ }
14
+ has(name) {
15
+ return this.providers.has(name);
16
+ }
17
+ list() {
18
+ return [...this.providers.keys()];
19
+ }
20
+ }
21
+ export const providerRegistry = new ProviderRegistry();
22
+ export function getProvider(name) {
23
+ return providerRegistry.get(name);
24
+ }
25
+ export function registerProvider(provider) {
26
+ providerRegistry.register(provider);
27
+ }
28
+ export function listProviders() {
29
+ return providerRegistry.list();
30
+ }
31
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/core/provider.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA8DnD,MAAM,gBAAgB;IACZ,SAAS,GAAG,IAAI,GAAG,EAAwB,CAAC;IAEpD,QAAQ,CAAC,QAAsB;QAC7B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,GAAG,CAAC,IAAY;QACd,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,oBAAoB,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACnE,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAED,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IACpC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,gBAAgB,EAAE,CAAC;AAEvD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAsB;IACrD,gBAAgB,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,gBAAgB,CAAC,IAAI,EAAE,CAAC;AACjC,CAAC"}
@@ -0,0 +1,35 @@
1
+ import type { Page } from "playwright";
2
+ export interface WaitUntilStableOptions {
3
+ /** How long the text must stop changing before we consider it stable. Default 2000ms. */
4
+ stableMs?: number;
5
+ /** Hard upper bound. Default 120_000ms. */
6
+ timeoutMs?: number;
7
+ /** Poll interval between samples. Default 300ms. */
8
+ pollIntervalMs?: number;
9
+ /** AbortSignal so callers can cancel the wait. */
10
+ signal?: AbortSignal;
11
+ /** Optional callback whenever the sampled text grows (useful for progress UI). */
12
+ onProgress?: (text: string) => void;
13
+ }
14
+ /**
15
+ * Watch a streaming source (typically the last assistant message) until its
16
+ * text stops mutating for `stableMs` consecutive milliseconds, or the
17
+ * `timeoutMs` budget is exhausted.
18
+ *
19
+ * `getText` should return "" (or throw) while the message doesn't exist yet;
20
+ * we treat both cases the same and keep polling.
21
+ *
22
+ * RFC §10 — also recommends combining this with "stop button vanished"
23
+ * and "send button re-enabled"; those checks live in the provider adapters
24
+ * because the selectors are provider-specific.
25
+ */
26
+ export declare function waitUntilStable(getText: () => Promise<string>, options?: WaitUntilStableOptions): Promise<string>;
27
+ export declare function sleep(ms: number): Promise<void>;
28
+ /**
29
+ * Convenience: wait until the assistant message count grows past a baseline.
30
+ * Useful right after pressing Enter, so we know the streaming has started.
31
+ */
32
+ export declare function waitForResponseStart(page: Page, selector: string, baseline: number, options?: {
33
+ timeoutMs?: number;
34
+ pollIntervalMs?: number;
35
+ }): Promise<void>;