给 My-Notion 做一个 Agent-Native CLI

9 阅读11分钟

这是我在开发 My-Notion 时做的一次工程化改造。

一开始,My-Notion 已经有了 Web 端文档编辑器、AI 侧边栏、RAG、Memory 和 Convex 后端。但如果我想让外部 AI Agent 帮我创建一篇文档,流程仍然很别扭:Agent 要么只能停留在“生成 Markdown 文本”,要么就需要我手动复制内容到网页里。

所以我做了一个 CLI:

npm install -g @mynotion/cli@beta
my-notion auth login
my-notion docs create --title "Agent Doc" --content-file ./doc.md

现在它已经发布到了 npm:www.npmjs.com/package/@my…

这篇文章不只是介绍命令怎么用,而是复盘一下:为什么一个 Notion-like 项目需要 CLI、这个 CLI 到底解决了什么问题、认证和安全边界怎么设计、MCP 和 Skills 为什么要一起做,以及发布 npm 包时踩过哪些坑。

为什么要做 CLI

My-Notion 的核心目标不是只做一个“网页版 Notion”,而是做一个 AI 原生的个人知识工作台。

Web 端负责人类交互:

用户
 └─ Web UI
    ├─ 文档树
    ├─ BlockNote 编辑器
    ├─ AI 侧边栏
    └─ 设置页 / Token 管理

但 Agent 的操作入口不应该依赖浏览器 UI。

我希望 Agent 能做这些事:

AI Agent
 ├─ 搜索 My-Notion 文档
 ├─ 读取文档内容
 ├─ 生成一篇 Markdown
 ├─ 安全写入 My-Notion
 └─ 必要时通过 MCP 工具调用

如果没有 CLI,中间会出现几个问题:

  1. Agent 无法稳定调用 Web UI。

  2. 让用户复制 Token 给 Agent 很危险。

  3. 文档写入缺少统一的错误契约和审计链路。

  4. MCP、Skills、脚本集成会各写一套逻辑,后面很难维护。

所以 CLI 的定位很明确:

My-Notion CLI = 人类用户 + AI Agent 共用的机器入口

它不是 Web 的替代品,而是 Web 能力的 Agent Adapter。

最终能力长什么样

当前 CLI 包名是 @mynotion/cli,命令名是 my-notion

核心能力分成四类:

模块能力
auth浏览器 Device Flow 登录、状态检查、退出登录
docs创建、读取、搜索、列出、更新、导入、导出、归档文档
tokens撤销当前 CLI Token
mcp启动 MCP STDIO server,暴露文档工具给 Agent

典型的人类用户流程是:

# 1. 安装 CLI
npm install -g @mynotion/cli@beta

# 2. 浏览器授权
my-notion auth login

# 3. 搜索文档
my-notion docs search --query "项目周报" --format json

# 4. 从 Markdown 创建文档
my-notion docs create \
  --title "项目周报" \
  --content-file ./weekly-report.md \
  --format json

典型的 Agent 流程是:

# 1. 检查登录态
my-notion auth status --format json

# 2. 如未登录,生成浏览器授权链接
my-notion auth login --no-open

# 3. 授权完成后写入文档
my-notion docs create \
  --title "调研报告" \
  --content-file /tmp/report.md \
  --format json

Agent 不需要知道配置文件在哪,也不应该把内部调试信息一股脑输出给用户。它只需要告诉用户:

请打开 [My-Notion CLI 授权](https://notion-j9zj.vercel.app/cli/auth?user_code=XXXX-XXXX),确认页面验证码为 XXXX-XXXX。

这也是这次 CLI 设计里很重要的一点:对人类隐藏细节,对机器保持稳定。

架构怎么串起来

整个链路可以拆成四层:

Agent / Human
  ↓
@mynotion/cli
  ↓
Convex HTTP Actions / Machine API
  ↓
Convex Database
  ↓
Web / Mobile 实时展示

更完整一点:

用户 / Agent
  ├─ my-notion docs create
  ├─ my-notion docs search
  └─ my-notion mcp serve --transport stdio

CLI
  ├─ auth: Device Flow
  ├─ docs: 文档 CRUD
  ├─ mcp: MCP STDIO server
  └─ config: prod / local 配置隔离

服务端
  ├─ /cli/v1/auth/*
  ├─ /cli/v1/docs/*
  ├─ PAT hash 校验
  ├─ requestId
  ├─ audit log
  └─ rate limit

这里有一个关键决策:CLI 不直接连接 Convex client runtime,而是走 Convex HTTP Actions 暴露出来的 Machine API。

原因是:

  1. CLI 是机器调用入口,需要稳定 HTTP 协议。

  2. 服务端必须自己解析 Token 对应的用户身份,不能信任客户端传来的 userId

  3. 错误码、限流、审计、requestId 都应该在 Machine API 层统一处理。

  4. 未来 MCP、Skills、CI、外部脚本都可以复用同一套接口。

认证:为什么不用手动复制 Token

最早的 CLI 认证方式很简单:用户在 Web 设置页复制一个 mnt_... Token,然后粘贴给 CLI。

这个方案能跑,但不适合 Agent 场景。

问题主要有三个:

  1. Agent 聊天窗口里容易泄漏完整 Token。

  2. 用户不知道什么时候该复制、复制到哪里。

  3. 本地调试和线上登录态容易混在一起。

所以后来改成了类似飞书 CLI 的浏览器 Device Flow。

流程是这样的:

CLI 发起登录
  ↓
服务端生成 device_code + user_code
  ↓
CLI 只展示带 user_code 的授权 URL
  ↓
用户在浏览器里登录 Clerk 并确认授权
  ↓
CLI 轮询授权结果
  ↓
服务端签发 CLI PAT
  ↓
CLI 保存到本地配置

注意这里的安全边界:

可以展示给用户:
  user_code
  https://notion-j9zj.vercel.app/cli/auth?user_code=XXXX-XXXX

不能展示给用户或日志:
  device_code
  完整 mnt_ Token

授权 URL 只能包含 user_code,不能把 device_code 放进 URL。

这样 Agent 只需要发一个 Markdown 链接给用户:

请打开 [My-Notion CLI 授权](https://notion-j9zj.vercel.app/cli/auth?user_code=XXXX-XXXX),确认页面验证码为 XXXX-XXXX。

用户确认后,CLI 自己完成轮询和写配置。

配置隔离:线上和本地不能混

CLI 默认面向线上用户,所以默认 profile 是 prod

~/.local/share/my-notion/config.json

本地开发则必须显式使用 --local

my-notion auth login \
  --local \
  --web-url http://localhost:3000 \
  --api-url "https://<dev-deployment>.convex.site"

本地配置单独存放:

~/.local/share/my-notion/config.local.json

这个隔离看起来只是一个小细节,但实际非常重要。

如果线上和本地复用同一个配置文件,调试时很容易出现:

以为自己在调本地
  ↓
实际请求打到了线上
  ↓
测试文档写进了生产环境

或者反过来:

以为 CLI 默认连线上
  ↓
配置里残留了本地 dev URL
  ↓
用户安装 npm 包后一运行就失败

所以现在的规则是:

  1. 默认永远连接线上。

  2. 本地调试必须显式 --local

  3. prod 和 local 使用不同配置文件。

  4. Agent 不需要向用户输出配置路径,只保留核心操作结果。

MCP:为什么不是只做 CLI 命令

CLI 命令适合脚本式调用:

my-notion docs create --title "总结" --content-file ./summary.md

但越来越多 Agent 开始通过 MCP 使用工具。为了让 My-Notion 能接入这类 Agent,我又在 CLI 里做了一个 MCP STDIO server:

my-notion mcp serve --transport stdio

当前暴露了四个工具:

Tool说明
my_notion_docs_search搜索文档
my_notion_docs_fetch读取文档
my_notion_docs_create创建文档
my_notion_docs_update更新文档

这里最重要的不是“能不能写”,而是默认不能直接写。

MCP 写工具默认 dryRun: true

{
  "title": "调研报告",
  "contentMarkdown": "# 调研报告\n\n...",
  "dryRun": true
}

dry-run 结果会明确告诉 Agent:

Dry run only. No My-Notion document was created.
Set dryRun=false only after explicit user approval.

也就是说,Agent 的安全链路是:

Dry-run
  ↓
Preview
  ↓
User Confirmation
  ↓
Commit

这比“Agent 调用工具就直接写入”要麻烦一点,但对于个人知识库来说,这个麻烦是值得的。

Skills:让 Agent 少猜一点

除了 CLI 和 MCP,我还把使用说明整理成了 Skills:

packages/my-notion-skills/
  ├─ my-notion-shared
  ├─ my-notion-docs
  └─ my-notion-mcp

发布 npm 包时,这些 Skills 会同步进 CLI 包:

packages/my-notion-cli/skills/

安装方式:

npx skills add @mynotion/cli -y -g

为什么要做 Skills?

因为如果只给 Agent 一个 README,它很容易每次都自由发挥:

  1. 有时让用户手动复制 Token。

  2. 有时输出一堆配置路径和调试信息。

  3. 有时忘记用 --format json

  4. 有时直接真实写入,不做 dry-run。

Skills 的作用就是把这些约束写成 Agent 能稳定遵守的操作规程:

授权链接必须是 Markdown 可点击链接
不要输出完整 Token
不要输出 device_code
长内容使用临时 Markdown 文件
MCP 写工具默认 dryRun
用户确认后才能真实写入

这类规则对人类来说有点啰嗦,但对 Agent 很关键。

npm 发布:从本地包到 @mynotion/cli

CLI 做完后,下一步就是发布 npm 包。

最终包信息是:

package: @mynotion/cli
version: 0.1.0-beta.0
bin:     my-notion
tag:     beta

package.json 里最关键的是这些字段:

{
  "name": "@mynotion/cli",
  "version": "0.1.0-beta.0",
  "bin": {
    "my-notion": "dist/index.js"
  },
  "files": [
    "dist",
    "README.md",
    "CHANGELOG.md",
    "docs",
    "skills",
    "LICENSE"
  ],
  "publishConfig": {
    "access": "public"
  }
}

发布前检查:

pnpm --filter @mynotion/cli test
pnpm --filter @mynotion/cli typecheck
pnpm --filter @mynotion/cli build
pnpm e2e:cli
pnpm e2e:cli:errors
pnpm e2e:mcp
pnpm sync:skills
pnpm sync:skills:package
pnpm sync:skills:check
cd packages/my-notion-cli && npm pack --dry-run

发布命令:

cd packages/my-notion-cli
npm publish --tag beta --access public

如果使用本地 token 文件:

npm publish --tag beta --access public --userconfig ./.npmrc.publish

这里也踩了一个小坑:scoped package 首次发布必须显式 --access public,否则 npm 会按私有包处理或直接报权限问题。

另外,如果账号开启了更严格的安全策略,还需要使用支持发布的 npm Access Token。这个 token 只能放在本地忽略文件里,不能提交到仓库。

错误契约:让 Agent 能看懂失败

CLI 给人用时,错误信息“差不多能看懂”就行。

但给 Agent 用时,错误必须稳定。

所以 Machine API 和 CLI 统一了错误契约:

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests",
    "requestId": "req_xxx"
  }
}

典型错误包括:

错误码含义
UNAUTHORIZED未登录或 Token 无效
TOKEN_REVOKEDToken 已撤销
TOKEN_EXPIREDToken 过期
INSUFFICIENT_SCOPE权限不足
NOT_FOUND文档不存在
VALIDATION_ERROR参数校验失败
RATE_LIMITED触发限流

还有几个细节:

  1. 所有响应都带 requestId

  2. 429 会带 Retry-Afterx-ratelimit-* headers。

  3. CLI 不会重试结构化的 RATE_LIMITED

  4. 审计日志记录 token prefix,但不记录完整 Token。

这些东西看起来不像产品功能,但它们决定了 Agent 集成能不能稳定运行。

这次开发里最重要的几个取舍

1. Markdown 还是 BlockNote JSON

当前 CLI 让 Agent 写的是 Markdown:

my-notion docs create --title "标题" --content-file ./doc.md

但 Web 编辑器底层是 BlockNote blocks。

这里有两条路线:

方案 A:Agent 输出 Markdown
  ↓
服务端或 CLI 转成 BlockNote blocks

方案 B:Agent 直接输出 BlockNote JSON
  ↓
服务端校验后写入

我目前更倾向于先保持 Markdown 作为 Agent 输入格式。

原因是:

  1. Markdown 对人类和 Agent 都更自然。

  2. 失败时容易调试。

  3. CLI 导入导出可以保持一致。

  4. 未来可以在服务端集中做 Markdown -> BlockNote 转换。

但这件事还没有完全定案。复杂块、图片、表格、嵌套结构越来越多时,受约束的 BlockNote JSON 也可能更准确。

所以后续会专门做一次格式策略决策,而不是现在草率拍板。

2. 初始化入口还不够完整

现在 CLI 已经可以安装、登录、写文档:

npm install -g @mynotion/cli@beta
my-notion auth login

但它还缺一个类似飞书 CLI 的统一初始化入口。

理想状态应该是:

my-notion config init

或者:

my-notion install

一次性完成:

  1. 检查 Node.js 版本。

  2. 检查 CLI 版本。

  3. 检查登录态。

  4. 引导浏览器授权。

  5. 检查 Skills 是否安装。

  6. 输出 MCP 配置建议。

  7. 给出下一步命令。

现在这项已经被提到最高优先级,因为用户不应该等到第一次执行业务命令时才发现自己没初始化。

3. MCP 还需要真实 Client 验证

虽然已经有 pnpm e2e:mcp,但真实 Agent 场景里的体验还要继续验证。

接下来要重点看:

  1. MCP Client 能不能正确发现工具。

  2. STDIO server 启动和退出是否稳定。

  3. dry-run 预览是否足够清楚。

  4. 用户确认后的真实写入是否顺滑。

  5. 错误结果是否能被 Agent 正确解释。

自动化测试能保证协议不坏,但不能完全代表真实 Agent 体验。

总结

这次做 @mynotion/cli,表面上是在给 My-Notion 加一个命令行工具,实际上是在补一条 Agent 操作通道。

最终链路是:

Agent 生成内容
  ↓
CLI / MCP 接收任务
  ↓
Device Flow 安全授权
  ↓
Machine API 校验 Token 和权限
  ↓
Convex 写入文档
  ↓
Web / Mobile 实时展示

这条链路让我比较满意的地方是:

  1. 用户不需要把完整 Token 粘贴给 Agent。

  2. Agent 可以用稳定 JSON 输出和错误码做自动化处理。

  3. MCP 写工具默认 dry-run,降低误写风险。

  4. Skills 把 Agent 操作规程固化下来,减少每次对话自由发挥。

  5. npm 包发布后,外部 Agent 可以直接安装和使用。

如果你也在做 AI 原生应用,我觉得 CLI 可能不是一个“附属功能”,而是很关键的一层。

Web UI 解决人怎么用,CLI / MCP 解决 Agent 怎么用。

当你的产品开始让 Agent 参与真实写入、真实编辑、真实自动化时,这层边界迟早要补上。

本文基于 My-Notion 项目的真实开发过程整理。项目是一个 AI 原生的个人版 Notion,CLI npm 包地址:@mynotion/cli