这是我在开发 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,中间会出现几个问题:
-
Agent 无法稳定调用 Web UI。
-
让用户复制 Token 给 Agent 很危险。
-
文档写入缺少统一的错误契约和审计链路。
-
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。
原因是:
-
CLI 是机器调用入口,需要稳定 HTTP 协议。
-
服务端必须自己解析 Token 对应的用户身份,不能信任客户端传来的
userId。 -
错误码、限流、审计、requestId 都应该在 Machine API 层统一处理。
-
未来 MCP、Skills、CI、外部脚本都可以复用同一套接口。
认证:为什么不用手动复制 Token
最早的 CLI 认证方式很简单:用户在 Web 设置页复制一个 mnt_... Token,然后粘贴给 CLI。
这个方案能跑,但不适合 Agent 场景。
问题主要有三个:
-
Agent 聊天窗口里容易泄漏完整 Token。
-
用户不知道什么时候该复制、复制到哪里。
-
本地调试和线上登录态容易混在一起。
所以后来改成了类似飞书 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 包后一运行就失败
所以现在的规则是:
-
默认永远连接线上。
-
本地调试必须显式
--local。 -
prod 和 local 使用不同配置文件。
-
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,它很容易每次都自由发挥:
-
有时让用户手动复制 Token。
-
有时输出一堆配置路径和调试信息。
-
有时忘记用
--format json。 -
有时直接真实写入,不做 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_REVOKED | Token 已撤销 |
TOKEN_EXPIRED | Token 过期 |
INSUFFICIENT_SCOPE | 权限不足 |
NOT_FOUND | 文档不存在 |
VALIDATION_ERROR | 参数校验失败 |
RATE_LIMITED | 触发限流 |
还有几个细节:
-
所有响应都带
requestId。 -
429会带Retry-After和x-ratelimit-*headers。 -
CLI 不会重试结构化的
RATE_LIMITED。 -
审计日志记录 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 输入格式。
原因是:
-
Markdown 对人类和 Agent 都更自然。
-
失败时容易调试。
-
CLI 导入导出可以保持一致。
-
未来可以在服务端集中做 Markdown -> BlockNote 转换。
但这件事还没有完全定案。复杂块、图片、表格、嵌套结构越来越多时,受约束的 BlockNote JSON 也可能更准确。
所以后续会专门做一次格式策略决策,而不是现在草率拍板。
2. 初始化入口还不够完整
现在 CLI 已经可以安装、登录、写文档:
npm install -g @mynotion/cli@beta
my-notion auth login
但它还缺一个类似飞书 CLI 的统一初始化入口。
理想状态应该是:
my-notion config init
或者:
my-notion install
一次性完成:
-
检查 Node.js 版本。
-
检查 CLI 版本。
-
检查登录态。
-
引导浏览器授权。
-
检查 Skills 是否安装。
-
输出 MCP 配置建议。
-
给出下一步命令。
现在这项已经被提到最高优先级,因为用户不应该等到第一次执行业务命令时才发现自己没初始化。
3. MCP 还需要真实 Client 验证
虽然已经有 pnpm e2e:mcp,但真实 Agent 场景里的体验还要继续验证。
接下来要重点看:
-
MCP Client 能不能正确发现工具。
-
STDIO server 启动和退出是否稳定。
-
dry-run 预览是否足够清楚。
-
用户确认后的真实写入是否顺滑。
-
错误结果是否能被 Agent 正确解释。
自动化测试能保证协议不坏,但不能完全代表真实 Agent 体验。
总结
这次做 @mynotion/cli,表面上是在给 My-Notion 加一个命令行工具,实际上是在补一条 Agent 操作通道。
最终链路是:
Agent 生成内容
↓
CLI / MCP 接收任务
↓
Device Flow 安全授权
↓
Machine API 校验 Token 和权限
↓
Convex 写入文档
↓
Web / Mobile 实时展示
这条链路让我比较满意的地方是:
-
用户不需要把完整 Token 粘贴给 Agent。
-
Agent 可以用稳定 JSON 输出和错误码做自动化处理。
-
MCP 写工具默认 dry-run,降低误写风险。
-
Skills 把 Agent 操作规程固化下来,减少每次对话自由发挥。
-
npm 包发布后,外部 Agent 可以直接安装和使用。
如果你也在做 AI 原生应用,我觉得 CLI 可能不是一个“附属功能”,而是很关键的一层。
Web UI 解决人怎么用,CLI / MCP 解决 Agent 怎么用。
当你的产品开始让 Agent 参与真实写入、真实编辑、真实自动化时,这层边界迟早要补上。
本文基于 My-Notion 项目的真实开发过程整理。项目是一个 AI 原生的个人版 Notion,CLI npm 包地址:@mynotion/cli。