@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
@@ -0,0 +1,64 @@
1
+ import { CliError } from "./errors.js";
2
+ import { validateCommandArguments } from "./input-validation.js";
3
+ import { CalendarProblem, calendarFailure, calendarInvalid, calendarKey, calendarQuery, calendarReadResult, calendarRoot, calendarUnknown, dailySummaryWriteBody, dailySummaryReceipt, validSummaryRevision } from "./calendar-contract.js";
4
+ export async function executeCalendar(args, flags, ctx) {
5
+ try {
6
+ validateCommandArguments(args, flags);
7
+ let path, key, options = { method: "GET", token: ctx.token };
8
+ if (args[0] === "calendar" || args[0] === "timeline") {
9
+ const names = { "start-date": "start_date", "end-date": "end_date", project: "project_id", "actor-kind": "actor_kind", "actor-name": "actor_name", "actor-source-id": "actor_source_id", kind: "kind", "only-core": "only_core", limit: "limit", cursor: "cursor" };
10
+ const input = {};
11
+ for (const [name, value] of Object.entries(flags)) {
12
+ if (!Object.hasOwn(names, name))
13
+ calendarInvalid();
14
+ if (name === "limit") {
15
+ if (typeof value !== "string" || !/^\d+$/.test(value))
16
+ calendarInvalid();
17
+ input[names[name]] = Number(value);
18
+ }
19
+ else if (name === "only-core") {
20
+ if (value !== true && value !== false && value !== "true" && value !== "false")
21
+ calendarInvalid();
22
+ input[names[name]] = value === true || value === "true";
23
+ }
24
+ else
25
+ input[names[name]] = value;
26
+ }
27
+ const query = new URLSearchParams(Object.entries(calendarQuery(input)).map(([k, v]) => [k, String(v)])).toString();
28
+ path = `${calendarRoot}/m1/${args[0]}${query ? `?${query}` : ""}`;
29
+ }
30
+ else if (args[0] === "daily-summary" && args[1] === "status") {
31
+ key = calendarKey(flags["idempotency-key"]);
32
+ path = `${calendarRoot}/m1/daily-summary-write-status`;
33
+ options.headers = { "Idempotency-Key": key };
34
+ }
35
+ else if (args[0] === "daily-summary" && args[1] === "save") {
36
+ key = calendarKey(flags["idempotency-key"]);
37
+ const rev = flags.revision;
38
+ if (typeof rev !== "string" || !/^(0|[1-9]\d*)$/.test(rev) || !validSummaryRevision(Number(rev)) || typeof flags.input !== "string" || !flags.input.trim())
39
+ calendarInvalid();
40
+ const body = dailySummaryWriteBody(await ctx.readInput(flags.input));
41
+ path = `${calendarRoot}/m2/daily-summary`;
42
+ options = { ...options, method: "POST", body, headers: { "Idempotency-Key": key, "If-Match": `"${rev}"` } };
43
+ }
44
+ else
45
+ return calendarInvalid();
46
+ let result;
47
+ try {
48
+ result = await ctx.api.request(path, options);
49
+ }
50
+ catch {
51
+ if (key)
52
+ throw calendarUnknown(key);
53
+ throw new CalendarProblem("network_error", "日历或时间线读取未完成。", "检查当前执行环境的网络;不要反复重试或重新授权。", 6);
54
+ }
55
+ if (![200, 201].includes(result.status))
56
+ throw calendarFailure(result.status, result.body, key, key ? (args[1] === "save" ? "write" : "status") : "read");
57
+ return { ...result, body: key ? dailySummaryReceipt(result.status, result.body, key) : calendarReadResult(result.status, result.body, args[0] === "calendar") };
58
+ }
59
+ catch (error) {
60
+ if (error instanceof CalendarProblem)
61
+ throw new CliError(error.code, error.humanMessage, error.nextAction, error.exitCode, error.details);
62
+ throw error;
63
+ }
64
+ }
@@ -38,6 +38,8 @@ export function commandAcceptsSwitch(args, capability) {
38
38
  eligible = op === "create" || op === "update" ? ["write_project"] : ["read_projects", "read_drafts"];
39
39
  break;
40
40
  case "search":
41
+ eligible = ["read_projects", "read_knowledge", "read_workbench"];
42
+ break;
41
43
  case "activity":
42
44
  eligible = ["read_projects", "read_knowledge"];
43
45
  break;
@@ -47,6 +49,9 @@ export function commandAcceptsSwitch(args, capability) {
47
49
  case "checkins":
48
50
  eligible = ["read_projects"];
49
51
  break;
52
+ case "context":
53
+ eligible = ["read_workbench"];
54
+ break;
50
55
  case "source":
51
56
  case "upload":
52
57
  eligible = ["write_knowledge_sources"];
package/dist/cli.js CHANGED
@@ -9,23 +9,30 @@ import { CredentialStore } from "./credential-store.js";
9
9
  import { executeCommand } from "./commands.js";
10
10
  import { startReportMode } from "./habit-day.js";
11
11
  import { KNOWLEDGE_GROWTH_HELP } from "./generated/knowledge-growth.js";
12
+ import { getAgentOperations } from "./generated/agent-operations.js";
13
+ import { exportSkillsBundle } from "./skills-export.js";
14
+ import { executeProjectBindingCLI, isProjectBindingCommand, projectBindingLocalScopeOutcome } from "./project-binding-cli.js";
12
15
  import { resolveConfig } from "./config.js";
13
16
  import { CliError } from "./errors.js";
14
17
  import { ApiClient } from "./http.js";
15
18
  import { explicitUnknownWrite, redact, renderError, renderResult } from "./output.js";
16
- import { localDoctor, localSetup, localVersion, manualUpdateInstructions, selectedPlatforms } from "./local-commands.js";
19
+ import { localDoctor, localSetup, localVersion, manualUpdateInstructions, selectedPlatforms, selectedSkillsDirectory } from "./local-commands.js";
17
20
  import { commandHelp } from "./command-help.js";
21
+ import { scheduleCommandUsages } from "./schedule-help.js";
22
+ import { calendarCommandUsages } from "./calendar-help.js";
23
+ import { WRITE_ROUTING_HELP } from "./write-routing.js";
18
24
  import { flagError, validateCommandArguments } from "./input-validation.js";
19
25
  const help = {
20
26
  brand: "一间草堂",
21
27
  usage: "caotang --agent <你的名字> <command> [--env production|test] [--human]",
28
+ write_routing: WRITE_ROUTING_HELP,
22
29
  advance_example: { command: "caotang advance CARD_ID --input -", input: { internalization_status: "applied", source_type: "project", source_id: "PROJECT_ID", evidence_summary: "经本人确认,这张卡实际用于该项目的登录回归检查。" } },
23
- commands: ["setup [--platform claude-code,codex,openclaw,kimi-code,kimi-work,workbuddy] [--no-open]", "doctor [--platform claude-code,codex,openclaw,kimi-code,kimi-work,workbuddy]", "version", "update", "auth login [--device-name name] [--client-name name] [--no-open]", "auth status", "auth logout", "context [--since cursor]", "context --start", "rules", "activity --start-date YYYY-MM-DD --end-date YYYY-MM-DD [--limit N --offset N]", "projects", "project <id>", "project create --input file|-", "project update <id> --input file|-", "progress list <id>", "checkins", "progress add <id> --input file|- [--idempotency-key key]", "cognition set <id> --input file|-", "stage set <id> --input file|-", "knowledge", "knowledge show <id>", "search [query]", "knowledge add --input file|-", "knowledge update <id> --input file|-", "source add <id> --input file|-", "link <card> <project> [--core]", "unlink <card> <project>", "advance <card> --input file|-", "upload <pdf> --card <id> [--source-title title]", "pending", "notifications [--filter all|unread|read|agent]", "drafts", "request delete --input file|-", "request archive --input file|-", "request visibility --input file|-"],
30
+ commands: ["setup [--platform claude-code,codex,openclaw,kimi-code,kimi-work,workbuddy] [--no-open]", "setup --skills-dir /absolute/parent [--no-open]", "doctor [--platform claude-code,codex,openclaw,kimi-code,kimi-work,workbuddy]", "doctor --skills-dir /absolute/parent", "skills export --output file.zip", "version", "update", "guide", "auth login [--device-name name] [--client-name name] [--no-open]", "auth status", "auth logout", "context [--since cursor]", "context --handoff", "context --start", "rules", "activity --start-date YYYY-MM-DD --end-date YYYY-MM-DD [--limit N --offset N]", "projects", "project <id>", "project create --input file|-", "project update <id> --input file|-", "progress list <id>", "checkins", "progress add <id> --input file|- [--idempotency-key key]", "cognition set <id> --input file|-", "stage set <id> --input file|-", "knowledge", "knowledge show <id>", "search [query]", "knowledge add --input file|-", "knowledge update <id> --input file|-", "source add <id> --input file|-", "link <card> <project> [--core]", "unlink <card> <project>", "advance <card> --input file|-", "upload <pdf> --card <id> [--source-title title]", "pending", "notifications [--filter all|unread|read|agent]", "drafts", "request delete --input file|-", "request archive --input file|-", "request visibility --input file|-"],
24
31
  platform_scope: "kimi-code、kimi-work(Kimi.app工作模式)和workbuddy仅支持macOS本机安装;目录发现不代表真实AI端到端通过。Kimi Code和Kimi工作模式是独立入口,不互相代替。",
25
32
  options: ["--limit", "--offset", "--q", "--project", "--topic", "--status", "--publication-status", "--start-date YYYY-MM-DD", "--end-date YYYY-MM-DD", "--date-field created_at|updated_at", "--since", "notifications: --filter", "upload: --source-title / --summary"],
26
33
  command_help: "每个命令用 --help 查看自己的字段和固定 JSON 示例;写入 JSON 支持 --input -。knowledge 不带参数是知识列表;记录一件事完成了,用 progress add。source add 的 source_title 选填;progress add 的 visibility 省略时默认 private。",
27
34
  shared_connection: "每条命令都带 --agent <你的名字>,只有这个 AI 的连接会被读取、复用或断开。auth logout 先经本人确认,再吊销这个 AI 的连接;其它 AI 不受影响。重新接入使用相同名字 setup --agent <你的名字>,由本人在网页批准。",
28
- query: "search [query]统一搜索项目、项目进展、知识和手写进展;省略或空白词按日期浏览,多个词全部命中、不分顺序和大小写,按字面匹配而非语义搜索。--status/--topic/--publication-status/--project只筛知识分支,--project不会限制其它类型;定向项目进展用progress list ID,仅知识用knowledge --q。搜索返回kind/snippet等统一结果项,不再是旧知识DTO;search、projects、knowledge可加--core-only只读筛选;结果is_core按项目已有标记、知识发芽及以上(applied/reviewed/templated)分别判断,它不是新的可写星标。只看核心项目默认不含已归档;显式 projects --core-only --status archived 可查已归档核心项目。只看核心不含项目进展和手写进展;要查核心项目的进展,先查核心项目,再用progress list ID。项目、知识、search、progress list、checkins支持北京时间含首尾日期与limit/offset分页,返回total、has_more、limit、offset;默认20/0,日期列表最多50条,pending最多100条。项目/知识默认updated_at,可created_at;进展固定created_at。pending默认有效待确认,--status all查历史。",
35
+ query: "search [query]统一搜索项目、项目进展、知识、手写进展、待办标题(pending/completed/cancelled,归read_workbench)和当天总结正文(归read_projects);省略或空白词按日期浏览,多个词全部命中、不分顺序和大小写,按字面匹配而非语义搜索。--status/--topic/--publication-status/--project只筛知识分支,--project不会限制其它类型;定向项目进展用progress list ID,仅知识用knowledge --q。搜索返回kind/snippet等统一结果项,不再是旧知识DTO;search、projects、knowledge可加--core-only只读筛选;结果is_core按项目已有标记、知识发芽及以上(applied/reviewed/templated)分别判断,它不是新的可写星标。只看核心项目默认不含已归档;显式 projects --core-only --status archived 可查已归档核心项目。只看核心不含项目进展、手写进展、待办和当天总结;待办不继承所属项目核心标记。AI知识分支默认published,显式--publication-status all仍受read_drafts限制;搜索至少需read_projects/read_knowledge/read_workbench之一。要查核心项目的进展,先查核心项目,再用progress list ID。项目、知识、search、progress list、checkins支持北京时间含首尾日期与limit/offset分页,返回total、has_more、limit、offset;默认20/0,日期列表最多50条,pending最多100条。项目/知识/待办默认updated_at,可created_at;进展固定created_at。当天总结仅返date/confirmed_at,按业务日期筛选、按confirmed_at排序,不伪造created_at/updated_at,date_field不改变总结日期口径。pending默认有效待确认,--status all查历史。",
29
36
  activity: "activity仅接受必填起止日期与limit/offset,日期真实且北京时间含首尾最多一个日历年,闰日周年归一到3月1日。days是全范围日统计,不是当前页;items/total只含可读明细,默认20/0、最多50条。null和未完整覆盖不能解释为0,草稿受限不计入;只有符合资格的知识应用进展可匿名计数,不返身份/正文。不支持q、date-field、项目/长势/核心筛选。",
30
37
  knowledge_growth: `机器枚举保持不变;可读知识返回growth,真实变化返回from_growth/to_growth,安全逐级提示返回current_growth/next_growth(中文名、说明、level/total)。未返回或未知状态不推断,不补发请求。五级长势:${KNOWLEDGE_GROWTH_HELP}`,
31
38
  growth_confirmation: "用户说某张卡已经用上时,可以主动问一次:“你把这张卡用在这个项目,做了这件事,要记为发芽吗?”复述已知事实,只问缺的;未来打算、沉默、拒绝或只报项目名都不是同意。没有项目或尚未关联,先问要不要新建或关联,不同意就不推进。播种到发芽先说清“先生根、再发芽”,本人明确同意两步后才逐级调用;第一步未确认成功不发第二步。第二步明确失败只报最后确认的级别;待审或结果未知说明哪个结果未确认,不自动重试。 从生根进入发芽必须带 source_type=project、已关联的本人项目 source_id、一句本人确认的实际使用说明 evidence_summary;同时需要写知识长势和写项目进展权限。状态、应用证据、项目时间线一起写入。网页从发芽以下直接跳到更高级也要填写这一表单。退级、同级、已发芽后的推进不要求补项目或说明;不回填历史。 link --core 是兼容关联参数,不会设置项目或知识的核心标记。",
@@ -35,12 +42,19 @@ const help = {
35
42
  safety: "不要将令牌放入参数。写入不会自动重试;待审核202退出0,不是业务完成;结果未知的写入退出6,不执行依赖步骤。202需按回执区分待处理与write_outcome_unknown(写入结果未确认,不保证后台队列,请先在网页核对)。"
36
43
  };
37
44
  const usageError = () => new CliError("invalid_arguments", "命令或参数无效。", "运行 --help,写入数据使用 --input file 或 --input -。");
45
+ help.commands.push(...scheduleCommandUsages);
46
+ help.commands.push(...calendarCommandUsages);
47
+ help.commands.push("project bind <id> [--include-subdirs] [--confirm DIGEST --local-workspace]", "project binding --local-workspace", "project unbind --local-workspace [--confirm DIGEST]");
38
48
  const parserOptions = {
39
- agent: { type: "string" }, env: { type: "string" }, human: { type: "boolean" }, help: { type: "boolean", short: "h" }, version: { type: "boolean", short: "v" }, platform: { type: "string" },
49
+ agent: { type: "string" }, env: { type: "string" }, human: { type: "boolean" }, help: { type: "boolean", short: "h" }, version: { type: "boolean", short: "v" }, platform: { type: "string" }, "skills-dir": { type: "string" }, output: { type: "string" },
50
+ confirm: { type: "string" }, "local-workspace": { type: "boolean" }, "include-subdirs": { type: "boolean" },
40
51
  input: { type: "string" }, limit: { type: "string" }, offset: { type: "string" }, q: { type: "string" }, project: { type: "string" }, topic: { type: "string" }, status: { type: "string" }, "publication-status": { type: "string" }, since: { type: "string" }, filter: { type: "string" },
41
- start: { type: "boolean" }, "start-date": { type: "string" }, "end-date": { type: "string" }, "date-field": { type: "string" }, "core-only": { type: "boolean" },
52
+ start: { type: "boolean" }, handoff: { type: "boolean" }, "start-date": { type: "string" }, "end-date": { type: "string" }, "date-field": { type: "string" }, "core-only": { type: "boolean" },
42
53
  "idempotency-key": { type: "string" }, card: { type: "string" }, "source-title": { type: "string" }, summary: { type: "string" }, core: { type: "boolean" },
43
- "device-name": { type: "string" }, "client-name": { type: "string" }, "no-open": { type: "boolean" }
54
+ "device-name": { type: "string" }, "client-name": { type: "string" }, "no-open": { type: "boolean" },
55
+ view: { type: "string" }, "group-by": { type: "string" }, revision: { type: "string" },
56
+ "actor-kind": { type: "string" }, "actor-name": { type: "string" }, "actor-source-id": { type: "string" },
57
+ kind: { type: "string" }, "only-core": { type: "string" }, cursor: { type: "string" }
44
58
  };
45
59
  // Node's strict parser treats a separate negative numeric value as an option.
46
60
  // Normalize only offset values, so both syntaxes reach the same range check;
@@ -168,14 +182,35 @@ export async function runCLI(args, options = {}) {
168
182
  throw usageError();
169
183
  positionals = ["version"];
170
184
  }
171
- if (!["setup", "doctor", "version", "update", "auth", "context", "rules", "activity", "projects", "project", "progress", "checkins", "cognition", "stage", "knowledge", "search", "source", "link", "unlink", "advance", "upload", "pending", "notifications", "drafts", "request"].includes(positionals[0] ?? ""))
185
+ if (!["setup", "doctor", "version", "update", "guide", "skills", "auth", "context", "rules", "activity", "projects", "project", "progress", "checkins", "cognition", "stage", "knowledge", "search", "source", "link", "unlink", "advance", "upload", "pending", "notifications", "drafts", "request", "schedule", "calendar", "timeline", "daily-summary"].includes(positionals[0] ?? ""))
172
186
  throw usageError();
173
187
  validateCommandArguments(positionals, flags);
188
+ if (positionals[0] === "guide") {
189
+ if (parsed.values.env !== undefined && !["production", "test"].includes(parsed.values.env)) {
190
+ throw new CliError("invalid_environment", "环境只能是 production 或 test。", "默认使用生产环境;测试时指定 --env test。");
191
+ }
192
+ if (parsed.values.agent !== undefined)
193
+ agentIdentity(parsed.values.agent);
194
+ stdout(renderResult({ status: 200, body: getAgentOperations() }, human, []));
195
+ return 0;
196
+ }
197
+ if (positionals[0] === "skills") {
198
+ if (positionals.length !== 2 || positionals[1] !== "export" || typeof flags.output !== "string" || !flags.output.trim()
199
+ || Object.keys(flags).some(key => key !== "output"))
200
+ throw usageError();
201
+ if (parsed.values.env !== undefined && !["production", "test"].includes(parsed.values.env))
202
+ throw new CliError("invalid_environment", "环境只能是 production 或 test。", "公共导出无需连接任何环境。");
203
+ if (parsed.values.agent !== undefined)
204
+ agentIdentity(parsed.values.agent);
205
+ stdout(renderResult(await exportSkillsBundle({ outputPath: flags.output, packageRoot: options.packageRoot }), human, []));
206
+ return 0;
207
+ }
174
208
  const localCommand = ["setup", "doctor", "version", "update"].includes(positionals[0]);
175
209
  if (localCommand) {
176
- const allowed = positionals[0] === "setup" ? ["platform", "device-name", "client-name", "no-open"] : positionals[0] === "doctor" ? ["platform"] : [];
210
+ const allowed = positionals[0] === "setup" ? ["platform", "skills-dir", "device-name", "client-name", "no-open"] : positionals[0] === "doctor" ? ["platform", "skills-dir"] : [];
177
211
  if (positionals.length !== 1 || Object.keys(flags).some(name => !allowed.includes(name)))
178
212
  throw usageError();
213
+ selectedSkillsDirectory(flags);
179
214
  selectedPlatforms(flags.platform, options.operatingSystem);
180
215
  }
181
216
  else if (flags.platform !== undefined)
@@ -195,6 +230,13 @@ export async function runCLI(args, options = {}) {
195
230
  return 0;
196
231
  }
197
232
  const agent = agentIdentity(parsed.values.agent);
233
+ if (isProjectBindingCommand(positionals)) {
234
+ const missingScope = projectBindingLocalScopeOutcome(positionals, flags);
235
+ if (missingScope) {
236
+ stdout(renderResult(missingScope.result, human, []));
237
+ return missingScope.exitCode;
238
+ }
239
+ }
198
240
  const env = options.env ?? process.env, homeDir = options.homeDir ?? homedir();
199
241
  let config, configurationFailure;
200
242
  try {
@@ -229,8 +271,14 @@ export async function runCLI(args, options = {}) {
229
271
  else {
230
272
  const credential = await resolveCredential(context);
231
273
  secrets.push(credential.token);
232
- result = await executeCommand(positionals, flags, { api, token: credential.token, readInput: options.readInput ?? ((file) => readInput(file, options.signal)),
233
- startMode: owner => startReportMode(config.homeDir, config.environment, owner, options.now?.() ?? Date.now()) });
274
+ if (isProjectBindingCommand(positionals)) {
275
+ const binding = await executeProjectBindingCLI(positionals, flags, { auth: context, token: credential.token, homeDir: config.homeDir, cwd: options.cwd ?? process.cwd(), localScopeConfirmed: flags["local-workspace"] === true });
276
+ result = binding.result;
277
+ exitCode = binding.exitCode;
278
+ }
279
+ else
280
+ result = await executeCommand(positionals, flags, { api, token: credential.token, readInput: options.readInput ?? ((file) => readInput(file, options.signal)),
281
+ startMode: owner => startReportMode(config.homeDir, config.environment, owner, options.now?.() ?? Date.now()) });
234
282
  // All validated business writes take --input except link, unlink and upload.
235
283
  if ((flags.input !== undefined || ["link", "unlink", "upload"].includes(positionals[0])) && explicitUnknownWrite(result))
236
284
  exitCode = 6;
@@ -1,4 +1,7 @@
1
1
  import { KNOWLEDGE_GROWTH_STEPS } from "./generated/knowledge-growth.js";
2
+ import { scheduleCommandHelp } from "./schedule-help.js";
3
+ import { calendarCommandHelp } from "./calendar-help.js";
4
+ import { WRITE_ROUTING_NOT_THIS } from "./write-routing.js";
2
5
  const dates = ["--start-date", "--end-date", "--date-field"];
3
6
  const page = ["--limit", "--offset"];
4
7
  const knowledge = [...page, "--core-only", "--q", "--project", "--topic", "--status", "--publication-status", ...dates];
@@ -27,6 +30,9 @@ const read = (command, args, description, options = [], notes) => ({
27
30
  example: { command: `caotang ${command}${args.replace(/<[^>]+>/g, id)}` }, ...(notes ? { notes } : {}),
28
31
  });
29
32
  const help = {
33
+ "project bind": { ...read("project bind", " <id>", "仅预览指定项目的本机目录关联;不写开工或平台业务。", ["--include-subdirs", "--confirm", "--local-workspace"], "先核对目录、实际服务、已核账号及真实项目;预览不创建本机绑定。仅本人明确同意后才以--confirm DIGEST和--local-workspace记录独立本机范围;参数和摘要不自证同意。确认重核范围,不读取目录正文,不上传路径。必要目标详情检查并非0内容轻量API。"), usage: "caotang project bind <id> [--include-subdirs] [--confirm DIGEST --local-workspace]" },
34
+ "project binding": { ...read("project binding", "", "查看当前本机目录的有效项目关联。", ["--local-workspace"], "本机范围未由本人明确确认时只报缺口;不同账号/实际服务不读取旧关联。目标不可读、目录变化或状态未知不显示旧项目ID/标题,也不自动修复。"), usage: "caotang project binding --local-workspace" },
35
+ "project unbind": { ...read("project unbind", "", "先预览,再按本人明确确认解除当前精确目录的本机关联。", ["--confirm", "--local-workspace"], "不删除、归档或修改平台项目;目标项目已不可读也可安全解除当前本机映射,不回显旧ID/标题。只继承父目录时需回绑定根目录确认,不能从子目录解除父绑定。确认摘要仅防范围变化,不替代本人同意。"), usage: "caotang project unbind --local-workspace [--confirm DIGEST]" },
30
36
  "project create": write("project create", "", "创建项目;标题和简介必填。", {
31
37
  title, summary: projectSummary, stage: "选填,最多 30 字符。", stages: "选填,最多 8 个阶段,每项最多 30 字符。",
32
38
  cognition_summary: "选填,最多 1000 字符。", visibility: privateOnly, status: "选填:draft、active、paused、archived、hidden;默认 active。",
@@ -34,14 +40,14 @@ const help = {
34
40
  "project update": write("project update", " <id>", "修改项目标题或简介,至少提供一个字段。", {
35
41
  title: "选填,去首尾空白后 2-80 个 Unicode 字符。", summary: "选填,去首尾空白后最多 280 字符;只有当前状态为 draft 的项目可清空,最终由服务端按当前状态校验。",
36
42
  }, [], { summary: "本人确认的新简介" }),
37
- "progress add": write("progress add", " <id>", "记录一件事完成了,用 progress add。", {
43
+ "progress add": write("progress add", " <id>", `${WRITE_ROUTING_NOT_THIS.progress} 记录一件事完成了,用 progress add。`, {
38
44
  body: "必填,2-360 个 Unicode 字符。", update_type: "选填:progress、knowledge_applied、cognition、stage;默认 progress。", visibility: privateOnly,
39
45
  }, ["body"], { body: "已完成本人确认的工作", visibility: "private" }, true),
40
- "cognition set": write("cognition set", " <id>", "更新项目认知摘要。", { cognition_summary: "必填字符串,最多 1000 字符,可为空。" }, ["cognition_summary"], { cognition_summary: "本人确认的新认识" }),
46
+ "cognition set": write("cognition set", " <id>", `${WRITE_ROUTING_NOT_THIS.cognition} 更新项目认知摘要。`, { cognition_summary: "必填字符串,最多 1000 字符,可为空。" }, ["cognition_summary"], { cognition_summary: "本人确认的新认识" }),
41
47
  "stage set": write("stage set", " <id>", "更新项目阶段;至少提供一个字段,不是记录已完成工作的入口。", {
42
48
  stage: "选填,非空,最多 30 字符。", stages: "选填,1-8 个非空阶段,每项最多 30 字符。", status: "选填:draft、active、paused。",
43
49
  }, [], { stage: "验证" }),
44
- "knowledge add": write("knowledge add", "", "保存本人确认的知识分析;intent 不是选填。", {
50
+ "knowledge add": write("knowledge add", "", `${WRITE_ROUTING_NOT_THIS.knowledge} 保存本人确认的知识分析;intent 不是选填。`, {
45
51
  title: "必填,2-160 字符。", intent: "必填,2-500 字符;用户没说时可拟一句用途并告知本人,确认后保存。",
46
52
  summary: "必填,1-4000 字符;分析正文不堆网址。", source_type: "必填:manual、douyin、xiaohongshu、forum、book、publisher_preview、expert、agent、web;不补默认值。",
47
53
  source_label: "选填,最多 300 字符。", topic: "选填,最多 120 字符。", linked_project_id: "选填,关联项目 ID。",
@@ -61,10 +67,11 @@ const help = {
61
67
  project: read("project", " <id>", "读取指定项目详情。"),
62
68
  knowledge: read("knowledge", "", "knowledge 不带参数是知识列表;记录一件事完成了,用 progress add。", knowledge, "默认 published;草稿用 --publication-status draft,需要读取知识和读取草稿权限。"),
63
69
  "knowledge show": read("knowledge show", " <id>", "读取指定知识详情。"),
64
- search: { ...read("search", "", "按名称或关键词统一搜索项目、进展和知识;省略查询词按日期浏览。", knowledge.filter(option => option !== "--q"), "--project、--topic、--status、--publication-status 只筛知识分支;定向项目进展用 progress list。"), usage: "caotang search [query]", example: { command: 'caotang search "项目名称"' } },
70
+ search: { ...read("search", "", "按名称或关键词统一搜索项目、进展、知识、待办标题和当天总结正文;省略查询词按日期浏览。", knowledge.filter(option => option !== "--q"), "--project、--topic、--status、--publication-status 只筛知识分支;定向项目进展用 progress list。待办含 pending(未完成)、completed(已完成)、cancelled(已取消),归 read_workbench;搜索须至少开启 read_projects、read_knowledge、read_workbench 之一。AI 知识分支默认 published,显式 --publication-status all 可含可读草稿;关闭 read_drafts 仍不读草稿。--core-only 不含待办和当天总结,待办不继承所属项目的核心标记。待办日期用 created_at / updated_at。当天总结归 read_projects,返回 date 和 confirmed_at;按业务日期筛选、按 confirmed_at 排序,不提供创建/更新时间,不伪造 created_at/updated_at,date_field 不改变总结日期口径。"), usage: "caotang search [query]", example: { command: 'caotang search "项目名称"' } },
65
71
  "progress list": read("progress list", " <id>", "读取指定项目的进展。", [...page, ...dates]),
66
72
  checkins: read("checkins", "", "列出手写进展。", [...page, ...dates]),
67
- context: read("context", "", "读取工作台上下文;--start 读取新工作对话的短上下文。", ["--start", "--limit", "--since"], "--start 不与 --limit 或 --since 混用;短读不覆盖账号全部内容,不据此判断账号为空。"),
73
+ context: read("context", "", "读取工作台上下文;--handoff 纯读接手,--start 读取新工作对话的短上下文并记录开工。", ["--handoff", "--project", "--start", "--limit", "--since"], "--handoff 不与 --start、--limit、--since 混用;获准整体摘要时可用 --project UUID 指定焦点,它不会缩小整体摘要范围。只查看某项目请先用 project ID / progress list ID 定向读取,不先拉全局简报。不支持新协议时不回退开工;--start 不与 --limit 或 --since 混用。短读不覆盖账号全部内容,不据此判断账号为空。"),
74
+ guide: read("guide", "", "离线读取公共 Agent 短操作合同,不读取身份、凭据或私人工作内容。"),
68
75
  rules: read("rules", "", "读取本人给 AI 的规矩。"),
69
76
  activity: { ...read("activity", "", "按北京时间起止日期读取每日活动统计。", ["--start-date", "--end-date", ...page]), usage: "caotang activity --start-date YYYY-MM-DD --end-date YYYY-MM-DD", example: { command: "caotang activity --start-date 2026-11-01 --end-date 2026-11-07" } },
70
77
  pending: read("pending", "", "读取待确认申请;默认有效待确认,--status all 查历史。", [...page, "--status"]),
@@ -73,8 +80,9 @@ const help = {
73
80
  link: read("link", " <card> <project>", "将知识关联到项目,需本人确认;--core 只是兼容关联参数,不设置核心标记。", ["--core"], "写入不自动重试,待审或结果未知时先在网页核对。"),
74
81
  unlink: read("unlink", " <card> <project>", "解除知识与项目关联,需本人确认;不会删除知识或项目。", [], "写入不自动重试,待审或结果未知时先在网页核对。"),
75
82
  upload: { ...read("upload", " <pdf> --card <id>", "上传本地普通 PDF 并关联知识卡,不接受链接或标准输入。", ["--card", "--source-title", "--summary"], "仅 1-10485760 字节的普通 PDF;不接受符号链接。不自动重试上传或申请替代凭证。"), fields: { card: "必填,知识卡 ID。", "source-title": "选填,最多 300 字符。", summary: "选填,最多 1200 字符。" }, example: { command: `caotang upload ./reference.pdf --card ${id}` } },
76
- setup: read("setup", "", "安装选定 AI 入口并连接草堂,不改 shell 或 PATH 配置。", ["--platform", "--device-name", "--client-name", "--no-open"], "--platform 可选 claude-code、codex、openclaw、kimi-code、kimi-work、workbuddy;可逗号分隔。"),
77
- doctor: read("doctor", "", "检查本机安装与连接状态,不输出凭据。", ["--platform"]),
83
+ setup: read("setup", "", "安装选定 AI 入口并连接草堂,不改 shell 或 PATH 配置。", ["--platform", "--skills-dir", "--device-name", "--client-name", "--no-open"], "--skills-dir 为七项技能子目录的绝对父目录,与 --platform 互斥;当前受管写入保留 HOME 围栏。--platform 可选 claude-code、codex、openclaw、kimi-code、kimi-work、workbuddy;可逗号分隔。"),
84
+ doctor: read("doctor", "", "检查本机安装与连接状态,不输出凭据。", ["--platform", "--skills-dir"], "显式技能目录与 --platform 互斥;多个已记录目录不自动选择或扫描。"),
85
+ "skills export": { ...read("skills export", "", "离线导出官方七项公共技能;不读取个人安装目录或身份,不上传。", ["--output"], "--output 必填新ZIP文件路径;父目录须已存在且安全,已有目标不覆盖。不等于安装、授权或宿主已加载。"), usage: "caotang skills export --output file.zip", example: { command: "caotang skills export --output ./caotang-skills.zip" } },
78
86
  version: read("version", "", "查看当前 CLI 候选版本。"),
79
87
  update: read("update", "", "当前为未公开发布候选,不能自动更新。"),
80
88
  "auth login": read("auth login", "", "已有有效凭据时验证并复用,不发起新授权;不是强制重新连接。", ["--device-name", "--client-name", "--no-open"], "没有凭据时发起授权,在指定环境的网页输入确认码并批准后回来。文件凭据明确 401 失效才清理后新授权;环境令牌覆盖不会回退,需先处理覆盖。若要切换账号,先明确同意共享影响后 auth logout,再在同一 --env 下 auth login 并由目标账号批准。不要读取凭据文件。"),
@@ -87,12 +95,14 @@ for (const operation of ["delete", "archive", "visibility"]) {
87
95
  ...(operation === "visibility" ? { visibility: "必填:public 或 private;这是申请,不是直接公开。" } : {}),
88
96
  }, ["target_type", "target_id", "reason", ...(operation === "visibility" ? ["visibility"] : [])], { target_type: "project", target_id: id, reason: "本人确认的申请", ...(operation === "visibility" ? { visibility: "private" } : {}) });
89
97
  }
98
+ Object.assign(help, scheduleCommandHelp);
99
+ Object.assign(help, calendarCommandHelp);
90
100
  export function commandHelp(positionals) {
91
101
  const [first, second] = positionals;
92
102
  const pair = `${first} ${second}`;
93
103
  if (Object.hasOwn(help, pair))
94
104
  return help[pair];
95
- if (["auth", "request", "progress", "source", "cognition", "stage"].includes(first))
105
+ if (["skills", "auth", "request", "progress", "source", "cognition", "stage", "schedule", "daily-summary"].includes(first))
96
106
  return undefined;
97
107
  if (first === "knowledge" && second !== undefined)
98
108
  return undefined;
package/dist/commands.js CHANGED
@@ -10,9 +10,12 @@ import { expectDailyOK, validateDailyDates } from "./query-daily.js";
10
10
  import { createText, createHTTPURL, validateCreateSources, isCreatePath, expectCreateOK, expectKnowledgeTextOK } from "./create-bundle.js";
11
11
  import { withKnowledgeGrowth } from "./knowledge-growth.js";
12
12
  import { readHabit } from "./habit-read.js";
13
+ import { readHandoff } from "./handoff-read.js";
13
14
  import { citationOrigin, webCitations } from "./web-citations.js";
14
15
  import { KNOWLEDGE_GROWTH_STEPS, knowledgeGrowthMetadata } from "./generated/knowledge-growth.js";
15
16
  import { inputDetails, invalidInput, validateInput, validateCommandArguments } from "./input-validation.js";
17
+ import { executeSchedule } from "./schedule.js";
18
+ import { executeCalendar } from "./calendar.js";
16
19
  const root = "/api/v1/agent";
17
20
  // Go limits display text by Unicode code points, not UTF-16 code units.
18
21
  const runeLength = (value) => [...value].length;
@@ -225,6 +228,10 @@ async function dispatch(p, flags, ctx) {
225
228
  allowedFlags(flags, []);
226
229
  return readHabit(ctx, false);
227
230
  }
231
+ if (p[0] === "context" && p.length === 1 && flags.handoff === true) {
232
+ allowedFlags(flags, ["handoff", "project"]);
233
+ return readHandoff(ctx, flags.project);
234
+ }
228
235
  if (p[0] === "context" && p.length === 1 && flags.start === true) {
229
236
  allowedFlags(flags, ["start"]);
230
237
  return readHabit(ctx, true);
@@ -323,6 +330,10 @@ async function dispatch(p, flags, ctx) {
323
330
  return invalid();
324
331
  }
325
332
  export async function executeCommand(positionals, flags, ctx) {
333
+ if (positionals[0] === "schedule")
334
+ return executeSchedule(positionals, flags, ctx);
335
+ if (["calendar", "timeline", "daily-summary"].includes(positionals[0]))
336
+ return executeCalendar(positionals, flags, ctx);
326
337
  try {
327
338
  validateCommandArguments(positionals, flags);
328
339
  const result = await dispatch(positionals, flags, ctx);
package/dist/doctor.js CHANGED
@@ -12,11 +12,14 @@ const localMessages = {
12
12
  platform_detection_failed: ["无法完成平台程序检查。", "请检查本地程序目录后重新运行只读检查,不要启动客户端。"],
13
13
  command_missing: ["一间草堂命令行尚未完整安装。", "请核对本地安装结果,再由本人决定是否运行 setup。"],
14
14
  command_modified: ["已有命令行与当前候选不一致。", "请保留现有文件,由本人核对安装来源和差异,不要自动覆盖或删除。"],
15
+ command_outdated: ["已核对为旧版受管命令行,需要更新。", "沿用原来的名字运行 npx @yijiancaotang/cli@latest setup --agent <你的名字> 更新,不需要重新批准连接。"],
15
16
  command_not_on_path: ["当前找不到可直接使用的一间草堂命令。", "请先核对安装是否完整;有已核验的完整路径时使用它,不修改 shell 或 PATH 配置。"],
16
17
  command_version_unknown: ["无法确认本地命令行版本。", "请核对本地安装版本,不要自动升级或执行未知程序。"],
18
+ skill_target_ambiguous: ["这个名字记录了多个技能目录,未自动选择。", "请保留本次明确的--skills-dir或--platform参数,不批量更新不同工作区。"],
17
19
  no_platform_detected: ["尚未识别到支持的 AI 客户端程序。", "请由本人确认 Claude Code、Codex、OpenClaw,或 macOS 上 Kimi Code、Kimi 工作模式、WorkBuddy 的程序安装位置后重新检查。"],
18
20
  skills_missing: ["已识别平台的技能包不完整。", "请按当前安装包的技能清单核对所选平台,再由本人决定是否运行 setup。"],
19
21
  skills_modified: ["已识别平台的技能内容与本包不一致。", "请保留现有技能,由本人核对差异;不要自动覆盖或删除。"],
22
+ skills_outdated: ["已核对为这个名字的旧版受管技能,需要更新。", "沿用原来的名字运行 npx @yijiancaotang/cli@latest setup --agent <你的名字> 更新,不需要重新批准连接。"],
20
23
  skills_unsafe: ["已识别平台的技能目录未通过安全检查。", "请由本人检查技能目录的属主、权限和符号链接,不要自动修复或提权。"],
21
24
  };
22
25
  const hasLocalCode = (code) => Object.hasOwn(localMessages, code);
@@ -149,20 +152,23 @@ export async function doctor(ctx) {
149
152
  }
150
153
  }
151
154
  }
152
- const ids = PLATFORM_IDS;
155
+ const ids = [...PLATFORM_IDS, ...(Array.isArray(local?.platforms) && local.platforms.some(platform => platform?.id === "generic") ? ["generic"] : [])];
153
156
  if (local && (!Array.isArray(local.platforms) || local.platforms.some((platform) => !platform || !ids.includes(platform.id)) || new Set(local.platforms.map((platform) => platform?.id)).size !== local.platforms.length))
154
157
  addLocal("local_inspection_failed");
158
+ if (local?.skill_platforms !== undefined && (!Array.isArray(local.skill_platforms) || local.skill_platforms.some(id => !ids.includes(id)) || new Set(local.skill_platforms).size !== local.skill_platforms.length))
159
+ addLocal("local_inspection_failed");
155
160
  const platforms = ids.map((id) => {
156
161
  const platform = Array.isArray(local?.platforms) ? local.platforms.find((candidate) => candidate?.id === id) : undefined;
157
162
  const detected = platform?.detected === true;
158
163
  const skills = Array.isArray(platform?.skills) ? platform.skills : [];
159
164
  let status = "not_checked";
160
165
  if (detected) {
161
- status = skills.some((skill) => !skill || !skillNames.includes(skill.name) || skill.status === "unsafe" || !["installed", "missing", "modified"].includes(skill.status)) ? "unsafe"
166
+ status = skills.some((skill) => !skill || !skillNames.includes(skill.name) || skill.status === "unsafe" || !["installed", "outdated", "missing", "modified"].includes(skill.status)) ? "unsafe"
162
167
  : skills.some((skill) => skill.status === "modified") ? "modified"
163
- : !completeSkillInventory(skills.map(skill => skill.name)) || skills.some((skill) => skill.status !== "installed") ? "missing" : "installed";
168
+ : !completeSkillInventory(skills.map(skill => skill.name)) || skills.some((skill) => skill.status === "missing") ? "missing"
169
+ : skills.some((skill) => skill.status === "outdated") ? "outdated" : "installed";
164
170
  if (status !== "installed")
165
- addLocal(status === "unsafe" ? "skills_unsafe" : status === "modified" ? "skills_modified" : "skills_missing");
171
+ addLocal(status === "unsafe" ? "skills_unsafe" : status === "modified" ? "skills_modified" : status === "outdated" ? "skills_outdated" : "skills_missing");
166
172
  }
167
173
  // Report aggregate skill state, not unchecked names or filesystem paths from the inspector.
168
174
  const installed = detected ? new Set(skills.filter((skill) => skill?.status === "installed" && skillNames.includes(skill.name)).map((skill) => skill.name)).size : 0;
@@ -210,5 +216,7 @@ export async function doctor(ctx) {
210
216
  authExit = failed.exitCode;
211
217
  }
212
218
  }
213
- return { result: { status: 200, body: { readiness_scope: "local_cli_and_skills", ...(ctx.agent ? { agent_name: ctx.agent.name, own_connection: auth.status === "valid", skill_platform: ctx.agent.platform ?? "generic" } : {}), end_to_end_verified: false, command, platforms, auth, network, issues, next_actions: [...new Set(issues.map((value) => value.next_action))], ready: issues.length === 0 && auth.status === "valid" } }, exitCode: authExit || localExit };
219
+ const skillPlatforms = platforms.filter(platform => platform.skills.status === "installed" || local?.skill_platforms?.includes(platform.id)).map(platform => platform.id);
220
+ const supplement = ctx.agent && skillPlatforms.length === 0 ? ["可以用 setup --platform <平台> 补装技能,不影响现有连接。", "仅检查了已知默认目录和本次显式目录参数;若以前使用自定义目录,请保留原来的目录参数再检查,不能据此判断其它位置是否装过。"] : [];
221
+ return { result: { status: 200, body: { readiness_scope: "local_cli_and_skills", ...(ctx.agent ? { agent_name: ctx.agent.name, own_connection: auth.status === "valid", skill_platform: skillPlatforms[0] ?? "generic", skill_platforms: skillPlatforms, skill_search_scope: "known_paths_only" } : {}), end_to_end_verified: false, command, platforms, auth, network, issues, next_actions: [...new Set([...issues.map((value) => value.next_action), ...supplement])], ready: issues.length === 0 && auth.status === "valid" } }, exitCode: authExit || localExit };
214
222
  }
@@ -0,0 +1,12 @@
1
+ const operations = {
2
+ "schema_version": 1,
3
+ "version": "1.0.1",
4
+ "format": "text/markdown",
5
+ "content": "# 一间草堂 Agent 公共短操作合同\n\n这是公共操作说明,合同版本独立于 CLI 软件版本;不含账号、连接身份、本人规矩或私人工作内容。获得本文不代表连接已批准、当前模型已理解或真实流程已验证。详细命令参数按当前入口的帮助取得,完整人用手册见同一服务的 `/resources/usage`。\n\n## 六类用途\n\n- 推进项目:查看有权项目、已确认进展和留下的下一步,围绕本人本次目标继续工作。\n- 记进展:先整理实际完成、未完成和下一步,确认内容及目标后私有保存。\n- 存知识:把本人选中的经验、方法或来源拟成知识卡,确认后默认私有保存。\n- 排待办:拟定具体事项和明确日期/时间;带日期的同一待办可在日历查看,不另建重复日程。\n- 找记录:按任务查询项目、进展或知识,需要时再读详情/下一页;说明实际范围和权限缺口。\n- 开工/收工:获准后读取有限工作事实;收工先拟稿,由本人选中要留下的进展、知识和待办,再确认保存。\n\nCLI默认JSON是完整的有界机器投影;`--human`是给人看的紧凑摘要,不包含全部规矩和记录。Agent正常读取保持默认JSON,不能拿人用摘要冒称已读完规矩或完整工作事实。\n\n## 公共帮助、接入命名与用户呈现\n\n- 仅看公共帮助/说明时不运行doctor、auth status、setup或login,不做身份预检或读取私有内容。诊断只在真实错误已经发生且任务确有必要时使用;已有明确任务直接按范围做,不先补一轮诊断。\n- 收到新接入请求,当前AI应自行选择能标识自己的名字补上`--agent`并在后续命令保持一致;本人明确指定时优先,真正身份歧义才问。不能默认把起名丢给用户,也不能借其他AI的已有连接。\n- 本人明确首次使用时才给六类简短用途、自然语言例句(如“看看这个项目上次做到哪了”)和手册入口,执行邀请仍按有效权限过滤;老用户或同对话已讲过,只报当前状态与下一步,不重播目录。普通用户摘要不贴schema、SHA、UUID、枚举或协议细节,除非其明确排错或问技术;Agent自身仍按机器JSON取得有界完整内容,不将其整包念给用户。\n- 本机version或release_status不代表npm或线上发布,缺少实际发布证据就说未核实,不把候选说成已发布。只有可信同源地址才能拼手册链接,否则说明网页“使用手册”入口,不猜URL或环境。\n\n这些是用途目录,不是对所有账号开放的执行邀请。只读或关闭写能力时不邀请保存;已有明确任务直接按范围推进,不反复泛问要不要做。\n\n## 能力与身份\n\nA终端加Skill、B终端无Skill共用完整CLI业务合同;B通过公共说明取得规则,不假称加载Skill。C只有MCP时复用实际可用工具和现有stdio/需Bearer的HTTP方式;HTTP地址存在不证明消费者已安全连接。D纯网页且无外部工具时,明确“当前AI尚未接通”,只给人用手册和网页操作出口。E共享库、多Agent或远程工作区可叠加A/B/C,不能借另一Agent的连接、同意或准备状态。按实际能力而非品牌分流;未知保持未知,不扫描私人目录或项目来试探能力。\n\n安装、可发现(只有名字/描述)、正文实际获得、行为验证分开报告;前一阶段不推出下一阶段,CLI载荷不自报模型ready。本会话已获得有效同源短合同可复用,详细章节按需读,不每次回灌整本手册。公共guide不自动登录、不请求私人简报。\n\n执行获准业务时沿用当前入口已有的正常元数据核身份、实际服务/账号及scope与账号开关的有效交集;已有明确任务不为补状态另跑身份预检,也不用私有内容试探权限。未知如实说明,不默认生产、读写或全开,不输出凭据。本人规矩是偏好,不能增加权限或取消确认。\n\n## 自然任务与读取范围\n\n“看看XXX项目/继续这个项目”是本次该项目的必要读取意图;已知ID先定向读取项目、相关进展和待办,未知ID只作必要定位,不先拉全局摘要,不泛问是否读取。“看看草堂/整体安排”才在有效同意和权限范围读工作摘要;账号或范围有歧义才核对。只有本人明确要求记录开始/开工时间才选start(CLI `context --start` / MCP `get_start_context`),普通查看、恢复或开始处理任务不记录开工。公共guide是Agent准备,不要求人说“开工了”或背命令/触发词。\n\n## 四种场景\n\n- 首次使用:本人明确首次时,给六类用途的简短指引和适用例子,再按需要问是否读取或整理;不以空号、新安装、新token推断用户新手。\n- 换Agent接手:新Agent先取得公共短合同,核自己的身份/权限和内容同意;老用户得到简短接手提示,不重播教程。没有可靠用户经验记录时保持unknown,不新建教程持久状态机。\n- 日常开工/恢复:复用有效说明,按已有同范围有效授权读取有限新鲜事实;按本人自然任务定向读取;仅明确要求记录开始时间才记录开工元数据。本人已指定任务时直接继续,不强制再选项目。\n- 重复接入/升级:只报告连接、安装或升级的实际结果,不自动再批准、读工作包、写开工或保存业务;名称/版本变化不制造人用首次状态。\n\n## 同意、权限与读取\n\n私人读取前,核当前接收AI、账号/服务、范围及适用有效同意。本人已明确让当前AI查看指定项目或草堂摘要且目标清楚时,直接按本次任务读取,不再泛问;只有接收方/账号/范围有实际歧义或尚无读取意图时才问一次。拒绝、沉默、含糊或换题不授权读取/写入,不换工具、账号或对话绕过。已有明确且可核的同范围授权沿用,不逐字段反复问;新Agent不能借旧AI的同意。\n\nread_only只能按有效范围读,不提出写入或写类申请。读写权限与各项开关分别核,写成功不保证能读正文。检查各层missing_sections;partial、缺权、失败、未记录、封顶与真空结果分开,null或失败不是0,也不能推断全账号为空。read_workbench控制日程/开工元数据,不作所有内容段的总开关;项目、知识及其草稿仍按各自资格过滤。法律待确认须本人网页处理,不代签、重签或绕过。\n\n## 纯接手与开工\n\n查看工作摘要可使用当前协议支持的CLI `context --handoff` / MCP `get_handoff_context`:只有当前客户端和服务端都支持新协议、本人已授权范围时才调用;纯接手不写业务、开工记录或持久已读游标,正常认证审计另列。协议不支持时明确版本缺口,保留已有显式业务查询;不静默用start替代,不自动升级或重新授权。\n\n既有CLI `context --start` / MCP `get_start_context`会尝试记录开工时间,只在本人明确要求记录开始/开工时间时使用;保留原字段和回执。元数据成功、失败、无权分别报告,不补写、不当业务成果,也不据此断言项目有新活动。公共说明/重复setup不触发开工。\n\n摘要以实际协议coverage、各段status/total/has_more及deferred说明覆盖范围;未提供、缺权或失败不说成空、已读或完整。项目概况、关联知识、连接记录、提醒和目录绑定只有当前协议实际支持且有权时才使用,不承诺旧服务已有。显式项目只覆盖本次,不自动永久绑定或新建项目;远程工作区无可信隔离依据时只显式选择。\n\n## 本机项目定位与通用安装\n\n本人问当前目录关联的项目时,仅在已经明确确认独立本机工作区范围后,可用CLI `project binding --local-workspace` 读取有效定位,再按本次任务定向查询;本轮明确指定项目优先,不自动改长期绑定。没有范围确认或可靠绑定就明确询问所需项目,不扫描目录、不先取全局摘要。MCP无本机目录时使用本次显式项目。\n\n永久关联先用 `project bind <project_id>` 看预览;展示实际账号/服务、目录和项目,只有本人明确同意后才按该预览的摘要执行 `--confirm DIGEST --local-workspace`。包含子目录需单独明确并保留 `--include-subdirs`。`project unbind --local-workspace` 同样先预览再确认,只解除本机关联,不删除草堂项目。参数或摘要不能自证本人同意;移动目录、切账号/实际服务、撤权、归档或不可靠共享/远程环境不沿用旧定位。\n\n名单外Skill宿主可在明确的安全父目录使用 `setup --agent <自己的名字> --skills-dir <绝对父目录>`;与 `--platform` 互斥。HOME外共享或远程导入先用离线 `skills export --output <新ZIP路径>`,按宿主实际支持的导入入口及本人授权处理。导出不含个人身份/凭据,不覆盖既有文件,也不代表宿主已加载;没有Skill仍可通过本文和当前命令帮助使用完整CLI。\n\n## 拟稿、对象关系与回执\n\n写入前列出拟稿、目标和私有范围,等本人确认具体选中项;要求整理/记录先开始拟稿,不等于确认保存。确认后只写选中项;相同内容/目标/范围的有效确认不重复问。公开、删除、归档等沿既有网页确认及开关,AI不代确认,不自动写本人规矩。\n\n项目、知识、进展和待办保原关系与同一ID,多视图不复制;关联不自动升核心、发芽或证明已应用。引用实际可读标题和当前环境的真实来源,不猜ID、链接或下一步;只有全局一条下一步时不套到每个项目。\n\n成功、待审、被拒、unknown及202的明确状态分别报告;只在回执确认时说已保存。精简回执或audit_pending不自行判业务失败。结果未知保留原请求、幂等键、连接及回执,只按权限核原笔,不自动重写、换键或因读不到正文重试;确认保存和原笔核对分开取证。\n\n规则提醒只表示平台可读记录,不等于现实停工;连接历史不证明另一AI或实时执行,归属不明保持中性。本文不启用推送、跨会话监听、任意网页OAuth、自动共享库写入或远程绑定连接器;不自动安装、修改个人配置或扩大权限。\n",
6
+ "sha256": "4e1fa9c1585b0d3d0779f8c2c1458ffba65605d027e07977aad1b4c4e902a2ec",
7
+ "manual_path": "/resources/usage"
8
+ };
9
+ // Static public data only: no identity lookup, private storage or runtime file access.
10
+ export function getAgentOperations() {
11
+ return { ...operations };
12
+ }
@@ -1,5 +1,5 @@
1
1
  import { asRecord, CliError, expectOK, networkFailure } from "./errors.js";
2
- import { verifiedHabit } from "./habit-wire.js";
2
+ import { verifiedHabit, habitWorkbenchDisabled } from "./habit-wire.js";
3
3
  export async function readHabit(ctx, start) {
4
4
  let result;
5
5
  try {
@@ -14,6 +14,8 @@ export async function readHabit(ctx, start) {
14
14
  throw new CliError("unauthorized", "草堂授权已失效,未读到本次内容。", "先继续当前工作,请本人核对设备授权;不要自动重新授权。", 3, { http_status: 401 });
15
15
  if (result.status >= 500)
16
16
  throw new CliError("server_error", "草堂服务暂时不可用,未读到本次内容。", "先继续当前工作,稍后核对服务;不要反复重试或重新授权。", 7, { http_status: result.status });
17
+ if (start && result.status === 403 && habitWorkbenchDisabled(result.body))
18
+ throw new CliError("capability_disabled", "“读取工作台”开关未开启,未读到本次开工内容。", "请本人到网页的 Agent 管理核对开关;本次不记开工,不换入口、重新授权或反复重试。", 4, { http_status: 403, required_capability: "read_workbench" });
17
19
  expectOK(result, "read");
18
20
  const body = verifiedHabit(result.body, start);
19
21
  if (result.status !== 200 || !body)
@@ -1,3 +1,4 @@
1
+ import { dailySummaryResult } from "./calendar-contract.js";
1
2
  import { validDate as validDay } from "./query-validation.js";
2
3
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
3
4
  const list = (value, n) => { if (!Array.isArray(value) || value.length > n)
@@ -25,8 +26,10 @@ function day(value) { const r = record(value); if (!validDay(r.date))
25
26
  throw Error(); return { kind: p.kind, id: id(p.id), summary: str(p.summary, 120), ...(p.project_id === undefined ? {} : { project_id: id(p.project_id) }) }; }); if (items.length > total)
26
27
  throw Error(); return { date: r.date, total, items }; }
27
28
  function step(value) { const r = record(value); return { id: id(r.id), project_id: id(r.project_id), summary: str(r.summary, 120), created_at: str(r.created_at, 64) }; }
28
- function gap(value) { const r = record(value); if (typeof r.reason !== "string" || !["capability_disabled", "read_failed", "community_disabled"].includes(r.reason))
29
- throw Error(); return { section: key(r.section), ...(r.required_capability === undefined ? {} : { required_capability: key(r.required_capability) }), reason: r.reason }; }
29
+ function gap(value) { const r = record(value), section = key(r.section); if (typeof r.reason !== "string" || !["capability_disabled", "read_failed", "community_disabled", "record_failed"].includes(r.reason))
30
+ throw Error(); if (r.reason === "record_failed" && (section !== "calendar_start" || r.required_capability !== "read_workbench"))
31
+ throw Error(); if (section === "calendar_start" && (r.required_capability !== "read_workbench" || !["capability_disabled", "record_failed"].includes(r.reason)))
32
+ throw Error(); return { section, ...(r.required_capability === undefined ? {} : { required_capability: key(r.required_capability) }), reason: r.reason }; }
30
33
  function official(value) { const r = record(value), total = count(r.unread_count), title = str(r.latest_title, 120); if ((total === 0 && title !== "") || (total > 0 && !title))
31
34
  throw Error(); return { unread_count: total, latest_title: title }; }
32
35
  // Project the bounded wire fields rather than forwarding unknown content or
@@ -43,14 +46,42 @@ export function verifiedHabit(value, start) {
43
46
  return;
44
47
  const rules = r.rules === null ? null : list(r.rules, 20).map(rule), p = r.pending === null ? null : pending(r.pending), days = r.recent_days === null ? null : list(r.recent_days, 3).map(day), next = r.next_step === null ? null : step(r.next_step), gaps = list(r.missing_sections, 32).map(gap);
45
48
  const owner = typeof r.owner_id === "string" && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}(?![\s\S])/i.test(r.owner_id) ? r.owner_id : undefined;
46
- const incomplete = rules === null || p === null || days === null || gaps.some(g => g.reason !== "community_disabled");
49
+ const schedules = r.schedule_items === undefined ? undefined : r.schedule_items === null ? null : verifiedScheduleStart(r.schedule_items);
50
+ const summary = r.last_daily_summary === undefined ? undefined : r.last_daily_summary === null ? null : dailySummaryResult(r.last_daily_summary);
51
+ const incomplete = rules === null || p === null || days === null || schedules === null || gaps.some(g => g.reason !== "community_disabled" && g.reason !== "record_failed");
52
+ const onlyStartRecordGap = gaps.length > 0 && gaps.every(g => g.section === "calendar_start" && g.reason === "record_failed");
47
53
  if ((incomplete || gaps.length > 0) && !r.partial)
48
54
  return;
49
- const contradicted = r.account_empty === true && (!!rules?.length || !!p?.total || !!days?.length || !!next);
55
+ const contradicted = r.account_empty === true && (!!rules?.length || !!p?.total || !!days?.length || !!next || !!schedules?.total_stored || !!summary);
50
56
  return { ...(owner ? { owner_id: owner } : {}), ...(r.official_notices === undefined ? {} : { official_notices: r.official_notices === null ? null : official(r.official_notices) }), rules, pending: p, recent_days: days, next_step: next,
51
- account_empty: !owner || r.partial || incomplete || contradicted ? null : r.account_empty, partial: r.partial, missing_sections: gaps };
57
+ ...(schedules === undefined ? {} : { schedule_items: schedules }), ...(summary === undefined ? {} : { last_daily_summary: summary }), account_empty: !owner || (r.partial && !onlyStartRecordGap) || incomplete || contradicted ? null : r.account_empty, partial: r.partial, missing_sections: gaps };
52
58
  }
53
59
  catch {
54
60
  return undefined;
55
61
  }
56
62
  }
63
+ import { verifiedScheduleStart } from "./schedule-start-wire.js";
64
+ // A closed content switch is diagnosed only from an exact, non-conflicting
65
+ // start receipt; bare 403 and unrelated capabilities remain unknown.
66
+ export function habitWorkbenchDisabled(value) {
67
+ const body = record(value);
68
+ if (!body)
69
+ return false;
70
+ const code = body.code ?? body.error;
71
+ return code === "capability_disabled" && (body.code === undefined || body.code === code) && (body.error === undefined || body.error === code)
72
+ && body.required_capability === "read_workbench" && (!Object.hasOwn(body, "missing_sections") || Array.isArray(body.missing_sections) && body.missing_sections.length === 0);
73
+ }
74
+ // Derived only from validated protocol metadata; upstream free text is ignored.
75
+ export function calendarStartNotice(value) {
76
+ const body = record(value), rows = body?.missing_sections;
77
+ if (!Array.isArray(rows))
78
+ return "";
79
+ const reasons = new Set(rows.slice(0, 32).filter(v => record(v)?.section === "calendar_start" && record(v)?.required_capability === "read_workbench").map(v => record(v)?.reason));
80
+ if (reasons.size !== 1)
81
+ return "";
82
+ if (reasons.has("record_failed"))
83
+ return "这次没记上开工时间,已读内容仍可使用;不自动重试。";
84
+ if (reasons.has("capability_disabled"))
85
+ return "读取工作台开关未开启,这次未记录开工时间;其余内容按原权限返回。";
86
+ return "";
87
+ }
@@ -0,0 +1,29 @@
1
+ import { CliError, expectOK, networkFailure } from "./errors.js";
2
+ import { verifiedWorkContext, validWorkContextProjectID } from "./work-context-wire.js";
3
+ export async function readHandoff(ctx, projectId) {
4
+ if (projectId !== undefined && !validWorkContextProjectID(projectId))
5
+ throw new CliError("invalid_arguments", "项目参数须为有效UUID。", "只用已核对的真实项目ID,不猜同名项目。", 2);
6
+ const selected = projectId?.toLowerCase();
7
+ const query = new URLSearchParams({ projection: "work_context_v1", ...(selected ? { project_id: selected } : {}) });
8
+ let result;
9
+ try {
10
+ result = await ctx.api.request("/api/v1/agent/m1/habit/handoff?" + query.toString(), { method: "GET", token: ctx.token });
11
+ }
12
+ catch (error) {
13
+ if (error instanceof CliError && error.code === "network_error")
14
+ throw networkFailure(error, true);
15
+ throw error;
16
+ }
17
+ if (result.status === 404 || result.status === 405) {
18
+ throw new CliError("handoff_unsupported", "当前服务不支持可核实的纯读接手协议,未读到本次内容。", "不要改用开工、自动升级、重新授权或重试;需要旧业务查询时请显式选择原有命令。", 7, { http_status: result.status });
19
+ }
20
+ if (result.status === 401)
21
+ throw new CliError("unauthorized", "草堂授权已失效,未读到本次内容。", "先继续当前工作,请本人核对设备授权;不要自动重新授权。", 3, { http_status: 401 });
22
+ if (result.status >= 500)
23
+ throw new CliError("server_error", "草堂服务暂时不可用,未读到本次内容。", "先继续当前工作,稍后核对服务;不要反复重试或重新授权。", 7, { http_status: result.status });
24
+ expectOK(result, "read");
25
+ const body = verifiedWorkContext(result.body, selected);
26
+ if (result.status !== 200 || !body)
27
+ throw new CliError("invalid_response", "纯读接手响应不完整,无法核实。", "先继续当前工作,不要把未读取的内容解释为没有资料;不改用开工或自动重试。", 7, { http_status: result.status });
28
+ return { ...result, body };
29
+ }