codiedev 0.9.83 → 0.9.85

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.
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SHA256_HEX = exports.PACK_FILES_MAX_TOTAL_BYTES = exports.PACK_FILE_MAX_BYTES = exports.PACK_MAX_FILES = void 0;
37
+ exports.packFilePathProblem = packFilePathProblem;
38
+ exports.blobsDir = blobsDir;
39
+ exports.blobPath = blobPath;
40
+ exports.sha256Of = sha256Of;
41
+ exports.ensureBlobs = ensureBlobs;
42
+ exports.parseFilesSection = parseFilesSection;
43
+ exports.stripFilesSection = stripFilesSection;
44
+ exports.expandHome = expandHome;
45
+ exports.collectLocalFiles = collectLocalFiles;
46
+ exports.rewritePluginRoot = rewritePluginRoot;
47
+ // Starter-pack files (SIG-127): skill subfolders, scripts, PDFs and runner
48
+ // folders shipped inside a pack's plugin dir.
49
+ //
50
+ // - Path rules mirror convex/lib/skillPackManifest.ts packFilePathProblem.
51
+ // The server checks them; the CLI checks again before any write, because a
52
+ // manifest is untrusted input that becomes paths on the member's machine.
53
+ // - Blobs are content-addressed (sha256 hex) and cached under
54
+ // <packs root>/.blobs/<sha256>, verified on download, so reinstalls and
55
+ // the portable copy never fetch twice.
56
+ // - `## Files` in pack.md maps local folders to plugin paths for
57
+ // `codiedev pack apply`: `- runner <- ./runner` (`.` = plugin root).
58
+ const crypto_1 = require("crypto");
59
+ const fs = __importStar(require("fs"));
60
+ const os = __importStar(require("os"));
61
+ const path = __importStar(require("path"));
62
+ exports.PACK_MAX_FILES = 500;
63
+ exports.PACK_FILE_MAX_BYTES = 15 * 1024 * 1024;
64
+ exports.PACK_FILES_MAX_TOTAL_BYTES = 50 * 1024 * 1024;
65
+ const PATH_MAX = 300;
66
+ const MAX_DEPTH = 16;
67
+ const FILE_SEGMENT = /^[A-Za-z0-9_@+][A-Za-z0-9._ @+=,-]{0,127}$/;
68
+ exports.SHA256_HEX = /^[0-9a-f]{64}$/;
69
+ /** Why a plugin-relative path can't be written, or null when it's fine. */
70
+ function packFilePathProblem(raw) {
71
+ if (typeof raw !== "string" || !raw)
72
+ return "empty path";
73
+ if (raw.length > PATH_MAX)
74
+ return `longer than ${PATH_MAX} characters`;
75
+ if (raw.includes("\\"))
76
+ return "use / as the separator";
77
+ if (raw.startsWith("/") || /^[A-Za-z]:/.test(raw))
78
+ return "must be relative to the pack folder";
79
+ const segments = raw.split("/");
80
+ if (segments.length > MAX_DEPTH)
81
+ return `nested deeper than ${MAX_DEPTH} folders`;
82
+ for (const seg of segments) {
83
+ if (!seg || seg === "." || seg === "..")
84
+ return "empty, . or .. segment";
85
+ if (!FILE_SEGMENT.test(seg) || seg.trim() !== seg || seg.endsWith("."))
86
+ return `unsupported name "${seg}"`;
87
+ }
88
+ const lower = raw.toLowerCase();
89
+ if (lower.startsWith(".claude-plugin/"))
90
+ return ".claude-plugin/ is written by Codiedev";
91
+ if (lower === "hooks/hooks.json")
92
+ return "hooks/hooks.json comes from the pack's hooks";
93
+ if (segments.length === 2 && segments[0].toLowerCase() === "agents" && lower.endsWith(".md"))
94
+ return "agents/*.md come from the pack's agents";
95
+ if (segments.length === 3 && segments[0].toLowerCase() === "skills" && segments[2].toLowerCase() === "skill.md") {
96
+ return "SKILL.md comes from the skill library";
97
+ }
98
+ return null;
99
+ }
100
+ // ─── Blob cache ─────────────────────────────────────────────────────────────
101
+ function blobsDir(root) {
102
+ return path.join(root, ".blobs");
103
+ }
104
+ function blobPath(dir, sha256) {
105
+ if (!exports.SHA256_HEX.test(sha256))
106
+ throw new Error(`bad sha256: ${sha256}`);
107
+ return path.join(dir, sha256);
108
+ }
109
+ function sha256Of(buf) {
110
+ return (0, crypto_1.createHash)("sha256").update(buf).digest("hex");
111
+ }
112
+ function hasBlob(dir, file) {
113
+ try {
114
+ return fs.statSync(blobPath(dir, file.sha256)).size === file.size;
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ }
120
+ /** Download every blob the manifest needs that isn't cached yet. Each body
121
+ * must match its sha256 and size or nothing is kept. */
122
+ async function ensureBlobs(dir, files, download) {
123
+ let downloaded = 0;
124
+ const seen = new Set();
125
+ for (const file of files) {
126
+ if (seen.has(file.sha256) || hasBlob(dir, file))
127
+ continue;
128
+ seen.add(file.sha256);
129
+ const body = await download(file);
130
+ if (body.length !== file.size || sha256Of(body) !== file.sha256) {
131
+ throw new Error(`${file.path}: downloaded bytes don't match the pack (checksum mismatch)`);
132
+ }
133
+ fs.mkdirSync(dir, { recursive: true });
134
+ const target = blobPath(dir, file.sha256);
135
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
136
+ fs.writeFileSync(tmp, body, { mode: 0o644 });
137
+ fs.renameSync(tmp, target);
138
+ downloaded += 1;
139
+ }
140
+ return { downloaded };
141
+ }
142
+ /** The `## Files` section of a pack.md, or null when there is none (then
143
+ * `pack apply` leaves the pack's files alone). Fenced blocks are skipped so
144
+ * agent bodies can't open a section. */
145
+ function parseFilesSection(markdown) {
146
+ const lines = markdown.replace(/^/, "").split("\n").map((l) => l.replace(/\r$/, ""));
147
+ let fence = null;
148
+ let inFiles = false;
149
+ let found = false;
150
+ const out = [];
151
+ for (let i = 0; i < lines.length; i++) {
152
+ const line = lines[i];
153
+ const f = line.match(/^(`{3,})/);
154
+ if (f) {
155
+ if (!fence)
156
+ fence = f[1];
157
+ else if (f[1].length >= fence.length && /^`+\s*$/.test(line))
158
+ fence = null;
159
+ continue;
160
+ }
161
+ if (fence)
162
+ continue;
163
+ const h2 = line.match(/^##\s+(.+?)\s*$/);
164
+ if (h2 && !line.startsWith("###")) {
165
+ inFiles = h2[1].toLowerCase() === "files";
166
+ if (inFiles)
167
+ found = true;
168
+ continue;
169
+ }
170
+ if (!inFiles || !line.trim())
171
+ continue;
172
+ const m = line.match(/^\s*[-*]\s+(.+?)\s+<-\s+(.+?)\s*$/);
173
+ if (!m)
174
+ throw new Error(`pack.md line ${i + 1}: expected "- <path in pack> <- <local path>" under ## Files`);
175
+ out.push({ dest: unquote(m[1]), source: unquote(m[2]), line: i + 1 });
176
+ }
177
+ return found ? out : null;
178
+ }
179
+ function unquote(value) {
180
+ const v = value.trim();
181
+ return (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")) ? v.slice(1, -1) : v;
182
+ }
183
+ /** pack.md with its `## Files` section (heading and body) removed. */
184
+ function stripFilesSection(markdown) {
185
+ const lines = markdown.split("\n");
186
+ const out = [];
187
+ let fence = null;
188
+ let skipping = false;
189
+ for (const raw of lines) {
190
+ const line = raw.replace(/\r$/, "");
191
+ const f = line.match(/^(`{3,})/);
192
+ if (f && !skipping) {
193
+ if (!fence)
194
+ fence = f[1];
195
+ else if (f[1].length >= fence.length && /^`+\s*$/.test(line))
196
+ fence = null;
197
+ out.push(raw);
198
+ continue;
199
+ }
200
+ if (!fence) {
201
+ const h2 = line.match(/^##\s+(.+?)\s*$/);
202
+ if (h2 && !line.startsWith("###"))
203
+ skipping = h2[1].toLowerCase() === "files";
204
+ }
205
+ if (!skipping)
206
+ out.push(raw);
207
+ }
208
+ return out.join("\n");
209
+ }
210
+ // ─── Collecting local files for `pack apply` ────────────────────────────────
211
+ // Build/cache/VCS clutter that never belongs in a pack.
212
+ const SKIP_DIRS = new Set(["__pycache__", ".git", "node_modules", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".ruff_cache", ".idea", ".vscode"]);
213
+ const SKIP_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
214
+ const SKIP_EXT = /\.(pyc|pyo|swp|tmp)$/i;
215
+ function expandHome(p, home = os.homedir()) {
216
+ return p === "~" ? home : p.startsWith("~/") ? path.join(home, p.slice(2)) : p;
217
+ }
218
+ /** Walk each `## Files` entry. Junk (caches, VCS, editor files) is skipped
219
+ * quietly; symlinks, hidden files and a skill folder's own SKILL.md are
220
+ * skipped with a note. Throws on a missing source or a bad destination. */
221
+ function collectLocalFiles(entries, baseDir, home = os.homedir()) {
222
+ const files = new Map();
223
+ const notes = [];
224
+ let total = 0;
225
+ const add = (dest, abs) => {
226
+ const problem = packFilePathProblem(dest);
227
+ if (problem) {
228
+ const parts = dest.split("/");
229
+ if (parts.length === 3 && parts[0] === "skills" && parts[2].toLowerCase() === "skill.md") {
230
+ notes.push(`skipped ${dest} (the skill itself comes from the skill library; push it with \`codiedev push\`)`);
231
+ return;
232
+ }
233
+ if (parts.some((s) => s.startsWith(".") && s !== "." && s !== "..")) {
234
+ notes.push(`skipped hidden file ${dest}`);
235
+ return;
236
+ }
237
+ throw new Error(`can't ship ${dest}: ${problem}`);
238
+ }
239
+ const stat = fs.statSync(abs);
240
+ if (stat.size > exports.PACK_FILE_MAX_BYTES)
241
+ throw new Error(`${abs} is over ${exports.PACK_FILE_MAX_BYTES / (1024 * 1024)}MB`);
242
+ const key = dest.toLowerCase();
243
+ if (files.has(key))
244
+ throw new Error(`two sources write ${dest}`);
245
+ const buf = fs.readFileSync(abs);
246
+ total += buf.length;
247
+ files.set(key, { path: dest, abs, size: buf.length, sha256: sha256Of(buf), ...((stat.mode & 0o111) !== 0 ? { executable: true } : {}) });
248
+ };
249
+ const walk = (dir, dest, depth) => {
250
+ if (depth > MAX_DEPTH)
251
+ throw new Error(`${dir} is nested too deep`);
252
+ for (const name of fs.readdirSync(dir).sort()) {
253
+ const abs = path.join(dir, name);
254
+ const lst = fs.lstatSync(abs);
255
+ const child = dest ? `${dest}/${name}` : name;
256
+ if (lst.isSymbolicLink()) {
257
+ notes.push(`skipped symlink ${child}`);
258
+ continue;
259
+ }
260
+ if (lst.isDirectory()) {
261
+ if (!SKIP_DIRS.has(name))
262
+ walk(abs, child, depth + 1);
263
+ continue;
264
+ }
265
+ if (!lst.isFile() || SKIP_FILES.has(name) || SKIP_EXT.test(name))
266
+ continue;
267
+ add(child, abs);
268
+ }
269
+ };
270
+ for (const entry of entries) {
271
+ const dest = entry.dest.replace(/^\.\/+/, "").replace(/\/+$/, "");
272
+ if (dest !== "." && (!dest || dest.startsWith("/") || dest.split("/").some((seg) => !seg || seg === "." || seg === ".."))) {
273
+ throw new Error(`pack.md line ${entry.line}: "${entry.dest}" must be a path inside the pack (or "." for its root)`);
274
+ }
275
+ const source = path.resolve(baseDir, expandHome(entry.source, home));
276
+ let stat;
277
+ try {
278
+ stat = fs.statSync(source);
279
+ }
280
+ catch {
281
+ throw new Error(`pack.md line ${entry.line}: ${entry.source} doesn't exist`);
282
+ }
283
+ if (stat.isDirectory())
284
+ walk(source, dest === "." ? "" : dest, 0);
285
+ else if (stat.isFile()) {
286
+ if (dest === "." || !dest)
287
+ throw new Error(`pack.md line ${entry.line}: give a file a path, e.g. "- tools/${path.basename(source)} <- ${entry.source}"`);
288
+ add(dest, source);
289
+ }
290
+ else
291
+ throw new Error(`pack.md line ${entry.line}: ${entry.source} is not a file or folder`);
292
+ }
293
+ const list = [...files.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
294
+ if (list.length > exports.PACK_MAX_FILES)
295
+ throw new Error(`a pack holds at most ${exports.PACK_MAX_FILES} files (found ${list.length})`);
296
+ if (total > exports.PACK_FILES_MAX_TOTAL_BYTES)
297
+ throw new Error(`pack files total over ${exports.PACK_FILES_MAX_TOTAL_BYTES / (1024 * 1024)}MB`);
298
+ return { files: list, notes };
299
+ }
300
+ // ─── Portable copies ────────────────────────────────────────────────────────
301
+ /** Only Claude Code sets ${CLAUDE_PLUGIN_ROOT}. In the copy other tools read,
302
+ * replace it with the plugin dir's real path (forward slashes, which every
303
+ * shell and Python accept, Windows included). */
304
+ function rewritePluginRoot(content, pluginRoot) {
305
+ const root = pluginRoot.split(path.sep).join("/");
306
+ return content.replace(/\$\{CLAUDE_PLUGIN_ROOT\}|\$CLAUDE_PLUGIN_ROOT(?![A-Za-z0-9_])/g, root);
307
+ }
@@ -259,8 +259,8 @@ function copyCursorSkills(home, manifest, pluginDirPath) {
259
259
  }
260
260
  fs.rmSync(dest, { recursive: true, force: true });
261
261
  }
262
- fs.mkdirSync(dest, { recursive: true });
263
- fs.copyFileSync(path.join(pluginDirPath, "skills", skill.name, "SKILL.md"), path.join(dest, "SKILL.md"));
262
+ // The whole skill folder: SKILL.md plus any pack files under it (SIG-127).
263
+ copyTreeNoSymlinks(path.join(pluginDirPath, "skills", skill.name), dest);
264
264
  fs.writeFileSync(path.join(dest, exports.CURSOR_MARKER), `${manifest.slug}\n`);
265
265
  }
266
266
  return notes;
@@ -321,8 +321,9 @@ function copyTreeNoSymlinks(source, dest, depth = 0) {
321
321
  const stat = fs.statSync(from); // follows links
322
322
  if (stat.isDirectory())
323
323
  copyTreeNoSymlinks(from, to, depth + 1);
324
+ // Keep the executable bit (pack scripts).
324
325
  else if (stat.isFile())
325
- fs.writeFileSync(to, fs.readFileSync(from));
326
+ fs.writeFileSync(to, fs.readFileSync(from), { mode: stat.mode & 0o777 });
326
327
  }
327
328
  }
328
329
  /** Copy the plugin dir into place via a sibling temp dir + rename. Refuses to
@@ -10,6 +10,8 @@ export interface InstallOptions {
10
10
  home?: string;
11
11
  run?: Runner;
12
12
  now?: () => number;
13
+ /** Where pack file blobs were downloaded (default <packs root>/.blobs). */
14
+ blobDir?: string;
13
15
  }
14
16
  export interface PackInstallResult {
15
17
  slug: string;
@@ -26,6 +28,8 @@ export declare function codexPackHooksPath(codexDir: string, slug: string, versi
26
28
  /** Trust entries for every registry pack installed into Codex. */
27
29
  export declare function codexPackTrustEntries(registry: Registry, codexDir: string): Map<string, CodexHookTrustEntry[]>;
28
30
  export declare function installPacks(options: InstallOptions): PackInstallResult[];
31
+ /** Drop cached blobs no installed pack references (best-effort). */
32
+ export declare function pruneBlobs(root: string, registry: Registry, dir?: string): number;
29
33
  export interface UninstallOptions {
30
34
  slugs: string[];
31
35
  env?: NodeJS.ProcessEnv;
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.codexPackHooksPath = codexPackHooksPath;
37
37
  exports.codexPackTrustEntries = codexPackTrustEntries;
38
38
  exports.installPacks = installPacks;
39
+ exports.pruneBlobs = pruneBlobs;
39
40
  exports.uninstallPacks = uninstallPacks;
40
41
  exports.formatUninstallLines = formatUninstallLines;
41
42
  exports.formatResultLines = formatResultLines;
@@ -50,6 +51,7 @@ const codexHookTrust_1 = require("../codexHookTrust");
50
51
  const harnesses_1 = require("./harnesses");
51
52
  const manifest_1 = require("./manifest");
52
53
  const paths_1 = require("./paths");
54
+ const files_1 = require("./files");
53
55
  const shim_1 = require("./shim");
54
56
  const store_1 = require("./store");
55
57
  function marketplaceEntries(registry) {
@@ -136,7 +138,7 @@ function installPacks(options) {
136
138
  for (const manifest of options.manifests) {
137
139
  const dir = (0, paths_1.pluginDir)(root, manifest.slug);
138
140
  const previous = registry.packs[manifest.slug];
139
- const files = Object.keys((0, store_1.pluginFiles)(manifest)).sort();
141
+ const files = [...Object.keys((0, store_1.pluginFiles)(manifest)), ...(manifest.files ?? []).map((f) => f.path)].sort();
140
142
  const results = [];
141
143
  if (options.dryRun) {
142
144
  for (const adapter of adapters) {
@@ -148,9 +150,11 @@ function installPacks(options) {
148
150
  output.push({ slug: manifest.slug, version: manifest.version, contentHash: manifest.contentHash, dir, files, results, harnesses: [] });
149
151
  continue;
150
152
  }
151
- (0, store_1.writePluginDir)(root, manifest);
152
- // Same plugin with agent model levels removed, for Codex/Copilot/Cursor.
153
- (0, store_1.writePluginDir)((0, paths_1.portableRoot)(root), manifest, { portable: true });
153
+ const blobDir = options.blobDir ?? (0, files_1.blobsDir)(root);
154
+ (0, store_1.writePluginDir)(root, manifest, { blobDir });
155
+ // Same plugin with agent model levels removed and ${CLAUDE_PLUGIN_ROOT}
156
+ // made a real path, for Codex/Copilot/Cursor/pi/Codie.
157
+ (0, store_1.writePluginDir)((0, paths_1.portableRoot)(root), manifest, { portable: true, blobDir });
154
158
  // Harnesses read the marketplace during install, so it must list this pack first.
155
159
  const pending = {
156
160
  packs: {
@@ -196,12 +200,14 @@ function installPacks(options) {
196
200
  description: manifest.description,
197
201
  ...(manifest.channel ? { channel: manifest.channel } : {}),
198
202
  ...(manifest.icon ? { icon: manifest.icon } : {}),
203
+ ...(manifest.files?.length ? { blobs: [...new Set(manifest.files.map((f) => f.sha256))] } : {}),
199
204
  };
200
205
  (0, store_1.writeRegistry)(root, registry);
201
206
  output.push({ slug: manifest.slug, ...(manifest.channel ? { channel: manifest.channel } : {}), version: manifest.version, contentHash: manifest.contentHash, dir, files, results, harnesses });
202
207
  }
203
208
  if (options.dryRun || options.manifests.length === 0)
204
209
  return output;
210
+ pruneBlobs(root, registry, options.blobDir);
205
211
  if (selected.has("codex") && detected.get("codex")) {
206
212
  const bySlug = regenerateCodexTrust(registry, env, home);
207
213
  for (const pack of output) {
@@ -223,6 +229,30 @@ function installPacks(options) {
223
229
  }
224
230
  return output;
225
231
  }
232
+ /** Drop cached blobs no installed pack references (best-effort). */
233
+ function pruneBlobs(root, registry, dir = (0, files_1.blobsDir)(root)) {
234
+ const keep = new Set(Object.values(registry.packs).flatMap((e) => e.blobs ?? []));
235
+ let removed = 0;
236
+ let entries = [];
237
+ try {
238
+ entries = fs.readdirSync(dir);
239
+ }
240
+ catch {
241
+ return 0;
242
+ }
243
+ for (const name of entries) {
244
+ if (keep.has(name))
245
+ continue;
246
+ try {
247
+ fs.rmSync(path.join(dir, name), { force: true });
248
+ removed += 1;
249
+ }
250
+ catch {
251
+ // in use or already gone
252
+ }
253
+ }
254
+ return removed;
255
+ }
226
256
  function regenerateCodexTrust(registry, env, home) {
227
257
  const codexDir = (0, harnessDirs_1.codexHome)(env, home);
228
258
  const bySlug = codexPackTrustEntries(registry, codexDir);
@@ -276,6 +306,7 @@ function uninstallPacks(options) {
276
306
  if (fs.existsSync(codexConfig))
277
307
  regenerateCodexTrust(registry, env, home);
278
308
  (0, shim_1.writeShim)((0, paths_1.shimPath)(root), shimInputs(registry));
309
+ pruneBlobs(root, registry);
279
310
  return output;
280
311
  }
281
312
  function formatUninstallLines(results) {
@@ -1,3 +1,4 @@
1
+ import { PackBundleFile } from "./files";
1
2
  export interface PackManifestFile {
2
3
  name: string;
3
4
  content: string;
@@ -17,6 +18,8 @@ export interface PackManifest {
17
18
  channel?: string;
18
19
  /** Display emoji/icon; never used in paths. */
19
20
  icon?: string;
21
+ /** SIG-127: extra files in the plugin dir, fetched by sha256. */
22
+ files?: PackBundleFile[];
20
23
  }
21
24
  export interface PackSummary {
22
25
  slug: string;
@@ -28,6 +31,7 @@ export interface PackSummary {
28
31
  itemCount?: number;
29
32
  agentCount?: number;
30
33
  hasHooks?: boolean;
34
+ fileCount?: number;
31
35
  }
32
36
  export declare function isSafeSegment(value: unknown): value is string;
33
37
  export declare function isSafeSkillName(value: unknown): value is string;
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- // Server manifest shape (GET /api/cli/packs/manifest) and its validation.
3
- // Everything here is untrusted input that becomes file paths and shell hooks
4
- // on the member's machine, so names are checked strictly before any write.
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.ManifestError = void 0;
7
4
  exports.isSafeSegment = isSafeSegment;
@@ -10,6 +7,8 @@ exports.parseManifest = parseManifest;
10
7
  exports.cleanLabel = cleanLabel;
11
8
  exports.displayName = displayName;
12
9
  exports.parseFrontmatter = parseFrontmatter;
10
+ // Server manifest shape (GET /api/cli/packs/manifest) and its validation.
11
+ const files_1 = require("./files");
13
12
  // One path segment: starts alphanumeric, then alphanumerics, dot, dash or
14
13
  // underscore. Rejects "", ".", "..", slashes, backslashes, absolute paths,
15
14
  // drive letters, NUL and whitespace.
@@ -49,6 +48,46 @@ function fileList(value, field, safe) {
49
48
  return { name: entry.name, content: entry.content, ...(artifactId ? { artifactId } : {}) };
50
49
  });
51
50
  }
51
+ function bundleFiles(value, reserved) {
52
+ if (value === undefined || value === null)
53
+ return [];
54
+ if (!Array.isArray(value))
55
+ throw new ManifestError("manifest.files must be an array");
56
+ if (value.length > files_1.PACK_MAX_FILES)
57
+ throw new ManifestError(`manifest.files has more than ${files_1.PACK_MAX_FILES} entries`);
58
+ const seen = new Set();
59
+ const dirs = new Set();
60
+ let total = 0;
61
+ const out = value.map((item, index) => {
62
+ const f = item;
63
+ if (!f || typeof f !== "object")
64
+ throw new ManifestError(`manifest.files[${index}] is not an object`);
65
+ const problem = (0, files_1.packFilePathProblem)(f.path);
66
+ if (problem)
67
+ throw new ManifestError(`manifest.files[${index}] has an unsafe path ${JSON.stringify(f.path)}: ${problem}`);
68
+ const p = f.path;
69
+ if (typeof f.sha256 !== "string" || !files_1.SHA256_HEX.test(f.sha256))
70
+ throw new ManifestError(`manifest.files[${index}].sha256 is not a sha256`);
71
+ const size = Number(f.size);
72
+ if (!Number.isInteger(size) || size < 0 || size > files_1.PACK_FILE_MAX_BYTES)
73
+ throw new ManifestError(`manifest.files[${index}].size is out of range`);
74
+ total += size;
75
+ const key = p.toLowerCase();
76
+ if (seen.has(key) || reserved.has(key))
77
+ throw new ManifestError(`manifest.files has a clashing path: ${p}`);
78
+ seen.add(key);
79
+ const parts = key.split("/");
80
+ for (let n = 1; n < parts.length; n++)
81
+ dirs.add(parts.slice(0, n).join("/"));
82
+ return { path: p, sha256: f.sha256, size, ...(f.executable === true ? { executable: true } : {}) };
83
+ });
84
+ for (const key of seen)
85
+ if (dirs.has(key))
86
+ throw new ManifestError(`manifest.files has a path that is also a folder: ${key}`);
87
+ if (total > files_1.PACK_FILES_MAX_TOTAL_BYTES)
88
+ throw new ManifestError("manifest.files is too large");
89
+ return out;
90
+ }
52
91
  /** Validate a manifest from the server. Throws ManifestError on anything
53
92
  * unsafe or malformed — a partial install of a tampered pack is worse than
54
93
  * none. */
@@ -91,15 +130,24 @@ function parseManifest(input) {
91
130
  hooksJson = m.hooksJson;
92
131
  }
93
132
  }
133
+ const skills = fileList(m.skills, "skills", isSafeSkillName);
134
+ const agents = fileList(m.agents, "agents", isSafeSegment);
135
+ // Files may never land on a path Codiedev writes itself.
136
+ const reserved = new Set([
137
+ ...skills.map((s) => `skills/${s.name}/skill.md`.toLowerCase()),
138
+ ...agents.map((a) => `agents/${a.name}.md`.toLowerCase()),
139
+ ]);
140
+ const files = bundleFiles(m.files, reserved);
94
141
  return {
95
142
  slug: m.slug,
96
143
  name: typeof m.name === "string" && m.name ? m.name : m.slug,
97
144
  description: typeof m.description === "string" ? m.description : "",
98
145
  version,
99
146
  contentHash: m.contentHash,
100
- skills: fileList(m.skills, "skills", isSafeSkillName),
101
- agents: fileList(m.agents, "agents", isSafeSegment),
147
+ skills,
148
+ agents,
102
149
  hooksJson,
150
+ ...(files.length ? { files } : {}),
103
151
  ...(cleanLabel(m.channel, 16) ? { channel: cleanLabel(m.channel, 16) } : {}),
104
152
  ...(cleanLabel(m.icon, 16) ? { icon: cleanLabel(m.icon, 16) } : {}),
105
153
  };
@@ -9,6 +9,8 @@ export interface RegistryEntry {
9
9
  description?: string;
10
10
  channel?: string;
11
11
  icon?: string;
12
+ /** sha256 of each pack file (SIG-127), so unused cached blobs can be dropped. */
13
+ blobs?: string[];
12
14
  }
13
15
  export interface Registry {
14
16
  packs: Record<string, RegistryEntry>;
@@ -20,16 +22,21 @@ export declare const AGENT_LEVELS: Set<string>;
20
22
  /** Agent file for tools other than Claude Code: drop a `model:` line unless it
21
23
  * is a full `provider/model` id (levels and `inherit` are Claude aliases). */
22
24
  export declare function portableAgentContent(content: string): string;
23
- /** The full file set for a pack's plugin dir, relative path → content. */
24
- export declare function pluginFiles(manifest: PackManifest, options?: {
25
+ export interface PluginWriteOptions {
26
+ /** The copy for tools other than Claude Code (Codex, Copilot, Cursor, pi, Codie). */
25
27
  portable?: boolean;
26
- }): Record<string, string>;
28
+ /** Where pack file blobs are cached (required when the manifest has files). */
29
+ blobDir?: string;
30
+ /** Portable copies: the plugin dir's final path, substituted for ${CLAUDE_PLUGIN_ROOT}. */
31
+ pluginRoot?: string;
32
+ }
33
+ /** The full file set for a pack's plugin dir, relative path → content. Pack
34
+ * files (blobs) are copied separately by writePluginDir. */
35
+ export declare function pluginFiles(manifest: PackManifest, options?: PluginWriteOptions): Record<string, string>;
27
36
  /** Build the plugin dir in a sibling temp dir and swap it into place, so a
28
37
  * failed write never leaves a half-updated plugin and stale files (a removed
29
38
  * skill, hooks that were cleared) never linger. */
30
- export declare function writePluginDir(root: string, manifest: PackManifest, options?: {
31
- portable?: boolean;
32
- }): string;
39
+ export declare function writePluginDir(root: string, manifest: PackManifest, options?: PluginWriteOptions): string;
33
40
  export declare function readRegistry(root: string): Registry;
34
41
  export declare function writeRegistry(root: string, registry: Registry): void;
35
42
  export interface MarketplaceEntry {
@@ -48,6 +48,7 @@ exports.writeMarketplace = writeMarketplace;
48
48
  const fs = __importStar(require("fs"));
49
49
  const skillMarker_1 = require("../skillMarker");
50
50
  const path = __importStar(require("path"));
51
+ const files_1 = require("./files");
51
52
  const paths_1 = require("./paths");
52
53
  function pluginVersion(version) {
53
54
  return `${version}.0.0`;
@@ -75,8 +76,10 @@ function portableAgentContent(content) {
75
76
  return content;
76
77
  return fm[1] + kept.join("\n") + fm[3] + content.slice(fm[0].length);
77
78
  }
78
- /** The full file set for a pack's plugin dir, relative path → content. */
79
+ /** The full file set for a pack's plugin dir, relative path → content. Pack
80
+ * files (blobs) are copied separately by writePluginDir. */
79
81
  function pluginFiles(manifest, options = {}) {
82
+ const rewrite = (content) => (options.portable && options.pluginRoot ? (0, files_1.rewritePluginRoot)(content, options.pluginRoot) : content);
80
83
  const files = {};
81
84
  files[path.join(".claude-plugin", "plugin.json")] = JSON.stringify({
82
85
  name: manifest.slug,
@@ -85,10 +88,10 @@ function pluginFiles(manifest, options = {}) {
85
88
  }, null, 2) + "\n";
86
89
  for (const skill of manifest.skills) {
87
90
  // Identity marker so the skill's runs attribute to this exact artifact.
88
- files[path.join("skills", skill.name, "SKILL.md")] = (0, skillMarker_1.stampSkillMarker)(skill.content, skill.artifactId);
91
+ files[path.join("skills", skill.name, "SKILL.md")] = (0, skillMarker_1.stampSkillMarker)(rewrite(skill.content), skill.artifactId);
89
92
  }
90
93
  for (const agent of manifest.agents) {
91
- files[path.join("agents", `${agent.name}.md`)] = options.portable ? portableAgentContent(agent.content) : agent.content;
94
+ files[path.join("agents", `${agent.name}.md`)] = options.portable ? rewrite(portableAgentContent(agent.content)) : agent.content;
92
95
  }
93
96
  if (manifest.hooksJson) {
94
97
  files[path.join("hooks", "hooks.json")] = manifest.hooksJson;
@@ -113,12 +116,28 @@ function writePluginDir(root, manifest, options = {}) {
113
116
  const tmp = path.join(parent, `.${manifest.slug}.tmp-${stamp}`);
114
117
  const old = path.join(parent, `.${manifest.slug}.old-${stamp}`);
115
118
  try {
116
- for (const [rel, content] of Object.entries(pluginFiles(manifest, options))) {
119
+ const text = pluginFiles(manifest, { ...options, pluginRoot: options.pluginRoot ?? target });
120
+ for (const [rel, content] of Object.entries(text)) {
117
121
  const file = path.join(tmp, rel);
118
122
  assertInside(tmp, file);
119
123
  fs.mkdirSync(path.dirname(file), { recursive: true });
120
124
  fs.writeFileSync(file, content, "utf8");
121
125
  }
126
+ const written = new Set(Object.keys(text).map((rel) => rel.split(path.sep).join("/").toLowerCase()));
127
+ for (const f of manifest.files ?? []) {
128
+ const problem = (0, files_1.packFilePathProblem)(f.path);
129
+ if (problem)
130
+ throw new Error(`refusing pack file ${f.path}: ${problem}`);
131
+ if (written.has(f.path.toLowerCase()))
132
+ throw new Error(`pack file ${f.path} clashes with a file Codiedev writes`);
133
+ if (!options.blobDir)
134
+ throw new Error(`pack file ${f.path} wasn't downloaded`);
135
+ const file = path.join(tmp, ...f.path.split("/"));
136
+ assertInside(tmp, file);
137
+ fs.mkdirSync(path.dirname(file), { recursive: true });
138
+ fs.copyFileSync((0, files_1.blobPath)(options.blobDir, f.sha256), file);
139
+ fs.chmodSync(file, f.executable ? 0o755 : 0o644);
140
+ }
122
141
  if (fs.existsSync(target))
123
142
  fs.renameSync(target, old);
124
143
  fs.renameSync(tmp, target);
@@ -1,5 +1,5 @@
1
1
  /** Session-start context uses each client's supported output contract. */
2
- export declare const TEAM_SKILL_CONTEXT = "This machine is connected to Codiedev, the team's artifact and skill library.\nBefore a substantial coding task, run codiedev search \"<brief task description>\" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.\nUse codiedev push <file.md> to share an artifact when asked, codiedev ask \"<question>\" for team knowledge, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.";
2
+ export declare const TEAM_SKILL_CONTEXT = "This machine is connected to Codiedev, the team's artifact and skill library.\nBefore a substantial coding task, run codiedev search \"<brief task description>\" and reuse a relevant team skill by running codiedev pull <exact-key> and following its instructions. If no skill fits, continue the task.\nUse codiedev push <file.md> to share an artifact when asked, and codiedev inbox to check messages. Use codiedev --help for other commands. If codiedev is unavailable, use npx codiedev@latest.";
3
3
  export declare function teamSkillSessionOutput(source: "cursor" | "copilot", context?: string): {
4
4
  additional_context: string;
5
5
  additionalContext?: undefined;