@porcupine/kuskus 0.1.0 → 0.1.1

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.
@@ -3,7 +3,29 @@
3
3
  "allow": [
4
4
  "Bash(npm install 2>&1)",
5
5
  "Bash(node -e \"import\\('@modelcontextprotocol/sdk/server/stdio.js'\\).then\\(m => console.log\\(Object.keys\\(m\\)\\)\\).catch\\(e => console.error\\(e.message\\)\\)\" && node -e \"import\\('zod'\\).then\\(m => console.log\\('zod ok'\\)\\).catch\\(e => console.error\\(e.message\\)\\)\")",
6
- "Bash(npx vitest run --reporter=verbose 2>&1)"
6
+ "Bash(npx vitest run --reporter=verbose 2>&1)",
7
+ "WebFetch(domain:opencode.ai)",
8
+ "Read(//Users/anak10thn/.config/opencode/**)",
9
+ "Read(//Users/anak10thn/**)",
10
+ "Bash(node bin/cli.js install 2>&1)",
11
+ "Bash(~/.local/bin/lightpanda --version 2>&1 || file ~/.local/bin/lightpanda)",
12
+ "Bash(echo \"ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY\" | head -c 50)",
13
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\nnpx vitest run --reporter=verbose 2>&1)",
14
+ "Bash(node -e \"\nimport\\('dotenv/config'\\);\nimport\\('./src/utils/browser.js'\\).then\\(async \\({ ensureBrowser }\\) => {\n const proc = await ensureBrowser\\({ port: 9222, host: '127.0.0.1', log: console.log }\\);\n\n const { SessionManager } = await import\\('./src/cdp/session.js'\\);\n const { createPageDomain } = await import\\('./src/cdp/domains/page.js'\\);\n const { createRuntimeDomain } = await import\\('./src/cdp/domains/runtime.js'\\);\n\n const sm = new SessionManager\\({ host: '127.0.0.1', port: 9222 }\\);\n await sm.connect\\(\\);\n\n const sc = await sm.getActiveSession\\(\\);\n const page = createPageDomain\\(sc\\);\n const rt = createRuntimeDomain\\(sc\\);\n\n console.log\\('Navigating to example.com...'\\);\n await page.navigate\\('https://example.com'\\);\n\n const title = await rt.evaluate\\('document.title'\\);\n const url = await page.getURL\\(\\);\n console.log\\('URL :', url\\);\n console.log\\('Title :', title\\);\n\n const screenshot = await page.screenshot\\(\\);\n console.log\\('Screenshot size:', screenshot.length, 'chars \\(base64\\)'\\);\n\n await sm.close\\(\\);\n proc?.kill\\(\\);\n process.exit\\(0\\);\n}\\).catch\\(e => { console.error\\('ERROR:', e.message\\); process.exit\\(1\\); }\\);\n\" 2>&1)",
15
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\necho \"Masukkan ANTHROPIC_API_KEY untuk test \\(atau set di .env\\):\"\nls .env 2>/dev/null && source .env 2>/dev/null\nprintenv ANTHROPIC_API_KEY | cut -c1-20 || echo \"\\(tidak ada\\)\")",
16
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\nnode -e \"\nimport\\('./src/utils/browser.js'\\).then\\(async \\({ ensureBrowser }\\) => {\n const proc = await ensureBrowser\\({ port: 9222, host: '127.0.0.1', log: console.log }\\);\n\n const { SessionManager } = await import\\('./src/cdp/session.js'\\);\n const { createPageDomain } = await import\\('./src/cdp/domains/page.js'\\);\n const { createDOMDomain } = await import\\('./src/cdp/domains/dom.js'\\);\n const { createInputDomain } = await import\\('./src/cdp/domains/input.js'\\);\n const { createRuntimeDomain } = await import\\('./src/cdp/domains/runtime.js'\\);\n const { htmlToReadableText } = await import\\('./src/utils/dom-to-text.js'\\);\n\n const sm = new SessionManager\\({ host: '127.0.0.1', port: 9222 }\\);\n await sm.connect\\(\\);\n const sc = await sm.getActiveSession\\(\\);\n\n const page = createPageDomain\\(sc\\);\n const dom = createDOMDomain\\(sc\\);\n const rt = createRuntimeDomain\\(sc\\);\n const inp = createInputDomain\\(sc\\);\n\n // 1. Navigate\n console.log\\('\\\\n[1] Navigate to https://example.com'\\);\n await page.navigate\\('https://example.com'\\);\n console.log\\(' URL:', await page.getURL\\(\\)\\);\n console.log\\(' Title:', await page.getTitle\\(\\)\\);\n\n // 2. DOM query\n console.log\\('\\\\n[2] querySelector h1'\\);\n const h1Id = await dom.querySelector\\('h1'\\);\n const h1HTML = await dom.getOuterHTML\\(h1Id\\);\n console.log\\(' h1:', h1HTML\\);\n\n // 3. Page content\n console.log\\('\\\\n[3] Page content \\(truncated\\)'\\);\n const html = await rt.evaluate\\('document.documentElement.outerHTML'\\);\n const text = htmlToReadableText\\(html, { maxLength: 300 }\\);\n console.log\\(' ', text.split\\('\\\\n'\\).slice\\(0,5\\).join\\('\\\\n '\\)\\);\n\n // 4. Navigate to google and search\n console.log\\('\\\\n[4] Navigate to google.com'\\);\n await page.navigate\\('https://google.com'\\);\n console.log\\(' URL:', await page.getURL\\(\\)\\);\n\n // 5. Screenshot \\(may return null on Lightpanda\\)\n console.log\\('\\\\n[5] Screenshot'\\);\n const ss = await page.screenshot\\(\\);\n console.log\\(' Result:', ss ? ss.length + ' chars base64' : 'null \\(unsupported by Lightpanda — OK\\)'\\);\n\n await sm.close\\(\\);\n proc?.kill\\(\\);\n console.log\\('\\\\nAll checks passed.'\\);\n process.exit\\(0\\);\n}\\).catch\\(e => { console.error\\('FAIL:', e.message, e.stack\\); process.exit\\(1\\); }\\);\n\" 2>&1)",
17
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\nnode -e \"\nimport\\('./src/utils/browser.js'\\).then\\(async \\({ ensureBrowser }\\) => {\n const proc = await ensureBrowser\\({ port: 9222, host: '127.0.0.1', log: console.log }\\);\n\n const { SessionManager } = await import\\('./src/cdp/session.js'\\);\n const { createPageDomain } = await import\\('./src/cdp/domains/page.js'\\);\n const { createDOMDomain } = await import\\('./src/cdp/domains/dom.js'\\);\n const { createInputDomain } = await import\\('./src/cdp/domains/input.js'\\);\n const { createRuntimeDomain } = await import\\('./src/cdp/domains/runtime.js'\\);\n const { htmlToReadableText } = await import\\('./src/utils/dom-to-text.js'\\);\n\n const sm = new SessionManager\\({ host: '127.0.0.1', port: 9222 }\\);\n await sm.connect\\(\\);\n const sc = await sm.getActiveSession\\(\\);\n\n const page = createPageDomain\\(sc\\);\n const dom = createDOMDomain\\(sc\\);\n const rt = createRuntimeDomain\\(sc\\);\n const inp = createInputDomain\\(sc\\);\n\n // 1. Navigate\n console.log\\('\\\\n[1] Navigate https://example.com'\\);\n await page.navigate\\('https://example.com'\\);\n console.log\\(' URL:', await page.getURL\\(\\)\\);\n console.log\\(' Title:', await page.getTitle\\(\\)\\);\n\n // 2. DOM query\n console.log\\('\\\\n[2] querySelector h1'\\);\n const h1Id = await dom.querySelector\\('h1'\\);\n const h1HTML = await dom.getOuterHTML\\(h1Id\\);\n console.log\\(' h1:', h1HTML.trim\\(\\)\\);\n\n // 3. Page content\n console.log\\('\\\\n[3] Page content'\\);\n const html = await rt.evaluate\\('document.documentElement.outerHTML'\\);\n const text = htmlToReadableText\\(html, { maxLength: 400 }\\);\n console.log\\(' ', text.replace\\(/\\\\n/g, '\\\\n '\\)\\);\n\n // 4. Screenshot\n console.log\\('\\\\n[4] Screenshot'\\);\n const ss = await page.screenshot\\(\\);\n console.log\\(' Result:', ss ? ss.length + ' chars base64' : 'null \\(unsupported — OK\\)'\\);\n\n // 5. list tabs\n console.log\\('\\\\n[5] List tabs'\\);\n const tabs = await sm.listTargets\\(\\);\n console.log\\(' Tabs:', JSON.stringify\\(tabs\\)\\);\n\n await sm.close\\(\\);\n proc?.kill\\(\\);\n console.log\\('\\\\nAll checks passed.'\\);\n process.exit\\(0\\);\n}\\).catch\\(e => { console.error\\('FAIL:', e.message\\); process.exit\\(1\\); }\\);\n\" 2>&1)",
18
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\nnode -e \"\nimport\\('./src/utils/browser.js'\\).then\\(async \\({ ensureBrowser }\\) => {\n const proc = await ensureBrowser\\({ port: 9222, host: '127.0.0.1', log: console.log }\\);\n\n const { SessionManager } = await import\\('./src/cdp/session.js'\\);\n const { createPageDomain } = await import\\('./src/cdp/domains/page.js'\\);\n const { createDOMDomain } = await import\\('./src/cdp/domains/dom.js'\\);\n const { createInputDomain } = await import\\('./src/cdp/domains/input.js'\\);\n const { createRuntimeDomain } = await import\\('./src/cdp/domains/runtime.js'\\);\n const { htmlToReadableText } = await import\\('./src/utils/dom-to-text.js'\\);\n\n const sm = new SessionManager\\({ host: '127.0.0.1', port: 9222 }\\);\n await sm.connect\\(\\);\n const sc = await sm.getActiveSession\\(\\);\n\n const page = createPageDomain\\(sc\\);\n const dom = createDOMDomain\\(sc\\);\n const rt = createRuntimeDomain\\(sc\\);\n const inp = createInputDomain\\(sc\\);\n\n console.log\\('\\\\n[1] Navigate https://example.com'\\);\n await page.navigate\\('https://example.com'\\);\n console.log\\(' URL:', await page.getURL\\(\\)\\);\n console.log\\(' Title:', await page.getTitle\\(\\)\\);\n\n console.log\\('\\\\n[2] querySelector h1'\\);\n const h1Id = await dom.querySelector\\('h1'\\);\n console.log\\(' h1:', \\(await dom.getOuterHTML\\(h1Id\\)\\).trim\\(\\)\\);\n\n console.log\\('\\\\n[3] Page content'\\);\n const html = await rt.evaluate\\('document.documentElement.outerHTML'\\);\n const text = htmlToReadableText\\(html, { maxLength: 300 }\\);\n console.log\\(' ', text.replace\\(/\\\\n/g, '\\\\n '\\)\\);\n\n console.log\\('\\\\n[4] Screenshot'\\);\n const ss = await page.screenshot\\(\\);\n console.log\\(' Result:', ss ? ss.length + ' chars base64' : 'null \\(unsupported — OK\\)'\\);\n\n console.log\\('\\\\n[5] List tabs'\\);\n const tabs = await sm.listTargets\\(\\);\n console.log\\(' Tabs:', JSON.stringify\\(tabs, null, 2\\)\\);\n\n console.log\\('\\\\n[6] New tab + navigate'\\);\n const sc2 = await sm.createTarget\\('https://httpbin.org/get'\\);\n const page2 = createPageDomain\\(sc2\\);\n await new Promise\\(r => setTimeout\\(r, 1500\\)\\);\n console.log\\(' Tab2 URL:', await page2.getURL\\(\\)\\);\n\n console.log\\('\\\\n[7] list tabs after new tab'\\);\n const tabs2 = await sm.listTargets\\(\\);\n console.log\\(' Tabs:', tabs2.map\\(t => t.url\\)\\);\n\n await sm.close\\(\\);\n proc?.kill\\(\\);\n console.log\\('\\\\nAll checks passed.'\\);\n process.exit\\(0\\);\n}\\).catch\\(e => { console.error\\('FAIL:', e.message\\); process.exit\\(1\\); }\\);\n\" 2>&1)",
19
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\nnode -e \"\nimport\\('./src/utils/browser.js'\\).then\\(async \\({ ensureBrowser }\\) => {\n const proc = await ensureBrowser\\({ port: 9222, host: '127.0.0.1', log: console.log }\\);\n\n const { SessionManager } = await import\\('./src/cdp/session.js'\\);\n const { createPageDomain } = await import\\('./src/cdp/domains/page.js'\\);\n const { createDOMDomain } = await import\\('./src/cdp/domains/dom.js'\\);\n const { createInputDomain } = await import\\('./src/cdp/domains/input.js'\\);\n const { createRuntimeDomain } = await import\\('./src/cdp/domains/runtime.js'\\);\n const { htmlToReadableText } = await import\\('./src/utils/dom-to-text.js'\\);\n\n const sm = new SessionManager\\({ host: '127.0.0.1', port: 9222 }\\);\n await sm.connect\\(\\);\n console.log\\('Capabilities:', sm.capabilities\\);\n\n const sc = await sm.getActiveSession\\(\\);\n const page = createPageDomain\\(sc, sm.capabilities\\);\n const dom = createDOMDomain\\(sc\\);\n const rt = createRuntimeDomain\\(sc\\);\n const inp = createInputDomain\\(sc\\);\n\n console.log\\('\\\\n[1] Navigate https://example.com'\\);\n await page.navigate\\('https://example.com'\\);\n console.log\\(' URL:', await page.getURL\\(\\)\\);\n console.log\\(' Title:', await page.getTitle\\(\\)\\);\n\n console.log\\('\\\\n[2] querySelector h1'\\);\n const h1Id = await dom.querySelector\\('h1'\\);\n console.log\\(' h1:', \\(await dom.getOuterHTML\\(h1Id\\)\\).trim\\(\\)\\);\n\n console.log\\('\\\\n[3] Page content'\\);\n const html = await rt.evaluate\\('document.documentElement.outerHTML'\\);\n const text = htmlToReadableText\\(html, { maxLength: 300 }\\);\n console.log\\(' ', text.replace\\(/\\\\n/g, '\\\\n '\\)\\);\n\n console.log\\('\\\\n[4] Screenshot'\\);\n const ss = await page.screenshot\\(\\);\n console.log\\(' Result:', ss ? ss.length + ' chars base64' : 'null \\(skipped — Lightpanda\\)'\\);\n\n console.log\\('\\\\n[5] List tabs'\\);\n const tabs = await sm.listTargets\\(\\);\n console.log\\(' Tabs:', JSON.stringify\\(tabs.map\\(t => t.url\\)\\)\\);\n\n console.log\\('\\\\n[6] New tab'\\);\n const sc2 = await sm.createTarget\\('https://httpbin.org/json'\\);\n const page2 = createPageDomain\\(sc2, sm.capabilities\\);\n await new Promise\\(r => setTimeout\\(r, 1500\\)\\);\n console.log\\(' Tab2 URL:', await page2.getURL\\(\\)\\);\n\n console.log\\('\\\\n[7] List tabs after new tab'\\);\n const tabs2 = await sm.listTargets\\(\\);\n console.log\\(' Tabs:', tabs2.map\\(t => t.url\\)\\);\n\n console.log\\('\\\\n[8] JS eval'\\);\n const rt2 = createRuntimeDomain\\(sc2\\);\n const json = await rt2.evaluate\\('JSON.stringify\\(document.title\\)'\\);\n console.log\\(' title:', json\\);\n\n await sm.close\\(\\);\n proc?.kill\\(\\);\n console.log\\('\\\\nAll checks passed.'\\);\n process.exit\\(0\\);\n}\\).catch\\(e => { console.error\\('FAIL:', e.message, '\\\\n', e.stack\\); process.exit\\(1\\); }\\);\n\" 2>&1)",
20
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\nnode -e \"\nimport\\('./src/utils/browser.js'\\).then\\(async \\({ ensureBrowser }\\) => {\n const proc = await ensureBrowser\\({ port: 9222, host: '127.0.0.1', log: console.log }\\);\n const { SessionManager } = await import\\('./src/cdp/session.js'\\);\n const { createPageDomain } = await import\\('./src/cdp/domains/page.js'\\);\n const { createDOMDomain } = await import\\('./src/cdp/domains/dom.js'\\);\n const { createInputDomain } = await import\\('./src/cdp/domains/input.js'\\);\n const { createRuntimeDomain } = await import\\('./src/cdp/domains/runtime.js'\\);\n const { htmlToReadableText } = await import\\('./src/utils/dom-to-text.js'\\);\n\n const sm = new SessionManager\\({ host: '127.0.0.1', port: 9222 }\\);\n await sm.connect\\(\\);\n console.log\\('Capabilities:', sm.capabilities\\);\n\n const sc = await sm.getActiveSession\\(\\);\n const page = createPageDomain\\(sc, sm.capabilities\\);\n const dom = createDOMDomain\\(sc\\);\n const rt = createRuntimeDomain\\(sc\\);\n const inp = createInputDomain\\(sc\\);\n\n console.log\\('\\\\n[1] Navigate example.com'\\);\n await page.navigate\\('https://example.com'\\);\n console.log\\(' URL:', await page.getURL\\(\\)\\);\n console.log\\(' Title:', await page.getTitle\\(\\)\\);\n\n console.log\\('\\\\n[2] querySelector h1'\\);\n const h1Id = await dom.querySelector\\('h1'\\);\n console.log\\(' h1:', \\(await dom.getOuterHTML\\(h1Id\\)\\).trim\\(\\)\\);\n\n console.log\\('\\\\n[3] Page content'\\);\n const html = await rt.evaluate\\('document.documentElement.outerHTML'\\);\n console.log\\(' ', htmlToReadableText\\(html, { maxLength: 250 }\\).replace\\(/\\\\n/g, '\\\\n '\\)\\);\n\n console.log\\('\\\\n[4] Screenshot \\(expect null on Lightpanda\\)'\\);\n console.log\\(' Result:', await page.screenshot\\(\\) ?? 'null \\(skipped\\)'\\);\n\n console.log\\('\\\\n[5] List tabs'\\);\n console.log\\(' Tabs:', JSON.stringify\\(\\(await sm.listTargets\\(\\)\\).map\\(t => t.url\\)\\)\\);\n\n console.log\\('\\\\n[6] createTarget \\(single-target fallback\\)'\\);\n const sc2 = await sm.createTarget\\('https://httpbin.org/json'\\);\n const page2 = createPageDomain\\(sc2, sm.capabilities\\);\n await new Promise\\(r => setTimeout\\(r, 1500\\)\\);\n console.log\\(' URL after createTarget:', await page2.getURL\\(\\)\\);\n\n console.log\\('\\\\n[7] click + type'\\);\n await page.navigate\\('https://example.com'\\);\n await new Promise\\(r => setTimeout\\(r, 1000\\)\\);\n const linkId = await dom.querySelector\\('a'\\);\n if \\(linkId\\) {\n await dom.scrollIntoView\\(linkId\\);\n const { x, y } = await dom.getCenter\\(linkId\\);\n console.log\\(' link center:', x, y\\);\n await inp.click\\(x, y\\);\n await new Promise\\(r => setTimeout\\(r, 1000\\)\\);\n console.log\\(' URL after click:', await page.getURL\\(\\)\\);\n }\n\n await sm.close\\(\\);\n proc?.kill\\(\\);\n console.log\\('\\\\nAll checks passed.'\\);\n process.exit\\(0\\);\n}\\).catch\\(e => { console.error\\('FAIL:', e.message\\); process.exit\\(1\\); }\\);\n\" 2>&1)",
21
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\nnode -e \"\nimport\\('./src/utils/browser.js'\\).then\\(async \\({ ensureBrowser }\\) => {\n const proc = await ensureBrowser\\({ port: 9222, host: '127.0.0.1', log: console.log }\\);\n const { SessionManager } = await import\\('./src/cdp/session.js'\\);\n const { createPageDomain } = await import\\('./src/cdp/domains/page.js'\\);\n const { createDOMDomain } = await import\\('./src/cdp/domains/dom.js'\\);\n const { createInputDomain } = await import\\('./src/cdp/domains/input.js'\\);\n const { createRuntimeDomain } = await import\\('./src/cdp/domains/runtime.js'\\);\n const { htmlToReadableText } = await import\\('./src/utils/dom-to-text.js'\\);\n\n const sm = new SessionManager\\({ host: '127.0.0.1', port: 9222 }\\);\n await sm.connect\\(\\);\n console.log\\('Capabilities:', sm.capabilities\\);\n\n const sc = await sm.getActiveSession\\(\\);\n const page = createPageDomain\\(sc, sm.capabilities\\);\n const dom = createDOMDomain\\(sc\\);\n const rt = createRuntimeDomain\\(sc\\);\n const inp = createInputDomain\\(sc\\);\n\n console.log\\('\\\\n[1] Navigate example.com'\\);\n await page.navigate\\('https://example.com'\\);\n console.log\\(' URL:', await page.getURL\\(\\)\\);\n console.log\\(' Title:', await page.getTitle\\(\\)\\);\n\n console.log\\('\\\\n[2] querySelector h1'\\);\n const h1Id = await dom.querySelector\\('h1'\\);\n console.log\\(' h1:', \\(await dom.getOuterHTML\\(h1Id\\)\\).trim\\(\\)\\);\n\n console.log\\('\\\\n[3] Page content'\\);\n const html = await rt.evaluate\\('document.documentElement.outerHTML'\\);\n console.log\\(' ', htmlToReadableText\\(html, { maxLength: 200 }\\).replace\\(/\\\\n/g, '\\\\n '\\)\\);\n\n console.log\\('\\\\n[4] Screenshot \\(expect null\\)'\\);\n console.log\\(' Result:', await page.screenshot\\(\\) ?? 'null \\(skipped — Lightpanda\\)'\\);\n\n console.log\\('\\\\n[5] List tabs'\\);\n console.log\\(' Tabs:', JSON.stringify\\(\\(await sm.listTargets\\(\\)\\).map\\(t => t.url\\)\\)\\);\n\n console.log\\('\\\\n[6] createTarget fallback — navigate to httpbin'\\);\n await sm.createTarget\\('https://httpbin.org/json'\\);\n await new Promise\\(r => setTimeout\\(r, 1500\\)\\);\n console.log\\(' URL:', await page.getURL\\(\\)\\);\n\n console.log\\('\\\\n[7] click link on example.com'\\);\n await page.navigate\\('https://example.com'\\);\n await new Promise\\(r => setTimeout\\(r, 800\\)\\);\n const linkId = await dom.querySelector\\('a'\\);\n if \\(linkId\\) {\n const { x, y } = await dom.getCenter\\(linkId\\);\n console.log\\(' click at', x, y\\);\n await inp.click\\(x, y\\);\n await new Promise\\(r => setTimeout\\(r, 1000\\)\\);\n console.log\\(' URL after click:', await page.getURL\\(\\)\\);\n } else {\n console.log\\(' no link found'\\);\n }\n\n console.log\\('\\\\n[8] evaluate JS'\\);\n const links = await rt.evaluate\\('JSON.stringify\\(Array.from\\(document.querySelectorAll\\(\\\\\"a\\\\\"\\)\\).map\\(a=>a.href\\)\\)'\\);\n console.log\\(' links:', links\\);\n\n await sm.close\\(\\);\n proc?.kill\\(\\);\n console.log\\('\\\\nAll checks passed.'\\);\n process.exit\\(0\\);\n}\\).catch\\(e => { console.error\\('FAIL:', e.message, '\\\\n', e.stack?.split\\('\\\\n'\\)[1]\\); process.exit\\(1\\); }\\);\n\" 2>&1)",
22
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\nnode -e \"\nimport\\('./src/utils/browser.js'\\).then\\(async \\({ ensureBrowser }\\) => {\n const proc = await ensureBrowser\\({ port: 9222, host: '127.0.0.1', log: console.log }\\);\n const { SessionManager } = await import\\('./src/cdp/session.js'\\);\n const { createPageDomain } = await import\\('./src/cdp/domains/page.js'\\);\n const { createDOMDomain } = await import\\('./src/cdp/domains/dom.js'\\);\n const { createInputDomain } = await import\\('./src/cdp/domains/input.js'\\);\n const { createRuntimeDomain } = await import\\('./src/cdp/domains/runtime.js'\\);\n const { htmlToReadableText } = await import\\('./src/utils/dom-to-text.js'\\);\n\n const sm = new SessionManager\\({ host: '127.0.0.1', port: 9222 }\\);\n await sm.connect\\(\\);\n\n const sc = await sm.getActiveSession\\(\\);\n const page = createPageDomain\\(sc, sm.capabilities\\);\n const dom = createDOMDomain\\(sc\\);\n const rt = createRuntimeDomain\\(sc\\);\n const inp = createInputDomain\\(sc\\);\n\n console.log\\('[1] navigate example.com'\\);\n await page.navigate\\('https://example.com'\\);\n console.log\\(' URL:', await page.getURL\\(\\), '| Title:', await page.getTitle\\(\\)\\);\n\n console.log\\('[2] h1 text'\\);\n const h1 = await dom.querySelector\\('h1'\\);\n console.log\\(' ', \\(await dom.getOuterHTML\\(h1\\)\\).trim\\(\\)\\);\n\n console.log\\('[3] page content'\\);\n const text = htmlToReadableText\\(await rt.evaluate\\('document.documentElement.outerHTML'\\), { maxLength: 200 }\\);\n console.log\\(' ', text.replace\\(/\\\\n/g, '\\\\n '\\)\\);\n\n console.log\\('[4] screenshot:', await page.screenshot\\(\\) ?? 'null \\(Lightpanda — OK\\)'\\);\n\n console.log\\('[5] tabs:', \\(await sm.listTargets\\(\\)\\).map\\(t => t.url\\)\\);\n\n console.log\\('[6] createTarget \\(single-target fallback\\)'\\);\n await sm.createTarget\\('https://httpbin.org/json'\\);\n await new Promise\\(r => setTimeout\\(r, 1500\\)\\);\n console.log\\(' URL:', await page.getURL\\(\\)\\);\n\n console.log\\('[7] click link'\\);\n await page.navigate\\('https://example.com'\\);\n await new Promise\\(r => setTimeout\\(r, 1000\\)\\);\n const link = await dom.querySelector\\('a'\\);\n if \\(link\\) {\n const { x, y } = await dom.getCenter\\(link\\);\n await inp.click\\(x, y\\);\n await new Promise\\(r => setTimeout\\(r, 1500\\)\\);\n console.log\\(' URL after click:', await page.getURL\\(\\)\\);\n }\n\n console.log\\('[8] type into input \\(httpbin form\\)'\\);\n await page.navigate\\('https://httpbin.org/forms/post'\\);\n await new Promise\\(r => setTimeout\\(r, 1500\\)\\);\n const custname = await dom.querySelector\\('input[name=custname]'\\);\n if \\(custname\\) {\n await dom.scrollIntoView\\(custname\\);\n const { x, y } = await dom.getCenter\\(custname\\);\n await inp.click\\(x, y\\);\n await inp.type\\('Kuskus Bot'\\);\n const val = await rt.evaluate\\('document.querySelector\\(\\\\\"input[name=custname]\\\\\"\\).value'\\);\n console.log\\(' input value:', val\\);\n } else {\n console.log\\(' input not found'\\);\n }\n\n await sm.close\\(\\);\n proc?.kill\\(\\);\n console.log\\('\\\\nAll checks passed.'\\);\n process.exit\\(0\\);\n}\\).catch\\(e => { console.error\\('FAIL:', e.message, '\\\\n', e.stack?.split\\('\\\\n'\\)[1]\\); process.exit\\(1\\); }\\);\n\" 2>&1)",
23
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\nnode -e \"\nimport\\('./src/utils/browser.js'\\).then\\(async \\({ ensureBrowser }\\) => {\n const proc = await ensureBrowser\\({ port: 9222, host: '127.0.0.1', log: \\(m\\) => process.stdout.write\\(m+'\\\\n'\\) }\\);\n const { SessionManager } = await import\\('./src/cdp/session.js'\\);\n const { createPageDomain } = await import\\('./src/cdp/domains/page.js'\\);\n const { createDOMDomain } = await import\\('./src/cdp/domains/dom.js'\\);\n const { createInputDomain } = await import\\('./src/cdp/domains/input.js'\\);\n const { createRuntimeDomain } = await import\\('./src/cdp/domains/runtime.js'\\);\n const { htmlToReadableText } = await import\\('./src/utils/dom-to-text.js'\\);\n\n const sm = new SessionManager\\({ host: '127.0.0.1', port: 9222 }\\);\n await sm.connect\\(\\);\n\n const sc = await sm.getActiveSession\\(\\);\n const page = createPageDomain\\(sc, sm.capabilities\\);\n const dom = createDOMDomain\\(sc\\);\n const rt = createRuntimeDomain\\(sc\\);\n const inp = createInputDomain\\(sc\\);\n\n process.stdout.write\\('[1] navigate example.com\\\\n'\\);\n await page.navigate\\('https://example.com'\\);\n process.stdout.write\\(' URL: ' + await page.getURL\\(\\) + '\\\\n'\\);\n process.stdout.write\\(' Title: ' + await page.getTitle\\(\\) + '\\\\n'\\);\n\n process.stdout.write\\('[2] h1: ' + \\(await dom.getOuterHTML\\(await dom.querySelector\\('h1'\\)\\)\\).trim\\(\\) + '\\\\n'\\);\n\n process.stdout.write\\('[3] screenshot: ' + \\(await page.screenshot\\(\\) ?? 'null \\(Lightpanda OK\\)'\\) + '\\\\n'\\);\n\n process.stdout.write\\('[4] tabs: ' + JSON.stringify\\(\\(await sm.listTargets\\(\\)\\).map\\(t=>t.url\\)\\) + '\\\\n'\\);\n\n process.stdout.write\\('[5] createTarget fallback\\\\n'\\);\n await sm.createTarget\\('https://httpbin.org/json'\\);\n await new Promise\\(r=>setTimeout\\(r,1500\\)\\);\n process.stdout.write\\(' URL: ' + await page.getURL\\(\\) + '\\\\n'\\);\n\n process.stdout.write\\('[6] click link\\\\n'\\);\n await page.navigate\\('https://example.com'\\);\n await new Promise\\(r=>setTimeout\\(r,1000\\)\\);\n const link = await dom.querySelector\\('a'\\);\n if \\(link\\) {\n const {x,y} = await dom.getCenter\\(link\\);\n await inp.click\\(x, y\\);\n await new Promise\\(r=>setTimeout\\(r,2000\\)\\);\n process.stdout.write\\(' URL after click: ' + await page.getURL\\(\\) + '\\\\n'\\);\n }\n\n process.stdout.write\\('[7] type into form\\\\n'\\);\n await page.navigate\\('https://httpbin.org/forms/post'\\);\n await new Promise\\(r=>setTimeout\\(r,2000\\)\\);\n const input = await dom.querySelector\\('input[name=custname]'\\);\n if \\(input\\) {\n await dom.scrollIntoView\\(input\\);\n const {x,y} = await dom.getCenter\\(input\\);\n await inp.click\\(x,y\\);\n await inp.type\\('Kuskus Bot'\\);\n const val = await rt.evaluate\\('document.querySelector\\(\\\\\"input[name=custname]\\\\\"\\).value'\\);\n process.stdout.write\\(' typed value: ' + val + '\\\\n'\\);\n }\n\n await sm.close\\(\\);\n proc?.kill\\(\\);\n process.stdout.write\\('\\\\nAll checks passed.\\\\n'\\);\n process.exit\\(0\\);\n}\\).catch\\(e => { process.stderr.write\\('FAIL: '+e.message+'\\\\n'\\); process.exit\\(1\\); }\\);\n\" 2>&1 | grep -v '^\\\\[32m')",
24
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\nnode -e \"\nimport\\('./src/utils/browser.js'\\).then\\(async \\({ ensureBrowser }\\) => {\n const proc = await ensureBrowser\\({ port: 9222, host: '127.0.0.1', log: m => process.stdout.write\\(m+'\\\\n'\\) }\\);\n const { SessionManager } = await import\\('./src/cdp/session.js'\\);\n const { createPageDomain } = await import\\('./src/cdp/domains/page.js'\\);\n const { createDOMDomain } = await import\\('./src/cdp/domains/dom.js'\\);\n const { createInputDomain } = await import\\('./src/cdp/domains/input.js'\\);\n const { createRuntimeDomain } = await import\\('./src/cdp/domains/runtime.js'\\);\n const { htmlToReadableText } = await import\\('./src/utils/dom-to-text.js'\\);\n\n const sm = new SessionManager\\({ host: '127.0.0.1', port: 9222 }\\);\n await sm.connect\\(\\);\n\n const sc = await sm.getActiveSession\\(\\);\n const page = createPageDomain\\(sc, sm.capabilities\\);\n const dom = createDOMDomain\\(sc\\);\n const rt = createRuntimeDomain\\(sc\\);\n const inp = createInputDomain\\(sc\\);\n\n console.log\\('[1] navigate + title'\\);\n await page.navigate\\('https://example.com'\\);\n console.log\\(' ✓', await page.getURL\\(\\), '|', await page.getTitle\\(\\)\\);\n\n console.log\\('[2] DOM query'\\);\n const h1 = await dom.querySelector\\('h1'\\);\n console.log\\(' ✓', \\(await dom.getOuterHTML\\(h1\\)\\).trim\\(\\)\\);\n\n console.log\\('[3] page content'\\);\n const text = htmlToReadableText\\(await rt.evaluate\\('document.documentElement.outerHTML'\\), {maxLength:200}\\);\n console.log\\(' ✓', text.split\\('\\\\n'\\)[0]\\);\n\n console.log\\('[4] screenshot capability'\\);\n console.log\\(' ✓ screenshot:', await page.screenshot\\(\\) ?? 'null \\(Lightpanda — skipped safely\\)'\\);\n\n console.log\\('[5] list tabs'\\);\n console.log\\(' ✓', \\(await sm.listTargets\\(\\)\\).map\\(t=>t.url\\)\\);\n\n console.log\\('[6] form fill + type'\\);\n await page.navigate\\('https://httpbin.org/forms/post'\\);\n await new Promise\\(r=>setTimeout\\(r,2000\\)\\);\n const custname = await dom.querySelector\\('input[name=custname]'\\);\n if \\(custname\\) {\n await dom.scrollIntoView\\(custname\\);\n const {x,y} = await dom.getCenter\\(custname\\);\n await inp.click\\(x,y\\);\n await inp.type\\('Kuskus Bot'\\);\n const val = await rt.evaluate\\('document.querySelector\\(\\\\\"input[name=custname]\\\\\"\\).value'\\);\n console.log\\(' ✓ typed:', val\\);\n } else { console.log\\(' ✗ input not found'\\); }\n\n console.log\\('[7] select dropdown'\\);\n const sel = await dom.querySelector\\('select[name=size]'\\);\n if \\(sel\\) {\n await rt.evaluate\\('const el=document.querySelector\\(\\\\\"select[name=size]\\\\\"\\);el.value=\\\\\"large\\\\\";el.dispatchEvent\\(new Event\\(\\\\\"change\\\\\",{bubbles:true}\\)\\)'\\);\n const val = await rt.evaluate\\('document.querySelector\\(\\\\\"select[name=size]\\\\\"\\).value'\\);\n console.log\\(' ✓ select value:', val\\);\n }\n\n console.log\\('[8] evaluate JS'\\);\n const links = await rt.evaluate\\('Array.from\\(document.querySelectorAll\\(\\\\\"a\\\\\"\\)\\).map\\(a=>a.textContent.trim\\(\\)\\).filter\\(Boolean\\)'\\);\n console.log\\(' ✓ links on page:', links\\);\n\n await sm.close\\(\\);\n proc?.kill\\(\\);\n console.log\\('\\\\n✓ All core checks passed.'\\);\n process.exit\\(0\\);\n}\\).catch\\(e => { console.error\\('✗ FAIL:', e.message\\); process.exit\\(1\\); }\\);\n\" 2>&1 | grep -v '^\\\\[3')",
25
+ "Bash(pkill lightpanda 2>/dev/null; sleep 0.3\nnode -e \"\nimport\\('./src/utils/browser.js'\\).then\\(async \\({ ensureBrowser }\\) => {\n const proc = await ensureBrowser\\({ port: 9222, host: '127.0.0.1', log: m => process.stdout.write\\(m+'\\\\n'\\) }\\);\n const { SessionManager } = await import\\('./src/cdp/session.js'\\);\n const { createPageDomain } = await import\\('./src/cdp/domains/page.js'\\);\n const { createDOMDomain } = await import\\('./src/cdp/domains/dom.js'\\);\n const { createRuntimeDomain } = await import\\('./src/cdp/domains/runtime.js'\\);\n const { Executor } = await import\\('./src/agent/executor.js'\\);\n\n const sm = new SessionManager\\({ host: '127.0.0.1', port: 9222 }\\);\n await sm.connect\\(\\);\n const exec = new Executor\\(sm\\);\n\n console.log\\('[1] navigate'\\);\n const r1 = await exec.execute\\('navigate', { url: 'https://example.com' }\\);\n console.log\\(' ', r1\\);\n\n console.log\\('[2] get_url'\\);\n const r2 = await exec.execute\\('get_url', {}\\);\n console.log\\(' ', r2\\);\n\n console.log\\('[3] get_page_content'\\);\n const r3 = await exec.execute\\('get_page_content', {}\\);\n console.log\\(' ', r3.slice\\(0, 150\\)\\);\n\n console.log\\('[4] screenshot'\\);\n const r4 = await exec.execute\\('screenshot', {}\\);\n console.log\\(' ', r4?.type === 'screenshot' ? 'screenshot captured' : r4\\);\n\n console.log\\('[5] evaluate_js'\\);\n const r5 = await exec.execute\\('evaluate_js', { script: 'document.title' }\\);\n console.log\\(' ', r5\\);\n\n console.log\\('[6] navigate to form'\\);\n await exec.execute\\('navigate', { url: 'https://httpbin.org/forms/post' }\\);\n await new Promise\\(r => setTimeout\\(r, 2000\\)\\);\n\n console.log\\('[7] type_text'\\);\n const r7 = await exec.execute\\('type_text', { selector: 'input[name=custname]', text: 'Kuskus Bot' }\\);\n console.log\\(' ', r7\\);\n\n console.log\\('[8] select_option'\\);\n const r8 = await exec.execute\\('select_option', { selector: 'select[name=size]', value: 'large' }\\);\n console.log\\(' ', r8\\);\n\n console.log\\('[9] evaluate — verify values'\\);\n const r9 = await exec.execute\\('evaluate_js', {\n script: 'JSON.stringify\\({ name: document.querySelector\\(\\\\\"input[name=custname]\\\\\"\\).value, size: document.querySelector\\(\\\\\"select[name=size]\\\\\"\\).value }\\)'\n }\\);\n console.log\\(' ', r9\\);\n\n await sm.close\\(\\);\n proc?.kill\\(\\);\n console.log\\('\\\\nAll executor checks passed!'\\);\n process.exit\\(0\\);\n}\\).catch\\(e => { console.error\\('FAIL:', e.message\\); process.exit\\(1\\); }\\);\n\" 2>&1 | grep -v 'INFO\\\\|wsUrl')",
26
+ "Bash(pkill -x lightpanda 2>/dev/null; sleep 1 && node /tmp/kuskus-test.mjs 2>&1 | grep -v \"INFO\\\\|wsUrl\")",
27
+ "Bash(pkill -x lightpanda 2>/dev/null; npx vitest run --reporter=verbose 2>&1)",
28
+ "Bash(npm install openai 2>&1 | tail -3)"
7
29
  ]
8
30
  }
9
31
  }
package/.env.example CHANGED
@@ -1,8 +1,19 @@
1
1
  # ── CLI only ──────────────────────────────────────────────────────────────────
2
2
  # Required only for `kuskus run/repl/script` commands.
3
3
  # The MCP server does NOT use an API key — the host model drives the agent.
4
+
5
+ # Provider: anthropic | openai
6
+ # Leave unset to auto-detect from model name (claude-* → anthropic, gpt-* → openai)
7
+ # AGENT_PROVIDER=anthropic
8
+
9
+ # API keys — only the key for your chosen provider is needed
4
10
  ANTHROPIC_API_KEY=sk-ant-...
11
+ # OPENAI_API_KEY=sk-...
12
+
13
+ # Model name — provider is auto-detected if AGENT_PROVIDER is not set
5
14
  AGENT_MODEL=claude-sonnet-4-6
15
+ # AGENT_MODEL=gpt-4o
16
+
6
17
  AGENT_MAX_STEPS=20
7
18
  AGENT_MAX_TOKENS=4096
8
19
  AGENT_INCLUDE_SCREENSHOT=true
@@ -11,7 +22,8 @@ AGENT_SCREENSHOT_QUALITY=80
11
22
  # ── Browser (CLI + MCP) ────────────────────────────────────────────────────────
12
23
  CDP_URL=ws://localhost:9222
13
24
  CDP_LAUNCH_BROWSER=false
14
- CDP_BROWSER_PATH=lightpanda
25
+ # Override auto-detect with a specific Chrome/Chromium binary
26
+ # CDP_BROWSER_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
15
27
  CDP_BROWSER_PORT=9222
16
28
 
17
29
  # ── Logging ───────────────────────────────────────────────────────────────────
package/README.md ADDED
@@ -0,0 +1,430 @@
1
+ <p align="center">
2
+ <img src="assets/logo.png" width="160" alt="Kuskus" />
3
+ </p>
4
+
5
+ <h1 align="center">Kuskus</h1>
6
+
7
+ <p align="center">
8
+ AI browser agent via Chrome DevTools Protocol — CLI + MCP Server
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@porcupine/kuskus"><img src="https://img.shields.io/npm/v/@porcupine/kuskus?color=a78bfa&label=npm" alt="npm" /></a>
13
+ <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="node" />
14
+ <img src="https://img.shields.io/badge/browser-Chromium-blue" alt="Chromium" />
15
+ <img src="https://img.shields.io/badge/protocol-CDP-blue" alt="CDP" />
16
+ </p>
17
+
18
+ ---
19
+
20
+ Kuskus controls a browser directly over the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) by auto-detecting an installed Chrome/Chromium build (or downloading one on demand).
21
+
22
+ Ships as two artifacts:
23
+
24
+ | | CLI | MCP Server |
25
+ |---|---|---|
26
+ | **Usage** | `kuskus run "task..."` | Claude Desktop, Cursor, OpenCode, etc. |
27
+ | **LLM** | Claude (via `ANTHROPIC_API_KEY`) | Host model — no key needed |
28
+ | **Role** | Full agent loop | Expose browser tools to any AI |
29
+
30
+ ---
31
+
32
+ ## Requirements
33
+
34
+ - Node.js >= 20
35
+ - Chrome or Chromium (auto-detected; falls back to downloading a Chromium build into `~/.local`)
36
+
37
+ ---
38
+
39
+ ## CLI
40
+
41
+ ### Install
42
+
43
+ ```bash
44
+ npm install -g @porcupine/kuskus
45
+ ```
46
+
47
+ Or use directly with npx (no install needed):
48
+
49
+ ```bash
50
+ npx @porcupine/kuskus run "your task here"
51
+ ```
52
+
53
+ ### Setup
54
+
55
+ ```bash
56
+ cp .env.example .env
57
+ ```
58
+
59
+ Set the API key for your chosen provider:
60
+
61
+ ```env
62
+ # Anthropic (Claude) — default
63
+ ANTHROPIC_API_KEY=sk-ant-...
64
+
65
+ # OpenAI
66
+ OPENAI_API_KEY=sk-...
67
+ ```
68
+
69
+ Provider is **auto-detected from the model name** — no need to set it explicitly:
70
+
71
+ | Model prefix | Provider |
72
+ |---|---|
73
+ | `claude-*` | Anthropic |
74
+ | `gpt-*`, `o1*`, `o3*`, `o4*`, `chatgpt-*` | OpenAI |
75
+
76
+ ### Commands
77
+
78
+ #### `run` — one-shot task
79
+
80
+ ```bash
81
+ kuskus run "go to news.ycombinator.com and summarize the top 5 posts"
82
+ ```
83
+
84
+ Options:
85
+
86
+ ```
87
+ --cdp-url <url> CDP WebSocket URL (default: ws://localhost:9222)
88
+ --provider <name> LLM provider: anthropic or openai (auto-detected if not set)
89
+ --model <model> Model name (default: claude-sonnet-4-6)
90
+ --max-steps <n> Max agent steps (default: 20)
91
+ --screenshots <dir> Save step screenshots to directory
92
+ --launch Auto-launch Chrome/Chromium before running
93
+ --no-headless Launch Chrome/Chromium with a visible window
94
+ --force-launch Shut down an existing debugging browser before launching
95
+ --output <format> Output format: text or json (default: text)
96
+ --debug Log raw CDP messages
97
+ ```
98
+
99
+ #### `repl` — interactive session
100
+
101
+ ```bash
102
+ kuskus repl --launch
103
+ ```
104
+
105
+ Special commands inside REPL:
106
+
107
+ ```
108
+ !screenshot Capture and save the current viewport
109
+ !tabs List open browser tabs
110
+ !history Show action history
111
+ !clear Reset agent memory
112
+ !exit Quit
113
+ ```
114
+
115
+ #### `script` — batch tasks from JSON
116
+
117
+ ```bash
118
+ kuskus script ./tasks.json --output json
119
+ ```
120
+
121
+ `tasks.json` format:
122
+
123
+ ```json
124
+ [
125
+ "go to github.com/lightpanda-io/browser and read the description",
126
+ "search google for nodejs best practices 2025 and list the top 3 links"
127
+ ]
128
+ ```
129
+
130
+ #### `install` — manually install Chromium
131
+
132
+ ```bash
133
+ kuskus install
134
+ # or force re-download
135
+ kuskus install --force
136
+ ```
137
+
138
+ #### `mcp` — start MCP server
139
+
140
+ ```bash
141
+ kuskus mcp
142
+ ```
143
+
144
+ > Chromium is downloaded and launched automatically. No API key required.
145
+
146
+ ---
147
+
148
+ ## MCP Server
149
+
150
+ The MCP server exposes browser control tools to any AI host — Claude Desktop, Cursor, OpenCode, or any MCP-compatible client. The host model drives the reasoning; Kuskus only executes browser actions.
151
+
152
+ ### Claude Desktop
153
+
154
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
155
+
156
+ ```json
157
+ {
158
+ "mcpServers": {
159
+ "kuskus": {
160
+ "command": "npx",
161
+ "args": ["-y", "@porcupine/kuskus", "mcp"],
162
+ "env": {
163
+ "CDP_URL": "ws://localhost:9222"
164
+ }
165
+ }
166
+ }
167
+ }
168
+ ```
169
+
170
+ ### OpenCode
171
+
172
+ Add to `~/.config/opencode/opencode.json`:
173
+
174
+ ```json
175
+ {
176
+ "mcp": {
177
+ "kuskus": {
178
+ "type": "local",
179
+ "command": ["npx", "-y", "@porcupine/kuskus", "mcp"],
180
+ "enabled": true,
181
+ "environment": {
182
+ "CDP_URL": "ws://localhost:9222"
183
+ }
184
+ }
185
+ }
186
+ }
187
+ ```
188
+
189
+ ### Cursor / other MCP clients
190
+
191
+ ```json
192
+ {
193
+ "mcpServers": {
194
+ "kuskus": {
195
+ "command": "npx",
196
+ "args": ["-y", "@porcupine/kuskus", "mcp"]
197
+ }
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### Available MCP Tools
203
+
204
+ #### Navigation
205
+ | Tool | Description |
206
+ |------|-------------|
207
+ | `browser_navigate` | Navigate to a URL |
208
+ | `browser_go_back` | Go back in history |
209
+ | `browser_go_forward` | Go forward in history |
210
+ | `browser_get_url` | Get current URL |
211
+
212
+ #### Observation
213
+ | Tool | Description |
214
+ |------|-------------|
215
+ | `browser_screenshot` | Capture viewport as PNG |
216
+ | `browser_get_content` | Get page text content |
217
+ | `browser_element_info` | Get element attributes and text |
218
+
219
+ #### Interaction
220
+ | Tool | Description |
221
+ |------|-------------|
222
+ | `browser_click` | Click element by CSS selector |
223
+ | `browser_type` | Type text into an input |
224
+ | `browser_key_press` | Press a key (Enter, Tab, Escape…) |
225
+ | `browser_scroll` | Scroll up or down |
226
+ | `browser_hover` | Hover over an element |
227
+ | `browser_select` | Select a `<select>` option |
228
+ | `browser_checkbox` | Check or uncheck a checkbox |
229
+
230
+ #### JavaScript
231
+ | Tool | Description |
232
+ |------|-------------|
233
+ | `browser_evaluate` | Execute JS and return result |
234
+ | `browser_extract` | Extract structured data via JS |
235
+
236
+ #### Tabs
237
+ | Tool | Description |
238
+ |------|-------------|
239
+ | `browser_list_tabs` | List all open tabs |
240
+ | `browser_new_tab` | Open a new tab |
241
+ | `browser_switch_tab` | Switch to a tab by ID |
242
+ | `browser_close_tab` | Close a tab |
243
+
244
+ #### Utility
245
+ | Tool | Description |
246
+ |------|-------------|
247
+ | `browser_wait` | Wait N milliseconds (max 10s) |
248
+
249
+ ### MCP Resources
250
+
251
+ | URI | Description |
252
+ |-----|-------------|
253
+ | `browser://screenshot` | Current viewport as PNG |
254
+ | `browser://page/content` | Current page text |
255
+ | `browser://page/url` | Current URL |
256
+ | `browser://tabs` | Open tabs as JSON |
257
+
258
+ ---
259
+
260
+ ## Architecture
261
+
262
+ ```
263
+ Entry Points
264
+ kuskus run / repl / script kuskus mcp
265
+ │ │
266
+ ▼ ▼
267
+ Agent Core MCP Server
268
+ (plan → execute loop) (expose tools directly)
269
+ Claude API + tool use no LLM — host model drives
270
+ │ │
271
+ └──────────────┬─────────────────┘
272
+
273
+ Executor (CDP tools)
274
+
275
+ SessionManager
276
+ (single WebSocket,
277
+ session multiplexing)
278
+
279
+ Chromium Browser
280
+ ws://localhost:9222
281
+ ```
282
+
283
+ ### How the agent loop works
284
+
285
+ ```
286
+ ┌─────────────────────────────────────────────┐
287
+ │ 1. Observe get_page_content + screenshot │
288
+ │ 2. Plan Claude picks next tool │
289
+ │ 3. Execute CDP command via Chromium │
290
+ │ 4. Remember append step to rolling history │
291
+ │ 5. Repeat until finish or max steps │
292
+ └─────────────────────────────────────────────┘
293
+ ```
294
+
295
+ ---
296
+
297
+ ## Configuration
298
+
299
+ All options via environment variables (`.env` file supported):
300
+
301
+ ```env
302
+ # CLI only — not needed for MCP
303
+ ANTHROPIC_API_KEY=sk-ant-... # for Claude models
304
+ OPENAI_API_KEY=sk-... # for GPT / o-series models
305
+
306
+ # Provider: anthropic | openai — auto-detected from model name if not set
307
+ # AGENT_PROVIDER=anthropic
308
+
309
+ AGENT_MODEL=claude-sonnet-4-6 # or gpt-4o, o3-mini, etc.
310
+ AGENT_MAX_STEPS=20
311
+ AGENT_MAX_TOKENS=4096
312
+ AGENT_INCLUDE_SCREENSHOT=true
313
+ AGENT_SCREENSHOT_QUALITY=80
314
+
315
+ # Browser (CLI + MCP)
316
+ CDP_URL=ws://localhost:9222
317
+ # CDP_BROWSER_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
318
+ CDP_BROWSER_PORT=9222
319
+
320
+ # Logging
321
+ LOG_LEVEL=info # debug | info | warn | error
322
+ LOG_FORMAT=pretty # pretty | logfmt
323
+ ```
324
+
325
+ ---
326
+
327
+ ## Browser Runtime
328
+
329
+ Kuskus looks for Chrome/Chromium automatically. It checks common install locations (`/Applications/Google Chrome.app`, `chromium`, etc.) and honours `CDP_BROWSER_PATH`, `CHROME_PATH`, and `GOOGLE_CHROME_BIN` if set.
330
+
331
+ When no suitable binary is found (and auto-install is allowed) Kuskus downloads the latest **Chromium for Testing** build to `~/.local/chrome/<version>` and symlinks it to `~/.local/bin/chromium`.
332
+
333
+ Supported platforms for auto-download:
334
+
335
+ | OS | Arch |
336
+ |----|------|
337
+ | Linux | x86_64, arm64 |
338
+ | macOS | x86_64 (Intel), arm64 (Apple Silicon) |
339
+
340
+ Use `CDP_BROWSER_PATH` to point at a custom binary if you prefer a specific channel (e.g. Chrome Canary) or an alternative CDP-compatible browser.
341
+
342
+ ---
343
+
344
+ ## Examples
345
+
346
+ ```bash
347
+ # With Claude (default)
348
+ kuskus run "go to https://github.com/lightpanda-io/browser and summarize the README" --launch
349
+
350
+ # With GPT-4o — provider auto-detected from model name
351
+ kuskus run "go to news.ycombinator.com and list the top 5 posts" --model gpt-4o --launch
352
+
353
+ # With o3-mini
354
+ kuskus run "go to https://httpbin.org/json and extract all fields" --model o3-mini --launch
355
+
356
+ # Force provider explicitly
357
+ kuskus run "..." --provider openai --model gpt-4o-mini --launch
358
+
359
+ # Interactive REPL
360
+ kuskus repl --launch
361
+ kuskus repl --model gpt-4o --launch
362
+
363
+ # Extract data as JSON
364
+ kuskus run "go to news.ycombinator.com, extract title and URL of each front page post" --launch --output json
365
+
366
+ # Batch tasks
367
+ kuskus script ./tasks.json --model gpt-4o --output json
368
+ ```
369
+
370
+ ---
371
+
372
+ ## Development
373
+
374
+ ```bash
375
+ git clone https://github.com/porcupine/kuskus
376
+ cd kuskus
377
+ npm install
378
+ cp .env.example .env
379
+
380
+ # Run tests
381
+ npm test
382
+
383
+ # Try the CLI
384
+ node bin/cli.js install # download Chromium for Testing
385
+ node bin/cli.js run "..." --launch
386
+ ```
387
+
388
+ ### Project structure
389
+
390
+ ```
391
+ kuskus/
392
+ ├── bin/
393
+ │ └── cli.js CLI entrypoint (run/repl/script/mcp/install)
394
+ ├── src/
395
+ │ ├── cdp/
396
+ │ │ ├── client.js WebSocket CDP client + session multiplexing
397
+ │ │ ├── session.js Target/tab manager
398
+ │ │ └── domains/
399
+ │ │ ├── page.js Navigate, screenshot, reload
400
+ │ │ ├── dom.js querySelector, getBoxModel, focus
401
+ │ │ ├── input.js Click, hover, scroll, key press
402
+ │ │ ├── runtime.js Evaluate JS
403
+ │ │ ├── network.js Request monitoring/intercept
404
+ │ │ └── target.js Multi-tab management
405
+ │ ├── agent/
406
+ │ │ ├── index.js KuskusAgent orchestrator
407
+ │ │ ├── planner.js LLM planning loop (provider-agnostic)
408
+ │ │ ├── providers.js Anthropic + OpenAI adapters, auto-detection
409
+ │ │ ├── executor.js Tool → CDP command mapping
410
+ │ │ ├── tools.js Tool definitions (JSON Schema)
411
+ │ │ ├── memory.js Rolling step history
412
+ │ │ └── prompts.js System prompt
413
+ │ ├── mcp/
414
+ │ │ ├── server.js MCP server (stdio transport)
415
+ │ │ └── handlers.js Tool + resource handlers
416
+ │ └── utils/
417
+ │ ├── chromium.js Chrome/Chromium detector + downloader
418
+ │ ├── browser.js Launch + CDP readiness check
419
+ │ ├── dom-to-text.js HTML → readable text for LLM
420
+ │ ├── screenshot.js Save screenshots to disk
421
+ │ └── logger.js Structured logger (pino)
422
+ ├── tests/
423
+ └── examples/
424
+ ```
425
+
426
+ ---
427
+
428
+ ## License
429
+
430
+ MIT
package/SKILL.md ADDED
@@ -0,0 +1,46 @@
1
+ # Kuskus Skill Overview
2
+
3
+ Kuskus is a CLI and MCP server that lets an AI agent control a real Chrome/Chromium browser over the Chrome DevTools Protocol. Use it when you need rich browser interactions (navigation, DOM scraping, screenshots) in automated workflows.
4
+
5
+ ## Capabilities
6
+ - Launches or attaches to a local Chrome/Chromium instance (headless or visible).
7
+ - Auto-detects Anthropic or OpenAI models based on name; supports GPT-4o and Claude Sonnet out of the box.
8
+ - Exposes a full tool palette (navigation, DOM queries, input, waits, screenshots) via JSON-schema definitions.
9
+ - Provides a REPL for interactive runs and a script runner for batch tasks.
10
+ - MCP server mode surfaces the same browser tools to host applications (Claude Desktop, Cursor, etc.).
11
+
12
+ ## Quick Start (CLI)
13
+ ```bash
14
+ npm install -g @porcupine/kuskus
15
+ export OPENAI_API_KEY=sk-...
16
+
17
+ # one-shot task (visible browser)
18
+ kuskus run "Visit https://example.com and report the heading" --model gpt-4o --launch --no-headless
19
+
20
+ # interactive REPL with Claude
21
+ export ANTHROPIC_API_KEY=sk-ant-...
22
+ ```
23
+
24
+ Key flags:
25
+ - `--launch` / `--no-headless` – start Chrome automatically, optionally with a window.
26
+ - `--force-launch` – shut down any existing debugging browser before launching.
27
+ - `--output json` – return structured data when tools emit payloads.
28
+
29
+ ## MCP Integration
30
+ Start the MCP server and let the host model drive planning:
31
+
32
+ ```bash
33
+ npx @porcupine/kuskus mcp --launch --no-headless
34
+ ```
35
+
36
+ Configure your MCP-compatible client to use the `kuskus` command. Available tools include page navigation, content extraction, screenshot capture, and tab management.
37
+
38
+ ## Deployment Notes
39
+ - Chrome auto-detection checks standard install paths and environment overrides (`CDP_BROWSER_PATH`, `CHROME_PATH`, `GOOGLE_CHROME_BIN`).
40
+ - When no browser is found, the CLI downloads the latest Chromium-for-Testing build into `~/.local/chrome/<version>`.
41
+ - Ensure `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` is set before running tasks that use those providers.
42
+
43
+ ## Troubleshooting
44
+ - Use `--debug` to stream CDP traffic and planner logs.
45
+ - If a previous headless session is blocking `--no-headless`, add `--force-launch` to close it before relaunch.
46
+ - Screenshots can be saved automatically with `--screenshots <dir>`.