gsd-pi 2.76.0-dev.76f9a2dc5 → 2.76.0-dev.97807402

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 (221) hide show
  1. package/dist/claude-cli-check.js +32 -3
  2. package/dist/mcp-server.d.ts +7 -0
  3. package/dist/mcp-server.js +35 -1
  4. package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
  6. package/dist/resources/extensions/gsd/auto/phases.js +14 -0
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +27 -0
  8. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
  10. package/dist/resources/extensions/gsd/auto-recovery.js +13 -0
  11. package/dist/resources/extensions/gsd/auto-start.js +27 -18
  12. package/dist/resources/extensions/gsd/auto-worktree.js +30 -48
  13. package/dist/resources/extensions/gsd/auto.js +13 -17
  14. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
  15. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  16. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  17. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  18. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +40 -4
  19. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +12 -1
  20. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
  21. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  22. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  23. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  24. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  25. package/dist/resources/extensions/gsd/gsd-db.js +115 -7
  26. package/dist/resources/extensions/gsd/guided-flow.js +189 -0
  27. package/dist/resources/extensions/gsd/health-widget.js +4 -1
  28. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  29. package/dist/resources/extensions/gsd/model-router.js +36 -3
  30. package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -9
  31. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  32. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  33. package/dist/resources/extensions/gsd/preferences.js +17 -17
  34. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  35. package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
  36. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  37. package/dist/resources/extensions/gsd/safety/file-change-validator.js +9 -3
  38. package/dist/resources/extensions/gsd/safety/safety-harness.js +4 -0
  39. package/dist/resources/extensions/gsd/token-counter.js +22 -5
  40. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  41. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  42. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  43. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  44. package/dist/resources/skills/verify-before-complete/SKILL.md +2 -1
  45. package/dist/resources/skills/write-docs/SKILL.md +2 -1
  46. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  47. package/dist/web/standalone/.next/BUILD_ID +1 -1
  48. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  49. package/dist/web/standalone/.next/build-manifest.json +2 -2
  50. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  51. package/dist/web/standalone/.next/required-server-files.json +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.html +1 -1
  69. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  76. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  78. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  79. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  80. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  81. package/dist/web/standalone/server.js +1 -1
  82. package/package.json +1 -1
  83. package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
  84. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
  85. package/packages/mcp-server/dist/remote-questions.js +732 -0
  86. package/packages/mcp-server/dist/remote-questions.js.map +1 -0
  87. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  88. package/packages/mcp-server/dist/server.js +18 -1
  89. package/packages/mcp-server/dist/server.js.map +1 -1
  90. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  91. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  92. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  93. package/packages/mcp-server/package.json +2 -1
  94. package/packages/mcp-server/src/remote-questions.test.ts +294 -0
  95. package/packages/mcp-server/src/remote-questions.ts +916 -0
  96. package/packages/mcp-server/src/server.ts +19 -1
  97. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  98. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  99. package/packages/mcp-server/tsconfig.test.json +19 -0
  100. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  101. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  102. package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -0
  103. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
  105. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  106. package/packages/pi-ai/dist/providers/simple-options.js +16 -1
  107. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  108. package/packages/pi-ai/src/providers/anthropic-shared.ts +3 -1
  109. package/packages/pi-ai/src/providers/simple-options.ts +17 -1
  110. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  111. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
  112. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
  114. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
  117. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  119. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  120. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  121. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  122. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  123. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  125. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  128. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  130. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
  133. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  134. package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
  135. package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
  136. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  137. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  138. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  139. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  140. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
  141. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  142. package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
  143. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
  144. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
  145. package/src/resources/extensions/gsd/auto/phases.ts +14 -0
  146. package/src/resources/extensions/gsd/auto/run-unit.ts +29 -0
  147. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  148. package/src/resources/extensions/gsd/auto-post-unit.ts +1 -2
  149. package/src/resources/extensions/gsd/auto-recovery.ts +15 -0
  150. package/src/resources/extensions/gsd/auto-start.ts +29 -19
  151. package/src/resources/extensions/gsd/auto-worktree.ts +34 -52
  152. package/src/resources/extensions/gsd/auto.ts +12 -17
  153. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
  154. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  155. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  156. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  157. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +42 -4
  158. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +13 -1
  159. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
  160. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  161. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  162. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  163. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  164. package/src/resources/extensions/gsd/gsd-db.ts +122 -7
  165. package/src/resources/extensions/gsd/guided-flow.ts +221 -0
  166. package/src/resources/extensions/gsd/health-widget.ts +3 -1
  167. package/src/resources/extensions/gsd/journal.ts +2 -1
  168. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  169. package/src/resources/extensions/gsd/model-router.ts +42 -1
  170. package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
  171. package/src/resources/extensions/gsd/preferences-types.ts +46 -0
  172. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  173. package/src/resources/extensions/gsd/preferences.ts +17 -17
  174. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  175. package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
  176. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  177. package/src/resources/extensions/gsd/safety/file-change-validator.ts +13 -2
  178. package/src/resources/extensions/gsd/safety/safety-harness.ts +6 -0
  179. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +116 -0
  180. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
  181. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  182. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  183. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  184. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
  185. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +1 -1
  186. package/src/resources/extensions/gsd/tests/escalation.test.ts +1 -1
  187. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  188. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  189. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +38 -0
  190. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +152 -1
  191. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/issue-4540-regressions.test.ts +288 -0
  193. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  194. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  196. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +19 -0
  197. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  198. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
  199. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  200. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
  201. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  202. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
  203. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
  204. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  205. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
  206. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +56 -0
  207. package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
  208. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
  209. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
  210. package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
  211. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  212. package/src/resources/extensions/gsd/token-counter.ts +22 -5
  213. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  214. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  215. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  216. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  217. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  218. package/src/resources/skills/verify-before-complete/SKILL.md +2 -1
  219. package/src/resources/skills/write-docs/SKILL.md +2 -1
  220. /package/dist/web/standalone/.next/static/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_buildManifest.js +0 -0
  221. /package/dist/web/standalone/.next/static/{UMCfv_sVnLXawpUAjvArc → pI48IF3dgfs0CBrYi2bh_}/_ssgManifest.js +0 -0
@@ -0,0 +1,732 @@
1
+ /**
2
+ * Remote Questions — self-contained MCP-server adapter
3
+ *
4
+ * Mirrors the routing logic from src/resources/extensions/ask-user-questions.ts
5
+ * but without any dependency on @gsd/pi-coding-agent or the main src/ tree.
6
+ * All channel adapters (Discord, Slack, Telegram), config resolution, HTTP
7
+ * calls, and polling are inlined here so packages/mcp-server remains a
8
+ * standalone package.
9
+ *
10
+ * Entry points consumed by server.ts:
11
+ * isRemoteConfigured() — cheap synchronous config check
12
+ * tryRemoteQuestions(...) — dispatch + poll + return result
13
+ */
14
+ import { readFileSync } from 'node:fs';
15
+ import { homedir } from 'node:os';
16
+ import { join } from 'node:path';
17
+ import { randomUUID } from 'node:crypto';
18
+ // ---------------------------------------------------------------------------
19
+ // Constants
20
+ // ---------------------------------------------------------------------------
21
+ const PER_REQUEST_TIMEOUT_MS = 15_000;
22
+ const DISCORD_API = 'https://discord.com/api/v10';
23
+ const SLACK_API = 'https://slack.com/api';
24
+ const TELEGRAM_API = 'https://api.telegram.org';
25
+ const DISCORD_NUMBER_EMOJIS = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣'];
26
+ const SLACK_NUMBER_REACTION_NAMES = ['one', 'two', 'three', 'four', 'five'];
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
+ const CHANNEL_ID_PATTERNS = {
34
+ slack: /^[A-Z0-9]{9,12}$/,
35
+ discord: /^\d{17,20}$/,
36
+ telegram: /^-?\d{5,20}$/,
37
+ };
38
+ const ENV_KEYS = {
39
+ slack: 'SLACK_BOT_TOKEN',
40
+ discord: 'DISCORD_BOT_TOKEN',
41
+ telegram: 'TELEGRAM_BOT_TOKEN',
42
+ };
43
+ // ---------------------------------------------------------------------------
44
+ // Config resolution — reads ~/.gsd/PREFERENCES.md YAML frontmatter
45
+ // ---------------------------------------------------------------------------
46
+ function clampNumber(value, fallback, min, max) {
47
+ const n = typeof value === 'number' ? value : Number(value);
48
+ if (!Number.isFinite(n))
49
+ return fallback;
50
+ return Math.max(min, Math.min(max, n));
51
+ }
52
+ /**
53
+ * Minimal YAML frontmatter reader. Handles:
54
+ * ---
55
+ * key: value
56
+ * nested_key:
57
+ * child: value
58
+ * ---
59
+ * Sufficient for the flat remote_questions config block.
60
+ */
61
+ function parseSimpleFrontmatter(content) {
62
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/m);
63
+ if (!match)
64
+ return {};
65
+ const yaml = match[1];
66
+ const result = {};
67
+ let currentSection = null;
68
+ const sectionData = {};
69
+ for (const rawLine of yaml.split('\n')) {
70
+ const line = rawLine.replace(/\r$/, '');
71
+ if (!line.trim() || line.trim().startsWith('#'))
72
+ continue;
73
+ // Top-level key (no indent)
74
+ const topMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)$/);
75
+ if (topMatch) {
76
+ currentSection = topMatch[1];
77
+ const val = topMatch[2].trim();
78
+ if (val) {
79
+ result[currentSection] = parseSimpleScalar(val);
80
+ currentSection = null; // scalar, no children
81
+ }
82
+ else {
83
+ sectionData[currentSection] = {};
84
+ result[currentSection] = sectionData[currentSection];
85
+ }
86
+ continue;
87
+ }
88
+ // Indented child key
89
+ const childMatch = line.match(/^\s+([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)$/);
90
+ if (childMatch && currentSection && sectionData[currentSection]) {
91
+ const childKey = childMatch[1];
92
+ const childVal = childMatch[2].trim();
93
+ sectionData[currentSection][childKey] = parseSimpleScalar(childVal);
94
+ }
95
+ }
96
+ return result;
97
+ }
98
+ function parseSimpleScalar(raw) {
99
+ const s = raw.replace(/^["']|["']$/g, '').trim();
100
+ if (s === 'true')
101
+ return true;
102
+ if (s === 'false')
103
+ return false;
104
+ if (s === 'null' || s === '~')
105
+ return null;
106
+ const n = Number(s);
107
+ if (s !== '' && !Number.isNaN(n))
108
+ return n;
109
+ return s;
110
+ }
111
+ function loadPreferencesFromFile(path) {
112
+ try {
113
+ const content = readFileSync(path, 'utf-8');
114
+ return parseSimpleFrontmatter(content);
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ function resolveRemoteConfig() {
121
+ const gsdHome = process.env['GSD_HOME'] ?? join(homedir(), '.gsd');
122
+ const globalPath = join(gsdHome, 'PREFERENCES.md');
123
+ const prefs = loadPreferencesFromFile(globalPath);
124
+ if (!prefs)
125
+ return null;
126
+ const rq = prefs['remote_questions'];
127
+ if (!rq || !rq['channel'] || !rq['channel_id'])
128
+ return null;
129
+ const channel = String(rq['channel']);
130
+ if (channel !== 'slack' && channel !== 'discord' && channel !== 'telegram')
131
+ return null;
132
+ const channelId = String(rq['channel_id']);
133
+ if (!CHANNEL_ID_PATTERNS[channel].test(channelId))
134
+ return null;
135
+ const token = process.env[ENV_KEYS[channel]];
136
+ if (!token)
137
+ return null;
138
+ const timeoutMs = clampNumber(rq['timeout_minutes'], DEFAULT_TIMEOUT_MINUTES, MIN_TIMEOUT_MINUTES, MAX_TIMEOUT_MINUTES) * 60 * 1000;
139
+ const pollIntervalMs = clampNumber(rq['poll_interval_seconds'], DEFAULT_POLL_INTERVAL_SECONDS, MIN_POLL_INTERVAL_SECONDS, MAX_POLL_INTERVAL_SECONDS) * 1000;
140
+ return { channel, channelId, timeoutMs, pollIntervalMs, token };
141
+ }
142
+ /**
143
+ * Cheap synchronous check — does not make any HTTP requests.
144
+ */
145
+ export function isRemoteConfigured() {
146
+ return resolveRemoteConfig() !== null;
147
+ }
148
+ // ---------------------------------------------------------------------------
149
+ // HTTP helper
150
+ // ---------------------------------------------------------------------------
151
+ async function apiRequest(url, method, body, authScheme, authToken, errorLabel) {
152
+ const headers = {
153
+ Authorization: `${authScheme} ${authToken}`,
154
+ };
155
+ const init = {
156
+ method,
157
+ headers,
158
+ signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
159
+ };
160
+ if (body !== undefined) {
161
+ headers['Content-Type'] = 'application/json';
162
+ init.body = JSON.stringify(body);
163
+ }
164
+ const response = await fetch(url, init);
165
+ if (response.status === 204)
166
+ return {};
167
+ if (!response.ok) {
168
+ const text = await response.text().catch(() => '');
169
+ const safeText = text.length > 200 ? text.slice(0, 200) + '\u2026' : text;
170
+ throw new Error(`${errorLabel} HTTP ${response.status}: ${safeText}`);
171
+ }
172
+ return response.json();
173
+ }
174
+ // ---------------------------------------------------------------------------
175
+ // Payload formatting
176
+ // ---------------------------------------------------------------------------
177
+ function formatForDiscord(prompt) {
178
+ const reactionEmojis = [];
179
+ const embeds = prompt.questions.map((q, questionIndex) => {
180
+ const supportsReactions = prompt.questions.length === 1;
181
+ const optionLines = q.options.map((opt, i) => {
182
+ const emoji = DISCORD_NUMBER_EMOJIS[i] ?? `${i + 1}.`;
183
+ if (supportsReactions && DISCORD_NUMBER_EMOJIS[i])
184
+ reactionEmojis.push(DISCORD_NUMBER_EMOJIS[i]);
185
+ return `${emoji} **${opt.label}** — ${opt.description}`;
186
+ });
187
+ const footerParts = [];
188
+ if (supportsReactions) {
189
+ footerParts.push(q.allowMultiple
190
+ ? 'Reply with comma-separated choices (`1,3`) or react with matching numbers'
191
+ : 'Reply with a number or react with the matching number');
192
+ }
193
+ else {
194
+ footerParts.push(`Question ${questionIndex + 1}/${prompt.questions.length} — reply with one line per question or use semicolons`);
195
+ }
196
+ footerParts.push(`Source: ${prompt.context.source}`);
197
+ return {
198
+ title: q.header,
199
+ description: q.question,
200
+ color: 0x7c3aed,
201
+ fields: [{ name: 'Options', value: optionLines.join('\n') }],
202
+ footer: { text: footerParts.join(' · ') },
203
+ };
204
+ });
205
+ return { embeds, reactionEmojis };
206
+ }
207
+ function formatForSlack(prompt) {
208
+ const blocks = [
209
+ { type: 'header', text: { type: 'plain_text', text: 'GSD needs your input' } },
210
+ ];
211
+ if (prompt.questions.length > 1) {
212
+ blocks.push({
213
+ type: 'context',
214
+ elements: [{ type: 'mrkdwn', text: 'Reply once in thread using one line per question or semicolons (`1; 2; custom note`).' }],
215
+ });
216
+ }
217
+ for (const q of prompt.questions) {
218
+ const supportsReactions = prompt.questions.length === 1;
219
+ blocks.push({ type: 'section', text: { type: 'mrkdwn', text: `*${q.header}*\n${q.question}` } });
220
+ blocks.push({
221
+ type: 'section',
222
+ text: { type: 'mrkdwn', text: q.options.map((opt, i) => `${i + 1}. *${opt.label}* — ${opt.description}`).join('\n') },
223
+ });
224
+ blocks.push({
225
+ type: 'context',
226
+ elements: [{
227
+ type: 'mrkdwn',
228
+ text: prompt.questions.length > 1
229
+ ? (q.allowMultiple ? 'For this question, use comma-separated numbers (`1,3`) or free text.' : 'For this question, use one number (`1`) or free text.')
230
+ : (q.allowMultiple
231
+ ? (supportsReactions ? 'Reply in thread with comma-separated numbers (`1,3`) or react with matching number emoji.' : 'Reply in thread with comma-separated numbers (`1,3`) or free text.')
232
+ : (supportsReactions ? 'Reply in thread with a number (`1`) or react with the matching number emoji.' : 'Reply in thread with a number (`1`) or free text.')),
233
+ }],
234
+ });
235
+ blocks.push({ type: 'divider' });
236
+ }
237
+ return blocks;
238
+ }
239
+ function formatForTelegram(prompt) {
240
+ const escape = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
241
+ const lines = ['<b>GSD needs your input</b>', ''];
242
+ for (let qi = 0; qi < prompt.questions.length; qi++) {
243
+ const q = prompt.questions[qi];
244
+ lines.push(`<b>${escape(q.header)}</b>`);
245
+ lines.push(escape(q.question));
246
+ lines.push('');
247
+ for (let i = 0; i < q.options.length; i++) {
248
+ lines.push(`${i + 1}. <b>${escape(q.options[i].label)}</b> — ${escape(q.options[i].description)}`);
249
+ }
250
+ lines.push('');
251
+ if (prompt.questions.length === 1) {
252
+ lines.push(q.allowMultiple ? 'Reply with comma-separated numbers (1,3) or free text.' : 'Reply with a number or tap a button below.');
253
+ }
254
+ else {
255
+ lines.push(`Question ${qi + 1}/${prompt.questions.length} — reply with one line per question or use semicolons.`);
256
+ }
257
+ if (qi < prompt.questions.length - 1)
258
+ lines.push('');
259
+ }
260
+ const result = {
261
+ text: lines.join('\n'),
262
+ parse_mode: 'HTML',
263
+ };
264
+ if (prompt.questions.length === 1 && prompt.questions[0].options.length <= 5) {
265
+ result.reply_markup = {
266
+ inline_keyboard: prompt.questions[0].options.map((opt, i) => [{
267
+ text: `${i + 1}. ${opt.label}`,
268
+ callback_data: `${prompt.id}:${i}`,
269
+ }]),
270
+ };
271
+ }
272
+ return result;
273
+ }
274
+ // ---------------------------------------------------------------------------
275
+ // Response parsing
276
+ // ---------------------------------------------------------------------------
277
+ function parseAnswerForQuestion(text, q) {
278
+ if (!text)
279
+ return { answers: [], user_note: 'No response provided' };
280
+ if (/^[\d,\s]+$/.test(text)) {
281
+ const nums = text
282
+ .split(',')
283
+ .map((s) => parseInt(s.trim(), 10))
284
+ .filter((n) => !Number.isNaN(n) && n >= 1 && n <= q.options.length);
285
+ if (nums.length > 0) {
286
+ const selected = nums.map((n) => q.options[n - 1].label);
287
+ return { answers: q.allowMultiple ? selected : [selected[0]] };
288
+ }
289
+ }
290
+ const single = parseInt(text, 10);
291
+ if (!Number.isNaN(single) && single >= 1 && single <= q.options.length) {
292
+ return { answers: [q.options[single - 1].label] };
293
+ }
294
+ const truncated = text.length > 500 ? text.slice(0, 500) + '\u2026' : text;
295
+ return { answers: [], user_note: truncated };
296
+ }
297
+ function parseTextReply(text, questions) {
298
+ const answers = {};
299
+ const trimmed = text.trim();
300
+ if (questions.length === 1) {
301
+ answers[questions[0].id] = parseAnswerForQuestion(trimmed, questions[0]);
302
+ return { answers };
303
+ }
304
+ const parts = trimmed.includes(';')
305
+ ? trimmed.split(';').map((s) => s.trim()).filter(Boolean)
306
+ : trimmed.split('\n').map((s) => s.trim()).filter(Boolean);
307
+ for (let i = 0; i < questions.length; i++) {
308
+ answers[questions[i].id] = parseAnswerForQuestion(parts[i] ?? '', questions[i]);
309
+ }
310
+ return { answers };
311
+ }
312
+ function parseDiscordReactions(reactions, questions) {
313
+ const answers = {};
314
+ if (questions.length !== 1) {
315
+ for (const q of questions) {
316
+ answers[q.id] = { answers: [], user_note: 'Discord reactions are only supported for single-question prompts' };
317
+ }
318
+ return { answers };
319
+ }
320
+ const q = questions[0];
321
+ const picked = reactions
322
+ .filter((r) => DISCORD_NUMBER_EMOJIS.includes(r.emoji) && r.count > 0)
323
+ .map((r) => q.options[DISCORD_NUMBER_EMOJIS.indexOf(r.emoji)]?.label)
324
+ .filter((l) => Boolean(l));
325
+ answers[q.id] = picked.length > 0
326
+ ? { answers: q.allowMultiple ? picked : [picked[0]] }
327
+ : { answers: [], user_note: 'No clear response via reactions' };
328
+ return { answers };
329
+ }
330
+ function parseSlackReactions(reactionNames, questions) {
331
+ const answers = {};
332
+ if (questions.length !== 1) {
333
+ for (const q of questions) {
334
+ answers[q.id] = { answers: [], user_note: 'Slack reactions are only supported for single-question prompts' };
335
+ }
336
+ return { answers };
337
+ }
338
+ const q = questions[0];
339
+ const picked = reactionNames
340
+ .filter((name) => SLACK_NUMBER_REACTION_NAMES.includes(name))
341
+ .map((name) => q.options[SLACK_NUMBER_REACTION_NAMES.indexOf(name)]?.label)
342
+ .filter((l) => Boolean(l));
343
+ answers[q.id] = picked.length > 0
344
+ ? { answers: q.allowMultiple ? picked : [picked[0]] }
345
+ : { answers: [], user_note: 'No clear response via reactions' };
346
+ return { answers };
347
+ }
348
+ function parseTelegramCallbackData(callbackData, questions, promptId) {
349
+ const pattern = new RegExp(`^${promptId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:(\\d+)$`);
350
+ const match = callbackData.match(pattern);
351
+ if (match && questions.length === 1) {
352
+ const idx = parseInt(match[1], 10);
353
+ const q = questions[0];
354
+ if (idx >= 0 && idx < q.options.length) {
355
+ return { answers: { [q.id]: { answers: [q.options[idx].label] } } };
356
+ }
357
+ }
358
+ return null;
359
+ }
360
+ // --- Discord ---
361
+ async function discordValidate(token, channelId) {
362
+ const meRes = await apiRequest(`${DISCORD_API}/users/@me`, 'GET', undefined, 'Bot', token, 'Discord API');
363
+ if (!meRes['id'])
364
+ throw new Error('Discord auth failed: invalid token');
365
+ const botUserId = String(meRes['id']);
366
+ let guildId = null;
367
+ try {
368
+ const chanRes = await apiRequest(`${DISCORD_API}/channels/${channelId}`, 'GET', undefined, 'Bot', token, 'Discord API');
369
+ if (chanRes['guild_id'])
370
+ guildId = String(chanRes['guild_id']);
371
+ }
372
+ catch { /* non-fatal */ }
373
+ return { botUserId, guildId };
374
+ }
375
+ async function discordSend(prompt, token, channelId, guildId) {
376
+ const { embeds, reactionEmojis } = formatForDiscord(prompt);
377
+ const res = await apiRequest(`${DISCORD_API}/channels/${channelId}/messages`, 'POST', { content: '**GSD needs your input** — reply to this message with your answer', embeds }, 'Bot', token, 'Discord API');
378
+ if (!res['id'])
379
+ throw new Error(`Discord send failed: ${JSON.stringify(res)}`);
380
+ const messageId = String(res['id']);
381
+ if (prompt.questions.length === 1) {
382
+ for (const emoji of reactionEmojis) {
383
+ try {
384
+ await apiRequest(`${DISCORD_API}/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`, 'PUT', undefined, 'Bot', token, 'Discord API');
385
+ }
386
+ catch { /* best-effort */ }
387
+ }
388
+ }
389
+ const threadUrl = guildId ? `https://discord.com/channels/${guildId}/${channelId}/${messageId}` : undefined;
390
+ return { ref: { id: prompt.id, channel: 'discord', messageId, channelId, threadUrl } };
391
+ }
392
+ async function discordPoll(prompt, ref, token, botUserId) {
393
+ // Try reactions first for single-question prompts
394
+ if (prompt.questions.length === 1) {
395
+ const reactions = [];
396
+ for (const emoji of DISCORD_NUMBER_EMOJIS) {
397
+ try {
398
+ const users = await apiRequest(`${DISCORD_API}/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent(emoji)}`, 'GET', undefined, 'Bot', token, 'Discord API');
399
+ if (Array.isArray(users)) {
400
+ const humanUsers = users.filter((u) => u['id'] !== botUserId);
401
+ if (humanUsers.length > 0)
402
+ reactions.push({ emoji, count: humanUsers.length });
403
+ }
404
+ }
405
+ catch (err) {
406
+ const msg = String(err.message ?? '');
407
+ if (msg.includes('HTTP 404'))
408
+ continue;
409
+ if (msg.includes('HTTP 401') || msg.includes('HTTP 403'))
410
+ throw err;
411
+ }
412
+ }
413
+ if (reactions.length > 0)
414
+ return parseDiscordReactions(reactions, prompt.questions);
415
+ }
416
+ // Try text replies
417
+ const messages = await apiRequest(`${DISCORD_API}/channels/${ref.channelId}/messages?after=${ref.messageId}&limit=10`, 'GET', undefined, 'Bot', token, 'Discord API');
418
+ if (!Array.isArray(messages))
419
+ return null;
420
+ const replies = messages.filter((m) => {
421
+ const msg = m;
422
+ const author = msg['author'];
423
+ const msgRef = msg['message_reference'];
424
+ return author?.['id'] && author['id'] !== botUserId && msgRef?.['message_id'] === ref.messageId && msg['content'];
425
+ });
426
+ if (replies.length === 0)
427
+ return null;
428
+ const first = replies[0];
429
+ return parseTextReply(String(first['content']), prompt.questions);
430
+ }
431
+ async function discordAcknowledge(ref, token) {
432
+ try {
433
+ await apiRequest(`${DISCORD_API}/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent('✅')}/@me`, 'PUT', undefined, 'Bot', token, 'Discord API');
434
+ }
435
+ catch { /* best-effort */ }
436
+ }
437
+ // --- Slack ---
438
+ async function slackValidate(token) {
439
+ const res = await apiRequest(`${SLACK_API}/auth.test`, 'GET', undefined, 'Bearer', token, 'Slack API');
440
+ if (!res['ok'])
441
+ throw new Error(`Slack auth failed: ${res['error'] ?? 'invalid token'}`);
442
+ return String(res['user_id'] ?? '');
443
+ }
444
+ async function slackSend(prompt, token, channelId) {
445
+ const res = await apiRequest(`${SLACK_API}/chat.postMessage`, 'POST', { channel: channelId, text: 'GSD needs your input', blocks: formatForSlack(prompt) }, 'Bearer', token, 'Slack API');
446
+ if (!res['ok'])
447
+ throw new Error(`Slack postMessage failed: ${res['error'] ?? 'unknown'}`);
448
+ const ts = String(res['ts']);
449
+ const channel = String(res['channel']);
450
+ if (prompt.questions.length === 1) {
451
+ const reactionNames = SLACK_NUMBER_REACTION_NAMES.slice(0, prompt.questions[0].options.length);
452
+ for (const name of reactionNames) {
453
+ try {
454
+ await apiRequest(`${SLACK_API}/reactions.add`, 'POST', { channel, timestamp: ts, name }, 'Bearer', token, 'Slack API');
455
+ }
456
+ catch { /* best-effort */ }
457
+ }
458
+ }
459
+ return {
460
+ ref: {
461
+ id: prompt.id,
462
+ channel: 'slack',
463
+ messageId: ts,
464
+ threadTs: ts,
465
+ channelId: channel,
466
+ threadUrl: `https://slack.com/archives/${channel}/p${ts.replace('.', '')}`,
467
+ },
468
+ };
469
+ }
470
+ async function slackPoll(prompt, ref, token, botUserId) {
471
+ // Check reactions for single-question prompts
472
+ if (prompt.questions.length === 1) {
473
+ const qs = new URLSearchParams({ channel: ref.channelId, timestamp: ref.messageId, full: 'true' }).toString();
474
+ const res = await apiRequest(`${SLACK_API}/reactions.get?${qs}`, 'GET', undefined, 'Bearer', token, 'Slack API');
475
+ if (res['ok']) {
476
+ const message = (res['message'] ?? {});
477
+ const reactions = Array.isArray(message.reactions) ? message.reactions : [];
478
+ const picked = reactions
479
+ .filter((r) => r.name && SLACK_NUMBER_REACTION_NAMES.includes(r.name))
480
+ .filter((r) => {
481
+ const count = Number(r.count ?? 0);
482
+ const users = Array.isArray(r.users) ? r.users.map(String) : [];
483
+ const botIncluded = botUserId ? users.includes(botUserId) : false;
484
+ return count > (botIncluded ? 1 : 0);
485
+ })
486
+ .map((r) => String(r.name));
487
+ if (picked.length > 0)
488
+ return parseSlackReactions(picked, prompt.questions);
489
+ }
490
+ }
491
+ // Check thread replies
492
+ const qs = new URLSearchParams({ channel: ref.channelId, ts: ref.threadTs, limit: '20' }).toString();
493
+ const res = await apiRequest(`${SLACK_API}/conversations.replies?${qs}`, 'GET', undefined, 'Bearer', token, 'Slack API');
494
+ if (!res['ok'])
495
+ return null;
496
+ const messages = (res['messages'] ?? []);
497
+ const userReplies = messages.filter((m) => m.ts !== ref.threadTs && m.user && m.user !== botUserId && m.text);
498
+ if (userReplies.length === 0)
499
+ return null;
500
+ return parseTextReply(String(userReplies[0].text), prompt.questions);
501
+ }
502
+ async function slackAcknowledge(ref, token) {
503
+ try {
504
+ await apiRequest(`${SLACK_API}/reactions.add`, 'POST', { channel: ref.channelId, timestamp: ref.messageId, name: 'white_check_mark' }, 'Bearer', token, 'Slack API');
505
+ }
506
+ catch { /* best-effort */ }
507
+ }
508
+ // --- Telegram ---
509
+ async function telegramValidate(token) {
510
+ const res = await apiRequest(`${TELEGRAM_API}/bot${token}/getMe`, 'GET', undefined, 'Bearer', token, 'Telegram API');
511
+ const result = res['result'];
512
+ if (!res['ok'] || !result?.['id'])
513
+ throw new Error('Telegram auth failed: invalid bot token');
514
+ return result['id'];
515
+ }
516
+ async function telegramSend(prompt, token, chatId) {
517
+ const payload = formatForTelegram(prompt);
518
+ const params = { chat_id: chatId, text: payload.text, parse_mode: payload.parse_mode };
519
+ if (payload.reply_markup)
520
+ params['reply_markup'] = payload.reply_markup;
521
+ const res = await apiRequest(`${TELEGRAM_API}/bot${token}/sendMessage`, 'POST', params, 'Bearer', token, 'Telegram API');
522
+ const result = res['result'];
523
+ if (!res['ok'] || !result?.['message_id'])
524
+ throw new Error(`Telegram sendMessage failed: ${JSON.stringify(res)}`);
525
+ const messageId = String(result['message_id']);
526
+ // Build public URL only for public channels (negative IDs are private groups)
527
+ const isPublic = !chatId.startsWith('-');
528
+ const messageUrl = isPublic ? `https://t.me/${chatId.replace('@', '')}/${messageId}` : undefined;
529
+ return { ref: { id: prompt.id, channel: 'telegram', messageId, channelId: chatId, threadUrl: messageUrl } };
530
+ }
531
+ async function telegramPoll(prompt, ref, token, botUserId, lastUpdateId) {
532
+ const params = {
533
+ offset: lastUpdateId.value + 1,
534
+ timeout: 0,
535
+ allowed_updates: ['message', 'callback_query'],
536
+ };
537
+ const res = await apiRequest(`${TELEGRAM_API}/bot${token}/getUpdates`, 'POST', params, 'Bearer', token, 'Telegram API');
538
+ if (!res['ok'] || !Array.isArray(res['result']))
539
+ return null;
540
+ for (const update of res['result']) {
541
+ if (update['update_id'] > lastUpdateId.value) {
542
+ lastUpdateId.value = update['update_id'];
543
+ }
544
+ // Callback query (inline keyboard button press)
545
+ if (update['callback_query']) {
546
+ const cq = update['callback_query'];
547
+ const msg = cq['message'];
548
+ const from = cq['from'];
549
+ if (msg && String(msg['chat']?.['id']) === ref.channelId &&
550
+ String(msg['message_id']) === ref.messageId && from?.['id'] !== botUserId) {
551
+ // Dismiss loading spinner
552
+ try {
553
+ await apiRequest(`${TELEGRAM_API}/bot${token}/answerCallbackQuery`, 'POST', { callback_query_id: cq['id'] }, 'Bearer', token, 'Telegram API');
554
+ }
555
+ catch { /* best-effort */ }
556
+ const callbackData = cq['data'] ? String(cq['data']) : null;
557
+ if (callbackData) {
558
+ const parsed = parseTelegramCallbackData(callbackData, prompt.questions, prompt.id);
559
+ if (parsed)
560
+ return parsed;
561
+ }
562
+ }
563
+ }
564
+ // Text message reply
565
+ if (update['message']) {
566
+ const msg = update['message'];
567
+ const from = msg['from'];
568
+ if (String(msg['chat']?.['id']) === ref.channelId &&
569
+ from?.['id'] !== botUserId && msg['text']) {
570
+ return parseTextReply(String(msg['text']), prompt.questions);
571
+ }
572
+ }
573
+ }
574
+ return null;
575
+ }
576
+ // ---------------------------------------------------------------------------
577
+ // Polling loop
578
+ // ---------------------------------------------------------------------------
579
+ function sleep(ms, signal) {
580
+ return new Promise((resolve) => {
581
+ if (signal?.aborted)
582
+ return resolve();
583
+ const timer = setTimeout(() => {
584
+ signal?.removeEventListener('abort', onAbort);
585
+ resolve();
586
+ }, ms);
587
+ const onAbort = () => { clearTimeout(timer); resolve(); };
588
+ signal?.addEventListener('abort', onAbort, { once: true });
589
+ });
590
+ }
591
+ async function pollUntilDone(config, prompt, ref, state, signal) {
592
+ while (Date.now() < prompt.timeoutAt && !signal?.aborted) {
593
+ try {
594
+ let answer = null;
595
+ if (config.channel === 'discord') {
596
+ answer = await discordPoll(prompt, ref, config.token, String(state.botUserId));
597
+ }
598
+ else if (config.channel === 'slack') {
599
+ answer = await slackPoll(prompt, ref, config.token, String(state.botUserId));
600
+ }
601
+ else {
602
+ answer = await telegramPoll(prompt, ref, config.token, state.botUserId, state.lastUpdateId);
603
+ }
604
+ if (answer)
605
+ return answer;
606
+ }
607
+ catch {
608
+ // Non-fatal poll error — wait and retry
609
+ }
610
+ await sleep(prompt.pollIntervalMs, signal);
611
+ }
612
+ return null;
613
+ }
614
+ // ---------------------------------------------------------------------------
615
+ // Public entry point
616
+ // ---------------------------------------------------------------------------
617
+ function buildPrompt(questions, config) {
618
+ const createdAt = Date.now();
619
+ return {
620
+ id: randomUUID(),
621
+ channel: config.channel,
622
+ createdAt,
623
+ timeoutAt: createdAt + config.timeoutMs,
624
+ pollIntervalMs: config.pollIntervalMs,
625
+ context: { source: 'ask_user_questions' },
626
+ questions: questions.map((q) => ({
627
+ id: q.id,
628
+ header: q.header,
629
+ question: q.question,
630
+ options: q.options,
631
+ allowMultiple: q.allowMultiple ?? false,
632
+ })),
633
+ };
634
+ }
635
+ function formatForTool(answer) {
636
+ const out = {};
637
+ for (const [id, data] of Object.entries(answer.answers)) {
638
+ const list = [...data.answers];
639
+ if (data.user_note)
640
+ list.push(`user_note: ${data.user_note}`);
641
+ out[id] = { answers: list };
642
+ }
643
+ return out;
644
+ }
645
+ /**
646
+ * Dispatch questions to the configured remote channel and wait for a response.
647
+ *
648
+ * Returns null when no remote channel is configured.
649
+ * Returns a tool result shaped like { content, details } on success or
650
+ * timeout — callers should check details.timed_out before trusting the result.
651
+ */
652
+ export async function tryRemoteQuestions(questions, signal) {
653
+ const config = resolveRemoteConfig();
654
+ if (!config)
655
+ return null;
656
+ const prompt = buildPrompt(questions, config);
657
+ // Validate auth and send the prompt
658
+ let ref;
659
+ let state;
660
+ try {
661
+ if (config.channel === 'discord') {
662
+ const { botUserId, guildId } = await discordValidate(config.token, config.channelId);
663
+ state = { botUserId, guildId };
664
+ const dispatch = await discordSend(prompt, config.token, config.channelId, guildId);
665
+ ref = dispatch.ref;
666
+ }
667
+ else if (config.channel === 'slack') {
668
+ const botUserId = await slackValidate(config.token);
669
+ state = { botUserId };
670
+ const dispatch = await slackSend(prompt, config.token, config.channelId);
671
+ ref = dispatch.ref;
672
+ }
673
+ else {
674
+ const botUserId = await telegramValidate(config.token);
675
+ state = { botUserId, lastUpdateId: { value: 0 } };
676
+ const dispatch = await telegramSend(prompt, config.token, config.channelId);
677
+ ref = dispatch.ref;
678
+ }
679
+ }
680
+ catch (err) {
681
+ return {
682
+ content: [{ type: 'text', text: `Remote questions failed (${config.channel}): ${err.message}` }],
683
+ details: { remote: true, channel: config.channel, error: true, status: 'failed' },
684
+ };
685
+ }
686
+ const answer = await pollUntilDone(config, prompt, ref, state, signal);
687
+ if (!answer) {
688
+ const timedOut = !signal?.aborted;
689
+ return {
690
+ content: [{
691
+ type: 'text',
692
+ text: JSON.stringify({
693
+ timed_out: timedOut,
694
+ channel: config.channel,
695
+ prompt_id: prompt.id,
696
+ timeout_minutes: config.timeoutMs / 60000,
697
+ thread_url: ref.threadUrl ?? null,
698
+ message: `User did not respond within ${config.timeoutMs / 60000} minutes.`,
699
+ }),
700
+ }],
701
+ details: {
702
+ remote: true,
703
+ channel: config.channel,
704
+ timed_out: timedOut,
705
+ promptId: prompt.id,
706
+ threadUrl: ref.threadUrl ?? null,
707
+ status: signal?.aborted ? 'cancelled' : 'timed_out',
708
+ },
709
+ };
710
+ }
711
+ // Best-effort acknowledgement
712
+ try {
713
+ if (config.channel === 'discord')
714
+ await discordAcknowledge(ref, config.token);
715
+ else if (config.channel === 'slack')
716
+ await slackAcknowledge(ref, config.token);
717
+ }
718
+ catch { /* best-effort */ }
719
+ return {
720
+ content: [{ type: 'text', text: JSON.stringify({ answers: formatForTool(answer) }) }],
721
+ details: {
722
+ remote: true,
723
+ channel: config.channel,
724
+ timed_out: false,
725
+ promptId: prompt.id,
726
+ threadUrl: ref.threadUrl ?? null,
727
+ questions,
728
+ status: 'answered',
729
+ },
730
+ };
731
+ }
732
+ //# sourceMappingURL=remote-questions.js.map