@oh-my-pi/pi-coding-agent 13.1.2 → 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 (235) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +33 -14
  4. package/src/async/job-manager.ts +43 -1
  5. package/src/capability/index.ts +1 -2
  6. package/src/capability/tool.ts +1 -1
  7. package/src/cli/args.ts +1 -2
  8. package/src/cli/config-cli.ts +1 -1
  9. package/src/cli/file-processor.ts +1 -2
  10. package/src/cli/grep-cli.ts +1 -1
  11. package/src/cli/jupyter-cli.ts +1 -1
  12. package/src/cli/plugin-cli.ts +1 -1
  13. package/src/cli/setup-cli.ts +1 -1
  14. package/src/cli/shell-cli.ts +1 -1
  15. package/src/cli/ssh-cli.ts +1 -1
  16. package/src/cli/stats-cli.ts +1 -2
  17. package/src/cli/update-cli.ts +1 -2
  18. package/src/cli/web-search-cli.ts +1 -1
  19. package/src/cli.ts +1 -1
  20. package/src/commands/launch.ts +2 -1
  21. package/src/commit/agentic/agent.ts +2 -1
  22. package/src/commit/agentic/index.ts +1 -2
  23. package/src/commit/agentic/prompts/system.md +3 -3
  24. package/src/commit/agentic/tools/propose-changelog.ts +30 -19
  25. package/src/commit/changelog/generate.ts +16 -6
  26. package/src/commit/changelog/index.ts +2 -1
  27. package/src/commit/pipeline.ts +1 -2
  28. package/src/commit/prompts/reduce-system.md +1 -1
  29. package/src/commit/types.ts +10 -1
  30. package/src/config/keybindings.ts +1 -2
  31. package/src/config/model-registry.ts +1 -1
  32. package/src/config/prompt-templates.ts +14 -2
  33. package/src/config/settings-schema.ts +10 -0
  34. package/src/config/settings.ts +25 -2
  35. package/src/config.ts +1 -2
  36. package/src/debug/index.ts +1 -1
  37. package/src/debug/report-bundle.ts +1 -2
  38. package/src/debug/system-info.ts +1 -2
  39. package/src/discovery/agents.ts +2 -2
  40. package/src/discovery/builtin.ts +24 -14
  41. package/src/discovery/claude-plugins.ts +3 -2
  42. package/src/discovery/claude.ts +9 -9
  43. package/src/discovery/codex.ts +3 -3
  44. package/src/discovery/cursor.ts +5 -4
  45. package/src/discovery/gemini.ts +5 -5
  46. package/src/discovery/helpers.ts +47 -69
  47. package/src/discovery/mcp-json.ts +3 -3
  48. package/src/discovery/opencode.ts +7 -8
  49. package/src/discovery/ssh.ts +3 -3
  50. package/src/discovery/vscode.ts +3 -2
  51. package/src/discovery/windsurf.ts +3 -2
  52. package/src/exa/company.ts +1 -1
  53. package/src/exa/factory.ts +1 -6
  54. package/src/exa/linkedin.ts +1 -1
  55. package/src/exa/mcp-client.ts +19 -8
  56. package/src/exa/search.ts +2 -2
  57. package/src/exa/types.ts +3 -3
  58. package/src/exec/bash-executor.ts +2 -1
  59. package/src/exec/non-interactive-env.ts +43 -0
  60. package/src/export/custom-share.ts +1 -1
  61. package/src/export/html/index.ts +1 -2
  62. package/src/extensibility/custom-commands/loader.ts +1 -2
  63. package/src/extensibility/plugins/installer.ts +1 -2
  64. package/src/extensibility/plugins/loader.ts +1 -2
  65. package/src/extensibility/plugins/manager.ts +3 -2
  66. package/src/extensibility/skills.ts +59 -115
  67. package/src/index.ts +1 -3
  68. package/src/internal-urls/docs-index.generated.ts +1 -1
  69. package/src/ipy/executor.ts +1 -2
  70. package/src/ipy/gateway-coordinator.ts +1 -2
  71. package/src/ipy/modules.ts +1 -1
  72. package/src/ipy/runtime.ts +1 -3
  73. package/src/main.ts +1 -2
  74. package/src/mcp/config.ts +1 -1
  75. package/src/mcp/transports/stdio.ts +1 -2
  76. package/src/memories/index.ts +1 -2
  77. package/src/modes/components/diff.ts +49 -19
  78. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  79. package/src/modes/components/extensions/inspector-panel.ts +8 -2
  80. package/src/modes/components/footer.ts +1 -2
  81. package/src/modes/components/status-line/segments.ts +1 -2
  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 +1 -1
  86. package/src/modes/controllers/selector-controller.ts +1 -1
  87. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  88. package/src/modes/interactive-mode.ts +2 -3
  89. package/src/modes/shared.ts +1 -2
  90. package/src/modes/theme/theme.ts +1 -2
  91. package/src/patch/index.ts +1 -25
  92. package/src/prompts/agents/designer.md +7 -10
  93. package/src/prompts/agents/explore.md +15 -23
  94. package/src/prompts/agents/init.md +23 -23
  95. package/src/prompts/agents/plan.md +14 -77
  96. package/src/prompts/agents/reviewer.md +6 -5
  97. package/src/prompts/agents/task.md +13 -11
  98. package/src/prompts/compaction/branch-summary.md +3 -3
  99. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  100. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  101. package/src/prompts/compaction/compaction-summary.md +5 -5
  102. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  103. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  104. package/src/prompts/memories/consolidation.md +5 -5
  105. package/src/prompts/memories/read-path.md +6 -6
  106. package/src/prompts/memories/stage_one_input.md +1 -1
  107. package/src/prompts/memories/stage_one_system.md +5 -5
  108. package/src/prompts/review-request.md +4 -4
  109. package/src/prompts/system/agent-creation-architect.md +17 -17
  110. package/src/prompts/system/agent-creation-user.md +2 -2
  111. package/src/prompts/system/custom-system-prompt.md +4 -4
  112. package/src/prompts/system/plan-mode-active.md +20 -20
  113. package/src/prompts/system/plan-mode-approved.md +7 -7
  114. package/src/prompts/system/plan-mode-reference.md +2 -2
  115. package/src/prompts/system/plan-mode-subagent.md +8 -8
  116. package/src/prompts/system/subagent-submit-reminder.md +5 -5
  117. package/src/prompts/system/subagent-system-prompt.md +29 -22
  118. package/src/prompts/system/subagent-user-prompt.md +7 -3
  119. package/src/prompts/system/summarization-system.md +1 -1
  120. package/src/prompts/system/system-prompt.md +201 -226
  121. package/src/prompts/system/title-system.md +2 -2
  122. package/src/prompts/system/ttsr-interrupt.md +1 -1
  123. package/src/prompts/system/web-search.md +16 -16
  124. package/src/prompts/tools/ask.md +1 -3
  125. package/src/prompts/tools/await.md +2 -4
  126. package/src/prompts/tools/bash.md +5 -7
  127. package/src/prompts/tools/browser.md +4 -6
  128. package/src/prompts/tools/calculator.md +1 -3
  129. package/src/prompts/tools/cancel-job.md +2 -4
  130. package/src/prompts/tools/exit-plan-mode.md +7 -7
  131. package/src/prompts/tools/fetch.md +0 -2
  132. package/src/prompts/tools/find.md +3 -5
  133. package/src/prompts/tools/gemini-image.md +6 -22
  134. package/src/prompts/tools/grep.md +4 -6
  135. package/src/prompts/tools/hashline.md +12 -15
  136. package/src/prompts/tools/lsp.md +1 -3
  137. package/src/prompts/tools/patch.md +7 -9
  138. package/src/prompts/tools/python.md +10 -14
  139. package/src/prompts/tools/read.md +0 -2
  140. package/src/prompts/tools/replace.md +5 -7
  141. package/src/prompts/tools/ssh.md +3 -5
  142. package/src/prompts/tools/task.md +6 -8
  143. package/src/prompts/tools/todo-write.md +7 -9
  144. package/src/prompts/tools/web-search.md +3 -5
  145. package/src/prompts/tools/write.md +3 -5
  146. package/src/sdk.ts +1 -2
  147. package/src/session/agent-session.ts +10 -26
  148. package/src/session/agent-storage.ts +1 -2
  149. package/src/session/history-storage.ts +1 -2
  150. package/src/session/session-manager.ts +10 -2
  151. package/src/ssh/connection-manager.ts +11 -2
  152. package/src/ssh/sshfs-mount.ts +7 -1
  153. package/src/system-prompt.ts +25 -103
  154. package/src/task/agents.ts +1 -1
  155. package/src/task/worktree.ts +1 -2
  156. package/src/tools/ask.ts +0 -1
  157. package/src/tools/await-tool.ts +6 -3
  158. package/src/tools/bash-interactive.ts +2 -45
  159. package/src/tools/bash.ts +5 -5
  160. package/src/tools/browser.ts +1 -2
  161. package/src/tools/gemini-image.ts +8 -28
  162. package/src/tools/json-tree.ts +2 -1
  163. package/src/tools/python.ts +1 -1
  164. package/src/tools/read.ts +1 -2
  165. package/src/tools/render-utils.ts +5 -2
  166. package/src/tools/todo-write.ts +11 -9
  167. package/src/utils/tools-manager.ts +1 -2
  168. package/src/web/scrapers/artifacthub.ts +2 -1
  169. package/src/web/scrapers/aur.ts +2 -1
  170. package/src/web/scrapers/biorxiv.ts +2 -1
  171. package/src/web/scrapers/bluesky.ts +2 -1
  172. package/src/web/scrapers/chocolatey.ts +2 -1
  173. package/src/web/scrapers/cisa-kev.ts +2 -1
  174. package/src/web/scrapers/clojars.ts +2 -1
  175. package/src/web/scrapers/coingecko.ts +2 -1
  176. package/src/web/scrapers/crates-io.ts +2 -1
  177. package/src/web/scrapers/crossref.ts +2 -1
  178. package/src/web/scrapers/discogs.ts +3 -1
  179. package/src/web/scrapers/discourse.ts +2 -1
  180. package/src/web/scrapers/dockerhub.ts +2 -1
  181. package/src/web/scrapers/fdroid.ts +2 -1
  182. package/src/web/scrapers/firefox-addons.ts +2 -1
  183. package/src/web/scrapers/flathub.ts +2 -1
  184. package/src/web/scrapers/gitlab.ts +1 -1
  185. package/src/web/scrapers/go-pkg.ts +2 -1
  186. package/src/web/scrapers/hackage.ts +2 -1
  187. package/src/web/scrapers/hackernews.ts +2 -1
  188. package/src/web/scrapers/hex.ts +2 -1
  189. package/src/web/scrapers/huggingface.ts +2 -1
  190. package/src/web/scrapers/jetbrains-marketplace.ts +2 -1
  191. package/src/web/scrapers/lemmy.ts +2 -1
  192. package/src/web/scrapers/lobsters.ts +2 -1
  193. package/src/web/scrapers/mastodon.ts +2 -1
  194. package/src/web/scrapers/maven.ts +2 -1
  195. package/src/web/scrapers/mdn.ts +2 -1
  196. package/src/web/scrapers/metacpan.ts +2 -1
  197. package/src/web/scrapers/musicbrainz.ts +3 -1
  198. package/src/web/scrapers/npm.ts +2 -1
  199. package/src/web/scrapers/nuget.ts +2 -1
  200. package/src/web/scrapers/nvd.ts +2 -1
  201. package/src/web/scrapers/ollama.ts +2 -1
  202. package/src/web/scrapers/open-vsx.ts +2 -1
  203. package/src/web/scrapers/opencorporates.ts +2 -1
  204. package/src/web/scrapers/openlibrary.ts +2 -1
  205. package/src/web/scrapers/orcid.ts +3 -1
  206. package/src/web/scrapers/osv.ts +2 -1
  207. package/src/web/scrapers/packagist.ts +2 -1
  208. package/src/web/scrapers/pub-dev.ts +2 -1
  209. package/src/web/scrapers/pubmed.ts +2 -1
  210. package/src/web/scrapers/pypi.ts +2 -1
  211. package/src/web/scrapers/rawg.ts +2 -8
  212. package/src/web/scrapers/reddit.ts +2 -1
  213. package/src/web/scrapers/repology.ts +2 -1
  214. package/src/web/scrapers/rfc.ts +2 -1
  215. package/src/web/scrapers/rubygems.ts +2 -1
  216. package/src/web/scrapers/searchcode.ts +2 -1
  217. package/src/web/scrapers/sec-edgar.ts +2 -1
  218. package/src/web/scrapers/semantic-scholar.ts +2 -1
  219. package/src/web/scrapers/snapcraft.ts +2 -1
  220. package/src/web/scrapers/sourcegraph.ts +2 -1
  221. package/src/web/scrapers/spdx.ts +2 -1
  222. package/src/web/scrapers/stackoverflow.ts +2 -1
  223. package/src/web/scrapers/terraform.ts +2 -1
  224. package/src/web/scrapers/types.ts +0 -11
  225. package/src/web/scrapers/vimeo.ts +2 -1
  226. package/src/web/scrapers/vscode-marketplace.ts +2 -1
  227. package/src/web/scrapers/w3c.ts +2 -1
  228. package/src/web/scrapers/wikidata.ts +2 -1
  229. package/src/web/search/index.ts +10 -14
  230. package/src/web/search/provider.ts +2 -2
  231. package/src/web/search/providers/codex.ts +1 -2
  232. package/src/web/search/providers/exa.ts +1 -6
  233. package/src/web/search/providers/gemini.ts +1 -1
  234. package/src/web/search/providers/perplexity.ts +1 -2
  235. package/src/web/search/providers/utils.ts +1 -1
@@ -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
  );
@@ -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,