shmakk 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +11 -0
- package/README.md +75 -1
- package/docs/index.html +154 -16
- package/docs/mcp.md +78 -0
- package/docs/ssh.md +82 -0
- package/docs/vibedit-analysis.md +375 -0
- package/docs/vim.md +110 -0
- package/docs/voice.md +4 -0
- package/package.json +9 -5
- package/scripts/test-vibedit.js +45 -0
- package/scripts/vibedit-demo.sh +52 -0
- package/skills/shmakk-skill-creator.md +269 -0
- package/src/_check.js +7 -0
- package/src/_check_schema.js +5 -0
- package/src/_cleanup.js +18 -0
- package/src/_fix.js +9 -0
- package/src/_test_import.js +15 -0
- package/src/agent.js +11 -4
- package/src/browser-daemon.js +209 -0
- package/src/browser.js +10 -0
- package/src/cli/browserDaemon.js +60 -0
- package/src/cli/connectBrowser.js +137 -0
- package/src/cli.js +235 -8
- package/src/completions.js +8 -0
- package/src/control.js +273 -1
- package/src/core/browserConnector.js +523 -0
- package/src/correction.js +6 -0
- package/src/electron.js +305 -0
- package/src/endpoints.js +74 -9
- package/src/index.js +24 -1
- package/src/llm.js +501 -61
- package/src/mobile.js +307 -0
- package/src/notify.js +51 -3
- package/src/orchestrator.js +35 -1
- package/src/pty.js +11 -6
- package/src/review.js +45 -11
- package/src/self-commands.js +153 -0
- package/src/session-convert.js +508 -0
- package/src/session-search.js +31 -0
- package/src/session.js +392 -46
- package/src/skills/browserActions.ts +984 -0
- package/src/skills.js +451 -24
- package/src/system-prompt.js +31 -25
- package/src/tools.js +81 -0
- package/src/vibedit/control.js +534 -0
- package/src/vibedit/electron.js +108 -0
- package/src/vibedit/files.js +171 -0
- package/src/vibedit/index.js +298 -0
- package/src/vibedit/overlay.js +1482 -0
- package/src/vibedit/prompts.js +245 -0
- package/src/vibedit/state.js +32 -0
- package/src/vim.js +410 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
// Browser connector — connects to a running Chrome instance via CDP.
|
|
2
|
+
// Unlike browser.js (which launches its own headless browser), this
|
|
3
|
+
// module connects to an already-running Chrome that the user launched
|
|
4
|
+
// with --remote-debugging-port. This preserves the user's logged-in
|
|
5
|
+
// sessions, cookies, extensions, and other state.
|
|
6
|
+
//
|
|
7
|
+
// Usage from agent tools:
|
|
8
|
+
// const bc = require('./core/browserConnector');
|
|
9
|
+
// await bc.ensureConnected(); // connect if not already
|
|
10
|
+
// await bc.navigate({ url: '...' }); // or click, type, readPage, etc.
|
|
11
|
+
//
|
|
12
|
+
// Usage from CLI:
|
|
13
|
+
// shmakk connect-browser // connect to default port 9222
|
|
14
|
+
// shmakk connect-browser --port 9230 // connect to a custom port
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const http = require('http');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
|
|
21
|
+
let pw = null; // playwright module (lazy-loaded)
|
|
22
|
+
let browser = null; // ConnectedBrowser instance
|
|
23
|
+
let page = null; // Active page
|
|
24
|
+
let _debugPort = 9222; // CDP port
|
|
25
|
+
|
|
26
|
+
const SCREENSHOT_DIR = '/tmp/shmakk-screenshots';
|
|
27
|
+
const CONNECTION_STATE_PATH = path.join(os.homedir(), '.config', 'shmakk', 'browser-connection.json');
|
|
28
|
+
|
|
29
|
+
// ── Logger ────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function log(level, message, detail) {
|
|
32
|
+
const ts = new Date().toISOString().slice(11, 23);
|
|
33
|
+
const prefix = `[shmakk:browser:${level.toUpperCase()}]`;
|
|
34
|
+
if (detail !== undefined) {
|
|
35
|
+
process.stderr.write(`${prefix} ${ts} ${message} ${JSON.stringify(detail)}\n`);
|
|
36
|
+
} else {
|
|
37
|
+
process.stderr.write(`${prefix} ${ts} ${message}\n`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Availability ──────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function isAvailable() {
|
|
44
|
+
try { require.resolve('playwright'); return true; } catch { return false; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isConnected() {
|
|
48
|
+
return !!(browser && browser.isConnected());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── CDP port detection ────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
// Discovers a running Chrome instance by scanning the browser's
|
|
54
|
+
// DevToolsActivePort file or by probing HTTP on the CDP port.
|
|
55
|
+
function detectCDPPort() {
|
|
56
|
+
// Try well-known profile locations
|
|
57
|
+
const home = os.homedir();
|
|
58
|
+
const candidates = [
|
|
59
|
+
path.join(home, '.config', 'google-chrome', 'DevToolsActivePort'),
|
|
60
|
+
path.join(home, '.config', 'chromium', 'DevToolsActivePort'),
|
|
61
|
+
path.join(home, '.config', 'google-chrome-unstable', 'DevToolsActivePort'),
|
|
62
|
+
path.join(home, 'snap', 'chromium', 'common', 'chromium', 'DevToolsActivePort'),
|
|
63
|
+
path.join(home, '.var', 'app', 'com.google.Chrome', 'config', 'google-chrome', 'DevToolsActivePort'),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
try {
|
|
68
|
+
if (fs.existsSync(candidate)) {
|
|
69
|
+
const content = fs.readFileSync(candidate, 'utf8').trim();
|
|
70
|
+
const port = parseInt(content.split('\n')[0], 10);
|
|
71
|
+
if (port > 0 && port < 65536) {
|
|
72
|
+
log('info', `detected CDP port ${port} from ${candidate}`);
|
|
73
|
+
return port;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch { /* ignore unreadable files */ }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Probe HTTP endpoint to verify Chrome's CDP is listening.
|
|
83
|
+
async function probeCDP(port) {
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const req = http.get(`http://127.0.0.1:${port}/json/version`, { timeout: 2000 }, (res) => {
|
|
86
|
+
let body = '';
|
|
87
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
88
|
+
res.on('end', () => {
|
|
89
|
+
try {
|
|
90
|
+
const data = JSON.parse(body);
|
|
91
|
+
if (data && data.webSocketDebuggerUrl) {
|
|
92
|
+
log('info', `CDP probe successful`, { browser: data.Browser, port });
|
|
93
|
+
resolve(true);
|
|
94
|
+
} else {
|
|
95
|
+
resolve(false);
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
resolve(false);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
req.on('error', () => resolve(false));
|
|
103
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Find a running Chrome CDP port. Returns null if none found.
|
|
108
|
+
async function findCDPPort() {
|
|
109
|
+
const detected = detectCDPPort();
|
|
110
|
+
if (detected) {
|
|
111
|
+
const ok = await probeCDP(detected);
|
|
112
|
+
if (ok) return detected;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fall back to scanning common ports
|
|
116
|
+
const commonPorts = [9222, 9223, 9229, 9230, 9333, 9444];
|
|
117
|
+
for (const port of commonPorts) {
|
|
118
|
+
if (await probeCDP(port)) return port;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Connection management ─────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
// Persist the last connection port for reconnection.
|
|
127
|
+
function saveConnectionState(state) {
|
|
128
|
+
try {
|
|
129
|
+
const dir = path.dirname(CONNECTION_STATE_PATH);
|
|
130
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
131
|
+
fs.writeFileSync(CONNECTION_STATE_PATH, JSON.stringify({ ...state, updatedAt: Date.now() }, null, 2));
|
|
132
|
+
} catch {
|
|
133
|
+
// Best-effort; not critical.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function loadConnectionState() {
|
|
138
|
+
try {
|
|
139
|
+
if (fs.existsSync(CONNECTION_STATE_PATH)) {
|
|
140
|
+
return JSON.parse(fs.readFileSync(CONNECTION_STATE_PATH, 'utf8'));
|
|
141
|
+
}
|
|
142
|
+
} catch { /* ignore */ }
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function ensurePage(args) {
|
|
147
|
+
const port = (args && Number(args.debugPort)) || _debugPort;
|
|
148
|
+
_debugPort = port;
|
|
149
|
+
|
|
150
|
+
if (page && !page.isClosed()) {
|
|
151
|
+
return page;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!pw) {
|
|
155
|
+
try {
|
|
156
|
+
pw = require('playwright');
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error(
|
|
159
|
+
'playwright not installed. Run:\n' +
|
|
160
|
+
' npm install playwright\n' +
|
|
161
|
+
' npx playwright install chromium',
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!browser || !browser.isConnected()) {
|
|
167
|
+
const wsEndpoint = `http://127.0.0.1:${port}`;
|
|
168
|
+
try {
|
|
169
|
+
browser = await pw.chromium.connectOverCDP(wsEndpoint);
|
|
170
|
+
log('info', `connected to Chrome via CDP port ${port}`);
|
|
171
|
+
saveConnectionState({ port, connectedAt: Date.now() });
|
|
172
|
+
} catch (e) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Cannot connect to Chrome at port ${port}. Make sure Chrome is running with:\n` +
|
|
175
|
+
` google-chrome-stable --remote-debugging-port=${port}\n` +
|
|
176
|
+
`Details: ${e.message}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const contexts = browser.contexts();
|
|
182
|
+
if (!contexts.length) {
|
|
183
|
+
throw new Error('No browser contexts found. Chrome may have no open windows.');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Prefer the default context (has user data) over incognito ones.
|
|
187
|
+
let ctx = contexts.find((c) => {
|
|
188
|
+
try { return c.cookies && !c.isIncognito?.(); } catch { return true; }
|
|
189
|
+
}) || contexts[0];
|
|
190
|
+
|
|
191
|
+
const pages = ctx.pages();
|
|
192
|
+
page = pages[pages.length - 1] || await ctx.newPage();
|
|
193
|
+
return page;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function connect(args) {
|
|
197
|
+
const port = (args && Number(args.port)) || _debugPort;
|
|
198
|
+
|
|
199
|
+
// Auto-detect if no explicit port and not already connected.
|
|
200
|
+
if (!args || !args.port) {
|
|
201
|
+
if (isConnected()) {
|
|
202
|
+
return { ok: true, connected: true, port: _debugPort, note: 'already connected' };
|
|
203
|
+
}
|
|
204
|
+
const autoPort = await findCDPPort();
|
|
205
|
+
if (autoPort) {
|
|
206
|
+
_debugPort = autoPort;
|
|
207
|
+
} else {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
error: 'No running Chrome instance found. Start Chrome with:\n' +
|
|
211
|
+
' google-chrome-stable --remote-debugging-port=9222',
|
|
212
|
+
hint: 'Pass --port to specify a custom CDP port',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
_debugPort = port;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const p = await ensurePage({ debugPort: _debugPort });
|
|
221
|
+
return {
|
|
222
|
+
ok: true,
|
|
223
|
+
connected: true,
|
|
224
|
+
port: _debugPort,
|
|
225
|
+
url: p.url(),
|
|
226
|
+
title: await p.title(),
|
|
227
|
+
};
|
|
228
|
+
} catch (e) {
|
|
229
|
+
return { ok: false, error: e.message, port: _debugPort };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function disconnect() {
|
|
234
|
+
if (browser) {
|
|
235
|
+
try { await browser.close(); } catch (e) {
|
|
236
|
+
log('warn', 'error closing browser', { error: e.message });
|
|
237
|
+
}
|
|
238
|
+
browser = null;
|
|
239
|
+
page = null;
|
|
240
|
+
log('info', 'disconnected from Chrome');
|
|
241
|
+
}
|
|
242
|
+
return { ok: true, disconnected: true };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Browser commands ──────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
async function navigate(args) {
|
|
248
|
+
const url = String(args.url || '').trim();
|
|
249
|
+
if (!url) return { error: 'url required' };
|
|
250
|
+
|
|
251
|
+
const p = await ensurePage(args);
|
|
252
|
+
try {
|
|
253
|
+
const resp = await p.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
url: p.url(),
|
|
257
|
+
title: await p.title(),
|
|
258
|
+
status: resp ? resp.status() : null,
|
|
259
|
+
};
|
|
260
|
+
} catch (e) {
|
|
261
|
+
return { error: `navigate failed: ${e.message}` };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function click(args) {
|
|
266
|
+
const sel = String(args.selector || '').trim();
|
|
267
|
+
if (!sel) return { error: 'selector required' };
|
|
268
|
+
|
|
269
|
+
const p = await ensurePage(args);
|
|
270
|
+
try {
|
|
271
|
+
await p.click(sel, { timeout: 5000 });
|
|
272
|
+
await p.waitForTimeout(500);
|
|
273
|
+
return { ok: true, clicked: sel, url: p.url(), title: await p.title() };
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return { error: `click failed: ${e.message}` };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function type(args) {
|
|
280
|
+
const sel = String(args.selector || '').trim();
|
|
281
|
+
const text = String(args.text ?? '');
|
|
282
|
+
if (!sel) return { error: 'selector required' };
|
|
283
|
+
|
|
284
|
+
const p = await ensurePage(args);
|
|
285
|
+
try {
|
|
286
|
+
await p.fill(sel, text, { timeout: 5000 });
|
|
287
|
+
return { ok: true, selector: sel, typed: text.length + ' chars' };
|
|
288
|
+
} catch (e) {
|
|
289
|
+
try {
|
|
290
|
+
await p.click(sel, { timeout: 3000 });
|
|
291
|
+
await p.keyboard.type(text);
|
|
292
|
+
return { ok: true, selector: sel, typed: text.length + ' chars', method: 'keyboard' };
|
|
293
|
+
} catch (e2) {
|
|
294
|
+
return { error: `type failed: ${e2.message}` };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function readPage(args) {
|
|
300
|
+
const p = await ensurePage(args);
|
|
301
|
+
try {
|
|
302
|
+
const data = await p.evaluate(() => {
|
|
303
|
+
const r = {
|
|
304
|
+
title: document.title,
|
|
305
|
+
url: location.href,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Headings
|
|
309
|
+
r.headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'))
|
|
310
|
+
.slice(0, 20)
|
|
311
|
+
.map((h) => ({ level: h.tagName.toLowerCase(), text: h.textContent.trim().slice(0, 200) }))
|
|
312
|
+
.filter((h) => h.text);
|
|
313
|
+
|
|
314
|
+
// Links
|
|
315
|
+
r.links = Array.from(document.querySelectorAll('a[href]'))
|
|
316
|
+
.slice(0, 30)
|
|
317
|
+
.map((a) => ({ text: a.textContent.trim().slice(0, 100), href: a.href }))
|
|
318
|
+
.filter((l) => l.text && l.href);
|
|
319
|
+
|
|
320
|
+
// Form inputs
|
|
321
|
+
r.inputs = Array.from(document.querySelectorAll('input, textarea, select'))
|
|
322
|
+
.slice(0, 20)
|
|
323
|
+
.map((el) => ({
|
|
324
|
+
tag: el.tagName.toLowerCase(),
|
|
325
|
+
type: el.type || '',
|
|
326
|
+
name: el.name || '',
|
|
327
|
+
id: el.id || '',
|
|
328
|
+
placeholder: el.placeholder || '',
|
|
329
|
+
label: el.labels && el.labels[0] ? el.labels[0].textContent.trim() : '',
|
|
330
|
+
value: el.type === 'password' ? '***' : (el.value || '').slice(0, 100),
|
|
331
|
+
}));
|
|
332
|
+
|
|
333
|
+
// Buttons
|
|
334
|
+
r.buttons = Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]'))
|
|
335
|
+
.slice(0, 15)
|
|
336
|
+
.map((b) => ({
|
|
337
|
+
text: (b.textContent || b.value || '').trim().slice(0, 100),
|
|
338
|
+
id: b.id || '',
|
|
339
|
+
}))
|
|
340
|
+
.filter((b) => b.text);
|
|
341
|
+
|
|
342
|
+
// Visible text content
|
|
343
|
+
const walker = document.createTreeWalker(
|
|
344
|
+
document.body || document.documentElement,
|
|
345
|
+
NodeFilter.SHOW_TEXT,
|
|
346
|
+
{
|
|
347
|
+
acceptNode: (node) => {
|
|
348
|
+
const el = node.parentElement;
|
|
349
|
+
if (!el) return NodeFilter.FILTER_REJECT;
|
|
350
|
+
const tag = el.tagName.toLowerCase();
|
|
351
|
+
if (['script', 'style', 'noscript', 'svg'].includes(tag)) return NodeFilter.FILTER_REJECT;
|
|
352
|
+
try {
|
|
353
|
+
const style = getComputedStyle(el);
|
|
354
|
+
if (style.display === 'none' || style.visibility === 'hidden') return NodeFilter.FILTER_REJECT;
|
|
355
|
+
} catch { /* ignore */ }
|
|
356
|
+
return node.textContent.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const texts = [];
|
|
362
|
+
let totalLen = 0;
|
|
363
|
+
while (walker.nextNode() && totalLen < 4000) {
|
|
364
|
+
const t = walker.currentNode.textContent.trim();
|
|
365
|
+
if (t) { texts.push(t); totalLen += t.length; }
|
|
366
|
+
}
|
|
367
|
+
r.text = texts.join(' ').slice(0, 4000);
|
|
368
|
+
|
|
369
|
+
return r;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
return data;
|
|
373
|
+
} catch (e) {
|
|
374
|
+
return { error: `read_page failed: ${e.message}` };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function screenshot(args) {
|
|
379
|
+
const p = await ensurePage(args);
|
|
380
|
+
try {
|
|
381
|
+
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
382
|
+
const name = `screenshot-${Date.now()}.png`;
|
|
383
|
+
const filePath = path.join(SCREENSHOT_DIR, name);
|
|
384
|
+
await p.screenshot({ path: filePath, fullPage: false });
|
|
385
|
+
const stats = fs.statSync(filePath);
|
|
386
|
+
return {
|
|
387
|
+
ok: true,
|
|
388
|
+
path: filePath,
|
|
389
|
+
size: stats.size,
|
|
390
|
+
url: p.url(),
|
|
391
|
+
title: await p.title(),
|
|
392
|
+
};
|
|
393
|
+
} catch (e) {
|
|
394
|
+
return { error: `screenshot failed: ${e.message}` };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function evaluate(args) {
|
|
399
|
+
const code = String(args.code || '').trim();
|
|
400
|
+
if (!code) return { error: 'code required' };
|
|
401
|
+
|
|
402
|
+
const p = await ensurePage(args);
|
|
403
|
+
try {
|
|
404
|
+
const result = await p.evaluate(code);
|
|
405
|
+
return { ok: true, result: JSON.stringify(result).slice(0, 8000) };
|
|
406
|
+
} catch (e) {
|
|
407
|
+
return { error: `evaluate failed: ${e.message}` };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function selectOption(args) {
|
|
412
|
+
const sel = String(args.selector || '').trim();
|
|
413
|
+
const val = String(args.text ?? '');
|
|
414
|
+
if (!sel) return { error: 'selector required' };
|
|
415
|
+
|
|
416
|
+
const p = await ensurePage(args);
|
|
417
|
+
try {
|
|
418
|
+
const selected = await p.selectOption(sel, val, { timeout: 5000 });
|
|
419
|
+
return { ok: true, selector: sel, selected };
|
|
420
|
+
} catch (e) {
|
|
421
|
+
return { error: `select failed: ${e.message}` };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function waitFor(args) {
|
|
426
|
+
const p = await ensurePage(args);
|
|
427
|
+
const sel = String(args.selector || '').trim();
|
|
428
|
+
const seconds = Number(args.seconds) || 2;
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
if (sel) {
|
|
432
|
+
await p.waitForSelector(sel, { timeout: seconds * 1000 });
|
|
433
|
+
return { ok: true, found: sel };
|
|
434
|
+
}
|
|
435
|
+
await p.waitForTimeout(Math.min(seconds, 10) * 1000);
|
|
436
|
+
return { ok: true, waited: seconds + 's' };
|
|
437
|
+
} catch (e) {
|
|
438
|
+
return { error: `wait failed: ${e.message}` };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function scroll(args) {
|
|
443
|
+
const dir = String(args.direction || 'down').toLowerCase();
|
|
444
|
+
const amount = Number(args.amount) || (dir === 'up' ? -600 : 600);
|
|
445
|
+
const p = await ensurePage(args);
|
|
446
|
+
try {
|
|
447
|
+
await p.mouse.wheel(0, amount);
|
|
448
|
+
await p.waitForTimeout(300);
|
|
449
|
+
return { ok: true, direction: dir, amount };
|
|
450
|
+
} catch (e) {
|
|
451
|
+
return { error: `scroll failed: ${e.message}` };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ── Status ────────────────────────────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
async function getStatus() {
|
|
458
|
+
if (!isConnected()) {
|
|
459
|
+
return { connected: false, port: _debugPort };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const p = await ensurePage({ debugPort: _debugPort });
|
|
464
|
+
return {
|
|
465
|
+
connected: true,
|
|
466
|
+
port: _debugPort,
|
|
467
|
+
url: p.url(),
|
|
468
|
+
title: await p.title(),
|
|
469
|
+
};
|
|
470
|
+
} catch (e) {
|
|
471
|
+
return { connected: false, port: _debugPort, error: e.message };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Dispatch ──────────────────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
const COMMANDS = {
|
|
478
|
+
navigate, click, type, read_page: readPage, screenshot,
|
|
479
|
+
evaluate, select: selectOption, wait: waitFor, scroll,
|
|
480
|
+
connect, disconnect, status: getStatus, close: disconnect,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
function classifyBrowserAction(args) {
|
|
484
|
+
const cmd = String(args.command || '').toLowerCase();
|
|
485
|
+
if (cmd === 'read_page' || cmd === 'screenshot' || cmd === 'wait' || cmd === 'status') return 'safe';
|
|
486
|
+
if (cmd === 'close' || cmd === 'disconnect') return 'unsafe';
|
|
487
|
+
if (cmd === 'connect') return 'safe'; // connecting is safe; actions after depend on user's session
|
|
488
|
+
return 'uncertain';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function executeBrowserAction(args) {
|
|
492
|
+
if (!isAvailable()) {
|
|
493
|
+
return { error: 'playwright not installed. Run: npm install playwright && npx playwright install chromium' };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const cmd = String(args.command || '').toLowerCase();
|
|
497
|
+
const fn = COMMANDS[cmd];
|
|
498
|
+
|
|
499
|
+
if (!fn) {
|
|
500
|
+
return {
|
|
501
|
+
error: `unknown browser command: ${cmd}. Available: ${Object.keys(COMMANDS).join(', ')}`,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
return await fn(args);
|
|
507
|
+
} catch (e) {
|
|
508
|
+
log('error', `command '${cmd}' failed`, { error: e.message });
|
|
509
|
+
return { error: `browser ${cmd} failed: ${e.message}` };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
module.exports = {
|
|
514
|
+
isAvailable,
|
|
515
|
+
isConnected,
|
|
516
|
+
connect,
|
|
517
|
+
disconnect,
|
|
518
|
+
ensurePage,
|
|
519
|
+
executeBrowserAction,
|
|
520
|
+
classifyBrowserAction,
|
|
521
|
+
getStatus,
|
|
522
|
+
findCDPPort,
|
|
523
|
+
};
|
package/src/correction.js
CHANGED
|
@@ -115,6 +115,12 @@ function preserveCase(original, corrected) {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
async function correct({ input, glossary, signal: _unused }) {
|
|
118
|
+
// Null/empty input: can happen at shell startup when precmd fires before
|
|
119
|
+
// any command is executed (especially in zsh). Nothing to correct.
|
|
120
|
+
if (!input || !input.trim()) {
|
|
121
|
+
return { category: 'not_a_correction', proposed: null, safety: 'uncertain', reason: 'empty input' };
|
|
122
|
+
}
|
|
123
|
+
|
|
118
124
|
// /-prefixed and "shmakk ..." commands are shmakk self-commands.
|
|
119
125
|
// They should never reach the correction engine — bail out immediately.
|
|
120
126
|
if (/^\//.test(input) || /^shmakk\s/i.test(input)) {
|