@yijiancaotang/cli 1.0.0 → 1.1.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.
@@ -0,0 +1,149 @@
1
+ import { createHash } from "node:crypto";
2
+ import { join, relative, resolve, sep, isAbsolute } from "node:path";
3
+ import { asRecord, CliError } from "./errors.js";
4
+ import { OwnedTree } from "./installation.js";
5
+ import { isPlatformId } from "./local-types.js";
6
+ import { bindSkillAgent, completeSkillInventory } from "./skill-assets.js";
7
+ const unsafe = () => new CliError("local_record_unsafe", "本机连接说明或技能安装记录未通过核对。", "保留现有连接和文件,先由本人核对本机记录;不要重新批准、覆盖或删除。", 7);
8
+ const digest = (value) => createHash("sha256").update(value).digest("hex");
9
+ const keys = (value, expected) => Object.keys(value).sort().join(",") === [...expected].sort().join(",");
10
+ const id = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value);
11
+ const hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
12
+ const label = (value) => typeof value === "string" && value.trim() === value && [...value].length >= 1 && [...value].length <= 80 && !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value) && !/[^\S ]/u.test(value);
13
+ const date = (value) => typeof value === "string" && /^\d{4}-\d\d-\d\dT/.test(value) && Number.isFinite(Date.parse(value));
14
+ // These records contain no credentials. Immutable, content-addressed receipts
15
+ // retain upgrade history; identical setup never rewrites or accumulates files.
16
+ // They document installation, and never authorize overwriting a skill.
17
+ export class LocalRecords {
18
+ agent;
19
+ environment;
20
+ origin;
21
+ tree;
22
+ connectionDirectory;
23
+ skillDirectory;
24
+ constructor(home, agent, environment, origin) {
25
+ this.agent = agent;
26
+ this.environment = environment;
27
+ this.origin = origin;
28
+ this.tree = new OwnedTree(home);
29
+ this.connectionDirectory = join(this.tree.home, ".caotang", "connection-info", environment, agent.fileKey);
30
+ this.skillDirectory = join(this.tree.home, ".caotang", "skill-receipts", agent.fileKey);
31
+ }
32
+ async read(path) {
33
+ try {
34
+ const bytes = await this.tree.read(path, 32768);
35
+ if (!bytes)
36
+ return null;
37
+ const value = asRecord(JSON.parse(bytes.toString("utf8")));
38
+ if (!value)
39
+ throw unsafe();
40
+ return value;
41
+ }
42
+ catch {
43
+ throw unsafe();
44
+ }
45
+ }
46
+ async connection(tokenId, secrets = []) {
47
+ if (!id(tokenId))
48
+ throw unsafe();
49
+ const record = await this.read(join(this.connectionDirectory, `${tokenId}.json`));
50
+ if (!record)
51
+ return null;
52
+ const connectionName = record.connection_name;
53
+ if (!keys(record, ["version", "environment", "origin", "agent_key", "token_id", "connection_name", "connected_at"])
54
+ || record.version !== 1 || record.environment !== this.environment || record.origin !== this.origin || record.agent_key !== this.agent.key || record.token_id !== tokenId
55
+ || !label(connectionName) || !date(record.connected_at) || secrets.some(secret => secret.length >= 8 && connectionName.includes(secret)))
56
+ throw unsafe();
57
+ return { connection_name: connectionName, connection_name_source: "local_authorization_record", connected_at: record.connected_at, connection_time_source: "device_authorization" };
58
+ }
59
+ async rememberConnection(tokenId, info, secrets = []) {
60
+ if (!id(tokenId) || info.connection_name_source !== "local_authorization_record" || !label(info.connection_name) || !date(info.connected_at)
61
+ || secrets.some(secret => secret.length >= 8 && info.connection_name.includes(secret)))
62
+ throw unsafe();
63
+ const record = { version: 1, environment: this.environment, origin: this.origin, agent_key: this.agent.key, token_id: tokenId, connection_name: info.connection_name, connected_at: info.connected_at };
64
+ try {
65
+ await this.tree.writeNew(join(this.connectionDirectory, `${tokenId}.json`), Buffer.from(JSON.stringify(record)), 0o600);
66
+ }
67
+ catch {
68
+ throw unsafe();
69
+ }
70
+ }
71
+ targetPath(path) {
72
+ if (typeof path !== "string" || !path || isAbsolute(path) || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(path))
73
+ throw unsafe();
74
+ const absolute = this.tree.within(join(this.tree.home, path));
75
+ if (absolute === this.tree.home || relative(this.tree.home, absolute) !== path || path.split(sep).includes(".."))
76
+ throw unsafe();
77
+ return absolute;
78
+ }
79
+ async targets() {
80
+ let names;
81
+ try {
82
+ names = await this.tree.list(this.skillDirectory);
83
+ }
84
+ catch {
85
+ throw unsafe();
86
+ }
87
+ if (!names)
88
+ return [];
89
+ if (names.length > 256)
90
+ throw unsafe();
91
+ const targets = new Map();
92
+ for (const name of names) {
93
+ if (!/^[a-z-]+\.[a-f0-9]{64}\.json$/.test(name))
94
+ throw unsafe();
95
+ const value = await this.read(join(this.skillDirectory, name));
96
+ if (!value || !keys(value, ["version", "agent_key", "platform", "skill_directory", "bundle_sha", "skills"]) || value.version !== 1 || value.agent_key !== this.agent.key
97
+ || typeof value.platform !== "string" || !isPlatformId(value.platform) || !hash(value.bundle_sha) || !Array.isArray(value.skills)
98
+ || !value.skills.every(item => { const entry = asRecord(item); return entry && keys(entry, ["name", "sha256"]) && typeof entry.name === "string" && hash(entry.sha256); })
99
+ || !completeSkillInventory(value.skills.map(item => item.name)) || name !== `${value.platform}.${digest(JSON.stringify(value))}.json`)
100
+ throw unsafe();
101
+ const skillDirectory = this.targetPath(value.skill_directory);
102
+ try {
103
+ await this.tree.list(skillDirectory);
104
+ }
105
+ catch {
106
+ throw unsafe();
107
+ }
108
+ // A retained receipt cannot make a removed directory appear installed.
109
+ // Keep the target so the ordinary inspector reports its missing skills.
110
+ const target = { id: value.platform, skillDirectory, detected: true, evidence: "skills" };
111
+ const previous = targets.get(target.id);
112
+ if (previous && previous.skillDirectory !== target.skillDirectory)
113
+ throw unsafe();
114
+ targets.set(target.id, target);
115
+ }
116
+ return [...targets.values()];
117
+ }
118
+ async rememberInstallation(targets, assets, bundleSha) {
119
+ if (!hash(bundleSha) || !completeSkillInventory(assets.map(asset => asset.name)))
120
+ throw unsafe();
121
+ for (const target of targets) {
122
+ const bound = assets.map(asset => bindSkillAgent(asset, this.agent.key));
123
+ if (!await verifiedSkillFiles(this.tree.home, target, bound))
124
+ throw unsafe();
125
+ const record = { version: 1, agent_key: this.agent.key, platform: target.id, skill_directory: relative(this.tree.home, resolve(target.skillDirectory)), bundle_sha: bundleSha, skills: bound.map(({ name, sha256 }) => ({ name, sha256 })) };
126
+ this.targetPath(record.skill_directory);
127
+ try {
128
+ await this.tree.writeNew(join(this.skillDirectory, `${target.id}.${digest(JSON.stringify(record))}.json`), Buffer.from(JSON.stringify(record)), 0o600);
129
+ }
130
+ catch {
131
+ throw unsafe();
132
+ }
133
+ }
134
+ }
135
+ }
136
+ export async function verifiedSkillFiles(home, target, assets) {
137
+ const tree = new OwnedTree(home);
138
+ if (!completeSkillInventory(assets.map(asset => asset.name)))
139
+ return false;
140
+ for (const asset of assets) {
141
+ const directory = join(target.skillDirectory, asset.name), entries = await tree.list(directory);
142
+ if (!entries || entries.length !== 1 || entries[0] !== "SKILL.md")
143
+ return false;
144
+ const bytes = await tree.read(join(directory, "SKILL.md"), 65536);
145
+ if (!bytes || digest(bytes) !== asset.sha256)
146
+ return false;
147
+ }
148
+ return true;
149
+ }
@@ -0,0 +1,35 @@
1
+ // Public CLI 1.0.0 skill templates from reviewed commit 74530387. These pins
2
+ // authorize only exact per-AI rendering after the complete old bundle verifies;
3
+ // installation receipts, directory names and same-name files are never proof.
4
+ export const LEGACY_MANAGED_SKILLS = [
5
+ {
6
+ "name": "caotang-context",
7
+ "description": "每个新对话第一件工作前,即使未提草堂也主动短读一次;闲聊和纯问答不读,接入同意门优先。",
8
+ "sha256": "e698b74c7fc0e81ae5155a842e5bcc026c0383cb528dac66ef4ee869703e89b1"
9
+ },
10
+ {
11
+ "name": "caotang-save-knowledge",
12
+ "description": "仅在要写入一间草堂私有知识、来源或长势时加载;确认内容再写,公开须本人网页确认。",
13
+ "sha256": "5c10105f96691697d7640e0c1e7a9ad5e8b79250dadc0c8d126e5538609391eb"
14
+ },
15
+ {
16
+ "name": "caotang-project-progress",
17
+ "description": "用户要记录一间草堂项目进展时,确认后保存私有进展,并按需局部更新认知摘要或阶段;项目公开须申请并由本人网页确认。",
18
+ "sha256": "bbece00f3f69fddb8ef57c12a4d84576238c962ae1526367cecdc102ac0cf409"
19
+ },
20
+ {
21
+ "name": "caotang-search",
22
+ "description": "用户询问一间草堂以前记录的项目、进展或知识时,先按词或时间检索,需要全文才读详情,并说明实际查询范围和权限缺口。",
23
+ "sha256": "d1feb8c82f841a0baa3fc4e70085d5eb90d0d7e35fdb08d3e3d0fd0d3bae967a"
24
+ },
25
+ {
26
+ "name": "caotang-workday",
27
+ "description": "仅在完成重要工作或收工时,整理一间草堂进展和下一步,先确认再保存;不承担开工。",
28
+ "sha256": "e6673787c66472fb58799b75da85894594da7c7da9a3b110d250a1166b1a358f"
29
+ },
30
+ {
31
+ "name": "caotang-first-connect",
32
+ "description": "一间草堂接入对话按入口首次安装和账号状态分流:有内容或未知先问一次再读,空账号只拟私有条目,同意才写;重复接入不问不读。",
33
+ "sha256": "47202fb64c5a9cd06ad06740be7f1ba1734400439bbb125c3138a0239b1bbd53"
34
+ }
35
+ ];
@@ -4,6 +4,7 @@ const knownSections = {
4
4
  daily_knowledge: { capability: "read_knowledge", label: "日汇总中的知识新增与长势" },
5
5
  search_projects: { capability: "read_projects", label: "搜索中的项目与进展" },
6
6
  search_knowledge: { capability: "read_knowledge", label: "搜索中的知识" },
7
+ search_schedule_items: { capability: "read_workbench", label: "搜索中的待办" },
7
8
  workbench: { capability: "read_workbench", label: "工作台" },
8
9
  briefing: { capability: "read_notifications", label: "简报" },
9
10
  knowledge: { capability: "read_knowledge", label: "知识内容" },
package/dist/output.js CHANGED
@@ -3,6 +3,7 @@ import { formatMissingSections } from "./missing-sections.js";
3
3
  import { projectedWriteCompleted } from "./write-permissions.js";
4
4
  import { formatSearchWarnings } from "./query-search.js";
5
5
  import { formatDailyNotice } from "./query-daily.js";
6
+ import { calendarStartNotice } from "./habit-wire.js";
6
7
  import { createDraftNotice } from "./create-bundle.js";
7
8
  const sensitiveKey = /^(token|access_token|token_value|token_hash|device_code|upload_nonce|upload_url|required_headers|authorization|cookie|set-cookie|x-opc-upload-token)$/i;
8
9
  export function redact(value, secrets, depth = 0) {
@@ -32,8 +33,9 @@ export function renderResult(result, human, secrets) {
32
33
  const warningNotice = human ? formatSearchWarnings(asRecord(data)?.data) : "";
33
34
  const dailyNotice = human ? formatDailyNotice(asRecord(data)?.data) : "";
34
35
  const draftNotice = human && [200, 201].includes(result.status) ? createDraftNotice(asRecord(data)?.data) : "";
36
+ const startNotice = human ? calendarStartNotice(asRecord(data)?.data) : "";
35
37
  const replayNotice = human && [200, 201].includes(result.status) && body?.idempotency_replayed === true ? "这是已有创建请求的回放结果,本次没有重新创建。\n" : "";
36
- return `${pendingNotice}${replayNotice}${writeNotice}${missing}${warningNotice}${dailyNotice}${draftNotice}${JSON.stringify(data, null, human ? 2 : undefined)}\n`;
38
+ return `${pendingNotice}${replayNotice}${writeNotice}${startNotice ? startNotice + "\n" : ""}${missing}${warningNotice}${dailyNotice}${draftNotice}${JSON.stringify(data, null, human ? 2 : undefined)}\n`;
37
39
  }
38
40
  // Only coherent transport/receipt evidence classifies the submitted write;
39
41
  // neither a drafted audit log nor a moderation-pending 202 means unknown.
package/dist/platforms.js CHANGED
@@ -2,6 +2,7 @@ import { constants } from "node:fs";
2
2
  import { access, lstat, readdir, realpath, stat } from "node:fs/promises";
3
3
  import { delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { asRecord, CliError } from "./errors.js";
5
+ import { PLATFORM_IDS, platformSupported } from "./local-types.js";
5
6
  const unsafe = () => new CliError("platform_path_unsafe", "平台目录未通过安全检查。", "请使用用户目录内的绝对路径,并检查目录和符号链接;不要提供目录中的配置内容。");
6
7
  const metadataError = (error) => {
7
8
  const code = asRecord(error)?.code;
@@ -87,6 +88,52 @@ async function skillDirectory(home, canonicalHome, root) {
87
88
  }
88
89
  return target;
89
90
  }
91
+ function configuredSkillRoot(home, env, id) {
92
+ switch (id) {
93
+ case "claude-code": return env.CLAUDE_CONFIG_DIR ?? join(home, ".claude");
94
+ case "codex": return join(home, ".agents");
95
+ case "openclaw": return env.OPENCLAW_STATE_DIR ?? (env.OPENCLAW_HOME === undefined ? join(home, ".openclaw") : (() => {
96
+ if (!absolute(env.OPENCLAW_HOME) || !within(home, resolve(env.OPENCLAW_HOME)))
97
+ throw unsafe();
98
+ return join(env.OPENCLAW_HOME, ".openclaw");
99
+ })());
100
+ case "kimi-code": return env.KIMI_CODE_HOME ?? join(home, ".kimi-code");
101
+ case "kimi-work": return join(home, "Library/Application Support/kimi-desktop/daimon-share/daimon");
102
+ case "workbuddy": return join(home, ".workbuddy");
103
+ }
104
+ }
105
+ // Bounded skill-location discovery only. No PATH, application bundle, client
106
+ // executable, configuration contents or arbitrary HOME traversal is involved.
107
+ export async function knownSkillTargets(options) {
108
+ if (!absolute(options.homeDir))
109
+ throw unsafe();
110
+ const home = resolve(options.homeDir);
111
+ let canonicalHome;
112
+ try {
113
+ canonicalHome = await realpath(home);
114
+ if (!(await stat(home)).isDirectory())
115
+ throw unsafe();
116
+ }
117
+ catch (error) {
118
+ if (error instanceof CliError)
119
+ throw error;
120
+ throw metadataError(error);
121
+ }
122
+ const targets = [];
123
+ for (const id of PLATFORM_IDS)
124
+ if (platformSupported(id, options.operatingSystem)) {
125
+ try {
126
+ targets.push({ id, detected: false, evidence: "none", skillDirectory: await skillDirectory(home, canonicalHome, configuredSkillRoot(home, options.env, id)) });
127
+ }
128
+ catch (error) {
129
+ // Only optional, bounded discovery isolates a candidate. Explicit or
130
+ // recorded targets are checked by their original strict path instead.
131
+ if (!options.skipInvalidCandidates)
132
+ throw error;
133
+ }
134
+ }
135
+ return targets;
136
+ }
90
137
  async function claudeDesktop(home) {
91
138
  const root = join(home, "Library", "Application Support", "Claude", "claude-code");
92
139
  for (const version of await directories(root)) {
@@ -122,15 +169,7 @@ export async function detectPlatforms(options) {
122
169
  throw unsafe();
123
170
  const selected = (id) => options.platformIds === undefined || options.platformIds.includes(id);
124
171
  const targets = [];
125
- const candidates = [
126
- { id: "claude-code", root: () => env.CLAUDE_CONFIG_DIR ?? join(home, ".claude") },
127
- { id: "codex", root: () => join(home, ".agents") },
128
- { id: "openclaw", root: () => env.OPENCLAW_STATE_DIR ?? (env.OPENCLAW_HOME === undefined ? join(home, ".openclaw") : (() => {
129
- if (!absolute(env.OPENCLAW_HOME) || !within(home, resolve(env.OPENCLAW_HOME)))
130
- throw unsafe();
131
- return join(env.OPENCLAW_HOME, ".openclaw");
132
- })()) },
133
- ];
172
+ const candidates = ["claude-code", "codex", "openclaw"].map(id => ({ id, root: () => configuredSkillRoot(home, env, id) }));
134
173
  for (const candidate of candidates) {
135
174
  if (selected(candidate.id))
136
175
  targets.push({ id: candidate.id, detected: false, evidence: "none", skillDirectory: await skillDirectory(home, canonicalHome, candidate.root()) });
@@ -167,9 +206,9 @@ export async function detectPlatforms(options) {
167
206
  // Do not inspect an unselected new platform's personal paths. Without a
168
207
  // selector, only a verified program makes its skill root relevant.
169
208
  for (const [id, app, binary, rootPath, command] of [
170
- ["kimi-code", "Kimi Code", "Kimi Code", env.KIMI_CODE_HOME ?? join(home, ".kimi-code"), "kimi"],
171
- ["kimi-work", "Kimi", "Kimi", join(home, "Library/Application Support/kimi-desktop/daimon-share/daimon"), null],
172
- ["workbuddy", "WorkBuddy", "Electron", join(home, ".workbuddy"), null],
209
+ ["kimi-code", "Kimi Code", "Kimi Code", configuredSkillRoot(home, env, "kimi-code"), "kimi"],
210
+ ["kimi-work", "Kimi", "Kimi", configuredSkillRoot(home, env, "kimi-work"), null],
211
+ ["workbuddy", "WorkBuddy", "Electron", configuredSkillRoot(home, env, "workbuddy"), null],
173
212
  ]) {
174
213
  if (!selected(id))
175
214
  continue;
@@ -30,10 +30,11 @@ export function expectSearchOK(result) {
30
30
  }
31
31
  const body = asRecord(result.body), code = body?.code ?? body?.error;
32
32
  if (result.status === 403 && code === "capability_disabled" && (body?.code === undefined || body.code === code) && (body?.error === undefined || body.error === code)) {
33
- const expected = [{ section: "search_projects", required_capability: "read_projects", reason: "capability_disabled" }, { section: "search_knowledge", required_capability: "read_knowledge", reason: "capability_disabled" }];
34
33
  const rows = Array.isArray(body?.missing_sections) ? body.missing_sections : [];
34
+ const schedule = rows.some(row => { const item = asRecord(row); return item?.section === "search_schedule_items" || item?.required_capability === "read_workbench"; });
35
+ const expected = [{ section: "search_projects", required_capability: "read_projects", reason: "capability_disabled" }, { section: "search_knowledge", required_capability: "read_knowledge", reason: "capability_disabled" }, ...(schedule ? [{ section: "search_schedule_items", required_capability: "read_workbench", reason: "capability_disabled" }] : [])];
35
36
  if (!Object.hasOwn(body, "required_capability") && expected.every(want => rows.some(row => { const item = asRecord(row); return item?.section === want.section && item.required_capability === want.required_capability && item.reason === want.reason; }))) {
36
- throw new CliError("capability_disabled", "“读取项目”和“读取知识”开关都未开启。", "请本人到网页的 Agent 管理核对并开启所需开关;不要重新授权、换入口或重试来绕过限制。", 4, { http_status: 403, missing_sections: expected });
37
+ throw new CliError("capability_disabled", schedule ? "“读取项目”、“读取知识”和“读取工作台”开关都未开启。" : "“读取项目”和“读取知识”开关都未开启。", "请本人到网页的 Agent 管理核对并开启所需开关;不要重新授权、换入口或重试来绕过限制。", 4, { http_status: 403, missing_sections: expected });
37
38
  }
38
39
  }
39
40
  const checked = expectQueryOK(result);