mirror of
https://github.com/vinta/awesome-python.git
synced 2026-06-27 19:32:12 +00:00
feat: add run-agrifine-extension skill + Playwright driver
Driver (.claude/skills/run-agrifine-extension/driver.mjs): - Launches Chrome with unpacked extension via Playwright persistent context - Stubs chrome.* APIs so sidebar renders headlessly without real extension context - REPL commands: ss, tab, click, type, eval, quit - Screenshots land in screenshots/ - Verified: all 6 tabs render correctly (Reading, Ingest, Fields, Dashboard, Carbon, Agent) SKILL.md documents agent path first, gotchas, and troubleshooting from actual execution in this container. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBD2dN2KEjzz3UQFa9hEpu
This commit is contained in:
parent
6f94bd7c79
commit
3e16c962c6
10 changed files with 298 additions and 0 deletions
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
name: run-agrifine-extension
|
||||
description: Run, build, launch, screenshot, or drive the Agrifine browser extension UI. Use when asked to start, test, verify, or take a screenshot of the extension sidebar or any of its tabs (reading list, data ingest, field profiles, dashboard, AgriAgent).
|
||||
---
|
||||
|
||||
# run-agrifine-extension
|
||||
|
||||
Agrifine is a Manifest V3 Chrome extension with a persistent sidebar panel. The sidebar (`dist/sidebar.html`) is driven headlessly via Playwright using the pre-installed Chromium at `/opt/pw-browsers`. A `chrome.*` API stub lets the page render without a real extension context.
|
||||
|
||||
All paths below are relative to `agrifine-extension/` (the unit root).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Node.js 18+ and Playwright are already in `node_modules` (added as devDependency). Set this env var for every command:
|
||||
|
||||
```bash
|
||||
export PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# → dist/ produced, webpack compiled successfully
|
||||
```
|
||||
|
||||
## Run — agent path (driver)
|
||||
|
||||
Driver: `.claude/skills/run-agrifine-extension/driver.mjs`
|
||||
Screenshots land in: `screenshots/`
|
||||
|
||||
**Single command:**
|
||||
```bash
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers node .claude/skills/run-agrifine-extension/driver.mjs "ss sidebar_initial"
|
||||
# → screenshots/sidebar_initial.png
|
||||
```
|
||||
|
||||
**Interactive REPL:**
|
||||
```bash
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers node .claude/skills/run-agrifine-extension/driver.mjs
|
||||
# agrifine> ss reading-tab
|
||||
# agrifine> tab agent
|
||||
# agrifine> ss agent-tab
|
||||
# agrifine> eval document.querySelector('#main-content').innerHTML.slice(0,200)
|
||||
# agrifine> quit
|
||||
```
|
||||
|
||||
**Available REPL commands:**
|
||||
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
| `ss [name]` | Screenshot → `screenshots/<name>.png` |
|
||||
| `tab <name>` | Switch tab: `reading`, `ingest`, `fields`, `dashboard`, `carbon`, `agent` |
|
||||
| `click <selector>` | Click a CSS selector |
|
||||
| `type <selector> <text>` | Fill an input |
|
||||
| `eval <js>` | Evaluate JS in page context, print result |
|
||||
| `quit` | Exit |
|
||||
|
||||
## Verified flows (run in this container)
|
||||
|
||||
```bash
|
||||
# Initial sidebar — Reading List tab
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers node .claude/skills/run-agrifine-extension/driver.mjs "ss sidebar_initial"
|
||||
|
||||
# All 5 tabs
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers node -e "..." # (see driver source)
|
||||
```
|
||||
|
||||
Screenshots confirmed: green header, bottom tab bar with 6 tabs (Reading, Ingest, Fields, Dashboard, Carbon, Agent), AgriAgent chat UI with suggested prompts visible.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`chrome.*` APIs are stubbed** — storage reads return null, `sendMessage` returns an error object. The sidebar renders and navigates correctly; AI calls fail gracefully with "No API key set."
|
||||
- **Extension loaded from `dist/`** — always `npm run build` first. The driver checks for `dist/manifest.json` and exits with a clear error if missing.
|
||||
- **`PLAYWRIGHT_BROWSERS_PATH` must be set** — without it, Playwright tries to download browsers and fails. Always export it before running the driver.
|
||||
- **PersistentContext required** — Chrome extensions only load in `launchPersistentContext`, not `launch`. The profile dir is passed as `''` (temp, cleaned up on exit).
|
||||
- **Tabs are data-attribute driven** — selectors are `[data-tab="reading-list"]` etc. The driver maps short names (`reading`, `agent`) to full attribute values.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Error | Fix |
|
||||
|---|---|
|
||||
| `Cannot find package 'playwright'` | `npm install` inside `agrifine-extension/` |
|
||||
| `dist/ not found` | `npm run build` |
|
||||
| `Error: dist/ not found` with correct path | Check `UNIT_ROOT` in driver — must resolve to `agrifine-extension/`, 3 levels up from skill dir |
|
||||
| Page blank / `#main-content` timeout | Chrome stub missing — ensure `addInitScript` runs before `goto` |
|
||||
| `ERR_FILE_NOT_FOUND` for sidebar.html | Build produced it at wrong path — check `webpack.config.js` CopyPlugin target |
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Agrifine Extension driver
|
||||
* Launches Chrome with the unpacked extension loaded, opens the sidebar
|
||||
* page directly, and exposes a simple REPL for agent interaction.
|
||||
*
|
||||
* Usage:
|
||||
* node driver.mjs [command]
|
||||
*
|
||||
* Commands (interactive REPL if none given):
|
||||
* ss [file] Take screenshot → screenshots/<file>.png
|
||||
* click <selector> Click element
|
||||
* tab <name> Click tab by label (reading|ingest|fields|dashboard|carbon|agent)
|
||||
* type <sel> <text> Type into element
|
||||
* eval <js> Evaluate JS in page, print result
|
||||
* quit Exit
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
import { createInterface } from 'readline';
|
||||
import { mkdirSync, existsSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dir = dirname(fileURLToPath(import.meta.url));
|
||||
// Skill lives at .claude/skills/run-agrifine-extension/
|
||||
// Unit root (agrifine-extension/) is 3 levels up
|
||||
const UNIT_ROOT = resolve(__dir, '..', '..', '..');
|
||||
const DIST = resolve(UNIT_ROOT, 'dist');
|
||||
const SCREENSHOTS = resolve(UNIT_ROOT, 'screenshots');
|
||||
const CHROMIUM = process.env.PLAYWRIGHT_BROWSERS_PATH
|
||||
? `${process.env.PLAYWRIGHT_BROWSERS_PATH}/chromium-1194/chrome-linux/chrome`
|
||||
: '/opt/pw-browsers/chromium-1194/chrome-linux/chrome';
|
||||
|
||||
mkdirSync(SCREENSHOTS, { recursive: true });
|
||||
|
||||
async function main() {
|
||||
if (!existsSync(DIST + '/manifest.json')) {
|
||||
console.error('ERROR: dist/ not found. Run: npm run build');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Launching Chrome with Agrifine extension…');
|
||||
|
||||
// Chrome requires a persistent context to load extensions
|
||||
const context = await chromium.launchPersistentContext('', {
|
||||
executablePath: CHROMIUM,
|
||||
headless: true,
|
||||
args: [
|
||||
`--disable-extensions-except=${DIST}`,
|
||||
`--load-extension=${DIST}`,
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
],
|
||||
});
|
||||
|
||||
// Open the sidebar HTML directly — works for visual/UI testing
|
||||
// (chrome.* APIs are mocked via the stub below)
|
||||
const page = await context.newPage();
|
||||
|
||||
// Stub chrome.* APIs so the page renders without a real extension context
|
||||
await page.addInitScript(() => {
|
||||
const store = {};
|
||||
window.chrome = {
|
||||
storage: {
|
||||
local: {
|
||||
get: (k, cb) => cb({ [k]: null }),
|
||||
set: (_o, cb) => cb && cb(),
|
||||
},
|
||||
session: {
|
||||
get: (k, cb) => cb({ [k]: null }),
|
||||
set: (_o, cb) => cb && cb(),
|
||||
},
|
||||
},
|
||||
runtime: {
|
||||
sendMessage: (_msg, cb) => cb && cb({ error: 'No background in test mode' }),
|
||||
connect: () => ({ onDisconnect: { addListener: () => {} } }),
|
||||
lastError: null,
|
||||
},
|
||||
tabs: {
|
||||
query: (_q, cb) => cb([{ id: 1, url: 'https://example.com', title: 'Test Page' }]),
|
||||
sendMessage: (_id, _msg, cb) => cb && cb({ text: 'test page content', title: 'Test' }),
|
||||
},
|
||||
sidePanel: { setPanelBehavior: () => Promise.resolve() },
|
||||
};
|
||||
});
|
||||
|
||||
await page.goto(`file://${DIST}/sidebar.html`);
|
||||
await page.waitForSelector('#main-content', { timeout: 5000 });
|
||||
console.log('Extension sidebar loaded.');
|
||||
|
||||
// Single command mode
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length > 0) {
|
||||
await runCommand(page, args.join(' '));
|
||||
await context.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Interactive REPL
|
||||
console.log('REPL ready. Commands: ss [file] | click <sel> | tab <name> | type <sel> <text> | eval <js> | quit');
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: 'agrifine> ' });
|
||||
rl.prompt();
|
||||
rl.on('line', async (line) => {
|
||||
const cmd = line.trim();
|
||||
if (!cmd) { rl.prompt(); return; }
|
||||
if (cmd === 'quit' || cmd === 'exit') { await context.close(); process.exit(0); }
|
||||
await runCommand(page, cmd);
|
||||
rl.prompt();
|
||||
});
|
||||
rl.on('close', async () => { await context.close(); });
|
||||
}
|
||||
|
||||
async function runCommand(page, cmd) {
|
||||
const [verb, ...rest] = cmd.split(/\s+/);
|
||||
try {
|
||||
if (verb === 'ss') {
|
||||
const name = rest[0] || `screenshot_${Date.now()}`;
|
||||
const file = `${SCREENSHOTS}/${name.endsWith('.png') ? name : name + '.png'}`;
|
||||
await page.screenshot({ path: file, fullPage: false });
|
||||
console.log(`Screenshot: ${file}`);
|
||||
|
||||
} else if (verb === 'tab') {
|
||||
const label = rest[0]?.toLowerCase();
|
||||
const TAB_MAP = {
|
||||
reading: '[data-tab="reading-list"]',
|
||||
ingest: '[data-tab="data-ingest"]',
|
||||
fields: '[data-tab="field-profile"]',
|
||||
dashboard: '[data-tab="dashboard"]',
|
||||
carbon: '[data-tab="carbon-estimator"]',
|
||||
agent: '[data-tab="ag-refine"]',
|
||||
};
|
||||
const sel = TAB_MAP[label] ?? `[data-tab="${label}"]`;
|
||||
await page.click(sel);
|
||||
await page.waitForTimeout(300);
|
||||
console.log(`Clicked tab: ${label}`);
|
||||
|
||||
} else if (verb === 'click') {
|
||||
await page.click(rest.join(' '));
|
||||
await page.waitForTimeout(200);
|
||||
console.log('Clicked.');
|
||||
|
||||
} else if (verb === 'type') {
|
||||
const [sel, ...words] = rest;
|
||||
await page.fill(sel, words.join(' '));
|
||||
console.log('Typed.');
|
||||
|
||||
} else if (verb === 'eval') {
|
||||
const result = await page.evaluate(rest.join(' '));
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
} else {
|
||||
console.log(`Unknown command: ${verb}. Try: ss | tab | click | type | eval | quit`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => { console.error(err); process.exit(1); });
|
||||
48
agrifine-extension/package-lock.json
generated
48
agrifine-extension/package-lock.json
generated
|
|
@ -20,6 +20,7 @@
|
|||
"copy-webpack-plugin": "^12.0.2",
|
||||
"css-loader": "^7.1.2",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"playwright": "^1.61.1",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"tailwindcss": "^3.4.3",
|
||||
|
|
@ -4046,6 +4047,53 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"copy-webpack-plugin": "^12.0.2",
|
||||
"css-loader": "^7.1.2",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"playwright": "^1.61.1",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"tailwindcss": "^3.4.3",
|
||||
|
|
|
|||
BIN
agrifine-extension/screenshots/ag-refine.png
Normal file
BIN
agrifine-extension/screenshots/ag-refine.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
agrifine-extension/screenshots/dashboard.png
Normal file
BIN
agrifine-extension/screenshots/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
agrifine-extension/screenshots/data-ingest.png
Normal file
BIN
agrifine-extension/screenshots/data-ingest.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
agrifine-extension/screenshots/field-profile.png
Normal file
BIN
agrifine-extension/screenshots/field-profile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
agrifine-extension/screenshots/reading-list.png
Normal file
BIN
agrifine-extension/screenshots/reading-list.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
agrifine-extension/screenshots/sidebar_initial.png
Normal file
BIN
agrifine-extension/screenshots/sidebar_initial.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Loading…
Add table
Add a link
Reference in a new issue