@aexol/spectral 0.7.1 → 0.7.5

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 (219) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/agent/agents.js +1 -1
  3. package/dist/agent/index.js +199 -184
  4. package/dist/commands/serve.js +0 -3
  5. package/dist/designer/data/systems/renault/DESIGN.md +1 -1
  6. package/dist/designer/philosophies.js +668 -0
  7. package/dist/mcp/sampling-handler.js +1 -1
  8. package/dist/memory/commands/status.js +1 -1
  9. package/dist/memory/compaction.js +2 -2
  10. package/dist/memory/config.js +1 -1
  11. package/dist/memory/debug-log.js +1 -1
  12. package/dist/memory/hooks/compaction-hook.js +29 -0
  13. package/dist/memory/index.js +2 -0
  14. package/dist/memory/observer.js +2 -2
  15. package/dist/memory/project-observations-store.js +14 -0
  16. package/dist/memory/tokens.js +1 -1
  17. package/dist/memory/tools/read-project-observations.js +82 -0
  18. package/dist/memory/tools/recall-observation.js +2 -2
  19. package/dist/pi/agent-core/agent-loop.js +501 -0
  20. package/dist/pi/agent-core/agent.js +401 -0
  21. package/dist/pi/agent-core/harness/agent-harness.js +899 -0
  22. package/dist/pi/agent-core/harness/compaction/branch-summarization.js +173 -0
  23. package/dist/pi/agent-core/harness/compaction/compaction.js +532 -0
  24. package/dist/pi/agent-core/harness/compaction/utils.js +130 -0
  25. package/dist/pi/agent-core/harness/env/nodejs.js +485 -0
  26. package/dist/pi/agent-core/harness/messages.js +101 -0
  27. package/dist/pi/agent-core/harness/prompt-templates.js +229 -0
  28. package/dist/pi/agent-core/harness/session/jsonl-repo.js +100 -0
  29. package/dist/pi/agent-core/harness/session/jsonl-storage.js +230 -0
  30. package/dist/pi/agent-core/harness/session/memory-repo.js +41 -0
  31. package/dist/pi/agent-core/harness/session/memory-storage.js +113 -0
  32. package/dist/pi/agent-core/harness/session/repo-utils.js +38 -0
  33. package/dist/pi/agent-core/harness/session/session.js +196 -0
  34. package/dist/pi/agent-core/harness/session/uuid.js +49 -0
  35. package/dist/pi/agent-core/harness/skills.js +310 -0
  36. package/dist/pi/agent-core/harness/system-prompt.js +29 -0
  37. package/dist/pi/agent-core/harness/types.js +93 -0
  38. package/dist/pi/agent-core/harness/utils/shell-output.js +125 -0
  39. package/dist/pi/agent-core/harness/utils/truncate.js +289 -0
  40. package/dist/pi/agent-core/index.js +24 -0
  41. package/dist/pi/agent-core/node.js +2 -0
  42. package/dist/pi/agent-core/proxy.js +277 -0
  43. package/dist/pi/agent-core/types.js +1 -0
  44. package/dist/pi/ai/api-registry.js +43 -0
  45. package/dist/pi/ai/cli.js +120 -0
  46. package/dist/pi/ai/env-api-keys.js +169 -0
  47. package/dist/pi/ai/image-models.generated.js +441 -0
  48. package/dist/pi/ai/image-models.js +22 -0
  49. package/dist/pi/ai/images-api-registry.js +21 -0
  50. package/dist/pi/ai/images.js +13 -0
  51. package/dist/pi/ai/index.js +18 -0
  52. package/dist/pi/ai/models.generated.js +16220 -0
  53. package/dist/pi/ai/models.js +70 -0
  54. package/dist/pi/ai/oauth.js +1 -0
  55. package/dist/pi/ai/providers/anthropic.js +945 -0
  56. package/dist/pi/ai/providers/faux.js +367 -0
  57. package/dist/pi/ai/providers/github-copilot-headers.js +28 -0
  58. package/dist/pi/ai/providers/openai-completions.js +945 -0
  59. package/dist/pi/ai/providers/openai-prompt-cache.js +9 -0
  60. package/dist/pi/ai/providers/register-builtins.js +97 -0
  61. package/dist/pi/ai/providers/simple-options.js +40 -0
  62. package/dist/pi/ai/providers/transform-messages.js +183 -0
  63. package/dist/pi/ai/session-resources.js +21 -0
  64. package/dist/pi/ai/stream.js +26 -0
  65. package/dist/pi/ai/types.js +1 -0
  66. package/dist/pi/ai/utils/diagnostics.js +24 -0
  67. package/dist/pi/ai/utils/event-stream.js +80 -0
  68. package/dist/pi/ai/utils/hash.js +13 -0
  69. package/dist/pi/ai/utils/headers.js +7 -0
  70. package/dist/pi/ai/utils/json-parse.js +112 -0
  71. package/dist/pi/ai/utils/node-http-proxy.js +96 -0
  72. package/dist/pi/ai/utils/oauth/anthropic.js +334 -0
  73. package/dist/pi/ai/utils/oauth/device-code.js +54 -0
  74. package/dist/pi/ai/utils/oauth/github-copilot.js +270 -0
  75. package/dist/pi/ai/utils/oauth/index.js +121 -0
  76. package/dist/pi/ai/utils/oauth/oauth-page.js +104 -0
  77. package/dist/pi/ai/utils/oauth/openai-codex.js +384 -0
  78. package/dist/pi/ai/utils/oauth/pkce.js +30 -0
  79. package/dist/pi/ai/utils/oauth/types.js +1 -0
  80. package/dist/pi/ai/utils/overflow.js +150 -0
  81. package/dist/pi/ai/utils/sanitize-unicode.js +25 -0
  82. package/dist/pi/ai/utils/typebox-helpers.js +20 -0
  83. package/dist/pi/ai/utils/validation.js +280 -0
  84. package/dist/pi/coding-agent/bun/cli.js +7 -0
  85. package/dist/pi/coding-agent/bun/restore-sandbox-env.js +31 -0
  86. package/dist/pi/coding-agent/cli/args.js +340 -0
  87. package/dist/pi/coding-agent/cli/file-processor.js +82 -0
  88. package/dist/pi/coding-agent/cli/initial-message.js +21 -0
  89. package/dist/pi/coding-agent/cli.js +17 -0
  90. package/dist/pi/coding-agent/config.js +414 -0
  91. package/dist/pi/coding-agent/core/agent-session-runtime.js +299 -0
  92. package/dist/pi/coding-agent/core/agent-session-services.js +117 -0
  93. package/dist/pi/coding-agent/core/agent-session.js +2498 -0
  94. package/dist/pi/coding-agent/core/auth-guidance.js +20 -0
  95. package/dist/pi/coding-agent/core/auth-storage.js +441 -0
  96. package/dist/pi/coding-agent/core/bash-executor.js +110 -0
  97. package/dist/pi/coding-agent/core/compaction/branch-summarization.js +242 -0
  98. package/dist/pi/coding-agent/core/compaction/compaction.js +624 -0
  99. package/dist/pi/coding-agent/core/compaction/index.js +6 -0
  100. package/dist/pi/coding-agent/core/compaction/utils.js +152 -0
  101. package/dist/pi/coding-agent/core/defaults.js +1 -0
  102. package/dist/pi/coding-agent/core/diagnostics.js +1 -0
  103. package/dist/pi/coding-agent/core/event-bus.js +24 -0
  104. package/dist/pi/coding-agent/core/exec.js +74 -0
  105. package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +248 -0
  106. package/dist/pi/coding-agent/core/export-html/index.js +225 -0
  107. package/dist/pi/coding-agent/core/export-html/tool-renderer.js +107 -0
  108. package/dist/pi/coding-agent/core/extensions/index.js +8 -0
  109. package/dist/pi/coding-agent/core/extensions/loader.js +485 -0
  110. package/dist/pi/coding-agent/core/extensions/runner.js +824 -0
  111. package/dist/pi/coding-agent/core/extensions/types.js +44 -0
  112. package/dist/pi/coding-agent/core/extensions/wrapper.js +21 -0
  113. package/dist/pi/coding-agent/core/footer-data-provider.js +309 -0
  114. package/dist/pi/coding-agent/core/http-dispatcher.js +47 -0
  115. package/dist/pi/coding-agent/core/index.js +11 -0
  116. package/dist/pi/coding-agent/core/keybindings.js +294 -0
  117. package/dist/pi/coding-agent/core/messages.js +122 -0
  118. package/dist/pi/coding-agent/core/model-registry.js +728 -0
  119. package/dist/pi/coding-agent/core/model-resolver.js +494 -0
  120. package/dist/pi/coding-agent/core/output-guard.js +58 -0
  121. package/dist/pi/coding-agent/core/package-manager.js +2020 -0
  122. package/dist/pi/coding-agent/core/prompt-templates.js +237 -0
  123. package/dist/pi/coding-agent/core/provider-display-names.js +32 -0
  124. package/dist/pi/coding-agent/core/resolve-config-value.js +125 -0
  125. package/dist/pi/coding-agent/core/resource-loader.js +733 -0
  126. package/dist/pi/coding-agent/core/sdk.js +282 -0
  127. package/dist/pi/coding-agent/core/session-cwd.js +37 -0
  128. package/dist/pi/coding-agent/core/session-manager.js +1146 -0
  129. package/dist/pi/coding-agent/core/settings-manager.js +794 -0
  130. package/dist/pi/coding-agent/core/skills.js +386 -0
  131. package/dist/pi/coding-agent/core/slash-commands.js +24 -0
  132. package/dist/pi/coding-agent/core/source-info.js +18 -0
  133. package/dist/pi/coding-agent/core/system-prompt.js +122 -0
  134. package/dist/pi/coding-agent/core/telemetry.js +8 -0
  135. package/dist/pi/coding-agent/core/timings.js +30 -0
  136. package/dist/pi/coding-agent/core/tools/bash.js +341 -0
  137. package/dist/pi/coding-agent/core/tools/edit-diff.js +344 -0
  138. package/dist/pi/coding-agent/core/tools/edit.js +324 -0
  139. package/dist/pi/coding-agent/core/tools/file-mutation-queue.js +36 -0
  140. package/dist/pi/coding-agent/core/tools/find.js +297 -0
  141. package/dist/pi/coding-agent/core/tools/grep.js +303 -0
  142. package/dist/pi/coding-agent/core/tools/index.js +111 -0
  143. package/dist/pi/coding-agent/core/tools/ls.js +168 -0
  144. package/dist/pi/coding-agent/core/tools/output-accumulator.js +183 -0
  145. package/dist/pi/coding-agent/core/tools/path-utils.js +61 -0
  146. package/dist/pi/coding-agent/core/tools/read.js +288 -0
  147. package/dist/pi/coding-agent/core/tools/render-utils.js +48 -0
  148. package/dist/pi/coding-agent/core/tools/tool-definition-wrapper.js +33 -0
  149. package/dist/pi/coding-agent/core/tools/truncate.js +214 -0
  150. package/dist/pi/coding-agent/core/tools/write.js +212 -0
  151. package/dist/pi/coding-agent/index.js +41 -0
  152. package/dist/pi/coding-agent/main.js +5 -0
  153. package/dist/pi/coding-agent/migrations.js +280 -0
  154. package/dist/pi/coding-agent/modes/index.js +7 -0
  155. package/dist/pi/coding-agent/modes/interactive/components/diff.js +132 -0
  156. package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +35 -0
  157. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +32 -0
  158. package/dist/pi/coding-agent/modes/interactive/interactive-mode.js +3 -0
  159. package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1023 -0
  160. package/dist/pi/coding-agent/modes/print-mode.js +130 -0
  161. package/dist/pi/coding-agent/modes/rpc/jsonl.js +48 -0
  162. package/dist/pi/coding-agent/modes/rpc/rpc-client.js +409 -0
  163. package/dist/pi/coding-agent/modes/rpc/rpc-mode.js +600 -0
  164. package/dist/pi/coding-agent/modes/rpc/rpc-types.js +7 -0
  165. package/dist/pi/coding-agent/utils/ansi.js +51 -0
  166. package/dist/pi/coding-agent/utils/changelog.js +86 -0
  167. package/dist/pi/coding-agent/utils/child-process.js +87 -0
  168. package/dist/pi/coding-agent/utils/clipboard-image.js +244 -0
  169. package/dist/pi/coding-agent/utils/clipboard-native.js +13 -0
  170. package/dist/pi/coding-agent/utils/clipboard.js +116 -0
  171. package/dist/pi/coding-agent/utils/exif-orientation.js +157 -0
  172. package/dist/pi/coding-agent/utils/frontmatter.js +25 -0
  173. package/dist/pi/coding-agent/utils/fs-watch.js +24 -0
  174. package/dist/pi/coding-agent/utils/git.js +162 -0
  175. package/dist/pi/coding-agent/utils/html.js +39 -0
  176. package/dist/pi/coding-agent/utils/image-convert.js +38 -0
  177. package/dist/pi/coding-agent/utils/image-resize.js +136 -0
  178. package/dist/pi/coding-agent/utils/mime.js +68 -0
  179. package/dist/pi/coding-agent/utils/paths.js +91 -0
  180. package/dist/pi/coding-agent/utils/photon.js +120 -0
  181. package/dist/pi/coding-agent/utils/pi-user-agent.js +4 -0
  182. package/dist/pi/coding-agent/utils/shell.js +194 -0
  183. package/dist/pi/coding-agent/utils/sleep.js +16 -0
  184. package/dist/pi/coding-agent/utils/syntax-highlight.js +117 -0
  185. package/dist/pi/coding-agent/utils/tools-manager.js +327 -0
  186. package/dist/pi/coding-agent/utils/version-check.js +81 -0
  187. package/dist/pi/coding-agent/utils/windows-self-update.js +76 -0
  188. package/dist/pi/tui/autocomplete.js +631 -0
  189. package/dist/pi/tui/components/box.js +103 -0
  190. package/dist/pi/tui/components/cancellable-loader.js +34 -0
  191. package/dist/pi/tui/components/editor.js +1915 -0
  192. package/dist/pi/tui/components/image.js +88 -0
  193. package/dist/pi/tui/components/input.js +425 -0
  194. package/dist/pi/tui/components/loader.js +68 -0
  195. package/dist/pi/tui/components/markdown.js +633 -0
  196. package/dist/pi/tui/components/select-list.js +158 -0
  197. package/dist/pi/tui/components/settings-list.js +184 -0
  198. package/dist/pi/tui/components/spacer.js +22 -0
  199. package/dist/pi/tui/components/text.js +88 -0
  200. package/dist/pi/tui/components/truncated-text.js +50 -0
  201. package/dist/pi/tui/editor-component.js +1 -0
  202. package/dist/pi/tui/fuzzy.js +109 -0
  203. package/dist/pi/tui/index.js +31 -0
  204. package/dist/pi/tui/keybindings.js +173 -0
  205. package/dist/pi/tui/keys.js +1172 -0
  206. package/dist/pi/tui/kill-ring.js +43 -0
  207. package/dist/pi/tui/stdin-buffer.js +360 -0
  208. package/dist/pi/tui/terminal-image.js +335 -0
  209. package/dist/pi/tui/terminal.js +324 -0
  210. package/dist/pi/tui/tui.js +1076 -0
  211. package/dist/pi/tui/undo-stack.js +24 -0
  212. package/dist/pi/tui/utils.js +1016 -0
  213. package/dist/relay/dispatcher.js +30 -0
  214. package/dist/server/handlers/queue.js +52 -0
  215. package/dist/server/pi-bridge.js +9 -1
  216. package/dist/server/session-stream.js +76 -111
  217. package/dist/server/storage.js +154 -2
  218. package/dist/server/title-generator.js +14 -153
  219. package/package.json +24 -6
@@ -0,0 +1,303 @@
1
+ import { createInterface } from "node:readline";
2
+ import { Text } from "../../../tui/index.js";
3
+ import { spawn } from "child_process";
4
+ import { readFileSync, statSync } from "fs";
5
+ import path from "path";
6
+ import { Type } from "typebox";
7
+ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
8
+ import { ensureTool } from "../../utils/tools-manager.js";
9
+ import { resolveToCwd } from "./path-utils.js";
10
+ import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
11
+ import { wrapToolDefinition } from "./tool-definition-wrapper.js";
12
+ import { DEFAULT_MAX_BYTES, formatSize, GREP_MAX_LINE_LENGTH, truncateHead, truncateLine, } from "./truncate.js";
13
+ const grepSchema = Type.Object({
14
+ pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
15
+ path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })),
16
+ glob: Type.Optional(Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" })),
17
+ ignoreCase: Type.Optional(Type.Boolean({ description: "Case-insensitive search (default: false)" })),
18
+ literal: Type.Optional(Type.Boolean({ description: "Treat pattern as literal string instead of regex (default: false)" })),
19
+ context: Type.Optional(Type.Number({ description: "Number of lines to show before and after each match (default: 0)" })),
20
+ limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" })),
21
+ });
22
+ const DEFAULT_LIMIT = 100;
23
+ const defaultGrepOperations = {
24
+ isDirectory: (p) => statSync(p).isDirectory(),
25
+ readFile: (p) => readFileSync(p, "utf-8"),
26
+ };
27
+ function formatGrepCall(args, theme) {
28
+ const pattern = str(args?.pattern);
29
+ const rawPath = str(args?.path);
30
+ const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
31
+ const glob = str(args?.glob);
32
+ const limit = args?.limit;
33
+ const invalidArg = invalidArgText(theme);
34
+ let text = theme.fg("toolTitle", theme.bold("grep")) +
35
+ " " +
36
+ (pattern === null ? invalidArg : theme.fg("accent", `/${pattern || ""}/`)) +
37
+ theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`);
38
+ if (glob)
39
+ text += theme.fg("toolOutput", ` (${glob})`);
40
+ if (limit !== undefined)
41
+ text += theme.fg("toolOutput", ` limit ${limit}`);
42
+ return text;
43
+ }
44
+ function formatGrepResult(result, options, theme, showImages) {
45
+ const output = getTextOutput(result, showImages).trim();
46
+ let text = "";
47
+ if (output) {
48
+ const lines = output.split("\n");
49
+ const maxLines = options.expanded ? lines.length : 15;
50
+ const displayLines = lines.slice(0, maxLines);
51
+ const remaining = lines.length - maxLines;
52
+ text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
53
+ if (remaining > 0) {
54
+ text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
55
+ }
56
+ }
57
+ const matchLimit = result.details?.matchLimitReached;
58
+ const truncation = result.details?.truncation;
59
+ const linesTruncated = result.details?.linesTruncated;
60
+ if (matchLimit || truncation?.truncated || linesTruncated) {
61
+ const warnings = [];
62
+ if (matchLimit)
63
+ warnings.push(`${matchLimit} matches limit`);
64
+ if (truncation?.truncated)
65
+ warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
66
+ if (linesTruncated)
67
+ warnings.push("some lines truncated");
68
+ text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
69
+ }
70
+ return text;
71
+ }
72
+ export function createGrepToolDefinition(cwd, options) {
73
+ const customOps = options?.operations;
74
+ return {
75
+ name: "grep",
76
+ label: "grep",
77
+ description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
78
+ promptSnippet: "Search file contents for patterns (respects .gitignore)",
79
+ parameters: grepSchema,
80
+ async execute(_toolCallId, { pattern, path: searchDir, glob, ignoreCase, literal, context, limit, }, signal, _onUpdate, _ctx) {
81
+ return new Promise((resolve, reject) => {
82
+ if (signal?.aborted) {
83
+ reject(new Error("Operation aborted"));
84
+ return;
85
+ }
86
+ let settled = false;
87
+ const settle = (fn) => {
88
+ if (!settled) {
89
+ settled = true;
90
+ fn();
91
+ }
92
+ };
93
+ (async () => {
94
+ try {
95
+ const rgPath = await ensureTool("rg", true);
96
+ if (!rgPath) {
97
+ settle(() => reject(new Error("ripgrep (rg) is not available and could not be downloaded")));
98
+ return;
99
+ }
100
+ const searchPath = resolveToCwd(searchDir || ".", cwd);
101
+ const ops = customOps ?? defaultGrepOperations;
102
+ let isDirectory;
103
+ try {
104
+ isDirectory = await ops.isDirectory(searchPath);
105
+ }
106
+ catch {
107
+ settle(() => reject(new Error(`Path not found: ${searchPath}`)));
108
+ return;
109
+ }
110
+ const contextValue = context && context > 0 ? context : 0;
111
+ const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);
112
+ const formatPath = (filePath) => {
113
+ if (isDirectory) {
114
+ const relative = path.relative(searchPath, filePath);
115
+ if (relative && !relative.startsWith("..")) {
116
+ return relative.replace(/\\/g, "/");
117
+ }
118
+ }
119
+ return path.basename(filePath);
120
+ };
121
+ const fileCache = new Map();
122
+ const getFileLines = async (filePath) => {
123
+ let lines = fileCache.get(filePath);
124
+ if (!lines) {
125
+ try {
126
+ const content = await ops.readFile(filePath);
127
+ lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
128
+ }
129
+ catch {
130
+ lines = [];
131
+ }
132
+ fileCache.set(filePath, lines);
133
+ }
134
+ return lines;
135
+ };
136
+ const args = ["--json", "--line-number", "--color=never", "--hidden"];
137
+ if (ignoreCase)
138
+ args.push("--ignore-case");
139
+ if (literal)
140
+ args.push("--fixed-strings");
141
+ if (glob)
142
+ args.push("--glob", glob);
143
+ args.push("--", pattern, searchPath);
144
+ const child = spawn(rgPath, args, { stdio: ["ignore", "pipe", "pipe"] });
145
+ const rl = createInterface({ input: child.stdout });
146
+ let stderr = "";
147
+ let matchCount = 0;
148
+ let matchLimitReached = false;
149
+ let linesTruncated = false;
150
+ let aborted = false;
151
+ let killedDueToLimit = false;
152
+ const outputLines = [];
153
+ const cleanup = () => {
154
+ rl.close();
155
+ signal?.removeEventListener("abort", onAbort);
156
+ };
157
+ const stopChild = (dueToLimit = false) => {
158
+ if (!child.killed) {
159
+ killedDueToLimit = dueToLimit;
160
+ child.kill();
161
+ }
162
+ };
163
+ const onAbort = () => {
164
+ aborted = true;
165
+ stopChild();
166
+ };
167
+ signal?.addEventListener("abort", onAbort, { once: true });
168
+ child.stderr?.on("data", (chunk) => {
169
+ stderr += chunk.toString();
170
+ });
171
+ const formatBlock = async (filePath, lineNumber) => {
172
+ const relativePath = formatPath(filePath);
173
+ const lines = await getFileLines(filePath);
174
+ if (!lines.length)
175
+ return [`${relativePath}:${lineNumber}: (unable to read file)`];
176
+ const block = [];
177
+ const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
178
+ const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
179
+ for (let current = start; current <= end; current++) {
180
+ const lineText = lines[current - 1] ?? "";
181
+ const sanitized = lineText.replace(/\r/g, "");
182
+ const isMatchLine = current === lineNumber;
183
+ // Truncate long lines so grep output stays compact.
184
+ const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
185
+ if (wasTruncated)
186
+ linesTruncated = true;
187
+ if (isMatchLine)
188
+ block.push(`${relativePath}:${current}: ${truncatedText}`);
189
+ else
190
+ block.push(`${relativePath}-${current}- ${truncatedText}`);
191
+ }
192
+ return block;
193
+ };
194
+ // Collect matches during streaming, then format them after rg exits.
195
+ const matches = [];
196
+ rl.on("line", (line) => {
197
+ if (!line.trim() || matchCount >= effectiveLimit)
198
+ return;
199
+ let event;
200
+ try {
201
+ event = JSON.parse(line);
202
+ }
203
+ catch {
204
+ return;
205
+ }
206
+ if (event.type === "match") {
207
+ matchCount++;
208
+ const filePath = event.data?.path?.text;
209
+ const lineNumber = event.data?.line_number;
210
+ const lineText = event.data?.lines?.text;
211
+ if (filePath && typeof lineNumber === "number")
212
+ matches.push({ filePath, lineNumber, lineText });
213
+ if (matchCount >= effectiveLimit) {
214
+ matchLimitReached = true;
215
+ stopChild(true);
216
+ }
217
+ }
218
+ });
219
+ child.on("error", (error) => {
220
+ cleanup();
221
+ settle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));
222
+ });
223
+ child.on("close", async (code) => {
224
+ cleanup();
225
+ if (aborted) {
226
+ settle(() => reject(new Error("Operation aborted")));
227
+ return;
228
+ }
229
+ if (!killedDueToLimit && code !== 0 && code !== 1) {
230
+ const errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;
231
+ settle(() => reject(new Error(errorMsg)));
232
+ return;
233
+ }
234
+ if (matchCount === 0) {
235
+ settle(() => resolve({ content: [{ type: "text", text: "No matches found" }], details: undefined }));
236
+ return;
237
+ }
238
+ // Format matches after streaming finishes so custom readFile() backends can be async.
239
+ for (const match of matches) {
240
+ if (contextValue === 0 && match.lineText !== undefined) {
241
+ const relativePath = formatPath(match.filePath);
242
+ const sanitized = match.lineText
243
+ .replace(/\r\n/g, "\n")
244
+ .replace(/\r/g, "")
245
+ .replace(/\n$/, "");
246
+ const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
247
+ if (wasTruncated)
248
+ linesTruncated = true;
249
+ outputLines.push(`${relativePath}:${match.lineNumber}: ${truncatedText}`);
250
+ }
251
+ else {
252
+ const block = await formatBlock(match.filePath, match.lineNumber);
253
+ outputLines.push(...block);
254
+ }
255
+ }
256
+ const rawOutput = outputLines.join("\n");
257
+ // Apply byte truncation. There is no line limit here because the match limit already capped rows.
258
+ const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
259
+ let output = truncation.content;
260
+ const details = {};
261
+ // Build actionable notices for truncation and match limits.
262
+ const notices = [];
263
+ if (matchLimitReached) {
264
+ notices.push(`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`);
265
+ details.matchLimitReached = effectiveLimit;
266
+ }
267
+ if (truncation.truncated) {
268
+ notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
269
+ details.truncation = truncation;
270
+ }
271
+ if (linesTruncated) {
272
+ notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);
273
+ details.linesTruncated = true;
274
+ }
275
+ if (notices.length > 0)
276
+ output += `\n\n[${notices.join(". ")}]`;
277
+ settle(() => resolve({
278
+ content: [{ type: "text", text: output }],
279
+ details: Object.keys(details).length > 0 ? details : undefined,
280
+ }));
281
+ });
282
+ }
283
+ catch (err) {
284
+ settle(() => reject(err));
285
+ }
286
+ })();
287
+ });
288
+ },
289
+ renderCall(args, theme, context) {
290
+ const text = context.lastComponent ?? new Text("", 0, 0);
291
+ text.setText(formatGrepCall(args, theme));
292
+ return text;
293
+ },
294
+ renderResult(result, options, theme, context) {
295
+ const text = context.lastComponent ?? new Text("", 0, 0);
296
+ text.setText(formatGrepResult(result, options, theme, context.showImages));
297
+ return text;
298
+ },
299
+ };
300
+ }
301
+ export function createGrepTool(cwd, options) {
302
+ return wrapToolDefinition(createGrepToolDefinition(cwd, options));
303
+ }
@@ -0,0 +1,111 @@
1
+ export { createBashTool, createBashToolDefinition, createLocalBashOperations, } from "./bash.js";
2
+ export { createEditTool, createEditToolDefinition, } from "./edit.js";
3
+ export { withFileMutationQueue } from "./file-mutation-queue.js";
4
+ export { createFindTool, createFindToolDefinition, } from "./find.js";
5
+ export { createGrepTool, createGrepToolDefinition, } from "./grep.js";
6
+ export { createLsTool, createLsToolDefinition, } from "./ls.js";
7
+ export { createReadTool, createReadToolDefinition, } from "./read.js";
8
+ export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, truncateLine, truncateTail, } from "./truncate.js";
9
+ export { createWriteTool, createWriteToolDefinition, } from "./write.js";
10
+ import { createBashTool, createBashToolDefinition } from "./bash.js";
11
+ import { createEditTool, createEditToolDefinition } from "./edit.js";
12
+ import { createFindTool, createFindToolDefinition } from "./find.js";
13
+ import { createGrepTool, createGrepToolDefinition } from "./grep.js";
14
+ import { createLsTool, createLsToolDefinition } from "./ls.js";
15
+ import { createReadTool, createReadToolDefinition } from "./read.js";
16
+ import { createWriteTool, createWriteToolDefinition } from "./write.js";
17
+ export const allToolNames = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
18
+ export function createToolDefinition(toolName, cwd, options) {
19
+ switch (toolName) {
20
+ case "read":
21
+ return createReadToolDefinition(cwd, options?.read);
22
+ case "bash":
23
+ return createBashToolDefinition(cwd, options?.bash);
24
+ case "edit":
25
+ return createEditToolDefinition(cwd, options?.edit);
26
+ case "write":
27
+ return createWriteToolDefinition(cwd, options?.write);
28
+ case "grep":
29
+ return createGrepToolDefinition(cwd, options?.grep);
30
+ case "find":
31
+ return createFindToolDefinition(cwd, options?.find);
32
+ case "ls":
33
+ return createLsToolDefinition(cwd, options?.ls);
34
+ default:
35
+ throw new Error(`Unknown tool name: ${toolName}`);
36
+ }
37
+ }
38
+ export function createTool(toolName, cwd, options) {
39
+ switch (toolName) {
40
+ case "read":
41
+ return createReadTool(cwd, options?.read);
42
+ case "bash":
43
+ return createBashTool(cwd, options?.bash);
44
+ case "edit":
45
+ return createEditTool(cwd, options?.edit);
46
+ case "write":
47
+ return createWriteTool(cwd, options?.write);
48
+ case "grep":
49
+ return createGrepTool(cwd, options?.grep);
50
+ case "find":
51
+ return createFindTool(cwd, options?.find);
52
+ case "ls":
53
+ return createLsTool(cwd, options?.ls);
54
+ default:
55
+ throw new Error(`Unknown tool name: ${toolName}`);
56
+ }
57
+ }
58
+ export function createCodingToolDefinitions(cwd, options) {
59
+ return [
60
+ createReadToolDefinition(cwd, options?.read),
61
+ createBashToolDefinition(cwd, options?.bash),
62
+ createEditToolDefinition(cwd, options?.edit),
63
+ createWriteToolDefinition(cwd, options?.write),
64
+ ];
65
+ }
66
+ export function createReadOnlyToolDefinitions(cwd, options) {
67
+ return [
68
+ createReadToolDefinition(cwd, options?.read),
69
+ createGrepToolDefinition(cwd, options?.grep),
70
+ createFindToolDefinition(cwd, options?.find),
71
+ createLsToolDefinition(cwd, options?.ls),
72
+ ];
73
+ }
74
+ export function createAllToolDefinitions(cwd, options) {
75
+ return {
76
+ read: createReadToolDefinition(cwd, options?.read),
77
+ bash: createBashToolDefinition(cwd, options?.bash),
78
+ edit: createEditToolDefinition(cwd, options?.edit),
79
+ write: createWriteToolDefinition(cwd, options?.write),
80
+ grep: createGrepToolDefinition(cwd, options?.grep),
81
+ find: createFindToolDefinition(cwd, options?.find),
82
+ ls: createLsToolDefinition(cwd, options?.ls),
83
+ };
84
+ }
85
+ export function createCodingTools(cwd, options) {
86
+ return [
87
+ createReadTool(cwd, options?.read),
88
+ createBashTool(cwd, options?.bash),
89
+ createEditTool(cwd, options?.edit),
90
+ createWriteTool(cwd, options?.write),
91
+ ];
92
+ }
93
+ export function createReadOnlyTools(cwd, options) {
94
+ return [
95
+ createReadTool(cwd, options?.read),
96
+ createGrepTool(cwd, options?.grep),
97
+ createFindTool(cwd, options?.find),
98
+ createLsTool(cwd, options?.ls),
99
+ ];
100
+ }
101
+ export function createAllTools(cwd, options) {
102
+ return {
103
+ read: createReadTool(cwd, options?.read),
104
+ bash: createBashTool(cwd, options?.bash),
105
+ edit: createEditTool(cwd, options?.edit),
106
+ write: createWriteTool(cwd, options?.write),
107
+ grep: createGrepTool(cwd, options?.grep),
108
+ find: createFindTool(cwd, options?.find),
109
+ ls: createLsTool(cwd, options?.ls),
110
+ };
111
+ }
@@ -0,0 +1,168 @@
1
+ import { Text } from "../../../tui/index.js";
2
+ import { existsSync, readdirSync, statSync } from "fs";
3
+ import nodePath from "path";
4
+ import { Type } from "typebox";
5
+ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
6
+ import { resolveToCwd } from "./path-utils.js";
7
+ import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
8
+ import { wrapToolDefinition } from "./tool-definition-wrapper.js";
9
+ import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js";
10
+ const lsSchema = Type.Object({
11
+ path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
12
+ limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })),
13
+ });
14
+ const DEFAULT_LIMIT = 500;
15
+ const defaultLsOperations = {
16
+ exists: existsSync,
17
+ stat: statSync,
18
+ readdir: readdirSync,
19
+ };
20
+ function formatLsCall(args, theme) {
21
+ const rawPath = str(args?.path);
22
+ const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
23
+ const limit = args?.limit;
24
+ const invalidArg = invalidArgText(theme);
25
+ let text = `${theme.fg("toolTitle", theme.bold("ls"))} ${path === null ? invalidArg : theme.fg("accent", path)}`;
26
+ if (limit !== undefined) {
27
+ text += theme.fg("toolOutput", ` (limit ${limit})`);
28
+ }
29
+ return text;
30
+ }
31
+ function formatLsResult(result, options, theme, showImages) {
32
+ const output = getTextOutput(result, showImages).trim();
33
+ let text = "";
34
+ if (output) {
35
+ const lines = output.split("\n");
36
+ const maxLines = options.expanded ? lines.length : 20;
37
+ const displayLines = lines.slice(0, maxLines);
38
+ const remaining = lines.length - maxLines;
39
+ text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
40
+ if (remaining > 0) {
41
+ text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
42
+ }
43
+ }
44
+ const entryLimit = result.details?.entryLimitReached;
45
+ const truncation = result.details?.truncation;
46
+ if (entryLimit || truncation?.truncated) {
47
+ const warnings = [];
48
+ if (entryLimit)
49
+ warnings.push(`${entryLimit} entries limit`);
50
+ if (truncation?.truncated)
51
+ warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
52
+ text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
53
+ }
54
+ return text;
55
+ }
56
+ export function createLsToolDefinition(cwd, options) {
57
+ const ops = options?.operations ?? defaultLsOperations;
58
+ return {
59
+ name: "ls",
60
+ label: "ls",
61
+ description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
62
+ promptSnippet: "List directory contents",
63
+ parameters: lsSchema,
64
+ async execute(_toolCallId, { path, limit }, signal, _onUpdate, _ctx) {
65
+ return new Promise((resolve, reject) => {
66
+ if (signal?.aborted) {
67
+ reject(new Error("Operation aborted"));
68
+ return;
69
+ }
70
+ const onAbort = () => reject(new Error("Operation aborted"));
71
+ signal?.addEventListener("abort", onAbort, { once: true });
72
+ (async () => {
73
+ try {
74
+ const dirPath = resolveToCwd(path || ".", cwd);
75
+ const effectiveLimit = limit ?? DEFAULT_LIMIT;
76
+ // Check if path exists.
77
+ if (!(await ops.exists(dirPath))) {
78
+ reject(new Error(`Path not found: ${dirPath}`));
79
+ return;
80
+ }
81
+ // Check if path is a directory.
82
+ const stat = await ops.stat(dirPath);
83
+ if (!stat.isDirectory()) {
84
+ reject(new Error(`Not a directory: ${dirPath}`));
85
+ return;
86
+ }
87
+ // Read directory entries.
88
+ let entries;
89
+ try {
90
+ entries = await ops.readdir(dirPath);
91
+ }
92
+ catch (e) {
93
+ reject(new Error(`Cannot read directory: ${e.message}`));
94
+ return;
95
+ }
96
+ // Sort alphabetically, case-insensitive.
97
+ entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
98
+ // Format entries with directory indicators.
99
+ const results = [];
100
+ let entryLimitReached = false;
101
+ for (const entry of entries) {
102
+ if (results.length >= effectiveLimit) {
103
+ entryLimitReached = true;
104
+ break;
105
+ }
106
+ const fullPath = nodePath.join(dirPath, entry);
107
+ let suffix = "";
108
+ try {
109
+ const entryStat = await ops.stat(fullPath);
110
+ if (entryStat.isDirectory())
111
+ suffix = "/";
112
+ }
113
+ catch {
114
+ // Skip entries we cannot stat.
115
+ continue;
116
+ }
117
+ results.push(entry + suffix);
118
+ }
119
+ signal?.removeEventListener("abort", onAbort);
120
+ if (results.length === 0) {
121
+ resolve({ content: [{ type: "text", text: "(empty directory)" }], details: undefined });
122
+ return;
123
+ }
124
+ const rawOutput = results.join("\n");
125
+ // Apply byte truncation. There is no separate line limit because entry count is already capped.
126
+ const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
127
+ let output = truncation.content;
128
+ const details = {};
129
+ // Build actionable notices for truncation and entry limits.
130
+ const notices = [];
131
+ if (entryLimitReached) {
132
+ notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
133
+ details.entryLimitReached = effectiveLimit;
134
+ }
135
+ if (truncation.truncated) {
136
+ notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
137
+ details.truncation = truncation;
138
+ }
139
+ if (notices.length > 0) {
140
+ output += `\n\n[${notices.join(". ")}]`;
141
+ }
142
+ resolve({
143
+ content: [{ type: "text", text: output }],
144
+ details: Object.keys(details).length > 0 ? details : undefined,
145
+ });
146
+ }
147
+ catch (e) {
148
+ signal?.removeEventListener("abort", onAbort);
149
+ reject(e);
150
+ }
151
+ })();
152
+ });
153
+ },
154
+ renderCall(args, theme, context) {
155
+ const text = context.lastComponent ?? new Text("", 0, 0);
156
+ text.setText(formatLsCall(args, theme));
157
+ return text;
158
+ },
159
+ renderResult(result, options, theme, context) {
160
+ const text = context.lastComponent ?? new Text("", 0, 0);
161
+ text.setText(formatLsResult(result, options, theme, context.showImages));
162
+ return text;
163
+ },
164
+ };
165
+ }
166
+ export function createLsTool(cwd, options) {
167
+ return wrapToolDefinition(createLsToolDefinition(cwd, options));
168
+ }