@steipete/oracle 0.17.1 → 0.17.3

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 (92) hide show
  1. package/dist/bin/oracle-cli.js +1 -1
  2. package/dist/bin/oracle-mcp.js +0 -0
  3. package/dist/bin/oracle.js +683 -0
  4. package/dist/docs-site/bridge.html +1 -17
  5. package/dist/docs-site/browser-mode.html +14 -20
  6. package/dist/docs-site/cli-reference.html +6 -6
  7. package/dist/docs-site/configuration.html +7 -33
  8. package/dist/docs-site/followup.html +9 -16
  9. package/dist/docs-site/gemini.html +6 -10
  10. package/dist/docs-site/index.html +7 -7
  11. package/dist/docs-site/install.html +1 -1
  12. package/dist/docs-site/manual-tests.html +1 -2
  13. package/dist/docs-site/mcp.html +5 -28
  14. package/dist/docs-site/mythical-pro-agents.html +10 -10
  15. package/dist/docs-site/openai-endpoints.html +1 -24
  16. package/dist/docs-site/quickstart.html +1 -1
  17. package/dist/docs-site/sessions.html +5 -6
  18. package/dist/docs-site/spec.html +1 -1
  19. package/dist/docs-site/windows-work.html +0 -1
  20. package/dist/markdansi/types/index.js +4 -0
  21. package/dist/oracle/bin/oracle-cli.js +472 -0
  22. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  23. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  24. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  25. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  26. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  27. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  28. package/dist/oracle/src/browser/config.js +33 -0
  29. package/dist/oracle/src/browser/constants.js +40 -0
  30. package/dist/oracle/src/browser/cookies.js +210 -0
  31. package/dist/oracle/src/browser/domDebug.js +36 -0
  32. package/dist/oracle/src/browser/index.js +331 -0
  33. package/dist/oracle/src/browser/pageActions.js +5 -0
  34. package/dist/oracle/src/browser/prompt.js +88 -0
  35. package/dist/oracle/src/browser/promptSummary.js +20 -0
  36. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  37. package/dist/oracle/src/browser/types.js +1 -0
  38. package/dist/oracle/src/browser/utils.js +62 -0
  39. package/dist/oracle/src/browserMode.js +1 -0
  40. package/dist/oracle/src/cli/browserConfig.js +44 -0
  41. package/dist/oracle/src/cli/dryRun.js +59 -0
  42. package/dist/oracle/src/cli/engine.js +17 -0
  43. package/dist/oracle/src/cli/errorUtils.js +9 -0
  44. package/dist/oracle/src/cli/help.js +70 -0
  45. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  46. package/dist/oracle/src/cli/options.js +103 -0
  47. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  48. package/dist/oracle/src/cli/rootAlias.js +30 -0
  49. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  50. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  51. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  52. package/dist/oracle/src/heartbeat.js +43 -0
  53. package/dist/oracle/src/oracle/client.js +48 -0
  54. package/dist/oracle/src/oracle/config.js +29 -0
  55. package/dist/oracle/src/oracle/errors.js +101 -0
  56. package/dist/oracle/src/oracle/files.js +220 -0
  57. package/dist/oracle/src/oracle/format.js +33 -0
  58. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  59. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  60. package/dist/oracle/src/oracle/request.js +48 -0
  61. package/dist/oracle/src/oracle/run.js +444 -0
  62. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  63. package/dist/oracle/src/oracle/types.js +1 -0
  64. package/dist/oracle/src/oracle.js +9 -0
  65. package/dist/oracle/src/sessionManager.js +205 -0
  66. package/dist/oracle/src/version.js +39 -0
  67. package/dist/scripts/chrome/browser-tools.js +295 -0
  68. package/dist/scripts/git-policy.js +0 -8
  69. package/dist/scripts/runner.js +1 -5
  70. package/dist/src/browser/actions/assistantResponse.js +71 -23
  71. package/dist/src/browser/actions/modelSelection.js +164 -5
  72. package/dist/src/browser/actions/thinkingTime.js +195 -18
  73. package/dist/src/browser/chromeCookies.js +312 -0
  74. package/dist/src/browser/index.js +5 -1
  75. package/dist/src/browser/keytarShim.js +56 -0
  76. package/dist/src/browser/policies.js +4 -0
  77. package/dist/src/browser/profileSync.js +141 -0
  78. package/dist/src/browser/projectSourcesRunner.js +2 -2
  79. package/dist/src/browser/reattach.js +47 -35
  80. package/dist/src/browser/windowsCookies.js +219 -0
  81. package/dist/src/cli/browserConfig.js +13 -4
  82. package/dist/src/cli/browserDefaults.js +6 -1
  83. package/dist/src/cli/options.js +1 -1
  84. package/dist/src/cli/projectSources.js +8 -1
  85. package/dist/src/mcp/tools/consult.js +1 -1
  86. package/dist/src/oracle/thinkingTime.js +6 -0
  87. package/dist/src/sessionManager.js +90 -9
  88. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  89. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  90. package/package.json +6 -6
  91. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  92. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -0,0 +1,205 @@
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 };
@@ -0,0 +1,39 @@
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
+ }
@@ -0,0 +1,295 @@
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,5 +1,4 @@
1
1
  import { resolve } from "node:path";
2
- const COMMIT_HELPER_SUBCOMMANDS = new Set(["add", "commit"]);
3
2
  const GUARDED_SUBCOMMANDS = new Set(["push", "pull", "merge", "rebase", "cherry-pick"]);
4
3
  const DESTRUCTIVE_SUBCOMMANDS = new Set([
5
4
  "reset",
@@ -90,12 +89,6 @@ export function analyzeGitExecution(commandArgs, workspaceDir) {
90
89
  workDir,
91
90
  };
92
91
  }
93
- export function requiresCommitHelper(subcommand) {
94
- if (!subcommand) {
95
- return false;
96
- }
97
- return COMMIT_HELPER_SUBCOMMANDS.has(subcommand);
98
- }
99
92
  export function requiresExplicitGitConsent(subcommand) {
100
93
  if (!subcommand) {
101
94
  return false;
@@ -120,7 +113,6 @@ export function evaluateGitPolicies(context) {
120
113
  const invocationArgv = context.invocation?.argv;
121
114
  const normalizedArgv = Array.isArray(invocationArgv) ? invocationArgv : [];
122
115
  return {
123
- requiresCommitHelper: requiresCommitHelper(context.subcommand),
124
116
  requiresExplicitConsent: requiresExplicitGitConsent(context.subcommand),
125
117
  isDestructive: isDestructiveGitSubcommand(context.command, normalizedArgv),
126
118
  };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
3
  * Sweetistics runner wrapper: enforces timeouts, git policy, and trash-safe deletes before dispatching any repo command.
4
- * When you tweak its behavior, add a short note to AGENTS.md via `./scripts/committer "docs: update AGENTS for runner" "AGENTS.md"` so other agents know the new expectations.
4
+ * When you tweak its behavior, add a short note to AGENTS.md so other agents know the new expectations.
5
5
  */
6
6
  import { spawn } from "node:child_process";
7
7
  import { cpSync, existsSync, renameSync, rmSync } from "node:fs";
@@ -670,10 +670,6 @@ function enforceGitPolicies(gitContext) {
670
670
  console.error('git rebase requires the user to explicitly type "rebase" in chat. Once they do, rerun with RUNNER_THE_USER_GAVE_ME_CONSENT=1 in the same command (e.g. RUNNER_THE_USER_GAVE_ME_CONSENT=1 ./runner git rebase --continue).');
671
671
  process.exit(1);
672
672
  }
673
- if (evaluation.requiresCommitHelper) {
674
- console.error('Direct git add/commit is disabled. Use ./scripts/committer "chore(runner): describe change" "scripts/runner.ts" instead—see AGENTS.md and ./scripts/committer for details. The helper auto-stashes unrelated files before committing.');
675
- process.exit(1);
676
- }
677
673
  if (evaluation.requiresExplicitConsent || evaluation.isDestructive) {
678
674
  if (hasConsentOverride) {
679
675
  if (ENABLE_DEBUG_LOGS) {