@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,94 +0,0 @@
1
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
- import { mkdtemp, rm, stat, utimes, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { SpillCache } from "./SpillCache";
6
-
7
- let previousPimHomeDir: string | undefined;
8
- let testPimHomeDir: string | undefined;
9
-
10
- beforeAll(async () => {
11
- previousPimHomeDir = process.env.PIM_HOME_DIR;
12
- testPimHomeDir = await mkdtemp(join(tmpdir(), "pim-spill-home-"));
13
- process.env.PIM_HOME_DIR = testPimHomeDir;
14
- });
15
-
16
- afterAll(async () => {
17
- if (previousPimHomeDir === undefined) {
18
- delete process.env.PIM_HOME_DIR;
19
- } else {
20
- process.env.PIM_HOME_DIR = previousPimHomeDir;
21
- }
22
- if (testPimHomeDir) {
23
- await rm(testPimHomeDir, { recursive: true, force: true });
24
- }
25
- });
26
-
27
- describe("SpillCache.write", () => {
28
- test("writes a prefixed UUIDv7 file with locked-down modes", async () => {
29
- const path = await SpillCache.write("fetch", "md", "# hello\nworld");
30
- expect(path).toBeTruthy();
31
- expect(path!.startsWith(join(SpillCache.dir(), "fetch-"))).toBe(true);
32
- expect(path!.endsWith(".md")).toBe(true);
33
-
34
- const dirMode = (await stat(SpillCache.dir())).mode & 0o777;
35
- const fileMode = (await stat(path!)).mode & 0o777;
36
- expect(dirMode).toBe(0o700);
37
- expect(fileMode).toBe(0o600);
38
-
39
- expect(await Bun.file(path!).text()).toBe("# hello\nworld");
40
- });
41
-
42
- test("accepts binary payloads", async () => {
43
- const path = await SpillCache.write(
44
- "bash",
45
- "out",
46
- new Uint8Array([65, 66])
47
- );
48
- expect(path).toBeTruthy();
49
- expect(await Bun.file(path!).text()).toBe("AB");
50
- });
51
- });
52
-
53
- describe("SpillCache.cleanup", () => {
54
- test("deletes only expired spill files across prefixes", async () => {
55
- const root = await mkdtemp(join(tmpdir(), "pim-spill-cleanup-"));
56
- const now = Date.now();
57
- const oldBash = join(root, "bash-0192ce11-26d5-7dc3-9305-1426de888c5a.out");
58
- const oldFetch = join(
59
- root,
60
- "fetch-0192ce11-26d5-7dc3-9305-1426de888c5b.md"
61
- );
62
- const recent = join(root, "bash-0192ce11-26d5-7dc4-8894-bc88d506d6ee.err");
63
- const invalidName = join(root, "bash-not-a-uuid.out");
64
- const unrelated = join(root, "other-old.out");
65
- try {
66
- await writeFile(oldBash, "old");
67
- await writeFile(oldFetch, "old");
68
- await writeFile(recent, "recent");
69
- await writeFile(invalidName, "invalid");
70
- await writeFile(unrelated, "unrelated");
71
- const oldDate = new Date(now - SpillCache.TTL_MS - 1000);
72
- await utimes(oldBash, oldDate, oldDate);
73
- await utimes(oldFetch, oldDate, oldDate);
74
- await utimes(invalidName, oldDate, oldDate);
75
- await utimes(unrelated, oldDate, oldDate);
76
-
77
- SpillCache.cleanup(root, now);
78
-
79
- expect(await Bun.file(oldBash).exists()).toBe(false);
80
- expect(await Bun.file(oldFetch).exists()).toBe(false);
81
- expect(await Bun.file(recent).exists()).toBe(true);
82
- expect(await Bun.file(invalidName).exists()).toBe(true);
83
- expect(await Bun.file(unrelated).exists()).toBe(true);
84
- } finally {
85
- await rm(root, { recursive: true, force: true });
86
- }
87
- });
88
-
89
- test("is a no-op when the cache dir is absent", () => {
90
- expect(() =>
91
- SpillCache.cleanup(join(tmpdir(), "pim-spill-missing-dir"), Date.now())
92
- ).not.toThrow();
93
- });
94
- });
@@ -1,392 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { Type, type TSchema } from "typebox";
3
- import { StringEnum, validateToolArguments } from "@earendil-works/pi-ai";
4
- import { Tools } from "./Tools";
5
-
6
- function runValidator(parameters: TSchema, args: unknown): Error {
7
- try {
8
- validateToolArguments({ name: "t", parameters } as never, {
9
- type: "toolCall",
10
- id: "1",
11
- name: "t",
12
- arguments: args as Record<string, unknown>,
13
- });
14
- } catch (e) {
15
- return e as Error;
16
- }
17
- throw new Error("expected validation to fail");
18
- }
19
-
20
- function rewrite(toolName: string, parameters: TSchema, args: unknown): string {
21
- return Tools.rewriteValidationError(
22
- toolName,
23
- parameters as never,
24
- runValidator(parameters, args),
25
- args
26
- );
27
- }
28
-
29
- describe("Tools.rewriteValidationError", () => {
30
- test("missing single required property at root", () => {
31
- const params = Type.Object({ path: Type.String() });
32
- expect(rewrite("read", params, {})).toBe(
33
- 'Validation failed for tool "read":\n - missing required property: path'
34
- );
35
- });
36
-
37
- test("missing multiple required properties at root", () => {
38
- const params = Type.Object({
39
- path: Type.String(),
40
- edits: Type.Array(Type.String()),
41
- });
42
- expect(rewrite("edit", params, {})).toBe(
43
- 'Validation failed for tool "edit":\n - missing required properties: path, edits'
44
- );
45
- });
46
-
47
- test("missing nested required property", () => {
48
- const params = Type.Object({
49
- path: Type.String(),
50
- edits: Type.Array(
51
- Type.Object({
52
- old_string: Type.String(),
53
- new_string: Type.String(),
54
- })
55
- ),
56
- });
57
- expect(
58
- rewrite("edit", params, { path: "foo", edits: [{ old_string: "x" }] })
59
- ).toBe(
60
- 'Validation failed for tool "edit":\n - missing required property at edits.0: new_string'
61
- );
62
- });
63
-
64
- test("constraint messages pass through with original path", () => {
65
- const params = Type.Object({
66
- limit: Type.Integer({ minimum: 1, maximum: 2000 }),
67
- });
68
- expect(rewrite("read", params, { limit: 99999 })).toBe(
69
- 'Validation failed for tool "read":\n - limit: must be <= 2000'
70
- );
71
- });
72
-
73
- test("strips Received arguments dump", () => {
74
- const params = Type.Object({ path: Type.String() });
75
- expect(rewrite("read", params, {})).not.toContain("Received arguments");
76
- });
77
-
78
- test("non-validation errors pass through unchanged", () => {
79
- expect(
80
- Tools.rewriteValidationError(
81
- "read",
82
- Type.Object({}) as never,
83
- new Error("boom")
84
- )
85
- ).toBe("boom");
86
- });
87
-
88
- test("union of literals collapses to enumerated values (bare strings, no quotes)", () => {
89
- const params = Type.Object({
90
- action: Type.Union([
91
- Type.Literal("create"),
92
- Type.Literal("list"),
93
- Type.Literal("delete"),
94
- Type.Literal("pause"),
95
- Type.Literal("resume"),
96
- Type.Literal("update_prompt"),
97
- ]),
98
- });
99
- expect(rewrite("task", params, { action: "foo" })).toBe(
100
- 'Validation failed for tool "task":\n - action: must be one of: create, list, delete, pause, resume, update_prompt'
101
- );
102
- });
103
-
104
- test("tagged union collapses to discriminator values (bare strings)", () => {
105
- const params = Type.Object({
106
- schedule: Type.Union([
107
- Type.Object({ type: Type.Literal("once"), at: Type.String() }),
108
- Type.Object({ type: Type.Literal("interval"), every: Type.String() }),
109
- Type.Object({ type: Type.Literal("cron"), expr: Type.String() }),
110
- ]),
111
- });
112
- expect(
113
- rewrite("task", params, { schedule: { type: "foo", at: "x" } })
114
- ).toBe(
115
- 'Validation failed for tool "task":\n - schedule: must match one of the allowed variants (type: once, interval, cron)'
116
- );
117
- });
118
-
119
- test("tagged union with discriminator match shows only matched branch errors", () => {
120
- const params = Type.Object({
121
- schedule: Type.Union([
122
- Type.Object({ type: Type.Literal("once"), at: Type.String() }),
123
- Type.Object({ type: Type.Literal("interval"), every: Type.String() }),
124
- Type.Object({ type: Type.Literal("cron"), expr: Type.String() }),
125
- ]),
126
- });
127
- expect(rewrite("task", params, { schedule: { type: "once" } })).toBe(
128
- 'Validation failed for tool "task":\n - missing required property at schedule: at'
129
- );
130
- });
131
-
132
- test("StringEnum lists valid values (bare strings)", () => {
133
- const params = Type.Object({
134
- outputMode: StringEnum(["files_with_matches", "content", "count"]),
135
- });
136
- expect(rewrite("grep", params, { outputMode: "invalid_enum_value" })).toBe(
137
- 'Validation failed for tool "grep":\n - outputMode: must be one of: files_with_matches, content, count'
138
- );
139
- });
140
- });
141
-
142
- describe("Tools.wrap quoted-enum coercion", () => {
143
- function wrapTool(params: TSchema) {
144
- return Tools.wrap({
145
- name: "task",
146
- label: "task",
147
- description: "test",
148
- parameters: params,
149
- async execute() {
150
- return { content: [{ type: "text", text: "" }], details: {} };
151
- },
152
- });
153
- }
154
-
155
- test("unwraps double-quoted enum value", () => {
156
- const wrapped = wrapTool(
157
- Type.Object({
158
- action: Type.Union([Type.Literal("create"), Type.Literal("list")]),
159
- })
160
- );
161
- expect(wrapped.prepareArguments!({ action: '"create"' })).toEqual({
162
- action: "create",
163
- });
164
- });
165
-
166
- test("unwraps single-quoted enum value", () => {
167
- const wrapped = wrapTool(Type.Object({ mode: StringEnum(["foo", "bar"]) }));
168
- expect(wrapped.prepareArguments!({ mode: "'bar'" })).toEqual({
169
- mode: "bar",
170
- });
171
- });
172
-
173
- test("unwraps backtick-quoted enum value", () => {
174
- const wrapped = wrapTool(Type.Object({ mode: StringEnum(["foo", "bar"]) }));
175
- expect(wrapped.prepareArguments!({ mode: "`foo`" })).toEqual({
176
- mode: "foo",
177
- });
178
- });
179
-
180
- test("does NOT unwrap when inner value is invalid (still errors)", () => {
181
- const wrapped = wrapTool(
182
- Type.Object({
183
- action: Type.Union([Type.Literal("create"), Type.Literal("list")]),
184
- })
185
- );
186
- expect(() => wrapped.prepareArguments!({ action: '"nope"' })).toThrow(
187
- 'Validation failed for tool "task":\n - action: must be one of: create, list'
188
- );
189
- });
190
-
191
- test("unwraps nested enum inside tagged union branch", () => {
192
- const wrapped = wrapTool(
193
- Type.Object({
194
- schedule: Type.Union([
195
- Type.Object({ type: Type.Literal("once"), at: Type.String() }),
196
- Type.Object({
197
- type: Type.Literal("interval"),
198
- every: Type.String(),
199
- }),
200
- ]),
201
- })
202
- );
203
- expect(
204
- wrapped.prepareArguments!({
205
- schedule: { type: '"once"', at: "2026-01-01T00:00:00Z" },
206
- })
207
- ).toEqual({
208
- schedule: { type: "once", at: "2026-01-01T00:00:00Z" },
209
- });
210
- });
211
-
212
- test("leaves non-enum string fields alone", () => {
213
- const wrapped = wrapTool(
214
- Type.Object({ prompt: Type.String(), action: StringEnum(["a"]) })
215
- );
216
- expect(
217
- wrapped.prepareArguments!({ prompt: '"hello"', action: "a" })
218
- ).toEqual({ prompt: '"hello"', action: "a" });
219
- });
220
- });
221
-
222
- describe("Tools.wrap strict type checks", () => {
223
- function wrapTool(params: TSchema) {
224
- return Tools.wrap({
225
- name: "t",
226
- label: "t",
227
- description: "test",
228
- parameters: params,
229
- async execute() {
230
- return { content: [{ type: "text", text: "" }], details: {} };
231
- },
232
- });
233
- }
234
-
235
- test('rejects null for string field instead of coercing to "null"', () => {
236
- const wrapped = wrapTool(Type.Object({ path: Type.String() }));
237
- expect(() => wrapped.prepareArguments!({ path: null })).toThrow(
238
- 'Validation failed for tool "t":\n - path: must not be null (expected string)'
239
- );
240
- });
241
-
242
- test("rejects null for integer field instead of coercing to 0", () => {
243
- const wrapped = wrapTool(Type.Object({ n: Type.Integer() }));
244
- expect(() => wrapped.prepareArguments!({ n: null })).toThrow(
245
- 'Validation failed for tool "t":\n - n: must not be null (expected integer)'
246
- );
247
- });
248
-
249
- test("rejects null for boolean field instead of coercing to false", () => {
250
- const wrapped = wrapTool(Type.Object({ b: Type.Boolean() }));
251
- expect(() => wrapped.prepareArguments!({ b: null })).toThrow(
252
- 'Validation failed for tool "t":\n - b: must not be null (expected boolean)'
253
- );
254
- });
255
-
256
- test("rejects float string for integer field instead of truncating", () => {
257
- const wrapped = wrapTool(Type.Object({ n: Type.Integer() }));
258
- expect(() => wrapped.prepareArguments!({ n: "42.5" })).toThrow(
259
- 'Validation failed for tool "t":\n - n: must be an integer (received "42.5" — fractional part would be truncated)'
260
- );
261
- });
262
-
263
- test("integer string with no fractional part still coerces", () => {
264
- const wrapped = wrapTool(Type.Object({ n: Type.Integer() }));
265
- expect(wrapped.prepareArguments!({ n: "42" })).toEqual({ n: 42 });
266
- });
267
-
268
- test("integer string like '42.0' is allowed (no precision loss)", () => {
269
- const wrapped = wrapTool(Type.Object({ n: Type.Integer() }));
270
- expect(wrapped.prepareArguments!({ n: "42.0" })).toEqual({ n: 42 });
271
- });
272
-
273
- test("string-to-bool coercion still works (defensible LLM quirk)", () => {
274
- const wrapped = wrapTool(Type.Object({ b: Type.Boolean() }));
275
- expect(wrapped.prepareArguments!({ b: "true" })).toEqual({ b: true });
276
- });
277
-
278
- test("null in a nested field is also rejected", () => {
279
- const wrapped = wrapTool(
280
- Type.Object({
281
- edits: Type.Array(Type.Object({ value: Type.String() })),
282
- })
283
- );
284
- expect(() =>
285
- wrapped.prepareArguments!({ edits: [{ value: null }] })
286
- ).toThrow(
287
- 'Validation failed for tool "t":\n - edits.0.value: must not be null (expected string)'
288
- );
289
- });
290
-
291
- test("null is not rejected when schema explicitly accepts it", () => {
292
- const wrapped = wrapTool(
293
- Type.Object({ x: Type.Union([Type.String(), Type.Null()]) })
294
- );
295
- expect(() => wrapped.prepareArguments!({ x: null })).not.toThrow();
296
- });
297
- });
298
-
299
- describe("Tools.wrap unknown property detection", () => {
300
- test("rejects unknown top-level key", () => {
301
- const params = Type.Object({ command: Type.String() });
302
- const wrapped = Tools.wrap({
303
- name: "bash",
304
- label: "bash",
305
- description: "test",
306
- parameters: params,
307
- async execute() {
308
- return { content: [{ type: "text", text: "" }], details: {} };
309
- },
310
- });
311
- expect(() =>
312
- wrapped.prepareArguments!({ command: "ls", fakeParam: "x" })
313
- ).toThrow(
314
- 'Validation failed for tool "bash":\n - unknown property: fakeParam'
315
- );
316
- });
317
-
318
- test("suggests close matches by edit distance", () => {
319
- const params = Type.Object({ headLimit: Type.Integer() });
320
- const wrapped = Tools.wrap({
321
- name: "grep",
322
- label: "grep",
323
- description: "test",
324
- parameters: params,
325
- async execute() {
326
- return { content: [{ type: "text", text: "" }], details: {} };
327
- },
328
- });
329
- expect(() =>
330
- wrapped.prepareArguments!({ headLimit: 1, headlimit: 1 })
331
- ).toThrow(
332
- 'Validation failed for tool "grep":\n - unknown property: headlimit (did you mean "headLimit"?)'
333
- );
334
- });
335
- });
336
-
337
- describe("Tools.wrap", () => {
338
- test("prepareArguments rewrites the thrown message", () => {
339
- const params = Type.Object({ path: Type.String() });
340
- const wrapped = Tools.wrap({
341
- name: "read",
342
- label: "read",
343
- description: "test",
344
- parameters: params,
345
- async execute() {
346
- return { content: [{ type: "text", text: "" }], details: {} };
347
- },
348
- });
349
- expect(() => wrapped.prepareArguments!({})).toThrow(
350
- 'Validation failed for tool "read":\n - missing required property: path'
351
- );
352
- });
353
-
354
- test("prepareArguments returns coerced args on success", () => {
355
- const params = Type.Object({ count: Type.Integer() });
356
- const wrapped = Tools.wrap({
357
- name: "t",
358
- label: "t",
359
- description: "test",
360
- parameters: params,
361
- async execute() {
362
- return { content: [{ type: "text", text: "" }], details: {} };
363
- },
364
- });
365
- expect(wrapped.prepareArguments!({ count: "42" })).toEqual({ count: 42 });
366
- });
367
- });
368
-
369
- describe("Tools.register", () => {
370
- test("forwards the wrapped def to pi.registerTool", () => {
371
- const params = Type.Object({ path: Type.String() });
372
- let captured: ReturnType<typeof Tools.wrap> | undefined;
373
- const fakePi = {
374
- registerTool(def: ReturnType<typeof Tools.wrap>) {
375
- captured = def;
376
- },
377
- };
378
- Tools.register(fakePi as never, {
379
- name: "read",
380
- label: "read",
381
- description: "test",
382
- parameters: params,
383
- async execute() {
384
- return { content: [{ type: "text", text: "" }], details: {} };
385
- },
386
- });
387
- expect(captured?.prepareArguments).toBeDefined();
388
- expect(() => captured!.prepareArguments!({})).toThrow(
389
- 'Validation failed for tool "read":\n - missing required property: path'
390
- );
391
- });
392
- });