skilld 1.5.2 → 1.5.4

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 (44) hide show
  1. package/dist/THIRD-PARTY-LICENSES.md +38 -0
  2. package/dist/_chunks/agent.mjs +281 -147
  3. package/dist/_chunks/agent.mjs.map +1 -1
  4. package/dist/_chunks/assemble.mjs +2 -1
  5. package/dist/_chunks/assemble.mjs.map +1 -1
  6. package/dist/_chunks/author.mjs +26 -2
  7. package/dist/_chunks/author.mjs.map +1 -1
  8. package/dist/_chunks/cli-helpers.mjs +22 -5
  9. package/dist/_chunks/cli-helpers.mjs.map +1 -1
  10. package/dist/_chunks/cli-helpers2.mjs +2 -1
  11. package/dist/_chunks/index3.d.mts.map +1 -1
  12. package/dist/_chunks/install.mjs +6 -2
  13. package/dist/_chunks/install.mjs.map +1 -1
  14. package/dist/_chunks/libs/@sinclair/typebox.mjs +2748 -0
  15. package/dist/_chunks/libs/@sinclair/typebox.mjs.map +1 -0
  16. package/dist/_chunks/list.mjs +2 -1
  17. package/dist/_chunks/list.mjs.map +1 -1
  18. package/dist/_chunks/prepare2.mjs +2 -1
  19. package/dist/_chunks/prepare2.mjs.map +1 -1
  20. package/dist/_chunks/prompts.mjs +10 -15
  21. package/dist/_chunks/prompts.mjs.map +1 -1
  22. package/dist/_chunks/rolldown-runtime.mjs +13 -0
  23. package/dist/_chunks/sanitize.mjs +3 -0
  24. package/dist/_chunks/sanitize.mjs.map +1 -1
  25. package/dist/_chunks/search-interactive.mjs +2 -1
  26. package/dist/_chunks/search-interactive.mjs.map +1 -1
  27. package/dist/_chunks/search.mjs +2 -1
  28. package/dist/_chunks/setup.mjs +2 -1
  29. package/dist/_chunks/setup.mjs.map +1 -1
  30. package/dist/_chunks/sources.mjs +20 -7
  31. package/dist/_chunks/sources.mjs.map +1 -1
  32. package/dist/_chunks/sync-shared.mjs +2 -1
  33. package/dist/_chunks/sync-shared2.mjs +8 -4
  34. package/dist/_chunks/sync-shared2.mjs.map +1 -1
  35. package/dist/_chunks/sync.mjs +1 -1
  36. package/dist/_chunks/sync2.mjs +2 -1
  37. package/dist/_chunks/uninstall.mjs +2 -1
  38. package/dist/_chunks/uninstall.mjs.map +1 -1
  39. package/dist/_chunks/wizard.mjs +1 -1
  40. package/dist/agent/index.d.mts.map +1 -1
  41. package/dist/agent/index.mjs +2 -1
  42. package/dist/cli.mjs +2 -1
  43. package/dist/cli.mjs.map +1 -1
  44. package/package.json +7 -1
@@ -0,0 +1,38 @@
1
+ # Licenses of Bundled Dependencies
2
+
3
+ The published artifact additionally contains code with the following licenses:
4
+ MIT
5
+
6
+ # Bundled Dependencies
7
+
8
+ ## @sinclair/typebox
9
+
10
+ License: MIT
11
+ By: sinclairzx81
12
+ Repository: https://github.com/sinclairzx81/typebox-legacy
13
+
14
+ > TypeBox
15
+ >
16
+ > Json Schema Type Builder with Static Type Resolution for TypeScript
17
+ >
18
+ > The MIT License (MIT)
19
+ >
20
+ > Copyright (c) 2017-2026 Haydn Paterson
21
+ >
22
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
23
+ > of this software and associated documentation files (the "Software"), to deal
24
+ > in the Software without restriction, including without limitation the rights
25
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
26
+ > copies of the Software, and to permit persons to whom the Software is
27
+ > furnished to do so, subject to the following conditions:
28
+ >
29
+ > The above copyright notice and this permission notice shall be included in
30
+ > all copies or substantial portions of the Software.
31
+ >
32
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
38
+ > THE SOFTWARE.
@@ -1,34 +1,25 @@
1
+ import { t as __exportAll } from "./rolldown-runtime.mjs";
1
2
  import { n as sanitizeMarkdown } from "./sanitize.mjs";
2
3
  import { h as writeSections, m as readCachedSection } from "./cache.mjs";
3
4
  import { i as resolveSkilldCommand } from "./shared.mjs";
4
5
  import { a as targets, t as detectInstalledAgents } from "./detect.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";
6
+ import { c as SECTION_OUTPUT_FILES, f as getSectionValidator, l as buildAllSectionPrompts, m as wrapSection, s as SECTION_MERGE_ORDER } from "./prompts.mjs";
7
+ import { t as Type } from "./libs/@sinclair/typebox.mjs";
6
8
  import { homedir } from "node:os";
7
9
  import { dirname, join } from "pathe";
8
10
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
9
- import { exec, spawn } from "node:child_process";
11
+ import { exec, execFileSync, spawn } from "node:child_process";
10
12
  import { isWindows } from "std-env";
11
13
  import { glob } from "tinyglobby";
12
14
  import { findDynamicImports, findStaticImports } from "mlly";
13
15
  import { createHash } from "node:crypto";
14
16
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
15
17
  import { promisify } from "node:util";
18
+ import { resolve as resolve$1 } from "node:path";
16
19
  import { getEnvApiKey, getModel, getModels, getProviders, streamSimple } from "@mariozechner/pi-ai";
17
20
  import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@mariozechner/pi-ai/oauth";
18
21
  import { readFile } from "node:fs/promises";
19
22
  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
32
23
  //#region src/agent/clis/claude.ts
33
24
  var claude_exports = /* @__PURE__ */ __exportAll({
34
25
  agentId: () => agentId$2,
@@ -455,93 +446,147 @@ function getAvailablePiAiModels() {
455
446
  }
456
447
  return available;
457
448
  }
458
- const REFERENCE_SUBDIRS = [
459
- "docs",
460
- "issues",
461
- "discussions",
462
- "releases"
449
+ const TOOLS = [
450
+ {
451
+ name: "Read",
452
+ description: "Read a file. Path is relative to the working directory (e.g. \"./.skilld/docs/api.md\").",
453
+ parameters: Type.Object({ path: Type.String({ description: "File path to read" }) })
454
+ },
455
+ {
456
+ name: "Glob",
457
+ description: "List files matching a glob pattern (e.g. \"./.skilld/docs/*.md\"). Returns newline-separated paths.",
458
+ parameters: Type.Object({
459
+ pattern: Type.String({ description: "Glob pattern" }),
460
+ no_ignore: Type.Optional(Type.Boolean({ description: "Include gitignored files" }))
461
+ })
462
+ },
463
+ {
464
+ name: "Write",
465
+ description: "Write content to a file.",
466
+ parameters: Type.Object({
467
+ path: Type.String({ description: "File path to write" }),
468
+ content: Type.String({ description: "File content" })
469
+ })
470
+ },
471
+ {
472
+ name: "Bash",
473
+ description: "Run a shell command. Use for `skilld search`, `skilld validate`, etc.",
474
+ parameters: Type.Object({ command: Type.String({ description: "Shell command to run" }) })
475
+ }
463
476
  ];
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 {}
477
+ const MAX_TOOL_TURNS = 30;
478
+ const SAFE_COMMANDS = new Set([
479
+ "skilld",
480
+ "ls",
481
+ "cat",
482
+ "find"
483
+ ]);
484
+ const SHELL_META_RE = /[;&|`$()<>]/;
485
+ /** Resolve a path safely within skilldDir, blocking traversal */
486
+ function resolveSandboxedPath(p, skilldDir) {
487
+ const resolved = resolve$1(skilldDir, String(p).replace(/^\.\/\.skilld\//, "./").replace(/^\.skilld\//, "./").replace(/^\.\//, ""));
488
+ if (!resolved.startsWith(`${skilldDir}/`) && resolved !== skilldDir) throw new Error(`Path traversal blocked: ${p}`);
489
+ return resolved;
490
+ }
491
+ /** Match a file path against a glob pattern using simple segment matching (no regex from user input) */
492
+ function globMatch(filePath, pattern) {
493
+ const segments = pattern.split("**");
494
+ if (segments.length === 1) {
495
+ const parts = pattern.split("*");
496
+ if (parts.length === 1) return filePath === pattern;
497
+ let pos = 0;
498
+ for (let i = 0; i < parts.length; i++) {
499
+ const part = parts[i];
500
+ if (!part) continue;
501
+ const idx = filePath.indexOf(part, pos);
502
+ if (idx === -1) return false;
503
+ if (i === 0 && idx !== 0) return false;
504
+ pos = idx + part.length;
501
505
  }
506
+ if (parts.at(-1) !== "") return pos === filePath.length;
507
+ return true;
502
508
  }
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;
509
+ let remaining = filePath;
510
+ for (let i = 0; i < segments.length; i++) {
511
+ const seg = segments[i];
512
+ if (!seg) continue;
513
+ const segParts = seg.split("*");
514
+ let pos = 0;
515
+ let matched = false;
516
+ for (let attempt = remaining.indexOf(segParts[0], 0); attempt !== -1; attempt = remaining.indexOf(segParts[0], attempt + 1)) {
517
+ pos = attempt;
518
+ matched = true;
519
+ for (const sp of segParts) {
520
+ if (!sp) continue;
521
+ const idx = remaining.indexOf(sp, pos);
522
+ if (idx === -1) {
523
+ matched = false;
524
+ break;
525
+ }
526
+ pos = idx + sp.length;
527
+ }
528
+ if (matched) break;
512
529
  }
530
+ if (!matched) return false;
531
+ remaining = remaining.slice(pos);
513
532
  }
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;
533
+ return true;
534
+ }
535
+ /** Execute a tool call against the .skilld/ directory */
536
+ function executeTool(toolCall, skilldDir) {
537
+ const args = toolCall.arguments;
538
+ switch (toolCall.name) {
539
+ case "Read": {
540
+ const filePath = resolveSandboxedPath(args.path, skilldDir);
541
+ if (!existsSync(filePath)) return `Error: file not found: ${args.path}`;
542
+ return sanitizeMarkdown(readFileSync(filePath, "utf-8"));
543
+ }
544
+ case "Glob": {
545
+ const pattern = String(args.pattern).replace(/^\.\/\.skilld\//, "./").replace(/^\.skilld\//, "./").replace(/^\.\//, "");
546
+ const results = [];
547
+ const walkDir = (dir, prefix) => {
548
+ if (!existsSync(dir)) return;
549
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
550
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
551
+ if (entry.isDirectory()) walkDir(join(dir, entry.name), relPath);
552
+ else results.push(`./.skilld/${relPath}`);
553
+ }
554
+ };
555
+ const baseDir = pattern.split("*")[0]?.replace(/\/$/, "") ?? "";
556
+ walkDir(join(skilldDir, baseDir), baseDir);
557
+ const matched = results.filter((r) => globMatch(r.replace(/^\.\/\.skilld\//, ""), pattern));
558
+ return matched.length > 0 ? matched.join("\n") : `No files matching: ${args.pattern}`;
559
+ }
560
+ case "Write":
561
+ writeFileSync(resolveSandboxedPath(args.path, skilldDir), sanitizeMarkdown(String(args.content)));
562
+ return "File written successfully.";
563
+ case "Bash": {
564
+ const cmd = String(args.command).trim();
565
+ const parts = cmd.split(/\s+/);
566
+ const bin = parts[0] ?? "";
567
+ if (!SAFE_COMMANDS.has(bin) || SHELL_META_RE.test(cmd)) return `Error: command not allowed. Only skilld, ls, cat, find commands are permitted.`;
568
+ try {
569
+ return execFileSync(bin, parts.slice(1), {
570
+ cwd: skilldDir,
571
+ timeout: 15e3,
572
+ encoding: "utf-8",
573
+ maxBuffer: 512 * 1024
574
+ }).trim();
575
+ } catch (err) {
576
+ return `Error: ${err.message}`;
529
577
  }
530
- break;
531
578
  }
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>`;
579
+ default: return `Unknown tool: ${toolCall.name}`;
580
+ }
535
581
  }
536
- /** Optimize a single section using pi-ai direct API */
582
+ /** Optimize a single section using pi-ai agentic API with tool use */
537
583
  async function optimizeSectionPiAi(opts) {
538
584
  const parsed = parsePiAiModelId(opts.model);
539
585
  if (!parsed) throw new Error(`Invalid pi-ai model ID: ${opts.model}. Expected format: pi:provider/model-id`);
540
586
  const model = getModel(parsed.provider, parsed.modelId);
541
587
  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;
588
+ const skilldDir = join(opts.skillDir, ".skilld");
589
+ const fullPrompt = opts.prompt;
545
590
  opts.onProgress?.({
546
591
  chunk: "[starting...]",
547
592
  type: "reasoning",
@@ -549,53 +594,103 @@ async function optimizeSectionPiAi(opts) {
549
594
  reasoning: "",
550
595
  section: opts.section
551
596
  });
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
- });
597
+ const messages = [{
598
+ role: "user",
599
+ content: [{
600
+ type: "text",
601
+ text: fullPrompt
602
+ }],
603
+ timestamp: Date.now()
604
+ }];
567
605
  let text = "";
568
- let usage;
569
- let cost;
570
- for await (const event of stream) {
606
+ let completed = false;
607
+ let totalUsage;
608
+ let totalCost;
609
+ let lastWriteContent = "";
610
+ for (let turn = 0; turn < MAX_TOOL_TURNS; turn++) {
571
611
  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;
612
+ const eventStream = streamSimple(model, {
613
+ systemPrompt: "You are a technical documentation expert generating SKILL.md sections for AI agent skills. Follow the format instructions exactly. Use the provided tools to explore reference files in ./.skilld/ before writing your output.",
614
+ messages,
615
+ tools: TOOLS
616
+ }, {
617
+ reasoning: turn === 0 ? "medium" : void 0,
618
+ maxTokens: 16384,
619
+ ...apiKey ? { apiKey } : {}
620
+ });
621
+ let assistantMessage;
622
+ let turnText = "";
623
+ for await (const event of eventStream) {
624
+ if (opts.signal?.aborted) throw new Error("pi-ai request timed out");
625
+ switch (event.type) {
626
+ case "text_delta":
627
+ turnText += event.delta;
628
+ opts.onProgress?.({
629
+ chunk: event.delta,
630
+ type: "text",
631
+ text: turnText,
632
+ reasoning: "",
633
+ section: opts.section
634
+ });
635
+ break;
636
+ case "toolcall_end": {
637
+ const tc = event.toolCall;
638
+ const hint = tc.name === "Read" || tc.name === "Write" ? `[${tc.name}: ${tc.arguments.path}]` : tc.name === "Bash" ? `[${tc.name}: ${tc.arguments.command}]` : `[${tc.name}: ${tc.arguments.pattern}]`;
639
+ opts.onProgress?.({
640
+ chunk: hint,
641
+ type: "reasoning",
642
+ text: "",
643
+ reasoning: hint,
644
+ section: opts.section
645
+ });
646
+ break;
590
647
  }
591
- break;
592
- case "error": throw new Error(event.error?.errorMessage ?? "pi-ai stream error");
648
+ case "done":
649
+ assistantMessage = event.message;
650
+ break;
651
+ case "error": throw new Error(event.error?.errorMessage ?? "pi-ai stream error");
652
+ }
653
+ }
654
+ if (!assistantMessage) throw new Error("pi-ai stream ended without a message");
655
+ if (assistantMessage.usage) {
656
+ if (totalUsage) {
657
+ totalUsage.input += assistantMessage.usage.input;
658
+ totalUsage.output += assistantMessage.usage.output;
659
+ } else totalUsage = {
660
+ input: assistantMessage.usage.input,
661
+ output: assistantMessage.usage.output
662
+ };
663
+ totalCost = (totalCost ?? 0) + (assistantMessage.usage.cost?.total ?? 0);
664
+ }
665
+ messages.push(assistantMessage);
666
+ const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
667
+ if (toolCalls.length === 0) {
668
+ text = turnText;
669
+ completed = true;
670
+ break;
671
+ }
672
+ for (const tc of toolCalls) {
673
+ const result = executeTool(tc, skilldDir);
674
+ if (tc.name === "Write") lastWriteContent = String(tc.arguments.content);
675
+ messages.push({
676
+ role: "toolResult",
677
+ toolCallId: tc.id,
678
+ toolName: tc.name,
679
+ content: [{
680
+ type: "text",
681
+ text: result
682
+ }],
683
+ isError: result.startsWith("Error:"),
684
+ timestamp: Date.now()
685
+ });
593
686
  }
594
687
  }
688
+ if (!completed) throw new Error(`pi-ai exceeded ${MAX_TOOL_TURNS} tool turns without completing`);
595
689
  return {
596
- text,
597
- usage,
598
- cost
690
+ text: text || lastWriteContent,
691
+ fullPrompt,
692
+ usage: totalUsage,
693
+ cost: totalCost
599
694
  };
600
695
  }
601
696
  //#endregion
@@ -630,16 +725,22 @@ function createToolProgress(log) {
630
725
  log.message(msg);
631
726
  }
632
727
  }
633
- return ({ type, chunk, section }) => {
728
+ return ({ type, chunk, text, section }) => {
634
729
  if (type === "text") {
635
730
  const key = section ?? "";
636
731
  const now = Date.now();
637
732
  if (now - (lastTextEmit.get(key) ?? 0) < TEXT_THROTTLE_MS) return;
638
733
  lastTextEmit.set(key, now);
639
- emit(`${section ? `\x1B[90m[${section}]\x1B[0m ` : ""}Writing...`);
734
+ const prefix = section ? `\x1B[90m[${section}]\x1B[0m ` : "";
735
+ const items = text ? text.match(/^- (?:BREAKING|DEPRECATED|NEW|CHANGED|REMOVED|Use |Do |Set |Add |Avoid |Always |Never |Prefer |Check |Ensure )/gm)?.length ?? 0 : 0;
736
+ emit(items > 0 ? `${prefix}Writing... \x1B[90m(${items} items)\x1B[0m` : `${prefix}Writing...`);
640
737
  return;
641
738
  }
642
739
  if (type !== "reasoning" || !chunk.startsWith("[")) return;
740
+ if (/^\[(?:starting|retrying|cached)/.test(chunk)) {
741
+ emit(`${section ? `\x1B[90m[${section}]\x1B[0m ` : ""}${chunk.slice(1, -1)}`);
742
+ return;
743
+ }
643
744
  const match = chunk.match(/^\[([^:[\]]+)(?::\s(.+))?\]$/);
644
745
  if (!match) return;
645
746
  const names = match[1].split(",").map((n) => n.trim());
@@ -805,6 +906,9 @@ async function optimizeSectionViaPiAi(opts) {
805
906
  const { section, prompt, outputFile, skillDir, model, onProgress, timeout, debug } = opts;
806
907
  const skilldDir = join(skillDir, ".skilld");
807
908
  const outputPath = join(skilldDir, outputFile);
909
+ const logsDir = join(skilldDir, "logs");
910
+ const logName = section.toUpperCase().replace(/-/g, "_");
911
+ if (existsSync(outputPath)) unlinkSync(outputPath);
808
912
  writeFileSync(join(skilldDir, `PROMPT_${section}.md`), prompt);
809
913
  try {
810
914
  const ac = new AbortController();
@@ -819,9 +923,8 @@ async function optimizeSectionViaPiAi(opts) {
819
923
  }).finally(() => clearTimeout(timer));
820
924
  const raw = result.text.trim();
821
925
  if (debug) {
822
- const logsDir = join(skilldDir, "logs");
823
- const logName = section.toUpperCase().replace(/-/g, "_");
824
926
  mkdirSync(logsDir, { recursive: true });
927
+ writeFileSync(join(skilldDir, `PROMPT_${section}.md`), result.fullPrompt);
825
928
  if (raw) writeFileSync(join(logsDir, `${logName}.md`), raw);
826
929
  }
827
930
  if (!raw) return {
@@ -846,11 +949,16 @@ async function optimizeSectionViaPiAi(opts) {
846
949
  cost: result.cost
847
950
  };
848
951
  } catch (err) {
952
+ const errMsg = err.message;
953
+ if (debug || errMsg) {
954
+ mkdirSync(logsDir, { recursive: true });
955
+ writeFileSync(join(logsDir, `${logName}.stderr.log`), errMsg);
956
+ }
849
957
  return {
850
958
  section,
851
959
  content: "",
852
960
  wasOptimized: false,
853
- error: err.message
961
+ error: errMsg
854
962
  };
855
963
  }
856
964
  }
@@ -1141,15 +1249,28 @@ async function optimizeDocs(opts) {
1141
1249
  prompt
1142
1250
  });
1143
1251
  }
1144
- for (const { section, prompt } of retryQueue) {
1145
- onProgress?.({
1146
- chunk: `[${section}: retrying...]`,
1147
- type: "reasoning",
1148
- text: "",
1149
- reasoning: "",
1150
- section
1151
- });
1152
- await setTimeout$1(STAGGER_MS);
1252
+ for (const { index, section, prompt } of retryQueue) {
1253
+ const rateLimitDelay = parseRateLimitDelay(getRetryError(spawnResults[index]));
1254
+ if (rateLimitDelay != null) {
1255
+ const waitSec = Math.max(rateLimitDelay, 5);
1256
+ onProgress?.({
1257
+ chunk: `[${section}] Rate limited, waiting ${waitSec}s...`,
1258
+ type: "reasoning",
1259
+ text: "",
1260
+ reasoning: "",
1261
+ section
1262
+ });
1263
+ await setTimeout$1(waitSec * 1e3);
1264
+ } else {
1265
+ onProgress?.({
1266
+ chunk: `[${section}: retrying...]`,
1267
+ type: "reasoning",
1268
+ text: "",
1269
+ reasoning: "",
1270
+ section
1271
+ });
1272
+ await setTimeout$1(STAGGER_MS);
1273
+ }
1153
1274
  const result = await optimizeSection({
1154
1275
  section,
1155
1276
  prompt,
@@ -1212,6 +1333,22 @@ async function optimizeDocs(opts) {
1212
1333
  debugLogsDir
1213
1334
  };
1214
1335
  }
1336
+ /** Check if an error string indicates a rate limit (429) */
1337
+ function isRateLimitError(error) {
1338
+ if (!error) return false;
1339
+ return /\b429\b/.test(error) || /rate.?limit/i.test(error) || /exhausted.*capacity/i.test(error) || /quota.*reset/i.test(error);
1340
+ }
1341
+ /** Parse delay hint from rate limit error (e.g. "reset after 5s" → 5). Returns undefined if not a rate limit. */
1342
+ function parseRateLimitDelay(error) {
1343
+ if (!error || !isRateLimitError(error)) return void 0;
1344
+ const match = error.match(/reset\s+after\s+(\d+)s/i);
1345
+ return match ? Number(match[1]) : 10;
1346
+ }
1347
+ /** Extract error string from a PromiseSettledResult */
1348
+ function getRetryError(result) {
1349
+ if (result.status === "rejected") return String(result.reason);
1350
+ return result.value.error;
1351
+ }
1215
1352
  /** Shorten absolute paths for display: /home/user/project/.claude/skills/vue/SKILL.md → .claude/.../SKILL.md */
1216
1353
  function shortenPath(p) {
1217
1354
  const refIdx = p.indexOf(".skilld/");
@@ -1243,10 +1380,7 @@ function cleanSectionOutput(content) {
1243
1380
  else cleaned = cleaned.slice(afterOpen).trim();
1244
1381
  }
1245
1382
  const firstMarker = cleaned.match(/^(##\s|- (?:BREAKING|DEPRECATED|NEW): )/m);
1246
- if (firstMarker?.index && firstMarker.index > 0) {
1247
- const preamble = cleaned.slice(0, firstMarker.index);
1248
- if (/\b(?:function|const |let |var |export |return |import |async |class )\b/.test(preamble)) cleaned = cleaned.slice(firstMarker.index).trim();
1249
- }
1383
+ if (firstMarker?.index && firstMarker.index > 0) cleaned = cleaned.slice(firstMarker.index).trim();
1250
1384
  const headingMatch = cleaned.match(/^(## .+)\n/);
1251
1385
  if (headingMatch) {
1252
1386
  const heading = headingMatch[1];