@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.
@@ -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, isPlatformId, platformSupported } from "./local-types.js";
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
- // The shipped runtime has one nested generated dependency. Keep the 64-file
342
- // JS limit across both categories; do not recursively trust dist subtrees.
343
- if (!modules.includes("cli.js") || modules.length + 1 > 64)
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
- files.push({ path: "dist/generated/knowledge-growth.js", bytes: await readSource(join(dist, "generated", "knowledge-growth.js")), mode: 0o600 });
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 (!isPlatformId(target.id) || !platformSupported(target.id, ctx.operatingSystem) || !isAbsolute(target.skillDirectory) || tree.within(target.skillDirectory) === tree.home)
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
- // The fixed 1.1.0 baseline is admitted only after its complete public runtime
424
- // inventory verifies. Never infer provenance from a version or receipt.
425
- async function reviewedV110Bundle(tree, value) {
426
- const pin = REVIEWED_MANAGED_BUNDLE_1_1_0, directory = join(tree.home, ".caotang", "cli", "releases", pin.sha);
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 (value.version === "1.1.0" && value.sha !== REVIEWED_MANAGED_BUNDLE_1_1_0.sha && entries.includes(REVIEWED_MANAGED_BUNDLE_1_1_0.sha))
516
- found.push(await reviewedV110Bundle(tree, value));
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
  }
@@ -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 records = recordsFor(ctx), recorded = await records?.targets() ?? [];
134
- const assets = await loadSkillAssets(ctx.packageRoot ?? root()), { targets } = await targetsFor(ctx, selectedPlatforms(flags.platform, ctx.operatingSystem), true, assets, recorded);
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
- ...(ctx.agent && !targets.length ? { generic_usage: { message: "暂未定位到这个名字的专用技能;若曾使用自定义目录,请保留原目录参数再检查。仍可按下列命令使用自己的连接。", commands: ["doctor", "context --start", "projects", "knowledge", "search", "auth status"].map(command => `caotang --agent ${shellWord(ctx.agent.name)} ${command}`) } } : {}),
228
- ...(body.ready === true ? { next_conversation: "请开新对话说‘开工了’。" } : {}) } } };
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
  }
@@ -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 { isPlatformId } from "./local-types.js";
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" || !isPlatformId(value.platform) || !hash(value.bundle_sha) || !Array.isArray(value.skills)
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 previous = targets.get(target.id);
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(target.id, target);
115
+ targets.set(key, target);
115
116
  }
116
117
  return [...targets.values()];
117
118
  }
@@ -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");
@@ -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,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
+ }