@callmeradical/augy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/chunk-EU54UQ4C.js +144 -0
- package/dist/chunk-KCQY4IDO.js +142 -0
- package/dist/chunk-PX2LVUHV.js +53 -0
- package/dist/chunk-R2TJ3UDO.js +43 -0
- package/dist/chunk-UJX6VJ3S.js +508 -0
- package/dist/chunk-YXCEZ3EY.js +74 -0
- package/dist/chunk-ZW6ZKHTF.js +136 -0
- package/dist/diff-KYSWUXMH.js +303 -0
- package/dist/index.js +92 -0
- package/dist/info-7BO4LXXS.js +119 -0
- package/dist/install-FPAGHWX2.js +192 -0
- package/dist/list-UJX2MYXK.js +70 -0
- package/dist/registry-QVCNZXBZ.js +38 -0
- package/dist/rollback-WAZV5HNG.js +105 -0
- package/dist/scan-DKN7YBT5.js +328 -0
- package/dist/search-2V2U23KC.js +78 -0
- package/dist/set-source-TITL27N3.js +63 -0
- package/dist/tap-3RD3XZ56.js +142 -0
- package/dist/uninstall-3C3XZZEC.js +101 -0
- package/dist/update-A3PSROUK.js +163 -0
- package/dist/versions-OMI6OFJC.js +13 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lars Cromley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# augy
|
|
2
|
+
|
|
3
|
+
**Homebrew for AI agent skills** — install, version, update, and rollback skills across OpenCode, Claude, and Codex from a single CLI.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
augy install tdd # install by name via a tap
|
|
7
|
+
augy update # upgrade everything with upstream changes
|
|
8
|
+
augy diff tdd # browse what changed before upgrading
|
|
9
|
+
augy rollback tdd abc1234 # something broke — go back
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g augy
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Node.js ≥ 18.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
|
|
26
|
+
Skills are directories containing a `SKILL.md` file that an AI agent loads as context. augy tracks which skills you have installed, where they came from (GitHub), and what version (commit SHA) is on disk — then lets you update, diff, and roll back just like a package manager.
|
|
27
|
+
|
|
28
|
+
**Supported agents**
|
|
29
|
+
|
|
30
|
+
| Agent | Default skills path |
|
|
31
|
+
|---|---|
|
|
32
|
+
| OpenCode | `~/.opencode/skills/` |
|
|
33
|
+
| Claude | `~/.claude/skills/` |
|
|
34
|
+
| Codex | `~/.codex/skills/` (or `$CODEX_HOME/skills`) |
|
|
35
|
+
|
|
36
|
+
A single skill can be deployed to multiple agents simultaneously.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
### `augy install [url]`
|
|
43
|
+
Install skills from a GitHub URL, `owner/repo[/path]` shorthand, or a bare name resolved via a registered tap.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
augy install https://github.com/mattpocock/skills/tree/main
|
|
47
|
+
augy install mattpocock/skills/skills/engineering/tdd
|
|
48
|
+
augy install tdd # resolves via taps
|
|
49
|
+
augy install tdd --agent opencode # target a specific agent
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `augy scan`
|
|
53
|
+
Find skills already on disk that augy doesn't know about. Auto-detects provenance via git remotes and `SKILL.md` frontmatter, groups results into *detected* vs *no provenance found* with filesystem paths shown for unknown skills. Imports them into the registry.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
augy scan
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `augy update [skill]`
|
|
60
|
+
Check all installed skills for upstream SHA drift. Shows a list of available upgrades, lets you select which to apply, archives the current version before overwriting.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
augy update # check + upgrade everything
|
|
64
|
+
augy update tdd # single skill
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `augy diff <skill> [sha1] [sha2]`
|
|
68
|
+
Interactive file-level diff browser. Three modes:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
augy diff tdd # installed ↔ upstream HEAD
|
|
72
|
+
augy diff tdd abc1234 # installed ↔ specific SHA
|
|
73
|
+
augy diff tdd abc1234 def5678 # two local archives
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `augy rollback <skill> [sha]`
|
|
77
|
+
Restore a skill to any previously archived version.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
augy rollback tdd # interactive version picker
|
|
81
|
+
augy rollback tdd abc1234 # specific SHA (short or full)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### `augy list`
|
|
85
|
+
Show all installed skills, their SHAs, agents, and update status.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
augy list
|
|
89
|
+
augy list --json # raw registry JSON
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `augy info <skill>`
|
|
93
|
+
Full metadata: source, SHA, agents with paths, version history, and a preview of the skill description.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
augy info tdd
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `augy search [query]`
|
|
100
|
+
Search all registered taps for available skills.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
augy search # full index
|
|
104
|
+
augy search tdd # filter by name
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `augy tap add|remove|list`
|
|
108
|
+
Manage trusted repos (taps) — once added, skills can be installed by bare name.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
augy tap add mattpocock/skills
|
|
112
|
+
augy tap add mattpocock/skills --path skills/engineering
|
|
113
|
+
augy tap list
|
|
114
|
+
augy tap remove mattpocock/skills
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `augy set-source <skill> <url>`
|
|
118
|
+
Attach a GitHub source to a skill imported without one (e.g. via `augy scan`). Enables updates and diffs. Accepts tree and blob URLs.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
augy set-source commit https://github.com/owner/repo/tree/main/skills/commit
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `augy uninstall <skill>`
|
|
125
|
+
Remove a skill from all agent paths and the registry. Optionally prune version archives.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
augy uninstall tdd
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `augy pin|unpin <skill>`
|
|
132
|
+
Pin a skill to skip it during `augy update`.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
augy pin tdd
|
|
136
|
+
augy unpin tdd
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Taps
|
|
142
|
+
|
|
143
|
+
Taps are trusted GitHub repos containing skills. Add one and install by name without knowing the full URL:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
augy tap add mattpocock/skills
|
|
147
|
+
augy install tdd # resolves to mattpocock/skills automatically
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Version storage
|
|
153
|
+
|
|
154
|
+
Every upgrade archives the current skill to `~/.augy/versions/<skill>/<sha>/` before overwriting it — giving you a full local snapshot history to diff or rollback to at any time.
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
~/.augy/
|
|
158
|
+
registry.json ← lockfile (human-readable JSON)
|
|
159
|
+
versions/
|
|
160
|
+
tdd/
|
|
161
|
+
7afa86d.../ ← snapshot before last upgrade
|
|
162
|
+
abc1234.../ ← older snapshot
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Environment variables
|
|
168
|
+
|
|
169
|
+
| Variable | Default | Description |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `AUGY_HOME` | `~/.augy` | Override augy's home directory |
|
|
172
|
+
| `CODEX_HOME` | `~/.codex` | Override Codex agent path |
|
|
173
|
+
| `GITHUB_TOKEN` | — | Raise GitHub API rate limit from 60 to 5000 req/hr |
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Docs
|
|
178
|
+
|
|
179
|
+
Full documentation at **[augy.dev](https://augy.dev)** *(coming soon — see `docs/` for the source)*.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// src/github.ts
|
|
2
|
+
function parseGitHubUrl(input) {
|
|
3
|
+
input = input.trim().replace(/\/$/, "");
|
|
4
|
+
if (input.startsWith("https://github.com/") || input.startsWith("http://github.com/")) {
|
|
5
|
+
const url = new URL(input.startsWith("http://") ? input.replace("http://", "https://") : input);
|
|
6
|
+
const parts2 = url.pathname.replace(/^\//, "").split("/");
|
|
7
|
+
const owner2 = parts2[0] ?? "";
|
|
8
|
+
const repo2 = parts2[1] ?? "";
|
|
9
|
+
if (parts2[2] === "tree") {
|
|
10
|
+
const ref = parts2[3];
|
|
11
|
+
const path2 = parts2.slice(4).join("/");
|
|
12
|
+
return { owner: owner2, repo: repo2, path: path2, ref };
|
|
13
|
+
}
|
|
14
|
+
if (parts2[2] === "blob") {
|
|
15
|
+
const ref = parts2[3];
|
|
16
|
+
const filePath = parts2.slice(4);
|
|
17
|
+
const lastPart = filePath.at(-1) ?? "";
|
|
18
|
+
const path2 = lastPart.includes(".") ? filePath.slice(0, -1).join("/") : filePath.join("/");
|
|
19
|
+
return { owner: owner2, repo: repo2, path: path2, ref };
|
|
20
|
+
}
|
|
21
|
+
return { owner: owner2, repo: repo2, path: parts2.slice(2).join("/"), ref: void 0 };
|
|
22
|
+
}
|
|
23
|
+
const parts = input.split("/");
|
|
24
|
+
if (parts.length < 2) throw new Error(`Cannot parse GitHub reference: "${input}"`);
|
|
25
|
+
const owner = parts[0];
|
|
26
|
+
const repo = parts[1];
|
|
27
|
+
const path = parts.slice(2).join("/");
|
|
28
|
+
return { owner, repo, path, ref: void 0 };
|
|
29
|
+
}
|
|
30
|
+
function apiHeaders() {
|
|
31
|
+
const token = process.env["GITHUB_TOKEN"];
|
|
32
|
+
return {
|
|
33
|
+
Accept: "application/vnd.github+json",
|
|
34
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
35
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function ghFetch(url) {
|
|
39
|
+
const res = await fetch(url, { headers: apiHeaders() });
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const body = await res.text().catch(() => "");
|
|
42
|
+
throw new Error(`GitHub API ${res.status}: ${url}
|
|
43
|
+
${body}`);
|
|
44
|
+
}
|
|
45
|
+
return res.json();
|
|
46
|
+
}
|
|
47
|
+
async function listContents(owner, repo, path, ref) {
|
|
48
|
+
const base = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`;
|
|
49
|
+
const url = ref ? `${base}?ref=${encodeURIComponent(ref)}` : base;
|
|
50
|
+
const result = await ghFetch(url);
|
|
51
|
+
if (!Array.isArray(result)) throw new Error(`Expected directory at "${path}", got a file`);
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
async function latestShaForPath(owner, repo, path, ref) {
|
|
55
|
+
let url = `https://api.github.com/repos/${owner}/${repo}/commits?path=${encodeURIComponent(path)}&per_page=1`;
|
|
56
|
+
if (ref) url += `&sha=${encodeURIComponent(ref)}`;
|
|
57
|
+
const commits = await ghFetch(url);
|
|
58
|
+
if (!commits.length) throw new Error(`No commits found for path "${path}" in ${owner}/${repo}`);
|
|
59
|
+
return commits[0].sha;
|
|
60
|
+
}
|
|
61
|
+
async function defaultBranch(owner, repo) {
|
|
62
|
+
const data = await ghFetch(
|
|
63
|
+
`https://api.github.com/repos/${owner}/${repo}`
|
|
64
|
+
);
|
|
65
|
+
return data.default_branch;
|
|
66
|
+
}
|
|
67
|
+
function isSkillFile(name) {
|
|
68
|
+
return name.toLowerCase() === "skill.md";
|
|
69
|
+
}
|
|
70
|
+
async function discoverSkills(coords) {
|
|
71
|
+
const { owner, repo, path, ref } = coords;
|
|
72
|
+
try {
|
|
73
|
+
const items = await listContents(owner, repo, path, ref);
|
|
74
|
+
if (items.some((i) => i.type === "file" && isSkillFile(i.name))) {
|
|
75
|
+
const skillName = path.split("/").filter(Boolean).at(-1) ?? repo;
|
|
76
|
+
const sha = await latestShaForPath(owner, repo, path || ".", ref);
|
|
77
|
+
return [{ name: skillName, repoPath: path, gigetSource: buildGigetSource(owner, repo, path, ref), sha }];
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
const commitSha = await resolveRefToCommitSha(owner, repo, ref);
|
|
82
|
+
const { tree, truncated } = await ghFetch(
|
|
83
|
+
`https://api.github.com/repos/${owner}/${repo}/git/trees/${commitSha}?recursive=1`
|
|
84
|
+
);
|
|
85
|
+
if (truncated) {
|
|
86
|
+
console.warn("Warning: repo tree truncated, falling back to shallow scan");
|
|
87
|
+
}
|
|
88
|
+
const prefix = path ? `${path}/` : "";
|
|
89
|
+
const skillMdPaths = tree.filter((item) => item.type === "blob" && item.path.startsWith(prefix) && isSkillFile(item.path.split("/").at(-1) ?? "")).map((item) => item.path.split("/").slice(0, -1).join("/"));
|
|
90
|
+
if (!skillMdPaths.length) return [];
|
|
91
|
+
const results = await Promise.allSettled(
|
|
92
|
+
skillMdPaths.map(async (skillPath) => {
|
|
93
|
+
const sha = await latestShaForPath(owner, repo, skillPath, ref);
|
|
94
|
+
const name = skillPath.split("/").at(-1);
|
|
95
|
+
return {
|
|
96
|
+
name,
|
|
97
|
+
repoPath: skillPath,
|
|
98
|
+
gigetSource: buildGigetSource(owner, repo, skillPath, ref),
|
|
99
|
+
sha
|
|
100
|
+
};
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
return results.filter((r) => r.status === "fulfilled").map((r) => r.value).sort((a, b) => a.name.localeCompare(b.name));
|
|
104
|
+
}
|
|
105
|
+
async function resolveRefToCommitSha(owner, repo, ref) {
|
|
106
|
+
const branch = ref ?? await defaultBranch(owner, repo);
|
|
107
|
+
if (/^[0-9a-f]{40}$/i.test(branch)) return branch;
|
|
108
|
+
try {
|
|
109
|
+
const data = await ghFetch(
|
|
110
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`
|
|
111
|
+
);
|
|
112
|
+
return data.object.sha;
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const data = await ghFetch(
|
|
117
|
+
`https://api.github.com/repos/${owner}/${repo}/git/refs/tags/${branch}`
|
|
118
|
+
);
|
|
119
|
+
return data.object.sha;
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Cannot resolve ref "${branch}" in ${owner}/${repo}`);
|
|
123
|
+
}
|
|
124
|
+
function buildGigetSource(owner, repo, path, ref) {
|
|
125
|
+
const p = path ? `/${path}` : "";
|
|
126
|
+
const r = ref ? `#${ref}` : "";
|
|
127
|
+
return `github:${owner}/${repo}${p}${r}`;
|
|
128
|
+
}
|
|
129
|
+
async function downloadSkill(gigetSource, destPath) {
|
|
130
|
+
const { downloadTemplate } = await import("giget");
|
|
131
|
+
await downloadTemplate(gigetSource, {
|
|
132
|
+
dir: destPath,
|
|
133
|
+
force: true,
|
|
134
|
+
preferOffline: false
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
parseGitHubUrl,
|
|
140
|
+
latestShaForPath,
|
|
141
|
+
discoverSkills,
|
|
142
|
+
buildGigetSource,
|
|
143
|
+
downloadSkill
|
|
144
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/provenance.ts
|
|
2
|
+
import { execFile } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import { readFile } from "fs/promises";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { join, relative } from "path";
|
|
7
|
+
var exec = promisify(execFile);
|
|
8
|
+
async function detectProvenance(skillPath, _skillName) {
|
|
9
|
+
const git = await tryGitProvenance(skillPath);
|
|
10
|
+
if (git) return git;
|
|
11
|
+
const fm = await tryFrontmatterProvenance(skillPath);
|
|
12
|
+
if (fm) return fm;
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
async function tryGitProvenance(skillPath) {
|
|
16
|
+
try {
|
|
17
|
+
const { stdout: root } = await exec("git", [
|
|
18
|
+
"-C",
|
|
19
|
+
skillPath,
|
|
20
|
+
"rev-parse",
|
|
21
|
+
"--show-toplevel"
|
|
22
|
+
]);
|
|
23
|
+
const repoRoot = root.trim();
|
|
24
|
+
const { stdout: remoteRaw } = await exec("git", [
|
|
25
|
+
"-C",
|
|
26
|
+
skillPath,
|
|
27
|
+
"remote",
|
|
28
|
+
"get-url",
|
|
29
|
+
"origin"
|
|
30
|
+
]);
|
|
31
|
+
const remoteUrl = remoteRaw.trim();
|
|
32
|
+
const httpsUrl = normaliseGitUrl(remoteUrl);
|
|
33
|
+
if (!httpsUrl) return void 0;
|
|
34
|
+
const repoPath = relative(repoRoot, skillPath).replace(/\\/g, "/");
|
|
35
|
+
const { stdout: shaRaw } = await exec("git", [
|
|
36
|
+
"-C",
|
|
37
|
+
skillPath,
|
|
38
|
+
"log",
|
|
39
|
+
"-1",
|
|
40
|
+
"--format=%H",
|
|
41
|
+
"--",
|
|
42
|
+
"."
|
|
43
|
+
]);
|
|
44
|
+
const sha = shaRaw.trim() || void 0;
|
|
45
|
+
const ownerRepo = githubOwnerRepo(httpsUrl);
|
|
46
|
+
if (!ownerRepo) return void 0;
|
|
47
|
+
const source = repoPath ? `${ownerRepo}/${repoPath}` : ownerRepo;
|
|
48
|
+
const gigetSource = repoPath ? `github:${ownerRepo}/${repoPath}` : `github:${ownerRepo}`;
|
|
49
|
+
return {
|
|
50
|
+
source,
|
|
51
|
+
gigetSource,
|
|
52
|
+
sha,
|
|
53
|
+
confidence: "git",
|
|
54
|
+
description: `git remote: ${httpsUrl} path: ${repoPath || "(root)"}`
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function tryFrontmatterProvenance(skillPath) {
|
|
61
|
+
const skillMdPath = join(skillPath, "SKILL.md");
|
|
62
|
+
if (!existsSync(skillMdPath)) return void 0;
|
|
63
|
+
try {
|
|
64
|
+
const raw = await readFile(skillMdPath, "utf8");
|
|
65
|
+
const fm = parseFrontmatter(raw);
|
|
66
|
+
if (!fm) return void 0;
|
|
67
|
+
const sourceRaw = fm.source ?? fm.repo ?? fm.origin;
|
|
68
|
+
if (!sourceRaw) return void 0;
|
|
69
|
+
const ownerRepo = githubOwnerRepo(normaliseGitUrl(sourceRaw) ?? sourceRaw);
|
|
70
|
+
if (!ownerRepo) return void 0;
|
|
71
|
+
return {
|
|
72
|
+
source: ownerRepo,
|
|
73
|
+
gigetSource: `github:${ownerRepo}`,
|
|
74
|
+
sha: void 0,
|
|
75
|
+
// frontmatter doesn't carry a SHA
|
|
76
|
+
confidence: "frontmatter",
|
|
77
|
+
description: `SKILL.md frontmatter source: ${sourceRaw}`
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function parseFrontmatter(content) {
|
|
84
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
85
|
+
if (!match) return void 0;
|
|
86
|
+
const block = match[1];
|
|
87
|
+
const result = {};
|
|
88
|
+
for (const line of block.split("\n")) {
|
|
89
|
+
const colonIdx = line.indexOf(":");
|
|
90
|
+
if (colonIdx === -1) continue;
|
|
91
|
+
const key = line.slice(0, colonIdx).trim().toLowerCase();
|
|
92
|
+
const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
93
|
+
if (key && value) result[key] = value;
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
function normaliseGitUrl(raw) {
|
|
98
|
+
raw = raw.trim();
|
|
99
|
+
if (raw.startsWith("https://github.com/")) return raw.replace(/\.git$/, "");
|
|
100
|
+
if (raw.startsWith("http://github.com/")) return raw.replace("http://", "https://").replace(/\.git$/, "");
|
|
101
|
+
const sshMatch = raw.match(/^git@github\.com:(.+?)(?:\.git)?$/);
|
|
102
|
+
if (sshMatch) return `https://github.com/${sshMatch[1]}`;
|
|
103
|
+
if (/^[\w.-]+\/[\w.-]/.test(raw)) return `https://github.com/${raw.replace(/\.git$/, "")}`;
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
function githubOwnerRepo(url) {
|
|
107
|
+
if (!url) return void 0;
|
|
108
|
+
try {
|
|
109
|
+
const u = new URL(url.startsWith("http") ? url : `https://github.com/${url}`);
|
|
110
|
+
if (!u.hostname.includes("github.com")) return void 0;
|
|
111
|
+
return u.pathname.replace(/^\//, "").replace(/\.git$/, "");
|
|
112
|
+
} catch {
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function injectSourceIntoSkillMd(skillPath, source) {
|
|
117
|
+
const skillMdPath = join(skillPath, "SKILL.md");
|
|
118
|
+
if (!existsSync(skillMdPath)) return;
|
|
119
|
+
const { writeFile } = await import("fs/promises");
|
|
120
|
+
const raw = await readFile(skillMdPath, "utf8");
|
|
121
|
+
const hasFrontmatter = /^---\r?\n/.test(raw);
|
|
122
|
+
let updated;
|
|
123
|
+
if (hasFrontmatter) {
|
|
124
|
+
if (/^source\s*:/m.test(raw.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? "")) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
updated = raw.replace(/^(---\r?\n)/, `$1source: ${source}
|
|
128
|
+
`);
|
|
129
|
+
} else {
|
|
130
|
+
updated = `---
|
|
131
|
+
source: ${source}
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
${raw}`;
|
|
135
|
+
}
|
|
136
|
+
await writeFile(skillMdPath, updated, "utf8");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
detectProvenance,
|
|
141
|
+
injectSourceIntoSkillMd
|
|
142
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
versionArchivePath
|
|
3
|
+
} from "./chunk-ZW6ZKHTF.js";
|
|
4
|
+
|
|
5
|
+
// src/versions.ts
|
|
6
|
+
import { cp, mkdir, rm } from "fs/promises";
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
async function archiveVersion(sourcePath, skillName, sha) {
|
|
9
|
+
const dest = versionArchivePath(skillName, sha);
|
|
10
|
+
if (existsSync(dest)) {
|
|
11
|
+
return dest;
|
|
12
|
+
}
|
|
13
|
+
await mkdir(dest, { recursive: true });
|
|
14
|
+
await cp(sourcePath, dest, { recursive: true });
|
|
15
|
+
return dest;
|
|
16
|
+
}
|
|
17
|
+
async function restoreVersion(skillName, sha, destPaths) {
|
|
18
|
+
const src = versionArchivePath(skillName, sha);
|
|
19
|
+
if (!existsSync(src)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`No archived version found for "${skillName}" @ ${sha.slice(0, 7)}.
|
|
22
|
+
Archive path: ${src}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
await Promise.all(
|
|
26
|
+
destPaths.map(async (dest) => {
|
|
27
|
+
await rm(dest, { recursive: true, force: true });
|
|
28
|
+
await mkdir(dest, { recursive: true });
|
|
29
|
+
await cp(src, dest, { recursive: true });
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
async function pruneVersions(skillName, keepShas = []) {
|
|
34
|
+
const { join } = await import("path");
|
|
35
|
+
const { versionArchivePath: archivePath, versionsDir } = await import("./registry-QVCNZXBZ.js");
|
|
36
|
+
const skillVersionsDir = join(versionsDir(), skillName);
|
|
37
|
+
if (!existsSync(skillVersionsDir)) return;
|
|
38
|
+
const { readdir } = await import("fs/promises");
|
|
39
|
+
const entries = await readdir(skillVersionsDir);
|
|
40
|
+
await Promise.all(
|
|
41
|
+
entries.filter((e) => !keepShas.includes(e)).map((e) => rm(join(skillVersionsDir, e), { recursive: true, force: true }))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function archiveExists(skillName, sha) {
|
|
45
|
+
return existsSync(versionArchivePath(skillName, sha));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
archiveVersion,
|
|
50
|
+
restoreVersion,
|
|
51
|
+
pruneVersions,
|
|
52
|
+
archiveExists
|
|
53
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/agents.ts
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
function resolveHome(p) {
|
|
5
|
+
return p.startsWith("~") ? join(homedir(), p.slice(1)) : p;
|
|
6
|
+
}
|
|
7
|
+
function codexSkillsPath() {
|
|
8
|
+
const codexHome = process.env["CODEX_HOME"];
|
|
9
|
+
if (codexHome) return join(codexHome, "skills");
|
|
10
|
+
return resolveHome("~/.codex/skills");
|
|
11
|
+
}
|
|
12
|
+
var AGENTS = [
|
|
13
|
+
{
|
|
14
|
+
id: "opencode",
|
|
15
|
+
name: "OpenCode",
|
|
16
|
+
skillsPath: resolveHome("~/.opencode/skills"),
|
|
17
|
+
skillFile: "SKILL.md"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "claude",
|
|
21
|
+
name: "Claude",
|
|
22
|
+
skillsPath: resolveHome("~/.claude/skills"),
|
|
23
|
+
skillFile: "SKILL.md"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "codex",
|
|
27
|
+
name: "Codex",
|
|
28
|
+
skillsPath: codexSkillsPath(),
|
|
29
|
+
skillFile: "SKILL.md"
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
function agentById(id) {
|
|
33
|
+
return AGENTS.find((a) => a.id === id);
|
|
34
|
+
}
|
|
35
|
+
function agentSkillPath(agent, skillName) {
|
|
36
|
+
return join(agent.skillsPath, skillName);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
AGENTS,
|
|
41
|
+
agentById,
|
|
42
|
+
agentSkillPath
|
|
43
|
+
};
|