Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1c2c4c7
feat(opencli): add adapter discovery search
Astro-Han Jun 11, 2026
223648c
feat(opencli): run adapter commands
Astro-Han Jun 11, 2026
fff3e44
feat(opencli): guard adapter page capabilities
Astro-Han Jun 11, 2026
5892e41
build(opencli): package adapter runtime assets
Astro-Han Jun 11, 2026
5165bee
fix(opencli): keep desktop adapter runtime resolvable
Astro-Han Jun 11, 2026
f24e6fd
fix(opencli): align adapter tools with browser permission
Astro-Han Jun 11, 2026
2248cce
fix(opencli): copy adapter runtime into desktop build
Astro-Han Jun 11, 2026
2b854ae
ci(opencli): cover adapter tests on windows
Astro-Han Jun 11, 2026
a9eb255
build(opencli): prune non-runtime package files
Astro-Han Jun 11, 2026
fc09560
fix(opencli): reject blank numeric adapter args
Astro-Han Jun 11, 2026
bff3c3c
fix(opencli): index implicit browser commands
Astro-Han Jun 11, 2026
d4de5ae
fix(opencli): widen adapter browser permissions
Astro-Han Jun 11, 2026
85d2313
fix(opencli): describe adapter args in search
Astro-Han Jun 11, 2026
4d0a4fa
refactor(opencli): expose adapter namespaces
Astro-Han Jun 11, 2026
ccbab97
refactor(opencli): locate adapter package by manifest
Astro-Han Jun 11, 2026
2c44257
test(opencli): document accepted closeWindow gap
Astro-Han Jun 11, 2026
04591b4
test(opencli): use effect tool harness
Astro-Han Jun 11, 2026
5646222
fix(opencli): gate adapter side effects
Astro-Han Jun 11, 2026
f28128e
fix(opencli): tolerate failed adapter imports
Astro-Han Jun 11, 2026
2a2a673
fix(opencli): avoid permissive null permission fallback
Astro-Han Jun 11, 2026
756b6b6
fix(opencli): tighten adapter permission helper type
Astro-Han Jun 11, 2026
6102e71
test(opencli): normalize adapter import URL expectations
Astro-Han Jun 11, 2026
d54b3b6
fix(opencli): tighten adapter write guardrails
Astro-Han Jun 12, 2026
c8a3204
fix(opencli): surface adapter load failures
Astro-Han Jun 12, 2026
4e211eb
refactor(opencli): reuse browser action runner
Astro-Han Jun 12, 2026
44adda9
fix(opencli): gate adapter imports behind permissions
Astro-Han Jun 12, 2026
b8a16c3
refactor(desktop): share opencli runtime filters
Astro-Han Jun 12, 2026
9d87079
fix(opencli): format empty adapter output
Astro-Han Jun 12, 2026
9a2cc6d
fix(opencli): match persistent pre-nav origin
Astro-Han Jun 12, 2026
f69e743
fix(opencli): ask with final command args
Astro-Han Jun 12, 2026
b44a159
fix(opencli): clarify write adapter cancellation
Astro-Han Jun 12, 2026
8d575c8
test(opencli): type command summary fixtures
Astro-Han Jun 12, 2026
8d48226
docs: credit bundled OpenCLI runtime
Astro-Han Jun 12, 2026
de743a0
refactor(opencli): ask once for run permission
Astro-Han Jun 12, 2026
3c839ad
refactor(opencli): keep search manifest-only
Astro-Han Jun 12, 2026
19cb08f
refactor(opencli): remove adapter load diagnostics
Astro-Han Jun 12, 2026
9b87953
test(opencli): remove search warning legacy cast
Astro-Han Jun 12, 2026
ba93036
refactor(opencli): reuse command summary metadata
Astro-Han Jun 12, 2026
caba1b4
fix(opencli): tighten browser adapter boundaries
Astro-Han Jun 12, 2026
c5351a5
test(opencli): pin global registry contract
Astro-Han Jun 12, 2026
0c8edf1
fix(opencli): enforce adapter execution boundaries
Astro-Han Jun 12, 2026
8e8472c
docs(opencli): narrow adapter coverage claims
Astro-Han Jun 12, 2026
8ad3599
fix(opencli): recheck browser adapter landings
Astro-Han Jun 12, 2026
222f223
refactor(opencli): drop unreachable write interruption branch
Astro-Han Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/windows-advisory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ jobs:
test/worktree
test/file/
test/github
test/opencli
test/settings
test/settings.test.ts
report_path: packages/opencode/.artifacts/unit/junit-windows-config-project.xml
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ PawWork is built for people who want AI agents to do real desktop work, not only
| Free without subscription | Yes (OpenCode Zen) | Limited (ChatGPT Free) | No (Pro $20/mo required) |
| Desktop app | macOS + Windows | macOS + Windows | macOS + Windows |
| Local file access | Full workspace access | Sandboxed by default | User-selected folders |
| Browser automation | Yes (supported OpenCLI adapters; coverage varies) | No | No |
| Local models | Yes (Ollama, LM Studio, etc.) | CLI only (Ollama) | Via gateway (Requesty) |
| Office files (Word/Excel/PPT) | Yes (via OfficeCLI) | No | No |
| Non-technical user focus | Yes (task cards, no terminal) | Developer-focused | Knowledge work + coding |
Expand Down Expand Up @@ -61,6 +62,13 @@ PawWork is built for people who want AI agents to do real desktop work, not only
- debug an API error with logs and source files
- build a small internal tool from a plain-language request

### Browser and Web Services

- search Twitter/X and draft a reply without leaving PawWork
- check trending topics on Bilibili or Xiaohongshu
- collect posts, comments, or media from supported web services
- interact with supported web services through PawWork's browser tools

## How It Works

1. Choose a workspace folder.
Expand Down Expand Up @@ -102,6 +110,8 @@ Thanks to the OpenCode project and community.

PawWork bundles [OfficeCLI](https://github.com/iOfficeAI/OfficeCLI) by iOfficeAI to handle Word, Excel, and PowerPoint files locally. Thanks to iOfficeAI for the Apache-2.0 open-source OfficeCLI project.

PawWork bundles [OpenCLI](https://github.com/jackwener/opencli) by [@jackwener](https://github.com/jackwener) to power browser-based adapters for supported sites. OpenCLI provides the CDP bridge and bundled command adapters that help PawWork interact with web services like Twitter/X, Bilibili, Xiaohongshu, and more where the adapter coverage fits the task. Thanks to @jackwener for the Apache-2.0 open-source OpenCLI project.

## FAQ

**Is PawWork free?**
Expand Down
10 changes: 10 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Codex App 和 Claude Cowork 的开源替代方案。自带免费额度,也支
| 免费无需订阅 | 有(OpenCode Zen) | 有限(ChatGPT Free) | 无(需 Pro $20/月) |
| 桌面应用 | macOS + Windows | macOS + Windows | macOS + Windows |
| 本地文件访问 | 完整工作区 | 默认沙箱 | 用户选择的文件夹 |
| 浏览器自动化 | 支持(OpenCLI 支持站点适配器,覆盖范围因站点而异) | 不支持 | 不支持 |
| 本地模型 | 支持(Ollama、LM Studio 等) | 仅 CLI 支持(Ollama) | 通过网关(Requesty) |
| Office 文件处理 | 支持(Word/Excel/PPT) | 不支持 | 不支持 |
| 面向非技术用户 | 是(任务卡片,无需终端) | 面向开发者 | 知识工作 + 编程 |
Expand Down Expand Up @@ -63,6 +64,13 @@ Codex App 和 Claude Cowork 的开源替代方案。自带免费额度,也支
- 结合日志和源码排查 API 报错
- 根据一句自然语言需求做一个小工具

### 浏览器和网页服务

- 搜索 Twitter/X,并在爪印里起草回复
- 查看 Bilibili 或小红书的热门内容
- 收集受支持网页服务里的帖子、评论或媒体
- 通过爪印的浏览器工具操作受支持的网页服务

## 工作方式

1. 选择一个工作文件夹。
Expand Down Expand Up @@ -104,6 +112,8 @@ bun run dev:desktop

爪印 PawWork 内置 iOfficeAI 的 [OfficeCLI](https://github.com/iOfficeAI/OfficeCLI),用于在本地处理 Word、Excel 和 PowerPoint 文件。感谢 iOfficeAI 以 Apache-2.0 开源 OfficeCLI。

爪印 PawWork 内置 [@jackwener](https://github.com/jackwener) 的 [OpenCLI](https://github.com/jackwener/opencli),用于支持基于浏览器的站点适配器。OpenCLI 提供 CDP bridge 和随包命令适配器,在适配覆盖范围匹配任务时,帮助爪印与 Twitter/X、Bilibili、小红书等网页服务交互。感谢 @jackwener 以 Apache-2.0 开源 OpenCLI 项目。

## 常见问题

**爪印 PawWork 免费吗?**
Expand Down
13 changes: 12 additions & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,18 @@ PawWork also bundles OfficeCLI's `officecli-*` and `morph-ppt*` skill directorie
- Project: https://github.com/iOfficeAI/OfficeCLI
- License: Apache License 2.0

The Apache License 2.0 text for OfficeCLI follows.
## OpenCLI

PawWork bundles `@jackwener/opencli` by @jackwener to power browser-based adapters for supported sites.

PawWork packages the OpenCLI runtime files needed by the desktop app, including its CDP bridge, command registry, browser helpers, bundled site adapters under `clis/**`, and `cli-manifest.json`.

- Project: https://github.com/jackwener/opencli
- Package: `@jackwener/opencli`
- Version: 1.8.3
- License: Apache License 2.0

The Apache License 2.0 text for OfficeCLI and OpenCLI follows.

## Apache License 2.0

Expand Down
70 changes: 70 additions & 0 deletions packages/desktop-electron/electron-builder-app-update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
getPublishConfig,
nativeWatcherFileSets,
nativeWatcherPackageNames,
openCliRuntimeFileSets,
openCliRuntimePackageNames,
} from "./electron-builder.config"
import { serializeAppUpdateConfig } from "./scripts/write-app-update-config"

Expand Down Expand Up @@ -104,6 +106,20 @@ describe("electron builder app-update config", () => {
)
})

test("third-party notices include bundled OpenCLI attribution", () => {
const notices = readFileSync(join(import.meta.dir, "../..", "THIRD_PARTY_NOTICES.md"), "utf8")
const opencodePackage = JSON.parse(
readFileSync(join(import.meta.dir, "..", "opencode", "package.json"), "utf8"),
) as { dependencies: Record<string, string> }
const openCliVersion = opencodePackage.dependencies["@jackwener/opencli"]

expect(notices).toContain("## OpenCLI")
expect(notices).toContain("https://github.com/jackwener/opencli")
expect(notices).toContain("`@jackwener/opencli`")
expect(notices).toContain(`Version: ${openCliVersion}`)
expect(notices).toContain("Apache License 2.0")
})

test("native watcher package list covers desktop targets", () => {
expect(nativeWatcherPackageNames()).toEqual([
"@parcel/watcher-darwin-arm64",
Expand Down Expand Up @@ -136,6 +152,60 @@ describe("electron builder app-update config", () => {
)
})

test("packages OpenCLI adapters and runtime dependencies for the embedded server", () => {
const config = createConfig("prod")
const resources = openCliRuntimeFileSets()

expect(openCliRuntimePackageNames()).toEqual([
"@jackwener/opencli",
"@mixmark-io/domino",
"@mozilla/readability",
"ansi-regex",
"argparse",
"cli-table3",
"commander",
"emoji-regex",
"is-fullwidth-code-point",
"js-yaml",
"string-width",
"strip-ansi",
"turndown",
"turndown-plugin-gfm",
"undici",
"ws",
])
expect(config.extraResources).toEqual(
expect.arrayContaining(
resources.map((resource) =>
expect.objectContaining({
from: resource.from,
to: resource.to,
}),
),
),
)
expect(resources.map((resource) => resource.to)).toEqual(
openCliRuntimePackageNames().map((packageName) => join("node_modules", ...packageName.split("/"))),
)
})

test("packages only OpenCLI runtime files", () => {
const resources = openCliRuntimeFileSets()

for (const resource of resources) {
expect(resource.filter).toEqual(
expect.arrayContaining([
"!**/.yarn/**",
"!**/{test,tests,__tests__,coverage}/**",
"!**/*.{test,spec}.{js,mjs,cjs,ts,tsx}",
]),
)
}
expect(resources.find((resource) => resource.to.endsWith(join("@jackwener", "opencli")))?.filter).toContain(
"!clis/test-utils.js",
)
})

test("afterPack writes app-update.yml to the packager-reported macOS resources path", async () => {
const root = mkdtempSync(join(tmpdir(), "pawwork-builder-config-"))
roots.push(root)
Expand Down
4 changes: 4 additions & 0 deletions packages/desktop-electron/electron-builder.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { promisify } from "node:util"

import type { Configuration } from "electron-builder"
import { writeAppUpdateConfig, type GitHubPublishConfig } from "./scripts/write-app-update-config"
import { openCliRuntimeFileSets, openCliRuntimePackageNames } from "./opencli-runtime"

export { openCliRuntimeFileSets, openCliRuntimePackageNames } from "./opencli-runtime"

const execFileAsync = promisify(execFile)
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..")
Expand Down Expand Up @@ -118,6 +121,7 @@ const getBase = (channel: Channel): Configuration => ({
},
extraResources: [
...nativeWatcherFileSets(),
...openCliRuntimeFileSets(),
{
from: path.join(rootDir, "skills"),
to: "skills",
Expand Down
9 changes: 8 additions & 1 deletion packages/desktop-electron/electron-vite.config.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test"
import { realpathSync } from "node:fs"
import { readFileSync, realpathSync } from "node:fs"
import path from "node:path"
import { createRendererWorkspaceConfig } from "./renderer-workspace-config"

Expand All @@ -15,3 +15,10 @@ test("renderer dedupes the ui workspace package", () => {

expect(dedupe).toContain("@opencode-ai/ui")
})

test("main build does not externalize OpenCLI from the desktop bundle", () => {
const source = readFileSync(path.join(import.meta.dir, "electron.vite.config.ts"), "utf8")

expect(source).toContain("externalizeDeps: { include: [nodePtyPkg] }")
expect(source).not.toContain("OPENCLI_EXTERNALS")
})
33 changes: 33 additions & 0 deletions packages/desktop-electron/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
embeddedServerMissingArtifactsMessage,
} from "./src/main/embedded-server-contract"
import { createRendererWorkspaceConfig } from "./renderer-workspace-config"
import { includeOpenCliRuntimeDirectory, includeOpenCliRuntimeFile, openCliRuntimePackages } from "./opencli-runtime"

const channel = (() => {
const raw = process.env.OPENCODE_CHANNEL
Expand All @@ -29,6 +30,28 @@ if (missingArtifacts.length > 0) {
const nodePtyPkg = `@lydell/node-pty-${process.platform}-${process.arch}`
const rendererWorkspaceConfig = createRendererWorkspaceConfig(process.cwd(), realpathSync)

async function copyOpenCliRuntimePackage(pkg: ReturnType<typeof openCliRuntimePackages>[number], target: string) {
const stack = [{ source: pkg.dir, destination: target, relativePath: "" }]
while (stack.length > 0) {
const current = stack.pop()!
await fs.mkdir(current.destination, { recursive: true })
for (const entry of await fs.readdir(current.source, { withFileTypes: true })) {
const source = path.join(current.source, entry.name)
const destination = path.join(current.destination, entry.name)
const relativePath = current.relativePath ? path.join(current.relativePath, entry.name) : entry.name
if (entry.isDirectory()) {
if (includeOpenCliRuntimeDirectory(pkg.name, relativePath)) {
stack.push({ source, destination, relativePath })
}
continue
}
if (entry.isFile() && includeOpenCliRuntimeFile(pkg.name, relativePath)) {
await fs.copyFile(source, destination)
}
}
}
}

export default defineConfig({
main: {
define: {
Expand Down Expand Up @@ -66,6 +89,16 @@ export default defineConfig({
}
},
},
{
name: "opencode:copy-opencli-runtime",
async writeBundle() {
for (const pkg of openCliRuntimePackages()) {
const target = path.join("./out/main/chunks/node_modules", ...pkg.name.split("/"))
await fs.rm(target, { recursive: true, force: true })
await copyOpenCliRuntimePackage(pkg, target)
}
},
},
],
},
preload: {
Expand Down
Loading
Loading