diff --git a/agrifine-extension/.claude/skills/run-agrifine-extension/SKILL.md b/agrifine-extension/.claude/skills/run-agrifine-extension/SKILL.md new file mode 100644 index 0000000..2df452b --- /dev/null +++ b/agrifine-extension/.claude/skills/run-agrifine-extension/SKILL.md @@ -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/.png` | +| `tab ` | Switch tab: `reading`, `ingest`, `fields`, `dashboard`, `carbon`, `agent` | +| `click ` | Click a CSS selector | +| `type ` | Fill an input | +| `eval ` | 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 | diff --git a/agrifine-extension/.claude/skills/run-agrifine-extension/driver.mjs b/agrifine-extension/.claude/skills/run-agrifine-extension/driver.mjs new file mode 100644 index 0000000..1c14cd1 --- /dev/null +++ b/agrifine-extension/.claude/skills/run-agrifine-extension/driver.mjs @@ -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/.png + * click Click element + * tab Click tab by label (reading|ingest|fields|dashboard|carbon|agent) + * type Type into element + * eval 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 | tab | type | eval | 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); }); diff --git a/agrifine-extension/package-lock.json b/agrifine-extension/package-lock.json index 1bc27de..bc4eb68 100644 --- a/agrifine-extension/package-lock.json +++ b/agrifine-extension/package-lock.json @@ -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", diff --git a/agrifine-extension/package.json b/agrifine-extension/package.json index a33d9c5..2270b00 100644 --- a/agrifine-extension/package.json +++ b/agrifine-extension/package.json @@ -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", diff --git a/agrifine-extension/screenshots/ag-refine.png b/agrifine-extension/screenshots/ag-refine.png new file mode 100644 index 0000000..1c79dde Binary files /dev/null and b/agrifine-extension/screenshots/ag-refine.png differ diff --git a/agrifine-extension/screenshots/dashboard.png b/agrifine-extension/screenshots/dashboard.png new file mode 100644 index 0000000..888e9f1 Binary files /dev/null and b/agrifine-extension/screenshots/dashboard.png differ diff --git a/agrifine-extension/screenshots/data-ingest.png b/agrifine-extension/screenshots/data-ingest.png new file mode 100644 index 0000000..742649a Binary files /dev/null and b/agrifine-extension/screenshots/data-ingest.png differ diff --git a/agrifine-extension/screenshots/field-profile.png b/agrifine-extension/screenshots/field-profile.png new file mode 100644 index 0000000..10690cf Binary files /dev/null and b/agrifine-extension/screenshots/field-profile.png differ diff --git a/agrifine-extension/screenshots/reading-list.png b/agrifine-extension/screenshots/reading-list.png new file mode 100644 index 0000000..da44622 Binary files /dev/null and b/agrifine-extension/screenshots/reading-list.png differ diff --git a/agrifine-extension/screenshots/sidebar_initial.png b/agrifine-extension/screenshots/sidebar_initial.png new file mode 100644 index 0000000..d2bd020 Binary files /dev/null and b/agrifine-extension/screenshots/sidebar_initial.png differ