@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
@@ -7,7 +7,7 @@ import * as fs from "node:fs/promises";
7
7
  import * as url from "node:url";
8
8
  import { getWorkProfile } from "@oh-my-pi/pi-natives";
9
9
  import { Container, Loader, type SelectItem, SelectList, Spacer, Text } from "@oh-my-pi/pi-tui";
10
- import { getSessionsDir } from "@oh-my-pi/pi-utils/dirs";
10
+ import { getSessionsDir } from "@oh-my-pi/pi-utils";
11
11
  import { DynamicBorder } from "../modes/components/dynamic-border";
12
12
  import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme";
13
13
  import type { InteractiveModeContext } from "../modes/types";
@@ -6,8 +6,7 @@
6
6
  import * as fs from "node:fs/promises";
7
7
  import * as path from "node:path";
8
8
  import type { WorkProfile } from "@oh-my-pi/pi-natives";
9
- import { isEnoent } from "@oh-my-pi/pi-utils";
10
- import { APP_NAME, getLogPath, getLogsDir, getReportsDir } from "@oh-my-pi/pi-utils/dirs";
9
+ import { APP_NAME, getLogPath, getLogsDir, getReportsDir, isEnoent } from "@oh-my-pi/pi-utils";
11
10
  import type { CpuProfile, HeapSnapshot } from "./profiler";
12
11
  import { collectSystemInfo, sanitizeEnv } from "./system-info";
13
12
 
@@ -3,8 +3,7 @@
3
3
  */
4
4
 
5
5
  import * as os from "node:os";
6
- import { formatBytes } from "@oh-my-pi/pi-utils";
7
- import { getProjectDir, VERSION } from "@oh-my-pi/pi-utils/dirs";
6
+ import { formatBytes, getProjectDir, VERSION } from "@oh-my-pi/pi-utils";
8
7
 
9
8
  export interface SystemInfo {
10
9
  os: string;
@@ -13,7 +13,7 @@ import { type Skill, skillCapability } from "../capability/skill";
13
13
  import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
14
14
  import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
15
15
  import type { LoadContext, LoadResult } from "../capability/types";
16
- import { buildRuleFromMarkdown, createSourceMeta, loadFilesFromDir, loadSkillsFromDir } from "./helpers";
16
+ import { buildRuleFromMarkdown, createSourceMeta, loadFilesFromDir, scanSkillsFromDir } from "./helpers";
17
17
 
18
18
  const PROVIDER_ID = "agents";
19
19
  const DISPLAY_NAME = "Agents (standard)";
@@ -28,7 +28,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
28
28
  const items: Skill[] = [];
29
29
  const warnings: string[] = [];
30
30
  for (const userSkillsDir of getUserAgentPathCandidates(ctx, "skills")) {
31
- const result = await loadSkillsFromDir(ctx, {
31
+ const result = await scanSkillsFromDir(ctx, {
32
32
  dir: userSkillsDir,
33
33
  providerId: PROVIDER_ID,
34
34
  level: "user",
@@ -4,7 +4,7 @@
4
4
  * Primary provider for OMP native configs. Supports all capabilities.
5
5
  */
6
6
  import * as path from "node:path";
7
- import { logger } from "@oh-my-pi/pi-utils";
7
+ import { logger, tryParseJson } from "@oh-my-pi/pi-utils";
8
8
  import { registerProvider } from "../capability";
9
9
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
10
10
  import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
@@ -30,9 +30,8 @@ import {
30
30
  expandEnvVarsDeep,
31
31
  getExtensionNameFromPath,
32
32
  loadFilesFromDir,
33
- loadSkillsFromDir,
34
- parseJSON,
35
33
  SOURCE_PATHS,
34
+ scanSkillsFromDir,
36
35
  } from "./helpers";
37
36
 
38
37
  const PROVIDER_ID = "native";
@@ -98,7 +97,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
98
97
 
99
98
  const parseMcpServers = (content: string, path: string, level: "user" | "project"): MCPServer[] => {
100
99
  const result: MCPServer[] = [];
101
- const data = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
100
+ const data = tryParseJson<{ mcpServers?: Record<string, unknown> }>(content);
102
101
  if (!data?.mcpServers) return result;
103
102
 
104
103
  const expanded = expandEnvVarsDeep(data.mcpServers);
@@ -245,7 +244,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
245
244
  const configDirs = await getConfigDirs(ctx);
246
245
  const results = await Promise.all(
247
246
  configDirs.map(({ dir, level }) =>
248
- loadSkillsFromDir(ctx, {
247
+ scanSkillsFromDir(ctx, {
249
248
  dir: path.join(dir, "skills"),
250
249
  providerId: PROVIDER_ID,
251
250
  level,
@@ -404,7 +403,7 @@ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<Extens
404
403
  if (!settingsContent) continue;
405
404
 
406
405
  const settingsPath = path.join(dir, "settings.json");
407
- const settingsData = parseJSON<{ extensions?: unknown }>(settingsContent);
406
+ const settingsData = tryParseJson<{ extensions?: unknown }>(settingsContent);
408
407
  const extensions = settingsData?.extensions;
409
408
  if (!Array.isArray(extensions)) continue;
410
409
 
@@ -508,7 +507,7 @@ async function loadExtensions(ctx: LoadContext): Promise<LoadResult<Extension>>
508
507
  if (!content) continue;
509
508
 
510
509
  const { extDir, manifestPath, entryName, level } = manifestCandidates[i];
511
- const manifest = parseJSON<ExtensionManifest>(content);
510
+ const manifest = tryParseJson<ExtensionManifest>(content);
512
511
  if (!manifest) {
513
512
  warnings.push(`Failed to parse ${manifestPath}`);
514
513
  continue;
@@ -655,7 +654,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
655
654
  extensions: ["json", "md", "ts", "js", "sh", "bash", "py"],
656
655
  transform: (name, content, path, source) => {
657
656
  if (name.endsWith(".json")) {
658
- const data = parseJSON<{ name?: string; description?: string }>(content);
657
+ const data = tryParseJson<{ name?: string; description?: string }>(content);
659
658
  const toolName = data?.name || name.replace(/\.json$/, "");
660
659
  const description =
661
660
  typeof data?.description === "string" && data.description.trim()
@@ -754,7 +753,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
754
753
  const content = await readFile(settingsPath);
755
754
  if (!content) continue;
756
755
 
757
- const data = parseJSON<Record<string, unknown>>(content);
756
+ const data = tryParseJson<Record<string, unknown>>(content);
758
757
  if (!data) {
759
758
  warnings.push(`Failed to parse ${settingsPath}`);
760
759
  continue;
@@ -11,7 +11,7 @@ import { type Skill, skillCapability } from "../capability/skill";
11
11
  import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
12
12
  import { type CustomTool, toolCapability } from "../capability/tool";
13
13
  import type { LoadContext, LoadResult } from "../capability/types";
14
- import { type ClaudePluginRoot, listClaudePluginRoots, loadFilesFromDir, loadSkillsFromDir } from "./helpers";
14
+ import { type ClaudePluginRoot, listClaudePluginRoots, loadFilesFromDir, scanSkillsFromDir } from "./helpers";
15
15
 
16
16
  const PROVIDER_ID = "claude-plugins";
17
17
  const DISPLAY_NAME = "Claude Code Marketplace";
@@ -31,7 +31,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
31
31
  const results = await Promise.all(
32
32
  roots.map(async root => {
33
33
  const skillsDir = path.join(root.path, "skills");
34
- return loadSkillsFromDir(ctx, {
34
+ return scanSkillsFromDir(ctx, {
35
35
  dir: skillsDir,
36
36
  providerId: PROVIDER_ID,
37
37
  level: root.scope,
@@ -5,6 +5,7 @@
5
5
  * Priority: 80 (tool-specific, below builtin but above shared standards)
6
6
  */
7
7
  import * as path from "node:path";
8
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
8
9
  import { registerProvider } from "../capability";
9
10
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
10
11
  import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
@@ -24,8 +25,7 @@ import {
24
25
  expandEnvVarsDeep,
25
26
  getExtensionNameFromPath,
26
27
  loadFilesFromDir,
27
- loadSkillsFromDir,
28
- parseJSON,
28
+ scanSkillsFromDir,
29
29
  } from "./helpers";
30
30
 
31
31
  const PROVIDER_ID = "claude";
@@ -77,7 +77,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
77
77
 
78
78
  const parseMcpServers = (content: string | null, path: string, level: "user" | "project"): MCPServer[] => {
79
79
  if (!content) return [];
80
- const json = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
80
+ const json = tryParseJson<{ mcpServers?: Record<string, unknown> }>(content);
81
81
  if (!json?.mcpServers) return [];
82
82
 
83
83
  const mcpServers = expandEnvVarsDeep(json.mcpServers);
@@ -163,8 +163,8 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
163
163
  const projectSkillsDir = path.join(getProjectClaude(ctx), "skills");
164
164
 
165
165
  const results = await Promise.all([
166
- loadSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user" }),
167
- loadSkillsFromDir(ctx, { dir: projectSkillsDir, providerId: PROVIDER_ID, level: "project" }),
166
+ scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user" }),
167
+ scanSkillsFromDir(ctx, { dir: projectSkillsDir, providerId: PROVIDER_ID, level: "project" }),
168
168
  ]);
169
169
 
170
170
  return {
@@ -396,7 +396,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
396
396
 
397
397
  const userContent = await readFile(userSettingsJson);
398
398
  if (userContent) {
399
- const data = parseJSON<Record<string, unknown>>(userContent);
399
+ const data = tryParseJson<Record<string, unknown>>(userContent);
400
400
  if (data) {
401
401
  items.push({
402
402
  path: userSettingsJson,
@@ -413,7 +413,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
413
413
  const projectSettingsJson = path.join(projectBase, "settings.json");
414
414
  const projectContent = await readFile(projectSettingsJson);
415
415
  if (projectContent) {
416
- const data = parseJSON<Record<string, unknown>>(projectContent);
416
+ const data = tryParseJson<Record<string, unknown>>(projectContent);
417
417
  if (data) {
418
418
  items.push({
419
419
  path: projectSettingsJson,
@@ -35,8 +35,8 @@ import {
35
35
  discoverExtensionModulePaths,
36
36
  getExtensionNameFromPath,
37
37
  loadFilesFromDir,
38
- loadSkillsFromDir,
39
38
  SOURCE_PATHS,
39
+ scanSkillsFromDir,
40
40
  } from "./helpers";
41
41
 
42
42
  const PROVIDER_ID = "codex";
@@ -214,12 +214,12 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
214
214
  const projectSkillsDir = path.join(codexDir, "skills");
215
215
 
216
216
  const results = await Promise.all([
217
- loadSkillsFromDir(ctx, {
217
+ scanSkillsFromDir(ctx, {
218
218
  dir: userSkillsDir,
219
219
  providerId: PROVIDER_ID,
220
220
  level: "user",
221
221
  }),
222
- loadSkillsFromDir(ctx, {
222
+ scanSkillsFromDir(ctx, {
223
223
  dir: projectSkillsDir,
224
224
  providerId: PROVIDER_ID,
225
225
  level: "project",
@@ -13,6 +13,8 @@
13
13
  * - rules: From rules/*.mdc files with MDC frontmatter (description, globs, alwaysApply)
14
14
  * - settings: From settings.json if present
15
15
  */
16
+
17
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
16
18
  import { registerProvider } from "../capability";
17
19
  import { readFile } from "../capability/fs";
18
20
  import { type MCPServer, mcpCapability } from "../capability/mcp";
@@ -28,7 +30,6 @@ import {
28
30
  getProjectPath,
29
31
  getUserPath,
30
32
  loadFilesFromDir,
31
- parseJSON,
32
33
  } from "./helpers";
33
34
 
34
35
  const PROVIDER_ID = "cursor";
@@ -46,7 +47,7 @@ function parseMCPServers(
46
47
  ): { items: MCPServer[]; warning?: string } {
47
48
  const items: MCPServer[] = [];
48
49
 
49
- const parsed = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
50
+ const parsed = tryParseJson<{ mcpServers?: Record<string, unknown> }>(content);
50
51
  if (!parsed?.mcpServers) {
51
52
  return { items, warning: `${path}: missing or invalid 'mcpServers' key` };
52
53
  }
@@ -158,7 +159,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
158
159
  const projectContentPromise = projectPath ? readFile(projectPath) : Promise.resolve(null);
159
160
 
160
161
  if (userContent && userPath) {
161
- const parsed = parseJSON<Record<string, unknown>>(userContent);
162
+ const parsed = tryParseJson<Record<string, unknown>>(userContent);
162
163
  if (parsed) {
163
164
  items.push({
164
165
  path: userPath,
@@ -173,7 +174,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
173
174
 
174
175
  const projectContent = await projectContentPromise;
175
176
  if (projectContent && projectPath) {
176
- const parsed = parseJSON<Record<string, unknown>>(projectContent);
177
+ const parsed = tryParseJson<Record<string, unknown>>(projectContent);
177
178
  if (parsed) {
178
179
  items.push({
179
180
  path: projectPath,
@@ -16,6 +16,7 @@
16
16
  * - settings: From settings.json
17
17
  */
18
18
  import * as path from "node:path";
19
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
19
20
  import { registerProvider } from "../capability";
20
21
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
21
22
  import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
@@ -33,7 +34,6 @@ import {
33
34
  getExtensionNameFromPath,
34
35
  getProjectPath,
35
36
  getUserPath,
36
- parseJSON,
37
37
  } from "./helpers";
38
38
 
39
39
  const PROVIDER_ID = "gemini";
@@ -80,7 +80,7 @@ async function loadMCPFromSettings(
80
80
  return { items, warnings };
81
81
  }
82
82
 
83
- const parsed = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
83
+ const parsed = tryParseJson<{ mcpServers?: Record<string, unknown> }>(content);
84
84
  if (!parsed) {
85
85
  warnings.push(`Invalid JSON in ${path}`);
86
86
  return { items, warnings };
@@ -206,7 +206,7 @@ async function loadExtensionsFromDir(extensionsDir: string, level: "user" | "pro
206
206
  for (const { entry, extPath, manifestPath, content } of results) {
207
207
  if (!content) continue;
208
208
 
209
- const manifest = parseJSON<ExtensionManifest>(content);
209
+ const manifest = tryParseJson<ExtensionManifest>(content);
210
210
  if (!manifest) {
211
211
  warnings.push(`Invalid JSON in ${manifestPath}`);
212
212
  continue;
@@ -268,7 +268,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
268
268
  if (userPath) {
269
269
  const content = await readFile(userPath);
270
270
  if (content) {
271
- const parsed = parseJSON<Record<string, unknown>>(content);
271
+ const parsed = tryParseJson<Record<string, unknown>>(content);
272
272
  if (parsed) {
273
273
  items.push({
274
274
  path: userPath,
@@ -287,7 +287,7 @@ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
287
287
  if (projectPath) {
288
288
  const content = await readFile(projectPath);
289
289
  if (content) {
290
- const parsed = parseJSON<Record<string, unknown>>(content);
290
+ const parsed = tryParseJson<Record<string, unknown>>(content);
291
291
  if (parsed) {
292
292
  items.push({
293
293
  path: projectPath,
@@ -1,7 +1,8 @@
1
+ import * as fs from "node:fs";
1
2
  import * as path from "node:path";
2
3
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
4
  import { FileType, glob } from "@oh-my-pi/pi-natives";
4
- import { CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs";
5
+ import { CONFIG_DIR_NAME, tryParseJson } from "@oh-my-pi/pi-utils";
5
6
  import { readFile } from "../capability/fs";
6
7
  import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
7
8
  import type { Skill, SkillFrontmatter } from "../capability/skill";
@@ -268,68 +269,56 @@ async function globIf(
268
269
  }
269
270
  }
270
271
 
271
- export async function loadSkillsFromDir(
272
+ export interface ScanSkillsFromDirOptions {
273
+ dir: string;
274
+ providerId: string;
275
+ level: "user" | "project";
276
+ requireDescription?: boolean;
277
+ }
278
+
279
+ export async function scanSkillsFromDir(
272
280
  _ctx: LoadContext,
273
- options: {
274
- dir: string;
275
- providerId: string;
276
- level: "user" | "project";
277
- requireDescription?: boolean;
278
- },
281
+ options: ScanSkillsFromDirOptions,
279
282
  ): Promise<LoadResult<Skill>> {
280
283
  const items: Skill[] = [];
281
284
  const warnings: string[] = [];
282
285
  const { dir, level, providerId, requireDescription = false } = options;
283
- // Use native glob to find all SKILL.md files one level deep
284
- // Pattern */SKILL.md matches <dir>/<subdir>/SKILL.md
285
- const discoveredMatches = new Set<string>();
286
- for (const match of await globIf(dir, "*/SKILL.md", FileType.File)) {
287
- discoveredMatches.add(match.path);
288
- }
289
- for (const match of await globIf(dir, "*", FileType.Dir, false)) {
290
- const skillRelPath = `${match.path}/SKILL.md`;
291
- const content = await readFile(path.join(dir, skillRelPath));
292
- if (content !== null) {
293
- discoveredMatches.add(skillRelPath);
294
- }
295
- }
296
- const matches = [...discoveredMatches].map(path => ({ path }));
297
- if (matches.length === 0) {
298
- return { items, warnings };
299
- }
300
286
 
301
- // Read all skill files in parallel
302
- const results = await Promise.all(
303
- matches.map(async match => {
304
- const skillFile = path.join(dir, match.path);
305
- const content = await readFile(skillFile);
306
- if (!content) {
307
- return { item: null as Skill | null, warning: null as string | null };
308
- }
309
- const { frontmatter, body } = parseFrontmatter(content, { source: skillFile });
287
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
288
+
289
+ const loadSkill = async (skillPath: string) => {
290
+ try {
291
+ const content = await readFile(skillPath);
292
+ if (!content) return;
293
+ const { frontmatter, body } = parseFrontmatter(content, { source: skillPath });
310
294
  if (requireDescription && !frontmatter.description) {
311
- return { item: null as Skill | null, warning: null as string | null };
295
+ return;
312
296
  }
297
+ const skillDirName = path.basename(path.dirname(skillPath));
298
+ items.push({
299
+ name: (frontmatter.name as string) || skillDirName,
300
+ path: skillPath,
301
+ content: body,
302
+ frontmatter: frontmatter as SkillFrontmatter,
303
+ level,
304
+ _source: createSourceMeta(providerId, skillPath, level),
305
+ });
306
+ } catch {
307
+ warnings.push(`Failed to read skill file: ${skillPath}`);
308
+ }
309
+ };
313
310
 
314
- // Extract skill name from path: "<skilldir>/SKILL.md" -> "<skilldir>"
315
- const skillDirName = path.basename(path.dirname(skillFile));
316
- return {
317
- item: {
318
- name: (frontmatter.name as string) || skillDirName,
319
- path: skillFile,
320
- content: body,
321
- frontmatter: frontmatter as SkillFrontmatter,
322
- level,
323
- _source: createSourceMeta(providerId, skillFile, level),
324
- },
325
- warning: null as string | null,
326
- };
327
- }),
328
- );
329
- for (const result of results) {
330
- if (result.warning) warnings.push(result.warning);
331
- if (result.item) items.push(result.item);
311
+ const work = [];
312
+ for (const entry of entries) {
313
+ if (entry.name.startsWith(".")) continue;
314
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
315
+ const skillPath = path.join(dir, entry.name, "SKILL.md");
316
+ if (fs.existsSync(skillPath)) {
317
+ work.push(loadSkill(skillPath));
318
+ }
332
319
  }
320
+ await Promise.all(work);
321
+
333
322
  return { items, warnings };
334
323
  }
335
324
 
@@ -337,7 +326,7 @@ export async function loadSkillsFromDir(
337
326
  * Expand environment variables in a string.
338
327
  * Supports ${VAR} and ${VAR:-default} syntax.
339
328
  */
340
- export function expandEnvVars(value: string, extraEnv?: Record<string, string>): string {
329
+ function expandEnvVars(value: string, extraEnv?: Record<string, string>): string {
341
330
  return value.replace(/\$\{([^}:]+)(?::-([^}]*))?\}/g, (_, varName: string, defaultValue?: string) => {
342
331
  const envValue = extraEnv?.[varName] ?? Bun.env[varName];
343
332
  if (envValue !== undefined) return envValue;
@@ -447,17 +436,6 @@ export async function loadFilesFromDir<T>(
447
436
  return { items, warnings };
448
437
  }
449
438
 
450
- /**
451
- * Parse JSON safely.
452
- */
453
- export function parseJSON<T>(content: string): T | null {
454
- try {
455
- return JSON.parse(content) as T;
456
- } catch {
457
- return null;
458
- }
459
- }
460
-
461
439
  /**
462
440
  * Calculate depth of target directory relative to current working directory.
463
441
  * Depth is the number of directory levels from cwd to target.
@@ -480,7 +458,7 @@ async function readExtensionModuleManifest(
480
458
  const content = await readFile(packageJsonPath);
481
459
  if (!content) return null;
482
460
 
483
- const pkg = parseJSON<{ omp?: ExtensionModuleManifest; pi?: ExtensionModuleManifest }>(content);
461
+ const pkg = tryParseJson<{ omp?: ExtensionModuleManifest; pi?: ExtensionModuleManifest }>(content);
484
462
  const manifest = pkg?.omp ?? pkg?.pi;
485
463
  if (manifest && typeof manifest === "object") {
486
464
  return manifest;
@@ -506,9 +484,9 @@ export async function discoverExtensionModulePaths(_ctx: LoadContext, dir: strin
506
484
  // 1. Direct *.ts or *.js files
507
485
  globIf(dir, "*.{ts,js}", FileType.File, false),
508
486
  // 2. Subdirectory index files
509
- globIf(dir, "*/index.{ts,js}", FileType.File),
487
+ globIf(dir, "*/index.{ts,js}", FileType.File, false),
510
488
  // 3. Subdirectory package.json files
511
- globIf(dir, "*/package.json", FileType.File),
489
+ globIf(dir, "*/package.json", FileType.File, false),
512
490
  ]);
513
491
 
514
492
  // Process direct files
@@ -617,7 +595,7 @@ export interface ClaudePluginRoot {
617
595
  * Parse Claude Code installed_plugins.json content.
618
596
  */
619
597
  export function parseClaudePluginsRegistry(content: string): ClaudePluginsRegistry | null {
620
- const data = parseJSON<ClaudePluginsRegistry>(content);
598
+ const data = tryParseJson<ClaudePluginsRegistry>(content);
621
599
  if (!data || typeof data !== "object") return null;
622
600
  if (
623
601
  typeof data.version !== "number" ||
@@ -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
  );