@tuan_son.dinh/gsd 2.6.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 (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +453 -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 +269 -0
  7. package/dist/loader.d.ts +2 -0
  8. package/dist/loader.js +70 -0
  9. package/dist/logo.d.ts +16 -0
  10. package/dist/logo.js +25 -0
  11. package/dist/onboarding.d.ts +43 -0
  12. package/dist/onboarding.js +418 -0
  13. package/dist/pi-migration.d.ts +14 -0
  14. package/dist/pi-migration.js +57 -0
  15. package/dist/resource-loader.d.ts +22 -0
  16. package/dist/resource-loader.js +60 -0
  17. package/dist/tool-bootstrap.d.ts +4 -0
  18. package/dist/tool-bootstrap.js +74 -0
  19. package/dist/wizard.d.ts +7 -0
  20. package/dist/wizard.js +25 -0
  21. package/package.json +60 -0
  22. package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +108 -0
  23. package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
  24. package/pkg/dist/modes/interactive/theme/dark.json +85 -0
  25. package/pkg/dist/modes/interactive/theme/light.json +84 -0
  26. package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
  27. package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
  28. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  29. package/pkg/dist/modes/interactive/theme/theme.js +949 -0
  30. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
  31. package/pkg/package.json +8 -0
  32. package/scripts/postinstall.js +127 -0
  33. package/src/resources/GSD-WORKFLOW.md +661 -0
  34. package/src/resources/agents/researcher.md +29 -0
  35. package/src/resources/agents/scout.md +56 -0
  36. package/src/resources/agents/worker.md +31 -0
  37. package/src/resources/extensions/ask-user-questions.ts +249 -0
  38. package/src/resources/extensions/bg-shell/index.ts +2808 -0
  39. package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  40. package/src/resources/extensions/browser-tools/core.js +1057 -0
  41. package/src/resources/extensions/browser-tools/index.ts +4989 -0
  42. package/src/resources/extensions/browser-tools/package.json +20 -0
  43. package/src/resources/extensions/context7/index.ts +428 -0
  44. package/src/resources/extensions/context7/package.json +11 -0
  45. package/src/resources/extensions/get-secrets-from-user.ts +352 -0
  46. package/src/resources/extensions/google-search/index.ts +323 -0
  47. package/src/resources/extensions/google-search/package.json +9 -0
  48. package/src/resources/extensions/gsd/activity-log.ts +69 -0
  49. package/src/resources/extensions/gsd/auto.ts +2744 -0
  50. package/src/resources/extensions/gsd/commands.ts +313 -0
  51. package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
  52. package/src/resources/extensions/gsd/dashboard-overlay.ts +521 -0
  53. package/src/resources/extensions/gsd/docs/preferences-reference.md +176 -0
  54. package/src/resources/extensions/gsd/doctor.ts +690 -0
  55. package/src/resources/extensions/gsd/files.ts +732 -0
  56. package/src/resources/extensions/gsd/git-service.ts +597 -0
  57. package/src/resources/extensions/gsd/gitignore.ts +168 -0
  58. package/src/resources/extensions/gsd/guided-flow.ts +817 -0
  59. package/src/resources/extensions/gsd/index.ts +558 -0
  60. package/src/resources/extensions/gsd/metrics.ts +374 -0
  61. package/src/resources/extensions/gsd/migrate/command.ts +218 -0
  62. package/src/resources/extensions/gsd/migrate/index.ts +42 -0
  63. package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
  64. package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
  65. package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
  66. package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
  67. package/src/resources/extensions/gsd/migrate/types.ts +370 -0
  68. package/src/resources/extensions/gsd/migrate/validator.ts +55 -0
  69. package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
  70. package/src/resources/extensions/gsd/observability-validator.ts +408 -0
  71. package/src/resources/extensions/gsd/package.json +11 -0
  72. package/src/resources/extensions/gsd/paths.ts +308 -0
  73. package/src/resources/extensions/gsd/preferences.ts +757 -0
  74. package/src/resources/extensions/gsd/prompt-loader.ts +50 -0
  75. package/src/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
  76. package/src/resources/extensions/gsd/prompts/complete-slice.md +29 -0
  77. package/src/resources/extensions/gsd/prompts/discuss.md +189 -0
  78. package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
  79. package/src/resources/extensions/gsd/prompts/execute-task.md +61 -0
  80. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -0
  81. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -0
  82. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +59 -0
  83. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -0
  84. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +23 -0
  85. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -0
  86. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +11 -0
  87. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
  88. package/src/resources/extensions/gsd/prompts/plan-milestone.md +65 -0
  89. package/src/resources/extensions/gsd/prompts/plan-slice.md +51 -0
  90. package/src/resources/extensions/gsd/prompts/queue.md +85 -0
  91. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
  92. package/src/resources/extensions/gsd/prompts/replan-slice.md +39 -0
  93. package/src/resources/extensions/gsd/prompts/research-milestone.md +37 -0
  94. package/src/resources/extensions/gsd/prompts/research-slice.md +28 -0
  95. package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
  96. package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
  97. package/src/resources/extensions/gsd/prompts/system.md +187 -0
  98. package/src/resources/extensions/gsd/prompts/worktree-merge.md +123 -0
  99. package/src/resources/extensions/gsd/session-forensics.ts +487 -0
  100. package/src/resources/extensions/gsd/skill-discovery.ts +137 -0
  101. package/src/resources/extensions/gsd/state.ts +460 -0
  102. package/src/resources/extensions/gsd/templates/context.md +76 -0
  103. package/src/resources/extensions/gsd/templates/decisions.md +8 -0
  104. package/src/resources/extensions/gsd/templates/milestone-summary.md +73 -0
  105. package/src/resources/extensions/gsd/templates/plan.md +131 -0
  106. package/src/resources/extensions/gsd/templates/preferences.md +24 -0
  107. package/src/resources/extensions/gsd/templates/project.md +31 -0
  108. package/src/resources/extensions/gsd/templates/reassessment.md +28 -0
  109. package/src/resources/extensions/gsd/templates/requirements.md +81 -0
  110. package/src/resources/extensions/gsd/templates/research.md +46 -0
  111. package/src/resources/extensions/gsd/templates/roadmap.md +118 -0
  112. package/src/resources/extensions/gsd/templates/slice-context.md +58 -0
  113. package/src/resources/extensions/gsd/templates/slice-summary.md +99 -0
  114. package/src/resources/extensions/gsd/templates/state.md +19 -0
  115. package/src/resources/extensions/gsd/templates/task-plan.md +52 -0
  116. package/src/resources/extensions/gsd/templates/task-summary.md +57 -0
  117. package/src/resources/extensions/gsd/templates/uat.md +54 -0
  118. package/src/resources/extensions/gsd/tests/activity-log-prune.test.ts +327 -0
  119. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +56 -0
  120. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
  121. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +225 -0
  122. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +160 -0
  123. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +341 -0
  124. package/src/resources/extensions/gsd/tests/derive-state.test.ts +689 -0
  125. package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
  126. package/src/resources/extensions/gsd/tests/doctor.test.ts +505 -0
  127. package/src/resources/extensions/gsd/tests/git-service.test.ts +1313 -0
  128. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +308 -0
  129. package/src/resources/extensions/gsd/tests/metrics-io.test.ts +201 -0
  130. package/src/resources/extensions/gsd/tests/metrics.test.ts +217 -0
  131. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
  132. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
  133. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
  134. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
  135. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
  136. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
  137. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +309 -0
  138. package/src/resources/extensions/gsd/tests/parsers.test.ts +1351 -0
  139. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +163 -0
  140. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +386 -0
  141. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +171 -0
  142. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
  143. package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
  144. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +521 -0
  145. package/src/resources/extensions/gsd/tests/requirements.test.ts +125 -0
  146. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +34 -0
  147. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
  148. package/src/resources/extensions/gsd/tests/run-uat.test.ts +348 -0
  149. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +247 -0
  150. package/src/resources/extensions/gsd/tests/workflow-config.test.mjs +53 -0
  151. package/src/resources/extensions/gsd/tests/workspace-index.test.ts +94 -0
  152. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
  153. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
  154. package/src/resources/extensions/gsd/tests/worktree.test.ts +264 -0
  155. package/src/resources/extensions/gsd/types.ts +159 -0
  156. package/src/resources/extensions/gsd/unit-runtime.ts +184 -0
  157. package/src/resources/extensions/gsd/workspace-index.ts +203 -0
  158. package/src/resources/extensions/gsd/worktree-command.ts +845 -0
  159. package/src/resources/extensions/gsd/worktree-manager.ts +392 -0
  160. package/src/resources/extensions/gsd/worktree.ts +183 -0
  161. package/src/resources/extensions/mac-tools/index.ts +852 -0
  162. package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  163. package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  164. package/src/resources/extensions/mcporter/index.ts +429 -0
  165. package/src/resources/extensions/remote-questions/config.ts +81 -0
  166. package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
  167. package/src/resources/extensions/remote-questions/format.ts +163 -0
  168. package/src/resources/extensions/remote-questions/manager.ts +192 -0
  169. package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
  170. package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
  171. package/src/resources/extensions/remote-questions/status.ts +31 -0
  172. package/src/resources/extensions/remote-questions/store.ts +77 -0
  173. package/src/resources/extensions/remote-questions/types.ts +75 -0
  174. package/src/resources/extensions/search-the-web/cache.ts +78 -0
  175. package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
  176. package/src/resources/extensions/search-the-web/format.ts +258 -0
  177. package/src/resources/extensions/search-the-web/http.ts +238 -0
  178. package/src/resources/extensions/search-the-web/index.ts +65 -0
  179. package/src/resources/extensions/search-the-web/native-search.ts +157 -0
  180. package/src/resources/extensions/search-the-web/provider.ts +118 -0
  181. package/src/resources/extensions/search-the-web/tavily.ts +116 -0
  182. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
  183. package/src/resources/extensions/search-the-web/tool-llm-context.ts +561 -0
  184. package/src/resources/extensions/search-the-web/tool-search.ts +576 -0
  185. package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
  186. package/src/resources/extensions/shared/confirm-ui.ts +126 -0
  187. package/src/resources/extensions/shared/interview-ui.ts +613 -0
  188. package/src/resources/extensions/shared/next-action-ui.ts +197 -0
  189. package/src/resources/extensions/shared/progress-widget.ts +282 -0
  190. package/src/resources/extensions/shared/terminal.ts +23 -0
  191. package/src/resources/extensions/shared/thinking-widget.ts +107 -0
  192. package/src/resources/extensions/shared/ui.ts +400 -0
  193. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  194. package/src/resources/extensions/slash-commands/audit.ts +88 -0
  195. package/src/resources/extensions/slash-commands/clear.ts +10 -0
  196. package/src/resources/extensions/slash-commands/create-extension.ts +297 -0
  197. package/src/resources/extensions/slash-commands/create-slash-command.ts +234 -0
  198. package/src/resources/extensions/slash-commands/index.ts +12 -0
  199. package/src/resources/extensions/subagent/agents.ts +126 -0
  200. package/src/resources/extensions/subagent/index.ts +1020 -0
  201. package/src/resources/extensions/voice/index.ts +195 -0
  202. package/src/resources/extensions/voice/speech-recognizer.swift +154 -0
  203. package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
  204. package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  205. package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  206. package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  207. package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  208. package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  209. package/src/resources/skills/frontend-design/SKILL.md +45 -0
  210. package/src/resources/skills/swiftui/SKILL.md +208 -0
  211. package/src/resources/skills/swiftui/references/animations.md +921 -0
  212. package/src/resources/skills/swiftui/references/architecture.md +1561 -0
  213. package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
  214. package/src/resources/skills/swiftui/references/navigation.md +1492 -0
  215. package/src/resources/skills/swiftui/references/networking-async.md +214 -0
  216. package/src/resources/skills/swiftui/references/performance.md +1706 -0
  217. package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
  218. package/src/resources/skills/swiftui/references/state-management.md +1443 -0
  219. package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
  220. package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
  221. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  222. package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
  223. package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  224. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  225. package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  226. package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
  227. package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
@@ -0,0 +1,429 @@
1
+ /**
2
+ * MCPorter Extension — Lazy MCP server integration for pi
3
+ *
4
+ * Provides on-demand access to all MCP servers configured on the system
5
+ * (via Claude Desktop, Cursor, VS Code, mcporter config, etc.) without
6
+ * registering every tool upfront. This keeps token usage near-zero until
7
+ * the agent actually needs an MCP tool.
8
+ *
9
+ * Three tools:
10
+ * mcp_servers — List available MCP servers (cached after first call)
11
+ * mcp_discover — Get tool signatures for a specific server
12
+ * mcp_call — Call a tool on an MCP server
13
+ *
14
+ * Requirements:
15
+ * - mcporter installed globally: npm i -g mcporter
16
+ */
17
+
18
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
19
+ import {
20
+ truncateHead,
21
+ DEFAULT_MAX_BYTES,
22
+ DEFAULT_MAX_LINES,
23
+ formatSize,
24
+ } from "@mariozechner/pi-coding-agent";
25
+ import { Text } from "@mariozechner/pi-tui";
26
+ import { Type } from "@sinclair/typebox";
27
+ import { execFile, exec } from "node:child_process";
28
+ import { promisify } from "node:util";
29
+
30
+ const execFileAsync = promisify(execFile);
31
+ const execAsync = promisify(exec);
32
+
33
+ // ─── Types ────────────────────────────────────────────────────────────────────
34
+
35
+ interface McpServer {
36
+ name: string;
37
+ status: string;
38
+ transport?: string;
39
+ tools: { name: string; description: string }[];
40
+ }
41
+
42
+ interface McpListResponse {
43
+ mode: string;
44
+ counts: { ok: number; auth: number; offline: number; http: number; error: number };
45
+ servers: McpServer[];
46
+ }
47
+
48
+ interface McpToolSchema {
49
+ name: string;
50
+ description: string;
51
+ inputSchema?: Record<string, unknown>;
52
+ }
53
+
54
+ interface McpServerDetail {
55
+ name: string;
56
+ status: string;
57
+ tools: McpToolSchema[];
58
+ }
59
+
60
+ // ─── Cache ────────────────────────────────────────────────────────────────────
61
+
62
+ let serverListCache: McpServer[] | null = null;
63
+ const serverDetailCache = new Map<string, McpServerDetail>();
64
+
65
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
66
+
67
+ function escapeShellArg(arg: string): string {
68
+ if (process.platform === "win32") {
69
+ return `"${arg.replace(/"/g, '""')}"`;
70
+ }
71
+ return `'${arg.replace(/'/g, "'\\''")}'`;
72
+ }
73
+
74
+ async function runMcporter(
75
+ args: string[],
76
+ signal?: AbortSignal,
77
+ timeoutMs = 30000,
78
+ ): Promise<string> {
79
+ // Cross-platform: use execFile on Windows to avoid quote handling issues
80
+ // On Windows, cmd.exe doesn't strip single quotes like Unix shells do
81
+ if (process.platform === "win32") {
82
+ const { stdout } = await execFileAsync("mcporter", args, {
83
+ timeout: timeoutMs,
84
+ maxBuffer: 1024 * 1024,
85
+ signal,
86
+ env: { ...process.env },
87
+ shell: true,
88
+ });
89
+ return stdout;
90
+ }
91
+ // Use shell exec so PATH resolution works on Unix
92
+ const escaped = args.map((a) => escapeShellArg(a)).join(" ");
93
+ const { stdout } = await execAsync(`mcporter ${escaped}`, {
94
+ timeout: timeoutMs,
95
+ maxBuffer: 1024 * 1024,
96
+ signal,
97
+ env: { ...process.env },
98
+ });
99
+ return stdout;
100
+ }
101
+
102
+ async function getServerList(signal?: AbortSignal): Promise<McpServer[]> {
103
+ if (serverListCache) return serverListCache;
104
+
105
+ const raw = await runMcporter(["list", "--json"], signal, 60000);
106
+ let data: McpListResponse;
107
+ try {
108
+ data = JSON.parse(raw) as McpListResponse;
109
+ } catch (e) {
110
+ throw new Error(`Failed to parse mcporter output: ${raw.slice(0, 300)}`);
111
+ }
112
+ if (!Array.isArray(data.servers)) {
113
+ throw new Error(`Unexpected mcporter response shape: ${JSON.stringify(Object.keys(data))}`);
114
+ }
115
+ serverListCache = data.servers;
116
+ return serverListCache;
117
+ }
118
+
119
+ async function getServerDetail(
120
+ serverName: string,
121
+ signal?: AbortSignal,
122
+ ): Promise<McpServerDetail> {
123
+ if (serverDetailCache.has(serverName)) return serverDetailCache.get(serverName)!;
124
+
125
+ const raw = await runMcporter(["list", serverName, "--schema", "--json"], signal);
126
+ const data = JSON.parse(raw) as McpServerDetail;
127
+ serverDetailCache.set(serverName, data);
128
+ return data;
129
+ }
130
+
131
+ function formatServerList(servers: McpServer[]): string {
132
+ if (servers.length === 0) return "No MCP servers found.";
133
+
134
+ const lines: string[] = [`${servers.length} MCP servers available:\n`];
135
+
136
+ for (const s of servers) {
137
+ const tools = s.tools ?? [];
138
+ const status = s.status === "ok" ? "✓" : s.status === "auth" ? "🔑" : "✗";
139
+ lines.push(`${status} ${s.name} — ${tools.length} tools (${s.status})`);
140
+ for (const t of tools) {
141
+ lines.push(` ${t.name}: ${t.description?.slice(0, 100) ?? ""}`);
142
+ }
143
+ }
144
+
145
+ lines.push("\nUse mcp_discover to see full tool schemas for a specific server.");
146
+ lines.push("Use mcp_call to invoke a tool: mcp_call(server, tool, args).");
147
+ return lines.join("\n");
148
+ }
149
+
150
+ function formatServerDetail(detail: McpServerDetail): string {
151
+ const lines: string[] = [`${detail.name} — ${detail.tools.length} tools:\n`];
152
+
153
+ for (const tool of detail.tools) {
154
+ lines.push(`## ${tool.name}`);
155
+ if (tool.description) lines.push(tool.description);
156
+ if (tool.inputSchema) {
157
+ lines.push("```json");
158
+ lines.push(JSON.stringify(tool.inputSchema, null, 2));
159
+ lines.push("```");
160
+ }
161
+ lines.push("");
162
+ }
163
+
164
+ lines.push(`Call with: mcp_call(server="${detail.name}", tool="<tool_name>", args={...})`);
165
+ return lines.join("\n");
166
+ }
167
+
168
+ // ─── Extension ────────────────────────────────────────────────────────────────
169
+
170
+ export default function (pi: ExtensionAPI) {
171
+ // ── mcp_servers ──────────────────────────────────────────────────────────
172
+
173
+ pi.registerTool({
174
+ name: "mcp_servers",
175
+ label: "MCP Servers",
176
+ description:
177
+ "List all available MCP servers discovered from your system (Claude Desktop, Cursor, VS Code, mcporter config). " +
178
+ "Shows server names, status, and tool counts. Use mcp_discover to get full tool schemas for a server.",
179
+ promptSnippet:
180
+ "List available MCP servers and their tools (lazy discovery via mcporter)",
181
+ promptGuidelines: [
182
+ "Call mcp_servers to see what MCP servers are available before trying to use one.",
183
+ "MCP servers provide external integrations (Twitter, Linear, Railway, etc.) via the Model Context Protocol.",
184
+ "After listing, use mcp_discover(server) to get tool schemas, then mcp_call(server, tool, args) to invoke.",
185
+ ],
186
+ parameters: Type.Object({
187
+ refresh: Type.Optional(
188
+ Type.Boolean({ description: "Force refresh the server list (default: use cache)" }),
189
+ ),
190
+ }),
191
+
192
+ async execute(_id, params, signal) {
193
+ if (params.refresh) serverListCache = null;
194
+
195
+ try {
196
+ const servers = await getServerList(signal);
197
+ return {
198
+ content: [{ type: "text", text: formatServerList(servers) }],
199
+ details: {
200
+ serverCount: servers.length,
201
+ cached: !params.refresh && serverListCache !== null,
202
+ },
203
+ };
204
+ } catch (err: unknown) {
205
+ const msg = err instanceof Error ? err.message : String(err);
206
+ throw new Error(
207
+ `Failed to list MCP servers. Is mcporter installed? (npm i -g mcporter)\n${msg}`,
208
+ );
209
+ }
210
+ },
211
+
212
+ renderCall(args, theme) {
213
+ let text = theme.fg("toolTitle", theme.bold("mcp_servers"));
214
+ if (args.refresh) text += theme.fg("warning", " (refresh)");
215
+ return new Text(text, 0, 0);
216
+ },
217
+
218
+ renderResult(result, { isPartial }, theme) {
219
+ if (isPartial) return new Text(theme.fg("warning", "Discovering MCP servers..."), 0, 0);
220
+ const d = result.details as { serverCount: number } | undefined;
221
+ return new Text(
222
+ theme.fg("success", `${d?.serverCount ?? 0} servers found`),
223
+ 0,
224
+ 0,
225
+ );
226
+ },
227
+ });
228
+
229
+ // ── mcp_discover ─────────────────────────────────────────────────────────
230
+
231
+ pi.registerTool({
232
+ name: "mcp_discover",
233
+ label: "MCP Discover",
234
+ description:
235
+ "Get detailed tool signatures and JSON schemas for a specific MCP server. " +
236
+ "Use this to understand what tools a server provides and what arguments they accept " +
237
+ "before calling them with mcp_call.",
238
+ promptSnippet:
239
+ "Get tool schemas for a specific MCP server before calling its tools",
240
+ promptGuidelines: [
241
+ "Call mcp_discover with a server name to see the full tool signatures before calling mcp_call.",
242
+ "The schemas show required and optional parameters with types and descriptions.",
243
+ ],
244
+ parameters: Type.Object({
245
+ server: Type.String({
246
+ description:
247
+ "MCP server name (from mcp_servers output), e.g. 'railway', 'twitter-mcp', 'linear'",
248
+ }),
249
+ }),
250
+
251
+ async execute(_id, params, signal) {
252
+ try {
253
+ const detail = await getServerDetail(params.server, signal);
254
+ const text = formatServerDetail(detail);
255
+
256
+ // Truncation guard
257
+ const truncation = truncateHead(text, {
258
+ maxLines: DEFAULT_MAX_LINES,
259
+ maxBytes: DEFAULT_MAX_BYTES,
260
+ });
261
+ let finalText = truncation.content;
262
+ if (truncation.truncated) {
263
+ finalText +=
264
+ `\n\n[Truncated: ${truncation.outputLines}/${truncation.totalLines} lines ` +
265
+ `(${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`;
266
+ }
267
+
268
+ return {
269
+ content: [{ type: "text", text: finalText }],
270
+ details: {
271
+ server: params.server,
272
+ toolCount: detail.tools.length,
273
+ cached: serverDetailCache.has(params.server),
274
+ },
275
+ };
276
+ } catch (err: unknown) {
277
+ const msg = err instanceof Error ? err.message : String(err);
278
+ throw new Error(`Failed to discover tools for "${params.server}": ${msg}`);
279
+ }
280
+ },
281
+
282
+ renderCall(args, theme) {
283
+ let text = theme.fg("toolTitle", theme.bold("mcp_discover "));
284
+ text += theme.fg("accent", args.server);
285
+ return new Text(text, 0, 0);
286
+ },
287
+
288
+ renderResult(result, { isPartial }, theme) {
289
+ if (isPartial)
290
+ return new Text(theme.fg("warning", "Discovering tools..."), 0, 0);
291
+ const d = result.details as { server: string; toolCount: number } | undefined;
292
+ return new Text(
293
+ theme.fg("success", `${d?.toolCount ?? 0} tools`) +
294
+ theme.fg("dim", ` · ${d?.server}`),
295
+ 0,
296
+ 0,
297
+ );
298
+ },
299
+ });
300
+
301
+ // ── mcp_call ─────────────────────────────────────────────────────────────
302
+
303
+ pi.registerTool({
304
+ name: "mcp_call",
305
+ label: "MCP Call",
306
+ description:
307
+ "Call a tool on an MCP server. Provide the server name, tool name, and arguments. " +
308
+ "Use mcp_discover first to see available tools and their required arguments.",
309
+ promptSnippet: "Call a tool on an MCP server via mcporter",
310
+ promptGuidelines: [
311
+ "Always use mcp_discover first to understand the tool's parameters before calling mcp_call.",
312
+ "Arguments are passed as a JSON object matching the tool's input schema.",
313
+ ],
314
+ parameters: Type.Object({
315
+ server: Type.String({
316
+ description: "MCP server name, e.g. 'railway', 'twitter-mcp'",
317
+ }),
318
+ tool: Type.String({
319
+ description: "Tool name on that server, e.g. 'railway_list_projects'",
320
+ }),
321
+ args: Type.Optional(
322
+ Type.Record(Type.String(), Type.Unknown(), {
323
+ description:
324
+ "Tool arguments as key-value pairs matching the tool's input schema",
325
+ }),
326
+ ),
327
+ }),
328
+
329
+ async execute(_id, params, signal) {
330
+ // Build mcporter call command: mcporter call server.tool key:value ...
331
+ const callTarget = `${params.server}.${params.tool}`;
332
+ const cliArgs = ["call", callTarget, "--output", "raw"];
333
+
334
+ if (params.args && Object.keys(params.args).length > 0) {
335
+ for (const [key, value] of Object.entries(params.args)) {
336
+ const strVal =
337
+ typeof value === "string" ? value : JSON.stringify(value);
338
+ cliArgs.push(`${key}:${strVal}`);
339
+ }
340
+ }
341
+
342
+ try {
343
+ const raw = await runMcporter(cliArgs, signal, 60000);
344
+
345
+ // Truncation guard
346
+ const truncation = truncateHead(raw, {
347
+ maxLines: DEFAULT_MAX_LINES,
348
+ maxBytes: DEFAULT_MAX_BYTES,
349
+ });
350
+ let finalText = truncation.content;
351
+ if (truncation.truncated) {
352
+ finalText +=
353
+ `\n\n[Output truncated: ${truncation.outputLines}/${truncation.totalLines} lines ` +
354
+ `(${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)})]`;
355
+ }
356
+
357
+ return {
358
+ content: [{ type: "text", text: finalText }],
359
+ details: {
360
+ server: params.server,
361
+ tool: params.tool,
362
+ charCount: finalText.length,
363
+ truncated: truncation.truncated,
364
+ },
365
+ };
366
+ } catch (err: unknown) {
367
+ const msg = err instanceof Error ? err.message : String(err);
368
+ throw new Error(
369
+ `MCP call failed: ${params.server}.${params.tool}\n${msg}`,
370
+ );
371
+ }
372
+ },
373
+
374
+ renderCall(args, theme) {
375
+ let text = theme.fg("toolTitle", theme.bold("mcp_call "));
376
+ text += theme.fg("accent", `${args.server}.${args.tool}`);
377
+ if (args.args && Object.keys(args.args).length > 0) {
378
+ const preview = Object.entries(args.args)
379
+ .slice(0, 3)
380
+ .map(([k, v]) => {
381
+ const val = typeof v === "string" ? v : JSON.stringify(v);
382
+ return `${k}:${val.length > 30 ? val.slice(0, 30) + "…" : val}`;
383
+ })
384
+ .join(" ");
385
+ text += " " + theme.fg("muted", preview);
386
+ }
387
+ return new Text(text, 0, 0);
388
+ },
389
+
390
+ renderResult(result, { isPartial, expanded }, theme) {
391
+ if (isPartial) return new Text(theme.fg("warning", "Calling MCP tool..."), 0, 0);
392
+
393
+ const d = result.details as {
394
+ server: string;
395
+ tool: string;
396
+ charCount: number;
397
+ truncated: boolean;
398
+ } | undefined;
399
+
400
+ let text = theme.fg("success", `✓ ${d?.server}.${d?.tool}`);
401
+ text += theme.fg("dim", ` · ${(d?.charCount ?? 0).toLocaleString()} chars`);
402
+ if (d?.truncated) text += theme.fg("warning", " · truncated");
403
+
404
+ if (expanded) {
405
+ const content = result.content[0];
406
+ if (content?.type === "text") {
407
+ const preview = content.text.split("\n").slice(0, 15).join("\n");
408
+ text += "\n\n" + theme.fg("dim", preview);
409
+ }
410
+ }
411
+
412
+ return new Text(text, 0, 0);
413
+ },
414
+ });
415
+
416
+ // ── Verify mcporter is available ─────────────────────────────────────────
417
+
418
+ pi.on("session_start", async (_event, ctx) => {
419
+ try {
420
+ const ver = (await runMcporter(["--version"], undefined, 5000)).trim();
421
+ ctx.ui.notify(`MCPorter ${ver} ready`, "info");
422
+ } catch {
423
+ ctx.ui.notify(
424
+ "MCPorter not found. Install with: npm i -g mcporter",
425
+ "error",
426
+ );
427
+ }
428
+ });
429
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Remote Questions — configuration resolution and validation
3
+ */
4
+
5
+ import { loadEffectiveGSDPreferences, type RemoteQuestionsConfig } from "../gsd/preferences.js";
6
+ import type { RemoteChannel } from "./types.js";
7
+
8
+ export interface ResolvedConfig {
9
+ channel: RemoteChannel;
10
+ channelId: string;
11
+ timeoutMs: number;
12
+ pollIntervalMs: number;
13
+ token: string;
14
+ }
15
+
16
+ const ENV_KEYS: Record<RemoteChannel, string> = {
17
+ slack: "SLACK_BOT_TOKEN",
18
+ discord: "DISCORD_BOT_TOKEN",
19
+ };
20
+
21
+ // Channel ID format validation — prevents SSRF if preferences are attacker-controlled
22
+ const CHANNEL_ID_PATTERNS: Record<RemoteChannel, RegExp> = {
23
+ slack: /^[A-Z0-9]{9,12}$/,
24
+ discord: /^\d{17,20}$/,
25
+ };
26
+
27
+ const DEFAULT_TIMEOUT_MINUTES = 5;
28
+ const DEFAULT_POLL_INTERVAL_SECONDS = 5;
29
+ const MIN_TIMEOUT_MINUTES = 1;
30
+ const MAX_TIMEOUT_MINUTES = 30;
31
+ const MIN_POLL_INTERVAL_SECONDS = 2;
32
+ const MAX_POLL_INTERVAL_SECONDS = 30;
33
+
34
+ export function resolveRemoteConfig(): ResolvedConfig | null {
35
+ const prefs = loadEffectiveGSDPreferences();
36
+ const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
37
+ if (!rq || !rq.channel || !rq.channel_id) return null;
38
+ if (rq.channel !== "slack" && rq.channel !== "discord") return null;
39
+
40
+ const channelId = String(rq.channel_id);
41
+ if (!CHANNEL_ID_PATTERNS[rq.channel].test(channelId)) return null;
42
+
43
+ const token = process.env[ENV_KEYS[rq.channel]];
44
+ if (!token) return null;
45
+
46
+ const timeoutMinutes = clampNumber(rq.timeout_minutes, DEFAULT_TIMEOUT_MINUTES, MIN_TIMEOUT_MINUTES, MAX_TIMEOUT_MINUTES);
47
+ const pollIntervalSeconds = clampNumber(rq.poll_interval_seconds, DEFAULT_POLL_INTERVAL_SECONDS, MIN_POLL_INTERVAL_SECONDS, MAX_POLL_INTERVAL_SECONDS);
48
+
49
+ return {
50
+ channel: rq.channel,
51
+ channelId,
52
+ timeoutMs: timeoutMinutes * 60 * 1000,
53
+ pollIntervalMs: pollIntervalSeconds * 1000,
54
+ token,
55
+ };
56
+ }
57
+
58
+ export function getRemoteConfigStatus(): string {
59
+ const prefs = loadEffectiveGSDPreferences();
60
+ const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
61
+ if (!rq || !rq.channel || !rq.channel_id) return "Remote questions: not configured";
62
+ if (rq.channel !== "slack" && rq.channel !== "discord") return `Remote questions: unknown channel type \"${rq.channel}\"`;
63
+ const channelId = String(rq.channel_id);
64
+ if (!CHANNEL_ID_PATTERNS[rq.channel].test(channelId)) return `Remote questions: invalid ${rq.channel} channel ID format`;
65
+ const envVar = ENV_KEYS[rq.channel];
66
+ if (!process.env[envVar]) return `Remote questions: ${envVar} not set — remote questions disabled`;
67
+
68
+ const timeoutMinutes = clampNumber(rq.timeout_minutes, DEFAULT_TIMEOUT_MINUTES, MIN_TIMEOUT_MINUTES, MAX_TIMEOUT_MINUTES);
69
+ const pollIntervalSeconds = clampNumber(rq.poll_interval_seconds, DEFAULT_POLL_INTERVAL_SECONDS, MIN_POLL_INTERVAL_SECONDS, MAX_POLL_INTERVAL_SECONDS);
70
+ return `Remote questions: ${rq.channel} configured (timeout ${timeoutMinutes}m, poll ${pollIntervalSeconds}s)`;
71
+ }
72
+
73
+ export function isValidChannelId(channel: RemoteChannel, id: string): boolean {
74
+ return CHANNEL_ID_PATTERNS[channel].test(id);
75
+ }
76
+
77
+ function clampNumber(value: unknown, fallback: number, min: number, max: number): number {
78
+ const n = typeof value === "number" ? value : Number(value);
79
+ if (!Number.isFinite(n)) return fallback;
80
+ return Math.max(min, Math.min(max, n));
81
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Remote Questions — Discord adapter
3
+ */
4
+
5
+ import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
6
+ import { formatForDiscord, parseDiscordResponse } from "./format.js";
7
+
8
+ const DISCORD_API = "https://discord.com/api/v10";
9
+ const PER_REQUEST_TIMEOUT_MS = 15_000;
10
+ const NUMBER_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"];
11
+
12
+ export class DiscordAdapter implements ChannelAdapter {
13
+ readonly name = "discord" as const;
14
+ private botUserId: string | null = null;
15
+ private readonly token: string;
16
+ private readonly channelId: string;
17
+
18
+ constructor(token: string, channelId: string) {
19
+ this.token = token;
20
+ this.channelId = channelId;
21
+ }
22
+
23
+ async validate(): Promise<void> {
24
+ const res = await this.discordApi("GET", "/users/@me");
25
+ if (!res.id) throw new Error("Discord auth failed: invalid token");
26
+ this.botUserId = String(res.id);
27
+ }
28
+
29
+ async sendPrompt(prompt: RemotePrompt): Promise<RemoteDispatchResult> {
30
+ const { embeds, reactionEmojis } = formatForDiscord(prompt);
31
+ const res = await this.discordApi("POST", `/channels/${this.channelId}/messages`, {
32
+ content: "**GSD needs your input** — reply to this message with your answer",
33
+ embeds,
34
+ });
35
+
36
+ if (!res.id) throw new Error(`Discord send failed: ${JSON.stringify(res)}`);
37
+
38
+ const messageId = String(res.id);
39
+ if (prompt.questions.length === 1) {
40
+ for (const emoji of reactionEmojis) {
41
+ try {
42
+ await this.discordApi("PUT", `/channels/${this.channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`);
43
+ } catch {
44
+ // Best-effort only
45
+ }
46
+ }
47
+ }
48
+
49
+ return {
50
+ ref: {
51
+ id: prompt.id,
52
+ channel: "discord",
53
+ messageId,
54
+ channelId: this.channelId,
55
+ },
56
+ };
57
+ }
58
+
59
+ async pollAnswer(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null> {
60
+ if (!this.botUserId) await this.validate();
61
+
62
+ if (prompt.questions.length === 1) {
63
+ const reactionAnswer = await this.checkReactions(prompt, ref);
64
+ if (reactionAnswer) return reactionAnswer;
65
+ }
66
+
67
+ return this.checkReplies(prompt, ref);
68
+ }
69
+
70
+ private async checkReactions(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null> {
71
+ const reactions: Array<{ emoji: string; count: number }> = [];
72
+ for (const emoji of NUMBER_EMOJIS) {
73
+ try {
74
+ const users = await this.discordApi("GET", `/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent(emoji)}`);
75
+ if (Array.isArray(users)) {
76
+ const humanUsers = users.filter((u: { id: string }) => u.id !== this.botUserId);
77
+ if (humanUsers.length > 0) reactions.push({ emoji, count: humanUsers.length });
78
+ }
79
+ } catch (err) {
80
+ const msg = String((err as Error).message ?? "");
81
+ // 404 = no reactions for this emoji — expected, continue
82
+ if (msg.includes("HTTP 404")) continue;
83
+ // 401/403 = auth failure — surface to caller so it can fail the poll
84
+ if (msg.includes("HTTP 401") || msg.includes("HTTP 403")) throw err;
85
+ // Other errors (rate limit, network) — skip this emoji, best-effort
86
+ }
87
+ }
88
+
89
+ if (reactions.length === 0) return null;
90
+ return parseDiscordResponse(reactions, null, prompt.questions);
91
+ }
92
+
93
+ private async checkReplies(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null> {
94
+ const messages = await this.discordApi("GET", `/channels/${ref.channelId}/messages?after=${ref.messageId}&limit=10`);
95
+ if (!Array.isArray(messages)) return null;
96
+
97
+ const replies = messages.filter(
98
+ (m: { author?: { id?: string }; message_reference?: { message_id?: string }; content?: string }) =>
99
+ m.author?.id &&
100
+ m.author.id !== this.botUserId &&
101
+ m.message_reference?.message_id === ref.messageId &&
102
+ m.content,
103
+ );
104
+
105
+ if (replies.length === 0) return null;
106
+ return parseDiscordResponse([], String(replies[0].content), prompt.questions);
107
+ }
108
+
109
+ private async discordApi(method: string, path: string, body?: unknown): Promise<any> {
110
+ const headers: Record<string, string> = { Authorization: `Bot ${this.token}` };
111
+ const init: RequestInit = { method, headers };
112
+ if (body) {
113
+ headers["Content-Type"] = "application/json";
114
+ init.body = JSON.stringify(body);
115
+ }
116
+
117
+ init.signal = AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS);
118
+ const response = await fetch(`${DISCORD_API}${path}`, init);
119
+ if (response.status === 204) return {};
120
+ if (!response.ok) {
121
+ const text = await response.text().catch(() => "");
122
+ // Limit error body length to avoid leaking verbose Discord error responses
123
+ const safeText = text.length > 200 ? text.slice(0, 200) + "…" : text;
124
+ throw new Error(`Discord API HTTP ${response.status}: ${safeText}`);
125
+ }
126
+ return response.json();
127
+ }
128
+ }