skills 1.6.0 → 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.
Files changed (2) hide show
  1. package/dist/cli.mjs +395 -30
  2. package/package.json +1 -1
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),
@@ -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.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
+ }
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) {
4933
- 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) {
5300
+ } else if (skills.length === 1 || options.yes) {
4938
5301
  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;
@@ -7794,6 +8157,8 @@ ${BOLD}Usage:${RESET} skills <command> [options]
7794
8157
  ${BOLD}Manage Skills:${RESET}
7795
8158
  add <package> Add a skill package (alias: a)
7796
8159
  e.g. vercel-labs/agent-skills
8160
+ notion
8161
+ https://notion.so/<skill-page>
7797
8162
  https://github.com/vercel-labs/agent-skills
7798
8163
  use <package>@<skill>
7799
8164
  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.0",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {