@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +116 -30
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +123 -178
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. package/src/utils/prompt-format.ts +0 -170
@@ -4,7 +4,7 @@
4
4
  * Handles `omp grep` subcommand for testing grep tool on Windows.
5
5
  */
6
6
  import * as path from "node:path";
7
- import { grep } from "@oh-my-pi/pi-natives";
7
+ import { GrepOutputMode, grep } from "@oh-my-pi/pi-natives";
8
8
  import { APP_NAME } from "@oh-my-pi/pi-utils";
9
9
  import chalk from "chalk";
10
10
 
@@ -14,7 +14,7 @@ export interface GrepCommandArgs {
14
14
  glob?: string;
15
15
  limit: number;
16
16
  context: number;
17
- mode: "content" | "filesWithMatches" | "count";
17
+ mode: GrepOutputMode;
18
18
  gitignore: boolean;
19
19
  }
20
20
 
@@ -32,7 +32,7 @@ export function parseGrepArgs(args: string[]): GrepCommandArgs | undefined {
32
32
  path: ".",
33
33
  limit: 20,
34
34
  context: 2,
35
- mode: "content",
35
+ mode: GrepOutputMode.Content,
36
36
  gitignore: true,
37
37
  };
38
38
 
@@ -47,9 +47,9 @@ export function parseGrepArgs(args: string[]): GrepCommandArgs | undefined {
47
47
  } else if (arg === "--context" || arg === "-C") {
48
48
  result.context = parseInt(args[++i], 10);
49
49
  } else if (arg === "--files" || arg === "-f") {
50
- result.mode = "filesWithMatches";
50
+ result.mode = GrepOutputMode.FilesWithMatches;
51
51
  } else if (arg === "--count" || arg === "-c") {
52
- result.mode = "count";
52
+ result.mode = GrepOutputMode.Count;
53
53
  } else if (arg === "--no-gitignore") {
54
54
  result.gitignore = false;
55
55
  } else if (!arg.startsWith("-")) {
@@ -89,7 +89,7 @@ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
89
89
  glob: cmd.glob,
90
90
  mode: cmd.mode,
91
91
  maxCount: cmd.limit,
92
- context: cmd.mode === "content" ? cmd.context : undefined,
92
+ context: cmd.mode === GrepOutputMode.Content ? cmd.context : undefined,
93
93
  hidden: true,
94
94
  gitignore: cmd.gitignore,
95
95
  });
@@ -105,7 +105,7 @@ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
105
105
  for (const match of result.matches) {
106
106
  const displayPath = match.path.replace(/\\/g, "/");
107
107
 
108
- if (cmd.mode === "content") {
108
+ if (cmd.mode === GrepOutputMode.Content) {
109
109
  if (match.contextBefore) {
110
110
  for (const ctx of match.contextBefore) {
111
111
  console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
@@ -118,7 +118,7 @@ export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
118
118
  }
119
119
  }
120
120
  console.log("");
121
- } else if (cmd.mode === "count") {
121
+ } else if (cmd.mode === GrepOutputMode.Count) {
122
122
  console.log(`${chalk.cyan(displayPath)}: ${match.matchCount ?? 0} matches`);
123
123
  } else {
124
124
  console.log(chalk.cyan(displayPath));
@@ -0,0 +1,78 @@
1
+ /**
2
+ * CLI handler for `omp grievances` — view reported tool issues from auto-QA.
3
+ */
4
+ import { Database } from "bun:sqlite";
5
+ import chalk from "chalk";
6
+ import { getAutoQaDbPath } from "../tools/report-tool-issue";
7
+
8
+ interface GrievanceRow {
9
+ id: number;
10
+ model: string;
11
+ version: string;
12
+ tool: string;
13
+ report: string;
14
+ }
15
+
16
+ export interface ListGrievancesOptions {
17
+ limit: number;
18
+ tool?: string;
19
+ json: boolean;
20
+ }
21
+
22
+ function openDb(): Database | null {
23
+ try {
24
+ const db = new Database(getAutoQaDbPath(), { readonly: true });
25
+ return db;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ export async function listGrievances(options: ListGrievancesOptions): Promise<void> {
32
+ const db = openDb();
33
+ if (!db) {
34
+ if (options.json) {
35
+ console.log("[]");
36
+ } else {
37
+ console.log(
38
+ chalk.dim("No grievances database found. Enable auto-QA with PI_AUTO_QA=1 or the dev.autoqa setting."),
39
+ );
40
+ }
41
+ return;
42
+ }
43
+
44
+ try {
45
+ let rows: GrievanceRow[];
46
+ if (options.tool) {
47
+ rows = db
48
+ .prepare("SELECT id, model, version, tool, report FROM grievances WHERE tool = ? ORDER BY id DESC LIMIT ?")
49
+ .all(options.tool, options.limit) as GrievanceRow[];
50
+ } else {
51
+ rows = db
52
+ .prepare("SELECT id, model, version, tool, report FROM grievances ORDER BY id DESC LIMIT ?")
53
+ .all(options.limit) as GrievanceRow[];
54
+ }
55
+
56
+ if (options.json) {
57
+ console.log(JSON.stringify(rows, null, 2));
58
+ return;
59
+ }
60
+
61
+ if (rows.length === 0) {
62
+ console.log(chalk.dim("No grievances recorded yet."));
63
+ return;
64
+ }
65
+
66
+ for (const row of rows) {
67
+ console.log(
68
+ `${chalk.dim(`#${row.id}`)} ${chalk.cyan(row.tool)} ${chalk.dim(`(${row.model} v${row.version})`)}`,
69
+ );
70
+ console.log(` ${row.report}`);
71
+ console.log();
72
+ }
73
+
74
+ console.log(chalk.dim(`Showing ${rows.length} most recent${options.tool ? ` for ${options.tool}` : ""}`));
75
+ } finally {
76
+ db.close();
77
+ }
78
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Read CLI command handler.
3
+ *
4
+ * Handles `omp read` subcommand — emits chunk-mode read output for files,
5
+ * and delegates URL reads through the read tool pipeline.
6
+ */
7
+ import * as path from "node:path";
8
+ import chalk from "chalk";
9
+ import { Settings } from "../config/settings";
10
+ import { formatChunkedRead, resolveAnchorStyle } from "../edit/modes/chunk";
11
+ import { getLanguageFromPath } from "../modes/theme/theme";
12
+ import type { ToolSession } from "../tools";
13
+ import { parseReadUrlTarget } from "../tools/fetch";
14
+ import { ReadTool } from "../tools/read";
15
+
16
+ export interface ReadCommandArgs {
17
+ path: string;
18
+ sel?: string;
19
+ }
20
+
21
+ function createCliReadSession(cwd: string, settings: Settings): ToolSession {
22
+ return {
23
+ cwd,
24
+ hasUI: false,
25
+ hasEditTool: true,
26
+ getSessionFile: () => null,
27
+ getSessionSpawns: () => null,
28
+ settings,
29
+ };
30
+ }
31
+
32
+ export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
33
+ const cwd = process.cwd();
34
+ const parsedUrlTarget = parseReadUrlTarget(cmd.path, cmd.sel);
35
+ if (parsedUrlTarget) {
36
+ const settings = await Settings.init({ cwd });
37
+ const tool = new ReadTool(createCliReadSession(cwd, settings));
38
+ const result = await tool.execute("cli-read", { path: cmd.path, sel: cmd.sel });
39
+ const text = result.content.find((content): content is { type: "text"; text: string } => content.type === "text");
40
+ console.log(text?.text ?? "");
41
+ return;
42
+ }
43
+
44
+ const filePath = path.resolve(cmd.path);
45
+ const file = Bun.file(filePath);
46
+ if (!(await file.exists())) {
47
+ console.error(chalk.red(`Error: File not found: ${cmd.path}`));
48
+ process.exit(1);
49
+ }
50
+
51
+ const readPath = cmd.sel ? `${filePath}:${cmd.sel}` : filePath;
52
+ const language = getLanguageFromPath(filePath);
53
+
54
+ try {
55
+ const result = await formatChunkedRead({
56
+ filePath,
57
+ readPath,
58
+ cwd,
59
+ language,
60
+ anchorStyle: resolveAnchorStyle(),
61
+ });
62
+ console.log(result.text);
63
+ } catch (err) {
64
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
65
+ process.exit(1);
66
+ }
67
+ }
@@ -4,7 +4,7 @@
4
4
  * Handles `omp setup <component>` to install dependencies for optional features.
5
5
  */
6
6
  import * as path from "node:path";
7
- import { APP_NAME, getPythonEnvDir } from "@oh-my-pi/pi-utils";
7
+ import { $which, APP_NAME, getPythonEnvDir } from "@oh-my-pi/pi-utils";
8
8
  import { $ } from "bun";
9
9
  import chalk from "chalk";
10
10
  import { theme } from "../modes/theme/theme";
@@ -90,12 +90,12 @@ async function checkPythonSetup(): Promise<PythonCheckResult> {
90
90
  managedEnvPath: MANAGED_PYTHON_ENV,
91
91
  };
92
92
 
93
- const systemPythonPath = Bun.which("python") ?? Bun.which("python3");
93
+ const systemPythonPath = $which("python") ?? $which("python3");
94
94
  const managedPath = managedPythonPath();
95
95
  const hasManagedEnv = await Bun.file(managedPath).exists();
96
96
 
97
- result.uvPath = Bun.which("uv") ?? undefined;
98
- result.pipPath = Bun.which("pip3") ?? Bun.which("pip") ?? undefined;
97
+ result.uvPath = $which("uv") ?? undefined;
98
+ result.pipPath = $which("pip3") ?? $which("pip") ?? undefined;
99
99
 
100
100
  const candidates = [systemPythonPath, hasManagedEnv ? managedPath : undefined].filter(
101
101
  (candidate): candidate is string => !!candidate,
@@ -7,7 +7,7 @@
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { pipeline } from "node:stream/promises";
10
- import { APP_NAME, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
10
+ import { $which, APP_NAME, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
11
11
  import { $ } from "bun";
12
12
  import chalk from "chalk";
13
13
  import { theme } from "../modes/theme/theme";
@@ -36,7 +36,7 @@ export function parseUpdateArgs(args: string[]): { force: boolean; check: boolea
36
36
  }
37
37
 
38
38
  async function getBunGlobalBinDir(): Promise<string | undefined> {
39
- if (!Bun.which("bun")) return undefined;
39
+ if (!$which("bun")) return undefined;
40
40
  try {
41
41
  const result = await $`bun pm bin -g`.quiet().nothrow();
42
42
  if (result.exitCode !== 0) return undefined;
@@ -167,7 +167,7 @@ function getBinaryName(): string {
167
167
  * Resolve the path that `omp` maps to in the user's PATH.
168
168
  */
169
169
  function resolveOmpPath(): string | undefined {
170
- return Bun.which(APP_NAME) ?? undefined;
170
+ return $which(APP_NAME) ?? undefined;
171
171
  }
172
172
 
173
173
  /**
package/src/cli.ts CHANGED
@@ -48,6 +48,8 @@ const commands: CommandEntry[] = [
48
48
  { name: "commit", load: () => import("./commands/commit").then(m => m.default) },
49
49
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
50
50
  { name: "grep", load: () => import("./commands/grep").then(m => m.default) },
51
+ { name: "grievances", load: () => import("./commands/grievances").then(m => m.default) },
52
+ { name: "read", load: () => import("./commands/read").then(m => m.default) },
51
53
  { name: "jupyter", load: () => import("./commands/jupyter").then(m => m.default) },
52
54
  { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
53
55
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Test grep tool.
3
3
  */
4
+ import { GrepOutputMode } from "@oh-my-pi/pi-natives";
4
5
  import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
6
  import { type GrepCommandArgs, runGrepCommand } from "../cli/grep-cli";
6
7
  import { initTheme } from "../modes/theme/theme";
@@ -25,7 +26,11 @@ export default class Grep extends Command {
25
26
  async run(): Promise<void> {
26
27
  const { args, flags } = await this.parse(Grep);
27
28
 
28
- const mode: GrepCommandArgs["mode"] = flags.count ? "count" : flags.files ? "filesWithMatches" : "content";
29
+ const mode: GrepCommandArgs["mode"] = flags.count
30
+ ? GrepOutputMode.Count
31
+ : flags.files
32
+ ? GrepOutputMode.FilesWithMatches
33
+ : GrepOutputMode.Content;
29
34
 
30
35
  const cmd: GrepCommandArgs = {
31
36
  pattern: args.pattern ?? "",
@@ -0,0 +1,20 @@
1
+ /**
2
+ * View recently reported tool issues from automated QA.
3
+ */
4
+ import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
+ import { listGrievances } from "../cli/grievances-cli";
6
+
7
+ export default class Grievances extends Command {
8
+ static description = "View reported tool issues (auto-QA grievances)";
9
+
10
+ static flags = {
11
+ limit: Flags.integer({ char: "n", description: "Number of recent issues to show", default: 20 }),
12
+ tool: Flags.string({ char: "t", description: "Filter by tool name" }),
13
+ json: Flags.boolean({ char: "j", description: "Output as JSON", default: false }),
14
+ };
15
+
16
+ async run(): Promise<void> {
17
+ const { flags } = await this.parse(Grievances);
18
+ await listGrievances({ limit: flags.limit, tool: flags.tool, json: flags.json });
19
+ }
20
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Chunk-mode read tool.
3
+ */
4
+ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
+ import { type ReadCommandArgs, runReadCommand } from "../cli/read-cli";
6
+ import { initTheme } from "../modes/theme/theme";
7
+
8
+ export default class Read extends Command {
9
+ static description = "Read a file as a chunk tree";
10
+
11
+ static args = {
12
+ path: Args.string({ description: "File path to read", required: true }),
13
+ };
14
+
15
+ static flags = {
16
+ sel: Flags.string({
17
+ char: "s",
18
+ description: "Chunk selector or line range (e.g. class_Foo.fn_bar, L10-L20)",
19
+ }),
20
+ };
21
+
22
+ async run(): Promise<void> {
23
+ const { args, flags } = await this.parse(Read);
24
+
25
+ const cmd: ReadCommandArgs = {
26
+ path: args.path ?? "",
27
+ sel: flags.sel,
28
+ };
29
+
30
+ await initTheme();
31
+ await runReadCommand(cmd);
32
+ }
33
+ }
@@ -1,11 +1,10 @@
1
1
  import { INTENT_FIELD, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
3
  import { Markdown } from "@oh-my-pi/pi-tui";
4
+ import { prompt } from "@oh-my-pi/pi-utils";
4
5
  import chalk from "chalk";
5
- import type { ControlledGit } from "../../commit/git";
6
6
  import typesDescriptionPrompt from "../../commit/prompts/types-description.md" with { type: "text" };
7
7
  import type { ModelRegistry } from "../../config/model-registry";
8
- import { renderPromptTemplate } from "../../config/prompt-templates";
9
8
  import type { Settings } from "../../config/settings";
10
9
  import { getMarkdownTheme } from "../../modes/theme/theme";
11
10
  import { createAgentSession } from "../../sdk";
@@ -18,7 +17,6 @@ import { createCommitTools } from "./tools";
18
17
 
19
18
  export interface CommitAgentInput {
20
19
  cwd: string;
21
- git: ControlledGit;
22
20
  model: Model<Api>;
23
21
  thinkingLevel?: ThinkingLevel;
24
22
  settings: Settings;
@@ -38,15 +36,14 @@ export interface ExistingChangelogEntries {
38
36
  }
39
37
 
40
38
  export async function runCommitAgentSession(input: CommitAgentInput): Promise<CommitAgentState> {
41
- const typesDescription = renderPromptTemplate(typesDescriptionPrompt);
42
- const systemPrompt = renderPromptTemplate(agentSystemPrompt, {
39
+ const typesDescription = prompt.render(typesDescriptionPrompt);
40
+ const systemPrompt = prompt.render(agentSystemPrompt, {
43
41
  types_description: typesDescription,
44
42
  });
45
43
  const state: CommitAgentState = { diffText: input.diffText };
46
44
  const spawns = "quick_task";
47
45
  const tools = createCommitTools({
48
46
  cwd: input.cwd,
49
- git: input.git,
50
47
  authStorage: input.authStorage,
51
48
  modelRegistry: input.modelRegistry,
52
49
  settings: input.settings,
@@ -152,7 +149,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
152
149
  });
153
150
 
154
151
  try {
155
- const prompt = renderPromptTemplate(agentUserPrompt, {
152
+ const agentUserMessage = prompt.render(agentUserPrompt, {
156
153
  user_context: input.userContext,
157
154
  changelog_targets: input.changelogTargets.length > 0 ? input.changelogTargets.join("\n") : undefined,
158
155
  existing_changelog_entries: input.existingChangelogEntries,
@@ -161,7 +158,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
161
158
  let retryCount = 0;
162
159
  const needsChangelog = input.requireChangelog && input.changelogTargets.length > 0;
163
160
 
164
- await session.prompt(prompt, {
161
+ await session.prompt(agentUserMessage, {
165
162
  attribution: "agent",
166
163
  expandPromptTemplates: false,
167
164
  });
@@ -1,17 +1,16 @@
1
1
  import * as path from "node:path";
2
2
  import { createInterface } from "node:readline/promises";
3
- import { $env, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
3
+ import { $env, getProjectDir, isEnoent, prompt } from "@oh-my-pi/pi-utils";
4
4
  import { applyChangelogProposals } from "../../commit/changelog";
5
5
  import { detectChangelogBoundaries } from "../../commit/changelog/detect";
6
6
  import { parseUnreleasedSection } from "../../commit/changelog/parse";
7
- import { ControlledGit } from "../../commit/git";
8
7
  import { formatCommitMessage } from "../../commit/message";
9
8
  import { resolvePrimaryModel, resolveSmolModel } from "../../commit/model-selection";
10
9
  import type { CommitCommandArgs, ConventionalAnalysis } from "../../commit/types";
11
10
  import { ModelRegistry } from "../../config/model-registry";
12
- import { renderPromptTemplate } from "../../config/prompt-templates";
13
11
  import { Settings } from "../../config/settings";
14
12
  import { discoverAuthStorage, discoverContextFiles } from "../../sdk";
13
+ import * as git from "../../utils/git";
15
14
  import { type ExistingChangelogEntries, runCommitAgentSession } from "./agent";
16
15
  import { generateFallbackProposal } from "./fallback";
17
16
  import splitConfirmPrompt from "./prompts/split-confirm.md" with { type: "text" };
@@ -20,25 +19,24 @@ import { computeDependencyOrder } from "./topo-sort";
20
19
  import { detectTrivialChange } from "./trivial";
21
20
 
22
21
  interface CommitExecutionContext {
23
- git: ControlledGit;
22
+ cwd: string;
24
23
  dryRun: boolean;
25
24
  push: boolean;
26
25
  }
27
26
 
28
27
  export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
29
28
  const cwd = getProjectDir();
30
- const git = new ControlledGit(cwd);
31
29
  const [settings, authStorage] = await Promise.all([Settings.init({ cwd }), discoverAuthStorage()]);
32
30
 
33
31
  process.stdout.write("● Resolving model...\n");
34
32
  const modelRegistry = new ModelRegistry(authStorage);
35
33
  await modelRegistry.refresh();
36
34
  const stagedFilesPromise = (async () => {
37
- let stagedFiles = await git.getStagedFiles();
35
+ let stagedFiles = await git.diff.changedFiles(cwd, { cached: true });
38
36
  if (stagedFiles.length === 0) {
39
37
  process.stdout.write("No staged changes detected, staging all changes...\n");
40
- await git.stageAll();
41
- stagedFiles = await git.getStagedFiles();
38
+ await git.stage.files(cwd);
39
+ stagedFiles = await git.diff.changedFiles(cwd, { cached: true });
42
40
  }
43
41
  return stagedFiles;
44
42
  })();
@@ -66,8 +64,8 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
66
64
  const [changelogBoundaries, contextFiles, numstat, diff] = await Promise.all([
67
65
  args.noChangelog ? [] : detectChangelogBoundaries(cwd, stagedFiles),
68
66
  discoverContextFiles(cwd),
69
- git.getNumstat(true),
70
- git.getDiff(true),
67
+ git.diff.numstat(cwd, { cached: true }),
68
+ git.diff(cwd, { cached: true }),
71
69
  ]);
72
70
  const changelogTargets = changelogBoundaries.map(boundary => boundary.changelogPath);
73
71
  if (!args.noChangelog) {
@@ -93,7 +91,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
93
91
  if (forceFallback) {
94
92
  process.stdout.write("● Forcing fallback commit generation...\n");
95
93
  const fallbackProposal = generateFallbackProposal(numstat);
96
- await runSingleCommit(fallbackProposal, { git, dryRun: args.dryRun, push: args.push });
94
+ await runSingleCommit(fallbackProposal, { cwd, dryRun: args.dryRun, push: args.push });
97
95
  return;
98
96
  }
99
97
 
@@ -110,7 +108,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
110
108
  summary: trivialChange.summary,
111
109
  warnings: [],
112
110
  };
113
- await runSingleCommit(trivialProposal, { git, dryRun: args.dryRun, push: args.push });
111
+ await runSingleCommit(trivialProposal, { cwd, dryRun: args.dryRun, push: args.push });
114
112
  return;
115
113
  }
116
114
 
@@ -129,7 +127,6 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
129
127
  try {
130
128
  commitState = await runCommitAgentSession({
131
129
  cwd,
132
- git,
133
130
  model: agentModel,
134
131
  thinkingLevel: agentThinkingLevel,
135
132
  settings,
@@ -169,7 +166,6 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
169
166
  }
170
167
  process.stdout.write("● Applying changelog entries...\n");
171
168
  const updated = await applyChangelogProposals({
172
- git,
173
169
  cwd,
174
170
  proposals: commitState.changelogProposal.entries,
175
171
  dryRun: args.dryRun,
@@ -188,13 +184,13 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
188
184
  }
189
185
 
190
186
  if (commitState.proposal) {
191
- await runSingleCommit(commitState.proposal, { git, dryRun: args.dryRun, push: args.push });
187
+ await runSingleCommit(commitState.proposal, { cwd, dryRun: args.dryRun, push: args.push });
192
188
  return;
193
189
  }
194
190
 
195
191
  if (commitState.splitProposal) {
196
192
  await runSplitCommit(commitState.splitProposal, {
197
- git,
193
+ cwd,
198
194
  dryRun: args.dryRun,
199
195
  push: args.push,
200
196
  additionalFiles: updatedChangelogFiles,
@@ -215,10 +211,10 @@ async function runSingleCommit(proposal: CommitProposal, ctx: CommitExecutionCon
215
211
  process.stdout.write(`${commitMessage}\n`);
216
212
  return;
217
213
  }
218
- await ctx.git.commit(commitMessage);
214
+ await git.commit(ctx.cwd, commitMessage);
219
215
  process.stdout.write("Commit created.\n");
220
216
  if (ctx.push) {
221
- await ctx.git.push();
217
+ await git.push(ctx.cwd);
222
218
  process.stdout.write("Pushed to remote.\n");
223
219
  }
224
220
  }
@@ -233,7 +229,7 @@ async function runSplitCommit(
233
229
  if (ctx.additionalFiles && ctx.additionalFiles.length > 0) {
234
230
  appendFilesToLastCommit(plan, ctx.additionalFiles);
235
231
  }
236
- const stagedFiles = await ctx.git.getStagedFiles();
232
+ const stagedFiles = await git.diff.changedFiles(ctx.cwd, { cached: true });
237
233
  const plannedFiles = new Set(plan.commits.flatMap(commit => commit.changes.map(change => change.path)));
238
234
  const missingFiles = stagedFiles.filter(file => !plannedFiles.has(file));
239
235
  if (missingFiles.length > 0) {
@@ -270,10 +266,10 @@ async function runSplitCommit(
270
266
  throw new Error(order.error);
271
267
  }
272
268
 
273
- await ctx.git.resetStaging();
269
+ await git.stage.reset(ctx.cwd);
274
270
  for (const commitIndex of order) {
275
271
  const commit = plan.commits[commitIndex];
276
- await ctx.git.stageHunks(commit.changes);
272
+ await git.stage.hunks(ctx.cwd, commit.changes);
277
273
  const analysis: ConventionalAnalysis = {
278
274
  type: commit.type,
279
275
  scope: commit.scope,
@@ -281,12 +277,12 @@ async function runSplitCommit(
281
277
  issueRefs: commit.issueRefs,
282
278
  };
283
279
  const message = formatCommitMessage(analysis, commit.summary);
284
- await ctx.git.commit(message);
285
- await ctx.git.resetStaging();
280
+ await git.commit(ctx.cwd, message);
281
+ await git.stage.reset(ctx.cwd);
286
282
  }
287
283
  process.stdout.write("Split commits created.\n");
288
284
  if (ctx.push) {
289
- await ctx.git.push();
285
+ await git.push(ctx.cwd);
290
286
  process.stdout.write("Pushed to remote.\n");
291
287
  }
292
288
  }
@@ -308,8 +304,8 @@ async function confirmSplitCommitPlan(plan: SplitCommitPlan): Promise<boolean> {
308
304
  }
309
305
  const rl = createInterface({ input: process.stdin, output: process.stdout });
310
306
  try {
311
- const prompt = renderPromptTemplate(splitConfirmPrompt, { count: plan.commits.length });
312
- const answer = await rl.question(prompt);
307
+ const splitConfirmQuestion = prompt.render(splitConfirmPrompt, { count: plan.commits.length });
308
+ const answer = await rl.question(splitConfirmQuestion);
313
309
  return ["y", "yes"].includes(answer.trim().toLowerCase());
314
310
  } finally {
315
311
  rl.close();
@@ -1,9 +1,9 @@
1
+ import { prompt } from "@oh-my-pi/pi-utils";
1
2
  import { Type } from "@sinclair/typebox";
2
3
  import analyzeFilePrompt from "../../../commit/agentic/prompts/analyze-file.md" with { type: "text" };
3
4
  import type { CommitAgentState } from "../../../commit/agentic/state";
4
5
  import type { NumstatEntry } from "../../../commit/types";
5
6
  import type { ModelRegistry } from "../../../config/model-registry";
6
- import { renderPromptTemplate } from "../../../config/prompt-templates";
7
7
  import type { Settings } from "../../../config/settings";
8
8
  import type { CustomTool, CustomToolContext } from "../../../extensibility/custom-tools/types";
9
9
  import type { AuthStorage } from "../../../session/auth-storage";
@@ -66,7 +66,7 @@ export function createAnalyzeFileTool(options: {
66
66
  const numstat = options.state.overview?.numstat ?? [];
67
67
  const tasks = params.files.map((file, index) => {
68
68
  const relatedFiles = formatRelatedFiles(params.files, file, numstat);
69
- const prompt = renderPromptTemplate(analyzeFilePrompt, {
69
+ const assignment = prompt.render(analyzeFilePrompt, {
70
70
  file,
71
71
  goal: params.goal,
72
72
  related_files: relatedFiles,
@@ -74,7 +74,7 @@ export function createAnalyzeFileTool(options: {
74
74
  return {
75
75
  id: `AnalyzeFile${index + 1}`,
76
76
  description: `Analyze ${file}`,
77
- assignment: prompt,
77
+ assignment,
78
78
  };
79
79
  });
80
80
  const taskParams: TaskParams = {
@@ -1,7 +1,7 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import type { CommitAgentState } from "../../../commit/agentic/state";
3
- import type { ControlledGit } from "../../../commit/git";
4
3
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
4
+ import * as git from "../../../utils/git";
5
5
 
6
6
  const TARGET_TOKENS = 30000;
7
7
  const CHARS_PER_TOKEN = 4;
@@ -136,10 +136,7 @@ const gitFileDiffSchema = Type.Object({
136
136
  staged: Type.Optional(Type.Boolean({ description: "Use staged changes (default: true)" })),
137
137
  });
138
138
 
139
- export function createGitFileDiffTool(
140
- git: ControlledGit,
141
- state: CommitAgentState,
142
- ): CustomTool<typeof gitFileDiffSchema> {
139
+ export function createGitFileDiffTool(cwd: string, state: CommitAgentState): CustomTool<typeof gitFileDiffSchema> {
143
140
  return {
144
141
  name: "git_file_diff",
145
142
  label: "Git File Diff",
@@ -167,7 +164,7 @@ export function createGitFileDiffTool(
167
164
 
168
165
  if (uncachedFiles.length > 0) {
169
166
  for (const file of uncachedFiles) {
170
- const diff = await git.getDiffForFiles([file], staged);
167
+ const diff = await git.diff(cwd, { cached: staged, files: [file] });
171
168
  if (diff) {
172
169
  diffs.set(file, diff);
173
170
  state.diffCache.set(cacheKey(file), diff);
@@ -1,7 +1,7 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import type { ControlledGit } from "../../../commit/git";
3
2
  import type { DiffHunk, FileHunks } from "../../../commit/types";
4
3
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
4
+ import * as git from "../../../utils/git";
5
5
 
6
6
  const gitHunkSchema = Type.Object({
7
7
  file: Type.String({ description: "File path" }),
@@ -15,7 +15,7 @@ function selectHunks(fileHunks: FileHunks, requested?: number[]): DiffHunk[] {
15
15
  return fileHunks.hunks.filter(hunk => wanted.has(hunk.index + 1));
16
16
  }
17
17
 
18
- export function createGitHunkTool(git: ControlledGit): CustomTool<typeof gitHunkSchema> {
18
+ export function createGitHunkTool(cwd: string): CustomTool<typeof gitHunkSchema> {
19
19
  return {
20
20
  name: "git_hunk",
21
21
  label: "Git Hunk",
@@ -23,7 +23,7 @@ export function createGitHunkTool(git: ControlledGit): CustomTool<typeof gitHunk
23
23
  parameters: gitHunkSchema,
24
24
  async execute(_toolCallId, params) {
25
25
  const staged = params.staged ?? true;
26
- const hunks = await git.getHunks([params.file], staged);
26
+ const hunks = await git.diff.hunks(cwd, [params.file], { cached: staged });
27
27
  const fileHunks = hunks.find(entry => entry.filename === params.file) ?? {
28
28
  filename: params.file,
29
29
  isBinary: false,