* feat(download): harden HTML→Markdown pipeline
Inspired by the MD-This-Page / markdown-viewer-extension analysis, tighten
the shared article→Markdown converter used by zhihu/weixin/web adapters:
- enable turndown-plugin-gfm (tables, strikethrough, task lists)
- strip script/style/noscript/iframe/canvas/form/button/dialog unconditionally
- strip SVG via a dedicated rule (not in HTMLElementTagNameMap)
- drop base64 data-URI images so they don't bloat .md output
- post-process: collapse NBSP, lone bullet/middle-dot residue,
trailing whitespace, and 3+ blank lines
- frontmatter shape guarantees ≤2 consecutive newlines even when
some metadata fields are absent
Adds a minimal local .d.ts for turndown-plugin-gfm and 6 new tests
covering GFM conversion, tag stripping, base64 drop, and whitespace cleanup.
* fix(download): emit canonical markdown strikethrough
* feat(download,browser): finish article pipeline polish
Per the follow-up from the MD-This-Page / markdown-viewer-extension
analysis, land the remaining items in the same PR instead of splitting:
article-download.ts
- extend STRIPPED_TAGS with header/footer/nav/aside (page chrome; the
article's title/author/publishTime are supplied as separate fields on
ArticleData, so duplicated DOM is redundant)
- new option ArticleDownloadOptions.cleanSelectors — per-adapter CSS
selector list removed before conversion, applied as a Turndown rule
via node.matches so invalid selectors fail silently
browser/article-extract.ts (new)
- generic Readability-based extraction that runs in-page via CDP
evaluate (no jsdom in Node)
- short-circuits non-HTML documents (text/plain, JSON, XML) and the
single-<pre> "browser rendering a plain text file" case
- clones the document before any mutation (preserves live page state
for subsequent snapshot / click)
- isProbablyReaderable gate, Readability.parse on the clone, then a
fallback chain main → [role="main"] → #main-content → … → body
- library sources are JSON-embedded and eval'd inside a Function scope
so their backticks / module.exports guards don't collide with the
surrounding IIFE
Tests
- article-download: page-chrome strip, cleanSelectors match + invalid
selector silently ignored (2 new)
- article-extract: JS generation contents, default fallback chain,
response normalization, null / malformed handling, and a Function()
parse check to catch any template-literal break-out in the embedded
Readability sources (8 new)
* fix(download): honor selector cleanup in fallback paths
* test(e2e): real-site regression for hardened article pipeline
Adds tests/e2e/article-download-pipeline.test.ts driving `opencli web read`
through 6 representative pages (example.com baseline, Wikipedia GFM tables,
MDN metadata, GitHub fenced code, Vercel SSR blog, Ruan Yifeng CJK+images)
and asserting the post-processing invariants: no base64/script/style leaks,
no blank-line runs, no residue, no trailing whitespace, no NBSP.
Graceful skip on bot detection / transient CDP errors, with a single retry.
All 6 sites pass locally (37s total).
* test(browser): add article extraction e2e fixtures
* fix: include adapter tests in default npm test
`npm test` only ran unit + extension projects, so adapter tests
(clis/**/*.test.js) were never exercised by the default test command.
Add --project adapter so they run alongside unit and extension tests.
* test: include adapter project in default npm test
* fix: project hygiene — docs, lint, daemon restart, code fence
- Update Node version requirement from >= 20 to >= 21 in 7 doc files
(README, README.zh-CN, installation guides, troubleshooting)
- Update adapter count from 79+ to 87+ in READMEs
- Remove duplicate `lint` script (identical to `typecheck`)
- Fix TESTING.md CI matrix: Node ['22'] instead of ['20', '22']
- Fix autofix SKILL.md code fence escaping (\``` → ~~~)
- Add daemon restart to postinstall so updated adapters are picked up
- Fix preuninstall to respect OPENCLI_DAEMON_PORT env var
* fix: align docs and skills with JS-first adapter contract
Adapters are now .js files (not .ts). Update all references across:
- README.md, README.zh-CN.md, CONTRIBUTING.md
- docs/guide/getting-started.md, docs/index.md
- skills/opencli-browser/SKILL.md, skills/opencli-explorer/SKILL.md
The runtime (discovery.ts) only loads .js from user clis/ directories,
and `opencli browser init` generates .js scaffolds. Documentation was
still teaching users to create .ts files.
* fix: update CI matrix to Node 22 only (drop Node 20)
package.json requires Node >= 21 (styleText dependency). The CI matrix
was still testing Node 20 which doesn't meet this requirement.
* fix: revert incorrect daemon restart from postinstall
The daemon (browser bridge) only handles CDP communication — it has no
knowledge of adapters. Adapter discovery, loading, and execution all
happen in the CLI process, which is fresh each invocation. The
_loadedModules cache in execution.ts is process-local and not a real
staleness concern. Remove the unnecessary restartDaemon() call.
- Remove mapDistToSource() from diagnostic.ts — mapped dist/clis/
paths back to clis/ but dist/clis/ no longer exists after JS-first
migration. The function always returned null.
- Simplify resolveAdapterSourcePath() to check candidates directly
without the dead dist→source mapping detour.
- Delete scripts/clean-yaml.cjs — walked dist/clis/ to delete YAML
files, but dist/clis/ no longer exists.
- Remove clean-yaml script entry from package.json.
* refactor(adapters): convert adapter layer from TypeScript to JavaScript
Core framework stays TypeScript; adapter layer moves to JS-first.
Adapters are essentially "executable config + browser scripts" that
barely use TS features — this simplifies the build/distribution pipeline
by removing the dist/clis/ intermediate compilation step.
Changes:
- Convert all 753 adapter files in clis/ from .ts to .js
- Update tsconfig to exclude clis/ from compilation
- Simplify build-manifest to scan clis/*.js directly (no dist/clis/)
- Update discovery, main, fetch-adapters to load JS adapters from clis/
- Update generate-verified to output .js artifacts
- Update package.json files field: dist/clis/ → clis/
- Fix all test files for the .ts → .js transition
* fix(main): use findPackageRoot for BUILTIN_CLIS path
The previous relative path (../../clis from __dirname) only worked for
dist/src/main.js but broke dev mode (tsx src/main.ts) where __dirname
is <repo>/src — resolving to /clis instead of <repo>/clis.
Use findPackageRoot() which works for both dev and prod paths.
* refactor: make daemon persistent, remove idle timeout
- Remove IdleManager and 4-hour idle auto-exit
- Daemon now stays alive until explicit shutdown or uninstall
- Add preuninstall hook for best-effort daemon cleanup on npm uninstall
- Update docs to reflect persistent daemon model
* fix: remove stale idle timeout references from code and docs
* refactor: remove daemon status/restart commands and lastCliRequestTime
- Remove `daemon status` and `daemon restart` CLI commands (doctor covers diagnostics)
- Remove `lastCliRequestTime` tracking (no longer needed without idle timeout)
- Keep only `daemon stop` as the explicit shutdown command
* Add AbortSignal.timeout(3s) to preuninstall shutdown fetch
Prevents npm uninstall from hanging if the daemon port accepts
connections but never responds.
Use Chrome CDP targetId (UUID) as the canonical page identity across
all layers (extension → daemon → CLI), demoting tabId to an
extension-internal routing detail.
- Add extension/src/identity.ts: bidirectional targetId ↔ tabId mapping
with lazy refresh via chrome.debugger.getTargets()
- Update protocol: Command.page and Result.page carry targetId
- Update background.ts: resolveCommandTabId() and pageScopedResult()
helpers; all page-scoped handlers return targetId
- Add sendCommandFull() to daemon-client for responses with page identity
- Update Page class: _page stores targetId, goto/selectTab extract it
- Update record.ts: injectedPages tracks by targetId
- Add extension tests to vitest config and CI test scripts
* refactor: migrate adapter imports to package exports
Replace all relative imports (../../src/registry.js, ../../browser/cdp.js, etc.)
with package exports (@jackwener/opencli/registry, @jackwener/opencli/errors, etc.)
across all 484 adapter files.
This decouples adapter import resolution from directory structure:
- User CLIs in ~/.opencli/clis/ resolve via node_modules symlink
- Internal adapters resolve via Node.js self-referencing
- No more shim files needed for import resolution
Changes:
- package.json: add sub-path exports for all public modules
- clis/**: replace relative imports with @jackwener/opencli/...
- discovery.ts: simplify ensureUserCliCompatShims to symlink-only
- registry-api.ts: export CommandArgs type
- Remove root-level shim directories (browser/, download/, pipeline/)
- Remove shim entries from tsconfig.json include and package.json files
* test: add regression tests for package exports
Prevents regressions like #788/#791 by:
1. Scanning all adapter files for forbidden relative imports
(../../src/, ../../browser/, etc.) — fails if any remain
2. Verifying every package.json export maps to an existing source file
18 new test cases.
* fix: use junction on Windows + broaden test patterns
- discovery.ts: use 'junction' symlink type on Windows (no admin required)
- package-exports.test.ts: generalize forbidden patterns to catch any
depth of ../ traversal (not just ../../ and ../../../)
* fix: update stale vi.mock/importActual paths in adapter tests
Test files still used old relative paths for vi.mock() and
vi.importActual() calls. Updated 5 test files to use package exports.
Also broadened regression test patterns to catch mock/importActual paths.
* fix: use rm instead of unlink for symlink cleanup, add warn on failure
Addresses review feedback from Astro-Han:
- rm() handles both symlinks and stale directories (unlink fails on dirs)
- Log a warning when symlink creation fails instead of silent catch
* docs: update import examples to use package exports
Update all documentation, contributing guides, and skills to use
@jackwener/opencli/registry instead of ../../src/registry.js.
Without this, users following the docs would write adapters with
broken imports since the old shim files are no longer created.
* refactor: move adapters from src/clis/ to root clis/ for monorepo separation
Separates CLI adapters from the core runtime to prepare for independent
adapter distribution via postinstall fetch.
Key changes:
- Move src/clis/ → clis/ (adapters at repo root)
- Change tsconfig rootDir from "src" to "." so tsc compiles both
- Create root-level shim files (registry.ts, errors.ts, etc.) so adapter
relative imports (../../registry.js) resolve correctly
- Update build-manifest.ts, main.ts paths for new dist/src/ structure
- Expand ensureUserCliCompatShims() to cover all adapter import targets
(types, utils, logger, launcher, browser/*, download/*, pipeline/*)
- Add scripts/fetch-adapters.js postinstall for ~/.opencli/clis/ sync
- Update vitest.config.ts adapter test paths
- Add package.json files field to exclude adapters from npm package
Official adapter files are unconditionally overwritten on update;
user-created files not in the manifest are preserved.
* fix: add dist/clis/ and cli-manifest.json to npm files, harden fetch-adapters
- Add dist/clis/ and dist/cli-manifest.json to package.json files field
so built-in adapters and manifest ship with the npm package
- Replace execSync with execFileSync to prevent command injection
- Add version check to skip redundant adapter fetches
- Track tmpRoot explicitly for reliable cleanup
* fix: address review blockers — manifest-based updates, global-only fetch, first-run fallback
1. Manifest-based update strategy:
- Read old manifest to identify previously-official files
- Clean up files removed upstream (in old manifest but not new)
- User-created files (never in any manifest) remain untouched
2. Only run fetch-adapters on global install (npm_config_global=true)
or explicit OPENCLI_FETCH=1, preventing heavy side effects for
local/dev installs
3. First-run fallback in discovery.ts:
- ensureUserAdapters() checks for adapter-manifest.json
- If missing and ~/.opencli/clis/ is empty, spawns fetch-adapters.js
- Guarantees adapters are available even with --ignore-scripts
* fix: remove OPENCLI_FETCH env var, use internal _OPENCLI_FIRST_RUN instead
* feat: also support OPENCLI_FETCH=1 for explicit adapter fetch trigger
* simplify: replace git clone with local copy from dist/clis/
Adapters already ship in the npm package (dist/clis/), so there's no
need to clone from GitHub. Copy directly from the installed package:
- Eliminates git, curl, tar dependencies
- No network calls in postinstall
- No timeout/offline issues
- Version always matches the installed CLI
- ~65 lines of clone/download code replaced by one cpSync loop
* fix: add prepare script so source installs trigger build
npm install from git (e.g. npm install github:jackwener/opencli) skips
prepublishOnly, so dist/ is never generated. The prepare hook runs on
git-based installs; the [ -d src ] guard skips it for registry installs.
* fix: include extension/dist in git so clone works out of the box
.gitignore had conflicting rules: line 3 tried to un-ignore extension/dist/
but line 26 re-ignored it. Remove the later rule so the built extension JS
is tracked in git — users can load the extension directly after clone.
* feat(runtime): add runtime detection utility for Bun/Node.js
Add runtime-detect.ts module that detects whether opencli is running
under Bun or Node.js via globalThis.Bun check. Includes helper
functions for version string and label formatting.
Add corresponding unit tests that work correctly under both runtimes.
* feat(runtime): integrate Bun runtime support into CLI tooling
- doctor: show runtime label (e.g. 'node v22.13.0') in diagnostic output
- package.json: add dev:bun, start:bun, test:bun convenience scripts
- E2E helpers: support OPENCLI_TEST_RUNTIME env var for runtime selection
* ci: add Bun compatibility test job and document runtime support
- ci.yml: add bun-test job using oven-sh/setup-bun@v2
- README.md: update Prerequisites to mention Bun, add Runtime Support
section with usage examples for dev:bun, start:bun, test:bun
* ci: pin Bun version in compatibility job
---------
Co-authored-by: jackwener <jakevingoo@gmail.com>