@aaroncql/pim-agent 0.0.1 → 0.1.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 (60) hide show
  1. package/README.md +19 -8
  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/bash/capture.test.ts +0 -126
  6. package/src/extensions/bash/format.test.ts +0 -240
  7. package/src/extensions/bash/run.test.ts +0 -262
  8. package/src/extensions/command-picker/ranker.test.ts +0 -46
  9. package/src/extensions/edit/edit.test.ts +0 -285
  10. package/src/extensions/file-picker/catalog.test.ts +0 -263
  11. package/src/extensions/file-picker/index.test.ts +0 -168
  12. package/src/extensions/file-picker/ranker.test.ts +0 -94
  13. package/src/extensions/footer/git.test.ts +0 -76
  14. package/src/extensions/footer/index.test.ts +0 -161
  15. package/src/extensions/footer/segments.test.ts +0 -164
  16. package/src/extensions/glob/glob.test.ts +0 -171
  17. package/src/extensions/glob/index.test.ts +0 -68
  18. package/src/extensions/glob/render.test.ts +0 -126
  19. package/src/extensions/grep/grep.test.ts +0 -387
  20. package/src/extensions/grep/index.test.ts +0 -68
  21. package/src/extensions/grep/render.test.ts +0 -269
  22. package/src/extensions/read/read.test.ts +0 -177
  23. package/src/extensions/read/render.test.ts +0 -61
  24. package/src/extensions/subagent/index.test.ts +0 -44
  25. package/src/extensions/subagent/render.test.ts +0 -292
  26. package/src/extensions/subagent/subagent.test.ts +0 -315
  27. package/src/extensions/system-prompt/prompt.test.ts +0 -64
  28. package/src/extensions/todo/index.test.ts +0 -244
  29. package/src/extensions/todo/render.test.ts +0 -180
  30. package/src/extensions/todo/todo.test.ts +0 -222
  31. package/src/extensions/tps/index.test.ts +0 -254
  32. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +0 -119
  33. package/src/extensions/web-fetch/fetch.test.ts +0 -244
  34. package/src/extensions/web-fetch/render.test.ts +0 -56
  35. package/src/extensions/web-search/ExaMcpClient.test.ts +0 -143
  36. package/src/extensions/web-search/render.test.ts +0 -21
  37. package/src/extensions/web-search/search.test.ts +0 -53
  38. package/src/extensions/working-indicator/index.test.ts +0 -21
  39. package/src/extensions/write/render.test.ts +0 -64
  40. package/src/extensions/write/write.test.ts +0 -108
  41. package/src/shared/DiffLines.test.ts +0 -193
  42. package/src/shared/DiffRenderer.test.ts +0 -206
  43. package/src/shared/EditMatcher.test.ts +0 -123
  44. package/src/shared/FileScanner.test.ts +0 -158
  45. package/src/shared/FuzzyMatcher.test.ts +0 -114
  46. package/src/shared/GitignoreFilter.test.ts +0 -64
  47. package/src/shared/Lines.test.ts +0 -25
  48. package/src/shared/McpClient.test.ts +0 -235
  49. package/src/shared/OutputBudget.test.ts +0 -99
  50. package/src/shared/Paths.test.ts +0 -51
  51. package/src/shared/PimSettings.test.ts +0 -90
  52. package/src/shared/Renderer.test.ts +0 -190
  53. package/src/shared/SpillCache.test.ts +0 -94
  54. package/src/shared/Tools.test.ts +0 -392
  55. package/src/telegram/Config.test.ts +0 -275
  56. package/src/telegram/Markdown.test.ts +0 -143
  57. package/src/telegram/Renderer.test.ts +0 -216
  58. package/src/telegram/SessionRegistry.test.ts +0 -89
  59. package/src/telegram/TaskScheduler.test.ts +0 -278
  60. 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
- });