From 87543d8eafa2524e6a5ede18212af101e4845248 Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 11:03:44 +0800 Subject: [PATCH 01/12] =?UTF-8?q?docs(agents):=20=E8=AE=B0=E5=BD=95=20CLI?= =?UTF-8?q?=20=E6=94=B9=E5=8A=A8=E7=9A=84=E4=B8=89=E7=B1=BB=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=AF=B9=E9=BD=90=E8=A7=84=E8=8C=83=EF=BC=88arch/guid?= =?UTF-8?q?e/skill=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index b8552b65..918cbda9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,9 +79,10 @@ npm --prefix apps/desktop run prepare:pragent # 对齐嵌入式 pr-agent 运 - **独立 Go module,不入 npm/Nx**:自带 `cli/go.mod`(纯 Go、无 CGO),非 workspace 成员、不进 Nx——根 `lint/typecheck/test/build` 不覆盖它,CLI 自成一套。 - **本地命令**(在 `cli/`):`go vet ./...` → `go test ./...` → `go build ./...`,改完 CLI 三步过了再收尾。`go.sum` 入库(锁校验和);构建产物(`bin/` / `meebox` 等)已 gitignore(见 `cli/.gitignore`)。 - **CI 分两条**:PR 门禁 [ci-cli.yml](.github/workflows/ci-cli.yml)(路径过滤 `cli/**`,跑 vet/test/build,与 Node 的 ci.yml 分开);发布产出在 [release.yml](.github/workflows/release.yml) 的 `cli` job(`v*` tag 触发,交叉编译 Windows / macOS / Linux×2,出压缩包挂同一 Release;Windows / macOS 用 `.zip`、Linux 用 `.tar.gz`)。版本经 `-ldflags -X …/cmd.version` 注入、与应用同 tag。 -- **压缩包即 skill 目录**:CLI 压缩包除二进制外一并打包 `LICENSE` + `cli/README.md` + `cli/SKILL.md`(frontmatter `name: meebox`)——解压投放到 agent 的 skills 目录即得可用 skill(面向 agent 交付的主形态)。改命令树 / 边界时同步更新 `SKILL.md` 与 `README.md`。 +- **压缩包即 skill 目录**:CLI 压缩包除二进制外一并打包 `LICENSE` + `cli/README.md` + `cli/SKILL.md`(frontmatter `name: meebox`)——解压投放到 agent 的 skills 目录即得可用 skill(面向 agent 交付的主形态)。 - **写边界**:CLI 做浏览 + **评审写动作**——approve / needswork(远端评审决断)与 comment(发评论),经服务端专用端点(复用 GUI 同源 controller)。仍**不开放**:merge(合并)与 pr-agent 变更类工具(publish 等,`instruct` 只读白名单 describe/review/ask/improve 在 CLI 与服务端双重把关)。新增命令先确认对应 API 端点已存在;放开新写端点须评估远端副作用。CLI 不得绕过 API 直连应用内部。 - **契约同步**:CLI 与服务端唯一耦合是 HTTP/JSON 线协议。当前手写 Go 结构对齐契约,契约增长后转 OpenAPI / Schema 代码生成。默认输出 YAML(人类向、保序)、`--output json` 供机器(亦保序);PR 列表返回精简投影、PR 标识对外为 `id`、PR 关联命令用 `--pr `。连接配置走 flag / 环境变量(`MEEBOX_API_URL` / `MEEBOX_TOKEN`)/ `~/.code-meeseeks/cli.yaml`,**不读 GUI 的 `config.yaml`**(避免越权触达连接层机密);代理遵循标准 `HTTP(S)_PROXY` / `NO_PROXY`。 +- **文档对齐**:CLI 改动(命令树 / 写边界 / 输出契约 / 连接配置)须同步三类文档,缺一即漂移——① **arch 设计**:[docs/arch/04-integration/02-cli.md](docs/arch/04-integration/02-cli.md)(命令树与边界),涉及 API 端点再改 [01-service-api.md](docs/arch/04-integration/01-service-api.md)(端点表);② **guide 用法**:[docs/guide/06-cli.md](docs/guide/06-cli.md);③ **skill 交付**:`cli/SKILL.md` + `cli/README.md`(随压缩包投放为 agent skill 的主形态)。新增 / 改命令务必三类齐更。 ## 约定 From 41661070470b4142859070f50c68e95bb3521d4c Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 11:26:46 +0800 Subject: [PATCH 02/12] =?UTF-8?q?feat(service):=20=E6=9C=AC=E5=9C=B0=20API?= =?UTF-8?q?=20=E6=96=B0=E5=A2=9E=20refresh=20=E4=B8=8E=20version=20?= =?UTF-8?q?=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - POST /api/v1/refresh:触发一次立即轮询刷新(复用 GUI 同源 poller.tick / prs:refresh), 返回本轮计数汇总;纯读远端 + 落本地,无远端写副作用。 - GET /api/v1/version:返回服务端(桌面应用)版本,供 CLI 同时展示客户端 + 服务端版本。 Co-Authored-By: Claude Opus 4.8 --- apps/desktop/src/main/services/api-server/routes.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/desktop/src/main/services/api-server/routes.ts b/apps/desktop/src/main/services/api-server/routes.ts index 854e760b..432415f5 100644 --- a/apps/desktop/src/main/services/api-server/routes.ts +++ b/apps/desktop/src/main/services/api-server/routes.ts @@ -10,6 +10,7 @@ import { } from '@meebox/shared'; import * as agentCtl from '../../controllers/agent.js'; import * as prCtl from '../../controllers/pr.js'; +import { buildAppInfo } from '../app.js'; import { getContext } from '../context.js'; import { HttpError } from './http.js'; import { toPrAgentRuns, toPrListItem } from './views.js'; @@ -96,6 +97,16 @@ const whoami: RouteHandler = () => { }; }; +/** + * 触发一次立即轮询刷新(等价 GUI 的手动刷新 / 窗口聚焦刷新):拉取所有连接的最新 PR、落本地, + * 返回本轮计数汇总(fetched / changed / added / removed / errors)。复用 GUI 同源 poller.tick + * (`prs:refresh`)。无远端写副作用(纯读远端 + 落本地),列为安全的开放动作。 + */ +const refresh: RouteHandler = () => prCtl.refreshPrs(NO_EVENT, undefined); + +/** 服务端(桌面应用)版本,供 CLI `system version` 同时展示客户端 + 服务端版本。 */ +const version: RouteHandler = () => ({ version: buildAppInfo(getContext().bootstrap).appVersion }); + /** * PR 列表:`category`(一级发现分类)+ `status`(二级状态 / 合并态)过滤 + `q` 检索 + * `skip`/`limit` 分页(默认 limit 100)。过滤语义复用 @meebox/shared 的纯谓词(与渲染层侧栏同源); @@ -206,6 +217,8 @@ const comment: RouteHandler = ({ params, body }) => { export const routes: Route[] = [ { method: 'GET', segments: seg('/api/v1/categories'), handler: categories }, { method: 'GET', segments: seg('/api/v1/whoami'), handler: whoami }, + { method: 'POST', segments: seg('/api/v1/refresh'), handler: refresh }, + { method: 'GET', segments: seg('/api/v1/version'), handler: version }, { method: 'GET', segments: seg('/api/v1/prs'), handler: listPrs }, { method: 'GET', segments: seg('/api/v1/prs/:id'), handler: showPr }, { method: 'GET', segments: seg('/api/v1/prs/:id/diff'), handler: diff }, From bf9584a2f2c70e3bcaec38ed301d458581384a66 Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 11:31:45 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat(cli):=20meebox=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20pr=20refresh=20=E4=B8=8E=20version=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E8=A7=84=E6=95=B4=E5=91=BD=E4=BB=A4=E6=A0=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pr refresh:触发一次立即刷新(POST /refresh),返回本轮变化计数;归入 pr 领域(刷新 PR 列表)。 - version:同时返回客户端(CLI)+ 服务端(应用)版本;服务端不可达时仅客户端、退出码仍 0 (根 --version flag 保留为快速客户端版本)。根层级系统性命令。 - categories 从顶层移入 pr 领域(它是 pr list 的筛选词表)。 - 系统性命令(whoami / version)保留在根层级,不套领域组。 - 同步更新 SKILL.md / README.md(skill 交付形态)与集成测试。 Co-Authored-By: Claude Opus 4.8 --- cli/README.md | 7 +++-- cli/SKILL.md | 10 +++++-- cli/cmd/categories.go | 3 +- cli/cmd/integration_test.go | 41 ++++++++++++++++++++++++-- cli/cmd/pr.go | 2 ++ cli/cmd/refresh.go | 18 ++++++++++++ cli/cmd/root.go | 2 +- cli/cmd/version.go | 57 +++++++++++++++++++++++++++++++++++++ 8 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 cli/cmd/refresh.go create mode 100644 cli/cmd/version.go diff --git a/cli/README.md b/cli/README.md index edfc7e3f..8adff98a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -48,12 +48,15 @@ defaults to `http://127.0.0.1:18765` when unset. ## Commands -Two domains, `pr` and `agent`, both PR-scoped via the required `--pr ` flag +Root-level `whoami` / `version` need no PR. Two domains — `pr` (also holds `categories` +and `refresh`) and `agent` — carry PR-scoped commands via the required `--pr ` flag (`id` comes from `pr list`): ```text meebox whoami -meebox categories +meebox version # CLI (client) + app (server) versions +meebox pr categories +meebox pr refresh # trigger one immediate poll for the latest PRs meebox pr list [--category ] [--status ] [--query ] [--skip N] [--limit N] meebox pr show --pr meebox pr diff --pr [--file ] [--side base|head] diff --git a/cli/SKILL.md b/cli/SKILL.md index 82aab248..0f049de1 100644 --- a/cli/SKILL.md +++ b/cli/SKILL.md @@ -30,9 +30,13 @@ meebox --output json pr list | jq '.[].id' # JSON for scripting ## Command map -Two domains, both PR-scoped via the **required `--pr `** flag (`id` comes from `pr list`): +Root-level `meebox whoami` and `meebox version` need no PR. The rest split into two domains — +`pr` and `agent`; PR-scoped subcommands take the **required `--pr `** flag (`id` from `pr list`), +while `pr categories` / `pr refresh` / `pr list` are collection-level (no `--pr`). **Browse / inspect — `pr`** +- `meebox pr categories` — the active platform's `categories` / `statuses` filter vocabulary for `pr list`. +- `meebox pr refresh` — trigger one immediate poll for the latest PRs (same as the app's manual refresh); returns change counts (fetched / changed / added / removed / errors). Run before `pr list` for fresh data. - `meebox pr list [--category review-requested|created|assigned|mentioned] [--status pending|approved|needs_work|conflict|mergeable] [--query ] [--skip N] [--limit N]` — paginated (default limit 100), slim fields (id / title / author / createdAt first). - `meebox pr show --pr ` — full detail incl. description. - `meebox pr diff --pr [--file --side base|head]` — changed files, or one file's content. @@ -50,7 +54,9 @@ Two domains, both PR-scoped via the **required `--pr `** flag (`id` comes fr - `meebox pr approve --pr ` · `meebox pr needswork --pr ` — post a review decision. - `meebox pr comment --pr ` — post a top-level comment. -Filter vocabulary: `meebox categories` lists the active platform's available `categories` / `statuses`. +Root-level (no PR): +- `meebox whoami` — current user + platform + connection (confirm your token resolves). +- `meebox version` — CLI (client) + app (server) versions; client-only when the server is unreachable. ## Typical loop diff --git a/cli/cmd/categories.go b/cli/cmd/categories.go index e3ee2a71..680e1449 100644 --- a/cli/cmd/categories.go +++ b/cli/cmd/categories.go @@ -2,8 +2,9 @@ package cmd import "github.com/spf13/cobra" -// newCategoriesCmd builds `meebox categories`: lists the enabled platform's available +// newCategoriesCmd builds `meebox pr categories`: lists the enabled platform's available // filter labels — `categories` (discovery) and `statuses` (review/merge) (GET /categories). +// Lives under `pr` because it is the filter vocabulary for `pr list` (--category / --status). func newCategoriesCmd() *cobra.Command { return &cobra.Command{ Use: "categories", diff --git a/cli/cmd/integration_test.go b/cli/cmd/integration_test.go index a7bdda07..52d25dc0 100644 --- a/cli/cmd/integration_test.go +++ b/cli/cmd/integration_test.go @@ -72,7 +72,7 @@ func TestCategories(t *testing.T) { srv := mockServer(&rec, 200, `{"platform":"github","categories":["review-requested"],"statuses":["all"]}`) defer srv.Close() - out, err := runCmd(base(srv.URL, "categories")...) + out, err := runCmd(base(srv.URL, "pr", "categories")...) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -104,6 +104,41 @@ func TestWhoami(t *testing.T) { } } +func TestPrRefreshPost(t *testing.T) { + var rec capturedReq + srv := mockServer(&rec, 200, `{"fetched":3,"changed":1,"added":1,"removed":0,"errors":0}`) + defer srv.Close() + + out, err := runCmd(base(srv.URL, "pr", "refresh")...) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if rec.method != http.MethodPost || rec.path != "/api/v1/refresh" { + t.Errorf("wrong request: %s %s", rec.method, rec.path) + } + if !strings.Contains(out, "added: 1") { + t.Errorf("output missing rendered field: %q", out) + } +} + +func TestVersion(t *testing.T) { + var rec capturedReq + srv := mockServer(&rec, 200, `{"version":"9.9.9"}`) + defer srv.Close() + + out, err := runCmd(base(srv.URL, "version")...) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if rec.method != http.MethodGet || rec.path != "/api/v1/version" { + t.Errorf("wrong request: %s %s", rec.method, rec.path) + } + // Both client (build-time "dev" in tests) and server versions render. + if !strings.Contains(out, "client:") || !strings.Contains(out, "server: 9.9.9") { + t.Errorf("version output missing client/server: %q", out) + } +} + func TestPrListFilters(t *testing.T) { var rec capturedReq srv := mockServer(&rec, 200, `[]`) @@ -298,7 +333,7 @@ func TestAuthFailureExitCode(t *testing.T) { srv := mockServer(&rec, 401, "") defer srv.Close() - _, err := runCmd(base(srv.URL, "categories")...) + _, err := runCmd(base(srv.URL, "pr", "categories")...) if err == nil { t.Fatal("expected auth error") } @@ -326,7 +361,7 @@ func TestOutputJSON(t *testing.T) { srv := mockServer(&rec, 200, `{"platform":"github"}`) defer srv.Close() - out, err := runCmd(base(srv.URL, "--output", "json", "categories")...) + out, err := runCmd(base(srv.URL, "--output", "json", "pr", "categories")...) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/cli/cmd/pr.go b/cli/cmd/pr.go index deba26dc..bbd12154 100644 --- a/cli/cmd/pr.go +++ b/cli/cmd/pr.go @@ -26,6 +26,8 @@ func newPrCmd() *cobra.Command { Short: "Browse and act on pull requests", } pr.AddCommand( + newCategoriesCmd(), + newRefreshCmd(), newPrListCmd(), newPrShowCmd(), newPrDiffCmd(), diff --git a/cli/cmd/refresh.go b/cli/cmd/refresh.go new file mode 100644 index 00000000..14020ece --- /dev/null +++ b/cli/cmd/refresh.go @@ -0,0 +1,18 @@ +package cmd + +import "github.com/spf13/cobra" + +// newRefreshCmd builds `meebox pr refresh`: trigger one immediate poll across all connections +// (the same action as the GUI's manual refresh), fetching the latest PRs into local state and +// returning a summary of what changed (POST /refresh). Returns counts: +// fetched / changed / added / removed / errors. Global — not PR-scoped, so no --pr flag. +func newRefreshCmd() *cobra.Command { + return &cobra.Command{ + Use: "refresh", + Short: "Trigger an immediate poll for the latest PRs", + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + return postAndRender("/api/v1/refresh", nil) + }, + } +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 15925c50..cb6ed2f4 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -39,7 +39,7 @@ func newRootCmd() *cobra.Command { root.AddCommand( newWhoamiCmd(), - newCategoriesCmd(), + newVersionCmd(), newPrCmd(), newAgentCmd(), ) diff --git a/cli/cmd/version.go b/cli/cmd/version.go new file mode 100644 index 00000000..55f55333 --- /dev/null +++ b/cli/cmd/version.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/huhamhire/code-meeseeks/cli/internal/render" + "github.com/spf13/cobra" +) + +// newVersionCmd builds `meebox version`: prints the CLI (client) version and, when +// the local API is reachable, the desktop app (server) version — mirroring `docker version`. +// The client version always renders; if the server can't be reached its field is null and a +// warning goes to stderr, but the exit stays 0 so the client version is usable offline. +// (The root `--version` flag remains the quick client-only path.) +func newVersionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Show the CLI (client) and app (server) versions", + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + out := struct { + Client string `json:"client"` + Server *string `json:"server"` + }{Client: version} + if sv, err := fetchServerVersion(); err != nil { + render.Errorln(fmt.Errorf("server version unavailable: %w", err)) + } else { + out.Server = &sv + } + raw, err := json.Marshal(out) + if err != nil { + return err + } + return renderData(raw) + }, + } +} + +// fetchServerVersion asks the local API for the desktop app version (GET /version). +func fetchServerVersion() (string, error) { + c, err := resolveClient() + if err != nil { + return "", err + } + data, err := c.Get("/api/v1/version", nil) + if err != nil { + return "", err + } + var sv struct { + Version string `json:"version"` + } + if err := json.Unmarshal(data, &sv); err != nil { + return "", err + } + return sv.Version, nil +} From ea99cbd97bb093d0fe92b2a16eb0788c68736ab7 Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 11:32:01 +0800 Subject: [PATCH 04/12] =?UTF-8?q?docs(cli):=20=E5=90=8C=E6=AD=A5=20refresh?= =?UTF-8?q?=20/=20version=20=E5=91=BD=E4=BB=A4=E4=B8=8E=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E6=A0=91=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit arch(service-api 端点表 + cli 命令树/分组哲学)、guide(用法表)三处对齐:根层级系统性命令 (whoami / version)+ pr 领域(含 categories / refresh)+ agent 领域。 Co-Authored-By: Claude Opus 4.8 --- docs/arch/04-integration/01-service-api.md | 5 ++++- docs/arch/04-integration/02-cli.md | 18 ++++++++++++++---- docs/guide/06-cli.md | 9 ++++++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/arch/04-integration/01-service-api.md b/docs/arch/04-integration/01-service-api.md index cd860893..26b3a9b1 100644 --- a/docs/arch/04-integration/01-service-api.md +++ b/docs/arch/04-integration/01-service-api.md @@ -9,7 +9,8 @@ 负责:服务监听开关与生命周期、bearer token 鉴权、请求路由与响应封装、把内部能力映射成稳定的 HTTP 契约。 开放的**写操作**限定为评审动作:approve / needswork(远端评审决断)与顶层 comment(发评论),复用 -GUI 同源 controller(见下「写边界」)。 +GUI 同源 controller(见下「写边界」)。另有 `POST …/refresh` 触发一次本地轮询刷新——虽用 POST,但 +**无远端写副作用**(纯读远端 + 落本地),不属评审写动作范畴,与合并 / 变更类工具的禁令无关。 **不负责**: @@ -120,6 +121,8 @@ service: | --- | --- | --- | | `GET /api/v1/whoami` | 当前身份:活动连接 PAT 所属用户(`name`/`displayName`/`slug`)+ 集成平台 + 连接显示名;无活动连接各项 null | 连接摘要(当前用户 + 平台) | | `GET /api/v1/categories` | 当前启用平台下可用的分类标签:`categories`(`PrDiscoveryFilter`)+ `statuses`(状态 / 合并态筛选),按平台能力裁剪 | 平台能力位 + 列表筛选语义 | +| `POST /api/v1/refresh` | 触发一次立即轮询刷新(拉取所有连接的最新 PR、落本地),返回本轮计数汇总(`PollResult`:fetched / changed / added / removed / errors);等价 GUI 手动刷新,无远端写副作用 | `poller.tick`(`prs:refresh` 同源) | +| `GET /api/v1/version` | 服务端(桌面应用)版本(`{ version }`),供 CLI `version` 命令同时展示客户端 + 服务端版本 | `buildAppInfo().appVersion`(`app:info` 同源) | | `GET /api/v1/prs` | PR 列表(**精简投影** `PrListItem`:字段序 id/title/author/createdAt 优先,去 description、人员仅 slug);query:`category`(一级)/`status`(二级)/`q`(检索)/`skip`+`limit`(分页,默认 limit 100) | `prs:list` + 列表筛选谓词 + 视图投影 | | `GET /api/v1/prs/{id}` | 描述详情(完整 `StoredPullRequest`:标题 / 描述 / 作者 / 分支 / 时间 / 状态 / 合并态) | `StoredPullRequest` | | `GET /api/v1/prs/{id}/diff` | 变更文件列表;带 `?path=&side=base\|head` 时取单文件内容 | `diff:listChangedFiles` / `diff:getFileContent` 同源 | diff --git a/docs/arch/04-integration/02-cli.md b/docs/arch/04-integration/02-cli.md index a21d6cdf..6ad0cc32 100644 --- a/docs/arch/04-integration/02-cli.md +++ b/docs/arch/04-integration/02-cli.md @@ -59,14 +59,24 @@ meebox [全局 flag] <组> <命令> [参数] 全局 flag:--api-url · --token · --output (yaml|json) · --quiet ``` -命令分两个领域组:`pr`(直接的 PR 实体操作)与 `agent`(评审 Agent 操作)。二者都用**必填 flag -`--pr `** 传 PR 标识(`id` 由 `pr list` 输出获得)——meebox 只管理 PR,故 agent **不再嵌进 `pr`** -(避免 `pr agent … --pr` 里 `pr` 重复),而与 `pr` 平级。 +命令分两类——**根层级系统性命令** 与 **两个领域组**: + +- **系统性命令(根层级)** —— `whoami`(身份)、`version`(客户端 + 服务端版本):与具体 PR / Agent 无关的 + 工具 / 会话层信息,直接置于根层级、不套领域组(符合 `kubectl version` / `gh` 等惯例)。 +- **`pr`** —— PR 相关操作:浏览 + 评审写动作,并含 `categories`(`pr list` 的筛选词表)与 `refresh` + (触发一次拉取、刷新 PR 列表)。 +- **`agent`** —— 评审 Agent 操作。 + +`pr` / `agent` 下的 PR 维度子命令用**必填 flag `--pr `** 传 PR 标识(`id` 由 `pr list` 输出获得)—— +agent **不嵌进 `pr`**(避免 `pr agent … --pr` 里 `pr` 重复),与 `pr` 平级;根层级系统性命令与 +`pr categories` / `pr refresh` / `pr list` 非 PR 维度,无需 `--pr`。 | 命令 | 用途 | 对应 API | | --- | --- | --- | | `meebox whoami` | 当前身份(用户 + 平台 + 连接名) | `GET /whoami` | -| `meebox categories` | 列当前启用平台的分类标签(`categories` 一级 + `statuses` 二级) | `GET /categories` | +| `meebox version` | 客户端(CLI)+ 服务端(应用)版本;服务端不可达时仅客户端、退出码仍 0 | `GET /version` | +| `meebox pr categories` | 列当前启用平台的分类标签(`categories` 一级 + `statuses` 二级)——`pr list` 的筛选词表 | `GET /categories` | +| `meebox pr refresh` | 触发一次立即轮询刷新(拉取最新 PR、落本地),返回本轮计数汇总(fetched / changed / added / removed / errors);等价 GUI 手动刷新 | `POST /refresh` | | `meebox pr list [--category <一级>] [--status <二级>] [--query <检索>] [--skip N] [--limit N]` | PR 列表(精简投影 + 分页,默认 limit 100) | `GET /prs` | | `meebox pr show --pr ` | 描述详情 | `GET /prs/{id}` | | `meebox pr diff --pr [--file ] [--side base\|head]` | 无 `--file` 列变更文件;有则取该文件内容 | `GET /prs/{id}/diff` | diff --git a/docs/guide/06-cli.md b/docs/guide/06-cli.md index be17d071..7ad5e498 100644 --- a/docs/guide/06-cli.md +++ b/docs/guide/06-cli.md @@ -54,13 +54,16 @@ meebox --api-url http://<主机>:18765 --token <令牌> pr list meebox [全局参数] <组> <命令> [参数] ``` -命令分 `pr`(直接的 PR 操作)与 `agent`(评审 Agent 操作)两个领域组,均用**必填参数 `--pr `** 指定 PR -(`id` 由 `meebox pr list` 输出获得)。 +根层级的系统性命令 `whoami` / `version` 与具体 PR 无关;其余命令分 `pr`(PR 操作,含 `categories` 筛选词表 +与 `refresh` 刷新)与 `agent`(评审 Agent 操作)两个领域组,其 PR 维度子命令用**必填参数 `--pr `** 指定 +PR(`id` 由 `meebox pr list` 输出获得)。 | 命令 | 用途 | | --- | --- | | `meebox whoami` | 当前登录身份与集成平台(用户 + 平台 + 连接名) | -| `meebox categories` | 列出当前平台可用的分类标签(一级发现分类 + 二级状态 / 合并态筛选) | +| `meebox version` | 客户端(CLI)+ 服务端(应用)版本;未连接服务端时仅显示客户端版本 | +| `meebox pr categories` | 列出当前平台可用的分类标签(一级发现分类 + 二级状态 / 合并态筛选)——`pr list` 的筛选词表 | +| `meebox pr refresh` | 触发一次立即刷新(拉取最新 PR),返回本轮变化计数(新增 / 变更 / 移除等);等同 GUI 里的手动刷新 | | `meebox pr list [--category <一级>] [--status <二级>] [--query <检索>] [--skip N] [--limit N]` | PR 列表(精简字段 + 分页,默认 limit 100) | | `meebox pr show --pr ` | PR 描述详情 | | `meebox pr diff --pr [--file <路径>] [--side base\|head]` | 无 `--file` 列变更文件;有则取该文件内容 | From 881e58ad4a2a66e3d4371e0a79309ce8459a643f Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 11:33:41 +0800 Subject: [PATCH 05/12] =?UTF-8?q?docs(agents):=20=E8=A1=A5=E5=85=85=20CLI?= =?UTF-8?q?=20/=20API=20=E5=91=BD=E4=BB=A4=E4=B8=8E=E7=AB=AF=E7=82=B9?= =?UTF-8?q?=E7=9A=84=E9=A2=86=E5=9F=9F=E5=BD=92=E7=B1=BB=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 918cbda9..286aa0d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,7 @@ npm --prefix apps/desktop run prepare:pragent # 对齐嵌入式 pr-agent 运 - **压缩包即 skill 目录**:CLI 压缩包除二进制外一并打包 `LICENSE` + `cli/README.md` + `cli/SKILL.md`(frontmatter `name: meebox`)——解压投放到 agent 的 skills 目录即得可用 skill(面向 agent 交付的主形态)。 - **写边界**:CLI 做浏览 + **评审写动作**——approve / needswork(远端评审决断)与 comment(发评论),经服务端专用端点(复用 GUI 同源 controller)。仍**不开放**:merge(合并)与 pr-agent 变更类工具(publish 等,`instruct` 只读白名单 describe/review/ask/improve 在 CLI 与服务端双重把关)。新增命令先确认对应 API 端点已存在;放开新写端点须评估远端副作用。CLI 不得绕过 API 直连应用内部。 - **契约同步**:CLI 与服务端唯一耦合是 HTTP/JSON 线协议。当前手写 Go 结构对齐契约,契约增长后转 OpenAPI / Schema 代码生成。默认输出 YAML(人类向、保序)、`--output json` 供机器(亦保序);PR 列表返回精简投影、PR 标识对外为 `id`、PR 关联命令用 `--pr `。连接配置走 flag / 环境变量(`MEEBOX_API_URL` / `MEEBOX_TOKEN`)/ `~/.code-meeseeks/cli.yaml`,**不读 GUI 的 `config.yaml`**(避免越权触达连接层机密);代理遵循标准 `HTTP(S)_PROXY` / `NO_PROXY`。 +- **领域归类**:CLI / API / GUI 都是同一 service 层之上的薄前端,命令树与端点应**镜像业务领域**。新增 CLI 命令 / API 端点按语义归位——PR 相关入 `pr`、评审 Agent 入 `agent`;与具体 PR / Agent 无关的**系统性 / 会话级**操作(whoami / version 等)置于**根层级**,不套领域组。归属看**语义而非是否 PR 维度**(如 `categories` / `refresh` 无 `--pr` 仍属 `pr`——它们服务于 PR 列表)。完整理由见 [CLI 设计](docs/arch/04-integration/02-cli.md)。 - **文档对齐**:CLI 改动(命令树 / 写边界 / 输出契约 / 连接配置)须同步三类文档,缺一即漂移——① **arch 设计**:[docs/arch/04-integration/02-cli.md](docs/arch/04-integration/02-cli.md)(命令树与边界),涉及 API 端点再改 [01-service-api.md](docs/arch/04-integration/01-service-api.md)(端点表);② **guide 用法**:[docs/guide/06-cli.md](docs/guide/06-cli.md);③ **skill 交付**:`cli/SKILL.md` + `cli/README.md`(随压缩包投放为 agent skill 的主形态)。新增 / 改命令务必三类齐更。 ## 约定 From 94457575b2ce02a7d4600783dc2c645c99faa270 Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 11:41:56 +0800 Subject: [PATCH 06/12] =?UTF-8?q?refactor(service):=20=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=20API=20=E8=B7=AF=E7=94=B1=E6=8C=89=E9=A2=86=E5=9F=9F=E6=8B=86?= =?UTF-8?q?=E5=88=86=EF=BC=8C=E8=81=9A=E5=90=88=E5=B1=82=E5=8F=AA=E5=81=9A?= =?UTF-8?q?=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 routes.ts 拆为 routes/ 子目录:system(whoami / version)、pr(列表 / 详情 / diff / 刷新 / 分类 / 评审写动作)、agent(Agent 操作),公共原语(Route 类型 / seg / NO_EVENT)归 shared; routes/index 只做聚合注册与路径匹配、不含业务逻辑。与 CLI 命令树的领域划分对称。纯结构调整, 端点与行为不变。 Co-Authored-By: Claude Opus 4.8 --- .../src/main/services/api-server/routes.ts | 262 ------------------ .../main/services/api-server/routes/agent.ts | 85 ++++++ .../main/services/api-server/routes/index.ts | 36 +++ .../src/main/services/api-server/routes/pr.ts | 116 ++++++++ .../main/services/api-server/routes/shared.ts | 30 ++ .../main/services/api-server/routes/system.ts | 39 +++ .../src/main/services/api-server/server.ts | 2 +- docs/arch/04-integration/01-service-api.md | 2 + 8 files changed, 309 insertions(+), 263 deletions(-) delete mode 100644 apps/desktop/src/main/services/api-server/routes.ts create mode 100644 apps/desktop/src/main/services/api-server/routes/agent.ts create mode 100644 apps/desktop/src/main/services/api-server/routes/index.ts create mode 100644 apps/desktop/src/main/services/api-server/routes/pr.ts create mode 100644 apps/desktop/src/main/services/api-server/routes/shared.ts create mode 100644 apps/desktop/src/main/services/api-server/routes/system.ts diff --git a/apps/desktop/src/main/services/api-server/routes.ts b/apps/desktop/src/main/services/api-server/routes.ts deleted file mode 100644 index 432415f5..00000000 --- a/apps/desktop/src/main/services/api-server/routes.ts +++ /dev/null @@ -1,262 +0,0 @@ -import type { IpcMainInvokeEvent } from 'electron'; -import type { DiffSide } from '@meebox/ipc'; -import { - ERROR_CODES, - PR_SECONDARY_FILTERS, - filterPullRequests, - type PrDiscoveryFilter, - type PrSecondaryFilter, - type ReviewRunTool, -} from '@meebox/shared'; -import * as agentCtl from '../../controllers/agent.js'; -import * as prCtl from '../../controllers/pr.js'; -import { buildAppInfo } from '../app.js'; -import { getContext } from '../context.js'; -import { HttpError } from './http.js'; -import { toPrAgentRuns, toPrListItem } from './views.js'; - -/** - * 本地 API 的路由表与处理器。处理器**复用 IPC controller 同源逻辑**——controller 形态为 - * `(event, req)` 且这些路径不触碰 event,故以 NO_EVENT 占位调用,避免在 HTTP 侧另起一套实现。 - * - * 写边界:开放**评审写操作**——approve / needswork(远端评审决断)与顶层 comment(发评论), - * 均复用 GUI 同源 controller。仍**不**暴露:merge(合并)、pr-agent 的变更类工具(publish 等, - * 见 agent/instruct 的只读白名单)。见 docs/arch/04-integration/01-service-api.md。 - */ - -// controller 形参 event 在被复用的只读 / 队列路径中均未使用,占位即可。 -const NO_EVENT = undefined as unknown as IpcMainInvokeEvent; - -/** API 仅允许的只读 Agent 指令(与工具注册表 isRun 只读族一致;写工具不在此列)。 */ -const READ_ONLY_TOOLS: ReadonlySet = new Set([ - 'describe', - 'review', - 'ask', - 'improve', -]); - -export interface RouteContext { - params: Record; - query: URLSearchParams; - body: unknown; -} - -export type RouteHandler = (rc: RouteContext) => Promise | unknown; - -export interface Route { - method: 'GET' | 'POST'; - segments: string[]; - handler: RouteHandler; -} - -function seg(path: string): string[] { - return path.split('/').filter(Boolean); -} - -/** 列表分页默认页大小(`limit` 缺省 / 非法 / ≤0 时取此值)。 */ -const DEFAULT_LIMIT = 100; - -/** 当前启用平台下可用的分类标签:`categories`(平台发现分类)+ `statuses`(状态 / 合并态筛选)。 */ -const categories: RouteHandler = () => { - const ctx = getContext(); - const activeId = ctx.bootstrap.config.active_connection_id; - const built = activeId - ? ctx.connectionRuntime.adapters.find((a) => a.connectionId === activeId) - : undefined; - const caps = built?.adapter.connection.capabilities(); - const categoryList: PrDiscoveryFilter[] = caps?.discoveryFilters - ? [...caps.discoveryFilters] - : ['review-requested']; - return { - platform: built?.adapter.kind ?? null, - categories: categoryList, - statuses: [...PR_SECONDARY_FILTERS], - }; -}; - -/** - * 当前身份与集成平台:活动连接的 PAT 所属用户(name / displayName / slug)+ 平台种类 + - * 连接显示名。无活动连接时各项为 null。刻意收窄——不带 capabilities(那是 GUI 降级用的大对象)。 - */ -const whoami: RouteHandler = () => { - const ctx = getContext(); - const activeId = ctx.bootstrap.config.active_connection_id; - const built = activeId - ? ctx.connectionRuntime.adapters.find((a) => a.connectionId === activeId) - : undefined; - if (!activeId || !built) { - return { platform: null, connectionId: null, displayName: null, user: null }; - } - const conn = ctx.bootstrap.config.connections.find((c) => c.id === activeId); - const user = built.adapter.connection.getCurrentUser(); - return { - platform: built.adapter.kind, - connectionId: activeId, - displayName: conn?.display_name ?? activeId, - user: user ? { name: user.name, displayName: user.displayName, slug: user.slug ?? null } : null, - }; -}; - -/** - * 触发一次立即轮询刷新(等价 GUI 的手动刷新 / 窗口聚焦刷新):拉取所有连接的最新 PR、落本地, - * 返回本轮计数汇总(fetched / changed / added / removed / errors)。复用 GUI 同源 poller.tick - * (`prs:refresh`)。无远端写副作用(纯读远端 + 落本地),列为安全的开放动作。 - */ -const refresh: RouteHandler = () => prCtl.refreshPrs(NO_EVENT, undefined); - -/** 服务端(桌面应用)版本,供 CLI `system version` 同时展示客户端 + 服务端版本。 */ -const version: RouteHandler = () => ({ version: buildAppInfo(getContext().bootstrap).appVersion }); - -/** - * PR 列表:`category`(一级发现分类)+ `status`(二级状态 / 合并态)过滤 + `q` 检索 + - * `skip`/`limit` 分页(默认 limit 100)。过滤语义复用 @meebox/shared 的纯谓词(与渲染层侧栏同源); - * 返回**精简列表投影**({@link toPrListItem},去 description 明细、人员仅 slug),此处仅解析参数 + 委派。 - */ -const listPrs: RouteHandler = async ({ query }) => { - const all = await prCtl.listPrs(NO_EVENT, undefined); - const filtered = filterPullRequests(all, { - primary: (query.get('category') as PrDiscoveryFilter) || undefined, - secondary: (query.get('status') as PrSecondaryFilter) || undefined, - query: query.get('q') ?? undefined, - }); - const skip = Math.max(0, Number.parseInt(query.get('skip') ?? '', 10) || 0); - const limitRaw = Number.parseInt(query.get('limit') ?? '', 10); - const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : DEFAULT_LIMIT; - return filtered.slice(skip, skip + limit).map(toPrListItem); -}; - -const showPr: RouteHandler = ({ params }) => getContext().pr.findPrOrThrow(params.id); - -const reviewers: RouteHandler = async ({ params }) => - (await getContext().pr.findPrOrThrow(params.id)).reviewers; - -/** 无 path → 变更文件列表;带 path → 取该文件某一侧(默认 head)内容。 */ -const diff: RouteHandler = ({ params, query }) => { - const path = query.get('path'); - if (path) { - const side: DiffSide = query.get('side') === 'base' ? 'base' : 'head'; - return prCtl.getFileContent(NO_EVENT, { localId: params.id, side, path }); - } - return prCtl.listChangedFiles(NO_EVENT, { localId: params.id }); -}; - -const activity: RouteHandler = ({ params }) => - prCtl.listActivity(NO_EVENT, { localId: params.id }); - -const commits: RouteHandler = ({ params }) => prCtl.listCommits(NO_EVENT, { localId: params.id }); - -const agentStatus: RouteHandler = ({ params }) => - agentCtl.getSession(NO_EVENT, { localId: params.id }); - -const agentHistory: RouteHandler = ({ params }) => - agentCtl.getConversation(NO_EVENT, { localId: params.id }); - -const agentReview: RouteHandler = ({ params }) => agentCtl.runReview(NO_EVENT, { localId: params.id }); - -/** 发送只读 Agent 指令(describe / review / ask / improve);写工具硬拒绝(403),无二次确认。 */ -const agentInstruct: RouteHandler = ({ params, body }) => { - const b = (body ?? {}) as { command?: string; args?: string }; - const command = (b.command ?? '').replace(/^\//, '') as ReviewRunTool; - if (!READ_ONLY_TOOLS.has(command)) { - throw new HttpError(403, ERROR_CODES.SV_WRITE_NOT_ALLOWED, { command: b.command ?? '' }); - } - if (command === 'ask' && !b.args?.trim()) { - throw new HttpError(400, ERROR_CODES.SV_BAD_REQUEST, { reason: 'ask requires args' }); - } - return agentCtl.runPragent(NO_EVENT, { localId: params.id, tool: command, question: b.args }); -}; - -/** 发送自然语言聊天(可触发 Agent 任务):运行中入队、否则起一轮自由规划兜底。 */ -const agentChat: RouteHandler = ({ params, body }) => { - const b = (body ?? {}) as { message?: string }; - if (!b.message?.trim()) { - throw new HttpError(400, ERROR_CODES.SV_BAD_REQUEST, { reason: 'message required' }); - } - return agentCtl.enqueueMessage(NO_EVENT, { localId: params.id, message: b.message }); -}; - -/** 中断该 PR 正在运行的 Agent(思考 / 执行任意阶段即时停)。PR 级停,非按单个工具 run。 */ -const agentStop: RouteHandler = ({ params }) => - agentCtl.stopAgent(NO_EVENT, { localId: params.id }); - -/** 该 PR 在运行队列里的 pr-agent runs(active + waiting),供按 run 取消前的发现。 */ -const agentRuns: RouteHandler = async ({ params }) => { - const snapshot = await agentCtl.getQueue(NO_EVENT, undefined); - return toPrAgentRuns(snapshot, params.id); -}; - -/** 取消该 PR 的某个 pr-agent run(active SIGKILL / waiting 出队)。先校验 run 归属该 PR。 */ -const agentRunCancel: RouteHandler = async ({ params }) => { - const snapshot = await agentCtl.getQueue(NO_EVENT, undefined); - const belongs = [...snapshot.active, ...snapshot.waiting].some( - (r) => r.runId === params.runId && r.prLocalId === params.id, - ); - if (!belongs) { - throw new HttpError(404, ERROR_CODES.SV_NOT_FOUND, { runId: params.runId, localId: params.id }); - } - return agentCtl.cancelPragent(NO_EVENT, { runId: params.runId }); -}; - -/** 评审决断「通过」:先写远端评审状态、再落本地(复用 GUI 同源 setPrStatus)。 */ -const approve: RouteHandler = ({ params }) => - prCtl.setPrStatus(NO_EVENT, { localId: params.id, status: 'approved' }); - -/** 评审决断「需修改」:先写远端评审状态、再落本地。 */ -const needswork: RouteHandler = ({ params }) => - prCtl.setPrStatus(NO_EVENT, { localId: params.id, status: 'needs_work' }); - -/** 发一条顶层(不锚文件)评论到远端 PR。body.body 为评论正文,空则 400。 */ -const comment: RouteHandler = ({ params, body }) => { - const b = (body ?? {}) as { body?: string }; - if (!b.body?.trim()) { - throw new HttpError(400, ERROR_CODES.SV_BAD_REQUEST, { reason: 'comment body required' }); - } - return prCtl.createComment(NO_EVENT, { localId: params.id, body: b.body }); -}; - -export const routes: Route[] = [ - { method: 'GET', segments: seg('/api/v1/categories'), handler: categories }, - { method: 'GET', segments: seg('/api/v1/whoami'), handler: whoami }, - { method: 'POST', segments: seg('/api/v1/refresh'), handler: refresh }, - { method: 'GET', segments: seg('/api/v1/version'), handler: version }, - { method: 'GET', segments: seg('/api/v1/prs'), handler: listPrs }, - { method: 'GET', segments: seg('/api/v1/prs/:id'), handler: showPr }, - { method: 'GET', segments: seg('/api/v1/prs/:id/diff'), handler: diff }, - { method: 'GET', segments: seg('/api/v1/prs/:id/activity'), handler: activity }, - { method: 'GET', segments: seg('/api/v1/prs/:id/commits'), handler: commits }, - { method: 'GET', segments: seg('/api/v1/prs/:id/reviewers'), handler: reviewers }, - { method: 'GET', segments: seg('/api/v1/prs/:id/agent'), handler: agentStatus }, - { method: 'GET', segments: seg('/api/v1/prs/:id/agent/conversation'), handler: agentHistory }, - { method: 'POST', segments: seg('/api/v1/prs/:id/agent/review'), handler: agentReview }, - { method: 'POST', segments: seg('/api/v1/prs/:id/agent/instruct'), handler: agentInstruct }, - { method: 'POST', segments: seg('/api/v1/prs/:id/agent/chat'), handler: agentChat }, - { method: 'POST', segments: seg('/api/v1/prs/:id/agent/stop'), handler: agentStop }, - { method: 'GET', segments: seg('/api/v1/prs/:id/agent/runs'), handler: agentRuns }, - { method: 'POST', segments: seg('/api/v1/prs/:id/agent/runs/:runId/cancel'), handler: agentRunCancel }, - { method: 'POST', segments: seg('/api/v1/prs/:id/approve'), handler: approve }, - { method: 'POST', segments: seg('/api/v1/prs/:id/needswork'), handler: needswork }, - { method: 'POST', segments: seg('/api/v1/prs/:id/comment'), handler: comment }, -]; - -/** 按方法 + 路径匹配路由,提取 `:param` 路径参数;无匹配返回 null。 */ -export function matchRoute( - method: string, - pathname: string, -): { route: Route; params: Record } | null { - const parts = seg(pathname); - for (const route of routes) { - if (route.method !== method || route.segments.length !== parts.length) continue; - const params: Record = {}; - let ok = true; - for (let i = 0; i < route.segments.length; i++) { - const s = route.segments[i]; - if (s.startsWith(':')) params[s.slice(1)] = decodeURIComponent(parts[i]); - else if (s !== parts[i]) { - ok = false; - break; - } - } - if (ok) return { route, params }; - } - return null; -} diff --git a/apps/desktop/src/main/services/api-server/routes/agent.ts b/apps/desktop/src/main/services/api-server/routes/agent.ts new file mode 100644 index 00000000..dcfb8848 --- /dev/null +++ b/apps/desktop/src/main/services/api-server/routes/agent.ts @@ -0,0 +1,85 @@ +import { ERROR_CODES, type ReviewRunTool } from '@meebox/shared'; +import * as agentCtl from '../../../controllers/agent.js'; +import { HttpError } from '../http.js'; +import { toPrAgentRuns } from '../views.js'; +import { NO_EVENT, seg, type Route, type RouteHandler } from './shared.js'; + +/** + * 评审 Agent 领域端点:状态 / 会话(浏览),auto review / 指令 / 聊天 / 中断(写入型,复用既有 run 队列), + * 以及按 run 的发现与取消。Agent `instruct` **仅只读工具**,变更类工具(publish 等)在 API 层硬拒绝。 + */ + +/** API 仅允许的只读 Agent 指令(与工具注册表 isRun 只读族一致;写工具不在此列)。 */ +const READ_ONLY_TOOLS: ReadonlySet = new Set([ + 'describe', + 'review', + 'ask', + 'improve', +]); + +const agentStatus: RouteHandler = ({ params }) => + agentCtl.getSession(NO_EVENT, { localId: params.id }); + +const agentHistory: RouteHandler = ({ params }) => + agentCtl.getConversation(NO_EVENT, { localId: params.id }); + +const agentReview: RouteHandler = ({ params }) => agentCtl.runReview(NO_EVENT, { localId: params.id }); + +/** 发送只读 Agent 指令(describe / review / ask / improve);写工具硬拒绝(403),无二次确认。 */ +const agentInstruct: RouteHandler = ({ params, body }) => { + const b = (body ?? {}) as { command?: string; args?: string }; + const command = (b.command ?? '').replace(/^\//, '') as ReviewRunTool; + if (!READ_ONLY_TOOLS.has(command)) { + throw new HttpError(403, ERROR_CODES.SV_WRITE_NOT_ALLOWED, { command: b.command ?? '' }); + } + if (command === 'ask' && !b.args?.trim()) { + throw new HttpError(400, ERROR_CODES.SV_BAD_REQUEST, { reason: 'ask requires args' }); + } + return agentCtl.runPragent(NO_EVENT, { localId: params.id, tool: command, question: b.args }); +}; + +/** 发送自然语言聊天(可触发 Agent 任务):运行中入队、否则起一轮自由规划兜底。 */ +const agentChat: RouteHandler = ({ params, body }) => { + const b = (body ?? {}) as { message?: string }; + if (!b.message?.trim()) { + throw new HttpError(400, ERROR_CODES.SV_BAD_REQUEST, { reason: 'message required' }); + } + return agentCtl.enqueueMessage(NO_EVENT, { localId: params.id, message: b.message }); +}; + +/** 中断该 PR 正在运行的 Agent(思考 / 执行任意阶段即时停)。PR 级停,非按单个工具 run。 */ +const agentStop: RouteHandler = ({ params }) => + agentCtl.stopAgent(NO_EVENT, { localId: params.id }); + +/** 该 PR 在运行队列里的 pr-agent runs(active + waiting),供按 run 取消前的发现。 */ +const agentRuns: RouteHandler = async ({ params }) => { + const snapshot = await agentCtl.getQueue(NO_EVENT, undefined); + return toPrAgentRuns(snapshot, params.id); +}; + +/** 取消该 PR 的某个 pr-agent run(active SIGKILL / waiting 出队)。先校验 run 归属该 PR。 */ +const agentRunCancel: RouteHandler = async ({ params }) => { + const snapshot = await agentCtl.getQueue(NO_EVENT, undefined); + const belongs = [...snapshot.active, ...snapshot.waiting].some( + (r) => r.runId === params.runId && r.prLocalId === params.id, + ); + if (!belongs) { + throw new HttpError(404, ERROR_CODES.SV_NOT_FOUND, { runId: params.runId, localId: params.id }); + } + return agentCtl.cancelPragent(NO_EVENT, { runId: params.runId }); +}; + +export const agentRoutes: Route[] = [ + { method: 'GET', segments: seg('/api/v1/prs/:id/agent'), handler: agentStatus }, + { method: 'GET', segments: seg('/api/v1/prs/:id/agent/conversation'), handler: agentHistory }, + { method: 'POST', segments: seg('/api/v1/prs/:id/agent/review'), handler: agentReview }, + { method: 'POST', segments: seg('/api/v1/prs/:id/agent/instruct'), handler: agentInstruct }, + { method: 'POST', segments: seg('/api/v1/prs/:id/agent/chat'), handler: agentChat }, + { method: 'POST', segments: seg('/api/v1/prs/:id/agent/stop'), handler: agentStop }, + { method: 'GET', segments: seg('/api/v1/prs/:id/agent/runs'), handler: agentRuns }, + { + method: 'POST', + segments: seg('/api/v1/prs/:id/agent/runs/:runId/cancel'), + handler: agentRunCancel, + }, +]; diff --git a/apps/desktop/src/main/services/api-server/routes/index.ts b/apps/desktop/src/main/services/api-server/routes/index.ts new file mode 100644 index 00000000..45293411 --- /dev/null +++ b/apps/desktop/src/main/services/api-server/routes/index.ts @@ -0,0 +1,36 @@ +import { agentRoutes } from './agent.js'; +import { prRoutes } from './pr.js'; +import { seg, type Route } from './shared.js'; +import { systemRoutes } from './system.js'; + +/** + * 本地 API 的路由**聚合注册 + 匹配**。各业务领域的处理器分置于同目录的 system / pr / agent 模块 + * (均复用 IPC controller 同源逻辑);本文件只做注册与路径匹配,不含业务逻辑。 + * 端点全表与写边界见 docs/arch/04-integration/01-service-api.md。 + */ +export const routes: Route[] = [...systemRoutes, ...prRoutes, ...agentRoutes]; + +export type { Route, RouteContext, RouteHandler } from './shared.js'; + +/** 按方法 + 路径匹配路由,提取 `:param` 路径参数;无匹配返回 null。 */ +export function matchRoute( + method: string, + pathname: string, +): { route: Route; params: Record } | null { + const parts = seg(pathname); + for (const route of routes) { + if (route.method !== method || route.segments.length !== parts.length) continue; + const params: Record = {}; + let ok = true; + for (let i = 0; i < route.segments.length; i++) { + const s = route.segments[i]; + if (s.startsWith(':')) params[s.slice(1)] = decodeURIComponent(parts[i]); + else if (s !== parts[i]) { + ok = false; + break; + } + } + if (ok) return { route, params }; + } + return null; +} diff --git a/apps/desktop/src/main/services/api-server/routes/pr.ts b/apps/desktop/src/main/services/api-server/routes/pr.ts new file mode 100644 index 00000000..60666364 --- /dev/null +++ b/apps/desktop/src/main/services/api-server/routes/pr.ts @@ -0,0 +1,116 @@ +import type { DiffSide } from '@meebox/ipc'; +import { + ERROR_CODES, + PR_SECONDARY_FILTERS, + filterPullRequests, + type PrDiscoveryFilter, + type PrSecondaryFilter, +} from '@meebox/shared'; +import * as prCtl from '../../../controllers/pr.js'; +import { getContext } from '../../context.js'; +import { HttpError } from '../http.js'; +import { toPrListItem } from '../views.js'; +import { NO_EVENT, seg, type Route, type RouteHandler } from './shared.js'; + +/** + * PR 领域端点:列表 / 详情 / diff / 动态 / 提交 / 评审人(浏览),刷新(refresh)与分类词表(categories), + * 以及评审写动作(approve / needswork / comment,真实远端写,复用 GUI 同源 controller)。 + * 仍**不**暴露 merge(合并)。写边界见 docs/arch/04-integration/01-service-api.md。 + */ + +/** 列表分页默认页大小(`limit` 缺省 / 非法 / ≤0 时取此值)。 */ +const DEFAULT_LIMIT = 100; + +/** 当前启用平台下可用的分类标签:`categories`(平台发现分类)+ `statuses`(状态 / 合并态筛选)。 */ +const categories: RouteHandler = () => { + const ctx = getContext(); + const activeId = ctx.bootstrap.config.active_connection_id; + const built = activeId + ? ctx.connectionRuntime.adapters.find((a) => a.connectionId === activeId) + : undefined; + const caps = built?.adapter.connection.capabilities(); + const categoryList: PrDiscoveryFilter[] = caps?.discoveryFilters + ? [...caps.discoveryFilters] + : ['review-requested']; + return { + platform: built?.adapter.kind ?? null, + categories: categoryList, + statuses: [...PR_SECONDARY_FILTERS], + }; +}; + +/** + * 触发一次立即轮询刷新(等价 GUI 的手动刷新 / 窗口聚焦刷新):拉取所有连接的最新 PR、落本地, + * 返回本轮计数汇总(fetched / changed / added / removed / errors)。复用 GUI 同源 poller.tick + * (`prs:refresh`)。无远端写副作用(纯读远端 + 落本地),列为安全的开放动作。 + */ +const refresh: RouteHandler = () => prCtl.refreshPrs(NO_EVENT, undefined); + +/** + * PR 列表:`category`(一级发现分类)+ `status`(二级状态 / 合并态)过滤 + `q` 检索 + + * `skip`/`limit` 分页(默认 limit 100)。过滤语义复用 @meebox/shared 的纯谓词(与渲染层侧栏同源); + * 返回**精简列表投影**({@link toPrListItem},去 description 明细、人员仅 slug),此处仅解析参数 + 委派。 + */ +const listPrs: RouteHandler = async ({ query }) => { + const all = await prCtl.listPrs(NO_EVENT, undefined); + const filtered = filterPullRequests(all, { + primary: (query.get('category') as PrDiscoveryFilter) || undefined, + secondary: (query.get('status') as PrSecondaryFilter) || undefined, + query: query.get('q') ?? undefined, + }); + const skip = Math.max(0, Number.parseInt(query.get('skip') ?? '', 10) || 0); + const limitRaw = Number.parseInt(query.get('limit') ?? '', 10); + const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : DEFAULT_LIMIT; + return filtered.slice(skip, skip + limit).map(toPrListItem); +}; + +const showPr: RouteHandler = ({ params }) => getContext().pr.findPrOrThrow(params.id); + +const reviewers: RouteHandler = async ({ params }) => + (await getContext().pr.findPrOrThrow(params.id)).reviewers; + +/** 无 path → 变更文件列表;带 path → 取该文件某一侧(默认 head)内容。 */ +const diff: RouteHandler = ({ params, query }) => { + const path = query.get('path'); + if (path) { + const side: DiffSide = query.get('side') === 'base' ? 'base' : 'head'; + return prCtl.getFileContent(NO_EVENT, { localId: params.id, side, path }); + } + return prCtl.listChangedFiles(NO_EVENT, { localId: params.id }); +}; + +const activity: RouteHandler = ({ params }) => + prCtl.listActivity(NO_EVENT, { localId: params.id }); + +const commits: RouteHandler = ({ params }) => prCtl.listCommits(NO_EVENT, { localId: params.id }); + +/** 评审决断「通过」:先写远端评审状态、再落本地(复用 GUI 同源 setPrStatus)。 */ +const approve: RouteHandler = ({ params }) => + prCtl.setPrStatus(NO_EVENT, { localId: params.id, status: 'approved' }); + +/** 评审决断「需修改」:先写远端评审状态、再落本地。 */ +const needswork: RouteHandler = ({ params }) => + prCtl.setPrStatus(NO_EVENT, { localId: params.id, status: 'needs_work' }); + +/** 发一条顶层(不锚文件)评论到远端 PR。body.body 为评论正文,空则 400。 */ +const comment: RouteHandler = ({ params, body }) => { + const b = (body ?? {}) as { body?: string }; + if (!b.body?.trim()) { + throw new HttpError(400, ERROR_CODES.SV_BAD_REQUEST, { reason: 'comment body required' }); + } + return prCtl.createComment(NO_EVENT, { localId: params.id, body: b.body }); +}; + +export const prRoutes: Route[] = [ + { method: 'GET', segments: seg('/api/v1/categories'), handler: categories }, + { method: 'POST', segments: seg('/api/v1/refresh'), handler: refresh }, + { method: 'GET', segments: seg('/api/v1/prs'), handler: listPrs }, + { method: 'GET', segments: seg('/api/v1/prs/:id'), handler: showPr }, + { method: 'GET', segments: seg('/api/v1/prs/:id/diff'), handler: diff }, + { method: 'GET', segments: seg('/api/v1/prs/:id/activity'), handler: activity }, + { method: 'GET', segments: seg('/api/v1/prs/:id/commits'), handler: commits }, + { method: 'GET', segments: seg('/api/v1/prs/:id/reviewers'), handler: reviewers }, + { method: 'POST', segments: seg('/api/v1/prs/:id/approve'), handler: approve }, + { method: 'POST', segments: seg('/api/v1/prs/:id/needswork'), handler: needswork }, + { method: 'POST', segments: seg('/api/v1/prs/:id/comment'), handler: comment }, +]; diff --git a/apps/desktop/src/main/services/api-server/routes/shared.ts b/apps/desktop/src/main/services/api-server/routes/shared.ts new file mode 100644 index 00000000..0e9c27a7 --- /dev/null +++ b/apps/desktop/src/main/services/api-server/routes/shared.ts @@ -0,0 +1,30 @@ +import type { IpcMainInvokeEvent } from 'electron'; + +/** + * 路由框架原语,供同目录各业务领域模块(system / pr / agent)与聚合器(index)共用。 + * 各域处理器**复用 IPC controller 同源逻辑**——controller 形态为 `(event, req)` 且这些路径不触碰 + * event,故以 {@link NO_EVENT} 占位调用,避免在 HTTP 侧另起一套实现。 + */ + +/** 单条路由处理器的入参:路径参数 / 查询串 / 已解析 body。 */ +export interface RouteContext { + params: Record; + query: URLSearchParams; + body: unknown; +} + +export type RouteHandler = (rc: RouteContext) => Promise | unknown; + +export interface Route { + method: 'GET' | 'POST'; + segments: string[]; + handler: RouteHandler; +} + +/** 把 `/api/v1/prs/:id` 切成非空段数组(注册与匹配共用)。 */ +export function seg(path: string): string[] { + return path.split('/').filter(Boolean); +} + +/** controller 形参 event 在被复用的只读 / 队列路径中均未使用,占位即可。 */ +export const NO_EVENT = undefined as unknown as IpcMainInvokeEvent; diff --git a/apps/desktop/src/main/services/api-server/routes/system.ts b/apps/desktop/src/main/services/api-server/routes/system.ts new file mode 100644 index 00000000..3296fe1d --- /dev/null +++ b/apps/desktop/src/main/services/api-server/routes/system.ts @@ -0,0 +1,39 @@ +import { buildAppInfo } from '../../app.js'; +import { getContext } from '../../context.js'; +import { seg, type Route, type RouteHandler } from './shared.js'; + +/** + * 系统性 / 会话级端点:与具体 PR / Agent 无关的工具层信息——身份(whoami)与版本(version)。 + * 对应 CLI 的根层级系统性命令。 + */ + +/** + * 当前身份与集成平台:活动连接的 PAT 所属用户(name / displayName / slug)+ 平台种类 + + * 连接显示名。无活动连接时各项为 null。刻意收窄——不带 capabilities(那是 GUI 降级用的大对象)。 + */ +const whoami: RouteHandler = () => { + const ctx = getContext(); + const activeId = ctx.bootstrap.config.active_connection_id; + const built = activeId + ? ctx.connectionRuntime.adapters.find((a) => a.connectionId === activeId) + : undefined; + if (!activeId || !built) { + return { platform: null, connectionId: null, displayName: null, user: null }; + } + const conn = ctx.bootstrap.config.connections.find((c) => c.id === activeId); + const user = built.adapter.connection.getCurrentUser(); + return { + platform: built.adapter.kind, + connectionId: activeId, + displayName: conn?.display_name ?? activeId, + user: user ? { name: user.name, displayName: user.displayName, slug: user.slug ?? null } : null, + }; +}; + +/** 服务端(桌面应用)版本,供 CLI `version` 同时展示客户端 + 服务端版本。 */ +const version: RouteHandler = () => ({ version: buildAppInfo(getContext().bootstrap).appVersion }); + +export const systemRoutes: Route[] = [ + { method: 'GET', segments: seg('/api/v1/whoami'), handler: whoami }, + { method: 'GET', segments: seg('/api/v1/version'), handler: version }, +]; diff --git a/apps/desktop/src/main/services/api-server/server.ts b/apps/desktop/src/main/services/api-server/server.ts index 75e7a82f..868d469f 100644 --- a/apps/desktop/src/main/services/api-server/server.ts +++ b/apps/desktop/src/main/services/api-server/server.ts @@ -4,7 +4,7 @@ import type { BootstrapResult } from '@meebox/config'; import { ERROR_CODES } from '@meebox/shared'; import type { Logger } from 'pino'; import { HttpError, readJsonBody, sendError, sendOk } from './http.js'; -import { matchRoute } from './routes.js'; +import { matchRoute } from './routes/index.js'; /** * 本地 API 服务监听器(见 docs/arch/04-integration/01-service-api.md)。 diff --git a/docs/arch/04-integration/01-service-api.md b/docs/arch/04-integration/01-service-api.md index 26b3a9b1..dec390a9 100644 --- a/docs/arch/04-integration/01-service-api.md +++ b/docs/arch/04-integration/01-service-api.md @@ -55,6 +55,8 @@ GUI 同源 controller(见下「写边界」)。另有 `POST …/refresh` 触 (`ctx.pr` / `ctx.orchestrator` / `ctx.poller` / `ctx.connectionRuntime` 等),**不重复业务逻辑**。 - 原则:核心能力沉在 service 层,IPC 与 HTTP 各自只做**薄封装 + 协议适配**。新增 API 端点前,先确保对应能力 在 service 层有可复用方法(必要时把 controller 内联逻辑下沉到 service)。 +- **路由按领域分模块**:HTTP 路由处理器按业务领域(系统性 / PR / Agent)分置于独立模块,聚合层只做**注册与路径 + 匹配**、不含业务逻辑——与 [CLI](02-cli.md) 命令树的领域划分对称,新增端点归入对应域、便于定位与扩展。 ### 写边界(放开评审动作,拒绝合并与变更类 Agent 工具) From f4c63da7c35d52d4c27f564e707ce4755881324d Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 11:47:07 +0800 Subject: [PATCH 07/12] =?UTF-8?q?feat(cli):=20=E6=96=B0=E5=A2=9E=20login?= =?UTF-8?q?=20=E5=91=BD=E4=BB=A4=E5=86=99=E5=85=A5=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E5=87=AD=E6=8D=AE=EF=BC=8C=E8=A1=A5=E9=BD=90=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E9=97=AD=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit meebox login --token [--server ]:把 token(与可选 server,默认 loopback) 持久化到 ~/.code-meeseeks/cli.yaml(0600 权限),后续命令免逐次传参。此前 CLI 只能读 cli.yaml (Resolve)却无写入手段,login 补上写侧,使配置管理自洽。根层级系统性命令;纯本地写、不校验令牌 (可随后 whoami 确认)。 settings 增 Save() 与 DefaultAPIURL()(后者同时去重 Resolve 的默认兜底)。同步 SKILL.md / README.md。 Co-Authored-By: Claude Opus 4.8 --- cli/README.md | 10 +++++++ cli/SKILL.md | 16 +++++++---- cli/cmd/integration_test.go | 21 ++++++++++++++ cli/cmd/login.go | 40 ++++++++++++++++++++++++++ cli/cmd/root.go | 1 + cli/internal/settings/settings.go | 30 ++++++++++++++++++- cli/internal/settings/settings_test.go | 18 ++++++++++++ 7 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 cli/cmd/login.go diff --git a/cli/README.md b/cli/README.md index 8adff98a..e2d8a571 100644 --- a/cli/README.md +++ b/cli/README.md @@ -46,6 +46,15 @@ Connection details must be provided explicitly; the CLI does **not** read the ap `~/.code-meeseeks/config.yaml` (which holds connection-layer secrets). The API URL defaults to `http://127.0.0.1:18765` when unset. +`meebox login --token [--server ]` persists the token (and optional server +URL) to `cli.yaml` so later commands need no flags/env — the write counterpart to the +`cli.yaml` read above: + +```bash +meebox login --token # default server http://127.0.0.1:18765 +meebox login --token --server http://host:18765 +``` + ## Commands Root-level `whoami` / `version` need no PR. Two domains — `pr` (also holds `categories` @@ -53,6 +62,7 @@ and `refresh`) and `agent` — carry PR-scoped commands via the required `--pr < (`id` comes from `pr list`): ```text +meebox login --token [--server ] # save credentials to cli.yaml meebox whoami meebox version # CLI (client) + app (server) versions meebox pr categories diff --git a/cli/SKILL.md b/cli/SKILL.md index 0f049de1..d1a6cda0 100644 --- a/cli/SKILL.md +++ b/cli/SKILL.md @@ -19,13 +19,18 @@ review outcomes. Output defaults to YAML; pass `--output json` when parsing resu ## Connect -Provide the API base URL + token explicitly — the CLI never reads the app's `config.yaml`: +Provide the API base URL + token explicitly — the CLI never reads the app's `config.yaml`. +Easiest is `meebox login` (persists to `~/.code-meeseeks/cli.yaml`); or use env vars: ```bash -export MEEBOX_API_URL=http://127.0.0.1:18765 # default port; override for remote hosts -export MEEBOX_TOKEN= # from Settings → Integration -meebox whoami # confirm the resolved user + platform -meebox --output json pr list | jq '.[].id' # JSON for scripting +meebox login --token # save token (default server http://127.0.0.1:18765) +meebox login --token --server http://host:18765 # remote server +# — or, instead of login — +export MEEBOX_API_URL=http://127.0.0.1:18765 # default port; override for remote hosts +export MEEBOX_TOKEN= # from Settings → Integration + +meebox whoami # confirm the resolved user + platform +meebox --output json pr list | jq '.[].id' # JSON for scripting ``` ## Command map @@ -55,6 +60,7 @@ while `pr categories` / `pr refresh` / `pr list` are collection-level (no `--pr` - `meebox pr comment --pr ` — post a top-level comment. Root-level (no PR): +- `meebox login --token [--server ]` — save credentials to `cli.yaml` (default server is loopback); later commands need no flags/env. - `meebox whoami` — current user + platform + connection (confirm your token resolves). - `meebox version` — CLI (client) + app (server) versions; client-only when the server is unreachable. diff --git a/cli/cmd/integration_test.go b/cli/cmd/integration_test.go index 52d25dc0..c5651f6d 100644 --- a/cli/cmd/integration_test.go +++ b/cli/cmd/integration_test.go @@ -5,6 +5,8 @@ import ( "io" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" @@ -139,6 +141,25 @@ func TestVersion(t *testing.T) { } } +func TestLoginWritesConfig(t *testing.T) { + dir := t.TempDir() + t.Setenv("HOME", dir) + t.Setenv("USERPROFILE", dir) + t.Setenv("MEEBOX_API_URL", "") + t.Setenv("MEEBOX_TOKEN", "") + + if _, err := runCmd("login", "--token", "tok123", "--server", "http://saved:9"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + data, err := os.ReadFile(filepath.Join(dir, ".code-meeseeks", "cli.yaml")) + if err != nil { + t.Fatalf("cli.yaml not written: %v", err) + } + if !strings.Contains(string(data), "tok123") || !strings.Contains(string(data), "http://saved:9") { + t.Errorf("cli.yaml missing token/server: %q", string(data)) + } +} + func TestPrListFilters(t *testing.T) { var rec capturedReq srv := mockServer(&rec, 200, `[]`) diff --git a/cli/cmd/login.go b/cli/cmd/login.go new file mode 100644 index 00000000..38b6820b --- /dev/null +++ b/cli/cmd/login.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "fmt" + + "github.com/huhamhire/code-meeseeks/cli/internal/render" + "github.com/huhamhire/code-meeseeks/cli/internal/settings" + "github.com/spf13/cobra" +) + +// newLoginCmd builds `meebox login --token [--server ]`: persists the API token +// (and optional server URL, default loopback) to ~/.code-meeseeks/cli.yaml so later commands +// need no `--token` / env each time. Completes CLI config management — the CLI could read +// cli.yaml but had no way to write it. Purely local: it saves credentials, it does not verify +// them against the server (run `meebox whoami` afterwards to confirm they resolve). +func newLoginCmd() *cobra.Command { + var token, server string + cmd := &cobra.Command{ + Use: "login", + Short: "Save the API token (and optional server URL) to ~/.code-meeseeks/cli.yaml", + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + if server == "" { + server = settings.DefaultAPIURL() + } + path, err := settings.Save(settings.Settings{APIURL: server, Token: token}) + if err != nil { + return err + } + if !gflags.quiet { + fmt.Fprintf(render.Stdout, "Saved credentials for %s to %s\n", server, path) + } + return nil + }, + } + cmd.Flags().StringVar(&token, "token", "", "API bearer token (from Settings → Integration)") + cmd.Flags().StringVar(&server, "server", "", "API base URL (default "+settings.DefaultAPIURL()+")") + _ = cmd.MarkFlagRequired("token") + return cmd +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index cb6ed2f4..b4cceb5b 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -38,6 +38,7 @@ func newRootCmd() *cobra.Command { pf.BoolVar(&gflags.quiet, "quiet", false, "suppress non-essential output") root.AddCommand( + newLoginCmd(), newWhoamiCmd(), newVersionCmd(), newPrCmd(), diff --git a/cli/internal/settings/settings.go b/cli/internal/settings/settings.go index 2fe70a0a..2b2d5b26 100644 --- a/cli/internal/settings/settings.go +++ b/cli/internal/settings/settings.go @@ -74,7 +74,7 @@ func Resolve(ov Overrides) (Settings, error) { } if s.APIURL == "" { - s.APIURL = fmt.Sprintf("http://%s:%d", defaultHost, defaultPort) + s.APIURL = DefaultAPIURL() } if s.Token == "" { return Settings{}, ErrNoToken @@ -82,6 +82,34 @@ func Resolve(ov Overrides) (Settings, error) { return s, nil } +// DefaultAPIURL is the loopback API base URL used when none is provided. +func DefaultAPIURL() string { + return fmt.Sprintf("http://%s:%d", defaultHost, defaultPort) +} + +// Save writes the connection settings to ~/.code-meeseeks/cli.yaml (creating the directory +// if needed) with owner-only file permissions, since the token is a secret. It overwrites +// any existing CLI config. Returns the path written. This is the write counterpart to the +// cli.yaml read in Resolve — the CLI's `login` command uses it to persist credentials. +func Save(s Settings) (string, error) { + home, ok := appHome() + if !ok { + return "", errors.New("cannot resolve home directory") + } + if err := os.MkdirAll(home, 0o700); err != nil { + return "", err + } + path := filepath.Join(home, "cli.yaml") + data, err := yaml.Marshal(cliConfig{APIURL: s.APIURL, Token: s.Token}) + if err != nil { + return "", err + } + if err := os.WriteFile(path, data, 0o600); err != nil { + return "", err + } + return path, nil +} + // appHome returns the app's fixed data directory (~/.code-meeseeks), shared by the GUI // and CLI. The CLI's own config (cli.yaml) lives here; the GUI's config.yaml also lives // here but the CLI never reads it (see package doc). diff --git a/cli/internal/settings/settings_test.go b/cli/internal/settings/settings_test.go index 0f39b2fa..0fb70e3a 100644 --- a/cli/internal/settings/settings_test.go +++ b/cli/internal/settings/settings_test.go @@ -55,3 +55,21 @@ func TestResolveNoTokenErrors(t *testing.T) { t.Fatalf("expected ErrNoToken, got %v", err) } } + +func TestSaveThenResolveRoundTrips(t *testing.T) { + isolateHome(t) + t.Setenv(EnvAPIURL, "") + t.Setenv(EnvToken, "") + + if _, err := Save(Settings{APIURL: "http://saved:9", Token: "saved-token"}); err != nil { + t.Fatalf("save failed: %v", err) + } + // With no flags/env, Resolve must read back exactly what Save wrote. + got, err := Resolve(Overrides{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.APIURL != "http://saved:9" || got.Token != "saved-token" { + t.Fatalf("round-trip mismatch, got %+v", got) + } +} From ef911a0804cab1d1137276404b3a2bf9767a4fea Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 11:47:15 +0800 Subject: [PATCH 08/12] =?UTF-8?q?docs(cli):=20=E8=A1=A5=E5=85=85=20login?= =?UTF-8?q?=20=E5=91=BD=E4=BB=A4=E7=9A=84=E8=BF=9E=E6=8E=A5=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E8=AF=B4=E6=98=8E=E4=B8=8E=E5=91=BD=E4=BB=A4=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- docs/arch/04-integration/02-cli.md | 10 +++++++--- docs/guide/06-cli.md | 12 +++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/arch/04-integration/02-cli.md b/docs/arch/04-integration/02-cli.md index 6ad0cc32..2a36d101 100644 --- a/docs/arch/04-integration/02-cli.md +++ b/docs/arch/04-integration/02-cli.md @@ -45,7 +45,9 @@ CLI 需 API base URL + token。来源优先级(高 → 低): 2. 环境变量:`MEEBOX_API_URL` / `MEEBOX_TOKEN`; 3. CLI 自身配置文件 `~/.code-meeseeks/cli.yaml`(与 GUI 的 `config.yaml` 同目录、独立文件,隔离二者配置)。 -连接信息须**显式提供**(flag / 环境变量 / `cli.yaml` 三者之一),token 缺失即报鉴权错误。 +连接信息须**显式提供**(flag / 环境变量 / `cli.yaml` 三者之一),token 缺失即报鉴权错误。`meebox login +--token [--server ]` 把 token(与可选 server,默认 loopback)写入 `cli.yaml`,免去后续每次传参—— +它是 CLI 唯一的配置**写入**命令,与 `cli.yaml` 的读取(上述优先级)配对,使配置管理自洽。 **不读取 GUI 主配置**:CLI 刻意**不**读应用主配置 `~/.code-meeseeks/config.yaml`。该文件承载连接层机密 (各代码平台的访问令牌等),若从中静默取服务令牌,等于让 CLI 触达其本不应接触的凭据——属预期外的越权访问, @@ -61,8 +63,9 @@ meebox [全局 flag] <组> <命令> [参数] 命令分两类——**根层级系统性命令** 与 **两个领域组**: -- **系统性命令(根层级)** —— `whoami`(身份)、`version`(客户端 + 服务端版本):与具体 PR / Agent 无关的 - 工具 / 会话层信息,直接置于根层级、不套领域组(符合 `kubectl version` / `gh` 等惯例)。 +- **系统性命令(根层级)** —— `login`(保存凭据到 `cli.yaml`)、`whoami`(身份)、`version`(客户端 + + 服务端版本):与具体 PR / Agent 无关的工具 / 会话层操作,直接置于根层级、不套领域组(符合 `kubectl version` + / `gh auth` 等惯例)。 - **`pr`** —— PR 相关操作:浏览 + 评审写动作,并含 `categories`(`pr list` 的筛选词表)与 `refresh` (触发一次拉取、刷新 PR 列表)。 - **`agent`** —— 评审 Agent 操作。 @@ -73,6 +76,7 @@ agent **不嵌进 `pr`**(避免 `pr agent … --pr` 里 `pr` 重复),与 ` | 命令 | 用途 | 对应 API | | --- | --- | --- | +| `meebox login --token [--server ]` | 保存 token(与可选 server,默认 loopback)到 `cli.yaml`,供后续命令免传参 | —(本地写,无 API) | | `meebox whoami` | 当前身份(用户 + 平台 + 连接名) | `GET /whoami` | | `meebox version` | 客户端(CLI)+ 服务端(应用)版本;服务端不可达时仅客户端、退出码仍 0 | `GET /version` | | `meebox pr categories` | 列当前启用平台的分类标签(`categories` 一级 + `statuses` 二级)——`pr list` 的筛选词表 | `GET /categories` | diff --git a/docs/guide/06-cli.md b/docs/guide/06-cli.md index 7ad5e498..72af1f1e 100644 --- a/docs/guide/06-cli.md +++ b/docs/guide/06-cli.md @@ -31,7 +31,16 @@ CLI 依赖应用内的本地 API 服务,默认关闭,需先在 **设置 → 2. 环境变量:`MEEBOX_API_URL` / `MEEBOX_TOKEN` 3. CLI 配置文件:`~/.code-meeseeks/cli.yaml`(字段 `api_url` / `token`) -连接信息须**显式提供**其一。令牌在设置页「集成」分区查看 / 复制。本机免逐次传参推荐用环境变量: +连接信息须**显式提供**其一。令牌在设置页「集成」分区查看 / 复制。最省事的方式是用 `meebox login` 存一次令牌 +(写入 `cli.yaml`),之后所有命令免传参: + +```bash +meebox login --token <令牌> # 默认连本机 http://127.0.0.1:18765 +meebox login --token <令牌> --server http://<主机>:18765 # 指定远端服务 +meebox pr list # 后续命令直接用已存的凭据 +``` + +或用环境变量(适合 CI / shell 注入): ```bash export MEEBOX_API_URL=http://127.0.0.1:18765 @@ -60,6 +69,7 @@ PR(`id` 由 `meebox pr list` 输出获得)。 | 命令 | 用途 | | --- | --- | +| `meebox login --token <令牌> [--server <地址>]` | 保存令牌(与可选服务地址)到 `cli.yaml`,后续命令免传参 | | `meebox whoami` | 当前登录身份与集成平台(用户 + 平台 + 连接名) | | `meebox version` | 客户端(CLI)+ 服务端(应用)版本;未连接服务端时仅显示客户端版本 | | `meebox pr categories` | 列出当前平台可用的分类标签(一级发现分类 + 二级状态 / 合并态筛选)——`pr list` 的筛选词表 | From de20115b7a9ee6e2d0ce3ec2c2ee0b6c73a31f72 Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 11:52:23 +0800 Subject: [PATCH 09/12] =?UTF-8?q?docs(readme):=20=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E7=89=B9=E6=80=A7=E5=BC=BA=E5=8C=96=20CLI=20/=20API=20?= =?UTF-8?q?=E7=9A=84=E5=A4=96=E9=83=A8=20Agent=20=E9=9B=86=E6=88=90?= =?UTF-8?q?=E8=A1=A8=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 564b51bd..d878cffd 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,9 @@ Code Meeseeks(内部开发代号 `meebox`)是命令行工具 [pr-agent](http #### 🔌 外部集成与 CLI -- **本地 API 服务** —— 可选开启一个本机 API,把 PR 浏览与评审 Agent 操作以接口形式开放给外部 agent / 脚本;默认仅本机可达、强制访问令牌鉴权,不开放合并与变更类工具。 -- **跨平台命令行工具 `meebox`** —— 随发布提供 Windows / macOS / Linux 命令行客户端,经本地 API 浏览 PR、操作评审 Agent 并执行评审写动作(approve / needswork / comment),便于脚本与外部 agent 集成;压缩包即 agent skill 目录,可直接投放。用法见 **[CLI 命令行工具](docs/guide/06-cli.md)**。 +- **可供外部 Agent 集成** —— PR 评审能力经本机 HTTP 接口 + 跨平台 CLI 对外开放,让本机 agentic 工具(如 claude / codex)、脚本、CI 把 PR 发现 / 浏览 / 评审 Agent 操作纳入自动化流程。 +- **本地 API 服务** —— 可选开启一个本机 API,将 PR 发现 / 浏览 / diff / 评审 Agent 操作 / 评审写动作以语言无关的 HTTP 契约开放出来;默认仅本机可达、强制访问令牌鉴权,不开放合并与变更类工具。 +- **跨平台命令行工具 `meebox`** —— 随发布提供 Windows / macOS / Linux 客户端,经本地 API 浏览 PR、驱动评审 Agent 并执行评审写动作(approve / needswork / comment);`meebox login` 一次存好凭据即可免传参,**压缩包即 agent skill 目录、可直接投放到 agent 的 skills 目录**。用法见 **[CLI 命令行工具](docs/guide/06-cli.md)**。 #### 🎨 界面与体验 From 41bff27f12c542622099365fb0b69b8af8acf1f9 Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 12:50:38 +0800 Subject: [PATCH 10/12] =?UTF-8?q?feat(cli):=20SKILL.md=20=E7=BB=8F=20go:em?= =?UTF-8?q?bed=20=E5=86=85=E5=B5=8C=E4=BA=8C=E8=BF=9B=E5=88=B6=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20skill=20=E5=91=BD=E4=BB=A4=E8=87=AA?= =?UTF-8?q?=E8=BF=B0=E7=94=A8=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 构建期用 go:embed 把 SKILL.md 内嵌进二进制,meebox skill 打印之——二进制即便脱离发布压缩包 (go install / 裸放 PATH)也能自述 agent 用法,且内嵌内容与随包 SKILL.md 构建期一致。根层级 系统性命令。刻意不做 --manifest 之类 function-calling JSON:skill 的消费形态是 markdown、非工具 schema 注入,真有此需求应从命令树生成而非另手维护。 Execute / newRootCmd 增 skillDoc 形参由 main 注入(embed 须置于 SKILL.md 所在的根包)。 Co-Authored-By: Claude Opus 4.8 --- cli/README.md | 1 + cli/SKILL.md | 1 + cli/cmd/integration_test.go | 15 ++++++++++++++- cli/cmd/root.go | 10 ++++++---- cli/cmd/skill.go | 27 +++++++++++++++++++++++++++ cli/main.go | 16 ++++++++++++++-- 6 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 cli/cmd/skill.go diff --git a/cli/README.md b/cli/README.md index e2d8a571..ee11a829 100644 --- a/cli/README.md +++ b/cli/README.md @@ -65,6 +65,7 @@ and `refresh`) and `agent` — carry PR-scoped commands via the required `--pr < meebox login --token [--server ] # save credentials to cli.yaml meebox whoami meebox version # CLI (client) + app (server) versions +meebox skill # print the embedded agent usage doc (SKILL.md) meebox pr categories meebox pr refresh # trigger one immediate poll for the latest PRs meebox pr list [--category ] [--status ] [--query ] [--skip N] [--limit N] diff --git a/cli/SKILL.md b/cli/SKILL.md index d1a6cda0..bd0d8916 100644 --- a/cli/SKILL.md +++ b/cli/SKILL.md @@ -63,6 +63,7 @@ Root-level (no PR): - `meebox login --token [--server ]` — save credentials to `cli.yaml` (default server is loopback); later commands need no flags/env. - `meebox whoami` — current user + platform + connection (confirm your token resolves). - `meebox version` — CLI (client) + app (server) versions; client-only when the server is unreachable. +- `meebox skill` — print this doc (SKILL.md is embedded in the binary), so usage is retrievable even without the file. ## Typical loop diff --git a/cli/cmd/integration_test.go b/cli/cmd/integration_test.go index c5651f6d..9def1eb6 100644 --- a/cli/cmd/integration_test.go +++ b/cli/cmd/integration_test.go @@ -49,6 +49,9 @@ func mockServer(rec *capturedReq, status int, dataJSON string) *httptest.Server })) } +// testSkillDoc stands in for the embedded SKILL.md (main injects the real one at build). +const testSkillDoc = "# meebox (test skill doc)\n" + // runCmd runs the root command with captured output, returning stdout + the error // (Execute()'s os.Exit wrapper is bypassed so tests can assert on the error). func runCmd(args ...string) (string, error) { @@ -57,7 +60,7 @@ func runCmd(args ...string) (string, error) { render.Stdout, render.Stderr = &buf, io.Discard defer func() { render.Stdout, render.Stderr = origOut, origErr }() - root := newRootCmd() + root := newRootCmd(testSkillDoc) root.SetArgs(args) err := root.Execute() return buf.String(), err @@ -160,6 +163,16 @@ func TestLoginWritesConfig(t *testing.T) { } } +func TestSkillPrintsEmbeddedDoc(t *testing.T) { + out, err := runCmd("skill") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if out != testSkillDoc { + t.Errorf("skill output = %q, want embedded doc %q", out, testSkillDoc) + } +} + func TestPrListFilters(t *testing.T) { var rec capturedReq srv := mockServer(&rec, 200, `[]`) diff --git a/cli/cmd/root.go b/cli/cmd/root.go index b4cceb5b..cbd24145 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -23,7 +23,7 @@ type globalFlags struct { var gflags globalFlags -func newRootCmd() *cobra.Command { +func newRootCmd(skillDoc string) *cobra.Command { root := &cobra.Command{ Use: "meebox", Short: "Code Meeseeks CLI — integrate PR review capabilities over the local API", @@ -41,6 +41,7 @@ func newRootCmd() *cobra.Command { newLoginCmd(), newWhoamiCmd(), newVersionCmd(), + newSkillCmd(skillDoc), newPrCmd(), newAgentCmd(), ) @@ -48,9 +49,10 @@ func newRootCmd() *cobra.Command { } // Execute runs the root command, printing errors to stderr and mapping them -// to process exit codes per docs/arch/04-integration/02-cli.md. -func Execute() { - if err := newRootCmd().Execute(); err != nil { +// to process exit codes per docs/arch/04-integration/02-cli.md. skillDoc is the +// SKILL.md embedded into the binary (see main), surfaced by `meebox skill`. +func Execute(skillDoc string) { + if err := newRootCmd(skillDoc).Execute(); err != nil { render.Errorln(err) os.Exit(render.ExitCodeFor(err)) } diff --git a/cli/cmd/skill.go b/cli/cmd/skill.go new file mode 100644 index 00000000..d388361c --- /dev/null +++ b/cli/cmd/skill.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "fmt" + + "github.com/huhamhire/code-meeseeks/cli/internal/render" + "github.com/spf13/cobra" +) + +// newSkillCmd builds `meebox skill`: prints the agent-facing usage doc (SKILL.md) embedded +// into the binary at build time. Lets the binary self-document even when detached from its +// release archive (self-introspection), and keeps the emitted doc in lock-step with the +// shipped SKILL.md. doc is injected from main; empty only in bare unit-test builds. +func newSkillCmd(doc string) *cobra.Command { + return &cobra.Command{ + Use: "skill", + Short: "Print the embedded agent usage doc (SKILL.md)", + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + if doc == "" { + return fmt.Errorf("skill doc not embedded in this build") + } + fmt.Fprint(render.Stdout, doc) + return nil + }, + } +} diff --git a/cli/main.go b/cli/main.go index 9b078614..3e6130bf 100644 --- a/cli/main.go +++ b/cli/main.go @@ -6,8 +6,20 @@ // not provided — integrators implement those against the platform directly. package main -import "github.com/huhamhire/code-meeseeks/cli/cmd" +import ( + _ "embed" + + "github.com/huhamhire/code-meeseeks/cli/cmd" +) + +// skillDoc is SKILL.md embedded at build time so the binary can self-document its agent +// usage (`meebox skill`) even when detached from its release archive. Embedding keeps the +// emitted doc in lock-step with the shipped SKILL.md. The embed must live in this root +// package because SKILL.md sits at the module root (go:embed cannot reach parent dirs). +// +//go:embed SKILL.md +var skillDoc string func main() { - cmd.Execute() + cmd.Execute(skillDoc) } From 3b35c1ee914b1caa4e0fd0306cabed56b725ca85 Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 12:51:03 +0800 Subject: [PATCH 11/12] =?UTF-8?q?docs(cli):=20=E8=A1=A5=E5=85=85=20skill?= =?UTF-8?q?=20=E5=91=BD=E4=BB=A4=E4=B8=8E=20Agent=20Skill=20=E9=9B=86?= =?UTF-8?q?=E6=88=90=E4=B8=80=E8=8A=82=EF=BC=88=E5=90=AB=E6=A1=86=E6=9E=B6?= =?UTF-8?q?=E6=97=A0=E5=85=B3=E6=8E=A5=E5=85=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit arch 补 skill 命令与 go:embed / 不做 manifest 的设计说明;guide 新增「作为 Agent Skill 集成」 一节,说明投放即用、二进制自述、集成流程与边界,并澄清 SKILL.md 自动发现是 Claude 约定、 其它框架经 shell + meebox skill + --output json 接入。 Co-Authored-By: Claude Opus 4.8 --- docs/arch/04-integration/02-cli.md | 9 +++++++-- docs/guide/06-cli.md | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/arch/04-integration/02-cli.md b/docs/arch/04-integration/02-cli.md index 2a36d101..cbe4079e 100644 --- a/docs/arch/04-integration/02-cli.md +++ b/docs/arch/04-integration/02-cli.md @@ -64,8 +64,8 @@ meebox [全局 flag] <组> <命令> [参数] 命令分两类——**根层级系统性命令** 与 **两个领域组**: - **系统性命令(根层级)** —— `login`(保存凭据到 `cli.yaml`)、`whoami`(身份)、`version`(客户端 + - 服务端版本):与具体 PR / Agent 无关的工具 / 会话层操作,直接置于根层级、不套领域组(符合 `kubectl version` - / `gh auth` 等惯例)。 + 服务端版本)、`skill`(打印内嵌的 SKILL.md):与具体 PR / Agent 无关的工具 / 会话层操作,直接置于根层级、 + 不套领域组(符合 `kubectl version` / `gh auth` 等惯例)。 - **`pr`** —— PR 相关操作:浏览 + 评审写动作,并含 `categories`(`pr list` 的筛选词表)与 `refresh` (触发一次拉取、刷新 PR 列表)。 - **`agent`** —— 评审 Agent 操作。 @@ -79,6 +79,7 @@ agent **不嵌进 `pr`**(避免 `pr agent … --pr` 里 `pr` 重复),与 ` | `meebox login --token [--server ]` | 保存 token(与可选 server,默认 loopback)到 `cli.yaml`,供后续命令免传参 | —(本地写,无 API) | | `meebox whoami` | 当前身份(用户 + 平台 + 连接名) | `GET /whoami` | | `meebox version` | 客户端(CLI)+ 服务端(应用)版本;服务端不可达时仅客户端、退出码仍 0 | `GET /version` | +| `meebox skill` | 打印构建时 `go:embed` 内嵌的 agent 使用说明(SKILL.md) | —(本地,无 API) | | `meebox pr categories` | 列当前启用平台的分类标签(`categories` 一级 + `statuses` 二级)——`pr list` 的筛选词表 | `GET /categories` | | `meebox pr refresh` | 触发一次立即轮询刷新(拉取最新 PR、落本地),返回本轮计数汇总(fetched / changed / added / removed / errors);等价 GUI 手动刷新 | `POST /refresh` | | `meebox pr list [--category <一级>] [--status <二级>] [--query <检索>] [--skip N] [--limit N]` | PR 列表(精简投影 + 分页,默认 limit 100) | `GET /prs` | @@ -129,6 +130,10 @@ agent **不嵌进 `pr`**(避免 `pr agent … --pr` 里 `pr` 重复),与 ` - **压缩包内容 = 可直接投放的 skill 目录**:除二进制外一并打包 `LICENSE` + `README.md` + `SKILL.md`。解压到 agent 的 skills 目录即得一个可用 skill——`SKILL.md`(frontmatter `name: meebox`)教 agent 用法,紧邻其驱动 的二进制。这是 CLI「面向 agent 交付」的主形态。 +- **二进制自述(`go:embed`)**:同一份 `SKILL.md` 经 `go:embed` 于构建期内嵌进二进制,`meebox skill` 打印之。 + 即便二进制脱离压缩包(如 `go install` 或裸放 `PATH`)也能自述用法,且内嵌内容与随包 `SKILL.md` 构建期一致。 + 刻意**不做** `--manifest` 之类的 function-calling JSON——skill 的消费形态是 markdown,非工具 schema 注入; + 真有此需求应从命令树生成、而非另手维护一份 JSON。 ## 分发与 CI diff --git a/docs/guide/06-cli.md b/docs/guide/06-cli.md index 72af1f1e..618dd1a1 100644 --- a/docs/guide/06-cli.md +++ b/docs/guide/06-cli.md @@ -72,6 +72,7 @@ PR(`id` 由 `meebox pr list` 输出获得)。 | `meebox login --token <令牌> [--server <地址>]` | 保存令牌(与可选服务地址)到 `cli.yaml`,后续命令免传参 | | `meebox whoami` | 当前登录身份与集成平台(用户 + 平台 + 连接名) | | `meebox version` | 客户端(CLI)+ 服务端(应用)版本;未连接服务端时仅显示客户端版本 | +| `meebox skill` | 打印内嵌的使用说明(SKILL.md),便于二进制脱离压缩包时自述用法 | | `meebox pr categories` | 列出当前平台可用的分类标签(一级发现分类 + 二级状态 / 合并态筛选)——`pr list` 的筛选词表 | | `meebox pr refresh` | 触发一次立即刷新(拉取最新 PR),返回本轮变化计数(新增 / 变更 / 移除等);等同 GUI 里的手动刷新 | | `meebox pr list [--category <一级>] [--status <二级>] [--query <检索>] [--skip N] [--limit N]` | PR 列表(精简字段 + 分页,默认 limit 100) | @@ -107,6 +108,24 @@ meebox pr list --output json | jq '.[].title' **退出码**:`0` 成功;非 0 表错误(`2` 鉴权失败、`3` 资源不存在、`1` 其他);错误信息打到 `stderr`。 +## 6. 作为 Agent Skill 集成 + +`meebox` 的主要交付形态是**可直接投放的 agent skill**:发布压缩包除二进制外一并含 `SKILL.md` / +`README.md` / `LICENSE`,整个解压目录即是一个可用 skill。 + +- **投放即用**:把解压目录放进 agent 的 skills 目录(如 `~/.claude/skills/meebox/`)。`SKILL.md` + (frontmatter `name: meebox`)向 agent 说明命令树、连接方式与写边界,紧邻其驱动的二进制。 +- **二进制自述**:同一份 `SKILL.md` 于构建期经 `go:embed` 内嵌进二进制,`meebox skill` 可打印之—— + 二进制即便脱离压缩包(如单独放入 `PATH`)也能取回用法,且内容与随包文档构建期一致。 +- **集成流程**:读 `SKILL.md` 了解能力 → `meebox login` 存一次凭据 → 以 `meebox pr list` / `pr show` / + `agent review` 等浏览与驱动评审 → 用 `meebox pr approve` / `needswork` / `comment` 记录结论;机器消费统一 + 取 `--output json`(其字段形状为稳定契约)。 +- **边界内建**:仅开放浏览 + 评审写动作,不含合并与变更类工具(详见下「注意事项」),agent 集成天然不会触发 + 高影响远端操作。 +- **框架无关的接入**:`SKILL.md` 的自动发现是 Claude Code 的 skill 约定,并非跨框架标准。其它 agent / 脚本 + 无需依赖该约定即可集成——直接以 shell 调用 `meebox`、用 `meebox skill` 或 `--help` 取用法、`--output json` + 取结构化结果。真正可移植的接口是「命令行 + JSON」,`SKILL.md` 自动发现只是 Claude 生态的锦上添花。 + ## 网络代理 `meebox` 遵循标准的 HTTP 代理环境变量(`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`,大小写均可),无需额外配置: From 3badea45fa2dfa0d19ec7011f31a6a2b1623f043 Mon Sep 17 00:00:00 2001 From: Hamhire Hu Date: Thu, 2 Jul 2026 12:55:31 +0800 Subject: [PATCH 12/12] =?UTF-8?q?docs(cli):=20=E8=A1=A5=E5=85=85=E4=BB=85?= =?UTF-8?q?=E6=9C=89=E4=BA=8C=E8=BF=9B=E5=88=B6=E6=97=B6=E7=BB=8F=20meebox?= =?UTF-8?q?=20skill=20=E9=87=8D=E5=BB=BA=20SKILL.md=20=E7=9A=84=20fallback?= =?UTF-8?q?=20=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit guide「Agent Skill 集成」一节与 README 补充:手头只有二进制(缺压缩包 / SKILL.md)时, 可用 `meebox skill > SKILL.md` 从内嵌副本导出、就地重建 skill 目录,内容与二进制同源。 Co-Authored-By: Claude Opus 4.8 --- cli/README.md | 4 +++- docs/guide/06-cli.md | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cli/README.md b/cli/README.md index ee11a829..7b894ea3 100644 --- a/cli/README.md +++ b/cli/README.md @@ -12,7 +12,9 @@ merging and the agent's publish/mutating tools are intentionally not exposed. `meebox` is also shipped as a drop-in agent **skill** — each release archive bundles [`SKILL.md`](SKILL.md) beside the binary, so unzipping it into an agent's skills -directory yields a working skill. +directory yields a working skill. `SKILL.md` is also embedded in the binary: if you have +only the binary, `meebox skill > SKILL.md` reconstructs the doc (a fallback for rebuilding +the skill directory without the archive). ## Build & run diff --git a/docs/guide/06-cli.md b/docs/guide/06-cli.md index 618dd1a1..4064cd37 100644 --- a/docs/guide/06-cli.md +++ b/docs/guide/06-cli.md @@ -117,6 +117,16 @@ meebox pr list --output json | jq '.[].title' (frontmatter `name: meebox`)向 agent 说明命令树、连接方式与写边界,紧邻其驱动的二进制。 - **二进制自述**:同一份 `SKILL.md` 于构建期经 `go:embed` 内嵌进二进制,`meebox skill` 可打印之—— 二进制即便脱离压缩包(如单独放入 `PATH`)也能取回用法,且内容与随包文档构建期一致。 +- **仅有二进制的 fallback**:若手头只有 `meebox` 二进制(缺压缩包 / `SKILL.md` 文件),用 `meebox skill` + 即可从二进制导出说明、就地重建 skill 目录,无需另找原始文件: + + ```bash + mkdir -p ~/.claude/skills/meebox + cp "$(command -v meebox)" ~/.claude/skills/meebox/ # 二进制放入 skill 目录 + meebox skill > ~/.claude/skills/meebox/SKILL.md # 从内嵌副本导出说明 + ``` + + 导出的内容与该二进制同源,天然匹配当前版本。 - **集成流程**:读 `SKILL.md` 了解能力 → `meebox login` 存一次凭据 → 以 `meebox pr list` / `pr show` / `agent review` 等浏览与驱动评审 → 用 `meebox pr approve` / `needswork` / `comment` 记录结论;机器消费统一 取 `--output json`(其字段形状为稳定契约)。