pi-crew 0.6.4 → 0.7.2

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 (41) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +68 -0
  3. package/package.json +2 -1
  4. package/src/errors.ts +20 -2
  5. package/src/extension/knowledge-injection.ts +71 -0
  6. package/src/extension/pi-api.ts +47 -0
  7. package/src/extension/register.ts +19 -6
  8. package/src/extension/registration/commands.ts +40 -1
  9. package/src/extension/registration/compaction-guard.ts +154 -14
  10. package/src/extension/team-tool/handle-settings.ts +57 -0
  11. package/src/extension/team-tool/inspect.ts +4 -1
  12. package/src/extension/team-tool/plan.ts +8 -1
  13. package/src/runtime/intercom-bridge.ts +5 -1
  14. package/src/runtime/replace.ts +555 -0
  15. package/src/runtime/resilient-edit.ts +166 -0
  16. package/src/runtime/single-agent-compose.ts +87 -0
  17. package/src/runtime/task-runner/prompt-builder.ts +6 -0
  18. package/src/runtime/team-runner.ts +24 -7
  19. package/src/schema/team-tool-schema.ts +6 -0
  20. package/src/state/usage.ts +73 -0
  21. package/src/ui/card-colors.ts +126 -0
  22. package/src/ui/deploy-bundled-themes.ts +71 -0
  23. package/src/ui/render-diff.ts +37 -3
  24. package/src/ui/settings-overlay.ts +70 -7
  25. package/src/ui/syntax-highlight.ts +42 -23
  26. package/src/ui/theme-discovery.ts +188 -0
  27. package/src/ui/tool-renderers/index.ts +27 -14
  28. package/themes/crew-catppuccin-latte.json +96 -0
  29. package/themes/crew-catppuccin-mocha.json +93 -0
  30. package/themes/crew-dark.json +90 -0
  31. package/themes/crew-dracula.json +83 -0
  32. package/themes/crew-gruvbox-dark.json +83 -0
  33. package/themes/crew-gruvbox-light.json +90 -0
  34. package/themes/crew-nord.json +85 -0
  35. package/themes/crew-one-dark.json +89 -0
  36. package/themes/crew-solarized-dark.json +90 -0
  37. package/themes/crew-solarized-light.json +92 -0
  38. package/themes/crew-tokyo-night.json +85 -0
  39. package/src/extension/registration/brief-tool-overrides.ts +0 -400
  40. package/src/runtime/budget-tracker.ts +0 -354
  41. package/src/state/memory-store.ts +0 -244
@@ -1,400 +0,0 @@
1
- /**
2
- * Brief tool overrides — re-registers built-in Pi tools with custom rendering.
3
- *
4
- * Inspired by oh-my-pi's pi-brief extension. Wraps each built-in tool
5
- * (read, bash, edit, write, find, grep, ls) keeping the original execute
6
- * but replacing renderCall/renderResult with themed, brief-aware versions.
7
- *
8
- * Brief mode shows CONTEXTUAL one-liners that preserve WHAT was done:
9
- * read ~/file.ts:1-50 → 142 lines · 2.3s
10
- * bash $ npm test → done · 12.5s
11
- * edit ~/file.ts → +3 -1 · 0.8s
12
- * write ~/file.ts (42 lines) → ✓ · 0.2s
13
- * find *.ts in ~/src → 5 files · 0.5s
14
- * grep /pattern/ in ~/src → 3 matches · 0.3s
15
- * ls ~/src → 8 entries · 0.1s
16
- *
17
- * Timing: ctx.state.startedAt is set in renderCall, read in renderResult.
18
- * NOTE: Pi's executionStarted is a boolean flag (NOT a timestamp).
19
- */
20
-
21
- import { homedir } from "node:os";
22
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
23
- import {
24
- createBashTool,
25
- createEditTool,
26
- createFindTool,
27
- createGrepTool,
28
- createLsTool,
29
- createReadTool,
30
- createWriteTool,
31
- } from "@earendil-works/pi-coding-agent";
32
- import { Text } from "@earendil-works/pi-tui";
33
- import { isBrief } from "../../ui/tool-renderers/brief-mode.ts";
34
-
35
- // ── Path shortening ────────────────────────────────────────────────────
36
-
37
- const HOME = homedir();
38
- function shortenPath(p: string): string {
39
- if (!p) return "";
40
- if (p.startsWith(HOME)) return `~${p.slice(HOME.length)}`;
41
- return p;
42
- }
43
-
44
- // ── Types ──────────────────────────────────────────────────────────────
45
-
46
- interface TextResult {
47
- content: Array<{ type: string; text?: string }>;
48
- }
49
-
50
- interface Theme {
51
- fg: (slot: string, text: string) => string;
52
- bold: (text: string) => string;
53
- }
54
-
55
- interface RenderCtx {
56
- args?: Record<string, unknown>;
57
- state?: Record<string, unknown>;
58
- }
59
-
60
- function fullText(result: TextResult): string | undefined {
61
- const c = result.content.find((x): x is { type: "text"; text: string } => x.type === "text");
62
- return c?.text;
63
- }
64
-
65
- function fullRender(result: TextResult, theme: Theme): Text {
66
- const text = fullText(result);
67
- if (!text) return new Text("", 0, 0);
68
- const lines = text
69
- .trim()
70
- .split("\n")
71
- .map((line) => theme.fg("toolOutput", line))
72
- .join("\n");
73
- return new Text(`\n${lines}`, 0, 0);
74
- }
75
-
76
- /** Record start time in ctx.state during renderCall (called once per tool invocation). */
77
- function noteStarted(ctx: RenderCtx | undefined): void {
78
- if (ctx?.state && !ctx.state.briefStartedAt) {
79
- ctx.state.briefStartedAt = Date.now();
80
- }
81
- }
82
-
83
- /** Compute elapsed from ctx.state.briefStartedAt. Returns " · 1.2s" or "". */
84
- function elapsed(ctx: RenderCtx | undefined): string {
85
- const startedAt = ctx?.state?.briefStartedAt as number | undefined;
86
- if (!startedAt || typeof startedAt !== "number") return "";
87
- const ms = Date.now() - startedAt;
88
- if (ms < 1000) return "";
89
- if (ms < 60_000) return ` · ${(ms / 1000).toFixed(1)}s`;
90
- const m = Math.floor(ms / 60_000), s = Math.floor((ms % 60_000) / 1000);
91
- return ` · ${m}m${s}s`;
92
- }
93
-
94
- /** Truncate to maxLen with ellipsis (visual). */
95
- function trunc(text: string, maxLen: number): string {
96
- if (text.length <= maxLen) return text;
97
- return text.slice(0, maxLen - 1) + "…";
98
- }
99
-
100
- /** Flatten multiline command to single line, truncated. */
101
- function flatCmd(cmd: string, maxLen: number): string {
102
- return trunc(cmd.replace(/\s+/g, " ").trim(), maxLen);
103
- }
104
-
105
- // ── Tool registration ──────────────────────────────────────────────────
106
-
107
- export function registerBriefToolOverrides(pi: ExtensionAPI, cwd: string): void {
108
- const tools = {
109
- read: createReadTool(cwd),
110
- bash: createBashTool(cwd),
111
- edit: createEditTool(cwd),
112
- write: createWriteTool(cwd),
113
- find: createFindTool(cwd),
114
- grep: createGrepTool(cwd),
115
- ls: createLsTool(cwd),
116
- };
117
-
118
- // ─── Read ───
119
- pi.registerTool({
120
- name: "read",
121
- label: "read",
122
- description: tools.read.description,
123
- parameters: tools.read.parameters,
124
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
125
- async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
126
- return tools.read.execute(toolCallId, params, signal, onUpdate);
127
- },
128
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
- renderCall(args: any, theme: any, ctx: any): any {
130
- noteStarted(ctx);
131
- const p = shortenPath(args.path || "");
132
- const pathDisplay = p ? theme.fg("accent", p) : theme.fg("toolOutput", "...");
133
- let text = `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`;
134
- if (args.offset !== undefined || args.limit !== undefined) {
135
- const startLine = args.offset ?? 1;
136
- const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
137
- text += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
138
- }
139
- return new Text(text, 0, 0);
140
- },
141
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
142
- renderResult(result: any, options: any, theme: any, ctx: any): any {
143
- if (!isBrief() || options.expanded) return fullRender(result, theme);
144
- const args = ctx?.args ?? {};
145
- const p = shortenPath(args.path || "?");
146
- let range = "";
147
- if (args.offset || args.limit) {
148
- const s = args.offset ?? 1;
149
- const e = args.limit ? s + args.limit - 1 : "";
150
- range = `:${s}${e ? `-${e}` : ""}`;
151
- }
152
- const text = fullText(result);
153
- const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
154
- const time = elapsed(ctx);
155
- const label = count === 0 ? "empty" : `${count} lines`;
156
- return new Text(
157
- `${theme.fg("toolTitle", "read")} ${theme.fg("accent", p)}${theme.fg("warning", range)} ${theme.fg("dim", "→")} ${theme.fg("muted", label)}${theme.fg("dim", time)}`,
158
- 0, 0,
159
- );
160
- },
161
- });
162
-
163
- // ─── Bash ───
164
- pi.registerTool({
165
- name: "bash",
166
- label: "bash",
167
- description: tools.bash.description,
168
- parameters: tools.bash.parameters,
169
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
- async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
171
- return tools.bash.execute(toolCallId, params, signal, onUpdate);
172
- },
173
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
- renderCall(args: any, theme: any, ctx: any): any {
175
- noteStarted(ctx);
176
- const command = args.command || "...";
177
- const timeout = args.timeout as number | undefined;
178
- const timeoutSuffix = timeout ? theme.fg("muted", ` (${timeout}s)`) : "";
179
- return new Text(theme.fg("toolTitle", theme.bold(`$ ${command}`)) + timeoutSuffix, 0, 0);
180
- },
181
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
- renderResult(result: any, options: any, theme: any, ctx: any): any {
183
- if (!isBrief() || options.expanded) return fullRender(result, theme);
184
- const args = ctx?.args ?? {};
185
- const cmd = flatCmd(String(args.command || "?"), 50);
186
- const text = fullText(result);
187
- const time = elapsed(ctx);
188
- let label: string;
189
- let color: string;
190
- if (!text || !text.trim()) {
191
- label = "done";
192
- color = "muted";
193
- } else {
194
- const lines = text.trim().split("\n");
195
- if (lines.length === 1) {
196
- label = trunc(lines[0]!, 40);
197
- color = "muted";
198
- } else {
199
- label = `${lines.length} lines`;
200
- color = "muted";
201
- }
202
- }
203
- return new Text(
204
- `${theme.fg("toolTitle", "$")} ${theme.fg("accent", cmd)} ${theme.fg("dim", "→")} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
205
- 0, 0,
206
- );
207
- },
208
- });
209
-
210
- // ─── Edit ───
211
- pi.registerTool({
212
- name: "edit",
213
- label: "edit",
214
- description: tools.edit.description,
215
- parameters: tools.edit.parameters,
216
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
- async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
218
- return tools.edit.execute(toolCallId, params, signal, onUpdate);
219
- },
220
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
221
- renderCall(args: any, theme: any, ctx: any): any {
222
- noteStarted(ctx);
223
- const p = shortenPath(args.path || "");
224
- const pathDisplay = p ? theme.fg("accent", p) : theme.fg("toolOutput", "...");
225
- return new Text(`${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`, 0, 0);
226
- },
227
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
228
- renderResult(result: any, options: any, theme: any, ctx: any): any {
229
- if (!isBrief() || options.expanded) return fullRender(result, theme);
230
- const args = ctx?.args ?? {};
231
- const p = shortenPath(args.path || "?");
232
- const text = fullText(result);
233
- const time = elapsed(ctx);
234
- if (text && (text.includes("Error") || text.includes("error"))) {
235
- return new Text(
236
- `${theme.fg("toolTitle", "edit")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("error", "failed")}${theme.fg("dim", time)}`,
237
- 0, 0,
238
- );
239
- }
240
- const added = text ? (text.match(/^\+ /gm) ?? []).length : 0;
241
- const removed = text ? (text.match(/^- /gm) ?? []).length : 0;
242
- if (added === 0 && removed === 0) {
243
- return new Text(
244
- `${theme.fg("toolTitle", "edit")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("success", "edited")}${theme.fg("dim", time)}`,
245
- 0, 0,
246
- );
247
- }
248
- return new Text(
249
- `${theme.fg("toolTitle", "edit")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("toolDiffAdded", `+${added} `)}${theme.fg("toolDiffRemoved", `-${removed}`)}${theme.fg("dim", time)}`,
250
- 0, 0,
251
- );
252
- },
253
- });
254
-
255
- // ─── Write ───
256
- pi.registerTool({
257
- name: "write",
258
- label: "write",
259
- description: tools.write.description,
260
- parameters: tools.write.parameters,
261
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
262
- async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
263
- return tools.write.execute(toolCallId, params, signal, onUpdate);
264
- },
265
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
266
- renderCall(args: any, theme: any, ctx: any): any {
267
- noteStarted(ctx);
268
- const p = shortenPath(args.path || "");
269
- const pathDisplay = p ? theme.fg("accent", p) : theme.fg("toolOutput", "...");
270
- const lineCount = args.content ? String(args.content).split("\n").length : 0;
271
- const lineInfo = lineCount > 0 ? theme.fg("muted", ` (${lineCount} lines)`) : "";
272
- return new Text(`${theme.fg("toolTitle", theme.bold("write"))} ${pathDisplay}${lineInfo}`, 0, 0);
273
- },
274
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
275
- renderResult(result: any, options: any, theme: any, ctx: any): any {
276
- if (!isBrief() || options.expanded) return fullRender(result, theme);
277
- const args = ctx?.args ?? {};
278
- const p = shortenPath(args.path || "?");
279
- const text = fullText(result);
280
- const time = elapsed(ctx);
281
- if (text) {
282
- return new Text(
283
- `${theme.fg("toolTitle", "write")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("error", trunc(text.trim().split("\n")[0] ?? "", 30))}${theme.fg("dim", time)}`,
284
- 0, 0,
285
- );
286
- }
287
- return new Text(
288
- `${theme.fg("toolTitle", "write")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg("success", "✓")}${theme.fg("dim", time)}`,
289
- 0, 0,
290
- );
291
- },
292
- });
293
-
294
- // ─── Find ───
295
- pi.registerTool({
296
- name: "find",
297
- label: "find",
298
- description: tools.find.description,
299
- parameters: tools.find.parameters,
300
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
301
- async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
302
- return tools.find.execute(toolCallId, params, signal, onUpdate);
303
- },
304
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
305
- renderCall(args: any, theme: any, ctx: any): any {
306
- noteStarted(ctx);
307
- const pattern = args.pattern || "";
308
- const p = shortenPath(args.path || ".");
309
- let text = `${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}`;
310
- text += theme.fg("toolOutput", ` in ${p}`);
311
- return new Text(text, 0, 0);
312
- },
313
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
314
- renderResult(result: any, options: any, theme: any, ctx: any): any {
315
- if (!isBrief() || options.expanded) return fullRender(result, theme);
316
- const args = ctx?.args ?? {};
317
- const pattern = trunc(String(args.pattern || "*"), 20);
318
- const p = shortenPath(args.path || ".");
319
- const text = fullText(result);
320
- const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
321
- const time = elapsed(ctx);
322
- const label = count === 0 ? "none" : `${count} files`;
323
- const color = count === 0 ? "dim" : "muted";
324
- return new Text(
325
- `${theme.fg("toolTitle", "find")} ${theme.fg("accent", pattern)} ${theme.fg("dim", `in ${p} →`)} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
326
- 0, 0,
327
- );
328
- },
329
- });
330
-
331
- // ─── Grep ───
332
- pi.registerTool({
333
- name: "grep",
334
- label: "grep",
335
- description: tools.grep.description,
336
- parameters: tools.grep.parameters,
337
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
338
- async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
339
- return tools.grep.execute(toolCallId, params, signal, onUpdate);
340
- },
341
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
342
- renderCall(args: any, theme: any, ctx: any): any {
343
- noteStarted(ctx);
344
- const pattern = args.pattern || "";
345
- const p = shortenPath(args.path || ".");
346
- let text = `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${pattern}/`)}`;
347
- text += theme.fg("toolOutput", ` in ${p}`);
348
- return new Text(text, 0, 0);
349
- },
350
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
351
- renderResult(result: any, options: any, theme: any, ctx: any): any {
352
- if (!isBrief() || options.expanded) return fullRender(result, theme);
353
- const args = ctx?.args ?? {};
354
- const pattern = trunc(String(args.pattern || "?"), 20);
355
- const p = shortenPath(args.path || ".");
356
- const text = fullText(result);
357
- const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
358
- const time = elapsed(ctx);
359
- const label = count === 0 ? "none" : `${count} matches`;
360
- const color = count === 0 ? "dim" : "muted";
361
- return new Text(
362
- `${theme.fg("toolTitle", "grep")} ${theme.fg("accent", `/${pattern}/`)} ${theme.fg("dim", `in ${p} →`)} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
363
- 0, 0,
364
- );
365
- },
366
- });
367
-
368
- // ─── Ls ───
369
- pi.registerTool({
370
- name: "ls",
371
- label: "ls",
372
- description: tools.ls.description,
373
- parameters: tools.ls.parameters,
374
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
375
- async execute(toolCallId: string, params: any, signal: any, onUpdate: any, ctx: any) {
376
- return tools.ls.execute(toolCallId, params, signal, onUpdate);
377
- },
378
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
379
- renderCall(args: any, theme: any, ctx: any): any {
380
- noteStarted(ctx);
381
- const p = shortenPath(args.path || ".");
382
- return new Text(`${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", p)}`, 0, 0);
383
- },
384
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
385
- renderResult(result: any, options: any, theme: any, ctx: any): any {
386
- if (!isBrief() || options.expanded) return fullRender(result, theme);
387
- const args = ctx?.args ?? {};
388
- const p = shortenPath(args.path || ".");
389
- const text = fullText(result);
390
- const count = text ? text.trim().split("\n").filter(Boolean).length : 0;
391
- const time = elapsed(ctx);
392
- const label = count === 0 ? "empty" : `${count} entries`;
393
- const color = count === 0 ? "dim" : "muted";
394
- return new Text(
395
- `${theme.fg("toolTitle", "ls")} ${theme.fg("accent", p)} ${theme.fg("dim", "→")} ${theme.fg(color, label)}${theme.fg("dim", time)}`,
396
- 0, 0,
397
- );
398
- },
399
- });
400
- }