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.
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +36 -13
- package/dist/commands/inbox.js +9 -0
- package/dist/commands/pack.d.ts +5 -2
- package/dist/commands/pack.js +129 -10
- package/dist/commands/ping.js +11 -7
- package/dist/commands/pingFiles.d.ts +22 -0
- package/dist/commands/pingFiles.js +155 -0
- package/dist/commands/reply.d.ts +5 -0
- package/dist/commands/reply.js +54 -0
- package/dist/commands/shared.d.ts +11 -0
- package/dist/commands/shared.js +57 -0
- package/dist/hook.js +28 -8
- package/dist/inboxFormat.d.ts +10 -0
- package/dist/inboxFormat.js +18 -1
- package/dist/mcp.js +1 -48
- package/dist/packs/files.d.ts +46 -0
- package/dist/packs/files.js +307 -0
- package/dist/packs/harnesses.js +4 -3
- package/dist/packs/install.d.ts +4 -0
- package/dist/packs/install.js +35 -4
- package/dist/packs/manifest.d.ts +4 -0
- package/dist/packs/manifest.js +53 -5
- package/dist/packs/store.d.ts +13 -6
- package/dist/packs/store.js +23 -4
- package/dist/teamSkillContext.d.ts +1 -1
- package/dist/teamSkillContext.js +1 -1
- package/dist/unreadNotice.d.ts +22 -0
- package/dist/unreadNotice.js +81 -0
- package/dist/utils.js +5 -4
- package/package.json +1 -1
- package/dist/commands/ask.d.ts +0 -1
- package/dist/commands/ask.js +0 -58
|
@@ -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
|
+
}
|
package/dist/packs/harnesses.js
CHANGED
|
@@ -259,8 +259,8 @@ function copyCursorSkills(home, manifest, pluginDirPath) {
|
|
|
259
259
|
}
|
|
260
260
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
261
261
|
}
|
|
262
|
-
|
|
263
|
-
|
|
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
|
package/dist/packs/install.d.ts
CHANGED
|
@@ -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;
|
package/dist/packs/install.js
CHANGED
|
@@ -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,
|
|
152
|
-
|
|
153
|
-
|
|
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) {
|
package/dist/packs/manifest.d.ts
CHANGED
|
@@ -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;
|
package/dist/packs/manifest.js
CHANGED
|
@@ -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
|
|
101
|
-
agents
|
|
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
|
};
|
package/dist/packs/store.d.ts
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
+
export interface PluginWriteOptions {
|
|
26
|
+
/** The copy for tools other than Claude Code (Codex, Copilot, Cursor, pi, Codie). */
|
|
25
27
|
portable?: boolean;
|
|
26
|
-
|
|
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 {
|
package/dist/packs/store.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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;
|