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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +33 -14
  4. package/scripts/generate-docs-index.ts +2 -2
  5. package/src/capability/index.ts +1 -2
  6. package/src/cli/args.ts +3 -3
  7. package/src/cli/config-cli.ts +1 -1
  8. package/src/cli/file-processor.ts +1 -2
  9. package/src/cli/grep-cli.ts +1 -1
  10. package/src/cli/jupyter-cli.ts +1 -1
  11. package/src/cli/plugin-cli.ts +1 -1
  12. package/src/cli/setup-cli.ts +1 -1
  13. package/src/cli/shell-cli.ts +1 -1
  14. package/src/cli/ssh-cli.ts +1 -1
  15. package/src/cli/stats-cli.ts +1 -2
  16. package/src/cli/update-cli.ts +1 -2
  17. package/src/cli/web-search-cli.ts +1 -1
  18. package/src/cli.ts +1 -1
  19. package/src/commands/launch.ts +2 -1
  20. package/src/commit/agentic/agent.ts +2 -1
  21. package/src/commit/agentic/index.ts +1 -2
  22. package/src/commit/agentic/prompts/system.md +3 -3
  23. package/src/commit/agentic/tools/propose-changelog.ts +30 -19
  24. package/src/commit/changelog/generate.ts +16 -6
  25. package/src/commit/changelog/index.ts +2 -1
  26. package/src/commit/pipeline.ts +1 -2
  27. package/src/commit/prompts/reduce-system.md +1 -1
  28. package/src/commit/types.ts +10 -1
  29. package/src/config/keybindings.ts +1 -2
  30. package/src/config/model-registry.ts +1 -1
  31. package/src/config/prompt-templates.ts +14 -2
  32. package/src/config/settings-schema.ts +36 -4
  33. package/src/config/settings.ts +19 -2
  34. package/src/config.ts +1 -2
  35. package/src/debug/index.ts +1 -1
  36. package/src/debug/report-bundle.ts +1 -2
  37. package/src/debug/system-info.ts +1 -2
  38. package/src/discovery/agents.ts +2 -2
  39. package/src/discovery/builtin.ts +8 -9
  40. package/src/discovery/claude-plugins.ts +2 -2
  41. package/src/discovery/claude.ts +30 -12
  42. package/src/discovery/codex.ts +3 -3
  43. package/src/discovery/cursor.ts +5 -4
  44. package/src/discovery/gemini.ts +5 -5
  45. package/src/discovery/helpers.ts +47 -69
  46. package/src/discovery/mcp-json.ts +3 -3
  47. package/src/discovery/opencode.ts +7 -8
  48. package/src/discovery/ssh.ts +3 -3
  49. package/src/discovery/vscode.ts +3 -2
  50. package/src/discovery/windsurf.ts +3 -2
  51. package/src/exa/company.ts +1 -1
  52. package/src/exa/factory.ts +1 -6
  53. package/src/exa/linkedin.ts +1 -1
  54. package/src/exa/mcp-client.ts +19 -8
  55. package/src/exa/search.ts +2 -2
  56. package/src/exa/types.ts +3 -3
  57. package/src/exec/bash-executor.ts +2 -1
  58. package/src/exec/non-interactive-env.ts +43 -0
  59. package/src/export/custom-share.ts +1 -1
  60. package/src/export/html/index.ts +1 -2
  61. package/src/extensibility/custom-commands/loader.ts +1 -2
  62. package/src/extensibility/plugins/installer.ts +1 -2
  63. package/src/extensibility/plugins/loader.ts +1 -2
  64. package/src/extensibility/plugins/manager.ts +3 -2
  65. package/src/extensibility/skills.ts +59 -115
  66. package/src/index.ts +1 -3
  67. package/src/internal-urls/docs-index.generated.ts +1 -1
  68. package/src/ipy/executor.ts +1 -2
  69. package/src/ipy/gateway-coordinator.ts +1 -2
  70. package/src/ipy/modules.ts +1 -1
  71. package/src/ipy/runtime.ts +2 -3
  72. package/src/main.ts +1 -2
  73. package/src/mcp/config.ts +2 -2
  74. package/src/mcp/transports/stdio.ts +1 -2
  75. package/src/memories/index.ts +1 -2
  76. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  77. package/src/modes/components/extensions/inspector-panel.ts +8 -2
  78. package/src/modes/components/footer.ts +1 -2
  79. package/src/modes/components/settings-defs.ts +17 -1
  80. package/src/modes/components/status-line/segments.ts +1 -2
  81. package/src/modes/components/status-line.ts +7 -5
  82. package/src/modes/components/tool-execution.ts +3 -10
  83. package/src/modes/components/welcome.ts +1 -1
  84. package/src/modes/controllers/command-controller.ts +1 -2
  85. package/src/modes/controllers/mcp-command-controller.ts +5 -4
  86. package/src/modes/controllers/selector-controller.ts +22 -1
  87. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  88. package/src/modes/interactive-mode.ts +11 -3
  89. package/src/modes/oauth-manual-input.ts +42 -0
  90. package/src/modes/shared.ts +1 -2
  91. package/src/modes/theme/theme.ts +1 -2
  92. package/src/modes/types.ts +2 -0
  93. package/src/patch/hashline.ts +19 -1
  94. package/src/patch/index.ts +1 -25
  95. package/src/prompts/agents/designer.md +7 -10
  96. package/src/prompts/agents/explore.md +15 -23
  97. package/src/prompts/agents/init.md +23 -23
  98. package/src/prompts/agents/plan.md +14 -77
  99. package/src/prompts/agents/reviewer.md +6 -5
  100. package/src/prompts/agents/task.md +13 -11
  101. package/src/prompts/compaction/branch-summary.md +3 -3
  102. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  103. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  104. package/src/prompts/compaction/compaction-summary.md +5 -5
  105. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  106. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  107. package/src/prompts/memories/consolidation.md +5 -5
  108. package/src/prompts/memories/read-path.md +6 -6
  109. package/src/prompts/memories/stage_one_input.md +1 -1
  110. package/src/prompts/memories/stage_one_system.md +5 -5
  111. package/src/prompts/review-request.md +4 -4
  112. package/src/prompts/system/agent-creation-architect.md +17 -17
  113. package/src/prompts/system/agent-creation-user.md +2 -2
  114. package/src/prompts/system/commit-message-system.md +2 -0
  115. package/src/prompts/system/custom-system-prompt.md +4 -4
  116. package/src/prompts/system/plan-mode-active.md +20 -20
  117. package/src/prompts/system/plan-mode-approved.md +7 -7
  118. package/src/prompts/system/plan-mode-reference.md +2 -2
  119. package/src/prompts/system/plan-mode-subagent.md +8 -8
  120. package/src/prompts/system/subagent-submit-reminder.md +5 -5
  121. package/src/prompts/system/subagent-system-prompt.md +29 -22
  122. package/src/prompts/system/subagent-user-prompt.md +7 -3
  123. package/src/prompts/system/summarization-system.md +1 -1
  124. package/src/prompts/system/system-prompt.md +214 -226
  125. package/src/prompts/system/title-system.md +2 -2
  126. package/src/prompts/system/ttsr-interrupt.md +1 -1
  127. package/src/prompts/system/web-search.md +16 -16
  128. package/src/prompts/tools/ask.md +1 -3
  129. package/src/prompts/tools/await.md +2 -4
  130. package/src/prompts/tools/bash.md +5 -7
  131. package/src/prompts/tools/browser.md +4 -6
  132. package/src/prompts/tools/calculator.md +1 -3
  133. package/src/prompts/tools/cancel-job.md +2 -4
  134. package/src/prompts/tools/exit-plan-mode.md +7 -7
  135. package/src/prompts/tools/fetch.md +0 -2
  136. package/src/prompts/tools/find.md +3 -5
  137. package/src/prompts/tools/gemini-image.md +6 -22
  138. package/src/prompts/tools/grep.md +4 -6
  139. package/src/prompts/tools/hashline.md +56 -15
  140. package/src/prompts/tools/lsp.md +1 -3
  141. package/src/prompts/tools/patch.md +7 -9
  142. package/src/prompts/tools/python.md +10 -14
  143. package/src/prompts/tools/read.md +0 -2
  144. package/src/prompts/tools/replace.md +5 -7
  145. package/src/prompts/tools/ssh.md +3 -5
  146. package/src/prompts/tools/task-summary.md +4 -4
  147. package/src/prompts/tools/task.md +7 -9
  148. package/src/prompts/tools/todo-write.md +7 -9
  149. package/src/prompts/tools/web-search.md +3 -5
  150. package/src/prompts/tools/write.md +3 -5
  151. package/src/sdk.ts +4 -2
  152. package/src/session/agent-session.ts +10 -26
  153. package/src/session/agent-storage.ts +1 -2
  154. package/src/session/history-storage.ts +1 -2
  155. package/src/session/session-manager.ts +10 -2
  156. package/src/slash-commands/builtin-registry.ts +26 -1
  157. package/src/ssh/connection-manager.ts +11 -2
  158. package/src/ssh/sshfs-mount.ts +7 -1
  159. package/src/system-prompt.ts +29 -103
  160. package/src/task/agents.ts +1 -1
  161. package/src/task/index.ts +211 -70
  162. package/src/task/render.ts +24 -8
  163. package/src/task/types.ts +6 -1
  164. package/src/task/worktree.ts +394 -32
  165. package/src/tools/ask.ts +0 -1
  166. package/src/tools/bash-interactive.ts +2 -45
  167. package/src/tools/bash.ts +5 -5
  168. package/src/tools/browser.ts +1 -2
  169. package/src/tools/gemini-image.ts +8 -28
  170. package/src/tools/json-tree.ts +2 -1
  171. package/src/tools/python.ts +1 -1
  172. package/src/tools/read.ts +1 -2
  173. package/src/tools/submit-result.ts +22 -23
  174. package/src/utils/commit-message-generator.ts +132 -0
  175. package/src/utils/tools-manager.ts +1 -2
  176. package/src/web/scrapers/artifacthub.ts +2 -1
  177. package/src/web/scrapers/aur.ts +2 -1
  178. package/src/web/scrapers/biorxiv.ts +2 -1
  179. package/src/web/scrapers/bluesky.ts +2 -1
  180. package/src/web/scrapers/chocolatey.ts +2 -1
  181. package/src/web/scrapers/cisa-kev.ts +2 -1
  182. package/src/web/scrapers/clojars.ts +2 -1
  183. package/src/web/scrapers/coingecko.ts +2 -1
  184. package/src/web/scrapers/crates-io.ts +2 -1
  185. package/src/web/scrapers/crossref.ts +2 -1
  186. package/src/web/scrapers/discogs.ts +3 -1
  187. package/src/web/scrapers/discourse.ts +2 -1
  188. package/src/web/scrapers/dockerhub.ts +2 -1
  189. package/src/web/scrapers/fdroid.ts +2 -1
  190. package/src/web/scrapers/firefox-addons.ts +2 -1
  191. package/src/web/scrapers/flathub.ts +2 -1
  192. package/src/web/scrapers/gitlab.ts +1 -1
  193. package/src/web/scrapers/go-pkg.ts +2 -1
  194. package/src/web/scrapers/hackage.ts +2 -1
  195. package/src/web/scrapers/hackernews.ts +2 -1
  196. package/src/web/scrapers/hex.ts +2 -1
  197. package/src/web/scrapers/huggingface.ts +2 -1
  198. package/src/web/scrapers/jetbrains-marketplace.ts +2 -1
  199. package/src/web/scrapers/lemmy.ts +2 -1
  200. package/src/web/scrapers/lobsters.ts +2 -1
  201. package/src/web/scrapers/mastodon.ts +2 -1
  202. package/src/web/scrapers/maven.ts +2 -1
  203. package/src/web/scrapers/mdn.ts +2 -1
  204. package/src/web/scrapers/metacpan.ts +2 -1
  205. package/src/web/scrapers/musicbrainz.ts +3 -1
  206. package/src/web/scrapers/npm.ts +2 -1
  207. package/src/web/scrapers/nuget.ts +2 -1
  208. package/src/web/scrapers/nvd.ts +2 -1
  209. package/src/web/scrapers/ollama.ts +2 -1
  210. package/src/web/scrapers/open-vsx.ts +2 -1
  211. package/src/web/scrapers/opencorporates.ts +2 -1
  212. package/src/web/scrapers/openlibrary.ts +2 -1
  213. package/src/web/scrapers/orcid.ts +3 -1
  214. package/src/web/scrapers/osv.ts +2 -1
  215. package/src/web/scrapers/packagist.ts +2 -1
  216. package/src/web/scrapers/pub-dev.ts +2 -1
  217. package/src/web/scrapers/pubmed.ts +2 -1
  218. package/src/web/scrapers/pypi.ts +2 -1
  219. package/src/web/scrapers/rawg.ts +2 -8
  220. package/src/web/scrapers/reddit.ts +2 -1
  221. package/src/web/scrapers/repology.ts +2 -1
  222. package/src/web/scrapers/rfc.ts +2 -1
  223. package/src/web/scrapers/rubygems.ts +2 -1
  224. package/src/web/scrapers/searchcode.ts +2 -1
  225. package/src/web/scrapers/sec-edgar.ts +2 -1
  226. package/src/web/scrapers/semantic-scholar.ts +2 -1
  227. package/src/web/scrapers/snapcraft.ts +2 -1
  228. package/src/web/scrapers/sourcegraph.ts +2 -1
  229. package/src/web/scrapers/spdx.ts +2 -1
  230. package/src/web/scrapers/stackoverflow.ts +2 -1
  231. package/src/web/scrapers/terraform.ts +2 -1
  232. package/src/web/scrapers/types.ts +0 -11
  233. package/src/web/scrapers/vimeo.ts +2 -1
  234. package/src/web/scrapers/vscode-marketplace.ts +2 -1
  235. package/src/web/scrapers/w3c.ts +2 -1
  236. package/src/web/scrapers/wikidata.ts +2 -1
  237. package/src/web/search/index.ts +10 -14
  238. package/src/web/search/provider.ts +2 -2
  239. package/src/web/search/providers/codex.ts +1 -2
  240. package/src/web/search/providers/exa.ts +42 -10
  241. package/src/web/search/providers/gemini.ts +1 -1
  242. package/src/web/search/providers/perplexity.ts +20 -9
  243. package/src/web/search/providers/utils.ts +1 -1
@@ -13,8 +13,8 @@ import { resolveReadPath } from "./path-utils";
13
13
  const DEFAULT_MODEL = "gemini-3-pro-image-preview";
14
14
  const DEFAULT_OPENROUTER_MODEL = "google/gemini-3-pro-image-preview";
15
15
  const DEFAULT_ANTIGRAVITY_MODEL = "gemini-3-pro-image";
16
- const DEFAULT_TIMEOUT_SECONDS = 120;
17
- const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
16
+ const IMAGE_TIMEOUT = 3 * 60 * 1000; // 3 minutes
17
+ const MAX_IMAGE_SIZE = 35 * 1024 * 1024;
18
18
 
19
19
  const ANTIGRAVITY_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
20
20
  const IMAGE_SYSTEM_INSTRUCTION =
@@ -76,13 +76,7 @@ const baseImageSchema = Type.Object(
76
76
  style: Type.Optional(
77
77
  Type.String({
78
78
  description:
79
- "Artistic style, mood, color grading (e.g., 'film noir mood, cinematic color grading', 'Studio Ghibli watercolor', 'photorealistic').",
80
- }),
81
- ),
82
- camera: Type.Optional(
83
- Type.String({
84
- description:
85
- "Lens and camera specs (e.g., 'Shot on 35mm, f/1.8', 'macro lens, extreme close-up', '85mm portrait lens').",
79
+ "Artistic style, mood, color grading, camera (e.g., 'film noir mood, cinematic color grading', 'Studio Ghibli watercolor', 'photorealistic').",
86
80
  }),
87
81
  ),
88
82
  text: Type.Optional(
@@ -94,23 +88,16 @@ const baseImageSchema = Type.Object(
94
88
  changes: Type.Optional(
95
89
  Type.Array(Type.String(), {
96
90
  description:
97
- "For edits: specific changes to make (e.g., ['Change the tie to green', 'Remove the car in background']). Use with input_images.",
98
- }),
99
- ),
100
- preserve: Type.Optional(
101
- Type.String({
102
- description:
103
- "For edits: what to keep unchanged (e.g., 'identity, face, hairstyle, lighting'). Use with input_images and changes.",
91
+ "For edits: specific changes to make, as well as, what to keep unchanged (e.g., ['Change the tie to green', 'Remove the car in background']). Use with input_images.",
104
92
  }),
105
93
  ),
106
94
  aspect_ratio: Type.Optional(aspectRatioSchema),
107
95
  image_size: Type.Optional(imageSizeSchema),
108
- input_images: Type.Optional(
96
+ input: Type.Optional(
109
97
  Type.Array(inputImageSchema, {
110
98
  description: "Optional input images for edits or variations.",
111
99
  }),
112
100
  ),
113
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 120)" })),
114
101
  },
115
102
  { additionalProperties: false },
116
103
  );
@@ -136,7 +123,6 @@ function assemblePrompt(params: GeminiImageParams): string {
136
123
  // Technical details as separate sentences
137
124
  if (params.composition) parts.push(params.composition);
138
125
  if (params.lighting) parts.push(params.lighting);
139
- if (params.camera) parts.push(params.camera);
140
126
  if (params.style) parts.push(params.style);
141
127
 
142
128
  // Join with periods for sentence structure
@@ -150,9 +136,6 @@ function assemblePrompt(params: GeminiImageParams): string {
150
136
  // Edit mode: changes and preserve directives
151
137
  if (params.changes?.length) {
152
138
  prompt += `\n\nChanges:\n${params.changes.map(c => `- ${c}`).join("\n")}`;
153
- if (params.preserve) {
154
- prompt += `\n\nPreserve: ${params.preserve}`;
155
- }
156
139
  }
157
140
 
158
141
  return prompt;
@@ -638,16 +621,13 @@ export const geminiImageTool: CustomTool<typeof geminiImageSchema, GeminiImageTo
638
621
  const cwd = ctx.sessionManager.getCwd();
639
622
 
640
623
  const resolvedImages: InlineImageData[] = [];
641
- if (params.input_images?.length) {
642
- for (const input of params.input_images) {
624
+ if (params.input?.length) {
625
+ for (const input of params.input) {
643
626
  resolvedImages.push(await resolveInputImage(input, cwd));
644
627
  }
645
628
  }
646
629
 
647
- const { timeout: rawTimeout = DEFAULT_TIMEOUT_SECONDS } = params;
648
- // Clamp to reasonable range: 1s - 600s (10 min)
649
- const timeoutSeconds = Math.max(1, Math.min(600, rawTimeout));
650
- const requestSignal = ptree.combineSignals(signal, timeoutSeconds * 1000);
630
+ const requestSignal = ptree.combineSignals(signal, IMAGE_TIMEOUT);
651
631
 
652
632
  if (provider === "antigravity") {
653
633
  if (!apiKey.projectId) {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * JSON tree rendering utilities shared across tool renderers.
3
3
  */
4
+ import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
4
5
  import type { Theme } from "../modes/theme/theme";
5
6
  import { truncateToWidth } from "./render-utils";
6
7
 
@@ -13,7 +14,7 @@ export const JSON_TREE_SCALAR_LEN_COLLAPSED = 60;
13
14
  export const JSON_TREE_SCALAR_LEN_EXPANDED = 2000;
14
15
 
15
16
  /** Keys injected by the harness that should not be displayed to users */
16
- const HIDDEN_ARG_KEYS = new Set(["agent__intent"]);
17
+ const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD]);
17
18
 
18
19
  /** Strip harness-internal keys from tool args for display */
19
20
  export function stripInternalArgs(args: Record<string, unknown>): Record<string, unknown> {
@@ -4,7 +4,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
4
4
  import type { ImageContent } from "@oh-my-pi/pi-ai";
5
5
  import type { Component } from "@oh-my-pi/pi-tui";
6
6
  import { Markdown, Text } from "@oh-my-pi/pi-tui";
7
- import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
7
+ import { getProjectDir } from "@oh-my-pi/pi-utils";
8
8
  import { type Static, Type } from "@sinclair/typebox";
9
9
  import { renderPromptTemplate } from "../config/prompt-templates";
10
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
package/src/tools/read.ts CHANGED
@@ -5,8 +5,7 @@ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
5
5
  import { FileType, glob } from "@oh-my-pi/pi-natives";
6
6
  import type { Component } from "@oh-my-pi/pi-tui";
7
7
  import { Text } from "@oh-my-pi/pi-tui";
8
- import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
9
- import { getRemoteDir } from "@oh-my-pi/pi-utils/dirs";
8
+ import { getRemoteDir, ptree, untilAborted } from "@oh-my-pi/pi-utils";
10
9
  import { type Static, Type } from "@sinclair/typebox";
11
10
  import { renderPromptTemplate } from "../config/prompt-templates";
12
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -4,8 +4,7 @@
4
4
  * Subagents must call this tool to finish and return structured JSON output.
5
5
  */
6
6
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
7
- import { StringEnum } from "@oh-my-pi/pi-ai";
8
- import type { Static, TObject } from "@sinclair/typebox";
7
+ import type { Static, TSchema } from "@sinclair/typebox";
9
8
  import { Type } from "@sinclair/typebox";
10
9
  import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
11
10
  import { subprocessToolRegistry } from "../task/subprocess-tool-registry";
@@ -52,13 +51,13 @@ function formatAjvErrors(errors: ErrorObject[] | null | undefined): string {
52
51
  .join("; ");
53
52
  }
54
53
 
55
- export class SubmitResultTool implements AgentTool<TObject, SubmitResultDetails> {
54
+ export class SubmitResultTool implements AgentTool<TSchema, SubmitResultDetails> {
56
55
  readonly name = "submit_result";
57
56
  readonly label = "Submit Result";
58
57
  readonly description =
59
58
  "Finish the task with structured JSON output. Call exactly once at the end of the task.\n\n" +
60
- "If you cannot complete the task, call with status='aborted' and an error message.";
61
- readonly parameters: TObject;
59
+ "If you cannot complete the task, call with an error message payload.";
60
+ readonly parameters: TSchema;
62
61
  readonly strict = true;
63
62
 
64
63
  readonly #validate?: ValidateFunction;
@@ -92,45 +91,45 @@ export class SubmitResultTool implements AgentTool<TObject, SubmitResultDetails>
92
91
  })
93
92
  : Type.Object({}, { additionalProperties: true, description: "Structured JSON output (no schema specified)" });
94
93
 
95
- this.parameters = Type.Object({
96
- data: Type.Optional(dataSchema),
97
- status: Type.Optional(
98
- StringEnum(["success", "aborted"], {
99
- description: "Use 'aborted' if the task cannot be completed, defaults to 'success'",
100
- }),
101
- ),
102
- error: Type.Optional(Type.String({ description: "Error message when status is 'aborted'" })),
103
- });
94
+ this.parameters = Type.Union([
95
+ Type.Object({
96
+ data: dataSchema,
97
+ }),
98
+ Type.Object({
99
+ error: Type.String({ description: "Error message when the task cannot be completed" }),
100
+ }),
101
+ ]);
104
102
  }
105
103
 
106
104
  async execute(
107
105
  _toolCallId: string,
108
- params: Static<TObject>,
106
+ params: Static<TSchema>,
109
107
  _signal?: AbortSignal,
110
108
  _onUpdate?: AgentToolUpdateCallback<SubmitResultDetails>,
111
109
  _context?: AgentToolContext,
112
110
  ): Promise<AgentToolResult<SubmitResultDetails>> {
113
- const status = (params.status ?? "success") as "success" | "aborted";
111
+ const raw = params as Record<string, unknown>;
112
+ const errorMessage = typeof raw.error === "string" ? raw.error : undefined;
113
+ const status = errorMessage !== undefined ? "aborted" : "success";
114
+ const data = raw.data;
114
115
 
115
- // Skip validation when aborting - data is optional for aborts
116
116
  if (status === "success") {
117
- if (params.data === undefined || params.data === null) {
118
- throw new Error("data is required when status is 'success' (got null/undefined)");
117
+ if (data === undefined || data === null) {
118
+ throw new Error("data is required when submit_result indicates success");
119
119
  }
120
120
  if (this.#schemaError) {
121
121
  throw new Error(`Invalid output schema: ${this.#schemaError}`);
122
122
  }
123
- if (this.#validate && !this.#validate(params.data)) {
123
+ if (this.#validate && !this.#validate(data)) {
124
124
  throw new Error(`Output does not match schema: ${formatAjvErrors(this.#validate.errors)}`);
125
125
  }
126
126
  }
127
127
 
128
- const responseText =
129
- status === "aborted" ? `Task aborted: ${params.error || "No reason provided"}` : "Result submitted.";
128
+ const responseText = status === "aborted" ? `Task aborted: ${errorMessage}` : "Result submitted.";
130
129
 
131
130
  return {
132
131
  content: [{ type: "text", text: responseText }],
133
- details: { data: params.data, status, error: params.error as string | undefined },
132
+ details: { data, status, error: errorMessage },
134
133
  };
135
134
  }
136
135
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Generate commit messages from diffs using a smol, fast model.
3
+ * Follows the same pattern as title-generator.ts.
4
+ */
5
+ import type { Api, Model } from "@oh-my-pi/pi-ai";
6
+ import { completeSimple } from "@oh-my-pi/pi-ai";
7
+ import { logger } from "@oh-my-pi/pi-utils";
8
+ import type { ModelRegistry } from "../config/model-registry";
9
+ import { parseModelString } from "../config/model-resolver";
10
+ import { renderPromptTemplate } from "../config/prompt-templates";
11
+ import MODEL_PRIO from "../priority.json" with { type: "json" };
12
+ import commitSystemPrompt from "../prompts/system/commit-message-system.md" with { type: "text" };
13
+
14
+ const COMMIT_SYSTEM_PROMPT = renderPromptTemplate(commitSystemPrompt);
15
+ const MAX_DIFF_CHARS = 4000;
16
+
17
+ /** File patterns that should be excluded from commit message generation diffs. */
18
+ const NOISE_SUFFIXES = [".lock", ".lockb", "-lock.json", "-lock.yaml"];
19
+
20
+ /** Strip diff hunks for noisy files that drown out real changes. */
21
+ function filterDiffNoise(diff: string): string {
22
+ const lines = diff.split("\n");
23
+ const filtered: string[] = [];
24
+ let skip = false;
25
+ for (const line of lines) {
26
+ if (line.startsWith("diff --git ")) {
27
+ const bPath = line.split(" b/")[1];
28
+ skip = bPath != null && NOISE_SUFFIXES.some(s => bPath.endsWith(s));
29
+ }
30
+ if (!skip) filtered.push(line);
31
+ }
32
+ return filtered.join("\n");
33
+ }
34
+
35
+ function getSmolModelCandidates(registry: ModelRegistry, savedSmolModel?: string): Model<Api>[] {
36
+ const availableModels = registry.getAvailable();
37
+ if (availableModels.length === 0) return [];
38
+
39
+ const candidates: Model<Api>[] = [];
40
+ const addCandidate = (model?: Model<Api>): void => {
41
+ if (!model) return;
42
+ if (candidates.some(c => c.provider === model.provider && c.id === model.id)) return;
43
+ candidates.push(model);
44
+ };
45
+
46
+ if (savedSmolModel) {
47
+ const parsed = parseModelString(savedSmolModel);
48
+ if (parsed) {
49
+ const match = availableModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
50
+ addCandidate(match);
51
+ }
52
+ }
53
+
54
+ for (const pattern of MODEL_PRIO.smol) {
55
+ const needle = pattern.toLowerCase();
56
+ addCandidate(availableModels.find(m => m.id.toLowerCase() === needle));
57
+ addCandidate(availableModels.find(m => m.id.toLowerCase().includes(needle)));
58
+ }
59
+
60
+ for (const model of availableModels) {
61
+ addCandidate(model);
62
+ }
63
+
64
+ return candidates;
65
+ }
66
+
67
+ /**
68
+ * Generate a commit message from a unified diff.
69
+ * Returns null if generation fails (caller should fall back to generic message).
70
+ */
71
+ export async function generateCommitMessage(
72
+ diff: string,
73
+ registry: ModelRegistry,
74
+ savedSmolModel?: string,
75
+ sessionId?: string,
76
+ ): Promise<string | null> {
77
+ const candidates = getSmolModelCandidates(registry, savedSmolModel);
78
+ if (candidates.length === 0) {
79
+ logger.debug("commit-msg-generator: no smol model found");
80
+ return null;
81
+ }
82
+
83
+ const cleanDiff = filterDiffNoise(diff);
84
+ const truncatedDiff =
85
+ cleanDiff.length > MAX_DIFF_CHARS ? `${cleanDiff.slice(0, MAX_DIFF_CHARS)}\n… (truncated)` : cleanDiff;
86
+ if (!truncatedDiff.trim()) {
87
+ logger.debug("commit-msg-generator: diff is empty after noise filtering");
88
+ return null;
89
+ }
90
+ const userMessage = `<diff>\n${truncatedDiff}\n</diff>`;
91
+
92
+ for (const model of candidates) {
93
+ const apiKey = await registry.getApiKey(model, sessionId);
94
+ if (!apiKey) continue;
95
+
96
+ try {
97
+ const response = await completeSimple(
98
+ model,
99
+ {
100
+ systemPrompt: COMMIT_SYSTEM_PROMPT,
101
+ messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
102
+ },
103
+ { apiKey, maxTokens: 60 },
104
+ );
105
+
106
+ if (response.stopReason === "error") {
107
+ logger.debug("commit-msg-generator: error", { model: model.id, error: response.errorMessage });
108
+ continue;
109
+ }
110
+
111
+ let msg = "";
112
+ for (const content of response.content) {
113
+ if (content.type === "text") msg += content.text;
114
+ }
115
+ msg = msg.trim();
116
+ if (!msg) continue;
117
+
118
+ // Clean up: remove wrapping quotes, backticks, trailing period
119
+ msg = msg.replace(/^[`"']|[`"']$/g, "").replace(/\.$/, "");
120
+
121
+ logger.debug("commit-msg-generator: generated", { model: model.id, msg });
122
+ return msg;
123
+ } catch (err) {
124
+ logger.debug("commit-msg-generator: error", {
125
+ model: model.id,
126
+ error: err instanceof Error ? err.message : String(err),
127
+ });
128
+ }
129
+ }
130
+
131
+ return null;
132
+ }
@@ -1,8 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { logger, ptree, TempDir } from "@oh-my-pi/pi-utils";
5
- import { APP_NAME, getToolsDir } from "@oh-my-pi/pi-utils/dirs";
4
+ import { APP_NAME, getToolsDir, logger, ptree, TempDir } from "@oh-my-pi/pi-utils";
6
5
 
7
6
  const TOOLS_DIR = getToolsDir();
8
7
  const TOOL_DOWNLOAD_TIMEOUT_MS = 15000;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types";
3
4
 
4
5
  interface ArtifactHubMaintainer {
5
6
  name: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types";
3
4
 
4
5
  interface AurPackage {
5
6
  Name: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, loadPage } from "./types";
3
4
 
4
5
  interface BiorxivPaper {
5
6
  biorxiv_doi?: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatNumber, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatNumber, loadPage } from "./types";
3
4
 
4
5
  const API_BASE = "https://public.api.bsky.app/xrpc";
5
6
 
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types";
3
4
 
4
5
  interface NuGetODataEntry {
5
6
  Id: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, loadPage } from "./types";
3
4
 
4
5
  interface KevEntry {
5
6
  cveID: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatNumber, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatNumber, loadPage } from "./types";
3
4
  import { asNumber, asString, isRecord } from "./utils";
4
5
 
5
6
  function formatLicenses(licenses: unknown): string[] {
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatNumber, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatNumber, loadPage } from "./types";
3
4
 
4
5
  interface CoinGeckoResponse {
5
6
  id: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatNumber, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatNumber, loadPage } from "./types";
3
4
 
4
5
  /**
5
6
  * Check if content looks like HTML
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, htmlToBasicMarkdown, loadPage } from "./types";
3
4
 
4
5
  interface CrossrefAuthor {
5
6
  given?: string;
@@ -4,8 +4,10 @@
4
4
  * Uses the Discogs API to extract structured metadata about releases.
5
5
  * API docs: https://www.discogs.com/developers
6
6
  */
7
+
8
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
7
9
  import type { RenderResult, SpecialHandler } from "./types";
8
- import { buildResult, loadPage, tryParseJson } from "./types";
10
+ import { buildResult, loadPage } from "./types";
9
11
 
10
12
  interface DiscogsArtist {
11
13
  name: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatIsoDate, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatIsoDate, htmlToBasicMarkdown, loadPage } from "./types";
3
4
 
4
5
  interface DiscourseUser {
5
6
  username?: string;
@@ -1,6 +1,7 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import { formatBytes } from "../../tools/render-utils";
2
3
  import type { RenderResult, SpecialHandler } from "./types";
3
- import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types";
4
+ import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types";
4
5
 
5
6
  interface DockerHubRepo {
6
7
  name: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { LocalizedText, RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, getLocalizedText, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, getLocalizedText, loadPage } from "./types";
3
4
 
4
5
  type FdroidPackage = {
5
6
  packageName?: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { LocalizedText, RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatNumber, getLocalizedText, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatNumber, getLocalizedText, htmlToBasicMarkdown, loadPage } from "./types";
3
4
 
4
5
  type AddonFile = {
5
6
  permissions?: string[];
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatIsoDate, formatNumber, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatIsoDate, formatNumber, htmlToBasicMarkdown, loadPage } from "./types";
3
4
 
4
5
  interface FlathubScreenshotSize {
5
6
  src?: string;
@@ -1,3 +1,4 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import {
2
3
  buildResult,
3
4
  formatIsoDate,
@@ -6,7 +7,6 @@ import {
6
7
  loadPage,
7
8
  type RenderResult,
8
9
  type SpecialHandler,
9
- tryParseJson,
10
10
  } from "./types";
11
11
 
12
12
  interface GitLabUrl {
@@ -1,6 +1,7 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import { parse as parseHtml } from "node-html-parser";
2
3
  import type { RenderResult, SpecialHandler } from "./types";
3
- import { buildResult, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types";
4
+ import { buildResult, htmlToBasicMarkdown, loadPage } from "./types";
4
5
 
5
6
  interface GoModuleInfo {
6
7
  Version: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, loadPage } from "./types";
3
4
 
4
5
  interface HackageVersionMap {
5
6
  [version: string]: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { SpecialHandler } from "./types";
2
- import { buildResult, decodeHtmlEntities, formatIsoDate, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, decodeHtmlEntities, formatIsoDate, loadPage } from "./types";
3
4
 
4
5
  interface HNItem {
5
6
  id: number;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { SpecialHandler } from "./types";
2
- import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types";
3
4
 
4
5
  /**
5
6
  * Handle Hex.pm (Elixir package registry) URLs via API
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { SpecialHandler } from "./types";
2
- import { buildResult, formatNumber, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatNumber, loadPage } from "./types";
3
4
 
4
5
  interface HfModelData {
5
6
  modelId: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatNumber, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatNumber, htmlToBasicMarkdown, loadPage } from "./types";
3
4
 
4
5
  interface PluginVendor {
5
6
  name?: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, loadPage } from "./types";
3
4
 
4
5
  interface LemmyCreator {
5
6
  name: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { SpecialHandler } from "./types";
2
- import { buildResult, formatIsoDate, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatIsoDate, loadPage } from "./types";
3
4
 
4
5
  // =============================================================================
5
6
  // Lobste.rs Types
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatNumber, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatNumber, htmlToBasicMarkdown, loadPage } from "./types";
3
4
 
4
5
  interface MastodonAccount {
5
6
  id: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatIsoDate, formatNumber, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatIsoDate, formatNumber, loadPage } from "./types";
3
4
 
4
5
  interface MavenDoc {
5
6
  id: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { SpecialHandler } from "./types";
2
- import { buildResult, htmlToBasicMarkdown, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, htmlToBasicMarkdown, loadPage } from "./types";
3
4
 
4
5
  interface MDNSection {
5
6
  type: string;
@@ -1,5 +1,6 @@
1
+ import { tryParseJson } from "@oh-my-pi/pi-utils";
1
2
  import type { RenderResult, SpecialHandler } from "./types";
2
- import { buildResult, formatIsoDate, loadPage, tryParseJson } from "./types";
3
+ import { buildResult, formatIsoDate, loadPage } from "./types";
3
4
 
4
5
  interface ModuleResponse {
5
6
  name: string;