@oh-my-pi/pi-coding-agent 13.2.0 → 13.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +33 -14
  4. package/src/capability/index.ts +1 -2
  5. package/src/cli/args.ts +1 -2
  6. package/src/cli/config-cli.ts +1 -1
  7. package/src/cli/file-processor.ts +1 -2
  8. package/src/cli/grep-cli.ts +1 -1
  9. package/src/cli/jupyter-cli.ts +1 -1
  10. package/src/cli/plugin-cli.ts +1 -1
  11. package/src/cli/setup-cli.ts +1 -1
  12. package/src/cli/shell-cli.ts +1 -1
  13. package/src/cli/ssh-cli.ts +1 -1
  14. package/src/cli/stats-cli.ts +1 -2
  15. package/src/cli/update-cli.ts +1 -2
  16. package/src/cli/web-search-cli.ts +1 -1
  17. package/src/cli.ts +1 -1
  18. package/src/commands/launch.ts +2 -1
  19. package/src/commit/agentic/agent.ts +2 -1
  20. package/src/commit/agentic/index.ts +1 -2
  21. package/src/commit/agentic/prompts/system.md +3 -3
  22. package/src/commit/agentic/tools/propose-changelog.ts +30 -19
  23. package/src/commit/changelog/generate.ts +16 -6
  24. package/src/commit/changelog/index.ts +2 -1
  25. package/src/commit/pipeline.ts +1 -2
  26. package/src/commit/prompts/reduce-system.md +1 -1
  27. package/src/commit/types.ts +10 -1
  28. package/src/config/keybindings.ts +1 -2
  29. package/src/config/model-registry.ts +1 -1
  30. package/src/config/prompt-templates.ts +14 -2
  31. package/src/config/settings.ts +9 -2
  32. package/src/config.ts +1 -2
  33. package/src/debug/index.ts +1 -1
  34. package/src/debug/report-bundle.ts +1 -2
  35. package/src/debug/system-info.ts +1 -2
  36. package/src/discovery/agents.ts +2 -2
  37. package/src/discovery/builtin.ts +8 -9
  38. package/src/discovery/claude-plugins.ts +2 -2
  39. package/src/discovery/claude.ts +7 -7
  40. package/src/discovery/codex.ts +3 -3
  41. package/src/discovery/cursor.ts +5 -4
  42. package/src/discovery/gemini.ts +5 -5
  43. package/src/discovery/helpers.ts +47 -69
  44. package/src/discovery/mcp-json.ts +3 -3
  45. package/src/discovery/opencode.ts +7 -8
  46. package/src/discovery/ssh.ts +3 -3
  47. package/src/discovery/vscode.ts +3 -2
  48. package/src/discovery/windsurf.ts +3 -2
  49. package/src/exa/company.ts +1 -1
  50. package/src/exa/factory.ts +1 -6
  51. package/src/exa/linkedin.ts +1 -1
  52. package/src/exa/mcp-client.ts +19 -8
  53. package/src/exa/search.ts +2 -2
  54. package/src/exa/types.ts +3 -3
  55. package/src/exec/bash-executor.ts +2 -1
  56. package/src/exec/non-interactive-env.ts +43 -0
  57. package/src/export/custom-share.ts +1 -1
  58. package/src/export/html/index.ts +1 -2
  59. package/src/extensibility/custom-commands/loader.ts +1 -2
  60. package/src/extensibility/plugins/installer.ts +1 -2
  61. package/src/extensibility/plugins/loader.ts +1 -2
  62. package/src/extensibility/plugins/manager.ts +3 -2
  63. package/src/extensibility/skills.ts +59 -115
  64. package/src/index.ts +1 -3
  65. package/src/internal-urls/docs-index.generated.ts +1 -1
  66. package/src/ipy/executor.ts +1 -2
  67. package/src/ipy/gateway-coordinator.ts +1 -2
  68. package/src/ipy/modules.ts +1 -1
  69. package/src/ipy/runtime.ts +1 -3
  70. package/src/main.ts +1 -2
  71. package/src/mcp/config.ts +1 -1
  72. package/src/mcp/transports/stdio.ts +1 -2
  73. package/src/memories/index.ts +1 -2
  74. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  75. package/src/modes/components/extensions/inspector-panel.ts +8 -2
  76. package/src/modes/components/footer.ts +1 -2
  77. package/src/modes/components/status-line/segments.ts +1 -2
  78. package/src/modes/components/tool-execution.ts +3 -10
  79. package/src/modes/components/welcome.ts +1 -1
  80. package/src/modes/controllers/command-controller.ts +1 -2
  81. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  82. package/src/modes/controllers/selector-controller.ts +1 -1
  83. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  84. package/src/modes/interactive-mode.ts +2 -3
  85. package/src/modes/shared.ts +1 -2
  86. package/src/modes/theme/theme.ts +1 -2
  87. package/src/patch/index.ts +1 -25
  88. package/src/prompts/agents/designer.md +7 -10
  89. package/src/prompts/agents/explore.md +15 -23
  90. package/src/prompts/agents/init.md +23 -23
  91. package/src/prompts/agents/plan.md +14 -77
  92. package/src/prompts/agents/reviewer.md +6 -5
  93. package/src/prompts/agents/task.md +13 -11
  94. package/src/prompts/compaction/branch-summary.md +3 -3
  95. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  96. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  97. package/src/prompts/compaction/compaction-summary.md +5 -5
  98. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  99. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  100. package/src/prompts/memories/consolidation.md +5 -5
  101. package/src/prompts/memories/read-path.md +6 -6
  102. package/src/prompts/memories/stage_one_input.md +1 -1
  103. package/src/prompts/memories/stage_one_system.md +5 -5
  104. package/src/prompts/review-request.md +4 -4
  105. package/src/prompts/system/agent-creation-architect.md +17 -17
  106. package/src/prompts/system/agent-creation-user.md +2 -2
  107. package/src/prompts/system/custom-system-prompt.md +4 -4
  108. package/src/prompts/system/plan-mode-active.md +20 -20
  109. package/src/prompts/system/plan-mode-approved.md +7 -7
  110. package/src/prompts/system/plan-mode-reference.md +2 -2
  111. package/src/prompts/system/plan-mode-subagent.md +8 -8
  112. package/src/prompts/system/subagent-submit-reminder.md +5 -5
  113. package/src/prompts/system/subagent-system-prompt.md +29 -22
  114. package/src/prompts/system/subagent-user-prompt.md +7 -3
  115. package/src/prompts/system/summarization-system.md +1 -1
  116. package/src/prompts/system/system-prompt.md +201 -226
  117. package/src/prompts/system/title-system.md +2 -2
  118. package/src/prompts/system/ttsr-interrupt.md +1 -1
  119. package/src/prompts/system/web-search.md +16 -16
  120. package/src/prompts/tools/ask.md +1 -3
  121. package/src/prompts/tools/await.md +2 -4
  122. package/src/prompts/tools/bash.md +5 -7
  123. package/src/prompts/tools/browser.md +4 -6
  124. package/src/prompts/tools/calculator.md +1 -3
  125. package/src/prompts/tools/cancel-job.md +2 -4
  126. package/src/prompts/tools/exit-plan-mode.md +7 -7
  127. package/src/prompts/tools/fetch.md +0 -2
  128. package/src/prompts/tools/find.md +3 -5
  129. package/src/prompts/tools/gemini-image.md +6 -22
  130. package/src/prompts/tools/grep.md +4 -6
  131. package/src/prompts/tools/hashline.md +12 -15
  132. package/src/prompts/tools/lsp.md +1 -3
  133. package/src/prompts/tools/patch.md +7 -9
  134. package/src/prompts/tools/python.md +10 -14
  135. package/src/prompts/tools/read.md +0 -2
  136. package/src/prompts/tools/replace.md +5 -7
  137. package/src/prompts/tools/ssh.md +3 -5
  138. package/src/prompts/tools/task.md +6 -8
  139. package/src/prompts/tools/todo-write.md +7 -9
  140. package/src/prompts/tools/web-search.md +3 -5
  141. package/src/prompts/tools/write.md +3 -5
  142. package/src/sdk.ts +1 -2
  143. package/src/session/agent-session.ts +10 -26
  144. package/src/session/agent-storage.ts +1 -2
  145. package/src/session/history-storage.ts +1 -2
  146. package/src/session/session-manager.ts +10 -2
  147. package/src/ssh/connection-manager.ts +11 -2
  148. package/src/ssh/sshfs-mount.ts +7 -1
  149. package/src/system-prompt.ts +25 -103
  150. package/src/task/agents.ts +1 -1
  151. package/src/task/worktree.ts +1 -2
  152. package/src/tools/ask.ts +0 -1
  153. package/src/tools/bash-interactive.ts +2 -45
  154. package/src/tools/bash.ts +5 -5
  155. package/src/tools/browser.ts +1 -2
  156. package/src/tools/gemini-image.ts +8 -28
  157. package/src/tools/json-tree.ts +2 -1
  158. package/src/tools/python.ts +1 -1
  159. package/src/tools/read.ts +1 -2
  160. package/src/utils/tools-manager.ts +1 -2
  161. package/src/web/scrapers/artifacthub.ts +2 -1
  162. package/src/web/scrapers/aur.ts +2 -1
  163. package/src/web/scrapers/biorxiv.ts +2 -1
  164. package/src/web/scrapers/bluesky.ts +2 -1
  165. package/src/web/scrapers/chocolatey.ts +2 -1
  166. package/src/web/scrapers/cisa-kev.ts +2 -1
  167. package/src/web/scrapers/clojars.ts +2 -1
  168. package/src/web/scrapers/coingecko.ts +2 -1
  169. package/src/web/scrapers/crates-io.ts +2 -1
  170. package/src/web/scrapers/crossref.ts +2 -1
  171. package/src/web/scrapers/discogs.ts +3 -1
  172. package/src/web/scrapers/discourse.ts +2 -1
  173. package/src/web/scrapers/dockerhub.ts +2 -1
  174. package/src/web/scrapers/fdroid.ts +2 -1
  175. package/src/web/scrapers/firefox-addons.ts +2 -1
  176. package/src/web/scrapers/flathub.ts +2 -1
  177. package/src/web/scrapers/gitlab.ts +1 -1
  178. package/src/web/scrapers/go-pkg.ts +2 -1
  179. package/src/web/scrapers/hackage.ts +2 -1
  180. package/src/web/scrapers/hackernews.ts +2 -1
  181. package/src/web/scrapers/hex.ts +2 -1
  182. package/src/web/scrapers/huggingface.ts +2 -1
  183. package/src/web/scrapers/jetbrains-marketplace.ts +2 -1
  184. package/src/web/scrapers/lemmy.ts +2 -1
  185. package/src/web/scrapers/lobsters.ts +2 -1
  186. package/src/web/scrapers/mastodon.ts +2 -1
  187. package/src/web/scrapers/maven.ts +2 -1
  188. package/src/web/scrapers/mdn.ts +2 -1
  189. package/src/web/scrapers/metacpan.ts +2 -1
  190. package/src/web/scrapers/musicbrainz.ts +3 -1
  191. package/src/web/scrapers/npm.ts +2 -1
  192. package/src/web/scrapers/nuget.ts +2 -1
  193. package/src/web/scrapers/nvd.ts +2 -1
  194. package/src/web/scrapers/ollama.ts +2 -1
  195. package/src/web/scrapers/open-vsx.ts +2 -1
  196. package/src/web/scrapers/opencorporates.ts +2 -1
  197. package/src/web/scrapers/openlibrary.ts +2 -1
  198. package/src/web/scrapers/orcid.ts +3 -1
  199. package/src/web/scrapers/osv.ts +2 -1
  200. package/src/web/scrapers/packagist.ts +2 -1
  201. package/src/web/scrapers/pub-dev.ts +2 -1
  202. package/src/web/scrapers/pubmed.ts +2 -1
  203. package/src/web/scrapers/pypi.ts +2 -1
  204. package/src/web/scrapers/rawg.ts +2 -8
  205. package/src/web/scrapers/reddit.ts +2 -1
  206. package/src/web/scrapers/repology.ts +2 -1
  207. package/src/web/scrapers/rfc.ts +2 -1
  208. package/src/web/scrapers/rubygems.ts +2 -1
  209. package/src/web/scrapers/searchcode.ts +2 -1
  210. package/src/web/scrapers/sec-edgar.ts +2 -1
  211. package/src/web/scrapers/semantic-scholar.ts +2 -1
  212. package/src/web/scrapers/snapcraft.ts +2 -1
  213. package/src/web/scrapers/sourcegraph.ts +2 -1
  214. package/src/web/scrapers/spdx.ts +2 -1
  215. package/src/web/scrapers/stackoverflow.ts +2 -1
  216. package/src/web/scrapers/terraform.ts +2 -1
  217. package/src/web/scrapers/types.ts +0 -11
  218. package/src/web/scrapers/vimeo.ts +2 -1
  219. package/src/web/scrapers/vscode-marketplace.ts +2 -1
  220. package/src/web/scrapers/w3c.ts +2 -1
  221. package/src/web/scrapers/wikidata.ts +2 -1
  222. package/src/web/search/index.ts +10 -14
  223. package/src/web/search/provider.ts +2 -2
  224. package/src/web/search/providers/codex.ts +1 -2
  225. package/src/web/search/providers/exa.ts +1 -6
  226. package/src/web/search/providers/gemini.ts +1 -1
  227. package/src/web/search/providers/perplexity.ts +1 -2
  228. package/src/web/search/providers/utils.ts +1 -1
@@ -31,12 +31,7 @@ export function createExaTool(
31
31
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
32
32
  try {
33
33
  const apiKey = await findApiKey();
34
- if (!apiKey) {
35
- return {
36
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
37
- details: { error: "EXA_API_KEY not found", toolName: name },
38
- };
39
- }
34
+ // Exa MCP endpoint is publicly accessible; API key is optional
40
35
  const args = transformParams ? transformParams(params as Record<string, unknown>) : params;
41
36
  const response = await callExaTool(mcpToolName, args, apiKey);
42
37
 
@@ -22,5 +22,5 @@ Parameters:
22
22
  Type.Object({
23
23
  query: Type.String({ description: "LinkedIn search query" }),
24
24
  }),
25
- "linkedin_search",
25
+ "linkedin_search_exa",
26
26
  );
@@ -17,8 +17,11 @@ export function findApiKey(): string | null {
17
17
  }
18
18
 
19
19
  /** Fetch available tools from Exa MCP */
20
- export async function fetchExaTools(apiKey: string, toolNames: string[]): Promise<MCPTool[]> {
21
- const url = `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(apiKey)}&toolNames=${encodeURIComponent(toolNames.join(","))}`;
20
+ export async function fetchExaTools(apiKey: string | null, toolNames: string[]): Promise<MCPTool[]> {
21
+ const params = new URLSearchParams();
22
+ if (apiKey) params.set("exaApiKey", apiKey);
23
+ params.set("toolNames", toolNames.join(","));
24
+ const url = `https://mcp.exa.ai/mcp?${params.toString()}`;
22
25
  const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
23
26
 
24
27
  if (response.error) {
@@ -43,8 +46,15 @@ export async function fetchWebsetsTools(apiKey: string): Promise<MCPTool[]> {
43
46
  }
44
47
 
45
48
  /** Call a tool on Exa MCP (simplified: toolName as first arg for easier use) */
46
- export async function callExaTool(toolName: string, args: Record<string, unknown>, apiKey: string): Promise<unknown> {
47
- const url = `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(apiKey)}&tools=${encodeURIComponent(toolName)}`;
49
+ export async function callExaTool(
50
+ toolName: string,
51
+ args: Record<string, unknown>,
52
+ apiKey: string | null,
53
+ ): Promise<unknown> {
54
+ const params = new URLSearchParams();
55
+ if (apiKey) params.set("exaApiKey", apiKey);
56
+ params.set("tools", toolName);
57
+ const url = `https://mcp.exa.ai/mcp?${params.toString()}`;
48
58
  const response = (await callMCP(url, "tools/call", {
49
59
  name: toolName,
50
60
  arguments: args,
@@ -174,15 +184,16 @@ export class MCPWrappedTool implements CustomTool<TSchema, ExaRenderDetails> {
174
184
  ): Promise<CustomToolResult<ExaRenderDetails>> {
175
185
  try {
176
186
  const apiKey = await findApiKey();
177
- if (!apiKey) {
187
+ // Websets tools require an API key; basic Exa MCP tools work without one
188
+ if (!apiKey && this.config.isWebsetsTool) {
178
189
  return {
179
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
180
- details: { error: "EXA_API_KEY not found", toolName: this.config.name },
190
+ content: [{ type: "text" as const, text: "Error: EXA_API_KEY required for Websets tools" }],
191
+ details: { error: "EXA_API_KEY required for Websets tools", toolName: this.config.name },
181
192
  };
182
193
  }
183
194
 
184
195
  const response = this.config.isWebsetsTool
185
- ? await callWebsetsTool(apiKey, this.config.mcpToolName, params as Record<string, unknown>)
196
+ ? await callWebsetsTool(apiKey!, this.config.mcpToolName, params as Record<string, unknown>)
186
197
  : await callExaTool(this.config.mcpToolName, params as Record<string, unknown>, apiKey);
187
198
 
188
199
  if (isSearchResponse(response)) {
package/src/exa/search.ts CHANGED
@@ -143,7 +143,7 @@ Similar parameters to exa_search, optimized for research depth.`,
143
143
  ),
144
144
  }),
145
145
  "web_search_exa",
146
- { transformParams: params => ({ ...params, type: "deep" }) },
146
+ { transformParams: params => ({ ...params, type: "auto" }) },
147
147
  );
148
148
 
149
149
  /** exa_search_code - Code-focused search */
@@ -195,7 +195,7 @@ Parameters:
195
195
  }),
196
196
  ),
197
197
  }),
198
- "crawling",
198
+ "crawling_exa",
199
199
  );
200
200
 
201
201
  export const searchTools: CustomTool<any, ExaRenderDetails>[] = [
package/src/exa/types.ts CHANGED
@@ -122,11 +122,11 @@ export const EXA_TOOL_MAPPINGS = {
122
122
  // Search tools
123
123
  web_search_exa: "exa_search",
124
124
  get_code_context_exa: "exa_search_code",
125
- crawling: "exa_crawl",
125
+ crawling_exa: "exa_crawl",
126
126
  // LinkedIn
127
- linkedin_search: "exa_linkedin",
127
+ linkedin_search_exa: "exa_linkedin",
128
128
  // Company
129
- company_research: "exa_company",
129
+ company_research_exa: "exa_company",
130
130
  // Researcher
131
131
  deep_researcher_start: "exa_researcher_start",
132
132
  deep_researcher_check: "exa_researcher_poll",
@@ -7,6 +7,7 @@ import { Shell } from "@oh-my-pi/pi-natives";
7
7
  import { Settings } from "../config/settings";
8
8
  import { OutputSink } from "../session/streaming-output";
9
9
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
10
+ import { NON_INTERACTIVE_ENV } from "./non-interactive-env";
10
11
 
11
12
  export interface BashExecutorOptions {
12
13
  cwd?: string;
@@ -97,7 +98,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
97
98
  {
98
99
  command: finalCommand,
99
100
  cwd: options?.cwd,
100
- env: options?.env,
101
+ env: options?.env ? { ...NON_INTERACTIVE_ENV, ...options.env } : NON_INTERACTIVE_ENV,
101
102
  timeoutMs: options?.timeout,
102
103
  signal,
103
104
  },
@@ -0,0 +1,43 @@
1
+ export const NON_INTERACTIVE_ENV: Readonly<Record<string, string>> = {
2
+ // Disable pagers so commands don't block on interactive views.
3
+ PAGER: "cat",
4
+ GIT_PAGER: "cat",
5
+ MANPAGER: "cat",
6
+ SYSTEMD_PAGER: "cat",
7
+ BAT_PAGER: "cat",
8
+ DELTA_PAGER: "cat",
9
+ GH_PAGER: "cat",
10
+ GLAB_PAGER: "cat",
11
+ PSQL_PAGER: "cat",
12
+ MYSQL_PAGER: "cat",
13
+ AWS_PAGER: "",
14
+ HOMEBREW_PAGER: "cat",
15
+ LESS: "FRX",
16
+ // Disable editor and terminal credential prompts.
17
+ GIT_EDITOR: "true",
18
+ VISUAL: "true",
19
+ EDITOR: "true",
20
+ GIT_TERMINAL_PROMPT: "0",
21
+ SSH_ASKPASS: "/usr/bin/false",
22
+ CI: "1",
23
+ // Package manager defaults for unattended execution.
24
+ npm_config_yes: "true",
25
+ npm_config_update_notifier: "false",
26
+ npm_config_fund: "false",
27
+ npm_config_audit: "false",
28
+ npm_config_progress: "false",
29
+ PNPM_DISABLE_SELF_UPDATE_CHECK: "true",
30
+ PNPM_UPDATE_NOTIFIER: "false",
31
+ YARN_ENABLE_TELEMETRY: "0",
32
+ YARN_ENABLE_PROGRESS_BARS: "0",
33
+ // Cross-language/tooling non-interactive defaults.
34
+ CARGO_TERM_PROGRESS_WHEN: "never",
35
+ DEBIAN_FRONTEND: "noninteractive",
36
+ PIP_NO_INPUT: "1",
37
+ PIP_DISABLE_PIP_VERSION_CHECK: "1",
38
+ TF_INPUT: "0",
39
+ TF_IN_AUTOMATION: "1",
40
+ GH_PROMPT_DISABLED: "1",
41
+ COMPOSER_NO_INTERACTION: "1",
42
+ CLOUDSDK_CORE_DISABLE_PROMPTS: "1",
43
+ };
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
- import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
9
+ import { getAgentDir } from "@oh-my-pi/pi-utils";
10
10
 
11
11
  export interface CustomShareResult {
12
12
  /** URL to display/open (optional - script may handle everything itself) */
@@ -1,7 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import type { AgentState } from "@oh-my-pi/pi-agent-core";
3
- import { isEnoent } from "@oh-my-pi/pi-utils";
4
- import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
3
+ import { APP_NAME, isEnoent } from "@oh-my-pi/pi-utils";
5
4
  import { getResolvedThemeColors, getThemeExportColors } from "../../modes/theme/theme";
6
5
  import { type SessionEntry, type SessionHeader, SessionManager } from "../../session/session-manager";
7
6
  // Pre-generated template (created by scripts/generate-template.ts at publish time)
@@ -7,8 +7,7 @@
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
10
- import { isEnoent, logger } from "@oh-my-pi/pi-utils";
11
- import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
10
+ import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
12
11
  import * as typebox from "@sinclair/typebox";
13
12
  import { getConfigDirs } from "../../config";
14
13
  import { execCommand } from "../../exec/exec";
@@ -1,7 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { isEnoent } from "@oh-my-pi/pi-utils";
4
- import { getAgentDir, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
3
+ import { getAgentDir, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
5
4
  import { extractPackageName } from "./parser";
6
5
  import type { InstalledPlugin } from "./types";
7
6
 
@@ -6,8 +6,7 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
- import { isEnoent } from "@oh-my-pi/pi-utils";
10
- import { getPluginsLockfile, getPluginsNodeModules, getPluginsPackageJson } from "@oh-my-pi/pi-utils/dirs";
9
+ import { getPluginsLockfile, getPluginsNodeModules, getPluginsPackageJson, isEnoent } from "@oh-my-pi/pi-utils";
11
10
  import { getConfigDirPaths } from "../../config";
12
11
  import type { InstalledPlugin, PluginManifest, PluginRuntimeConfig, ProjectPluginOverrides } from "./types";
13
12
 
@@ -1,6 +1,5 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { isEnoent, logger } from "@oh-my-pi/pi-utils";
4
3
  import {
5
4
  getPluginsDir,
6
5
  getPluginsLockfile,
@@ -8,7 +7,9 @@ import {
8
7
  getPluginsPackageJson,
9
8
  getProjectDir,
10
9
  getProjectPluginOverridesPath,
11
- } from "@oh-my-pi/pi-utils/dirs";
10
+ isEnoent,
11
+ logger,
12
+ } from "@oh-my-pi/pi-utils";
12
13
  import { extractPackageName, parsePluginSpec } from "./parser";
13
14
  import type {
14
15
  DoctorCheck,
@@ -1,18 +1,12 @@
1
1
  import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import { logger } from "@oh-my-pi/pi-utils";
4
- import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
2
+ import * as os from "node:os";
3
+ import { getProjectDir } from "@oh-my-pi/pi-utils";
5
4
  import { skillCapability } from "../capability/skill";
6
5
  import type { SourceMeta } from "../capability/types";
7
6
  import type { SkillsSettings } from "../config/settings";
8
- import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
9
- import { loadCapability } from "../discovery";
7
+ import { type Skill as CapabilitySkill, loadCapability } from "../discovery";
8
+ import { scanSkillsFromDir } from "../discovery/helpers";
10
9
  import { expandTilde } from "../tools/path-utils";
11
- import { parseFrontmatter } from "../utils/frontmatter";
12
- import { addIgnoreRules, createIgnoreMatcher, type IgnoreMatcher, shouldIgnore } from "../utils/ignore-files";
13
-
14
- // Re-export SkillFrontmatter for backward compatibility
15
- export type { ImportedSkillFrontmatter as SkillFrontmatter };
16
10
 
17
11
  export interface Skill {
18
12
  name: string;
@@ -41,91 +35,31 @@ export interface LoadSkillsFromDirOptions {
41
35
  source: string;
42
36
  }
43
37
 
44
- async function readFileContent(filePath: string): Promise<string | null> {
45
- try {
46
- return await fs.readFile(filePath, "utf-8");
47
- } catch {
48
- return null;
49
- }
50
- }
51
-
52
- /**
53
- * Load skills from a directory recursively.
54
- * Skills are directories containing a SKILL.md file with frontmatter including a description.
55
- * Respects .gitignore, .ignore, and .fdignore files.
56
- */
57
38
  export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Promise<LoadSkillsResult> {
58
- const skills: Skill[] = [];
59
- const warnings: SkillWarning[] = [];
60
- const seenPaths = new Set<string>();
61
- const rootDir = options.dir;
62
-
63
- async function addSkill(skillFile: string, skillDir: string, dirName: string): Promise<void> {
64
- if (seenPaths.has(skillFile)) return;
65
- try {
66
- const content = await fs.readFile(skillFile, "utf-8");
67
- const { frontmatter } = parseFrontmatter(content, { source: skillFile });
68
- const name = (frontmatter.name as string) || dirName;
69
- const description = frontmatter.description as string;
70
-
71
- if (description) {
72
- seenPaths.add(skillFile);
73
- skills.push({
74
- name,
75
- description,
76
- filePath: skillFile,
77
- baseDir: skillDir,
78
- source: options.source,
79
- });
80
- }
81
- } catch (error) {
82
- logger.warn("Failed to load skill", { path: skillFile, error: String(error) });
83
- }
84
- }
85
-
86
- async function scanDir(dir: string, ig: IgnoreMatcher): Promise<void> {
87
- try {
88
- // Add ignore rules from this directory
89
- await addIgnoreRules(ig, dir, rootDir, readFileContent);
90
-
91
- // First check if this directory itself is a skill
92
- const selfSkillFile = path.join(dir, "SKILL.md");
93
- try {
94
- const s = await fs.stat(selfSkillFile);
95
- if (s.isFile()) {
96
- await addSkill(selfSkillFile, dir, path.basename(dir));
97
- // This directory is a skill, don't recurse
98
- return;
99
- }
100
- } catch {
101
- // No SKILL.md in this directory
102
- }
103
-
104
- // Recurse into subdirectories
105
- const entries = await fs.readdir(dir, { withFileTypes: true });
106
-
107
- for (const entry of entries) {
108
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
109
-
110
- const fullPath = path.join(dir, entry.name);
111
- const isDir = entry.isDirectory();
112
-
113
- // Check if this entry should be ignored
114
- if (shouldIgnore(ig, rootDir, fullPath, isDir)) continue;
115
-
116
- if (isDir) {
117
- await scanDir(fullPath, ig);
118
- }
119
- }
120
- } catch (err) {
121
- warnings.push({ skillPath: dir, message: `Failed to read directory: ${err}` });
122
- }
123
- }
124
-
125
- const ig = createIgnoreMatcher();
126
- await scanDir(options.dir, ig);
39
+ const [rawProviderId, rawLevel] = options.source.split(":", 2);
40
+ const providerId = rawProviderId || "custom";
41
+ const level: "user" | "project" = rawLevel === "project" ? "project" : "user";
42
+ const result = await scanSkillsFromDir(
43
+ { cwd: getProjectDir(), home: os.homedir() },
44
+ {
45
+ dir: options.dir,
46
+ providerId,
47
+ level,
48
+ requireDescription: true,
49
+ },
50
+ );
127
51
 
128
- return { skills, warnings };
52
+ return {
53
+ skills: result.items.map(capSkill => ({
54
+ name: capSkill.name,
55
+ description: typeof capSkill.frontmatter?.description === "string" ? capSkill.frontmatter.description : "",
56
+ filePath: capSkill.path,
57
+ baseDir: capSkill.path.replace(/\/SKILL\.md$/, ""),
58
+ source: options.source,
59
+ _source: capSkill._source,
60
+ })),
61
+ warnings: (result.warnings ?? []).map(message => ({ skillPath: options.dir, message })),
62
+ };
129
63
  }
130
64
 
131
65
  export interface LoadSkillsOptions extends SkillsSettings {
@@ -225,45 +159,55 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
225
159
  message: `name collision: "${capSkill.name}" already loaded from ${existing.filePath}, skipping this one`,
226
160
  });
227
161
  } else {
228
- // Transform capability skill to legacy format
229
- const skill: Skill = {
162
+ skillMap.set(capSkill.name, {
230
163
  name: capSkill.name,
231
- description: capSkill.frontmatter?.description || "",
164
+ description: typeof capSkill.frontmatter?.description === "string" ? capSkill.frontmatter.description : "",
232
165
  filePath: capSkill.path,
233
166
  baseDir: capSkill.path.replace(/\/SKILL\.md$/, ""),
234
167
  source: `${capSkill._source.provider}:${capSkill.level}`,
235
168
  _source: capSkill._source,
236
- };
237
- skillMap.set(capSkill.name, skill);
169
+ });
238
170
  realPathSet.add(resolvedPath);
239
171
  }
240
172
  }
241
173
 
242
- // Process custom directories - scan directly without using full provider system
243
- const allCustomSkills: Array<{ skill: Skill; path: string }> = [];
244
- const customScanResults = await Promise.all(
245
- customDirectories.map(dir => loadSkillsFromDir({ dir: expandTilde(dir), source: "custom" })),
174
+ const customDirectoryResults = await Promise.all(
175
+ customDirectories.map(async dir => {
176
+ const expandedDir = expandTilde(dir);
177
+ const scanResult = await scanSkillsFromDir(
178
+ { cwd, home: os.homedir() },
179
+ {
180
+ dir: expandedDir,
181
+ providerId: "custom",
182
+ level: "user",
183
+ requireDescription: true,
184
+ },
185
+ );
186
+ return { expandedDir, scanResult };
187
+ }),
246
188
  );
247
- for (const customSkills of customScanResults) {
248
- for (const s of customSkills.skills) {
249
- if (matchesIgnorePatterns(s.name)) continue;
250
- if (!matchesIncludePatterns(s.name)) continue;
189
+
190
+ const allCustomSkills: Array<{ skill: Skill; path: string }> = [];
191
+ for (const { expandedDir, scanResult } of customDirectoryResults) {
192
+ for (const capSkill of scanResult.items) {
193
+ if (matchesIgnorePatterns(capSkill.name)) continue;
194
+ if (!matchesIncludePatterns(capSkill.name)) continue;
251
195
  allCustomSkills.push({
252
196
  skill: {
253
- name: s.name,
254
- description: s.description,
255
- filePath: s.filePath,
256
- baseDir: s.filePath.replace(/\/SKILL\.md$/, ""),
197
+ name: capSkill.name,
198
+ description:
199
+ typeof capSkill.frontmatter?.description === "string" ? capSkill.frontmatter.description : "",
200
+ filePath: capSkill.path,
201
+ baseDir: capSkill.path.replace(/\/SKILL\.md$/, ""),
257
202
  source: "custom:user",
258
- _source: { provider: "custom", providerName: "Custom", path: s.filePath, level: "user" },
203
+ _source: { ...capSkill._source, providerName: "Custom" },
259
204
  },
260
- path: s.filePath,
205
+ path: capSkill.path,
261
206
  });
262
207
  }
263
- collisionWarnings.push(...customSkills.warnings);
208
+ collisionWarnings.push(...(scanResult.warnings ?? []).map(message => ({ skillPath: expandedDir, message })));
264
209
  }
265
210
 
266
- // Batch resolve custom skill paths
267
211
  const customRealPaths = await Promise.all(
268
212
  allCustomSkills.map(async ({ path }) => {
269
213
  try {
@@ -293,6 +237,6 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
293
237
 
294
238
  return {
295
239
  skills: Array.from(skillMap.values()),
296
- warnings: [...result.warnings.map(w => ({ skillPath: "", message: w })), ...collisionWarnings],
240
+ warnings: [...(result.warnings ?? []).map(w => ({ skillPath: "", message: w })), ...collisionWarnings],
297
241
  };
298
242
  }
package/src/index.ts CHANGED
@@ -6,8 +6,7 @@ export { StringEnum } from "@oh-my-pi/pi-ai";
6
6
  // Re-export TUI components for custom tool rendering
7
7
  export { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
8
8
  // Logging
9
- export { logger } from "@oh-my-pi/pi-utils";
10
- export { getAgentDir, VERSION } from "@oh-my-pi/pi-utils/dirs";
9
+ export { getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
11
10
  export { formatKeyHint, formatKeyHints } from "./config/keybindings";
12
11
  export { ModelRegistry } from "./config/model-registry";
13
12
  // Prompt templates
@@ -89,7 +88,6 @@ export {
89
88
  loadSkills,
90
89
  loadSkillsFromDir,
91
90
  type Skill,
92
- type SkillFrontmatter,
93
91
  type SkillWarning,
94
92
  } from "./extensibility/skills";
95
93
  // Slash commands