@rbbtsn0w/adg 0.1.0-beta.1 → 0.1.0-beta.3

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,40 @@ 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
+
36
70
  # Concepts (common)
37
71
 
38
72
  These apply the same whether you run a released build or the source tree.
@@ -99,7 +133,7 @@ The command surface is identical in both modes — **only the launcher differs**
99
133
 
100
134
  | Mode | Launcher | Setup |
101
135
  |------|----------|-------|
102
- | Released build | `adg …` | install the package (see [Using a released build](#using-a-released-build)) |
136
+ | Released build | `adg …` | install the package (see [Install and quick start](#install-and-quick-start)) |
103
137
  | From source (debug) | `node bin/adg.ts …` | clone + `npm install` (see [Developing from source](#developing-from-source)) |
104
138
 
105
139
  The examples below use the released `adg` launcher. **When running from source,
@@ -233,40 +267,6 @@ private discovery path:
233
267
 
234
268
  ---
235
269
 
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
270
  # Developing from source
271
271
 
272
272
  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.1",
3
+ "version": "0.1.0-beta.3",
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(