@oh-my-pi/pi-coding-agent 13.2.0 → 13.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 (243) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +33 -14
  4. package/scripts/generate-docs-index.ts +2 -2
  5. package/src/capability/index.ts +1 -2
  6. package/src/cli/args.ts +3 -3
  7. package/src/cli/config-cli.ts +1 -1
  8. package/src/cli/file-processor.ts +1 -2
  9. package/src/cli/grep-cli.ts +1 -1
  10. package/src/cli/jupyter-cli.ts +1 -1
  11. package/src/cli/plugin-cli.ts +1 -1
  12. package/src/cli/setup-cli.ts +1 -1
  13. package/src/cli/shell-cli.ts +1 -1
  14. package/src/cli/ssh-cli.ts +1 -1
  15. package/src/cli/stats-cli.ts +1 -2
  16. package/src/cli/update-cli.ts +1 -2
  17. package/src/cli/web-search-cli.ts +1 -1
  18. package/src/cli.ts +1 -1
  19. package/src/commands/launch.ts +2 -1
  20. package/src/commit/agentic/agent.ts +2 -1
  21. package/src/commit/agentic/index.ts +1 -2
  22. package/src/commit/agentic/prompts/system.md +3 -3
  23. package/src/commit/agentic/tools/propose-changelog.ts +30 -19
  24. package/src/commit/changelog/generate.ts +16 -6
  25. package/src/commit/changelog/index.ts +2 -1
  26. package/src/commit/pipeline.ts +1 -2
  27. package/src/commit/prompts/reduce-system.md +1 -1
  28. package/src/commit/types.ts +10 -1
  29. package/src/config/keybindings.ts +1 -2
  30. package/src/config/model-registry.ts +1 -1
  31. package/src/config/prompt-templates.ts +14 -2
  32. package/src/config/settings-schema.ts +36 -4
  33. package/src/config/settings.ts +19 -2
  34. package/src/config.ts +1 -2
  35. package/src/debug/index.ts +1 -1
  36. package/src/debug/report-bundle.ts +1 -2
  37. package/src/debug/system-info.ts +1 -2
  38. package/src/discovery/agents.ts +2 -2
  39. package/src/discovery/builtin.ts +8 -9
  40. package/src/discovery/claude-plugins.ts +2 -2
  41. package/src/discovery/claude.ts +30 -12
  42. package/src/discovery/codex.ts +3 -3
  43. package/src/discovery/cursor.ts +5 -4
  44. package/src/discovery/gemini.ts +5 -5
  45. package/src/discovery/helpers.ts +47 -69
  46. package/src/discovery/mcp-json.ts +3 -3
  47. package/src/discovery/opencode.ts +7 -8
  48. package/src/discovery/ssh.ts +3 -3
  49. package/src/discovery/vscode.ts +3 -2
  50. package/src/discovery/windsurf.ts +3 -2
  51. package/src/exa/company.ts +1 -1
  52. package/src/exa/factory.ts +1 -6
  53. package/src/exa/linkedin.ts +1 -1
  54. package/src/exa/mcp-client.ts +19 -8
  55. package/src/exa/search.ts +2 -2
  56. package/src/exa/types.ts +3 -3
  57. package/src/exec/bash-executor.ts +2 -1
  58. package/src/exec/non-interactive-env.ts +43 -0
  59. package/src/export/custom-share.ts +1 -1
  60. package/src/export/html/index.ts +1 -2
  61. package/src/extensibility/custom-commands/loader.ts +1 -2
  62. package/src/extensibility/plugins/installer.ts +1 -2
  63. package/src/extensibility/plugins/loader.ts +1 -2
  64. package/src/extensibility/plugins/manager.ts +3 -2
  65. package/src/extensibility/skills.ts +59 -115
  66. package/src/index.ts +1 -3
  67. package/src/internal-urls/docs-index.generated.ts +1 -1
  68. package/src/ipy/executor.ts +1 -2
  69. package/src/ipy/gateway-coordinator.ts +1 -2
  70. package/src/ipy/modules.ts +1 -1
  71. package/src/ipy/runtime.ts +2 -3
  72. package/src/main.ts +1 -2
  73. package/src/mcp/config.ts +2 -2
  74. package/src/mcp/transports/stdio.ts +1 -2
  75. package/src/memories/index.ts +1 -2
  76. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  77. package/src/modes/components/extensions/inspector-panel.ts +8 -2
  78. package/src/modes/components/footer.ts +1 -2
  79. package/src/modes/components/settings-defs.ts +17 -1
  80. package/src/modes/components/status-line/segments.ts +1 -2
  81. package/src/modes/components/status-line.ts +7 -5
  82. package/src/modes/components/tool-execution.ts +3 -10
  83. package/src/modes/components/welcome.ts +1 -1
  84. package/src/modes/controllers/command-controller.ts +1 -2
  85. package/src/modes/controllers/mcp-command-controller.ts +5 -4
  86. package/src/modes/controllers/selector-controller.ts +22 -1
  87. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  88. package/src/modes/interactive-mode.ts +11 -3
  89. package/src/modes/oauth-manual-input.ts +42 -0
  90. package/src/modes/shared.ts +1 -2
  91. package/src/modes/theme/theme.ts +1 -2
  92. package/src/modes/types.ts +2 -0
  93. package/src/patch/hashline.ts +19 -1
  94. package/src/patch/index.ts +1 -25
  95. package/src/prompts/agents/designer.md +7 -10
  96. package/src/prompts/agents/explore.md +15 -23
  97. package/src/prompts/agents/init.md +23 -23
  98. package/src/prompts/agents/plan.md +14 -77
  99. package/src/prompts/agents/reviewer.md +6 -5
  100. package/src/prompts/agents/task.md +13 -11
  101. package/src/prompts/compaction/branch-summary.md +3 -3
  102. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  103. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  104. package/src/prompts/compaction/compaction-summary.md +5 -5
  105. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  106. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  107. package/src/prompts/memories/consolidation.md +5 -5
  108. package/src/prompts/memories/read-path.md +6 -6
  109. package/src/prompts/memories/stage_one_input.md +1 -1
  110. package/src/prompts/memories/stage_one_system.md +5 -5
  111. package/src/prompts/review-request.md +4 -4
  112. package/src/prompts/system/agent-creation-architect.md +17 -17
  113. package/src/prompts/system/agent-creation-user.md +2 -2
  114. package/src/prompts/system/commit-message-system.md +2 -0
  115. package/src/prompts/system/custom-system-prompt.md +4 -4
  116. package/src/prompts/system/plan-mode-active.md +20 -20
  117. package/src/prompts/system/plan-mode-approved.md +7 -7
  118. package/src/prompts/system/plan-mode-reference.md +2 -2
  119. package/src/prompts/system/plan-mode-subagent.md +8 -8
  120. package/src/prompts/system/subagent-submit-reminder.md +5 -5
  121. package/src/prompts/system/subagent-system-prompt.md +29 -22
  122. package/src/prompts/system/subagent-user-prompt.md +7 -3
  123. package/src/prompts/system/summarization-system.md +1 -1
  124. package/src/prompts/system/system-prompt.md +214 -226
  125. package/src/prompts/system/title-system.md +2 -2
  126. package/src/prompts/system/ttsr-interrupt.md +1 -1
  127. package/src/prompts/system/web-search.md +16 -16
  128. package/src/prompts/tools/ask.md +1 -3
  129. package/src/prompts/tools/await.md +2 -4
  130. package/src/prompts/tools/bash.md +5 -7
  131. package/src/prompts/tools/browser.md +4 -6
  132. package/src/prompts/tools/calculator.md +1 -3
  133. package/src/prompts/tools/cancel-job.md +2 -4
  134. package/src/prompts/tools/exit-plan-mode.md +7 -7
  135. package/src/prompts/tools/fetch.md +0 -2
  136. package/src/prompts/tools/find.md +3 -5
  137. package/src/prompts/tools/gemini-image.md +6 -22
  138. package/src/prompts/tools/grep.md +4 -6
  139. package/src/prompts/tools/hashline.md +56 -15
  140. package/src/prompts/tools/lsp.md +1 -3
  141. package/src/prompts/tools/patch.md +7 -9
  142. package/src/prompts/tools/python.md +10 -14
  143. package/src/prompts/tools/read.md +0 -2
  144. package/src/prompts/tools/replace.md +5 -7
  145. package/src/prompts/tools/ssh.md +3 -5
  146. package/src/prompts/tools/task-summary.md +4 -4
  147. package/src/prompts/tools/task.md +7 -9
  148. package/src/prompts/tools/todo-write.md +7 -9
  149. package/src/prompts/tools/web-search.md +3 -5
  150. package/src/prompts/tools/write.md +3 -5
  151. package/src/sdk.ts +4 -2
  152. package/src/session/agent-session.ts +10 -26
  153. package/src/session/agent-storage.ts +1 -2
  154. package/src/session/history-storage.ts +1 -2
  155. package/src/session/session-manager.ts +10 -2
  156. package/src/slash-commands/builtin-registry.ts +26 -1
  157. package/src/ssh/connection-manager.ts +11 -2
  158. package/src/ssh/sshfs-mount.ts +7 -1
  159. package/src/system-prompt.ts +29 -103
  160. package/src/task/agents.ts +1 -1
  161. package/src/task/index.ts +211 -70
  162. package/src/task/render.ts +24 -8
  163. package/src/task/types.ts +6 -1
  164. package/src/task/worktree.ts +394 -32
  165. package/src/tools/ask.ts +0 -1
  166. package/src/tools/bash-interactive.ts +2 -45
  167. package/src/tools/bash.ts +5 -5
  168. package/src/tools/browser.ts +1 -2
  169. package/src/tools/gemini-image.ts +8 -28
  170. package/src/tools/json-tree.ts +2 -1
  171. package/src/tools/python.ts +1 -1
  172. package/src/tools/read.ts +1 -2
  173. package/src/tools/submit-result.ts +22 -23
  174. package/src/utils/commit-message-generator.ts +132 -0
  175. package/src/utils/tools-manager.ts +1 -2
  176. package/src/web/scrapers/artifacthub.ts +2 -1
  177. package/src/web/scrapers/aur.ts +2 -1
  178. package/src/web/scrapers/biorxiv.ts +2 -1
  179. package/src/web/scrapers/bluesky.ts +2 -1
  180. package/src/web/scrapers/chocolatey.ts +2 -1
  181. package/src/web/scrapers/cisa-kev.ts +2 -1
  182. package/src/web/scrapers/clojars.ts +2 -1
  183. package/src/web/scrapers/coingecko.ts +2 -1
  184. package/src/web/scrapers/crates-io.ts +2 -1
  185. package/src/web/scrapers/crossref.ts +2 -1
  186. package/src/web/scrapers/discogs.ts +3 -1
  187. package/src/web/scrapers/discourse.ts +2 -1
  188. package/src/web/scrapers/dockerhub.ts +2 -1
  189. package/src/web/scrapers/fdroid.ts +2 -1
  190. package/src/web/scrapers/firefox-addons.ts +2 -1
  191. package/src/web/scrapers/flathub.ts +2 -1
  192. package/src/web/scrapers/gitlab.ts +1 -1
  193. package/src/web/scrapers/go-pkg.ts +2 -1
  194. package/src/web/scrapers/hackage.ts +2 -1
  195. package/src/web/scrapers/hackernews.ts +2 -1
  196. package/src/web/scrapers/hex.ts +2 -1
  197. package/src/web/scrapers/huggingface.ts +2 -1
  198. package/src/web/scrapers/jetbrains-marketplace.ts +2 -1
  199. package/src/web/scrapers/lemmy.ts +2 -1
  200. package/src/web/scrapers/lobsters.ts +2 -1
  201. package/src/web/scrapers/mastodon.ts +2 -1
  202. package/src/web/scrapers/maven.ts +2 -1
  203. package/src/web/scrapers/mdn.ts +2 -1
  204. package/src/web/scrapers/metacpan.ts +2 -1
  205. package/src/web/scrapers/musicbrainz.ts +3 -1
  206. package/src/web/scrapers/npm.ts +2 -1
  207. package/src/web/scrapers/nuget.ts +2 -1
  208. package/src/web/scrapers/nvd.ts +2 -1
  209. package/src/web/scrapers/ollama.ts +2 -1
  210. package/src/web/scrapers/open-vsx.ts +2 -1
  211. package/src/web/scrapers/opencorporates.ts +2 -1
  212. package/src/web/scrapers/openlibrary.ts +2 -1
  213. package/src/web/scrapers/orcid.ts +3 -1
  214. package/src/web/scrapers/osv.ts +2 -1
  215. package/src/web/scrapers/packagist.ts +2 -1
  216. package/src/web/scrapers/pub-dev.ts +2 -1
  217. package/src/web/scrapers/pubmed.ts +2 -1
  218. package/src/web/scrapers/pypi.ts +2 -1
  219. package/src/web/scrapers/rawg.ts +2 -8
  220. package/src/web/scrapers/reddit.ts +2 -1
  221. package/src/web/scrapers/repology.ts +2 -1
  222. package/src/web/scrapers/rfc.ts +2 -1
  223. package/src/web/scrapers/rubygems.ts +2 -1
  224. package/src/web/scrapers/searchcode.ts +2 -1
  225. package/src/web/scrapers/sec-edgar.ts +2 -1
  226. package/src/web/scrapers/semantic-scholar.ts +2 -1
  227. package/src/web/scrapers/snapcraft.ts +2 -1
  228. package/src/web/scrapers/sourcegraph.ts +2 -1
  229. package/src/web/scrapers/spdx.ts +2 -1
  230. package/src/web/scrapers/stackoverflow.ts +2 -1
  231. package/src/web/scrapers/terraform.ts +2 -1
  232. package/src/web/scrapers/types.ts +0 -11
  233. package/src/web/scrapers/vimeo.ts +2 -1
  234. package/src/web/scrapers/vscode-marketplace.ts +2 -1
  235. package/src/web/scrapers/w3c.ts +2 -1
  236. package/src/web/scrapers/wikidata.ts +2 -1
  237. package/src/web/search/index.ts +10 -14
  238. package/src/web/search/provider.ts +2 -2
  239. package/src/web/search/providers/codex.ts +1 -2
  240. package/src/web/search/providers/exa.ts +42 -10
  241. package/src/web/search/providers/gemini.ts +1 -1
  242. package/src/web/search/providers/perplexity.ts +20 -9
  243. package/src/web/search/providers/utils.ts +1 -1
@@ -7,12 +7,12 @@
7
7
  * Priority: 5 (low, as this is a fallback after tool-specific providers)
8
8
  */
9
9
  import * as path from "node:path";
10
- import { logger } from "@oh-my-pi/pi-utils";
10
+ import { logger, tryParseJson } from "@oh-my-pi/pi-utils";
11
11
  import { registerProvider } from "../capability";
12
12
  import { readFile } from "../capability/fs";
13
13
  import { type MCPServer, mcpCapability } from "../capability/mcp";
14
14
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
15
- import { createSourceMeta, expandEnvVarsDeep, parseJSON } from "./helpers";
15
+ import { createSourceMeta, expandEnvVarsDeep } from "./helpers";
16
16
 
17
17
  const PROVIDER_ID = "mcp-json";
18
18
  const DISPLAY_NAME = "MCP Config";
@@ -115,7 +115,7 @@ async function loadMCPJsonFile(
115
115
  return { items, warnings };
116
116
  }
117
117
 
118
- const config = parseJSON<MCPConfigFile>(content);
118
+ const config = tryParseJson<MCPConfigFile>(content);
119
119
  if (!config) {
120
120
  warnings.push(`Failed to parse JSON in ${path}`);
121
121
  return { items, warnings };
@@ -16,7 +16,7 @@
16
16
  * Priority: 55 (tool-specific provider)
17
17
  */
18
18
  import * as path from "node:path";
19
- import { logger } from "@oh-my-pi/pi-utils";
19
+ import { logger, tryParseJson } from "@oh-my-pi/pi-utils";
20
20
  import { registerProvider } from "../capability";
21
21
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
22
22
  import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
@@ -35,8 +35,7 @@ import {
35
35
  getProjectPath,
36
36
  getUserPath,
37
37
  loadFilesFromDir,
38
- loadSkillsFromDir,
39
- parseJSON,
38
+ scanSkillsFromDir,
40
39
  } from "./helpers";
41
40
 
42
41
  const PROVIDER_ID = "opencode";
@@ -51,7 +50,7 @@ async function loadJsonConfig(configPath: string): Promise<Record<string, unknow
51
50
  const content = await readFile(configPath);
52
51
  if (!content) return null;
53
52
 
54
- const parsed = parseJSON<Record<string, unknown>>(content);
53
+ const parsed = tryParseJson<Record<string, unknown>>(content);
55
54
  if (!parsed) {
56
55
  logger.warn("Failed to parse OpenCode JSON config", { path: configPath });
57
56
  return null;
@@ -190,7 +189,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
190
189
 
191
190
  if (userSkillsDir) {
192
191
  promises.push(
193
- loadSkillsFromDir(ctx, {
192
+ scanSkillsFromDir(ctx, {
194
193
  dir: userSkillsDir,
195
194
  providerId: PROVIDER_ID,
196
195
  level: "user",
@@ -200,7 +199,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
200
199
 
201
200
  if (projectSkillsDir) {
202
201
  promises.push(
203
- loadSkillsFromDir(ctx, {
202
+ scanSkillsFromDir(ctx, {
204
203
  dir: projectSkillsDir,
205
204
  providerId: PROVIDER_ID,
206
205
  level: "project",
@@ -307,7 +306,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
307
306
  if (userConfigPath) {
308
307
  const content = await readFile(userConfigPath);
309
308
  if (content) {
310
- const parsed = parseJSON<Record<string, unknown>>(content);
309
+ const parsed = tryParseJson<Record<string, unknown>>(content);
311
310
  if (parsed) {
312
311
  items.push({
313
312
  path: userConfigPath,
@@ -325,7 +324,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
325
324
  const projectConfigPath = path.join(ctx.cwd, "opencode.json");
326
325
  const content = await readFile(projectConfigPath);
327
326
  if (content) {
328
- const parsed = parseJSON<Record<string, unknown>>(content);
327
+ const parsed = tryParseJson<Record<string, unknown>>(content);
329
328
  if (parsed) {
330
329
  items.push({
331
330
  path: projectConfigPath,
@@ -5,13 +5,13 @@
5
5
  * Priority: 5 (low, project/user config discovery)
6
6
  */
7
7
  import * as path from "node:path";
8
- import { getSSHConfigPath } from "@oh-my-pi/pi-utils/dirs";
8
+ import { getSSHConfigPath, tryParseJson } from "@oh-my-pi/pi-utils";
9
9
  import { registerProvider } from "../capability";
10
10
  import { readFile } from "../capability/fs";
11
11
  import { type SSHHost, sshCapability } from "../capability/ssh";
12
12
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
13
13
  import { expandTilde } from "../tools/path-utils";
14
- import { createSourceMeta, expandEnvVarsDeep, parseJSON } from "./helpers";
14
+ import { createSourceMeta, expandEnvVarsDeep } from "./helpers";
15
15
 
16
16
  const PROVIDER_ID = "ssh-json";
17
17
  const DISPLAY_NAME = "SSH Config";
@@ -95,7 +95,7 @@ async function loadSshJsonFile(
95
95
  if (content === null) {
96
96
  return { items, warnings };
97
97
  }
98
- const parsed = parseJSON<SSHConfigFile>(content);
98
+ const parsed = tryParseJson<SSHConfigFile>(content);
99
99
  if (!parsed) {
100
100
  warnings.push(`Failed to parse JSON in ${filePath}`);
101
101
  return { items, warnings };
@@ -4,11 +4,12 @@
4
4
  * Loads config from `.vscode` directory (project-only).
5
5
  * Supports MCP server discovery from `mcp.json` with nested `mcp.servers` structure.
6
6
  */
7
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
7
8
  import { registerProvider } from "../capability";
8
9
  import { readFile } from "../capability/fs";
9
10
  import { type MCPServer, mcpCapability } from "../capability/mcp";
10
11
  import type { LoadContext, LoadResult } from "../capability/types";
11
- import { createSourceMeta, expandEnvVarsDeep, getProjectPath, parseJSON } from "./helpers";
12
+ import { createSourceMeta, expandEnvVarsDeep, getProjectPath } from "./helpers";
12
13
 
13
14
  const PROVIDER_ID = "vscode";
14
15
  const DISPLAY_NAME = "VS Code";
@@ -57,7 +58,7 @@ async function loadMCPConfig(
57
58
  return { items, warnings };
58
59
  }
59
60
 
60
- const parsed = parseJSON<{ mcp?: { servers?: Record<string, unknown> } }>(content);
61
+ const parsed = tryParseJson<{ mcp?: { servers?: Record<string, unknown> } }>(content);
61
62
  if (!parsed) {
62
63
  warnings.push(`Invalid JSON in ${path}`);
63
64
  return { items, warnings };
@@ -10,6 +10,8 @@
10
10
  * - Rules from .windsurf/rules/*.md and ~/.codeium/windsurf/memories/global_rules.md
11
11
  * - Legacy .windsurfrules file
12
12
  */
13
+
14
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
13
15
  import { registerProvider } from "../capability";
14
16
  import { readFile } from "../capability/fs";
15
17
  import { type MCPServer, mcpCapability } from "../capability/mcp";
@@ -22,7 +24,6 @@ import {
22
24
  getProjectPath,
23
25
  getUserPath,
24
26
  loadFilesFromDir,
25
- parseJSON,
26
27
  } from "./helpers";
27
28
 
28
29
  const PROVIDER_ID = "windsurf";
@@ -78,7 +79,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
78
79
  for (const { content, path, scope } of configs) {
79
80
  if (!content || !path) continue;
80
81
 
81
- const config = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
82
+ const config = tryParseJson<{ mcpServers?: Record<string, unknown> }>(content);
82
83
  if (!config?.mcpServers) continue;
83
84
 
84
85
  for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
@@ -22,5 +22,5 @@ Parameters:
22
22
  Type.Object({
23
23
  company_name: Type.String({ description: "Name of the company to research" }),
24
24
  }),
25
- "company_research",
25
+ "company_research_exa",
26
26
  );
@@ -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