@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 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
+ };