@steipete/oracle 0.9.0 → 0.10.0

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 (177) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +61 -48
  3. package/dist/bin/oracle-cli.js +455 -402
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/assistantResponse.js +149 -101
  18. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  19. package/dist/src/browser/actions/attachments.js +246 -150
  20. package/dist/src/browser/actions/domEvents.js +2 -2
  21. package/dist/src/browser/actions/modelSelection.js +275 -117
  22. package/dist/src/browser/actions/navigation.js +161 -137
  23. package/dist/src/browser/actions/promptComposer.js +100 -64
  24. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  25. package/dist/src/browser/actions/thinkingTime.js +207 -110
  26. package/dist/src/browser/chromeLifecycle.js +62 -60
  27. package/dist/src/browser/config.js +34 -15
  28. package/dist/src/browser/constants.js +17 -12
  29. package/dist/src/browser/cookies.js +19 -19
  30. package/dist/src/browser/detect.js +62 -62
  31. package/dist/src/browser/domDebug.js +1 -1
  32. package/dist/src/browser/index.js +390 -295
  33. package/dist/src/browser/modelStrategy.js +1 -1
  34. package/dist/src/browser/pageActions.js +5 -5
  35. package/dist/src/browser/policies.js +16 -13
  36. package/dist/src/browser/profileState.js +44 -39
  37. package/dist/src/browser/prompt.js +72 -42
  38. package/dist/src/browser/promptSummary.js +5 -5
  39. package/dist/src/browser/providerDomFlow.js +1 -1
  40. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  42. package/dist/src/browser/providers/index.js +2 -2
  43. package/dist/src/browser/reattach.js +67 -34
  44. package/dist/src/browser/reattachHelpers.js +31 -26
  45. package/dist/src/browser/sessionRunner.js +37 -25
  46. package/dist/src/browser/utils.js +9 -9
  47. package/dist/src/browserMode.js +1 -1
  48. package/dist/src/cli/bridge/claudeConfig.js +16 -16
  49. package/dist/src/cli/bridge/client.js +28 -20
  50. package/dist/src/cli/bridge/codexConfig.js +16 -16
  51. package/dist/src/cli/bridge/doctor.js +47 -39
  52. package/dist/src/cli/bridge/host.js +58 -56
  53. package/dist/src/cli/browserConfig.js +62 -48
  54. package/dist/src/cli/browserDefaults.js +27 -26
  55. package/dist/src/cli/bundleWarnings.js +1 -1
  56. package/dist/src/cli/clipboard.js +11 -2
  57. package/dist/src/cli/detach.js +2 -2
  58. package/dist/src/cli/dryRun.js +29 -25
  59. package/dist/src/cli/duplicatePromptGuard.js +3 -3
  60. package/dist/src/cli/engine.js +9 -9
  61. package/dist/src/cli/errorUtils.js +1 -1
  62. package/dist/src/cli/fileSize.js +3 -3
  63. package/dist/src/cli/format.js +2 -2
  64. package/dist/src/cli/help.js +28 -28
  65. package/dist/src/cli/hiddenAliases.js +3 -3
  66. package/dist/src/cli/markdownBundle.js +7 -7
  67. package/dist/src/cli/markdownRenderer.js +15 -15
  68. package/dist/src/cli/notifier.js +77 -67
  69. package/dist/src/cli/options.js +127 -106
  70. package/dist/src/cli/oscUtils.js +1 -1
  71. package/dist/src/cli/promptRequirement.js +2 -2
  72. package/dist/src/cli/renderOutput.js +1 -1
  73. package/dist/src/cli/rootAlias.js +1 -1
  74. package/dist/src/cli/runOptions.js +32 -28
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +95 -81
  77. package/dist/src/cli/sessionLineage.js +6 -2
  78. package/dist/src/cli/sessionRunner.js +103 -93
  79. package/dist/src/cli/sessionTable.js +26 -23
  80. package/dist/src/cli/stdin.js +22 -0
  81. package/dist/src/cli/tagline.js +121 -124
  82. package/dist/src/cli/tui/index.js +139 -128
  83. package/dist/src/cli/writeOutputPath.js +5 -5
  84. package/dist/src/config.js +7 -7
  85. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  86. package/dist/src/gemini-web/client.js +76 -70
  87. package/dist/src/gemini-web/executionMode.js +6 -8
  88. package/dist/src/gemini-web/executor.js +98 -93
  89. package/dist/src/gemini-web/index.js +1 -1
  90. package/dist/src/mcp/server.js +16 -12
  91. package/dist/src/mcp/tools/consult.js +51 -47
  92. package/dist/src/mcp/tools/sessionResources.js +12 -12
  93. package/dist/src/mcp/tools/sessions.js +26 -17
  94. package/dist/src/mcp/types.js +5 -5
  95. package/dist/src/mcp/utils.js +15 -7
  96. package/dist/src/oracle/background.js +15 -15
  97. package/dist/src/oracle/claude.js +53 -25
  98. package/dist/src/oracle/client.js +50 -41
  99. package/dist/src/oracle/config.js +96 -66
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +55 -46
  102. package/dist/src/oracle/finishLine.js +10 -8
  103. package/dist/src/oracle/format.js +3 -3
  104. package/dist/src/oracle/gemini.js +37 -33
  105. package/dist/src/oracle/logging.js +7 -7
  106. package/dist/src/oracle/markdown.js +28 -28
  107. package/dist/src/oracle/modelResolver.js +16 -16
  108. package/dist/src/oracle/multiModelRunner.js +12 -12
  109. package/dist/src/oracle/oscProgress.js +8 -8
  110. package/dist/src/oracle/promptAssembly.js +6 -3
  111. package/dist/src/oracle/request.js +16 -13
  112. package/dist/src/oracle/run.js +156 -134
  113. package/dist/src/oracle/runUtils.js +8 -5
  114. package/dist/src/oracle/tokenEstimate.js +6 -6
  115. package/dist/src/oracle/tokenStats.js +5 -5
  116. package/dist/src/oracle/tokenStringifier.js +5 -5
  117. package/dist/src/oracle.js +12 -12
  118. package/dist/src/oracleHome.js +3 -3
  119. package/dist/src/remote/client.js +25 -25
  120. package/dist/src/remote/health.js +20 -20
  121. package/dist/src/remote/remoteServiceConfig.js +9 -9
  122. package/dist/src/remote/server.js +129 -118
  123. package/dist/src/sessionManager.js +77 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/README.md +2 -0
  127. package/package.json +66 -62
  128. package/vendor/oracle-notifier/README.md +2 -0
  129. package/dist/markdansi/types/index.js +0 -4
  130. package/dist/oracle/bin/oracle-cli.js +0 -472
  131. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  132. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  133. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  134. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  135. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  136. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  137. package/dist/oracle/src/browser/config.js +0 -33
  138. package/dist/oracle/src/browser/constants.js +0 -40
  139. package/dist/oracle/src/browser/cookies.js +0 -210
  140. package/dist/oracle/src/browser/domDebug.js +0 -36
  141. package/dist/oracle/src/browser/index.js +0 -331
  142. package/dist/oracle/src/browser/pageActions.js +0 -5
  143. package/dist/oracle/src/browser/prompt.js +0 -88
  144. package/dist/oracle/src/browser/promptSummary.js +0 -20
  145. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  146. package/dist/oracle/src/browser/types.js +0 -1
  147. package/dist/oracle/src/browser/utils.js +0 -62
  148. package/dist/oracle/src/browserMode.js +0 -1
  149. package/dist/oracle/src/cli/browserConfig.js +0 -44
  150. package/dist/oracle/src/cli/dryRun.js +0 -59
  151. package/dist/oracle/src/cli/engine.js +0 -17
  152. package/dist/oracle/src/cli/errorUtils.js +0 -9
  153. package/dist/oracle/src/cli/help.js +0 -70
  154. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  155. package/dist/oracle/src/cli/options.js +0 -103
  156. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  157. package/dist/oracle/src/cli/rootAlias.js +0 -30
  158. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  159. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  160. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  161. package/dist/oracle/src/heartbeat.js +0 -43
  162. package/dist/oracle/src/oracle/client.js +0 -48
  163. package/dist/oracle/src/oracle/config.js +0 -29
  164. package/dist/oracle/src/oracle/errors.js +0 -101
  165. package/dist/oracle/src/oracle/files.js +0 -220
  166. package/dist/oracle/src/oracle/format.js +0 -33
  167. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  168. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  169. package/dist/oracle/src/oracle/request.js +0 -48
  170. package/dist/oracle/src/oracle/run.js +0 -444
  171. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  172. package/dist/oracle/src/oracle/types.js +0 -1
  173. package/dist/oracle/src/oracle.js +0 -9
  174. package/dist/oracle/src/sessionManager.js +0 -205
  175. package/dist/oracle/src/version.js +0 -39
  176. package/dist/scripts/chrome/browser-tools.js +0 -295
  177. package/dist/src/browser/profileSync.js +0 -141
@@ -1,205 +0,0 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
- import fs from 'node:fs/promises';
4
- import { createWriteStream } from 'node:fs';
5
- const ORACLE_HOME = process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), '.oracle');
6
- const SESSIONS_DIR = path.join(ORACLE_HOME, 'sessions');
7
- const MAX_STATUS_LIMIT = 1000;
8
- const DEFAULT_SLUG = 'session';
9
- const MAX_SLUG_WORDS = 5;
10
- const MIN_CUSTOM_SLUG_WORDS = 3;
11
- async function ensureDir(dirPath) {
12
- await fs.mkdir(dirPath, { recursive: true });
13
- }
14
- export async function ensureSessionStorage() {
15
- await ensureDir(SESSIONS_DIR);
16
- }
17
- function slugify(text, maxWords = MAX_SLUG_WORDS) {
18
- const normalized = text?.toLowerCase() ?? '';
19
- const words = normalized.match(/[a-z0-9]+/g) ?? [];
20
- const trimmed = words.slice(0, maxWords);
21
- return trimmed.length > 0 ? trimmed.join('-') : DEFAULT_SLUG;
22
- }
23
- function countSlugWords(slug) {
24
- return slug.split('-').filter(Boolean).length;
25
- }
26
- function normalizeCustomSlug(candidate) {
27
- const slug = slugify(candidate, MAX_SLUG_WORDS);
28
- const wordCount = countSlugWords(slug);
29
- if (wordCount < MIN_CUSTOM_SLUG_WORDS || wordCount > MAX_SLUG_WORDS) {
30
- throw new Error(`Custom slug must include between ${MIN_CUSTOM_SLUG_WORDS} and ${MAX_SLUG_WORDS} words.`);
31
- }
32
- return slug;
33
- }
34
- export function createSessionId(prompt, customSlug) {
35
- if (customSlug) {
36
- return normalizeCustomSlug(customSlug);
37
- }
38
- return slugify(prompt);
39
- }
40
- function sessionDir(id) {
41
- return path.join(SESSIONS_DIR, id);
42
- }
43
- function metaPath(id) {
44
- return path.join(sessionDir(id), 'session.json');
45
- }
46
- function logPath(id) {
47
- return path.join(sessionDir(id), 'output.log');
48
- }
49
- function requestPath(id) {
50
- return path.join(sessionDir(id), 'request.json');
51
- }
52
- async function fileExists(targetPath) {
53
- try {
54
- await fs.access(targetPath);
55
- return true;
56
- }
57
- catch {
58
- return false;
59
- }
60
- }
61
- async function ensureUniqueSessionId(baseSlug) {
62
- let candidate = baseSlug;
63
- let suffix = 2;
64
- while (await fileExists(sessionDir(candidate))) {
65
- candidate = `${baseSlug}-${suffix}`;
66
- suffix += 1;
67
- }
68
- return candidate;
69
- }
70
- export async function initializeSession(options, cwd) {
71
- await ensureSessionStorage();
72
- const baseSlug = createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
73
- const sessionId = await ensureUniqueSessionId(baseSlug);
74
- const dir = sessionDir(sessionId);
75
- await ensureDir(dir);
76
- const mode = options.mode ?? 'api';
77
- const browserConfig = options.browserConfig;
78
- const metadata = {
79
- id: sessionId,
80
- createdAt: new Date().toISOString(),
81
- status: 'pending',
82
- promptPreview: (options.prompt || '').slice(0, 160),
83
- model: options.model,
84
- cwd,
85
- mode,
86
- browser: browserConfig ? { config: browserConfig } : undefined,
87
- options: {
88
- prompt: options.prompt,
89
- file: options.file ?? [],
90
- model: options.model,
91
- maxInput: options.maxInput,
92
- system: options.system,
93
- maxOutput: options.maxOutput,
94
- silent: options.silent,
95
- filesReport: options.filesReport,
96
- slug: sessionId,
97
- mode,
98
- browserConfig,
99
- verbose: options.verbose,
100
- heartbeatIntervalMs: options.heartbeatIntervalMs,
101
- browserInlineFiles: options.browserInlineFiles,
102
- background: options.background,
103
- },
104
- };
105
- await fs.writeFile(metaPath(sessionId), JSON.stringify(metadata, null, 2), 'utf8');
106
- await fs.writeFile(requestPath(sessionId), JSON.stringify(metadata.options, null, 2), 'utf8');
107
- await fs.writeFile(logPath(sessionId), '', 'utf8');
108
- return metadata;
109
- }
110
- export async function readSessionMetadata(sessionId) {
111
- try {
112
- const raw = await fs.readFile(metaPath(sessionId), 'utf8');
113
- return JSON.parse(raw);
114
- }
115
- catch {
116
- return null;
117
- }
118
- }
119
- export async function updateSessionMetadata(sessionId, updates) {
120
- const existing = (await readSessionMetadata(sessionId)) ?? { id: sessionId };
121
- const next = { ...existing, ...updates };
122
- await fs.writeFile(metaPath(sessionId), JSON.stringify(next, null, 2), 'utf8');
123
- return next;
124
- }
125
- export function createSessionLogWriter(sessionId) {
126
- const stream = createWriteStream(logPath(sessionId), { flags: 'a' });
127
- const logLine = (line = '') => {
128
- stream.write(`${line}\n`);
129
- };
130
- const writeChunk = (chunk) => {
131
- stream.write(chunk);
132
- return true;
133
- };
134
- return { stream, logLine, writeChunk, logPath: logPath(sessionId) };
135
- }
136
- export async function listSessionsMetadata() {
137
- await ensureSessionStorage();
138
- const entries = await fs.readdir(SESSIONS_DIR).catch(() => []);
139
- const metas = [];
140
- for (const entry of entries) {
141
- const meta = await readSessionMetadata(entry);
142
- if (meta) {
143
- metas.push(meta);
144
- }
145
- }
146
- return metas.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
147
- }
148
- export function filterSessionsByRange(metas, { hours = 24, includeAll = false, limit = 100 }) {
149
- const maxLimit = Math.min(limit, MAX_STATUS_LIMIT);
150
- let filtered = metas;
151
- if (!includeAll) {
152
- const cutoff = Date.now() - hours * 60 * 60 * 1000;
153
- filtered = metas.filter((meta) => new Date(meta.createdAt).getTime() >= cutoff);
154
- }
155
- const limited = filtered.slice(0, maxLimit);
156
- const truncated = filtered.length > maxLimit;
157
- return { entries: limited, truncated, total: filtered.length };
158
- }
159
- export async function readSessionLog(sessionId) {
160
- try {
161
- return await fs.readFile(logPath(sessionId), 'utf8');
162
- }
163
- catch {
164
- return '';
165
- }
166
- }
167
- export async function deleteSessionsOlderThan({ hours = 24, includeAll = false, } = {}) {
168
- await ensureSessionStorage();
169
- const entries = await fs.readdir(SESSIONS_DIR).catch(() => []);
170
- if (!entries.length) {
171
- return { deleted: 0, remaining: 0 };
172
- }
173
- const cutoff = includeAll ? Number.NEGATIVE_INFINITY : Date.now() - hours * 60 * 60 * 1000;
174
- let deleted = 0;
175
- for (const entry of entries) {
176
- const dir = sessionDir(entry);
177
- let createdMs;
178
- const meta = await readSessionMetadata(entry);
179
- if (meta?.createdAt) {
180
- const parsed = Date.parse(meta.createdAt);
181
- if (!Number.isNaN(parsed)) {
182
- createdMs = parsed;
183
- }
184
- }
185
- if (createdMs == null) {
186
- try {
187
- const stats = await fs.stat(dir);
188
- createdMs = stats.birthtimeMs || stats.mtimeMs;
189
- }
190
- catch {
191
- continue;
192
- }
193
- }
194
- if (includeAll || (createdMs != null && createdMs < cutoff)) {
195
- await fs.rm(dir, { recursive: true, force: true });
196
- deleted += 1;
197
- }
198
- }
199
- const remaining = Math.max(entries.length - deleted, 0);
200
- return { deleted, remaining };
201
- }
202
- export async function wait(ms) {
203
- return new Promise((resolve) => setTimeout(resolve, ms));
204
- }
205
- export { ORACLE_HOME, SESSIONS_DIR, MAX_STATUS_LIMIT };
@@ -1,39 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- let cachedVersion = null;
5
- export function getCliVersion() {
6
- if (cachedVersion) {
7
- return cachedVersion;
8
- }
9
- cachedVersion = readVersionFromPackage();
10
- return cachedVersion;
11
- }
12
- function readVersionFromPackage() {
13
- const modulePath = fileURLToPath(import.meta.url);
14
- let currentDir = path.dirname(modulePath);
15
- const filesystemRoot = path.parse(currentDir).root;
16
- // biome-ignore lint/nursery/noUnnecessaryConditions: deliberate sentinel loop to walk up directories
17
- while (true) {
18
- const candidate = path.join(currentDir, 'package.json');
19
- try {
20
- const raw = readFileSync(candidate, 'utf8');
21
- const parsed = JSON.parse(raw);
22
- const version = typeof parsed.version === 'string' && parsed.version.trim().length > 0
23
- ? parsed.version.trim()
24
- : '0.0.0';
25
- return version;
26
- }
27
- catch (error) {
28
- const code = error instanceof Error && 'code' in error ? error.code : undefined;
29
- if (code && code !== 'ENOENT') {
30
- break;
31
- }
32
- }
33
- if (currentDir === filesystemRoot) {
34
- break;
35
- }
36
- currentDir = path.dirname(currentDir);
37
- }
38
- return '0.0.0';
39
- }
@@ -1,295 +0,0 @@
1
- import { Command } from 'commander';
2
- import { execSync, spawn } from 'node:child_process';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import puppeteer from 'puppeteer-core';
6
- const DEFAULT_PORT = 9222;
7
- const DEFAULT_PROFILE_DIR = path.join(os.homedir(), '.cache', 'scraping');
8
- const DEFAULT_CHROME_BIN = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
9
- function browserURL(port) {
10
- return `http://localhost:${port}`;
11
- }
12
- async function connectBrowser(port) {
13
- return puppeteer.connect({ browserURL: browserURL(port), defaultViewport: null });
14
- }
15
- async function getActivePage(port) {
16
- const browser = await connectBrowser(port);
17
- const pages = await browser.pages();
18
- const page = pages.at(-1);
19
- if (!page) {
20
- await browser.disconnect();
21
- throw new Error('No active tab found');
22
- }
23
- return { browser, page };
24
- }
25
- const program = new Command();
26
- program
27
- .name('browser-tools')
28
- .description('Minimal Chrome DevTools helpers inspired by Mario Zechner’s “What if you don’t need MCP?” article.')
29
- .configureHelp({
30
- sortSubcommands: true,
31
- })
32
- .showSuggestionAfterError();
33
- program
34
- .command('start')
35
- .description('Launch Chrome with remote debugging enabled.')
36
- .option('-p, --port <number>', 'Remote debugging port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
37
- .option('--profile', 'Copy your existing Chrome profile before launching.', false)
38
- .option('--profile-dir <path>', 'Directory for the temporary Chrome profile.', DEFAULT_PROFILE_DIR)
39
- .option('--chrome-path <path>', 'Path to the Chrome binary.', DEFAULT_CHROME_BIN)
40
- .action(async (options) => {
41
- const { port, profile, profileDir, chromePath } = options;
42
- try {
43
- execSync("killall 'Google Chrome'", { stdio: 'ignore' });
44
- }
45
- catch {
46
- // ignore
47
- }
48
- await new Promise((resolve) => setTimeout(resolve, 1000));
49
- execSync(`mkdir -p "${profileDir}"`);
50
- if (profile) {
51
- const source = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome') + '/';
52
- execSync(`rsync -a --delete "${source}" "${profileDir}/"`, { stdio: 'ignore' });
53
- }
54
- spawn(chromePath, ['--remote-debugging-port=' + port, `--user-data-dir=${profileDir}`, '--no-first-run', '--disable-popup-blocking'], {
55
- detached: true,
56
- stdio: 'ignore',
57
- }).unref();
58
- let connected = false;
59
- for (let attempt = 0; attempt < 30; attempt++) {
60
- try {
61
- const browser = await connectBrowser(port);
62
- await browser.disconnect();
63
- connected = true;
64
- break;
65
- }
66
- catch {
67
- await new Promise((resolve) => setTimeout(resolve, 500));
68
- }
69
- }
70
- if (!connected) {
71
- console.error(`✗ Failed to start Chrome on port ${port}`);
72
- process.exit(1);
73
- }
74
- console.log(`✓ Chrome listening on http://localhost:${port}${profile ? ' (with your profile)' : ''}`);
75
- });
76
- program
77
- .command('nav <url>')
78
- .description('Navigate the current tab or open a new tab.')
79
- .option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
80
- .option('--new', 'Open in a new tab instead of reusing the current tab.', false)
81
- .action(async (url, options) => {
82
- const port = options.port;
83
- const browser = await connectBrowser(port);
84
- try {
85
- if (options.new) {
86
- const page = await browser.newPage();
87
- await page.goto(url, { waitUntil: 'domcontentloaded' });
88
- console.log('✓ Opened in new tab:', url);
89
- }
90
- else {
91
- const pages = await browser.pages();
92
- const page = pages.at(-1);
93
- if (!page) {
94
- throw new Error('No active tab found');
95
- }
96
- await page.goto(url, { waitUntil: 'domcontentloaded' });
97
- console.log('✓ Navigated current tab to:', url);
98
- }
99
- }
100
- finally {
101
- await browser.disconnect();
102
- }
103
- });
104
- program
105
- .command('eval <code...>')
106
- .description('Evaluate JavaScript in the active page context.')
107
- .option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
108
- .action(async (code, options) => {
109
- const snippet = code.join(' ');
110
- const port = options.port;
111
- const { browser, page } = await getActivePage(port);
112
- try {
113
- const result = await page.evaluate((body) => {
114
- const AsyncFunctionConstructor = Object.getPrototypeOf(async function () { }).constructor;
115
- return new AsyncFunctionConstructor(`return (${body})`)();
116
- }, snippet);
117
- if (Array.isArray(result)) {
118
- result.forEach((entry, index) => {
119
- if (index > 0)
120
- console.log('');
121
- Object.entries(entry).forEach(([key, value]) => console.log(`${key}: ${value}`));
122
- });
123
- }
124
- else if (typeof result === 'object' && result !== null) {
125
- Object.entries(result).forEach(([key, value]) => console.log(`${key}: ${value}`));
126
- }
127
- else {
128
- console.log(result);
129
- }
130
- }
131
- finally {
132
- await browser.disconnect();
133
- }
134
- });
135
- program
136
- .command('screenshot')
137
- .description('Capture the current viewport and print the temp PNG path.')
138
- .option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
139
- .action(async (options) => {
140
- const port = options.port;
141
- const { browser, page } = await getActivePage(port);
142
- try {
143
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
144
- const filePath = path.join(os.tmpdir(), `screenshot-${timestamp}.png`);
145
- await page.screenshot({ path: filePath });
146
- console.log(filePath);
147
- }
148
- finally {
149
- await browser.disconnect();
150
- }
151
- });
152
- program
153
- .command('pick <message...>')
154
- .description('Interactive DOM picker that prints metadata for clicked elements.')
155
- .option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
156
- .action(async (messageParts, options) => {
157
- const message = messageParts.join(' ');
158
- const port = options.port;
159
- const { browser, page } = await getActivePage(port);
160
- try {
161
- await page.evaluate(() => {
162
- const scope = globalThis;
163
- if (scope.pickOverlayInjected) {
164
- return;
165
- }
166
- scope.pickOverlayInjected = true;
167
- scope.pick = async (prompt) => new Promise((resolve) => {
168
- const selections = [];
169
- const selectedElements = new Set();
170
- const overlay = document.createElement('div');
171
- overlay.style.cssText =
172
- 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none';
173
- const highlight = document.createElement('div');
174
- highlight.style.cssText =
175
- 'position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);transition:all 0.05s ease';
176
- overlay.appendChild(highlight);
177
- const banner = document.createElement('div');
178
- banner.style.cssText =
179
- 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:#fff;padding:12px 24px;border-radius:8px;font:14px system-ui;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647';
180
- const updateBanner = () => {
181
- banner.textContent = `${prompt} (${selections.length} selected, Cmd/Ctrl+click to add, Enter to finish, ESC to cancel)`;
182
- };
183
- const cleanup = () => {
184
- document.removeEventListener('mousemove', onMove, true);
185
- document.removeEventListener('click', onClick, true);
186
- document.removeEventListener('keydown', onKey, true);
187
- overlay.remove();
188
- banner.remove();
189
- selectedElements.forEach((el) => (el.style.outline = ''));
190
- };
191
- const serialize = (el) => {
192
- const parents = [];
193
- let current = el.parentElement;
194
- while (current && current !== document.body) {
195
- const id = current.id ? `#${current.id}` : '';
196
- const cls = current.className ? `.${current.className.trim().split(/\\s+/).join('.')}` : '';
197
- parents.push(`${current.tagName.toLowerCase()}${id}${cls}`);
198
- current = current.parentElement;
199
- }
200
- return {
201
- tag: el.tagName.toLowerCase(),
202
- id: el.id || null,
203
- class: el.className || null,
204
- text: el.textContent?.trim()?.slice(0, 200) || null,
205
- html: el.outerHTML.slice(0, 500),
206
- parents: parents.join(' > '),
207
- };
208
- };
209
- const onMove = (event) => {
210
- const node = document.elementFromPoint(event.clientX, event.clientY);
211
- if (!node || overlay.contains(node) || banner.contains(node))
212
- return;
213
- const rect = node.getBoundingClientRect();
214
- highlight.style.cssText = `position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);top:${rect.top}px;left:${rect.left}px;width:${rect.width}px;height:${rect.height}px`;
215
- };
216
- const onClick = (event) => {
217
- if (banner.contains(event.target))
218
- return;
219
- event.preventDefault();
220
- event.stopPropagation();
221
- const node = document.elementFromPoint(event.clientX, event.clientY);
222
- if (!node || overlay.contains(node) || banner.contains(node))
223
- return;
224
- if (event.metaKey || event.ctrlKey) {
225
- if (!selectedElements.has(node)) {
226
- selectedElements.add(node);
227
- node.style.outline = '3px solid #10b981';
228
- selections.push(serialize(node));
229
- updateBanner();
230
- }
231
- }
232
- else {
233
- cleanup();
234
- const info = serialize(node);
235
- resolve(selections.length > 0 ? selections : info);
236
- }
237
- };
238
- const onKey = (event) => {
239
- if (event.key === 'Escape') {
240
- cleanup();
241
- resolve(null);
242
- }
243
- else if (event.key === 'Enter' && selections.length > 0) {
244
- cleanup();
245
- resolve(selections);
246
- }
247
- };
248
- document.addEventListener('mousemove', onMove, true);
249
- document.addEventListener('click', onClick, true);
250
- document.addEventListener('keydown', onKey, true);
251
- document.body.append(overlay, banner);
252
- updateBanner();
253
- });
254
- });
255
- const result = await page.evaluate((msg) => {
256
- const pickFn = window.pick;
257
- if (!pickFn) {
258
- return null;
259
- }
260
- return pickFn(msg);
261
- }, message);
262
- if (Array.isArray(result)) {
263
- result.forEach((entry, index) => {
264
- if (index > 0)
265
- console.log('');
266
- Object.entries(entry).forEach(([key, value]) => console.log(`${key}: ${value}`));
267
- });
268
- }
269
- else if (result && typeof result === 'object') {
270
- Object.entries(result).forEach(([key, value]) => console.log(`${key}: ${value}`));
271
- }
272
- else {
273
- console.log(result);
274
- }
275
- }
276
- finally {
277
- await browser.disconnect();
278
- }
279
- });
280
- program
281
- .command('cookies')
282
- .description('Dump cookies from the active tab as JSON.')
283
- .option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
284
- .action(async (options) => {
285
- const port = options.port;
286
- const { browser, page } = await getActivePage(port);
287
- try {
288
- const cookies = await page.cookies();
289
- console.log(JSON.stringify(cookies, null, 2));
290
- }
291
- finally {
292
- await browser.disconnect();
293
- }
294
- });
295
- program.parseAsync(process.argv);
@@ -1,141 +0,0 @@
1
- import { mkdir, rm, cp as copyDir } from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
- import { spawnSync } from 'node:child_process';
4
- import path from 'node:path';
5
- import { defaultProfileRoot, expandPath, looksLikePath } from './chromeCookies.js';
6
- const DIR_EXCLUDES = [
7
- 'Cache',
8
- 'Code Cache',
9
- 'GPUCache',
10
- 'Service Worker',
11
- 'Crashpad',
12
- 'BrowserMetrics*',
13
- 'GrShaderCache',
14
- 'ShaderCache',
15
- 'OptimizationGuide',
16
- ];
17
- const FILE_EXCLUDES = [
18
- 'SingletonLock',
19
- 'SingletonSocket',
20
- 'SingletonCookie',
21
- '*.lock',
22
- 'lockfile',
23
- 'Lock',
24
- '*.tmp',
25
- 'DevToolsActivePort',
26
- path.join('Default', 'DevToolsActivePort'),
27
- path.join('Sessions', '*'),
28
- 'Current Session',
29
- 'Current Tabs',
30
- 'Last Session',
31
- 'Last Tabs',
32
- ];
33
- export async function syncChromeProfile(options) {
34
- const { targetDir } = options;
35
- await mkdir(targetDir, { recursive: true });
36
- const { sourceDir, profileName } = await resolveProfileSource(options.profile, options.explicitPath);
37
- const logger = options.logger;
38
- if (!existsSync(sourceDir)) {
39
- throw new Error(`Chrome profile not found at ${sourceDir}. Log in once in Chrome, then retry.`);
40
- }
41
- // Clean any stale DevTools ports/locks in the target before copying.
42
- await rm(targetDir, { recursive: true, force: true }).catch(() => undefined);
43
- await mkdir(targetDir, { recursive: true });
44
- const result = process.platform === 'win32'
45
- ? await copyWithRobocopy(sourceDir, targetDir, logger)
46
- : await copyWithRsync(sourceDir, targetDir, logger);
47
- // Remove lock files in the copied profile to avoid "already running" errors.
48
- await removeLocks(targetDir);
49
- return {
50
- source: sourceDir,
51
- profileName,
52
- method: result.method,
53
- status: result.copied ? 'copied' : 'skipped',
54
- };
55
- }
56
- async function copyWithRsync(sourceDir, targetDir, logger) {
57
- const rsyncArgs = [
58
- '-a',
59
- '--delete',
60
- ...DIR_EXCLUDES.flatMap((entry) => ['--exclude', entry]),
61
- ...FILE_EXCLUDES.flatMap((entry) => ['--exclude', entry]),
62
- `${sourceDir}/`,
63
- `${targetDir}/`,
64
- ];
65
- const attempt = spawnSync('rsync', rsyncArgs, { stdio: 'pipe' });
66
- if (!attempt.error && (attempt.status ?? 0) === 0) {
67
- return { copied: true, method: 'rsync' };
68
- }
69
- logger?.('rsync unavailable or failed; falling back to Node copy');
70
- await copyDirWithFilter(sourceDir, targetDir);
71
- return copyWithNodeFs();
72
- }
73
- async function copyWithRobocopy(sourceDir, targetDir, logger) {
74
- const args = [sourceDir, targetDir, '/MIR', '/NFL', '/NDL', '/NJH', '/NJS', '/NP', '/Z'];
75
- if (DIR_EXCLUDES.length) {
76
- args.push('/XD', ...DIR_EXCLUDES);
77
- }
78
- if (FILE_EXCLUDES.length) {
79
- args.push('/XF', ...FILE_EXCLUDES);
80
- }
81
- const attempt = spawnSync('robocopy', args, { stdio: 'pipe' });
82
- const exitCode = attempt.status ?? 0;
83
- // Robocopy treats 0-7 as success/partial success; >=8 is failure.
84
- if (!attempt.error && exitCode < 8) {
85
- return { copied: true, method: 'robocopy' };
86
- }
87
- logger?.('robocopy failed; falling back to Node copy');
88
- await copyDirWithFilter(sourceDir, targetDir);
89
- return copyWithNodeFs();
90
- }
91
- function copyWithNodeFs() {
92
- return { copied: true, method: 'node' };
93
- }
94
- function shouldExclude(relativePath) {
95
- const normalized = relativePath.replace(/\\/g, '/');
96
- return DIR_EXCLUDES.some((entry) => normalized === entry || normalized.startsWith(`${entry}/`)) ||
97
- FILE_EXCLUDES.some((entry) => {
98
- if (entry.endsWith('*')) {
99
- return normalized.startsWith(entry.slice(0, -1));
100
- }
101
- if (entry.includes('*')) {
102
- // simple glob support for BrowserMetrics*
103
- const prefix = entry.replace('*', '');
104
- return normalized.startsWith(prefix);
105
- }
106
- return path.basename(normalized) === entry || normalized.endsWith(`/${entry}`);
107
- });
108
- }
109
- async function removeLocks(targetDir) {
110
- const lockNames = ['SingletonLock', 'SingletonSocket', 'SingletonCookie', 'DevToolsActivePort'];
111
- for (const lock of lockNames) {
112
- await rm(path.join(targetDir, lock), { force: true }).catch(() => undefined);
113
- await rm(path.join(targetDir, 'Default', lock), { force: true }).catch(() => undefined);
114
- }
115
- }
116
- async function resolveProfileSource(profile, explicitPath) {
117
- const profileName = profile?.trim() ? profile.trim() : 'Default';
118
- if (explicitPath?.trim()) {
119
- const resolved = expandPath(explicitPath.trim());
120
- if (resolved.toLowerCase().endsWith('cookies')) {
121
- return { sourceDir: path.dirname(resolved), profileName };
122
- }
123
- return { sourceDir: resolved, profileName };
124
- }
125
- if (looksLikePath(profileName)) {
126
- return { sourceDir: expandPath(profileName), profileName };
127
- }
128
- const baseRoot = await defaultProfileRoot();
129
- return { sourceDir: path.join(baseRoot, profileName), profileName };
130
- }
131
- async function copyDirWithFilter(sourceDir, targetDir) {
132
- await copyDir(sourceDir, targetDir, {
133
- recursive: true,
134
- filter: async (source) => {
135
- const rel = path.relative(sourceDir, source);
136
- if (!rel)
137
- return true;
138
- return !shouldExclude(rel);
139
- },
140
- });
141
- }