gsd-pi 2.19.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.
- package/README.md +5 -1
- package/dist/cli.js +3 -3
- package/dist/onboarding.d.ts +3 -1
- package/dist/onboarding.js +77 -3
- package/dist/remote-questions-config.d.ts +1 -1
- package/dist/resources/extensions/google-search/index.ts +164 -47
- package/dist/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/dist/resources/extensions/gsd/auto.ts +424 -30
- package/dist/resources/extensions/gsd/commands.ts +518 -36
- package/dist/resources/extensions/gsd/context-budget.ts +243 -0
- package/dist/resources/extensions/gsd/context-store.ts +195 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/dist/resources/extensions/gsd/db-writer.ts +341 -0
- package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/dist/resources/extensions/gsd/doctor.ts +283 -2
- package/dist/resources/extensions/gsd/export.ts +81 -2
- package/dist/resources/extensions/gsd/files.ts +39 -9
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
- package/dist/resources/extensions/gsd/history.ts +0 -1
- package/dist/resources/extensions/gsd/index.ts +277 -1
- package/dist/resources/extensions/gsd/md-importer.ts +526 -0
- package/dist/resources/extensions/gsd/metrics.ts +39 -3
- package/dist/resources/extensions/gsd/notifications.ts +0 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/dist/resources/extensions/gsd/preferences.ts +125 -150
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/quick.ts +156 -0
- package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/dist/resources/extensions/gsd/skill-health.ts +417 -0
- package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/dist/resources/extensions/gsd/types.ts +29 -0
- package/dist/resources/extensions/gsd/undo.ts +0 -1
- package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/dist/resources/extensions/remote-questions/config.ts +4 -2
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/dist/resources/extensions/remote-questions/format.ts +154 -8
- package/dist/resources/extensions/remote-questions/manager.ts +9 -7
- package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/dist/resources/extensions/remote-questions/types.ts +2 -1
- package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/dist/resources/extensions/voice/index.ts +4 -3
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
- package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
- package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
- package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
- package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
- package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
- package/src/resources/extensions/google-search/index.ts +164 -47
- package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/src/resources/extensions/gsd/auto.ts +424 -30
- package/src/resources/extensions/gsd/commands.ts +518 -36
- package/src/resources/extensions/gsd/context-budget.ts +243 -0
- package/src/resources/extensions/gsd/context-store.ts +195 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/src/resources/extensions/gsd/db-writer.ts +341 -0
- package/src/resources/extensions/gsd/debug-logger.ts +178 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/src/resources/extensions/gsd/doctor.ts +283 -2
- package/src/resources/extensions/gsd/export.ts +81 -2
- package/src/resources/extensions/gsd/files.ts +39 -9
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gsd-db.ts +752 -0
- package/src/resources/extensions/gsd/guided-flow.ts +26 -1
- package/src/resources/extensions/gsd/history.ts +0 -1
- package/src/resources/extensions/gsd/index.ts +277 -1
- package/src/resources/extensions/gsd/md-importer.ts +526 -0
- package/src/resources/extensions/gsd/metrics.ts +39 -3
- package/src/resources/extensions/gsd/notifications.ts +0 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/src/resources/extensions/gsd/preferences.ts +125 -150
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/quick.ts +156 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/src/resources/extensions/gsd/skill-health.ts +417 -0
- package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/src/resources/extensions/gsd/types.ts +29 -0
- package/src/resources/extensions/gsd/undo.ts +0 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/src/resources/extensions/gsd/worktree-command.ts +18 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/src/resources/extensions/remote-questions/config.ts +4 -2
- package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/src/resources/extensions/remote-questions/format.ts +154 -8
- package/src/resources/extensions/remote-questions/manager.ts +9 -7
- package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/src/resources/extensions/remote-questions/types.ts +2 -1
- package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/src/resources/extensions/voice/index.ts +4 -3
|
@@ -18,7 +18,8 @@ export interface DiscordEmbed {
|
|
|
18
18
|
footer?: { text: string };
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const
|
|
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:
|
|
51
|
-
?
|
|
52
|
-
|
|
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,8 +94,8 @@ 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 =
|
|
68
|
-
if (supportsReactions &&
|
|
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
|
|
|
@@ -130,8 +160,33 @@ export function parseDiscordResponse(
|
|
|
130
160
|
|
|
131
161
|
const q = questions[0];
|
|
132
162
|
const picked = reactions
|
|
133
|
-
.filter((r) =>
|
|
134
|
-
.map((r) => q.options[
|
|
163
|
+
.filter((r) => DISCORD_NUMBER_EMOJIS.includes(r.emoji) && r.count > 0)
|
|
164
|
+
.map((r) => q.options[DISCORD_NUMBER_EMOJIS.indexOf(r.emoji)]?.label)
|
|
165
|
+
.filter(Boolean) as string[];
|
|
166
|
+
|
|
167
|
+
answers[q.id] = picked.length > 0
|
|
168
|
+
? { answers: q.allowMultiple ? picked : [picked[0]] }
|
|
169
|
+
: { answers: [], user_note: "No clear response via reactions" };
|
|
170
|
+
|
|
171
|
+
return { answers };
|
|
172
|
+
}
|
|
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)
|
|
135
190
|
.filter(Boolean) as string[];
|
|
136
191
|
|
|
137
192
|
answers[q.id] = picked.length > 0
|
|
@@ -141,6 +196,97 @@ export function parseDiscordResponse(
|
|
|
141
196
|
return { answers };
|
|
142
197
|
}
|
|
143
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
+
|
|
144
290
|
function parseAnswerForQuestion(text: string, q: RemoteQuestion): { answers: string[]; user_note?: string } {
|
|
145
291
|
if (!text) return { answers: [], user_note: "No response provided" };
|
|
146
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 {
|
|
@@ -77,10 +78,10 @@ export async function tryRemoteQuestions(
|
|
|
77
78
|
|
|
78
79
|
markPromptAnswered(prompt.id, answer);
|
|
79
80
|
|
|
80
|
-
//
|
|
81
|
-
if (
|
|
81
|
+
// Best-effort acknowledgement gives remote users a visible receipt signal.
|
|
82
|
+
if (dispatch.ref) {
|
|
82
83
|
try {
|
|
83
|
-
await
|
|
84
|
+
await adapter.acknowledgeAnswer?.(dispatch.ref);
|
|
84
85
|
} catch { /* best-effort */ }
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -119,9 +120,9 @@ function createPrompt(questions: QuestionInput[], config: ResolvedConfig): Remot
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
function createAdapter(config: ResolvedConfig): ChannelAdapter {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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);
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
async function pollUntilDone(
|
|
@@ -181,6 +182,7 @@ const TOKEN_PATTERNS = [
|
|
|
181
182
|
/xoxb-[A-Za-z0-9\-]+/g, // Slack bot tokens
|
|
182
183
|
/xoxp-[A-Za-z0-9\-]+/g, // Slack user tokens
|
|
183
184
|
/xoxa-[A-Za-z0-9\-]+/g, // Slack app tokens
|
|
185
|
+
/\d{8,10}:[A-Za-z0-9_-]{35}/g, // Telegram bot tokens
|
|
184
186
|
/[A-Za-z0-9_\-.]{20,}/g, // Long opaque secrets (Discord tokens, etc.)
|
|
185
187
|
];
|
|
186
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
|
|
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
|
-
|
|
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:",
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
|
|
6
|
-
import { formatForSlack, parseSlackReply } from "./format.js";
|
|
6
|
+
import { formatForSlack, parseSlackReply, parseSlackReactionResponse, SLACK_NUMBER_REACTION_NAMES } from "./format.js";
|
|
7
7
|
|
|
8
8
|
const SLACK_API = "https://slack.com/api";
|
|
9
9
|
const PER_REQUEST_TIMEOUT_MS = 15_000;
|
|
10
|
+
const SLACK_ACK_REACTION = "white_check_mark";
|
|
10
11
|
|
|
11
12
|
export class SlackAdapter implements ChannelAdapter {
|
|
12
13
|
readonly name = "slack" as const;
|
|
@@ -36,6 +37,17 @@ export class SlackAdapter implements ChannelAdapter {
|
|
|
36
37
|
|
|
37
38
|
const ts = String(res.ts);
|
|
38
39
|
const channel = String(res.channel);
|
|
40
|
+
if (prompt.questions.length === 1) {
|
|
41
|
+
const reactionNames = SLACK_NUMBER_REACTION_NAMES.slice(0, prompt.questions[0].options.length);
|
|
42
|
+
for (const name of reactionNames) {
|
|
43
|
+
try {
|
|
44
|
+
await this.slackApi("reactions.add", { channel, timestamp: ts, name });
|
|
45
|
+
} catch {
|
|
46
|
+
// Best-effort only
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
return {
|
|
40
52
|
ref: {
|
|
41
53
|
id: prompt.id,
|
|
@@ -51,6 +63,11 @@ export class SlackAdapter implements ChannelAdapter {
|
|
|
51
63
|
async pollAnswer(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null> {
|
|
52
64
|
if (!this.botUserId) await this.validate();
|
|
53
65
|
|
|
66
|
+
if (prompt.questions.length === 1) {
|
|
67
|
+
const reactionAnswer = await this.checkReactions(prompt, ref);
|
|
68
|
+
if (reactionAnswer) return reactionAnswer;
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
const res = await this.slackApi("conversations.replies", {
|
|
55
72
|
channel: ref.channelId,
|
|
56
73
|
ts: ref.threadTs!,
|
|
@@ -66,9 +83,48 @@ export class SlackAdapter implements ChannelAdapter {
|
|
|
66
83
|
return parseSlackReply(String(userReplies[0].text), prompt.questions);
|
|
67
84
|
}
|
|
68
85
|
|
|
86
|
+
async acknowledgeAnswer(ref: RemotePromptRef): Promise<void> {
|
|
87
|
+
try {
|
|
88
|
+
await this.slackApi("reactions.add", {
|
|
89
|
+
channel: ref.channelId,
|
|
90
|
+
timestamp: ref.messageId,
|
|
91
|
+
name: SLACK_ACK_REACTION,
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
// Best-effort only
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async checkReactions(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null> {
|
|
99
|
+
const res = await this.slackApi("reactions.get", {
|
|
100
|
+
channel: ref.channelId,
|
|
101
|
+
timestamp: ref.messageId,
|
|
102
|
+
full: "true",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!res.ok) return null;
|
|
106
|
+
|
|
107
|
+
const message = (res.message ?? {}) as {
|
|
108
|
+
reactions?: Array<{ name?: string; count?: number; users?: string[] }>;
|
|
109
|
+
};
|
|
110
|
+
const reactions = Array.isArray(message.reactions) ? message.reactions : [];
|
|
111
|
+
const picked = reactions
|
|
112
|
+
.filter((reaction) => reaction.name && SLACK_NUMBER_REACTION_NAMES.includes(reaction.name))
|
|
113
|
+
.filter((reaction) => {
|
|
114
|
+
const count = Number(reaction.count ?? 0);
|
|
115
|
+
const users = Array.isArray(reaction.users) ? reaction.users.map(String) : [];
|
|
116
|
+
const botIncluded = this.botUserId ? users.includes(this.botUserId) : false;
|
|
117
|
+
return count > (botIncluded ? 1 : 0);
|
|
118
|
+
})
|
|
119
|
+
.map((reaction) => String(reaction.name));
|
|
120
|
+
|
|
121
|
+
if (picked.length === 0) return null;
|
|
122
|
+
return parseSlackReactionResponse(picked, prompt.questions);
|
|
123
|
+
}
|
|
124
|
+
|
|
69
125
|
private async slackApi(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
70
126
|
const url = `${SLACK_API}/${method}`;
|
|
71
|
-
const isGet = method === "conversations.replies" || method === "auth.test";
|
|
127
|
+
const isGet = method === "conversations.replies" || method === "auth.test" || method === "reactions.get";
|
|
72
128
|
|
|
73
129
|
let response: Response;
|
|
74
130
|
if (isGet) {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Questions — Telegram adapter
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
|
|
6
|
+
import { formatForTelegram, parseTelegramResponse } from "./format.js";
|
|
7
|
+
|
|
8
|
+
const TELEGRAM_API = "https://api.telegram.org";
|
|
9
|
+
const PER_REQUEST_TIMEOUT_MS = 15_000;
|
|
10
|
+
|
|
11
|
+
export class TelegramAdapter implements ChannelAdapter {
|
|
12
|
+
readonly name = "telegram" as const;
|
|
13
|
+
private botUserId: number | null = null;
|
|
14
|
+
private lastUpdateId = 0;
|
|
15
|
+
private lastSentText = "";
|
|
16
|
+
private readonly token: string;
|
|
17
|
+
private readonly chatId: string;
|
|
18
|
+
|
|
19
|
+
constructor(token: string, chatId: string) {
|
|
20
|
+
this.token = token;
|
|
21
|
+
this.chatId = chatId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async validate(): Promise<void> {
|
|
25
|
+
const res = await this.telegramApi("getMe");
|
|
26
|
+
if (!res.ok || !res.result?.id) throw new Error("Telegram auth failed: invalid bot token");
|
|
27
|
+
this.botUserId = res.result.id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async sendPrompt(prompt: RemotePrompt): Promise<RemoteDispatchResult> {
|
|
31
|
+
const payload = formatForTelegram(prompt);
|
|
32
|
+
this.lastSentText = payload.text;
|
|
33
|
+
|
|
34
|
+
const params: Record<string, unknown> = {
|
|
35
|
+
chat_id: this.chatId,
|
|
36
|
+
text: payload.text,
|
|
37
|
+
parse_mode: payload.parse_mode,
|
|
38
|
+
};
|
|
39
|
+
if (payload.reply_markup) {
|
|
40
|
+
params.reply_markup = payload.reply_markup;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const res = await this.telegramApi("sendMessage", params);
|
|
44
|
+
if (!res.ok || !res.result?.message_id) {
|
|
45
|
+
throw new Error(`Telegram sendMessage failed: ${JSON.stringify(res)}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const messageId = String(res.result.message_id);
|
|
49
|
+
const messageUrl = this.buildMessageUrl(this.chatId, messageId);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
ref: {
|
|
53
|
+
id: prompt.id,
|
|
54
|
+
channel: "telegram",
|
|
55
|
+
messageId,
|
|
56
|
+
channelId: this.chatId,
|
|
57
|
+
threadUrl: messageUrl,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async pollAnswer(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null> {
|
|
63
|
+
if (!this.botUserId) await this.validate();
|
|
64
|
+
|
|
65
|
+
const res = await this.telegramApi("getUpdates", {
|
|
66
|
+
offset: this.lastUpdateId + 1,
|
|
67
|
+
timeout: 0,
|
|
68
|
+
allowed_updates: ["message", "callback_query"],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!res.ok || !Array.isArray(res.result)) return null;
|
|
72
|
+
|
|
73
|
+
for (const update of res.result) {
|
|
74
|
+
// Advance offset for all updates to prevent reprocessing
|
|
75
|
+
if (update.update_id > this.lastUpdateId) {
|
|
76
|
+
this.lastUpdateId = update.update_id;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Handle callback_query (inline keyboard button press)
|
|
80
|
+
if (update.callback_query) {
|
|
81
|
+
const cq = update.callback_query;
|
|
82
|
+
const msg = cq.message;
|
|
83
|
+
if (
|
|
84
|
+
msg &&
|
|
85
|
+
String(msg.chat?.id) === ref.channelId &&
|
|
86
|
+
String(msg.message_id) === ref.messageId &&
|
|
87
|
+
cq.from?.id !== this.botUserId
|
|
88
|
+
) {
|
|
89
|
+
// Dismiss the loading spinner on the button
|
|
90
|
+
try {
|
|
91
|
+
await this.telegramApi("answerCallbackQuery", { callback_query_id: cq.id });
|
|
92
|
+
} catch { /* best-effort */ }
|
|
93
|
+
|
|
94
|
+
return parseTelegramResponse(cq.data ?? null, null, prompt.questions, prompt.id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle text reply (reply_to_message)
|
|
99
|
+
if (update.message) {
|
|
100
|
+
const msg = update.message;
|
|
101
|
+
if (
|
|
102
|
+
String(msg.chat?.id) === ref.channelId &&
|
|
103
|
+
msg.reply_to_message &&
|
|
104
|
+
String(msg.reply_to_message.message_id) === ref.messageId &&
|
|
105
|
+
msg.from?.id !== this.botUserId &&
|
|
106
|
+
msg.text
|
|
107
|
+
) {
|
|
108
|
+
return parseTelegramResponse(null, msg.text, prompt.questions, prompt.id);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Acknowledge receipt by editing the original message to append a checkmark.
|
|
118
|
+
* Best-effort — failures are silently ignored.
|
|
119
|
+
*/
|
|
120
|
+
async acknowledgeAnswer(ref: RemotePromptRef): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
await this.telegramApi("editMessageText", {
|
|
123
|
+
chat_id: ref.channelId,
|
|
124
|
+
message_id: parseInt(ref.messageId, 10),
|
|
125
|
+
text: this.lastSentText + "\n\n✅ Answered",
|
|
126
|
+
parse_mode: "HTML",
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
// Best-effort — don't let acknowledgement failures affect the flow
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private buildMessageUrl(chatId: string, messageId: string): string | undefined {
|
|
134
|
+
// Supergroups have chat IDs starting with -100
|
|
135
|
+
if (chatId.startsWith("-100")) {
|
|
136
|
+
return `https://t.me/c/${chatId.slice(4)}/${messageId}`;
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async telegramApi(method: string, params?: Record<string, unknown>): Promise<any> {
|
|
142
|
+
const url = `${TELEGRAM_API}/bot${this.token}/${method}`;
|
|
143
|
+
const init: RequestInit = {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "Content-Type": "application/json" },
|
|
146
|
+
signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (params) {
|
|
150
|
+
init.body = JSON.stringify(params);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const response = await fetch(url, init);
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
const text = await response.text().catch(() => "");
|
|
156
|
+
const safeText = text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
157
|
+
throw new Error(`Telegram API HTTP ${response.status}: ${safeText}`);
|
|
158
|
+
}
|
|
159
|
+
return response.json();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Remote Questions — shared types
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export type RemoteChannel = "slack" | "discord";
|
|
5
|
+
export type RemoteChannel = "slack" | "discord" | "telegram";
|
|
6
6
|
|
|
7
7
|
export interface RemoteQuestionOption {
|
|
8
8
|
label: string;
|
|
@@ -72,4 +72,5 @@ export interface ChannelAdapter {
|
|
|
72
72
|
validate(): Promise<void>;
|
|
73
73
|
sendPrompt(prompt: RemotePrompt): Promise<RemoteDispatchResult>;
|
|
74
74
|
pollAnswer(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null>;
|
|
75
|
+
acknowledgeAnswer?(ref: RemotePromptRef): Promise<void>;
|
|
75
76
|
}
|