@yijiancaotang/cli 0.0.0-stage → 1.0.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 (48) hide show
  1. package/README.md +279 -2
  2. package/bin/caotang.cjs +15 -0
  3. package/dist/agent-identity.js +14 -0
  4. package/dist/auth.js +337 -0
  5. package/dist/capability-result.js +80 -0
  6. package/dist/cli.js +248 -0
  7. package/dist/command-help.js +100 -0
  8. package/dist/commands.js +350 -0
  9. package/dist/config.js +14 -0
  10. package/dist/connection-copy.js +40 -0
  11. package/dist/context.js +30 -0
  12. package/dist/contracts.js +1 -0
  13. package/dist/create-bundle.js +103 -0
  14. package/dist/create-file-failure.js +18 -0
  15. package/dist/credential-store.js +594 -0
  16. package/dist/doctor.js +214 -0
  17. package/dist/errors.js +214 -0
  18. package/dist/generated/knowledge-growth.js +37 -0
  19. package/dist/habit-day.js +58 -0
  20. package/dist/habit-read.js +30 -0
  21. package/dist/habit-wire.js +56 -0
  22. package/dist/http.js +201 -0
  23. package/dist/input-validation.js +76 -0
  24. package/dist/installation.js +491 -0
  25. package/dist/knowledge-growth.js +85 -0
  26. package/dist/local-commands.js +136 -0
  27. package/dist/local-lock.js +245 -0
  28. package/dist/local-types.js +3 -0
  29. package/dist/missing-sections.js +71 -0
  30. package/dist/onboarding.js +48 -0
  31. package/dist/output.js +52 -0
  32. package/dist/pending-requests.js +45 -0
  33. package/dist/platforms.js +200 -0
  34. package/dist/query-daily.js +70 -0
  35. package/dist/query-search.js +42 -0
  36. package/dist/query-validation.js +45 -0
  37. package/dist/skill-assets.js +136 -0
  38. package/dist/upload.js +130 -0
  39. package/dist/web-citations.js +68 -0
  40. package/dist/write-permissions.js +41 -0
  41. package/package.json +27 -4
  42. package/skills/caotang-context/SKILL.md +47 -0
  43. package/skills/caotang-first-connect/SKILL.md +62 -0
  44. package/skills/caotang-project-progress/SKILL.md +87 -0
  45. package/skills/caotang-save-knowledge/SKILL.md +40 -0
  46. package/skills/caotang-search/SKILL.md +68 -0
  47. package/skills/caotang-workday/SKILL.md +34 -0
  48. package/skills/manifest.json +11 -0
package/dist/doctor.js ADDED
@@ -0,0 +1,214 @@
1
+ import { authStatus, credentialTarget } from "./auth.js";
2
+ import { isAbsolute, normalize } from "node:path";
3
+ import { asRecord, CliError, READ_NETWORK_NEXT_ACTION, networkFailure } from "./errors.js";
4
+ import { PLATFORM_IDS } from "./local-types.js";
5
+ const recovery = "请先在网页核对设备,并吊销未保存的设备,再决定是否重新授权;不要自动重试领取令牌。";
6
+ import { completeSkillInventory, skillNames } from "./skill-assets.js";
7
+ const localMessages = {
8
+ local_inspection_failed: ["无法完成本地安装检查。", "请检查本地安装目录与文件安全后重新运行只读检查,不要自动覆盖或修复。"],
9
+ local_permission_denied: ["没有权限完成本地安装检查。", "请由本人检查本地安装目录的访问权限,不要自动提权或修改权限。"],
10
+ platform_permission_denied: ["没有权限检查平台程序目录。", "请由本人检查程序目录的访问权限,不要自动提权或修改权限。"],
11
+ platform_path_unsafe: ["平台目录未通过安全检查。", "请使用用户目录内的绝对路径,并由本人检查符号链接和权限。"],
12
+ platform_detection_failed: ["无法完成平台程序检查。", "请检查本地程序目录后重新运行只读检查,不要启动客户端。"],
13
+ command_missing: ["一间草堂命令行尚未完整安装。", "请核对本地安装结果,再由本人决定是否运行 setup。"],
14
+ command_modified: ["已有命令行与当前候选不一致。", "请保留现有文件,由本人核对安装来源和差异,不要自动覆盖或删除。"],
15
+ command_not_on_path: ["当前找不到可直接使用的一间草堂命令。", "请先核对安装是否完整;有已核验的完整路径时使用它,不修改 shell 或 PATH 配置。"],
16
+ command_version_unknown: ["无法确认本地命令行版本。", "请核对本地安装版本,不要自动升级或执行未知程序。"],
17
+ no_platform_detected: ["尚未识别到支持的 AI 客户端程序。", "请由本人确认 Claude Code、Codex、OpenClaw,或 macOS 上 Kimi Code、Kimi 工作模式、WorkBuddy 的程序安装位置后重新检查。"],
18
+ skills_missing: ["已识别平台的技能包不完整。", "请按当前安装包的技能清单核对所选平台,再由本人决定是否运行 setup。"],
19
+ skills_modified: ["已识别平台的技能内容与本包不一致。", "请保留现有技能,由本人核对差异;不要自动覆盖或删除。"],
20
+ skills_unsafe: ["已识别平台的技能目录未通过安全检查。", "请由本人检查技能目录的属主、权限和符号链接,不要自动修复或提权。"],
21
+ };
22
+ const hasLocalCode = (code) => Object.hasOwn(localMessages, code);
23
+ const localAliases = {
24
+ command_not_installed: "command_missing",
25
+ no_supported_platform: "no_platform_detected",
26
+ skill_missing: "skills_missing",
27
+ skill_modified: "skills_modified",
28
+ skill_unsafe: "skills_unsafe",
29
+ };
30
+ function canonicalLocalCode(value) {
31
+ if (typeof value !== "string")
32
+ return undefined;
33
+ if (hasLocalCode(value))
34
+ return value;
35
+ return Object.hasOwn(localAliases, value) ? localAliases[value] : undefined;
36
+ }
37
+ const issue = (code, message, next_action) => ({ code, message, next_action });
38
+ const localIssue = (code) => issue(code, ...localMessages[code]);
39
+ const authMessages = {
40
+ unauthorized: ["当前授权缺失或已失效。", "请由本人核对网页授权状态,需要接入时再运行 auth login;不要输出令牌。"],
41
+ invalid_token: ["环境中的授权令牌格式无效。", "请由本人检查或取消当前环境的令牌覆盖,不要输出令牌。"],
42
+ invalid_token_environment: ["通用令牌变量没有绑定目标环境。", "使用按环境区分的开发变量;不要把测试令牌传给生产环境。"],
43
+ credential_unsafe: ["本地凭证未通过安全检查。", "请由本人检查凭证目录、属主、权限和目标环境,不要输出凭证内容。"],
44
+ credential_symlink: ["本地凭证路径包含符号链接,已拒绝访问。", "请由本人改用不含符号链接的真实用户目录和凭证路径后重试;不要自动删除链接、迁移凭证或放宽权限,也不要输出凭证内容。"],
45
+ credential_busy: ["另一项凭证操作尚未完成。", "请先等待同环境命令结束,再由本人核查残留锁;不要自动删锁或重新授权。"],
46
+ credential_conflict: ["当前环境已有另一份凭证。", recovery],
47
+ credential_migration_incomplete: ["旧凭据尚未确认清理,迁移未完成。", "请由本人处理旧凭据限制;不要读取其内容、绕过保护或重新授权。"],
48
+ authorization_delivery_unknown: ["无法确认领取结果,令牌可能已经交付。", recovery],
49
+ credential_save_failed: ["设备令牌已交付,但未能安全保存。", recovery],
50
+ installation_permission_denied: ["没有权限完成本地凭据操作。", "请由本人核对目标目录的访问、清空或删除限制;不要提权、放宽权限或自动重新授权。"],
51
+ invalid_auth_response: ["无法确认授权响应。", recovery],
52
+ invalid_response: ["无法确认服务响应。", recovery],
53
+ authorization_consumed: ["令牌已交付过,不能补领。", recovery],
54
+ expired_token: ["确认码已过期,这次没有完成接入。", "需要接入时,请本人重新运行 setup 或 auth login,取得新确认码后批准。"],
55
+ access_denied: ["这次接入已取消,没有建立新连接。", "只有本人需要时才重新发起接入。"],
56
+ authorization_cancelled: ["授权操作已取消。", "请先在网页核对实际状态,再决定是否重新授权;不要自动重试。"],
57
+ cancelled: ["操作已取消,授权状态尚未确认。", "请先在网页核对实际状态,再决定是否重新授权;不要自动重试。"],
58
+ network_error: ["无法完成授权联网验证。", READ_NETWORK_NEXT_ACTION],
59
+ permission_denied: ["授权检查被权限规则拒绝。", "请在网页检查设备权限和账号能力,不要自动扩大授权范围。"],
60
+ rate_limited: ["授权检查受到频率限制。", "请稍后重新运行只读 doctor,不要自动重试签发授权。"],
61
+ server_error: ["授权服务暂时不可用。", "请稍后重新运行只读 doctor,不要据此重新签发设备授权。"],
62
+ authorization_check_failed: ["无法完成授权检查。", "请由本人检查本地凭证和接入环境,不要输出凭证内容或自动重新授权。"],
63
+ authorization_failed: ["上一次授权操作未能确认完成。", recovery],
64
+ };
65
+ function authFailure(error, fromSetup, responseStatus, attempted = false, targetDirectory) {
66
+ const failure = error instanceof CliError ? error : undefined;
67
+ const exitCode = failure && [2, 3, 4, 5, 6, 7, 130].includes(failure.exitCode) ? failure.exitCode : 2;
68
+ let code = fromSetup ? "authorization_failed" : "authorization_check_failed";
69
+ if (failure && Object.hasOwn(authMessages, failure.code))
70
+ code = failure.code;
71
+ else if (exitCode === 130)
72
+ code = "cancelled";
73
+ else if (exitCode === 4)
74
+ code = "permission_denied";
75
+ else if (exitCode === 5)
76
+ code = "rate_limited";
77
+ else if (exitCode === 7)
78
+ code = "server_error";
79
+ else if (exitCode === 6 && !fromSetup)
80
+ code = "network_error";
81
+ const configurationFailure = code === "invalid_token_environment" || code === "invalid_token";
82
+ // A returned response is transport evidence; exception details are not.
83
+ const http = responseStatus;
84
+ const reached = typeof http === "number" && Number.isInteger(http) && http >= 100 && http <= 599;
85
+ let status = "unknown", network = "not_checked";
86
+ if (!fromSetup) {
87
+ if (reached)
88
+ network = "reachable";
89
+ if (code === "network_error" && attempted && !reached)
90
+ network = "unreachable";
91
+ if (exitCode === 7 && reached && Number(http) >= 500)
92
+ network = "unavailable";
93
+ if (code === "unauthorized")
94
+ status = http === 401 ? "invalid" : "missing";
95
+ }
96
+ if (configurationFailure)
97
+ status = "invalid";
98
+ const [message, nextAction] = code === "network_error" && !fromSetup ? [networkFailure(error, true).message, READ_NETWORK_NEXT_ACTION] : authMessages[code];
99
+ // Credential symlinks fail during the pre-authorization read. A later save
100
+ // failure is wrapped as credential_save_failed and still needs recovery.
101
+ const needsDeliveryRecovery = fromSetup && !configurationFailure && !["authorization_cancelled", "cancelled", "credential_symlink", "credential_migration_incomplete", "expired_token", "access_denied"].includes(code);
102
+ const action = needsDeliveryRecovery ? recovery : nextAction;
103
+ const links = code === "credential_save_failed" && asRecord(failure?.details)?.reason === "multiple_links";
104
+ return { issue: issue(code, links ? "设备令牌已交付,但凭据文件的链接数不是 1,未能安全保存。" : message, code === "installation_permission_denied" && targetDirectory ? `${action}目标目录:${targetDirectory}。` : action), exitCode, auth: { status }, network: { status: network } };
105
+ }
106
+ export async function doctor(ctx) {
107
+ const issues = [];
108
+ const add = (value) => { if (!issues.some((existing) => existing.code === value.code))
109
+ issues.push(value); };
110
+ let localExit = 0;
111
+ const addLocal = (code) => {
112
+ add(localIssue(code));
113
+ localExit = code === "platform_permission_denied" || code === "local_permission_denied" ? 4 : localExit || 2;
114
+ };
115
+ let local;
116
+ try {
117
+ const inspected = await ctx.inspectLocal();
118
+ if (!asRecord(inspected))
119
+ addLocal("local_inspection_failed");
120
+ else
121
+ local = inspected;
122
+ }
123
+ catch (error) {
124
+ const permission = (error instanceof CliError && error.exitCode === 4) || ["EACCES", "EPERM"].includes(String(asRecord(error)?.code));
125
+ addLocal((error instanceof CliError ? canonicalLocalCode(error.code) : undefined) ?? (permission ? "local_permission_denied" : "local_inspection_failed"));
126
+ }
127
+ const executable = local?.command?.executable_path;
128
+ const safeExecutable = typeof executable === "string" && executable.length <= 4096 && isAbsolute(executable) && normalize(executable) === executable
129
+ && !/[\x00-\x1f\x7f]/.test(executable) && executable.endsWith("/.local/bin/caotang") && local?.command?.installed === true ? executable : undefined;
130
+ const command = { installed: local?.command?.installed === true, on_path: local?.command?.on_path === true, version: "", ...(safeExecutable ? { executable_path: safeExecutable } : {}) };
131
+ if (local) {
132
+ const version = local.command?.version;
133
+ if (typeof version === "string" && version.length <= 80 && /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?(?:\+[A-Za-z0-9.-]+)?$/.test(version))
134
+ command.version = version;
135
+ if (!command.installed)
136
+ addLocal("command_missing");
137
+ if (!command.on_path && !safeExecutable)
138
+ addLocal("command_not_on_path");
139
+ if (command.installed && !command.version)
140
+ addLocal("command_version_unknown");
141
+ if (!Array.isArray(local.issues))
142
+ addLocal("local_inspection_failed");
143
+ else
144
+ for (const value of local.issues) {
145
+ const code = canonicalLocalCode(asRecord(value)?.code) ?? "local_inspection_failed";
146
+ if (code !== "no_platform_detected" || !ctx.agent || ctx.agent.platform) {
147
+ if (code !== "command_not_on_path" || !safeExecutable)
148
+ addLocal(code);
149
+ }
150
+ }
151
+ }
152
+ const ids = PLATFORM_IDS;
153
+ 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
+ addLocal("local_inspection_failed");
155
+ const platforms = ids.map((id) => {
156
+ const platform = Array.isArray(local?.platforms) ? local.platforms.find((candidate) => candidate?.id === id) : undefined;
157
+ const detected = platform?.detected === true;
158
+ const skills = Array.isArray(platform?.skills) ? platform.skills : [];
159
+ let status = "not_checked";
160
+ if (detected) {
161
+ status = skills.some((skill) => !skill || !skillNames.includes(skill.name) || skill.status === "unsafe" || !["installed", "missing", "modified"].includes(skill.status)) ? "unsafe"
162
+ : skills.some((skill) => skill.status === "modified") ? "modified"
163
+ : !completeSkillInventory(skills.map(skill => skill.name)) || skills.some((skill) => skill.status !== "installed") ? "missing" : "installed";
164
+ if (status !== "installed")
165
+ addLocal(status === "unsafe" ? "skills_unsafe" : status === "modified" ? "skills_modified" : "skills_missing");
166
+ }
167
+ // Report aggregate skill state, not unchecked names or filesystem paths from the inspector.
168
+ const installed = detected ? new Set(skills.filter((skill) => skill?.status === "installed" && skillNames.includes(skill.name)).map((skill) => skill.name)).size : 0;
169
+ return { id, detected, skills: { status, installed, expected: skillNames.length } };
170
+ });
171
+ if (local && !platforms.some((platform) => platform.detected) && (!ctx.agent || ctx.agent.platform))
172
+ addLocal("no_platform_detected");
173
+ let auth = { status: "unknown" }, network = { status: "not_checked" }, authExit = 0;
174
+ if (ctx.authFailure !== undefined) {
175
+ const failed = authFailure(ctx.authFailure, true, undefined, false, credentialTarget(ctx.auth, ctx.authFailure).target_directory);
176
+ add(failed.issue);
177
+ auth = failed.auth;
178
+ network = failed.network;
179
+ authExit = failed.exitCode;
180
+ }
181
+ else {
182
+ let attempted = false, responseStatus;
183
+ try {
184
+ const validation = {
185
+ ...ctx.auth,
186
+ api: {
187
+ baseUrl: ctx.auth.api.baseUrl,
188
+ async request(path, options) {
189
+ attempted = true;
190
+ const response = await ctx.auth.api.request(path, options);
191
+ responseStatus = response.status;
192
+ return response;
193
+ },
194
+ upload: (url, headers, bytes) => ctx.auth.api.upload(url, headers, bytes),
195
+ },
196
+ };
197
+ const checked = asRecord((await authStatus(validation)).body);
198
+ if (checked?.status !== "authenticated" || !["file", "environment"].includes(String(checked.source)) || !["manual", "device"].includes(String(checked.kind)) || !["read_only", "read_write"].includes(String(checked.scope)))
199
+ throw new CliError("invalid_auth_response", "无法确认授权响应。", recovery, 7);
200
+ auth = { status: "valid", source: checked.source, kind: checked.kind, scope: checked.scope };
201
+ if (typeof checked.user_id === "string" && typeof checked.token_id === "string")
202
+ ctx.onIdentity?.({ userId: checked.user_id, tokenId: checked.token_id, kind: auth.kind, scope: auth.scope });
203
+ network = { status: "reachable" };
204
+ }
205
+ catch (error) {
206
+ const failed = authFailure(error, false, responseStatus, attempted, credentialTarget(ctx.auth, error).target_directory);
207
+ add(failed.issue);
208
+ auth = failed.auth;
209
+ network = failed.network;
210
+ authExit = failed.exitCode;
211
+ }
212
+ }
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 };
214
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,214 @@
1
+ import { writeMissingSections } from "./write-permissions.js";
2
+ import { KNOWLEDGE_GROWTH_STEPS, knowledgeGrowthMetadata } from "./generated/knowledge-growth.js";
3
+ export class CliError extends Error {
4
+ code;
5
+ nextAction;
6
+ exitCode;
7
+ details;
8
+ constructor(code, message, nextAction, exitCode = 2, details = {}) {
9
+ super(message);
10
+ this.code = code;
11
+ this.nextAction = nextAction;
12
+ this.exitCode = exitCode;
13
+ this.details = details;
14
+ this.name = "CliError";
15
+ }
16
+ toJSON() { return { code: this.code, message: this.message, next_action: this.nextAction, ...this.details }; }
17
+ }
18
+ export const asRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
19
+ export function legalConsentRequiredError(httpStatus = 403) {
20
+ const message = "协议已更新,请本人在网页确认后继续使用。";
21
+ return new CliError("legal_consent_required", message, "同意后原连接会恢复;不要重新授权、安装、读取凭据或换入口绕过确认。退出和注销仍可使用。", 4, { http_status: httpStatus, user_message: message, requires_user_action: true });
22
+ }
23
+ export const validToken = (value) => typeof value === "string" && value.length >= 16 && value.length <= 8192 && !/[\s\x00-\x1f\x7f]/.test(value);
24
+ export const READ_NETWORK_NEXT_ACTION = "请先检查当前执行环境的联网权限和网络;恢复后可重新运行只读命令,不要连续重试或重新授权。";
25
+ export function networkFailure(error, read) {
26
+ const known = asRecord(asRecord(error)?.details)?.network_cause;
27
+ const code = asRecord(error)?.code ?? asRecord(asRecord(error)?.cause)?.code;
28
+ const cause = known === "local_permission_denied" || code === "EPERM" || code === "EACCES" ? "local_permission_denied"
29
+ : known === "timeout" || code === "ETIMEDOUT" ? "timeout" : "unknown";
30
+ const message = read && cause === "local_permission_denied" ? "当前执行环境没有联网权限,未读到草堂内容。"
31
+ : read ? "网络请求未完成,未读到草堂内容。" : "无法确认一间草堂的请求结果。";
32
+ return new CliError("network_error", message, read ? READ_NETWORK_NEXT_ACTION : "先在网页核对结果,保留原幂等键;不要自动重试或重新上传。", 6, { network_cause: cause });
33
+ }
34
+ export function directPublicWriteError(local = false) {
35
+ return new CliError(local ? "invalid_arguments" : "direct_public_write_forbidden", "Agent 不能直接公开,请发起申请,由用户在网页确认", "直接写入的 visibility 只接受 private 或省略;不要自动改值重发。知识卡或项目需公开时,按本人明确意图使用 request visibility 发起申请,等本人网页确认;进展没有独立公开申请入口。不要重新授权或重试来绕过限制。", local ? 2 : 4, local ? {} : { http_status: 403 });
36
+ }
37
+ const codesByStatus = {
38
+ 400: ["invalid_request", "invalid_upload"],
39
+ 401: ["unauthorized", "upload_unauthorized"],
40
+ 403: ["forbidden", "token_scope_insufficient", "second_confirmation_required", "capability_disabled", "moderation_blocked", "access_denied", "authorization_unavailable"],
41
+ 404: ["not_found", "write_target_not_found", "upload_not_found"],
42
+ 409: ["idempotency_conflict", "storage_quota_exceeded", "upload_unverified", "upload_in_progress", "upload_conflict", "authorization_consumed", "device_limit_reached"],
43
+ 410: ["expired_token"],
44
+ 429: ["slow_down", "device_issue_rate_limited", "device_auth_rate_limited", "rate_limited"],
45
+ 500: ["write_failed", "write_publication_failed"],
46
+ 503: ["storage_unavailable", "upload_busy", "device_auth_unavailable"]
47
+ };
48
+ // Account switches actually used by CLI operations. Never interpolate a
49
+ // provider-supplied capability, message, ID or title into user guidance.
50
+ const switchMessages = {
51
+ read_knowledge: "“读取知识”开关未开启。", read_projects: "“读取项目”开关未开启。",
52
+ read_workbench: "“读取工作台”开关未开启。", read_drafts: "“读取草稿”开关未开启。", read_notifications: "“读取通知”开关未开启。",
53
+ write_knowledge: "“写入知识卡”开关未开启。", write_knowledge_stage: "“推进知识长势”开关未开启。",
54
+ write_knowledge_sources: "“管理知识来源”开关未开启。", write_project: "“写入项目”开关未开启。",
55
+ write_project_update: "“写入项目进展”开关未开启。", write_project_cognition: "“写入认知摘要”开关未开启。",
56
+ delete_or_archive: "“删除或归档申请”开关未开启;申请仍须本人在网页确认。",
57
+ };
58
+ export function unverifiedSwitchError() {
59
+ return new CliError("permission_denied", "当前操作权限不足,无法核实具体开关。", "请本人到网页的 Agent 管理核对账号与设备权限;不要重新授权或重试来绕过限制。", 4, { http_status: 403 });
60
+ }
61
+ export function singleSwitchMetadata(body) {
62
+ return !Object.hasOwn(body, "missing_sections") || Array.isArray(body.missing_sections) && body.missing_sections.length === 0;
63
+ }
64
+ // Only the stage PATCH may interpret these receipts. Do not reflect IDs or server text.
65
+ export function checkApplicationReceipt(result, requestedStage) {
66
+ const body = asRecord(result.body);
67
+ if (!body)
68
+ return;
69
+ const code = body.code ?? body.error;
70
+ if ((body.code !== undefined && body.code !== code) || (body.error !== undefined && body.error !== code)) {
71
+ if (result.status === 403)
72
+ throw new CliError("permission_denied", "当前操作权限不足。", "请核对设备与账号的权限;无法确定缺少哪个开关,不要自动重试。", 4, { http_status: 403 });
73
+ return;
74
+ }
75
+ const details = { http_status: result.status };
76
+ if (result.status === 400 && code === "knowledge_application_evidence_required")
77
+ throw new CliError(code, "未推进:需要一个已关联的项目和一句实际使用说明。", "和本人核对哪张卡、哪个项目、具体怎么用;没有项目先问是否新建或关联。不要自动重试。", 2, details);
78
+ if (result.status === 409 && code === "knowledge_application_project_unavailable")
79
+ throw new CliError(code, "未推进:这个项目不能作为本次应用的关联项目。", "请本人选择自己已关联的可用项目,不猜测原因或替换项目重试。", 2, details);
80
+ if (result.status === 403 && code === "capability_disabled" && !singleSwitchMetadata(body))
81
+ throw unverifiedSwitchError();
82
+ if (result.status === 403 && code === "capability_disabled" && body.required_capability === "write_project_update") {
83
+ // Agent advancement cannot jump levels: only an applied request can
84
+ // first cross this boundary. Never infer the card's current stage.
85
+ if (requestedStage !== "applied")
86
+ throw unverifiedSwitchError();
87
+ throw new CliError(code, "未推进:“写入项目进展”开关未开启。", "请本人到网页的 Agent 管理核对开关;不要重新授权、重试或移除应用说明来绕过权限。", 4, { ...details, required_capability: "write_project_update" });
88
+ }
89
+ }
90
+ export function apiError(result, requestKind = "write") {
91
+ const body = asRecord(result.body);
92
+ const raw = body?.code ?? body?.error;
93
+ if (result.status === 403 && body) {
94
+ if ((body.code !== undefined && body.code !== raw) || (body.error !== undefined && body.error !== raw))
95
+ return unverifiedSwitchError();
96
+ if (raw === "legal_consent_required")
97
+ return legalConsentRequiredError(result.status);
98
+ if (raw === "direct_public_write_forbidden")
99
+ return directPublicWriteError();
100
+ if (raw === "token_scope_insufficient")
101
+ return new CliError("token_scope_insufficient", "当前设备的权限范围不足。", "请本人在网页核对设备的只读或读写权限,不要重试来绕过限制。", 4, { http_status: 403 });
102
+ if (raw === "capability_disabled") {
103
+ const capability = body.required_capability;
104
+ if (typeof capability === "string" && Object.hasOwn(switchMessages, capability) && singleSwitchMetadata(body))
105
+ return new CliError("capability_disabled", switchMessages[capability], "请本人到网页的 Agent 管理核对并开启所需开关;不要重新授权或重试来绕过限制。", 4, { http_status: 403, required_capability: capability });
106
+ if (Object.hasOwn(body, "required_capability"))
107
+ return unverifiedSwitchError();
108
+ return new CliError("capability_disabled", "有账号能力开关未开启,但未能核实具体名称。", "请本人到网页的 Agent 管理核对;不要重新授权或重试来绕过限制。", 4, { http_status: 403 });
109
+ }
110
+ }
111
+ if (result.status === 409 && body && raw === "knowledge_stage_step_required"
112
+ && (body.code === undefined || body.code === raw) && (body.error === undefined || body.error === raw)) {
113
+ return knowledgeStageStepError(body);
114
+ }
115
+ if (result.status === 409 && raw === "duplicate_title") {
116
+ const missing = writeMissingSections(body?.missing_sections);
117
+ if (missing.length)
118
+ return new CliError("duplicate_title", "同名内容已存在,读取权限未开启,无法显示详情。", "请到网页核对内容或开启所需读取权限;不要重复创建或猜测已有内容的 ID。", 2, { http_status: 409, missing_sections: missing });
119
+ }
120
+ const code = typeof raw === "string" && codesByStatus[result.status]?.includes(raw) ? raw : result.status === 401 ? "unauthorized" : result.status === 403 ? "permission_denied" : result.status === 429 ? "rate_limited" : result.status >= 500 ? "server_error" : "request_rejected";
121
+ const exitCode = result.status === 401 ? 3 : result.status === 403 ? 4 : result.status === 429 ? 5 : result.status >= 500 ? 7 : 2;
122
+ const details = { http_status: result.status };
123
+ if (typeof result.retryAfterSeconds === "number" && Number.isFinite(result.retryAfterSeconds) && result.retryAfterSeconds >= 0)
124
+ details.retry_after_seconds = Math.min(86400, Math.ceil(result.retryAfterSeconds));
125
+ if (["write_failed", "write_target_not_found", "write_publication_failed"].includes(code) && body?.write_status === "failed") {
126
+ details.write_status = "failed";
127
+ if (typeof body.request_id === "string" && /^[A-Za-z0-9_-]{1,80}(?![\s\S])/.test(body.request_id))
128
+ details.request_id = body.request_id;
129
+ if (body.audit_pending === true && ["automatic", "manual_required"].includes(String(body.audit_recovery))) {
130
+ details.audit_pending = true;
131
+ details.audit_recovery = body.audit_recovery;
132
+ }
133
+ }
134
+ const authorizationActions = {
135
+ device_limit_reached: "设备数已达上限,请先在网页吊销不再使用的设备。",
136
+ device_issue_rate_limited: "设备签发过于频繁,请等待后由本人重新操作。",
137
+ authorization_consumed: "令牌已交付过,不能补领。请先在网页核对并吊销该设备,再重新授权。",
138
+ access_denied: "授权已被拒绝;只有本人需要接入时才重新发起。",
139
+ expired_token: "确认码已过期;需要接入时由本人重新发起。"
140
+ };
141
+ if (requestKind === "read" && result.status === 404) {
142
+ return new CliError(code, "未找到这条内容,或当前账号无法访问。", "请检查内容 ID,或先从项目、知识列表中查找可访问的内容。", exitCode, details);
143
+ }
144
+ if (requestKind === "read" && result.status === 400) {
145
+ return new CliError(code, "读取请求的参数无效。", "请核对命令参数、查询条件和内容 ID 后再查询。", exitCode, details);
146
+ }
147
+ return new CliError(code, "一间草堂请求未完成。", authorizationActions[code] ?? (result.status === 401 ? "请重新授权;不要输出或转发令牌。" : result.status === 403 ? "请在网页检查设备权限、账号能力或待确认事项。" : result.status === 429 ? "按返回的等待时间处理,不要自动重复写入。" : "先在网页核对实际结果,不要自动重试、换键或重新上传。"), exitCode, details);
148
+ }
149
+ function knowledgeStageStepError(body) {
150
+ let message = "知识长势只能逐级推进,不能跳级;本次未写入。";
151
+ let nextAction = "请在网页核对长势后按级推进;不要自动重试或自动补写中间级。";
152
+ const details = { http_status: 409 };
153
+ const missing = writeMissingSections(body.missing_sections);
154
+ if (missing.length && missing.every(row => row.section === "knowledge_result" || row.section === "draft_knowledge_result")) {
155
+ details.missing_sections = missing;
156
+ message += "读取权限未开启,无法显示当前级和下一可用级。";
157
+ nextAction = "请在一间草堂网页核对或开启所需读取权限;不要自动重试或自动补写中间级。";
158
+ }
159
+ else if (!Object.hasOwn(body, "missing_sections") || (Array.isArray(body.missing_sections) && body.missing_sections.length === 0)) {
160
+ // A partial, malformed or permission-projected response never licenses
161
+ // exposing one side of the pair. Rebuild only the five known adjacent enums.
162
+ const stages = KNOWLEDGE_GROWTH_STEPS.map(step => step.value);
163
+ const current = typeof body.current_status === "string" ? stages.indexOf(body.current_status) : -1;
164
+ if (current >= 0 && current < stages.length - 1 && body.next_status === stages[current + 1]) {
165
+ details.current_status = stages[current];
166
+ details.next_status = stages[current + 1];
167
+ details.current_growth = knowledgeGrowthMetadata(stages[current]);
168
+ details.next_growth = knowledgeGrowthMetadata(stages[current + 1]);
169
+ message += `当前第${current + 1}/5级,下一可用级为第${current + 2}/5级。`;
170
+ }
171
+ }
172
+ return new CliError("knowledge_stage_step_required", message, nextAction, 2, details);
173
+ }
174
+ export function expectOK(result, requestKind = "write") {
175
+ if (result.status < 200 || result.status >= 300)
176
+ throw apiError(result, requestKind);
177
+ return result;
178
+ }
179
+ /** Existing knowledge changes only; create has its own candidate/receipt rules. */
180
+ export function expectKnowledgeChangeNotBlocked(result) {
181
+ const body = asRecord(result.body), code = body?.code ?? body?.error;
182
+ if (result.status === 422 && code === "content_blocked"
183
+ && (body?.code === undefined || body.code === code) && (body?.error === undefined || body.error === code)) {
184
+ throw new CliError("content_blocked", "本次公开变更被审核拦截,不代表用户输入格式错误;已有内容保留,本次公开变更未生效。", "请先到网页核对审核与内容;不要自动改写或重试,也不要推断内容已转为私有。", 2, { http_status: 422 });
185
+ }
186
+ return result;
187
+ }
188
+ /** Only the project title/summary PATCH adopts these fixed backend codes. */
189
+ export function expectProjectTextOK(result) {
190
+ const body = asRecord(result.body);
191
+ const code = body?.code ?? body?.error;
192
+ if (result.status === 409 && (body?.code === "duplicate_title" || body?.error === "duplicate_title")) {
193
+ if (code === "duplicate_title" && (body?.code === undefined || body.code === code) && (body?.error === undefined || body.error === code)) {
194
+ const rows = writeMissingSections(body?.missing_sections);
195
+ const missing = rows.every(row => row.section === "project_result" || row.section === "draft_project_result") ? rows : [];
196
+ throw new CliError(code, missing.length ? "同名项目已存在,本次未保存。读取权限未开启,无法显示详情。" : "同名项目已存在,本次未保存。", "请先在网页核对同名项目,再由本人决定后续修改;不要自动重试。", 2, { http_status: 409, ...(missing.length ? { missing_sections: missing } : {}) });
197
+ }
198
+ // Conflicting aliases cannot activate the legacy missing-only duplicate
199
+ // explanation. Keep a generic conflict without adopting its metadata.
200
+ return expectOK({ ...result, body: undefined });
201
+ }
202
+ if ((body?.code === undefined || body.code === code) && (body?.error === undefined || body.error === code)) {
203
+ if (result.status === 400 && code === "invalid_project_text") {
204
+ throw new CliError(code, "项目标题或简介不符合要求,本次未保存。", "请检查去除首尾空白后的标题为2–80个Unicode字符、简介不超过280个Unicode字符,且非草稿项目简介必填;核对后再决定修改,不要自动重试。", 2, { http_status: 400 });
205
+ }
206
+ if (result.status === 409 && code === "project_changed") {
207
+ throw new CliError(code, "项目已发生变化,本次未写入。", "请先在网页核对项目当前内容和状态,再决定后续修改;不要自动重试。", 2, { http_status: 409 });
208
+ }
209
+ if (result.status === 503 && code === "moderation_unavailable") {
210
+ throw new CliError(code, "项目审核暂不可用,本次未保存。", "请由本人先在网页核对项目和审核状态,再决定后续操作;不要自动重试。", 7, { http_status: 503 });
211
+ }
212
+ }
213
+ return expectOK(result);
214
+ }
@@ -0,0 +1,37 @@
1
+ // Generated by scripts/generate-knowledge-growth.mjs from shared/knowledge-growth.json.
2
+ // Do not edit this file directly; regenerate all three consumers together.
3
+ export const KNOWLEDGE_GROWTH_STEPS = [
4
+ {
5
+ "value": "captured",
6
+ "label": "播种",
7
+ "description": "刚存下来,还没细看"
8
+ },
9
+ {
10
+ "value": "summarized",
11
+ "label": "生根",
12
+ "description": "看懂了,写下了自己的理解"
13
+ },
14
+ {
15
+ "value": "applied",
16
+ "label": "发芽",
17
+ "description": "在项目里真用上了"
18
+ },
19
+ {
20
+ "value": "reviewed",
21
+ "label": "开花",
22
+ "description": "回头看过,见到效果了"
23
+ },
24
+ {
25
+ "value": "templated",
26
+ "label": "结果",
27
+ "description": "固定成方法,以后反复用"
28
+ }
29
+ ];
30
+ export function knowledgeGrowthMetadata(value) {
31
+ const index = KNOWLEDGE_GROWTH_STEPS.findIndex((step) => step.value === value);
32
+ if (index < 0)
33
+ return undefined;
34
+ const step = KNOWLEDGE_GROWTH_STEPS[index];
35
+ return { label: step.label, description: step.description, level: index + 1, total: 5 };
36
+ }
37
+ export const KNOWLEDGE_GROWTH_HELP = KNOWLEDGE_GROWTH_STEPS.map((step, index) => `${step.value}(${step.label},${index + 1}/5:${step.description})`).join(";");
@@ -0,0 +1,58 @@
1
+ import { createHash } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import { OwnedTree } from "./installation.js";
4
+ import { validDate } from "./query-validation.js";
5
+ export const isResourceID = (value) => typeof value === "string" && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}(?![\s\S])/i.test(value);
6
+ const formatDay = new Intl.DateTimeFormat("sv-SE", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" });
7
+ // A single empty, date-named directory is the marker. There is no content or
8
+ // credential file to parse, truncate or overwrite. The owned-tree checks pin
9
+ // each filesystem operation to its verified parent; we only inspect this one
10
+ // environment/owner. A crash, broken marker or unowned entry is never repaired.
11
+ export async function startReportMode(home, environment, owner, now) {
12
+ try {
13
+ if (!isResourceID(owner) || !["production", "test"].includes(environment) || !Number.isFinite(now))
14
+ return "brief";
15
+ const day = formatDay.format(now);
16
+ if (!validDate(day))
17
+ return "brief";
18
+ const tree = new OwnedTree(home);
19
+ const account = createHash("sha256").update(owner.toLowerCase()).digest("hex");
20
+ const root = join(tree.home, ".caotang", "habit-days", environment, account), lockPath = join(root, ".lock");
21
+ const lock = await tree.createDirectory(lockPath);
22
+ if (!lock)
23
+ return "brief";
24
+ try {
25
+ const contents = await tree.list(root);
26
+ if (!contents || !contents.includes(".lock"))
27
+ return "brief";
28
+ const prior = contents.filter(entry => entry !== ".lock");
29
+ if (prior.length === 0) {
30
+ await tree.createDirectory(join(root, day));
31
+ return "brief";
32
+ }
33
+ if (prior.length !== 1 || !validDate(prior[0]) || prior[0] >= day)
34
+ return "brief";
35
+ const old = join(root, prior[0]);
36
+ const previous = await tree.directory(old, false, async (dirs) => dirs.at(-1).identity);
37
+ if (!previous || (await tree.list(old))?.length !== 0)
38
+ return "brief";
39
+ const dest = join(root, day), created = await tree.createDirectory(dest);
40
+ if (!created)
41
+ return "brief";
42
+ try {
43
+ await tree.removeDirectory(old, previous);
44
+ }
45
+ catch {
46
+ await tree.removeDirectory(dest, created);
47
+ return "brief";
48
+ }
49
+ return "detailed";
50
+ }
51
+ finally {
52
+ await tree.removeDirectory(lockPath, lock);
53
+ }
54
+ }
55
+ catch {
56
+ return "brief";
57
+ }
58
+ }
@@ -0,0 +1,30 @@
1
+ import { asRecord, CliError, expectOK, networkFailure } from "./errors.js";
2
+ import { verifiedHabit } from "./habit-wire.js";
3
+ export async function readHabit(ctx, start) {
4
+ let result;
5
+ try {
6
+ result = await ctx.api.request("/api/v1/agent/m1/" + (start ? "habit/start" : "rules"), { method: "GET", token: ctx.token });
7
+ }
8
+ catch (error) {
9
+ if (error instanceof CliError && error.code === "network_error")
10
+ throw networkFailure(error, true);
11
+ throw error;
12
+ }
13
+ if (result.status === 401)
14
+ throw new CliError("unauthorized", "草堂授权已失效,未读到本次内容。", "先继续当前工作,请本人核对设备授权;不要自动重新授权。", 3, { http_status: 401 });
15
+ if (result.status >= 500)
16
+ throw new CliError("server_error", "草堂服务暂时不可用,未读到本次内容。", "先继续当前工作,稍后核对服务;不要反复重试或重新授权。", 7, { http_status: result.status });
17
+ expectOK(result, "read");
18
+ const body = verifiedHabit(result.body, start);
19
+ if (result.status !== 200 || !body)
20
+ throw new CliError("invalid_response", "草堂短读响应不完整,无法核实。", "先继续当前工作,不要把未读取的内容解释为没有资料。", 7, { http_status: result.status });
21
+ if (!start)
22
+ return { ...result, body };
23
+ let mode = "brief";
24
+ try {
25
+ if (ctx.startMode && body.rules !== null && !body.missing_sections.some(row => asRecord(row)?.reason === "read_failed"))
26
+ mode = await ctx.startMode(body.owner_id);
27
+ }
28
+ catch { /* Optional local state: stay brief. */ }
29
+ return { ...result, body: { ...body, briefing_mode: mode } };
30
+ }
@@ -0,0 +1,56 @@
1
+ import { validDate as validDay } from "./query-validation.js";
2
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
3
+ const list = (value, n) => { if (!Array.isArray(value) || value.length > n)
4
+ throw Error(); return value; };
5
+ const str = (v, max) => { if (typeof v !== "string" || [...v].length > max || v.includes("\0"))
6
+ throw Error(); return v; };
7
+ const id = (v) => { const s = str(v, 128); if (!/^[a-z0-9_-]+(?![\s\S])/i.test(s))
8
+ throw Error(); return s; };
9
+ const key = (v) => { const s = str(v, 64); if (!/^[a-z][a-z0-9_]*(?![\s\S])/.test(s))
10
+ throw Error(); return s; };
11
+ const count = (v) => { if (typeof v !== "number" || !Number.isSafeInteger(v) || v < 0)
12
+ throw Error(); return v; };
13
+ function rule(value) {
14
+ const r = record(value), text = str(r.text, 200);
15
+ // The Go writer rejects controls before trimming Unicode White_Space.
16
+ // A literal BOM is content, while NEL / lone surrogates cannot be stored.
17
+ if (/[\p{Cc}\u2028\u2029\uD800-\uDFFF]/u.test(text) || !text || text.replace(/^\p{White_Space}+|\p{White_Space}+$/gu, "") !== text)
18
+ throw Error();
19
+ return { id: id(r.id), text, created_at: str(r.created_at, 64), updated_at: str(r.updated_at, 64) };
20
+ }
21
+ function pending(value) { const r = record(value), total = count(r.total), items = list(r.items, 3).map(value => { const p = record(value); return { id: id(p.id), action_type: key(p.action_type), summary: str(p.summary, 120) }; }); if (items.length > total)
22
+ throw Error(); return { total, items }; }
23
+ function day(value) { const r = record(value); if (!validDay(r.date))
24
+ throw Error(); const total = count(r.total), items = list(r.items, 3).map(value => { const p = record(value); if (p.kind !== "project_update" && p.kind !== "checkin")
25
+ 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
+ throw Error(); return { date: r.date, total, items }; }
27
+ 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 }; }
30
+ 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
+ throw Error(); return { unread_count: total, latest_title: title }; }
32
+ // Project the bounded wire fields rather than forwarding unknown content or
33
+ // treating a missing section as empty. Unknown account truth wins over empty.
34
+ export function verifiedHabit(value, start) {
35
+ try {
36
+ const r = record(value);
37
+ if (!start) {
38
+ if (r.limit !== 20 || r.max_chars !== 200)
39
+ return;
40
+ return { items: list(r.items, 20).map(rule), limit: 20, max_chars: 200 };
41
+ }
42
+ if (typeof r.partial !== "boolean" || !(typeof r.account_empty === "boolean" || r.account_empty === null))
43
+ return;
44
+ 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
+ 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");
47
+ if ((incomplete || gaps.length > 0) && !r.partial)
48
+ return;
49
+ const contradicted = r.account_empty === true && (!!rules?.length || !!p?.total || !!days?.length || !!next);
50
+ 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 };
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }