@poncho-ai/cli 0.6.0 → 0.6.2

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 (50) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/dist/chunk-2TLKQG7R.js +5360 -0
  3. package/dist/chunk-3FY4LP2E.js +4981 -0
  4. package/dist/chunk-3WQANPEG.js +5086 -0
  5. package/dist/chunk-44DXWF6D.js +5450 -0
  6. package/dist/chunk-62G3MI43.js +5316 -0
  7. package/dist/chunk-6J2JICGH.js +5135 -0
  8. package/dist/chunk-6KLC6MWK.js +5357 -0
  9. package/dist/chunk-6LG2DUWF.js +5181 -0
  10. package/dist/chunk-ASAXSYEZ.js +5179 -0
  11. package/dist/chunk-B5B5LAR2.js +5181 -0
  12. package/dist/chunk-C7T4EJNQ.js +4934 -0
  13. package/dist/chunk-EPG7ZYDE.js +5452 -0
  14. package/dist/chunk-FMRRGTJX.js +5041 -0
  15. package/dist/chunk-HHMFEU26.js +5451 -0
  16. package/dist/chunk-JRJY6LUC.js +5178 -0
  17. package/dist/chunk-O7SJY7YQ.js +5177 -0
  18. package/dist/chunk-PYP4SKOI.js +5125 -0
  19. package/dist/chunk-VECWMU7E.js +5276 -0
  20. package/dist/chunk-XEBDWQI6.js +5178 -0
  21. package/dist/chunk-YW2D7Z22.js +5360 -0
  22. package/dist/chunk-YZXMEO2T.js +5177 -0
  23. package/dist/cli.js +1 -1
  24. package/dist/index.d.ts +18 -2
  25. package/dist/index.js +11 -1
  26. package/dist/run-interactive-ink-6EJ6Z5HE.js +494 -0
  27. package/dist/run-interactive-ink-72BHZB7Q.js +494 -0
  28. package/dist/run-interactive-ink-BNRIM52Y.js +494 -0
  29. package/dist/run-interactive-ink-BZNBOELJ.js +494 -0
  30. package/dist/run-interactive-ink-GODBXZF3.js +494 -0
  31. package/dist/run-interactive-ink-J4AISGNQ.js +494 -0
  32. package/dist/run-interactive-ink-K75SE2J2.js +494 -0
  33. package/dist/run-interactive-ink-M2XKKPIJ.js +494 -0
  34. package/dist/run-interactive-ink-MITWAF7L.js +494 -0
  35. package/dist/run-interactive-ink-NR5BRFUF.js +494 -0
  36. package/dist/run-interactive-ink-OGNG6UYE.js +494 -0
  37. package/dist/run-interactive-ink-P3VNJEXK.js +494 -0
  38. package/dist/run-interactive-ink-PHLW5YWV.js +494 -0
  39. package/dist/run-interactive-ink-PVU3XABN.js +494 -0
  40. package/dist/run-interactive-ink-SLSK7BY5.js +494 -0
  41. package/dist/run-interactive-ink-TRPYQYHG.js +494 -0
  42. package/dist/run-interactive-ink-U2RPRBIR.js +494 -0
  43. package/dist/run-interactive-ink-U2WTGZJ3.js +494 -0
  44. package/dist/run-interactive-ink-UHBFYNNB.js +494 -0
  45. package/dist/run-interactive-ink-XQDUN6OS.js +494 -0
  46. package/dist/run-interactive-ink-XUHSJCGH.js +494 -0
  47. package/package.json +1 -1
  48. package/src/index.ts +322 -27
  49. package/src/web-ui.ts +350 -33
  50. package/test/cli.test.ts +232 -1
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { access, cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { existsSync } from "node:fs";
4
4
  import {
5
5
  createServer,
@@ -7,7 +7,7 @@ import {
7
7
  type Server,
8
8
  type ServerResponse,
9
9
  } from "node:http";
10
- import { dirname, relative, resolve } from "node:path";
10
+ import { basename, dirname, normalize, relative, resolve } from "node:path";
11
11
  import { createRequire } from "node:module";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import {
@@ -328,13 +328,29 @@ poncho tools
328
328
  Install skills from a local path or remote repository, then verify discovery:
329
329
 
330
330
  \`\`\`bash
331
- # Install skills into ./skills
332
- poncho add <repo-or-path>
331
+ # Install all skills from a source package/repo
332
+ poncho skills add <repo-or-path>
333
+
334
+ # Install one specific skill path from a source
335
+ poncho skills add <repo-or-path> <relative-skill-path>
336
+
337
+ # Remove all installed skills from a source
338
+ poncho skills remove <repo-or-path>
339
+
340
+ # Remove one installed skill path from a source
341
+ poncho skills remove <repo-or-path> <relative-skill-path>
342
+
343
+ # List installed skills
344
+ poncho skills list
333
345
 
334
346
  # Verify loaded tools
335
347
  poncho tools
336
348
  \`\`\`
337
349
 
350
+ \`poncho skills add\` copies discovered skill directories (folders that contain \`SKILL.md\`) into \`skills/<source>/...\`.
351
+ If a destination folder already exists, the command fails instead of overwriting files.
352
+ \`poncho add\` and \`poncho remove\` remain available as aliases.
353
+
338
354
  After adding skills, run \`poncho dev\` or \`poncho run --interactive\` and ask the agent to use them.
339
355
 
340
356
  ## Configure MCP Servers (Remote)
@@ -385,6 +401,16 @@ Pattern format is strict slash-only:
385
401
  - MCP: \`server/tool\`, \`server/*\`
386
402
  - Scripts: relative paths such as \`./scripts/file.ts\`, \`./scripts/*\`, \`./tools/deploy.ts\`
387
403
 
404
+ Skill authoring guardrails:
405
+
406
+ - Every \`SKILL.md\` must include YAML frontmatter between \`---\` markers.
407
+ - Include at least \`name\` (required for discovery) and \`description\`.
408
+ - Put tool intent in frontmatter using \`allowed-tools\` and \`approval-required\`.
409
+ - \`approval-required\` is stricter than allowed access:
410
+ - MCP entries in \`approval-required\` must also appear in \`allowed-tools\`.
411
+ - Script entries outside \`./scripts/\` must also appear in \`allowed-tools\`.
412
+ - Keep MCP server connection details in \`poncho.config.js\`, not in \`SKILL.md\`.
413
+
388
414
  ## Configuration
389
415
 
390
416
  Core files:
@@ -520,7 +546,9 @@ const copyIfExists = async (sourcePath: string, destinationPath: string): Promis
520
546
  return;
521
547
  }
522
548
  await mkdir(dirname(destinationPath), { recursive: true });
523
- await cp(sourcePath, destinationPath, { recursive: true });
549
+ // Build outputs should contain materialized files, not symlinks to paths
550
+ // that may not exist inside deployment artifacts (e.g. .agents/skills/*).
551
+ await cp(sourcePath, destinationPath, { recursive: true, dereference: true });
524
552
  };
525
553
 
526
554
  const resolveCliEntrypoint = async (): Promise<string> => {
@@ -1798,48 +1826,278 @@ const resolveSkillRoot = (
1798
1826
  }
1799
1827
  };
1800
1828
 
1801
- /**
1802
- * Recursively check whether a directory (or any immediate sub-directory
1803
- * tree) contains at least one SKILL.md file.
1804
- */
1805
- const findSkillManifest = async (dir: string, depth = 2): Promise<boolean> => {
1829
+ const normalizeSkillSourceName = (value: string): string => {
1830
+ const normalized = value
1831
+ .trim()
1832
+ .replace(/\\/g, "/")
1833
+ .replace(/^@/, "")
1834
+ .replace(/[\/\s]+/g, "-")
1835
+ .replace(/[^a-zA-Z0-9._-]/g, "-")
1836
+ .replace(/-+/g, "-")
1837
+ .replace(/^-|-$/g, "");
1838
+ return normalized.length > 0 ? normalized : "skills";
1839
+ };
1840
+
1841
+ const collectSkillManifests = async (dir: string, depth = 2): Promise<string[]> => {
1842
+ const manifests: string[] = [];
1843
+ const localManifest = resolve(dir, "SKILL.md");
1806
1844
  try {
1807
- await access(resolve(dir, "SKILL.md"));
1808
- return true;
1845
+ await access(localManifest);
1846
+ manifests.push(localManifest);
1809
1847
  } catch {
1810
1848
  // Not found at this level — look one level deeper (e.g. skills/<name>/SKILL.md)
1811
1849
  }
1812
- if (depth <= 0) return false;
1850
+
1851
+ if (depth <= 0) return manifests;
1852
+
1813
1853
  try {
1814
- const { readdir } = await import("node:fs/promises");
1815
1854
  const entries = await readdir(dir, { withFileTypes: true });
1816
1855
  for (const entry of entries) {
1817
- if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
1818
- const found = await findSkillManifest(resolve(dir, entry.name), depth - 1);
1819
- if (found) return true;
1856
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1857
+
1858
+ let isDir = entry.isDirectory();
1859
+ // Dirent reports symlinks separately; resolve target type via stat()
1860
+ if (!isDir && entry.isSymbolicLink()) {
1861
+ try {
1862
+ const s = await stat(resolve(dir, entry.name));
1863
+ isDir = s.isDirectory();
1864
+ } catch {
1865
+ continue; // broken symlink — skip
1866
+ }
1867
+ }
1868
+
1869
+ if (isDir) {
1870
+ manifests.push(...(await collectSkillManifests(resolve(dir, entry.name), depth - 1)));
1820
1871
  }
1821
1872
  }
1822
1873
  } catch {
1823
1874
  // ignore read errors
1824
1875
  }
1825
- return false;
1876
+
1877
+ return manifests;
1826
1878
  };
1827
1879
 
1828
1880
  const validateSkillPackage = async (
1829
1881
  workingDir: string,
1830
1882
  packageNameOrPath: string,
1831
- ): Promise<void> => {
1883
+ ): Promise<{ skillRoot: string; manifests: string[] }> => {
1832
1884
  const skillRoot = resolveSkillRoot(workingDir, packageNameOrPath);
1833
- const hasSkill = await findSkillManifest(skillRoot);
1834
- if (!hasSkill) {
1885
+ const manifests = await collectSkillManifests(skillRoot);
1886
+ if (manifests.length === 0) {
1835
1887
  throw new Error(`Skill validation failed: no SKILL.md found in ${skillRoot}`);
1836
1888
  }
1889
+ return { skillRoot, manifests };
1837
1890
  };
1838
1891
 
1839
- export const addSkill = async (workingDir: string, packageNameOrPath: string): Promise<void> => {
1892
+ const selectSkillManifests = async (
1893
+ skillRoot: string,
1894
+ manifests: string[],
1895
+ relativeSkillPath?: string,
1896
+ ): Promise<string[]> => {
1897
+ if (!relativeSkillPath) return manifests;
1898
+
1899
+ const normalized = normalize(relativeSkillPath);
1900
+ if (normalized.startsWith("..") || normalized.startsWith("/")) {
1901
+ throw new Error(`Invalid skill path "${relativeSkillPath}": path must be within package root.`);
1902
+ }
1903
+
1904
+ const candidate = resolve(skillRoot, normalized);
1905
+ const relativeToRoot = relative(skillRoot, candidate).split("\\").join("/");
1906
+ if (relativeToRoot.startsWith("..") || relativeToRoot.startsWith("/")) {
1907
+ throw new Error(`Invalid skill path "${relativeSkillPath}": path escapes package root.`);
1908
+ }
1909
+
1910
+ const candidateAsFile = candidate.toLowerCase().endsWith("skill.md")
1911
+ ? candidate
1912
+ : resolve(candidate, "SKILL.md");
1913
+ if (!existsSync(candidateAsFile)) {
1914
+ throw new Error(
1915
+ `Skill path "${relativeSkillPath}" does not point to a directory (or file) containing SKILL.md.`,
1916
+ );
1917
+ }
1918
+
1919
+ const selected = manifests.filter((manifest) => resolve(manifest) === resolve(candidateAsFile));
1920
+ if (selected.length === 0) {
1921
+ throw new Error(`Skill path "${relativeSkillPath}" was not discovered as a valid skill manifest.`);
1922
+ }
1923
+ return selected;
1924
+ };
1925
+
1926
+ const copySkillsIntoProject = async (
1927
+ workingDir: string,
1928
+ manifests: string[],
1929
+ sourceName: string,
1930
+ ): Promise<string[]> => {
1931
+ const skillsDir = resolve(workingDir, "skills", normalizeSkillSourceName(sourceName));
1932
+ await mkdir(skillsDir, { recursive: true });
1933
+
1934
+ const destinations = new Map<string, string>();
1935
+ for (const manifest of manifests) {
1936
+ const sourceSkillDir = dirname(manifest);
1937
+ const skillFolderName = basename(sourceSkillDir);
1938
+ if (destinations.has(skillFolderName)) {
1939
+ throw new Error(
1940
+ `Skill copy failed: multiple skill directories map to "skills/${skillFolderName}" (${destinations.get(skillFolderName)} and ${sourceSkillDir}).`,
1941
+ );
1942
+ }
1943
+ destinations.set(skillFolderName, sourceSkillDir);
1944
+ }
1945
+
1946
+ const copied: string[] = [];
1947
+ for (const [skillFolderName, sourceSkillDir] of destinations.entries()) {
1948
+ const destinationSkillDir = resolve(skillsDir, skillFolderName);
1949
+ if (existsSync(destinationSkillDir)) {
1950
+ throw new Error(
1951
+ `Skill copy failed: destination already exists at ${destinationSkillDir}. Remove or rename it and try again.`,
1952
+ );
1953
+ }
1954
+ await cp(sourceSkillDir, destinationSkillDir, {
1955
+ recursive: true,
1956
+ dereference: true,
1957
+ force: false,
1958
+ errorOnExist: true,
1959
+ });
1960
+ copied.push(relative(workingDir, destinationSkillDir).split("\\").join("/"));
1961
+ }
1962
+
1963
+ return copied.sort();
1964
+ };
1965
+
1966
+ export const copySkillsFromPackage = async (
1967
+ workingDir: string,
1968
+ packageNameOrPath: string,
1969
+ options?: { path?: string },
1970
+ ): Promise<string[]> => {
1971
+ const { skillRoot, manifests } = await validateSkillPackage(workingDir, packageNameOrPath);
1972
+ const selected = await selectSkillManifests(skillRoot, manifests, options?.path);
1973
+ const sourceName = resolveInstalledPackageName(packageNameOrPath) ?? basename(skillRoot);
1974
+ return await copySkillsIntoProject(workingDir, selected, sourceName);
1975
+ };
1976
+
1977
+ export const addSkill = async (
1978
+ workingDir: string,
1979
+ packageNameOrPath: string,
1980
+ options?: { path?: string },
1981
+ ): Promise<void> => {
1840
1982
  await runInstallCommand(workingDir, packageNameOrPath);
1841
- await validateSkillPackage(workingDir, packageNameOrPath);
1842
- process.stdout.write(`Added skill: ${packageNameOrPath}\n`);
1983
+ const copiedSkills = await copySkillsFromPackage(workingDir, packageNameOrPath, options);
1984
+ process.stdout.write(
1985
+ `Added ${copiedSkills.length} skill${copiedSkills.length === 1 ? "" : "s"} from ${packageNameOrPath}:\n`,
1986
+ );
1987
+ for (const copied of copiedSkills) {
1988
+ process.stdout.write(`- ${copied}\n`);
1989
+ }
1990
+ };
1991
+
1992
+ const getSkillFolderNames = (manifests: string[]): string[] => {
1993
+ const names = new Set<string>();
1994
+ for (const manifest of manifests) {
1995
+ names.add(basename(dirname(manifest)));
1996
+ }
1997
+ return Array.from(names).sort();
1998
+ };
1999
+
2000
+ export const removeSkillsFromPackage = async (
2001
+ workingDir: string,
2002
+ packageNameOrPath: string,
2003
+ options?: { path?: string },
2004
+ ): Promise<{ removed: string[]; missing: string[] }> => {
2005
+ const { skillRoot, manifests } = await validateSkillPackage(workingDir, packageNameOrPath);
2006
+ const selected = await selectSkillManifests(skillRoot, manifests, options?.path);
2007
+ const skillsDir = resolve(workingDir, "skills");
2008
+ const sourceName = normalizeSkillSourceName(
2009
+ resolveInstalledPackageName(packageNameOrPath) ?? basename(skillRoot),
2010
+ );
2011
+ const sourceSkillsDir = resolve(skillsDir, sourceName);
2012
+ const skillNames = getSkillFolderNames(selected);
2013
+
2014
+ const removed: string[] = [];
2015
+ const missing: string[] = [];
2016
+
2017
+ if (!options?.path && existsSync(sourceSkillsDir)) {
2018
+ await rm(sourceSkillsDir, { recursive: true, force: false });
2019
+ removed.push(`skills/${sourceName}`);
2020
+ return { removed, missing };
2021
+ }
2022
+
2023
+ for (const skillName of skillNames) {
2024
+ const destinationSkillDir = resolve(sourceSkillsDir, skillName);
2025
+ const normalized = relative(skillsDir, destinationSkillDir).split("\\").join("/");
2026
+ if (normalized.startsWith("..") || normalized.startsWith("/")) {
2027
+ throw new Error(`Refusing to remove path outside skills directory: ${destinationSkillDir}`);
2028
+ }
2029
+
2030
+ if (!existsSync(destinationSkillDir)) {
2031
+ missing.push(`skills/${sourceName}/${skillName}`);
2032
+ continue;
2033
+ }
2034
+
2035
+ await rm(destinationSkillDir, { recursive: true, force: false });
2036
+ removed.push(`skills/${sourceName}/${skillName}`);
2037
+ }
2038
+
2039
+ return { removed, missing };
2040
+ };
2041
+
2042
+ export const removeSkillPackage = async (
2043
+ workingDir: string,
2044
+ packageNameOrPath: string,
2045
+ options?: { path?: string },
2046
+ ): Promise<void> => {
2047
+ const result = await removeSkillsFromPackage(workingDir, packageNameOrPath, options);
2048
+ process.stdout.write(
2049
+ `Removed ${result.removed.length} skill${result.removed.length === 1 ? "" : "s"} from ${packageNameOrPath}:\n`,
2050
+ );
2051
+ for (const removed of result.removed) {
2052
+ process.stdout.write(`- ${removed}\n`);
2053
+ }
2054
+ if (result.missing.length > 0) {
2055
+ process.stdout.write(
2056
+ `Skipped ${result.missing.length} missing skill${result.missing.length === 1 ? "" : "s"}:\n`,
2057
+ );
2058
+ for (const missing of result.missing) {
2059
+ process.stdout.write(`- ${missing}\n`);
2060
+ }
2061
+ }
2062
+ };
2063
+
2064
+ export const listInstalledSkills = async (
2065
+ workingDir: string,
2066
+ sourceName?: string,
2067
+ ): Promise<string[]> => {
2068
+ const skillsRoot = resolve(workingDir, "skills");
2069
+ const resolvedSourceName = sourceName
2070
+ ? resolveInstalledPackageName(sourceName) ?? sourceName
2071
+ : undefined;
2072
+ const targetRoot = sourceName
2073
+ ? resolve(skillsRoot, normalizeSkillSourceName(resolvedSourceName ?? sourceName))
2074
+ : skillsRoot;
2075
+ if (!existsSync(targetRoot)) {
2076
+ return [];
2077
+ }
2078
+ const manifests = await collectSkillManifests(targetRoot, sourceName ? 1 : 2);
2079
+ return manifests
2080
+ .map((manifest) => relative(workingDir, dirname(manifest)).split("\\").join("/"))
2081
+ .sort();
2082
+ };
2083
+
2084
+ export const listSkills = async (workingDir: string, sourceName?: string): Promise<void> => {
2085
+ const skills = await listInstalledSkills(workingDir, sourceName);
2086
+ if (skills.length === 0) {
2087
+ process.stdout.write("No installed skills found.\n");
2088
+ return;
2089
+ }
2090
+ const resolvedSourceName = sourceName
2091
+ ? resolveInstalledPackageName(sourceName) ?? sourceName
2092
+ : undefined;
2093
+ process.stdout.write(
2094
+ sourceName
2095
+ ? `Installed skills for ${normalizeSkillSourceName(resolvedSourceName ?? sourceName)}:\n`
2096
+ : "Installed skills:\n",
2097
+ );
2098
+ for (const skill of skills) {
2099
+ process.stdout.write(`- ${skill}\n`);
2100
+ }
1843
2101
  };
1844
2102
 
1845
2103
  export const runTests = async (
@@ -2390,12 +2648,49 @@ export const buildCli = (): Command => {
2390
2648
  await listTools(process.cwd());
2391
2649
  });
2392
2650
 
2651
+ const skillsCommand = program.command("skills").description("Manage installed skills");
2652
+ skillsCommand
2653
+ .command("add")
2654
+ .argument("<source>", "skill package name/path")
2655
+ .argument("[skillPath]", "optional path to one specific skill within source")
2656
+ .description("Install and copy skills into ./skills/<source>/...")
2657
+ .action(async (source: string, skillPath?: string) => {
2658
+ await addSkill(process.cwd(), source, { path: skillPath });
2659
+ });
2660
+
2661
+ skillsCommand
2662
+ .command("remove")
2663
+ .argument("<source>", "skill package name/path")
2664
+ .argument("[skillPath]", "optional path to one specific skill within source")
2665
+ .description("Remove installed skills from ./skills/<source>/...")
2666
+ .action(async (source: string, skillPath?: string) => {
2667
+ await removeSkillPackage(process.cwd(), source, { path: skillPath });
2668
+ });
2669
+
2670
+ skillsCommand
2671
+ .command("list")
2672
+ .argument("[source]", "optional source package/folder")
2673
+ .description("List installed skills")
2674
+ .action(async (source?: string) => {
2675
+ await listSkills(process.cwd(), source);
2676
+ });
2677
+
2393
2678
  program
2394
2679
  .command("add")
2395
2680
  .argument("<packageOrPath>", "skill package name/path")
2396
- .description("Add a skill package and validate SKILL.md")
2397
- .action(async (packageOrPath: string) => {
2398
- await addSkill(process.cwd(), packageOrPath);
2681
+ .option("--path <relativePath>", "only copy a specific skill path from the package")
2682
+ .description("Alias for `poncho skills add <source> [skillPath]`")
2683
+ .action(async (packageOrPath: string, options: { path?: string }) => {
2684
+ await addSkill(process.cwd(), packageOrPath, { path: options.path });
2685
+ });
2686
+
2687
+ program
2688
+ .command("remove")
2689
+ .argument("<packageOrPath>", "skill package name/path")
2690
+ .option("--path <relativePath>", "only remove a specific skill path from the package")
2691
+ .description("Alias for `poncho skills remove <source> [skillPath]`")
2692
+ .action(async (packageOrPath: string, options: { path?: string }) => {
2693
+ await removeSkillPackage(process.cwd(), packageOrPath, { path: options.path });
2399
2694
  });
2400
2695
 
2401
2696
  program