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,242 @@
1
+ import type { ExtensionAPI, ExtensionContext, ToolCallEventResult } from "@mariozechner/pi-coding-agent";
2
+ import { Key } from "@mariozechner/pi-tui";
3
+
4
+ const QUESTION_MODE_TOOLS = ["read", "grep", "find", "ls", "mcp", "exa_search"] as const;
5
+ const FALLBACK_TOOLS = ["read", "bash", "edit", "write"] as const;
6
+
7
+ interface QuestionModeState {
8
+ enabled: boolean;
9
+ restoreTools: string[];
10
+ }
11
+
12
+ function normalizeTools(input: unknown): string[] {
13
+ if (!Array.isArray(input)) return [];
14
+ return input.filter((value): value is string => typeof value === "string" && value.trim().length > 0);
15
+ }
16
+
17
+ export default function questionModeExtension(pi: ExtensionAPI): void {
18
+ let enabled = false;
19
+ let restoreTools: string[] = [];
20
+
21
+ function getAvailableToolNames(): Set<string> {
22
+ return new Set(pi.getAllTools().map((tool) => tool.name));
23
+ }
24
+
25
+ function filterAvailableTools(tools: readonly string[]): string[] {
26
+ const available = getAvailableToolNames();
27
+ return tools.filter((tool) => available.has(tool));
28
+ }
29
+
30
+ function getQuestionModeTools(): string[] {
31
+ const preferred = filterAvailableTools(QUESTION_MODE_TOOLS);
32
+ if (preferred.length > 0) return preferred;
33
+
34
+ const current = pi.getActiveTools();
35
+ if (current.length > 0) return current;
36
+
37
+ return filterAvailableTools(FALLBACK_TOOLS);
38
+ }
39
+
40
+ function getRestoreTools(): string[] {
41
+ const candidate = restoreTools.length > 0 ? restoreTools : Array.from(FALLBACK_TOOLS);
42
+ const filtered = filterAvailableTools(candidate);
43
+
44
+ if (filtered.length > 0) return filtered;
45
+
46
+ const current = pi.getActiveTools();
47
+ return current.length > 0 ? current : ["read"];
48
+ }
49
+
50
+ function applyQuestionModeTools(): string[] {
51
+ const tools = getQuestionModeTools();
52
+ if (tools.length > 0) {
53
+ pi.setActiveTools(tools);
54
+ }
55
+ return tools;
56
+ }
57
+
58
+ function applyRestoreTools(): string[] {
59
+ const tools = getRestoreTools();
60
+ if (tools.length > 0) {
61
+ pi.setActiveTools(tools);
62
+ }
63
+ return tools;
64
+ }
65
+
66
+ function updateStatus(ctx: ExtensionContext): void {
67
+ if (enabled) {
68
+ ctx.ui.setStatus("question-mode", "question-mode:on");
69
+ } else {
70
+ ctx.ui.setStatus("question-mode", undefined);
71
+ }
72
+ }
73
+
74
+ function persistState(): void {
75
+ pi.appendEntry<QuestionModeState>("question-mode-state", {
76
+ enabled,
77
+ restoreTools,
78
+ });
79
+ }
80
+
81
+ function restoreFromBranch(ctx: ExtensionContext): void {
82
+ const branchEntries = ctx.sessionManager.getBranch();
83
+ let state: QuestionModeState | undefined;
84
+
85
+ for (const entry of branchEntries) {
86
+ if (entry.type === "custom" && entry.customType === "question-mode-state") {
87
+ const data = entry.data as QuestionModeState | undefined;
88
+ if (data) {
89
+ state = {
90
+ enabled: Boolean(data.enabled),
91
+ restoreTools: normalizeTools(data.restoreTools),
92
+ };
93
+ }
94
+ }
95
+ }
96
+
97
+ if (!state) {
98
+ enabled = false;
99
+ restoreTools = [];
100
+ updateStatus(ctx);
101
+ return;
102
+ }
103
+
104
+ enabled = state.enabled;
105
+ restoreTools = state.restoreTools;
106
+
107
+ if (enabled) {
108
+ applyQuestionModeTools();
109
+ }
110
+
111
+ updateStatus(ctx);
112
+ }
113
+
114
+ function getStatusSummary(): string {
115
+ const mode = enabled ? "on" : "off";
116
+ const tools = pi.getActiveTools();
117
+ return `question-mode:${mode} | active tools: ${tools.join(", ")}`;
118
+ }
119
+
120
+ function enableQuestionMode(ctx: ExtensionContext): void {
121
+ if (enabled) {
122
+ ctx.ui.notify("Question mode is already enabled.", "info");
123
+ return;
124
+ }
125
+
126
+ restoreTools = pi.getActiveTools();
127
+ enabled = true;
128
+ const tools = applyQuestionModeTools();
129
+ persistState();
130
+ updateStatus(ctx);
131
+ ctx.ui.notify(`Question mode enabled. Tools: ${tools.join(", ")}`, "info");
132
+ }
133
+
134
+ function disableQuestionMode(ctx: ExtensionContext): void {
135
+ if (!enabled) {
136
+ ctx.ui.notify("Question mode is already disabled.", "info");
137
+ return;
138
+ }
139
+
140
+ enabled = false;
141
+ const restored = applyRestoreTools();
142
+ persistState();
143
+ updateStatus(ctx);
144
+ ctx.ui.notify(`Question mode disabled. Restored tools: ${restored.join(", ")}`, "info");
145
+ }
146
+
147
+ pi.registerShortcut(Key.ctrl("q"), {
148
+ description: "Toggle question mode",
149
+ handler: (ctx) => {
150
+ if (enabled) {
151
+ disableQuestionMode(ctx);
152
+ } else {
153
+ enableQuestionMode(ctx);
154
+ }
155
+ },
156
+ });
157
+
158
+ pi.registerCommand("question-mode", {
159
+ description: "Toggle strict read-only Q&A mode (no edits, no bash)",
160
+ handler: async (args, ctx) => {
161
+ const action = args.trim().toLowerCase();
162
+
163
+ if (action === "status") {
164
+ ctx.ui.notify(getStatusSummary(), "info");
165
+ return;
166
+ }
167
+
168
+ if (action === "on" || action === "enable") {
169
+ enableQuestionMode(ctx);
170
+ return;
171
+ }
172
+
173
+ if (action === "off" || action === "disable") {
174
+ disableQuestionMode(ctx);
175
+ return;
176
+ }
177
+
178
+ if (action === "" || action === "toggle") {
179
+ if (enabled) {
180
+ disableQuestionMode(ctx);
181
+ } else {
182
+ enableQuestionMode(ctx);
183
+ }
184
+ return;
185
+ }
186
+
187
+ ctx.ui.notify("Usage: /question-mode [on|off|toggle|status]", "warning");
188
+ },
189
+ });
190
+
191
+ pi.on("tool_call", async (event): Promise<ToolCallEventResult | undefined> => {
192
+ if (!enabled) return undefined;
193
+
194
+ if (event.toolName === "edit" || event.toolName === "write") {
195
+ return {
196
+ block: true,
197
+ reason: "Question mode blocks file modifications. Disable with /question-mode off to make changes.",
198
+ };
199
+ }
200
+
201
+ if (event.toolName === "bash") {
202
+ return {
203
+ block: true,
204
+ reason: "Question mode blocks bash to keep exploration read-only.",
205
+ };
206
+ }
207
+
208
+ return undefined;
209
+ });
210
+
211
+ pi.on("before_agent_start", async () => {
212
+ if (!enabled) return undefined;
213
+
214
+ return {
215
+ message: {
216
+ customType: "question-mode-context",
217
+ content: `[QUESTION MODE ACTIVE]\nYou are answering a user question in strict read-only mode.\n\nRules:\n- Focus on explanation and analysis.\n- Do not edit or create files.\n- Do not run shell commands that modify files, install dependencies, or change git state.\n- Use only available read-only tools to gather evidence.\n- For codebase discovery, audits, and cross-referencing, prefer augment_context_engine over exhaustive ls/Read traversals.\n- If code changes would help, describe them as a follow-up and ask for confirmation first.`,
218
+ display: false,
219
+ },
220
+ };
221
+ });
222
+
223
+ pi.on("turn_start", async (_event, ctx) => {
224
+ if (!enabled) return;
225
+
226
+ applyQuestionModeTools();
227
+ updateStatus(ctx);
228
+ persistState();
229
+ });
230
+
231
+ pi.on("session_start", async (_event, ctx) => {
232
+ restoreFromBranch(ctx);
233
+ });
234
+
235
+ pi.on("session_tree", async (_event, ctx) => {
236
+ restoreFromBranch(ctx);
237
+ });
238
+
239
+ pi.on("session_fork", async (_event, ctx) => {
240
+ restoreFromBranch(ctx);
241
+ });
242
+ }
@@ -0,0 +1,141 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ function normalizeName(name: string | undefined | null): string {
4
+ return (name ?? "").trim();
5
+ }
6
+
7
+ function getDefaultNameFromFirstUserMessage(ctx: any): string {
8
+ const branch = ctx.sessionManager.getBranch();
9
+
10
+ for (const entry of branch) {
11
+ if (entry?.type !== "message") continue;
12
+ if (entry?.message?.role !== "user") continue;
13
+
14
+ const content = entry.message.content;
15
+ if (typeof content === "string") {
16
+ const value = content.replace(/\s+/g, " ").trim();
17
+ if (value) return value;
18
+ continue;
19
+ }
20
+
21
+ if (Array.isArray(content)) {
22
+ const textPart = content.find((part: any) => part?.type === "text" && typeof part?.text === "string");
23
+ const value = (textPart?.text ?? "").replace(/\s+/g, " ").trim();
24
+ if (value) return value;
25
+ }
26
+ }
27
+
28
+ return "work session";
29
+ }
30
+
31
+ function getEasternTimestamp(): string {
32
+ const now = new Date();
33
+ const date = new Intl.DateTimeFormat("en-CA", {
34
+ timeZone: "America/New_York",
35
+ year: "numeric",
36
+ month: "2-digit",
37
+ day: "2-digit",
38
+ }).formatToParts(now);
39
+ const y = date.find(p => p.type === "year")!.value;
40
+ const m = date.find(p => p.type === "month")!.value;
41
+ const d = date.find(p => p.type === "day")!.value;
42
+
43
+ const time = new Intl.DateTimeFormat("en-US", {
44
+ timeZone: "America/New_York",
45
+ hour: "numeric",
46
+ minute: "2-digit",
47
+ hour12: true,
48
+ }).format(now);
49
+ const compact = time.replace(/\s/g, "");
50
+
51
+ return `${y}-${m}-${d} ${compact}`;
52
+ }
53
+
54
+ function isShutdownFromReload(): boolean {
55
+ const stack = new Error().stack ?? "";
56
+ return stack.includes("AgentSession.reload") || stack.includes("handleReloadCommand");
57
+ }
58
+
59
+ async function ensureSessionNameAndConfirmExit(pi: ExtensionAPI, ctx: any): Promise<boolean> {
60
+ if (!ctx.hasUI) return false;
61
+
62
+ const current = normalizeName(pi.getSessionName());
63
+ if (current) {
64
+ const confirmed = await ctx.ui.confirm("Exit session?", `Session: ${current}\n\nDo you want to exit pi now?`);
65
+ if (!confirmed) {
66
+ ctx.ui.notify("Exit canceled", "info");
67
+ return false;
68
+ }
69
+ return true;
70
+ }
71
+
72
+ const suggested = getDefaultNameFromFirstUserMessage(ctx);
73
+ const input = await ctx.ui.input("Session name required before exit", suggested);
74
+ const name = normalizeName(input);
75
+
76
+ if (!name) {
77
+ const fallback = `${getEasternTimestamp()} ${suggested}`;
78
+ pi.setSessionName(fallback);
79
+ ctx.ui.notify(`Auto-named: ${fallback}`, "info");
80
+ return true;
81
+ }
82
+
83
+ pi.setSessionName(name);
84
+ ctx.ui.notify(`Session named: ${name}`, "info");
85
+ return true;
86
+ }
87
+
88
+ export default function (pi: ExtensionAPI) {
89
+ const guardedExit = async (ctx: any) => {
90
+ const ok = await ensureSessionNameAndConfirmExit(pi, ctx);
91
+ if (!ok) return;
92
+ ctx.shutdown();
93
+ };
94
+
95
+ // Built-in /quit and /exit cannot be overridden by extensions.
96
+ // Provide guarded alternatives.
97
+ pi.registerCommand("safe-quit", {
98
+ description: "Exit pi with guardrails (requires session name, confirms if already named)",
99
+ handler: async (_args, ctx) => {
100
+ await guardedExit(ctx);
101
+ },
102
+ });
103
+
104
+ pi.registerCommand("q", {
105
+ description: "Alias for /safe-quit",
106
+ handler: async (_args, ctx) => {
107
+ await guardedExit(ctx);
108
+ },
109
+ });
110
+
111
+ pi.registerShortcut("ctrl+shift+q", {
112
+ description: "Safe exit with session-name enforcement",
113
+ handler: async (ctx) => {
114
+ await guardedExit(ctx);
115
+ },
116
+ });
117
+
118
+ // Catch built-in /quit (which cannot be overridden by extension commands).
119
+ // Note: session_shutdown also fires on /reload, so we skip prompts in that path.
120
+ pi.on("session_shutdown", async (_event, ctx) => {
121
+ if (!ctx.hasUI) return;
122
+ if (isShutdownFromReload()) return;
123
+
124
+ const current = normalizeName(pi.getSessionName());
125
+ if (current) return;
126
+
127
+ const suggested = getDefaultNameFromFirstUserMessage(ctx);
128
+ const input = await ctx.ui.input("Session is closing. Enter a name to save it", suggested);
129
+ const name = normalizeName(input);
130
+
131
+ if (!name) {
132
+ const fallback = `${getEasternTimestamp()} ${suggested}`;
133
+ pi.setSessionName(fallback);
134
+ ctx.ui.notify(`Auto-named: ${fallback}`, "info");
135
+ return;
136
+ }
137
+
138
+ pi.setSessionName(name);
139
+ ctx.ui.notify(`Session named: ${name}`, "info");
140
+ });
141
+ }