@yijiancaotang/cli 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +311 -2
- package/bin/caotang.cjs +15 -0
- package/dist/agent-identity.js +14 -0
- package/dist/auth.js +340 -0
- 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 +85 -0
- package/dist/cli.js +257 -0
- package/dist/command-help.js +105 -0
- package/dist/commands.js +356 -0
- package/dist/config.js +14 -0
- package/dist/connection-copy.js +40 -0
- package/dist/context.js +30 -0
- package/dist/contracts.js +1 -0
- package/dist/create-bundle.js +103 -0
- package/dist/create-file-failure.js +18 -0
- package/dist/credential-store.js +594 -0
- package/dist/doctor.js +221 -0
- package/dist/errors.js +214 -0
- package/dist/generated/knowledge-growth.js +37 -0
- package/dist/habit-day.js +58 -0
- package/dist/habit-read.js +32 -0
- package/dist/habit-wire.js +87 -0
- package/dist/http.js +211 -0
- package/dist/input-validation.js +76 -0
- package/dist/installation.js +761 -0
- package/dist/knowledge-growth.js +85 -0
- package/dist/local-commands.js +229 -0
- package/dist/local-lock.js +245 -0
- package/dist/local-records.js +149 -0
- package/dist/local-types.js +3 -0
- package/dist/managed-skill-baseline.js +35 -0
- package/dist/missing-sections.js +72 -0
- package/dist/onboarding.js +48 -0
- package/dist/output.js +54 -0
- package/dist/pending-requests.js +45 -0
- package/dist/platforms.js +239 -0
- package/dist/query-daily.js +70 -0
- package/dist/query-search.js +43 -0
- package/dist/query-validation.js +45 -0
- 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/skill-assets.js +136 -0
- package/dist/upload.js +130 -0
- package/dist/web-citations.js +68 -0
- package/dist/write-permissions.js +41 -0
- package/dist/write-routing.js +26 -0
- package/package.json +27 -4
- package/skills/caotang-context/SKILL.md +52 -0
- package/skills/caotang-first-connect/SKILL.md +74 -0
- package/skills/caotang-project-progress/SKILL.md +91 -0
- package/skills/caotang-save-knowledge/SKILL.md +42 -0
- package/skills/caotang-schedule/SKILL.md +54 -0
- package/skills/caotang-search/SKILL.md +79 -0
- package/skills/caotang-workday/SKILL.md +81 -0
- package/skills/manifest.json +33 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join, relative, resolve, sep, isAbsolute } from "node:path";
|
|
3
|
+
import { asRecord, CliError } from "./errors.js";
|
|
4
|
+
import { OwnedTree } from "./installation.js";
|
|
5
|
+
import { isPlatformId } from "./local-types.js";
|
|
6
|
+
import { bindSkillAgent, completeSkillInventory } from "./skill-assets.js";
|
|
7
|
+
const unsafe = () => new CliError("local_record_unsafe", "本机连接说明或技能安装记录未通过核对。", "保留现有连接和文件,先由本人核对本机记录;不要重新批准、覆盖或删除。", 7);
|
|
8
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
9
|
+
const keys = (value, expected) => Object.keys(value).sort().join(",") === [...expected].sort().join(",");
|
|
10
|
+
const id = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value);
|
|
11
|
+
const hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
12
|
+
const label = (value) => typeof value === "string" && value.trim() === value && [...value].length >= 1 && [...value].length <= 80 && !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value) && !/[^\S ]/u.test(value);
|
|
13
|
+
const date = (value) => typeof value === "string" && /^\d{4}-\d\d-\d\dT/.test(value) && Number.isFinite(Date.parse(value));
|
|
14
|
+
// These records contain no credentials. Immutable, content-addressed receipts
|
|
15
|
+
// retain upgrade history; identical setup never rewrites or accumulates files.
|
|
16
|
+
// They document installation, and never authorize overwriting a skill.
|
|
17
|
+
export class LocalRecords {
|
|
18
|
+
agent;
|
|
19
|
+
environment;
|
|
20
|
+
origin;
|
|
21
|
+
tree;
|
|
22
|
+
connectionDirectory;
|
|
23
|
+
skillDirectory;
|
|
24
|
+
constructor(home, agent, environment, origin) {
|
|
25
|
+
this.agent = agent;
|
|
26
|
+
this.environment = environment;
|
|
27
|
+
this.origin = origin;
|
|
28
|
+
this.tree = new OwnedTree(home);
|
|
29
|
+
this.connectionDirectory = join(this.tree.home, ".caotang", "connection-info", environment, agent.fileKey);
|
|
30
|
+
this.skillDirectory = join(this.tree.home, ".caotang", "skill-receipts", agent.fileKey);
|
|
31
|
+
}
|
|
32
|
+
async read(path) {
|
|
33
|
+
try {
|
|
34
|
+
const bytes = await this.tree.read(path, 32768);
|
|
35
|
+
if (!bytes)
|
|
36
|
+
return null;
|
|
37
|
+
const value = asRecord(JSON.parse(bytes.toString("utf8")));
|
|
38
|
+
if (!value)
|
|
39
|
+
throw unsafe();
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw unsafe();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async connection(tokenId, secrets = []) {
|
|
47
|
+
if (!id(tokenId))
|
|
48
|
+
throw unsafe();
|
|
49
|
+
const record = await this.read(join(this.connectionDirectory, `${tokenId}.json`));
|
|
50
|
+
if (!record)
|
|
51
|
+
return null;
|
|
52
|
+
const connectionName = record.connection_name;
|
|
53
|
+
if (!keys(record, ["version", "environment", "origin", "agent_key", "token_id", "connection_name", "connected_at"])
|
|
54
|
+
|| record.version !== 1 || record.environment !== this.environment || record.origin !== this.origin || record.agent_key !== this.agent.key || record.token_id !== tokenId
|
|
55
|
+
|| !label(connectionName) || !date(record.connected_at) || secrets.some(secret => secret.length >= 8 && connectionName.includes(secret)))
|
|
56
|
+
throw unsafe();
|
|
57
|
+
return { connection_name: connectionName, connection_name_source: "local_authorization_record", connected_at: record.connected_at, connection_time_source: "device_authorization" };
|
|
58
|
+
}
|
|
59
|
+
async rememberConnection(tokenId, info, secrets = []) {
|
|
60
|
+
if (!id(tokenId) || info.connection_name_source !== "local_authorization_record" || !label(info.connection_name) || !date(info.connected_at)
|
|
61
|
+
|| secrets.some(secret => secret.length >= 8 && info.connection_name.includes(secret)))
|
|
62
|
+
throw unsafe();
|
|
63
|
+
const record = { version: 1, environment: this.environment, origin: this.origin, agent_key: this.agent.key, token_id: tokenId, connection_name: info.connection_name, connected_at: info.connected_at };
|
|
64
|
+
try {
|
|
65
|
+
await this.tree.writeNew(join(this.connectionDirectory, `${tokenId}.json`), Buffer.from(JSON.stringify(record)), 0o600);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw unsafe();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
targetPath(path) {
|
|
72
|
+
if (typeof path !== "string" || !path || isAbsolute(path) || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(path))
|
|
73
|
+
throw unsafe();
|
|
74
|
+
const absolute = this.tree.within(join(this.tree.home, path));
|
|
75
|
+
if (absolute === this.tree.home || relative(this.tree.home, absolute) !== path || path.split(sep).includes(".."))
|
|
76
|
+
throw unsafe();
|
|
77
|
+
return absolute;
|
|
78
|
+
}
|
|
79
|
+
async targets() {
|
|
80
|
+
let names;
|
|
81
|
+
try {
|
|
82
|
+
names = await this.tree.list(this.skillDirectory);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw unsafe();
|
|
86
|
+
}
|
|
87
|
+
if (!names)
|
|
88
|
+
return [];
|
|
89
|
+
if (names.length > 256)
|
|
90
|
+
throw unsafe();
|
|
91
|
+
const targets = new Map();
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
if (!/^[a-z-]+\.[a-f0-9]{64}\.json$/.test(name))
|
|
94
|
+
throw unsafe();
|
|
95
|
+
const value = await this.read(join(this.skillDirectory, name));
|
|
96
|
+
if (!value || !keys(value, ["version", "agent_key", "platform", "skill_directory", "bundle_sha", "skills"]) || value.version !== 1 || value.agent_key !== this.agent.key
|
|
97
|
+
|| typeof value.platform !== "string" || !isPlatformId(value.platform) || !hash(value.bundle_sha) || !Array.isArray(value.skills)
|
|
98
|
+
|| !value.skills.every(item => { const entry = asRecord(item); return entry && keys(entry, ["name", "sha256"]) && typeof entry.name === "string" && hash(entry.sha256); })
|
|
99
|
+
|| !completeSkillInventory(value.skills.map(item => item.name)) || name !== `${value.platform}.${digest(JSON.stringify(value))}.json`)
|
|
100
|
+
throw unsafe();
|
|
101
|
+
const skillDirectory = this.targetPath(value.skill_directory);
|
|
102
|
+
try {
|
|
103
|
+
await this.tree.list(skillDirectory);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw unsafe();
|
|
107
|
+
}
|
|
108
|
+
// A retained receipt cannot make a removed directory appear installed.
|
|
109
|
+
// Keep the target so the ordinary inspector reports its missing skills.
|
|
110
|
+
const target = { id: value.platform, skillDirectory, detected: true, evidence: "skills" };
|
|
111
|
+
const previous = targets.get(target.id);
|
|
112
|
+
if (previous && previous.skillDirectory !== target.skillDirectory)
|
|
113
|
+
throw unsafe();
|
|
114
|
+
targets.set(target.id, target);
|
|
115
|
+
}
|
|
116
|
+
return [...targets.values()];
|
|
117
|
+
}
|
|
118
|
+
async rememberInstallation(targets, assets, bundleSha) {
|
|
119
|
+
if (!hash(bundleSha) || !completeSkillInventory(assets.map(asset => asset.name)))
|
|
120
|
+
throw unsafe();
|
|
121
|
+
for (const target of targets) {
|
|
122
|
+
const bound = assets.map(asset => bindSkillAgent(asset, this.agent.key));
|
|
123
|
+
if (!await verifiedSkillFiles(this.tree.home, target, bound))
|
|
124
|
+
throw unsafe();
|
|
125
|
+
const record = { version: 1, agent_key: this.agent.key, platform: target.id, skill_directory: relative(this.tree.home, resolve(target.skillDirectory)), bundle_sha: bundleSha, skills: bound.map(({ name, sha256 }) => ({ name, sha256 })) };
|
|
126
|
+
this.targetPath(record.skill_directory);
|
|
127
|
+
try {
|
|
128
|
+
await this.tree.writeNew(join(this.skillDirectory, `${target.id}.${digest(JSON.stringify(record))}.json`), Buffer.from(JSON.stringify(record)), 0o600);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw unsafe();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function verifiedSkillFiles(home, target, assets) {
|
|
137
|
+
const tree = new OwnedTree(home);
|
|
138
|
+
if (!completeSkillInventory(assets.map(asset => asset.name)))
|
|
139
|
+
return false;
|
|
140
|
+
for (const asset of assets) {
|
|
141
|
+
const directory = join(target.skillDirectory, asset.name), entries = await tree.list(directory);
|
|
142
|
+
if (!entries || entries.length !== 1 || entries[0] !== "SKILL.md")
|
|
143
|
+
return false;
|
|
144
|
+
const bytes = await tree.read(join(directory, "SKILL.md"), 65536);
|
|
145
|
+
if (!bytes || digest(bytes) !== asset.sha256)
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export const PLATFORM_IDS = ["claude-code", "codex", "openclaw", "kimi-code", "kimi-work", "workbuddy"];
|
|
2
|
+
export const isPlatformId = (value) => PLATFORM_IDS.some(id => id === value);
|
|
3
|
+
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
|
+
];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { asRecord } from "./errors.js";
|
|
2
|
+
const knownSections = {
|
|
3
|
+
daily_progress: { capability: "read_projects", label: "日汇总中的进展" },
|
|
4
|
+
daily_knowledge: { capability: "read_knowledge", label: "日汇总中的知识新增与长势" },
|
|
5
|
+
search_projects: { capability: "read_projects", label: "搜索中的项目与进展" },
|
|
6
|
+
search_knowledge: { capability: "read_knowledge", label: "搜索中的知识" },
|
|
7
|
+
search_schedule_items: { capability: "read_workbench", label: "搜索中的待办" },
|
|
8
|
+
workbench: { capability: "read_workbench", label: "工作台" },
|
|
9
|
+
briefing: { capability: "read_notifications", label: "简报" },
|
|
10
|
+
knowledge: { capability: "read_knowledge", label: "知识内容" },
|
|
11
|
+
recent_knowledge: { capability: "read_knowledge", label: "最近知识" },
|
|
12
|
+
draft_knowledge: { capability: "read_drafts", label: "草稿知识" },
|
|
13
|
+
recent_projects: { capability: "read_projects", label: "最近项目" },
|
|
14
|
+
project_updates: { capability: "read_projects", label: "项目进展" },
|
|
15
|
+
draft_projects: { capability: "read_projects", label: "项目草稿" },
|
|
16
|
+
project_drafts: { capability: "read_drafts", label: "项目草稿" },
|
|
17
|
+
project_draft_checkins: { capability: "read_drafts", label: "项目草稿关联的手写进展" },
|
|
18
|
+
project_evidence: { capability: "read_projects", label: "项目证据文字" },
|
|
19
|
+
draft_project_evidence: { capability: "read_drafts", label: "项目草稿证据文字" },
|
|
20
|
+
knowledge_applied_updates: { capability: "read_knowledge", label: "知识应用进展正文" },
|
|
21
|
+
draft_knowledge_applied_updates: { capability: "read_drafts", label: "可能含草稿知识的应用进展正文" },
|
|
22
|
+
knowledge_result: { capability: "read_knowledge", label: "知识写入结果" },
|
|
23
|
+
project_result: { capability: "read_projects", label: "项目写入结果" },
|
|
24
|
+
draft_knowledge_result: { capability: "read_drafts", label: "知识草稿写入结果" },
|
|
25
|
+
draft_project_result: { capability: "read_drafts", label: "项目草稿写入结果" },
|
|
26
|
+
};
|
|
27
|
+
const capabilityLabels = {
|
|
28
|
+
read_workbench: "读取工作台", read_notifications: "读取通知", read_knowledge: "读取知识",
|
|
29
|
+
read_drafts: "读取草稿", read_projects: "读取项目",
|
|
30
|
+
};
|
|
31
|
+
function reasonLabel(reason) {
|
|
32
|
+
if (reason === "capability_disabled")
|
|
33
|
+
return "开关未开启";
|
|
34
|
+
if (reason === "token_scope_insufficient")
|
|
35
|
+
return "设备权限范围不足";
|
|
36
|
+
if (["permission_denied", "forbidden", "access_denied", "authorization_unavailable", "moderation_blocked"].includes(reason))
|
|
37
|
+
return "权限不足或访问受限";
|
|
38
|
+
return "具体原因尚未核实";
|
|
39
|
+
}
|
|
40
|
+
/** Read only protocol locations; never traverse cards, titles, summaries or payloads. */
|
|
41
|
+
export function formatMissingSections(value) {
|
|
42
|
+
const body = asRecord(value);
|
|
43
|
+
if (!body)
|
|
44
|
+
return "";
|
|
45
|
+
const sources = [
|
|
46
|
+
{ prefix: "", value: body },
|
|
47
|
+
{ prefix: "工作台中的", value: asRecord(body.workbench) },
|
|
48
|
+
{ prefix: "简报中的", value: asRecord(body.briefing) },
|
|
49
|
+
];
|
|
50
|
+
const lines = [], seen = new Set();
|
|
51
|
+
for (const source of sources) {
|
|
52
|
+
const rows = source.value?.missing_sections;
|
|
53
|
+
if (!Array.isArray(rows))
|
|
54
|
+
continue;
|
|
55
|
+
// Bound invalid/repeated server metadata to the fixed protocol section set.
|
|
56
|
+
for (const row of rows.slice(0, 64)) {
|
|
57
|
+
const item = asRecord(row);
|
|
58
|
+
if (!item || typeof item.section !== "string" || !Object.hasOwn(knownSections, item.section)
|
|
59
|
+
|| typeof item.reason !== "string")
|
|
60
|
+
continue;
|
|
61
|
+
const section = knownSections[item.section];
|
|
62
|
+
if (item.required_capability !== section.capability)
|
|
63
|
+
continue;
|
|
64
|
+
const key = source.prefix + item.section;
|
|
65
|
+
if (seen.has(key))
|
|
66
|
+
continue;
|
|
67
|
+
seen.add(key);
|
|
68
|
+
lines.push(`- ${source.prefix}${section.label}:未读取,${reasonLabel(item.reason)}。请在网页检查“${capabilityLabels[section.capability]}”开关及设备权限。`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return lines.length ? `部分内容未读取,这不代表没有资料:\n${lines.join("\n")}\n` : "";
|
|
72
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { resolveCredential } from "./auth.js";
|
|
3
|
+
import { verifiedHabit } from "./habit-wire.js";
|
|
4
|
+
import { connectionCopy } from "./connection-copy.js";
|
|
5
|
+
export const platformNames = {
|
|
6
|
+
"claude-code": "Claude Code", codex: "Codex", openclaw: "OpenClaw",
|
|
7
|
+
"kimi-code": "Kimi Code", "kimi-work": "Kimi 工作模式", workbuddy: "WorkBuddy",
|
|
8
|
+
};
|
|
9
|
+
export function defaultConnectionNames(platforms, computerName = hostname(), agentName) {
|
|
10
|
+
if (agentName !== undefined)
|
|
11
|
+
return { clientName: agentName, deviceName: agentName };
|
|
12
|
+
const clientName = agentName ?? (platforms.length === 1 ? platformNames[platforms[0]] : "caotang");
|
|
13
|
+
const name = computerName.trim().replace(/\.local$/i, "");
|
|
14
|
+
// Unknown/random/container hosts are not reliable names for a person's computer.
|
|
15
|
+
const reliable = [...name].length <= 48 && /[a-z\p{Script=Han}]/iu.test(name)
|
|
16
|
+
&& !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\/\\@]/u.test(name)
|
|
17
|
+
&& !/^(?:[a-f\d-]{12,}|ip-\d|localhost$|runner|docker|container|sandbox|codespace|pod-)/i.test(name);
|
|
18
|
+
return { clientName, deviceName: reliable ? `${clientName} · ${name}` : clientName };
|
|
19
|
+
}
|
|
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
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function onboardingMessage(newlyConnected, state, scope) {
|
|
38
|
+
const action = newlyConnected.length === 0 ? "none" : state === "empty" ? "introduce" : "ask_once";
|
|
39
|
+
const message = action === "none" ? connectionCopy.repeated
|
|
40
|
+
: scope === "read_only" ? action === "ask_once" ? connectionCopy.read_only_existing : connectionCopy.read_only
|
|
41
|
+
: action === "introduce" ? connectionCopy.first_empty : connectionCopy.first_existing;
|
|
42
|
+
return {
|
|
43
|
+
account_state: state,
|
|
44
|
+
onboarding: { action, newly_connected_platforms: newlyConnected },
|
|
45
|
+
user_message: message,
|
|
46
|
+
skill_activation: newlyConnected.map(platform => ({ platform, message: platform === "codex" || platform === "claude-code" ? "新对话可以加载本次安装的技能。" : "如果看不到技能,请开新对话或重启应用;本次没有自动重启。" })),
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { asRecord, CliError } from "./errors.js";
|
|
2
|
+
import { formatMissingSections } from "./missing-sections.js";
|
|
3
|
+
import { projectedWriteCompleted } from "./write-permissions.js";
|
|
4
|
+
import { formatSearchWarnings } from "./query-search.js";
|
|
5
|
+
import { formatDailyNotice } from "./query-daily.js";
|
|
6
|
+
import { calendarStartNotice } from "./habit-wire.js";
|
|
7
|
+
import { createDraftNotice } from "./create-bundle.js";
|
|
8
|
+
const sensitiveKey = /^(token|access_token|token_value|token_hash|device_code|upload_nonce|upload_url|required_headers|authorization|cookie|set-cookie|x-opc-upload-token)$/i;
|
|
9
|
+
export function redact(value, secrets, depth = 0) {
|
|
10
|
+
if (depth > 128)
|
|
11
|
+
throw new CliError("invalid_response", "响应层级超出可安全输出范围。", "请缩小读取范围后核对。", 7);
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
let safe = value;
|
|
14
|
+
for (const secret of secrets)
|
|
15
|
+
if (secret.length >= 8)
|
|
16
|
+
safe = safe.split(secret).join("[REDACTED]");
|
|
17
|
+
return safe;
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(value))
|
|
20
|
+
return value.map((item) => redact(item, secrets, depth + 1));
|
|
21
|
+
if (value !== null && typeof value === "object")
|
|
22
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !sensitiveKey.test(key) && !secrets.some((secret) => secret.length >= 8 && key.includes(secret))).map(([key, item]) => [key, redact(item, secrets, depth + 1)]));
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
export function renderResult(result, human, secrets) {
|
|
26
|
+
const data = redact({ http_status: result.status, data: result.body, ...(result.operationAudit?.length ? { operation_audit: result.operationAudit } : {}) }, secrets);
|
|
27
|
+
const missing = human ? formatMissingSections(asRecord(data)?.data) : "";
|
|
28
|
+
const writeNotice = human && projectedWriteCompleted(result) ? "写入已完成;以下内容因读取权限未开启而没有返回,不是写入失败。\n" : "";
|
|
29
|
+
const body = asRecord(result.body);
|
|
30
|
+
const unknownWrite = explicitUnknownWrite(result);
|
|
31
|
+
const pendingNotice = human && result.status === 202
|
|
32
|
+
? body?.original_receipt === true && body.idempotency_replayed === true ? "这是原回执,当前状态请到网页核对;本次未重新执行。\n" : unknownWrite ? "写入结果暂无法确认,请先在网页核对;不要自动重试。\n" : "请求仍待处理,请在网页核对。\n" : "";
|
|
33
|
+
const warningNotice = human ? formatSearchWarnings(asRecord(data)?.data) : "";
|
|
34
|
+
const dailyNotice = human ? formatDailyNotice(asRecord(data)?.data) : "";
|
|
35
|
+
const draftNotice = human && [200, 201].includes(result.status) ? createDraftNotice(asRecord(data)?.data) : "";
|
|
36
|
+
const startNotice = human ? calendarStartNotice(asRecord(data)?.data) : "";
|
|
37
|
+
const replayNotice = human && [200, 201].includes(result.status) && body?.idempotency_replayed === true ? "这是已有创建请求的回放结果,本次没有重新创建。\n" : "";
|
|
38
|
+
return `${pendingNotice}${replayNotice}${writeNotice}${startNotice ? startNotice + "\n" : ""}${missing}${warningNotice}${dailyNotice}${draftNotice}${JSON.stringify(data, null, human ? 2 : undefined)}\n`;
|
|
39
|
+
}
|
|
40
|
+
// Only coherent transport/receipt evidence classifies the submitted write;
|
|
41
|
+
// neither a drafted audit log nor a moderation-pending 202 means unknown.
|
|
42
|
+
export function explicitUnknownWrite(result) {
|
|
43
|
+
const b = asRecord(result.body);
|
|
44
|
+
if (result.status !== 202 || !b)
|
|
45
|
+
return false;
|
|
46
|
+
return (b.code === undefined || b.code === "write_outcome_unknown")
|
|
47
|
+
&& (b.error === undefined || b.error === "write_outcome_unknown")
|
|
48
|
+
&& (b.write_status === undefined || b.write_status === "unknown")
|
|
49
|
+
&& (b.code === "write_outcome_unknown" || b.error === "write_outcome_unknown" || b.write_status === "unknown");
|
|
50
|
+
}
|
|
51
|
+
export function renderError(error, human, secrets) {
|
|
52
|
+
const data = redact(error.toJSON(), secrets);
|
|
53
|
+
return `${human ? formatMissingSections(data) : ""}${JSON.stringify(data, null, human ? 2 : undefined)}\n`;
|
|
54
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { asRecord, CliError } from "./errors.js";
|
|
3
|
+
import { expectCapabilityResult } from "./capability-result.js";
|
|
4
|
+
import { validateInput } from "./input-validation.js";
|
|
5
|
+
const uuid = (value) => typeof value === "string" && /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(value);
|
|
6
|
+
const invalid = () => new CliError("invalid_arguments", "待确认申请参数无效。", "使用 --input 提供目标类型knowledge_card或project、目标UUID和reason;改可见性另填public或private,不提供确认、权限或幂等参数。");
|
|
7
|
+
/** Mirrors the approved MCP pending envelope without importing MCP runtime dependencies. */
|
|
8
|
+
export function createPendingRequest(operation, value) {
|
|
9
|
+
if (!["delete", "archive", "visibility"].includes(operation))
|
|
10
|
+
throw invalid();
|
|
11
|
+
const input = validateInput(value, {
|
|
12
|
+
fields: {
|
|
13
|
+
target_type: value => typeof value === "string" && ["knowledge_card", "project"].includes(value),
|
|
14
|
+
target_id: uuid,
|
|
15
|
+
reason: value => typeof value === "string" && value.trim().length > 0 && [...value.trim()].length <= 1000,
|
|
16
|
+
expires_in_hours: value => Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 168,
|
|
17
|
+
...(operation === "visibility" ? { visibility: (value) => typeof value === "string" && ["public", "private"].includes(value) } : {}),
|
|
18
|
+
},
|
|
19
|
+
required: ["target_type", "target_id", "reason", ...(operation === "visibility" ? ["visibility"] : [])],
|
|
20
|
+
}, ["request", operation]);
|
|
21
|
+
const visibility = operation === "visibility";
|
|
22
|
+
const label = input.target_type === "project" ? "项目" : "知识卡";
|
|
23
|
+
const shortID = input.target_id.slice(0, 8);
|
|
24
|
+
const summary = visibility ? `待确认可见性变更:${label} (${shortID}) -> ${input.visibility}` : `待确认${operation === "archive" ? "归档" : "删除"}:${label} (${shortID})`;
|
|
25
|
+
return {
|
|
26
|
+
capability: "delete_or_archive",
|
|
27
|
+
tool_name: visibility ? "request_public_visibility_change" : "request_delete_or_archive",
|
|
28
|
+
action_type: visibility ? "public_visibility_change" : "delete_or_archive",
|
|
29
|
+
source_type: "manual", source_id: input.target_id, summary,
|
|
30
|
+
payload_json: { target_type: input.target_type, target_id: input.target_id, ...(visibility ? { visibility: input.visibility } : { operation }), reason: input.reason },
|
|
31
|
+
...(input.expires_in_hours === undefined ? {} : { expires_in_hours: input.expires_in_hours }),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function requestPendingAction(operation, input, ctx) {
|
|
35
|
+
const request = createPendingRequest(operation, input);
|
|
36
|
+
const result = expectCapabilityResult(await ctx.api.request("/api/v1/agent/pending-actions", { method: "POST", token: ctx.token, body: request }), "delete_or_archive", "write");
|
|
37
|
+
const body = asRecord(result.body), pending = asRecord(body?.pending_action), log = asRecord(body?.action_log);
|
|
38
|
+
if (result.status !== 202 || body?.requires_confirmation !== true || !uuid(body.pending_action_id)
|
|
39
|
+
|| pending?.id !== body.pending_action_id || pending.status !== "pending" || pending.action_type !== request.action_type
|
|
40
|
+
|| !isDeepStrictEqual(pending.payload_json, request.payload_json)
|
|
41
|
+
|| log?.action_status !== "drafted" || log.tool_name !== request.tool_name) {
|
|
42
|
+
throw new CliError("pending_result_unknown", "无法核实待确认申请的创建结果。", "先到网页核对待确认记录;不要自动重试或重复发起申请。", 6, { http_status: result.status });
|
|
43
|
+
}
|
|
44
|
+
return { ...result, body: { ...body, message: "申请已创建,等你在网页确认。" } };
|
|
45
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, lstat, readdir, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { asRecord, CliError } from "./errors.js";
|
|
5
|
+
import { PLATFORM_IDS, platformSupported } from "./local-types.js";
|
|
6
|
+
const unsafe = () => new CliError("platform_path_unsafe", "平台目录未通过安全检查。", "请使用用户目录内的绝对路径,并检查目录和符号链接;不要提供目录中的配置内容。");
|
|
7
|
+
const metadataError = (error) => {
|
|
8
|
+
const code = asRecord(error)?.code;
|
|
9
|
+
return code === "EACCES" || code === "EPERM"
|
|
10
|
+
? new CliError("platform_permission_denied", "没有权限检查平台程序目录。", "请由本人检查程序目录的访问权限,不要自动提权或修改权限。", 4)
|
|
11
|
+
: code === "ELOOP" ? unsafe()
|
|
12
|
+
: new CliError("platform_detection_failed", "无法完成平台程序检查。", "请检查本地程序目录后重新执行只读检查,不要启动客户端或输出配置内容。");
|
|
13
|
+
};
|
|
14
|
+
const missing = (error) => ["ENOENT", "ENOTDIR"].includes(String(asRecord(error)?.code));
|
|
15
|
+
const absolute = (path) => typeof path === "string" && isAbsolute(path) && !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(path);
|
|
16
|
+
const within = (home, path) => {
|
|
17
|
+
const part = relative(home, path);
|
|
18
|
+
return part !== ".." && !part.startsWith(`..${sep}`) && !isAbsolute(part);
|
|
19
|
+
};
|
|
20
|
+
async function executable(path) {
|
|
21
|
+
try {
|
|
22
|
+
const info = await stat(path);
|
|
23
|
+
if (!info.isFile() || (info.mode & 0o111) === 0)
|
|
24
|
+
return false;
|
|
25
|
+
try {
|
|
26
|
+
await access(path, constants.X_OK);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (["EACCES", "EPERM", "ENOENT", "ENOTDIR"].includes(String(asRecord(error)?.code)))
|
|
30
|
+
return false;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (missing(error))
|
|
37
|
+
return false;
|
|
38
|
+
throw metadataError(error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function directories(path) {
|
|
42
|
+
try {
|
|
43
|
+
return (await readdir(path, { withFileTypes: true }))
|
|
44
|
+
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
|
45
|
+
.map((entry) => entry.name).sort();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (missing(error))
|
|
49
|
+
return [];
|
|
50
|
+
throw metadataError(error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function skillDirectory(home, canonicalHome, root) {
|
|
54
|
+
if (!absolute(root))
|
|
55
|
+
throw unsafe();
|
|
56
|
+
const target = resolve(root, "skills");
|
|
57
|
+
if (!within(home, resolve(root)) || !within(home, target))
|
|
58
|
+
throw unsafe();
|
|
59
|
+
// Check existing ancestry as metadata only, including symlink escapes before a missing leaf.
|
|
60
|
+
let current = home;
|
|
61
|
+
for (const component of relative(home, target).split(sep).filter(Boolean)) {
|
|
62
|
+
current = join(current, component);
|
|
63
|
+
try {
|
|
64
|
+
const info = await lstat(current);
|
|
65
|
+
if (info.isSymbolicLink()) {
|
|
66
|
+
let resolved;
|
|
67
|
+
try {
|
|
68
|
+
resolved = await realpath(current);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (missing(error))
|
|
72
|
+
throw unsafe();
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
if (!within(canonicalHome, resolved) || !(await stat(current)).isDirectory())
|
|
76
|
+
throw unsafe();
|
|
77
|
+
}
|
|
78
|
+
else if (!info.isDirectory())
|
|
79
|
+
throw unsafe();
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (asRecord(error)?.code === "ENOENT")
|
|
83
|
+
break;
|
|
84
|
+
if (error instanceof CliError)
|
|
85
|
+
throw error;
|
|
86
|
+
throw metadataError(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return target;
|
|
90
|
+
}
|
|
91
|
+
function configuredSkillRoot(home, env, id) {
|
|
92
|
+
switch (id) {
|
|
93
|
+
case "claude-code": return env.CLAUDE_CONFIG_DIR ?? join(home, ".claude");
|
|
94
|
+
case "codex": return join(home, ".agents");
|
|
95
|
+
case "openclaw": return env.OPENCLAW_STATE_DIR ?? (env.OPENCLAW_HOME === undefined ? join(home, ".openclaw") : (() => {
|
|
96
|
+
if (!absolute(env.OPENCLAW_HOME) || !within(home, resolve(env.OPENCLAW_HOME)))
|
|
97
|
+
throw unsafe();
|
|
98
|
+
return join(env.OPENCLAW_HOME, ".openclaw");
|
|
99
|
+
})());
|
|
100
|
+
case "kimi-code": return env.KIMI_CODE_HOME ?? join(home, ".kimi-code");
|
|
101
|
+
case "kimi-work": return join(home, "Library/Application Support/kimi-desktop/daimon-share/daimon");
|
|
102
|
+
case "workbuddy": return join(home, ".workbuddy");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Bounded skill-location discovery only. No PATH, application bundle, client
|
|
106
|
+
// executable, configuration contents or arbitrary HOME traversal is involved.
|
|
107
|
+
export async function knownSkillTargets(options) {
|
|
108
|
+
if (!absolute(options.homeDir))
|
|
109
|
+
throw unsafe();
|
|
110
|
+
const home = resolve(options.homeDir);
|
|
111
|
+
let canonicalHome;
|
|
112
|
+
try {
|
|
113
|
+
canonicalHome = await realpath(home);
|
|
114
|
+
if (!(await stat(home)).isDirectory())
|
|
115
|
+
throw unsafe();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof CliError)
|
|
119
|
+
throw error;
|
|
120
|
+
throw metadataError(error);
|
|
121
|
+
}
|
|
122
|
+
const targets = [];
|
|
123
|
+
for (const id of PLATFORM_IDS)
|
|
124
|
+
if (platformSupported(id, options.operatingSystem)) {
|
|
125
|
+
try {
|
|
126
|
+
targets.push({ id, detected: false, evidence: "none", skillDirectory: await skillDirectory(home, canonicalHome, configuredSkillRoot(home, options.env, id)) });
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
// Only optional, bounded discovery isolates a candidate. Explicit or
|
|
130
|
+
// recorded targets are checked by their original strict path instead.
|
|
131
|
+
if (!options.skipInvalidCandidates)
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return targets;
|
|
136
|
+
}
|
|
137
|
+
async function claudeDesktop(home) {
|
|
138
|
+
const root = join(home, "Library", "Application Support", "Claude", "claude-code");
|
|
139
|
+
for (const version of await directories(root)) {
|
|
140
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version))
|
|
141
|
+
continue;
|
|
142
|
+
for (const hash of await directories(join(root, version))) {
|
|
143
|
+
if (!/^[a-f0-9]{6,64}$/i.test(hash))
|
|
144
|
+
continue;
|
|
145
|
+
if (await executable(join(root, version, hash, "claude.app", "Contents", "MacOS", "claude")))
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
export async function detectPlatforms(options) {
|
|
152
|
+
if (!absolute(options.homeDir))
|
|
153
|
+
throw unsafe();
|
|
154
|
+
const home = resolve(options.homeDir);
|
|
155
|
+
let canonicalHome;
|
|
156
|
+
try {
|
|
157
|
+
canonicalHome = await realpath(home);
|
|
158
|
+
if (!(await stat(home)).isDirectory())
|
|
159
|
+
throw unsafe();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (error instanceof CliError)
|
|
163
|
+
throw error;
|
|
164
|
+
throw metadataError(error);
|
|
165
|
+
}
|
|
166
|
+
const { env } = options;
|
|
167
|
+
const roots = options.applicationRoots ?? ["/Applications", join(home, "Applications")];
|
|
168
|
+
if (!roots.every(absolute))
|
|
169
|
+
throw unsafe();
|
|
170
|
+
const selected = (id) => options.platformIds === undefined || options.platformIds.includes(id);
|
|
171
|
+
const targets = [];
|
|
172
|
+
const candidates = ["claude-code", "codex", "openclaw"].map(id => ({ id, root: () => configuredSkillRoot(home, env, id) }));
|
|
173
|
+
for (const candidate of candidates) {
|
|
174
|
+
if (selected(candidate.id))
|
|
175
|
+
targets.push({ id: candidate.id, detected: false, evidence: "none", skillDirectory: await skillDirectory(home, canonicalHome, candidate.root()) });
|
|
176
|
+
}
|
|
177
|
+
const pathRoots = (env.PATH ?? "").split(delimiter).filter(absolute);
|
|
178
|
+
for (const target of targets) {
|
|
179
|
+
const command = target.id === "claude-code" ? "claude" : target.id;
|
|
180
|
+
for (const root of pathRoots) {
|
|
181
|
+
if (await executable(join(root, command))) {
|
|
182
|
+
target.detected = true;
|
|
183
|
+
target.evidence = "executable";
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (target.detected)
|
|
188
|
+
continue;
|
|
189
|
+
if (target.id === "claude-code") {
|
|
190
|
+
target.detected = await claudeDesktop(home);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const app = target.id === "codex" ? "Codex" : "OpenClaw";
|
|
194
|
+
for (const root of roots) {
|
|
195
|
+
if (await executable(join(root, `${app}.app`, "Contents", "MacOS", app))
|
|
196
|
+
|| (target.id === "codex" && await executable(join(root, "ChatGPT.app", "Contents", "Resources", "codex-cli", "CodexCLI.app", "Contents", "MacOS", "codex")))) {
|
|
197
|
+
target.detected = true;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (target.detected)
|
|
203
|
+
target.evidence = "desktop";
|
|
204
|
+
}
|
|
205
|
+
if ((options.operatingSystem ?? process.platform) === "darwin") {
|
|
206
|
+
// Do not inspect an unselected new platform's personal paths. Without a
|
|
207
|
+
// selector, only a verified program makes its skill root relevant.
|
|
208
|
+
for (const [id, app, binary, rootPath, command] of [
|
|
209
|
+
["kimi-code", "Kimi Code", "Kimi Code", configuredSkillRoot(home, env, "kimi-code"), "kimi"],
|
|
210
|
+
["kimi-work", "Kimi", "Kimi", configuredSkillRoot(home, env, "kimi-work"), null],
|
|
211
|
+
["workbuddy", "WorkBuddy", "Electron", configuredSkillRoot(home, env, "workbuddy"), null],
|
|
212
|
+
]) {
|
|
213
|
+
if (!selected(id))
|
|
214
|
+
continue;
|
|
215
|
+
let detected = false;
|
|
216
|
+
let evidence = "none";
|
|
217
|
+
if (command)
|
|
218
|
+
for (const root of pathRoots) {
|
|
219
|
+
if (await executable(join(root, command))) {
|
|
220
|
+
detected = true;
|
|
221
|
+
evidence = "executable";
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!detected)
|
|
226
|
+
for (const root of roots) {
|
|
227
|
+
if (await executable(join(root, `${app}.app`, "Contents", "MacOS", binary))) {
|
|
228
|
+
detected = true;
|
|
229
|
+
evidence = "desktop";
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!detected && options.platformIds === undefined)
|
|
234
|
+
continue;
|
|
235
|
+
targets.push({ id, detected, evidence, skillDirectory: await skillDirectory(home, canonicalHome, rootPath) });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return targets;
|
|
239
|
+
}
|