@yijiancaotang/cli 1.1.0 → 1.2.1
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 +35 -23
- package/dist/cli.js +47 -8
- package/dist/command-help.js +9 -4
- package/dist/commands.js +5 -0
- package/dist/doctor.js +2 -1
- package/dist/generated/agent-operations.js +12 -0
- package/dist/handoff-read.js +29 -0
- package/dist/handoff-wire.js +42 -0
- package/dist/input-validation.js +14 -0
- package/dist/installation.js +39 -16
- package/dist/local-commands.js +38 -14
- package/dist/local-records.js +5 -4
- package/dist/local-types.js +1 -0
- package/dist/onboarding.js +4 -20
- package/dist/output.js +4 -0
- package/dist/platforms.js +18 -0
- package/dist/project-binding-cli.js +110 -0
- package/dist/project-binding.js +370 -0
- package/dist/reviewed-managed-bundle-baseline.js +780 -0
- package/dist/setup-guidance.js +40 -0
- package/dist/skills-export.js +206 -0
- package/dist/work-context-human.js +67 -0
- package/dist/work-context-wire.js +391 -0
- package/package.json +2 -2
- package/skills/caotang-context/SKILL.md +16 -8
- package/skills/caotang-first-connect/SKILL.md +41 -39
- package/skills/manifest.json +2 -2
package/dist/installation.js
CHANGED
|
@@ -4,11 +4,12 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { CliError, asRecord } from "./errors.js";
|
|
7
|
-
import { PLATFORM_IDS,
|
|
7
|
+
import { PLATFORM_IDS, isSkillTargetId, platformSupported } from "./local-types.js";
|
|
8
8
|
import { bindSkillAgent, completeSkillInventory } from "./skill-assets.js";
|
|
9
9
|
import { acquireLocalLock } from "./local-lock.js";
|
|
10
10
|
import { LEGACY_MANAGED_SKILLS } from "./managed-skill-baseline.js";
|
|
11
|
-
import { REVIEWED_MANAGED_BUNDLE_1_1_0 } from "./reviewed-managed-bundle-baseline.js";
|
|
11
|
+
import { REVIEWED_MANAGED_BUNDLE_1_1_0, REVIEWED_NPM_MANAGED_BUNDLE_1_1_0, REVIEWED_NPM_MANAGED_BUNDLE_1_2_0 } from "./reviewed-managed-bundle-baseline.js";
|
|
12
|
+
const reviewedManagedBundles = [REVIEWED_MANAGED_BUNDLE_1_1_0, REVIEWED_NPM_MANAGED_BUNDLE_1_1_0, REVIEWED_NPM_MANAGED_BUNDLE_1_2_0];
|
|
12
13
|
const same = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
13
14
|
const missing = (error) => asRecord(error)?.code === "ENOENT";
|
|
14
15
|
const unsafe = () => new CliError("installation_unsafe", "本地安装位置未通过安全检查。", "请人工检查属主、权限、符号链接与目标目录;不要自动删除、覆盖或修改凭据。");
|
|
@@ -338,16 +339,35 @@ async function bundle(ctx, tree) {
|
|
|
338
339
|
if (!(await lstat(dist)).isDirectory() || await realpath(dist) !== dist)
|
|
339
340
|
throw sourceInvalid();
|
|
340
341
|
const modules = (await readdir(dist)).filter(name => name.endsWith(".js")).sort();
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
if (!modules.includes("cli.js")
|
|
342
|
+
// Only reviewed generated dependencies may enter a stable runtime bundle.
|
|
343
|
+
// Old fixtures/releases with no operations import retain the original closure.
|
|
344
|
+
if (!modules.includes("cli.js"))
|
|
344
345
|
throw sourceInvalid();
|
|
345
346
|
for (const name of modules) {
|
|
346
347
|
if (!/^[a-z][a-z0-9-]*\.js$/.test(name))
|
|
347
348
|
throw sourceInvalid();
|
|
348
349
|
files.push({ path: `dist/${name}`, bytes: await readSource(join(dist, name)), mode: 0o600 });
|
|
349
350
|
}
|
|
350
|
-
|
|
351
|
+
const generatedModules = ["knowledge-growth.js"];
|
|
352
|
+
const operationsPath = join(dist, "generated", "agent-operations.js");
|
|
353
|
+
let hasOperations = false;
|
|
354
|
+
try {
|
|
355
|
+
await lstat(operationsPath);
|
|
356
|
+
hasOperations = true;
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (!missing(error))
|
|
360
|
+
throw sourceInvalid();
|
|
361
|
+
}
|
|
362
|
+
const requiresOperations = files.some(file => file.path.startsWith("dist/") && file.bytes.toString("utf8").includes("generated/agent-operations.js"));
|
|
363
|
+
if (requiresOperations && !hasOperations)
|
|
364
|
+
throw sourceInvalid();
|
|
365
|
+
if (hasOperations)
|
|
366
|
+
generatedModules.push("agent-operations.js");
|
|
367
|
+
if (modules.length + generatedModules.length > 64)
|
|
368
|
+
throw sourceInvalid();
|
|
369
|
+
for (const name of generatedModules)
|
|
370
|
+
files.push({ path: `dist/generated/${name}`, bytes: await readSource(join(dist, "generated", name)), mode: 0o600 });
|
|
351
371
|
files.push({ path: "skills/manifest.json", bytes: Buffer.from(JSON.stringify({ formatVersion: 1, skills: ctx.assets.map(({ name, description }) => ({ name, description })) })), mode: 0o600 });
|
|
352
372
|
for (const asset of ctx.assets)
|
|
353
373
|
files.push({ path: `skills/${asset.name}/SKILL.md`, bytes: Buffer.from(asset.content), mode: 0o600 });
|
|
@@ -362,10 +382,10 @@ async function bundle(ctx, tree) {
|
|
|
362
382
|
return { files, sha, directory, command, shim, version: info.version };
|
|
363
383
|
}
|
|
364
384
|
function validateTargets(ctx, tree) {
|
|
365
|
-
if (ctx.targets.length > PLATFORM_IDS.length || new Set(ctx.targets.map(target => target.id)).size !== ctx.targets.length)
|
|
385
|
+
if (ctx.targets.length > PLATFORM_IDS.length + 1 || new Set(ctx.targets.map(target => target.id)).size !== ctx.targets.length)
|
|
366
386
|
throw unsafe();
|
|
367
387
|
for (const target of ctx.targets) {
|
|
368
|
-
if (!
|
|
388
|
+
if (!isSkillTargetId(target.id) || target.id !== "generic" && !platformSupported(target.id, ctx.operatingSystem) || !isAbsolute(target.skillDirectory) || tree.within(target.skillDirectory) === tree.home)
|
|
369
389
|
throw unsafe();
|
|
370
390
|
}
|
|
371
391
|
}
|
|
@@ -420,10 +440,10 @@ async function onPath(command, env) {
|
|
|
420
440
|
}
|
|
421
441
|
return false;
|
|
422
442
|
}
|
|
423
|
-
//
|
|
424
|
-
// inventory verifies. Never
|
|
425
|
-
async function
|
|
426
|
-
const
|
|
443
|
+
// Each fixed baseline is admitted only after its own complete public runtime
|
|
444
|
+
// inventory verifies. Never combine files across pins or trust a version/receipt.
|
|
445
|
+
async function reviewedBundle(tree, value, pin) {
|
|
446
|
+
const directory = join(tree.home, ".caotang", "cli", "releases", pin.sha);
|
|
427
447
|
const children = new Map([["", new Set()]]);
|
|
428
448
|
for (const file of pin.files) {
|
|
429
449
|
const parts = file.path.split("/");
|
|
@@ -463,7 +483,7 @@ async function reviewedV110Bundle(tree, value) {
|
|
|
463
483
|
// Keep the reviewed 1.0.0 six-skill baseline unchanged. A directory digest alone
|
|
464
484
|
// cannot authorize replacing somebody's same-name skill.
|
|
465
485
|
async function legacyBundles(ctx, tree, value) {
|
|
466
|
-
if (!["1.0.1", "1.1.0"].includes(value.version) || ctx.agentName === undefined)
|
|
486
|
+
if (!["1.0.1", "1.1.0", "1.1.1", "1.2.0", "1.2.1"].includes(value.version) || ctx.agentName === undefined)
|
|
467
487
|
return [];
|
|
468
488
|
const releases = join(tree.home, ".caotang", "cli", "releases"), entries = await tree.list(releases) ?? [];
|
|
469
489
|
if (entries.length > 128)
|
|
@@ -512,8 +532,11 @@ async function legacyBundles(ctx, tree, value) {
|
|
|
512
532
|
throw conflict();
|
|
513
533
|
found.push({ bundle: { files, sha, version: "1.0.0", directory, command: value.command, shim: managedShim(directory) }, assets });
|
|
514
534
|
}
|
|
515
|
-
if (
|
|
516
|
-
|
|
535
|
+
if (["1.1.0", "1.1.1", "1.2.0", "1.2.1"].includes(value.version)) {
|
|
536
|
+
for (const pin of reviewedManagedBundles)
|
|
537
|
+
if (value.sha !== pin.sha && entries.includes(pin.sha))
|
|
538
|
+
found.push(await reviewedBundle(tree, value, pin));
|
|
539
|
+
}
|
|
517
540
|
return found;
|
|
518
541
|
}
|
|
519
542
|
async function installPlan(ctx, tree, value) {
|
|
@@ -708,7 +731,7 @@ export async function inspectLocalInstallation(ctx) {
|
|
|
708
731
|
}
|
|
709
732
|
skills.push({ name: asset.name, status });
|
|
710
733
|
if (target.detected && status !== "installed")
|
|
711
|
-
issue(status === "missing" ? "skill_missing" : status === "modified" ? "skill_modified" : "skill_unsafe", "本地技能缺失、已修改或不满足安全要求。", status === "missing" ? `运行 setup --platform ${target.id} 安装缺失技能。` : "保留现有技能并人工核对,不自动覆盖或修复。");
|
|
734
|
+
issue(status === "missing" ? "skill_missing" : status === "modified" ? "skill_modified" : "skill_unsafe", "本地技能缺失、已修改或不满足安全要求。", status === "missing" ? (target.id === "generic" ? "保留本次 --skills-dir 的明确目录再运行 setup;不会猜测其他技能位置。" : `运行 setup --platform ${target.id} 安装缺失技能。`) : "保留现有技能并人工核对,不自动覆盖或修复。");
|
|
712
735
|
}
|
|
713
736
|
platforms.push({ id: target.id, detected: target.detected, skill_directory: target.skillDirectory, skills });
|
|
714
737
|
}
|
package/dist/local-commands.js
CHANGED
|
@@ -1,18 +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
9
|
import { inspectLocalInstallation, inspectManagedOrigins, installLocal } from "./installation.js";
|
|
10
10
|
import { isPlatformId, platformSupported } from "./local-types.js";
|
|
11
|
-
import { detectPlatforms, knownSkillTargets } from "./platforms.js";
|
|
11
|
+
import { detectPlatforms, explicitSkillsTarget, knownSkillTargets } from "./platforms.js";
|
|
12
12
|
import { loadSkillAssets } from "./skill-assets.js";
|
|
13
13
|
import { LocalRecords } from "./local-records.js";
|
|
14
14
|
import { defaultConnectionNames, inspectAccountState, onboardingMessage } from "./onboarding.js";
|
|
15
15
|
import { connectionCopy, setupFailureMessage } from "./connection-copy.js";
|
|
16
|
+
import { setupGuidance } from "./setup-guidance.js";
|
|
16
17
|
const root = () => fileURLToPath(new URL("../", import.meta.url));
|
|
17
18
|
const invalid = () => new CliError("invalid_arguments", "命令或参数无效。", "运行 --help;平台支持claude-code、codex、openclaw,以及仅macOS支持的kimi-code、kimi-work和workbuddy。");
|
|
18
19
|
export function selectedPlatforms(value, operatingSystem = process.platform) {
|
|
@@ -27,6 +28,15 @@ export function selectedPlatforms(value, operatingSystem = process.platform) {
|
|
|
27
28
|
throw new CliError("platform_unsupported", "所选新增平台尚不支持当前系统。", "kimi-code、kimi-work和workbuddy仅支持macOS本机安装;不要套用或猜测其它系统的技能目录。");
|
|
28
29
|
return values;
|
|
29
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
|
+
}
|
|
30
40
|
export async function localVersion(packageRoot = root()) {
|
|
31
41
|
const failed = () => new CliError("version_unavailable", "无法核对当前命令行版本。", "请使用完整的一间草堂命令行构建;不要运行不明升级脚本。", 7);
|
|
32
42
|
try {
|
|
@@ -68,8 +78,19 @@ function connectionTimeInBeijing(value) {
|
|
|
68
78
|
return `${fields.year}-${fields.month}-${fields.day} ${fields.hour}:${fields.minute}`;
|
|
69
79
|
}
|
|
70
80
|
const recordsFor = (ctx) => ctx.agent ? new LocalRecords(ctx.homeDir, ctx.agent, ctx.auth.environment ?? "production", new URL(ctx.auth.api.baseUrl).origin) : undefined;
|
|
71
|
-
async function targetsFor(ctx, ids, setup, assets, recorded) {
|
|
81
|
+
async function targetsFor(ctx, ids, setup, assets, recorded, skillsDirectory) {
|
|
72
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));
|
|
73
94
|
if (ctx.agent) {
|
|
74
95
|
if (ids && ids.length !== 1)
|
|
75
96
|
throw new CliError("invalid_arguments", "每次接入只给一个 AI 平台安装技能。", "请带自己的 --agent 名字,一次只选一个 --platform;其它 AI 分别接入。");
|
|
@@ -80,13 +101,13 @@ async function targetsFor(ctx, ids, setup, assets, recorded) {
|
|
|
80
101
|
if (own.length > 0 || !ctx.agent.platform)
|
|
81
102
|
return { targets: own, origins };
|
|
82
103
|
}
|
|
83
|
-
ids = ids ?? (recorded.length ? recorded.map(target => target.id) : ctx.agent.platform ? [ctx.agent.platform] : []);
|
|
104
|
+
ids = ids ?? (recorded.length ? recorded.map(target => target.id).filter(isPlatformId) : ctx.agent.platform ? [ctx.agent.platform] : []);
|
|
84
105
|
}
|
|
85
106
|
const redirected = (id) => id === "claude-code" ? ctx.env.CLAUDE_CONFIG_DIR !== undefined
|
|
86
107
|
: id === "openclaw" ? ctx.env.OPENCLAW_STATE_DIR !== undefined || ctx.env.OPENCLAW_HOME !== undefined
|
|
87
108
|
: id === "kimi-code" && ctx.env.KIMI_CODE_HOME !== undefined;
|
|
88
|
-
const preferred = recorded.filter(target => (!ids || ids.includes(target.id)) && !redirected(target.id));
|
|
89
|
-
if (preferred.some(target => !platformSupported(target.id, ctx.operatingSystem)))
|
|
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)))
|
|
90
111
|
throw new CliError("platform_unsupported", "已记录的技能平台不支持当前系统。", "请在原支持的系统上检查该平台;不会猜测新目录或搬动技能。", 2);
|
|
91
112
|
const discoveryIds = ids?.filter(id => !preferred.some(target => target.id === id));
|
|
92
113
|
// A recorded custom target must not cause inspection of an unrelated default
|
|
@@ -100,15 +121,16 @@ async function targetsFor(ctx, ids, setup, assets, recorded) {
|
|
|
100
121
|
target.evidence = "skills";
|
|
101
122
|
}
|
|
102
123
|
}
|
|
103
|
-
return { targets: ids ? all.filter(target => ids.includes(target.id)) : setup ? all.filter(target => target.detected) : all, origins };
|
|
124
|
+
return { targets: ids ? all.filter(target => isPlatformId(target.id) && ids.includes(target.id)) : setup ? all.filter(target => target.detected) : all, origins };
|
|
104
125
|
}
|
|
105
126
|
export async function localDoctor(ctx, flags, authFailure) {
|
|
127
|
+
const skillsDirectory = selectedSkillsDirectory(flags);
|
|
106
128
|
return doctor({
|
|
107
129
|
auth: ctx.auth, authFailure, agent: ctx.agent,
|
|
108
130
|
inspectLocal: async () => {
|
|
109
131
|
const assets = await loadSkillAssets(ctx.packageRoot ?? root());
|
|
110
|
-
const recorded = await recordsFor(ctx)?.targets() ?? [];
|
|
111
|
-
const selection = await targetsFor(ctx, selectedPlatforms(flags.platform, ctx.operatingSystem), false, assets, recorded);
|
|
132
|
+
const recorded = skillsDirectory === undefined ? await recordsFor(ctx)?.targets() ?? [] : [];
|
|
133
|
+
const selection = await targetsFor(ctx, selectedPlatforms(flags.platform, ctx.operatingSystem), false, assets, recorded, skillsDirectory);
|
|
112
134
|
const inspected = await inspectLocalInstallation({ ...ctx, agentName: ctx.agent?.key, assets, targets: selection.targets });
|
|
113
135
|
const platforms = inspected.platforms.map(platform => {
|
|
114
136
|
const origin = selection.origins.platforms.find(origin => origin.id === platform.id && origin.skill_directory === platform.skill_directory);
|
|
@@ -123,15 +145,16 @@ export async function localDoctor(ctx, flags, authFailure) {
|
|
|
123
145
|
issues.splice(index, 1);
|
|
124
146
|
issues.push({ code: "command_outdated", message: "已核对为旧版受管命令行,需要更新。", next_action: "沿用原来的名字运行 npx @yijiancaotang/cli@latest setup --agent <你的名字> 更新,不需要重新批准连接。" });
|
|
125
147
|
}
|
|
126
|
-
const skillPlatforms = selection.targets.filter(target => recorded.some(record => record.id === target.id)
|
|
148
|
+
const skillPlatforms = selection.targets.filter(target => recorded.some(record => record.id === target.id && record.skillDirectory === target.skillDirectory)
|
|
127
149
|
|| selection.origins.platforms.some(origin => origin.id === target.id && origin.skill_directory === target.skillDirectory && origin.ownKnownCount > 0)).map(target => target.id);
|
|
128
150
|
return { ...inspected, command, platforms, issues, skill_platforms: skillPlatforms };
|
|
129
151
|
},
|
|
130
152
|
});
|
|
131
153
|
}
|
|
132
154
|
export async function localSetup(ctx, flags) {
|
|
133
|
-
const
|
|
134
|
-
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);
|
|
135
158
|
const local = { ...ctx, agentName: ctx.agent?.key, assets, targets };
|
|
136
159
|
const installed = await installLocal(local);
|
|
137
160
|
await records?.rememberInstallation(targets, assets, installed.bundleSha);
|
|
@@ -224,6 +247,7 @@ export async function localSetup(ctx, flags) {
|
|
|
224
247
|
} : {};
|
|
225
248
|
return { exitCode: report.exitCode, result: { status: 200, body: { ...body, operation: "setup", local_installation: { changed: installed.changed, bundle_sha: installed.bundleSha }, authorization_attempted: authorizationAttempted,
|
|
226
249
|
new_authorization_confirmed: confirmed, ...(confirmed ? { new_authorization_owner_id: newAuthorizationOwner } : {}), reused_existing: reused, ...connection, ...greeting, ...onboardingCopy, ...reuseCopy,
|
|
227
|
-
|
|
228
|
-
...(
|
|
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: "可以直接说‘看看草堂里的某个项目’;无需固定口令。" } : {}) } } };
|
|
229
253
|
}
|
package/dist/local-records.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { join, relative, resolve, sep, isAbsolute } from "node:path";
|
|
3
3
|
import { asRecord, CliError } from "./errors.js";
|
|
4
4
|
import { OwnedTree } from "./installation.js";
|
|
5
|
-
import {
|
|
5
|
+
import { isSkillTargetId } from "./local-types.js";
|
|
6
6
|
import { bindSkillAgent, completeSkillInventory } from "./skill-assets.js";
|
|
7
7
|
const unsafe = () => new CliError("local_record_unsafe", "本机连接说明或技能安装记录未通过核对。", "保留现有连接和文件,先由本人核对本机记录;不要重新批准、覆盖或删除。", 7);
|
|
8
8
|
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
@@ -94,7 +94,7 @@ export class LocalRecords {
|
|
|
94
94
|
throw unsafe();
|
|
95
95
|
const value = await this.read(join(this.skillDirectory, name));
|
|
96
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" || !
|
|
97
|
+
|| typeof value.platform !== "string" || !isSkillTargetId(value.platform) || !hash(value.bundle_sha) || !Array.isArray(value.skills)
|
|
98
98
|
|| !value.skills.every(item => { const entry = asRecord(item); return entry && keys(entry, ["name", "sha256"]) && typeof entry.name === "string" && hash(entry.sha256); })
|
|
99
99
|
|| !completeSkillInventory(value.skills.map(item => item.name)) || name !== `${value.platform}.${digest(JSON.stringify(value))}.json`)
|
|
100
100
|
throw unsafe();
|
|
@@ -108,10 +108,11 @@ export class LocalRecords {
|
|
|
108
108
|
// A retained receipt cannot make a removed directory appear installed.
|
|
109
109
|
// Keep the target so the ordinary inspector reports its missing skills.
|
|
110
110
|
const target = { id: value.platform, skillDirectory, detected: true, evidence: "skills" };
|
|
111
|
-
const
|
|
111
|
+
const key = target.id === "generic" ? `${target.id}\0${target.skillDirectory}` : target.id;
|
|
112
|
+
const previous = targets.get(key);
|
|
112
113
|
if (previous && previous.skillDirectory !== target.skillDirectory)
|
|
113
114
|
throw unsafe();
|
|
114
|
-
targets.set(
|
|
115
|
+
targets.set(key, target);
|
|
115
116
|
}
|
|
116
117
|
return [...targets.values()];
|
|
117
118
|
}
|
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");
|
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,3 +1,4 @@
|
|
|
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";
|
|
@@ -24,6 +25,9 @@ export function redact(value, secrets, depth = 0) {
|
|
|
24
25
|
}
|
|
25
26
|
export function renderResult(result, human, secrets) {
|
|
26
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";
|
|
27
31
|
const missing = human ? formatMissingSections(asRecord(data)?.data) : "";
|
|
28
32
|
const writeNotice = human && projectedWriteCompleted(result) ? "写入已完成;以下内容因读取权限未开启而没有返回,不是写入失败。\n" : "";
|
|
29
33
|
const body = asRecord(result.body);
|
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 { OwnedTree } from "./installation.js";
|
|
5
6
|
import { PLATFORM_IDS, platformSupported } from "./local-types.js";
|
|
6
7
|
const unsafe = () => new CliError("platform_path_unsafe", "平台目录未通过安全检查。", "请使用用户目录内的绝对路径,并检查目录和符号链接;不要提供目录中的配置内容。");
|
|
7
8
|
const metadataError = (error) => {
|
|
@@ -88,6 +89,23 @@ async function skillDirectory(home, canonicalHome, root) {
|
|
|
88
89
|
}
|
|
89
90
|
return target;
|
|
90
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
|
+
}
|
|
91
109
|
function configuredSkillRoot(home, env, id) {
|
|
92
110
|
switch (id) {
|
|
93
111
|
case "claude-code": return env.CLAUDE_CONFIG_DIR ?? join(home, ".claude");
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { authStatus } from "./auth.js";
|
|
2
|
+
import { asRecord, CliError } from "./errors.js";
|
|
3
|
+
import { previewProjectBinding, confirmProjectBinding, readProjectBinding, unbindProjectBinding } from "./project-binding.js";
|
|
4
|
+
export function isProjectBindingCommand(command) { return command[0] === "project" && ["bind", "binding", "unbind"].includes(command[1]); }
|
|
5
|
+
function operation(command, flags) { return command[1] === "bind" ? flags.confirm === undefined ? "project_bind_preview" : "project_bind" : command[1] === "unbind" ? flags.confirm === undefined ? "project_unbind_preview" : "project_unbind" : "project_binding"; }
|
|
6
|
+
export function projectBindingLocalScopeOutcome(command, flags) {
|
|
7
|
+
if (!isProjectBindingCommand(command) || flags["local-workspace"] === true || command[1] === "bind" && flags.confirm === undefined)
|
|
8
|
+
return;
|
|
9
|
+
return { result: { status: 200, body: { schema_version: 1, operation: operation(command, flags), status: "local_scope_required", reason: "independent_local_workspace_not_confirmed" } }, exitCode: 2 };
|
|
10
|
+
}
|
|
11
|
+
function serviceOrigin(value) {
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(value);
|
|
14
|
+
if (typeof value !== "string" || value.trim() !== value || url.username || url.password || url.search || url.hash || url.pathname !== "/" || url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)))
|
|
15
|
+
throw Error();
|
|
16
|
+
return url.origin;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new CliError("project_binding_origin_unverified", "当前关联操作的服务地址未通过核对。", "保留当前账号和本机元数据,先核对实际服务;不要借环境标签使用另一服务的关联。", 7);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const identityError = () => new CliError("invalid_auth_response", "无法核对本次关联操作的账号身份。", "先核对当前连接,不读取旧关联或自动切换账号。", 7);
|
|
23
|
+
const cancelled = (auth) => { if (auth.signal?.aborted)
|
|
24
|
+
throw new CliError("authorization_cancelled", "本次关联操作已取消。", "需要时重新查看本机预览,不自动确认或重写。", 130); };
|
|
25
|
+
// The caller selects the credential once. Flags carry an already-obtained human
|
|
26
|
+
// decision; neither a flag nor a digest is proof that the human consented.
|
|
27
|
+
export async function createVerifiedProjectBindingConnection(auth, selectedToken, options) {
|
|
28
|
+
cancelled(auth);
|
|
29
|
+
const origin = serviceOrigin(auth.api.baseUrl);
|
|
30
|
+
if (auth.store.origin !== origin || options.expectedOrigin !== undefined && serviceOrigin(options.expectedOrigin) !== origin)
|
|
31
|
+
throw identityError();
|
|
32
|
+
const who = asRecord((await authStatus({ ...auth, tokenOverride: selectedToken })).body);
|
|
33
|
+
if (!who || who.status !== "authenticated" || typeof who.user_id !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(who.user_id) || serviceOrigin(auth.api.baseUrl) !== origin || auth.store.origin !== origin)
|
|
34
|
+
throw identityError();
|
|
35
|
+
const owner = who.user_id;
|
|
36
|
+
let validation;
|
|
37
|
+
const context = {
|
|
38
|
+
homeDir: options.homeDir, cwd: options.cwd, apiBaseUrl: origin, serviceBase: "/api/v1/agent", verifiedOwnerId: owner, localScopeConfirmed: options.localScopeConfirmed === true,
|
|
39
|
+
async readProject(id) {
|
|
40
|
+
cancelled(auth);
|
|
41
|
+
if (serviceOrigin(auth.api.baseUrl) !== origin || auth.store.origin !== origin)
|
|
42
|
+
throw identityError();
|
|
43
|
+
let response;
|
|
44
|
+
try {
|
|
45
|
+
response = await auth.api.request(`/api/v1/agent/m1/projects/${encodeURIComponent(id)}`, { method: "GET", token: selectedToken, signal: auth.signal });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
validation = { reason: auth.signal?.aborted ? "cancelled" : "transport_unavailable" };
|
|
49
|
+
throw new CliError("project_binding_target_unverified", "未核实关联目标。", "保留本机元数据,不自动确认或重试。", auth.signal?.aborted ? 130 : 7);
|
|
50
|
+
}
|
|
51
|
+
cancelled(auth);
|
|
52
|
+
if (response.status === 403 || response.status === 404) {
|
|
53
|
+
validation = { reason: response.status === 403 ? "permission_denied" : "not_found_or_unreadable", http_status: response.status };
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (response.status !== 200) {
|
|
57
|
+
validation = { reason: response.status === 401 ? "authorization_unverified" : response.status >= 500 ? "server_unavailable" : "project_response_unverified", http_status: response.status };
|
|
58
|
+
throw new CliError("project_binding_target_unverified", "未核实关联目标。", "保留本机元数据,不自动确认、重试或扩大读取。", response.status === 401 ? 3 : 7);
|
|
59
|
+
}
|
|
60
|
+
const body = asRecord(response.body), project = asRecord(body?.project);
|
|
61
|
+
// This existing endpoint reads the one necessary project context, not a
|
|
62
|
+
// zero-content metadata API. Binding never receives summary/updates/cards.
|
|
63
|
+
if (!body || Object.hasOwn(body, "code") || Object.hasOwn(body, "error") || !project || project.owner_id !== owner || typeof project.id !== "string" || project.id.toLowerCase() !== id.toLowerCase() || typeof project.title !== "string" || typeof project.status !== "string" || serviceOrigin(auth.api.baseUrl) !== origin || auth.store.origin !== origin) {
|
|
64
|
+
validation = { reason: "project_owner_or_metadata_unverified", http_status: 200 };
|
|
65
|
+
throw new CliError("project_binding_target_unverified", "未核实关联目标的归属或元数据。", "保留本机元数据,不根据旧项目名称猜测或确认。", 7);
|
|
66
|
+
}
|
|
67
|
+
return { id: project.id, title: project.title, status: project.status };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
return { context, projectCheck: () => validation };
|
|
71
|
+
}
|
|
72
|
+
function outcomeExit(body, check) {
|
|
73
|
+
if (["bound", "unbound", "not_bound", "confirmation_required"].includes(body.status))
|
|
74
|
+
return 0;
|
|
75
|
+
if (check?.reason === "cancelled")
|
|
76
|
+
return 130;
|
|
77
|
+
if (check?.http_status === 401)
|
|
78
|
+
return 3;
|
|
79
|
+
if (check?.http_status === 403)
|
|
80
|
+
return 4;
|
|
81
|
+
if (body.status === "unknown")
|
|
82
|
+
return body.reason === "project_binding_update_unknown" || body.reason === "project_binding_busy" ? 6 : 7;
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
export async function executeProjectBindingCLI(command, flags, options) {
|
|
86
|
+
const scope = projectBindingLocalScopeOutcome(command, flags);
|
|
87
|
+
if (scope)
|
|
88
|
+
return scope;
|
|
89
|
+
let connection;
|
|
90
|
+
try {
|
|
91
|
+
connection = await createVerifiedProjectBindingConnection(options.auth, options.token, options);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const known = error instanceof CliError ? error : undefined;
|
|
95
|
+
const http = asRecord(known?.details)?.http_status;
|
|
96
|
+
return { result: { status: 200, body: { schema_version: 1, operation: operation(command, flags), status: "unknown", reason: "identity_validation_unavailable", identity_check: { reason: known?.code === "unauthorized" ? "authorization_unverified" : known?.code === "project_binding_origin_unverified" ? "service_origin_unverified" : known?.exitCode === 130 ? "cancelled" : "identity_unverified", ...(typeof http === "number" ? { http_status: http } : {}) } } }, exitCode: known?.exitCode === 130 ? 130 : known?.code === "unauthorized" ? 3 : known?.exitCode === 4 ? 4 : 7 };
|
|
97
|
+
}
|
|
98
|
+
cancelled(options.auth);
|
|
99
|
+
let body;
|
|
100
|
+
if (command[1] === "bind") {
|
|
101
|
+
const binding = { projectId: command[2], includeSubdirs: flags["include-subdirs"] === true };
|
|
102
|
+
body = flags.confirm === undefined ? await previewProjectBinding(connection.context, binding) : await confirmProjectBinding(connection.context, { ...binding, previewDigest: String(flags.confirm), localScopeConfirmed: true });
|
|
103
|
+
}
|
|
104
|
+
else if (command[1] === "binding")
|
|
105
|
+
body = await readProjectBinding(connection.context);
|
|
106
|
+
else
|
|
107
|
+
body = await unbindProjectBinding(connection.context, { ...(flags.confirm === undefined ? {} : { previewDigest: String(flags.confirm), localScopeConfirmed: true }) });
|
|
108
|
+
const check = connection.projectCheck();
|
|
109
|
+
return { result: { status: 200, body: { ...body, ...(check ? { project_check: check } : {}) } }, exitCode: outcomeExit(body, check) };
|
|
110
|
+
}
|