pi-agent-toolkit 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 (53) hide show
  1. package/dist/dotfiles/AGENTS.md +197 -0
  2. package/dist/dotfiles/APPEND_SYSTEM.md +78 -0
  3. package/dist/dotfiles/agent-modes.json +12 -0
  4. package/dist/dotfiles/agent-skills/exa-search/.env.example +4 -0
  5. package/dist/dotfiles/agent-skills/exa-search/SKILL.md +234 -0
  6. package/dist/dotfiles/agent-skills/exa-search/scripts/exa-api.cjs +197 -0
  7. package/dist/dotfiles/auth.json.template +5 -0
  8. package/dist/dotfiles/damage-control-rules.yaml +318 -0
  9. package/dist/dotfiles/extensions/btw.ts +1031 -0
  10. package/dist/dotfiles/extensions/commit-approval.ts +590 -0
  11. package/dist/dotfiles/extensions/context.ts +578 -0
  12. package/dist/dotfiles/extensions/control.ts +1748 -0
  13. package/dist/dotfiles/extensions/damage-control/index.ts +543 -0
  14. package/dist/dotfiles/extensions/damage-control/node_modules/.package-lock.json +22 -0
  15. package/dist/dotfiles/extensions/damage-control/package-lock.json +28 -0
  16. package/dist/dotfiles/extensions/damage-control/package.json +7 -0
  17. package/dist/dotfiles/extensions/dirty-repo-guard.ts +56 -0
  18. package/dist/dotfiles/extensions/exa-enforce.ts +51 -0
  19. package/dist/dotfiles/extensions/exa-search-tool.ts +384 -0
  20. package/dist/dotfiles/extensions/execute-command/index.ts +82 -0
  21. package/dist/dotfiles/extensions/files.ts +1112 -0
  22. package/dist/dotfiles/extensions/loop.ts +446 -0
  23. package/dist/dotfiles/extensions/pr-approval.ts +730 -0
  24. package/dist/dotfiles/extensions/qna-interactive.ts +532 -0
  25. package/dist/dotfiles/extensions/question-mode.ts +242 -0
  26. package/dist/dotfiles/extensions/require-session-name-on-exit.ts +141 -0
  27. package/dist/dotfiles/extensions/review.ts +2091 -0
  28. package/dist/dotfiles/extensions/session-breakdown.ts +1629 -0
  29. package/dist/dotfiles/extensions/term-notify.ts +150 -0
  30. package/dist/dotfiles/extensions/tilldone.ts +527 -0
  31. package/dist/dotfiles/extensions/todos.ts +2082 -0
  32. package/dist/dotfiles/extensions/tools.ts +146 -0
  33. package/dist/dotfiles/extensions/uv.ts +123 -0
  34. package/dist/dotfiles/global-skills/brainstorm/SKILL.md +10 -0
  35. package/dist/dotfiles/global-skills/cli-detector/SKILL.md +192 -0
  36. package/dist/dotfiles/global-skills/gh-issue-creator/SKILL.md +173 -0
  37. package/dist/dotfiles/global-skills/google-chat-cards-v2/SKILL.md +237 -0
  38. package/dist/dotfiles/global-skills/google-chat-cards-v2/references/bridge_tap_implementation.md +466 -0
  39. package/dist/dotfiles/global-skills/technical-docs/SKILL.md +204 -0
  40. package/dist/dotfiles/global-skills/technical-docs/references/diagrams.md +168 -0
  41. package/dist/dotfiles/global-skills/technical-docs/references/examples.md +449 -0
  42. package/dist/dotfiles/global-skills/technical-docs/scripts/validate_docs.py +352 -0
  43. package/dist/dotfiles/global-skills/whats-new/SKILL.md +159 -0
  44. package/dist/dotfiles/intercepted-commands/pip +7 -0
  45. package/dist/dotfiles/intercepted-commands/pip3 +7 -0
  46. package/dist/dotfiles/intercepted-commands/poetry +10 -0
  47. package/dist/dotfiles/intercepted-commands/python +104 -0
  48. package/dist/dotfiles/intercepted-commands/python3 +104 -0
  49. package/dist/dotfiles/mcp.json.template +32 -0
  50. package/dist/dotfiles/models.json +27 -0
  51. package/dist/dotfiles/settings.json +25 -0
  52. package/dist/index.js +1344 -0
  53. package/package.json +34 -0
@@ -0,0 +1,590 @@
1
+ import { basename } from "node:path";
2
+
3
+ import type {
4
+ ExtensionAPI,
5
+ ExtensionContext,
6
+ UserBashEventResult,
7
+ ToolCallEventResult,
8
+ Theme,
9
+ } from "@mariozechner/pi-coding-agent";
10
+ import { isToolCallEventType, keyHint } from "@mariozechner/pi-coding-agent";
11
+ import type { TUI, KeybindingsManager } from "@mariozechner/pi-tui";
12
+ import { matchesKey } from "@mariozechner/pi-tui";
13
+
14
+ const APPROVE_OPTION = "Approve commit";
15
+ const DENY_OPTION = "Deny commit";
16
+
17
+ const REASON_INTERACTIVE_APPROVAL_REQUIRED =
18
+ "git commit blocked: interactive approval is required.";
19
+ const REASON_APPROVAL_DENIED = "git commit blocked: approval denied.";
20
+
21
+ const PREVIEW_REUSE_PREVIOUS_MESSAGE =
22
+ "(reusing previous commit message via --no-edit)";
23
+ const PREVIEW_EDITOR_FALLBACK =
24
+ "(no -m/--message provided; git may open your editor)";
25
+
26
+ const GIT_GLOBAL_OPTIONS_WITH_VALUE = new Set<string>([
27
+ "-c",
28
+ "-C",
29
+ "--exec-path",
30
+ "--git-dir",
31
+ "--work-tree",
32
+ "--namespace",
33
+ "--super-prefix",
34
+ "--config-env",
35
+ ]);
36
+
37
+ interface CommitInvocation {
38
+ args: string[];
39
+ }
40
+
41
+ interface CommitMetadata {
42
+ messages: string[];
43
+ hasNoEdit: boolean;
44
+ }
45
+
46
+ function shellSplit(input: string): string[] {
47
+ const command = input.trim();
48
+ const tokens: string[] = [];
49
+ let current = "";
50
+ let quote: '"' | "'" | null = null;
51
+
52
+ const flushCurrent = () => {
53
+ if (current) {
54
+ tokens.push(current);
55
+ current = "";
56
+ }
57
+ };
58
+
59
+ for (let i = 0; i < command.length; i += 1) {
60
+ const ch = command[i] ?? "";
61
+
62
+ if (quote === "'") {
63
+ if (ch === "'") {
64
+ quote = null;
65
+ } else {
66
+ current += ch;
67
+ }
68
+ continue;
69
+ }
70
+
71
+ if (quote === '"') {
72
+ if (ch === '"') {
73
+ quote = null;
74
+ continue;
75
+ }
76
+
77
+ if (ch === "\\") {
78
+ const next = command[i + 1] ?? "";
79
+ if (next === '"' || next === "\\" || next === "$" || next === "`") {
80
+ current += next;
81
+ i += 1;
82
+ } else if (next === "\n") {
83
+ i += 1;
84
+ } else {
85
+ current += "\\";
86
+ }
87
+ continue;
88
+ }
89
+
90
+ current += ch;
91
+ continue;
92
+ }
93
+
94
+ if (ch === '"' || ch === "'") {
95
+ quote = ch;
96
+ continue;
97
+ }
98
+
99
+ if (ch === "\\") {
100
+ const next = command[i + 1];
101
+ if (next !== undefined) {
102
+ current += next;
103
+ i += 1;
104
+ } else {
105
+ current += "\\";
106
+ }
107
+ continue;
108
+ }
109
+
110
+ if (/\s/.test(ch)) {
111
+ flushCurrent();
112
+ continue;
113
+ }
114
+
115
+ current += ch;
116
+ }
117
+
118
+ flushCurrent();
119
+ return tokens;
120
+ }
121
+
122
+ function splitByShellOperators(command: string): string[] {
123
+ // Collapse backslash-newline continuations before splitting,
124
+ // exactly as bash does for line continuations.
125
+ const normalized = command.replace(/\\\n\s*/g, " ");
126
+
127
+ // Quote-aware split: only split on &&, ||, ;, \n when outside quotes.
128
+ const segments: string[] = [];
129
+ let current = "";
130
+ let quote: '"' | "'" | null = null;
131
+
132
+ for (let i = 0; i < normalized.length; i += 1) {
133
+ const ch = normalized[i] ?? "";
134
+
135
+ if (quote) {
136
+ current += ch;
137
+ if (ch === quote) quote = null;
138
+ continue;
139
+ }
140
+
141
+ if (ch === '"' || ch === "'") {
142
+ current += ch;
143
+ quote = ch;
144
+ continue;
145
+ }
146
+
147
+ if (ch === "\n" || ch === ";") {
148
+ if (current.trim()) segments.push(current.trim());
149
+ current = "";
150
+ continue;
151
+ }
152
+
153
+ if (ch === "&" && normalized[i + 1] === "&") {
154
+ if (current.trim()) segments.push(current.trim());
155
+ current = "";
156
+ i += 1;
157
+ continue;
158
+ }
159
+
160
+ if (ch === "|" && normalized[i + 1] === "|") {
161
+ if (current.trim()) segments.push(current.trim());
162
+ current = "";
163
+ i += 1;
164
+ continue;
165
+ }
166
+
167
+ current += ch;
168
+ }
169
+
170
+ if (current.trim()) segments.push(current.trim());
171
+ return segments;
172
+ }
173
+
174
+ function isGitExecutable(token: string): boolean {
175
+ return basename(token).toLowerCase() === "git";
176
+ }
177
+
178
+ function isEnvAssignment(token: string): boolean {
179
+ return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(token);
180
+ }
181
+
182
+ function findGitSubcommandIndex(tokens: string[]): number {
183
+ let gitTokenIndex = 0;
184
+ while (
185
+ gitTokenIndex < tokens.length &&
186
+ isEnvAssignment(tokens[gitTokenIndex] ?? "")
187
+ ) {
188
+ gitTokenIndex += 1;
189
+ }
190
+
191
+ if (
192
+ tokens.length < gitTokenIndex + 2 ||
193
+ !isGitExecutable(tokens[gitTokenIndex] ?? "")
194
+ ) {
195
+ return -1;
196
+ }
197
+
198
+ let i = gitTokenIndex + 1;
199
+ while (i < tokens.length) {
200
+ const token = tokens[i] ?? "";
201
+
202
+ if (!token.startsWith("-")) {
203
+ return i;
204
+ }
205
+
206
+ if (token === "--") {
207
+ return -1;
208
+ }
209
+
210
+ if (token.startsWith("--")) {
211
+ if (token.includes("=")) {
212
+ i += 1;
213
+ continue;
214
+ }
215
+
216
+ if (GIT_GLOBAL_OPTIONS_WITH_VALUE.has(token)) {
217
+ i += 2;
218
+ continue;
219
+ }
220
+
221
+ i += 1;
222
+ continue;
223
+ }
224
+
225
+ if (token.startsWith("-c") && token.length > 2) {
226
+ i += 1;
227
+ continue;
228
+ }
229
+
230
+ if (token.startsWith("-C") && token.length > 2) {
231
+ i += 1;
232
+ continue;
233
+ }
234
+
235
+ if (GIT_GLOBAL_OPTIONS_WITH_VALUE.has(token)) {
236
+ i += 2;
237
+ continue;
238
+ }
239
+
240
+ i += 1;
241
+ }
242
+
243
+ return -1;
244
+ }
245
+
246
+ function parseCommitInvocation(command: string): CommitInvocation | null {
247
+ for (const segment of splitByShellOperators(command)) {
248
+ const tokens = shellSplit(segment);
249
+ const subcommandIndex = findGitSubcommandIndex(tokens);
250
+
251
+ if (subcommandIndex >= 0 && tokens[subcommandIndex] === "commit") {
252
+ return { args: tokens.slice(subcommandIndex + 1) };
253
+ }
254
+ }
255
+
256
+ return null;
257
+ }
258
+
259
+ function parseCommitMetadata(args: string[]): CommitMetadata {
260
+ const messages: string[] = [];
261
+ let hasNoEdit = false;
262
+
263
+ const addMessage = (value: string | undefined): boolean => {
264
+ if (!value) {
265
+ return false;
266
+ }
267
+
268
+ messages.push(value);
269
+ return true;
270
+ };
271
+
272
+ for (let i = 0; i < args.length; i += 1) {
273
+ const token = args[i] ?? "";
274
+
275
+ if (token === "--") {
276
+ break;
277
+ }
278
+
279
+ if (token === "--no-edit") {
280
+ hasNoEdit = true;
281
+ continue;
282
+ }
283
+
284
+ if (token === "-m" || token === "--message") {
285
+ if (addMessage(args[i + 1])) {
286
+ i += 1;
287
+ }
288
+ continue;
289
+ }
290
+
291
+ if (token.startsWith("--message=")) {
292
+ addMessage(token.slice("--message=".length));
293
+ continue;
294
+ }
295
+
296
+ if (token.startsWith("-m") && token.length > 2 && !token.startsWith("--")) {
297
+ addMessage(token.slice(2));
298
+ }
299
+ }
300
+
301
+ return { messages, hasNoEdit };
302
+ }
303
+
304
+ function parseCommitMetadataFromCommand(
305
+ command: string,
306
+ ): CommitMetadata | null {
307
+ const invocation = parseCommitInvocation(command);
308
+ if (!invocation) {
309
+ return null;
310
+ }
311
+
312
+ return parseCommitMetadata(invocation.args);
313
+ }
314
+
315
+ function getCommitMessagePreview(metadata: CommitMetadata): string {
316
+ if (metadata.messages.length > 0) {
317
+ return metadata.messages.join("\n\n");
318
+ }
319
+
320
+ if (metadata.hasNoEdit) {
321
+ return PREVIEW_REUSE_PREVIOUS_MESSAGE;
322
+ }
323
+
324
+ return PREVIEW_EDITOR_FALLBACK;
325
+ }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // Commit message validation
329
+ // ---------------------------------------------------------------------------
330
+
331
+ interface ValidationIssue {
332
+ level: "error" | "warning";
333
+ message: string;
334
+ }
335
+
336
+ interface ValidationResult {
337
+ issues: ValidationIssue[];
338
+ hasErrors: boolean;
339
+ }
340
+
341
+ const CONVENTIONAL_COMMIT_RE =
342
+ /^(feat|fix|refactor|docs|test|chore|style|perf|ci|build)(\(.+?\))?: .+/;
343
+
344
+ function messageHasBody(metadata: CommitMetadata): boolean {
345
+ if (metadata.messages.length >= 2) {
346
+ return metadata.messages.slice(1).some((m) => m.trim().length > 0);
347
+ }
348
+ if (metadata.messages.length === 1) {
349
+ const parts = (metadata.messages[0] ?? "").split(/\n\n/);
350
+ return parts.length > 1 && parts.slice(1).some((p) => p.trim().length > 0);
351
+ }
352
+ return false;
353
+ }
354
+
355
+ function validateCommitMessage(metadata: CommitMetadata): ValidationResult {
356
+ const issues: ValidationIssue[] = [];
357
+
358
+ if (metadata.messages.length === 0) {
359
+ return { issues, hasErrors: false };
360
+ }
361
+
362
+ const firstMessage = metadata.messages[0] ?? "";
363
+ const subject = firstMessage.split("\n")[0] ?? "";
364
+
365
+ if (!CONVENTIONAL_COMMIT_RE.test(subject)) {
366
+ issues.push({
367
+ level: "error",
368
+ message:
369
+ "Subject must use Conventional Commits: type(scope): subject",
370
+ });
371
+ }
372
+
373
+ if (subject.length > 72) {
374
+ issues.push({
375
+ level: "warning",
376
+ message: `Subject is ${subject.length} chars (keep under 72, ideally under 50)`,
377
+ });
378
+ }
379
+
380
+ if (!messageHasBody(metadata)) {
381
+ issues.push({
382
+ level: "error",
383
+ message:
384
+ "Missing commit body. Add a second -m explaining why this change was made.",
385
+ });
386
+ }
387
+
388
+ return {
389
+ issues,
390
+ hasErrors: issues.some((i) => i.level === "error"),
391
+ };
392
+ }
393
+
394
+ function formatValidationIssues(issues: ValidationIssue[]): string {
395
+ return issues
396
+ .map((i) => ` ${i.level === "error" ? "[x]" : "[!]"} ${i.message}`)
397
+ .join("\n");
398
+ }
399
+
400
+ // ---------------------------------------------------------------------------
401
+ // Approval prompt
402
+ // ---------------------------------------------------------------------------
403
+
404
+ function buildApprovalPrompt(
405
+ metadata: CommitMetadata,
406
+ issues?: ValidationIssue[],
407
+ ): string {
408
+ const messagePreview = getCommitMessagePreview(metadata);
409
+
410
+ const lines = [
411
+ "Commit message preview:",
412
+ "",
413
+ messagePreview,
414
+ ];
415
+
416
+ if (issues && issues.length > 0) {
417
+ lines.push("");
418
+ lines.push("Issues:");
419
+ lines.push(formatValidationIssues(issues));
420
+ }
421
+
422
+ lines.push("");
423
+ lines.push("Approve this commit?");
424
+ return lines.join("\n");
425
+ }
426
+
427
+ async function requestApproval(
428
+ ctx: ExtensionContext,
429
+ metadata: CommitMetadata,
430
+ issues?: ValidationIssue[],
431
+ ): Promise<boolean> {
432
+ const messagePreview = getCommitMessagePreview(metadata);
433
+ const issueLines = issues && issues.length > 0
434
+ ? formatValidationIssues(issues)
435
+ : undefined;
436
+
437
+ const options = [APPROVE_OPTION, DENY_OPTION];
438
+
439
+ const result = await ctx.ui.custom<boolean>(
440
+ (tui: TUI, theme: Theme, _kb: KeybindingsManager, done: (result: boolean) => void) => {
441
+ let selected = 0;
442
+
443
+ function render(width: number): string[] {
444
+ const lines: string[] = [];
445
+ const rule = theme.fg("dim", "-".repeat(Math.min(width, 60)));
446
+
447
+ lines.push(theme.bold("Commit message preview:"));
448
+ lines.push("");
449
+ for (const line of messagePreview.split("\n")) {
450
+ lines.push(" " + line);
451
+ }
452
+
453
+ if (issueLines) {
454
+ lines.push("");
455
+ lines.push(rule);
456
+ lines.push(theme.fg("warning", "Issues:"));
457
+ for (const line of issueLines.split("\n")) {
458
+ lines.push(theme.fg("warning", line));
459
+ }
460
+ }
461
+
462
+ lines.push("");
463
+ lines.push(rule);
464
+ lines.push(theme.bold("Approve this commit?"));
465
+ lines.push("");
466
+
467
+ for (let i = 0; i < options.length; i++) {
468
+ const label = options[i]!;
469
+ if (i === selected) {
470
+ lines.push(theme.fg("accent", `> ${label}`));
471
+ } else {
472
+ lines.push(theme.fg("dim", ` ${label}`));
473
+ }
474
+ }
475
+
476
+ lines.push("");
477
+ lines.push(theme.fg("dim", "Up/Down select Enter confirm Esc deny"));
478
+
479
+ return lines;
480
+ }
481
+
482
+ function handleInput(data: string): void {
483
+ if (matchesKey(data, "up") || matchesKey(data, "left")) {
484
+ selected = selected > 0 ? selected - 1 : options.length - 1;
485
+ tui.requestRender();
486
+ } else if (matchesKey(data, "down") || matchesKey(data, "right")) {
487
+ selected = (selected + 1) % options.length;
488
+ tui.requestRender();
489
+ } else if (matchesKey(data, "enter")) {
490
+ done(selected === 0);
491
+ } else if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
492
+ done(false);
493
+ }
494
+ }
495
+
496
+ return {
497
+ render,
498
+ handleInput,
499
+ invalidate() {},
500
+ };
501
+ },
502
+ );
503
+
504
+ return result === true;
505
+ }
506
+
507
+ function userBashBlocked(message: string): UserBashEventResult {
508
+ return {
509
+ result: {
510
+ output: `${message}\n`,
511
+ exitCode: 1,
512
+ cancelled: false,
513
+ truncated: false,
514
+ },
515
+ };
516
+ }
517
+
518
+ export default function commitApprovalExtension(pi: ExtensionAPI) {
519
+ pi.on("tool_call", async (event, ctx): Promise<ToolCallEventResult | undefined> => {
520
+ if (!isToolCallEventType("bash", event)) {
521
+ return;
522
+ }
523
+
524
+ const command = String(event.input.command ?? "").trim();
525
+ if (!command) {
526
+ return;
527
+ }
528
+
529
+ const metadata = parseCommitMetadataFromCommand(command);
530
+ if (!metadata) {
531
+ return;
532
+ }
533
+
534
+ const validation = validateCommitMessage(metadata);
535
+
536
+ if (validation.hasErrors) {
537
+ return {
538
+ block: true,
539
+ reason: `git commit blocked: message does not meet standards.\n${formatValidationIssues(validation.issues)}\nFix the commit message and retry.`,
540
+ };
541
+ }
542
+
543
+ if (!ctx.hasUI) {
544
+ return {
545
+ block: true,
546
+ reason: REASON_INTERACTIVE_APPROVAL_REQUIRED,
547
+ };
548
+ }
549
+
550
+ const approved = await requestApproval(ctx, metadata, validation.issues);
551
+ if (!approved) {
552
+ return {
553
+ block: true,
554
+ reason: REASON_APPROVAL_DENIED,
555
+ };
556
+ }
557
+
558
+ return;
559
+ });
560
+
561
+ pi.on("user_bash", async (event, ctx) => {
562
+ const command = event.command.trim();
563
+ if (!command) {
564
+ return;
565
+ }
566
+
567
+ const metadata = parseCommitMetadataFromCommand(command);
568
+ if (!metadata) {
569
+ return;
570
+ }
571
+
572
+ const validation = validateCommitMessage(metadata);
573
+
574
+ if (!ctx.hasUI) {
575
+ if (validation.hasErrors) {
576
+ return userBashBlocked(
577
+ `git commit blocked: message does not meet standards.\n${formatValidationIssues(validation.issues)}`,
578
+ );
579
+ }
580
+ return userBashBlocked(REASON_INTERACTIVE_APPROVAL_REQUIRED);
581
+ }
582
+
583
+ const approved = await requestApproval(ctx, metadata, validation.issues);
584
+ if (!approved) {
585
+ return userBashBlocked(REASON_APPROVAL_DENIED);
586
+ }
587
+
588
+ return;
589
+ });
590
+ }