@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.
- package/README.md +70 -26
- package/dist/auth.js +7 -4
- package/dist/calendar-contract.js +358 -0
- package/dist/calendar-help.js +12 -0
- package/dist/calendar.js +64 -0
- package/dist/capability-result.js +5 -0
- package/dist/cli.js +58 -10
- package/dist/command-help.js +18 -8
- package/dist/commands.js +11 -0
- package/dist/doctor.js +13 -5
- package/dist/generated/agent-operations.js +12 -0
- package/dist/habit-read.js +3 -1
- package/dist/habit-wire.js +36 -5
- package/dist/handoff-read.js +29 -0
- package/dist/handoff-wire.js +42 -0
- package/dist/http.js +11 -1
- package/dist/input-validation.js +14 -0
- package/dist/installation.js +334 -45
- package/dist/local-commands.js +133 -16
- package/dist/local-records.js +150 -0
- package/dist/local-types.js +1 -0
- package/dist/managed-skill-baseline.js +35 -0
- package/dist/missing-sections.js +1 -0
- package/dist/onboarding.js +4 -20
- package/dist/output.js +7 -1
- package/dist/platforms.js +69 -12
- package/dist/project-binding-cli.js +110 -0
- package/dist/project-binding.js +370 -0
- package/dist/query-search.js +3 -2
- package/dist/reviewed-managed-bundle-baseline.js +356 -0
- package/dist/schedule-contract.js +289 -0
- package/dist/schedule-help.js +35 -0
- package/dist/schedule-start-wire.js +80 -0
- package/dist/schedule.js +70 -0
- package/dist/setup-guidance.js +40 -0
- package/dist/skill-assets.js +1 -1
- package/dist/skills-export.js +206 -0
- package/dist/web-citations.js +1 -1
- package/dist/work-context-human.js +67 -0
- package/dist/work-context-wire.js +391 -0
- package/dist/write-routing.js +26 -0
- package/package.json +2 -2
- package/skills/caotang-context/SKILL.md +20 -7
- package/skills/caotang-first-connect/SKILL.md +48 -34
- package/skills/caotang-project-progress/SKILL.md +6 -2
- package/skills/caotang-save-knowledge/SKILL.md +6 -4
- package/skills/caotang-schedule/SKILL.md +54 -0
- package/skills/caotang-search/SKILL.md +11 -0
- package/skills/caotang-workday/SKILL.md +56 -9
- package/skills/manifest.json +28 -6
package/dist/local-commands.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
135
|
-
...(
|
|
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
|
+
}
|
package/dist/local-types.js
CHANGED
|
@@ -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
|
+
];
|
package/dist/missing-sections.js
CHANGED
|
@@ -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/onboarding.js
CHANGED
|
@@ -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(
|
|
21
|
-
|
|
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:
|
|
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",
|
|
171
|
-
["kimi-work", "Kimi", "Kimi",
|
|
172
|
-
["workbuddy", "WorkBuddy", "Electron",
|
|
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;
|