skillshark 0.1.0 โ 0.3.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.md +65 -6
- package/bin/skillshark.js +53 -16
- package/package.json +10 -2
- package/src/agents.js +336 -0
- package/src/discover.js +93 -41
- package/src/gh.js +32 -9
- package/src/install.js +294 -120
- package/src/interactive.js +189 -0
- package/src/share.js +36 -17
- package/src/source.js +33 -9
- package/src/transports/gist.js +60 -18
- package/src/transports/repo.js +54 -21
- package/src/ui.js +25 -0
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# SkillShark ๐ฆ
|
|
2
2
|
|
|
3
|
+
**[holy-coders.github.io/skillshark](https://holy-coders.github.io/skillshark/)** ยท [npm](https://www.npmjs.com/package/skillshark)
|
|
4
|
+
|
|
3
5
|
**Share an agent skill like you'd share a file.** SkillShark packages a Claude Code skill (or command) into a secret GitHub gist and hands you an unlisted, self-verifying link; the receiver installs it with one command and zero setup โ no GitHub account, no server, no registry.
|
|
4
6
|
|
|
5
7
|
> โ ๏ธ **Secret gists are unlisted, NOT private โ anyone with the link can read them.**
|
|
@@ -9,29 +11,40 @@
|
|
|
9
11
|
|
|
10
12
|
## Install
|
|
11
13
|
|
|
12
|
-
Zero-install:
|
|
14
|
+
Zero-install (stable, from npm):
|
|
13
15
|
|
|
14
16
|
```sh
|
|
15
17
|
npx skillshark install <link>
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
Want the **beta** โ whatever is on `main` right now, ahead of the npm release?
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npx github:Holy-Coders/skillshark install <link>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Both forms take the exact same commands and flags; the `github:` one just builds from the repo's latest commit instead of the published package. Or install globally:
|
|
19
27
|
|
|
20
28
|
```sh
|
|
21
|
-
npm install -g skillshark
|
|
29
|
+
npm install -g skillshark # stable
|
|
30
|
+
npm install -g github:Holy-Coders/skillshark # beta from main
|
|
22
31
|
```
|
|
23
32
|
|
|
33
|
+
Once it's on your PATH, running bare **`skillshark`** opens an interactive session โ pick a local skill to share from a menu, paste a link to install, inspect, or revoke, all guided with previews and spinners. Every flag-driven form below works too (and is what you want in scripts/CI).
|
|
34
|
+
|
|
24
35
|
Requirements: Node โฅ 20. **Senders** also need the [GitHub CLI](https://cli.github.com) authenticated (`gh auth login`). **Receivers need nothing** โ public links are fetched over plain anonymous HTTPS.
|
|
25
36
|
|
|
26
37
|
## The four commands
|
|
27
38
|
|
|
28
|
-
**share** โ package a skill and get an unlisted link
|
|
39
|
+
**share** โ package a skill and get an unlisted link. Your clipboard receives the full **paste-and-go install one-liner**, so the receiver just pastes it into any terminal:
|
|
29
40
|
|
|
30
41
|
```sh
|
|
31
42
|
skillshark share /j
|
|
32
|
-
# โ https://gist.github.com/
|
|
43
|
+
# clipboard โ npx skillshark install 'https://gist.github.com/8a1bc94โฆ#fp=3f9a7c21'
|
|
33
44
|
```
|
|
34
45
|
|
|
46
|
+
(`-q` still prints the bare URL for scripts; `--json` includes both `url` and `installCommand`.)
|
|
47
|
+
|
|
35
48
|
Accepts a name (`j`, `/j` โ resolved across `./.claude/skills`, `./.claude/commands`, and their `~/` equivalents) or any path. Useful flags: `--expires 30m|6h|24h|7d|30d` (advisory, default 7d), `--dry-run`, `--name`, `--force`, `--no-clipboard`, `-q` (print only the URL).
|
|
36
49
|
|
|
37
50
|
**install** โ download, verify, preview, confirm, copy:
|
|
@@ -42,7 +55,10 @@ skillshark install <gist-id> # bare id works too
|
|
|
42
55
|
skillshark install gh:acme/skills/review@main # any public repo path
|
|
43
56
|
```
|
|
44
57
|
|
|
45
|
-
Skills land in `.claude/skills/<name>/`, commands in `.claude/commands/<name>.md` (project scope when the cwd
|
|
58
|
+
Skills land in `.claude/skills/<name>/`, commands in `.claude/commands/<name>.md` (project scope when the cwd looks like a project, else `--project`/`--global`/`--dir`). Useful flags: `--yes`, `--force`, `--allow-exec`, `--dir <path>`, and:
|
|
59
|
+
|
|
60
|
+
- `--name <name>` โ install under a different name. The directory/filename changes and the artifact's frontmatter `name:` is rewritten to match, so two variants of the same skill can live side by side.
|
|
61
|
+
- `--agent <id>` โ install for a different tool entirely (see below).
|
|
46
62
|
|
|
47
63
|
**inspect** โ look before you leap (writes nothing):
|
|
48
64
|
|
|
@@ -62,6 +78,49 @@ The gist dies immediately; anyone holding the link gets "deleted by the sender."
|
|
|
62
78
|
(GitHub's anonymous API cache can serve a just-deleted gist for up to ~a minute
|
|
63
79
|
before the 404 propagates everywhere.)
|
|
64
80
|
|
|
81
|
+
## Cross-agent sharing (v0.2)
|
|
82
|
+
|
|
83
|
+
SkillShark speaks seven tools' on-disk dialects. You can **share from** any of them (bare names resolve across all of these locations) and **install to** any of them with `--agent <id>`:
|
|
84
|
+
|
|
85
|
+
| `--agent` | Artifacts | Where they land |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `claude-code` | skills, commands | `.claude/skills/<n>/`, `.claude/commands/<n>.md` (project or `~/`) |
|
|
88
|
+
| `cursor` | rules, commands | `.cursor/rules/<n>.mdc` (project), `.cursor/commands/<n>.md` |
|
|
89
|
+
| `codex` | prompts | `~/.codex/prompts/<n>.md` (Codex only reads global) |
|
|
90
|
+
| `copilot` | prompt files | `.github/prompts/<n>.prompt.md` (project) |
|
|
91
|
+
| `windsurf` | rules, workflows | `.windsurf/rules/<n>.md`, `.windsurf/workflows/<n>.md` (project) |
|
|
92
|
+
| `gemini` | commands | `.gemini/commands/<n>.toml` (TOML, project or `~/`) |
|
|
93
|
+
| `opencode` | commands | `.opencode/command/<n>.md`, `~/.config/opencode/command/<n>.md` |
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
skillshark share draftpr # found in ~/.codex/prompts/draftpr.md
|
|
97
|
+
skillshark install <link> --agent cursor # lands as .cursor/commands/draftpr.md
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Crossing agents **converts** the artifact: the instructions (frontmatter + body) are re-rendered in the target's dialect โ YAML frontmatter for Claude/Copilot/opencode, `.mdc` for Cursor rules, TOML `prompt`/`description` for Gemini, plain markdown where the tool wants it. Two honest limits, stated loudly at install time:
|
|
101
|
+
|
|
102
|
+
- **Bundled files don't cross.** A Claude skill's `scripts/` and reference files have no equivalent elsewhere; converting installs the instructions only and names every file left behind.
|
|
103
|
+
- **Conversion is best-effort.** A skill written for one tool may assume features another doesn't have. Read the result.
|
|
104
|
+
|
|
105
|
+
Same-agent installs are always byte-verbatim โ conversion only happens when you cross.
|
|
106
|
+
|
|
107
|
+
## GitHub Enterprise (v0.3)
|
|
108
|
+
|
|
109
|
+
If your company runs GitHub Enterprise (GHES or `*.ghe.com`), SkillShark works entirely inside it โ nothing touches public github.com:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
gh auth login --hostname ghe.corp.com # once, sender and receivers alike
|
|
113
|
+
skillshark share j --host ghe.corp.com # gist lives on YOUR GitHub
|
|
114
|
+
skillshark install 'https://ghe.corp.com/gist/<id>#fp=<hex>'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- **Links carry their host.** Receivers don't need `--host` โ an enterprise URL routes itself. `GH_HOST` (or `SKILLSHARK_HOST`) sets the default for bare ids and `gh:owner/repo` sources.
|
|
118
|
+
- **The privacy property:** enterprise links are fetched exclusively through the receiver's own `gh` auth. No anonymous request ever leaves for an enterprise host (enforced by tests), and unauthenticated users get a clear `gh auth login --hostname โฆ` pointer instead of a leak.
|
|
119
|
+
- **One honest limit:** the gists API truncates inline content at ~1 MB and enterprise receivers can't fetch around it anonymously, so enterprise *gist* shares are capped at ~900 KB encoded. Bigger skills: put them in a repo on your GHES and share `gh:owner/repo/path` with `--host`.
|
|
120
|
+
- `revoke` remembers which host a share went to and deletes it there.
|
|
121
|
+
|
|
122
|
+
Public github.com behavior is completely unchanged: receivers still need no account and the receive path still never invokes `gh`.
|
|
123
|
+
|
|
65
124
|
## Security model
|
|
66
125
|
|
|
67
126
|
- **SkillShark never executes package content.** Install = copy files. No postinstall hooks, no scripts, ever.
|
package/bin/skillshark.js
CHANGED
|
@@ -5,15 +5,17 @@ import os from 'node:os';
|
|
|
5
5
|
import { CliError } from '../src/errors.js';
|
|
6
6
|
import { VERSION } from '../src/version.js';
|
|
7
7
|
import { getConfigDir } from '../src/config.js';
|
|
8
|
-
import { makeUi, realPrompts } from '../src/ui.js';
|
|
8
|
+
import { makeUi, realPrompts, attachSpinner } from '../src/ui.js';
|
|
9
9
|
import { makeGhApi } from '../src/gh.js';
|
|
10
10
|
import { copyToClipboard } from '../src/clipboard.js';
|
|
11
11
|
import { runShare, runRevoke } from '../src/share.js';
|
|
12
12
|
import { runInstall, runInspect } from '../src/install.js';
|
|
13
|
+
import { runInteractive } from '../src/interactive.js';
|
|
13
14
|
|
|
14
15
|
const HELP = `skillshark โ share agent skills like files
|
|
15
16
|
|
|
16
17
|
USAGE
|
|
18
|
+
skillshark interactive session (pick, share, install โ guided)
|
|
17
19
|
skillshark <command> [options]
|
|
18
20
|
|
|
19
21
|
COMMANDS
|
|
@@ -31,17 +33,34 @@ GLOBAL OPTIONS
|
|
|
31
33
|
-y, --yes Skip prompts (non-interactive)
|
|
32
34
|
-q, --quiet Print only the essential result (URL or path)
|
|
33
35
|
--json Machine-readable output
|
|
36
|
+
--host <h> GitHub Enterprise hostname (default github.com; also
|
|
37
|
+
honors GH_HOST). Enterprise links carry their host, so
|
|
38
|
+
receivers usually don't need this flag.
|
|
34
39
|
--no-color Disable color (NO_COLOR is also honored)
|
|
35
40
|
-h, --help Show help (try: skillshark help <command>)
|
|
36
41
|
-V, --version Show version
|
|
37
42
|
|
|
43
|
+
ENTERPRISE (privacy)
|
|
44
|
+
skillshark share j --host ghe.corp.com share on your GHES โ never leaves it
|
|
45
|
+
Enterprise links are fetched through YOUR gh auth (gh auth login --hostname
|
|
46
|
+
ghe.corp.com); no anonymous request ever touches an enterprise host.
|
|
47
|
+
|
|
38
48
|
EXAMPLES
|
|
49
|
+
skillshark interactive: menus for everything below
|
|
39
50
|
skillshark share /j share the "j" skill (secret gist)
|
|
40
51
|
skillshark install <gist-url|id> install a shared skill
|
|
52
|
+
skillshark install <link> --name jmp install under a different name
|
|
53
|
+
skillshark install <link> --agent codex convert for another tool
|
|
41
54
|
skillshark install gh:acme/skills/review install straight from a repo path
|
|
42
55
|
skillshark inspect <gist-url> --cat SKILL.md
|
|
43
56
|
skillshark revoke j delete the share
|
|
44
57
|
|
|
58
|
+
AGENTS (share from and install to)
|
|
59
|
+
claude-code (skills, commands) ยท cursor (rules, commands) ยท codex (prompts)
|
|
60
|
+
copilot (prompt files) ยท windsurf (rules, workflows) ยท gemini (commands)
|
|
61
|
+
opencode (commands). Cross-agent installs convert the instructions to the
|
|
62
|
+
target's dialect; a skill's bundled scripts can't follow it (warned loudly).
|
|
63
|
+
|
|
45
64
|
Secret gists are unlisted, NOT private โ anyone with the link can read them.
|
|
46
65
|
SkillShark never executes package content; install only copies files.`;
|
|
47
66
|
|
|
@@ -62,10 +81,16 @@ unlisted, not private: anyone holding it can read the gist. Undo with
|
|
|
62
81
|
install: `skillshark install <source> โ download, verify, preview, confirm, copy
|
|
63
82
|
|
|
64
83
|
-y, --yes Install without prompting (documented as dangerous)
|
|
65
|
-
--
|
|
66
|
-
|
|
67
|
-
--
|
|
68
|
-
|
|
84
|
+
--name <name> Install under a different name (renames the artifact
|
|
85
|
+
and updates its frontmatter name)
|
|
86
|
+
--agent <id> Target agent: claude-code | cursor | codex | copilot |
|
|
87
|
+
windsurf | gemini | opencode. Crossing agents converts
|
|
88
|
+
the artifact's instructions to the target's dialect โ
|
|
89
|
+
bundled support files can't come along (warned).
|
|
90
|
+
--project Install at project scope (where the agent supports it)
|
|
91
|
+
--global Install at user/global scope
|
|
92
|
+
--dir <path> Install verbatim into an explicit directory (required
|
|
93
|
+
for bundle packages; skips agent conventions)
|
|
69
94
|
--force Overwrite an existing, differing artifact
|
|
70
95
|
--allow-exec Keep executable bits (stripped by default)
|
|
71
96
|
-q, --quiet Print only the installed path
|
|
@@ -94,6 +119,7 @@ const GLOBAL_FLAGS = {
|
|
|
94
119
|
yes: { short: 'y', key: 'yes' },
|
|
95
120
|
quiet: { short: 'q', key: 'quiet' },
|
|
96
121
|
json: { key: 'json' },
|
|
122
|
+
host: { takesValue: true, key: 'host' },
|
|
97
123
|
'no-color': { key: 'noColor' },
|
|
98
124
|
help: { short: 'h', key: 'help' },
|
|
99
125
|
version: { short: 'V', key: 'version' },
|
|
@@ -113,6 +139,7 @@ const COMMAND_FLAGS = {
|
|
|
113
139
|
dir: { takesValue: true, key: 'dir' },
|
|
114
140
|
'allow-exec': { key: 'allowExec' },
|
|
115
141
|
agent: { takesValue: true, key: 'agent' },
|
|
142
|
+
name: { takesValue: true, key: 'name' },
|
|
116
143
|
},
|
|
117
144
|
inspect: {
|
|
118
145
|
cat: { takesValue: true, key: 'cat' },
|
|
@@ -123,7 +150,8 @@ const COMMAND_FLAGS = {
|
|
|
123
150
|
|
|
124
151
|
function parseArgv(argv) {
|
|
125
152
|
const [first, ...rest] = argv;
|
|
126
|
-
if (!first
|
|
153
|
+
if (!first) return { command: 'interactive', opts: {}, positionals: [] };
|
|
154
|
+
if (first === 'help') {
|
|
127
155
|
return { command: 'help', topic: rest[0] ?? null, opts: {}, positionals: [] };
|
|
128
156
|
}
|
|
129
157
|
if (first === '--help' || first === '-h') return { command: 'help', topic: null, opts: {}, positionals: [] };
|
|
@@ -192,19 +220,10 @@ async function main() {
|
|
|
192
220
|
return 0;
|
|
193
221
|
}
|
|
194
222
|
|
|
195
|
-
const known = new Set(['share', 'install', 'inspect', 'revoke']);
|
|
223
|
+
const known = new Set(['share', 'install', 'inspect', 'revoke', 'interactive']);
|
|
196
224
|
if (!known.has(parsed.command)) {
|
|
197
225
|
throw new CliError(`Unknown command "${parsed.command}". Commands: share, install, inspect, revoke. Try: skillshark --help`, 2);
|
|
198
226
|
}
|
|
199
|
-
if (parsed.opts.agent && parsed.opts.agent !== 'claude-code') {
|
|
200
|
-
throw new CliError(`Only --agent claude-code is supported in v0.1 (got "${parsed.opts.agent}").`, 2);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const arg = parsed.positionals[0];
|
|
204
|
-
if (!arg) {
|
|
205
|
-
const noun = parsed.command === 'share' ? '<path|name>' : parsed.command === 'revoke' ? '<id|name>' : '<source>';
|
|
206
|
-
throw new CliError(`Usage: skillshark ${parsed.command} ${noun}. Try: skillshark help ${parsed.command}`, 2);
|
|
207
|
-
}
|
|
208
227
|
if (parsed.positionals.length > 1) {
|
|
209
228
|
throw new CliError(`Too many arguments: ${parsed.positionals.slice(1).join(' ')}`, 2);
|
|
210
229
|
}
|
|
@@ -223,6 +242,24 @@ async function main() {
|
|
|
223
242
|
ghApi: makeGhApi(),
|
|
224
243
|
clipboard: (text) => copyToClipboard(text),
|
|
225
244
|
};
|
|
245
|
+
if (effectiveTTY) await attachSpinner(ui);
|
|
246
|
+
|
|
247
|
+
// no command (TTY) โ the interactive session; piped โ help text
|
|
248
|
+
if (parsed.command === 'interactive') {
|
|
249
|
+
if (!effectiveTTY) {
|
|
250
|
+
ui.out(HELP);
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
return runInteractive(deps);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const arg = parsed.positionals[0];
|
|
257
|
+
if (!arg) {
|
|
258
|
+
// bare subcommand in a TTY โ that command's wizard; piped โ usage error
|
|
259
|
+
if (effectiveTTY) return runInteractive(deps, parsed.command);
|
|
260
|
+
const noun = parsed.command === 'share' ? '<path|name>' : parsed.command === 'revoke' ? '<id|name>' : '<source>';
|
|
261
|
+
throw new CliError(`Usage: skillshark ${parsed.command} ${noun}. Try: skillshark help ${parsed.command}`, 2);
|
|
262
|
+
}
|
|
226
263
|
|
|
227
264
|
switch (parsed.command) {
|
|
228
265
|
case 'share':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillshark",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Share agent skills like files โ secret gists out, safe verified installs in. No server; GitHub is the backend.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
"src",
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Holy-Coders/skillshark.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://holy-coders.github.io/skillshark/",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/Holy-Coders/skillshark/issues"
|
|
21
|
+
},
|
|
14
22
|
"keywords": [
|
|
15
23
|
"claude-code",
|
|
16
24
|
"agent-skills",
|
|
@@ -19,7 +27,7 @@
|
|
|
19
27
|
"cli",
|
|
20
28
|
"sharing"
|
|
21
29
|
],
|
|
22
|
-
"author": "
|
|
30
|
+
"author": "Holy Coders",
|
|
23
31
|
"engines": {
|
|
24
32
|
"node": ">=20"
|
|
25
33
|
},
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// The adapter registry: the ONLY place agent-specific knowledge lives.
|
|
2
|
+
// Each adapter encodes one tool's on-disk conventions (verified against the
|
|
3
|
+
// tools' docs, June 2026) and how to render a canonical artifact into its
|
|
4
|
+
// dialect. Cross-agent installs are honest best-effort conversions: markdown
|
|
5
|
+
// instructions travel; bundled support files cannot leave a Claude skill.
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { CliError } from './errors.js';
|
|
9
|
+
|
|
10
|
+
export const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
11
|
+
|
|
12
|
+
// canonical = { name, description, body } โ body is the primary document
|
|
13
|
+
// with its source-dialect frontmatter stripped.
|
|
14
|
+
|
|
15
|
+
// --- dialect helpers ---------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function yamlFront(fields) {
|
|
18
|
+
const lines = Object.entries(fields)
|
|
19
|
+
.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
20
|
+
.map(([k, v]) => `${k}: ${String(v)}`);
|
|
21
|
+
if (!lines.length) return '';
|
|
22
|
+
return `---\n${lines.join('\n')}\n---\n\n`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// TOML basic strings: escape backslashes and quotes so the parsed prompt is
|
|
26
|
+
// byte-identical to the source body.
|
|
27
|
+
function tomlBasicString(s) {
|
|
28
|
+
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function tomlMultiline(s) {
|
|
32
|
+
const escaped = s.replace(/\\/g, '\\\\').replace(/"""/g, '""\\"');
|
|
33
|
+
return `"""\n${escaped}\n"""`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function renderGeminiToml({ description, body }) {
|
|
37
|
+
let out = '';
|
|
38
|
+
if (description) out += `description = ${tomlBasicString(description)}\n\n`;
|
|
39
|
+
out += `prompt = ${tomlMultiline(body.replace(/\n$/, ''))}\n`;
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Parse the gemini TOML dialect (our generated shape + the common hand-written
|
|
44
|
+
// variants: basic/literal multiline strings and single-line prompts).
|
|
45
|
+
export function parseGeminiToml(text) {
|
|
46
|
+
const out = { description: '', body: null };
|
|
47
|
+
const desc = text.match(/^[ \t]*description[ \t]*=[ \t]*"((?:[^"\\]|\\.)*)"[ \t]*$/m);
|
|
48
|
+
if (desc) out.description = desc[1].replace(/\\(["\\])/g, '$1');
|
|
49
|
+
|
|
50
|
+
let m = text.match(/^[ \t]*prompt[ \t]*=[ \t]*"""\n?([\s\S]*?)"""[ \t]*$/m);
|
|
51
|
+
if (m) {
|
|
52
|
+
out.body = m[1].replace(/""\\"/g, '"""').replace(/\\\\/g, '\\').replace(/\n$/, '');
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
m = text.match(/^[ \t]*prompt[ \t]*=[ \t]*'''\n?([\s\S]*?)'''[ \t]*$/m);
|
|
56
|
+
if (m) {
|
|
57
|
+
out.body = m[1].replace(/\n$/, '');
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
m = text.match(/^[ \t]*prompt[ \t]*=[ \t]*"((?:[^"\\]|\\.)*)"[ \t]*$/m);
|
|
61
|
+
if (m) {
|
|
62
|
+
out.body = m[1].replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\(["\\])/g, '$1');
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
return out; // body null โ caller decides how honest to be
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Rewrite (or leave alone) the `name:` value in a YAML frontmatter block.
|
|
69
|
+
export function rewriteFrontmatterName(text, newName) {
|
|
70
|
+
if (!text.startsWith('---')) return text;
|
|
71
|
+
const end = text.indexOf('\n---', 3);
|
|
72
|
+
if (end === -1) return text;
|
|
73
|
+
const head = text.slice(0, end);
|
|
74
|
+
if (!/^name[ \t]*:/m.test(head.slice(4))) return text;
|
|
75
|
+
const newHead = head.replace(/^(name[ \t]*:[ \t]*).*$/m, `$1${newName}`);
|
|
76
|
+
return newHead + text.slice(end);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- the registry -------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
// Adapter shape:
|
|
82
|
+
// label human name
|
|
83
|
+
// detect({cwd, home}) is this tool present here?
|
|
84
|
+
// locations share-side lookup spots for a bare <name>
|
|
85
|
+
// { kind, scope, rel(name) } rel is relative to cwd (project) or home (global)
|
|
86
|
+
// mapKind(type) incoming artifact type โ this agent's kind (null = can't)
|
|
87
|
+
// scopes(kind) 'both' | 'project' | 'global'
|
|
88
|
+
// targetRel(kind, name) install path segments, relative to the scope root
|
|
89
|
+
// container(kind) 'dir' | 'file'
|
|
90
|
+
// render(kind, canonical) content for a converted install
|
|
91
|
+
// parse(content, ext) native file โ { description, body } (canonical-ish)
|
|
92
|
+
const mdParse = (content) => {
|
|
93
|
+
// lazy import avoided: tiny local frontmatter read
|
|
94
|
+
const fm = {};
|
|
95
|
+
let body = content;
|
|
96
|
+
if (content.startsWith('---')) {
|
|
97
|
+
const end = content.indexOf('\n---', 3);
|
|
98
|
+
if (end !== -1) {
|
|
99
|
+
for (const line of content.slice(4, end).split('\n')) {
|
|
100
|
+
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
101
|
+
if (m) fm[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
|
|
102
|
+
}
|
|
103
|
+
body = content.slice(end + 4).replace(/^\n/, '');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { name: fm.name, description: fm.description ?? '', body };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const AGENTS = {
|
|
110
|
+
'claude-code': {
|
|
111
|
+
label: 'Claude Code',
|
|
112
|
+
detect: ({ cwd, home }) =>
|
|
113
|
+
existsSync(path.join(cwd, '.claude')) || existsSync(path.join(home, '.claude')),
|
|
114
|
+
locations: [
|
|
115
|
+
{ kind: 'skill', scope: 'project', container: 'dir', rel: (n) => ['.claude', 'skills', n] },
|
|
116
|
+
{ kind: 'command', scope: 'project', container: 'file', rel: (n) => ['.claude', 'commands', `${n}.md`] },
|
|
117
|
+
{ kind: 'skill', scope: 'global', container: 'dir', rel: (n) => ['.claude', 'skills', n] },
|
|
118
|
+
{ kind: 'command', scope: 'global', container: 'file', rel: (n) => ['.claude', 'commands', `${n}.md`] },
|
|
119
|
+
],
|
|
120
|
+
mapKind: (type) => (type === 'skill' || type === 'rule' ? 'skill' : type === 'bundle' ? null : 'command'),
|
|
121
|
+
scopes: () => 'both',
|
|
122
|
+
container: (kind) => (kind === 'skill' ? 'dir' : 'file'),
|
|
123
|
+
targetRel: (kind, name) =>
|
|
124
|
+
kind === 'skill' ? ['.claude', 'skills', name] : ['.claude', 'commands', `${name}.md`],
|
|
125
|
+
render: (kind, { name, description, body }) => {
|
|
126
|
+
if (kind === 'skill') {
|
|
127
|
+
return { filename: 'SKILL.md', content: yamlFront({ name, description }) + body };
|
|
128
|
+
}
|
|
129
|
+
return { filename: `${name}.md`, content: yamlFront({ description }) + body };
|
|
130
|
+
},
|
|
131
|
+
parse: mdParse,
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
cursor: {
|
|
135
|
+
label: 'Cursor',
|
|
136
|
+
detect: ({ cwd, home }) =>
|
|
137
|
+
existsSync(path.join(cwd, '.cursor')) || existsSync(path.join(home, '.cursor')),
|
|
138
|
+
locations: [
|
|
139
|
+
{ kind: 'rule', scope: 'project', container: 'file', rel: (n) => ['.cursor', 'rules', `${n}.mdc`] },
|
|
140
|
+
{ kind: 'command', scope: 'project', container: 'file', rel: (n) => ['.cursor', 'commands', `${n}.md`] },
|
|
141
|
+
{ kind: 'command', scope: 'global', container: 'file', rel: (n) => ['.cursor', 'commands', `${n}.md`] },
|
|
142
|
+
],
|
|
143
|
+
mapKind: (type) => (type === 'bundle' ? null : type === 'rule' ? 'rule' : 'command'),
|
|
144
|
+
scopes: (kind) => (kind === 'rule' ? 'project' : 'both'),
|
|
145
|
+
container: () => 'file',
|
|
146
|
+
targetRel: (kind, name) =>
|
|
147
|
+
kind === 'rule' ? ['.cursor', 'rules', `${name}.mdc`] : ['.cursor', 'commands', `${name}.md`],
|
|
148
|
+
render: (kind, { name, description, body }) => {
|
|
149
|
+
if (kind === 'rule') {
|
|
150
|
+
return { filename: `${name}.mdc`, content: yamlFront({ description, alwaysApply: 'false' }) + body };
|
|
151
|
+
}
|
|
152
|
+
return { filename: `${name}.md`, content: body };
|
|
153
|
+
},
|
|
154
|
+
parse: mdParse,
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
codex: {
|
|
158
|
+
label: 'Codex CLI',
|
|
159
|
+
detect: ({ cwd, home }) =>
|
|
160
|
+
existsSync(path.join(cwd, '.codex')) || existsSync(path.join(home, '.codex')),
|
|
161
|
+
locations: [
|
|
162
|
+
{ kind: 'prompt', scope: 'global', container: 'file', rel: (n) => ['.codex', 'prompts', `${n}.md`] },
|
|
163
|
+
],
|
|
164
|
+
mapKind: (type) => (type === 'bundle' ? null : 'prompt'),
|
|
165
|
+
scopes: () => 'global', // Codex scans only ~/.codex/prompts (top-level)
|
|
166
|
+
container: () => 'file',
|
|
167
|
+
targetRel: (kind, name) => ['.codex', 'prompts', `${name}.md`],
|
|
168
|
+
render: (kind, { description, body, name }) => ({
|
|
169
|
+
filename: `${name}.md`,
|
|
170
|
+
content: yamlFront({ description }) + body,
|
|
171
|
+
}),
|
|
172
|
+
parse: mdParse,
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
copilot: {
|
|
176
|
+
label: 'GitHub Copilot',
|
|
177
|
+
detect: ({ cwd }) =>
|
|
178
|
+
existsSync(path.join(cwd, '.github', 'prompts')) ||
|
|
179
|
+
existsSync(path.join(cwd, '.github', 'copilot-instructions.md')) ||
|
|
180
|
+
existsSync(path.join(cwd, '.github', 'instructions')),
|
|
181
|
+
locations: [
|
|
182
|
+
{ kind: 'prompt', scope: 'project', container: 'file', rel: (n) => ['.github', 'prompts', `${n}.prompt.md`] },
|
|
183
|
+
],
|
|
184
|
+
mapKind: (type) => (type === 'bundle' ? null : 'prompt'),
|
|
185
|
+
scopes: () => 'project',
|
|
186
|
+
container: () => 'file',
|
|
187
|
+
targetRel: (kind, name) => ['.github', 'prompts', `${name}.prompt.md`],
|
|
188
|
+
render: (kind, { description, body, name }) => ({
|
|
189
|
+
filename: `${name}.prompt.md`,
|
|
190
|
+
content: yamlFront({ description }) + body,
|
|
191
|
+
}),
|
|
192
|
+
parse: mdParse,
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
windsurf: {
|
|
196
|
+
label: 'Windsurf',
|
|
197
|
+
detect: ({ cwd, home }) =>
|
|
198
|
+
existsSync(path.join(cwd, '.windsurf')) ||
|
|
199
|
+
existsSync(path.join(home, '.codeium', 'windsurf')),
|
|
200
|
+
locations: [
|
|
201
|
+
{ kind: 'rule', scope: 'project', container: 'file', rel: (n) => ['.windsurf', 'rules', `${n}.md`] },
|
|
202
|
+
{ kind: 'workflow', scope: 'project', container: 'file', rel: (n) => ['.windsurf', 'workflows', `${n}.md`] },
|
|
203
|
+
],
|
|
204
|
+
mapKind: (type) => (type === 'bundle' ? null : type === 'rule' ? 'rule' : 'workflow'),
|
|
205
|
+
scopes: () => 'project',
|
|
206
|
+
container: () => 'file',
|
|
207
|
+
targetRel: (kind, name) =>
|
|
208
|
+
kind === 'rule' ? ['.windsurf', 'rules', `${name}.md`] : ['.windsurf', 'workflows', `${name}.md`],
|
|
209
|
+
render: (kind, { description, body, name }) => {
|
|
210
|
+
if (kind === 'rule') return { filename: `${name}.md`, content: body };
|
|
211
|
+
return { filename: `${name}.md`, content: yamlFront({ description }) + body };
|
|
212
|
+
},
|
|
213
|
+
parse: mdParse,
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
gemini: {
|
|
217
|
+
label: 'Gemini CLI',
|
|
218
|
+
detect: ({ cwd, home }) =>
|
|
219
|
+
existsSync(path.join(cwd, '.gemini')) || existsSync(path.join(home, '.gemini')),
|
|
220
|
+
locations: [
|
|
221
|
+
{ kind: 'command', scope: 'project', container: 'file', rel: (n) => ['.gemini', 'commands', `${n}.toml`] },
|
|
222
|
+
{ kind: 'command', scope: 'global', container: 'file', rel: (n) => ['.gemini', 'commands', `${n}.toml`] },
|
|
223
|
+
],
|
|
224
|
+
mapKind: (type) => (type === 'bundle' ? null : 'command'),
|
|
225
|
+
scopes: () => 'both',
|
|
226
|
+
container: () => 'file',
|
|
227
|
+
targetRel: (kind, name) => ['.gemini', 'commands', `${name}.toml`],
|
|
228
|
+
render: (kind, canonical) => ({
|
|
229
|
+
filename: `${canonical.name}.toml`,
|
|
230
|
+
content: renderGeminiToml(canonical),
|
|
231
|
+
}),
|
|
232
|
+
parse: (content, ext) => {
|
|
233
|
+
if (ext === '.toml') {
|
|
234
|
+
const parsed = parseGeminiToml(content);
|
|
235
|
+
if (parsed.body === null) {
|
|
236
|
+
throw new CliError(
|
|
237
|
+
"Couldn't read a prompt out of this Gemini command โ install it unconverted with --agent gemini.",
|
|
238
|
+
1,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return { description: parsed.description, body: parsed.body };
|
|
242
|
+
}
|
|
243
|
+
return mdParse(content);
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
opencode: {
|
|
248
|
+
label: 'opencode',
|
|
249
|
+
detect: ({ cwd, home }) =>
|
|
250
|
+
existsSync(path.join(cwd, '.opencode')) ||
|
|
251
|
+
existsSync(path.join(home, '.config', 'opencode')),
|
|
252
|
+
locations: [
|
|
253
|
+
{ kind: 'command', scope: 'project', container: 'file', rel: (n) => ['.opencode', 'command', `${n}.md`] },
|
|
254
|
+
{ kind: 'command', scope: 'global', container: 'file', rel: (n) => ['.config', 'opencode', 'command', `${n}.md`] },
|
|
255
|
+
],
|
|
256
|
+
mapKind: (type) => (type === 'bundle' ? null : 'command'),
|
|
257
|
+
scopes: () => 'both',
|
|
258
|
+
container: () => 'file',
|
|
259
|
+
targetRel: (kind, name, scope) =>
|
|
260
|
+
scope === 'global'
|
|
261
|
+
? ['.config', 'opencode', 'command', `${name}.md`]
|
|
262
|
+
: ['.opencode', 'command', `${name}.md`],
|
|
263
|
+
render: (kind, { description, body, name }) => ({
|
|
264
|
+
filename: `${name}.md`,
|
|
265
|
+
content: yamlFront({ description }) + body,
|
|
266
|
+
}),
|
|
267
|
+
parse: mdParse,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export const AGENT_IDS = Object.keys(AGENTS);
|
|
272
|
+
|
|
273
|
+
export function getAgent(id) {
|
|
274
|
+
const a = AGENTS[id];
|
|
275
|
+
if (!a) {
|
|
276
|
+
throw new CliError(`Unknown agent "${id}". Supported: ${AGENT_IDS.join(', ')}.`, 2);
|
|
277
|
+
}
|
|
278
|
+
return a;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function detectAgents(ctx) {
|
|
282
|
+
return AGENT_IDS.filter((id) => AGENTS[id].detect(ctx));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Explicit-path classification: which agent convention does this path follow?
|
|
286
|
+
export function classifyByConvention(absPath, isDir) {
|
|
287
|
+
const norm = absPath.split(path.sep).join('/');
|
|
288
|
+
const rules = [
|
|
289
|
+
[/\/\.claude\/skills\/[^/]+$/, true, 'skill', 'claude-code'],
|
|
290
|
+
[/\/\.claude\/commands\/[^/]+\.md$/, false, 'command', 'claude-code'],
|
|
291
|
+
[/\/\.cursor\/rules\/[^/]+\.mdc$/, false, 'rule', 'cursor'],
|
|
292
|
+
[/\/\.cursor\/commands\/[^/]+\.md$/, false, 'command', 'cursor'],
|
|
293
|
+
[/\/\.codex\/prompts\/[^/]+\.md$/, false, 'prompt', 'codex'],
|
|
294
|
+
[/\/\.github\/prompts\/[^/]+\.prompt\.md$/, false, 'prompt', 'copilot'],
|
|
295
|
+
[/\/\.windsurf\/rules\/[^/]+\.md$/, false, 'rule', 'windsurf'],
|
|
296
|
+
[/\/\.windsurf\/workflows\/[^/]+\.md$/, false, 'workflow', 'windsurf'],
|
|
297
|
+
[/\/\.gemini\/commands\/[^/]+\.toml$/, false, 'command', 'gemini'],
|
|
298
|
+
[/\/(\.opencode|opencode)\/command\/[^/]+\.md$/, false, 'command', 'opencode'],
|
|
299
|
+
];
|
|
300
|
+
for (const [re, wantDir, type, agent] of rules) {
|
|
301
|
+
if (wantDir === isDir && re.test(norm)) return { type, agent };
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Strip an artifact filename down to its bare name (handles ".prompt.md").
|
|
307
|
+
export function artifactBaseName(filename) {
|
|
308
|
+
return filename.replace(/\.prompt\.md$/, '').replace(/\.[^.]+$/, '');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Extract the canonical document from a verified package: the primary doc,
|
|
312
|
+
// parsed with the SOURCE agent's dialect.
|
|
313
|
+
export function extractCanonical(manifest, readFileSyncish) {
|
|
314
|
+
const files = manifest.files;
|
|
315
|
+
let primary =
|
|
316
|
+
files.find((f) => f.path === 'SKILL.md') ??
|
|
317
|
+
(files.length === 1 ? files[0] : files.find((f) => /\.(md|mdc|toml)$/.test(f.path) && !f.path.includes('/')));
|
|
318
|
+
if (!primary) {
|
|
319
|
+
throw new CliError(
|
|
320
|
+
`This package has no convertible primary document. Install it natively${manifest.agent ? ` (--agent ${manifest.agent})` : ''} or with --dir.`,
|
|
321
|
+
2,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
const content = readFileSyncish(primary.path);
|
|
325
|
+
const ext = path.extname(primary.path);
|
|
326
|
+
const sourceAgent = AGENTS[manifest.agent] ?? AGENTS['claude-code'];
|
|
327
|
+
const parsed = sourceAgent.parse(content, ext);
|
|
328
|
+
const dropped = files.filter((f) => f.path !== primary.path).map((f) => f.path);
|
|
329
|
+
return {
|
|
330
|
+
name: manifest.name,
|
|
331
|
+
description: (parsed.description || manifest.description || '').trim(),
|
|
332
|
+
body: parsed.body.replace(/^\n+/, ''),
|
|
333
|
+
primaryPath: primary.path,
|
|
334
|
+
dropped,
|
|
335
|
+
};
|
|
336
|
+
}
|