@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.67

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 (93) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/docs/sdk.md +1 -1
  3. package/package.json +5 -5
  4. package/scripts/generate-template.ts +6 -6
  5. package/src/cli/args.ts +3 -0
  6. package/src/core/agent-session.ts +39 -0
  7. package/src/core/bash-executor.ts +3 -3
  8. package/src/core/cursor/exec-bridge.ts +95 -88
  9. package/src/core/custom-commands/bundled/review/index.ts +142 -145
  10. package/src/core/custom-commands/bundled/wt/index.ts +68 -66
  11. package/src/core/custom-commands/loader.ts +4 -6
  12. package/src/core/custom-tools/index.ts +2 -2
  13. package/src/core/custom-tools/loader.ts +66 -61
  14. package/src/core/custom-tools/types.ts +4 -4
  15. package/src/core/custom-tools/wrapper.ts +61 -25
  16. package/src/core/event-bus.ts +19 -47
  17. package/src/core/extensions/index.ts +8 -4
  18. package/src/core/extensions/loader.ts +160 -120
  19. package/src/core/extensions/types.ts +4 -4
  20. package/src/core/extensions/wrapper.ts +149 -100
  21. package/src/core/hooks/index.ts +1 -1
  22. package/src/core/hooks/tool-wrapper.ts +96 -70
  23. package/src/core/hooks/types.ts +1 -2
  24. package/src/core/index.ts +1 -0
  25. package/src/core/mcp/index.ts +6 -2
  26. package/src/core/mcp/json-rpc.ts +88 -0
  27. package/src/core/mcp/loader.ts +22 -4
  28. package/src/core/mcp/manager.ts +202 -48
  29. package/src/core/mcp/tool-bridge.ts +143 -55
  30. package/src/core/mcp/tool-cache.ts +122 -0
  31. package/src/core/python-executor.ts +3 -9
  32. package/src/core/sdk.ts +33 -32
  33. package/src/core/session-manager.ts +30 -0
  34. package/src/core/settings-manager.ts +54 -1
  35. package/src/core/ssh/ssh-executor.ts +6 -84
  36. package/src/core/streaming-output.ts +107 -53
  37. package/src/core/tools/ask.ts +92 -93
  38. package/src/core/tools/bash.ts +103 -94
  39. package/src/core/tools/calculator.ts +41 -26
  40. package/src/core/tools/complete.ts +76 -66
  41. package/src/core/tools/context.ts +22 -24
  42. package/src/core/tools/exa/index.ts +1 -1
  43. package/src/core/tools/exa/mcp-client.ts +56 -101
  44. package/src/core/tools/find.ts +250 -253
  45. package/src/core/tools/git.ts +39 -33
  46. package/src/core/tools/grep.ts +440 -427
  47. package/src/core/tools/index.ts +63 -61
  48. package/src/core/tools/ls.ts +119 -114
  49. package/src/core/tools/lsp/clients/biome-client.ts +5 -7
  50. package/src/core/tools/lsp/clients/index.ts +4 -4
  51. package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
  52. package/src/core/tools/lsp/config.ts +2 -2
  53. package/src/core/tools/lsp/index.ts +604 -578
  54. package/src/core/tools/notebook.ts +121 -119
  55. package/src/core/tools/output.ts +163 -147
  56. package/src/core/tools/patch/applicator.ts +1100 -0
  57. package/src/core/tools/patch/diff.ts +362 -0
  58. package/src/core/tools/patch/fuzzy.ts +647 -0
  59. package/src/core/tools/patch/index.ts +430 -0
  60. package/src/core/tools/patch/normalize.ts +220 -0
  61. package/src/core/tools/patch/normative.ts +73 -0
  62. package/src/core/tools/patch/parser.ts +528 -0
  63. package/src/core/tools/patch/shared.ts +257 -0
  64. package/src/core/tools/patch/types.ts +244 -0
  65. package/src/core/tools/python.ts +139 -136
  66. package/src/core/tools/read.ts +239 -216
  67. package/src/core/tools/render-utils.ts +196 -77
  68. package/src/core/tools/renderers.ts +6 -2
  69. package/src/core/tools/ssh.ts +99 -80
  70. package/src/core/tools/task/executor.ts +11 -7
  71. package/src/core/tools/task/index.ts +352 -343
  72. package/src/core/tools/task/worker.ts +13 -23
  73. package/src/core/tools/todo-write.ts +74 -59
  74. package/src/core/tools/web-fetch.ts +54 -47
  75. package/src/core/tools/web-search/index.ts +27 -16
  76. package/src/core/tools/write.ts +108 -47
  77. package/src/core/ttsr.ts +106 -152
  78. package/src/core/voice.ts +49 -39
  79. package/src/index.ts +16 -12
  80. package/src/lib/worktree/index.ts +1 -9
  81. package/src/modes/interactive/components/diff.ts +15 -8
  82. package/src/modes/interactive/components/settings-defs.ts +42 -0
  83. package/src/modes/interactive/components/tool-execution.ts +46 -8
  84. package/src/modes/interactive/controllers/event-controller.ts +6 -19
  85. package/src/modes/interactive/controllers/input-controller.ts +1 -1
  86. package/src/modes/interactive/utils/ui-helpers.ts +5 -1
  87. package/src/modes/rpc/rpc-mode.ts +99 -81
  88. package/src/prompts/tools/patch.md +76 -0
  89. package/src/prompts/tools/read.md +1 -1
  90. package/src/prompts/tools/{edit.md → replace.md} +1 -0
  91. package/src/utils/shell.ts +0 -40
  92. package/src/core/tools/edit-diff.ts +0 -574
  93. package/src/core/tools/edit.ts +0 -345
@@ -1,35 +1,35 @@
1
- export { type AskToolDetails, askTool, createAskTool } from "./ask";
2
- export { type BashOperations, type BashToolDetails, type BashToolOptions, createBashTool } from "./bash";
3
- export { type CalculatorToolDetails, createCalculatorTool } from "./calculator";
4
- export { createCompleteTool } from "./complete";
5
- export { createEditTool, type EditToolDetails } from "./edit";
1
+ export { AskTool, type AskToolDetails } from "./ask";
2
+ export { type BashOperations, BashTool, type BashToolDetails, type BashToolOptions } from "./bash";
3
+ export { CalculatorTool, type CalculatorToolDetails } from "./calculator";
4
+ export { CompleteTool } from "./complete";
6
5
  // Exa MCP tools (22 tools)
7
6
  export { exaTools } from "./exa/index";
8
7
  export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types";
9
- export { createFindTool, type FindOperations, type FindToolDetails, type FindToolOptions } from "./find";
8
+ export { type FindOperations, FindTool, type FindToolDetails, type FindToolOptions } from "./find";
10
9
  export { setPreferredImageProvider } from "./gemini-image";
11
- export { createGitTool, type GitToolDetails, gitTool } from "./git";
12
- export { createGrepTool, type GrepOperations, type GrepToolDetails, type GrepToolOptions } from "./grep";
13
- export { createLsTool, type LsOperations, type LsToolDetails, type LsToolOptions } from "./ls";
10
+ export { GitTool, type GitToolDetails } from "./git";
11
+ export { type GrepOperations, GrepTool, type GrepToolDetails, type GrepToolOptions } from "./grep";
12
+ export { type LsOperations, LsTool, type LsToolDetails, type LsToolOptions } from "./ls";
14
13
  export {
15
- createLspTool,
16
14
  type FileDiagnosticsResult,
17
15
  type FileFormatResult,
18
16
  getLspStatus,
19
17
  type LspServerStatus,
18
+ LspTool,
20
19
  type LspToolDetails,
21
20
  type LspWarmupOptions,
22
21
  type LspWarmupResult,
23
22
  warmupLspServers,
24
23
  } from "./lsp/index";
25
- export { createNotebookTool, type NotebookToolDetails } from "./notebook";
26
- export { createOutputTool, type OutputToolDetails } from "./output";
27
- export { createPythonTool, type PythonToolDetails } from "./python";
28
- export { createReadTool, type ReadToolDetails } from "./read";
24
+ export { NotebookTool, type NotebookToolDetails } from "./notebook";
25
+ export { OutputTool, type OutputToolDetails } from "./output";
26
+ export { EditTool, type EditToolDetails } from "./patch";
27
+ export { PythonTool, type PythonToolDetails, type PythonToolOptions } from "./python";
28
+ export { ReadTool, type ReadToolDetails } from "./read";
29
29
  export { reportFindingTool, type SubmitReviewDetails } from "./review";
30
- export { createSshTool, type SSHToolDetails } from "./ssh";
31
- export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
32
- export { createTodoWriteTool, type TodoItem, type TodoWriteToolDetails } from "./todo-write";
30
+ export { loadSshTool, type SSHToolDetails, SshTool } from "./ssh";
31
+ export { BUNDLED_AGENTS, TaskTool } from "./task/index";
32
+ export { type TodoItem, TodoWriteTool, type TodoWriteToolDetails } from "./todo-write";
33
33
  export {
34
34
  DEFAULT_MAX_BYTES,
35
35
  DEFAULT_MAX_LINES,
@@ -40,10 +40,9 @@ export {
40
40
  truncateLine,
41
41
  truncateTail,
42
42
  } from "./truncate";
43
- export { createWebFetchTool, type WebFetchToolDetails } from "./web-fetch";
43
+ export { WebFetchTool, type WebFetchToolDetails } from "./web-fetch";
44
44
  export {
45
45
  companyWebSearchTools,
46
- createWebSearchTool,
47
46
  exaWebSearchTools,
48
47
  getWebSearchTools,
49
48
  hasExaWebSearch,
@@ -51,6 +50,7 @@ export {
51
50
  setPreferredWebSearchProvider,
52
51
  type WebSearchProvider,
53
52
  type WebSearchResponse,
53
+ WebSearchTool,
54
54
  type WebSearchToolsOptions,
55
55
  webSearchCodeContextTool,
56
56
  webSearchCompanyTool,
@@ -58,9 +58,8 @@ export {
58
58
  webSearchCustomTool,
59
59
  webSearchDeepTool,
60
60
  webSearchLinkedinTool,
61
- webSearchTool,
62
61
  } from "./web-search/index";
63
- export { createWriteTool, type WriteToolDetails } from "./write";
62
+ export { WriteTool, type WriteToolDetails } from "./write";
64
63
 
65
64
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
66
65
  import type { EventBus } from "../event-bus";
@@ -68,27 +67,27 @@ import { logger } from "../logger";
68
67
  import { getPreludeDocs, warmPythonEnvironment } from "../python-executor";
69
68
  import { checkPythonKernelAvailability } from "../python-kernel";
70
69
  import type { BashInterceptorRule } from "../settings-manager";
71
- import { createAskTool } from "./ask";
72
- import { createBashTool } from "./bash";
73
- import { createCalculatorTool } from "./calculator";
74
- import { createCompleteTool } from "./complete";
75
- import { createEditTool } from "./edit";
76
- import { createFindTool } from "./find";
77
- import { createGitTool } from "./git";
78
- import { createGrepTool } from "./grep";
79
- import { createLsTool } from "./ls";
80
- import { createLspTool } from "./lsp/index";
81
- import { createNotebookTool } from "./notebook";
82
- import { createOutputTool } from "./output";
83
- import { createPythonTool } from "./python";
84
- import { createReadTool } from "./read";
70
+ import { AskTool } from "./ask";
71
+ import { BashTool } from "./bash";
72
+ import { CalculatorTool } from "./calculator";
73
+ import { CompleteTool } from "./complete";
74
+ import { FindTool } from "./find";
75
+ import { GitTool } from "./git";
76
+ import { GrepTool } from "./grep";
77
+ import { LsTool } from "./ls";
78
+ import { LspTool } from "./lsp/index";
79
+ import { NotebookTool } from "./notebook";
80
+ import { OutputTool } from "./output";
81
+ import { EditTool } from "./patch";
82
+ import { PythonTool } from "./python";
83
+ import { ReadTool } from "./read";
85
84
  import { reportFindingTool } from "./review";
86
- import { createSshTool } from "./ssh";
87
- import { createTaskTool } from "./task/index";
88
- import { createTodoWriteTool } from "./todo-write";
89
- import { createWebFetchTool } from "./web-fetch";
90
- import { createWebSearchTool } from "./web-search/index";
91
- import { createWriteTool } from "./write";
85
+ import { loadSshTool } from "./ssh";
86
+ import { TaskTool } from "./task/index";
87
+ import { TodoWriteTool } from "./todo-write";
88
+ import { WebFetchTool } from "./web-fetch";
89
+ import { WebSearchTool } from "./web-search/index";
90
+ import { WriteTool } from "./write";
92
91
 
93
92
  /** Tool type (AgentTool from pi-ai) */
94
93
  export type Tool = AgentTool<any, any, any>;
@@ -126,10 +125,13 @@ export interface ToolSession {
126
125
  /** Settings manager (optional) */
127
126
  settings?: {
128
127
  getImageAutoResize(): boolean;
128
+ getReadLineNumbers?(): boolean;
129
129
  getLspFormatOnWrite(): boolean;
130
130
  getLspDiagnosticsOnWrite(): boolean;
131
131
  getLspDiagnosticsOnEdit(): boolean;
132
132
  getEditFuzzyMatch(): boolean;
133
+ getEditFuzzyThreshold?(): number;
134
+ getEditPatchMode?(): boolean;
133
135
  getGitToolEnabled(): boolean;
134
136
  getBashInterceptorEnabled(): boolean;
135
137
  getBashInterceptorSimpleLsEnabled(): boolean;
@@ -143,29 +145,29 @@ export interface ToolSession {
143
145
  type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
144
146
 
145
147
  export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
146
- ask: createAskTool,
147
- bash: createBashTool,
148
- python: createPythonTool,
149
- calc: createCalculatorTool,
150
- ssh: createSshTool,
151
- edit: createEditTool,
152
- find: createFindTool,
153
- git: createGitTool,
154
- grep: createGrepTool,
155
- ls: createLsTool,
156
- lsp: createLspTool,
157
- notebook: createNotebookTool,
158
- output: createOutputTool,
159
- read: createReadTool,
160
- task: createTaskTool,
161
- todo_write: createTodoWriteTool,
162
- web_fetch: createWebFetchTool,
163
- web_search: createWebSearchTool,
164
- write: createWriteTool,
148
+ ask: AskTool.createIf,
149
+ bash: (s) => new BashTool(s),
150
+ python: (s) => new PythonTool(s),
151
+ calc: (s) => new CalculatorTool(s),
152
+ ssh: loadSshTool,
153
+ edit: (s) => new EditTool(s),
154
+ find: (s) => new FindTool(s),
155
+ git: GitTool.createIf,
156
+ grep: (s) => new GrepTool(s),
157
+ ls: (s) => new LsTool(s),
158
+ lsp: LspTool.createIf,
159
+ notebook: (s) => new NotebookTool(s),
160
+ output: (s) => new OutputTool(s),
161
+ read: (s) => new ReadTool(s),
162
+ task: TaskTool.create,
163
+ todo_write: (s) => new TodoWriteTool(s),
164
+ web_fetch: (s) => new WebFetchTool(s),
165
+ web_search: (s) => new WebSearchTool(s),
166
+ write: (s) => new WriteTool(s),
165
167
  };
166
168
 
167
169
  export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
168
- complete: createCompleteTool,
170
+ complete: (s) => new CompleteTool(s),
169
171
  report_finding: () => reportFindingTool,
170
172
  };
171
173
 
@@ -1,7 +1,6 @@
1
1
  import nodePath from "node:path";
2
- import type { AgentTool } from "@oh-my-pi/pi-agent-core";
3
- import type { Component } from "@oh-my-pi/pi-tui";
4
- import { Text } from "@oh-my-pi/pi-tui";
2
+ import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
3
+ import { type Component, Text } from "@oh-my-pi/pi-tui";
5
4
  import { Type } from "@sinclair/typebox";
6
5
  import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
7
6
  import type { RenderResultOptions } from "../custom-tools/types";
@@ -69,129 +68,135 @@ const defaultLsOperations: LsOperations = {
69
68
  },
70
69
  };
71
70
 
72
- export function createLsTool(session: ToolSession, options?: LsToolOptions): AgentTool<typeof lsSchema> {
73
- const ops = options?.operations ?? defaultLsOperations;
74
-
75
- return {
76
- name: "ls",
77
- label: "Ls",
78
- description: `List directory contents with modification times. Returns entries sorted alphabetically, with '/' suffix for directories and relative age (e.g., "2d ago", "just now"). Includes dotfiles. Output is truncated to 500 entries or 50KB (whichever is hit first).`,
79
- parameters: lsSchema,
80
- execute: async (
81
- _toolCallId: string,
82
- { path, limit }: { path?: string; limit?: number },
83
- signal?: AbortSignal,
84
- ) => {
85
- return untilAborted(signal, async () => {
86
- const dirPath = resolveToCwd(path || ".", session.cwd);
87
- const effectiveLimit = limit ?? DEFAULT_LIMIT;
88
-
89
- // Check if path exists and is a directory
90
- const dirStat = await ops.stat(dirPath);
91
- if (!dirStat) {
92
- throw new Error(`Path not found: ${dirPath}`);
93
- }
71
+ export class LsTool implements AgentTool<typeof lsSchema, LsToolDetails> {
72
+ public readonly name = "ls";
73
+ public readonly label = "Ls";
74
+ public readonly description =
75
+ 'List directory contents with modification times. Returns entries sorted alphabetically, with \'/\' suffix for directories and relative age (e.g., "2d ago", "just now"). Includes dotfiles. Output is truncated to 500 entries or 50KB (whichever is hit first).';
76
+ public readonly parameters = lsSchema;
77
+
78
+ private readonly session: ToolSession;
79
+ private readonly ops: LsOperations;
80
+
81
+ constructor(session: ToolSession, options?: LsToolOptions) {
82
+ this.session = session;
83
+ this.ops = options?.operations ?? defaultLsOperations;
84
+ }
85
+
86
+ public async execute(
87
+ _toolCallId: string,
88
+ { path, limit }: { path?: string; limit?: number },
89
+ signal?: AbortSignal,
90
+ ): Promise<AgentToolResult<LsToolDetails>> {
91
+ return untilAborted(signal, async () => {
92
+ const dirPath = resolveToCwd(path || ".", this.session.cwd);
93
+ const effectiveLimit = limit ?? DEFAULT_LIMIT;
94
+
95
+ // Check if path exists and is a directory
96
+ const dirStat = await this.ops.stat(dirPath);
97
+ if (!dirStat) {
98
+ throw new Error(`Path not found: ${dirPath}`);
99
+ }
94
100
 
95
- if (!dirStat.isDirectory()) {
96
- throw new Error(`Not a directory: ${dirPath}`);
97
- }
101
+ if (!dirStat.isDirectory()) {
102
+ throw new Error(`Not a directory: ${dirPath}`);
103
+ }
98
104
 
99
- // Read directory entries
100
- let entries: string[];
101
- try {
102
- entries = await ops.readdir(dirPath);
103
- } catch (error) {
104
- const message = error instanceof Error ? error.message : String(error);
105
- throw new Error(`Cannot read directory: ${message}`);
106
- }
105
+ // Read directory entries
106
+ let entries: string[];
107
+ try {
108
+ entries = await this.ops.readdir(dirPath);
109
+ } catch (error) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ throw new Error(`Cannot read directory: ${message}`);
112
+ }
107
113
 
108
- // Sort alphabetically (case-insensitive)
109
- entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
110
-
111
- // Format entries with directory indicators
112
- const results: string[] = [];
113
- let entryLimitReached = false;
114
- let dirCount = 0;
115
- let fileCount = 0;
116
-
117
- for (const entry of entries) {
118
- signal?.throwIfAborted();
119
- if (results.length >= effectiveLimit) {
120
- entryLimitReached = true;
121
- break;
122
- }
123
-
124
- const fullPath = nodePath.join(dirPath, entry);
125
- let suffix = "";
126
- let age = "";
127
-
128
- const entryStat = await ops.stat(fullPath);
129
- if (!entryStat) {
130
- // Skip entries we can't stat
131
- continue;
132
- }
133
-
134
- if (entryStat.isDirectory()) {
135
- suffix = "/";
136
- dirCount += 1;
137
- } else {
138
- fileCount += 1;
139
- }
140
- // Calculate age from mtime
141
- const ageSeconds = Math.floor((Date.now() - entryStat.mtimeMs) / 1000);
142
- age = formatAge(ageSeconds);
143
-
144
- // Format: "name/ (2d ago)" or "name (just now)"
145
- const line = age ? `${entry}${suffix} (${age})` : entry + suffix;
146
- results.push(line);
147
- }
114
+ // Sort alphabetically (case-insensitive)
115
+ entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
148
116
 
149
- if (results.length === 0) {
150
- return { content: [{ type: "text", text: "(empty directory)" }], details: undefined };
151
- }
117
+ // Format entries with directory indicators
118
+ const results: string[] = [];
119
+ let entryLimitReached = false;
120
+ let dirCount = 0;
121
+ let fileCount = 0;
152
122
 
153
- // Apply byte truncation (no line limit since we already have entry limit)
154
- const rawOutput = results.join("\n");
155
- const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
156
-
157
- let output = truncation.content;
158
- const details: LsToolDetails = {
159
- entries: results,
160
- dirCount,
161
- fileCount,
162
- };
163
- const truncationReasons: Array<"entryLimit" | "byteLimit"> = [];
164
-
165
- // Build notices
166
- const notices: string[] = [];
167
-
168
- if (entryLimitReached) {
169
- notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
170
- details.entryLimitReached = effectiveLimit;
171
- truncationReasons.push("entryLimit");
123
+ for (const entry of entries) {
124
+ signal?.throwIfAborted();
125
+ if (results.length >= effectiveLimit) {
126
+ entryLimitReached = true;
127
+ break;
172
128
  }
173
129
 
174
- if (truncation.truncated) {
175
- notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
176
- details.truncation = truncation;
177
- truncationReasons.push("byteLimit");
178
- }
130
+ const fullPath = nodePath.join(dirPath, entry);
131
+ let suffix = "";
132
+ let age = "";
179
133
 
180
- if (truncationReasons.length > 0) {
181
- details.truncationReasons = truncationReasons;
134
+ const entryStat = await this.ops.stat(fullPath);
135
+ if (!entryStat) {
136
+ // Skip entries we can't stat
137
+ continue;
182
138
  }
183
139
 
184
- if (notices.length > 0) {
185
- output += `\n\n[${notices.join(". ")}]`;
140
+ if (entryStat.isDirectory()) {
141
+ suffix = "/";
142
+ dirCount += 1;
143
+ } else {
144
+ fileCount += 1;
186
145
  }
146
+ // Calculate age from mtime
147
+ const ageSeconds = Math.floor((Date.now() - entryStat.mtimeMs) / 1000);
148
+ age = formatAge(ageSeconds);
149
+
150
+ // Format: "name/ (2d ago)" or "name (just now)"
151
+ const line = age ? `${entry}${suffix} (${age})` : entry + suffix;
152
+ results.push(line);
153
+ }
154
+
155
+ if (results.length === 0) {
156
+ return { content: [{ type: "text", text: "(empty directory)" }], details: {} };
157
+ }
158
+
159
+ // Apply byte truncation (no line limit since we already have entry limit)
160
+ const rawOutput = results.join("\n");
161
+ const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
162
+
163
+ let output = truncation.content;
164
+ const details: LsToolDetails = {
165
+ entries: results,
166
+ dirCount,
167
+ fileCount,
168
+ };
169
+ const truncationReasons: Array<"entryLimit" | "byteLimit"> = [];
170
+
171
+ // Build notices
172
+ const notices: string[] = [];
173
+
174
+ if (entryLimitReached) {
175
+ notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
176
+ details.entryLimitReached = effectiveLimit;
177
+ truncationReasons.push("entryLimit");
178
+ }
179
+
180
+ if (truncation.truncated) {
181
+ notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
182
+ details.truncation = truncation;
183
+ truncationReasons.push("byteLimit");
184
+ }
185
+
186
+ if (truncationReasons.length > 0) {
187
+ details.truncationReasons = truncationReasons;
188
+ }
189
+
190
+ if (notices.length > 0) {
191
+ output += `\n\n[${notices.join(". ")}]`;
192
+ }
187
193
 
188
- return {
189
- content: [{ type: "text", text: output }],
190
- details,
191
- };
192
- });
193
- },
194
- };
194
+ return {
195
+ content: [{ type: "text", text: output }],
196
+ details,
197
+ };
198
+ });
199
+ }
195
200
  }
196
201
 
197
202
  // =============================================================================
@@ -110,6 +110,11 @@ export class BiomeClient implements LinterClient {
110
110
  private config: ServerConfig;
111
111
  private cwd: string;
112
112
 
113
+ /** Factory method for creating BiomeClient instances */
114
+ static create(config: ServerConfig, cwd: string): LinterClient {
115
+ return new BiomeClient(config, cwd);
116
+ }
117
+
113
118
  constructor(config: ServerConfig, cwd: string) {
114
119
  this.config = config;
115
120
  this.cwd = cwd;
@@ -198,10 +203,3 @@ export class BiomeClient implements LinterClient {
198
203
  // Nothing to dispose for CLI client
199
204
  }
200
205
  }
201
-
202
- /**
203
- * Factory function to create a Biome client.
204
- */
205
- export function createBiomeClient(config: ServerConfig, cwd: string): LinterClient {
206
- return new BiomeClient(config, cwd);
207
- }
@@ -5,11 +5,11 @@
5
5
  * Different implementations can use LSP protocol, CLI tools, or other mechanisms.
6
6
  */
7
7
 
8
- export { BiomeClient, createBiomeClient } from "./biome-client";
9
- export { createLspLinterClient, LspLinterClient } from "./lsp-linter-client";
8
+ export { BiomeClient } from "./biome-client";
9
+ export { LspLinterClient } from "./lsp-linter-client";
10
10
 
11
11
  import type { LinterClient, ServerConfig } from "../types";
12
- import { createLspLinterClient } from "./lsp-linter-client";
12
+ import { LspLinterClient } from "./lsp-linter-client";
13
13
 
14
14
  // Cache of linter clients by server name + cwd
15
15
  const clientCache = new Map<string, LinterClient>();
@@ -31,7 +31,7 @@ export function getLinterClient(serverName: string, config: ServerConfig, cwd: s
31
31
  client = config.createClient(config, cwd);
32
32
  } else {
33
33
  // Default to LSP
34
- client = createLspLinterClient(config, cwd);
34
+ client = LspLinterClient.create(config, cwd);
35
35
  }
36
36
 
37
37
  clientCache.set(key, client);
@@ -26,6 +26,11 @@ export class LspLinterClient implements LinterClient {
26
26
  private cwd: string;
27
27
  private client: LspClient | null = null;
28
28
 
29
+ /** Factory method for creating LspLinterClient instances */
30
+ static create(config: ServerConfig, cwd: string): LinterClient {
31
+ return new LspLinterClient(config, cwd);
32
+ }
33
+
29
34
  constructor(config: ServerConfig, cwd: string) {
30
35
  this.config = config;
31
36
  this.cwd = cwd;
@@ -89,10 +94,3 @@ export class LspLinterClient implements LinterClient {
89
94
  // Client lifecycle is managed globally, nothing to dispose here
90
95
  }
91
96
  }
92
-
93
- /**
94
- * Factory function to create an LSP linter client.
95
- */
96
- export function createLspLinterClient(config: ServerConfig, cwd: string): LinterClient {
97
- return new LspLinterClient(config, cwd);
98
- }
@@ -4,7 +4,7 @@ import { YAML } from "bun";
4
4
  import { globSync } from "glob";
5
5
  import { getConfigDirPaths } from "../../../config";
6
6
  import { logger } from "../../logger";
7
- import { createBiomeClient } from "./clients/biome-client";
7
+ import { BiomeClient } from "./clients/biome-client";
8
8
  import DEFAULTS from "./defaults.json" with { type: "json" };
9
9
  import type { ServerConfig } from "./types";
10
10
 
@@ -137,7 +137,7 @@ function applyRuntimeDefaults(servers: Record<string, ServerConfig>): Record<str
137
137
  const updated: Record<string, ServerConfig> = { ...servers };
138
138
 
139
139
  if (updated.biome) {
140
- updated.biome = { ...updated.biome, createClient: createBiomeClient };
140
+ updated.biome = { ...updated.biome, createClient: BiomeClient.create };
141
141
  }
142
142
 
143
143
  if (updated.omnisharp?.args) {