skills 1.5.26 → 1.7.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 +12 -1
- package/dist/cli.mjs +440 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,13 @@ The CLI for the open agent skills ecosystem.
|
|
|
6
6
|
Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [75 more](#supported-agents).
|
|
7
7
|
<!-- agent-list:end -->
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
<p>
|
|
10
|
+
<a href="https://vercel.com/labs#labs-products"><img alt="Vercel Labs Product" src="https://img.shields.io/badge/LABS-PRODUCT-0a0a0a.svg?style=for-the-badge&logo=Vercel&labelColor=000000" height="28"></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/skills"><img alt="npm version: skills" src="https://img.shields.io/npm/v/skills.svg?style=for-the-badge&labelColor=000000" height="28"></a>
|
|
12
|
+
<a href="https://github.com/vercel-labs/skills/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/github/license/vercel-labs/skills.svg?style=for-the-badge&labelColor=000000" height="28"></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/skills"><img alt="npm downloads per month: skills" src="https://img.shields.io/npm/dm/skills.svg?style=for-the-badge&labelColor=000000&label=npm%20downloads" height="28"></a>
|
|
14
|
+
<a href="https://skills.sh/vercel-labs/skills"><img alt="skills.sh" src="https://skills.sh/b/vercel-labs/skills?style=for-the-badge" height="28"></a>
|
|
15
|
+
</p>
|
|
10
16
|
|
|
11
17
|
## Install a Skill
|
|
12
18
|
|
|
@@ -40,6 +46,10 @@ npx skills add https://github.com/vercel-labs/agent-skills/tree/main/skills/web-
|
|
|
40
46
|
# GitLab URL
|
|
41
47
|
npx skills add https://gitlab.com/org/repo
|
|
42
48
|
|
|
49
|
+
# Azure Repos (Azure DevOps Services or Server)
|
|
50
|
+
npx skills add https://dev.azure.com/org/project/_git/repo
|
|
51
|
+
npx skills add https://dev.azure.com/org/project/_git/repo?path=/skills/web-design&version=GBmain
|
|
52
|
+
|
|
43
53
|
# Any git URL
|
|
44
54
|
npx skills add git@github.com:vercel-labs/agent-skills.git
|
|
45
55
|
|
|
@@ -61,6 +71,7 @@ npx skills add ssh://git@git.example.com/acme/private-skills.git
|
|
|
61
71
|
|
|
62
72
|
# HTTPS on any Git host (uses your configured Git credential helper)
|
|
63
73
|
npx skills add https://git.example.com/acme/private-skills.git
|
|
74
|
+
npx skills add https://dev.azure.com/org/project/_git/private-skills
|
|
64
75
|
```
|
|
65
76
|
|
|
66
77
|
For GitHub HTTPS and shorthand sources, `skills` first uses normal Git credentials. If that fails and GitHub CLI is authenticated, it tries `gh repo clone`, followed by SSH. It does not execute `gh auth token` or copy the stored GitHub CLI credential into the Node.js process.
|
package/dist/cli.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { basename, dirname, extname, isAbsolute, join, normalize, relative, reso
|
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
import { stripVTControlCharacters } from "node:util";
|
|
13
13
|
import { createWriteStream } from "node:fs";
|
|
14
|
-
import { dirname as dirname$1, join as join$1, normalize as normalize$1, resolve as resolve$1, sep as sep$1 } from "node:path";
|
|
14
|
+
import { dirname as dirname$1, join as join$1, normalize as normalize$1, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "node:path";
|
|
15
15
|
import { homedir, platform, tmpdir } from "os";
|
|
16
16
|
import * as readline from "readline";
|
|
17
17
|
import { Writable } from "stream";
|
|
@@ -20,13 +20,13 @@ import { execFile, spawn, spawnSync } from "child_process";
|
|
|
20
20
|
import { access, chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
|
|
21
21
|
import { parse, stringify } from "yaml";
|
|
22
22
|
import { createHash } from "crypto";
|
|
23
|
-
import { mkdir as mkdir$1, mkdtemp as mkdtemp$1, readFile as readFile$1, rm as rm$1, stat as stat$1, writeFile as writeFile$1 } from "node:fs/promises";
|
|
23
|
+
import { cp as cp$1, mkdir as mkdir$1, mkdtemp as mkdtemp$1, readFile as readFile$1, rm as rm$1, stat as stat$1, writeFile as writeFile$1 } from "node:fs/promises";
|
|
24
24
|
import { createHash as createHash$1 } from "node:crypto";
|
|
25
25
|
import { crc32, gunzipSync, inflateRawSync } from "node:zlib";
|
|
26
26
|
import { tmpdir as tmpdir$1 } from "node:os";
|
|
27
27
|
import { pipeline } from "node:stream/promises";
|
|
28
28
|
import * as tar from "tar";
|
|
29
|
-
import { execFile as execFile$1 } from "node:child_process";
|
|
29
|
+
import { execFile as execFile$1, spawn as spawn$1 } from "node:child_process";
|
|
30
30
|
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
31
31
|
const DEFAULT_GITHUB_HOST = "github.com";
|
|
32
32
|
function getGitHubHost() {
|
|
@@ -91,6 +91,46 @@ function sanitizeSubpath(subpath) {
|
|
|
91
91
|
for (const segment of segments) if (segment === "..") throw new Error(`Unsafe subpath: "${subpath}" contains path traversal segments. Subpaths must not contain ".." components.`);
|
|
92
92
|
return subpath;
|
|
93
93
|
}
|
|
94
|
+
function azureReposVersionToRef(version) {
|
|
95
|
+
if (!version) return;
|
|
96
|
+
return /^(?:GB|GT)(.+)$/i.exec(version)?.[1] || void 0;
|
|
97
|
+
}
|
|
98
|
+
function parseAzureReposSource(input, fragmentRef, fragmentSkillFilter) {
|
|
99
|
+
if (!input.startsWith("http://") && !input.startsWith("https://")) return null;
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = new URL(input);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
107
|
+
const gitIndex = segments.indexOf("_git");
|
|
108
|
+
if (gitIndex < 0 || gitIndex === segments.length - 1) return null;
|
|
109
|
+
const repo = segments[gitIndex + 1].replace(/\.git$/, "");
|
|
110
|
+
if (!repo) return null;
|
|
111
|
+
const clonePath = "/" + [
|
|
112
|
+
...segments.slice(0, gitIndex),
|
|
113
|
+
"_git",
|
|
114
|
+
repo
|
|
115
|
+
].map((segment) => {
|
|
116
|
+
try {
|
|
117
|
+
return encodeURIComponent(decodeURIComponent(segment));
|
|
118
|
+
} catch {
|
|
119
|
+
return encodeURIComponent(segment);
|
|
120
|
+
}
|
|
121
|
+
}).join("/");
|
|
122
|
+
const cloneUrl = `${parsed.protocol}//${parsed.host}${clonePath}`;
|
|
123
|
+
const ref = azureReposVersionToRef(parsed.searchParams.get("version")) || fragmentRef;
|
|
124
|
+
const pathParam = parsed.searchParams.get("path");
|
|
125
|
+
const subpath = pathParam ? sanitizeSubpath(pathParam.replace(/^\/+/, "")) : void 0;
|
|
126
|
+
return {
|
|
127
|
+
type: "git",
|
|
128
|
+
url: cloneUrl,
|
|
129
|
+
...ref ? { ref } : {},
|
|
130
|
+
...subpath ? { subpath } : {},
|
|
131
|
+
...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
94
134
|
function isLocalPath(input) {
|
|
95
135
|
return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || /^[a-zA-Z]:[/\\]/.test(input);
|
|
96
136
|
}
|
|
@@ -113,6 +153,9 @@ function looksLikeGitSource(input) {
|
|
|
113
153
|
const pathname = parsed.pathname;
|
|
114
154
|
if (isGitHubHost(parsed.host)) return /^\/[^/]+\/[^/]+(?:\.git)?(?:\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
|
|
115
155
|
if (parsed.hostname === "gitlab.com") return /^\/.+?\/[^/]+(?:\.git)?(?:\/-\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
|
|
156
|
+
const azureSegments = pathname.split("/").filter(Boolean);
|
|
157
|
+
const azureGitIndex = azureSegments.indexOf("_git");
|
|
158
|
+
if (azureGitIndex >= 0 && azureGitIndex < azureSegments.length - 1) return true;
|
|
116
159
|
} catch {}
|
|
117
160
|
if (/^https?:\/\/.+\.git(?:$|[/?])/i.test(input)) return true;
|
|
118
161
|
return !input.includes(":") && !input.startsWith(".") && !input.startsWith("/") && /^([^/]+)\/([^/]+)(?:\/(.+)|@(.+))?$/.test(input);
|
|
@@ -245,6 +288,8 @@ function parseSource(input) {
|
|
|
245
288
|
...fragmentRef ? { ref: fragmentRef } : {}
|
|
246
289
|
};
|
|
247
290
|
}
|
|
291
|
+
const azureRepos = parseAzureReposSource(input, fragmentRef, fragmentSkillFilter);
|
|
292
|
+
if (azureRepos) return azureRepos;
|
|
248
293
|
const githubHost = getGitHubHost();
|
|
249
294
|
const shorthandSourceType = githubHost === "github.com" ? "github" : "git";
|
|
250
295
|
const atSkillMatch = input.match(/^([^/]+)\/([^/@]+)@(.+)$/);
|
|
@@ -1183,7 +1228,7 @@ function normalizeSkillName$1(name) {
|
|
|
1183
1228
|
function normalizeRelativePath(path) {
|
|
1184
1229
|
return path.split(sep).join("/").replace(/\/+/g, "/");
|
|
1185
1230
|
}
|
|
1186
|
-
function isRecord(value) {
|
|
1231
|
+
function isRecord$1(value) {
|
|
1187
1232
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1188
1233
|
}
|
|
1189
1234
|
function shouldInstallInternalSkills() {
|
|
@@ -1226,7 +1271,7 @@ async function parseSkillMd(skillMdPath, options) {
|
|
|
1226
1271
|
warnSkippedSkill(skillMdPath, `frontmatter "name" and "description" must be strings (got ${typeof data.name} and ${typeof data.description})`);
|
|
1227
1272
|
return null;
|
|
1228
1273
|
}
|
|
1229
|
-
const metadata = isRecord(data.metadata) ? data.metadata : void 0;
|
|
1274
|
+
const metadata = isRecord$1(data.metadata) ? data.metadata : void 0;
|
|
1230
1275
|
if (metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
|
|
1231
1276
|
return {
|
|
1232
1277
|
name: sanitizeMetadata(data.name),
|
|
@@ -3515,17 +3560,20 @@ const DEFAULT_DOWNLOAD_MAX_BYTES = 10 * 1024 * 1024;
|
|
|
3515
3560
|
const DEFAULT_EXTRACT_MAX_BYTES = 25 * 1024 * 1024;
|
|
3516
3561
|
const DEFAULT_EXTRACT_MAX_FILES = 1e3;
|
|
3517
3562
|
const FETCH_TIMEOUT_MS = 3e4;
|
|
3563
|
+
function positiveIntegerOr(value, fallback) {
|
|
3564
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
3565
|
+
}
|
|
3518
3566
|
function getPositiveIntegerEnv(name, fallback) {
|
|
3519
3567
|
const raw = process.env[name];
|
|
3520
3568
|
if (!raw) return fallback;
|
|
3521
3569
|
const parsed = Number.parseInt(raw, 10);
|
|
3522
3570
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
3523
3571
|
}
|
|
3524
|
-
function getDownloadLimits() {
|
|
3572
|
+
function getDownloadLimits(options) {
|
|
3525
3573
|
return {
|
|
3526
|
-
downloadMaxBytes: getPositiveIntegerEnv("SKILLS_DOWNLOAD_MAX_BYTES", DEFAULT_DOWNLOAD_MAX_BYTES),
|
|
3527
|
-
extractMaxBytes: getPositiveIntegerEnv("SKILLS_EXTRACT_MAX_BYTES", DEFAULT_EXTRACT_MAX_BYTES),
|
|
3528
|
-
extractMaxFiles: getPositiveIntegerEnv("SKILLS_EXTRACT_MAX_FILES", DEFAULT_EXTRACT_MAX_FILES)
|
|
3574
|
+
downloadMaxBytes: getPositiveIntegerEnv("SKILLS_DOWNLOAD_MAX_BYTES", positiveIntegerOr(options.downloadMaxBytes, DEFAULT_DOWNLOAD_MAX_BYTES)),
|
|
3575
|
+
extractMaxBytes: getPositiveIntegerEnv("SKILLS_EXTRACT_MAX_BYTES", positiveIntegerOr(options.extractMaxBytes, DEFAULT_EXTRACT_MAX_BYTES)),
|
|
3576
|
+
extractMaxFiles: getPositiveIntegerEnv("SKILLS_EXTRACT_MAX_FILES", positiveIntegerOr(options.extractMaxFiles, DEFAULT_EXTRACT_MAX_FILES))
|
|
3529
3577
|
};
|
|
3530
3578
|
}
|
|
3531
3579
|
function isPathSafe$1(basePath, targetPath) {
|
|
@@ -3659,8 +3707,8 @@ async function getSingleTopLevelDirectory(dir) {
|
|
|
3659
3707
|
if (visibleEntries.length !== 1 || !visibleEntries[0].isDirectory()) return null;
|
|
3660
3708
|
return join$1(dir, visibleEntries[0].name);
|
|
3661
3709
|
}
|
|
3662
|
-
async function downloadSource(url) {
|
|
3663
|
-
const limits = getDownloadLimits();
|
|
3710
|
+
async function downloadSource(url, options = {}) {
|
|
3711
|
+
const limits = getDownloadLimits(options);
|
|
3664
3712
|
const tempDir = await mkdtemp$1(join$1(tmpdir$1(), "skills-download-"));
|
|
3665
3713
|
const downloadedFile = join$1(tempDir, "source.download");
|
|
3666
3714
|
const extractDir = join$1(tempDir, "extract");
|
|
@@ -4124,7 +4172,348 @@ async function tryBlobInstall(ownerRepo, options = {}) {
|
|
|
4124
4172
|
tree
|
|
4125
4173
|
};
|
|
4126
4174
|
}
|
|
4127
|
-
var version$1 = "1.
|
|
4175
|
+
var version$1 = "1.7.0";
|
|
4176
|
+
const NOTION_API_VERSION = "2026-03-11";
|
|
4177
|
+
const NTN_TIMEOUT_MS = 3e4;
|
|
4178
|
+
const NTN_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
4179
|
+
const NOTION_DOWNLOAD_MAX_BYTES = 50 * 1024 * 1024;
|
|
4180
|
+
const NOTION_EXTRACT_MAX_BYTES = 100 * 1024 * 1024;
|
|
4181
|
+
const NOTION_EXTRACT_MAX_FILES = 5e3;
|
|
4182
|
+
function isRecord(value) {
|
|
4183
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4184
|
+
}
|
|
4185
|
+
function assertString(value, field) {
|
|
4186
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`Notion Agent Plugins response is missing ${field}`);
|
|
4187
|
+
return value;
|
|
4188
|
+
}
|
|
4189
|
+
function optionalMetadata(value, field) {
|
|
4190
|
+
if (value === void 0 || value === null) return "";
|
|
4191
|
+
if (typeof value !== "string") throw new Error(`Notion Agent Plugins response has an invalid ${field}`);
|
|
4192
|
+
return sanitizeMetadata(value);
|
|
4193
|
+
}
|
|
4194
|
+
function parsePack(value) {
|
|
4195
|
+
if (!isRecord(value)) throw new Error("Notion Agent Plugins response contains an invalid pack");
|
|
4196
|
+
if (typeof value.name !== "string") throw new Error("Notion Agent Plugins response is missing pack.name");
|
|
4197
|
+
const name = sanitizeMetadata(value.name);
|
|
4198
|
+
if (!name) return null;
|
|
4199
|
+
return {
|
|
4200
|
+
id: assertString(value.id, "pack.id"),
|
|
4201
|
+
name,
|
|
4202
|
+
description: optionalMetadata(value.description, "pack.description"),
|
|
4203
|
+
version_id: assertString(value.version_id, "pack.version_id")
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4206
|
+
function parsePackList(value) {
|
|
4207
|
+
if (!isRecord(value) || !Array.isArray(value.results)) throw new Error("Notion Agent Plugins list response is invalid");
|
|
4208
|
+
if (typeof value.has_more !== "boolean") throw new Error("Notion Agent Plugins list response is missing has_more");
|
|
4209
|
+
const nextCursor = value.next_cursor;
|
|
4210
|
+
if (nextCursor !== null && typeof nextCursor !== "string") throw new Error("Notion Agent Plugins list response has an invalid next_cursor");
|
|
4211
|
+
return {
|
|
4212
|
+
results: value.results.map(parsePack).filter((pack) => pack !== null),
|
|
4213
|
+
next_cursor: nextCursor,
|
|
4214
|
+
has_more: value.has_more
|
|
4215
|
+
};
|
|
4216
|
+
}
|
|
4217
|
+
function parseDirectory(value, label) {
|
|
4218
|
+
if (!isRecord(value)) throw new Error(`Notion ${label} directory response is invalid`);
|
|
4219
|
+
return {
|
|
4220
|
+
id: assertString(value.id, `${label} directory id`),
|
|
4221
|
+
version_id: assertString(value.version_id, `${label} directory version_id`),
|
|
4222
|
+
url: assertString(value.url, `${label} directory url`)
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
function downloadNotionDirectory(url) {
|
|
4226
|
+
return downloadSource(url, {
|
|
4227
|
+
downloadMaxBytes: NOTION_DOWNLOAD_MAX_BYTES,
|
|
4228
|
+
extractMaxBytes: NOTION_EXTRACT_MAX_BYTES,
|
|
4229
|
+
extractMaxFiles: NOTION_EXTRACT_MAX_FILES
|
|
4230
|
+
});
|
|
4231
|
+
}
|
|
4232
|
+
function debugNtn(args) {
|
|
4233
|
+
if (process.env.SKILLS_DEBUG === "1") console.error(`[notion] ntn ${args.join(" ")}`);
|
|
4234
|
+
}
|
|
4235
|
+
async function runNtnApi(args) {
|
|
4236
|
+
debugNtn(args);
|
|
4237
|
+
return new Promise((resolve, reject) => {
|
|
4238
|
+
const child = spawn$1("ntn", args, {
|
|
4239
|
+
env: process.env,
|
|
4240
|
+
stdio: [
|
|
4241
|
+
"ignore",
|
|
4242
|
+
"pipe",
|
|
4243
|
+
"pipe"
|
|
4244
|
+
]
|
|
4245
|
+
});
|
|
4246
|
+
const stdoutChunks = [];
|
|
4247
|
+
const stderrChunks = [];
|
|
4248
|
+
let outputBytes = 0;
|
|
4249
|
+
let settled = false;
|
|
4250
|
+
const finish = (callback) => {
|
|
4251
|
+
if (settled) return;
|
|
4252
|
+
settled = true;
|
|
4253
|
+
clearTimeout(timeout);
|
|
4254
|
+
callback();
|
|
4255
|
+
};
|
|
4256
|
+
const fail = (message) => finish(() => reject(new Error(message)));
|
|
4257
|
+
const append = (chunks, chunk) => {
|
|
4258
|
+
outputBytes += chunk.length;
|
|
4259
|
+
if (outputBytes > NTN_MAX_BUFFER_BYTES) {
|
|
4260
|
+
child.kill("SIGTERM");
|
|
4261
|
+
fail("ntn api output exceeded 10 MiB");
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
chunks.push(chunk);
|
|
4265
|
+
};
|
|
4266
|
+
const timeout = setTimeout(() => {
|
|
4267
|
+
child.kill("SIGTERM");
|
|
4268
|
+
fail(`ntn api timed out after ${NTN_TIMEOUT_MS / 1e3} seconds`);
|
|
4269
|
+
}, NTN_TIMEOUT_MS);
|
|
4270
|
+
child.stdout.on("data", (chunk) => append(stdoutChunks, chunk));
|
|
4271
|
+
child.stderr.on("data", (chunk) => append(stderrChunks, chunk));
|
|
4272
|
+
child.on("error", (error) => {
|
|
4273
|
+
if (error.code === "ENOENT") {
|
|
4274
|
+
fail("Notion CLI (ntn) is required. Install it from:\nhttps://developers.notion.com/cli/get-started/overview\nThen run `ntn login`.");
|
|
4275
|
+
return;
|
|
4276
|
+
}
|
|
4277
|
+
fail(`Unable to start ntn: ${stripTerminalEscapes(error.message)}`);
|
|
4278
|
+
});
|
|
4279
|
+
child.on("close", (code) => {
|
|
4280
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
4281
|
+
const stderr = stripTerminalEscapes(Buffer.concat(stderrChunks).toString("utf8")).trim();
|
|
4282
|
+
if (code === 0) {
|
|
4283
|
+
finish(() => resolve(stdout));
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4286
|
+
fail(`ntn api failed${stderr ? `: ${stderr}` : ` with exit code ${code ?? "unknown"}`}`);
|
|
4287
|
+
});
|
|
4288
|
+
});
|
|
4289
|
+
}
|
|
4290
|
+
async function fetchNotionJson(args, label, runNtn = runNtnApi) {
|
|
4291
|
+
const output = await runNtn([
|
|
4292
|
+
"api",
|
|
4293
|
+
...args,
|
|
4294
|
+
"--notion-version",
|
|
4295
|
+
NOTION_API_VERSION
|
|
4296
|
+
]);
|
|
4297
|
+
try {
|
|
4298
|
+
return JSON.parse(output);
|
|
4299
|
+
} catch (error) {
|
|
4300
|
+
if (error instanceof SyntaxError) throw new Error(`ntn returned invalid JSON for ${label}`);
|
|
4301
|
+
throw error;
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
async function fetchNotionPacks(options = {}) {
|
|
4305
|
+
const runNtn = options.runNtn ?? runNtnApi;
|
|
4306
|
+
const packs = [];
|
|
4307
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
4308
|
+
let cursor = null;
|
|
4309
|
+
do {
|
|
4310
|
+
const args = ["/v1/ai/plugins", "page_size==100"];
|
|
4311
|
+
if (cursor) args.push(`start_cursor==${cursor}`);
|
|
4312
|
+
const page = parsePackList(await fetchNotionJson(args, "the Notion packs list", runNtn));
|
|
4313
|
+
packs.push(...page.results);
|
|
4314
|
+
if (!page.has_more) break;
|
|
4315
|
+
if (!page.next_cursor || seenCursors.has(page.next_cursor)) throw new Error("Notion Agent Plugins pagination returned an invalid cursor");
|
|
4316
|
+
cursor = page.next_cursor;
|
|
4317
|
+
seenCursors.add(cursor);
|
|
4318
|
+
} while (cursor);
|
|
4319
|
+
return packs;
|
|
4320
|
+
}
|
|
4321
|
+
async function fetchNotionPackDirectory(pack, options = {}) {
|
|
4322
|
+
const directory = parseDirectory(await fetchNotionJson([`/v1/ai/plugins/${encodeURIComponent(pack.id)}`], `Notion pack ${JSON.stringify(pack.name)}`, options.runNtn), "pack");
|
|
4323
|
+
if (directory.id !== pack.id || directory.version_id !== pack.version_id) throw new Error(`Notion pack ${JSON.stringify(pack.name)} changed while preparing installation`);
|
|
4324
|
+
return directory;
|
|
4325
|
+
}
|
|
4326
|
+
async function fetchNotionWorkspaceName(options = {}) {
|
|
4327
|
+
const runNtn = options.runNtn ?? runNtnApi;
|
|
4328
|
+
try {
|
|
4329
|
+
const value = JSON.parse(await runNtn([
|
|
4330
|
+
"api",
|
|
4331
|
+
"/v1/users/me",
|
|
4332
|
+
"--notion-version",
|
|
4333
|
+
NOTION_API_VERSION
|
|
4334
|
+
]));
|
|
4335
|
+
if (!isRecord(value) || !isRecord(value.bot)) return null;
|
|
4336
|
+
const name = value.bot.workspace_name;
|
|
4337
|
+
return typeof name === "string" && name.length > 0 ? sanitizeMetadata(name) : null;
|
|
4338
|
+
} catch {
|
|
4339
|
+
return null;
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
function inWorkspace(workspace) {
|
|
4343
|
+
return workspace ? ` in workspace ${import_picocolors.default.cyan(workspace)}` : "";
|
|
4344
|
+
}
|
|
4345
|
+
function isNotionSource(source) {
|
|
4346
|
+
return source.toLowerCase() === "notion";
|
|
4347
|
+
}
|
|
4348
|
+
const NOTION_HOSTNAME = /(^|\.)notion\.(so|com)$/;
|
|
4349
|
+
const NOTION_PAGE_ID = /(?:^|-)([0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
|
|
4350
|
+
function formatPageId(rawId) {
|
|
4351
|
+
const hex = rawId.replace(/-/g, "");
|
|
4352
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
4353
|
+
}
|
|
4354
|
+
function parseNotionSkillUrl(source) {
|
|
4355
|
+
let url;
|
|
4356
|
+
try {
|
|
4357
|
+
url = new URL(source);
|
|
4358
|
+
} catch {
|
|
4359
|
+
return null;
|
|
4360
|
+
}
|
|
4361
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
4362
|
+
if (!NOTION_HOSTNAME.test(url.hostname.toLowerCase())) return null;
|
|
4363
|
+
const lastSegment = url.pathname.split("/").filter(Boolean).pop();
|
|
4364
|
+
if (!lastSegment) return null;
|
|
4365
|
+
let decoded;
|
|
4366
|
+
try {
|
|
4367
|
+
decoded = decodeURIComponent(lastSegment);
|
|
4368
|
+
} catch {
|
|
4369
|
+
decoded = lastSegment;
|
|
4370
|
+
}
|
|
4371
|
+
const match = decoded.toLowerCase().match(NOTION_PAGE_ID);
|
|
4372
|
+
return match ? formatPageId(match[1]) : null;
|
|
4373
|
+
}
|
|
4374
|
+
async function fetchNotionSkillDirectory(pageId, options = {}) {
|
|
4375
|
+
return parseDirectory(await fetchNotionJson([`/v1/ai/skills/${encodeURIComponent(pageId)}`], `Notion skill ${pageId}`, options.runNtn), "skill");
|
|
4376
|
+
}
|
|
4377
|
+
async function prepareNotionSkillSource(pageId, options = {}) {
|
|
4378
|
+
const download = options.download ?? downloadNotionDirectory;
|
|
4379
|
+
const spinner$4 = spinner();
|
|
4380
|
+
spinner$4.start("Fetching Notion skill with ntn…");
|
|
4381
|
+
try {
|
|
4382
|
+
const [directory, workspace] = await Promise.all([fetchNotionSkillDirectory(pageId, { runNtn: options.runNtn }), fetchNotionWorkspaceName({ runNtn: options.runNtn })]);
|
|
4383
|
+
const downloaded = await download(directory.url);
|
|
4384
|
+
spinner$4.stop(`Downloaded skill from Notion${inWorkspace(workspace)}`);
|
|
4385
|
+
return {
|
|
4386
|
+
rootDir: downloaded.rootDir,
|
|
4387
|
+
tempDir: downloaded.tempDir
|
|
4388
|
+
};
|
|
4389
|
+
} catch (error) {
|
|
4390
|
+
spinner$4.stop(import_picocolors.default.red("Failed to prepare Notion skill"));
|
|
4391
|
+
throw error;
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
function normalizeSelector(value) {
|
|
4395
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4396
|
+
}
|
|
4397
|
+
function filterPacks(packs, selectors) {
|
|
4398
|
+
if (selectors.includes("*")) return packs;
|
|
4399
|
+
const normalized = new Set(selectors.map(normalizeSelector));
|
|
4400
|
+
return packs.filter((pack) => normalized.has(normalizeSelector(pack.name)) || normalized.has(pack.id.toLowerCase()));
|
|
4401
|
+
}
|
|
4402
|
+
function sortedPacks(packs) {
|
|
4403
|
+
return [...packs].sort((a, b) => a.name.localeCompare(b.name));
|
|
4404
|
+
}
|
|
4405
|
+
function printAvailablePacks(packs) {
|
|
4406
|
+
console.log();
|
|
4407
|
+
log.step(import_picocolors.default.bold("Available Notion packs"));
|
|
4408
|
+
for (const pack of sortedPacks(packs)) {
|
|
4409
|
+
log.message(import_picocolors.default.cyan(pack.name));
|
|
4410
|
+
if (pack.description) log.message(` ${import_picocolors.default.dim(pack.description)}`);
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
async function prepareNotionPackSource(options = {}) {
|
|
4414
|
+
const spinner$5 = spinner();
|
|
4415
|
+
spinner$5.start("Fetching Notion packs with ntn…");
|
|
4416
|
+
let packs;
|
|
4417
|
+
let workspace = null;
|
|
4418
|
+
try {
|
|
4419
|
+
[packs, workspace] = await Promise.all([fetchNotionPacks({ runNtn: options.runNtn }), fetchNotionWorkspaceName({ runNtn: options.runNtn })]);
|
|
4420
|
+
} catch (error) {
|
|
4421
|
+
spinner$5.stop(import_picocolors.default.red("Failed to load Notion packs"));
|
|
4422
|
+
throw error;
|
|
4423
|
+
}
|
|
4424
|
+
spinner$5.stop(`Found ${import_picocolors.default.green(packs.length)} Notion pack${packs.length === 1 ? "" : "s"}${inWorkspace(workspace)}`);
|
|
4425
|
+
if (packs.length === 0) throw new Error("Notion returned no packs for the authenticated workspace");
|
|
4426
|
+
if (options.list) {
|
|
4427
|
+
printAvailablePacks(packs);
|
|
4428
|
+
outro(import_picocolors.default.dim("Select a pack by running the command without --list."));
|
|
4429
|
+
return null;
|
|
4430
|
+
}
|
|
4431
|
+
let selectedPacks;
|
|
4432
|
+
if (options.skill && options.skill.length > 0) {
|
|
4433
|
+
selectedPacks = filterPacks(packs, options.skill);
|
|
4434
|
+
if (selectedPacks.length === 0) throw new Error(`No matching Notion packs found for: ${options.skill.join(", ")}`);
|
|
4435
|
+
} else if (options.yes || !process.stdin.isTTY) selectedPacks = packs;
|
|
4436
|
+
else {
|
|
4437
|
+
const selected = await searchMultiselect({
|
|
4438
|
+
message: `Select Notion packs to install ${import_picocolors.default.dim("(space to toggle)")}`,
|
|
4439
|
+
items: sortedPacks(packs).map((pack) => ({
|
|
4440
|
+
value: pack,
|
|
4441
|
+
label: pack.name,
|
|
4442
|
+
detail: pack.description || "Notion plugin pack"
|
|
4443
|
+
})),
|
|
4444
|
+
required: true,
|
|
4445
|
+
maxVisible: 20,
|
|
4446
|
+
searchable: false,
|
|
4447
|
+
showDetail: true,
|
|
4448
|
+
showSelectedSummary: false
|
|
4449
|
+
});
|
|
4450
|
+
if (typeof selected === "symbol") {
|
|
4451
|
+
cancel("Selection cancelled");
|
|
4452
|
+
return null;
|
|
4453
|
+
}
|
|
4454
|
+
selectedPacks = selected;
|
|
4455
|
+
}
|
|
4456
|
+
note(selectedPacks.map((pack) => import_picocolors.default.cyan(pack.name)).join("\n"), `Selected ${selectedPacks.length} Notion pack${selectedPacks.length === 1 ? "" : "s"}`);
|
|
4457
|
+
const stagingDir = await mkdtemp$1(join$1(tmpdir$1(), "skills-notion-"));
|
|
4458
|
+
const packsDir = join$1(stagingDir, "packs");
|
|
4459
|
+
const manifestPlugins = [];
|
|
4460
|
+
const download = options.download ?? downloadNotionDirectory;
|
|
4461
|
+
const discover = options.discover ?? discoverSkills;
|
|
4462
|
+
let skillCount = 0;
|
|
4463
|
+
spinner$5.start("Preparing selected Notion packs…");
|
|
4464
|
+
try {
|
|
4465
|
+
await mkdir$1(packsDir, { recursive: true });
|
|
4466
|
+
for (const [index, pack] of selectedPacks.entries()) {
|
|
4467
|
+
let downloaded = null;
|
|
4468
|
+
try {
|
|
4469
|
+
downloaded = await download((await fetchNotionPackDirectory(pack, { runNtn: options.runNtn })).url);
|
|
4470
|
+
const skills = await discover(downloaded.rootDir, void 0, {
|
|
4471
|
+
includeInternal: true,
|
|
4472
|
+
fullDepth: true
|
|
4473
|
+
});
|
|
4474
|
+
if (skills.length === 0) throw new Error("pack contains no valid skills");
|
|
4475
|
+
const directoryName = `pack-${index + 1}-${sanitizeName(pack.name).slice(0, 100)}`;
|
|
4476
|
+
const targetDir = join$1(packsDir, directoryName);
|
|
4477
|
+
await cp$1(downloaded.rootDir, targetDir, {
|
|
4478
|
+
recursive: true,
|
|
4479
|
+
force: false,
|
|
4480
|
+
errorOnExist: true
|
|
4481
|
+
});
|
|
4482
|
+
manifestPlugins.push({
|
|
4483
|
+
name: pack.name,
|
|
4484
|
+
source: `./${directoryName}`,
|
|
4485
|
+
skills: skills.map((skill) => {
|
|
4486
|
+
const skillPath = relative$1(downloaded.rootDir, skill.path).split(sep$1).join("/");
|
|
4487
|
+
return skillPath ? `./${skillPath}` : "./.";
|
|
4488
|
+
})
|
|
4489
|
+
});
|
|
4490
|
+
skillCount += skills.length;
|
|
4491
|
+
} catch (error) {
|
|
4492
|
+
const detail = error instanceof Error ? error.message : "Unknown error";
|
|
4493
|
+
throw new Error(`Failed to prepare Notion pack ${JSON.stringify(pack.name)}: ${detail}`);
|
|
4494
|
+
} finally {
|
|
4495
|
+
if (downloaded) await cleanupTempDir(downloaded.tempDir).catch(() => {});
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
const manifestDir = join$1(stagingDir, ".claude-plugin");
|
|
4499
|
+
await mkdir$1(manifestDir, { recursive: true });
|
|
4500
|
+
await writeFile$1(join$1(manifestDir, "marketplace.json"), `${JSON.stringify({
|
|
4501
|
+
metadata: { pluginRoot: "./packs" },
|
|
4502
|
+
plugins: manifestPlugins
|
|
4503
|
+
}, null, 2)}\n`, "utf8");
|
|
4504
|
+
spinner$5.stop(`Prepared ${import_picocolors.default.green(selectedPacks.length)} Notion pack${selectedPacks.length === 1 ? "" : "s"} with ${import_picocolors.default.green(skillCount)} skill${skillCount === 1 ? "" : "s"}`);
|
|
4505
|
+
return {
|
|
4506
|
+
rootDir: stagingDir,
|
|
4507
|
+
tempDir: stagingDir,
|
|
4508
|
+
packCount: selectedPacks.length,
|
|
4509
|
+
skillCount
|
|
4510
|
+
};
|
|
4511
|
+
} catch (error) {
|
|
4512
|
+
spinner$5.stop(import_picocolors.default.red("Failed to prepare Notion packs"));
|
|
4513
|
+
await cleanupTempDir(stagingDir).catch(() => {});
|
|
4514
|
+
throw error;
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4128
4517
|
const isCancelled$1 = (value) => typeof value === "symbol";
|
|
4129
4518
|
const EVE_AGENT_LABEL = "eve agent";
|
|
4130
4519
|
async function isSourcePrivate(source) {
|
|
@@ -4365,6 +4754,15 @@ function isSkillsShPackUrl(url) {
|
|
|
4365
4754
|
return false;
|
|
4366
4755
|
}
|
|
4367
4756
|
}
|
|
4757
|
+
function logAutoSelectedSkills(entries) {
|
|
4758
|
+
const only = entries.length === 1 ? entries[0] : null;
|
|
4759
|
+
if (!only) {
|
|
4760
|
+
log.info(`Installing all ${entries.length} skills`);
|
|
4761
|
+
return;
|
|
4762
|
+
}
|
|
4763
|
+
log.info(`Skill: ${import_picocolors.default.cyan(only.label)}`);
|
|
4764
|
+
if (only.description) log.message(import_picocolors.default.dim(only.description));
|
|
4765
|
+
}
|
|
4368
4766
|
async function handleWellKnownSkills(source, url, options, spinner) {
|
|
4369
4767
|
spinner.start("Discovering skills from well-known endpoint...");
|
|
4370
4768
|
let skills = [];
|
|
@@ -4400,9 +4798,13 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
4400
4798
|
process.exit(0);
|
|
4401
4799
|
}
|
|
4402
4800
|
let selectedSkills;
|
|
4801
|
+
const logWellKnown = (chosen) => logAutoSelectedSkills(chosen.map((s) => ({
|
|
4802
|
+
label: s.installName,
|
|
4803
|
+
description: s.description
|
|
4804
|
+
})));
|
|
4403
4805
|
if (options.skill?.includes("*")) {
|
|
4404
4806
|
selectedSkills = skills;
|
|
4405
|
-
|
|
4807
|
+
logWellKnown(selectedSkills);
|
|
4406
4808
|
} else if (options.skill && options.skill.length > 0) {
|
|
4407
4809
|
selectedSkills = skills.filter((s) => options.skill.some((name) => s.installName.toLowerCase() === name.toLowerCase() || s.name.toLowerCase() === name.toLowerCase()));
|
|
4408
4810
|
if (selectedSkills.length === 0) {
|
|
@@ -4411,13 +4813,9 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
4411
4813
|
for (const s of skills) log.message(` - ${s.installName}`);
|
|
4412
4814
|
process.exit(1);
|
|
4413
4815
|
}
|
|
4414
|
-
} else if (skills.length === 1) {
|
|
4415
|
-
selectedSkills = skills;
|
|
4416
|
-
const firstSkill = skills[0];
|
|
4417
|
-
log.info(`Skill: ${import_picocolors.default.cyan(firstSkill.installName)}`);
|
|
4418
|
-
} else if (options.yes) {
|
|
4816
|
+
} else if (skills.length === 1 || options.yes) {
|
|
4419
4817
|
selectedSkills = skills;
|
|
4420
|
-
|
|
4818
|
+
logWellKnown(selectedSkills);
|
|
4421
4819
|
} else {
|
|
4422
4820
|
const selected = await searchMultiselect({
|
|
4423
4821
|
message: "Select skills to install",
|
|
@@ -4726,15 +5124,26 @@ async function runAdd(args, options = {}) {
|
|
|
4726
5124
|
else if (!process.stdin.isTTY) showInstallTip();
|
|
4727
5125
|
let tempDir = null;
|
|
4728
5126
|
try {
|
|
5127
|
+
let effectiveSource = source;
|
|
5128
|
+
let notionSourceLabel = null;
|
|
5129
|
+
const notionSkillPageId = parseNotionSkillUrl(source);
|
|
5130
|
+
if (isNotionSource(source) || notionSkillPageId) {
|
|
5131
|
+
const prepared = notionSkillPageId ? await prepareNotionSkillSource(notionSkillPageId) : await prepareNotionPackSource(options);
|
|
5132
|
+
if (!prepared) return;
|
|
5133
|
+
effectiveSource = prepared.rootDir;
|
|
5134
|
+
tempDir = prepared.tempDir;
|
|
5135
|
+
notionSourceLabel = "packCount" in prepared ? `${prepared.packCount} selected Notion pack${prepared.packCount === 1 ? "" : "s"}` : "Notion page";
|
|
5136
|
+
options.skill = ["*"];
|
|
5137
|
+
}
|
|
4729
5138
|
const spinner$3 = jsonMode ? {
|
|
4730
5139
|
start: () => {},
|
|
4731
5140
|
stop: () => {},
|
|
4732
5141
|
message: () => {}
|
|
4733
5142
|
} : spinner();
|
|
4734
5143
|
spinner$3.start("Parsing source…");
|
|
4735
|
-
const parsed = parseSource(
|
|
4736
|
-
let directDownload = parsed.type === "download";
|
|
4737
|
-
spinner$3.stop(`Source: ${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
|
|
5144
|
+
const parsed = parseSource(effectiveSource);
|
|
5145
|
+
let directDownload = parsed.type === "download" || notionSourceLabel !== null;
|
|
5146
|
+
spinner$3.stop(notionSourceLabel !== null ? `Source: ${notionSourceLabel}` : `Source: ${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
|
|
4738
5147
|
const ownerRepoRaw = parsed.type === "well-known" || parsed.type === "download" ? null : getOwnerRepo(parsed);
|
|
4739
5148
|
const repoPrivacyPromise = (() => {
|
|
4740
5149
|
if (parsed.type !== "github") return Promise.resolve(null);
|
|
@@ -4864,9 +5273,13 @@ async function runAdd(args, options = {}) {
|
|
|
4864
5273
|
emitJsonAndExit(0);
|
|
4865
5274
|
}
|
|
4866
5275
|
let selectedSkills;
|
|
5276
|
+
const logChosen = (chosen) => logAutoSelectedSkills(chosen.map((s) => ({
|
|
5277
|
+
label: getSkillDisplayName(s),
|
|
5278
|
+
description: s.description
|
|
5279
|
+
})));
|
|
4867
5280
|
if (options.skill?.includes("*")) {
|
|
4868
5281
|
selectedSkills = skills;
|
|
4869
|
-
|
|
5282
|
+
logChosen(selectedSkills);
|
|
4870
5283
|
} else if (options.skill && options.skill.length > 0) {
|
|
4871
5284
|
selectedSkills = filterSkills(skills, options.skill);
|
|
4872
5285
|
if (jsonMode) {
|
|
@@ -4884,14 +5297,9 @@ async function runAdd(args, options = {}) {
|
|
|
4884
5297
|
emitJsonAndExit(1, `No matching skills found for: ${options.skill.join(", ")}`);
|
|
4885
5298
|
}
|
|
4886
5299
|
log.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => import_picocolors.default.cyan(getSkillDisplayName(s))).join(", ")}`);
|
|
4887
|
-
} else if (skills.length === 1) {
|
|
4888
|
-
selectedSkills = skills;
|
|
4889
|
-
const firstSkill = skills[0];
|
|
4890
|
-
log.info(`Skill: ${import_picocolors.default.cyan(getSkillDisplayName(firstSkill))}`);
|
|
4891
|
-
log.message(import_picocolors.default.dim(firstSkill.description));
|
|
4892
|
-
} else if (options.yes) {
|
|
5300
|
+
} else if (skills.length === 1 || options.yes) {
|
|
4893
5301
|
selectedSkills = skills;
|
|
4894
|
-
|
|
5302
|
+
logChosen(selectedSkills);
|
|
4895
5303
|
} else {
|
|
4896
5304
|
const sortedSkills = [...skills].sort((a, b) => {
|
|
4897
5305
|
if (a.pluginName && !b.pluginName) return -1;
|
|
@@ -7749,6 +8157,8 @@ ${BOLD}Usage:${RESET} skills <command> [options]
|
|
|
7749
8157
|
${BOLD}Manage Skills:${RESET}
|
|
7750
8158
|
add <package> Add a skill package (alias: a)
|
|
7751
8159
|
e.g. vercel-labs/agent-skills
|
|
8160
|
+
notion
|
|
8161
|
+
https://notion.so/<skill-page>
|
|
7752
8162
|
https://github.com/vercel-labs/agent-skills
|
|
7753
8163
|
use <package>@<skill>
|
|
7754
8164
|
Generate a prompt for using one skill without installing it
|