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:
Claude 2026-06-27 04:05:08 +00:00
parent 6f94bd7c79
commit 3e16c962c6
No known key found for this signature in database
10 changed files with 298 additions and 0 deletions

View file

@ -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 |

View file

@ -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); });

View file

@ -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",

View file

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB