@oh-my-pi/pi-coding-agent 12.18.3 → 12.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/package.json +35 -27
  3. package/src/async/index.ts +1 -0
  4. package/src/async/job-manager.ts +341 -0
  5. package/src/cli/file-processor.ts +3 -3
  6. package/src/cli/list-models.ts +3 -17
  7. package/src/cli/stats-cli.ts +3 -22
  8. package/src/cli/web-search-cli.ts +8 -16
  9. package/src/commit/agentic/agent.ts +6 -9
  10. package/src/commit/agentic/index.ts +44 -50
  11. package/src/commit/agentic/state.ts +0 -9
  12. package/src/commit/agentic/tools/propose-commit.ts +1 -30
  13. package/src/commit/agentic/tools/schemas.ts +31 -0
  14. package/src/commit/agentic/tools/split-commit.ts +1 -30
  15. package/src/commit/agentic/validation.ts +1 -18
  16. package/src/commit/analysis/conventional.ts +3 -50
  17. package/src/commit/analysis/summary.ts +2 -13
  18. package/src/commit/changelog/detect.ts +4 -1
  19. package/src/commit/changelog/generate.ts +2 -25
  20. package/src/commit/changelog/index.ts +1 -2
  21. package/src/commit/cli.ts +4 -12
  22. package/src/commit/map-reduce/reduce-phase.ts +2 -43
  23. package/src/commit/pipeline.ts +7 -15
  24. package/src/commit/utils.ts +44 -0
  25. package/src/config/prompt-templates.ts +1 -81
  26. package/src/config/settings-schema.ts +20 -1
  27. package/src/config.ts +2 -3
  28. package/src/debug/index.ts +1 -6
  29. package/src/debug/system-info.ts +2 -6
  30. package/src/discovery/builtin.ts +5 -9
  31. package/src/discovery/helpers.ts +0 -26
  32. package/src/discovery/ssh.ts +1 -8
  33. package/src/exa/company.ts +8 -39
  34. package/src/exa/factory.ts +64 -0
  35. package/src/exa/index.ts +0 -16
  36. package/src/exa/linkedin.ts +8 -39
  37. package/src/exa/mcp-client.ts +0 -64
  38. package/src/exa/researcher.ts +17 -59
  39. package/src/exa/search.ts +30 -154
  40. package/src/extensibility/custom-tools/loader.ts +3 -41
  41. package/src/extensibility/extensions/loader.ts +2 -9
  42. package/src/extensibility/hooks/loader.ts +3 -20
  43. package/src/extensibility/hooks/runner.ts +3 -19
  44. package/src/extensibility/plugins/installer.ts +2 -1
  45. package/src/extensibility/plugins/loader.ts +29 -117
  46. package/src/extensibility/skills.ts +2 -89
  47. package/src/extensibility/slash-commands.ts +1 -63
  48. package/src/extensibility/utils.ts +38 -0
  49. package/src/index.ts +9 -25
  50. package/src/internal-urls/index.ts +1 -0
  51. package/src/internal-urls/jobs-protocol.ts +118 -0
  52. package/src/ipy/kernel.ts +2 -0
  53. package/src/lsp/config.ts +1 -5
  54. package/src/lsp/lspmux.ts +0 -17
  55. package/src/lsp/utils.ts +2 -24
  56. package/src/main.ts +16 -24
  57. package/src/mcp/client.ts +1 -46
  58. package/src/mcp/render.ts +8 -1
  59. package/src/mcp/tool-cache.ts +1 -5
  60. package/src/mcp/transports/http.ts +2 -7
  61. package/src/mcp/transports/stdio.ts +2 -7
  62. package/src/modes/components/bash-execution.ts +2 -16
  63. package/src/modes/components/extensions/inspector-panel.ts +8 -18
  64. package/src/modes/components/footer.ts +10 -50
  65. package/src/modes/components/model-selector.ts +2 -21
  66. package/src/modes/components/python-execution.ts +2 -16
  67. package/src/modes/components/settings-selector.ts +1 -10
  68. package/src/modes/components/status-line/segments.ts +8 -25
  69. package/src/modes/components/status-line.ts +14 -31
  70. package/src/modes/components/tool-execution.ts +8 -2
  71. package/src/modes/controllers/command-controller.ts +71 -30
  72. package/src/modes/controllers/event-controller.ts +34 -4
  73. package/src/modes/controllers/mcp-command-controller.ts +3 -34
  74. package/src/modes/controllers/selector-controller.ts +2 -2
  75. package/src/modes/controllers/ssh-command-controller.ts +3 -34
  76. package/src/modes/interactive-mode.ts +6 -2
  77. package/src/modes/rpc/rpc-client.ts +1 -5
  78. package/src/modes/shared.ts +73 -0
  79. package/src/modes/types.ts +1 -0
  80. package/src/modes/utils/ui-helpers.ts +26 -2
  81. package/src/patch/hashline.ts +6 -286
  82. package/src/patch/index.ts +6 -57
  83. package/src/patch/normalize.ts +22 -65
  84. package/src/patch/shared.ts +16 -16
  85. package/src/prompts/system/custom-system-prompt.md +0 -10
  86. package/src/prompts/system/system-prompt.md +69 -89
  87. package/src/prompts/tools/async-result.md +5 -0
  88. package/src/prompts/tools/bash.md +5 -0
  89. package/src/prompts/tools/cancel-job.md +7 -0
  90. package/src/prompts/tools/hashline.md +0 -16
  91. package/src/prompts/tools/poll-jobs.md +7 -0
  92. package/src/prompts/tools/task.md +4 -0
  93. package/src/sdk.ts +70 -6
  94. package/src/session/agent-session.ts +43 -6
  95. package/src/session/agent-storage.ts +69 -278
  96. package/src/session/auth-storage.ts +14 -1430
  97. package/src/session/session-manager.ts +69 -5
  98. package/src/session/session-storage.ts +1 -5
  99. package/src/session/streaming-output.ts +637 -76
  100. package/src/slash-commands/builtin-registry.ts +8 -0
  101. package/src/ssh/connection-manager.ts +4 -12
  102. package/src/ssh/sshfs-mount.ts +3 -7
  103. package/src/ssh/utils.ts +8 -0
  104. package/src/system-prompt.ts +24 -90
  105. package/src/task/executor.ts +11 -1
  106. package/src/task/index.ts +258 -13
  107. package/src/task/parallel.ts +32 -0
  108. package/src/task/render.ts +15 -7
  109. package/src/task/types.ts +5 -0
  110. package/src/tools/ask.ts +4 -7
  111. package/src/tools/bash-interactive.ts +4 -5
  112. package/src/tools/bash.ts +125 -41
  113. package/src/tools/cancel-job.ts +93 -0
  114. package/src/tools/fetch.ts +7 -27
  115. package/src/tools/find.ts +3 -3
  116. package/src/tools/gemini-image.ts +15 -14
  117. package/src/tools/grep.ts +3 -3
  118. package/src/tools/index.ts +13 -29
  119. package/src/tools/json-tree.ts +12 -1
  120. package/src/tools/jtd-to-json-schema.ts +10 -74
  121. package/src/tools/jtd-to-typescript.ts +10 -72
  122. package/src/tools/jtd-utils.ts +102 -0
  123. package/src/tools/notebook.ts +4 -9
  124. package/src/tools/output-meta.ts +52 -26
  125. package/src/tools/path-utils.ts +13 -7
  126. package/src/tools/poll-jobs.ts +178 -0
  127. package/src/tools/python.ts +32 -35
  128. package/src/tools/read.ts +61 -82
  129. package/src/tools/render-utils.ts +8 -159
  130. package/src/tools/ssh.ts +7 -20
  131. package/src/tools/submit-result.ts +1 -1
  132. package/src/tools/tool-errors.ts +0 -30
  133. package/src/tools/tool-result.ts +1 -2
  134. package/src/tools/write.ts +8 -10
  135. package/src/tui/code-cell.ts +8 -3
  136. package/src/tui/status-line.ts +4 -4
  137. package/src/tui/types.ts +0 -1
  138. package/src/tui/utils.ts +1 -14
  139. package/src/utils/command-args.ts +76 -0
  140. package/src/utils/file-mentions.ts +15 -19
  141. package/src/utils/frontmatter.ts +5 -10
  142. package/src/utils/shell-snapshot.ts +0 -11
  143. package/src/utils/title-generator.ts +0 -12
  144. package/src/web/scrapers/artifacthub.ts +7 -16
  145. package/src/web/scrapers/arxiv.ts +3 -8
  146. package/src/web/scrapers/aur.ts +8 -22
  147. package/src/web/scrapers/biorxiv.ts +5 -14
  148. package/src/web/scrapers/bluesky.ts +13 -36
  149. package/src/web/scrapers/brew.ts +5 -10
  150. package/src/web/scrapers/cheatsh.ts +2 -12
  151. package/src/web/scrapers/chocolatey.ts +63 -26
  152. package/src/web/scrapers/choosealicense.ts +3 -18
  153. package/src/web/scrapers/cisa-kev.ts +4 -18
  154. package/src/web/scrapers/clojars.ts +6 -33
  155. package/src/web/scrapers/coingecko.ts +25 -33
  156. package/src/web/scrapers/crates-io.ts +7 -26
  157. package/src/web/scrapers/crossref.ts +4 -18
  158. package/src/web/scrapers/devto.ts +11 -41
  159. package/src/web/scrapers/discogs.ts +7 -10
  160. package/src/web/scrapers/discourse.ts +6 -31
  161. package/src/web/scrapers/dockerhub.ts +12 -35
  162. package/src/web/scrapers/fdroid.ts +8 -33
  163. package/src/web/scrapers/firefox-addons.ts +10 -34
  164. package/src/web/scrapers/flathub.ts +7 -24
  165. package/src/web/scrapers/github-gist.ts +2 -12
  166. package/src/web/scrapers/github.ts +9 -47
  167. package/src/web/scrapers/gitlab.ts +130 -185
  168. package/src/web/scrapers/go-pkg.ts +12 -22
  169. package/src/web/scrapers/hackage.ts +88 -43
  170. package/src/web/scrapers/hackernews.ts +25 -45
  171. package/src/web/scrapers/hex.ts +19 -36
  172. package/src/web/scrapers/huggingface.ts +26 -91
  173. package/src/web/scrapers/iacr.ts +3 -8
  174. package/src/web/scrapers/jetbrains-marketplace.ts +9 -20
  175. package/src/web/scrapers/lemmy.ts +5 -23
  176. package/src/web/scrapers/lobsters.ts +16 -28
  177. package/src/web/scrapers/mastodon.ts +24 -43
  178. package/src/web/scrapers/maven.ts +6 -21
  179. package/src/web/scrapers/mdn.ts +7 -11
  180. package/src/web/scrapers/metacpan.ts +9 -41
  181. package/src/web/scrapers/musicbrainz.ts +4 -28
  182. package/src/web/scrapers/npm.ts +8 -25
  183. package/src/web/scrapers/nuget.ts +14 -37
  184. package/src/web/scrapers/nvd.ts +6 -28
  185. package/src/web/scrapers/ollama.ts +7 -34
  186. package/src/web/scrapers/open-vsx.ts +5 -19
  187. package/src/web/scrapers/opencorporates.ts +30 -14
  188. package/src/web/scrapers/openlibrary.ts +49 -33
  189. package/src/web/scrapers/orcid.ts +4 -18
  190. package/src/web/scrapers/osv.ts +7 -24
  191. package/src/web/scrapers/packagist.ts +9 -24
  192. package/src/web/scrapers/pub-dev.ts +7 -50
  193. package/src/web/scrapers/pubmed.ts +54 -21
  194. package/src/web/scrapers/pypi.ts +8 -26
  195. package/src/web/scrapers/rawg.ts +11 -19
  196. package/src/web/scrapers/readthedocs.ts +4 -9
  197. package/src/web/scrapers/reddit.ts +5 -15
  198. package/src/web/scrapers/repology.ts +8 -20
  199. package/src/web/scrapers/rfc.ts +5 -14
  200. package/src/web/scrapers/rubygems.ts +6 -21
  201. package/src/web/scrapers/searchcode.ts +8 -36
  202. package/src/web/scrapers/sec-edgar.ts +4 -18
  203. package/src/web/scrapers/semantic-scholar.ts +15 -35
  204. package/src/web/scrapers/snapcraft.ts +5 -19
  205. package/src/web/scrapers/sourcegraph.ts +5 -43
  206. package/src/web/scrapers/spdx.ts +4 -18
  207. package/src/web/scrapers/spotify.ts +4 -23
  208. package/src/web/scrapers/stackoverflow.ts +8 -13
  209. package/src/web/scrapers/terraform.ts +9 -37
  210. package/src/web/scrapers/tldr.ts +3 -7
  211. package/src/web/scrapers/twitter.ts +3 -7
  212. package/src/web/scrapers/types.ts +105 -27
  213. package/src/web/scrapers/utils.ts +97 -103
  214. package/src/web/scrapers/vimeo.ts +7 -27
  215. package/src/web/scrapers/vscode-marketplace.ts +8 -17
  216. package/src/web/scrapers/w3c.ts +6 -14
  217. package/src/web/scrapers/wikidata.ts +5 -19
  218. package/src/web/scrapers/wikipedia.ts +2 -12
  219. package/src/web/scrapers/youtube.ts +5 -34
  220. package/src/web/search/index.ts +0 -9
  221. package/src/web/search/providers/anthropic.ts +3 -2
  222. package/src/web/search/providers/brave.ts +3 -18
  223. package/src/web/search/providers/exa.ts +1 -12
  224. package/src/web/search/providers/kimi.ts +5 -44
  225. package/src/web/search/providers/perplexity.ts +1 -12
  226. package/src/web/search/providers/synthetic.ts +3 -26
  227. package/src/web/search/providers/utils.ts +36 -0
  228. package/src/web/search/providers/zai.ts +9 -50
  229. package/src/web/search/types.ts +0 -28
  230. package/src/web/search/utils.ts +17 -0
  231. package/src/tools/output-utils.ts +0 -63
  232. package/src/tools/truncate.ts +0 -385
  233. package/src/web/search/auth.ts +0 -178
package/src/commit/cli.ts CHANGED
@@ -36,7 +36,7 @@ export function parseCommitArgs(args: string[]): CommitCommandArgs | undefined {
36
36
  case "--context": {
37
37
  const value = args[i + 1];
38
38
  if (!value || value.startsWith("-")) {
39
- writeStderr(chalk.red("Error: --context requires a value"));
39
+ process.stderr.write(`${chalk.red("Error: --context requires a value")}\n`);
40
40
  process.exit(1);
41
41
  }
42
42
  result.context = value;
@@ -46,7 +46,7 @@ export function parseCommitArgs(args: string[]): CommitCommandArgs | undefined {
46
46
  case "--model": {
47
47
  const value = args[i + 1];
48
48
  if (!value || value.startsWith("-")) {
49
- writeStderr(chalk.red("Error: --model requires a value"));
49
+ process.stderr.write(`${chalk.red("Error: --model requires a value")}\n`);
50
50
  process.exit(1);
51
51
  }
52
52
  result.model = value;
@@ -58,7 +58,7 @@ export function parseCommitArgs(args: string[]): CommitCommandArgs | undefined {
58
58
  break;
59
59
  default:
60
60
  if (flag.startsWith("-")) {
61
- writeStderr(chalk.red(`Error: Unknown flag ${flag}`));
61
+ process.stderr.write(`${chalk.red(`Error: Unknown flag ${flag}`)}\n`);
62
62
  process.exit(1);
63
63
  }
64
64
  }
@@ -81,13 +81,5 @@ export function printCommitHelp(): void {
81
81
  " --model, -m Override model selection",
82
82
  " --help, -h Show this help message",
83
83
  ];
84
- writeStdout(lines.join("\n"));
85
- }
86
-
87
- function writeStdout(message: string): void {
88
- process.stdout.write(`${message}\n`);
89
- }
90
-
91
- function writeStderr(message: string): void {
92
- process.stderr.write(`${message}\n`);
84
+ process.stdout.write(`${lines.join("\n")}\n`);
93
85
  }
@@ -1,10 +1,11 @@
1
- import type { Api, AssistantMessage, Model, ToolCall } from "@oh-my-pi/pi-ai";
1
+ import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
2
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
3
3
  import { Type } from "@sinclair/typebox";
4
4
  import reduceSystemPrompt from "../../commit/prompts/reduce-system.md" with { type: "text" };
5
5
  import reduceUserPrompt from "../../commit/prompts/reduce-user.md" with { type: "text" };
6
6
  import type { ChangelogCategory, ConventionalAnalysis, FileObservation } from "../../commit/types";
7
7
  import { renderPromptTemplate } from "../../config/prompt-templates";
8
+ import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
8
9
 
9
10
  const ReduceTool = {
10
11
  name: "create_conventional_analysis",
@@ -101,45 +102,3 @@ function parseAnalysisResponse(message: AssistantMessage): ConventionalAnalysis
101
102
  };
102
103
  return normalizeAnalysis(parsed);
103
104
  }
104
-
105
- function parseJsonPayload(text: string): unknown {
106
- const trimmed = text.trim();
107
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
108
- return JSON.parse(trimmed) as unknown;
109
- }
110
- const match = trimmed.match(/\{[\s\S]*\}/);
111
- if (!match) {
112
- throw new Error("No JSON payload found in reduce response");
113
- }
114
- return JSON.parse(match[0]) as unknown;
115
- }
116
-
117
- function normalizeAnalysis(parsed: {
118
- type: ConventionalAnalysis["type"];
119
- scope: string | null;
120
- details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
121
- issue_refs: string[];
122
- }): ConventionalAnalysis {
123
- return {
124
- type: parsed.type,
125
- scope: parsed.scope?.trim() || null,
126
- details: parsed.details.map(detail => ({
127
- text: detail.text.trim(),
128
- changelogCategory: detail.user_visible ? detail.changelog_category : undefined,
129
- userVisible: detail.user_visible ?? false,
130
- })),
131
- issueRefs: parsed.issue_refs ?? [],
132
- };
133
- }
134
-
135
- function extractToolCall(message: AssistantMessage, name: string): ToolCall | undefined {
136
- return message.content.find(content => content.type === "toolCall" && content.name === name) as ToolCall | undefined;
137
- }
138
-
139
- function extractTextContent(message: AssistantMessage): string {
140
- return message.content
141
- .filter(content => content.type === "text")
142
- .map(content => content.text)
143
- .join("")
144
- .trim();
145
- }
@@ -61,12 +61,12 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
61
61
  const git = new ControlledGit(cwd);
62
62
  let stagedFiles = await git.getStagedFiles();
63
63
  if (stagedFiles.length === 0) {
64
- writeStdout("No staged changes detected, staging all changes...");
64
+ process.stdout.write("No staged changes detected, staging all changes...\n");
65
65
  await git.stageAll();
66
66
  stagedFiles = await git.getStagedFiles();
67
67
  }
68
68
  if (stagedFiles.length === 0) {
69
- writeStderr("No changes to commit.");
69
+ process.stderr.write("No changes to commit.\n");
70
70
  return;
71
71
  }
72
72
 
@@ -123,16 +123,16 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
123
123
  const commitMessage = formatCommitMessage(analysis, summary.summary);
124
124
 
125
125
  if (args.dryRun) {
126
- writeStdout("\nGenerated commit message:\n");
127
- writeStdout(commitMessage);
126
+ process.stdout.write("\nGenerated commit message:\n");
127
+ process.stdout.write(`${commitMessage}\n`);
128
128
  return;
129
129
  }
130
130
 
131
131
  await git.commit(commitMessage);
132
- writeStdout("Commit created.");
132
+ process.stdout.write("Commit created.\n");
133
133
  if (args.push) {
134
134
  await git.push();
135
- writeStdout("Pushed to remote.");
135
+ process.stdout.write("Pushed to remote.\n");
136
136
  }
137
137
  }
138
138
 
@@ -163,7 +163,7 @@ async function generateAnalysis(input: {
163
163
  maxFileTokens: input.commitSettings.mapReduceMaxFileTokens,
164
164
  })
165
165
  ) {
166
- writeStdout("Large diff detected, using map-reduce analysis...");
166
+ process.stdout.write("Large diff detected, using map-reduce analysis...\n");
167
167
  return runMapReduceAnalysis({
168
168
  model: input.primaryModel,
169
169
  apiKey: input.primaryApiKey,
@@ -233,11 +233,3 @@ function buildRetryContext(base: string | undefined, errors: string[]): string {
233
233
  errors: errors.join("; "),
234
234
  });
235
235
  }
236
-
237
- function writeStdout(message: string): void {
238
- process.stdout.write(`${message}\n`);
239
- }
240
-
241
- function writeStderr(message: string): void {
242
- process.stderr.write(`${message}\n`);
243
- }
@@ -0,0 +1,44 @@
1
+ import type { AssistantMessage, ToolCall } from "@oh-my-pi/pi-ai";
2
+ import type { ChangelogCategory, ConventionalAnalysis } from "./types";
3
+
4
+ export function extractToolCall(message: AssistantMessage, name: string): ToolCall | undefined {
5
+ return message.content.find(content => content.type === "toolCall" && content.name === name) as ToolCall | undefined;
6
+ }
7
+
8
+ export function extractTextContent(message: AssistantMessage): string {
9
+ return message.content
10
+ .filter(content => content.type === "text")
11
+ .map(content => content.text)
12
+ .join("")
13
+ .trim();
14
+ }
15
+
16
+ export function parseJsonPayload(text: string): unknown {
17
+ const trimmed = text.trim();
18
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
19
+ return JSON.parse(trimmed) as unknown;
20
+ }
21
+ const match = trimmed.match(/\{[\s\S]*\}/);
22
+ if (!match) {
23
+ throw new Error("No JSON payload found in response");
24
+ }
25
+ return JSON.parse(match[0]) as unknown;
26
+ }
27
+
28
+ export function normalizeAnalysis(parsed: {
29
+ type: ConventionalAnalysis["type"];
30
+ scope: string | null;
31
+ details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
32
+ issue_refs: string[];
33
+ }): ConventionalAnalysis {
34
+ return {
35
+ type: parsed.type,
36
+ scope: parsed.scope?.trim() || null,
37
+ details: parsed.details.map(detail => ({
38
+ text: detail.text.trim(),
39
+ changelogCategory: detail.user_visible ? detail.changelog_category : undefined,
40
+ userVisible: detail.user_visible ?? false,
41
+ })),
42
+ issueRefs: parsed.issue_refs ?? [],
43
+ };
44
+ }
@@ -5,6 +5,7 @@ import { getProjectDir, getProjectPromptsDir, getPromptsDir } from "@oh-my-pi/pi
5
5
  import Handlebars from "handlebars";
6
6
  import { computeLineHash } from "../patch/hashline";
7
7
  import { jtdToTypeScript } from "../tools/jtd-to-typescript";
8
+ import { parseCommandArgs, substituteArgs } from "../utils/command-args";
8
9
  import { parseFrontmatter } from "../utils/frontmatter";
9
10
 
10
11
  /**
@@ -313,87 +314,6 @@ function optimizePromptLayout(input: string): string {
313
314
  return s.trim();
314
315
  }
315
316
 
316
- /**
317
- * Parse command arguments respecting quoted strings (bash-style)
318
- * Returns array of arguments
319
- */
320
- export function parseCommandArgs(argsString: string): string[] {
321
- const args: string[] = [];
322
- let current = "";
323
- let inQuote: string | null = null;
324
-
325
- for (let i = 0; i < argsString.length; i++) {
326
- const char = argsString[i];
327
-
328
- if (inQuote) {
329
- if (char === inQuote) {
330
- inQuote = null;
331
- } else {
332
- current += char;
333
- }
334
- } else if (char === '"' || char === "'") {
335
- inQuote = char;
336
- } else if (char === " " || char === "\t") {
337
- if (current) {
338
- args.push(current);
339
- current = "";
340
- }
341
- } else {
342
- current += char;
343
- }
344
- }
345
-
346
- if (current) {
347
- args.push(current);
348
- }
349
-
350
- return args;
351
- }
352
-
353
- /**
354
- * Substitute argument placeholders in template content
355
- * Supports $1, $2, ... for positional args, $@ and $ARGUMENTS for all args
356
- *
357
- * Note: Replacement happens on the template string only. Argument values
358
- * containing patterns like $1, $@, or $ARGUMENTS are NOT recursively substituted.
359
- */
360
- export function substituteArgs(content: string, args: string[]): string {
361
- let result = content;
362
-
363
- // Replace $1, $2, etc. with positional args FIRST (before wildcards)
364
- // This prevents wildcard replacement values containing $<digit> patterns from being re-substituted
365
- result = result.replace(/\$(\d+)/g, (_, num) => {
366
- const index = parseInt(num, 10) - 1;
367
- return args[index] ?? "";
368
- });
369
-
370
- result = result.replace(/\$@\[(\d+)(?::(\d*)?)?\]/g, (_, startRaw: string, lengthRaw?: string) => {
371
- const start = Number.parseInt(startRaw, 10);
372
- if (!Number.isFinite(start) || start < 1) return "";
373
- const startIndex = start - 1;
374
- if (startIndex >= args.length) return "";
375
-
376
- if (lengthRaw === undefined || lengthRaw === "") {
377
- return args.slice(startIndex).join(" ");
378
- }
379
-
380
- const length = Number.parseInt(lengthRaw, 10);
381
- if (!Number.isFinite(length) || length <= 0) return "";
382
- return args.slice(startIndex, startIndex + length).join(" ");
383
- });
384
-
385
- // Pre-compute all args joined (optimization)
386
- const allArgs = args.join(" ");
387
-
388
- // Replace $ARGUMENTS with all args joined (new syntax, aligns with Claude, Codex, OpenCode)
389
- result = result.replace(/\$ARGUMENTS/g, allArgs);
390
-
391
- // Replace $@ with all args joined (existing syntax)
392
- result = result.replace(/\$@/g, allArgs);
393
-
394
- return result;
395
- }
396
-
397
317
  /**
398
318
  * Recursively scan a directory for .md files (and symlinks to .md files) and load them as prompt templates
399
319
  */
@@ -403,7 +403,7 @@ export const SETTINGS_SCHEMA = {
403
403
  // ─────────────────────────────────────────────────────────────────────────
404
404
  "todo.reminders": {
405
405
  type: "boolean",
406
- default: false,
406
+ default: true,
407
407
  ui: { tab: "agent", label: "Todo reminders", description: "Remind agent to complete todos before stopping" },
408
408
  },
409
409
  "todo.reminders.max": {
@@ -511,6 +511,25 @@ export const SETTINGS_SCHEMA = {
511
511
  description: "Ask the agent to describe the intent of each tool call before executing it",
512
512
  },
513
513
  },
514
+ "async.enabled": {
515
+ type: "boolean",
516
+ default: false,
517
+ ui: {
518
+ tab: "tools",
519
+ label: "Async execution",
520
+ description: "Enable async bash commands and background task execution",
521
+ },
522
+ },
523
+ "async.maxJobs": {
524
+ type: "number",
525
+ default: 15,
526
+ ui: {
527
+ tab: "tools",
528
+ label: "Async max jobs",
529
+ description: "Maximum concurrent background jobs (1-100)",
530
+ submenu: true,
531
+ },
532
+ },
514
533
 
515
534
  // ─────────────────────────────────────────────────────────────────────────
516
535
  // Task tool settings
package/src/config.ts CHANGED
@@ -7,6 +7,7 @@ import type { TSchema } from "@sinclair/typebox";
7
7
  import { Value } from "@sinclair/typebox/value";
8
8
  import { Ajv, type ErrorObject, type ValidateFunction } from "ajv";
9
9
  import { JSONC, YAML } from "bun";
10
+ import { expandTilde } from "./tools/path-utils";
10
11
 
11
12
  const priorityList = [
12
13
  { dir: CONFIG_DIR_NAME, globalAgentDir: `${CONFIG_DIR_NAME}/agent` },
@@ -27,9 +28,7 @@ export function getPackageDir(): string {
27
28
  // Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly)
28
29
  const envDir = process.env.PI_PACKAGE_DIR;
29
30
  if (envDir) {
30
- if (envDir === "~") return os.homedir();
31
- if (envDir.startsWith("~/")) return os.homedir() + envDir.slice(1);
32
- return envDir;
31
+ return expandTilde(envDir);
33
32
  }
34
33
 
35
34
  let dir = import.meta.dir;
@@ -11,6 +11,7 @@ import { getSessionsDir } from "@oh-my-pi/pi-utils/dirs";
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";
14
+ import { formatBytes } from "../tools/render-utils";
14
15
  import { openPath } from "../utils/open";
15
16
  import { DebugLogViewerComponent } from "./log-viewer";
16
17
  import { generateHeapSnapshotData, type ProfilerSession, startCpuProfile } from "./profiler";
@@ -417,12 +418,6 @@ export class DebugSelectorComponent extends Container {
417
418
  }
418
419
  }
419
420
 
420
- function formatBytes(bytes: number): string {
421
- if (bytes < 1024) return `${bytes} B`;
422
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
423
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
424
- }
425
-
426
421
  /**
427
422
  * Show the debug selector.
428
423
  */
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * System information collection for debug reports.
3
3
  */
4
+
4
5
  import * as os from "node:os";
6
+ import { formatBytes } from "@oh-my-pi/pi-utils";
5
7
  import { getProjectDir, VERSION } from "@oh-my-pi/pi-utils/dirs";
6
8
 
7
9
  export interface SystemInfo {
@@ -71,12 +73,6 @@ export async function collectSystemInfo(): Promise<SystemInfo> {
71
73
  };
72
74
  }
73
75
 
74
- /** Format bytes to human-readable string */
75
- function formatBytes(bytes: number): string {
76
- const gb = bytes / (1024 * 1024 * 1024);
77
- return `${gb.toFixed(1)} GB`;
78
- }
79
-
80
76
  /** Format system info for display */
81
77
  export function formatSystemInfo(info: SystemInfo): string {
82
78
  const lines = [
@@ -21,6 +21,7 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c
21
21
  import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
22
22
  import { type CustomTool, toolCapability } from "../capability/tool";
23
23
  import type { LoadContext, LoadResult } from "../capability/types";
24
+ import { expandTilde } from "../tools/path-utils";
24
25
  import { parseFrontmatter } from "../utils/frontmatter";
25
26
  import {
26
27
  buildRuleFromMarkdown,
@@ -363,16 +364,11 @@ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<Extens
363
364
  const warnings: string[] = [];
364
365
 
365
366
  const resolveExtensionPath = (rawPath: string): string => {
366
- if (rawPath.startsWith("~/")) {
367
- return path.join(ctx.home, rawPath.slice(2));
367
+ const expanded = expandTilde(rawPath, ctx.home);
368
+ if (path.isAbsolute(expanded)) {
369
+ return expanded;
368
370
  }
369
- if (rawPath.startsWith("~")) {
370
- return path.join(ctx.home, rawPath.slice(1));
371
- }
372
- if (path.isAbsolute(rawPath)) {
373
- return rawPath;
374
- }
375
- return path.resolve(ctx.cwd, rawPath);
371
+ return path.resolve(ctx.cwd, expanded);
376
372
  };
377
373
 
378
374
  const createExtensionModule = (extPath: string, level: "user" | "project"): ExtensionModule => ({
@@ -1,7 +1,3 @@
1
- /**
2
- * Shared helpers for discovery providers.
3
- */
4
- import * as os from "node:os";
5
1
  import * as path from "node:path";
6
2
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
3
  import { FileType, glob } from "@oh-my-pi/pi-natives";
@@ -14,28 +10,6 @@ import { parseFrontmatter } from "../utils/frontmatter";
14
10
  import type { IgnoreMatcher } from "../utils/ignore-files";
15
11
 
16
12
  const VALID_THINKING_LEVELS: readonly string[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
17
- const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
18
-
19
- /**
20
- * Normalize unicode spaces to regular spaces.
21
- */
22
- export function normalizeUnicodeSpaces(str: string): string {
23
- return str.replace(UNICODE_SPACES, " ");
24
- }
25
-
26
- /**
27
- * Expand ~ to home directory and normalize unicode spaces.
28
- */
29
- export function expandPath(p: string): string {
30
- const normalized = normalizeUnicodeSpaces(p);
31
- if (normalized.startsWith("~/")) {
32
- return path.join(os.homedir(), normalized.slice(2));
33
- }
34
- if (normalized.startsWith("~")) {
35
- return path.join(os.homedir(), normalized.slice(1));
36
- }
37
- return normalized;
38
- }
39
13
 
40
14
  /**
41
15
  * Standard paths for each config source.
@@ -10,6 +10,7 @@ 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
+ import { expandTilde } from "../tools/path-utils";
13
14
  import { createSourceMeta, expandEnvVarsDeep, parseJSON } from "./helpers";
14
15
 
15
16
  const PROVIDER_ID = "ssh-json";
@@ -30,14 +31,6 @@ interface SSHConfigFile {
30
31
  >;
31
32
  }
32
33
 
33
- function expandTilde(value: string, home: string): string {
34
- if (value === "~") return home;
35
- if (value.startsWith("~/") || value.startsWith("~\\")) {
36
- return `${home}${value.slice(1)}`;
37
- }
38
- return value;
39
- }
40
-
41
34
  function parsePort(value: number | string | undefined): number | undefined {
42
35
  if (value === undefined) return undefined;
43
36
  if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
@@ -5,53 +5,22 @@
5
5
  */
6
6
  import { Type } from "@sinclair/typebox";
7
7
  import type { CustomTool } from "../extensibility/custom-tools/types";
8
- import { callExaTool, findApiKey, formatSearchResults, isSearchResponse } from "./mcp-client";
8
+ import { createExaTool } from "./factory";
9
9
  import type { ExaRenderDetails } from "./types";
10
10
 
11
11
  /** exa_company - Company research */
12
- export const companyTool: CustomTool<any, ExaRenderDetails> = {
13
- name: "exa_company",
14
- label: "Exa Company",
15
- description: `Research companies using Exa's comprehensive data sources.
12
+ export const companyTool: CustomTool<any, ExaRenderDetails> = createExaTool(
13
+ "exa_company",
14
+ "Exa Company",
15
+ `Research companies using Exa's comprehensive data sources.
16
16
 
17
17
  Returns detailed company information including overview, news, financials, and key people.
18
18
 
19
19
  Parameters:
20
20
  - company_name: Name of the company to research (e.g., "OpenAI", "Google", "Y Combinator")`,
21
21
 
22
- parameters: Type.Object({
22
+ Type.Object({
23
23
  company_name: Type.String({ description: "Name of the company to research" }),
24
24
  }),
25
-
26
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
27
- try {
28
- const apiKey = await findApiKey();
29
- if (!apiKey) {
30
- return {
31
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
32
- details: { error: "EXA_API_KEY not found", toolName: "exa_company" },
33
- };
34
- }
35
- const response = await callExaTool("company_research", params, apiKey);
36
-
37
- if (isSearchResponse(response)) {
38
- const formatted = formatSearchResults(response);
39
- return {
40
- content: [{ type: "text" as const, text: formatted }],
41
- details: { response, toolName: "exa_company" },
42
- };
43
- }
44
-
45
- return {
46
- content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
47
- details: { raw: response, toolName: "exa_company" },
48
- };
49
- } catch (error) {
50
- const message = error instanceof Error ? error.message : String(error);
51
- return {
52
- content: [{ type: "text" as const, text: `Error: ${message}` }],
53
- details: { error: message, toolName: "exa_company" },
54
- };
55
- }
56
- },
57
- };
25
+ "company_research",
26
+ );
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared factory for creating Exa tools with consistent error handling and response formatting.
3
+ */
4
+ import type { TObject, TProperties } from "@sinclair/typebox";
5
+ import type { CustomTool } from "../extensibility/custom-tools/types";
6
+ import { callExaTool, findApiKey, formatSearchResults, isSearchResponse } from "./mcp-client";
7
+ import type { ExaRenderDetails } from "./types";
8
+
9
+ /** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */
10
+ export function createExaTool(
11
+ name: string,
12
+ label: string,
13
+ description: string,
14
+ parameters: TObject<TProperties>,
15
+ mcpToolName: string,
16
+ options?: {
17
+ /** When true, checks isSearchResponse and formats with formatSearchResults. Default: true */
18
+ formatResponse?: boolean;
19
+ /** Transform params before passing to callExaTool */
20
+ transformParams?: (params: Record<string, unknown>) => Record<string, unknown>;
21
+ },
22
+ ): CustomTool<any, ExaRenderDetails> {
23
+ const formatResponse = options?.formatResponse ?? true;
24
+ const transformParams = options?.transformParams;
25
+
26
+ return {
27
+ name,
28
+ label,
29
+ description,
30
+ parameters,
31
+ async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
32
+ try {
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
+ }
40
+ const args = transformParams ? transformParams(params as Record<string, unknown>) : params;
41
+ const response = await callExaTool(mcpToolName, args, apiKey);
42
+
43
+ if (formatResponse && isSearchResponse(response)) {
44
+ const formatted = formatSearchResults(response);
45
+ return {
46
+ content: [{ type: "text" as const, text: formatted }],
47
+ details: { response, toolName: name },
48
+ };
49
+ }
50
+
51
+ return {
52
+ content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
53
+ details: { raw: response, toolName: name },
54
+ };
55
+ } catch (error) {
56
+ const message = error instanceof Error ? error.message : String(error);
57
+ return {
58
+ content: [{ type: "text" as const, text: `Error: ${message}` }],
59
+ details: { error: message, toolName: name },
60
+ };
61
+ }
62
+ },
63
+ };
64
+ }
package/src/exa/index.ts CHANGED
@@ -8,7 +8,6 @@
8
8
  * - 2 researcher tools (start, poll)
9
9
  * - 14 websets tools (CRUD, items, search, enrichment, monitor)
10
10
  */
11
- import type { ExaSettings } from "../config/settings";
12
11
  import type { CustomTool } from "../extensibility/custom-tools/types";
13
12
  import { companyTool } from "./company";
14
13
  import { linkedinTool } from "./linkedin";
@@ -26,21 +25,6 @@ export const exaTools: CustomTool<any, ExaRenderDetails>[] = [
26
25
  ...websetsTools,
27
26
  ];
28
27
 
29
- /** Get Exa tools filtered by settings */
30
- export function getExaTools(settings: Required<ExaSettings>): CustomTool<any, ExaRenderDetails>[] {
31
- if (!settings.enabled) return [];
32
-
33
- const tools: CustomTool<any, ExaRenderDetails>[] = [];
34
-
35
- if (settings.enableSearch) tools.push(...searchTools);
36
- if (settings.enableLinkedin) tools.push(linkedinTool);
37
- if (settings.enableCompany) tools.push(companyTool);
38
- if (settings.enableResearcher) tools.push(...researcherTools);
39
- if (settings.enableWebsets) tools.push(...websetsTools);
40
-
41
- return tools;
42
- }
43
-
44
28
  export { companyTool } from "./company";
45
29
  export { linkedinTool } from "./linkedin";
46
30
  export {