dsh-codex-port 0.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.en.md ADDED
@@ -0,0 +1,68 @@
1
+ [中文](README.md)
2
+
3
+ # dsh-codex-port
4
+
5
+ Move the whole **official Codex plugin family** into DSH: scan `~/.codex` unpacked plugins and plugin caches, then batch-port their skills into DSH skills (automatic frontmatter conversion, codex-only files stripped, name sanitization, idempotent skips).
6
+
7
+ > Measured on a real machine: 186 official Codex plugins, 583 skills — one port run moved 577 successfully, 0 failures.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ dsh plugin --profile web add dsh-codex-port
13
+ ```
14
+
15
+ Requires the Codex CLI to be installed (the `~/.codex` directory must exist).
16
+
17
+ ## Configuration
18
+
19
+ Everything is optional; defaults just work:
20
+
21
+ ```yaml
22
+ - id: codex-port
23
+ name: 'dsh-codex-port'
24
+ config:
25
+ # codexHome: C:\Users\you\.codex # Codex home (default ~/.codex)
26
+ # targetDir: C:\Users\you\.dsh\skills # target skills dir (default <DSH_HOME>/skills)
27
+ # overwrite: true # overwrite same-name skills (default skip)
28
+ ```
29
+
30
+ ## Tools
31
+
32
+ | Tool | Purpose | Key parameters |
33
+ | :-- | :-- | :-- |
34
+ | `codex_list` | List discovered Codex plugins and skills | `plugin`/`skill` filters, `limit` 1-200 |
35
+ | `codex_port` | Batch-port skills into DSH | `plugins`/`skills` filters, `targetDir`, `overwrite` |
36
+ | `codex_status` | Compare source vs target: ported/missing counts | none |
37
+
38
+ ### Examples
39
+
40
+ ```text
41
+ codex_list {} # what does Codex have?
42
+ codex_list { plugin: remotion } # one plugin only
43
+ codex_port {} # port everything (same names auto-skip)
44
+ codex_port { plugins: [remotion, hyperframes] }
45
+ codex_port { skills: [video-best], overwrite: true }
46
+ codex_status {} # how many are left?
47
+ ```
48
+
49
+ Ported skills are immediately usable from the DSH skills directory; the agent triggers them by their descriptions.
50
+
51
+ ## Porting rules
52
+
53
+ - **Frontmatter conversion**: Codex `name/description/metadata` → DSH `name/description/compatibility/allowed-tools`, multi-line descriptions preserved
54
+ - **Codex-only files stripped**: `agents/*.yaml` and friends stay behind
55
+ - **Name sanitization**: invalid characters become underscores; `..` traversal rejected outright
56
+ - **Idempotent**: same-name skills are skipped by default; `overwrite=true` to replace
57
+ - **Safe**: pure filesystem operations, zero runtime dependencies (yaml parsing only)
58
+
59
+ ## Development
60
+
61
+ ```bash
62
+ pnpm install
63
+ pnpm test # build + 33 tests
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ [English](README.en.md)
2
+
3
+ # dsh-codex-port
4
+
5
+ 把 **Codex 官方插件全家桶**一键搬进 DSH:扫描 `~/.codex` 里的解包插件与插件缓存,把它们的技能批量移植为 DSH 技能(frontmatter 自动转换、codex 专属文件剔除、名称清洗、幂等跳过)。
6
+
7
+ > 本机实测:186 个 Codex 官方插件、583 个技能,一次移植 577 个成功、0 失败。
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ dsh plugin --profile web add dsh-codex-port
13
+ ```
14
+
15
+ 需要本机装有 Codex CLI(`~/.codex` 目录存在即可)。
16
+
17
+ ## 配置
18
+
19
+ 全部可选,默认即可用:
20
+
21
+ ```yaml
22
+ - id: codex-port
23
+ name: 'dsh-codex-port'
24
+ config:
25
+ # codexHome: C:\Users\you\.codex # Codex 家目录(默认 ~/.codex)
26
+ # targetDir: C:\Users\you\.dsh\skills # 目标技能目录(默认 <DSH_HOME>/skills)
27
+ # overwrite: true # 覆盖同名技能(默认跳过)
28
+ ```
29
+
30
+ ## 工具一览
31
+
32
+ | 工具 | 作用 | 关键参数 |
33
+ | :-- | :-- | :-- |
34
+ | `codex_list` | 列出发现的 Codex 插件与技能 | `plugin`/`skill` 过滤,`limit` 1-200 |
35
+ | `codex_port` | 批量移植为 DSH 技能 | `plugins`/`skills` 过滤,`targetDir`,`overwrite` |
36
+ | `codex_status` | 对比源与目标:已移植/未移植统计 | 无 |
37
+
38
+ ### 示例
39
+
40
+ ```text
41
+ codex_list {} # 看看 Codex 里有什么
42
+ codex_list { plugin: remotion } # 只看某个插件
43
+ codex_port {} # 全部移植(同名自动跳过)
44
+ codex_port { plugins: [remotion, hyperframes] }
45
+ codex_port { skills: [video-best], overwrite: true }
46
+ codex_status {} # 还有多少没移植
47
+ ```
48
+
49
+ 移植后 DSH 技能目录立即可用,agent 按技能描述自动触发。
50
+
51
+ ## 移植规则
52
+
53
+ - **frontmatter 转换**:Codex 的 `name/description/metadata` → DSH 的 `name/description/compatibility/allowed-tools`,多行描述完整保留
54
+ - **剔除 codex 专属文件**:`agents/*.yaml` 等子代理描述不带走
55
+ - **名称清洗**:非法字符替换为下划线,`..` 路径穿越直接拒绝
56
+ - **幂等**:同名技能默认跳过,`overwrite=true` 才覆盖
57
+ - **安全**:纯文件系统操作,零运行时依赖(仅 yaml 解析)
58
+
59
+ ## 开发
60
+
61
+ ```bash
62
+ pnpm install
63
+ pnpm test # 构建 + 33 个测试
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,16 @@
1
+ # dsh-codex-port 组合包补丁:把 Codex 技能移植工具插件行插入 profile。
2
+ # 本补丁按 id 覆盖;用户应在自己 profile 的 cordis.patch.yml 里覆盖本行的整个
3
+ # config(见 README 的配置示例)。配置缺失时插件仍会加载,扫描默认 ~/.codex。
4
+ #
5
+ # 示例(用户 profile 的 cordis.patch.yml):
6
+ # - id: codex-port
7
+ # name: 'dsh-codex-port'
8
+ # config:
9
+ # # codexHome: C:\Users\you\.codex # Codex 家目录(默认 ~/.codex)
10
+ # # targetDir: C:\Users\you\.dsh\skills # 目标技能目录(默认 <DSH_HOME>/skills)
11
+ # # overwrite: true # 覆盖同名技能(默认跳过)
12
+ #
13
+ - insert:
14
+ - id: codex-port
15
+ name: 'dsh-codex-port'
16
+ config: {}
@@ -0,0 +1,20 @@
1
+ /** 插件行配置(cordis.patch.yml 里的 config 段,可缺省)。 */
2
+ export interface CodexPortConfig {
3
+ codexHome?: string;
4
+ targetDir?: string;
5
+ overwrite?: boolean;
6
+ }
7
+ /** 解析后的配置。 */
8
+ export interface ResolvedCodexPortConfig {
9
+ codexHome: string;
10
+ targetDir: string;
11
+ overwrite: boolean;
12
+ }
13
+ /** 默认 DSH 技能目录:$DSH_HOME/skills,否则 ~/.dsh/skills。 */
14
+ export declare function defaultTargetDir(env?: NodeJS.ProcessEnv): string;
15
+ /**
16
+ * 解析并校验配置(目录存在性在执行时惰性检查,这里只校验类型)。
17
+ */
18
+ export declare function resolveConfig(config: CodexPortConfig | undefined | null): ResolvedCodexPortConfig;
19
+ /** 惰性校验:目录不存在时抛中文指引。 */
20
+ export declare function assertCodexHome(codexHome: string): void;
package/lib/config.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * dsh-codex-port 配置解析:Codex 家目录、目标技能目录、覆写策略。
3
+ *
4
+ * @module dsh-codex-port/config
5
+ */
6
+ import { homedir } from 'node:os';
7
+ import { existsSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ /** 默认 DSH 技能目录:$DSH_HOME/skills,否则 ~/.dsh/skills。 */
10
+ export function defaultTargetDir(env = process.env) {
11
+ const home = env.DSH_HOME ?? '';
12
+ return home !== '' ? join(home, 'skills') : join(homedir(), '.dsh', 'skills');
13
+ }
14
+ /**
15
+ * 解析并校验配置(目录存在性在执行时惰性检查,这里只校验类型)。
16
+ */
17
+ export function resolveConfig(config) {
18
+ const cfg = config ?? {};
19
+ const codexHome = typeof cfg.codexHome === 'string' && cfg.codexHome.trim() !== '' ? cfg.codexHome.trim() : join(homedir(), '.codex');
20
+ const targetDir = typeof cfg.targetDir === 'string' && cfg.targetDir.trim() !== '' ? cfg.targetDir.trim() : defaultTargetDir();
21
+ const overwrite = cfg.overwrite === true;
22
+ return { codexHome, targetDir, overwrite };
23
+ }
24
+ /** 惰性校验:目录不存在时抛中文指引。 */
25
+ export function assertCodexHome(codexHome) {
26
+ if (!existsSync(codexHome)) {
27
+ throw new Error('Codex 家目录不存在:' + codexHome + '。请检查是否安装了 Codex CLI,或在 cordis.patch.yml 里用 codexHome 显式指定。');
28
+ }
29
+ }
@@ -0,0 +1,24 @@
1
+ /** 单个技能源。 */
2
+ export interface CodexSkillSource {
3
+ skillDirName: string;
4
+ skillName: string;
5
+ description: string;
6
+ sourceDir: string;
7
+ skillFile: string;
8
+ }
9
+ /** 单个插件。 */
10
+ export interface CodexPluginInfo {
11
+ name: string;
12
+ version: string;
13
+ description: string;
14
+ homepage: string;
15
+ license: string;
16
+ sourceDir: string;
17
+ skills: CodexSkillSource[];
18
+ }
19
+ /**
20
+ * 发现所有 Codex 插件。
21
+ * 顺序:解包目录(.tmp/plugins/plugins/*)优先,缓存目录(plugins/cache/**)其次;
22
+ * 同名插件按 版本最新 > 目录修改时间 去重。
23
+ */
24
+ export declare function discoverPlugins(codexHome: string): CodexPluginInfo[];
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 插件发现:扫描 Codex 家目录里的解包插件(.tmp/plugins/plugins/*)与
3
+ * 插件缓存(plugins/cache/<分类>/<名>/<版本或哈希>/),解析 plugin.json 与 skills。
4
+ *
5
+ * @module dsh-codex-port/discover
6
+ */
7
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ function asRecord(value) {
10
+ return typeof value === 'object' && value !== null ? value : {};
11
+ }
12
+ function isDir(path) {
13
+ try {
14
+ return statSync(path).isDirectory();
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ /** 读取 plugin.json;缺失或非法返回 null。 */
21
+ function readPluginManifest(pluginDir) {
22
+ const manifestPath = join(pluginDir, '.codex-plugin', 'plugin.json');
23
+ if (!existsSync(manifestPath))
24
+ return null;
25
+ try {
26
+ const raw = JSON.parse(readFileSync(manifestPath, 'utf8'));
27
+ const name = typeof raw.name === 'string' ? raw.name : '';
28
+ if (name === '')
29
+ return null;
30
+ return {
31
+ name,
32
+ version: typeof raw.version === 'string' ? raw.version : 'unknown',
33
+ description: typeof raw.description === 'string' ? raw.description : '',
34
+ homepage: typeof raw.homepage === 'string' ? raw.homepage : '',
35
+ license: typeof raw.license === 'string' ? raw.license : 'unknown',
36
+ };
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /** 从插件目录枚举技能(skills/<dir>/SKILL.md)。 */
43
+ function enumerateSkills(pluginDir) {
44
+ const skillsDir = join(pluginDir, 'skills');
45
+ if (!isDir(skillsDir))
46
+ return [];
47
+ const result = [];
48
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
49
+ if (!entry.isDirectory())
50
+ continue;
51
+ const skillDir = join(skillsDir, entry.name);
52
+ const skillFile = join(skillDir, 'SKILL.md');
53
+ if (!existsSync(skillFile))
54
+ continue;
55
+ let skillName = entry.name;
56
+ let description = '';
57
+ try {
58
+ const text = readFileSync(skillFile, 'utf8');
59
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
60
+ if (match) {
61
+ // 粗略读取 name/description(完整解析在移植时用 YAML 做)
62
+ const nameMatch = /^name:\s*(.+)$/m.exec(match[1]);
63
+ const descMatch = /^description:\s*(.+)$/m.exec(match[1]);
64
+ if (nameMatch)
65
+ skillName = nameMatch[1].trim().replace(/^["']|["']$/g, '');
66
+ if (descMatch)
67
+ description = descMatch[1].trim().replace(/^["']|["']$/g, '').slice(0, 160);
68
+ }
69
+ }
70
+ catch { /* 读取失败保持目录名 */ }
71
+ result.push({ skillDirName: entry.name, skillName, description, sourceDir: skillDir, skillFile });
72
+ }
73
+ return result;
74
+ }
75
+ /**
76
+ * 发现所有 Codex 插件。
77
+ * 顺序:解包目录(.tmp/plugins/plugins/*)优先,缓存目录(plugins/cache/**)其次;
78
+ * 同名插件按 版本最新 > 目录修改时间 去重。
79
+ */
80
+ export function discoverPlugins(codexHome) {
81
+ const found = new Map();
82
+ const seen = new Set();
83
+ const candidates = [];
84
+ const unpacked = join(codexHome, '.tmp', 'plugins', 'plugins');
85
+ if (isDir(unpacked)) {
86
+ for (const entry of readdirSync(unpacked, { withFileTypes: true })) {
87
+ if (entry.isDirectory())
88
+ candidates.push({ dir: join(unpacked, entry.name), priority: 2 });
89
+ }
90
+ }
91
+ const cacheRoot = join(codexHome, 'plugins', 'cache');
92
+ if (isDir(cacheRoot)) {
93
+ for (const category of readdirSync(cacheRoot, { withFileTypes: true })) {
94
+ if (!category.isDirectory())
95
+ continue;
96
+ const categoryDir = join(cacheRoot, category.name);
97
+ for (const nameEntry of readdirSync(categoryDir, { withFileTypes: true })) {
98
+ if (!nameEntry.isDirectory())
99
+ continue;
100
+ const nameDir = join(categoryDir, nameEntry.name);
101
+ if (existsSync(join(nameDir, '.codex-plugin', 'plugin.json'))) {
102
+ candidates.push({ dir: nameDir, priority: 1 });
103
+ }
104
+ else {
105
+ for (const child of readdirSync(nameDir, { withFileTypes: true })) {
106
+ if (child.isDirectory())
107
+ candidates.push({ dir: join(nameDir, child.name), priority: 0 });
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ for (const candidate of candidates) {
114
+ const manifest = readPluginManifest(candidate.dir);
115
+ if (manifest === null)
116
+ continue;
117
+ const key = manifest.name.toLowerCase();
118
+ const skills = enumerateSkills(candidate.dir);
119
+ const info = { ...manifest, sourceDir: candidate.dir, skills };
120
+ const existing = found.get(key);
121
+ if (existing === undefined) {
122
+ found.set(key, info);
123
+ seen.add(key + ':' + candidate.dir);
124
+ }
125
+ else {
126
+ const betterPriority = candidate.priority > (candidates.find((c) => c.dir === existing.sourceDir)?.priority ?? -1);
127
+ const newerVersion = manifest.version !== 'unknown' && manifest.version !== existing.version && manifest.version > existing.version;
128
+ if (betterPriority || newerVersion)
129
+ found.set(key, info);
130
+ }
131
+ }
132
+ return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
133
+ }
@@ -0,0 +1,29 @@
1
+ /** 安全的技能名:DSH 技能目录名约束。 */
2
+ export declare const SKILL_NAME_PATTERN: RegExp;
3
+ /**
4
+ * 清洗技能名:非法字符替换为下划线;空则返回 null(跳过该技能)。
5
+ */
6
+ export declare function sanitizeSkillName(name: string): string | null;
7
+ export interface ParsedSkillFile {
8
+ frontmatter: Record<string, unknown>;
9
+ body: string;
10
+ hasFrontmatter: boolean;
11
+ }
12
+ /**
13
+ * 拆分 SKILL.md 的 frontmatter 与正文。
14
+ */
15
+ export declare function parseSkillFile(text: string): ParsedSkillFile;
16
+ /** 插件来源信息(用于 compatibility 行)。 */
17
+ export interface PluginAttribution {
18
+ pluginName: string;
19
+ homepage: string;
20
+ license: string;
21
+ }
22
+ /**
23
+ * 生成 DSH 格式 frontmatter 文本(含首尾 ---)。
24
+ */
25
+ export declare function toDshFrontmatter(frontmatter: Record<string, unknown>, attribution: PluginAttribution): string;
26
+ /**
27
+ * 转换整个 SKILL.md 文本。
28
+ */
29
+ export declare function convertSkillFile(text: string, attribution: PluginAttribution, fallbackName: string): string;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * frontmatter 转换引擎:读取 Codex SKILL.md 的 YAML frontmatter,
3
+ * 生成 DSH 格式(name/description/compatibility/allowed-tools)。
4
+ *
5
+ * @module dsh-codex-port/frontmatter
6
+ */
7
+ import YAML from 'yaml';
8
+ /** 安全的技能名:DSH 技能目录名约束。 */
9
+ export const SKILL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
10
+ /**
11
+ * 清洗技能名:非法字符替换为下划线;空则返回 null(跳过该技能)。
12
+ */
13
+ export function sanitizeSkillName(name) {
14
+ const cleaned = name.trim().replace(/[^A-Za-z0-9._-]/g, '_');
15
+ if (cleaned === '' || !SKILL_NAME_PATTERN.test(cleaned))
16
+ return null;
17
+ if (cleaned.includes('..'))
18
+ return null;
19
+ return cleaned;
20
+ }
21
+ /**
22
+ * 拆分 SKILL.md 的 frontmatter 与正文。
23
+ */
24
+ export function parseSkillFile(text) {
25
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
26
+ if (!match)
27
+ return { frontmatter: {}, body: text, hasFrontmatter: false };
28
+ try {
29
+ const parsed = YAML.parse(match[1]);
30
+ const record = typeof parsed === 'object' && parsed !== null ? parsed : {};
31
+ return { frontmatter: record, body: text.slice(match[0].length), hasFrontmatter: true };
32
+ }
33
+ catch {
34
+ return { frontmatter: {}, body: text, hasFrontmatter: false };
35
+ }
36
+ }
37
+ /**
38
+ * 生成 DSH 格式 frontmatter 文本(含首尾 ---)。
39
+ */
40
+ export function toDshFrontmatter(frontmatter, attribution) {
41
+ const rawName = typeof frontmatter.name === 'string' ? frontmatter.name : '';
42
+ const fallbackName = typeof frontmatter.title === 'string' ? frontmatter.title : '';
43
+ const name = rawName !== '' ? rawName : fallbackName;
44
+ const description = typeof frontmatter.description === 'string' ? frontmatter.description : '';
45
+ const compatibility = 'Ported from Codex plugin ' + attribution.pluginName + (attribution.homepage !== '' ? ' (' + attribution.homepage + ')' : '') + (attribution.license !== '' && attribution.license !== 'unknown' ? ', license ' + attribution.license : '') + '.';
46
+ const doc = { name, description, compatibility, 'allowed-tools': 'Bash' };
47
+ return '---\n' + YAML.stringify(doc).trimEnd() + '\n---\n';
48
+ }
49
+ /**
50
+ * 转换整个 SKILL.md 文本。
51
+ */
52
+ export function convertSkillFile(text, attribution, fallbackName) {
53
+ const parsed = parseSkillFile(text);
54
+ const frontmatter = parsed.hasFrontmatter ? parsed.frontmatter : { name: fallbackName, description: '' };
55
+ return toDshFrontmatter(frontmatter, attribution) + '\n' + parsed.body.trimStart();
56
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * dsh-codex-port —— Codex 插件 → DSH 技能移植工具插件(node 半身)。
3
+ *
4
+ * 插件导出 apply(ctx, config):注册三个面向模型的工具(codex_list / codex_port /
5
+ * codex_status),扫描 ~/.codex 的解包插件与缓存,把官方 Codex 插件技能批量移植为
6
+ * DSH 技能。纯文件系统操作,零运行时依赖(仅 yaml 解析)。
7
+ *
8
+ * @module dsh-codex-port
9
+ */
10
+ import { type CodexPortConfig } from './config.js';
11
+ import { type CodexPortToolDefinition } from './tools.js';
12
+ /** cordis 服务注入:apply 里要用 ctx.tools,必须显式声明。 */
13
+ export declare const inject: string[];
14
+ /** 插件所需的最小 ctx 面。 */
15
+ export interface CodexPortPluginContext {
16
+ tools: {
17
+ register(definition: CodexPortToolDefinition): () => void;
18
+ };
19
+ on?(event: string, listener: () => void): () => void;
20
+ }
21
+ /**
22
+ * 插件入口:解析配置并注册三个移植工具。
23
+ */
24
+ export declare function apply(ctx: CodexPortPluginContext, config?: CodexPortConfig | null): void;
25
+ export * from './config.js';
26
+ export * from './discover.js';
27
+ export * from './frontmatter.js';
28
+ export * from './port.js';
29
+ export * from './tools.js';
package/lib/index.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * dsh-codex-port —— Codex 插件 → DSH 技能移植工具插件(node 半身)。
3
+ *
4
+ * 插件导出 apply(ctx, config):注册三个面向模型的工具(codex_list / codex_port /
5
+ * codex_status),扫描 ~/.codex 的解包插件与缓存,把官方 Codex 插件技能批量移植为
6
+ * DSH 技能。纯文件系统操作,零运行时依赖(仅 yaml 解析)。
7
+ *
8
+ * @module dsh-codex-port
9
+ */
10
+ import { resolveConfig } from './config.js';
11
+ import { buildCodexPortTools } from './tools.js';
12
+ /** cordis 服务注入:apply 里要用 ctx.tools,必须显式声明。 */
13
+ export const inject = ['tools'];
14
+ /**
15
+ * 插件入口:解析配置并注册三个移植工具。
16
+ */
17
+ export function apply(ctx, config) {
18
+ let cfg;
19
+ try {
20
+ cfg = resolveConfig(config);
21
+ }
22
+ catch (error) {
23
+ console.warn('[dsh-codex-port] ' + (error instanceof Error ? error.message : String(error)));
24
+ cfg = resolveConfig(null);
25
+ }
26
+ const disposers = [];
27
+ for (const definition of buildCodexPortTools(cfg)) {
28
+ disposers.push(ctx.tools.register(definition));
29
+ }
30
+ if (typeof ctx.on === 'function') {
31
+ ctx.on('dispose', () => {
32
+ for (const dispose of disposers)
33
+ dispose();
34
+ });
35
+ }
36
+ }
37
+ export * from './config.js';
38
+ export * from './discover.js';
39
+ export * from './frontmatter.js';
40
+ export * from './port.js';
41
+ export * from './tools.js';
package/lib/port.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { type CodexPluginInfo, type CodexSkillSource } from './discover.js';
2
+ /** 单个技能的移植结果。 */
3
+ export interface PortResult {
4
+ skill: string;
5
+ plugin: string;
6
+ status: 'ported' | 'skipped' | 'failed';
7
+ reason: string;
8
+ files: number;
9
+ }
10
+ /**
11
+ * 移植单个技能到目标目录。
12
+ */
13
+ export declare function portSkill(skill: CodexSkillSource, plugin: CodexPluginInfo, targetDir: string, overwrite: boolean): PortResult;
package/lib/port.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 移植执行:目录拷贝(剔除 agents 等 codex 专属文件)+ SKILL.md 转换 + 幂等跳过。
3
+ *
4
+ * @module dsh-codex-port/port
5
+ */
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, rmSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { convertSkillFile, sanitizeSkillName } from './frontmatter.js';
9
+ /** codex 专属目录,移植时剔除。 */
10
+ const EXCLUDED_DIRS = new Set(['agents']);
11
+ /** 递归拷贝,跳过排除目录;返回拷贝文件数。 */
12
+ function copyTree(source, target) {
13
+ mkdirSync(target, { recursive: true });
14
+ let count = 0;
15
+ for (const entry of readdirSync(source, { withFileTypes: true })) {
16
+ if (entry.isDirectory() && EXCLUDED_DIRS.has(entry.name))
17
+ continue;
18
+ const from = join(source, entry.name);
19
+ const to = join(target, entry.name);
20
+ if (entry.isDirectory()) {
21
+ count += copyTree(from, to);
22
+ }
23
+ else {
24
+ writeFileSync(to, readFileSync(from));
25
+ count += 1;
26
+ }
27
+ }
28
+ return count;
29
+ }
30
+ /**
31
+ * 移植单个技能到目标目录。
32
+ */
33
+ export function portSkill(skill, plugin, targetDir, overwrite) {
34
+ const safeName = sanitizeSkillName(skill.skillName !== '' ? skill.skillName : skill.skillDirName);
35
+ if (safeName === null) {
36
+ return { skill: skill.skillDirName, plugin: plugin.name, status: 'failed', reason: '技能名清洗后为空或不合法,已跳过(可能是纯中文或含危险字符的名称)', files: 0 };
37
+ }
38
+ const target = join(targetDir, safeName);
39
+ if (existsSync(target) && !overwrite) {
40
+ return { skill: safeName, plugin: plugin.name, status: 'skipped', reason: '目标技能已存在(overwrite=false),跳过以免覆盖', files: 0 };
41
+ }
42
+ try {
43
+ if (existsSync(target))
44
+ rmSync(target, { recursive: true, force: true });
45
+ let files = copyTree(skill.sourceDir, target);
46
+ // 重写 SKILL.md 的 frontmatter
47
+ const skillFile = join(target, 'SKILL.md');
48
+ const original = readFileSync(skillFile, 'utf8');
49
+ const converted = convertSkillFile(original, { pluginName: plugin.name, homepage: plugin.homepage, license: plugin.license }, safeName);
50
+ writeFileSync(skillFile, converted, 'utf8');
51
+ files = Math.max(files, 1);
52
+ return { skill: safeName, plugin: plugin.name, status: 'ported', reason: '', files };
53
+ }
54
+ catch (error) {
55
+ return { skill: safeName, plugin: plugin.name, status: 'failed', reason: error instanceof Error ? error.message : String(error), files: 0 };
56
+ }
57
+ }
package/lib/tools.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { type ResolvedCodexPortConfig } from './config.js';
2
+ /** 模型可见的内容块。 */
3
+ export interface ContentBlock {
4
+ type: 'text';
5
+ text: string;
6
+ }
7
+ /** 注册给 ctx.tools.register 的原始工具定义。 */
8
+ export interface CodexPortToolDefinition {
9
+ name: string;
10
+ description: string;
11
+ parameters: {
12
+ type: 'object';
13
+ properties: Record<string, unknown>;
14
+ required?: string[];
15
+ };
16
+ output: {
17
+ schema: Record<string, unknown>;
18
+ render(args: unknown, value: unknown): ContentBlock[];
19
+ };
20
+ execute(args: unknown, exec: unknown): Promise<unknown>;
21
+ }
22
+ /**
23
+ * 构建三个工具定义。
24
+ */
25
+ export declare function buildCodexPortTools(config: ResolvedCodexPortConfig): CodexPortToolDefinition[];
package/lib/tools.js ADDED
@@ -0,0 +1,267 @@
1
+ /**
2
+ * 三个面向模型的移植工具:codex_list / codex_port / codex_status。
3
+ *
4
+ * @module dsh-codex-port/tools
5
+ */
6
+ import { existsSync, mkdirSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+ import { assertCodexHome } from './config.js';
9
+ import { discoverPlugins } from './discover.js';
10
+ import { portSkill } from './port.js';
11
+ import { sanitizeSkillName } from './frontmatter.js';
12
+ /** 编译作者 DSL 为原始 JSON Schema。 */
13
+ function compileParameters(spec) {
14
+ const properties = {};
15
+ const required = [];
16
+ for (const [key, prop] of Object.entries(spec)) {
17
+ if (prop?.required === true)
18
+ required.push(key);
19
+ const node = {};
20
+ if (typeof prop?.type === 'string')
21
+ node.type = prop.type;
22
+ if (typeof prop?.description === 'string')
23
+ node.description = prop.description;
24
+ if (prop?.type === 'array' && prop.items !== null && typeof prop.items === 'object') {
25
+ node.items = { type: 'string' };
26
+ }
27
+ properties[key] = node;
28
+ }
29
+ return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
30
+ }
31
+ function asRecord(value) {
32
+ return typeof value === 'object' && value !== null ? value : {};
33
+ }
34
+ function optionalString(args, key) {
35
+ const value = args[key];
36
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
37
+ }
38
+ function stringArray(args, key) {
39
+ const value = args[key];
40
+ if (!Array.isArray(value))
41
+ return [];
42
+ return value.filter((item) => typeof item === 'string' && item.trim() !== '').map((item) => item.trim());
43
+ }
44
+ const MAX_PLUGIN_LIMIT = 200;
45
+ const pluginShape = {
46
+ type: 'object',
47
+ properties: {
48
+ name: { type: 'string' },
49
+ version: { type: 'string' },
50
+ description: { type: 'string' },
51
+ homepage: { type: 'string' },
52
+ license: { type: 'string' },
53
+ skillCount: { type: 'integer' },
54
+ skills: {
55
+ type: 'array',
56
+ items: {
57
+ type: 'object',
58
+ properties: { name: { type: 'string' }, description: { type: 'string' } },
59
+ additionalProperties: true,
60
+ },
61
+ },
62
+ },
63
+ additionalProperties: true,
64
+ };
65
+ const listSchema = {
66
+ type: 'object',
67
+ properties: {
68
+ count: { type: 'integer' },
69
+ total: { type: 'integer' },
70
+ codexHome: { type: 'string' },
71
+ plugins: { type: 'array', items: pluginShape },
72
+ },
73
+ additionalProperties: true,
74
+ };
75
+ const portItemSchema = {
76
+ type: 'object',
77
+ properties: { skill: { type: 'string' }, plugin: { type: 'string' }, reason: { type: 'string' }, files: { type: 'integer' } },
78
+ additionalProperties: true,
79
+ };
80
+ const portSchema = {
81
+ type: 'object',
82
+ properties: {
83
+ targetDir: { type: 'string' },
84
+ total: { type: 'integer' },
85
+ counts: { type: 'object', properties: { ported: { type: 'integer' }, skipped: { type: 'integer' }, failed: { type: 'integer' } }, additionalProperties: true },
86
+ ported: { type: 'array', items: portItemSchema },
87
+ skipped: { type: 'array', items: portItemSchema },
88
+ failed: { type: 'array', items: portItemSchema },
89
+ },
90
+ additionalProperties: true,
91
+ };
92
+ const statusSchema = {
93
+ type: 'object',
94
+ properties: {
95
+ codexHome: { type: 'string' },
96
+ targetDir: { type: 'string' },
97
+ plugins: { type: 'integer' },
98
+ skills: { type: 'integer' },
99
+ installed: { type: 'integer' },
100
+ missing: { type: 'integer' },
101
+ missingNames: { type: 'array', items: { type: 'string' } },
102
+ },
103
+ additionalProperties: true,
104
+ };
105
+ /**
106
+ * 构建三个工具定义。
107
+ */
108
+ export function buildCodexPortTools(config) {
109
+ const getPlugins = () => {
110
+ assertCodexHome(config.codexHome);
111
+ return discoverPlugins(config.codexHome);
112
+ };
113
+ const codexList = {
114
+ name: 'codex_list',
115
+ description: '列出 ~/.codex 里发现的 Codex 官方插件与其技能(含解包目录与插件缓存)。可选 plugin 过滤插件名、skill 过滤技能名。limit 控制返回插件数(1-200,默认 20)。',
116
+ parameters: compileParameters({
117
+ plugin: { type: 'string', description: '按插件名过滤(不区分大小写,子串匹配,可选)。' },
118
+ skill: { type: 'string', description: '按技能名过滤(不区分大小写,子串匹配,可选)。' },
119
+ limit: { type: 'integer', description: '返回插件数 1-200,默认 20。' },
120
+ }),
121
+ output: {
122
+ schema: listSchema,
123
+ render: (_args, value) => {
124
+ const rec = asRecord(value);
125
+ const plugins = Array.isArray(rec.plugins) ? rec.plugins : [];
126
+ const lines = ['发现 ' + rec.total + ' 个 Codex 插件(返回前 ' + plugins.length + ' 个):'];
127
+ for (const plugin of plugins) {
128
+ const p = asRecord(plugin);
129
+ lines.push('- ' + p.name + '@' + p.version + '(' + p.skillCount + ' 个技能):' + String(p.description ?? '').slice(0, 80));
130
+ }
131
+ return [{ type: 'text', text: lines.join('\n') }];
132
+ },
133
+ },
134
+ async execute(rawArgs) {
135
+ const args = asRecord(rawArgs);
136
+ const plugins = getPlugins();
137
+ const pluginFilter = optionalString(args, 'plugin')?.toLowerCase();
138
+ const skillFilter = optionalString(args, 'skill')?.toLowerCase();
139
+ const limitRaw = args.limit;
140
+ const limit = typeof limitRaw === 'number' && Number.isInteger(limitRaw) ? Math.min(MAX_PLUGIN_LIMIT, Math.max(1, limitRaw)) : 20;
141
+ const filtered = plugins.filter((plugin) => {
142
+ if (pluginFilter !== undefined && !plugin.name.toLowerCase().includes(pluginFilter))
143
+ return false;
144
+ if (skillFilter !== undefined && !plugin.skills.some((skill) => skill.skillName.toLowerCase().includes(skillFilter)))
145
+ return false;
146
+ return true;
147
+ });
148
+ const rows = filtered.slice(0, limit).map((plugin) => ({
149
+ name: plugin.name,
150
+ version: plugin.version,
151
+ description: plugin.description,
152
+ homepage: plugin.homepage,
153
+ license: plugin.license,
154
+ skillCount: plugin.skills.length,
155
+ skills: plugin.skills.map((skill) => ({ name: skill.skillName, description: skill.description })),
156
+ }));
157
+ return { count: rows.length, total: plugins.length, codexHome: config.codexHome, plugins: rows };
158
+ },
159
+ };
160
+ const codexPort = {
161
+ name: 'codex_port',
162
+ description: '把 Codex 插件技能移植为 DSH 技能(写入 DSH 技能目录)。frontmatter 自动转换、剔除 agents 等 codex 专属文件、名称清洗、同名默认跳过(overwrite=true 覆盖)。plugins/skills 参数可限定范围,缺省移植全部。',
163
+ parameters: compileParameters({
164
+ plugins: { type: 'array', items: { type: 'string' }, description: '只移植这些插件(插件名,不区分大小写,可选)。' },
165
+ skills: { type: 'array', items: { type: 'string' }, description: '只移植这些技能(技能名,不区分大小写,可选)。' },
166
+ targetDir: { type: 'string', description: '目标技能目录(可选,默认 <DSH_HOME>/skills)。' },
167
+ overwrite: { type: 'boolean', description: '是否覆盖同名技能(默认 false=跳过)。' },
168
+ }),
169
+ output: {
170
+ schema: portSchema,
171
+ render: (_args, value) => {
172
+ const rec = asRecord(value);
173
+ const counts = asRecord(rec.counts);
174
+ const lines = ['移植完成:新移植 ' + counts.ported + ' 个,跳过 ' + counts.skipped + ' 个,失败 ' + counts.failed + ' 个。目标目录:' + rec.targetDir];
175
+ const ported = Array.isArray(rec.ported) ? rec.ported : [];
176
+ for (const item of ported.slice(0, 20)) {
177
+ const row = asRecord(item);
178
+ lines.push('- ' + row.skill + '(来自 ' + row.plugin + ',' + row.files + ' 个文件)');
179
+ }
180
+ return [{ type: 'text', text: lines.join('\n') }];
181
+ },
182
+ },
183
+ async execute(rawArgs) {
184
+ const args = asRecord(rawArgs);
185
+ const plugins = getPlugins();
186
+ const pluginFilter = stringArray(args, 'plugins').map((name) => name.toLowerCase());
187
+ const skillFilter = stringArray(args, 'skills').map((name) => name.toLowerCase());
188
+ const targetRaw = optionalString(args, 'targetDir');
189
+ const targetDir = targetRaw !== undefined ? resolve(targetRaw) : config.targetDir;
190
+ const overwrite = args.overwrite === true ? true : config.overwrite;
191
+ mkdirSync(targetDir, { recursive: true });
192
+ const selected = [];
193
+ for (const plugin of plugins) {
194
+ if (pluginFilter.length > 0 && !pluginFilter.includes(plugin.name.toLowerCase()))
195
+ continue;
196
+ const skills = skillFilter.length > 0
197
+ ? plugin.skills.filter((skill) => skillFilter.includes(skill.skillName.toLowerCase()))
198
+ : plugin.skills;
199
+ if (skills.length > 0)
200
+ selected.push({ plugin, skills });
201
+ }
202
+ const ported = [];
203
+ const skipped = [];
204
+ const failed = [];
205
+ for (const group of selected) {
206
+ for (const skill of group.skills) {
207
+ const result = portSkill(skill, group.plugin, targetDir, overwrite);
208
+ if (result.status === 'ported')
209
+ ported.push(result);
210
+ else if (result.status === 'skipped')
211
+ skipped.push(result);
212
+ else
213
+ failed.push(result);
214
+ }
215
+ }
216
+ return {
217
+ targetDir,
218
+ total: ported.length + skipped.length + failed.length,
219
+ counts: { ported: ported.length, skipped: skipped.length, failed: failed.length },
220
+ ported: ported.map((r) => ({ skill: r.skill, plugin: r.plugin, files: r.files })),
221
+ skipped: skipped.map((r) => ({ skill: r.skill, plugin: r.plugin, reason: r.reason })),
222
+ failed: failed.map((r) => ({ skill: r.skill, plugin: r.plugin, reason: r.reason })),
223
+ };
224
+ },
225
+ };
226
+ const codexStatus = {
227
+ name: 'codex_status',
228
+ description: '对比 Codex 技能源与 DSH 技能目录:报告可用技能总数、已移植数、未移植清单(前 100 个)。',
229
+ parameters: compileParameters({}),
230
+ output: {
231
+ schema: statusSchema,
232
+ render: (_args, value) => {
233
+ const rec = asRecord(value);
234
+ return [{ type: 'text', text: 'Codex 技能 ' + rec.skills + ' 个,已移植 ' + rec.installed + ' 个,未移植 ' + rec.missing + ' 个。目标目录:' + rec.targetDir }];
235
+ },
236
+ },
237
+ async execute() {
238
+ const plugins = getPlugins();
239
+ const allSkills = [];
240
+ for (const plugin of plugins) {
241
+ for (const skill of plugin.skills)
242
+ allSkills.push({ name: skill.skillName, plugin: plugin.name });
243
+ }
244
+ const missingNames = [];
245
+ let installed = 0;
246
+ for (const skill of allSkills) {
247
+ const safeName = sanitizeSkillName(skill.name);
248
+ if (safeName !== null && existsSync(resolve(config.targetDir, safeName))) {
249
+ installed += 1;
250
+ }
251
+ else {
252
+ missingNames.push(skill.name);
253
+ }
254
+ }
255
+ return {
256
+ codexHome: config.codexHome,
257
+ targetDir: config.targetDir,
258
+ plugins: plugins.length,
259
+ skills: allSkills.length,
260
+ installed,
261
+ missing: allSkills.length - installed,
262
+ missingNames: missingNames.slice(0, 100),
263
+ };
264
+ },
265
+ };
266
+ return [codexList, codexPort, codexStatus];
267
+ }
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "dsh-codex-port",
3
+ "version": "0.1.0",
4
+ "description": "DSH 技能移植工具插件:把 ~/.codex 里的 Codex 官方插件(170+ 个)一键移植为 DSH 技能(codex_list / codex_port / codex_status),frontmatter 自动转换、名称清洗、幂等跳过。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "README.en.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "prepare": "tsc -p tsconfig.json",
25
+ "typecheck": "tsc -p tsconfig.json --noEmit",
26
+ "test": "pnpm run build && node --test \"test/*.test.mjs\"",
27
+ "prepublishOnly": "pnpm run build"
28
+ },
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ }
33
+ },
34
+ "keywords": [
35
+ "dsh",
36
+ "deepseek-harness",
37
+ "plugin",
38
+ "codex",
39
+ "skills",
40
+ "migration"
41
+ ],
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "dependencies": {
47
+ "yaml": "^2.9.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^24.0.0",
51
+ "typescript": "^5.6.0"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "https://github.com/STARDUSTLC666/dsh-codex-port"
56
+ },
57
+ "bugs": {
58
+ "url": "https://github.com/STARDUSTLC666/dsh-codex-port/issues"
59
+ },
60
+ "homepage": "https://github.com/STARDUSTLC666/dsh-codex-port#readme",
61
+ "author": "stardustlc",
62
+ "packageManager": "pnpm@11.7.0"
63
+ }