@kata-sh/cli 0.1.0 → 0.1.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 (199) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/app-paths.d.ts +4 -0
  4. package/dist/app-paths.js +6 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +56 -0
  7. package/dist/loader.d.ts +2 -0
  8. package/dist/loader.js +95 -0
  9. package/dist/resource-loader.d.ts +18 -0
  10. package/dist/resource-loader.js +50 -0
  11. package/dist/wizard.d.ts +15 -0
  12. package/dist/wizard.js +159 -0
  13. package/package.json +50 -21
  14. package/pkg/dist/modes/interactive/theme/dark.json +85 -0
  15. package/pkg/dist/modes/interactive/theme/light.json +84 -0
  16. package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
  17. package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
  18. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  19. package/pkg/dist/modes/interactive/theme/theme.js +949 -0
  20. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
  21. package/pkg/package.json +8 -0
  22. package/scripts/postinstall.js +45 -0
  23. package/src/resources/AGENTS.md +108 -0
  24. package/src/resources/KATA-WORKFLOW.md +661 -0
  25. package/src/resources/agents/researcher.md +29 -0
  26. package/src/resources/agents/scout.md +56 -0
  27. package/src/resources/agents/worker.md +31 -0
  28. package/src/resources/extensions/ask-user-questions.ts +200 -0
  29. package/src/resources/extensions/bg-shell/index.ts +2758 -0
  30. package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  31. package/src/resources/extensions/browser-tools/core.js +1057 -0
  32. package/src/resources/extensions/browser-tools/index.ts +4916 -0
  33. package/src/resources/extensions/browser-tools/package.json +20 -0
  34. package/src/resources/extensions/context7/index.ts +428 -0
  35. package/src/resources/extensions/context7/package.json +11 -0
  36. package/src/resources/extensions/get-secrets-from-user.ts +352 -0
  37. package/src/resources/extensions/github/formatters.ts +207 -0
  38. package/src/resources/extensions/github/gh-api.ts +537 -0
  39. package/src/resources/extensions/github/index.ts +778 -0
  40. package/src/resources/extensions/kata/activity-log.ts +88 -0
  41. package/src/resources/extensions/kata/auto.ts +2786 -0
  42. package/src/resources/extensions/kata/commands.ts +355 -0
  43. package/src/resources/extensions/kata/crash-recovery.ts +85 -0
  44. package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
  45. package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
  46. package/src/resources/extensions/kata/doctor.ts +683 -0
  47. package/src/resources/extensions/kata/files.ts +730 -0
  48. package/src/resources/extensions/kata/gitignore.ts +165 -0
  49. package/src/resources/extensions/kata/guided-flow.ts +976 -0
  50. package/src/resources/extensions/kata/index.ts +556 -0
  51. package/src/resources/extensions/kata/metrics.ts +397 -0
  52. package/src/resources/extensions/kata/observability-validator.ts +408 -0
  53. package/src/resources/extensions/kata/package.json +11 -0
  54. package/src/resources/extensions/kata/paths.ts +346 -0
  55. package/src/resources/extensions/kata/preferences.ts +695 -0
  56. package/src/resources/extensions/kata/prompt-loader.ts +50 -0
  57. package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
  58. package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
  59. package/src/resources/extensions/kata/prompts/discuss.md +151 -0
  60. package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
  61. package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
  62. package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
  63. package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
  64. package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
  65. package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
  66. package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
  67. package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
  68. package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
  69. package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
  70. package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
  71. package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
  72. package/src/resources/extensions/kata/prompts/queue.md +85 -0
  73. package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
  74. package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
  75. package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
  76. package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
  77. package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
  78. package/src/resources/extensions/kata/prompts/system.md +341 -0
  79. package/src/resources/extensions/kata/session-forensics.ts +550 -0
  80. package/src/resources/extensions/kata/skill-discovery.ts +137 -0
  81. package/src/resources/extensions/kata/state.ts +509 -0
  82. package/src/resources/extensions/kata/templates/context.md +76 -0
  83. package/src/resources/extensions/kata/templates/decisions.md +8 -0
  84. package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
  85. package/src/resources/extensions/kata/templates/plan.md +133 -0
  86. package/src/resources/extensions/kata/templates/preferences.md +15 -0
  87. package/src/resources/extensions/kata/templates/project.md +31 -0
  88. package/src/resources/extensions/kata/templates/reassessment.md +28 -0
  89. package/src/resources/extensions/kata/templates/requirements.md +81 -0
  90. package/src/resources/extensions/kata/templates/research.md +46 -0
  91. package/src/resources/extensions/kata/templates/roadmap.md +118 -0
  92. package/src/resources/extensions/kata/templates/slice-context.md +58 -0
  93. package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
  94. package/src/resources/extensions/kata/templates/state.md +19 -0
  95. package/src/resources/extensions/kata/templates/task-plan.md +52 -0
  96. package/src/resources/extensions/kata/templates/task-summary.md +57 -0
  97. package/src/resources/extensions/kata/templates/uat.md +54 -0
  98. package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
  99. package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
  100. package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
  101. package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
  102. package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
  103. package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
  104. package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
  105. package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
  106. package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
  107. package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
  108. package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
  109. package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
  110. package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
  111. package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
  112. package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
  113. package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
  114. package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
  115. package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
  116. package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
  117. package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
  118. package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
  119. package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
  120. package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
  121. package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
  122. package/src/resources/extensions/kata/types.ts +159 -0
  123. package/src/resources/extensions/kata/unit-runtime.ts +163 -0
  124. package/src/resources/extensions/kata/workspace-index.ts +203 -0
  125. package/src/resources/extensions/kata/worktree.ts +182 -0
  126. package/src/resources/extensions/mac-tools/index.ts +852 -0
  127. package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  128. package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  129. package/src/resources/extensions/search-the-web/cache.ts +78 -0
  130. package/src/resources/extensions/search-the-web/format.ts +258 -0
  131. package/src/resources/extensions/search-the-web/http.ts +238 -0
  132. package/src/resources/extensions/search-the-web/index.ts +68 -0
  133. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
  134. package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
  135. package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
  136. package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
  137. package/src/resources/extensions/shared/confirm-ui.ts +126 -0
  138. package/src/resources/extensions/shared/interview-ui.ts +822 -0
  139. package/src/resources/extensions/shared/next-action-ui.ts +235 -0
  140. package/src/resources/extensions/shared/progress-widget.ts +282 -0
  141. package/src/resources/extensions/shared/thinking-widget.ts +107 -0
  142. package/src/resources/extensions/shared/ui.ts +400 -0
  143. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  144. package/src/resources/extensions/slash-commands/audit.ts +92 -0
  145. package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
  146. package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
  147. package/src/resources/extensions/slash-commands/index.ts +12 -0
  148. package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
  149. package/src/resources/extensions/subagent/agents.ts +126 -0
  150. package/src/resources/extensions/subagent/index.ts +1293 -0
  151. package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
  152. package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  153. package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  154. package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  155. package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  156. package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  157. package/src/resources/skills/frontend-design/SKILL.md +45 -0
  158. package/src/resources/skills/swiftui/SKILL.md +208 -0
  159. package/src/resources/skills/swiftui/references/animations.md +921 -0
  160. package/src/resources/skills/swiftui/references/architecture.md +1561 -0
  161. package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
  162. package/src/resources/skills/swiftui/references/navigation.md +1492 -0
  163. package/src/resources/skills/swiftui/references/networking-async.md +214 -0
  164. package/src/resources/skills/swiftui/references/performance.md +1706 -0
  165. package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
  166. package/src/resources/skills/swiftui/references/state-management.md +1443 -0
  167. package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
  168. package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
  169. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  170. package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
  171. package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  172. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  173. package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  174. package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
  175. package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
  176. package/dist/commands/task.d.ts +0 -9
  177. package/dist/commands/task.d.ts.map +0 -1
  178. package/dist/commands/task.js +0 -129
  179. package/dist/commands/task.js.map +0 -1
  180. package/dist/commands/task.test.d.ts +0 -2
  181. package/dist/commands/task.test.d.ts.map +0 -1
  182. package/dist/commands/task.test.js +0 -169
  183. package/dist/commands/task.test.js.map +0 -1
  184. package/dist/e2e/task-e2e.test.d.ts +0 -2
  185. package/dist/e2e/task-e2e.test.d.ts.map +0 -1
  186. package/dist/e2e/task-e2e.test.js +0 -173
  187. package/dist/e2e/task-e2e.test.js.map +0 -1
  188. package/dist/index.d.ts +0 -3
  189. package/dist/index.d.ts.map +0 -1
  190. package/dist/index.js +0 -93
  191. package/dist/index.js.map +0 -1
  192. package/dist/slug.d.ts +0 -2
  193. package/dist/slug.d.ts.map +0 -1
  194. package/dist/slug.js +0 -12
  195. package/dist/slug.js.map +0 -1
  196. package/dist/slug.test.d.ts +0 -2
  197. package/dist/slug.test.d.ts.map +0 -1
  198. package/dist/slug.test.js +0 -32
  199. package/dist/slug.test.js.map +0 -1
@@ -0,0 +1,778 @@
1
+ /**
2
+ * GitHub Extension — /gh
3
+ *
4
+ * Full-suite GitHub issues and PR tracker/helper for pi.
5
+ * Provides LLM tools + /gh slash command for managing issues, PRs,
6
+ * reviews, labels, milestones, and comments.
7
+ *
8
+ * Auth: gh CLI (preferred) → GITHUB_TOKEN env var (fallback)
9
+ *
10
+ * Tools:
11
+ * github_issues — list, view, create, update, close, search issues
12
+ * github_prs — list, view, create, update, diff, files, checks for PRs
13
+ * github_comments — list, add comments on issues/PRs
14
+ * github_reviews — list, create reviews, request reviewers
15
+ * github_labels — list, create labels; list, create milestones
16
+ *
17
+ * Commands:
18
+ * /gh issues [state] — browse issues
19
+ * /gh prs [state] — browse PRs
20
+ * /gh view <number> — view issue or PR detail
21
+ * /gh create issue — create issue interactively
22
+ * /gh create pr — create PR from current branch
23
+ * /gh labels — list labels
24
+ * /gh milestones — list milestones
25
+ * /gh status — show auth + repo status
26
+ */
27
+
28
+ import { Type } from "@sinclair/typebox";
29
+ import { StringEnum } from "@mariozechner/pi-ai";
30
+ import { Text } from "@mariozechner/pi-tui";
31
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
32
+ import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@mariozechner/pi-coding-agent";
33
+ import { showConfirm } from "../shared/confirm-ui.js";
34
+
35
+ import {
36
+ isAuthenticated,
37
+ authMethod,
38
+ detectRepo,
39
+ getCurrentBranch,
40
+ getDefaultBranch,
41
+ type RepoInfo,
42
+ listIssues,
43
+ getIssue,
44
+ createIssue,
45
+ updateIssue,
46
+ addComment,
47
+ listComments,
48
+ listPullRequests,
49
+ getPullRequest,
50
+ createPullRequest,
51
+ updatePullRequest,
52
+ getPullRequestDiff,
53
+ listPullRequestFiles,
54
+ listReviews,
55
+ createReview,
56
+ requestReviewers,
57
+ listCheckRuns,
58
+ listLabels,
59
+ createLabel,
60
+ listMilestones,
61
+ createMilestone,
62
+ searchIssues,
63
+ } from "./gh-api.js";
64
+
65
+ import {
66
+ formatIssueList,
67
+ formatIssueDetail,
68
+ formatPRList,
69
+ formatPRDetail,
70
+ formatCommentList,
71
+ formatReviewList,
72
+ formatFileChanges,
73
+ formatLabelList,
74
+ formatMilestoneList,
75
+ } from "./formatters.js";
76
+
77
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
78
+
79
+ function requireRepo(cwd: string): RepoInfo {
80
+ const repo = detectRepo(cwd);
81
+ if (!repo) throw new Error("Not in a GitHub repository. Run this from a git repo with a GitHub remote.");
82
+ return repo;
83
+ }
84
+
85
+ function requireAuth(): void {
86
+ if (!isAuthenticated()) {
87
+ throw new Error("Not authenticated to GitHub. Install and authenticate `gh` CLI, or set GITHUB_TOKEN env var.");
88
+ }
89
+ }
90
+
91
+ function truncateOutput(text: string): string {
92
+ const result = truncateHead(text, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
93
+ if (result.truncated) {
94
+ return result.content + `\n\n[Output truncated: showing ${result.outputLines}/${result.totalLines} lines]`;
95
+ }
96
+ return result.content;
97
+ }
98
+
99
+ function textResult(text: string, details?: Record<string, unknown>) {
100
+ return {
101
+ content: [{ type: "text" as const, text: truncateOutput(text) }],
102
+ ...(details ? { details } : {}),
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Confirmation gate for outward-facing GitHub actions.
108
+ * Shows a themed yes/no confirmation in interactive mode.
109
+ * In non-interactive mode (no UI), blocks the action.
110
+ * Returns the rejected textResult if denied, or undefined if confirmed.
111
+ */
112
+ async function confirmAction(
113
+ ctx: ExtensionContext,
114
+ action: string,
115
+ ): Promise<ReturnType<typeof textResult> | undefined> {
116
+ if (!ctx.hasUI) {
117
+ return textResult(`Blocked: "${action}" requires user confirmation but no UI is available.`);
118
+ }
119
+ const confirmed = await showConfirm(ctx, {
120
+ title: "GitHub",
121
+ message: action,
122
+ });
123
+ if (!confirmed) {
124
+ return textResult(`Cancelled: user declined "${action}".`);
125
+ }
126
+ return undefined;
127
+ }
128
+
129
+ // ─── Extension ────────────────────────────────────────────────────────────────
130
+
131
+ export default function (pi: ExtensionAPI) {
132
+ // ─── Tool: github_issues ────────────────────────────────────────────────
133
+
134
+ pi.registerTool({
135
+ name: "github_issues",
136
+ label: "GitHub Issues",
137
+ description: "Manage GitHub issues: list, view, create, update, close, reopen, or search issues in the current repository.",
138
+ promptSnippet: "List, view, create, update, close, reopen, or search GitHub issues",
139
+ promptGuidelines: [
140
+ "Use github_issues to interact with GitHub issues instead of running `gh` CLI commands directly.",
141
+ "When listing issues, default to state='open' and include relevant filters like labels or assignee.",
142
+ "When searching, use GitHub search syntax in the query (e.g., 'is:open label:bug').",
143
+ "Mutating actions (create, update, close, reopen) require user confirmation before executing.",
144
+ ],
145
+ parameters: Type.Object({
146
+ action: StringEnum(["list", "view", "create", "update", "close", "reopen", "search"] as const),
147
+ number: Type.Optional(Type.Number({ description: "Issue number (for view/update/close/reopen)" })),
148
+ title: Type.Optional(Type.String({ description: "Issue title (for create)" })),
149
+ body: Type.Optional(Type.String({ description: "Issue body (for create/update)" })),
150
+ labels: Type.Optional(Type.String({ description: "Comma-separated labels (for list filter or create/update)" })),
151
+ assignee: Type.Optional(Type.String({ description: "Assignee username (for list filter or create/update)" })),
152
+ assignees: Type.Optional(Type.String({ description: "Comma-separated assignees (for create/update)" })),
153
+ milestone: Type.Optional(Type.String({ description: "Milestone number or title (for list filter)" })),
154
+ state: Type.Optional(StringEnum(["open", "closed", "all"] as const)),
155
+ query: Type.Optional(Type.String({ description: "Search query using GitHub search syntax (for search action)" })),
156
+ per_page: Type.Optional(Type.Number({ description: "Results per page (default 30, max 100)" })),
157
+ }),
158
+
159
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
160
+ requireAuth();
161
+ const repo = requireRepo(ctx.cwd);
162
+
163
+ switch (params.action) {
164
+ case "list": {
165
+ const issues = await listIssues(repo, {
166
+ state: params.state,
167
+ labels: params.labels,
168
+ assignee: params.assignee,
169
+ milestone: params.milestone,
170
+ per_page: params.per_page,
171
+ });
172
+ return textResult(
173
+ `Issues in ${repo.fullName} (${params.state ?? "open"}):\n\n${formatIssueList(issues)}`,
174
+ { issues: issues.map((i) => ({ number: i.number, title: i.title, state: i.state })) },
175
+ );
176
+ }
177
+ case "view": {
178
+ if (!params.number) return textResult("Error: 'number' is required for view action.");
179
+ const issue = await getIssue(repo, params.number);
180
+ const comments = await listComments(repo, params.number);
181
+ let text = formatIssueDetail(issue);
182
+ if (comments.length) {
183
+ text += `\n\n## Comments (${comments.length})\n\n${formatCommentList(comments)}`;
184
+ }
185
+ return textResult(text, { issue: { number: issue.number, title: issue.title, state: issue.state } });
186
+ }
187
+ case "create": {
188
+ if (!params.title) return textResult("Error: 'title' is required for create action.");
189
+ const createGate = await confirmAction(ctx, `Create issue "${params.title}"?`);
190
+ if (createGate) return createGate;
191
+ const newIssue = await createIssue(repo, {
192
+ title: params.title,
193
+ body: params.body,
194
+ labels: params.labels?.split(",").map((l) => l.trim()),
195
+ assignees: params.assignees?.split(",").map((a) => a.trim()),
196
+ });
197
+ return textResult(
198
+ `Created issue #${newIssue.number}: ${newIssue.title}\n${newIssue.html_url}`,
199
+ { issue: { number: newIssue.number, title: newIssue.title } },
200
+ );
201
+ }
202
+ case "update": {
203
+ if (!params.number) return textResult("Error: 'number' is required for update action.");
204
+ const updateGate = await confirmAction(ctx, `Update issue #${params.number}?`);
205
+ if (updateGate) return updateGate;
206
+ const updated = await updateIssue(repo, params.number, {
207
+ title: params.title,
208
+ body: params.body,
209
+ labels: params.labels?.split(",").map((l) => l.trim()),
210
+ assignees: params.assignees?.split(",").map((a) => a.trim()),
211
+ });
212
+ return textResult(
213
+ `Updated issue #${updated.number}: ${updated.title}\n${updated.html_url}`,
214
+ { issue: { number: updated.number, title: updated.title } },
215
+ );
216
+ }
217
+ case "close": {
218
+ if (!params.number) return textResult("Error: 'number' is required for close action.");
219
+ const closeGate = await confirmAction(ctx, `Close issue #${params.number}?`);
220
+ if (closeGate) return closeGate;
221
+ const closed = await updateIssue(repo, params.number, { state: "closed" });
222
+ return textResult(`Closed issue #${closed.number}: ${closed.title}`, { issue: { number: closed.number } });
223
+ }
224
+ case "reopen": {
225
+ if (!params.number) return textResult("Error: 'number' is required for reopen action.");
226
+ const reopenGate = await confirmAction(ctx, `Reopen issue #${params.number}?`);
227
+ if (reopenGate) return reopenGate;
228
+ const reopened = await updateIssue(repo, params.number, { state: "open" });
229
+ return textResult(`Reopened issue #${reopened.number}: ${reopened.title}`, { issue: { number: reopened.number } });
230
+ }
231
+ case "search": {
232
+ if (!params.query) return textResult("Error: 'query' is required for search action.");
233
+ const q = `repo:${repo.fullName} ${params.query}`;
234
+ const results = await searchIssues(q, { per_page: params.per_page });
235
+ const issuesOnly = results.items.filter((i) => !i.pull_request);
236
+ return textResult(
237
+ `Search results (${results.total_count} total, showing ${issuesOnly.length}):\n\n${formatIssueList(issuesOnly)}`,
238
+ { total: results.total_count },
239
+ );
240
+ }
241
+ default:
242
+ return textResult(`Unknown action: ${params.action}`);
243
+ }
244
+ },
245
+
246
+ renderCall(args, theme) {
247
+ let text = theme.fg("toolTitle", theme.bold("github_issues "));
248
+ text += theme.fg("muted", `${args.action ?? "?"}`);
249
+ if (args.number) text += theme.fg("accent", ` #${args.number}`);
250
+ if (args.title) text += theme.fg("dim", ` "${args.title}"`);
251
+ if (args.query) text += theme.fg("dim", ` "${args.query}"`);
252
+ return new Text(text, 0, 0);
253
+ },
254
+
255
+ renderResult(result, { expanded, isPartial }, theme) {
256
+ if (isPartial) return new Text(theme.fg("warning", "Fetching from GitHub..."), 0, 0);
257
+ const content = result.content?.[0]?.type === "text" ? result.content[0].text : "";
258
+ if (!expanded) {
259
+ const firstLine = content.split("\n")[0] ?? "";
260
+ return new Text(theme.fg("success", "✓ ") + firstLine, 0, 0);
261
+ }
262
+ return new Text(content, 0, 0);
263
+ },
264
+ });
265
+
266
+ // ─── Tool: github_prs ───────────────────────────────────────────────────
267
+
268
+ pi.registerTool({
269
+ name: "github_prs",
270
+ label: "GitHub PRs",
271
+ description: "Manage GitHub pull requests: list, view, create, update, get diff, list files, and check CI status.",
272
+ promptSnippet: "List, view, create, update, diff, files, and checks for GitHub pull requests",
273
+ promptGuidelines: [
274
+ "Use github_prs to interact with GitHub pull requests instead of running `gh` CLI commands directly.",
275
+ "Use action='diff' to see the actual code changes in a PR.",
276
+ "Use action='files' for a summary of changed files without the full diff.",
277
+ "Use action='checks' to see CI/CD status for a PR.",
278
+ "Mutating actions (create, update) require user confirmation before executing.",
279
+ ],
280
+ parameters: Type.Object({
281
+ action: StringEnum(["list", "view", "create", "update", "diff", "files", "checks"] as const),
282
+ number: Type.Optional(Type.Number({ description: "PR number (for view/update/diff/files/checks)" })),
283
+ title: Type.Optional(Type.String({ description: "PR title (for create)" })),
284
+ body: Type.Optional(Type.String({ description: "PR body (for create/update)" })),
285
+ head: Type.Optional(Type.String({ description: "Head branch (for create, defaults to current branch)" })),
286
+ base: Type.Optional(Type.String({ description: "Base branch (for create, defaults to repo default branch)" })),
287
+ draft: Type.Optional(Type.Boolean({ description: "Create as draft PR (for create)" })),
288
+ state: Type.Optional(StringEnum(["open", "closed", "all"] as const)),
289
+ per_page: Type.Optional(Type.Number({ description: "Results per page (default 30, max 100)" })),
290
+ }),
291
+
292
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
293
+ requireAuth();
294
+ const repo = requireRepo(ctx.cwd);
295
+
296
+ switch (params.action) {
297
+ case "list": {
298
+ const prs = await listPullRequests(repo, {
299
+ state: params.state,
300
+ per_page: params.per_page,
301
+ });
302
+ return textResult(
303
+ `Pull requests in ${repo.fullName} (${params.state ?? "open"}):\n\n${formatPRList(prs)}`,
304
+ { prs: prs.map((p) => ({ number: p.number, title: p.title, state: p.state, draft: p.draft })) },
305
+ );
306
+ }
307
+ case "view": {
308
+ if (!params.number) return textResult("Error: 'number' is required for view action.");
309
+ const pr = await getPullRequest(repo, params.number);
310
+ const reviews = await listReviews(repo, params.number);
311
+ let text = formatPRDetail(pr);
312
+ if (reviews.length) {
313
+ text += `\n\n## Reviews (${reviews.length})\n\n${formatReviewList(reviews)}`;
314
+ }
315
+ return textResult(text, { pr: { number: pr.number, title: pr.title, state: pr.state } });
316
+ }
317
+ case "create": {
318
+ if (!params.title) return textResult("Error: 'title' is required for create action.");
319
+ const head = params.head ?? getCurrentBranch(ctx.cwd);
320
+ if (!head) return textResult("Error: Could not determine current branch. Provide 'head' parameter.");
321
+ const base = params.base ?? getDefaultBranch(ctx.cwd);
322
+ const createPRGate = await confirmAction(ctx, `Create PR "${params.title}" (${head} → ${base})?`);
323
+ if (createPRGate) return createPRGate;
324
+ const newPR = await createPullRequest(repo, {
325
+ title: params.title,
326
+ body: params.body,
327
+ head,
328
+ base,
329
+ draft: params.draft,
330
+ });
331
+ return textResult(
332
+ `Created PR #${newPR.number}: ${newPR.title}\n${newPR.head.ref} → ${newPR.base.ref}\n${newPR.html_url}`,
333
+ { pr: { number: newPR.number, title: newPR.title } },
334
+ );
335
+ }
336
+ case "update": {
337
+ if (!params.number) return textResult("Error: 'number' is required for update action.");
338
+ const updatePRGate = await confirmAction(ctx, `Update PR #${params.number}?`);
339
+ if (updatePRGate) return updatePRGate;
340
+ const updated = await updatePullRequest(repo, params.number, {
341
+ title: params.title,
342
+ body: params.body,
343
+ base: params.base,
344
+ });
345
+ return textResult(
346
+ `Updated PR #${updated.number}: ${updated.title}\n${updated.html_url}`,
347
+ { pr: { number: updated.number, title: updated.title } },
348
+ );
349
+ }
350
+ case "diff": {
351
+ if (!params.number) return textResult("Error: 'number' is required for diff action.");
352
+ const diff = await getPullRequestDiff(repo, params.number);
353
+ return textResult(`Diff for PR #${params.number}:\n\n${diff}`);
354
+ }
355
+ case "files": {
356
+ if (!params.number) return textResult("Error: 'number' is required for files action.");
357
+ const files = await listPullRequestFiles(repo, params.number);
358
+ return textResult(
359
+ `Changed files in PR #${params.number}:\n\n${formatFileChanges(files)}`,
360
+ { files: files.map((f) => ({ filename: f.filename, status: f.status })) },
361
+ );
362
+ }
363
+ case "checks": {
364
+ if (!params.number) return textResult("Error: 'number' is required for checks action.");
365
+ const pr = await getPullRequest(repo, params.number);
366
+ const checks = await listCheckRuns(repo, pr.head.sha);
367
+ if (!checks.check_runs.length) {
368
+ return textResult(`No CI checks found for PR #${params.number}.`);
369
+ }
370
+ const lines = checks.check_runs.map((c) => {
371
+ const icon = c.conclusion === "success" ? "✓" : c.conclusion === "failure" ? "✗" : c.status === "in_progress" ? "⟳" : "…";
372
+ return `${icon} ${c.name}: ${c.conclusion ?? c.status}`;
373
+ });
374
+ return textResult(
375
+ `CI checks for PR #${params.number}:\n\n${lines.join("\n")}`,
376
+ { checks: checks.check_runs.map((c) => ({ name: c.name, conclusion: c.conclusion, status: c.status })) },
377
+ );
378
+ }
379
+ default:
380
+ return textResult(`Unknown action: ${params.action}`);
381
+ }
382
+ },
383
+
384
+ renderCall(args, theme) {
385
+ let text = theme.fg("toolTitle", theme.bold("github_prs "));
386
+ text += theme.fg("muted", `${args.action ?? "?"}`);
387
+ if (args.number) text += theme.fg("accent", ` #${args.number}`);
388
+ if (args.title) text += theme.fg("dim", ` "${args.title}"`);
389
+ return new Text(text, 0, 0);
390
+ },
391
+
392
+ renderResult(result, { expanded, isPartial }, theme) {
393
+ if (isPartial) return new Text(theme.fg("warning", "Fetching from GitHub..."), 0, 0);
394
+ const content = result.content?.[0]?.type === "text" ? result.content[0].text : "";
395
+ if (!expanded) {
396
+ const firstLine = content.split("\n")[0] ?? "";
397
+ return new Text(theme.fg("success", "✓ ") + firstLine, 0, 0);
398
+ }
399
+ return new Text(content, 0, 0);
400
+ },
401
+ });
402
+
403
+ // ─── Tool: github_comments ──────────────────────────────────────────────
404
+
405
+ pi.registerTool({
406
+ name: "github_comments",
407
+ label: "GitHub Comments",
408
+ description: "List or add comments on GitHub issues and pull requests.",
409
+ promptSnippet: "List or add comments on GitHub issues and PRs",
410
+ parameters: Type.Object({
411
+ action: StringEnum(["list", "add"] as const),
412
+ number: Type.Number({ description: "Issue or PR number" }),
413
+ body: Type.Optional(Type.String({ description: "Comment body text (for add)" })),
414
+ }),
415
+
416
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
417
+ requireAuth();
418
+ const repo = requireRepo(ctx.cwd);
419
+
420
+ switch (params.action) {
421
+ case "list": {
422
+ const comments = await listComments(repo, params.number);
423
+ return textResult(
424
+ `Comments on #${params.number} (${comments.length}):\n\n${formatCommentList(comments)}`,
425
+ { count: comments.length },
426
+ );
427
+ }
428
+ case "add": {
429
+ if (!params.body) return textResult("Error: 'body' is required for add action.");
430
+ const addGate = await confirmAction(ctx, `Add comment on #${params.number}?`);
431
+ if (addGate) return addGate;
432
+ const comment = await addComment(repo, params.number, params.body);
433
+ return textResult(
434
+ `Added comment on #${params.number}: ${comment.html_url}`,
435
+ { comment: { id: comment.id } },
436
+ );
437
+ }
438
+ default:
439
+ return textResult(`Unknown action: ${params.action}`);
440
+ }
441
+ },
442
+
443
+ renderCall(args, theme) {
444
+ let text = theme.fg("toolTitle", theme.bold("github_comments "));
445
+ text += theme.fg("muted", `${args.action ?? "?"}`);
446
+ text += theme.fg("accent", ` #${args.number ?? "?"}`);
447
+ return new Text(text, 0, 0);
448
+ },
449
+
450
+ renderResult(result, { expanded, isPartial }, theme) {
451
+ if (isPartial) return new Text(theme.fg("warning", "Fetching..."), 0, 0);
452
+ const content = result.content?.[0]?.type === "text" ? result.content[0].text : "";
453
+ if (!expanded) {
454
+ const firstLine = content.split("\n")[0] ?? "";
455
+ return new Text(theme.fg("success", "✓ ") + firstLine, 0, 0);
456
+ }
457
+ return new Text(content, 0, 0);
458
+ },
459
+ });
460
+
461
+ // ─── Tool: github_reviews ───────────────────────────────────────────────
462
+
463
+ pi.registerTool({
464
+ name: "github_reviews",
465
+ label: "GitHub Reviews",
466
+ description: "Manage GitHub PR reviews: list reviews, submit a review (approve/request changes/comment), or request reviewers.",
467
+ promptSnippet: "List reviews, submit reviews, or request reviewers on GitHub PRs",
468
+ promptGuidelines: [
469
+ "Use event='APPROVE' to approve, 'REQUEST_CHANGES' to request changes, 'COMMENT' for a general review comment.",
470
+ "Use action='request_reviewers' to assign reviewers to a PR.",
471
+ ],
472
+ parameters: Type.Object({
473
+ action: StringEnum(["list", "submit", "request_reviewers"] as const),
474
+ number: Type.Number({ description: "PR number" }),
475
+ body: Type.Optional(Type.String({ description: "Review body text (for submit)" })),
476
+ event: Type.Optional(StringEnum(["APPROVE", "REQUEST_CHANGES", "COMMENT"] as const)),
477
+ reviewers: Type.Optional(Type.String({ description: "Comma-separated reviewer usernames (for request_reviewers)" })),
478
+ }),
479
+
480
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
481
+ requireAuth();
482
+ const repo = requireRepo(ctx.cwd);
483
+
484
+ switch (params.action) {
485
+ case "list": {
486
+ const reviews = await listReviews(repo, params.number);
487
+ return textResult(
488
+ `Reviews on PR #${params.number} (${reviews.length}):\n\n${formatReviewList(reviews)}`,
489
+ { count: reviews.length },
490
+ );
491
+ }
492
+ case "submit": {
493
+ if (!params.event) return textResult("Error: 'event' is required for submit action (APPROVE, REQUEST_CHANGES, or COMMENT).");
494
+ const submitGate = await confirmAction(ctx, `Submit ${params.event} review on PR #${params.number}?`);
495
+ if (submitGate) return submitGate;
496
+ const review = await createReview(repo, params.number, {
497
+ body: params.body,
498
+ event: params.event,
499
+ });
500
+ return textResult(
501
+ `Submitted review on PR #${params.number}: ${review.state}\n${review.html_url}`,
502
+ { review: { id: review.id, state: review.state } },
503
+ );
504
+ }
505
+ case "request_reviewers": {
506
+ if (!params.reviewers) return textResult("Error: 'reviewers' is required for request_reviewers action.");
507
+ const reviewerList = params.reviewers.split(",").map((r) => r.trim());
508
+ const reviewersGate = await confirmAction(ctx, `Request reviewers on PR #${params.number}: ${reviewerList.join(", ")}?`);
509
+ if (reviewersGate) return reviewersGate;
510
+ await requestReviewers(repo, params.number, reviewerList);
511
+ return textResult(
512
+ `Requested reviewers on PR #${params.number}: ${reviewerList.join(", ")}`,
513
+ { reviewers: reviewerList },
514
+ );
515
+ }
516
+ default:
517
+ return textResult(`Unknown action: ${params.action}`);
518
+ }
519
+ },
520
+
521
+ renderCall(args, theme) {
522
+ let text = theme.fg("toolTitle", theme.bold("github_reviews "));
523
+ text += theme.fg("muted", `${args.action ?? "?"}`);
524
+ text += theme.fg("accent", ` #${args.number ?? "?"}`);
525
+ if (args.event) text += theme.fg("dim", ` ${args.event}`);
526
+ return new Text(text, 0, 0);
527
+ },
528
+
529
+ renderResult(result, { expanded, isPartial }, theme) {
530
+ if (isPartial) return new Text(theme.fg("warning", "Processing..."), 0, 0);
531
+ const content = result.content?.[0]?.type === "text" ? result.content[0].text : "";
532
+ if (!expanded) {
533
+ const firstLine = content.split("\n")[0] ?? "";
534
+ return new Text(theme.fg("success", "✓ ") + firstLine, 0, 0);
535
+ }
536
+ return new Text(content, 0, 0);
537
+ },
538
+ });
539
+
540
+ // ─── Tool: github_labels ────────────────────────────────────────────────
541
+
542
+ pi.registerTool({
543
+ name: "github_labels",
544
+ label: "GitHub Labels",
545
+ description: "Manage GitHub labels and milestones: list/create labels, list/create milestones.",
546
+ promptSnippet: "List or create GitHub labels and milestones",
547
+ parameters: Type.Object({
548
+ action: StringEnum(["list_labels", "create_label", "list_milestones", "create_milestone"] as const),
549
+ name: Type.Optional(Type.String({ description: "Label or milestone name (for create)" })),
550
+ color: Type.Optional(Type.String({ description: "Label hex color without # (for create_label, e.g. 'ff0000')" })),
551
+ description: Type.Optional(Type.String({ description: "Description (for create)" })),
552
+ due_on: Type.Optional(Type.String({ description: "Milestone due date ISO 8601 (for create_milestone, e.g. '2025-12-31T00:00:00Z')" })),
553
+ }),
554
+
555
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
556
+ requireAuth();
557
+ const repo = requireRepo(ctx.cwd);
558
+
559
+ switch (params.action) {
560
+ case "list_labels": {
561
+ const labels = await listLabels(repo);
562
+ return textResult(`Labels in ${repo.fullName}:\n\n${formatLabelList(labels)}`, { count: labels.length });
563
+ }
564
+ case "create_label": {
565
+ if (!params.name) return textResult("Error: 'name' is required for create_label.");
566
+ const labelGate = await confirmAction(ctx, `Create label "${params.name}"?`);
567
+ if (labelGate) return labelGate;
568
+ const label = await createLabel(repo, {
569
+ name: params.name,
570
+ color: params.color ?? "ededed",
571
+ description: params.description,
572
+ });
573
+ return textResult(`Created label: ${label.name} (#${label.color})`, { label: { name: label.name } });
574
+ }
575
+ case "list_milestones": {
576
+ const milestones = await listMilestones(repo);
577
+ return textResult(`Milestones in ${repo.fullName}:\n\n${formatMilestoneList(milestones)}`, { count: milestones.length });
578
+ }
579
+ case "create_milestone": {
580
+ if (!params.name) return textResult("Error: 'name' is required for create_milestone.");
581
+ const milestoneGate = await confirmAction(ctx, `Create milestone "${params.name}"?`);
582
+ if (milestoneGate) return milestoneGate;
583
+ const ms = await createMilestone(repo, {
584
+ title: params.name,
585
+ description: params.description,
586
+ due_on: params.due_on,
587
+ });
588
+ return textResult(`Created milestone: ${ms.title} (#${ms.number})`, { milestone: { number: ms.number, title: ms.title } });
589
+ }
590
+ default:
591
+ return textResult(`Unknown action: ${params.action}`);
592
+ }
593
+ },
594
+
595
+ renderCall(args, theme) {
596
+ let text = theme.fg("toolTitle", theme.bold("github_labels "));
597
+ text += theme.fg("muted", `${args.action ?? "?"}`);
598
+ if (args.name) text += theme.fg("dim", ` "${args.name}"`);
599
+ return new Text(text, 0, 0);
600
+ },
601
+
602
+ renderResult(result, { expanded, isPartial }, theme) {
603
+ if (isPartial) return new Text(theme.fg("warning", "Fetching..."), 0, 0);
604
+ const content = result.content?.[0]?.type === "text" ? result.content[0].text : "";
605
+ if (!expanded) {
606
+ const firstLine = content.split("\n")[0] ?? "";
607
+ return new Text(theme.fg("success", "✓ ") + firstLine, 0, 0);
608
+ }
609
+ return new Text(content, 0, 0);
610
+ },
611
+ });
612
+
613
+ // ─── Slash command: /gh ──────────────────────────────────────────────────
614
+
615
+ pi.registerCommand("gh", {
616
+ description: "GitHub helper: /gh issues|prs|view|create|labels|milestones|status",
617
+
618
+ getArgumentCompletions: (prefix: string) => {
619
+ const subcommands = ["issues", "prs", "view", "create", "labels", "milestones", "status"];
620
+ const parts = prefix.trim().split(/\s+/);
621
+
622
+ if (parts.length <= 1) {
623
+ return subcommands
624
+ .filter((cmd) => cmd.startsWith(parts[0] ?? ""))
625
+ .map((cmd) => ({ value: cmd, label: cmd }));
626
+ }
627
+
628
+ if (parts[0] === "issues" || parts[0] === "prs") {
629
+ const states = ["open", "closed", "all"];
630
+ const statePrefix = parts[1] ?? "";
631
+ return states
632
+ .filter((s) => s.startsWith(statePrefix))
633
+ .map((s) => ({ value: `${parts[0]} ${s}`, label: s }));
634
+ }
635
+
636
+ if (parts[0] === "create") {
637
+ const types = ["issue", "pr"];
638
+ const typePrefix = parts[1] ?? "";
639
+ return types
640
+ .filter((t) => t.startsWith(typePrefix))
641
+ .map((t) => ({ value: `create ${t}`, label: t }));
642
+ }
643
+
644
+ return [];
645
+ },
646
+
647
+ handler: async (args, ctx) => {
648
+ const parts = args.trim().split(/\s+/);
649
+ const sub = parts[0];
650
+ const rest = parts.slice(1).join(" ");
651
+
652
+ if (!isAuthenticated()) {
653
+ ctx.ui.notify("Not authenticated to GitHub. Install `gh` CLI or set GITHUB_TOKEN.", "error");
654
+ return;
655
+ }
656
+
657
+ const repo = detectRepo(ctx.cwd);
658
+ if (!repo && sub !== "status") {
659
+ ctx.ui.notify("Not in a GitHub repository.", "error");
660
+ return;
661
+ }
662
+
663
+ try {
664
+ switch (sub) {
665
+ case "issues": {
666
+ const state = (rest as "open" | "closed" | "all") || "open";
667
+ const issues = await listIssues(repo!, { state });
668
+ const display = `Issues in ${repo!.fullName} (${state}):\n\n${formatIssueList(issues)}`;
669
+ pi.sendMessage({ customType: "github", content: display, display: true });
670
+ break;
671
+ }
672
+ case "prs": {
673
+ const state = (rest as "open" | "closed" | "all") || "open";
674
+ const prs = await listPullRequests(repo!, { state });
675
+ const display = `Pull requests in ${repo!.fullName} (${state}):\n\n${formatPRList(prs)}`;
676
+ pi.sendMessage({ customType: "github", content: display, display: true });
677
+ break;
678
+ }
679
+ case "view": {
680
+ const num = parseInt(rest, 10);
681
+ if (isNaN(num)) {
682
+ ctx.ui.notify("Usage: /gh view <number>", "error");
683
+ return;
684
+ }
685
+ // Try as issue first, then PR
686
+ try {
687
+ const issue = await getIssue(repo!, num);
688
+ if (issue.pull_request) {
689
+ // It's a PR
690
+ const pr = await getPullRequest(repo!, num);
691
+ const reviews = await listReviews(repo!, num);
692
+ let text = formatPRDetail(pr);
693
+ if (reviews.length) text += `\n\n## Reviews\n\n${formatReviewList(reviews)}`;
694
+ pi.sendMessage({ customType: "github", content: text, display: true });
695
+ } else {
696
+ const comments = await listComments(repo!, num);
697
+ let text = formatIssueDetail(issue);
698
+ if (comments.length) text += `\n\n## Comments\n\n${formatCommentList(comments)}`;
699
+ pi.sendMessage({ customType: "github", content: text, display: true });
700
+ }
701
+ } catch {
702
+ ctx.ui.notify(`Could not find issue or PR #${num}`, "error");
703
+ }
704
+ break;
705
+ }
706
+ case "create": {
707
+ const type = parts[1];
708
+ if (type === "issue") {
709
+ ctx.ui.notify("Use the agent to create an issue: tell it the title, description, and labels you want.", "info");
710
+ } else if (type === "pr") {
711
+ const branch = getCurrentBranch(ctx.cwd);
712
+ const base = getDefaultBranch(ctx.cwd);
713
+ ctx.ui.notify(
714
+ `Current branch: ${branch}\nBase: ${base}\n\nTell the agent the PR title and description to create it.`,
715
+ "info",
716
+ );
717
+ } else {
718
+ ctx.ui.notify("Usage: /gh create issue|pr", "error");
719
+ }
720
+ break;
721
+ }
722
+ case "labels": {
723
+ const labels = await listLabels(repo!);
724
+ pi.sendMessage({ customType: "github", content: `Labels in ${repo!.fullName}:\n\n${formatLabelList(labels)}`, display: true });
725
+ break;
726
+ }
727
+ case "milestones": {
728
+ const milestones = await listMilestones(repo!);
729
+ pi.sendMessage({
730
+ customType: "github",
731
+ content: `Milestones in ${repo!.fullName}:\n\n${formatMilestoneList(milestones)}`,
732
+ display: true,
733
+ });
734
+ break;
735
+ }
736
+ case "status": {
737
+ const auth = authMethod();
738
+ const repoStr = repo ? `${repo.fullName}` : "not detected";
739
+ const branch = repo ? getCurrentBranch(ctx.cwd) ?? "unknown" : "n/a";
740
+ const text = `GitHub Extension Status\n\nAuth: ${auth}\nRepo: ${repoStr}\nBranch: ${branch}`;
741
+ pi.sendMessage({ customType: "github", content: text, display: true });
742
+ break;
743
+ }
744
+ default:
745
+ ctx.ui.notify("Usage: /gh issues|prs|view|create|labels|milestones|status", "info");
746
+ }
747
+ } catch (e: unknown) {
748
+ const msg = e instanceof Error ? e.message : String(e);
749
+ ctx.ui.notify(`GitHub error: ${msg}`, "error");
750
+ }
751
+ },
752
+ });
753
+
754
+ // ─── Message renderer ───────────────────────────────────────────────────
755
+
756
+ pi.registerMessageRenderer("github", (message, _options, theme) => {
757
+ const content = message.content ?? "";
758
+ // Apply some light styling to the GitHub output
759
+ const styled = content
760
+ .replace(/^(# .+)$/gm, (m: string) => theme.fg("accent", theme.bold(m)))
761
+ .replace(/(●)/g, theme.fg("success", "$1"))
762
+ .replace(/(✓)/g, theme.fg("success", "$1"))
763
+ .replace(/(✗)/g, theme.fg("error", "$1"))
764
+ .replace(/(⊕)/g, theme.fg("accent", "$1"))
765
+ .replace(/(◇)/g, theme.fg("dim", "$1"))
766
+ .replace(/(https:\/\/github\.com\S+)/g, theme.fg("mdLink", "$1"));
767
+ return new Text(styled, 0, 0);
768
+ });
769
+
770
+ // ─── Session start notification ─────────────────────────────────────────
771
+
772
+ pi.on("session_start", async (_event, ctx) => {
773
+ const auth = authMethod();
774
+ if (auth === "none") {
775
+ ctx.ui.notify("GitHub extension: not authenticated. Install `gh` CLI or set GITHUB_TOKEN.", "warning");
776
+ }
777
+ });
778
+ }