@rbbtsn0w/adg 0.1.0-beta.2 → 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/README.md CHANGED
@@ -33,6 +33,96 @@ See [docs/authoring.md](docs/authoring.md) to author a plugin, and
33
33
 
34
34
  ---
35
35
 
36
+ # Install and quick start
37
+
38
+ Install the CLI once, then run `adg` from anywhere:
39
+
40
+ ```bash
41
+ npm install -g @rbbtsn0w/adg # stable channel
42
+ npm install -g @rbbtsn0w/adg@beta # pre-release channel
43
+ # or run ad-hoc, no install:
44
+ npx @rbbtsn0w/adg --help
45
+ ```
46
+
47
+ Typical end-user flow — pull a marketplace into your global store, then load it
48
+ into the runtimes you use:
49
+
50
+ ```bash
51
+ # 1) collect plugins into the global store (~/.agents/plugins)
52
+ adg plugins add anthropics/knowledge-work-plugins --ref main --global
53
+ # large monorepo? fetch only what you need:
54
+ adg plugins add anthropics/knowledge-work-plugins --ref main --sparse engineering --global
55
+
56
+ # 2) load into the runtimes you use
57
+ adg plugins link --target codex --global # Codex discovers ~/.agents/plugins natively
58
+ adg plugins link --target claude --global # Claude loads via ~/.claude/skills symlinks
59
+
60
+ # 3) keep it current
61
+ adg plugins update --global
62
+ adg plugins list --global
63
+ ```
64
+
65
+ `adg` is the only command you invoke — no Node build step beyond the global
66
+ install. To hack on the CLI itself, see [Developing from source](#developing-from-source).
67
+
68
+ ---
69
+
70
+ # Works with existing ecosystems
71
+
72
+ ADG is **not** a new plugin format you have to migrate to. Any repo that already
73
+ ships `.claude-plugin/` or `.codex-plugin/` manifests is ingested as-is: on the
74
+ way in, `add` discovers each native manifest and **reverse-adapts** it into a
75
+ canonical `.agents/.plugin.json` (the inverse of `adapt`), then ADG manages and
76
+ re-projects it like any first-party plugin. No fork, no edits upstream.
77
+
78
+ The two examples below are real, popular repositories — neither is ADG-native.
79
+
80
+ ### Example 1 — `anthropics/knowledge-work-plugins` (a category monorepo)
81
+
82
+ A marketplace monorepo where each top-level category (`engineering/`,
83
+ `marketing/`, `legal/`, …) is its own plugin with a `.claude-plugin/plugin.json`
84
+ and a `skills/` tree. Pull the whole thing, or sparse-checkout just the
85
+ categories you want:
86
+
87
+ ```bash
88
+ # whole marketplace into the global store
89
+ adg plugins add anthropics/knowledge-work-plugins --ref main --global
90
+
91
+ # or fetch only one category from the large monorepo
92
+ adg plugins add anthropics/knowledge-work-plugins --ref main --sparse engineering --global
93
+
94
+ # each category's .claude-plugin manifest is reverse-adapted on import,
95
+ # then projected back onto the runtimes you use
96
+ adg plugins link --target claude --global # → ~/.claude/skills/<plugin>:<skill>
97
+ adg plugins link --target codex --global # native, zero-copy
98
+ adg plugins list --global
99
+ ```
100
+
101
+ ### Example 2 — `obra/superpowers` (a single multi-runtime plugin)
102
+
103
+ A single skills plugin that already ships `.claude-plugin/`, `.codex-plugin/` and
104
+ a `skills/` library. Because the native manifests are already present, ADG simply
105
+ adopts it — discovery picks up the existing manifest, records provenance and a
106
+ content hash in the lock, and from then on it updates like any ADG plugin:
107
+
108
+ ```bash
109
+ adg plugins add obra/superpowers --ref main --global
110
+
111
+ # now under management — same lifecycle as a first-party plugin
112
+ adg plugins list --global
113
+ adg plugins update --global
114
+ adg plugins link --target claude --global
115
+ ```
116
+
117
+ > Both repos are pulled by `owner/repo` shorthand over a shallow clone (sparse
118
+ > checkout when `--sparse` is given). Provenance — `{type:"github",repo,ref,path}`
119
+ > — plus a `sha256` integrity hash land in `.plugin-lock.json`, so the install is
120
+ > reproducible regardless of which ecosystem the plugin originally came from. See
121
+ > [Importing existing inventory (via `add`)](#importing-existing-inventory-via-add)
122
+ > for the discovery and reverse-adaptation details.
123
+
124
+ ---
125
+
36
126
  # Concepts (common)
37
127
 
38
128
  These apply the same whether you run a released build or the source tree.
@@ -99,7 +189,7 @@ The command surface is identical in both modes — **only the launcher differs**
99
189
 
100
190
  | Mode | Launcher | Setup |
101
191
  |------|----------|-------|
102
- | Released build | `adg …` | install the package (see [Using a released build](#using-a-released-build)) |
192
+ | Released build | `adg …` | install the package (see [Install and quick start](#install-and-quick-start)) |
103
193
  | From source (debug) | `node bin/adg.ts …` | clone + `npm install` (see [Developing from source](#developing-from-source)) |
104
194
 
105
195
  The examples below use the released `adg` launcher. **When running from source,
@@ -233,40 +323,6 @@ private discovery path:
233
323
 
234
324
  ---
235
325
 
236
- # Using a released build
237
-
238
- > Pre-release: not yet published to npm. This is the intended end-user flow.
239
-
240
- Install the CLI once, then use the `adg` command from anywhere:
241
-
242
- ```bash
243
- npm install -g adg # or: npx adg <command>
244
- adg --help
245
- ```
246
-
247
- Typical end-user workflow — bring a marketplace into your global environment and
248
- load it into your runtimes:
249
-
250
- ```bash
251
- # 1) collect plugins into the global store (~/.agents/plugins)
252
- adg plugins add anthropics/knowledge-work-plugins --ref main --global
253
- # large monorepo? fetch only what you need:
254
- adg plugins add anthropics/knowledge-work-plugins --ref main --sparse engineering --global
255
-
256
- # 2) load into the runtimes you use
257
- adg plugins link --target codex --global # Codex discovers ~/.agents/plugins natively
258
- adg plugins link --target claude --global # Claude loads via ~/.claude/skills symlinks
259
-
260
- # 3) keep it current
261
- adg plugins update --global
262
- adg plugins list --global
263
- ```
264
-
265
- No Node toolchain step is required beyond the global install; `adg` is the only
266
- command you invoke.
267
-
268
- ---
269
-
270
326
  # Developing from source
271
327
 
272
328
  For working on the CLI itself, or testing a plugin before release. The CLI runs
@@ -1,9 +1,8 @@
1
1
  import { existsSync, lstatSync, readdirSync, readlinkSync, rmdirSync, rmSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
3
  import { claudeSkillsDir, lockPath, marketplacePath, pluginDir } from "../paths.js";
4
4
  import { readLock, removeEntry, writeLock } from "../lock.js";
5
5
  import { readMarketplace, removeMarketplacePlugin, writeMarketplace } from "../marketplace.js";
6
- import { basename } from "node:path";
7
6
  import { resolveAgents } from "../agents/index.js";
8
7
  /**
9
8
  * Remove an installed plugin: delete its directory, drop it from
@@ -12,7 +12,8 @@ import { resolveAgents } from "../agents/index.js";
12
12
  * changed are reported as `changed`. A missing plugin directory is reported as
13
13
  * an issue rather than silently dropped.
14
14
  *
15
- * With `resync`, changed plugins are re-adapted (honoring their selection) and
15
+ * Changed plugins always have their runtime manifests regenerated (honoring
16
+ * their selection). With `resync`, the regenerated content is additionally
16
17
  * re-installed into the agents so Claude/Codex reflect the new content.
17
18
  */
18
19
  export function updateLock(pluginsDir, now = new Date().toISOString(), opts = {}) {
@@ -1,7 +1,6 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { ADG_MANIFEST_PATH, collectIssues } from "../manifest.js";
4
- import { readFileSync } from "node:fs";
5
4
  /**
6
5
  * Validate a plugin's manifest against the ADG schema and check that the
7
6
  * directories/files it references actually exist.
@@ -1,10 +1,16 @@
1
1
  export function parseVersion(v) {
2
2
  const core = v.trim().replace(/^[v=]/, "").split(/[-+]/)[0] ?? "";
3
3
  const parts = core.split(".");
4
- if (parts.length !== 3 || parts.some((p) => !/^\d+$/.test(p))) {
4
+ // Accept partial ranges (`1`, `1.2`) by defaulting missing minor/patch to 0,
5
+ // matching how semver ranges are commonly written in manifests.
6
+ if (parts.length < 1 || parts.length > 3 || parts.some((p) => !/^\d+$/.test(p))) {
5
7
  throw new Error(`invalid semantic version: "${v}"`);
6
8
  }
7
- return [Number(parts[0]), Number(parts[1]), Number(parts[2])];
9
+ return [
10
+ Number(parts[0]),
11
+ parts[1] !== undefined ? Number(parts[1]) : 0,
12
+ parts[2] !== undefined ? Number(parts[2]) : 0,
13
+ ];
8
14
  }
9
15
  export function compare(a, b) {
10
16
  for (let i = 0; i < 3; i++) {
@@ -552,6 +552,13 @@ export class WellKnownProvider {
552
552
  const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
553
553
  const dataStart = localHeaderOffset + 30 + localFileNameLength + localExtraLength;
554
554
  const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
555
+ // ADG patch: reject zip bombs before decompressing. The central directory
556
+ // declares the uncompressed size, so check it against the running budget
557
+ // up front — inflateRawSync would otherwise allocate the full (potentially
558
+ // multi-GB) output and OOM the process before addArchiveFile's check runs.
559
+ if (runningTotal.bytes + uncompressedSize > MAX_ARCHIVE_UNPACKED_BYTES) {
560
+ throw new Error('Archive exceeds maximum unpacked size');
561
+ }
555
562
  let content;
556
563
  if (method === 0) {
557
564
  content = compressed;
@@ -72,7 +72,18 @@ export function parseOwnerRepo(ownerRepo) {
72
72
  */
73
73
  export async function isRepoPrivate(owner, repo) {
74
74
  try {
75
- const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
75
+ // ADG patch: GitHub's API rejects requests without a User-Agent (403), and
76
+ // authenticating raises rate limits and reaches private repos. Read the
77
+ // token from env directly to keep this best-effort check silent (no gh spawn).
78
+ const headers = {
79
+ 'User-Agent': 'skills-cli',
80
+ Accept: 'application/vnd.github.v3+json',
81
+ };
82
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
83
+ if (token) {
84
+ headers.Authorization = `Bearer ${token}`;
85
+ }
86
+ const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers });
76
87
  // If repo doesn't exist or we don't have access, assume private to be safe
77
88
  if (!res.ok) {
78
89
  return null; // Unable to determine
@@ -87,23 +87,31 @@ export function buildUsePrompt(input) {
87
87
  }
88
88
  export async function materializeUseSkill(skill) {
89
89
  const tempRoot = await mkdtemp(join(tmpdir(), 'skills-use-'));
90
- const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
91
- if (!isPathSafe(tempRoot, skillDir)) {
92
- throw new Error('Invalid skill name: potential path traversal detected');
93
- }
94
- await mkdir(skillDir, { recursive: true });
95
- if (skill.kind === 'blob') {
96
- await writeSnapshotFiles(skillDir, skill.files);
97
- }
98
- else if (skill.kind === 'well-known') {
99
- await writeMapFiles(skillDir, skill.files);
90
+ // ADG patch: clean up tempRoot if any setup step fails. runUse only tracks
91
+ // cloneTempDir, so without this a failed materialize would leak the temp dir.
92
+ try {
93
+ const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
94
+ if (!isPathSafe(tempRoot, skillDir)) {
95
+ throw new Error('Invalid skill name: potential path traversal detected');
96
+ }
97
+ await mkdir(skillDir, { recursive: true });
98
+ if (skill.kind === 'blob') {
99
+ await writeSnapshotFiles(skillDir, skill.files);
100
+ }
101
+ else if (skill.kind === 'well-known') {
102
+ await writeMapFiles(skillDir, skill.files);
103
+ }
104
+ else {
105
+ await copySkillDirectory(skill.path, skillDir);
106
+ }
107
+ const skillMd = skill.rawContent ?? (await readFile(join(skillDir, 'SKILL.md'), 'utf-8'));
108
+ const hasSupportingFiles = await containsSupportingFiles(skillDir, skillDir);
109
+ return { tempRoot, skillDir, skillMd, hasSupportingFiles };
100
110
  }
101
- else {
102
- await copySkillDirectory(skill.path, skillDir);
111
+ catch (error) {
112
+ await cleanupTempDir(tempRoot).catch(() => { });
113
+ throw error;
103
114
  }
104
- const skillMd = skill.rawContent ?? (await readFile(join(skillDir, 'SKILL.md'), 'utf-8'));
105
- const hasSupportingFiles = await containsSupportingFiles(skillDir, skillDir);
106
- return { tempRoot, skillDir, skillMd, hasSupportingFiles };
107
115
  }
108
116
  export async function runUse(sourceArgs, options = {}, parseErrors = []) {
109
117
  let cloneTempDir = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rbbtsn0w/adg",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0",
4
4
  "description": "Agent Directory Group (ADG) toolkit — two domains: plugins and skills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -725,6 +725,14 @@ export class WellKnownProvider implements HostProvider {
725
725
  const dataStart = localHeaderOffset + 30 + localFileNameLength + localExtraLength;
726
726
  const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
727
727
 
728
+ // ADG patch: reject zip bombs before decompressing. The central directory
729
+ // declares the uncompressed size, so check it against the running budget
730
+ // up front — inflateRawSync would otherwise allocate the full (potentially
731
+ // multi-GB) output and OOM the process before addArchiveFile's check runs.
732
+ if (runningTotal.bytes + uncompressedSize > MAX_ARCHIVE_UNPACKED_BYTES) {
733
+ throw new Error('Archive exceeds maximum unpacked size');
734
+ }
735
+
728
736
  let content: Buffer;
729
737
  if (method === 0) {
730
738
  content = compressed;
@@ -82,7 +82,18 @@ export function parseOwnerRepo(ownerRepo: string): { owner: string; repo: string
82
82
  */
83
83
  export async function isRepoPrivate(owner: string, repo: string): Promise<boolean | null> {
84
84
  try {
85
- const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
85
+ // ADG patch: GitHub's API rejects requests without a User-Agent (403), and
86
+ // authenticating raises rate limits and reaches private repos. Read the
87
+ // token from env directly to keep this best-effort check silent (no gh spawn).
88
+ const headers: Record<string, string> = {
89
+ 'User-Agent': 'skills-cli',
90
+ Accept: 'application/vnd.github.v3+json',
91
+ };
92
+ const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
93
+ if (token) {
94
+ headers.Authorization = `Bearer ${token}`;
95
+ }
96
+ const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers });
86
97
 
87
98
  // If repo doesn't exist or we don't have access, assume private to be safe
88
99
  if (!res.ok) {
@@ -157,26 +157,33 @@ export function buildUsePrompt(input: {
157
157
 
158
158
  export async function materializeUseSkill(skill: UseSkill): Promise<MaterializedUseSkill> {
159
159
  const tempRoot = await mkdtemp(join(tmpdir(), 'skills-use-'));
160
- const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
160
+ // ADG patch: clean up tempRoot if any setup step fails. runUse only tracks
161
+ // cloneTempDir, so without this a failed materialize would leak the temp dir.
162
+ try {
163
+ const skillDir = join(tempRoot, sanitizeName(skill.directoryName || skill.name));
161
164
 
162
- if (!isPathSafe(tempRoot, skillDir)) {
163
- throw new Error('Invalid skill name: potential path traversal detected');
164
- }
165
+ if (!isPathSafe(tempRoot, skillDir)) {
166
+ throw new Error('Invalid skill name: potential path traversal detected');
167
+ }
165
168
 
166
- await mkdir(skillDir, { recursive: true });
169
+ await mkdir(skillDir, { recursive: true });
167
170
 
168
- if (skill.kind === 'blob') {
169
- await writeSnapshotFiles(skillDir, skill.files);
170
- } else if (skill.kind === 'well-known') {
171
- await writeMapFiles(skillDir, skill.files);
172
- } else {
173
- await copySkillDirectory(skill.path, skillDir);
174
- }
171
+ if (skill.kind === 'blob') {
172
+ await writeSnapshotFiles(skillDir, skill.files);
173
+ } else if (skill.kind === 'well-known') {
174
+ await writeMapFiles(skillDir, skill.files);
175
+ } else {
176
+ await copySkillDirectory(skill.path, skillDir);
177
+ }
175
178
 
176
- const skillMd = skill.rawContent ?? (await readFile(join(skillDir, 'SKILL.md'), 'utf-8'));
177
- const hasSupportingFiles = await containsSupportingFiles(skillDir, skillDir);
179
+ const skillMd = skill.rawContent ?? (await readFile(join(skillDir, 'SKILL.md'), 'utf-8'));
180
+ const hasSupportingFiles = await containsSupportingFiles(skillDir, skillDir);
178
181
 
179
- return { tempRoot, skillDir, skillMd, hasSupportingFiles };
182
+ return { tempRoot, skillDir, skillMd, hasSupportingFiles };
183
+ } catch (error) {
184
+ await cleanupTempDir(tempRoot).catch(() => {});
185
+ throw error;
186
+ }
180
187
  }
181
188
 
182
189
  export async function runUse(