Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"files": [
"dist/src/",
"clis/",
"skills/webcmd-*/**",
"skills/webcmd/**",
"skill-data/**",
"cli-manifest.json",
"scripts/",
"README.md",
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
27 changes: 27 additions & 0 deletions skills/webcmd/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
name: webcmd
description: Use Webcmd when an agent needs deterministic CLI access to websites, browser sessions, desktop apps, or local tools. Prefer Webcmd adapters before raw browser control, and load current workflow instructions from the installed CLI.
allowed-tools: Bash(webcmd:*), Bash(npx webcmd:*)
---

# Webcmd

This is a discovery skill. It keeps installation to one skill and points agents at version-matched instructions from the installed CLI.

## Start Here

```bash
webcmd skills list
webcmd skills get webcmd-usage
```

Use `webcmd skills get <name>` for specialized workflows:

- `webcmd-browser` for ad-hoc browser control.
- `webcmd-adapter-author` for writing reusable adapters.
- `webcmd-autofix` for repairing broken adapters.
- `webcmd-browser-sitemap` for sitemap-aware browser work.
- `webcmd-sitemap-author` for capturing reusable site knowledge.
- `smart-search` for search and research routing.

The CLI serves skill content that matches its installed version, so do not rely on copied stale instructions.
17 changes: 14 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -821,11 +821,21 @@ export function createProgram(BUILTIN_CLIS: string, USER_CLIS: string): Command

const skillsCmd = program
.command('skills')
.description('Read bundled Webcmd skills');
.description('Read bundled Webcmd skills')
.action(() => {
const rows = listWebcmdSkills();
renderOutput(rows, {
fmt: 'table',
fmtExplicit: false,
columns: ['name', 'description', 'version', 'path'],
title: 'webcmd/skills/list',
source: 'webcmd skills',
});
});

skillsCmd
.command('list')
.description('List bundled webcmd-* skills')
.description('List bundled Webcmd skills')
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
.action((opts) => {
const rows = listWebcmdSkills();
Expand All @@ -840,7 +850,8 @@ export function createProgram(BUILTIN_CLIS: string, USER_CLIS: string): Command

skillsCmd
.command('read')
.description("Print a bundled webcmd-* skill's SKILL.md or reference file")
.alias('get')
.description("Print a bundled Webcmd skill's SKILL.md or reference file")
.argument('<skill>', 'Skill name, or skill/path like webcmd-browser/references/foo.md')
.argument('[path]', 'Path under the skill directory')
.option('--json', 'Output a JSON envelope instead of raw markdown', false)
Expand Down
25 changes: 15 additions & 10 deletions src/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { listWebcmdSkills, readWebcmdSkill } from './skills.js';

function makePackageRoot(): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-skills-'));
fs.mkdirSync(path.join(root, 'skills', 'webcmd-browser', 'references'), { recursive: true });
fs.mkdirSync(path.join(root, 'skills', 'webcmd-autofix'), { recursive: true });
fs.mkdirSync(path.join(root, 'skills', 'smart-search'), { recursive: true });
fs.mkdirSync(path.join(root, 'skill-data', 'webcmd-browser', 'references'), { recursive: true });
fs.mkdirSync(path.join(root, 'skill-data', 'webcmd-autofix'), { recursive: true });
fs.mkdirSync(path.join(root, 'skill-data', 'smart-search'), { recursive: true });
fs.writeFileSync(path.join(root, 'package.json'), '{"name":"@agentrhq/webcmd"}\n');
fs.writeFileSync(path.join(root, 'skills', 'webcmd-browser', 'SKILL.md'), [
fs.writeFileSync(path.join(root, 'skill-data', 'webcmd-browser', 'SKILL.md'), [
'---',
'name: webcmd-browser',
'description: Browser control skill',
Expand All @@ -23,15 +23,15 @@ function makePackageRoot(): string {
'Body.',
'',
].join('\n'));
fs.writeFileSync(path.join(root, 'skills', 'webcmd-browser', 'references', 'targets.md'), '# Targets\n');
fs.writeFileSync(path.join(root, 'skills', 'webcmd-autofix', 'SKILL.md'), [
fs.writeFileSync(path.join(root, 'skill-data', 'webcmd-browser', 'references', 'targets.md'), '# Targets\n');
fs.writeFileSync(path.join(root, 'skill-data', 'webcmd-autofix', 'SKILL.md'), [
'---',
'name: webcmd-autofix',
'description: Fix adapters: keep scope narrow',
'---',
'',
].join('\n'));
fs.writeFileSync(path.join(root, 'skills', 'smart-search', 'SKILL.md'), [
fs.writeFileSync(path.join(root, 'skill-data', 'smart-search', 'SKILL.md'), [
'---',
'name: smart-search',
'description: Search skill',
Expand All @@ -42,10 +42,11 @@ function makePackageRoot(): string {
}

describe('webcmd skills content', () => {
it('lists only webcmd-prefixed skills', () => {
it('lists bundled skills', () => {
const root = makePackageRoot();

expect(listWebcmdSkills(root).map((skill) => skill.name)).toEqual([
'smart-search',
'webcmd-autofix',
'webcmd-browser',
]);
Expand All @@ -68,10 +69,14 @@ describe('webcmd skills content', () => {
expect(readWebcmdSkill('webcmd-browser', 'references/targets.md', root).content).toBe('# Targets\n');
});

it('rejects non-webcmd skills and path traversal', () => {
it('rejects unknown skills and path traversal', () => {
const root = makePackageRoot();

expect(() => readWebcmdSkill('smart-search', '', root)).toThrow(ArgumentError);
expect(readWebcmdSkill('smart-search', '', root)).toMatchObject({
skill: 'smart-search',
path: 'SKILL.md',
});
expect(() => readWebcmdSkill('not-real', '', root)).toThrow(ArgumentError);
expect(() => readWebcmdSkill('webcmd-browser/../smart-search/SKILL.md', '', root)).toThrow(ArgumentError);
expect(() => readWebcmdSkill('webcmd-browser', '../../package.json', root)).toThrow(ArgumentError);
});
Expand Down
10 changes: 4 additions & 6 deletions src/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,23 @@ interface SkillFrontmatter {
}

export function getSkillsRoot(packageRoot: string = findPackageRoot(MODULE_FILE)): string {
return path.join(packageRoot, 'skills');
const skillDataRoot = path.join(packageRoot, 'skill-data');
return isDirectory(skillDataRoot) ? skillDataRoot : path.join(packageRoot, 'skills');
}

export function listWebcmdSkills(packageRoot?: string): WebcmdSkillInfo[] {
const skillsRoot = getSkillsRoot(packageRoot);
if (!fs.existsSync(skillsRoot)) return [];

return fs.readdirSync(skillsRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name.startsWith('webcmd-'))
.filter((entry) => entry.isDirectory())
.map((entry) => readSkillInfo(skillsRoot, entry.name))
.filter((entry): entry is WebcmdSkillInfo => entry !== null)
.sort((a, b) => a.name.localeCompare(b.name));
}

export function readWebcmdSkill(target: string, relpath = '', packageRoot?: string): WebcmdSkillReadResult {
const { name, pathInSkill } = parseSkillTarget(target, relpath);
if (!name.startsWith('webcmd-')) {
throw new ArgumentError(`Unknown Webcmd skill: ${name}`, 'Run "webcmd skills list" to see available Webcmd skills.');
}

const skillsRoot = getSkillsRoot(packageRoot);
const skillRoot = path.join(skillsRoot, name);
Expand All @@ -60,7 +58,7 @@ export function readWebcmdSkill(target: string, relpath = '', packageRoot?: stri
throw new ArgumentError(`Invalid skill path: ${relativePath}`, 'Skill paths must stay inside the selected Webcmd skill.');
}
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
throw new ArgumentError(`Skill file not found: ${name}/${relativePath}`, 'Run "webcmd skills list <skill>" is not supported yet; read SKILL.md or a known references/... file.');
throw new ArgumentError(`Skill file not found: ${name}/${relativePath}`, 'Run "webcmd skills read <skill>" or read a known references/... file.');
}

return {
Expand Down
Loading