chore: sync with upstream e7cb442 + update zh translations

This commit is contained in:
xuxiang
2026-02-02 18:57:56 +08:00
parent 6f87d43c19
commit d7cafbe582
66 changed files with 9395 additions and 1465 deletions

90
.claude/skills/oneskill/.gitignore vendored Normal file
View File

@@ -0,0 +1,90 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.test
.env.production
# parcel-bundler cache
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line if your project uses Gatsby and you want to debug the
# build, as it contains some public assets. These assets are usually
# generated from other files and don't need to be in version control.
# public
# vue-cli build runner target
target/
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Tern device bin file
.tern-port
# Stores VS Code state
.vscode
# Build output
dist/
# OS metadata
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,7 @@
{
"source": "xu-xiang/oneskill",
"sourceType": "git",
"repoUrl": "https://github.com/xu-xiang/oneskill",
"subpath": "",
"installedAt": "2026-01-31T16:23:32.899Z"
}

View File

@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of Your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -0,0 +1,88 @@
<div align="center">
# OneSkill 元管理器Meta-Manager
**AI 智能体技能Agent Skills的通用桥梁。**
从 OpenSkills 注册表中发现、安装并映射功能到您的环境。
[![](https://img.shields.io/npm/v/oneskill?color=brightgreen)](https://www.npmjs.com/package/oneskill)
[![](https://img.shields.io/npm/l/oneskill)](LICENSE)
[**🇺🇸 English**](README.md) | [**🇨🇳 中文指南**](README_CN.md)
</div>
---
## ⚡️ 什么是 OneSkill
**OneSkill** 是一款专为 AI 智能体Agent以及人类设计的元工具用于轻松扩展其功能。它是 [OpenSkills](https://github.com/Starttoaster/openskills) 生态系统的搜索引擎和工作流管理器Workflow Manager
虽然 `openskills` 处理文件的原始安装,但 **OneSkill** 提供:
1. **智能搜索Intelligent Search**:使用自然语言或关键词找到适合该任务的工具。
2. **工作流指南Workflow Guidance**为智能体Agent安全获取新技能提供标准化流程。
3. **环境映射Environment Mapping**:至关重要的一点是,它弥合了 `openskills`(标准结构)与 **Gemini CLI**(自定义结构)等使用者之间的鸿沟。
## 🚀 快速开始
您无需永久安装。只需使用 `npx` 运行即可。
```bash
# 搜索技能(例如,用于浏览网页)
npx oneskill search "puppeteer browser"
# 搜索按流行度排序的数据库工具
npx oneskill search "database" --sort stars
```
## 🛠 工作流
为您的智能体Agent添加新功能的标准生命周期
1. **搜索Search**:查找技能。
```bash
npx oneskill search "github integration"
```
2. **安装Install**:使用标准的 `openskills` 安装程序。
```bash
npx openskills install anthropics/skills
```
3. **映射Map对 Gemini 至关重要)**:如果您正在使用 **Gemini CLI**,则必须将安装的技能映射到您的配置中。
```bash
# 将安装的技能映射到 Gemini 的配置
npx oneskill map --target gemini
```
## 📖 命令参考
### `search`
在全局注册表中搜索技能。
```bash
npx oneskill search <query> [options]
# 选项:
# --category <name> 按类别过滤
# --sort <field> 按 'stars'、'created' 或 'updated' 排序
# --limit <number> 限制结果数量默认值10
```
### `map`
为特定的智能体Agent环境生成配置。
```bash
npx oneskill map --target <env>
# 目标:
# gemini 生成/更新 Gemini CLI 配置
```
### `list`
列出本地映射的技能(`openskills list` 的封装)。
```bash
npx oneskill list
```
---
<div align="center">
<sub>由 OneSkill 社区用 ❤️ 构建</sub>
</div>

View File

@@ -0,0 +1,92 @@
<div align="center">
# OneSkill 元管理器 (Meta-Manager)
**AI 智能体 (Agent) 技能的通用桥梁**
帮助你发现、安装并将 OpenSkills 注册表中的能力映射到你的运行环境。
[![](https://img.shields.io/npm/v/oneskill?color=brightgreen)](https://www.npmjs.com/package/oneskill)
[![](https://img.shields.io/npm/l/oneskill)](LICENSE)
[**🇺🇸 English**](README.md) | [**🇨🇳 中文指南**](README_CN.md)
</div>
---
## ⚡️ 什么是 OneSkill
**OneSkill** 是一个为 AI 智能体 (Agent) 设计的通用技能管理工具。它作为 [OpenSkills](https://github.com/Starttoaster/openskills) 生态系统的搜索引擎和工作流管理器 (Workflow Manager),帮助你发现、安装并将能力映射到你的运行环境中。
虽然 `openskills` 负责文件的下载安装,但 **OneSkill** 提供了:
1. **智能搜索**: 支持通过自然语言或关键词搜索注册表中的技能 (Skill)。
2. **工作流引导**: 为智能体 (Agent) 提供了一套标准的扩展能力流程(搜索 -> 确认 -> 安装)。
3. **环境映射 (Mapping)**: 解决了安装路径与运行环境不一致的问题。特别是对于 **Gemini CLI** 用户OneSkill 能自动将下载的技能 (Skill) 映射到 Gemini 的配置文件中。
## 🚀 快速开始
无需全局安装,直接使用 `npx` 运行即可:
```bash
# 搜索技能 (例如:想要网页浏览能力)
npx oneskill search "puppeteer browser"
# 搜索数据库相关技能,并按星级排序
npx oneskill search "database" --sort stars
```
## 🛠 使用流程
为你的智能体 (Agent) 添加新能力的推荐步骤:
1. **搜索 (Search)**: 查找你需要的技能 (Skill)。
```bash
npx oneskill search "github integration"
```
2. **安装 (Install)**: 使用 `openskills` 标准命令进行下载。
```bash
npx openskills install anthropics/skills
```
3. **映射 (Map)**: **(Gemini 用户必读)**
`openskills` 默认将文件下载到通用目录Gemini CLI 无法直接读取。必须执行映射 (Mapping) 命令:
```bash
# 自动识别已安装的技能 (Skill) 并配置到 Gemini
npx oneskill map --target gemini
```
*如果你的技能 (Skill) 是全局安装的 (加了 --global),这里也需要加 --global。*
## 📖 命令参考
### `search` (搜索)
在全局注册表中搜索技能 (Skill)。
```bash
npx oneskill search <查询词> [选项]
# 选项:
# --category <name> 按分类筛选
# --sort <field> 排序方式: 'stars' (星级), 'created' (创建时间), 'updated' (更新时间)
# --limit <number> 限制返回数量 (默认: 10)
```
### `map` (映射)
为特定环境生成配置。
```bash
npx oneskill map --target <环境>
# 支持的目标:
# gemini 更新 Gemini CLI 的配置与路径映射
```
### `list` (列表)
查看本地已安装的技能 (Skill)。
```bash
npx oneskill list
```
---
<div align="center">
<sub>Built with ❤️ by the OneSkill Community</sub>
</div>

View File

@@ -0,0 +1,59 @@
---
name: oneskill
description: 发现技能 (Skill),迭代查询,并在任何环境中自动安装技能。
---
# OneSkill 元管理器 (Meta-Manager)
使用此技能来发现新功能、优化搜索查询,并使用 OpenSkills 简化技能设置。这为扩展环境功能提供了一种统一的方式。
## 何时使用
- 当用户要求的某些功能你目前不具备时。
- 当任务复杂、属于特定领域,或在尝试 2 次后仍被反复阻断时。
- 当可能存在更好的技能Skill例如网页浏览、GitHub 集成、数据库管理、云基础设施)。
## 工作流 (Workflow)
1. 搜索注册表:
- 运行:`npx oneskill search "<query>" [options]`
- 支持的选项:`--category``--limit``--offset``--sort`
- 示例:
- `npx oneskill search "browser" --sort stars`
- `npx oneskill search "" --category database --limit 5`
2. 分析结果:
- 确定最佳匹配项,或优化查询并再次搜索。
3. 与用户确认:
- 说明该技能的功能及其来源。
4. 在获得明确批准后进行安装(使用 openskills
- 运行:`npx openskills install <slug-or-repo>`
- 示例:`npx openskills install anthropics/skills`
5. 处理特定环境的设置:
- **Gemini CLI 用户:** `openskills` 不会自动配置 Gemini。安装后你**必须**运行映射命令:
- `npx oneskill map --target gemini`(如果是全局安装,请添加 `--global`
6. 应用新技能以完成原始请求。
## OpenSkills 基础
- `npx openskills install <source> [options]` # 从 GitHub、本地路径或私有仓库安装
- `npx openskills sync [-y] [-o <path>]` # 更新 AGENTS.md (或自定义输出)
- `npx openskills list` # 显示已安装的技能
- `npx openskills read <name>` # 加载技能(供智能体 (Agent) 使用)
- `npx openskills update [name...]` # 更新已安装的技能(默认:全部)
- `npx openskills manage` # 移除技能(交互式)
- `npx openskills remove <name>` # 移除特定技能
示例:
- `npx openskills install anthropics/skills`
- `npx openskills sync`
默认设置:安装在项目本地(`./.claude/skills`,或者带 `--universal` 参数安装在 `./.agent/skills`)。使用 `--global` 安装在 `~/.claude/skills`
## 安全提示 (Safety Reminders)
- 未经用户明确确认,请勿安装。
- 除非用户同意覆盖现有目标,否则避免使用 `--force-map`
- 使用 openskills 进行安装/更新OneSkill 仅为 Gemini 提供搜索和映射。
- 对于 Gemini请在安装后运行 `npx oneskill map --target gemini`
- 默认安装/映射是项目本地的,与 openskills 相同;全局安装请使用 `--global`
- 安装 OneSkill 本身时,建议使用 `--global`,以便在跨项目时可用。

2026
.claude/skills/oneskill/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
{
"name": "oneskill",
"version": "0.1.0",
"description": "Meta-skill manager for AI coding agents",
"type": "module",
"main": "./dist/cli.js",
"bin": {
"oneskill": "dist/cli.js"
},
"files": [
"dist",
"README.md",
"SKILL.md"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"keywords": [
"skills",
"ai",
"agents",
"meta-skill",
"codex",
"gemini",
"claude"
],
"author": "OneSkill Contributors",
"license": "Apache-2.0",
"engines": {
"node": ">=20.6.0"
},
"dependencies": {
"chalk": "^5.6.2",
"commander": "^12.1.0"
},
"devDependencies": {
"@types/node": "^24.9.1",
"tsup": "^8.5.0",
"typescript": "^5.9.3",
"vitest": "^4.0.3"
}
}

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { runSearch } from './commands/search.js';
import { runInfo } from './commands/info.js';
import { runList } from './commands/list.js';
import { runDoctor } from './commands/doctor.js';
import { runSync } from './commands/sync.js';
import { runMap } from './commands/map.js';
import { getOneskillVersion } from './core/versions.js';
const program = new Command();
program
.name('oneskill')
.description('Meta-skill manager for AI coding agents')
.version(getOneskillVersion());
program
.command('search <query>')
.description('Search the skill registry')
.option('--registry <url>', 'Registry base URL override')
.option('--category <slug>', 'Filter by category slug')
.option('--limit <n>', 'Results per page (max 100)', (value) => Number.parseInt(value, 10))
.option('--offset <n>', 'Pagination offset', (value) => Number.parseInt(value, 10))
.option('--sort <value>', 'Sort by: votes, recent, stars')
.action(async (query: string, options: { registry?: string; category?: string; limit?: number; offset?: number; sort?: string }) => {
await runSearch(query, options);
});
program
.command('info <slug>')
.description('Fetch skill info from registry')
.option('--registry <url>', 'Registry base URL override')
.action(async (slug: string, options: { registry?: string }) => {
await runInfo(slug, options);
});
program
.command('sync')
.description('Forward to openskills sync')
.option('-y, --yes', 'Skip interactive selection, sync all skills')
.option('-o, --output <path>', 'Output file path (default: AGENTS.md)')
.action(async (options: { yes?: boolean; output?: string }) => {
await runSync(options);
});
program
.command('map')
.description('Map installed skills into Gemini directory')
.option('--target <target>', 'Target environment (gemini only)')
.option('--global', 'Map from global openskills install', false)
.option('--universal', 'Map from universal (.agent/skills)', false)
.option('--force-map', 'Overwrite target mapping if it exists', false)
.action(async (options: { target?: string; global?: boolean; universal?: boolean; forceMap?: boolean }) => {
await runMap({
target: options.target as 'gemini' | undefined,
global: options.global,
universal: options.universal,
forceMap: options.forceMap,
});
});
program
.command('list')
.description('List managed skills')
.option('--root <path>', 'Override workspace root')
.action(async (options: { root?: string }) => {
await runList(options);
});
program
.command('doctor')
.description('Diagnose OneSkill environment')
.option('--root <path>', 'Override workspace root')
.action(async (options: { root?: string }) => {
await runDoctor(options);
});
program.parseAsync(process.argv).catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});

View File

@@ -0,0 +1,19 @@
import { detectRoot } from '../core/root.js';
import { printJson } from '../utils/json.js';
export interface DoctorCommandOptions {
root?: string;
}
export async function runDoctor(options: DoctorCommandOptions): Promise<void> {
const rootInfo = detectRoot(options.root || process.cwd());
const root = rootInfo.root;
const paths = {
agent: `${root}/.agent/skills`,
claude: `${root}/.claude/skills`,
gemini: `${root}/.gemini/skills`,
codex: `${root}/.codex/skills`,
oneskillLogs: `${root}/.oneskill/logs`,
};
printJson({ schemaVersion: '1', root: rootInfo.root, reason: rootInfo.reason, paths });
}

View File

@@ -0,0 +1,11 @@
import { fetchRegistryInfo } from '../core/registry.js';
import { printJson } from '../utils/json.js';
export interface InfoCommandOptions {
registry?: string;
}
export async function runInfo(slug: string, options: InfoCommandOptions): Promise<void> {
const result = await fetchRegistryInfo(slug, options.registry);
printJson({ schemaVersion: '1', item: result.item });
}

View File

@@ -0,0 +1,16 @@
import { spawnSync } from 'child_process';
import { resolveOpenskillsCli } from '../core/openskills.js';
export interface ListCommandOptions {}
export async function runList(_options: ListCommandOptions): Promise<void> {
const cliPath = resolveOpenskillsCli();
const args = [cliPath, 'list'];
const result = spawnSync(process.execPath, args, {
cwd: process.cwd(),
stdio: 'inherit',
});
if (result.status !== 0) {
throw new Error('openskills list failed');
}
}

View File

@@ -0,0 +1,23 @@
import type { TargetEnvironment } from '../core/types.js';
import { mapInstalledSkills } from '../core/map.js';
import { printJson } from '../utils/json.js';
export interface MapCommandOptions {
target?: TargetEnvironment;
global?: boolean;
universal?: boolean;
forceMap?: boolean;
}
export async function runMap(options: MapCommandOptions): Promise<void> {
if (!options.target) {
throw new Error('map requires --target gemini');
}
const result = mapInstalledSkills({
target: options.target,
global: options.global,
universal: options.universal,
forceMap: options.forceMap,
});
printJson({ schemaVersion: '1', ...result });
}

View File

@@ -0,0 +1,32 @@
import { searchRegistry } from '../core/registry.js';
import { printJson } from '../utils/json.js';
export interface SearchCommandOptions {
registry?: string;
category?: string;
limit?: number;
offset?: number;
sort?: string;
}
export async function runSearch(query: string, options: SearchCommandOptions): Promise<void> {
const result = await searchRegistry(
{
q: query,
category: options.category,
limit: options.limit,
offset: options.offset,
sort: options.sort,
},
options.registry
);
const raw = result.raw as { registry?: unknown; version?: unknown; pagination?: unknown } | undefined;
printJson({
schemaVersion: '1',
query,
registry: raw?.registry,
version: raw?.version,
pagination: raw?.pagination,
items: result.items,
});
}

View File

@@ -0,0 +1,25 @@
import { spawnSync } from 'child_process';
import { resolveOpenskillsCli } from '../core/openskills.js';
export interface SyncCommandOptions {
yes?: boolean;
output?: string;
}
export async function runSync(options: SyncCommandOptions): Promise<void> {
const cliPath = resolveOpenskillsCli();
const args = [cliPath, 'sync'];
if (options.yes) {
args.push('--yes');
}
if (options.output) {
args.push('--output', options.output);
}
const result = spawnSync(process.execPath, args, {
cwd: process.cwd(),
stdio: 'inherit',
});
if (result.status !== 0) {
throw new Error('openskills sync failed');
}
}

View File

@@ -0,0 +1,24 @@
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
import { dirname, resolve, sep } from 'path';
export function ensureDir(path: string): void {
mkdirSync(path, { recursive: true });
}
export function readJsonFile<T>(path: string): T | null {
if (!existsSync(path)) return null;
const content = readFileSync(path, 'utf-8');
return JSON.parse(content) as T;
}
export function writeJsonFile(path: string, data: unknown): void {
ensureDir(dirname(path));
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf-8');
}
export function isPathInside(targetPath: string, baseDir: string): boolean {
const resolvedTarget = resolve(targetPath);
const resolvedBase = resolve(baseDir);
const baseWithSep = resolvedBase.endsWith(sep) ? resolvedBase : resolvedBase + sep;
return resolvedTarget === resolvedBase || resolvedTarget.startsWith(baseWithSep);
}

View File

@@ -0,0 +1,23 @@
import { join } from 'path';
import { readJsonFile, writeJsonFile, ensureDir } from './fs.js';
import type { LockFile, LockedSkill } from './types.js';
const LOCK_FILE_NAME = 'lock.json';
export function getLockPath(root: string): string {
return join(root, '.oneskill', LOCK_FILE_NAME);
}
export function readLock(root: string): LockFile | null {
return readJsonFile<LockFile>(getLockPath(root));
}
export function writeLock(root: string, lock: LockFile): void {
ensureDir(join(root, '.oneskill'));
writeJsonFile(getLockPath(root), lock);
}
export function upsertLockSkill(lock: LockFile, skill: LockedSkill): void {
lock.skills[skill.id] = skill;
lock.updatedAt = new Date().toISOString();
}

View File

@@ -0,0 +1,11 @@
import { appendFileSync } from 'fs';
import { join } from 'path';
import { ensureDir } from './fs.js';
export function appendLog(root: string, event: Record<string, unknown>): void {
const logDir = join(root, '.oneskill', 'logs');
ensureDir(logDir);
const date = new Date().toISOString().slice(0, 10);
const logPath = join(logDir, `${date}.jsonl`);
appendFileSync(logPath, JSON.stringify(event) + '\n', 'utf-8');
}

View File

@@ -0,0 +1,33 @@
import { lstatSync, readdirSync } from 'fs';
import { join } from 'path';
import type { ManifestSummary } from './types.js';
export function buildManifestSummary(root: string): ManifestSummary {
let files = 0;
let bytes = 0;
let hasSymlinks = false;
const walk = (dir: string): void => {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const stat = lstatSync(fullPath);
if (stat.isSymbolicLink()) {
hasSymlinks = true;
files += 1;
continue;
}
if (stat.isDirectory()) {
walk(fullPath);
continue;
}
if (stat.isFile()) {
files += 1;
bytes += stat.size;
}
}
};
walk(root);
return { files, bytes, hasSymlinks };
}

View File

@@ -0,0 +1,49 @@
import { existsSync, readdirSync } from 'fs';
import { basename, join } from 'path';
import { homedir } from 'os';
import { mapSkill } from './mapping.js';
import { detectRoot } from './root.js';
import type { TargetEnvironment } from './types.js';
export interface MapOptions {
target: TargetEnvironment;
global?: boolean;
universal?: boolean;
forceMap?: boolean;
}
function getSourceBase(root: string, options: MapOptions): string {
const baseRoot = options.global ? homedir() : root;
const folder = options.universal ? '.agent/skills' : '.claude/skills';
return join(baseRoot, folder);
}
function getTargetRoot(root: string, options: MapOptions): string {
return options.global ? homedir() : root;
}
export function mapInstalledSkills(options: MapOptions): { mapped: number; sourceBase: string; targetRoot: string } {
if (options.target !== 'gemini') {
throw new Error('map currently supports only --target gemini');
}
const rootInfo = detectRoot(process.cwd());
const sourceBase = getSourceBase(rootInfo.root, options);
const targetRoot = getTargetRoot(rootInfo.root, options);
if (!existsSync(sourceBase)) {
return { mapped: 0, sourceBase, targetRoot };
}
const entries = readdirSync(sourceBase, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => join(sourceBase, entry.name));
let mapped = 0;
for (const dir of entries) {
const name = basename(dir);
mapSkill(targetRoot, 'gemini', name, dir, { forceMap: options.forceMap });
mapped += 1;
}
return { mapped, sourceBase, targetRoot };
}

View File

@@ -0,0 +1,85 @@
import { existsSync, lstatSync, readlinkSync, rmSync, symlinkSync, cpSync, writeFileSync } from 'fs';
import { join, resolve } from 'path';
import { ensureDir, isPathInside } from './fs.js';
import type { SkillMappingRecord, TargetEnvironment } from './types.js';
const TARGET_DIRS: Record<TargetEnvironment, string> = {
codex: '.codex/skills',
gemini: '.gemini/skills',
claude: '.claude/skills',
agent: '.agent/skills',
};
export interface MapOptions {
forceMap?: boolean;
codexHome?: string;
}
function getTargetBase(root: string, target: TargetEnvironment, options?: MapOptions): string {
if (target === 'codex' && options?.codexHome) {
return resolve(options.codexHome, 'skills');
}
return join(root, TARGET_DIRS[target]);
}
function readLinkTarget(path: string): string | null {
try {
return readlinkSync(path);
} catch {
return null;
}
}
export function mapSkill(
root: string,
target: TargetEnvironment,
skillName: string,
storePath: string,
options?: MapOptions
): SkillMappingRecord {
const baseDir = getTargetBase(root, target, options);
const targetPath = join(baseDir, skillName);
if (target !== 'codex' && !isPathInside(targetPath, root)) {
throw new Error(`Target path escapes root: ${targetPath}`);
}
ensureDir(baseDir);
if (existsSync(targetPath)) {
const stat = lstatSync(targetPath);
if (stat.isSymbolicLink()) {
const linkTarget = readLinkTarget(targetPath);
if (linkTarget && resolve(linkTarget) === resolve(storePath)) {
return { target, path: targetPath, mode: process.platform === 'win32' ? 'junction' : 'symlink', updatedAt: new Date().toISOString() };
}
if (!options?.forceMap) {
throw new Error(`Target already exists (symlink): ${targetPath}`);
}
} else {
if (!options?.forceMap) {
throw new Error(`Target already exists: ${targetPath}`);
}
}
rmSync(targetPath, { recursive: true, force: true });
}
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
try {
symlinkSync(storePath, targetPath, linkType);
return { target, path: targetPath, mode: process.platform === 'win32' ? 'junction' : 'symlink', updatedAt: new Date().toISOString() };
} catch (error) {
if (!options?.forceMap) {
// Fall through to copy on Windows permission errors or other failures.
}
}
cpSync(storePath, targetPath, { recursive: true, dereference: false });
writeFileSync(
join(targetPath, '.oneskill-meta.json'),
JSON.stringify({ source: storePath, mappedAt: new Date().toISOString(), mode: 'copy' }, null, 2) + '\n',
'utf-8'
);
return { target, path: targetPath, mode: 'copy', updatedAt: new Date().toISOString() };
}

View File

@@ -0,0 +1,42 @@
import { readFileSync, existsSync } from 'fs';
import { dirname, resolve } from 'path';
import { createRequire } from 'module';
import { spawnSync } from 'child_process';
const require = createRequire(import.meta.url);
export function resolveOpenskillsCli(): string {
const pkgPath = require.resolve('openskills/package.json');
const pkgDir = dirname(pkgPath);
const content = readFileSync(pkgPath, 'utf-8');
const parsed = JSON.parse(content) as { bin?: Record<string, string> | string; main?: string };
const bin = parsed.bin;
let candidate: string | null = null;
if (typeof bin === 'string') {
candidate = resolve(pkgDir, bin);
} else if (bin && typeof bin === 'object') {
const first = Object.values(bin)[0];
if (first) candidate = resolve(pkgDir, first);
} else if (parsed.main) {
candidate = resolve(pkgDir, parsed.main);
}
if (candidate && existsSync(candidate)) {
return candidate;
}
// Build openskills locally if the CLI entry is missing.
const result = spawnSync('npm', ['run', 'build'], {
cwd: pkgDir,
stdio: 'inherit',
});
if (result.status !== 0) {
throw new Error('Failed to build openskills');
}
if (candidate && existsSync(candidate)) {
return candidate;
}
throw new Error('Unable to resolve openskills CLI entry');
}

View File

@@ -0,0 +1,110 @@
import type { RegistryInfoResponse, RegistrySearchResponse, SkillListItem } from './types.js';
const DEFAULT_REGISTRY_URL = 'https://skillsdirectory.com/api/registry';
export interface SearchParams {
q?: string;
category?: string;
limit?: number;
offset?: number;
sort?: string;
}
function getRegistryBase(override?: string): string {
return (override || process.env.ONESKILL_REGISTRY_URL || DEFAULT_REGISTRY_URL).replace(/\/$/, '');
}
async function fetchJson(url: string): Promise<unknown> {
const res = await fetch(url, { headers: { 'accept': 'application/json' } });
if (!res.ok) {
const text = await res.text();
throw new Error(`Registry request failed (${res.status}): ${text.slice(0, 200)}`);
}
return res.json();
}
function normalizeSkill(item: Record<string, unknown>): SkillListItem {
const slug = String(item.slug || item.id || item.name || '');
const name = String(item.name || item.title || slug || '');
const description = String(item.description || item.summary || '');
const repository = String(item.repository || item.repo || item.url || '');
const verified = typeof item.verified === 'boolean' ? item.verified : undefined;
const stars =
typeof item.stars === 'number'
? item.stars
: typeof (item.github as { stars?: unknown } | undefined)?.stars === 'number'
? (item.github as { stars: number }).stars
: undefined;
const tags = Array.isArray(item.tags) ? item.tags.map(String) : undefined;
const authorObj = item.author as { name?: unknown; url?: unknown } | undefined;
const authorName = typeof item.author === 'string' ? String(item.author) : authorObj?.name ? String(authorObj.name) : undefined;
const author = authorName
? { name: authorName, url: authorObj?.url ? String(authorObj.url) : undefined }
: undefined;
const signals = {
lastUpdated: item.lastUpdated ? String(item.lastUpdated) : undefined,
license:
item.license ? String(item.license)
: typeof (item.github as { license?: unknown } | undefined)?.license === 'string'
? String((item.github as { license: string }).license)
: undefined,
riskHints: Array.isArray(item.riskHints) ? item.riskHints.map(String) : undefined,
};
return {
schemaVersion: '1',
slug,
name,
description,
repository,
verified,
stars,
tags,
author,
signals,
};
}
function normalizeSearchPayload(payload: unknown): SkillListItem[] {
if (!payload || typeof payload !== 'object') return [];
const data = payload as Record<string, unknown>;
const candidates =
(Array.isArray(data.skills) ? data.skills : null) ||
(Array.isArray(data.items) ? data.items : null) ||
(Array.isArray(data.results) ? data.results : null) ||
(Array.isArray(data.data) ? data.data : null) ||
(Array.isArray(payload) ? (payload as unknown[]) : null);
if (!candidates) return [];
return candidates
.filter((item) => item && typeof item === 'object')
.map((item) => normalizeSkill(item as Record<string, unknown>));
}
export async function searchRegistry(params: SearchParams, overrideUrl?: string): Promise<RegistrySearchResponse> {
const base = getRegistryBase(overrideUrl);
const url = new URL(base);
if (params.q) url.searchParams.set('q', params.q);
if (params.category) url.searchParams.set('category', params.category);
if (typeof params.limit === 'number') url.searchParams.set('limit', String(params.limit));
if (typeof params.offset === 'number') url.searchParams.set('offset', String(params.offset));
if (params.sort) url.searchParams.set('sort', params.sort);
const raw = await fetchJson(url.toString());
const items = normalizeSearchPayload(raw);
return { items, raw };
}
export async function fetchRegistryInfo(slug: string, overrideUrl?: string): Promise<RegistryInfoResponse> {
const base = getRegistryBase(overrideUrl);
const url = `${base}/${encodeURIComponent(slug)}`;
const raw = await fetchJson(url);
if (raw && typeof raw === 'object') {
const record = raw as Record<string, unknown>;
const candidate = (record.skill as Record<string, unknown> | undefined) || (record.item as Record<string, unknown> | undefined) || record;
if (candidate && typeof candidate === 'object') {
return { item: normalizeSkill(candidate as Record<string, unknown>), raw };
}
}
throw new Error('Registry info failed');
}

View File

@@ -0,0 +1,38 @@
import { existsSync, readFileSync } from 'fs';
import { dirname, join, resolve } from 'path';
import type { RootDetection } from './types.js';
const MARKER_DIRS = ['.git', '.agent', '.claude', '.gemini', '.codex'];
function hasOneskillConfig(pkgPath: string): boolean {
try {
const content = readFileSync(pkgPath, 'utf-8');
const parsed = JSON.parse(content) as { oneskill?: unknown };
return Boolean(parsed.oneskill);
} catch {
return false;
}
}
function hasMarkers(dir: string): string | null {
for (const marker of MARKER_DIRS) {
if (existsSync(join(dir, marker))) return marker;
}
if (existsSync(join(dir, 'AGENTS.md'))) return 'AGENTS.md';
const pkgPath = join(dir, 'package.json');
if (existsSync(pkgPath) && hasOneskillConfig(pkgPath)) return 'package.json:oneskill';
return null;
}
export function detectRoot(startDir: string): RootDetection {
let current = resolve(startDir);
while (true) {
const reason = hasMarkers(current);
if (reason) return { root: current, reason };
const parent = dirname(current);
if (parent === current) {
return { root: resolve(startDir), reason: 'fallback:cwd' };
}
current = parent;
}
}

View File

@@ -0,0 +1,81 @@
export type TargetEnvironment = 'codex' | 'gemini' | 'claude' | 'agent';
export interface SkillListItem {
schemaVersion: '1';
slug: string;
name: string;
description: string;
repository: string;
verified?: boolean;
stars?: number;
tags?: string[];
author?: {
name: string;
url?: string;
};
signals?: {
lastUpdated?: string;
license?: string;
riskHints?: string[];
};
}
export interface RegistryInfoResponse {
item: SkillListItem;
raw: unknown;
}
export interface RegistrySearchResponse {
items: SkillListItem[];
raw: unknown;
}
export interface ManifestSummary {
files: number;
bytes: number;
hasSymlinks: boolean;
}
export interface SkillSource {
input: string;
type: 'slug' | 'repository' | 'local';
repository?: string;
ref?: string;
}
export interface SkillMappingRecord {
target: TargetEnvironment;
path: string;
mode: 'symlink' | 'junction' | 'copy';
updatedAt: string;
}
export interface LockedSkill {
id: string;
name: string;
source: SkillSource;
installedAt: string;
storePath: string;
manifest: ManifestSummary;
mappings: SkillMappingRecord[];
}
export interface LockFile {
schemaVersion: '1';
root: string;
oneskillVersion: string;
openskillsVersion: string;
updatedAt: string;
skills: Record<string, LockedSkill>;
}
export interface RootDetection {
root: string;
reason: string;
}
export interface InstallResult {
root: string;
skills: LockedSkill[];
target?: TargetEnvironment;
}

View File

@@ -0,0 +1,29 @@
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
export function getOneskillVersion(): string {
const __dirname = dirname(fileURLToPath(import.meta.url));
const pkgPath = join(__dirname, '../../package.json');
try {
const content = readFileSync(pkgPath, 'utf-8');
const parsed = JSON.parse(content) as { version?: string };
return parsed.version || '0.0.0';
} catch {
return '0.0.0';
}
}
export function getOpenskillsVersion(): string {
try {
const pkgPath = require.resolve('openskills/package.json');
const content = readFileSync(pkgPath, 'utf-8');
const parsed = JSON.parse(content) as { version?: string };
return parsed.version || '0.0.0';
} catch {
return '0.0.0';
}
}

View File

@@ -0,0 +1,3 @@
export function printJson(data: unknown): void {
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
}

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"declaration": true,
"skipLibCheck": true,
"esModuleInterop": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/cli.ts'],
format: ['esm'],
dts: false,
sourcemap: true,
clean: true,
target: 'node20'
});