@aaroncql/pim-agent 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +94 -66
  2. package/bin/pim.ts +55 -3
  3. package/package.json +20 -5
  4. package/src/extensions/_init/index.ts +3 -2
  5. package/src/extensions/apply-patch/coordinator.ts +49 -0
  6. package/src/extensions/apply-patch/executor.ts +566 -0
  7. package/src/extensions/apply-patch/index.ts +74 -0
  8. package/src/extensions/apply-patch/matcher.ts +66 -0
  9. package/src/extensions/apply-patch/model.ts +34 -0
  10. package/src/extensions/apply-patch/parser.ts +381 -0
  11. package/src/extensions/apply-patch/render.ts +261 -0
  12. package/src/extensions/apply-patch/schema.ts +43 -0
  13. package/src/extensions/apply-patch/types.ts +30 -0
  14. package/src/extensions/bash/index.ts +3 -3
  15. package/src/extensions/edit/index.ts +2 -1
  16. package/src/extensions/glob/index.ts +3 -1
  17. package/src/extensions/glob/schema.ts +2 -1
  18. package/src/extensions/grep/index.ts +3 -1
  19. package/src/extensions/grep/render.ts +18 -4
  20. package/src/extensions/grep/schema.ts +1 -1
  21. package/src/extensions/read/index.ts +36 -9
  22. package/src/extensions/read/render.ts +31 -3
  23. package/src/extensions/subagent/index.ts +4 -1
  24. package/src/extensions/todo/index.ts +4 -3
  25. package/src/extensions/web-search/index.ts +2 -1
  26. package/src/extensions/write/index.ts +2 -1
  27. package/src/shared/PatchSummary.ts +82 -0
  28. package/src/telegram/Renderer.ts +190 -4
  29. package/src/extensions/bash/capture.test.ts +0 -126
  30. package/src/extensions/bash/format.test.ts +0 -240
  31. package/src/extensions/bash/run.test.ts +0 -262
  32. package/src/extensions/command-picker/ranker.test.ts +0 -46
  33. package/src/extensions/edit/edit.test.ts +0 -285
  34. package/src/extensions/file-picker/catalog.test.ts +0 -263
  35. package/src/extensions/file-picker/index.test.ts +0 -168
  36. package/src/extensions/file-picker/ranker.test.ts +0 -94
  37. package/src/extensions/footer/git.test.ts +0 -76
  38. package/src/extensions/footer/index.test.ts +0 -161
  39. package/src/extensions/footer/segments.test.ts +0 -164
  40. package/src/extensions/glob/glob.test.ts +0 -171
  41. package/src/extensions/glob/index.test.ts +0 -68
  42. package/src/extensions/glob/render.test.ts +0 -126
  43. package/src/extensions/grep/grep.test.ts +0 -387
  44. package/src/extensions/grep/index.test.ts +0 -68
  45. package/src/extensions/grep/render.test.ts +0 -269
  46. package/src/extensions/read/read.test.ts +0 -177
  47. package/src/extensions/read/render.test.ts +0 -61
  48. package/src/extensions/subagent/index.test.ts +0 -44
  49. package/src/extensions/subagent/render.test.ts +0 -292
  50. package/src/extensions/subagent/subagent.test.ts +0 -315
  51. package/src/extensions/system-prompt/prompt.test.ts +0 -64
  52. package/src/extensions/todo/index.test.ts +0 -244
  53. package/src/extensions/todo/render.test.ts +0 -180
  54. package/src/extensions/todo/todo.test.ts +0 -222
  55. package/src/extensions/tps/index.test.ts +0 -254
  56. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +0 -119
  57. package/src/extensions/web-fetch/fetch.test.ts +0 -244
  58. package/src/extensions/web-fetch/render.test.ts +0 -56
  59. package/src/extensions/web-search/ExaMcpClient.test.ts +0 -143
  60. package/src/extensions/web-search/render.test.ts +0 -21
  61. package/src/extensions/web-search/search.test.ts +0 -53
  62. package/src/extensions/working-indicator/index.test.ts +0 -21
  63. package/src/extensions/write/render.test.ts +0 -64
  64. package/src/extensions/write/write.test.ts +0 -108
  65. package/src/shared/DiffLines.test.ts +0 -193
  66. package/src/shared/DiffRenderer.test.ts +0 -206
  67. package/src/shared/EditMatcher.test.ts +0 -123
  68. package/src/shared/FileScanner.test.ts +0 -158
  69. package/src/shared/FuzzyMatcher.test.ts +0 -114
  70. package/src/shared/GitignoreFilter.test.ts +0 -64
  71. package/src/shared/Lines.test.ts +0 -25
  72. package/src/shared/McpClient.test.ts +0 -235
  73. package/src/shared/OutputBudget.test.ts +0 -99
  74. package/src/shared/Paths.test.ts +0 -51
  75. package/src/shared/PimSettings.test.ts +0 -90
  76. package/src/shared/Renderer.test.ts +0 -190
  77. package/src/shared/SpillCache.test.ts +0 -94
  78. package/src/shared/Tools.test.ts +0 -392
  79. package/src/telegram/Config.test.ts +0 -275
  80. package/src/telegram/Markdown.test.ts +0 -143
  81. package/src/telegram/Renderer.test.ts +0 -216
  82. package/src/telegram/SessionRegistry.test.ts +0 -89
  83. package/src/telegram/TaskScheduler.test.ts +0 -278
  84. package/src/telegram/TaskTool.test.ts +0 -179
@@ -1,387 +0,0 @@
1
- import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterAll, describe, expect, test } from "bun:test";
5
- import { buildMatcher, findMatches } from "./grep";
6
-
7
- const tempRoots: string[] = [];
8
-
9
- const tempRoot = async (): Promise<string> => {
10
- const root = await mkdtemp(join(tmpdir(), "pim-grep-tool-"));
11
- tempRoots.push(root);
12
- return root;
13
- };
14
-
15
- afterAll(async () => {
16
- await Promise.all(
17
- tempRoots.map((root) => rm(root, { force: true, recursive: true }))
18
- );
19
- });
20
-
21
- const makeMatcher = (
22
- pattern: string,
23
- options?: {
24
- readonly caseInsensitive?: boolean;
25
- readonly matchAcrossLines?: boolean;
26
- }
27
- ) =>
28
- buildMatcher({
29
- pattern,
30
- caseInsensitive: options?.caseInsensitive ?? false,
31
- matchAcrossLines: options?.matchAcrossLines ?? false,
32
- });
33
-
34
- const defaultScanOptions = {
35
- includeDotfiles: false,
36
- includeIgnored: false,
37
- } as const;
38
-
39
- describe("buildMatcher", () => {
40
- test("compiles regexes with no flags by default", () => {
41
- const matcher = makeMatcher("alpha");
42
- expect(matcher.regex.flags).toBe("");
43
- expect(matcher.regex.test("alpha")).toBe(true);
44
- expect(matcher.regex.test("Alpha")).toBe(false);
45
- });
46
-
47
- test("applies the i flag for caseInsensitive regexes", () => {
48
- const matcher = makeMatcher("alpha", { caseInsensitive: true });
49
- expect(matcher.regex.flags).toBe("i");
50
- expect(matcher.regex.test("Alpha")).toBe(true);
51
- });
52
-
53
- test("applies the s flag for matchAcrossLines regexes", () => {
54
- const matcher = makeMatcher(".", { matchAcrossLines: true });
55
- expect(matcher.regex.flags).toBe("s");
56
- expect(matcher.regex.test("\n")).toBe(true);
57
- });
58
-
59
- test("throws an actionable error on invalid regex syntax", () => {
60
- expect(() => makeMatcher("(")).toThrow(/Invalid regular expression/);
61
- });
62
- });
63
-
64
- describe("findMatches", () => {
65
- test("returns content matches with line numbers", async () => {
66
- const root = await tempRoot();
67
- const nested = join(root, "nested");
68
- const older = join(root, "older.txt");
69
- const newer = join(nested, "newer.txt");
70
-
71
- await mkdir(nested);
72
- await writeFile(older, "alpha\nbeta", "utf8");
73
- await writeFile(newer, "gamma\nalphabet\nalpha", "utf8");
74
- await utimes(
75
- older,
76
- new Date("2024-01-01T00:00:00Z"),
77
- new Date("2024-01-01T00:00:00Z")
78
- );
79
- await utimes(
80
- newer,
81
- new Date("2024-01-02T00:00:00Z"),
82
- new Date("2024-01-02T00:00:00Z")
83
- );
84
-
85
- const matches = await findMatches(
86
- root,
87
- undefined,
88
- makeMatcher("alpha"),
89
- defaultScanOptions
90
- );
91
-
92
- expect(matches.map((match) => match.filePath)).toEqual([newer, older]);
93
- expect(matches[0]?.lines).toEqual([
94
- { lineNumber: 2, text: "alphabet" },
95
- { lineNumber: 3, text: "alpha" },
96
- ]);
97
- expect(matches[1]?.lines).toEqual([{ lineNumber: 1, text: "alpha" }]);
98
- });
99
-
100
- test("escapes regex metacharacters when searching literal text", async () => {
101
- const root = await tempRoot();
102
- const path = join(root, "code.ts");
103
- await writeFile(path, "useFoo(\nfoo.bar[0]\n", "utf8");
104
-
105
- const matches = await findMatches(
106
- root,
107
- undefined,
108
- makeMatcher("foo\\.bar\\[0\\]"),
109
- defaultScanOptions
110
- );
111
-
112
- expect(matches.map((match) => match.filePath)).toEqual([path]);
113
- expect(matches[0]?.lines).toEqual([{ lineNumber: 2, text: "foo.bar[0]" }]);
114
- });
115
-
116
- test("supports regular expressions", async () => {
117
- const root = await tempRoot();
118
- const path = join(root, "code.ts");
119
- await writeFile(path, "alpha\nbeta\n", "utf8");
120
-
121
- const matches = await findMatches(
122
- root,
123
- undefined,
124
- makeMatcher("^a.*a$"),
125
- defaultScanOptions
126
- );
127
-
128
- expect(matches.map((match) => match.filePath)).toEqual([path]);
129
- expect(matches[0]?.lines).toEqual([{ lineNumber: 1, text: "alpha" }]);
130
- });
131
-
132
- test("matches escaped dots and alternation as regex syntax", async () => {
133
- const root = await tempRoot();
134
- const schema = join(root, "src", "extensions", "todo", "schema.ts");
135
- const helper = join(root, "src", "shared", "arrays.ts");
136
-
137
- await mkdir(join(root, "src", "extensions", "todo"), { recursive: true });
138
- await mkdir(join(root, "src", "shared"), { recursive: true });
139
- await writeFile(schema, "const x = Type.Union([Type.String()]);\n", "utf8");
140
- await writeFile(
141
- helper,
142
- "export const oneOrMany = normalizeArray;\n",
143
- "utf8"
144
- );
145
-
146
- const typeUnionMatches = await findMatches(
147
- root,
148
- "src/extensions/**/schema.ts",
149
- makeMatcher("Type\\.Union"),
150
- defaultScanOptions
151
- );
152
- const alternationMatches = await findMatches(
153
- root,
154
- "src/**/*.ts",
155
- makeMatcher("StringOrArray|OneOrMany|oneOrMany|normalizeArray"),
156
- defaultScanOptions
157
- );
158
-
159
- expect(typeUnionMatches.map((match) => match.filePath)).toEqual([schema]);
160
- expect(alternationMatches.map((match) => match.filePath)).toEqual([helper]);
161
- });
162
-
163
- test("matchAcrossLines enables regex matches spanning line breaks", async () => {
164
- const root = await tempRoot();
165
- const path = join(root, "block.txt");
166
- await writeFile(path, "before\nBEGIN\nmiddle\nEND\nafter\n", "utf8");
167
-
168
- const withoutAcrossLines = await findMatches(
169
- root,
170
- undefined,
171
- makeMatcher("BEGIN.*END"),
172
- defaultScanOptions
173
- );
174
- const withAcrossLines = await findMatches(
175
- root,
176
- undefined,
177
- makeMatcher("BEGIN.*END", { matchAcrossLines: true }),
178
- defaultScanOptions
179
- );
180
-
181
- expect(withoutAcrossLines).toEqual([]);
182
- expect(withAcrossLines.map((match) => match.filePath)).toEqual([path]);
183
- expect(withAcrossLines[0]?.ranges).toEqual([
184
- { startLineNumber: 2, endLineNumber: 4 },
185
- ]);
186
- expect(withAcrossLines[0]?.lines).toEqual([
187
- { lineNumber: 2, text: "BEGIN" },
188
- { lineNumber: 3, text: "middle" },
189
- { lineNumber: 4, text: "END" },
190
- ]);
191
- });
192
-
193
- test("matchAcrossLines enables exact regex matches spanning line breaks", async () => {
194
- const root = await tempRoot();
195
- const path = join(root, "block.txt");
196
- await writeFile(path, "BEGIN\nmiddle\nEND\n", "utf8");
197
-
198
- const matches = await findMatches(
199
- root,
200
- undefined,
201
- makeMatcher("BEGIN\nmiddle", { matchAcrossLines: true }),
202
- defaultScanOptions
203
- );
204
-
205
- expect(matches.map((match) => match.filePath)).toEqual([path]);
206
- expect(matches[0]?.ranges).toEqual([
207
- { startLineNumber: 1, endLineNumber: 2 },
208
- ]);
209
- });
210
-
211
- test("respects gitignore, dotfiles, and the always-ignored defaults", async () => {
212
- const root = await tempRoot();
213
- const src = join(root, "src");
214
- const ignored = join(src, "ignored.ts");
215
- const kept = join(src, "kept.ts");
216
- const nodeModules = join(root, "node_modules", "pkg", "x.ts");
217
- const dot = join(root, ".secret", "x.ts");
218
-
219
- await mkdir(src, { recursive: true });
220
- await mkdir(join(root, "node_modules", "pkg"), { recursive: true });
221
- await mkdir(join(root, ".secret"), { recursive: true });
222
- await writeFile(join(root, ".gitignore"), "ignored.ts\n", "utf8");
223
- await writeFile(ignored, "needle\n", "utf8");
224
- await writeFile(kept, "needle\n", "utf8");
225
- await writeFile(nodeModules, "needle\n", "utf8");
226
- await writeFile(dot, "needle\n", "utf8");
227
-
228
- const matches = await findMatches(
229
- root,
230
- undefined,
231
- makeMatcher("needle"),
232
- defaultScanOptions
233
- );
234
-
235
- expect(matches.map((match) => match.filePath)).toEqual([kept]);
236
- });
237
-
238
- test("can include dotfiles and ignored paths", async () => {
239
- const root = await tempRoot();
240
- const kept = join(root, "kept.ts");
241
- const ignored = join(root, "ignored.ts");
242
- const dot = join(root, ".secret", "x.ts");
243
-
244
- await mkdir(join(root, ".secret"), { recursive: true });
245
- await writeFile(join(root, ".gitignore"), "ignored.ts\n", "utf8");
246
- await writeFile(kept, "needle\n", "utf8");
247
- await writeFile(ignored, "needle\n", "utf8");
248
- await writeFile(dot, "needle\n", "utf8");
249
-
250
- const matches = await findMatches(root, undefined, makeMatcher("needle"), {
251
- includeDotfiles: true,
252
- includeIgnored: true,
253
- });
254
-
255
- expect(matches.map((match) => match.filePath).sort()).toEqual(
256
- [dot, ignored, kept].sort()
257
- );
258
- });
259
-
260
- test("searches direct file paths even when they are dotfiles or ignored", async () => {
261
- const root = await tempRoot();
262
- const ignored = join(root, "ignored.ts");
263
- const dotfile = join(root, ".env");
264
-
265
- await writeFile(join(root, ".gitignore"), "ignored.ts\n", "utf8");
266
- await writeFile(ignored, "needle\n", "utf8");
267
- await writeFile(dotfile, "needle\n", "utf8");
268
-
269
- const ignoredMatches = await findMatches(
270
- ignored,
271
- undefined,
272
- makeMatcher("needle"),
273
- defaultScanOptions
274
- );
275
- const dotfileMatches = await findMatches(
276
- dotfile,
277
- undefined,
278
- makeMatcher("needle"),
279
- defaultScanOptions
280
- );
281
-
282
- expect(ignoredMatches.map((match) => match.filePath)).toEqual([ignored]);
283
- expect(dotfileMatches.map((match) => match.filePath)).toEqual([dotfile]);
284
- });
285
-
286
- test("filters by glob", async () => {
287
- const root = await tempRoot();
288
- const ts = join(root, "a.ts");
289
- const md = join(root, "a.md");
290
-
291
- await writeFile(ts, "needle", "utf8");
292
- await writeFile(md, "needle", "utf8");
293
-
294
- const matches = await findMatches(
295
- root,
296
- "**/*.ts",
297
- makeMatcher("needle"),
298
- defaultScanOptions
299
- );
300
-
301
- expect(matches.map((match) => match.filePath)).toEqual([ts]);
302
- });
303
-
304
- test("excludes a single glob pattern", async () => {
305
- const root = await tempRoot();
306
- const source = join(root, "src", "app.ts");
307
- const test = join(root, "src", "app.test.ts");
308
-
309
- await mkdir(join(root, "src"), { recursive: true });
310
- await writeFile(source, "needle", "utf8");
311
- await writeFile(test, "needle", "utf8");
312
-
313
- const matches = await findMatches(root, "**/*.ts", makeMatcher("needle"), {
314
- ...defaultScanOptions,
315
- exclude: ["**/*.test.ts"],
316
- });
317
-
318
- expect(matches.map((match) => match.filePath)).toEqual([source]);
319
- });
320
-
321
- test("excludes multiple glob patterns", async () => {
322
- const root = await tempRoot();
323
- const source = join(root, "src", "app.ts");
324
- const test = join(root, "src", "app.test.ts");
325
- const generated = join(root, "src", "generated", "types.ts");
326
-
327
- await mkdir(join(root, "src", "generated"), { recursive: true });
328
- await writeFile(source, "needle", "utf8");
329
- await writeFile(test, "needle", "utf8");
330
- await writeFile(generated, "needle", "utf8");
331
-
332
- const matches = await findMatches(root, "**/*.ts", makeMatcher("needle"), {
333
- ...defaultScanOptions,
334
- exclude: ["**/*.test.ts", "src/generated/**"],
335
- });
336
-
337
- expect(matches.map((match) => match.filePath)).toEqual([source]);
338
- });
339
-
340
- test("skips binary files", async () => {
341
- const root = await tempRoot();
342
- const text = join(root, "text.txt");
343
- const binary = join(root, "data.bin");
344
-
345
- await writeFile(text, "needle\n", "utf8");
346
- await Bun.write(binary, new Uint8Array([0x6e, 0x00, 0x65, 0x65]));
347
-
348
- const matches = await findMatches(
349
- root,
350
- undefined,
351
- makeMatcher("n"),
352
- defaultScanOptions
353
- );
354
-
355
- expect(matches.map((match) => match.filePath)).toEqual([text]);
356
- });
357
-
358
- test("matches a single file path directly", async () => {
359
- const root = await tempRoot();
360
- const path = join(root, "notes.txt");
361
- await writeFile(path, "alpha\nbeta\nalphabet", "utf8");
362
-
363
- const matches = await findMatches(
364
- path,
365
- undefined,
366
- makeMatcher("alpha"),
367
- defaultScanOptions
368
- );
369
-
370
- expect(matches.length).toBe(1);
371
- expect(matches[0]?.lines).toEqual([
372
- { lineNumber: 1, text: "alpha" },
373
- { lineNumber: 3, text: "alphabet" },
374
- ]);
375
- });
376
-
377
- test("throws an actionable error when the path does not exist", async () => {
378
- const root = await tempRoot();
379
- const missing = join(root, "nope");
380
-
381
- await expect(
382
- findMatches(missing, undefined, makeMatcher("x"), defaultScanOptions)
383
- ).rejects.toThrow(
384
- `Path not found: ${missing}. Use glob to locate the file or directory, or verify the path.`
385
- );
386
- });
387
- });
@@ -1,68 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type {
3
- AgentToolResult,
4
- ExtensionAPI,
5
- Theme,
6
- ToolDefinition,
7
- } from "@earendil-works/pi-coding-agent";
8
- import registerGrep from "./index";
9
-
10
- const stubTheme = {
11
- bold: (text: string) => text,
12
- fg: (_color: string, text: string) => text,
13
- } as unknown as Theme;
14
-
15
- function registeredTool(): ToolDefinition {
16
- let tool: ToolDefinition | undefined;
17
- registerGrep({
18
- registerTool(def: ToolDefinition): void {
19
- tool = def;
20
- },
21
- } as unknown as ExtensionAPI);
22
-
23
- if (tool === undefined) {
24
- throw new Error("grep tool was not registered");
25
- }
26
- return tool;
27
- }
28
-
29
- describe("grep tool renderer", () => {
30
- test("updates the visible call title with the file count when the result renders", () => {
31
- const tool = registeredTool();
32
- const args = { pattern: "alpha" };
33
- const state = {};
34
- const callContext = {
35
- args,
36
- toolCallId: "grep-1",
37
- invalidate: () => {},
38
- lastComponent: undefined,
39
- state,
40
- cwd: "/repo",
41
- executionStarted: true,
42
- argsComplete: true,
43
- isPartial: false,
44
- expanded: false,
45
- showImages: true,
46
- isError: false,
47
- };
48
- const callComponent = tool.renderCall!(args, stubTheme, callContext);
49
-
50
- expect(callComponent.render(120).join("\n")).not.toContain("(2 files)");
51
-
52
- const result: AgentToolResult<unknown> = {
53
- content: [{ type: "text", text: "src/a.ts\nsrc/b.ts" }],
54
- details: { fileCount: 2 },
55
- };
56
- tool.renderResult!(
57
- result,
58
- { expanded: false, isPartial: false },
59
- stubTheme,
60
- {
61
- ...callContext,
62
- lastComponent: undefined,
63
- }
64
- );
65
-
66
- expect(callComponent.render(120).join("\n")).toContain("(2 files)");
67
- });
68
- });