@yijiancaotang/cli 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +70 -26
  2. package/dist/auth.js +7 -4
  3. package/dist/calendar-contract.js +358 -0
  4. package/dist/calendar-help.js +12 -0
  5. package/dist/calendar.js +64 -0
  6. package/dist/capability-result.js +5 -0
  7. package/dist/cli.js +58 -10
  8. package/dist/command-help.js +18 -8
  9. package/dist/commands.js +11 -0
  10. package/dist/doctor.js +13 -5
  11. package/dist/generated/agent-operations.js +12 -0
  12. package/dist/habit-read.js +3 -1
  13. package/dist/habit-wire.js +36 -5
  14. package/dist/handoff-read.js +29 -0
  15. package/dist/handoff-wire.js +42 -0
  16. package/dist/http.js +11 -1
  17. package/dist/input-validation.js +14 -0
  18. package/dist/installation.js +334 -45
  19. package/dist/local-commands.js +133 -16
  20. package/dist/local-records.js +150 -0
  21. package/dist/local-types.js +1 -0
  22. package/dist/managed-skill-baseline.js +35 -0
  23. package/dist/missing-sections.js +1 -0
  24. package/dist/onboarding.js +4 -20
  25. package/dist/output.js +7 -1
  26. package/dist/platforms.js +69 -12
  27. package/dist/project-binding-cli.js +110 -0
  28. package/dist/project-binding.js +370 -0
  29. package/dist/query-search.js +3 -2
  30. package/dist/reviewed-managed-bundle-baseline.js +356 -0
  31. package/dist/schedule-contract.js +289 -0
  32. package/dist/schedule-help.js +35 -0
  33. package/dist/schedule-start-wire.js +80 -0
  34. package/dist/schedule.js +70 -0
  35. package/dist/setup-guidance.js +40 -0
  36. package/dist/skill-assets.js +1 -1
  37. package/dist/skills-export.js +206 -0
  38. package/dist/web-citations.js +1 -1
  39. package/dist/work-context-human.js +67 -0
  40. package/dist/work-context-wire.js +391 -0
  41. package/dist/write-routing.js +26 -0
  42. package/package.json +2 -2
  43. package/skills/caotang-context/SKILL.md +20 -7
  44. package/skills/caotang-first-connect/SKILL.md +48 -34
  45. package/skills/caotang-project-progress/SKILL.md +6 -2
  46. package/skills/caotang-save-knowledge/SKILL.md +6 -4
  47. package/skills/caotang-schedule/SKILL.md +54 -0
  48. package/skills/caotang-search/SKILL.md +11 -0
  49. package/skills/caotang-workday/SKILL.md +56 -9
  50. package/skills/manifest.json +28 -6
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # 一间草堂命令行
2
2
 
3
- 一间草堂 CLI 1.0.0。npm 包名为 `@yijiancaotang/cli`,命令为 `caotang`。
3
+ 通用接手的默认JSON供Agent读取完整有界投影;`--human`只供人便读,省略详细规矩/记录,不作为Agent已取得完整上下文的证明。
4
+
5
+ 一间草堂 CLI,版本独立于网页发布。npm 包名为 `@yijiancaotang/cli`,命令为 `caotang`。
4
6
 
5
7
  ## 接入
6
8
 
@@ -18,7 +20,21 @@ caotang --agent "Codex" doctor
18
20
  caotang --agent "Codex" projects
19
21
  ```
20
22
 
21
- 同一电脑的不同 AI 分别在网页批准一次;再次接入只复用自己的连接。已知平台安装的技能会固定带上本次名字,不把一把钥匙发给多个 AI。`--platform` 只选一个已知平台安装技能;名单外 AI 不装技能,按 setup 返回的通用命令说明使用。确认接好后提示:“开个新对话,说‘开工了’。”
23
+ 同一电脑的不同 AI 分别在网页批准一次;再次接入只复用自己的连接。已知平台安装的技能会固定带上本次名字,不把一把钥匙发给多个 AI。`--platform`只选一个已知平台;名单外Skill宿主可用互斥的`--skills-dir`显式父目录,HOME外共享/远程位置先导出干净ZIP再按宿主实际入口导入。无Skill但能执行CLI仍可用完整业务。当前分支的新说明/问法是候选,安装、正文取得与行为验证分开,不强制所有宿主新对话或重启。
24
+
25
+ 同名复用会返回 `reused_existing: true`、`connection_name`、`connection_name_source` 和 `next_action`;首次新批准为 `false`。给用户的一句是“这个名字已经接过了,沿用原来的连接,不用再批准。”,同时必须转述“如果你不是它,请用你自己的名字重新运行 setup --agent <你的名字>,不要继续用这条连接。”旧版本没有可靠名称记录时只报本机规范化名字并说明来源;只有实际设备授权记录提供的时间才返回 `connected_at`,不从文件时间猜。新批准的说明另存于当前环境、当前名字、当前连接的非密记录,不改变原凭据格式;本机名称不代表网页后来改过的标签。
26
+
27
+ 环境授权覆盖会单独返回 `connection_name_source=environment_override` 和 `reuse_notice`,不称为本机同名连接。若这不是自己的连接,先由本人处理覆盖,再以自己的名字接入;不能换名字继续借用覆盖。只读或首次接入仍遵守 `onboarding_message` 和原 `onboarding.action` 的说明,不把复用提示当作新的读写同意。
28
+
29
+ ### 更新到这版说明
30
+
31
+ MCP 接入随服务器部署生效。命令行接入让原来的 AI 运行:
32
+
33
+ ```sh
34
+ npx @yijiancaotang/cli@latest setup --agent "原来的名字"
35
+ ```
36
+
37
+ 保留原来选择的环境和必要目录参数;同名有效连接不再批准。已核验的标准 CLI 1.0.0(六项技能)或 1.1.0(七项技能)可在原位置安全升级,保留旧程序包;个人改动、未知版本或不安全文件会拒绝覆盖,不自动删除或强制修复。旧自定义目录若不再提供原参数,不能靠扫描整台电脑找回。
22
38
 
23
39
  本机连接按环境和 AI 名字分开保存。现有旧文件和验证接口没有可靠平台名,旧共用钥匙归“命令行”,仅显式 `setup/auth login --agent 命令行` 时懒迁移;原文件不改写、不删除,其他 AI 不读取它、各自接入。网页吊销一条连接或当前名字运行 logout 不影响其他 AI。
24
40
 
@@ -39,22 +55,47 @@ npm run test:agent-cli
39
55
 
40
56
  ### 按需加载
41
57
 
42
- 每个新对话第一件工作前(改报价、写邮件、写代码都算),即使未提草堂也主动短读一次;闲聊和纯问答不读。只选 `caotang-context`,已有可信短读就复用,不为开工加载第二技能。接入同意/拒绝门优先,不靠用户说“开工了”触发。第一条命令直接短读,不先运行 `command -v`、`env`、`auth status` 或 `ToolSearch`;出错后才排查。沿用接入时用户已选环境,正式版默认生产,不重新确认环境、不自行切换;明确使用其他环境时保留本次已选参数。
58
+ 新工作对话/恢复按有效授权与本次任务范围纯读接手;闲聊和纯问答不读。先取得同源`caotang guide`公共短合同,当前会话已有有效正文就复用,不把清单描述当已加载。只选`caotang-context`,已有可信短读就复用,不重复读取或加载第二个开工技能。接入同意/拒绝门优先,项目请求先定向查询,整体查看才用`context --handoff`;只有明确要求记录开始/开工时间才用`context --start`。公共准备与同意完成后第一条私人读取按本次任务定向执行,不先运行`command -v`、`env`、`auth status`或`ToolSearch`;出错后才排查。沿用用户已选环境,不重新确认环境、不自行切换;新协议不支持不回退start。
43
59
 
44
60
  若本人明确限定只用 caotang 命令,就不使用 MCP;不改配置,误调用其他入口即停。
45
61
 
46
- `caotang-workday` 只负责完成重要工作/收工,不再包含开工短读或引导加载第二技能。正常开工不跑doctor;仅出错且需要安装诊断时使用。用户明确要接着某个项目且短读不足时才查详情,短读能回答就不扩查。
62
+ `caotang-workday` 是记录入口:本人说“记一下/存到草堂”,或完成重要工作、收工时先判断去处,分组列出,问一次确认再写。它不包含开工短读。正常开工不跑doctor;仅出错且需要安装诊断时使用。用户明确要接着某个项目且短读不足时才查详情,短读能回答就不扩查。
47
63
 
48
- `caotang-save-knowledge` 只在写入时加载。字段、长度和JSON例子按当前动作读取现有专属 `--help`,不预读整套帮助、不新增参考工具;公开、删除、本人确认、不读凭据、权限、待审、未知写与长势确认仍留在技能正文。技能描述是触发指引,不是“每次开工都会读”的实测保证。
64
+ `caotang-project-progress` 和 `caotang-save-knowledge` 在分流并确认后执行。知识先搜已有卡,有同主题就补内容或来源,没有才新建。字段、长度和JSON例子按当前动作读取现有专属 `--help`,不预读整套帮助、不新增参考工具;公开、删除、本人确认、不读凭据、权限、待审、未知写与长势确认仍留在技能正文。技能描述是触发指引,不是“每次开工都会读”的实测保证。
65
+
66
+ ### 记到哪
67
+
68
+ | 事情 | 去处 |
69
+ | --- | --- |
70
+ | 项目上已经做完、定下或遇到的事 | 项目进展,短写结果、证据和未完成部分。 |
71
+ | 换个项目还用得上的方法、结论、资料、坑 | 知识卡,先搜再补或建,一次最多两张候选。 |
72
+ | 下次开工先做的一件事 | 单独一条 `【下一步】…`,只一条。 |
73
+ | 其它将来要做的事 | 待办:按 `caotang-schedule` 复述日期或未定日期,明确确认后写入,不另建项目或卡。 |
74
+ | 对项目的整体判断变了 | 覆盖认知摘要。 |
75
+ | 项目走到了新阶段 | 阶段。 |
76
+ | 本人的习惯、偏好、规矩 | 不写;建议本人去网页“我的规矩”加,可给一句拟稿。 |
77
+ | 已有卡真的用上了 | 进展写卡名,按现有逐级规则问是否发芽。 |
78
+
79
+ 同一件事可提“进展+知识卡”,分别写做了什么和学到什么,不复制同一段两遍。一次按去处分组列全;“都记”才全写,只选一组就只写那组,拒绝、沉默、含糊都不写,同件事不再问。拿不准就在候选里给两个去处让本人选,不猜着写。新项目仅本人明确要求时建;闲聊、小问题不问,只读连接不提保存。平台不判断、不自动拆分、不接大模型。
49
80
 
50
81
  本机安装不等于真实 AI 的业务验收,也不证明输入/输出 Token 消耗。AGENTS.md、CLAUDE.md 等常驻用户配置不写;如需另行修改,须取得本人明确范围许可。
51
82
 
83
+ ## 自然任务与读取范围
84
+
85
+ “看看XXX项目/继续这个项目”是本次该项目的必要读取意图;已知ID先定向读取项目、相关进展和待办,未知ID只作必要定位,不先拉全局摘要,不泛问是否读取。“看看草堂/整体安排”才在有效同意和权限范围读工作摘要;账号或范围有歧义才核对。只有本人明确要求记录开始/开工时间才选start(CLI `context --start` / MCP `get_start_context`),普通查看、恢复或开始处理任务不记录开工。公共guide是Agent准备,不要求人说“开工了”或背命令/触发词。
86
+
52
87
  ## 安装与诊断
53
88
 
54
89
  | 命令 | 行为 |
55
90
  | --- | --- |
56
- | setup --agent 名字 [--platform 平台] [--no-open] | 独立接入该 AI;platform 只给一个已知平台安装技能;无名字提示下一步,不申请连接。 |
57
- | doctor --agent 名字 [--platform 平台] | 只读检查该 AI 的技能、独立连接和实时有效性,不安装、不启动 AI 客户端。 |
91
+ | setup --agent 名字 [--platform 平台 或 --skills-dir 绝对父目录] [--no-open] | 独立接入该 AI;platform 只给一个已知平台安装技能;无名字提示下一步,不申请连接。 |
92
+ | doctor --agent 名字 [--platform 平台 或 --skills-dir 绝对父目录] | 只读检查该 AI 的技能、独立连接和实时有效性,不安装、不启动 AI 客户端。 |
93
+ | guide | 无需身份的离线公共操作说明;没有Skill的Agent也先取得同源规则。 |
94
+ | context --handoff [--project UUID] | 有权且已有任务/同意时读取完整有界工作包;不记录开工。 |
95
+ | skills export --output 新ZIP路径 | 离线导出官方七项技能和说明,不含私人数据、拒绝覆盖。 |
96
+ | project bind UUID [--include-subdirs] | 先预览目录/实际服务/账号/目标;本人确认后才加--confirm DIGEST --local-workspace保存本机关联。 |
97
+ | project binding --local-workspace | 仅在本人已确认独立本机范围时读取有效定位;本轮显式项目优先。 |
98
+ | project unbind --local-workspace [--confirm DIGEST] | 先预览,再按确认解除精确目录关联;不删除或归档草堂项目。 |
58
99
  | version / --version | 离线返回包/Node版本及 `release_status=released`,不读取凭据、不构造网络客户端 |
59
100
  | update | 只返回“运行 `npx @yijiancaotang/cli@latest setup --agent <你的名字>` 更新到最新版。”;不查 registry、不下载、不自动升级。 |
60
101
 
@@ -63,8 +104,8 @@ npm run test:agent-cli
63
104
 
64
105
  本机锁只面向同一主机/PID命名空间的本地常规文件系统,不支持共享网络盘。固定单文件追加申请/释放记录,按先后顺序仅首个未释放的活跃或未知持有者占有;只跳过已确认死亡者,不删除、改名、截断或调用外部程序。没有16次崩溃或64KiB准入耗尽;文件数量固定,但日志字节和读取工作量随使用增长,不声称磁盘体积恒定。保留旧协议标记,活跃、未知或不安全旧标记继续拒绝;不能与旧改名/压缩协议进程并发混跑。未知旧目录/损坏记录不自动修复。文件系统的保护规则可能阻止本地安装,请按实际诊断核对。
65
106
 
66
- 支持 Claude Code、Codex、OpenClaw;`kimi-code`、`kimi-work`、`workbuddy` 仅支持macOS本机安装。平台检测只检查程序元数据,不运行客户端;`kimi-code`可检测PATH中的 `kimi` 或桌面bundle,`kimi-work`与`workbuddy`不支持PATH命令检测,不能把 `kimi` 当作工作模式。每次只选一个 `--platform`,非macOS拒绝这三项;安装只面向当前名字对应的平台,未检测到程序不能报ready。未选新平台的技能根异常不得阻断旧平台。
67
- 六项 canonical 技能(开工续接、沉淀知识、项目进展、查找、完成收工、首次接入)与manifest继续作为共用来源。各平台沿用相同的按需加载和安全确认边界,不改 MCP 配置,不提供卸载命令。
107
+ 支持 Claude Code、Codex、OpenClaw;`kimi-code`、`kimi-work`、`workbuddy` 仅支持macOS本机安装。平台检测只检查程序元数据和已核实的本名字技能,不运行客户端;`kimi-code`可检测PATH中的 `kimi` 或桌面bundle,`kimi-work`与`workbuddy`不支持PATH命令检测,不能把 `kimi` 当作工作模式。每次只选一个 `--platform`,非macOS拒绝这三项;安装只面向当前名字对应的平台。已核实安装过的平台算检测到,不靠宿主 App 猜。未选新平台的技能根异常不得阻断旧平台。
108
+ 七项canonical技能(接手续接、沉淀知识、项目进展、查找、完成收工、首次接入、日程待办)与manifest继续作为共用来源。各平台沿用相同的按需加载和安全确认边界,不改 MCP 配置,不提供卸载命令。
68
109
 
69
110
  | 平台 | 用户级技能目标 |
70
111
  | --- | --- |
@@ -80,34 +121,37 @@ npm run test:agent-cli
80
121
  变更使用无异步间隙的父目录inode绑定与独占新建,不覆盖旧文件;新文件先为000,持句柄写完并校验后才开放最终权限,开放后再核内容和权限。局部失败仅通过自建fd尽力降回000,不按可能已被替换的路径修改文件;文件系统若拒绝降权则不能保证恢复,先保留现状并人工核对。
81
122
  中断可能留下部分安装或锁,未完成文件不应运行;不自动清理,不承诺多文件事务或SIGKILL恢复。
82
123
 
124
+ 上述独占新建是首次安装路径;标准 1.0.0 升级例外先核旧完整程序包、六份本名字标准技能及原 inode,再持句柄更新技能、最后更新命令入口,并写公开内容恢复 journal。普通失败只回退仍与本次写入一致的原文件;发现替换或无法核实就报 `installation_upgrade_unknown`,保留现场,不说更新成功。不删除、改名个人文件、旧程序包或凭据,不承诺断电原子切换。
125
+
83
126
  doctor的JSON位于`data`:`ready`、`issues[]`、`next_actions[]`、`command`、`platforms`、`auth`(status/source/kind/scope)、`network`。
127
+ `skill_platforms` 列出本名字实际核实的技能平台;`skill_platform` 为固定平台顺序中第一项,无可核实安装时为 `generic`,不是宿主或最近使用平台。安装记录和七份技能文件要一起核验;没有新记录的标准旧版也可在已知目录识别为 `outdated`,不假装已更新。缺失、个人修改或不安全状态不算 ready。没有技能时提示“可以用 setup --platform <平台> 补装技能,不影响现有连接。”,同时保留原 `--agent`。`skill_search_scope=known_paths_only` 说明只查已有记录或六个平台的已知路径;丢失旧自定义目录参数时如实说明范围,不全盘扫描。
84
128
  `readiness_scope=local_cli_and_skills`和`end_to_end_verified=false`明确不代表AI会话已读写业务。缺凭据时network=not_checked;断网不重签,401不删凭据;setup交付未知保留网页核对/吊销指引,不重新领取。
85
129
 
86
130
  `setup` 的 `data.authorization_attempted` 表示本次是否实际调用了新设备授权的开始请求。复用已有有效授权,或在发起前因读取/验证/取消失败时为 `false`;已发起后即使遭拒、断网或结果未知仍为 `true`。它不表示授权成功,成功与否以最终 `auth`、`issues` 和退出码为准。
87
131
 
88
132
  ### 接入对话
89
133
 
90
- 首次按当前 AI 是否第一次安装技能判断;新授权不等于首次,同一个 AI 已有连接但首次安装技能时仍可能是首次。`data.onboarding.newly_connected_platforms` 仅列本次首次接好的入口,不包括已装重复 setup、恢复或更新。核对当前 AI 的入口,不能把其他 AI 的首次状态套过来。认证和 ready 原字段保留,开场字段不是接入成功证明。
134
+ ### 候选guidance:人的经验、Agent准备与内容同意分开
91
135
 
92
- `data.account_state` 只含 `empty|has_content|unknown`,不带任何内容,不为判断空账号先读工作台;`data.onboarding.action` 为 `introduce|ask_once|none`。技能转述 `data.user_message` 的 CLI 固定人话,不贴字段名或账号 ID,包括 `auth.status`、`read_only`、`ready` 等,不让模型编造状态;用户问细节时才给必要说明。缺字段、矛盾或入口不明时不猜、不自动读写。
136
+ 本分支新增`data.guidance.schema_version=1`,只含公共元数据与同源说明索引,不请求私人内容、不造教程标记。`user_experience`保持unknown/not_recorded,人的首次使用只据本人明确意图;新安装、新token、空号或连接数都不能推导。`agent_preparation`提供合同版本/SHA及`caotang guide`,正文取得证据unknown,不能由CLI自报模型ready。
93
137
 
94
- CLI 给用户看的话以统一接入提示为准,只含固定文案和已验证的 URL、确认码格式化;技能和接入页用同样的说法,不各写一套。不主动介绍功能或堆卖点;用户问“能做什么”时才说明。AI 可按语境调整,但不能变长,不能贴字段。introduce 保留为动作标记,不要求功能介绍;下表只说明行为边界,短句按统一文案输出。
138
+ 认证和ready原字段保留。`account_state`的empty、has_content、unknown,以及`onboarding.action`的introduce、ask_once、none和`onboarding.newly_connected_platforms`继续原枚举/含义;新授权不是人的首次标记,旧introduce不重定义为教程完成。旧`user_message`/`onboarding_message`固定文案保留兼容,但新Skill同一问题只说一次,不各问一遍。`next_conversation`/`skill_activation`是旧接续资料,不据此强制新对话或开工。
139
+
140
+ | 当前场景 | 新Skill处理 |
141
+ | --- | --- |
142
+ | 本人明确首次使用/主动问用法 | 先简短介绍六类用途及适用例子,目录取同源guide;具体执行邀请按有效能力过滤。 |
143
+ | 老用户换Agent | 新Agent仍先取得说明,核自己的连接/权限/内容同意;已有明确任务直接做,不重播教程。 |
144
+ | 日常查看/恢复 | 复用有效说明;项目任务定向查,整体查看才读摘要,普通查看不记时间。 |
145
+ | 重复setup/升级 | 只报沿用及安装状态,不自动读工作包、写开工或再发批准码。 |
95
146
 
96
- | 场景 | 状态 / 动作 | 对话行为 |
97
- | --- | --- | --- |
98
- | 首次且空 | empty / introduce | 有写权限才用短句问是否保存;同意整理后只用当前对话拟私有条目,具体条目明确同意才写,没有素材不凑数。 |
99
- | 首次有内容 | has_content / ask_once | 问一次是否读工作台,说明内容会发给当前 AI 服务,同意后才读。 |
100
- | 首次未知 | unknown / ask_once | 不说账号空;同样问一次并说明内容会发给当前 AI 服务,同意后才读。 |
101
- | 已装重复 setup | 任意 / none | 只说“已经接好了,不用批准。”,不介绍、不问、不读。 |
102
- | 日常新工作对话 | 不在接入对话 | 维持一次短读与按模式简报。 |
147
+ 未经同意不读,接入同意门优先;不同意、沉默或含糊不读,本对话不再问,不换搜索、详情、MCP或会话绕过。已有明确且可核同范围有效授权沿用。短读范围有限,只含规矩、有效待确认和近期记录;未查全量项目、知识和历史,不能推断全账号为空。只看可信account_state或account_empty,缺权、null和空数组不是空,不主动建议清理。
103
148
 
104
- 接入对话优先于 context 的自动短读;workday不承担开工。不同意、沉默或含糊不读,本对话不再问,不换搜索/详情/MCP绕过。本人之后主动明确要求读取时,只按新授权范围操作,不能当写入同意。短读只含规矩、有效待确认和有限近期记录,不含全量项目、知识、历史;只看可信 `account_state` 或 `account_empty` 判断空,不由空数组、缺权或部分结果推断全账号空,不主动建议清理。
149
+ read_only只能读,不能承诺写或发起写类待确认;只读不提保存,也不问“要存几条吗”。RW也不等于细分能力全开:guidance账号开关和有效细权未知就保unknown,不邀请未核写入。本人要求时可留对话草稿;改权限要在网页重新批准一条连接,账号开关到Agent 管理核对读写权限,由本人处理。只读不能靠反复auth login升级。有效连接运行auth login会复用、不新签,不能把auth login说成重新授权或升级权限的办法。
105
150
 
106
- `read_only` 只能读,不能承诺写或发写类申请;只读不提保存,也不问“要存几条吗”,按统一文案的只读分支说明。本人要求时可留对话内草稿。只读需要改权限时说“改权限要在网页重新批准一条连接。”账号开关仍须到 Agent 管理核对读写权限,由本人处理,不自动更换连接;只读不能靠反复 `auth login` 升级。不能把 `auth login` 说成重新授权或升级权限的办法。不换工具、代签或自行扩大权限。
151
+ 连接结果按已核名称和来源解释,本机名字不代表网页改名;缺时间不编。已复用且是自己连接时,用户问怎么批准只报沿用,不再发批准链接或码;“如果你不是它”时停止借用。用户摘要不贴字段名或账号ID,用户问细节时才给必要说明。确认码过期、拒绝/取消准确说明;authorization_delivery_unknown请本人在Agent 管理核对,确需本机查询才用caotang auth status。auth logout只断开当前名字的连接,不影响其他 AI,先说明影响并获本人同意。
107
152
 
108
- 已复用连接,用户问“怎么批准”时,只说“已经接好了,不用批准。”不发批准链接或码,不要求再次 login。已有有效连接再次运行 `auth login` 时只会复用、不新签。
153
+ 候选文案仅供本人预览;功能指引只在本人明确首次或主动问用法时给,不主动推销或每次长播。只转述已验证URL、确认码、名称和有据时间,不伪造示例码。安装、可发现、正文实际获得、行为验证分别取证;不由安装成功推定全部技能生效,不按品牌承诺新对话一定加载,不让AI自动重启。现有六安装入口是回归样本,不是通用范围白名单。
109
154
 
110
- Codex、Claude Code 已证实新对话能读取技能;不由安装成功推定全部技能已生效或真实读写全部通过。OpenClaw、Kimi Code、Kimi.app 工作模式、WorkBuddy 保守提示“如果看不到技能,开新对话或重启应用”,由本人操作,不能要求 AI 自动重启。
111
155
 
112
156
  ## 授权与输出
113
157
 
@@ -141,9 +185,9 @@ login 只走浏览器确认码,不接收密码。stderr 按行输出安全JSON
141
185
 
142
186
  - `caotang --agent "Codex" context --start` 做一次短读,不与旧 `context --since` 筛选混用。始终沿用当前账号/环境。除规矩外,每个段落受自己原有的读取权限控制;最近 **三个有记录的北京时间日期**而非最近三个日历日,每日最多 3 条、每条摘录最多 120 个字符;等待本人确认的申请最多 3 条。只在确切可见的本人项目进展中取最新的独立 `【下一步】` 标记。隐藏、不可用和 0 分开解释,不扩大读取。
143
187
  - 本人的有效令牌(包括只读)可 `caotang --agent "Codex" rules` / MCP `get_my_rules`,开工也能读到;**没有 Agent 写规矩的命令、工具或待确认申请**。增删改仅在网页“我的规矩”,最多 20 条,每条 200 个 Unicode 字符;不要填写任何秘密。规矩是偏好,不是授权。
144
- - CLI 只按环境、已核账号及北京时间日期决定简报长短,不记业务内容、对话或授权证据。缺少可信状态/访问失败时保守用简短模式;MCP `get_start_context` 一律简短,并提示“要详细就说一声”。用户说这次别问就不再提;长期不汇报/不询问可由本人写进规矩。敏感信息先去除,不自动重新授权/安装启动钩子。
188
+ - CLI 只按环境、已核账号及北京时间日期决定简报长短,不记业务内容、对话或授权证据。缺少可信状态/访问失败时保守用简短模式;仅要求记录开始时间才用MCP `get_start_context`,一律简短,并提示“要详细就说一声”。用户说这次别问就不再提;长期不汇报/不询问可由本人写进规矩。敏感信息先去除,不自动重新授权/安装启动钩子。
145
189
  - `caotang-context` 独占开工/续接短读,短读不足且任务确需知识时才检索复用结果/开花卡,引用真实环境出处。`caotang-workday` 只在完成重要工作或收工时整理候选,让本人挑要记录的项目进展/知识/认知,明确同意才写。独立进展正文 `【下一步】具体动作` 仅保存本人同意的下一步;未确认写入保留去敏建议,不自动重试。
146
- - `caotang-first-connect` 按上方接入对话规则分流,不看是否新授权、不预读内容判空。MCP 无可靠首接标记,仅本人明确说刚接入时问一次并说明内容去向;不查回收站、本机文件或其它会话。六技能安装/核验不代表真实模型已完成验收。
190
+ - `caotang-first-connect` 按上方接入对话规则分流,不看是否新授权、不预读内容判空。MCP 无可靠首接标记,仅本人明确说刚接入时问一次并说明内容去向;不查回收站、本机文件或其它会话。七项技能安装/核验不代表真实模型已完成验收。
147
191
 
148
192
  列表查询:`projects`、`knowledge`、`search`、`progress list ID`、`checkins`支持`--limit 1..50`(默认20)和`--offset`;返回真实过滤后`total`、`has_more`、`limit`、`offset`,空页不丢总数。项目详情仍只附最近10条,`updates_total`才是完整数量,全部记录从`progress list ID`翻页读取。排序按选定时间倒序,再按ID倒序;同次响应计数与当前页一致,但分次翻页不是冻结快照,期间有写入可能改变页面。
149
193
 
@@ -273,7 +317,7 @@ PDF仅检查边界/文件头并转交原始字节,不解析全文。流程只
273
317
 
274
318
  ## 范围与限制
275
319
 
276
- CLI 负责在本人授权范围内读写草堂数据,不提供内置大模型,不抓取或解析来源全文,不自动写入、重试、公开或执行删除。日历、日程等后续功能不属于此版本的命令范围。
320
+ CLI 负责在本人授权范围内读写草堂数据,不提供内置大模型,不抓取或解析来源全文,不自动写入、重试、公开或执行删除。本版包含日程、待办、日历、当天总结和时间线命令;没有重复日程、子任务、他人分配或邮件、手机推送。
277
321
 
278
322
  `version` 只说明本机包版本,不表示远端服务或 npm registry 的发布状态。`update` 只输出手动更新命令,不查询 registry、不下载或自动安装;由本人决定是否执行。生产部署和 npm 发布仍是独立的维护操作。
279
323
 
package/dist/auth.js CHANGED
@@ -153,9 +153,11 @@ function delivered(value, deviceName, clientName, deviceCode) {
153
153
  || !date(metadata.created_at) || !label(metadata.device_name) || !label(metadata.client_name)
154
154
  || metadata.device_name !== deviceName || metadata.client_name !== clientName
155
155
  || (metadata.revoked_at !== undefined && metadata.revoked_at !== "")
156
- || (metadata.last_used_at !== undefined && !date(metadata.last_used_at)))
156
+ || (metadata.last_used_at !== undefined && !date(metadata.last_used_at))
157
+ || [body.access_token, deviceCode].some(secret => [metadata.client_name, metadata.device_name].some(name => typeof name === "string" && name.includes(secret))))
157
158
  throw invalid();
158
- return { token: body.access_token, who: identity({ user_id: metadata.user_id, token_id: metadata.id, kind: "device", scope: body.scope }, [body.access_token, deviceCode]) };
159
+ return { token: body.access_token, who: identity({ user_id: metadata.user_id, token_id: metadata.id, kind: "device", scope: body.scope }, [body.access_token, deviceCode]),
160
+ connection: { connection_name: metadata.client_name, connection_name_source: "device_authorization", connected_at: metadata.created_at, connection_time_source: "device_authorization" } };
159
161
  }
160
162
  function retryAfter(...values) {
161
163
  const present = values.filter((value) => value !== undefined);
@@ -179,7 +181,8 @@ export async function authLogin(ctx, options = {}) {
179
181
  if (existing) {
180
182
  try {
181
183
  const result = authenticated(await validate(ctx, existing), existing);
182
- return { ...result, body: { ...asRecord(result.body), authorization_attempted: false, new_authorization_confirmed: false, user_message: connectionCopy.repeated } };
184
+ return { ...result, body: { ...asRecord(result.body), authorization_attempted: false, new_authorization_confirmed: false, reused_existing: true,
185
+ ...(ctx.agent ? { connection_name: ctx.agent.key, connection_name_source: existing.source === "environment" ? "environment_override" : "local_agent_key" } : {}), user_message: connectionCopy.repeated } };
183
186
  }
184
187
  catch (error) {
185
188
  if (!(error instanceof CliError) || error.exitCode !== 3 || asRecord(error.details)?.http_status !== 401 || existing.source === "environment")
@@ -268,7 +271,7 @@ export async function authLogin(ctx, options = {}) {
268
271
  throw new CliError("credential_save_failed", links ? "设备令牌已交付,但凭据文件的链接数不是 1,未能安全保存。" : "设备令牌已交付,但未能安全保存。", recovery, 2, links ? { cause: "credential_unsafe", reason: "multiple_links" } : undefined);
269
272
  }
270
273
  const confirmed = authenticated(token.who, { source: "file" });
271
- return { ...confirmed, body: { ...asRecord(confirmed.body), authorization_attempted: true, new_authorization_confirmed: true } };
274
+ return { ...confirmed, body: { ...asRecord(confirmed.body), authorization_attempted: true, new_authorization_confirmed: true, reused_existing: false, ...token.connection } };
272
275
  }
273
276
  const body = asRecord(result.body);
274
277
  if ((result.status === 202 && code === "authorization_pending") || (result.status === 429 && (code === "slow_down" || code === "device_auth_rate_limited"))) {
@@ -0,0 +1,358 @@
1
+ import { scheduleUUID, scheduleKey, validScheduleDate, validScheduleTime, scheduleReadResult } from "./schedule-contract.js";
2
+ export const calendarRoot = "/api/v1/agent";
3
+ export const calendarKinds = ["project_created", "project_stage", "project_update", "checkin", "knowledge_created", "knowledge_growth", "schedule_completed", "schedule_cancelled", "daily_summary"];
4
+ const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
5
+ const scalar = (v, max = 5000) => typeof v === "string" && [...v].length <= max && !v.includes("\0") && ![...v].some(c => { const n = c.codePointAt(0); return n >= 0xd800 && n <= 0xdfff; });
6
+ const natural = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
7
+ const revision = (v) => natural(v) && v > 0;
8
+ const actors = ["user", "agent", "unknown"];
9
+ const trim = (v) => v.replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "");
10
+ export class CalendarProblem extends Error {
11
+ code;
12
+ humanMessage;
13
+ nextAction;
14
+ exitCode;
15
+ details;
16
+ constructor(code, humanMessage, nextAction, exitCode = 2, details = {}) {
17
+ super(JSON.stringify({ code, message: humanMessage, next_action: nextAction, ...details }));
18
+ this.code = code;
19
+ this.humanMessage = humanMessage;
20
+ this.nextAction = nextAction;
21
+ this.exitCode = exitCode;
22
+ this.details = details;
23
+ }
24
+ }
25
+ export function calendarInvalid() {
26
+ throw new CalendarProblem("invalid_arguments", "日历、时间线或当天总结参数无效。", "请查看本命令帮助;日期使用真实 YYYY-MM-DD,含首尾最多一个日历年;只提交本人确认的一句话,保持原幂等键和当前版本。");
27
+ }
28
+ function invalidResponse() {
29
+ throw new CalendarProblem("invalid_response", "日历或时间线返回了无法核实的内容。", "请核对服务版本和实际读取范围,不把异常响应、权限缺口或未记录的历史解释为没有活动。", 7);
30
+ }
31
+ const leap = (y) => y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0);
32
+ const ordinal = (y, m, d) => { const p = y - 1; return 365 * p + Math.floor(p / 4) - Math.floor(p / 100) + Math.floor(p / 400) + [31, leap(y) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31].slice(0, m - 1).reduce((s, n) => s + n, 0) + d - 1; };
33
+ function validRange(start, end) {
34
+ const [sy, sm, sd] = start.split("-").map(Number), [ey, em, ed] = end.split("-").map(Number);
35
+ const nm = sm === 2 && sd === 29 && !leap(sy + 1) ? 3 : sm, nd = nm !== sm ? 1 : sd;
36
+ return start <= end && ordinal(ey, em, ed) + 1 <= ordinal(sy + 1, nm, nd);
37
+ }
38
+ export function calendarQuery(input) {
39
+ const out = {};
40
+ for (const [name, value] of Object.entries(input)) {
41
+ if (!["start_date", "end_date", "project_id", "actor_kind", "actor_name", "actor_source_id", "kind", "only_core", "limit", "cursor"].includes(name))
42
+ calendarInvalid();
43
+ if (value === undefined)
44
+ continue;
45
+ if (name === "only_core") {
46
+ if (typeof value !== "boolean")
47
+ calendarInvalid();
48
+ }
49
+ else if (name === "limit") {
50
+ if (!natural(value) || value < 1 || value > 100)
51
+ calendarInvalid();
52
+ }
53
+ else {
54
+ if (!scalar(value, name === "cursor" ? 4096 : name === "actor_name" ? 80 : 100) || !value)
55
+ calendarInvalid();
56
+ if ((name === "start_date" || name === "end_date") && !validScheduleDate(value))
57
+ calendarInvalid();
58
+ if ((name === "project_id" || name === "actor_source_id") && !scheduleUUID(value))
59
+ calendarInvalid();
60
+ if (name === "actor_kind" && !actors.includes(value))
61
+ calendarInvalid();
62
+ if (name === "actor_name" && (trim(value) !== value || /[\p{Cc}\u2028\u2029]/u.test(value)))
63
+ calendarInvalid();
64
+ if (name === "kind" && !calendarKinds.includes(value))
65
+ calendarInvalid();
66
+ if (name === "cursor" && /\s/u.test(value))
67
+ calendarInvalid();
68
+ }
69
+ out[name] = value;
70
+ }
71
+ if ((input.start_date === undefined) !== (input.end_date === undefined))
72
+ calendarInvalid();
73
+ if (typeof input.start_date === "string" && typeof input.end_date === "string" && !validRange(input.start_date, input.end_date))
74
+ calendarInvalid();
75
+ if (input.actor_source_id !== undefined && input.actor_kind !== undefined && input.actor_kind !== "agent")
76
+ calendarInvalid();
77
+ return out;
78
+ }
79
+ export function dailySummaryText(value) {
80
+ if (!scalar(value, 1000) || /[\p{Cc}\u2028\u2029]/u.test(value))
81
+ return calendarInvalid();
82
+ const text = trim(value);
83
+ if (!text || [...text].length > 200)
84
+ return calendarInvalid();
85
+ return text;
86
+ }
87
+ // Format-only validation is also used for immutable historical read receipts.
88
+ export function dailySummaryBody(value) {
89
+ if (!record(value) || Object.keys(value).some(k => !["date", "summary", "confirmed"].includes(k)) || value.confirmed !== true || !validScheduleDate(value.date))
90
+ return calendarInvalid();
91
+ return { date: value.date, summary: dailySummaryText(value.summary), confirmed: true };
92
+ }
93
+ export function dailySummaryDateBounds(now = Date.now()) {
94
+ if (!Number.isFinite(now) || Number.isNaN(new Date(now).getTime()))
95
+ return calendarInvalid();
96
+ const parts = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(new Date(now));
97
+ const part = (type) => parts.find(value => value.type === type).value;
98
+ const max_date = part("year").padStart(4, "0") + "-" + part("month") + "-" + part("day");
99
+ const min_date = new Date(Date.parse(max_date + "T00:00:00Z") - 30 * 86400000).toISOString().slice(0, 10);
100
+ return { min_date, max_date, timezone: "Asia/Shanghai" };
101
+ }
102
+ export function dailySummaryWriteBody(value, now = Date.now()) {
103
+ const body = dailySummaryBody(value), bounds = dailySummaryDateBounds(now);
104
+ if (body.date < bounds.min_date || body.date > bounds.max_date)
105
+ throw new CalendarProblem("summary_date_out_of_range", "当天总结只接受北京时间今天及此前30天(含)。", "请在返回的日期范围内重新确认;不接受未来日期,保留原幂等键,不自动重试。", 2, bounds);
106
+ return body;
107
+ }
108
+ export function calendarKey(value) { try {
109
+ return scheduleKey(value);
110
+ }
111
+ catch {
112
+ return calendarInvalid();
113
+ } }
114
+ export function validSummaryRevision(value) { return natural(value); }
115
+ export function calendarUnknown(key, status) {
116
+ return new CalendarProblem("write_outcome_unknown", "当天总结写入结果未确认,不能视为已收工。", "保留原幂等键和连接,用 daily-summary status 或 get_daily_summary_write_status 核对;不要换键、自动重试或执行依赖写入。", 6, { write_status: "unknown", idempotency_key: key, ...(status === undefined ? {} : { http_status: status }) });
117
+ }
118
+ export function calendarFailure(status, body, key, operation = "read") {
119
+ if (key && (status === 202 || status >= 500))
120
+ return calendarUnknown(key, status);
121
+ const raw = record(body) ? body.code ?? body.error : undefined;
122
+ const known = { 400: ["summary_date_out_of_range", "invalid_calendar_query", "invalid_calendar_input", "invalid_daily_summary", "invalid_summary_input", "summary_confirmation_required", "invalid_idempotency_key", "invalid_summary_revision"], 401: ["not_authenticated"], 403: ["token_scope_insufficient", "capability_disabled", "second_confirmation_required", "legal_consent_required", "account_unavailable"], 404: ["summary_not_found", "daily_summary_not_found"], 409: ["calendar_cursor_stale", "summary_revision_conflict", "idempotency_conflict"], 415: ["invalid_content_type"], 429: ["rate_limited"], 503: ["calendar_unavailable", "summary_unavailable"] };
123
+ const code = record(body) && (body.code === undefined || body.code === raw) && (body.error === undefined || body.error === raw) && typeof raw === "string" && known[status]?.includes(raw) ? raw : "request_rejected";
124
+ const details = { http_status: status, ...(key ? { idempotency_key: key } : {}) };
125
+ if (code === "summary_date_out_of_range" && operation === "write") {
126
+ if (record(body) && body.timezone === "Asia/Shanghai" && validScheduleDate(body.min_date) && validScheduleDate(body.max_date)) {
127
+ const [ay, am, ad] = body.min_date.split("-").map(Number), [by, bm, bd] = body.max_date.split("-").map(Number);
128
+ if (ordinal(by, bm, bd) - ordinal(ay, am, ad) === 30)
129
+ Object.assign(details, { min_date: body.min_date, max_date: body.max_date, timezone: "Asia/Shanghai" });
130
+ }
131
+ return new CalendarProblem("summary_date_out_of_range", "当天总结只接受北京时间今天及此前30天(含)。", "请按可接受的日期范围重新确认;不接受未来日期,保留原幂等键,不自动重试。", 2, details);
132
+ }
133
+ const allowedCapabilities = operation === "write" ? ["write_project_update"] : operation === "status" ? [] : ["read_projects", "read_knowledge", "read_workbench", "read_drafts"];
134
+ if (code === "capability_disabled" && record(body) && allowedCapabilities.includes(body.required_capability) && (body.missing_sections === undefined || Array.isArray(body.missing_sections) && body.missing_sections.length === 0))
135
+ details.required_capability = body.required_capability;
136
+ const next = code === "calendar_cursor_stale" ? "数据或权限已变化;从第一页重新读取,不自动轮询或拼接新旧页面。" : status === 403 ? "请本人在网页核对账号开关、连接只读/读写范围或协议确认;不要重新授权或换入口绕过限制。" : status === 409 ? "先核对当前总结和版本;保留原幂等键,不自动重试或覆盖。" : "请核对命令输入、服务版本和实际状态,不把失败解释为没有活动,也不自动重试写入。";
137
+ return new CalendarProblem(code, code === "calendar_cursor_stale" ? "时间线游标已失效。" : status === 403 ? "当前操作权限不足。" : "日历或当天总结请求未完成。", next, status === 401 ? 3 : status === 403 ? 4 : status >= 500 ? 7 : 2, details);
138
+ }
139
+ function actor(value) {
140
+ if (typeof value.actor_kind !== "string" || !actors.includes(value.actor_kind) || !scalar(value.actor_name, 80) || !value.actor_name || /[\p{Cc}\u2028\u2029]/u.test(value.actor_name))
141
+ return invalidResponse();
142
+ const out = { actor_kind: value.actor_kind, actor_name: value.actor_name };
143
+ if (value.actor_source_id !== undefined) {
144
+ if (value.actor_kind !== "agent" || !scheduleUUID(value.actor_source_id))
145
+ invalidResponse();
146
+ out.actor_source_id = value.actor_source_id;
147
+ }
148
+ if (value.actor_kind === "unknown" && value.actor_name !== "未记录")
149
+ invalidResponse();
150
+ return out;
151
+ }
152
+ export function dailySummaryResult(value) {
153
+ if (!record(value) || !scheduleUUID(value.id) || !validScheduleDate(value.date) || !validScheduleTime(value.confirmed_at) || !revision(value.revision) || typeof value.modified !== "boolean")
154
+ return invalidResponse();
155
+ let body;
156
+ try {
157
+ body = dailySummaryBody({ date: value.date, summary: value.summary, confirmed: true });
158
+ }
159
+ catch {
160
+ return invalidResponse();
161
+ }
162
+ if (body.summary !== value.summary)
163
+ return invalidResponse();
164
+ return { id: value.id, date: body.date, summary: body.summary, confirmed_at: value.confirmed_at, revision: value.revision, ...actor(value), modified: value.modified };
165
+ }
166
+ const sections = { calendar_projects: "read_projects", calendar_knowledge: "read_knowledge", calendar_schedule: "read_workbench", daily_progress: "read_projects", daily_knowledge: "read_knowledge", project_drafts: "read_drafts", project_draft_checkins: "read_drafts", draft_knowledge: "read_drafts", knowledge_applied_updates: "read_knowledge", draft_knowledge_applied_updates: "read_drafts", daily_summary: "read_projects", daily_activity: "read_workbench", schedule_items: "read_workbench" };
167
+ function missingSections(value, receipt = false) {
168
+ if (!Array.isArray(value))
169
+ return invalidResponse();
170
+ const seen = new Set();
171
+ return value.map(raw => { if (!record(raw) || typeof raw.section !== "string" || sections[raw.section] !== raw.required_capability || !["capability_disabled", ...(receipt ? ["read_failed"] : [])].includes(raw.reason) || seen.has(raw.section))
172
+ return invalidResponse(); seen.add(raw.section); return { section: raw.section, required_capability: raw.required_capability, reason: raw.reason }; });
173
+ }
174
+ function eventResult(value) {
175
+ if (!record(value) || typeof value.kind !== "string" || !calendarKinds.includes(value.kind) || !scalar(value.id, 200) || !/^[A-Za-z0-9:._-]+$/.test(value.id) || !validScheduleDate(value.date) || !validScheduleTime(value.occurred_at) || !scalar(value.snippet) || !scalar(value.title, 300) || typeof value.modified !== "boolean")
176
+ return invalidResponse();
177
+ const out = { kind: value.kind, id: value.id, date: value.date, occurred_at: value.occurred_at, ...actor(value), snippet: value.snippet, title: value.title, modified: value.modified };
178
+ for (const name of ["project_id", "knowledge_card_id"]) {
179
+ if (value[name] !== undefined) {
180
+ if (!scheduleUUID(value[name]))
181
+ invalidResponse();
182
+ out[name] = value[name];
183
+ }
184
+ }
185
+ if (value.project_title !== undefined) {
186
+ if (!scalar(value.project_title, 300) || !out.project_id)
187
+ invalidResponse();
188
+ out.project_title = value.project_title;
189
+ }
190
+ for (const name of ["from_status", "to_status"]) {
191
+ const status = value[name];
192
+ if (status === undefined)
193
+ continue;
194
+ if (typeof status !== "string")
195
+ return invalidResponse();
196
+ if (value.kind === "knowledge_growth") {
197
+ if (!["captured", "summarized", "applied", "reviewed", "templated"].includes(status))
198
+ return invalidResponse();
199
+ }
200
+ else if (value.kind === "project_stage") {
201
+ // Project stages are user text, bounded by the existing 30-code-point contract.
202
+ if (!scalar(status, 30) || !status || trim(status) !== status || /[\p{Cc}\u2028\u2029]/u.test(status))
203
+ return invalidResponse();
204
+ }
205
+ else {
206
+ if (status !== "")
207
+ return invalidResponse();
208
+ continue;
209
+ }
210
+ out[name] = status;
211
+ }
212
+ return out;
213
+ }
214
+ export function calendarReadResult(status, value, calendar) {
215
+ if (status !== 200 || !record(value) || value.timezone !== "Asia/Shanghai" || !validScheduleDate(value.start_date) || !validScheduleDate(value.end_date) || !validRange(value.start_date, value.end_date) || !Array.isArray(value.items) || !natural(value.total) || !natural(value.limit) || value.limit < 1 || value.limit > 100 || typeof value.has_more !== "boolean" || value.next_cursor !== undefined && !scalar(value.next_cursor, 4096) || typeof value.partial !== "boolean")
216
+ return invalidResponse();
217
+ const cursor = value.next_cursor ?? "";
218
+ if (value.has_more ? (!cursor || value.items.length !== value.limit) : cursor !== "")
219
+ invalidResponse();
220
+ const items = value.items.map(eventResult), ids = new Set();
221
+ if (items.length > value.limit || items.length > value.total)
222
+ invalidResponse();
223
+ let prior = Infinity;
224
+ for (const item of items) {
225
+ const k = `${item.kind}:${item.id}`;
226
+ if (ids.has(k) || String(item.date) < value.start_date || String(item.date) > value.end_date)
227
+ invalidResponse();
228
+ ids.add(k);
229
+ const time = Date.parse(item.occurred_at);
230
+ if (time > prior)
231
+ invalidResponse();
232
+ prior = time;
233
+ }
234
+ const missing = missingSections(value.missing_sections === undefined ? [] : value.missing_sections);
235
+ if (!value.partial && missing.length)
236
+ invalidResponse();
237
+ const out = { timezone: "Asia/Shanghai", start_date: value.start_date, end_date: value.end_date, items, total: value.total, limit: value.limit, has_more: value.has_more, next_cursor: cursor, partial: value.partial, missing_sections: missing };
238
+ if (Array.isArray(value.warnings)) {
239
+ const warnings = [...new Set(value.warnings.filter((v) => typeof v === "string" && ["historical_mirror_ambiguous", "historical_project_stage_not_recorded", "historical_actor_not_recorded", "historical_knowledge_growth_not_recorded", "actor_attribution_incomplete"].includes(v)))];
240
+ if (warnings.length)
241
+ out.warnings = warnings;
242
+ }
243
+ if (value.event_coverage !== undefined) {
244
+ if (!Array.isArray(value.event_coverage))
245
+ return invalidResponse();
246
+ const seen = new Set();
247
+ out.event_coverage = value.event_coverage.map(raw => { if (!record(raw) || typeof raw.stream !== "string" || !["project_stage", "actor", "knowledge_growth"].includes(raw.stream) || !validScheduleTime(raw.started_at) || seen.has(raw.stream))
248
+ return invalidResponse(); seen.add(raw.stream); return { stream: raw.stream, started_at: raw.started_at }; });
249
+ }
250
+ if (value.accumulation !== undefined || value.forgotten !== undefined) {
251
+ const a = value.accumulation, f = value.forgotten;
252
+ if (!record(a) || a.scope !== "filtered_period_events" || !validScheduleTime(a.as_of) || !natural(a.event_total) || a.event_total !== value.total || !natural(a.growth_total) || a.growth_total > a.event_total || !record(f) || f.scope !== "current_readable_assets" || !validScheduleTime(f.as_of))
253
+ return invalidResponse();
254
+ out.accumulation = { scope: a.scope, as_of: a.as_of, event_total: a.event_total, growth_total: a.growth_total, applied_cards: assetGroup(a.applied_cards, ["applied"]), reviewed_cards: assetGroup(a.reviewed_cards, ["reviewed"]), templated_cards: assetGroup(a.templated_cards, ["templated"]) };
255
+ out.forgotten = { scope: f.scope, as_of: f.as_of, projects: assetGroup(f.projects, ["draft", "active", "paused"]), knowledge: assetGroup(f.knowledge, ["captured"]) };
256
+ }
257
+ if (calendar || value.days !== undefined) {
258
+ if (!Array.isArray(value.days))
259
+ return invalidResponse();
260
+ const dates = new Set();
261
+ let dayTotal = 0;
262
+ out.days = value.days.map(raw => {
263
+ if (!record(raw) || !validScheduleDate(raw.date) || raw.date < value.start_date || raw.date > value.end_date || dates.has(raw.date) || !natural(raw.total) || !Array.isArray(raw.schedule_items) || !natural(raw.schedule_total) || typeof raw.schedule_has_more !== "boolean")
264
+ return invalidResponse();
265
+ dates.add(raw.date);
266
+ dayTotal += raw.total;
267
+ const day = { date: raw.date, total: raw.total };
268
+ for (const field of ["started_at", "finished_at"]) {
269
+ if (raw[field] !== null && !validScheduleTime(raw[field]))
270
+ invalidResponse();
271
+ day[field] = raw[field];
272
+ }
273
+ if (raw.started_actor_name !== null && (!scalar(raw.started_actor_name, 80) || !raw.started_actor_name))
274
+ invalidResponse();
275
+ if (raw.started_token_id !== null && !scheduleUUID(raw.started_token_id))
276
+ invalidResponse();
277
+ if (raw.started_at === null && (raw.started_actor_name !== null || raw.started_token_id !== null))
278
+ invalidResponse();
279
+ day.started_actor_name = raw.started_actor_name;
280
+ day.started_token_id = raw.started_token_id;
281
+ day.summary = raw.summary === null ? null : dailySummaryResult(raw.summary);
282
+ if (day.summary && (day.summary.date !== raw.date || raw.finished_at !== day.summary.confirmed_at))
283
+ invalidResponse();
284
+ // The same schedule projector used by existing schedule reads validates each entry.
285
+ let projected;
286
+ try {
287
+ projected = raw.schedule_items.map(item => scheduleReadResult(200, { timezone: "Asia/Shanghai", schedule_item: item }, true).schedule_item);
288
+ }
289
+ catch {
290
+ return invalidResponse();
291
+ }
292
+ if (projected.length > raw.schedule_total || raw.schedule_has_more !== (projected.length < raw.schedule_total))
293
+ invalidResponse();
294
+ day.schedule_items = projected;
295
+ day.schedule_total = raw.schedule_total;
296
+ day.schedule_has_more = raw.schedule_has_more;
297
+ return day;
298
+ });
299
+ if (dayTotal !== value.total)
300
+ invalidResponse();
301
+ }
302
+ return out;
303
+ }
304
+ function assetGroup(value, statuses) {
305
+ if (!record(value) || !Array.isArray(value.items) || value.items.length > 20 || !natural(value.total) || value.items.length > value.total || typeof value.has_more !== "boolean" || value.has_more !== (value.items.length < value.total))
306
+ return invalidResponse();
307
+ const seen = new Set();
308
+ const items = value.items.map(raw => { if (!record(raw) || !scheduleUUID(raw.id) || seen.has(raw.id) || !scalar(raw.title, 300) || !raw.title || typeof raw.status !== "string" || !statuses.includes(raw.status))
309
+ return invalidResponse(); seen.add(raw.id); const out = { id: raw.id, title: raw.title, status: raw.status }; if (raw.project_id !== undefined) {
310
+ if (!scheduleUUID(raw.project_id))
311
+ return invalidResponse();
312
+ out.project_id = raw.project_id;
313
+ } if (raw.project_title !== undefined) {
314
+ if (!out.project_id || !scalar(raw.project_title, 300) || !raw.project_title)
315
+ return invalidResponse();
316
+ out.project_title = raw.project_title;
317
+ } return out; });
318
+ return { items, total: value.total, has_more: value.has_more };
319
+ }
320
+ export function dailySummaryReceipt(status, value, key) {
321
+ if (![200, 201].includes(status) || !record(value) || !scheduleUUID(value.id) || value.write_status !== "confirmed" || typeof value.replayed !== "boolean" || value.code !== undefined || value.error !== undefined || value.read_permission_missing !== undefined && typeof value.read_permission_missing !== "boolean" || value.read_unavailable !== undefined && typeof value.read_unavailable !== "boolean")
322
+ throw calendarUnknown(key, status);
323
+ const out = { id: value.id, write_status: "confirmed", replayed: value.replayed };
324
+ try {
325
+ const missing = missingSections(value.missing_sections === undefined ? [] : value.missing_sections, true);
326
+ if (value.read_permission_missing === true || value.read_unavailable === true || missing.length) {
327
+ const unavailable = value.read_unavailable === true;
328
+ if (value.read_permission_missing === true && unavailable || missing.length !== 1 || missing[0].section !== "daily_summary" || missing[0].reason !== (unavailable ? "read_failed" : "capability_disabled"))
329
+ throw calendarUnknown(key, status);
330
+ out[unavailable ? "read_unavailable" : "read_permission_missing"] = true;
331
+ out.missing_sections = missing;
332
+ }
333
+ else {
334
+ out.daily_summary = dailySummaryResult(value.daily_summary);
335
+ if (out.daily_summary.id !== value.id)
336
+ throw calendarUnknown(key, status);
337
+ }
338
+ }
339
+ catch {
340
+ throw calendarUnknown(key, status);
341
+ }
342
+ return out;
343
+ }
344
+ export function isCalendarHeaderPath(path, method) { return method === "POST" && path === `${calendarRoot}/m2/daily-summary` || method === "GET" && path === `${calendarRoot}/m1/daily-summary-write-status`; }
345
+ export function calendarHeaders(path, method, value) {
346
+ if (!isCalendarHeaderPath(path, method) || !record(value) || Object.keys(value).some(k => !["Idempotency-Key", "If-Match"].includes(k)))
347
+ return calendarInvalid();
348
+ const out = { "Idempotency-Key": calendarKey(value["Idempotency-Key"]) };
349
+ if (method === "POST") {
350
+ const rev = value["If-Match"];
351
+ if (typeof rev !== "string" || !/^"(0|[1-9]\d*)"(?![\s\S])/.test(rev) || !validSummaryRevision(Number(rev.slice(1, -1))))
352
+ return calendarInvalid();
353
+ out["If-Match"] = rev;
354
+ }
355
+ else if (value["If-Match"] !== undefined)
356
+ return calendarInvalid();
357
+ return out;
358
+ }
@@ -0,0 +1,12 @@
1
+ import { dailySummaryDateBounds } from "./calendar-contract.js";
2
+ const key = "calendar-example-key-0001";
3
+ const readOptions = ["--start-date", "--end-date", "--project", "--actor-kind", "--actor-name", "--actor-source-id", "--kind", "--only-core", "--limit", "--cursor"];
4
+ const notes = "只读本人可见事件;默认最近7个北京时间自然日,含首尾最多一个日历年。按项目、本人/AI名称或来源ID、类型筛选;旧来源显示未记录,断开连接保留历史名称。同名AI可用来源ID区分。日历当天明细与时间线同源;总结只取每天最新一句,撤销的待办完成不显示。total是筛选后全部可读事件数,items只是当前页;partial/missing_sections不是没有记录。next_cursor不为空才翻下一页;数据或权限变化返回calendar_cursor_stale时从第一页重新读,不自动轮询或拼接新旧页面。";
5
+ export const calendarCommandHelp = {};
6
+ for (const command of ["calendar", "timeline"]) {
7
+ calendarCommandHelp[command] = { command, usage: `caotang ${command}`, description: command === "calendar" ? "按日读取开工、当天总结、事件及日程;未记录开工/未收工如实留空。" : "按时间倒序读取本人和各AI做过的事;接手另一个AI的活之前,先看它最近做了什么。", options: readOptions, fields: { "start-date": "选填,真实YYYY-MM-DD;与end-date同时给或同时省;默认最近7天。", "end-date": "选填,真实YYYY-MM-DD;与start-date同时给或同时省;默认今天。", project: "本人项目UUID,不接受none。", "actor-kind": "user、agent或unknown。", "actor-name": "确切来源名称,最多80个Unicode码点。", "actor-source-id": "可信来源连接UUID,配合agent筛选,不猜ID。", kind: "project_created、project_stage、project_update、checkin、knowledge_created、knowledge_growth、schedule_completed、schedule_cancelled或daily_summary。", "only-core": "true或false,省略为false。", limit: "1-100,默认50。", cursor: "上一页返回的不透明next_cursor,最多4096字符;与同一筛选和权限一起使用。" }, example: { command: `caotang ${command} --start-date 2026-11-09 --end-date 2026-11-09 --actor-name Codex` }, notes };
8
+ }
9
+ const writeNotes = "总结日期只接受北京时间今天及此前30个自然日(含),不接受未来;超范围返回summary_date_out_of_range和min_date/max_date。先向本人展示完整日期(北京时间)和当天核心一句,得到确认后才写confirmed:true;平台不能核实口头确认,也不生成或改写总结。summary去首尾空白后1-200个Unicode码点,不含换行/控制符。写归write_project_update,读归read_projects;只读连接不能写。一次保存成功记录收工时刻,同日修改只展示最新一句并标已修改,原键回放不推进收工。幂等键16-128位安全ASCII;revision=0仅首次创建,修改用读回revision。202、网络失败或畸形回执不是成功,保留原键和连接,用daily-summary status核对,不自动换键重试。N22精简回执只确认ID与成败,不代表读到了正文。";
10
+ calendarCommandHelp["daily-summary save"] = { command: "daily-summary save", usage: "caotang daily-summary save --input file|- --idempotency-key KEY --revision N", description: "保存本人已确认的当天核心一句,同时记录收工;同日修改使用当前版本。", options: ["--input", "--idempotency-key", "--revision"], fields: { date: "必填,真实YYYY-MM-DD;仅北京时间今天及此前30天(含),不接受未来。", summary: "必填,已确认的1-200码点单行原文。", confirmed: "必填且只能为true。", revision: "--revision N;首次0,修改为最近读回的正安全整数。" }, required_fields: ["date", "summary", "confirmed"], example: { command: `caotang daily-summary save --input - --idempotency-key ${key} --revision 0`, input: { date: dailySummaryDateBounds().max_date, summary: "本人已确认的当天核心一句", confirmed: true } }, notes: writeNotes };
11
+ calendarCommandHelp["daily-summary status"] = { command: "daily-summary status", usage: "caotang daily-summary status --idempotency-key KEY", description: "只读核对同一连接的原总结回执;原键只放header,不放query/body。", options: ["--idempotency-key"], example: { command: `caotang daily-summary status --idempotency-key ${key}` }, notes: writeNotes };
12
+ export const calendarCommandUsages = Object.values(calendarCommandHelp).map(v => v.usage.replace(/^caotang /, ""));