gsd-pi 2.18.0 → 2.20.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 (289) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +3 -3
  3. package/dist/onboarding.d.ts +3 -1
  4. package/dist/onboarding.js +77 -3
  5. package/dist/remote-questions-config.d.ts +1 -1
  6. package/dist/resources/extensions/google-search/index.ts +164 -47
  7. package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +148 -39
  9. package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
  10. package/dist/resources/extensions/gsd/auto.ts +690 -39
  11. package/dist/resources/extensions/gsd/captures.ts +384 -0
  12. package/dist/resources/extensions/gsd/commands.ts +654 -36
  13. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  14. package/dist/resources/extensions/gsd/context-budget.ts +243 -0
  15. package/dist/resources/extensions/gsd/context-store.ts +195 -0
  16. package/dist/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  17. package/dist/resources/extensions/gsd/db-writer.ts +341 -0
  18. package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
  19. package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
  20. package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  21. package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
  22. package/dist/resources/extensions/gsd/doctor.ts +283 -2
  23. package/dist/resources/extensions/gsd/export.ts +81 -2
  24. package/dist/resources/extensions/gsd/files.ts +39 -9
  25. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  26. package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
  27. package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
  28. package/dist/resources/extensions/gsd/history.ts +0 -1
  29. package/dist/resources/extensions/gsd/index.ts +277 -1
  30. package/dist/resources/extensions/gsd/md-importer.ts +526 -0
  31. package/dist/resources/extensions/gsd/metrics.ts +84 -0
  32. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  33. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  34. package/dist/resources/extensions/gsd/notifications.ts +0 -1
  35. package/dist/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  36. package/dist/resources/extensions/gsd/preferences.ts +198 -150
  37. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
  39. package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  40. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  41. package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
  42. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  43. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  44. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  45. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  46. package/dist/resources/extensions/gsd/quick.ts +156 -0
  47. package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
  48. package/dist/resources/extensions/gsd/skill-health.ts +417 -0
  49. package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
  50. package/dist/resources/extensions/gsd/state.ts +30 -0
  51. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  52. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  53. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  54. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  55. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  57. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  58. package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  59. package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  60. package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  61. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  62. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  63. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  64. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  65. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  66. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  67. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  68. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  69. package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  70. package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  71. package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  72. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  73. package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  74. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  75. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  76. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  77. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  78. package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  79. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  80. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  81. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  82. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  83. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  84. package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  85. package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  86. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  87. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  88. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  89. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  90. package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  91. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  92. package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  93. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  94. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  95. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  96. package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  97. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  98. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  99. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  100. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  101. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  102. package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  103. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  104. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  105. package/dist/resources/extensions/gsd/types.ts +29 -0
  106. package/dist/resources/extensions/gsd/undo.ts +0 -1
  107. package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
  108. package/dist/resources/extensions/gsd/visualizer-data.ts +505 -0
  109. package/dist/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  110. package/dist/resources/extensions/gsd/visualizer-views.ts +755 -0
  111. package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
  112. package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
  113. package/dist/resources/extensions/remote-questions/config.ts +4 -2
  114. package/dist/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  115. package/dist/resources/extensions/remote-questions/format.ts +166 -14
  116. package/dist/resources/extensions/remote-questions/manager.ts +14 -4
  117. package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
  118. package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  119. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  120. package/dist/resources/extensions/remote-questions/types.ts +2 -1
  121. package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  122. package/dist/resources/extensions/voice/index.ts +4 -3
  123. package/package.json +1 -1
  124. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
  126. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  129. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
  131. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
  133. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
  135. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
  137. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
  139. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
  140. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
  142. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
  144. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
  146. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  148. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
  150. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  152. package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
  153. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  154. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
  156. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  158. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  160. package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
  161. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  162. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
  163. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  164. package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
  165. package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
  166. package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
  167. package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
  168. package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
  169. package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
  170. package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
  171. package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
  172. package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
  173. package/src/resources/extensions/google-search/index.ts +164 -47
  174. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  175. package/src/resources/extensions/gsd/auto-prompts.ts +148 -39
  176. package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
  177. package/src/resources/extensions/gsd/auto.ts +690 -39
  178. package/src/resources/extensions/gsd/captures.ts +384 -0
  179. package/src/resources/extensions/gsd/commands.ts +654 -36
  180. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  181. package/src/resources/extensions/gsd/context-budget.ts +243 -0
  182. package/src/resources/extensions/gsd/context-store.ts +195 -0
  183. package/src/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  184. package/src/resources/extensions/gsd/db-writer.ts +341 -0
  185. package/src/resources/extensions/gsd/debug-logger.ts +178 -0
  186. package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
  187. package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  188. package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
  189. package/src/resources/extensions/gsd/doctor.ts +283 -2
  190. package/src/resources/extensions/gsd/export.ts +81 -2
  191. package/src/resources/extensions/gsd/files.ts +39 -9
  192. package/src/resources/extensions/gsd/git-service.ts +6 -0
  193. package/src/resources/extensions/gsd/gsd-db.ts +752 -0
  194. package/src/resources/extensions/gsd/guided-flow.ts +26 -1
  195. package/src/resources/extensions/gsd/history.ts +0 -1
  196. package/src/resources/extensions/gsd/index.ts +277 -1
  197. package/src/resources/extensions/gsd/md-importer.ts +526 -0
  198. package/src/resources/extensions/gsd/metrics.ts +84 -0
  199. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  200. package/src/resources/extensions/gsd/model-router.ts +256 -0
  201. package/src/resources/extensions/gsd/notifications.ts +0 -1
  202. package/src/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  203. package/src/resources/extensions/gsd/preferences.ts +198 -150
  204. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  205. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
  206. package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  207. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  208. package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
  209. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  210. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  211. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  212. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  213. package/src/resources/extensions/gsd/quick.ts +156 -0
  214. package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
  215. package/src/resources/extensions/gsd/skill-health.ts +417 -0
  216. package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
  217. package/src/resources/extensions/gsd/state.ts +30 -0
  218. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  219. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  220. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  221. package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  222. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  223. package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  224. package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  225. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  226. package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  227. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  228. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  229. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  230. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  231. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  232. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  233. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  234. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  235. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  236. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  237. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  238. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  239. package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  240. package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  241. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  242. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  243. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  244. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  245. package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  246. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  247. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  248. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  249. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  250. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  251. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  252. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  253. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  254. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  255. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  256. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  257. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  258. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  259. package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  260. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  261. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  262. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  263. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  264. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  265. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  266. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  267. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  268. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  269. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  270. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  271. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  272. package/src/resources/extensions/gsd/types.ts +29 -0
  273. package/src/resources/extensions/gsd/undo.ts +0 -1
  274. package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
  275. package/src/resources/extensions/gsd/visualizer-data.ts +505 -0
  276. package/src/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  277. package/src/resources/extensions/gsd/visualizer-views.ts +755 -0
  278. package/src/resources/extensions/gsd/worktree-command.ts +18 -0
  279. package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
  280. package/src/resources/extensions/remote-questions/config.ts +4 -2
  281. package/src/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  282. package/src/resources/extensions/remote-questions/format.ts +166 -14
  283. package/src/resources/extensions/remote-questions/manager.ts +14 -4
  284. package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
  285. package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  286. package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  287. package/src/resources/extensions/remote-questions/types.ts +2 -1
  288. package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  289. package/src/resources/extensions/voice/index.ts +4 -3
@@ -13,6 +13,7 @@
13
13
  import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
14
14
  import { loadPrompt } from "./prompt-loader.js";
15
15
  import { autoCommitCurrentBranch } from "./worktree.js";
16
+ import { runWorktreePostCreateHook } from "./auto-worktree.js";
16
17
  import { showConfirm } from "../shared/confirm-ui.js";
17
18
  import { gsdRoot, milestonesDir } from "./paths.js";
18
19
  import {
@@ -360,6 +361,12 @@ async function handleCreate(
360
361
  const mainBase = originalCwd ?? basePath;
361
362
  const info = createWorktree(mainBase, name);
362
363
 
364
+ // Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
365
+ const hookError = runWorktreePostCreateHook(mainBase, info.path);
366
+ if (hookError) {
367
+ ctx.ui.notify(hookError, "warning");
368
+ }
369
+
363
370
  // Track original cwd before switching
364
371
  if (!originalCwd) originalCwd = basePath;
365
372
 
@@ -672,6 +679,17 @@ async function handleMerge(
672
679
  // Try a direct squash-merge first. Only fall back to LLM on conflict.
673
680
  const commitType = inferCommitType(name);
674
681
  const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
682
+
683
+ // Reconcile worktree DB into main DB before squash merge
684
+ const wtDbPath = join(worktreePath(basePath, name), ".gsd", "gsd.db");
685
+ const mainDbPath = join(basePath, ".gsd", "gsd.db");
686
+ if (existsSync(wtDbPath) && existsSync(mainDbPath)) {
687
+ try {
688
+ const { reconcileWorktreeDb } = await import("./gsd-db.js");
689
+ reconcileWorktreeDb(mainDbPath, wtDbPath);
690
+ } catch { /* non-fatal */ }
691
+ }
692
+
675
693
  try {
676
694
  mergeWorktreeToMain(basePath, name, commitMessage);
677
695
  ctx.ui.notify(
@@ -94,7 +94,7 @@ export function worktreeBranchName(name: string): string {
94
94
  *
95
95
  * @param opts.branch — override the default `worktree/<name>` branch name
96
96
  */
97
- export function createWorktree(basePath: string, name: string, opts: { branch?: string; startPoint?: string } = {}): WorktreeInfo {
97
+ export function createWorktree(basePath: string, name: string, opts: { branch?: string; startPoint?: string; reuseExistingBranch?: boolean } = {}): WorktreeInfo {
98
98
  // Validate name: alphanumeric, hyphens, underscores only
99
99
  if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
100
100
  throw new Error(`Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
@@ -133,9 +133,16 @@ export function createWorktree(basePath: string, name: string, opts: { branch?:
133
133
  );
134
134
  }
135
135
 
136
- // Reset the stale branch to the start point, then attach worktree to it
137
- nativeBranchForceReset(basePath, branch, startPoint);
138
- nativeWorktreeAdd(basePath, wtPath, branch);
136
+ if (opts.reuseExistingBranch) {
137
+ // Attach worktree to the existing branch as-is (preserving commits).
138
+ // Used when resuming auto-mode: the milestone branch has valid work
139
+ // from prior sessions that must not be reset.
140
+ nativeWorktreeAdd(basePath, wtPath, branch);
141
+ } else {
142
+ // Reset the stale branch to the start point, then attach worktree to it
143
+ nativeBranchForceReset(basePath, branch, startPoint);
144
+ nativeWorktreeAdd(basePath, wtPath, branch);
145
+ }
139
146
  } else {
140
147
  nativeWorktreeAdd(basePath, wtPath, branch, true, startPoint);
141
148
  }
@@ -16,12 +16,14 @@ export interface ResolvedConfig {
16
16
  const ENV_KEYS: Record<RemoteChannel, string> = {
17
17
  slack: "SLACK_BOT_TOKEN",
18
18
  discord: "DISCORD_BOT_TOKEN",
19
+ telegram: "TELEGRAM_BOT_TOKEN",
19
20
  };
20
21
 
21
22
  // Channel ID format validation — prevents SSRF if preferences are attacker-controlled
22
23
  const CHANNEL_ID_PATTERNS: Record<RemoteChannel, RegExp> = {
23
24
  slack: /^[A-Z0-9]{9,12}$/,
24
25
  discord: /^\d{17,20}$/,
26
+ telegram: /^-?\d{5,20}$/,
25
27
  };
26
28
 
27
29
  const DEFAULT_TIMEOUT_MINUTES = 5;
@@ -35,7 +37,7 @@ export function resolveRemoteConfig(): ResolvedConfig | null {
35
37
  const prefs = loadEffectiveGSDPreferences();
36
38
  const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
37
39
  if (!rq || !rq.channel || !rq.channel_id) return null;
38
- if (rq.channel !== "slack" && rq.channel !== "discord") return null;
40
+ if (rq.channel !== "slack" && rq.channel !== "discord" && rq.channel !== "telegram") return null;
39
41
 
40
42
  const channelId = String(rq.channel_id);
41
43
  if (!CHANNEL_ID_PATTERNS[rq.channel].test(channelId)) return null;
@@ -59,7 +61,7 @@ export function getRemoteConfigStatus(): string {
59
61
  const prefs = loadEffectiveGSDPreferences();
60
62
  const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
61
63
  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}\"`;
64
+ if (rq.channel !== "slack" && rq.channel !== "discord" && rq.channel !== "telegram") return `Remote questions: unknown channel type \"${rq.channel}\"`;
63
65
  const channelId = String(rq.channel_id);
64
66
  if (!CHANNEL_ID_PATTERNS[rq.channel].test(channelId)) return `Remote questions: invalid ${rq.channel} channel ID format`;
65
67
  const envVar = ENV_KEYS[rq.channel];
@@ -3,15 +3,14 @@
3
3
  */
4
4
 
5
5
  import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
6
- import { formatForDiscord, parseDiscordResponse } from "./format.js";
6
+ import { formatForDiscord, parseDiscordResponse, DISCORD_NUMBER_EMOJIS } from "./format.js";
7
7
 
8
8
  const DISCORD_API = "https://discord.com/api/v10";
9
9
  const PER_REQUEST_TIMEOUT_MS = 15_000;
10
- const NUMBER_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"];
11
-
12
10
  export class DiscordAdapter implements ChannelAdapter {
13
11
  readonly name = "discord" as const;
14
12
  private botUserId: string | null = null;
13
+ private guildId: string | null = null;
15
14
  private readonly token: string;
16
15
  private readonly channelId: string;
17
16
 
@@ -24,6 +23,17 @@ export class DiscordAdapter implements ChannelAdapter {
24
23
  const res = await this.discordApi("GET", "/users/@me");
25
24
  if (!res.id) throw new Error("Discord auth failed: invalid token");
26
25
  this.botUserId = String(res.id);
26
+
27
+ // Resolve guild ID for message URL generation.
28
+ // The channel belongs to a guild — fetch channel info to discover it.
29
+ try {
30
+ const channelInfo = await this.discordApi("GET", `/channels/${this.channelId}`);
31
+ if (channelInfo.guild_id) {
32
+ this.guildId = String(channelInfo.guild_id);
33
+ }
34
+ } catch {
35
+ // Non-fatal — message URLs will be omitted if guild ID can't be resolved
36
+ }
27
37
  }
28
38
 
29
39
  async sendPrompt(prompt: RemotePrompt): Promise<RemoteDispatchResult> {
@@ -46,12 +56,18 @@ export class DiscordAdapter implements ChannelAdapter {
46
56
  }
47
57
  }
48
58
 
59
+ // Build message URL if guild ID is available
60
+ const messageUrl = this.guildId
61
+ ? `https://discord.com/channels/${this.guildId}/${this.channelId}/${messageId}`
62
+ : undefined;
63
+
49
64
  return {
50
65
  ref: {
51
66
  id: prompt.id,
52
67
  channel: "discord",
53
68
  messageId,
54
69
  channelId: this.channelId,
70
+ threadUrl: messageUrl,
55
71
  },
56
72
  };
57
73
  }
@@ -67,9 +83,24 @@ export class DiscordAdapter implements ChannelAdapter {
67
83
  return this.checkReplies(prompt, ref);
68
84
  }
69
85
 
86
+ /**
87
+ * Acknowledge that an answer was received by adding a ✅ reaction to the
88
+ * original prompt message. Best-effort — failures are silently ignored.
89
+ */
90
+ async acknowledgeAnswer(ref: RemotePromptRef): Promise<void> {
91
+ try {
92
+ await this.discordApi(
93
+ "PUT",
94
+ `/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent("✅")}/@me`,
95
+ );
96
+ } catch {
97
+ // Best-effort — don't let acknowledgement failures affect the flow
98
+ }
99
+ }
100
+
70
101
  private async checkReactions(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null> {
71
102
  const reactions: Array<{ emoji: string; count: number }> = [];
72
- for (const emoji of NUMBER_EMOJIS) {
103
+ for (const emoji of DISCORD_NUMBER_EMOJIS) {
73
104
  try {
74
105
  const users = await this.discordApi("GET", `/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent(emoji)}`);
75
106
  if (Array.isArray(users)) {
@@ -18,7 +18,8 @@ export interface DiscordEmbed {
18
18
  footer?: { text: string };
19
19
  }
20
20
 
21
- const NUMBER_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"];
21
+ export const DISCORD_NUMBER_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"];
22
+ export const SLACK_NUMBER_REACTION_NAMES = ["one", "two", "three", "four", "five"];
22
23
  const MAX_USER_NOTE_LENGTH = 500;
23
24
 
24
25
  export function formatForSlack(prompt: RemotePrompt): SlackBlock[] {
@@ -29,7 +30,18 @@ export function formatForSlack(prompt: RemotePrompt): SlackBlock[] {
29
30
  },
30
31
  ];
31
32
 
33
+ if (prompt.questions.length > 1) {
34
+ blocks.push({
35
+ type: "context",
36
+ elements: [{
37
+ type: "mrkdwn",
38
+ text: "Reply once in thread using one line per question or semicolons (`1; 2; custom note`).",
39
+ }],
40
+ });
41
+ }
42
+
32
43
  for (const q of prompt.questions) {
44
+ const supportsReactions = prompt.questions.length === 1;
33
45
  blocks.push({
34
46
  type: "section",
35
47
  text: { type: "mrkdwn", text: `*${q.header}*\n${q.question}` },
@@ -47,15 +59,33 @@ export function formatForSlack(prompt: RemotePrompt): SlackBlock[] {
47
59
  type: "context",
48
60
  elements: [{
49
61
  type: "mrkdwn",
50
- text: q.allowMultiple
51
- ? "Reply in thread with comma-separated numbers (`1,3`) or free text."
52
- : "Reply in thread with a number (`1`) or free text.",
62
+ text: prompt.questions.length > 1
63
+ ? (q.allowMultiple
64
+ ? "For this question, use comma-separated numbers (`1,3`) or free text."
65
+ : "For this question, use one number (`1`) or free text.")
66
+ : (q.allowMultiple
67
+ ? (supportsReactions
68
+ ? "Reply in thread with comma-separated numbers (`1,3`) or react with matching number emoji."
69
+ : "Reply in thread with comma-separated numbers (`1,3`) or free text.")
70
+ : (supportsReactions
71
+ ? "Reply in thread with a number (`1`) or react with the matching number emoji."
72
+ : "Reply in thread with a number (`1`) or free text.")),
53
73
  }],
54
74
  });
55
75
 
56
76
  blocks.push({ type: "divider" });
57
77
  }
58
78
 
79
+ if (prompt.context?.source) {
80
+ blocks.push({
81
+ type: "context",
82
+ elements: [{
83
+ type: "mrkdwn",
84
+ text: `Source: \`${prompt.context.source}\``,
85
+ }],
86
+ });
87
+ }
88
+
59
89
  return blocks;
60
90
  }
61
91
 
@@ -64,23 +94,29 @@ export function formatForDiscord(prompt: RemotePrompt): { embeds: DiscordEmbed[]
64
94
  const embeds: DiscordEmbed[] = prompt.questions.map((q, questionIndex) => {
65
95
  const supportsReactions = prompt.questions.length === 1;
66
96
  const optionLines = q.options.map((opt, i) => {
67
- const emoji = NUMBER_EMOJIS[i] ?? `${i + 1}.`;
68
- if (supportsReactions && NUMBER_EMOJIS[i]) reactionEmojis.push(NUMBER_EMOJIS[i]);
97
+ const emoji = DISCORD_NUMBER_EMOJIS[i] ?? `${i + 1}.`;
98
+ if (supportsReactions && DISCORD_NUMBER_EMOJIS[i]) reactionEmojis.push(DISCORD_NUMBER_EMOJIS[i]);
69
99
  return `${emoji} **${opt.label}** — ${opt.description}`;
70
100
  });
71
101
 
72
- const footerText = supportsReactions
73
- ? (q.allowMultiple
74
- ? "Reply with comma-separated choices (`1,3`) or react with matching numbers"
75
- : "Reply with a number or react with the matching number")
76
- : `Question ${questionIndex + 1}/${prompt.questions.length} reply with one line per question or use semicolons`;
102
+ const footerParts: string[] = [];
103
+ if (supportsReactions) {
104
+ footerParts.push(q.allowMultiple
105
+ ? "Reply with comma-separated choices (`1,3`) or react with matching numbers"
106
+ : "Reply with a number or react with the matching number");
107
+ } else {
108
+ footerParts.push(`Question ${questionIndex + 1}/${prompt.questions.length} — reply with one line per question or use semicolons`);
109
+ }
110
+ if (prompt.context?.source) {
111
+ footerParts.push(`Source: ${prompt.context.source}`);
112
+ }
77
113
 
78
114
  return {
79
115
  title: q.header,
80
116
  description: q.question,
81
117
  color: 0x7c3aed,
82
118
  fields: [{ name: "Options", value: optionLines.join("\n") }],
83
- footer: { text: footerText },
119
+ footer: { text: footerParts.join(" · ") },
84
120
  };
85
121
  });
86
122
 
@@ -124,8 +160,8 @@ export function parseDiscordResponse(
124
160
 
125
161
  const q = questions[0];
126
162
  const picked = reactions
127
- .filter((r) => NUMBER_EMOJIS.includes(r.emoji) && r.count > 0)
128
- .map((r) => q.options[NUMBER_EMOJIS.indexOf(r.emoji)]?.label)
163
+ .filter((r) => DISCORD_NUMBER_EMOJIS.includes(r.emoji) && r.count > 0)
164
+ .map((r) => q.options[DISCORD_NUMBER_EMOJIS.indexOf(r.emoji)]?.label)
129
165
  .filter(Boolean) as string[];
130
166
 
131
167
  answers[q.id] = picked.length > 0
@@ -135,6 +171,122 @@ export function parseDiscordResponse(
135
171
  return { answers };
136
172
  }
137
173
 
174
+ export function parseSlackReactionResponse(
175
+ reactionNames: string[],
176
+ questions: RemoteQuestion[],
177
+ ): RemoteAnswer {
178
+ const answers: RemoteAnswer["answers"] = {};
179
+ if (questions.length !== 1) {
180
+ for (const q of questions) {
181
+ answers[q.id] = { answers: [], user_note: "Slack reactions are only supported for single-question prompts" };
182
+ }
183
+ return { answers };
184
+ }
185
+
186
+ const q = questions[0];
187
+ const picked = reactionNames
188
+ .filter((name) => SLACK_NUMBER_REACTION_NAMES.includes(name))
189
+ .map((name) => q.options[SLACK_NUMBER_REACTION_NAMES.indexOf(name)]?.label)
190
+ .filter(Boolean) as string[];
191
+
192
+ answers[q.id] = picked.length > 0
193
+ ? { answers: q.allowMultiple ? picked : [picked[0]] }
194
+ : { answers: [], user_note: "No clear response via reactions" };
195
+
196
+ return { answers };
197
+ }
198
+
199
+ export interface TelegramInlineButton {
200
+ text: string;
201
+ callback_data: string;
202
+ }
203
+
204
+ export interface TelegramInlineKeyboardMarkup {
205
+ inline_keyboard: TelegramInlineButton[][];
206
+ }
207
+
208
+ export interface TelegramMessage {
209
+ text: string;
210
+ parse_mode: "HTML";
211
+ reply_markup?: TelegramInlineKeyboardMarkup;
212
+ }
213
+
214
+ function escapeHtml(s: string): string {
215
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
216
+ }
217
+
218
+ export function formatForTelegram(prompt: RemotePrompt): TelegramMessage {
219
+ const lines: string[] = ["<b>GSD needs your input</b>", ""];
220
+
221
+ for (let qi = 0; qi < prompt.questions.length; qi++) {
222
+ const q = prompt.questions[qi];
223
+ lines.push(`<b>${escapeHtml(q.header)}</b>`);
224
+ lines.push(escapeHtml(q.question));
225
+ lines.push("");
226
+
227
+ for (let i = 0; i < q.options.length; i++) {
228
+ lines.push(`${i + 1}. <b>${escapeHtml(q.options[i].label)}</b> — ${escapeHtml(q.options[i].description)}`);
229
+ }
230
+
231
+ lines.push("");
232
+ if (prompt.questions.length === 1) {
233
+ lines.push(q.allowMultiple
234
+ ? "Reply with comma-separated numbers (1,3) or free text."
235
+ : "Reply with a number or tap a button below.");
236
+ } else {
237
+ lines.push(`Question ${qi + 1}/${prompt.questions.length} — reply with one line per question or use semicolons.`);
238
+ }
239
+
240
+ if (qi < prompt.questions.length - 1) lines.push("");
241
+ }
242
+
243
+ const result: TelegramMessage = {
244
+ text: lines.join("\n"),
245
+ parse_mode: "HTML",
246
+ };
247
+
248
+ // Inline keyboard for single-question with <=5 options
249
+ const isSingle = prompt.questions.length === 1;
250
+ if (isSingle && prompt.questions[0].options.length <= 5) {
251
+ result.reply_markup = {
252
+ inline_keyboard: prompt.questions[0].options.map((opt, i) => [{
253
+ text: `${i + 1}. ${opt.label}`,
254
+ callback_data: `${prompt.id}:${i}`,
255
+ }]),
256
+ };
257
+ }
258
+
259
+ return result;
260
+ }
261
+
262
+ export function parseTelegramResponse(
263
+ callbackData: string | null,
264
+ replyText: string | null,
265
+ questions: RemoteQuestion[],
266
+ promptId: string,
267
+ ): RemoteAnswer {
268
+ // Handle callback_data from inline keyboard button press
269
+ if (callbackData) {
270
+ const match = callbackData.match(new RegExp(`^${promptId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:(\\d+)$`));
271
+ if (match && questions.length === 1) {
272
+ const idx = parseInt(match[1], 10);
273
+ const q = questions[0];
274
+ if (idx >= 0 && idx < q.options.length) {
275
+ return { answers: { [q.id]: { answers: [q.options[idx].label] } } };
276
+ }
277
+ }
278
+ }
279
+
280
+ // Handle text reply — delegate to parseSlackReply (text parsing is format-agnostic)
281
+ if (replyText) return parseSlackReply(replyText, questions);
282
+
283
+ const answers: RemoteAnswer["answers"] = {};
284
+ for (const q of questions) {
285
+ answers[q.id] = { answers: [], user_note: "No response provided" };
286
+ }
287
+ return { answers };
288
+ }
289
+
138
290
  function parseAnswerForQuestion(text: string, q: RemoteQuestion): { answers: string[]; user_note?: string } {
139
291
  if (!text) return { answers: [], user_note: "No response provided" };
140
292
 
@@ -5,8 +5,9 @@
5
5
  import { randomUUID } from "node:crypto";
6
6
  import type { ChannelAdapter, RemotePrompt, RemoteQuestion, RemoteAnswer } from "./types.js";
7
7
  import { resolveRemoteConfig, type ResolvedConfig } from "./config.js";
8
- import { SlackAdapter } from "./slack-adapter.js";
9
8
  import { DiscordAdapter } from "./discord-adapter.js";
9
+ import { SlackAdapter } from "./slack-adapter.js";
10
+ import { TelegramAdapter } from "./telegram-adapter.js";
10
11
  import { createPromptRecord, writePromptRecord, markPromptAnswered, markPromptDispatched, markPromptStatus, updatePromptRecord } from "./store.js";
11
12
 
12
13
  interface ToolResult {
@@ -76,6 +77,14 @@ export async function tryRemoteQuestions(
76
77
  }
77
78
 
78
79
  markPromptAnswered(prompt.id, answer);
80
+
81
+ // Best-effort acknowledgement gives remote users a visible receipt signal.
82
+ if (dispatch.ref) {
83
+ try {
84
+ await adapter.acknowledgeAnswer?.(dispatch.ref);
85
+ } catch { /* best-effort */ }
86
+ }
87
+
79
88
  return {
80
89
  content: [{ type: "text", text: JSON.stringify({ answers: formatForTool(answer) }) }],
81
90
  details: {
@@ -111,9 +120,9 @@ function createPrompt(questions: QuestionInput[], config: ResolvedConfig): Remot
111
120
  }
112
121
 
113
122
  function createAdapter(config: ResolvedConfig): ChannelAdapter {
114
- return config.channel === "slack"
115
- ? new SlackAdapter(config.token, config.channelId)
116
- : new DiscordAdapter(config.token, config.channelId);
123
+ if (config.channel === "slack") return new SlackAdapter(config.token, config.channelId);
124
+ if (config.channel === "telegram") return new TelegramAdapter(config.token, config.channelId);
125
+ return new DiscordAdapter(config.token, config.channelId);
117
126
  }
118
127
 
119
128
  async function pollUntilDone(
@@ -173,6 +182,7 @@ const TOKEN_PATTERNS = [
173
182
  /xoxb-[A-Za-z0-9\-]+/g, // Slack bot tokens
174
183
  /xoxp-[A-Za-z0-9\-]+/g, // Slack user tokens
175
184
  /xoxa-[A-Za-z0-9\-]+/g, // Slack app tokens
185
+ /\d{8,10}:[A-Za-z0-9_-]{35}/g, // Telegram bot tokens
176
186
  /[A-Za-z0-9_\-.]{20,}/g, // Long opaque secrets (Discord tokens, etc.)
177
187
  ];
178
188
 
@@ -21,6 +21,7 @@ export async function handleRemote(
21
21
 
22
22
  if (trimmed === "slack") return handleSetupSlack(ctx);
23
23
  if (trimmed === "discord") return handleSetupDiscord(ctx);
24
+ if (trimmed === "telegram") return handleSetupTelegram(ctx);
24
25
  if (trimmed === "status") return handleRemoteStatus(ctx);
25
26
  if (trimmed === "disconnect") return handleDisconnect(ctx);
26
27
 
@@ -36,9 +37,28 @@ async function handleSetupSlack(ctx: ExtensionCommandContext): Promise<void> {
36
37
  const auth = await fetchJson("https://slack.com/api/auth.test", { headers: { Authorization: `Bearer ${token}` } });
37
38
  if (!auth?.ok) return void ctx.ui.notify("Token validation failed — check the token and app install.", "error");
38
39
 
39
- const channelId = await promptInput(ctx, "Channel ID", "Paste the Slack channel ID (e.g. C0123456789)");
40
+ const channels = await listSlackChannels(token);
41
+ const MANUAL_OPTION = "Enter channel ID manually";
42
+ let channelId: string;
43
+
44
+ if (!channels || channels.length === 0) {
45
+ ctx.ui.notify("Could not list Slack channels — falling back to manual entry.", "warning");
46
+ channelId = await promptSlackChannelId(ctx) ?? "";
47
+ } else {
48
+ const channelOptions = [...channels.map((channel) => channel.label), MANUAL_OPTION];
49
+ const selectedChannel = await ctx.ui.select("Select a Slack channel", channelOptions);
50
+ if (!selectedChannel) return void ctx.ui.notify("Slack setup cancelled.", "info");
51
+
52
+ if (selectedChannel === MANUAL_OPTION) {
53
+ channelId = await promptSlackChannelId(ctx) ?? "";
54
+ } else {
55
+ const chosen = channels.find((channel) => channel.label === selectedChannel);
56
+ if (!chosen) return void ctx.ui.notify("Slack setup cancelled.", "info");
57
+ channelId = chosen.id;
58
+ }
59
+ }
60
+
40
61
  if (!channelId) return void ctx.ui.notify("Slack setup cancelled.", "info");
41
- if (!isValidChannelId("slack", channelId)) return void ctx.ui.notify("Invalid Slack channel ID format — expected 9-12 uppercase alphanumeric characters.", "error");
42
62
 
43
63
  const send = await fetchJson("https://slack.com/api/chat.postMessage", {
44
64
  method: "POST",
@@ -136,6 +156,32 @@ async function handleSetupDiscord(ctx: ExtensionCommandContext): Promise<void> {
136
156
  ctx.ui.notify(`Discord connected — remote questions enabled for channel ${channelId}.`, "info");
137
157
  }
138
158
 
159
+ async function handleSetupTelegram(ctx: ExtensionCommandContext): Promise<void> {
160
+ const token = await promptMaskedInput(ctx, "Telegram Bot Token", "Paste your bot token from @BotFather");
161
+ if (!token) return void ctx.ui.notify("Telegram setup cancelled.", "info");
162
+ if (!/^\d+:[A-Za-z0-9_-]+$/.test(token)) return void ctx.ui.notify("Invalid token format — Telegram bot tokens look like 123456789:ABCdefGHI...", "warning");
163
+
164
+ ctx.ui.notify("Validating token...", "info");
165
+ const auth = await fetchJson(`https://api.telegram.org/bot${token}/getMe`);
166
+ if (!auth?.ok || !auth?.result?.id) return void ctx.ui.notify("Token validation failed — check the bot token.", "error");
167
+
168
+ const chatId = await promptInput(ctx, "Chat ID", "Paste the Telegram chat ID (e.g. -1001234567890)");
169
+ if (!chatId) return void ctx.ui.notify("Telegram setup cancelled.", "info");
170
+ if (!isValidChannelId("telegram", chatId)) return void ctx.ui.notify("Invalid Telegram chat ID format — expected a numeric ID (can be negative for groups).", "error");
171
+
172
+ const send = await fetchJson(`https://api.telegram.org/bot${token}/sendMessage`, {
173
+ method: "POST",
174
+ headers: { "Content-Type": "application/json" },
175
+ body: JSON.stringify({ chat_id: chatId, text: "GSD remote questions connected." }),
176
+ });
177
+ if (!send?.ok) return void ctx.ui.notify(`Could not send to chat: ${send?.description ?? "unknown error"}`, "error");
178
+
179
+ saveProviderToken("telegram_bot", token);
180
+ process.env.TELEGRAM_BOT_TOKEN = token;
181
+ saveRemoteQuestionsConfig("telegram", chatId);
182
+ ctx.ui.notify(`Telegram connected — remote questions enabled for chat ${chatId}.`, "info");
183
+ }
184
+
139
185
  async function handleRemoteStatus(ctx: ExtensionCommandContext): Promise<void> {
140
186
  const status = getRemoteConfigStatus();
141
187
  const config = resolveRemoteConfig();
@@ -161,9 +207,11 @@ async function handleDisconnect(ctx: ExtensionCommandContext): Promise<void> {
161
207
  if (!channel) return void ctx.ui.notify("No remote channel configured — nothing to disconnect.", "info");
162
208
 
163
209
  removeRemoteQuestionsConfig();
164
- removeProviderToken(channel === "slack" ? "slack_bot" : "discord_bot");
210
+ const providerMap: Record<string, string> = { slack: "slack_bot", discord: "discord_bot", telegram: "telegram_bot" };
211
+ removeProviderToken(providerMap[channel] ?? channel);
165
212
  if (channel === "slack") delete process.env.SLACK_BOT_TOKEN;
166
213
  if (channel === "discord") delete process.env.DISCORD_BOT_TOKEN;
214
+ if (channel === "telegram") delete process.env.TELEGRAM_BOT_TOKEN;
167
215
  ctx.ui.notify(`Remote questions disconnected (${channel}).`, "info");
168
216
  }
169
217
 
@@ -181,6 +229,7 @@ async function handleRemoteMenu(ctx: ExtensionCommandContext): Promise<void> {
181
229
  " /gsd remote disconnect",
182
230
  " /gsd remote slack",
183
231
  " /gsd remote discord",
232
+ " /gsd remote telegram",
184
233
  ]
185
234
  : [
186
235
  "No remote question channel configured.",
@@ -188,6 +237,7 @@ async function handleRemoteMenu(ctx: ExtensionCommandContext): Promise<void> {
188
237
  "Commands:",
189
238
  " /gsd remote slack",
190
239
  " /gsd remote discord",
240
+ " /gsd remote telegram",
191
241
  " /gsd remote status",
192
242
  ];
193
243
 
@@ -203,6 +253,52 @@ async function fetchJson(url: string, init?: RequestInit): Promise<any> {
203
253
  }
204
254
  }
205
255
 
256
+ async function listSlackChannels(token: string): Promise<Array<{ id: string; label: string }> | null> {
257
+ const headers = { Authorization: `Bearer ${token}` };
258
+ const channels: Array<{ id: string; label: string; name: string }> = [];
259
+ let cursor = "";
260
+
261
+ do {
262
+ const params = new URLSearchParams({
263
+ exclude_archived: "true",
264
+ limit: "200",
265
+ types: "public_channel,private_channel",
266
+ });
267
+ if (cursor) params.set("cursor", cursor);
268
+
269
+ const response = await fetchJson(`https://slack.com/api/users.conversations?${params.toString()}`, { headers });
270
+ if (!response?.ok || !Array.isArray(response.channels)) {
271
+ return channels.length > 0 ? channels.map(({ id, label }) => ({ id, label })) : null;
272
+ }
273
+
274
+ for (const channel of response.channels as Array<{ id?: string; name?: string; is_private?: boolean }>) {
275
+ if (!channel.id || !channel.name) continue;
276
+ channels.push({
277
+ id: channel.id,
278
+ name: channel.name,
279
+ label: channel.is_private ? `[private] ${channel.name}` : `#${channel.name}`,
280
+ });
281
+ }
282
+
283
+ cursor = typeof response.response_metadata?.next_cursor === "string"
284
+ ? response.response_metadata.next_cursor
285
+ : "";
286
+ } while (cursor);
287
+
288
+ channels.sort((a, b) => a.name.localeCompare(b.name));
289
+ return channels.map(({ id, label }) => ({ id, label }));
290
+ }
291
+
292
+ async function promptSlackChannelId(ctx: ExtensionCommandContext): Promise<string | null> {
293
+ const channelId = await promptInput(ctx, "Channel ID", "Paste the Slack channel ID (e.g. C0123456789)");
294
+ if (!channelId) return null;
295
+ if (!isValidChannelId("slack", channelId)) {
296
+ ctx.ui.notify("Invalid Slack channel ID format — expected 9-12 uppercase alphanumeric characters.", "error");
297
+ return null;
298
+ }
299
+ return channelId;
300
+ }
301
+
206
302
  function getAuthStorage(): AuthStorage {
207
303
  const authPath = join(process.env.HOME ?? "", ".gsd", "agent", "auth.json");
208
304
  mkdirSync(dirname(authPath), { recursive: true });
@@ -219,7 +315,7 @@ function removeProviderToken(provider: string): void {
219
315
  auth.set(provider, { type: "api_key", key: "" });
220
316
  }
221
317
 
222
- export function saveRemoteQuestionsConfig(channel: "slack" | "discord", channelId: string): void {
318
+ export function saveRemoteQuestionsConfig(channel: "slack" | "discord" | "telegram", channelId: string): void {
223
319
  const prefsPath = getGlobalGSDPreferencesPath();
224
320
  const block = [
225
321
  "remote_questions:",