@oh-my-pi/pi-coding-agent 10.2.1 → 10.2.3

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 (67) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/package.json +7 -7
  3. package/src/commit/agentic/prompts/analyze-file.md +7 -7
  4. package/src/commit/agentic/prompts/session-user.md +4 -4
  5. package/src/commit/agentic/prompts/system.md +14 -16
  6. package/src/commit/prompts/analysis-system.md +7 -9
  7. package/src/commit/prompts/analysis-user.md +0 -3
  8. package/src/commit/prompts/changelog-system.md +14 -19
  9. package/src/commit/prompts/file-observer-system.md +2 -2
  10. package/src/commit/prompts/reduce-system.md +13 -23
  11. package/src/commit/prompts/summary-system.md +7 -21
  12. package/src/config/settings-schema.ts +135 -38
  13. package/src/cursor.ts +2 -1
  14. package/src/debug/index.ts +1 -1
  15. package/src/debug/report-bundle.ts +1 -1
  16. package/src/extensibility/extensions/index.ts +0 -11
  17. package/src/extensibility/extensions/types.ts +1 -30
  18. package/src/extensibility/hooks/types.ts +1 -31
  19. package/src/index.ts +0 -11
  20. package/src/ipy/prelude.py +1 -113
  21. package/src/lsp/index.ts +66 -515
  22. package/src/lsp/render.ts +0 -11
  23. package/src/lsp/types.ts +3 -87
  24. package/src/modes/components/settings-defs.ts +3 -2
  25. package/src/modes/components/settings-selector.ts +14 -14
  26. package/src/modes/interactive-mode.ts +5 -5
  27. package/src/modes/theme/theme.ts +45 -1
  28. package/src/prompts/agents/designer.md +23 -27
  29. package/src/prompts/agents/explore.md +28 -38
  30. package/src/prompts/agents/init.md +17 -17
  31. package/src/prompts/agents/plan.md +21 -27
  32. package/src/prompts/agents/reviewer.md +37 -37
  33. package/src/prompts/compaction/branch-summary.md +9 -9
  34. package/src/prompts/compaction/compaction-summary.md +8 -12
  35. package/src/prompts/compaction/compaction-update-summary.md +17 -19
  36. package/src/prompts/review-request.md +12 -13
  37. package/src/prompts/system/custom-system-prompt.md +6 -26
  38. package/src/prompts/system/plan-mode-active.md +23 -35
  39. package/src/prompts/system/plan-mode-subagent.md +7 -7
  40. package/src/prompts/system/subagent-system-prompt.md +7 -7
  41. package/src/prompts/system/system-prompt.md +84 -125
  42. package/src/prompts/system/web-search.md +10 -10
  43. package/src/prompts/tools/ask.md +12 -15
  44. package/src/prompts/tools/bash.md +7 -7
  45. package/src/prompts/tools/exit-plan-mode.md +6 -6
  46. package/src/prompts/tools/gemini-image.md +4 -4
  47. package/src/prompts/tools/grep.md +4 -4
  48. package/src/prompts/tools/lsp.md +12 -19
  49. package/src/prompts/tools/patch.md +26 -30
  50. package/src/prompts/tools/python.md +14 -57
  51. package/src/prompts/tools/read.md +4 -4
  52. package/src/prompts/tools/replace.md +8 -8
  53. package/src/prompts/tools/ssh.md +14 -27
  54. package/src/prompts/tools/task.md +23 -35
  55. package/src/prompts/tools/todo-write.md +29 -38
  56. package/src/prompts/tools/write.md +3 -3
  57. package/src/sdk.ts +0 -2
  58. package/src/session/agent-session.ts +27 -6
  59. package/src/system-prompt.ts +1 -219
  60. package/src/task/agents.ts +2 -1
  61. package/src/tools/bash-interceptor.ts +0 -24
  62. package/src/tools/bash.ts +1 -7
  63. package/src/tools/index.ts +8 -3
  64. package/src/tools/read.ts +74 -17
  65. package/src/tools/renderers.ts +0 -2
  66. package/src/lsp/rust-analyzer.ts +0 -184
  67. package/src/tools/ls.ts +0 -307
package/src/tools/bash.ts CHANGED
@@ -11,7 +11,7 @@ import type { Theme } from "../modes/theme/theme";
11
11
  import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
12
12
  import { renderOutputBlock, renderStatusLine } from "../tui";
13
13
  import type { ToolSession } from ".";
14
- import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor";
14
+ import { checkBashInterception } from "./bash-interceptor";
15
15
  import { applyHeadTail, normalizeBashCommand } from "./bash-normalize";
16
16
  import type { OutputMeta } from "./output-meta";
17
17
  import { allocateOutputArtifact, createTailBuffer } from "./output-utils";
@@ -83,12 +83,6 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
83
83
  if (interception.block) {
84
84
  throw new ToolError(interception.message ?? "Command blocked");
85
85
  }
86
- if (this.session.settings.get("bashInterceptor.simpleLs")) {
87
- const lsInterception = checkSimpleLsInterception(command, ctx?.toolNames ?? []);
88
- if (lsInterception.block) {
89
- throw new ToolError(lsInterception.message ?? "Command blocked");
90
- }
91
- }
92
86
  }
93
87
 
94
88
  const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
@@ -22,7 +22,6 @@ import { ExitPlanModeTool } from "./exit-plan-mode";
22
22
  import { FetchTool } from "./fetch";
23
23
  import { FindTool } from "./find";
24
24
  import { GrepTool } from "./grep";
25
- import { LsTool } from "./ls";
26
25
  import { NotebookTool } from "./notebook";
27
26
  import { wrapToolsWithMetaNotice } from "./output-meta";
28
27
  import { PythonTool } from "./python";
@@ -76,7 +75,6 @@ export { FetchTool, type FetchToolDetails } from "./fetch";
76
75
  export { type FindOperations, FindTool, type FindToolDetails, type FindToolOptions } from "./find";
77
76
  export { setPreferredImageProvider } from "./gemini-image";
78
77
  export { type GrepOperations, GrepTool, type GrepToolDetails, type GrepToolOptions } from "./grep";
79
- export { type LsOperations, LsTool, type LsToolDetails, type LsToolOptions } from "./ls";
80
78
  export { NotebookTool, type NotebookToolDetails } from "./notebook";
81
79
  export { PythonTool, type PythonToolDetails, type PythonToolOptions } from "./python";
82
80
  export { ReadTool, type ReadToolDetails } from "./read";
@@ -170,7 +168,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
170
168
  edit: s => new EditTool(s),
171
169
  find: s => new FindTool(s),
172
170
  grep: s => new GrepTool(s),
173
- ls: s => new LsTool(s),
174
171
  lsp: LspTool.createIf,
175
172
  notebook: s => new NotebookTool(s),
176
173
  read: s => new ReadTool(s),
@@ -278,6 +275,14 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
278
275
  if (name === "lsp") return enableLsp;
279
276
  if (name === "bash") return allowBash;
280
277
  if (name === "python") return allowPython;
278
+ if (name === "todo_write") return !includeSubmitResult && session.settings.get("todo.enabled");
279
+ if (name === "find") return session.settings.get("find.enabled");
280
+ if (name === "grep") return session.settings.get("grep.enabled");
281
+ if (name === "notebook") return session.settings.get("notebook.enabled");
282
+ if (name === "fetch") return session.settings.get("fetch.enabled");
283
+ if (name === "web_search") return session.settings.get("web_search.enabled");
284
+ if (name === "lsp") return session.settings.get("lsp.enabled");
285
+ if (name === "calc") return session.settings.get("calc.enabled");
281
286
  return true;
282
287
  };
283
288
  if (includeSubmitResult && requestedTools && !requestedTools.includes("submit_result")) {
package/src/tools/read.ts CHANGED
@@ -19,10 +19,9 @@ import { formatDimensionNote, resizeImage } from "../utils/image-resize";
19
19
  import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
20
20
  import { ensureTool } from "../utils/tools-manager";
21
21
  import { applyListLimit } from "./list-limit";
22
- import { LsTool } from "./ls";
23
22
  import type { OutputMeta } from "./output-meta";
24
23
  import { resolveReadPath, resolveToCwd } from "./path-utils";
25
- import { shortenPath, wrapBrackets } from "./render-utils";
24
+ import { formatAge, shortenPath, wrapBrackets } from "./render-utils";
26
25
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
27
26
  import { toolResult } from "./tool-result";
28
27
  import {
@@ -519,7 +518,7 @@ const readSchema = Type.Object({
519
518
 
520
519
  export interface ReadToolDetails {
521
520
  truncation?: TruncationResult;
522
- redirectedTo?: "ls";
521
+ isDirectory?: boolean;
523
522
  resolvedPath?: string;
524
523
  meta?: OutputMeta;
525
524
  }
@@ -530,7 +529,7 @@ type ReadParams = { path: string; offset?: number; limit?: number; lines?: boole
530
529
  * Read tool implementation.
531
530
  *
532
531
  * Reads files with support for images, documents (via markitdown), and text.
533
- * Directories redirect to the ls tool.
532
+ * Directories return a formatted listing with modification times.
534
533
  */
535
534
  export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
536
535
  public readonly name = "read";
@@ -542,20 +541,18 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
542
541
  private readonly session: ToolSession;
543
542
  private readonly autoResizeImages: boolean;
544
543
  private readonly defaultLineNumbers: boolean;
545
- private readonly lsTool: LsTool;
546
544
 
547
545
  constructor(session: ToolSession) {
548
546
  this.session = session;
549
547
  this.autoResizeImages = session.settings.get("images.autoResize");
550
548
  this.defaultLineNumbers = session.settings.get("readLineNumbers");
551
- this.lsTool = new LsTool(session);
552
549
  this.description = renderPromptTemplate(readDescription, {
553
550
  DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
554
551
  });
555
552
  }
556
553
 
557
554
  public async execute(
558
- toolCallId: string,
555
+ _toolCallId: string,
559
556
  params: ReadParams,
560
557
  signal?: AbortSignal,
561
558
  _onUpdate?: AgentToolUpdateCallback<ReadToolDetails>,
@@ -606,13 +603,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
606
603
  }
607
604
 
608
605
  if (isDirectory) {
609
- const lsResult = await this.lsTool.execute(toolCallId, { path: readPath, limit }, signal);
610
- const details: ReadToolDetails = {
611
- redirectedTo: "ls",
612
- truncation: lsResult.details?.truncation,
613
- meta: lsResult.details?.meta,
614
- };
615
- return toolResult(details).content(lsResult.content).done();
606
+ return this.readDirectory(absolutePath, limit, signal);
616
607
  }
617
608
 
618
609
  const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
@@ -982,6 +973,75 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
982
973
  }
983
974
  return resultBuilder.done();
984
975
  }
976
+
977
+ /** Read directory contents as a formatted listing */
978
+ private async readDirectory(
979
+ absolutePath: string,
980
+ limit: number | undefined,
981
+ signal?: AbortSignal,
982
+ ): Promise<AgentToolResult<ReadToolDetails>> {
983
+ const DEFAULT_LIMIT = 500;
984
+ const effectiveLimit = limit ?? DEFAULT_LIMIT;
985
+
986
+ let entries: string[];
987
+ try {
988
+ entries = await fs.readdir(absolutePath);
989
+ } catch (error) {
990
+ const message = error instanceof Error ? error.message : String(error);
991
+ throw new ToolError(`Cannot read directory: ${message}`);
992
+ }
993
+
994
+ // Sort alphabetically (case-insensitive)
995
+ entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
996
+
997
+ const listLimit = applyListLimit(entries, { limit: effectiveLimit });
998
+ const limitedEntries = listLimit.items;
999
+ const limitMeta = listLimit.meta;
1000
+
1001
+ // Format entries with directory indicators and ages
1002
+ const results: string[] = [];
1003
+
1004
+ for (const entry of limitedEntries) {
1005
+ throwIfAborted(signal);
1006
+ const fullPath = path.join(absolutePath, entry);
1007
+ let suffix = "";
1008
+ let age = "";
1009
+
1010
+ try {
1011
+ const entryStat = await fs.stat(fullPath);
1012
+ suffix = entryStat.isDirectory() ? "/" : "";
1013
+ const ageSeconds = Math.floor((Date.now() - entryStat.mtimeMs) / 1000);
1014
+ age = formatAge(ageSeconds);
1015
+ } catch {
1016
+ // Skip entries we can't stat
1017
+ continue;
1018
+ }
1019
+
1020
+ const line = age ? `${entry}${suffix} (${age})` : entry + suffix;
1021
+ results.push(line);
1022
+ }
1023
+
1024
+ if (results.length === 0) {
1025
+ return { content: [{ type: "text", text: "(empty directory)" }], details: {} };
1026
+ }
1027
+
1028
+ const output = results.join("\n");
1029
+ const truncation = truncateHead(output, { maxLines: Number.MAX_SAFE_INTEGER });
1030
+
1031
+ const details: ReadToolDetails = {
1032
+ isDirectory: true,
1033
+ };
1034
+
1035
+ const resultBuilder = toolResult(details)
1036
+ .text(truncation.content)
1037
+ .limits({ resultLimit: limitMeta.resultLimit?.reached });
1038
+ if (truncation.truncated) {
1039
+ resultBuilder.truncation(truncation, { direction: "head" });
1040
+ details.truncation = truncation;
1041
+ }
1042
+
1043
+ return resultBuilder.done();
1044
+ }
985
1045
  }
986
1046
 
987
1047
  // =============================================================================
@@ -1029,9 +1089,6 @@ export const readToolRenderer = {
1029
1089
  const warningLines: string[] = [];
1030
1090
  const truncation = details?.meta?.truncation;
1031
1091
  const fallback = details?.truncation;
1032
- if (details?.redirectedTo) {
1033
- warningLines.push(uiTheme.fg("warning", wrapBrackets(`Redirected to ${details.redirectedTo}`, uiTheme)));
1034
- }
1035
1092
  if (details?.resolvedPath) {
1036
1093
  warningLines.push(uiTheme.fg("dim", wrapBrackets(`Resolved path: ${details.resolvedPath}`, uiTheme)));
1037
1094
  }
@@ -16,7 +16,6 @@ import { calculatorToolRenderer } from "./calculator";
16
16
  import { fetchToolRenderer } from "./fetch";
17
17
  import { findToolRenderer } from "./find";
18
18
  import { grepToolRenderer } from "./grep";
19
- import { lsToolRenderer } from "./ls";
20
19
  import { notebookToolRenderer } from "./notebook";
21
20
  import { pythonToolRenderer } from "./python";
22
21
  import { readToolRenderer } from "./read";
@@ -49,7 +48,6 @@ export const toolRenderers: Record<string, ToolRenderer> = {
49
48
  edit: editToolRenderer as ToolRenderer,
50
49
  find: findToolRenderer as ToolRenderer,
51
50
  grep: grepToolRenderer as ToolRenderer,
52
- ls: lsToolRenderer as ToolRenderer,
53
51
  lsp: lspToolRenderer as ToolRenderer,
54
52
  notebook: notebookToolRenderer as ToolRenderer,
55
53
  read: readToolRenderer as ToolRenderer,
@@ -1,184 +0,0 @@
1
- import { sendNotification, sendRequest } from "./client";
2
- import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types";
3
- import { fileToUri } from "./utils";
4
-
5
- /**
6
- * Run flycheck (cargo check) and collect diagnostics.
7
- * Sends rust-analyzer/runFlycheck notification and waits for diagnostics to accumulate.
8
- *
9
- * @param client - LSP client instance
10
- * @param file - Optional file path to check (if not provided, checks entire workspace)
11
- * @returns Array of all collected diagnostics
12
- */
13
- export async function flycheck(client: LspClient, file?: string): Promise<Diagnostic[]> {
14
- const textDocument = file ? { uri: fileToUri(file) } : null;
15
-
16
- const countDiagnostics = (diagnostics: Map<string, Diagnostic[]>): number => {
17
- let count = 0;
18
- for (const diags of diagnostics.values()) {
19
- count += diags.length;
20
- }
21
- return count;
22
- };
23
-
24
- // Capture current diagnostic version before triggering flycheck
25
- const initialDiagnosticsVersion = client.diagnosticsVersion;
26
- const initialDiagnosticsCount = countDiagnostics(client.diagnostics);
27
-
28
- await sendNotification(client, "rust-analyzer/runFlycheck", { textDocument });
29
-
30
- // Bounded polling: wait for diagnostics to stabilize or timeout
31
- // Poll every 100ms for up to 8 seconds (80 iterations)
32
- const pollIntervalMs = 100;
33
- const maxPollIterations = 80;
34
- const stabilityThreshold = 3; // Consider stable after 3 iterations without change
35
- const minStableDurationMs = 2000; // Avoid early exit when diagnostics are re-published unchanged.
36
- const startTime = Date.now();
37
- let lastDiagnosticsVersion = initialDiagnosticsVersion;
38
- let lastDiagnosticsCount = initialDiagnosticsCount;
39
- let stableIterations = 0;
40
-
41
- for (let i = 0; i < maxPollIterations; i++) {
42
- await Bun.sleep(pollIntervalMs);
43
-
44
- const currentDiagnosticsVersion = client.diagnosticsVersion;
45
- const currentDiagnosticsCount = countDiagnostics(client.diagnostics);
46
-
47
- // Check if diagnostics have stabilized
48
- if (currentDiagnosticsVersion === lastDiagnosticsVersion && currentDiagnosticsCount === lastDiagnosticsCount) {
49
- stableIterations++;
50
- const elapsedMs = Date.now() - startTime;
51
- const countChangedFromStart = currentDiagnosticsCount !== initialDiagnosticsCount;
52
- if (
53
- currentDiagnosticsVersion !== initialDiagnosticsVersion &&
54
- stableIterations >= stabilityThreshold &&
55
- (countChangedFromStart || elapsedMs >= minStableDurationMs)
56
- ) {
57
- break;
58
- }
59
- } else {
60
- stableIterations = 0;
61
- lastDiagnosticsVersion = currentDiagnosticsVersion;
62
- lastDiagnosticsCount = currentDiagnosticsCount;
63
- }
64
- }
65
-
66
- // Collect all diagnostics from client
67
- const allDiags: Diagnostic[] = [];
68
- for (const diags of Array.from(client.diagnostics.values())) {
69
- allDiags.push(...diags);
70
- }
71
-
72
- return allDiags;
73
- }
74
-
75
- /**
76
- * Expand macro at the given position.
77
- *
78
- * @param client - LSP client instance
79
- * @param file - File path containing the macro
80
- * @param line - 1-based line number
81
- * @param character - 1-based character offset
82
- * @returns ExpandMacroResult with macro name and expansion, or null if no macro at position
83
- */
84
- export async function expandMacro(
85
- client: LspClient,
86
- file: string,
87
- line: number,
88
- character: number,
89
- ): Promise<ExpandMacroResult | null> {
90
- const result = (await sendRequest(client, "rust-analyzer/expandMacro", {
91
- textDocument: { uri: fileToUri(file) },
92
- position: { line: line - 1, character: character - 1 },
93
- })) as ExpandMacroResult | null;
94
-
95
- return result;
96
- }
97
-
98
- /**
99
- * Perform structural search and replace (SSR).
100
- *
101
- * @param client - LSP client instance
102
- * @param pattern - Search pattern
103
- * @param replacement - Replacement pattern
104
- * @param parseOnly - If true, returns matches only; if false, returns WorkspaceEdit to apply
105
- * @returns WorkspaceEdit containing matches or changes to apply
106
- */
107
- export async function ssr(
108
- client: LspClient,
109
- pattern: string,
110
- replacement: string,
111
- parseOnly = true,
112
- ): Promise<WorkspaceEdit> {
113
- const result = (await sendRequest(client, "experimental/ssr", {
114
- query: `${pattern} ==>> ${replacement}`,
115
- parseOnly,
116
- textDocument: { uri: "" }, // SSR searches workspace-wide
117
- position: { line: 0, character: 0 },
118
- selections: [],
119
- })) as WorkspaceEdit;
120
-
121
- return result;
122
- }
123
-
124
- /**
125
- * Get runnables (tests, binaries, examples) for a file.
126
- *
127
- * @param client - LSP client instance
128
- * @param file - File path to query
129
- * @param line - Optional 1-based line number to get runnables at specific position
130
- * @returns Array of Runnable items
131
- */
132
- export async function runnables(client: LspClient, file: string, line?: number): Promise<Runnable[]> {
133
- const params: { textDocument: { uri: string }; position?: { line: number; character: number } } = {
134
- textDocument: { uri: fileToUri(file) },
135
- };
136
-
137
- if (line !== undefined) {
138
- params.position = { line: line - 1, character: 0 };
139
- }
140
-
141
- const result = (await sendRequest(client, "experimental/runnables", params)) as Runnable[];
142
- return result ?? [];
143
- }
144
-
145
- /**
146
- * Get related tests for a position (e.g., tests for a function).
147
- *
148
- * @param client - LSP client instance
149
- * @param file - File path
150
- * @param line - 1-based line number
151
- * @param character - 1-based character offset
152
- * @returns Array of test runnable labels
153
- */
154
- export async function relatedTests(
155
- client: LspClient,
156
- file: string,
157
- line: number,
158
- character: number,
159
- ): Promise<string[]> {
160
- const tests = (await sendRequest(client, "rust-analyzer/relatedTests", {
161
- textDocument: { uri: fileToUri(file) },
162
- position: { line: line - 1, character: character - 1 },
163
- })) as RelatedTest[];
164
-
165
- if (!tests?.length) return [];
166
-
167
- const labels: string[] = [];
168
- for (const t of tests) {
169
- if (t.runnable?.label) {
170
- labels.push(t.runnable.label);
171
- }
172
- }
173
-
174
- return labels;
175
- }
176
-
177
- /**
178
- * Reload workspace (re-index Cargo projects).
179
- *
180
- * @param client - LSP client instance
181
- */
182
- export async function reloadWorkspace(client: LspClient): Promise<void> {
183
- await sendRequest(client, "rust-analyzer/reloadWorkspace", null);
184
- }