u1s1-cli 1.6.0 → 1.7.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/dist/import/index.js +7 -1
- package/dist/import/skills.d.ts +89 -0
- package/dist/import/skills.js +492 -0
- package/dist/import/util.d.ts +1 -1
- package/dist/import/util.js +1 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/import/index.js
CHANGED
|
@@ -126,7 +126,7 @@ function parseFlags(args) {
|
|
|
126
126
|
}
|
|
127
127
|
function printHelp() {
|
|
128
128
|
console.log("");
|
|
129
|
-
console.log(" 把 Claude Code / Codex
|
|
129
|
+
console.log(" 把 Claude Code / Codex 的历史对话和技能导入 u1s1");
|
|
130
130
|
console.log("");
|
|
131
131
|
console.log(" 用法:");
|
|
132
132
|
console.log(" u1s1 import 导入当前目录的对话");
|
|
@@ -136,6 +136,7 @@ function printHelp() {
|
|
|
136
136
|
console.log(" u1s1 import --cwd 目录 指定项目目录(默认当前目录)");
|
|
137
137
|
console.log(" u1s1 import --dry-run 只看会导哪些,不写盘");
|
|
138
138
|
console.log(" u1s1 import --force 已经导过的也再导一遍");
|
|
139
|
+
console.log(" u1s1 import skills 导入 Claude Code / Codex 等工具的技能(SKILL.md),详见 u1s1 import skills -h");
|
|
139
140
|
console.log("");
|
|
140
141
|
console.log(" 导入后在对应项目里跑 u1s1,输入 /resume 就能看到。");
|
|
141
142
|
console.log("");
|
|
@@ -255,6 +256,11 @@ function printImportSummary(summary) {
|
|
|
255
256
|
console.log("");
|
|
256
257
|
}
|
|
257
258
|
export async function importCommand(args) {
|
|
259
|
+
if (args[0] === "skills" || args[0] === "skill") {
|
|
260
|
+
const { importSkillsCommand } = await import("./skills.js");
|
|
261
|
+
await importSkillsCommand(args.slice(1));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
258
264
|
if (args.includes("-h") || args.includes("--help")) {
|
|
259
265
|
printHelp();
|
|
260
266
|
return;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `u1s1 import skills`: 把 Claude Code / Codex(以及任意 --from 目录)里的
|
|
3
|
+
* Agent Skills(SKILL.md)复制进 ~/.u1s1/agent/skills。pi 用同一套 SKILL.md
|
|
4
|
+
* 规范,所以只需要按 pi 自己的加载器校验一遍,合规的原样复制,不合规的列出来跳过。
|
|
5
|
+
*/
|
|
6
|
+
export type SkillTool = "claude" | "codex" | "custom";
|
|
7
|
+
export interface SkillSource {
|
|
8
|
+
tool: SkillTool;
|
|
9
|
+
label: string;
|
|
10
|
+
dir: string;
|
|
11
|
+
scope: "user" | "project" | "custom";
|
|
12
|
+
}
|
|
13
|
+
export interface DiscoveredSkill {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
tool: SkillTool;
|
|
17
|
+
label: string;
|
|
18
|
+
/** SKILL.md 所在目录;单文件技能时为该 .md 文件所在目录。 */
|
|
19
|
+
baseDir: string;
|
|
20
|
+
skillFile: string;
|
|
21
|
+
/** 单文件技能(skills 根目录直接放 foo.md)只复制那一个文件。 */
|
|
22
|
+
singleFile: boolean;
|
|
23
|
+
files: number;
|
|
24
|
+
bytes: number;
|
|
25
|
+
hints: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface SkillProblem {
|
|
28
|
+
path: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
export interface SkillImportRecord {
|
|
32
|
+
name: string;
|
|
33
|
+
tool: SkillTool;
|
|
34
|
+
sourcePath: string;
|
|
35
|
+
destPath: string;
|
|
36
|
+
importedAt: string;
|
|
37
|
+
}
|
|
38
|
+
export interface SkillImportIndex {
|
|
39
|
+
version: 1;
|
|
40
|
+
items: Record<string, SkillImportRecord>;
|
|
41
|
+
}
|
|
42
|
+
export interface SkillImportPlan {
|
|
43
|
+
pending: DiscoveredSkill[];
|
|
44
|
+
/** 已由本命令导过、这次不重复导(--force 可重导)。 */
|
|
45
|
+
alreadyImported: DiscoveredSkill[];
|
|
46
|
+
/** 目标目录已有同名技能但不是本命令导入的,不覆盖。 */
|
|
47
|
+
conflicts: DiscoveredSkill[];
|
|
48
|
+
/** 多个来源同名,只保留先发现的那个。 */
|
|
49
|
+
duplicates: DiscoveredSkill[];
|
|
50
|
+
}
|
|
51
|
+
export interface SkillImportResultItem {
|
|
52
|
+
name: string;
|
|
53
|
+
status: "imported" | "error";
|
|
54
|
+
destPath?: string;
|
|
55
|
+
detail?: string;
|
|
56
|
+
}
|
|
57
|
+
/** 超过这个体积的技能目录多半塞了数据集或依赖,先不动。 */
|
|
58
|
+
export declare const MAX_SKILL_BYTES: number;
|
|
59
|
+
export declare function skillsDestDir(): string;
|
|
60
|
+
export declare function skillIndexPath(): string;
|
|
61
|
+
/** Claude Code / Codex 各自约定的技能目录;项目级排在用户级前面,先发现的同名技能优先。 */
|
|
62
|
+
export declare function defaultSkillSources(cwd: string, home?: string): SkillSource[];
|
|
63
|
+
export declare function customSkillSource(dir: string): SkillSource;
|
|
64
|
+
/**
|
|
65
|
+
* 用 pi 自己的加载器扫一遍来源目录:能被 pi 认出来、且没有任何告警(名称/说明不合规)
|
|
66
|
+
* 的才算兼容,其他的进 problems 并带上 pi 给的原因。
|
|
67
|
+
*/
|
|
68
|
+
export declare function discoverSkills(sources: SkillSource[]): {
|
|
69
|
+
skills: DiscoveredSkill[];
|
|
70
|
+
problems: SkillProblem[];
|
|
71
|
+
};
|
|
72
|
+
export declare function loadSkillIndex(path?: string): SkillImportIndex;
|
|
73
|
+
export declare function planSkillImport(skills: DiscoveredSkill[], opts: {
|
|
74
|
+
destDir: string;
|
|
75
|
+
index: SkillImportIndex;
|
|
76
|
+
force: boolean;
|
|
77
|
+
}): SkillImportPlan;
|
|
78
|
+
/** 复制到 <destDir>/<name>;单文件技能落成 <name>/SKILL.md,这样 --remove 只需删一个目录。 */
|
|
79
|
+
export declare function copySkill(skill: DiscoveredSkill, destDir: string): string;
|
|
80
|
+
export declare function runSkillImport(pending: DiscoveredSkill[], index: SkillImportIndex, destDir: string): SkillImportResultItem[];
|
|
81
|
+
/** 只删本命令导入过的技能,别人手放进去的同名目录不碰。 */
|
|
82
|
+
export declare function removeImportedSkill(name: string, index: SkillImportIndex, destDir: string): {
|
|
83
|
+
ok: true;
|
|
84
|
+
} | {
|
|
85
|
+
ok: false;
|
|
86
|
+
reason: string;
|
|
87
|
+
};
|
|
88
|
+
export declare function printSkillsHelp(): void;
|
|
89
|
+
export declare function importSkillsCommand(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { basename, join, resolve } from "node:path";
|
|
6
|
+
import { loadSkillsFromDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { formatHomePath } from "../brand.js";
|
|
8
|
+
import { agentDir } from "../config.js";
|
|
9
|
+
import { asRecord, asString, formatBytes, listHomeClaudeDirs, oneLine, uniqueExistingDirs } from "./util.js";
|
|
10
|
+
import { readJsonIfExists, writeJson } from "./write.js";
|
|
11
|
+
const TOOL_LABEL = {
|
|
12
|
+
claude: "Claude Code",
|
|
13
|
+
codex: "Codex",
|
|
14
|
+
custom: "自定义目录",
|
|
15
|
+
};
|
|
16
|
+
const SCRIPT_EXTENSIONS = new Set([".sh", ".bash", ".zsh", ".ps1", ".bat", ".cmd", ".py", ".js", ".mjs", ".cjs", ".ts", ".rb", ".pl"]);
|
|
17
|
+
/** 超过这个体积的技能目录多半塞了数据集或依赖,先不动。 */
|
|
18
|
+
export const MAX_SKILL_BYTES = 20 * 1024 * 1024;
|
|
19
|
+
const MAX_SKILL_FILES = 2000;
|
|
20
|
+
const LARGE_SKILL_BYTES = 1024 * 1024;
|
|
21
|
+
export function skillsDestDir() {
|
|
22
|
+
return join(agentDir, "skills");
|
|
23
|
+
}
|
|
24
|
+
export function skillIndexPath() {
|
|
25
|
+
return join(agentDir, "imported-skills.json");
|
|
26
|
+
}
|
|
27
|
+
/** Claude Code / Codex 各自约定的技能目录;项目级排在用户级前面,先发现的同名技能优先。 */
|
|
28
|
+
export function defaultSkillSources(cwd, home = homedir()) {
|
|
29
|
+
const candidates = [
|
|
30
|
+
{ tool: "claude", scope: "project", dir: join(cwd, ".claude", "skills") },
|
|
31
|
+
{ tool: "codex", scope: "project", dir: join(cwd, ".agents", "skills") },
|
|
32
|
+
...listHomeClaudeDirs(home).map((dir) => ({ tool: "claude", scope: "user", dir: join(dir, "skills") })),
|
|
33
|
+
{ tool: "codex", scope: "user", dir: join(home, ".codex", "skills") },
|
|
34
|
+
{ tool: "codex", scope: "user", dir: join(home, ".agents", "skills") },
|
|
35
|
+
];
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const c of candidates) {
|
|
39
|
+
const [dir] = uniqueExistingDirs([c.dir]);
|
|
40
|
+
if (!dir || seen.has(dir))
|
|
41
|
+
continue;
|
|
42
|
+
seen.add(dir);
|
|
43
|
+
out.push({ tool: c.tool, label: TOOL_LABEL[c.tool], dir, scope: c.scope });
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
export function customSkillSource(dir) {
|
|
48
|
+
return { tool: "custom", label: TOOL_LABEL.custom, dir: resolve(dir), scope: "custom" };
|
|
49
|
+
}
|
|
50
|
+
function walkStats(dir, stats, depth = 0) {
|
|
51
|
+
if (stats.truncated || depth > 16)
|
|
52
|
+
return;
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (entry.name === "node_modules" || entry.name === ".git")
|
|
62
|
+
continue;
|
|
63
|
+
const full = join(dir, entry.name);
|
|
64
|
+
let st;
|
|
65
|
+
try {
|
|
66
|
+
st = statSync(full);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (st.isDirectory()) {
|
|
72
|
+
walkStats(full, stats, depth + 1);
|
|
73
|
+
}
|
|
74
|
+
else if (st.isFile()) {
|
|
75
|
+
stats.files += 1;
|
|
76
|
+
stats.bytes += st.size;
|
|
77
|
+
const dot = entry.name.lastIndexOf(".");
|
|
78
|
+
const ext = dot >= 0 ? entry.name.slice(dot).toLowerCase() : "";
|
|
79
|
+
const executable = process.platform !== "win32" && (st.mode & 0o111) !== 0;
|
|
80
|
+
if (SCRIPT_EXTENSIONS.has(ext) || executable)
|
|
81
|
+
stats.scripts += 1;
|
|
82
|
+
}
|
|
83
|
+
if (stats.files > MAX_SKILL_FILES || stats.bytes > MAX_SKILL_BYTES) {
|
|
84
|
+
stats.truncated = true;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function skillHints(stats) {
|
|
90
|
+
const hints = [];
|
|
91
|
+
if (stats.scripts > 0)
|
|
92
|
+
hints.push(`含 ${stats.scripts} 个脚本,只导可信来源`);
|
|
93
|
+
if (stats.bytes > LARGE_SKILL_BYTES)
|
|
94
|
+
hints.push(`体积 ${formatBytes(stats.bytes)}`);
|
|
95
|
+
return hints;
|
|
96
|
+
}
|
|
97
|
+
const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
98
|
+
/** 单文件技能(skills 根目录直接放 foo.md)没写 name 时 pi 会拿父目录名,导入后按文件名落目录更合理。 */
|
|
99
|
+
function singleFileSkillName(filePath, fallback) {
|
|
100
|
+
try {
|
|
101
|
+
const { frontmatter } = parseFrontmatter(readFileSync(filePath, "utf8"));
|
|
102
|
+
const declared = frontmatter["name"];
|
|
103
|
+
if (typeof declared === "string" && declared.trim())
|
|
104
|
+
return declared.trim();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return fallback;
|
|
108
|
+
}
|
|
109
|
+
return basename(filePath).replace(/\.md$/i, "");
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 用 pi 自己的加载器扫一遍来源目录:能被 pi 认出来、且没有任何告警(名称/说明不合规)
|
|
113
|
+
* 的才算兼容,其他的进 problems 并带上 pi 给的原因。
|
|
114
|
+
*/
|
|
115
|
+
export function discoverSkills(sources) {
|
|
116
|
+
const skills = [];
|
|
117
|
+
const problems = [];
|
|
118
|
+
for (const source of sources) {
|
|
119
|
+
let result;
|
|
120
|
+
try {
|
|
121
|
+
result = loadSkillsFromDir({ dir: source.dir, source: source.label });
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
problems.push({ path: source.dir, reason: e instanceof Error ? e.message : String(e) });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const reasons = new Map();
|
|
128
|
+
for (const diag of result.diagnostics) {
|
|
129
|
+
if (diag.type === "collision")
|
|
130
|
+
continue;
|
|
131
|
+
const key = diag.path ?? source.dir;
|
|
132
|
+
reasons.set(key, [...(reasons.get(key) ?? []), diag.message]);
|
|
133
|
+
}
|
|
134
|
+
for (const [path, list] of reasons)
|
|
135
|
+
problems.push({ path, reason: list.join("; ") });
|
|
136
|
+
for (const skill of result.skills) {
|
|
137
|
+
if (reasons.has(skill.filePath))
|
|
138
|
+
continue;
|
|
139
|
+
const singleFile = basename(skill.filePath) !== "SKILL.md";
|
|
140
|
+
const name = singleFile ? singleFileSkillName(skill.filePath, skill.name) : skill.name;
|
|
141
|
+
if (!SKILL_NAME_RE.test(name) || name.length > 64) {
|
|
142
|
+
problems.push({ path: skill.filePath, reason: `技能名「${name}」不合规:只能用小写字母、数字和连字符` });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const stats = { files: 0, bytes: 0, scripts: 0, truncated: false };
|
|
146
|
+
if (singleFile) {
|
|
147
|
+
try {
|
|
148
|
+
stats.files = 1;
|
|
149
|
+
stats.bytes = statSync(skill.filePath).size;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
stats.files = 0;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
walkStats(skill.baseDir, stats);
|
|
157
|
+
}
|
|
158
|
+
if (stats.truncated) {
|
|
159
|
+
problems.push({
|
|
160
|
+
path: singleFile ? skill.filePath : skill.baseDir,
|
|
161
|
+
reason: `技能目录太大(超过 ${formatBytes(MAX_SKILL_BYTES)} 或 ${MAX_SKILL_FILES} 个文件),先跳过`,
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
skills.push({
|
|
166
|
+
name,
|
|
167
|
+
description: skill.description,
|
|
168
|
+
tool: source.tool,
|
|
169
|
+
label: source.label,
|
|
170
|
+
baseDir: skill.baseDir,
|
|
171
|
+
skillFile: skill.filePath,
|
|
172
|
+
singleFile,
|
|
173
|
+
files: stats.files,
|
|
174
|
+
bytes: stats.bytes,
|
|
175
|
+
hints: skillHints(stats),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { skills, problems };
|
|
180
|
+
}
|
|
181
|
+
export function loadSkillIndex(path = skillIndexPath()) {
|
|
182
|
+
const rec = asRecord(readJsonIfExists(path));
|
|
183
|
+
const items = asRecord(rec?.["items"]);
|
|
184
|
+
const out = { version: 1, items: {} };
|
|
185
|
+
if (!items)
|
|
186
|
+
return out;
|
|
187
|
+
for (const [key, value] of Object.entries(items)) {
|
|
188
|
+
const item = asRecord(value);
|
|
189
|
+
if (!item)
|
|
190
|
+
continue;
|
|
191
|
+
const name = asString(item["name"]);
|
|
192
|
+
const tool = asString(item["tool"]);
|
|
193
|
+
const sourcePath = asString(item["sourcePath"]);
|
|
194
|
+
const destPath = asString(item["destPath"]);
|
|
195
|
+
const importedAt = asString(item["importedAt"]);
|
|
196
|
+
if (!name || !sourcePath || !destPath || !importedAt)
|
|
197
|
+
continue;
|
|
198
|
+
if (tool !== "claude" && tool !== "codex" && tool !== "custom")
|
|
199
|
+
continue;
|
|
200
|
+
out.items[key] = { name, tool, sourcePath, destPath, importedAt };
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
export function planSkillImport(skills, opts) {
|
|
205
|
+
const plan = { pending: [], alreadyImported: [], conflicts: [], duplicates: [] };
|
|
206
|
+
const seen = new Set();
|
|
207
|
+
for (const skill of skills) {
|
|
208
|
+
if (seen.has(skill.name)) {
|
|
209
|
+
plan.duplicates.push(skill);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
seen.add(skill.name);
|
|
213
|
+
const dest = join(opts.destDir, skill.name);
|
|
214
|
+
const record = opts.index.items[skill.name];
|
|
215
|
+
const destExists = existsSync(dest);
|
|
216
|
+
if (record && destExists && !opts.force) {
|
|
217
|
+
plan.alreadyImported.push(skill);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (!record && destExists) {
|
|
221
|
+
plan.conflicts.push(skill);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
plan.pending.push(skill);
|
|
225
|
+
}
|
|
226
|
+
return plan;
|
|
227
|
+
}
|
|
228
|
+
/** 复制到 <destDir>/<name>;单文件技能落成 <name>/SKILL.md,这样 --remove 只需删一个目录。 */
|
|
229
|
+
export function copySkill(skill, destDir) {
|
|
230
|
+
const dest = join(destDir, skill.name);
|
|
231
|
+
if (resolve(skill.baseDir) === resolve(dest))
|
|
232
|
+
throw new Error("来源就是目标目录");
|
|
233
|
+
mkdirSync(destDir, { recursive: true });
|
|
234
|
+
rmSync(dest, { recursive: true, force: true });
|
|
235
|
+
if (skill.singleFile) {
|
|
236
|
+
mkdirSync(dest, { recursive: true });
|
|
237
|
+
cpSync(skill.skillFile, join(dest, "SKILL.md"));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
cpSync(skill.baseDir, dest, {
|
|
241
|
+
recursive: true,
|
|
242
|
+
dereference: true,
|
|
243
|
+
filter: (src) => {
|
|
244
|
+
const name = basename(src);
|
|
245
|
+
return name !== "node_modules" && name !== ".git";
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return dest;
|
|
250
|
+
}
|
|
251
|
+
export function runSkillImport(pending, index, destDir) {
|
|
252
|
+
const items = [];
|
|
253
|
+
for (const skill of pending) {
|
|
254
|
+
try {
|
|
255
|
+
const destPath = copySkill(skill, destDir);
|
|
256
|
+
index.items[skill.name] = {
|
|
257
|
+
name: skill.name,
|
|
258
|
+
tool: skill.tool,
|
|
259
|
+
sourcePath: skill.singleFile ? skill.skillFile : skill.baseDir,
|
|
260
|
+
destPath,
|
|
261
|
+
importedAt: new Date().toISOString(),
|
|
262
|
+
};
|
|
263
|
+
items.push({ name: skill.name, status: "imported", destPath });
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
items.push({ name: skill.name, status: "error", detail: e instanceof Error ? e.message : String(e) });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return items;
|
|
270
|
+
}
|
|
271
|
+
/** 只删本命令导入过的技能,别人手放进去的同名目录不碰。 */
|
|
272
|
+
export function removeImportedSkill(name, index, destDir) {
|
|
273
|
+
const record = index.items[name];
|
|
274
|
+
if (!record)
|
|
275
|
+
return { ok: false, reason: "不是 u1s1 import skills 导入的技能,请手动处理" };
|
|
276
|
+
const expected = join(destDir, name);
|
|
277
|
+
if (resolve(record.destPath) !== resolve(expected)) {
|
|
278
|
+
return { ok: false, reason: `记录的位置 ${record.destPath} 与技能目录不一致,请手动处理` };
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
if (existsSync(expected) && lstatSync(expected).isDirectory())
|
|
282
|
+
rmSync(expected, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
286
|
+
}
|
|
287
|
+
delete index.items[name];
|
|
288
|
+
return { ok: true };
|
|
289
|
+
}
|
|
290
|
+
function parseSkillsArgs(args) {
|
|
291
|
+
const opts = {
|
|
292
|
+
cwd: process.cwd(),
|
|
293
|
+
from: [],
|
|
294
|
+
tools: undefined,
|
|
295
|
+
dryRun: false,
|
|
296
|
+
force: false,
|
|
297
|
+
yes: false,
|
|
298
|
+
list: false,
|
|
299
|
+
remove: [],
|
|
300
|
+
};
|
|
301
|
+
const needValue = (flag, i) => {
|
|
302
|
+
const next = args[i + 1];
|
|
303
|
+
if (!next || next.startsWith("-")) {
|
|
304
|
+
console.error(` ${flag} 后面要跟一个值`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
return next;
|
|
308
|
+
};
|
|
309
|
+
for (let i = 0; i < args.length; i++) {
|
|
310
|
+
const a = args[i];
|
|
311
|
+
if (a === "--dry-run")
|
|
312
|
+
opts.dryRun = true;
|
|
313
|
+
else if (a === "--force" || a === "--update")
|
|
314
|
+
opts.force = true;
|
|
315
|
+
else if (a === "-y" || a === "--yes")
|
|
316
|
+
opts.yes = true;
|
|
317
|
+
else if (a === "--list")
|
|
318
|
+
opts.list = true;
|
|
319
|
+
else if (a === "--from")
|
|
320
|
+
opts.from.push(needValue(a, i++));
|
|
321
|
+
else if (a === "--cwd")
|
|
322
|
+
opts.cwd = needValue(a, i++);
|
|
323
|
+
else if (a === "--remove")
|
|
324
|
+
opts.remove.push(needValue(a, i++));
|
|
325
|
+
else if (a.startsWith("-")) {
|
|
326
|
+
console.error(` 不认识参数 ${a}`);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
const tool = parseToolArg(a);
|
|
331
|
+
opts.tools = opts.tools ? [...opts.tools, tool] : [tool];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return opts;
|
|
335
|
+
}
|
|
336
|
+
function parseToolArg(raw) {
|
|
337
|
+
const aliases = {
|
|
338
|
+
claude: "claude",
|
|
339
|
+
"claude-code": "claude",
|
|
340
|
+
cc: "claude",
|
|
341
|
+
anthropic: "claude",
|
|
342
|
+
codex: "codex",
|
|
343
|
+
openai: "codex",
|
|
344
|
+
};
|
|
345
|
+
const tool = aliases[raw.toLowerCase()];
|
|
346
|
+
if (!tool) {
|
|
347
|
+
console.error(` 不认识来源「${raw}」,可选: claude / codex;其他工具用 --from 目录`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
return tool;
|
|
351
|
+
}
|
|
352
|
+
export function printSkillsHelp() {
|
|
353
|
+
console.log("");
|
|
354
|
+
console.log(" 把 Claude Code / Codex 等工具的技能(SKILL.md)导入 u1s1");
|
|
355
|
+
console.log("");
|
|
356
|
+
console.log(" 用法:");
|
|
357
|
+
console.log(" u1s1 import skills 自动找本机 Claude Code / Codex 的技能");
|
|
358
|
+
console.log(" u1s1 import skills claude 只导 Claude Code 的");
|
|
359
|
+
console.log(" u1s1 import skills codex 只导 Codex 的");
|
|
360
|
+
console.log(" u1s1 import skills --from 目录 其他工具:指定放 SKILL.md 的目录(可重复)");
|
|
361
|
+
console.log(" u1s1 import skills --cwd 目录 项目级技能按这个目录找(默认当前目录)");
|
|
362
|
+
console.log(" u1s1 import skills --dry-run 只看会导哪些,不写盘");
|
|
363
|
+
console.log(" u1s1 import skills --update 已导过的重新复制一遍(同 --force)");
|
|
364
|
+
console.log(" u1s1 import skills --list 看已导入的技能");
|
|
365
|
+
console.log(" u1s1 import skills --remove 名称 删掉某个导入的技能");
|
|
366
|
+
console.log("");
|
|
367
|
+
console.log(` 技能会复制到 ${formatHomePath(skillsDestDir())},下次进对话就能用:`);
|
|
368
|
+
console.log(" 模型会按说明自动调用,也可以在会话里输入 /skill:名称 手动调用。");
|
|
369
|
+
console.log("");
|
|
370
|
+
}
|
|
371
|
+
async function confirm(question) {
|
|
372
|
+
if (!input.isTTY || !output.isTTY)
|
|
373
|
+
return true;
|
|
374
|
+
const rl = createInterface({ input, output });
|
|
375
|
+
try {
|
|
376
|
+
const ans = (await rl.question(question)).trim().toLowerCase();
|
|
377
|
+
return ans === "" || ans === "y" || ans === "yes" || ans === "是";
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
rl.close();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function describeSource(skill) {
|
|
384
|
+
return formatHomePath(skill.singleFile ? skill.skillFile : skill.baseDir);
|
|
385
|
+
}
|
|
386
|
+
function printList(index) {
|
|
387
|
+
const items = Object.values(index.items).sort((a, b) => a.name.localeCompare(b.name));
|
|
388
|
+
console.log("");
|
|
389
|
+
if (items.length === 0) {
|
|
390
|
+
console.log(" 还没导入过技能。跑 u1s1 import skills 试试。");
|
|
391
|
+
console.log("");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
console.log(` 已导入 ${items.length} 个技能(${formatHomePath(skillsDestDir())}):`);
|
|
395
|
+
for (const item of items) {
|
|
396
|
+
const missing = existsSync(item.destPath) ? "" : "(目录已不在)";
|
|
397
|
+
console.log(` · ${item.name} [${TOOL_LABEL[item.tool]}] ${missing}`);
|
|
398
|
+
console.log(` 来自 ${formatHomePath(item.sourcePath)}`);
|
|
399
|
+
}
|
|
400
|
+
console.log("");
|
|
401
|
+
}
|
|
402
|
+
function printPreview(skills, plan, problems, sources) {
|
|
403
|
+
console.log("");
|
|
404
|
+
console.log(` 在 ${sources.length} 个目录里找到 ${skills.length} 个技能:`);
|
|
405
|
+
for (const source of sources)
|
|
406
|
+
console.log(` ${formatHomePath(source.dir)} [${source.label}]`);
|
|
407
|
+
console.log("");
|
|
408
|
+
for (const skill of plan.pending) {
|
|
409
|
+
console.log(` + ${skill.name} [${skill.label}] ${oneLine(skill.description, 60)}`);
|
|
410
|
+
console.log(` ${describeSource(skill)}${skill.hints.length ? ` ⚠ ${skill.hints.join(";")}` : ""}`);
|
|
411
|
+
}
|
|
412
|
+
for (const skill of plan.alreadyImported)
|
|
413
|
+
console.log(` · ${skill.name} 已导过(加 --update 重导)`);
|
|
414
|
+
for (const skill of plan.conflicts) {
|
|
415
|
+
console.log(` · ${skill.name} 目标目录已有同名技能(不是本命令导的),不覆盖`);
|
|
416
|
+
}
|
|
417
|
+
for (const skill of plan.duplicates)
|
|
418
|
+
console.log(` · ${skill.name} 与前面同名,只保留先发现的(${describeSource(skill)})`);
|
|
419
|
+
for (const problem of problems) {
|
|
420
|
+
console.log(` × ${formatHomePath(problem.path)} 不兼容,跳过: ${oneLine(problem.reason, 80)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
export async function importSkillsCommand(args) {
|
|
424
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
425
|
+
printSkillsHelp();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const opts = parseSkillsArgs(args);
|
|
429
|
+
const destDir = skillsDestDir();
|
|
430
|
+
const index = loadSkillIndex();
|
|
431
|
+
if (opts.remove.length > 0) {
|
|
432
|
+
for (const name of opts.remove) {
|
|
433
|
+
const result = removeImportedSkill(name, index, destDir);
|
|
434
|
+
if (result.ok)
|
|
435
|
+
console.log(` ✓ 已删除技能 ${name}`);
|
|
436
|
+
else
|
|
437
|
+
console.log(` × ${name}: ${result.reason}`);
|
|
438
|
+
}
|
|
439
|
+
writeJson(skillIndexPath(), index);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (opts.list) {
|
|
443
|
+
printList(index);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const sources = [
|
|
447
|
+
...opts.from.map(customSkillSource),
|
|
448
|
+
...defaultSkillSources(opts.cwd).filter((s) => !opts.tools || opts.tools.includes(s.tool)),
|
|
449
|
+
];
|
|
450
|
+
const missingFrom = opts.from.filter((dir) => !existsSync(dir));
|
|
451
|
+
for (const dir of missingFrom)
|
|
452
|
+
console.log(` --from 目录不存在: ${dir}`);
|
|
453
|
+
if (sources.length === 0) {
|
|
454
|
+
console.log("");
|
|
455
|
+
console.log(" 没找到 Claude Code / Codex 的技能目录(~/.claude/skills、~/.codex/skills、项目里的 .claude/skills 或 .agents/skills)。");
|
|
456
|
+
console.log(" 其他工具的技能可以用 --from 指定目录,只要里面有 SKILL.md。");
|
|
457
|
+
console.log("");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const { skills, problems } = discoverSkills(sources);
|
|
461
|
+
const plan = planSkillImport(skills, { destDir, index, force: opts.force });
|
|
462
|
+
printPreview(skills, plan, problems, sources);
|
|
463
|
+
if (plan.pending.length === 0) {
|
|
464
|
+
console.log("");
|
|
465
|
+
console.log(skills.length === 0 ? " 这些目录里没有能用的技能。" : " 没有新的可导。想重导一遍就加 --update。");
|
|
466
|
+
console.log("");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (opts.dryRun) {
|
|
470
|
+
console.log("");
|
|
471
|
+
console.log(` 预演结束,以上 ${plan.pending.length} 个还没真正导入。`);
|
|
472
|
+
console.log("");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (!opts.yes && !(await confirm(` 导入这 ${plan.pending.length} 个技能?(回车=好 / n=取消) `))) {
|
|
476
|
+
console.log(" 已取消。");
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const results = runSkillImport(plan.pending, index, destDir);
|
|
480
|
+
writeJson(skillIndexPath(), index);
|
|
481
|
+
const imported = results.filter((r) => r.status === "imported").length;
|
|
482
|
+
const errors = results.filter((r) => r.status === "error");
|
|
483
|
+
console.log("");
|
|
484
|
+
console.log(` ✓ 导入 ${imported} 个技能到 ${formatHomePath(destDir)}${errors.length ? `,失败 ${errors.length}` : ""}。`);
|
|
485
|
+
for (const item of errors)
|
|
486
|
+
console.log(` × ${item.name}: ${item.detail ?? "失败"}`);
|
|
487
|
+
if (imported > 0) {
|
|
488
|
+
console.log(" 下次进 u1s1 对话就能用:模型会按说明自动调用,或输入 /skill:名称 手动调用。");
|
|
489
|
+
console.log(` 管理: u1s1 import skills --list / --update / --remove 名称`);
|
|
490
|
+
}
|
|
491
|
+
console.log("");
|
|
492
|
+
}
|
package/dist/import/util.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export declare const MAX_TOOL_RESULT_CHARS = 80000;
|
|
|
2
2
|
export declare const MAX_TEXT_CHARS = 200000;
|
|
3
3
|
export declare const PREVIEW_TITLE_CHARS = 48;
|
|
4
4
|
export declare function resolveExistingDir(path: string): string | undefined;
|
|
5
|
-
export declare function listHomeClaudeDirs(): string[];
|
|
5
|
+
export declare function listHomeClaudeDirs(home?: string): string[];
|
|
6
6
|
export declare function uniqueExistingDirs(paths: Array<string | undefined>): string[];
|
|
7
7
|
export declare function encodeClaudeProjectDir(cwd: string): string;
|
|
8
8
|
export declare function samePath(a: string, b: string): boolean;
|
package/dist/import/util.js
CHANGED
|
@@ -15,8 +15,7 @@ export function resolveExistingDir(path) {
|
|
|
15
15
|
return undefined;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
export function listHomeClaudeDirs() {
|
|
19
|
-
const home = homedir();
|
|
18
|
+
export function listHomeClaudeDirs(home = homedir()) {
|
|
20
19
|
const out = [];
|
|
21
20
|
try {
|
|
22
21
|
for (const name of readdirSync(home)) {
|
package/dist/index.js
CHANGED
|
@@ -529,7 +529,7 @@ async function run() {
|
|
|
529
529
|
console.log(" u1s1 deploy list 查看已发布的站点");
|
|
530
530
|
console.log(" u1s1 deploy remove 删除已发布的站点");
|
|
531
531
|
console.log(" u1s1 feedback \"一句话\" 反馈问题或建议(自动建工单,--bug/--question…)");
|
|
532
|
-
console.log(" u1s1 import 导入历史会话");
|
|
532
|
+
console.log(" u1s1 import 导入历史会话(import skills 导入技能)");
|
|
533
533
|
console.log(" u1s1 bench 模型编码能力评测");
|
|
534
534
|
console.log(" u1s1 --version 查看版本");
|
|
535
535
|
console.log("");
|