@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
@@ -1,17 +1,19 @@
1
1
  import { shellWord } from "./agent-identity.js";
2
2
  import { constants } from "node:fs";
3
3
  import { open } from "node:fs/promises";
4
- import { join } from "node:path";
4
+ import { isAbsolute, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { authLogin } from "./auth.js";
7
7
  import { doctor } from "./doctor.js";
8
8
  import { CliError, asRecord } from "./errors.js";
9
- import { inspectLocalInstallation, installLocal } from "./installation.js";
9
+ import { inspectLocalInstallation, inspectManagedOrigins, installLocal } from "./installation.js";
10
10
  import { isPlatformId, platformSupported } from "./local-types.js";
11
- import { detectPlatforms } from "./platforms.js";
11
+ import { detectPlatforms, explicitSkillsTarget, knownSkillTargets } from "./platforms.js";
12
12
  import { loadSkillAssets } from "./skill-assets.js";
13
+ import { LocalRecords } from "./local-records.js";
13
14
  import { defaultConnectionNames, inspectAccountState, onboardingMessage } from "./onboarding.js";
14
- import { setupFailureMessage } from "./connection-copy.js";
15
+ import { connectionCopy, setupFailureMessage } from "./connection-copy.js";
16
+ import { setupGuidance } from "./setup-guidance.js";
15
17
  const root = () => fileURLToPath(new URL("../", import.meta.url));
16
18
  const invalid = () => new CliError("invalid_arguments", "命令或参数无效。", "运行 --help;平台支持claude-code、codex、openclaw,以及仅macOS支持的kimi-code、kimi-work和workbuddy。");
17
19
  export function selectedPlatforms(value, operatingSystem = process.platform) {
@@ -26,6 +28,15 @@ export function selectedPlatforms(value, operatingSystem = process.platform) {
26
28
  throw new CliError("platform_unsupported", "所选新增平台尚不支持当前系统。", "kimi-code、kimi-work和workbuddy仅支持macOS本机安装;不要套用或猜测其它系统的技能目录。");
27
29
  return values;
28
30
  }
31
+ export function selectedSkillsDirectory(flags) {
32
+ const value = flags["skills-dir"];
33
+ if (value === undefined)
34
+ return undefined;
35
+ if (flags.platform !== undefined || typeof value !== "string" || !value.trim() || !isAbsolute(value)
36
+ || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value))
37
+ throw new CliError("invalid_arguments", "--skills-dir须为明确的绝对父目录,且不能与--platform同时使用。", "指定七项技能子目录的共同父目录;不提供配置内容,不以此绑定项目。", 2);
38
+ return value;
39
+ }
29
40
  export async function localVersion(packageRoot = root()) {
30
41
  const failed = () => new CliError("version_unavailable", "无法核对当前命令行版本。", "请使用完整的一间草堂命令行构建;不要运行不明升级脚本。", 7);
31
42
  try {
@@ -53,33 +64,110 @@ export async function localVersion(packageRoot = root()) {
53
64
  export function manualUpdateInstructions() {
54
65
  return { status: 200, body: { operation: "update", update_policy: "manual", next_action: "运行 npx @yijiancaotang/cli@latest setup --agent <你的名字> 更新到最新版。" } };
55
66
  }
56
- async function targetsFor(ctx, ids, setup) {
67
+ function connectionTimeInBeijing(value) {
68
+ if (!value)
69
+ return undefined;
70
+ const timestamp = Date.parse(value);
71
+ if (!Number.isFinite(timestamp))
72
+ return undefined;
73
+ const parts = new Intl.DateTimeFormat("en-GB", {
74
+ timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit",
75
+ hour: "2-digit", minute: "2-digit", hourCycle: "h23",
76
+ }).formatToParts(timestamp);
77
+ const fields = Object.fromEntries(parts.map(part => [part.type, part.value]));
78
+ return `${fields.year}-${fields.month}-${fields.day} ${fields.hour}:${fields.minute}`;
79
+ }
80
+ const recordsFor = (ctx) => ctx.agent ? new LocalRecords(ctx.homeDir, ctx.agent, ctx.auth.environment ?? "production", new URL(ctx.auth.api.baseUrl).origin) : undefined;
81
+ async function targetsFor(ctx, ids, setup, assets, recorded, skillsDirectory) {
82
+ const inspect = (targets) => inspectManagedOrigins({ ...ctx, agentName: ctx.agent?.key, assets, targets });
83
+ if (skillsDirectory !== undefined) {
84
+ const target = await explicitSkillsTarget({ homeDir: ctx.homeDir, skillsDirectory });
85
+ return { targets: [target], origins: await inspect([target]) };
86
+ }
87
+ if (ids === undefined && recorded.some(target => target.id === "generic")) {
88
+ if (recorded.length !== 1)
89
+ throw new CliError("skill_target_ambiguous", "这个名字记录了多个技能目录,未自动选择。", "请保留本次明确的--skills-dir或--platform参数;不会批量更新不同工作区。", 2);
90
+ const target = await explicitSkillsTarget({ homeDir: ctx.homeDir, skillsDirectory: recorded[0].skillDirectory });
91
+ return { targets: [target], origins: await inspect([target]) };
92
+ }
93
+ recorded = recorded.filter(target => isPlatformId(target.id));
57
94
  if (ctx.agent) {
58
95
  if (ids && ids.length !== 1)
59
96
  throw new CliError("invalid_arguments", "每次接入只给一个 AI 平台安装技能。", "请带自己的 --agent 名字,一次只选一个 --platform;其它 AI 分别接入。");
60
- ids = ids ?? (ctx.agent.platform ? [ctx.agent.platform] : []);
61
- if (!ids.length)
62
- return [];
97
+ if (ids === undefined && recorded.length === 0) {
98
+ const candidates = await knownSkillTargets({ ...ctx, skipInvalidCandidates: true }), origins = await inspect(candidates);
99
+ const own = candidates.filter(target => origins.platforms.some(platform => platform.id === target.id && platform.skill_directory === target.skillDirectory && platform.ownKnownCount > 0))
100
+ .map(target => ({ ...target, detected: true, evidence: "skills" }));
101
+ if (own.length > 0 || !ctx.agent.platform)
102
+ return { targets: own, origins };
103
+ }
104
+ ids = ids ?? (recorded.length ? recorded.map(target => target.id).filter(isPlatformId) : ctx.agent.platform ? [ctx.agent.platform] : []);
105
+ }
106
+ const redirected = (id) => id === "claude-code" ? ctx.env.CLAUDE_CONFIG_DIR !== undefined
107
+ : id === "openclaw" ? ctx.env.OPENCLAW_STATE_DIR !== undefined || ctx.env.OPENCLAW_HOME !== undefined
108
+ : id === "kimi-code" && ctx.env.KIMI_CODE_HOME !== undefined;
109
+ const preferred = recorded.filter(target => isPlatformId(target.id) && (!ids || ids.includes(target.id)) && !redirected(target.id));
110
+ if (preferred.some(target => isPlatformId(target.id) && !platformSupported(target.id, ctx.operatingSystem)))
111
+ throw new CliError("platform_unsupported", "已记录的技能平台不支持当前系统。", "请在原支持的系统上检查该平台;不会猜测新目录或搬动技能。", 2);
112
+ const discoveryIds = ids?.filter(id => !preferred.some(target => target.id === id));
113
+ // A recorded custom target must not cause inspection of an unrelated default
114
+ // directory just to rediscover the same platform.
115
+ const discovered = discoveryIds?.length === 0 ? [] : await detectPlatforms({ homeDir: ctx.homeDir, env: ctx.env, applicationRoots: ctx.applicationRoots, operatingSystem: ctx.operatingSystem, platformIds: discoveryIds });
116
+ const all = [...preferred.map(target => ({ ...target })), ...discovered];
117
+ const origins = await inspect(all);
118
+ for (const target of all) {
119
+ if (ctx.agent && !target.detected && origins.platforms.some(platform => platform.id === target.id && platform.skill_directory === target.skillDirectory && platform.ownKnownCount > 0)) {
120
+ target.detected = true;
121
+ target.evidence = "skills";
122
+ }
63
123
  }
64
- const all = await detectPlatforms({ homeDir: ctx.homeDir, env: ctx.env, applicationRoots: ctx.applicationRoots, operatingSystem: ctx.operatingSystem, platformIds: ids });
65
- return ids ? all.filter(target => ids.includes(target.id)) : setup ? all.filter(target => target.detected) : all;
124
+ return { targets: ids ? all.filter(target => isPlatformId(target.id) && ids.includes(target.id)) : setup ? all.filter(target => target.detected) : all, origins };
66
125
  }
67
126
  export async function localDoctor(ctx, flags, authFailure) {
127
+ const skillsDirectory = selectedSkillsDirectory(flags);
68
128
  return doctor({
69
129
  auth: ctx.auth, authFailure, agent: ctx.agent,
70
130
  inspectLocal: async () => {
71
131
  const assets = await loadSkillAssets(ctx.packageRoot ?? root());
72
- return inspectLocalInstallation({ ...ctx, agentName: ctx.agent?.key, assets, targets: await targetsFor(ctx, selectedPlatforms(flags.platform, ctx.operatingSystem), false) });
132
+ const recorded = skillsDirectory === undefined ? await recordsFor(ctx)?.targets() ?? [] : [];
133
+ const selection = await targetsFor(ctx, selectedPlatforms(flags.platform, ctx.operatingSystem), false, assets, recorded, skillsDirectory);
134
+ const inspected = await inspectLocalInstallation({ ...ctx, agentName: ctx.agent?.key, assets, targets: selection.targets });
135
+ const platforms = inspected.platforms.map(platform => {
136
+ const origin = selection.origins.platforms.find(origin => origin.id === platform.id && origin.skill_directory === platform.skill_directory);
137
+ return origin ? { ...platform, skills: origin.skills } : platform;
138
+ });
139
+ const issues = inspected.issues.filter(issue => !["skill_missing", "skill_modified", "skill_unsafe"].includes(issue.code));
140
+ let command = inspected.command;
141
+ if (selection.origins.command.legacyManaged) {
142
+ command = { ...command, installed: true, ...selection.origins.command };
143
+ for (let index = issues.length - 1; index >= 0; index--)
144
+ if (issues[index].code === "command_modified")
145
+ issues.splice(index, 1);
146
+ issues.push({ code: "command_outdated", message: "已核对为旧版受管命令行,需要更新。", next_action: "沿用原来的名字运行 npx @yijiancaotang/cli@latest setup --agent <你的名字> 更新,不需要重新批准连接。" });
147
+ }
148
+ const skillPlatforms = selection.targets.filter(target => recorded.some(record => record.id === target.id && record.skillDirectory === target.skillDirectory)
149
+ || selection.origins.platforms.some(origin => origin.id === target.id && origin.skill_directory === target.skillDirectory && origin.ownKnownCount > 0)).map(target => target.id);
150
+ return { ...inspected, command, platforms, issues, skill_platforms: skillPlatforms };
73
151
  },
74
152
  });
75
153
  }
76
154
  export async function localSetup(ctx, flags) {
77
- const assets = await loadSkillAssets(ctx.packageRoot ?? root()), targets = await targetsFor(ctx, selectedPlatforms(flags.platform, ctx.operatingSystem), true);
155
+ const skillsDirectory = selectedSkillsDirectory(flags);
156
+ const records = recordsFor(ctx), recorded = skillsDirectory === undefined ? await records?.targets() ?? [] : [];
157
+ const assets = await loadSkillAssets(ctx.packageRoot ?? root()), { targets } = await targetsFor(ctx, selectedPlatforms(flags.platform, ctx.operatingSystem), true, assets, recorded, skillsDirectory);
78
158
  const local = { ...ctx, agentName: ctx.agent?.key, assets, targets };
79
159
  const installed = await installLocal(local);
160
+ await records?.rememberInstallation(targets, assets, installed.bundleSha);
161
+ for (const target of targets) {
162
+ if (!target.detected) {
163
+ target.detected = true;
164
+ target.evidence = "skills";
165
+ }
166
+ }
80
167
  const newEntries = installed.newlyInstalledPlatforms;
81
168
  let authFailure;
82
169
  let authorizationAttempted = false;
170
+ const currentSecrets = new Set(ctx.auth.tokenOverride ? [ctx.auth.tokenOverride] : []);
83
171
  // Observe the actual start request after authLogin's credential/abort checks.
84
172
  // Validation and doctor requests do not constitute a new authorization attempt.
85
173
  const observedAuth = {
@@ -89,6 +177,8 @@ export async function localSetup(ctx, flags) {
89
177
  request(path, options) {
90
178
  if (path === "/api/v1/agent/device-auth/start" && options?.method === "POST")
91
179
  authorizationAttempted = true;
180
+ if (path === "/api/v1/agent/mcp-token/validate" && typeof options?.token === "string")
181
+ currentSecrets.add(options.token);
92
182
  return ctx.auth.api.request(path, options);
93
183
  },
94
184
  upload: (url, headers, bytes) => ctx.auth.api.upload(url, headers, bytes),
@@ -97,10 +187,12 @@ export async function localSetup(ctx, flags) {
97
187
  };
98
188
  let newAuthorizationOwner;
99
189
  let loginIdentity;
190
+ let loginResult = null;
100
191
  const names = defaultConnectionNames(targets.map(target => target.id), ctx.computerName, ctx.agent?.name);
101
192
  try {
102
193
  const login = await authLogin(observedAuth, { device_name: flags["device-name"] ?? names.deviceName, client_name: flags["client-name"] ?? names.clientName, noOpen: flags["no-open"] === true });
103
194
  const who = asRecord(login.body);
195
+ loginResult = who;
104
196
  if (typeof who?.user_id === "string" && typeof who.token_id === "string" && (who.kind === "manual" || who.kind === "device") && (who.scope === "read_only" || who.scope === "read_write")) {
105
197
  loginIdentity = { userId: who.user_id, tokenId: who.token_id, kind: who.kind, scope: who.scope };
106
198
  }
@@ -111,7 +203,7 @@ export async function localSetup(ctx, flags) {
111
203
  authFailure = error instanceof CliError ? error : new CliError("authorization_delivery_unknown", "无法确认授权操作的结果。", "先在网页核对并吊销未保存的设备,不要自动重新签发。", 6);
112
204
  }
113
205
  let checkedIdentity;
114
- const report = await doctor({ auth: ctx.auth, authFailure, agent: ctx.agent, inspectLocal: () => inspectLocalInstallation(local), onIdentity: who => { checkedIdentity = who; } });
206
+ const report = await doctor({ auth: ctx.auth, authFailure, agent: ctx.agent, inspectLocal: async () => ({ ...await inspectLocalInstallation(local), skill_platforms: targets.map(target => target.id) }), onIdentity: who => { checkedIdentity = who; } });
115
207
  const body = asRecord(report.result.body);
116
208
  if (!body)
117
209
  throw new CliError("diagnostic_unavailable", "本地安装已执行,但无法确认最终诊断。", "先运行doctor只读核对,不要直接重新授权。", 7);
@@ -129,8 +221,33 @@ export async function localSetup(ctx, flags) {
129
221
  const accountState = newEntries.length && body.ready === true && checkedIdentity ? await inspectAccountState(ctx.auth, checkedIdentity.userId) : "unknown";
130
222
  const greeting = body.ready === true && checkedIdentity ? onboardingMessage(newEntries, accountState, checkedIdentity.scope)
131
223
  : { account_state: "unknown", onboarding: { action: "none", newly_connected_platforms: [] }, user_message: setupFailureMessage(identityChanged ? "authorization_identity_changed" : authFailure?.code) };
224
+ const reused = Boolean(sameConnection && loginResult?.reused_existing === true);
225
+ let connection;
226
+ if (sameConnection && ctx.agent && checkedIdentity) {
227
+ if (reused) {
228
+ connection = ctx.auth.tokenOverride !== undefined ? { connection_name: ctx.agent.key, connection_name_source: "environment_override" }
229
+ : await records?.connection(checkedIdentity.tokenId, [...currentSecrets]) ?? { connection_name: ctx.agent.key, connection_name_source: "local_agent_key" };
230
+ }
231
+ else if (body.ready === true && loginResult?.new_authorization_confirmed === true && typeof loginResult.connection_name === "string" && typeof loginResult.connected_at === "string") {
232
+ connection = { connection_name: loginResult.connection_name, connection_name_source: "local_authorization_record", connected_at: loginResult.connected_at, connection_time_source: "device_authorization" };
233
+ await records?.rememberConnection(checkedIdentity.tokenId, connection, [...currentSecrets]);
234
+ }
235
+ }
236
+ const onboardingCopy = reused && checkedIdentity && (connection?.connection_name_source !== "environment_override"
237
+ ? greeting.onboarding.action !== "none" || checkedIdentity.scope === "read_only"
238
+ : greeting.onboarding.action === "none" && checkedIdentity.scope === "read_only")
239
+ ? { onboarding_message: greeting.onboarding.action !== "none" ? greeting.user_message : connectionCopy.read_only } : {};
240
+ const connectionDisplayTime = connectionTimeInBeijing(connection?.connected_at);
241
+ const reuseCopy = reused && connection && connection.connection_name_source !== "environment_override" ? {
242
+ user_message: "这个名字已经接过了,沿用原来的连接,不用再批准。",
243
+ next_action: `已沿用名为 ${connection.connection_name} 的连接${connectionDisplayTime ? `(${connectionDisplayTime} 接入)` : ""}。如果你不是它,请用你自己的名字重新运行 setup --agent <你的名字>,不要继续用这条连接。`,
244
+ ...(connection.connection_name_source === "local_agent_key" ? { connection_name_note: "本机仅记有规范化名字,原连接名称和接入时间未记录。" } : {}),
245
+ } : reused && connection?.connection_name_source === "environment_override" ? {
246
+ reuse_notice: "本次沿用环境中提供的授权,不是本机保存的同名连接。若要独立接入,请由本人先处理环境授权覆盖,再用自己的名字运行 setup --agent <你的名字>。",
247
+ } : {};
132
248
  return { exitCode: report.exitCode, result: { status: 200, body: { ...body, operation: "setup", local_installation: { changed: installed.changed, bundle_sha: installed.bundleSha }, authorization_attempted: authorizationAttempted,
133
- new_authorization_confirmed: confirmed, ...(confirmed ? { new_authorization_owner_id: newAuthorizationOwner } : {}), ...greeting,
134
- ...(ctx.agent && !targets.length ? { generic_usage: { message: "这个 AI 没有专用技能安装位置;请按下列命令使用自己的连接。", commands: ["doctor", "context --start", "projects", "knowledge", "search", "auth status"].map(command => `caotang --agent ${shellWord(ctx.agent.name)} ${command}`) } } : {}),
135
- ...(body.ready === true ? { next_conversation: "请开新对话说‘开工了’。" } : {}) } } };
249
+ new_authorization_confirmed: confirmed, ...(confirmed ? { new_authorization_owner_id: newAuthorizationOwner } : {}), reused_existing: reused, ...connection, ...greeting, ...onboardingCopy, ...reuseCopy,
250
+ guidance: setupGuidance({ ready: body.ready === true, connectionVerified: Boolean(sameConnection), agentName: ctx.agent?.name, environment: ctx.auth.environment, baseUrl: ctx.auth.api.baseUrl, credentialOrigin: ctx.auth.store.origin, scope: checkedIdentity?.scope }),
251
+ ...(ctx.agent && !targets.length ? { generic_usage: { message: "暂未定位到这个名字的专用技能;若曾使用自定义目录,请保留原目录参数再检查。仍可按下列命令使用自己的连接;context --handoff接手需服务端支持新协议,仅本人明确开工才用context --start。", commands: ["doctor", "context --handoff", "projects", "knowledge", "search", "auth status"].map(command => `caotang --agent ${shellWord(ctx.agent.name)} ${command}`) } } : {}),
252
+ ...(body.ready === true ? { next_conversation: "可以直接说‘看看草堂里的某个项目’;无需固定口令。" } : {}) } } };
136
253
  }
@@ -0,0 +1,150 @@
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 { isSkillTargetId } 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" || !isSkillTargetId(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 key = target.id === "generic" ? `${target.id}\0${target.skillDirectory}` : target.id;
112
+ const previous = targets.get(key);
113
+ if (previous && previous.skillDirectory !== target.skillDirectory)
114
+ throw unsafe();
115
+ targets.set(key, target);
116
+ }
117
+ return [...targets.values()];
118
+ }
119
+ async rememberInstallation(targets, assets, bundleSha) {
120
+ if (!hash(bundleSha) || !completeSkillInventory(assets.map(asset => asset.name)))
121
+ throw unsafe();
122
+ for (const target of targets) {
123
+ const bound = assets.map(asset => bindSkillAgent(asset, this.agent.key));
124
+ if (!await verifiedSkillFiles(this.tree.home, target, bound))
125
+ throw unsafe();
126
+ 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 })) };
127
+ this.targetPath(record.skill_directory);
128
+ try {
129
+ await this.tree.writeNew(join(this.skillDirectory, `${target.id}.${digest(JSON.stringify(record))}.json`), Buffer.from(JSON.stringify(record)), 0o600);
130
+ }
131
+ catch {
132
+ throw unsafe();
133
+ }
134
+ }
135
+ }
136
+ }
137
+ export async function verifiedSkillFiles(home, target, assets) {
138
+ const tree = new OwnedTree(home);
139
+ if (!completeSkillInventory(assets.map(asset => asset.name)))
140
+ return false;
141
+ for (const asset of assets) {
142
+ const directory = join(target.skillDirectory, asset.name), entries = await tree.list(directory);
143
+ if (!entries || entries.length !== 1 || entries[0] !== "SKILL.md")
144
+ return false;
145
+ const bytes = await tree.read(join(directory, "SKILL.md"), 65536);
146
+ if (!bytes || digest(bytes) !== asset.sha256)
147
+ return false;
148
+ }
149
+ return true;
150
+ }
@@ -1,3 +1,4 @@
1
1
  export const PLATFORM_IDS = ["claude-code", "codex", "openclaw", "kimi-code", "kimi-work", "workbuddy"];
2
+ export const isSkillTargetId = (value) => value === "generic" || isPlatformId(value);
2
3
  export const isPlatformId = (value) => PLATFORM_IDS.some(id => id === value);
3
4
  export const platformSupported = (id, operatingSystem = process.platform) => operatingSystem === "darwin" || (id !== "kimi-code" && id !== "kimi-work" && id !== "workbuddy");
@@ -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: "知识内容" },
@@ -1,10 +1,8 @@
1
1
  import { hostname } from "node:os";
2
- import { resolveCredential } from "./auth.js";
3
- import { verifiedHabit } from "./habit-wire.js";
4
2
  import { connectionCopy } from "./connection-copy.js";
5
3
  export const platformNames = {
6
4
  "claude-code": "Claude Code", codex: "Codex", openclaw: "OpenClaw",
7
- "kimi-code": "Kimi Code", "kimi-work": "Kimi 工作模式", workbuddy: "WorkBuddy",
5
+ "kimi-code": "Kimi Code", "kimi-work": "Kimi 工作模式", workbuddy: "WorkBuddy", generic: "通用技能目录",
8
6
  };
9
7
  export function defaultConnectionNames(platforms, computerName = hostname(), agentName) {
10
8
  if (agentName !== undefined)
@@ -17,22 +15,8 @@ export function defaultConnectionNames(platforms, computerName = hostname(), age
17
15
  && !/^(?:[a-f\d-]{12,}|ip-\d|localhost$|runner|docker|container|sandbox|codespace|pod-)/i.test(name);
18
16
  return { clientName, deviceName: reliable ? `${clientName} · ${name}` : clientName };
19
17
  }
20
- export async function inspectAccountState(auth, owner) {
21
- if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(owner))
22
- return "unknown";
23
- try {
24
- const credential = await resolveCredential(auth);
25
- const result = await auth.api.request("/api/v1/agent/m1/habit/start", { method: "GET", token: credential.token, signal: auth.signal });
26
- if (result.status !== 200)
27
- return "unknown";
28
- const body = verifiedHabit(result.body, true);
29
- if (body?.owner_id !== owner)
30
- return "unknown";
31
- return body.account_empty === true ? "empty" : body.account_empty === false ? "has_content" : "unknown";
32
- }
33
- catch {
34
- return "unknown";
35
- }
18
+ export async function inspectAccountState(_auth, _owner) {
19
+ return "unknown";
36
20
  }
37
21
  export function onboardingMessage(newlyConnected, state, scope) {
38
22
  const action = newlyConnected.length === 0 ? "none" : state === "empty" ? "introduce" : "ask_once";
@@ -43,6 +27,6 @@ export function onboardingMessage(newlyConnected, state, scope) {
43
27
  account_state: state,
44
28
  onboarding: { action, newly_connected_platforms: newlyConnected },
45
29
  user_message: message,
46
- skill_activation: newlyConnected.map(platform => ({ platform, message: platform === "codex" || platform === "claude-code" ? "新对话可以加载本次安装的技能。" : "如果看不到技能,请开新对话或重启应用;本次没有自动重启。" })),
30
+ skill_activation: newlyConnected.map(platform => ({ platform, message: "技能安装不代表当前 AI 已取得说明正文;请按宿主支持的方式加载,或运行 caotang guide 获取公共说明。" })),
47
31
  };
48
32
  }
package/dist/output.js CHANGED
@@ -1,8 +1,10 @@
1
+ import { workContextHuman } from "./work-context-human.js";
1
2
  import { asRecord, CliError } from "./errors.js";
2
3
  import { formatMissingSections } from "./missing-sections.js";
3
4
  import { projectedWriteCompleted } from "./write-permissions.js";
4
5
  import { formatSearchWarnings } from "./query-search.js";
5
6
  import { formatDailyNotice } from "./query-daily.js";
7
+ import { calendarStartNotice } from "./habit-wire.js";
6
8
  import { createDraftNotice } from "./create-bundle.js";
7
9
  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
10
  export function redact(value, secrets, depth = 0) {
@@ -23,6 +25,9 @@ export function redact(value, secrets, depth = 0) {
23
25
  }
24
26
  export function renderResult(result, human, secrets) {
25
27
  const data = redact({ http_status: result.status, data: result.body, ...(result.operationAudit?.length ? { operation_audit: result.operationAudit } : {}) }, secrets);
28
+ const summary = human ? workContextHuman(asRecord(data)?.data) : undefined;
29
+ if (summary !== undefined)
30
+ return summary + "\n";
26
31
  const missing = human ? formatMissingSections(asRecord(data)?.data) : "";
27
32
  const writeNotice = human && projectedWriteCompleted(result) ? "写入已完成;以下内容因读取权限未开启而没有返回,不是写入失败。\n" : "";
28
33
  const body = asRecord(result.body);
@@ -32,8 +37,9 @@ export function renderResult(result, human, secrets) {
32
37
  const warningNotice = human ? formatSearchWarnings(asRecord(data)?.data) : "";
33
38
  const dailyNotice = human ? formatDailyNotice(asRecord(data)?.data) : "";
34
39
  const draftNotice = human && [200, 201].includes(result.status) ? createDraftNotice(asRecord(data)?.data) : "";
40
+ const startNotice = human ? calendarStartNotice(asRecord(data)?.data) : "";
35
41
  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`;
42
+ return `${pendingNotice}${replayNotice}${writeNotice}${startNotice ? startNotice + "\n" : ""}${missing}${warningNotice}${dailyNotice}${draftNotice}${JSON.stringify(data, null, human ? 2 : undefined)}\n`;
37
43
  }
38
44
  // Only coherent transport/receipt evidence classifies the submitted write;
39
45
  // neither a drafted audit log nor a moderation-pending 202 means unknown.
package/dist/platforms.js CHANGED
@@ -2,6 +2,8 @@ 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 { OwnedTree } from "./installation.js";
6
+ import { PLATFORM_IDS, platformSupported } from "./local-types.js";
5
7
  const unsafe = () => new CliError("platform_path_unsafe", "平台目录未通过安全检查。", "请使用用户目录内的绝对路径,并检查目录和符号链接;不要提供目录中的配置内容。");
6
8
  const metadataError = (error) => {
7
9
  const code = asRecord(error)?.code;
@@ -87,6 +89,69 @@ async function skillDirectory(home, canonicalHome, root) {
87
89
  }
88
90
  return target;
89
91
  }
92
+ // An explicit parent directory is a target, never a request to scan a host or
93
+ // append another "skills" component. Current managed writes retain HOME's fence.
94
+ export async function explicitSkillsTarget(options) {
95
+ const failure = () => new CliError("platform_path_unsafe", "通用技能目录未通过安全检查。", "当前受管安装仅支持本人用户目录内的真实绝对路径;共享或远程位置可先离线导出技能ZIP,再按宿主正式入口导入,不会放宽属主或链接保护。", 2);
96
+ if (!absolute(options.homeDir) || !absolute(options.skillsDirectory))
97
+ throw failure();
98
+ const home = resolve(options.homeDir), path = resolve(options.skillsDirectory);
99
+ if (path === home || !within(home, path))
100
+ throw failure();
101
+ try {
102
+ await new OwnedTree(home).directory(path, false, async () => true);
103
+ }
104
+ catch {
105
+ throw failure();
106
+ }
107
+ return { id: "generic", detected: true, evidence: "explicit", skillDirectory: path };
108
+ }
109
+ function configuredSkillRoot(home, env, id) {
110
+ switch (id) {
111
+ case "claude-code": return env.CLAUDE_CONFIG_DIR ?? join(home, ".claude");
112
+ case "codex": return join(home, ".agents");
113
+ case "openclaw": return env.OPENCLAW_STATE_DIR ?? (env.OPENCLAW_HOME === undefined ? join(home, ".openclaw") : (() => {
114
+ if (!absolute(env.OPENCLAW_HOME) || !within(home, resolve(env.OPENCLAW_HOME)))
115
+ throw unsafe();
116
+ return join(env.OPENCLAW_HOME, ".openclaw");
117
+ })());
118
+ case "kimi-code": return env.KIMI_CODE_HOME ?? join(home, ".kimi-code");
119
+ case "kimi-work": return join(home, "Library/Application Support/kimi-desktop/daimon-share/daimon");
120
+ case "workbuddy": return join(home, ".workbuddy");
121
+ }
122
+ }
123
+ // Bounded skill-location discovery only. No PATH, application bundle, client
124
+ // executable, configuration contents or arbitrary HOME traversal is involved.
125
+ export async function knownSkillTargets(options) {
126
+ if (!absolute(options.homeDir))
127
+ throw unsafe();
128
+ const home = resolve(options.homeDir);
129
+ let canonicalHome;
130
+ try {
131
+ canonicalHome = await realpath(home);
132
+ if (!(await stat(home)).isDirectory())
133
+ throw unsafe();
134
+ }
135
+ catch (error) {
136
+ if (error instanceof CliError)
137
+ throw error;
138
+ throw metadataError(error);
139
+ }
140
+ const targets = [];
141
+ for (const id of PLATFORM_IDS)
142
+ if (platformSupported(id, options.operatingSystem)) {
143
+ try {
144
+ targets.push({ id, detected: false, evidence: "none", skillDirectory: await skillDirectory(home, canonicalHome, configuredSkillRoot(home, options.env, id)) });
145
+ }
146
+ catch (error) {
147
+ // Only optional, bounded discovery isolates a candidate. Explicit or
148
+ // recorded targets are checked by their original strict path instead.
149
+ if (!options.skipInvalidCandidates)
150
+ throw error;
151
+ }
152
+ }
153
+ return targets;
154
+ }
90
155
  async function claudeDesktop(home) {
91
156
  const root = join(home, "Library", "Application Support", "Claude", "claude-code");
92
157
  for (const version of await directories(root)) {
@@ -122,15 +187,7 @@ export async function detectPlatforms(options) {
122
187
  throw unsafe();
123
188
  const selected = (id) => options.platformIds === undefined || options.platformIds.includes(id);
124
189
  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
- ];
190
+ const candidates = ["claude-code", "codex", "openclaw"].map(id => ({ id, root: () => configuredSkillRoot(home, env, id) }));
134
191
  for (const candidate of candidates) {
135
192
  if (selected(candidate.id))
136
193
  targets.push({ id: candidate.id, detected: false, evidence: "none", skillDirectory: await skillDirectory(home, canonicalHome, candidate.root()) });
@@ -167,9 +224,9 @@ export async function detectPlatforms(options) {
167
224
  // Do not inspect an unselected new platform's personal paths. Without a
168
225
  // selector, only a verified program makes its skill root relevant.
169
226
  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],
227
+ ["kimi-code", "Kimi Code", "Kimi Code", configuredSkillRoot(home, env, "kimi-code"), "kimi"],
228
+ ["kimi-work", "Kimi", "Kimi", configuredSkillRoot(home, env, "kimi-work"), null],
229
+ ["workbuddy", "WorkBuddy", "Electron", configuredSkillRoot(home, env, "workbuddy"), null],
173
230
  ]) {
174
231
  if (!selected(id))
175
232
  continue;