skilld 1.2.2 → 1.3.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 (61) hide show
  1. package/README.md +21 -20
  2. package/dist/_chunks/agent.mjs +471 -17
  3. package/dist/_chunks/agent.mjs.map +1 -1
  4. package/dist/_chunks/assemble.mjs +2 -2
  5. package/dist/_chunks/assemble.mjs.map +1 -1
  6. package/dist/_chunks/cache.mjs +8 -2
  7. package/dist/_chunks/cache.mjs.map +1 -1
  8. package/dist/_chunks/cache2.mjs +2 -2
  9. package/dist/_chunks/cache2.mjs.map +1 -1
  10. package/dist/_chunks/cli-helpers.mjs +421 -0
  11. package/dist/_chunks/cli-helpers.mjs.map +1 -0
  12. package/dist/_chunks/detect.mjs +51 -22
  13. package/dist/_chunks/detect.mjs.map +1 -1
  14. package/dist/_chunks/detect2.mjs +2 -0
  15. package/dist/_chunks/embedding-cache.mjs +13 -4
  16. package/dist/_chunks/embedding-cache.mjs.map +1 -1
  17. package/dist/_chunks/formatting.mjs +1 -286
  18. package/dist/_chunks/formatting.mjs.map +1 -1
  19. package/dist/_chunks/index.d.mts.map +1 -1
  20. package/dist/_chunks/index2.d.mts.map +1 -1
  21. package/dist/_chunks/install.mjs +6 -4
  22. package/dist/_chunks/install.mjs.map +1 -1
  23. package/dist/_chunks/list.mjs +3 -2
  24. package/dist/_chunks/list.mjs.map +1 -1
  25. package/dist/_chunks/pool.mjs +3 -2
  26. package/dist/_chunks/pool.mjs.map +1 -1
  27. package/dist/_chunks/prompts.mjs +38 -4
  28. package/dist/_chunks/prompts.mjs.map +1 -1
  29. package/dist/_chunks/sanitize.mjs +7 -0
  30. package/dist/_chunks/sanitize.mjs.map +1 -1
  31. package/dist/_chunks/search-interactive.mjs +3 -2
  32. package/dist/_chunks/search-interactive.mjs.map +1 -1
  33. package/dist/_chunks/search.mjs +4 -3
  34. package/dist/_chunks/search.mjs.map +1 -1
  35. package/dist/_chunks/setup.mjs +27 -0
  36. package/dist/_chunks/setup.mjs.map +1 -0
  37. package/dist/_chunks/shared.mjs +6 -2
  38. package/dist/_chunks/shared.mjs.map +1 -1
  39. package/dist/_chunks/skills.mjs +1 -1
  40. package/dist/_chunks/sources.mjs +8 -8
  41. package/dist/_chunks/sources.mjs.map +1 -1
  42. package/dist/_chunks/sync.mjs +390 -108
  43. package/dist/_chunks/sync.mjs.map +1 -1
  44. package/dist/_chunks/uninstall.mjs +16 -2
  45. package/dist/_chunks/uninstall.mjs.map +1 -1
  46. package/dist/agent/index.d.mts +22 -4
  47. package/dist/agent/index.d.mts.map +1 -1
  48. package/dist/agent/index.mjs +3 -3
  49. package/dist/cli.mjs +619 -328
  50. package/dist/cli.mjs.map +1 -1
  51. package/dist/retriv/index.d.mts +18 -3
  52. package/dist/retriv/index.d.mts.map +1 -1
  53. package/dist/retriv/index.mjs +30 -1
  54. package/dist/retriv/index.mjs.map +1 -1
  55. package/dist/retriv/worker.d.mts +2 -0
  56. package/dist/retriv/worker.d.mts.map +1 -1
  57. package/dist/retriv/worker.mjs +1 -0
  58. package/dist/retriv/worker.mjs.map +1 -1
  59. package/dist/sources/index.mjs +1 -1
  60. package/package.json +9 -8
  61. package/dist/_chunks/chunk.mjs +0 -15
package/README.md CHANGED
@@ -21,7 +21,7 @@ Skilld generates [agent skills](https://agentskills.io/home) from the references
21
21
  <table>
22
22
  <tbody>
23
23
  <td align="center">
24
- <sub>Made possible by my <a href="https://github.com/sponsors/harlan-zw">Sponsor Program 💖</a><br> Follow me <a href="https://twitter.com/harlan_zw">@harlan_zw</a> 🐦 Join <a href="https://discord.gg/275MBUBvgP">Discord</a> for help</sub><br>
24
+ <sub>Made possible by my <a href="https://github.com/sponsors/harlan-zw">Sponsor Program 💖</a><br> Follow me <a href="https://twitter.com/harlan_zw">@harlan_zw</a> 🐦 - Join <a href="https://discord.gg/275MBUBvgP">Discord</a> for help</sub><br>
25
25
  </td>
26
26
  </tbody>
27
27
  </table>
@@ -32,11 +32,11 @@ Skilld generates [agent skills](https://agentskills.io/home) from the references
32
32
  - 🌍 **Any Source: Opt-in** - Any NPM dependency or GitHub source, docs auto-resolved
33
33
  - 📦 **Bleeding Edge Context** - Latest issues, discussions, and releases. Always use the latest best practices and avoid deprecated patterns.
34
34
  - 📚 **Opt-in LLM Sections** - Enhance skills with LLM-generated `Best Practices`, `API Changes`, or your own custom prompts
35
- - 🧩 **No Agent Required** - Export prompts and run them in any LLM (ChatGPT, Claude web, API). No CLI agent dependency.
35
+ - 🧩 **Use Any Agent** - Choose your agent: CLI , [pi](https://github.com/badlogic/pi-mono/tree/main/packages/ai) agents or no agent at all.
36
36
  - 🔍 **Semantic Search** - Query indexed docs across all skills via [retriv](https://github.com/harlan-zw/retriv) embeddings
37
- - 🧠 **Context-Aware** - Follows [Claude Code skill best practices](https://code.claude.com/docs/en/skills#add-supporting-files): SKILL.md stays under 500 lines, references are separate files the agent discovers on-demand not inlined into context
37
+ - 🧠 **Context-Aware** - Follows [Claude Code skill best practices](https://code.claude.com/docs/en/skills#add-supporting-files): SKILL.md stays under 500 lines, references are separate files the agent discovers on-demand - not inlined into context
38
38
  - 🎯 **Safe & Versioned** - Prompt injection sanitization, version-aware caching, auto-updates on new releases
39
- - 🤝 **Ecosystem** - Compatible with [`npx skills`](https://skills.sh/) and [skills-npm](https://github.com/antfu/skills-npm)
39
+ - 🤝 **Ecosystem** - Compatible with [`npx skills`](https://skills.sh/) and [skills-npm](https://github.com/antfu/skills-npm). Skilld auto-detects and uses skills-npm packages when available.
40
40
 
41
41
  ## Quick Start
42
42
 
@@ -56,11 +56,11 @@ npx -y skilld add vue
56
56
 
57
57
  If you need to re-configure skilld, just run `npx -y skilld config` to update your agent, model, or preferences.
58
58
 
59
- **No agent CLI?** No problem choose "No agent" when prompted. You get a base skill immediately, plus portable prompts you can run in any LLM:
59
+ **No agent CLI?** No problem - choose "No agent" when prompted. You get a base skill immediately, plus portable prompts you can run in any LLM:
60
60
 
61
61
  ```bash
62
62
  npx -y skilld add vue
63
- # Choose "No agent" base skill + prompts exported
63
+ # Choose "No agent" -> base skill + prompts exported
64
64
  # Paste prompts into ChatGPT/Claude web, save outputs, then:
65
65
  npx -y skilld assemble
66
66
  ```
@@ -195,12 +195,12 @@ No Claude, Gemini, or Codex CLI? Choose "No agent" when prompted. You get a base
195
195
 
196
196
  ```bash
197
197
  skilld add vue
198
- # Choose "No agent" installs to .claude/skills/vue-skilld/
198
+ # Choose "No agent" -> installs to .claude/skills/vue-skilld/
199
199
 
200
200
  # What you get:
201
- # SKILL.md base skill (works immediately)
202
- # PROMPT_*.md prompts to enhance it with any LLM
203
- # references/ docs, issues, releases as real files
201
+ # SKILL.md <- base skill (works immediately)
202
+ # PROMPT_*.md <- prompts to enhance it with any LLM
203
+ # references/ <- docs, issues, releases as real files
204
204
 
205
205
  # Run each PROMPT_*.md in ChatGPT/Claude web/any LLM
206
206
  # Save outputs as _BEST_PRACTICES.md, _API_CHANGES.md, then:
@@ -220,7 +220,7 @@ skilld eject vue --out ./skills/ # Custom path
220
220
  skilld eject vue --from 2025-07-01 # Only recent releases/issues
221
221
  ```
222
222
 
223
- Share via `skilld add owner/repo` consumers get fully functional skills with no LLM cost.
223
+ Share via `skilld add owner/repo` - consumers get fully functional skills with no LLM cost.
224
224
 
225
225
  ### CLI Options
226
226
 
@@ -242,20 +242,20 @@ Several approaches exist for steering agent knowledge. Each fills a different ni
242
242
 
243
243
  | Approach | Versioned | Curated | No Opt-in | Local | Any LLM |
244
244
  |:---------|:---------:|:-------:|:---------:|:-----:|:-------:|
245
- | **Manual rules** | | | | | |
246
- | **llms.txt** | ~ | | | | |
247
- | **MCP servers** | | | | | |
248
- | **skills.sh** | | ~ | | | |
249
- | **skills-npm** | | | | | |
250
- | **skilld** | | | | | |
245
+ | **Manual rules** | - | yes | yes | yes | yes |
246
+ | **llms.txt** | ~ | - | - | - | yes |
247
+ | **MCP servers** | yes | - | - | - | - |
248
+ | **skills.sh** | - | ~ | yes | - | - |
249
+ | **skills-npm** | yes | yes | - | yes | - |
250
+ | **skilld** | yes | yes | yes | yes | yes |
251
251
 
252
- > **Versioned** tied to your installed package version. **Curated** distilled best practices, not raw docs. **No Opt-in** works without the package author doing anything. **Local** runs on your machine, no external service dependency. **Any LLM** works with any LLM, not just agent CLIs.
252
+ > **Versioned** - tied to your installed package version. **Curated** - distilled best practices, not raw docs. **No Opt-in** - works without the package author doing anything. **Local** - runs on your machine, no external service dependency. **Any LLM** - works with any LLM, not just agent CLIs.
253
253
 
254
254
  - **Manual rules** (CLAUDE.md, .cursorrules): full control, but you need to already know the best practices and maintain them across every dep.
255
255
  - **[llms.txt](https://llmstxt.org/)**: standard convention for exposing docs to LLMs, but it's full docs not curated guidance and requires author adoption.
256
- - **MCP servers**: live, version-aware responses, but adds per-request latency and the maintainer has to build and maintain a server.
256
+ - **MCP servers** (Context7, etc.): live, version-aware responses, but adds per-request latency and the maintainer has to build and maintain a server.
257
257
  - **[skills.sh](https://skills.sh/)**: easy skill sharing with a growing ecosystem, but community-sourced without version-awareness or author oversight.
258
- - **[skills-npm](https://github.com/antfu/skills-npm)**: the ideal end-state: zero-token skills shipped by the package author, but requires every maintainer to opt in.
258
+ - **[skills-npm](https://github.com/antfu/skills-npm)**: the ideal end-state: zero-token skills shipped by the package author, but requires every maintainer to opt in. Skilld auto-detects and uses skills-npm packages when available.
259
259
  - **skilld**: generates version-aware skills from existing docs, changelogs, issues, and discussions. Works for any package without author opt-in.
260
260
 
261
261
  ## Telemetry
@@ -274,6 +274,7 @@ DO_NOT_TRACK=1
274
274
  ## Related
275
275
 
276
276
  - [skills-npm](https://github.com/antfu/skills-npm) - Convention for shipping agent skills in npm packages
277
+ - [agentskills.io](https://agentskills.io) - Agent skills specification
277
278
  - [mdream](https://github.com/harlan-zw/mdream) - HTML to Markdown converter
278
279
  - [retriv](https://github.com/harlan-zw/retriv) - Vector search with sqlite-vec
279
280
 
@@ -1,9 +1,8 @@
1
- import { t as __exportAll } from "./chunk.mjs";
2
1
  import { n as sanitizeMarkdown } from "./sanitize.mjs";
3
2
  import { g as readCachedSection, v as writeSections } from "./cache.mjs";
4
3
  import { i as resolveSkilldCommand } from "./shared.mjs";
5
4
  import { a as targets, t as detectInstalledAgents } from "./detect.mjs";
6
- import { c as SECTION_OUTPUT_FILES, f as getSectionValidator, l as buildAllSectionPrompts, m as wrapSection, s as SECTION_MERGE_ORDER } from "./prompts.mjs";
5
+ import { c as SECTION_OUTPUT_FILES, f as getSectionValidator, l as buildAllSectionPrompts, m as wrapSection, p as portabilizePrompt, s as SECTION_MERGE_ORDER } from "./prompts.mjs";
7
6
  import { homedir } from "node:os";
8
7
  import { dirname, join } from "pathe";
9
8
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
@@ -12,10 +11,24 @@ import { isWindows } from "std-env";
12
11
  import { glob } from "tinyglobby";
13
12
  import { findDynamicImports, findStaticImports } from "mlly";
14
13
  import { createHash } from "node:crypto";
15
- import { setTimeout } from "node:timers/promises";
14
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
16
15
  import { promisify } from "node:util";
16
+ import { getEnvApiKey, getModel, getModels, getProviders, streamSimple } from "@mariozechner/pi-ai";
17
+ import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@mariozechner/pi-ai/oauth";
17
18
  import { readFile } from "node:fs/promises";
18
19
  import { parseSync } from "oxc-parser";
20
+ //#region \0rolldown/runtime.js
21
+ var __defProp = Object.defineProperty;
22
+ var __exportAll = (all, no_symbols) => {
23
+ let target = {};
24
+ for (var name in all) __defProp(target, name, {
25
+ get: all[name],
26
+ enumerable: true
27
+ });
28
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
29
+ return target;
30
+ };
31
+ //#endregion
19
32
  //#region src/agent/clis/claude.ts
20
33
  var claude_exports = /* @__PURE__ */ __exportAll({
21
34
  agentId: () => agentId$2,
@@ -268,6 +281,324 @@ function parseLine(line) {
268
281
  return {};
269
282
  }
270
283
  //#endregion
284
+ //#region src/agent/clis/pi-ai.ts
285
+ function isPiAiModel(model) {
286
+ return model.startsWith("pi:");
287
+ }
288
+ /** Parse a pi:provider/model-id string → { provider, modelId } */
289
+ function parsePiAiModelId(model) {
290
+ if (!model.startsWith("pi:")) return null;
291
+ const rest = model.slice(3);
292
+ const slashIdx = rest.indexOf("/");
293
+ if (slashIdx === -1) return null;
294
+ return {
295
+ provider: rest.slice(0, slashIdx),
296
+ modelId: rest.slice(slashIdx + 1)
297
+ };
298
+ }
299
+ /** pi coding agent stores auth here; env var can override */
300
+ const PI_AGENT_AUTH_PATH = join(process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent"), "auth.json");
301
+ /** skilld's own auth file — used when user logs in via skilld */
302
+ const SKILLD_AUTH_PATH = join(homedir(), ".skilld", "pi-ai-auth.json");
303
+ function readAuthFile(path) {
304
+ if (!existsSync(path)) return {};
305
+ try {
306
+ return JSON.parse(readFileSync(path, "utf-8"));
307
+ } catch {
308
+ return {};
309
+ }
310
+ }
311
+ /** Load auth from pi coding agent first (~/.pi/agent/auth.json), then skilld's own */
312
+ function loadAuth() {
313
+ const piAuth = readAuthFile(PI_AGENT_AUTH_PATH);
314
+ return {
315
+ ...readAuthFile(SKILLD_AUTH_PATH),
316
+ ...piAuth
317
+ };
318
+ }
319
+ /** Save auth to skilld's own file — never writes to pi agent's auth */
320
+ function saveAuth(auth) {
321
+ mkdirSync(join(homedir(), ".skilld"), {
322
+ recursive: true,
323
+ mode: 448
324
+ });
325
+ writeFileSync(SKILLD_AUTH_PATH, JSON.stringify(auth, null, 2), { mode: 384 });
326
+ }
327
+ /**
328
+ * Overrides for model-provider → OAuth-provider mapping.
329
+ * Most providers share the same ID in both systems (auto-matched).
330
+ * Only list exceptions where the IDs diverge.
331
+ */
332
+ const OAUTH_PROVIDER_OVERRIDES = {
333
+ google: "google-gemini-cli",
334
+ openai: "openai-codex"
335
+ };
336
+ /** Resolve model provider ID → OAuth provider ID */
337
+ function resolveOAuthProviderId(modelProvider) {
338
+ if (OAUTH_PROVIDER_OVERRIDES[modelProvider]) return OAUTH_PROVIDER_OVERRIDES[modelProvider];
339
+ if (new Set(getOAuthProviders().map((p) => p.id)).has(modelProvider)) return modelProvider;
340
+ return null;
341
+ }
342
+ /** Resolve API key for a provider — checks env vars first, then OAuth credentials */
343
+ async function resolveApiKey(provider) {
344
+ const envKey = getEnvApiKey(provider);
345
+ if (envKey) return envKey;
346
+ const oauthProviderId = resolveOAuthProviderId(provider);
347
+ if (!oauthProviderId) return null;
348
+ const auth = loadAuth();
349
+ if (!auth[oauthProviderId]) return null;
350
+ const result = await getOAuthApiKey(oauthProviderId, auth);
351
+ if (!result) return null;
352
+ const skilldAuth = readAuthFile(SKILLD_AUTH_PATH);
353
+ skilldAuth[oauthProviderId] = {
354
+ type: "oauth",
355
+ ...result.newCredentials
356
+ };
357
+ saveAuth(skilldAuth);
358
+ return result.apiKey;
359
+ }
360
+ /** Get available OAuth providers for login */
361
+ function getOAuthProviderList() {
362
+ const auth = loadAuth();
363
+ return getOAuthProviders().map((p) => ({
364
+ id: p.id,
365
+ name: p.name ?? p.id,
366
+ loggedIn: !!auth[p.id]
367
+ }));
368
+ }
369
+ /** Run OAuth login for a provider, saving credentials to ~/.skilld/ */
370
+ async function loginOAuthProvider(providerId, callbacks) {
371
+ const provider = getOAuthProvider(providerId);
372
+ if (!provider) return false;
373
+ const credentials = await provider.login({
374
+ onAuth: (info) => callbacks.onAuth(info.url, info.instructions),
375
+ onPrompt: async (prompt) => callbacks.onPrompt(prompt.message, prompt.placeholder),
376
+ onProgress: (msg) => callbacks.onProgress?.(msg)
377
+ });
378
+ const auth = loadAuth();
379
+ auth[providerId] = {
380
+ type: "oauth",
381
+ ...credentials
382
+ };
383
+ saveAuth(auth);
384
+ return true;
385
+ }
386
+ /** Remove OAuth credentials for a provider */
387
+ function logoutOAuthProvider(providerId) {
388
+ const auth = loadAuth();
389
+ delete auth[providerId];
390
+ saveAuth(auth);
391
+ }
392
+ const MIN_CONTEXT_WINDOW = 32e3;
393
+ /** Legacy model patterns — old generations that clutter the model list */
394
+ const LEGACY_MODEL_PATTERNS = [
395
+ /^claude-3-/,
396
+ /^claude-3\.5-/,
397
+ /^claude-3\.7-/,
398
+ /^gpt-4(?!\.\d)/,
399
+ /^o1/,
400
+ /^o3-mini/,
401
+ /^gemini-1\./,
402
+ /^gemini-2\.0/,
403
+ /^gemini-live-/,
404
+ /-preview-\d{2}-\d{2,4}$/,
405
+ /-\d{8}$/
406
+ ];
407
+ function isLegacyModel(modelId) {
408
+ return LEGACY_MODEL_PATTERNS.some((p) => p.test(modelId));
409
+ }
410
+ /** Preferred model per provider for auto-selection (cheapest reliable option) */
411
+ const RECOMMENDED_MODELS = {
412
+ anthropic: /haiku/,
413
+ google: /flash/,
414
+ openai: /gpt-4\.1-mini/
415
+ };
416
+ /** Get all pi-ai models for providers with auth configured */
417
+ function getAvailablePiAiModels() {
418
+ const providers = getProviders();
419
+ const auth = loadAuth();
420
+ const available = [];
421
+ const recommendedPicked = /* @__PURE__ */ new Set();
422
+ for (const provider of providers) {
423
+ let authSource = "none";
424
+ if (getEnvApiKey(provider)) authSource = "env";
425
+ else {
426
+ const oauthId = resolveOAuthProviderId(provider);
427
+ if (oauthId && auth[oauthId]) authSource = "oauth";
428
+ }
429
+ if (authSource === "none") continue;
430
+ const models = getModels(provider);
431
+ const recPattern = RECOMMENDED_MODELS[provider];
432
+ let recModelId = null;
433
+ if (recPattern) {
434
+ for (const model of models) if (!isLegacyModel(model.id) && recPattern.test(model.id)) {
435
+ recModelId = model.id;
436
+ break;
437
+ }
438
+ }
439
+ for (const model of models) {
440
+ if (model.contextWindow && model.contextWindow < MIN_CONTEXT_WINDOW) continue;
441
+ if (isLegacyModel(model.id)) continue;
442
+ const id = `pi:${provider}/${model.id}`;
443
+ const ctx = model.contextWindow ? ` · ${Math.round(model.contextWindow / 1e3)}k ctx` : "";
444
+ const cost = model.cost?.input ? ` · $${model.cost.input}/Mtok` : "";
445
+ const isRecommended = model.id === recModelId && !recommendedPicked.has(provider);
446
+ if (isRecommended) recommendedPicked.add(provider);
447
+ available.push({
448
+ id,
449
+ name: model.name || model.id,
450
+ hint: `${authSource === "oauth" ? "OAuth" : "API key"}${ctx}${cost}`,
451
+ authSource,
452
+ recommended: isRecommended
453
+ });
454
+ }
455
+ }
456
+ return available;
457
+ }
458
+ const REFERENCE_SUBDIRS = [
459
+ "docs",
460
+ "issues",
461
+ "discussions",
462
+ "releases"
463
+ ];
464
+ const MAX_REFERENCE_CHARS = 15e4;
465
+ /** Read reference files from .skilld/ and format as inline context */
466
+ function collectReferenceContent(skillDir) {
467
+ const skilldDir = join(skillDir, ".skilld");
468
+ if (!existsSync(skilldDir)) return "";
469
+ const files = [];
470
+ let totalChars = 0;
471
+ for (const subdir of REFERENCE_SUBDIRS) {
472
+ const dirPath = join(skilldDir, subdir);
473
+ if (!existsSync(dirPath)) continue;
474
+ const indexPath = join(dirPath, "_INDEX.md");
475
+ if (existsSync(indexPath) && totalChars < MAX_REFERENCE_CHARS) {
476
+ const content = sanitizeMarkdown(readFileSync(indexPath, "utf-8"));
477
+ if (totalChars + content.length <= MAX_REFERENCE_CHARS) {
478
+ files.push({
479
+ path: `references/${subdir}/_INDEX.md`,
480
+ content
481
+ });
482
+ totalChars += content.length;
483
+ }
484
+ }
485
+ const entries = readdirSync(dirPath, { recursive: true });
486
+ for (const entry of entries) {
487
+ if (totalChars >= MAX_REFERENCE_CHARS) break;
488
+ const entryStr = String(entry);
489
+ if (!entryStr.endsWith(".md") || entryStr === "_INDEX.md") continue;
490
+ const fullPath = join(dirPath, entryStr);
491
+ if (!existsSync(fullPath)) continue;
492
+ try {
493
+ const content = sanitizeMarkdown(readFileSync(fullPath, "utf-8"));
494
+ if (totalChars + content.length > MAX_REFERENCE_CHARS) continue;
495
+ files.push({
496
+ path: `references/${subdir}/${entryStr}`,
497
+ content
498
+ });
499
+ totalChars += content.length;
500
+ } catch {}
501
+ }
502
+ }
503
+ const readmePath = join(skilldDir, "pkg", "README.md");
504
+ if (existsSync(readmePath) && totalChars < MAX_REFERENCE_CHARS) {
505
+ const content = sanitizeMarkdown(readFileSync(readmePath, "utf-8"));
506
+ if (totalChars + content.length <= MAX_REFERENCE_CHARS) {
507
+ files.push({
508
+ path: "references/pkg/README.md",
509
+ content
510
+ });
511
+ totalChars += content.length;
512
+ }
513
+ }
514
+ const pkgDir = join(skilldDir, "pkg");
515
+ if (existsSync(pkgDir) && totalChars < MAX_REFERENCE_CHARS) try {
516
+ const pkgEntries = readdirSync(pkgDir, { recursive: true });
517
+ for (const entry of pkgEntries) {
518
+ const entryStr = String(entry);
519
+ if (!entryStr.endsWith(".d.ts")) continue;
520
+ const fullPath = join(pkgDir, entryStr);
521
+ if (!existsSync(fullPath)) continue;
522
+ const content = sanitizeMarkdown(readFileSync(fullPath, "utf-8"));
523
+ if (totalChars + content.length <= MAX_REFERENCE_CHARS) {
524
+ files.push({
525
+ path: `references/pkg/${entryStr}`,
526
+ content
527
+ });
528
+ totalChars += content.length;
529
+ }
530
+ break;
531
+ }
532
+ } catch {}
533
+ if (files.length === 0) return "";
534
+ return `<reference-files>\n${files.map((f) => `<file path="${f.path}">\n${f.content.replaceAll("</file>", "&lt;/file&gt;").replaceAll("</reference-files>", "&lt;/reference-files&gt;")}\n</file>`).join("\n")}\n</reference-files>`;
535
+ }
536
+ /** Optimize a single section using pi-ai direct API */
537
+ async function optimizeSectionPiAi(opts) {
538
+ const parsed = parsePiAiModelId(opts.model);
539
+ if (!parsed) throw new Error(`Invalid pi-ai model ID: ${opts.model}. Expected format: pi:provider/model-id`);
540
+ const model = getModel(parsed.provider, parsed.modelId);
541
+ const apiKey = await resolveApiKey(parsed.provider);
542
+ const portablePrompt = portabilizePrompt(opts.prompt, opts.section);
543
+ const references = collectReferenceContent(opts.skillDir);
544
+ const fullPrompt = references ? `${portablePrompt}\n\n## Reference Content\n\nThe following files are provided inline for your reference:\n\n${references}` : portablePrompt;
545
+ opts.onProgress?.({
546
+ chunk: "[starting...]",
547
+ type: "reasoning",
548
+ text: "",
549
+ reasoning: "",
550
+ section: opts.section
551
+ });
552
+ const stream = streamSimple(model, {
553
+ systemPrompt: "You are a technical documentation expert generating SKILL.md sections for AI agent skills. Output clean, structured markdown following the format instructions exactly.",
554
+ messages: [{
555
+ role: "user",
556
+ content: [{
557
+ type: "text",
558
+ text: fullPrompt
559
+ }],
560
+ timestamp: Date.now()
561
+ }]
562
+ }, {
563
+ reasoning: "medium",
564
+ maxTokens: 16384,
565
+ ...apiKey ? { apiKey } : {}
566
+ });
567
+ let text = "";
568
+ let usage;
569
+ let cost;
570
+ for await (const event of stream) {
571
+ if (opts.signal?.aborted) throw new Error("pi-ai request timed out");
572
+ switch (event.type) {
573
+ case "text_delta":
574
+ text += event.delta;
575
+ opts.onProgress?.({
576
+ chunk: event.delta,
577
+ type: "text",
578
+ text,
579
+ reasoning: "",
580
+ section: opts.section
581
+ });
582
+ break;
583
+ case "done":
584
+ if (event.message?.usage) {
585
+ usage = {
586
+ input: event.message.usage.input,
587
+ output: event.message.usage.output
588
+ };
589
+ cost = event.message.usage.cost?.total;
590
+ }
591
+ break;
592
+ case "error": throw new Error(event.error?.errorMessage ?? "pi-ai stream error");
593
+ }
594
+ }
595
+ return {
596
+ text,
597
+ usage,
598
+ cost
599
+ };
600
+ }
601
+ //#endregion
271
602
  //#region src/agent/clis/index.ts
272
603
  const TOOL_VERBS = {
273
604
  Read: "Reading",
@@ -286,6 +617,9 @@ const TOOL_VERBS = {
286
617
  function createToolProgress(log) {
287
618
  let lastMsg = "";
288
619
  let repeatCount = 0;
620
+ /** Per-section timestamp of last "Writing..." emission — throttles text_delta spam */
621
+ const lastTextEmit = /* @__PURE__ */ new Map();
622
+ const TEXT_THROTTLE_MS = 2e3;
289
623
  function emit(msg) {
290
624
  if (msg === lastMsg) {
291
625
  repeatCount++;
@@ -298,6 +632,10 @@ function createToolProgress(log) {
298
632
  }
299
633
  return ({ type, chunk, section }) => {
300
634
  if (type === "text") {
635
+ const key = section ?? "";
636
+ const now = Date.now();
637
+ if (now - (lastTextEmit.get(key) ?? 0) < TEXT_THROTTLE_MS) return;
638
+ lastTextEmit.set(key, now);
301
639
  emit(`${section ? `\x1B[90m[${section}]\x1B[0m ` : ""}Writing...`);
302
640
  return;
303
641
  }
@@ -338,18 +676,42 @@ const CLI_PARSE_LINE = {
338
676
  gemini: parseLine,
339
677
  codex: parseLine$1
340
678
  };
679
+ /** Map CLI agent IDs to their LLM provider name (not the agent/tool name) */
680
+ const CLI_PROVIDER_NAMES = {
681
+ "claude-code": "Anthropic",
682
+ "gemini-cli": "Google",
683
+ "codex": "OpenAI"
684
+ };
685
+ const PI_PROVIDER_NAMES = {
686
+ "anthropic": "Anthropic",
687
+ "google": "Google",
688
+ "google-antigravity": "Antigravity",
689
+ "google-gemini-cli": "Google Gemini",
690
+ "google-vertex": "Google Vertex",
691
+ "openai": "OpenAI",
692
+ "openai-codex": "OpenAI Codex",
693
+ "github-copilot": "GitHub Copilot",
694
+ "groq": "Groq",
695
+ "mistral": "Mistral",
696
+ "xai": "xAI"
697
+ };
341
698
  const CLI_MODELS = Object.fromEntries(CLI_DEFS.flatMap((def) => Object.entries(def.models).map(([id, entry]) => [id, {
342
699
  ...entry,
343
700
  cli: def.cli,
344
701
  agentId: def.agentId
345
702
  }])));
346
703
  function getModelName(id) {
704
+ if (isPiAiModel(id)) return parsePiAiModelId(id)?.modelId ?? id;
347
705
  return CLI_MODELS[id]?.name ?? id;
348
706
  }
349
707
  function getModelLabel(id) {
708
+ if (isPiAiModel(id)) {
709
+ const parsed = parsePiAiModelId(id);
710
+ return parsed ? `${PI_PROVIDER_NAMES[parsed.provider] ?? parsed.provider} · ${parsed.modelId}` : id;
711
+ }
350
712
  const config = CLI_MODELS[id];
351
713
  if (!config) return id;
352
- return `${targets[config.agentId]?.displayName ?? config.cli} · ${config.name}`;
714
+ return `${CLI_PROVIDER_NAMES[config.agentId] ?? config.cli} · ${config.name}`;
353
715
  }
354
716
  async function getAvailableModels() {
355
717
  const execAsync = promisify(exec);
@@ -365,14 +727,37 @@ async function getAvailableModels() {
365
727
  }
366
728
  }));
367
729
  const availableAgentIds = new Set(cliChecks.filter((id) => id != null));
368
- return Object.entries(CLI_MODELS).filter(([_, config]) => availableAgentIds.has(config.agentId)).map(([id, config]) => ({
369
- id,
370
- name: config.name,
371
- hint: config.hint,
372
- recommended: config.recommended,
373
- agentId: config.agentId,
374
- agentName: targets[config.agentId]?.displayName ?? config.agentId
375
- }));
730
+ const cliModels = Object.entries(CLI_MODELS).filter(([_, config]) => availableAgentIds.has(config.agentId)).map(([id, config]) => {
731
+ const providerName = CLI_PROVIDER_NAMES[config.agentId] ?? targets[config.agentId]?.displayName ?? config.agentId;
732
+ return {
733
+ id,
734
+ name: config.name,
735
+ hint: config.hint,
736
+ recommended: config.recommended,
737
+ agentId: config.agentId,
738
+ agentName: providerName,
739
+ provider: config.agentId,
740
+ providerName: `${providerName} (via ${config.cli} CLI)`,
741
+ vendorGroup: providerName
742
+ };
743
+ });
744
+ const piAiEntries = getAvailablePiAiModels().map((m) => {
745
+ const piProvider = parsePiAiModelId(m.id)?.provider ?? "pi-ai";
746
+ const displayName = PI_PROVIDER_NAMES[piProvider] ?? piProvider;
747
+ const authLabel = m.authSource === "env" ? "API" : "OAuth";
748
+ return {
749
+ id: m.id,
750
+ name: m.name,
751
+ hint: m.hint,
752
+ recommended: m.recommended,
753
+ agentId: "pi-ai",
754
+ agentName: `pi-ai (${m.authSource})`,
755
+ provider: `pi:${piProvider}:${m.authSource}`,
756
+ providerName: `${displayName} (${authLabel})`,
757
+ vendorGroup: displayName
758
+ };
759
+ });
760
+ return [...cliModels, ...piAiEntries];
376
761
  }
377
762
  /** Resolve symlinks in .skilld/ to get real paths for --add-dir */
378
763
  function resolveReferenceDirs(skillDir) {
@@ -416,9 +801,72 @@ function setCache(prompt, model, section, text) {
416
801
  timestamp: Date.now()
417
802
  }), { mode: 384 });
418
803
  }
419
- /** Spawn a single CLI process for one section */
804
+ async function optimizeSectionViaPiAi(opts) {
805
+ const { section, prompt, outputFile, skillDir, model, onProgress, timeout, debug } = opts;
806
+ const skilldDir = join(skillDir, ".skilld");
807
+ const outputPath = join(skilldDir, outputFile);
808
+ writeFileSync(join(skilldDir, `PROMPT_${section}.md`), prompt);
809
+ try {
810
+ const ac = new AbortController();
811
+ const timer = setTimeout(() => ac.abort(), timeout);
812
+ const result = await optimizeSectionPiAi({
813
+ section,
814
+ prompt,
815
+ skillDir,
816
+ model,
817
+ onProgress,
818
+ signal: ac.signal
819
+ }).finally(() => clearTimeout(timer));
820
+ const raw = result.text.trim();
821
+ if (debug) {
822
+ const logsDir = join(skilldDir, "logs");
823
+ const logName = section.toUpperCase().replace(/-/g, "_");
824
+ mkdirSync(logsDir, { recursive: true });
825
+ if (raw) writeFileSync(join(logsDir, `${logName}.md`), raw);
826
+ }
827
+ if (!raw) return {
828
+ section,
829
+ content: "",
830
+ wasOptimized: false,
831
+ error: "pi-ai returned empty response"
832
+ };
833
+ const content = cleanSectionOutput(raw);
834
+ if (content) writeFileSync(outputPath, content);
835
+ const validator = getSectionValidator(section);
836
+ const warnings = (content && validator ? validator(content) : []).map((w) => ({
837
+ section,
838
+ warning: w.warning
839
+ }));
840
+ return {
841
+ section,
842
+ content,
843
+ wasOptimized: !!content,
844
+ warnings: warnings?.length ? warnings : void 0,
845
+ usage: result.usage,
846
+ cost: result.cost
847
+ };
848
+ } catch (err) {
849
+ return {
850
+ section,
851
+ content: "",
852
+ wasOptimized: false,
853
+ error: err.message
854
+ };
855
+ }
856
+ }
857
+ /** Spawn a single CLI process for one section, or call pi-ai directly */
420
858
  function optimizeSection(opts) {
421
859
  const { section, prompt, outputFile, skillDir, model, onProgress, timeout, debug, preExistingFiles } = opts;
860
+ if (isPiAiModel(model)) return optimizeSectionViaPiAi({
861
+ section,
862
+ prompt,
863
+ outputFile,
864
+ skillDir,
865
+ model,
866
+ onProgress,
867
+ timeout,
868
+ debug
869
+ });
422
870
  const cliConfig = CLI_MODELS[model];
423
871
  if (!cliConfig) return Promise.resolve({
424
872
  section,
@@ -579,7 +1027,13 @@ async function optimizeDocs(opts) {
579
1027
  wasOptimized: false,
580
1028
  error: "No valid sections to generate"
581
1029
  };
582
- if (!CLI_MODELS[model]) return {
1030
+ if (isPiAiModel(model)) {
1031
+ if (!new Set(getAvailablePiAiModels().map((m) => m.id)).has(model)) return {
1032
+ optimized: "",
1033
+ wasOptimized: false,
1034
+ error: `Pi model unavailable or not authenticated: ${model}`
1035
+ };
1036
+ } else if (!CLI_MODELS[model]) return {
583
1037
  optimized: "",
584
1038
  wasOptimized: false,
585
1039
  error: `No CLI mapping for model: ${model}`
@@ -659,7 +1113,7 @@ async function optimizeDocs(opts) {
659
1113
  preExistingFiles
660
1114
  });
661
1115
  if (i === 0) return run();
662
- return setTimeout(i * STAGGER_MS).then(run);
1116
+ return setTimeout$1(i * STAGGER_MS).then(run);
663
1117
  })) : [];
664
1118
  const allResults = [...cachedResults];
665
1119
  let totalUsage;
@@ -694,7 +1148,7 @@ async function optimizeDocs(opts) {
694
1148
  reasoning: "",
695
1149
  section
696
1150
  });
697
- await setTimeout(STAGGER_MS);
1151
+ await setTimeout$1(STAGGER_MS);
698
1152
  const result = await optimizeSection({
699
1153
  section,
700
1154
  prompt,
@@ -990,6 +1444,6 @@ function isNodeBuiltin(pkg) {
990
1444
  return NODE_BUILTINS.has(base.split("/")[0]);
991
1445
  }
992
1446
  //#endregion
993
- export { getModelLabel as a, getAvailableModels as i, cleanSectionOutput as n, getModelName as o, createToolProgress as r, optimizeDocs as s, detectImportedPackages as t };
1447
+ export { getModelLabel as a, getOAuthProviderList as c, __exportAll as d, getAvailableModels as i, loginOAuthProvider as l, cleanSectionOutput as n, getModelName as o, createToolProgress as r, optimizeDocs as s, detectImportedPackages as t, logoutOAuthProvider as u };
994
1448
 
995
1449
  //# sourceMappingURL=agent.mjs.map