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,51 @@
1
+ /**
2
+ * Exa Enforce Extension
3
+ *
4
+ * Blocks ad-hoc web fetching (curl, wget, python requests, axios, fetch, etc.)
5
+ * and redirects the model to use the exa-search skill instead.
6
+ */
7
+
8
+ import type { ExtensionAPI, ToolCallEventResult } from "@mariozechner/pi-coding-agent";
9
+ import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
10
+
11
+ export default function (pi: ExtensionAPI) {
12
+ // Patterns that indicate ad-hoc web fetching
13
+ const fetchPatterns = [
14
+ /\bcurl\s/i,
15
+ /\bwget\s/i,
16
+ /\bhttpie\b/i,
17
+ /\bhttp\s+(GET|POST|PUT|DELETE|PATCH|HEAD)\b/i,
18
+ /\brequests\.(get|post|put|delete|patch|head)\b/i,
19
+ /\burllib\.request/i,
20
+ /\bimport\s+requests\b/i,
21
+ /\bfrom\s+requests\s+import\b/i,
22
+ /\baxios[\.(]/i,
23
+ /\bnode-fetch\b/i,
24
+ /\bfetch\s*\(\s*['"`]https?:/i,
25
+ /\bgot\s*\(\s*['"`]https?:/i,
26
+ /\bscraping\b.*\bimport\b|\bimport\b.*\b(scrapy|beautifulsoup|bs4|cheerio|puppeteer|playwright)\b/i,
27
+ /\bSelenium\b/i,
28
+ /\blynx\s+-dump/i,
29
+ /\bpython3?\s+-c\b[^\n]*\burllib\b/i,
30
+ /\bpython3?\s+-c\b[^\n]*\brequests\b/i,
31
+ ];
32
+
33
+ const BLOCK_REASON =
34
+ "Direct web fetching is not allowed. Use the exa-search skill instead.\n\n" +
35
+ "Load it with: /skill:exa-search\n\n" +
36
+ "Exa supports: semantic search, find-similar, content extraction, answers, and structured research.";
37
+
38
+ pi.on("tool_call", async (event, ctx): Promise<ToolCallEventResult | undefined> => {
39
+ // Check bash commands
40
+ if (isToolCallEventType("bash", event)) {
41
+ const cmd = String(event.input.command ?? "");
42
+ const matched = fetchPatterns.some((p) => p.test(cmd));
43
+ if (matched) {
44
+ ctx.ui.notify("Blocked: use exa-search skill for web lookups", "error");
45
+ return { block: true, reason: BLOCK_REASON };
46
+ }
47
+ }
48
+
49
+ return undefined;
50
+ });
51
+ }
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Exa Search Tool
3
+ *
4
+ * Registers a callable `exa_search` tool that wraps the exa-search skill's
5
+ * helper script via pi.exec(). Works in all modes including question-mode
6
+ * and plan-mode since it does not depend on bash.
7
+ *
8
+ * Supports all 5 Exa endpoints: search, contents, findsimilar, answer, research.
9
+ */
10
+
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { existsSync } from "node:fs";
14
+
15
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
+ import { Type } from "@sinclair/typebox";
17
+ import { StringEnum } from "@mariozechner/pi-ai";
18
+ import { Text } from "@mariozechner/pi-tui";
19
+
20
+ // ── Script resolution ─────────────────────────────────────────────────
21
+ const SCRIPT_CANDIDATES = [
22
+ join(homedir(), ".pi", "agent", "skills", "exa-search", "scripts", "exa-api.cjs"),
23
+ join(homedir(), ".agents", "skills", "exa-search", "scripts", "exa-api.cjs"),
24
+ ];
25
+
26
+ function resolveScript(): string | undefined {
27
+ return SCRIPT_CANDIDATES.find((p) => existsSync(p));
28
+ }
29
+
30
+ // ── Schema ────────────────────────────────────────────────────────────
31
+ const ExaSearchParams = Type.Object({
32
+ endpoint: StringEnum(
33
+ ["search", "contents", "findsimilar", "answer", "research"] as const,
34
+ { description: "Exa API endpoint to call" },
35
+ ),
36
+ query: Type.Optional(
37
+ Type.String({ description: "Search query (search, answer, research endpoints)" }),
38
+ ),
39
+ url: Type.Optional(
40
+ Type.String({ description: "URL to find similar pages for (findsimilar endpoint)" }),
41
+ ),
42
+ ids: Type.Optional(
43
+ Type.Array(Type.String(), { description: "Result IDs to fetch content for (contents endpoint)" }),
44
+ ),
45
+ numResults: Type.Optional(
46
+ Type.Number({ description: "Number of results to return (default: 10)" }),
47
+ ),
48
+ type: Type.Optional(
49
+ StringEnum(["auto", "neural", "fast", "deep"] as const, {
50
+ description: "Search type (default: auto)",
51
+ }),
52
+ ),
53
+ category: Type.Optional(
54
+ Type.String({
55
+ description: "Category filter: company, people, research paper, news, pdf, github, tweet",
56
+ }),
57
+ ),
58
+ includeDomains: Type.Optional(
59
+ Type.Array(Type.String(), { description: "Restrict results to these domains" }),
60
+ ),
61
+ excludeDomains: Type.Optional(
62
+ Type.Array(Type.String(), { description: "Exclude results from these domains" }),
63
+ ),
64
+ startPublishedDate: Type.Optional(
65
+ Type.String({ description: "Filter results published after this ISO date (e.g. 2025-01-01)" }),
66
+ ),
67
+ endPublishedDate: Type.Optional(
68
+ Type.String({ description: "Filter results published before this ISO date" }),
69
+ ),
70
+ includeText: Type.Optional(
71
+ Type.Array(Type.String(), { description: "Pages must contain these strings" }),
72
+ ),
73
+ excludeText: Type.Optional(
74
+ Type.Array(Type.String(), { description: "Pages must not contain these strings" }),
75
+ ),
76
+ text: Type.Optional(
77
+ Type.Boolean({ description: "Include full text in results (default: true for search)" }),
78
+ ),
79
+ highlights: Type.Optional(
80
+ Type.Boolean({ description: "Include highlights in results" }),
81
+ ),
82
+ summary: Type.Optional(
83
+ Type.Boolean({ description: "Include summary in results" }),
84
+ ),
85
+ input: Type.Optional(
86
+ Type.String({ description: "Research question (research endpoint)" }),
87
+ ),
88
+ });
89
+
90
+ // ── Payload builders ──────────────────────────────────────────────────
91
+
92
+ interface Params {
93
+ endpoint: string;
94
+ query?: string;
95
+ url?: string;
96
+ ids?: string[];
97
+ numResults?: number;
98
+ type?: string;
99
+ category?: string;
100
+ includeDomains?: string[];
101
+ excludeDomains?: string[];
102
+ startPublishedDate?: string;
103
+ endPublishedDate?: string;
104
+ includeText?: string[];
105
+ excludeText?: string[];
106
+ text?: boolean;
107
+ highlights?: boolean;
108
+ summary?: boolean;
109
+ input?: string;
110
+ }
111
+
112
+ function buildPayload(params: Params): Record<string, unknown> {
113
+ const { endpoint } = params;
114
+
115
+ switch (endpoint) {
116
+ case "search":
117
+ return {
118
+ query: params.query,
119
+ type: params.type ?? "auto",
120
+ numResults: params.numResults ?? 10,
121
+ ...(params.category && { category: params.category }),
122
+ ...(params.includeDomains?.length && { includeDomains: params.includeDomains }),
123
+ ...(params.excludeDomains?.length && { excludeDomains: params.excludeDomains }),
124
+ ...(params.startPublishedDate && { startPublishedDate: params.startPublishedDate }),
125
+ ...(params.endPublishedDate && { endPublishedDate: params.endPublishedDate }),
126
+ ...(params.includeText?.length && { includeText: params.includeText }),
127
+ ...(params.excludeText?.length && { excludeText: params.excludeText }),
128
+ contents: {
129
+ text: params.text ?? true,
130
+ highlights: params.highlights ?? true,
131
+ summary: params.summary ?? true,
132
+ },
133
+ };
134
+
135
+ case "contents":
136
+ return {
137
+ ids: params.ids ?? [],
138
+ text: params.text ?? true,
139
+ highlights: params.highlights ?? false,
140
+ summary: params.summary ?? true,
141
+ };
142
+
143
+ case "findsimilar":
144
+ return {
145
+ url: params.url,
146
+ numResults: params.numResults ?? 10,
147
+ ...(params.category && { category: params.category }),
148
+ ...(params.includeDomains?.length && { includeDomains: params.includeDomains }),
149
+ ...(params.excludeDomains?.length && { excludeDomains: params.excludeDomains }),
150
+ ...(params.startPublishedDate && { startPublishedDate: params.startPublishedDate }),
151
+ contents: {
152
+ text: params.text ?? true,
153
+ summary: params.summary ?? true,
154
+ },
155
+ };
156
+
157
+ case "answer":
158
+ return {
159
+ query: params.query,
160
+ numResults: params.numResults ?? 5,
161
+ ...(params.includeDomains?.length && { includeDomains: params.includeDomains }),
162
+ ...(params.excludeDomains?.length && { excludeDomains: params.excludeDomains }),
163
+ };
164
+
165
+ case "research":
166
+ return {
167
+ input: params.input ?? params.query,
168
+ model: "auto",
169
+ stream: false,
170
+ citation_format: "numbered",
171
+ };
172
+
173
+ default:
174
+ return {};
175
+ }
176
+ }
177
+
178
+ // ── Result formatting ─────────────────────────────────────────────────
179
+
180
+ interface ExaResult {
181
+ title?: string;
182
+ url?: string;
183
+ text?: string;
184
+ summary?: string;
185
+ highlights?: string[];
186
+ score?: number;
187
+ publishedDate?: string;
188
+ id?: string;
189
+ }
190
+
191
+ interface ExaResponse {
192
+ results?: ExaResult[];
193
+ answer?: string;
194
+ context?: string;
195
+ data?: unknown;
196
+ costDollars?: number;
197
+ requestId?: string;
198
+ searchType?: string;
199
+ }
200
+
201
+ function formatResultsForLLM(endpoint: string, response: ExaResponse): string {
202
+ const parts: string[] = [];
203
+
204
+ if (endpoint === "answer" && response.answer) {
205
+ parts.push(response.answer);
206
+ if (response.context) {
207
+ parts.push("", "Sources:", response.context);
208
+ }
209
+ } else if (endpoint === "research") {
210
+ if (response.data) {
211
+ parts.push(JSON.stringify(response.data, null, 2));
212
+ } else {
213
+ parts.push(JSON.stringify(response, null, 2));
214
+ }
215
+ } else if (response.results && response.results.length > 0) {
216
+ for (let i = 0; i < response.results.length; i++) {
217
+ const r = response.results[i];
218
+ const num = i + 1;
219
+ parts.push(`[${num}] ${r.title ?? "(no title)"}`);
220
+ if (r.url) parts.push(` ${r.url}`);
221
+ if (r.publishedDate) parts.push(` Published: ${r.publishedDate}`);
222
+ if (r.summary) parts.push(` ${r.summary}`);
223
+ if (r.text) {
224
+ const preview = r.text.length > 500 ? r.text.slice(0, 500) + "..." : r.text;
225
+ parts.push(` ${preview}`);
226
+ }
227
+ if (r.highlights && r.highlights.length > 0) {
228
+ parts.push(` Highlights: ${r.highlights.join(" | ")}`);
229
+ }
230
+ parts.push("");
231
+ }
232
+ } else {
233
+ parts.push("No results found.");
234
+ }
235
+
236
+ if (response.costDollars != null) {
237
+ const cost = Number(response.costDollars);
238
+ parts.push(`[Cost: $${Number.isFinite(cost) ? cost.toFixed(4) : response.costDollars}]`);
239
+ }
240
+
241
+ return parts.join("\n");
242
+ }
243
+
244
+ // ── Details for rendering & state ─────────────────────────────────────
245
+
246
+ interface ExaSearchDetails {
247
+ endpoint: string;
248
+ query?: string;
249
+ url?: string;
250
+ resultCount: number;
251
+ cost?: number;
252
+ }
253
+
254
+ // ── Extension ─────────────────────────────────────────────────────────
255
+
256
+ export default function exaSearchTool(pi: ExtensionAPI): void {
257
+ const scriptPath = resolveScript();
258
+
259
+ if (!scriptPath) {
260
+ // Skill not installed -- skip tool registration silently
261
+ return;
262
+ }
263
+
264
+ pi.registerTool({
265
+ name: "exa_search",
266
+ label: "Exa Search",
267
+ description:
268
+ "Web search, content extraction, similar-page discovery, direct answers, " +
269
+ "and structured research via the Exa API. Use instead of bash-based web fetching.",
270
+
271
+ promptSnippet:
272
+ "Semantic web search, find similar pages, get direct answers, or run structured research via Exa",
273
+
274
+ promptGuidelines: [
275
+ "Use exa_search for all web search, documentation lookup, and research tasks.",
276
+ "Prefer the 'search' endpoint with includeDomains for official docs verification.",
277
+ "Use 'answer' for direct factual questions, 'research' for structured synthesis.",
278
+ "Use 'findsimilar' when the user provides a reference URL and wants related pages.",
279
+ "Use 'contents' to fetch full text for result IDs from a previous search.",
280
+ ],
281
+
282
+ parameters: ExaSearchParams,
283
+
284
+ async execute(_toolCallId, params, signal, onUpdate) {
285
+ if (signal?.aborted) {
286
+ throw new Error("Exa search was cancelled");
287
+ }
288
+
289
+ const { endpoint } = params;
290
+ const label = endpoint === "findsimilar" ? "find similar" : endpoint;
291
+ const queryPreview = params.query ?? params.url ?? params.input ?? "(no query)";
292
+
293
+ onUpdate?.({
294
+ content: [{ type: "text", text: `Searching Exa (${label}): ${queryPreview}` }],
295
+ });
296
+
297
+ const payload = buildPayload(params);
298
+ const payloadJson = JSON.stringify(payload);
299
+
300
+ const result = await pi.exec("node", [scriptPath, endpoint, payloadJson], {
301
+ signal,
302
+ timeout: 65_000,
303
+ });
304
+
305
+ if (result.killed) {
306
+ throw new Error("Exa search timed out");
307
+ }
308
+
309
+ if (result.code !== 0) {
310
+ const errorMsg = (result.stderr || result.stdout || "Unknown error").trim();
311
+ throw new Error(`Exa API error: ${errorMsg}`);
312
+ }
313
+
314
+ let response: ExaResponse;
315
+ try {
316
+ response = JSON.parse(result.stdout);
317
+ } catch {
318
+ throw new Error(`Failed to parse Exa response: ${result.stdout.slice(0, 200)}`);
319
+ }
320
+
321
+ const formatted = formatResultsForLLM(endpoint, response);
322
+ const resultCount = response.results?.length ?? (response.answer ? 1 : 0);
323
+
324
+ return {
325
+ content: [{ type: "text", text: formatted }],
326
+ details: {
327
+ endpoint,
328
+ query: params.query ?? params.url ?? params.input,
329
+ resultCount,
330
+ cost: response.costDollars,
331
+ } satisfies ExaSearchDetails,
332
+ };
333
+ },
334
+
335
+ renderCall(args, theme) {
336
+ const endpoint = args.endpoint ?? "search";
337
+ const query = args.query ?? args.url ?? args.input ?? "";
338
+ let text = theme.fg("toolTitle", theme.bold("exa "));
339
+ text += theme.fg("accent", endpoint);
340
+ if (query) {
341
+ const preview = query.length > 60 ? query.slice(0, 60) + "..." : query;
342
+ text += " " + theme.fg("muted", preview);
343
+ }
344
+ if (args.includeDomains?.length) {
345
+ text += " " + theme.fg("dim", `[${args.includeDomains.join(", ")}]`);
346
+ }
347
+ return new Text(text, 0, 0);
348
+ },
349
+
350
+ renderResult(result, { expanded, isPartial }, theme) {
351
+ if (isPartial) {
352
+ return new Text(theme.fg("warning", "Searching..."), 0, 0);
353
+ }
354
+
355
+ const details = result.details as ExaSearchDetails | undefined;
356
+ if (!details || !details.endpoint) {
357
+ const first = result.content[0];
358
+ return new Text(first?.type === "text" ? first.text : "(no output)", 0, 0);
359
+ }
360
+
361
+ const countLabel =
362
+ details.resultCount === 1
363
+ ? "1 result"
364
+ : `${details.resultCount} results`;
365
+
366
+ let text = theme.fg("success", countLabel);
367
+ text += " " + theme.fg("muted", `(${details.endpoint})`);
368
+
369
+ if (details.cost != null) {
370
+ const cost = Number(details.cost);
371
+ text += " " + theme.fg("dim", `[$${Number.isFinite(cost) ? cost.toFixed(4) : details.cost}]`);
372
+ }
373
+
374
+ if (expanded) {
375
+ const content = result.content[0];
376
+ if (content?.type === "text") {
377
+ text += "\n" + theme.fg("dim", content.text);
378
+ }
379
+ }
380
+
381
+ return new Text(text, 0, 0);
382
+ },
383
+ });
384
+ }
@@ -0,0 +1,82 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+
4
+ export default function (pi: ExtensionAPI) {
5
+ // Queue of commands to execute after agent turn ends
6
+ let pendingCommand: { command: string; reason?: string } | null = null;
7
+
8
+ // Tool to execute a command/message directly (self-invoke)
9
+ pi.registerTool({
10
+ name: "execute_command",
11
+ label: "Execute Command",
12
+ description: `Execute a slash command or send a message as if the user typed it. The message is added to the session history and triggers a new turn. Use this to:
13
+ - Self-invoke /answer after asking multiple questions
14
+ - Run /reload after creating skills
15
+ - Execute any slash command programmatically
16
+ - Send follow-up prompts to yourself
17
+
18
+ The command/message appears in the conversation as a user message.`,
19
+ promptSnippet:
20
+ "Execute a slash command or send a message as if the user typed it. " +
21
+ "Use to self-invoke /answer after asking questions, run /reload after creating skills, or send follow-up prompts.",
22
+
23
+ parameters: Type.Object({
24
+ command: Type.String({
25
+ description: "The command or message to execute (e.g., '/answer', '/reload', or any text)"
26
+ }),
27
+ reason: Type.Optional(
28
+ Type.String({
29
+ description: "Optional explanation for why you're executing this command (shown to user)"
30
+ })
31
+ ),
32
+ }),
33
+
34
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
35
+ const { command, reason } = params;
36
+
37
+ // Store command to be executed after agent turn ends
38
+ pendingCommand = { command, reason };
39
+
40
+ const explanation = reason
41
+ ? `Queued for execution: ${command}\nReason: ${reason}`
42
+ : `Queued for execution: ${command}`;
43
+
44
+ return {
45
+ content: [{ type: "text", text: explanation }],
46
+ details: {
47
+ command,
48
+ reason,
49
+ queued: true,
50
+ },
51
+ };
52
+ },
53
+ });
54
+
55
+ // Execute pending command after agent turn completes
56
+ pi.on("agent_end", async (event, ctx) => {
57
+ if (pendingCommand) {
58
+ const { command } = pendingCommand;
59
+ pendingCommand = null;
60
+
61
+ // Special handling for /answer via event bus (needs context)
62
+ if (command === "/answer") {
63
+ setTimeout(() => {
64
+ pi.events.emit("trigger:answer", ctx);
65
+ }, 100);
66
+ }
67
+ // Auto-execute slash commands via sendUserMessage
68
+ else if (command.startsWith("/")) {
69
+ setTimeout(() => {
70
+ pi.sendUserMessage(command);
71
+ }, 100);
72
+ }
73
+ // For non-command text, prefill editor and notify
74
+ else {
75
+ if (ctx.hasUI) {
76
+ ctx.ui.setEditorText(command);
77
+ ctx.ui.notify(`Press Enter to send: ${command}`, "info");
78
+ }
79
+ }
80
+ }
81
+ });
82
+ }