@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,269 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { OutputBudget } from "../../shared/OutputBudget";
3
- import type { GrepMatch } from "./grep";
4
- import { formatTitle, renderMatches } from "./render";
5
-
6
- const fixture: readonly GrepMatch[] = [
7
- {
8
- filePath: "/repo/older.ts",
9
- mtime: 1_000,
10
- fileLines: ["alpha"],
11
- ranges: [{ startLineNumber: 1, endLineNumber: 1 }],
12
- lines: [{ lineNumber: 1, text: "alpha" }],
13
- },
14
- {
15
- filePath: "/repo/newer.ts",
16
- mtime: 2_000,
17
- fileLines: ["intro", "alpha", "middle", "tail", "alphabet"],
18
- ranges: [
19
- { startLineNumber: 2, endLineNumber: 2 },
20
- { startLineNumber: 5, endLineNumber: 5 },
21
- ],
22
- lines: [
23
- { lineNumber: 2, text: "alpha" },
24
- { lineNumber: 5, text: "alphabet" },
25
- ],
26
- },
27
- ];
28
-
29
- const relativeOptions = {
30
- cwd: "/repo",
31
- pathFormat: "relative",
32
- context: 0,
33
- } as const;
34
-
35
- const absoluteOptions = {
36
- cwd: "/repo",
37
- pathFormat: "absolute",
38
- context: 0,
39
- } as const;
40
-
41
- describe("renderMatches", () => {
42
- test("files_with_matches sorts by recency desc and renders relative paths", () => {
43
- const outcome = renderMatches(
44
- fixture,
45
- "files_with_matches",
46
- 1000,
47
- relativeOptions
48
- );
49
- expect(outcome.body).toBe("newer.ts\nolder.ts");
50
- expect(outcome.fileCount).toBe(2);
51
- expect(outcome.totalMatches).toBe(3);
52
- expect(outcome.totalItems).toBe(2);
53
- expect(outcome.truncated).toBe(false);
54
- });
55
-
56
- test("can render absolute paths", () => {
57
- const outcome = renderMatches(
58
- fixture,
59
- "files_with_matches",
60
- 1000,
61
- absoluteOptions
62
- );
63
- expect(outcome.body).toBe("/repo/newer.ts\n/repo/older.ts");
64
- });
65
-
66
- test("content emits path:line:text lines without markers when context is omitted", () => {
67
- const outcome = renderMatches(fixture, "content", 1000, relativeOptions);
68
- expect(outcome.body).toBe(
69
- ["newer.ts:2:alpha", "newer.ts:5:alphabet", "older.ts:1:alpha"].join("\n")
70
- );
71
- expect(outcome.totalItems).toBe(3);
72
- });
73
-
74
- test("content can include context lines and distinguish matches", () => {
75
- const outcome = renderMatches(fixture, "content", 1000, {
76
- ...relativeOptions,
77
- context: 1,
78
- });
79
- expect(outcome.body).toBe(
80
- [
81
- " newer.ts:1:intro",
82
- "> newer.ts:2:alpha",
83
- " newer.ts:3:middle",
84
- " newer.ts:4:tail",
85
- "> newer.ts:5:alphabet",
86
- "> older.ts:1:alpha",
87
- ].join("\n")
88
- );
89
- });
90
-
91
- test("content inserts separators between non-overlapping context blocks", () => {
92
- const outcome = renderMatches(
93
- [
94
- {
95
- filePath: "/repo/gapped.ts",
96
- mtime: 3_000,
97
- fileLines: ["1", "2", "hit", "4", "5", "6", "hit", "8", "9"],
98
- ranges: [
99
- { startLineNumber: 3, endLineNumber: 3 },
100
- { startLineNumber: 7, endLineNumber: 7 },
101
- ],
102
- lines: [
103
- { lineNumber: 3, text: "hit" },
104
- { lineNumber: 7, text: "hit" },
105
- ],
106
- },
107
- ],
108
- "content",
109
- 1000,
110
- { ...relativeOptions, context: 1 }
111
- );
112
-
113
- expect(outcome.body).toBe(
114
- [
115
- " gapped.ts:2:2",
116
- "> gapped.ts:3:hit",
117
- " gapped.ts:4:4",
118
- "--",
119
- " gapped.ts:6:6",
120
- "> gapped.ts:7:hit",
121
- " gapped.ts:8:8",
122
- ].join("\n")
123
- );
124
- });
125
-
126
- test("count sorts by match count desc, then mtime desc, then path asc", () => {
127
- const outcome = renderMatches(fixture, "count", 1000, relativeOptions);
128
- expect(outcome.body).toBe("newer.ts:2\nolder.ts:1");
129
- });
130
-
131
- test("flags truncation when results exceed headLimit", () => {
132
- const outcome = renderMatches(fixture, "content", 2, relativeOptions);
133
- expect(outcome.body).toBe("newer.ts:2:alpha\nnewer.ts:5:alphabet");
134
- expect(outcome.truncated).toBe(true);
135
- expect(outcome.visibleItems).toBe(2);
136
- expect(outcome.totalItems).toBe(3);
137
- });
138
-
139
- test("returns a no-match outcome when there are no results", () => {
140
- const outcome = renderMatches([], "content", 1000, relativeOptions);
141
- expect(outcome.body).toBe("No matches.");
142
- expect(outcome.truncated).toBe(false);
143
- expect(outcome.fileCount).toBe(0);
144
- expect(outcome.totalMatches).toBe(0);
145
- });
146
-
147
- test("truncates matched lines from minified files so a single hit cannot blow up the body", () => {
148
- const minified = "a".repeat(50_000);
149
- const outcome = renderMatches(
150
- [
151
- {
152
- filePath: "/repo/dist/bundle.js",
153
- mtime: 1_000,
154
- fileLines: [minified],
155
- ranges: [{ startLineNumber: 1, endLineNumber: 1 }],
156
- lines: [{ lineNumber: 1, text: minified }],
157
- },
158
- ],
159
- "content",
160
- 1000,
161
- relativeOptions
162
- );
163
-
164
- expect(outcome.body).toContain(
165
- `(line truncated to ${OutputBudget.maxLineLength} chars)`
166
- );
167
- expect(Buffer.byteLength(outcome.body, "utf8")).toBeLessThanOrEqual(
168
- OutputBudget.maxBytes
169
- );
170
- });
171
-
172
- test("byte cap drops trailing rendered lines when many matches each push toward the cap", () => {
173
- const longText = "x".repeat(OutputBudget.maxLineLength);
174
- const fileLines = Array.from({ length: 40 }, () => longText);
175
- const ranges = fileLines.map((_, index) => ({
176
- startLineNumber: index + 1,
177
- endLineNumber: index + 1,
178
- }));
179
- const lines = fileLines.map((text, index) => ({
180
- lineNumber: index + 1,
181
- text,
182
- }));
183
-
184
- const outcome = renderMatches(
185
- [
186
- {
187
- filePath: "/repo/big.txt",
188
- mtime: 1_000,
189
- fileLines,
190
- ranges,
191
- lines,
192
- },
193
- ],
194
- "content",
195
- 1000,
196
- relativeOptions
197
- );
198
-
199
- expect(outcome.truncated).toBe(true);
200
- expect(outcome.visibleItems).toBeLessThan(outcome.totalItems);
201
- expect(Buffer.byteLength(outcome.body, "utf8")).toBeLessThanOrEqual(
202
- OutputBudget.maxBytes
203
- );
204
- });
205
- });
206
-
207
- describe("formatTitle", () => {
208
- test("uses relative path under cwd and includes glob", () => {
209
- const title = formatTitle({
210
- pattern: "alpha",
211
- path: "/repo/src",
212
- glob: "**/*.ts",
213
- cwd: "/repo",
214
- });
215
- expect(title).toBe("/alpha/ in src **/*.ts");
216
- });
217
-
218
- test("formats regex patterns with slashes", () => {
219
- const title = formatTitle({
220
- pattern: "^alpha$",
221
- path: "/repo/src",
222
- glob: undefined,
223
- cwd: "/repo",
224
- });
225
- expect(title).toBe("/^alpha$/ in src");
226
- });
227
-
228
- test("falls back to '.' when path is omitted", () => {
229
- const title = formatTitle({
230
- pattern: "alpha",
231
- path: undefined,
232
- glob: undefined,
233
- cwd: "/repo",
234
- });
235
- expect(title).toBe("/alpha/ in .");
236
- });
237
-
238
- test("resolves relative paths in titles", () => {
239
- const title = formatTitle({
240
- pattern: "alpha",
241
- path: "src",
242
- glob: undefined,
243
- cwd: "/repo",
244
- });
245
- expect(title).toBe("/alpha/ in src");
246
- });
247
-
248
- test("appends pluralized file count when provided", () => {
249
- const title = formatTitle({
250
- pattern: "alpha",
251
- path: "/repo/src",
252
- glob: "**/*.ts",
253
- cwd: "/repo",
254
- fileCount: 3,
255
- });
256
- expect(title).toBe("/alpha/ in src **/*.ts (3 files)");
257
- });
258
-
259
- test("uses singular noun for a single file", () => {
260
- const title = formatTitle({
261
- pattern: "alpha",
262
- path: undefined,
263
- glob: undefined,
264
- cwd: "/repo",
265
- fileCount: 1,
266
- });
267
- expect(title).toBe("/alpha/ in . (1 file)");
268
- });
269
- });
@@ -1,177 +0,0 @@
1
- import { chmod, mkdir, mkdtemp, rm, 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 { OutputBudget } from "../../shared/OutputBudget";
6
- import { buildReadRange, readFile } from "./read";
7
-
8
- const MAX_LINE_LENGTH = OutputBudget.maxLineLength;
9
-
10
- const tempRoots: string[] = [];
11
-
12
- const tempRoot = async (): Promise<string> => {
13
- const root = await mkdtemp(join(tmpdir(), "pim-read-tool-"));
14
- tempRoots.push(root);
15
- return root;
16
- };
17
-
18
- afterAll(async () => {
19
- await Promise.all(
20
- tempRoots.map((root) => rm(root, { force: true, recursive: true }))
21
- );
22
- });
23
-
24
- describe("readFile", () => {
25
- test("emits inclusive LINE:CONTENT ranges", async () => {
26
- const root = await tempRoot();
27
- const path = join(root, "notes.txt");
28
- await writeFile(path, "alpha\nbeta\ngamma", "utf8");
29
-
30
- const outcome = await readFile(path, buildReadRange(2, 2));
31
- expect(outcome.body).toBe("2:beta");
32
- expect(outcome.totalLines).toBe(3);
33
- expect(outcome.visibleStart).toBe(2);
34
- expect(outcome.visibleEnd).toBe(2);
35
- expect(outcome.truncatedByByteCap).toBe(false);
36
- expect(outcome.truncatedByEnd).toBe(true);
37
- expect(outcome.nextStart).toBe(3);
38
- });
39
-
40
- test("does not surface a phantom final line for files ending in a newline", async () => {
41
- const root = await tempRoot();
42
- const path = join(root, "notes.txt");
43
- await writeFile(path, "alpha\nbeta\ngamma\n", "utf8");
44
-
45
- const outcome = await readFile(path, buildReadRange(undefined, undefined));
46
- expect(outcome.body).toBe(["1:alpha", "2:beta", "3:gamma"].join("\n"));
47
- expect(outcome.totalLines).toBe(3);
48
- expect(outcome.truncatedByEnd).toBe(false);
49
- });
50
-
51
- test("strips UTF-8 BOM from output and reports it in details", async () => {
52
- const root = await tempRoot();
53
- const path = join(root, "bom.txt");
54
- await writeFile(path, "\uFEFFalpha\nbeta", "utf8");
55
-
56
- const outcome = await readFile(path, buildReadRange(undefined, undefined));
57
- expect(outcome.body).toBe("1:alpha\n2:beta");
58
- expect(outcome.hadBom).toBe(true);
59
- });
60
-
61
- test("truncates very long individual lines", async () => {
62
- const root = await tempRoot();
63
- const path = join(root, "long-line.txt");
64
- await writeFile(path, `${"x".repeat(MAX_LINE_LENGTH + 10)}\nshort`, "utf8");
65
-
66
- const outcome = await readFile(path, buildReadRange(1, 1));
67
- expect(outcome.body).toBe(
68
- `1:${"x".repeat(MAX_LINE_LENGTH)}... (line truncated to ${MAX_LINE_LENGTH} chars)`
69
- );
70
- });
71
-
72
- test("throws on empty files and out-of-range starts", async () => {
73
- const root = await tempRoot();
74
- const path = join(root, "empty.txt");
75
- await writeFile(path, "", "utf8");
76
-
77
- await expect(
78
- readFile(path, buildReadRange(undefined, undefined))
79
- ).rejects.toThrow("File is empty. Use the write tool to create content.");
80
-
81
- const populated = join(root, "small.txt");
82
- await writeFile(populated, "alpha\nbeta", "utf8");
83
- await expect(
84
- readFile(populated, buildReadRange(99, undefined))
85
- ).rejects.toThrow(
86
- "Start 99 is beyond end of file (2 lines total). Use start=1 to read from the beginning, or start=2 to read the last line."
87
- );
88
- });
89
-
90
- test("rejects directories and binary files", async () => {
91
- const root = await tempRoot();
92
- const nested = join(root, "nested");
93
- const binary = join(root, "data.bin");
94
-
95
- await mkdir(nested);
96
- await Bun.write(binary, new Uint8Array([1, 0, 2]));
97
-
98
- await expect(
99
- readFile(nested, buildReadRange(undefined, undefined))
100
- ).rejects.toThrow(`Path is a directory: ${nested}`);
101
-
102
- await expect(
103
- readFile(binary, buildReadRange(undefined, undefined))
104
- ).rejects.toThrow("Read only supports UTF-8 text files");
105
- });
106
-
107
- test("returns missing-file error with sibling suggestions", async () => {
108
- const root = await tempRoot();
109
- const missing = join(root, "note.txt");
110
- await writeFile(join(root, "notes.txt"), "alpha", "utf8");
111
-
112
- await expect(
113
- readFile(missing, buildReadRange(undefined, undefined))
114
- ).rejects.toThrow(/Did you mean one of these\?[\s\S]*notes\.txt/);
115
- });
116
-
117
- test("surfaces permission-denied as a structured error", async () => {
118
- if (process.getuid?.() === 0) {
119
- return;
120
- }
121
-
122
- const root = await tempRoot();
123
- const path = join(root, "locked.txt");
124
- await writeFile(path, "secret", "utf8");
125
- await chmod(path, 0o000);
126
-
127
- try {
128
- await expect(
129
- readFile(path, buildReadRange(undefined, undefined))
130
- ).rejects.toThrow(`Permission denied reading ${path}.`);
131
- } finally {
132
- await chmod(path, 0o644);
133
- }
134
- });
135
-
136
- test("applies a 32 KiB head-only byte cap and reports pagination metadata", async () => {
137
- const root = await tempRoot();
138
- const path = join(root, "long.txt");
139
- const lines = Array.from(
140
- { length: 80 },
141
- (_, index) => `${index + 1}: ${"x".repeat(500)}`
142
- );
143
- await writeFile(path, lines.join("\n"), "utf8");
144
-
145
- const outcome = await readFile(path, buildReadRange(undefined, undefined));
146
- expect(Buffer.byteLength(outcome.body, "utf8")).toBeLessThanOrEqual(
147
- 32 * 1024
148
- );
149
- expect(outcome.truncatedByByteCap).toBe(true);
150
- expect(outcome.truncatedByEnd).toBe(true);
151
- expect(outcome.nextStart).toBe(outcome.visibleEnd + 1);
152
- expect(outcome.totalLines).toBe(80);
153
- });
154
- });
155
-
156
- describe("buildReadRange", () => {
157
- test("rejects end before start", () => {
158
- expect(() => buildReadRange(5, 4)).toThrow(
159
- "Read end line 4 must be >= start line 5."
160
- );
161
- });
162
-
163
- test("rejects non-positive integers", () => {
164
- expect(() => buildReadRange(0, undefined)).toThrow(
165
- "Read start 0 must be a positive integer."
166
- );
167
- expect(() => buildReadRange(undefined, -1)).toThrow(
168
- "Read end -1 must be a positive integer."
169
- );
170
- });
171
-
172
- test("defaults start to 1", () => {
173
- expect(buildReadRange(undefined, undefined)).toEqual({
174
- start: 1,
175
- });
176
- });
177
- });
@@ -1,61 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { formatTitlePath } from "./render";
3
-
4
- describe("formatTitlePath", () => {
5
- const cwd = "/work/repo";
6
-
7
- test("renders relative path without format suffix", () => {
8
- expect(
9
- formatTitlePath({
10
- path: "/work/repo/src/foo.ts",
11
- cwd,
12
- start: undefined,
13
- end: undefined,
14
- })
15
- ).toBe("src/foo.ts");
16
- });
17
-
18
- test("renders explicit start-end range", () => {
19
- expect(
20
- formatTitlePath({
21
- path: "/work/repo/src/foo.ts",
22
- cwd,
23
- start: 40,
24
- end: 80,
25
- })
26
- ).toBe("src/foo.ts:40-80");
27
- });
28
-
29
- test("renders start-only range", () => {
30
- expect(
31
- formatTitlePath({
32
- path: "/work/repo/src/foo.ts",
33
- cwd,
34
- start: 40,
35
- end: undefined,
36
- })
37
- ).toBe("src/foo.ts:40");
38
- });
39
-
40
- test("falls back to absolute path when outside cwd", () => {
41
- expect(
42
- formatTitlePath({
43
- path: "/etc/hosts",
44
- cwd,
45
- start: undefined,
46
- end: undefined,
47
- })
48
- ).toBe("/etc/hosts");
49
- });
50
-
51
- test("placeholder when path is missing", () => {
52
- expect(
53
- formatTitlePath({
54
- path: undefined,
55
- cwd,
56
- start: undefined,
57
- end: undefined,
58
- })
59
- ).toBe("...");
60
- });
61
- });
@@ -1,44 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { validateToolArguments } from "@earendil-works/pi-ai";
4
- import registerSubagent from "./index";
5
- import { subagentSchema } from "./schema";
6
-
7
- function validate(args: unknown): void {
8
- validateToolArguments(
9
- { name: "subagent", parameters: subagentSchema } as never,
10
- {
11
- type: "toolCall",
12
- id: "1",
13
- name: "subagent",
14
- arguments: args as Record<string, unknown>,
15
- }
16
- );
17
- }
18
-
19
- describe("subagent extension registration", () => {
20
- test("schema rejects an empty prompt", () => {
21
- expect(() => validate({ prompt: "" })).toThrow();
22
- });
23
-
24
- test("registers one parallel tool without a prompt snippet", () => {
25
- let tool:
26
- | {
27
- readonly name: string;
28
- readonly executionMode?: string;
29
- readonly promptSnippet?: string;
30
- readonly parameters: unknown;
31
- }
32
- | undefined;
33
- registerSubagent({
34
- registerTool(def) {
35
- tool = def;
36
- },
37
- } as ExtensionAPI);
38
-
39
- expect(tool?.name).toBe("subagent");
40
- expect(tool?.executionMode).toBe("parallel");
41
- expect(tool?.promptSnippet).toBeUndefined();
42
- expect(tool?.parameters).toBe(subagentSchema);
43
- });
44
- });