diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeb42f0..51de1fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,3 +45,4 @@ jobs: echo "All openclaw version fields are up to date ($LATEST)." - run: npx tsc --noEmit - run: npx vitest run + - run: npm run build diff --git a/.github/workflows/openclaw_version_tests.yml b/.github/workflows/openclaw_version_tests.yml new file mode 100644 index 0000000..0b80c7e --- /dev/null +++ b/.github/workflows/openclaw_version_tests.yml @@ -0,0 +1,160 @@ +name: OpenClaw Version Test + +on: + pull_request: + branches: [main] + schedule: + # Mondays 07:00 UTC — 8am Prague in winter (CET), 9am Prague in summer (CEST). + - cron: "0 7 * * 1" + workflow_dispatch: + +concurrency: + group: openclaw-version-test-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + discover: + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.filter.outputs.versions }} + steps: + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - id: filter + name: Pick latest 3 OpenClaw versions + shell: bash + run: | + set -euo pipefail + + # Drop pre-releases (anything with '-'), sort by semver, take the latest 3. + versions=$( + npm view openclaw versions --json \ + | jq -r '.[] | select(test("-") | not)' \ + | sort -V \ + | tail -n 3 + ) + + if [ -z "$versions" ]; then + echo "No stable OpenClaw versions found on npm." >&2 + exit 1 + fi + + json=$(echo "$versions" | jq -R . | jq -sc .) + echo "versions=$json" >> "$GITHUB_OUTPUT" + echo "Selected versions: $json" + + test: + needs: discover + runs-on: ubuntu-latest + name: OpenClaw ${{ matrix.version }} + strategy: + fail-fast: false + matrix: + version: ${{ fromJson(needs.discover.outputs.versions) }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install OpenClaw ${{ matrix.version }} and smoke-test plugin + shell: bash + run: | + set -euo pipefail + + # On PRs, pack the PR's source and install that tarball. On schedule / + # manual dispatch, install the currently-published @latest from npm. + if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + echo "::group::npm install && npm pack (PR branch)" + cd "$GITHUB_WORKSPACE" + npm install + tarball=$(npm pack | tail -n 1) + PLUGIN_SPEC="$GITHUB_WORKSPACE/$tarball" + echo "Packed: $PLUGIN_SPEC" + echo "::endgroup::" + else + PLUGIN_SPEC="@apify/apify-openclaw-plugin@latest" + echo "Using published spec: $PLUGIN_SPEC" + fi + + WORKDIR="$RUNNER_TEMP/openclaw-test" + mkdir -p "$WORKDIR" + cd "$WORKDIR" + + echo "::group::npm install openclaw@${{ matrix.version }}" + npm init -y >/dev/null + npm install "openclaw@${{ matrix.version }}" + echo "::endgroup::" + + echo "::group::openclaw plugins install $PLUGIN_SPEC" + npx --no-install openclaw plugins install "$PLUGIN_SPEC" + echo "::endgroup::" + + echo "::group::openclaw plugins list" + list_output=$(npx --no-install openclaw plugins list) + echo "$list_output" + if ! echo "$list_output" | grep -q "apify-openclaw-plugin"; then + echo "FAIL: apify-openclaw-plugin not present in 'plugins list' output" >&2 + exit 1 + fi + echo "::endgroup::" + + echo "::group::openclaw plugins inspect apify-openclaw-plugin --runtime --json" + inspect_json=$(npx --no-install openclaw plugins inspect apify-openclaw-plugin --runtime --json) + echo "$inspect_json" | jq . + if ! echo "$inspect_json" | jq -e '.. | strings | select(. == "apify")' >/dev/null; then + echo "FAIL: 'apify' tool not found in plugin runtime inspect output" >&2 + exit 1 + fi + echo "::endgroup::" + + echo "OK: plugin loaded and apify tool is registered on openclaw ${{ matrix.version }}" + + # Stable aggregator check — mark THIS one as required in branch protection. + # The matrix job names ("OpenClaw 2026.5.X") rotate as discovery picks up new + # versions, but this job's name never changes. + required: + needs: [discover, test] + if: always() + runs-on: ubuntu-latest + steps: + - name: All OpenClaw version tests must pass + shell: bash + run: | + if [[ "${{ needs.discover.result }}" != "success" || "${{ needs.test.result }}" != "success" ]]; then + echo "discover=${{ needs.discover.result }} test=${{ needs.test.result }}" + echo "One or more OpenClaw version tests failed." + exit 1 + fi + echo "All OpenClaw version tests passed." + + # Slack notification on scheduled failure — disabled for now. + # To re-enable: uncomment this job and set the `SLACK_WEBHOOK_URL` repo secret. + # + # notify: + # needs: test + # if: always() && needs.test.result == 'failure' && github.event_name == 'schedule' + # runs-on: ubuntu-latest + # steps: + # - name: Notify Slack on scheduled failure + # env: + # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + # RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + # shell: bash + # run: | + # if [ -z "${SLACK_WEBHOOK_URL:-}" ]; then + # echo "SLACK_WEBHOOK_URL secret not configured — skipping notification." + # exit 0 + # fi + # + # payload=$(jq -nc \ + # --arg text "🚨 *OpenClaw Version Test failed* (scheduled run) + # One or more OpenClaw versions failed the plugin install smoke test. + # <${RUN_URL}|View run details>" \ + # '{text: $text}') + # + # curl -sS -X POST -H 'Content-Type: application/json' \ + # --data "$payload" "$SLACK_WEBHOOK_URL" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3daa224..97b00c1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -50,6 +50,9 @@ jobs: - name: Run tests run: npx vitest run + - name: Build + run: npm run build + - name: Commit version bump uses: apify/actions/signed-commit@v1.1.2 with: diff --git a/CLAUDE.md b/CLAUDE.md index 51cc493..debe093 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ Single tool with 3 actions: The tool description includes instructions for the agent: - **Sub-agent delegation:** Tool should be used by a sub-agent that returns only relevant extracted data, not raw dumps. - **Batching:** Batch multiple URLs into a single run (e.g. `startUrls: [{url: "..."}, ...]`). -- **Known actors:** Compact comma-separated list of 57 actors across Instagram, Facebook, TikTok, YouTube, Google Maps, and more. +- **Known actors:** Compact comma-separated list of 20k+ actors across Instagram, Facebook, TikTok, YouTube, Google Maps, and more. - **Support:** Directs users to integrations@apify.com for issues. ## Key Architecture Decisions @@ -79,8 +79,46 @@ The wizard merges safely: preserves existing config, adds to `tools.alsoAllow` w - **Type-check:** `npx tsc --noEmit` - **Test:** `npx vitest run` - **Pack (dry run):** `npm pack --dry-run` +- **Bump OpenClaw to latest:** `npm run bump:openclaw` *(install-tests against the latest stable openclaw, then updates `devDependencies.openclaw` + both `compat` fields if it passes; user reviews and commits)* - **Current state:** 1 test file, 10 tests passing. +## Updating the OpenClaw Version + +The `OpenClaw Version Test` workflow blocks any PR whose pinned openclaw version lags behind the latest stable on npm. To keep up: + +1. Run `npm run bump:openclaw`. The script packs the plugin, installs it against `openclaw@latest` in a temp directory, runs `plugins list` + `plugins inspect` (same smoke as CI), then runs local `typecheck` + `vitest`. If any step fails, no files are touched. +2. On success, `package.json` (`devDependencies.openclaw`, `openclaw.compat.builtWithOpenClawVersion`, `openclaw.compat.pluginSdkVersion`) and `package-lock.json` are updated. Review `git diff`, then commit as `chore: bump openclaw to `. +3. `peerDependencies.openclaw` uses `">="` and is intentionally not touched. + +Claude should default to this script when asked to bump OpenClaw or when the version test is failing — do not run the underlying `npm install --save-dev openclaw@X` + `npm pkg set ...` commands manually. + +## CI Workflows + +### `ci.yml` — fast PR gate +- Runs `npx tsc --noEmit` + `npx vitest run` on Node 22. +- Triggers on push/PR to `main`. + +### `openclaw_version_tests.yml` — OpenClaw runtime compatibility matrix +Validates the plugin actually installs and loads inside a real OpenClaw runtime. + +- **Triggers:** + - `pull_request` to `main` — packs the PR branch (`npm install && npm pack`) and installs that tarball, so each PR is validated against its own diff. + - `schedule` — Mondays at `07:00` UTC (`0 7 * * 1`), which is 8am Prague (CET) in winter / 9am Prague (CEST) in summer. + - `workflow_dispatch` — manual re-run. +- **Schedule / dispatch mode:** installs `@apify/apify-openclaw-plugin@latest` from npm (validates the currently-shipping release). +- **Versions tested:** the `discover` job calls `npm view openclaw versions --json`, drops pre-releases (anything containing `-`), `sort -V | tail -n 3` to pick the latest 3 stable versions. Auto-updates — no manual list maintenance. +- **Smoke test per version:** all four phases run in **one consolidated bash step** that `cd`s to `$RUNNER_TEMP/openclaw-test` (splitting across steps with `working-directory:` caused a path mismatch — don't do that): + 1. `npm install openclaw@` in a fresh `$RUNNER_TEMP/openclaw-test` dir. + 2. `npx openclaw plugins install ` (tarball path on PR; `@latest` on schedule). + 3. `npx openclaw plugins list` — must contain `apify-openclaw-plugin`. + 4. `npx openclaw plugins inspect apify-openclaw-plugin --runtime --json` — must surface the `apify` tool name (matched loosely via `jq '.. | strings | select(. == "apify")'` since the JSON shape may evolve across OpenClaw versions). +- **Aggregator job (`required`)** — runs after the matrix with `if: always()`, fails if `discover` or `test` didn't succeed. This is the **stable required status check** in branch protection — the per-version matrix legs (`OpenClaw 2026.x.y`) rotate as discovery picks up new releases, so don't pin those. +- **Slack notification** — a `notify` job is scaffolded but commented out. To re-enable, uncomment it and add a `SLACK_WEBHOOK_URL` repo secret; it only fires on `schedule` failures. + +### Branch protection on `main` +- `required` (the aggregator job above) is enforced as a required status check via the GitHub branch protection API. +- Set up via `gh api -X PUT repos/apify/apify-openclaw-plugin/branches/main/protection --input -` with a JSON body — the flag-form `-F nested.key=value` doesn't work for the nested object schema. + ## Coding Style - TypeScript (ESM). Prefer strict typing; avoid `any`. diff --git a/README.md b/README.md index 2c3e031..7a09fdb 100644 --- a/README.md +++ b/README.md @@ -180,15 +180,22 @@ The tool description instructs agents to delegate `apify` calls to a sub-agent. npm install # Type check -npx tsc --noEmit +npm run typecheck # Run tests -npx vitest run +npm test -# Pack (dry run) +# Build compiled output to dist/ (required for publish) +npm run build + +# Pack (dry run) — npm runs `prepublishOnly` (build) automatically before packing npm pack --dry-run ``` +`dist/` is generated by `npm run build` and is not checked in. The published npm +tarball ships `dist/` so newer OpenClaw versions (which no longer JIT-load +TypeScript) can install the plugin. + ## Support For issues with this integration, contact [integrations@apify.com](mailto:integrations@apify.com). diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 838950e..365af7d 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -1,7 +1,10 @@ { "id": "apify-openclaw-plugin", "name": "Apify", - "description": "Universal web scraping and data extraction via Apify — scrape any platform using 57+ Actors across social media, maps, search, e-commerce, and more.", + "description": "Universal web scraping and data extraction via Apify — scrape any platform using 20k+ Actors across social media, maps, search, e-commerce, and more.", + "contracts": { + "tools": ["apify"] + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/package-lock.json b/package-lock.json index d3b7cdc..d427edd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "typebox": "^1.1.38" }, "devDependencies": { - "openclaw": "^2026.5.18", + "openclaw": "^2026.5.19", "typescript": "^5.7.0", "vitest": "^3.0.0" }, @@ -3107,9 +3107,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -3782,16 +3782,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -4643,25 +4633,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/file-type": { - "version": "22.0.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.1.tgz", - "integrity": "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.5", - "token-types": "^6.1.2", - "uint8array-extras": "^1.5.0" - }, - "engines": { - "node": ">=22" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -5289,16 +5260,6 @@ "node": ">= 10" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -5938,9 +5899,9 @@ } }, "node_modules/openclaw": { - "version": "2026.5.18", - "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.5.18.tgz", - "integrity": "sha512-a9p2jdD0SEFUIxyCeOsf8gcO7fdo3vn1zGSYi04gA5mE+J1gHCSJTmk+R+hDPg6XOgHLXD+S2PrKi/74qTGPKw==", + "version": "2026.5.19", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.5.19.tgz", + "integrity": "sha512-5Pn5hcRDVv3eeWYDp4IPPuvyz3yD+Vrdobykl/vi35/49ZldWUz02pDT5rTGMCInDelNZGaWoXAfm5vFdqha8g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6007,6 +5968,35 @@ "sqlite-vec": "0.1.9" } }, + "node_modules/openclaw/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/openclaw/node_modules/file-type": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.1.tgz", + "integrity": "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/openclaw/node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -7302,6 +7292,16 @@ "node": ">=8" } }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", diff --git a/package.json b/package.json index 291a412..7206bc2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,16 @@ "url": "https://github.com/apify/apify-openclaw-plugin/issues" }, "homepage": "https://github.com/apify/apify-openclaw-plugin#readme", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "clean": "rm -rf dist", + "build": "npm run clean && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "prepack": "npm run build", + "bump:openclaw": "node scripts/bump-openclaw.mjs" + }, "keywords": [ "openclaw", "apify", @@ -30,6 +40,7 @@ "facebook" ], "files": [ + "dist/", "src/", "scripts/", "openclaw.plugin.json", @@ -49,7 +60,7 @@ } }, "devDependencies": { - "openclaw": "^2026.5.18", + "openclaw": "^2026.5.19", "typescript": "^5.7.0", "vitest": "^3.0.0" }, @@ -57,11 +68,11 @@ "id": "apify-openclaw-plugin", "compat": { "pluginApi": ">=1.0.0", - "builtWithOpenClawVersion": "2026.5.18", - "pluginSdkVersion": "2026.5.18" + "builtWithOpenClawVersion": "2026.5.19", + "pluginSdkVersion": "2026.5.19" }, "extensions": [ - "./src/index.ts" + "./dist/index.js" ] } } diff --git a/scripts/bump-openclaw.mjs b/scripts/bump-openclaw.mjs new file mode 100644 index 0000000..9e46d75 --- /dev/null +++ b/scripts/bump-openclaw.mjs @@ -0,0 +1,236 @@ +#!/usr/bin/env node +// Bump openclaw to the latest stable version on npm, gated on the same smoke checks CI runs. +// +// Flow: +// 1. Resolve latest stable openclaw version (skip pre-releases, semver-sort). +// 2. If devDependencies.openclaw is already at latest, exit 0. +// 3. Pack the plugin (npm pack) and install it against openclaw@latest in a temp workdir. +// 4. Run `openclaw plugins list` + `plugins inspect` smoke checks (mirrors openclaw_version_tests.yml). +// 5. Run local `npm run typecheck` and `npm run test`. +// 6. Only if all of the above pass, update devDependencies.openclaw and the two +// openclaw.compat.* fields in package.json (via `npm install --save-dev` + `npm pkg set`). +// +// On any failure no package.json edits happen. The temp workdir and local .tgz tarball +// are always cleaned up. The user reviews `git diff` and commits manually — this script +// does not stage or commit anything. + +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const PLUGIN_ID = "apify-openclaw-plugin"; +const TOOL_NAME = "apify"; + +function run(cmd, args, opts = {}) { + const result = spawnSync(cmd, args, { + stdio: "inherit", + shell: false, + ...opts, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + const where = opts.cwd ? ` (cwd: ${opts.cwd})` : ""; + throw new Error(`Command failed${where}: ${cmd} ${args.join(" ")}`); + } + return result; +} + +function capture(cmd, args, opts = {}) { + const result = spawnSync(cmd, args, { + stdio: ["ignore", "pipe", "inherit"], + encoding: "utf8", + shell: false, + ...opts, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + const where = opts.cwd ? ` (cwd: ${opts.cwd})` : ""; + throw new Error(`Command failed${where}: ${cmd} ${args.join(" ")}`); + } + return result.stdout; +} + +function compareSemver(a, b) { + const pa = a.split(".").map(Number); + const pb = b.split(".").map(Number); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const da = pa[i] ?? 0; + const db = pb[i] ?? 0; + if (da !== db) return da - db; + } + return 0; +} + +function latestStableOpenclaw() { + const json = capture("npm", ["view", "openclaw", "versions", "--json"]); + const all = JSON.parse(json); + const stable = all.filter((v) => !v.includes("-")); + if (stable.length === 0) throw new Error("No stable openclaw versions on npm."); + stable.sort(compareSemver); + return stable[stable.length - 1]; +} + +function currentDevVersion() { + const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")); + const raw = pkg.devDependencies?.openclaw; + if (!raw) throw new Error("devDependencies.openclaw is missing from package.json"); + return raw.replace(/^[\^~>=<\s]+/, "").trim(); +} + +function smokeTest(latest) { + console.log(`\n=== Packing plugin ===`); + run("npm", ["install"], { cwd: REPO_ROOT }); + const packOut = capture("npm", ["pack"], { cwd: REPO_ROOT }); + const tarballName = packOut.trim().split("\n").pop(); + const tarballPath = path.join(REPO_ROOT, tarballName); + console.log(`Packed: ${tarballPath}`); + + // Snapshot user's global OpenClaw state so the smoke test leaves no trace. + // `openclaw plugins install` writes to ~/.openclaw/extensions// and updates + // ~/.openclaw/openclaw.json. We snapshot both and restore in `finally`. + const extDir = path.join(os.homedir(), ".openclaw", "extensions", PLUGIN_ID); + const configFile = path.join(os.homedir(), ".openclaw", "openclaw.json"); + const snapshot = { extBackup: null, configContents: null }; + + if (fs.existsSync(extDir)) { + snapshot.extBackup = `${extDir}.bump-backup-${Date.now()}`; + fs.renameSync(extDir, snapshot.extBackup); + console.log(`Snapshotted existing extension dir -> ${snapshot.extBackup}`); + } + if (fs.existsSync(configFile)) { + snapshot.configContents = fs.readFileSync(configFile, "utf8"); + console.log(`Snapshotted ~/.openclaw/openclaw.json (will restore on exit)`); + } + + const workdir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-bump-")); + console.log(`\n=== Smoke-testing in ${workdir} ===`); + + try { + run("npm", ["init", "-y"], { cwd: workdir }); + run("npm", ["install", `openclaw@${latest}`], { cwd: workdir }); + run("npx", ["--no-install", "openclaw", "plugins", "install", tarballPath], { cwd: workdir }); + + const listOut = capture("npx", ["--no-install", "openclaw", "plugins", "list"], { cwd: workdir }); + process.stdout.write(listOut); + if (!listOut.includes(PLUGIN_ID)) { + throw new Error(`FAIL: '${PLUGIN_ID}' not present in 'plugins list' output`); + } + + const inspectOut = capture( + "npx", + ["--no-install", "openclaw", "plugins", "inspect", PLUGIN_ID, "--runtime", "--json"], + { cwd: workdir }, + ); + process.stdout.write(inspectOut); + // openclaw --json sometimes wraps the payload with clack TUI chrome (e.g. "│\nā—‡ \n{...}"). + // Slice from the first `{` to the last `}` before parsing. clack chrome chars don't include braces. + const firstBrace = inspectOut.indexOf("{"); + const lastBrace = inspectOut.lastIndexOf("}"); + if (firstBrace === -1 || lastBrace === -1 || lastBrace < firstBrace) { + throw new Error("FAIL: no JSON object found in plugins inspect output"); + } + let inspectJson; + try { + inspectJson = JSON.parse(inspectOut.slice(firstBrace, lastBrace + 1)); + } catch (err) { + throw new Error(`FAIL: plugins inspect did not return valid JSON: ${err.message}`); + } + if (!containsStringValue(inspectJson, TOOL_NAME)) { + throw new Error(`FAIL: '${TOOL_NAME}' tool not found in plugin runtime inspect output`); + } + + console.log(`OK: plugin loaded and ${TOOL_NAME} tool is registered on openclaw ${latest}`); + } finally { + try { + fs.rmSync(workdir, { recursive: true, force: true }); + } catch (err) { + console.warn(`Warning: failed to remove ${workdir}: ${err.message}`); + } + try { + fs.rmSync(tarballPath, { force: true }); + } catch (err) { + console.warn(`Warning: failed to remove ${tarballPath}: ${err.message}`); + } + + // Restore user's global OpenClaw state to what it was before the smoke test. + try { + fs.rmSync(extDir, { recursive: true, force: true }); + } catch (err) { + console.warn(`Warning: failed to remove ${extDir}: ${err.message}`); + } + if (snapshot.extBackup && fs.existsSync(snapshot.extBackup)) { + try { + fs.renameSync(snapshot.extBackup, extDir); + console.log(`Restored ${extDir} from snapshot.`); + } catch (err) { + console.warn(`Warning: failed to restore ${extDir} from ${snapshot.extBackup}: ${err.message}`); + } + } + if (snapshot.configContents !== null) { + try { + fs.writeFileSync(configFile, snapshot.configContents); + console.log(`Restored ${configFile} from snapshot.`); + } catch (err) { + console.warn(`Warning: failed to restore ${configFile}: ${err.message}`); + } + } + } +} + +function containsStringValue(node, target) { + if (typeof node === "string") return node === target; + if (Array.isArray(node)) return node.some((n) => containsStringValue(n, target)); + if (node && typeof node === "object") { + for (const v of Object.values(node)) { + if (containsStringValue(v, target)) return true; + } + } + return false; +} + +function applyBump(latest) { + console.log(`\n=== Applying bump to package.json ===`); + run("npm", ["install", "--save-dev", `openclaw@${latest}`], { cwd: REPO_ROOT }); + run( + "npm", + [ + "pkg", + "set", + `openclaw.compat.builtWithOpenClawVersion=${latest}`, + `openclaw.compat.pluginSdkVersion=${latest}`, + ], + { cwd: REPO_ROOT }, + ); +} + +function main() { + const latest = latestStableOpenclaw(); + const current = currentDevVersion(); + console.log(`Current devDependencies.openclaw: ${current}`); + console.log(`Latest stable on npm: ${latest}`); + + if (compareSemver(current, latest) >= 0) { + console.log(`Already on openclaw@${current}. Nothing to do.`); + return; + } + + smokeTest(latest); + + console.log(`\n=== Local checks ===`); + run("npm", ["run", "typecheck"], { cwd: REPO_ROOT }); + run("npm", ["run", "test"], { cwd: REPO_ROOT }); + + applyBump(latest); + + console.log(`\nBumped openclaw ${current} -> ${latest} — review and commit.`); +} + +try { + main(); +} catch (err) { + console.error(`\n${err.message}`); + process.exit(1); +} diff --git a/src/cli.ts b/src/cli.ts index 453debd..ae85d86 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -88,48 +88,49 @@ async function applyConfigChanges( selectedTools: string[], allSelected: boolean, ): Promise { - if (!api.runtime?.config?.loadConfig || !api.runtime?.config?.writeConfigFile) { + if (!api.runtime?.config?.mutateConfigFile) { throw new Error("Config write API not available — update OpenClaw and retry."); } - const cfg = api.runtime.config.loadConfig(); - - // Merge plugin entry - if (!cfg.plugins) cfg.plugins = {}; - if (!cfg.plugins.entries) cfg.plugins.entries = {}; - const existing = cfg.plugins.entries["apify-openclaw-plugin"] ?? {}; - const existingPluginConfig = - typeof existing.config === "object" && existing.config !== null - ? (existing.config as Record) - : {}; - cfg.plugins.entries["apify-openclaw-plugin"] = { - ...existing, - enabled: true, - config: { - ...existingPluginConfig, - apiKey, - maxResults: existingPluginConfig.maxResults ?? 20, - }, - }; - - // Pin trust: add plugin id to plugins.allow so OpenClaw doesn't warn about - // discovered non-bundled plugins auto-loading. - if (!Array.isArray(cfg.plugins.allow)) cfg.plugins.allow = []; - if (!cfg.plugins.allow.includes("apify-openclaw-plugin")) { - cfg.plugins.allow.push("apify-openclaw-plugin"); - } - - // Merge tools.alsoAllow (add selected tools, avoid duplicates) - if (!cfg.tools) cfg.tools = {}; - if (!cfg.tools.alsoAllow) cfg.tools.alsoAllow = []; - const toolsToAdd = allSelected ? ["group:plugins"] : selectedTools; - for (const t of toolsToAdd) { - if (!cfg.tools.alsoAllow.includes(t)) { - cfg.tools.alsoAllow.push(t); - } - } + await api.runtime.config.mutateConfigFile({ + afterWrite: { mode: "restart", reason: "Apply Apify plugin config" }, + mutate: (cfg) => { + // Merge plugin entry + if (!cfg.plugins) cfg.plugins = {}; + if (!cfg.plugins.entries) cfg.plugins.entries = {}; + const existing = cfg.plugins.entries["apify-openclaw-plugin"] ?? {}; + const existingPluginConfig = + typeof existing.config === "object" && existing.config !== null + ? (existing.config as Record) + : {}; + cfg.plugins.entries["apify-openclaw-plugin"] = { + ...existing, + enabled: true, + config: { + ...existingPluginConfig, + apiKey, + maxResults: existingPluginConfig.maxResults ?? 20, + }, + }; + + // Pin trust: add plugin id to plugins.allow so OpenClaw doesn't warn about + // discovered non-bundled plugins auto-loading. + if (!Array.isArray(cfg.plugins.allow)) cfg.plugins.allow = []; + if (!cfg.plugins.allow.includes("apify-openclaw-plugin")) { + cfg.plugins.allow.push("apify-openclaw-plugin"); + } - await api.runtime.config.writeConfigFile(cfg); + // Merge tools.alsoAllow (add selected tools, avoid duplicates) + if (!cfg.tools) cfg.tools = {}; + if (!cfg.tools.alsoAllow) cfg.tools.alsoAllow = []; + const toolsToAdd = allSelected ? ["group:plugins"] : selectedTools; + for (const t of toolsToAdd) { + if (!cfg.tools.alsoAllow.includes(t)) { + cfg.tools.alsoAllow.push(t); + } + } + }, + }); } function printManualConfig(apiKey: string, selectedTools: string[], allSelected: boolean): void { diff --git a/src/index.ts b/src/index.ts index 2284120..1c45f89 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ export default { id: "apify-openclaw-plugin", name: "Apify", description: - "Web scraping and data extraction via Apify — scrape any platform using 57+ actors across social media, maps, search, e-commerce, and more.", + "Web scraping and data extraction via Apify — scrape any platform using 20k+ actors across social media, maps, search, e-commerce, and more.", register(api: OpenClawPluginApi) { const cfg = { pluginConfig: api.pluginConfig }; const tool = createApifyScraperTool(cfg); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..da6d5b1 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "allowImportingTsExtensions": false + }, + "include": ["src/**/*"], + "exclude": ["test/**/*", "dist/**/*", "node_modules"] +}