Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
86238d4
refactor: extract getErrorMessage and DEFAULT_DAEMON_PORT to shared m…
shouldnotappearcalm Mar 23, 2026
ff162ad
docs(skill): reorganize architecture by domain taxonomy
shouldnotappearcalm Mar 25, 2026
a7dddfb
docs(skill): align skill-architecture to Anthropic skill spec
shouldnotappearcalm Mar 25, 2026
fc05b79
feat(skill): add standalone platform-based opencli skill
shouldnotappearcalm Mar 25, 2026
22a9999
refactor(skill): move to skills dir and complete platform command docs
shouldnotappearcalm Mar 25, 2026
1aefd7b
docs(skill): generate full platform command docs from source
shouldnotappearcalm Mar 25, 2026
afdfb1d
docs(skill): simplify command docs to usage and args only
shouldnotappearcalm Mar 25, 2026
d352205
docs(skill): normalize all command docs to usage and parameter meanings
shouldnotappearcalm Mar 25, 2026
68a16d5
docs(skill): align SKILL frontmatter and remove workflow references
shouldnotappearcalm Mar 25, 2026
71b8356
docs(skill): add supported capability summary to description
shouldnotappearcalm Mar 25, 2026
f324311
docs(skill): add setup prerequisites and manual extension install guide
shouldnotappearcalm Mar 25, 2026
787919f
docs(skill): add install bootstrap and doctor/extension guidance in S…
shouldnotappearcalm Mar 25, 2026
361bf72
docs(skill): clarify chrome extension install location from README
shouldnotappearcalm Mar 25, 2026
030ba84
docs: add skill install guide to READMEs and enhance root SKILL recor…
shouldnotappearcalm Mar 25, 2026
1891dea
docs(skill): remove skill README and add record workflow to SKILL
shouldnotappearcalm Mar 25, 2026
df00f92
Merge branch 'jackwener:main' into main
shouldnotappearcalm Mar 25, 2026
4281aa5
rename opencli skill and refresh README integration docs
shouldnotappearcalm Mar 25, 2026
875d9f8
chore: remove skill-architecture docs and restore root SKILL.md
shouldnotappearcalm Mar 25, 2026
32efe10
docs(skills): convert opencli-skill command references to English
shouldnotappearcalm Mar 25, 2026
f3b0f31
Merge branch 'jackwener:main' into main
shouldnotappearcalm Mar 26, 2026
164ed7d
docs(skills): add missing command references for dictionary, douyin, …
shouldnotappearcalm Mar 26, 2026
6eb03e6
docs(skill): sync opencli skill docs from source adapters
shouldnotappearcalm Apr 2, 2026
03d259f
merge: resolve upstream conflicts for docs files
shouldnotappearcalm Apr 2, 2026
7b29576
docs(skill): re-sync generated skill docs after upstream merge
shouldnotappearcalm Apr 2, 2026
acd3d80
docs(readme): restore assistant usage guidance for opencli-skill refs
shouldnotappearcalm Apr 2, 2026
0b8aff4
docs: align README files with upstream
shouldnotappearcalm Apr 2, 2026
399caf1
docs(readme): add concise opencli-skill reference section
shouldnotappearcalm Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/skill-docs-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: skill-docs-check

on:
pull_request:
paths:
- 'src/clis/**'
- 'skills/opencli-skill/**'
- 'scripts/sync-*.mjs'
- 'scripts/check-*.sh'
- 'package.json'
- '.github/workflows/skill-docs-check.yml'
push:
branches: [main]
paths:
- 'src/clis/**'
- 'skills/opencli-skill/**'
- 'scripts/sync-*.mjs'
- 'scripts/check-*.sh'
- 'package.json'
workflow_dispatch:

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- run: npm run skill:check
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ npx skills add jackwener/opencli --skill opencli-explorer # Adapter developme
npx skills add jackwener/opencli --skill opencli-oneshot # Quick command reference
```

#### OpenCLI Skill (for coding assistants)

If you want a lightweight, prompt-friendly skill package, see:
- https://github.com/joeseesun/opencli-skill

It focuses on three things:
- Natural-language routing for common supported platforms
- Reusing Chrome login session (no API keys)
- Agent-friendly command examples (`-f json` by default)

---

### For Developers
Expand Down
10 changes: 10 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ npx skills add jackwener/opencli --skill opencli-explorer # 适配器开发
npx skills add jackwener/opencli --skill opencli-oneshot # 快速命令参考
```

#### OpenCLI Skill(面向编码助手)

如果你希望一个更轻量、提示词友好的 skill 包,可以看:
- https://github.com/joeseesun/opencli-skill

核心定位:
- 面向常见平台的自然语言命令路由
- 复用 Chrome 登录态(无需 API Key)
- 默认提供更适合 Agent 的 `-f json` 命令示例

## 内置命令

运行 `opencli list` 查看完整注册表。
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@
"test:e2e": "vitest run --project e2e",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
"docs:preview": "vitepress preview docs",
"skill:commands:sync": "node scripts/sync-skill-command-reference.mjs",
"skill:commands:check": "bash scripts/check-skill-command-reference.sh",
"skill:md:sync": "node scripts/sync-skill-md.mjs",
"skill:md:check": "bash scripts/check-skill-md.sh",
"skill:sync": "node scripts/sync-opencli-skill-docs.mjs",
"skill:check": "bash scripts/check-opencli-skill-docs.sh"
},
"keywords": [
"cli",
Expand Down
16 changes: 16 additions & 0 deletions scripts/check-opencli-skill-docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"

node scripts/sync-opencli-skill-docs.mjs >/dev/null

if ! git diff --quiet -- skills/opencli-skill/SKILL.md skills/opencli-skill/references/commands; then
echo "❌ OpenCLI skill docs are out of sync."
echo "Run: node scripts/sync-opencli-skill-docs.mjs"
git --no-pager diff --name-only -- skills/opencli-skill/SKILL.md skills/opencli-skill/references/commands
exit 1
fi

echo "✅ OpenCLI skill docs are in sync."
16 changes: 16 additions & 0 deletions scripts/check-skill-command-reference.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"

node scripts/sync-skill-command-reference.mjs >/dev/null

if ! git diff --quiet -- skills/opencli-skill/references/commands; then
echo "❌ Skill command references are out of sync with src/clis/."
echo "Run: node scripts/sync-skill-command-reference.mjs"
git --no-pager diff --name-only -- skills/opencli-skill/references/commands
exit 1
fi

echo "✅ Skill command references are in sync."
16 changes: 16 additions & 0 deletions scripts/check-skill-md.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"

node scripts/sync-skill-md.mjs >/dev/null

if ! git diff --quiet -- skills/opencli-skill/SKILL.md; then
echo "❌ SKILL.md is out of sync with src/clis/."
echo "Run: node scripts/sync-skill-md.mjs"
git --no-pager diff --name-only -- skills/opencli-skill/SKILL.md
exit 1
fi

echo "✅ SKILL.md is in sync."
14 changes: 14 additions & 0 deletions scripts/sync-opencli-skill-docs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env node
import { execSync } from 'node:child_process';
import path from 'node:path';

const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');

function run(cmd) {
execSync(cmd, { cwd: root, stdio: 'inherit' });
}

run('node scripts/sync-skill-command-reference.mjs');
run('node scripts/sync-skill-md.mjs');

console.log('✅ OpenCLI skill docs synced.');
206 changes: 206 additions & 0 deletions scripts/sync-skill-command-reference.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';

const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
const srcClisDir = path.join(root, 'src', 'clis');
const skillCommandsDir = path.join(root, 'skills', 'opencli-skill', 'references', 'commands');

const IGNORE_BASENAMES = new Set(['utils', 'shared', 'test-utils', 'index']);
const IGNORE_SUFFIXES = ['.test'];
const IGNORE_PATTERNS = [/(^|-)utils?($|-)/, /(^|-)helpers?($|-)/, /(^|-)shared($|-)/];
const VALID_EXTS = new Set(['.ts', '.js', '.yaml', '.yml']);

function normalizeNewlines(s) { return s.replace(/\r\n/g, '\n'); }
function normalizeSpacing(s) {
const lines = normalizeNewlines(s).split('\n');
const out = [];
let blank = 0;
for (const line of lines) {
if (!line.trim()) {
blank += 1;
if (blank <= 1) out.push('');
} else {
blank = 0;
out.push(line.replace(/[ \t]+$/g, ''));
}
}
while (out.length && out[out.length - 1] === '') out.pop();
return out.join('\n') + '\n';
}

function listPlatforms() {
return fs.readdirSync(srcClisDir, { withFileTypes: true })
.filter((d) => d.isDirectory() && !d.name.startsWith('_'))
.map((d) => d.name)
.sort((a, b) => a.localeCompare(b));
}

function listCommandFiles(platform) {
const dir = path.join(srcClisDir, platform);
return fs.readdirSync(dir, { withFileTypes: true })
.filter((e) => e.isFile())
.map((e) => ({ name: e.name, ext: path.extname(e.name), base: path.basename(e.name, path.extname(e.name)), full: path.join(dir, e.name) }))
.filter((f) => VALID_EXTS.has(f.ext))
.filter((f) => !IGNORE_BASENAMES.has(f.base))
.filter((f) => !IGNORE_SUFFIXES.some((s) => f.base.endsWith(s)))
.filter((f) => !IGNORE_PATTERNS.some((p) => p.test(f.base)))
.sort((a, b) => a.base.localeCompare(b.base));
}

function parseDocSections(content) {
const map = new Map();
const regex = /^###\s+([^\n]+)\n([\s\S]*?)(?=\n###\s+|$)/gm;
let m;
while ((m = regex.exec(content)) !== null) {
map.set(m[1].trim(), m[2].replace(/^\n+/, '').replace(/\n+$/, ''));
}
return map;
}

function matchQuoted(text, re) {
const m = text.match(re);
return m?.[2]?.trim();
}

function parseTsMeta(filePath) {
const src = fs.readFileSync(filePath, 'utf8');
const description = matchQuoted(src, /\bdescription\s*:\s*(["'`])([\s\S]*?)\1/);

const args = [];
const argsBlock = src.match(/\bargs\s*:\s*\[([\s\S]*?)\]\s*,\s*(?:columns|func)\s*:/);
if (argsBlock) {
const rowRegex = /\{([\s\S]*?)\}/g;
let row;
while ((row = rowRegex.exec(argsBlock[1])) !== null) {
const body = row[1];
const name = matchQuoted(body, /\bname\s*:\s*(["'`])([\s\S]*?)\1/);
if (!name) continue;
const required = /\brequired\s*:\s*true\b/.test(body);
const positional = /\bpositional\s*:\s*true\b/.test(body);
const type = matchQuoted(body, /\btype\s*:\s*(["'`])([\s\S]*?)\1/);
const def = body.match(/\bdefault\s*:\s*([^,\n}]+)/)?.[1]?.trim();
const help = matchQuoted(body, /\bhelp\s*:\s*(["'`])([\s\S]*?)\1/);
args.push({ name, required, positional, type, defaultValue: def, help });
}
}

return { description, args };
}

function parseYamlMeta(filePath) {
const src = fs.readFileSync(filePath, 'utf8');
const description = src.match(/^description:\s*(.+)$/m)?.[1]?.trim()?.replace(/^['"]|['"]$/g, '');

const args = [];
const lines = src.split(/\r?\n/);
let inArgs = false;
let cur = null;
for (const raw of lines) {
const line = raw.replace(/\t/g, ' ');
if (!inArgs) {
if (/^args:\s*$/.test(line)) inArgs = true;
continue;
}
if (/^[A-Za-z_][\w-]*:\s*/.test(line)) break;
const itemStart = line.match(/^\s*-\s+name:\s*(.+)\s*$/);
if (itemStart) {
if (cur?.name) args.push(cur);
cur = { name: itemStart[1].trim().replace(/^['"]|['"]$/g, ''), required: false, positional: false };
continue;
}
if (!cur) continue;
const kv = line.match(/^\s+([\w-]+):\s*(.+)\s*$/);
if (!kv) continue;
const key = kv[1];
const val = kv[2].trim();
if (key === 'required') cur.required = /^true$/i.test(val);
else if (key === 'positional') cur.positional = /^true$/i.test(val);
else if (key === 'type') cur.type = val.replace(/^['"]|['"]$/g, '');
else if (key === 'default') cur.defaultValue = val;
else if (key === 'help') cur.help = val.replace(/^['"]|['"]$/g, '');
}
if (cur?.name) args.push(cur);

return { description, args };
}

function parseCommandMeta(file) {
try {
if (file.ext === '.yaml' || file.ext === '.yml') return parseYamlMeta(file.full);
if (file.ext === '.ts' || file.ext === '.js') return parseTsMeta(file.full);
} catch {}
return { description: undefined, args: [] };
}

function renderArgs(args) {
if (!args.length) return '- Args: None';
return ['- Args:', ...args.map((a) => {
const tags = [a.required ? 'required' : 'optional'];
if (a.type) tags.push(`type: ${a.type}`);
if (a.defaultValue !== undefined) tags.push(`default: ${a.defaultValue}`);
const suffix = a.help ? `; ${a.help}` : '';
return ` - \`${a.name}\`(${tags.join('; ')})${suffix}`;
})].join('\n');
}

function existingIsTodo(body) {
if (!body) return true;
return /^- Purpose:\s*(TODO|.+ operation)\b/m.test(body);
}

function buildSection(platform, cmd, existingBody, meta) {
const purpose = meta.description || `${platform} ${cmd} operation`;
const usage = `- Usage: \`opencli ${platform} ${cmd} [options] -f json\``;
const argsBlock = renderArgs(meta.args);

if (existingBody && existingBody.trim() && !existingIsTodo(existingBody)) {
const body = existingBody.trim();
const lines = body.split('\n');
const hasPurpose = lines.some((l) => l.startsWith('- Purpose:'));
const hasArgs = lines.some((l) => l.startsWith('- Args:'));
const hasUsage = lines.some((l) => l.startsWith('- Usage:'));

const out = [...lines];
if (!hasPurpose) out.unshift(`- Purpose: ${purpose}`);
if (!hasArgs) out.push(argsBlock);
if (!hasUsage) out.push(usage);
return `### ${cmd}\n${out.join('\n').trim()}\n`;
}

return `### ${cmd}\n- Purpose: ${purpose}\n${argsBlock}\n${usage}\n`;
}

function buildDoc(platform, files, oldContent) {
const existing = oldContent ? parseDocSections(oldContent) : new Map();
const parts = [`# ${platform}`, '', '## Commands', ''];

for (const file of files) {
const meta = parseCommandMeta(file);
parts.push(buildSection(platform, file.base, existing.get(file.base), meta).trimEnd());
parts.push('');
}

return normalizeSpacing(parts.join('\n'));
}

fs.mkdirSync(skillCommandsDir, { recursive: true });
const updated = [];

for (const platform of listPlatforms()) {
const files = listCommandFiles(platform);
const docPath = path.join(skillCommandsDir, `${platform}.md`);
const oldContent = fs.existsSync(docPath) ? normalizeNewlines(fs.readFileSync(docPath, 'utf8')) : '';
const next = buildDoc(platform, files, oldContent);
if (normalizeSpacing(oldContent || '') !== next) {
fs.writeFileSync(docPath, next, 'utf8');
updated.push(path.relative(root, docPath));
}
}

if (updated.length) {
console.log(`Updated ${updated.length} command reference files:`);
for (const f of updated) console.log(`- ${f}`);
} else {
console.log('Command references already in sync.');
}
Loading
Loading