skills 1.6.0 → 1.7.1

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.
Files changed (3) hide show
  1. package/README.md +1 -3
  2. package/dist/cli.mjs +421 -39
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -290,7 +290,7 @@ Skills can be installed to any of these agents:
290
290
  | IBM Bob | `bob` | `.bob/skills/` | `~/.bob/skills/` |
291
291
  | Claude Code | `claude-code` | `.claude/skills/` | `~/.claude/skills/` |
292
292
  | OpenClaw | `openclaw` | `skills/` | `~/.openclaw/skills/` |
293
- | Cline, Dexto, Kimi Code CLI, Loaf, Sarvam Code, Warp, Zed | `cline`, `dexto`, `kimi-code-cli`, `loaf`, `sarvam-code`, `warp`, `zed` | `.agents/skills/` | `~/.agents/skills/` |
293
+ | Cline, Dexto, Kimi Code CLI, Loaf, Pi, Sarvam Code, Warp, Zed | `cline`, `dexto`, `kimi-code-cli`, `loaf`, `pi`, `sarvam-code`, `warp`, `zed` | `.agents/skills/` | `~/.agents/skills/` |
294
294
  | CodeArts Agent | `codearts-agent` | `.codeartsdoer/skills/` | `~/.codeartsdoer/skills/` |
295
295
  | CodeBuddy | `codebuddy` | `.codebuddy/skills/` | `~/.codebuddy/skills/` |
296
296
  | Codemaker | `codemaker` | `.codemaker/skills/` | `~/.codemaker/skills/` |
@@ -330,7 +330,6 @@ Skills can be installed to any of these agents:
330
330
  | OpenCode | `opencode` | `.agents/skills/` | `~/.config/opencode/skills/` |
331
331
  | OpenHands | `openhands` | `.openhands/skills/` | `~/.openhands/skills/` |
332
332
  | Ona | `ona` | `.ona/skills/` | `~/.ona/skills/` |
333
- | Pi | `pi` | `.pi/skills/` | `~/.pi/agent/skills/` |
334
333
  | Posit Assistant | `posit-assistant` | `.posit/assistant/skills/` | `~/.posit/assistant/skills/` |
335
334
  | Qoder | `qoder` | `.qoder/skills/` | `~/.qoder/skills/` |
336
335
  | Qoder CN | `qoder-cn` | `.qoder/skills/` | `~/.qoder-cn/skills/` |
@@ -463,7 +462,6 @@ discover `SKILL.md` files outside these container directories (e.g. under
463
462
  - `.mux/skills/`
464
463
  - `.openhands/skills/`
465
464
  - `.ona/skills/`
466
- - `.pi/skills/`
467
465
  - `.posit/assistant/skills/`
468
466
  - `.qoder/skills/`
469
467
  - `.qwen/skills/`
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() {
@@ -1228,7 +1228,7 @@ function normalizeSkillName$1(name) {
1228
1228
  function normalizeRelativePath(path) {
1229
1229
  return path.split(sep).join("/").replace(/\/+/g, "/");
1230
1230
  }
1231
- function isRecord(value) {
1231
+ function isRecord$1(value) {
1232
1232
  return typeof value === "object" && value !== null && !Array.isArray(value);
1233
1233
  }
1234
1234
  function shouldInstallInternalSkills() {
@@ -1271,7 +1271,7 @@ async function parseSkillMd(skillMdPath, options) {
1271
1271
  warnSkippedSkill(skillMdPath, `frontmatter "name" and "description" must be strings (got ${typeof data.name} and ${typeof data.description})`);
1272
1272
  return null;
1273
1273
  }
1274
- const metadata = isRecord(data.metadata) ? data.metadata : void 0;
1274
+ const metadata = isRecord$1(data.metadata) ? data.metadata : void 0;
1275
1275
  if (metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
1276
1276
  return {
1277
1277
  name: sanitizeMetadata(data.name),
@@ -1918,8 +1918,8 @@ const agents = {
1918
1918
  pi: {
1919
1919
  name: "pi",
1920
1920
  displayName: "Pi",
1921
- skillsDir: ".pi/skills",
1922
- globalSkillsDir: join(home, ".pi/agent/skills"),
1921
+ skillsDir: ".agents/skills",
1922
+ globalSkillsDir: join(home, ".agents/skills"),
1923
1923
  detectInstalled: async () => {
1924
1924
  return existsSync(join(home, ".pi/agent"));
1925
1925
  }
@@ -2785,8 +2785,8 @@ async function listInstalledSkills(options = {}) {
2785
2785
  } catch {}
2786
2786
  return Array.from(skillsMap.values());
2787
2787
  }
2788
- const TELEMETRY_URL = "https://add-skill.vercel.sh/t";
2789
- const AUDIT_URL = "https://add-skill.vercel.sh/audit";
2788
+ const TELEMETRY_URL = "https://www.skills.sh/tele/t";
2789
+ const AUDIT_URL = "https://www.skills.sh/tele/audit";
2790
2790
  let cliVersion = null;
2791
2791
  let detectedAgentName = null;
2792
2792
  function setDetectedAgent(agentName) {
@@ -3560,17 +3560,20 @@ const DEFAULT_DOWNLOAD_MAX_BYTES = 10 * 1024 * 1024;
3560
3560
  const DEFAULT_EXTRACT_MAX_BYTES = 25 * 1024 * 1024;
3561
3561
  const DEFAULT_EXTRACT_MAX_FILES = 1e3;
3562
3562
  const FETCH_TIMEOUT_MS = 3e4;
3563
+ function positiveIntegerOr(value, fallback) {
3564
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
3565
+ }
3563
3566
  function getPositiveIntegerEnv(name, fallback) {
3564
3567
  const raw = process.env[name];
3565
3568
  if (!raw) return fallback;
3566
3569
  const parsed = Number.parseInt(raw, 10);
3567
3570
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
3568
3571
  }
3569
- function getDownloadLimits() {
3572
+ function getDownloadLimits(options) {
3570
3573
  return {
3571
- downloadMaxBytes: getPositiveIntegerEnv("SKILLS_DOWNLOAD_MAX_BYTES", DEFAULT_DOWNLOAD_MAX_BYTES),
3572
- extractMaxBytes: getPositiveIntegerEnv("SKILLS_EXTRACT_MAX_BYTES", DEFAULT_EXTRACT_MAX_BYTES),
3573
- 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))
3574
3577
  };
3575
3578
  }
3576
3579
  function isPathSafe$1(basePath, targetPath) {
@@ -3704,8 +3707,8 @@ async function getSingleTopLevelDirectory(dir) {
3704
3707
  if (visibleEntries.length !== 1 || !visibleEntries[0].isDirectory()) return null;
3705
3708
  return join$1(dir, visibleEntries[0].name);
3706
3709
  }
3707
- async function downloadSource(url) {
3708
- const limits = getDownloadLimits();
3710
+ async function downloadSource(url, options = {}) {
3711
+ const limits = getDownloadLimits(options);
3709
3712
  const tempDir = await mkdtemp$1(join$1(tmpdir$1(), "skills-download-"));
3710
3713
  const downloadedFile = join$1(tempDir, "source.download");
3711
3714
  const extractDir = join$1(tempDir, "extract");
@@ -4169,7 +4172,348 @@ async function tryBlobInstall(ownerRepo, options = {}) {
4169
4172
  tree
4170
4173
  };
4171
4174
  }
4172
- var version$1 = "1.6.0";
4175
+ var version$1 = "1.7.1";
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
+ }
4173
4517
  const isCancelled$1 = (value) => typeof value === "symbol";
4174
4518
  const EVE_AGENT_LABEL = "eve agent";
4175
4519
  async function isSourcePrivate(source) {
@@ -4410,6 +4754,15 @@ function isSkillsShPackUrl(url) {
4410
4754
  return false;
4411
4755
  }
4412
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
+ }
4413
4766
  async function handleWellKnownSkills(source, url, options, spinner) {
4414
4767
  spinner.start("Discovering skills from well-known endpoint...");
4415
4768
  let skills = [];
@@ -4445,9 +4798,13 @@ async function handleWellKnownSkills(source, url, options, spinner) {
4445
4798
  process.exit(0);
4446
4799
  }
4447
4800
  let selectedSkills;
4801
+ const logWellKnown = (chosen) => logAutoSelectedSkills(chosen.map((s) => ({
4802
+ label: s.installName,
4803
+ description: s.description
4804
+ })));
4448
4805
  if (options.skill?.includes("*")) {
4449
4806
  selectedSkills = skills;
4450
- log.info(`Installing all ${skills.length} skills`);
4807
+ logWellKnown(selectedSkills);
4451
4808
  } else if (options.skill && options.skill.length > 0) {
4452
4809
  selectedSkills = skills.filter((s) => options.skill.some((name) => s.installName.toLowerCase() === name.toLowerCase() || s.name.toLowerCase() === name.toLowerCase()));
4453
4810
  if (selectedSkills.length === 0) {
@@ -4456,13 +4813,9 @@ async function handleWellKnownSkills(source, url, options, spinner) {
4456
4813
  for (const s of skills) log.message(` - ${s.installName}`);
4457
4814
  process.exit(1);
4458
4815
  }
4459
- } else if (skills.length === 1) {
4816
+ } else if (skills.length === 1 || options.yes) {
4460
4817
  selectedSkills = skills;
4461
- const firstSkill = skills[0];
4462
- log.info(`Skill: ${import_picocolors.default.cyan(firstSkill.installName)}`);
4463
- } else if (options.yes) {
4464
- selectedSkills = skills;
4465
- log.info(`Installing all ${skills.length} skills`);
4818
+ logWellKnown(selectedSkills);
4466
4819
  } else {
4467
4820
  const selected = await searchMultiselect({
4468
4821
  message: "Select skills to install",
@@ -4771,15 +5124,26 @@ async function runAdd(args, options = {}) {
4771
5124
  else if (!process.stdin.isTTY) showInstallTip();
4772
5125
  let tempDir = null;
4773
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
+ }
4774
5138
  const spinner$3 = jsonMode ? {
4775
5139
  start: () => {},
4776
5140
  stop: () => {},
4777
5141
  message: () => {}
4778
5142
  } : spinner();
4779
5143
  spinner$3.start("Parsing source…");
4780
- const parsed = parseSource(source);
4781
- let directDownload = parsed.type === "download";
4782
- 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)}` : ""}`);
4783
5147
  const ownerRepoRaw = parsed.type === "well-known" || parsed.type === "download" ? null : getOwnerRepo(parsed);
4784
5148
  const repoPrivacyPromise = (() => {
4785
5149
  if (parsed.type !== "github") return Promise.resolve(null);
@@ -4909,9 +5273,13 @@ async function runAdd(args, options = {}) {
4909
5273
  emitJsonAndExit(0);
4910
5274
  }
4911
5275
  let selectedSkills;
5276
+ const logChosen = (chosen) => logAutoSelectedSkills(chosen.map((s) => ({
5277
+ label: getSkillDisplayName(s),
5278
+ description: s.description
5279
+ })));
4912
5280
  if (options.skill?.includes("*")) {
4913
5281
  selectedSkills = skills;
4914
- log.info(`Installing all ${skills.length} skills`);
5282
+ logChosen(selectedSkills);
4915
5283
  } else if (options.skill && options.skill.length > 0) {
4916
5284
  selectedSkills = filterSkills(skills, options.skill);
4917
5285
  if (jsonMode) {
@@ -4929,14 +5297,9 @@ async function runAdd(args, options = {}) {
4929
5297
  emitJsonAndExit(1, `No matching skills found for: ${options.skill.join(", ")}`);
4930
5298
  }
4931
5299
  log.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => import_picocolors.default.cyan(getSkillDisplayName(s))).join(", ")}`);
4932
- } else if (skills.length === 1) {
5300
+ } else if (skills.length === 1 || options.yes) {
4933
5301
  selectedSkills = skills;
4934
- const firstSkill = skills[0];
4935
- log.info(`Skill: ${import_picocolors.default.cyan(getSkillDisplayName(firstSkill))}`);
4936
- log.message(import_picocolors.default.dim(firstSkill.description));
4937
- } else if (options.yes) {
4938
- selectedSkills = skills;
4939
- log.info(`Installing all ${skills.length} skills`);
5302
+ logChosen(selectedSkills);
4940
5303
  } else {
4941
5304
  const sortedSkills = [...skills].sort((a, b) => {
4942
5305
  if (a.pluginName && !b.pluginName) return -1;
@@ -5260,9 +5623,9 @@ async function runAdd(args, options = {}) {
5260
5623
  installedSkillHashes.set(skillDisplayName, computedHash);
5261
5624
  } catch {}
5262
5625
  }
5263
- if (successful.length > 0 && installGlobally && normalizedSource) {
5626
+ if (successful.length > 0 && installGlobally && (normalizedSource || parsed.type === "local" && !directDownload)) {
5264
5627
  let cachedTree;
5265
- if (parsed.type === "github" && !blobResult) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
5628
+ if (parsed.type === "github" && !blobResult && normalizedSource) cachedTree = await fetchRepoTree(normalizedSource, parsed.ref, getGitHubToken);
5266
5629
  for (const skill of selectedSkills) {
5267
5630
  const skillDisplayName = getSkillDisplayName(skill);
5268
5631
  if (successfulSkillNames.has(skillDisplayName)) try {
@@ -5277,9 +5640,12 @@ async function runAdd(args, options = {}) {
5277
5640
  } else if (skillPathValue && tempDir) {
5278
5641
  const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPathValue)));
5279
5642
  if (hash) skillFolderHash = hash;
5643
+ } else if (parsed.type === "local") {
5644
+ const hash = await computeSkillFolderHash(skill.path);
5645
+ if (hash) skillFolderHash = hash;
5280
5646
  }
5281
5647
  await addSkillToLock(skill.name, {
5282
- source: lockSource || normalizedSource,
5648
+ source: lockSource || normalizedSource || parsed.url,
5283
5649
  sourceType: parsed.type,
5284
5650
  sourceUrl: parsed.url,
5285
5651
  ref: parsed.ref,
@@ -6562,7 +6928,7 @@ async function removeCommand(skillNames, options) {
6562
6928
  if (successful.length > 0) {
6563
6929
  const bySource = /* @__PURE__ */ new Map();
6564
6930
  for (const r of successful) {
6565
- const source = r.source || "local";
6931
+ const source = r.sourceType === "local" ? "local" : r.source || "local";
6566
6932
  const existing = bySource.get(source) || { skills: [] };
6567
6933
  existing.skills.push(r.skill);
6568
6934
  existing.sourceType = r.sourceType;
@@ -7182,7 +7548,7 @@ async function updateProjectSkills(options = {}) {
7182
7548
  if (hasUniversal) targetParts.push("Universal");
7183
7549
  targetParts.push(...targetAgentNames);
7184
7550
  if (targetParts.length > 0) console.log(`${TEXT$1}Updating for: ${targetParts.join(", ")}${RESET$1}`);
7185
- console.log(`${TEXT$1}Refreshing ${updatable.length + wellKnownCount} skill(s)…${RESET$1}`);
7551
+ console.log(`${TEXT$1}Checking ${updatable.length + wellKnownCount} skill(s)…${RESET$1}`);
7186
7552
  console.log();
7187
7553
  const { successCount: wkSuccessCount, failCount: wkFailCount } = await processWellKnownUpdates(wellKnownGroups, false, options);
7188
7554
  successCount += wkSuccessCount;
@@ -7196,6 +7562,7 @@ async function updateProjectSkills(options = {}) {
7196
7562
  }
7197
7563
  const localLock = await readLocalLock();
7198
7564
  const cliEntry = join(__dirname$1, "..", "bin", "cli.mjs");
7565
+ let upToDateCount = 0;
7199
7566
  if (updatable.length > 0 && !existsSync(cliEntry)) {
7200
7567
  console.log(`${DIM$1}✗ CLI entrypoint not found at ${cliEntry}${RESET$1}`);
7201
7568
  return {
@@ -7213,6 +7580,7 @@ async function updateProjectSkills(options = {}) {
7213
7580
  let tempDir = null;
7214
7581
  let deletedSkills = [];
7215
7582
  let resolvedPaths = null;
7583
+ const latestHashes = /* @__PURE__ */ new Map();
7216
7584
  if (cloneSource === null) {
7217
7585
  failCount += skillsForSource.length;
7218
7586
  console.log(`${DIM$1}✗ Cannot update ${source}: skills-lock.json is missing sourceUrl for this generic Git source${RESET$1}`);
@@ -7230,6 +7598,10 @@ async function updateProjectSkills(options = {}) {
7230
7598
  const resolution = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredLocations);
7231
7599
  deletedSkills = resolution.deletedSkills;
7232
7600
  resolvedPaths = resolution.resolvedPaths;
7601
+ for (const [name, skillPath] of resolution.resolvedPaths) try {
7602
+ const hash = await computeSkillFolderHash(join(tempDir, dirname(skillPath)));
7603
+ if (hash) latestHashes.set(name, hash);
7604
+ } catch {}
7233
7605
  } catch (error) {
7234
7606
  console.log(`${DIM$1}✗ Failed to check for deleted skills from ${source}${RESET$1}`);
7235
7607
  } finally {
@@ -7248,6 +7620,12 @@ async function updateProjectSkills(options = {}) {
7248
7620
  ...skill.entry,
7249
7621
  skillPath: resolvedPath
7250
7622
  } : skill.entry;
7623
+ const relocated = entry.skillPath !== skill.entry.skillPath;
7624
+ const latestHash = latestHashes.get(skill.name);
7625
+ if (!relocated && latestHash && latestHash === skill.entry.computedHash) {
7626
+ upToDateCount++;
7627
+ continue;
7628
+ }
7251
7629
  console.log(`${TEXT$1}Updating ${safeName}…${RESET$1}`);
7252
7630
  const installUrl = buildLocalUpdateSource(entry);
7253
7631
  if (!installUrl) {
@@ -7284,6 +7662,8 @@ async function updateProjectSkills(options = {}) {
7284
7662
  }
7285
7663
  }
7286
7664
  }
7665
+ if (upToDateCount > 0) if (upToDateCount === updatable.length && wkSuccessCount === 0 && wkFailCount === 0) console.log(`${TEXT$1}✓ All project skills are up to date${RESET$1}`);
7666
+ else console.log(`${DIM$1}${upToDateCount} project skill(s) already up to date${RESET$1}`);
7287
7667
  printLegacyProjectSkills(legacy);
7288
7668
  return {
7289
7669
  successCount,
@@ -7794,6 +8174,8 @@ ${BOLD}Usage:${RESET} skills <command> [options]
7794
8174
  ${BOLD}Manage Skills:${RESET}
7795
8175
  add <package> Add a skill package (alias: a)
7796
8176
  e.g. vercel-labs/agent-skills
8177
+ notion
8178
+ https://notion.so/<skill-page>
7797
8179
  https://github.com/vercel-labs/agent-skills
7798
8180
  use <package>@<skill>
7799
8181
  Generate a prompt for using one skill without installing it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {