gsd-pi 2.18.0 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-dashboard.ts +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +148 -39
- package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/dist/resources/extensions/gsd/auto.ts +690 -39
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +654 -36
- package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
- 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 +51 -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 +84 -0
- package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/dist/resources/extensions/gsd/model-router.ts +256 -0
- package/dist/resources/extensions/gsd/notifications.ts +0 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +72 -2
- package/dist/resources/extensions/gsd/preferences.ts +198 -150
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- 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/reassess-roadmap.md +6 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- 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/captures.test.ts +438 -0
- package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -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/feature-branch-lifecycle-integration.test.ts +434 -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/milestone-transition-worktree.test.ts +144 -0
- package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -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 +488 -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/routing-history.test.ts +215 -62
- 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/triage-dispatch.test.ts +224 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -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 +290 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
- 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/triage-resolution.ts +200 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +175 -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 +505 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +337 -0
- package/dist/resources/extensions/gsd/visualizer-views.ts +755 -0
- 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 +35 -4
- package/dist/resources/extensions/remote-questions/format.ts +166 -14
- package/dist/resources/extensions/remote-questions/manager.ts +14 -4
- 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-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +148 -39
- package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/src/resources/extensions/gsd/auto.ts +690 -39
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +654 -36
- package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
- 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 +51 -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 +84 -0
- package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/src/resources/extensions/gsd/model-router.ts +256 -0
- package/src/resources/extensions/gsd/notifications.ts +0 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +72 -2
- package/src/resources/extensions/gsd/preferences.ts +198 -150
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- 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/reassess-roadmap.md +6 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- 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/captures.test.ts +438 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -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/feature-branch-lifecycle-integration.test.ts +434 -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/milestone-transition-worktree.test.ts +144 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -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 +488 -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/routing-history.test.ts +215 -62
- 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/triage-dispatch.test.ts +224 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -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 +290 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
- 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/triage-resolution.ts +200 -0
- package/src/resources/extensions/gsd/triage-ui.ts +175 -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 +505 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +337 -0
- package/src/resources/extensions/gsd/visualizer-views.ts +755 -0
- 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 +35 -4
- package/src/resources/extensions/remote-questions/format.ts +166 -14
- package/src/resources/extensions/remote-questions/manager.ts +14 -4
- 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
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
14
14
|
import { loadPrompt } from "./prompt-loader.js";
|
|
15
15
|
import { autoCommitCurrentBranch } from "./worktree.js";
|
|
16
|
+
import { runWorktreePostCreateHook } from "./auto-worktree.js";
|
|
16
17
|
import { showConfirm } from "../shared/confirm-ui.js";
|
|
17
18
|
import { gsdRoot, milestonesDir } from "./paths.js";
|
|
18
19
|
import {
|
|
@@ -360,6 +361,12 @@ async function handleCreate(
|
|
|
360
361
|
const mainBase = originalCwd ?? basePath;
|
|
361
362
|
const info = createWorktree(mainBase, name);
|
|
362
363
|
|
|
364
|
+
// Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
|
|
365
|
+
const hookError = runWorktreePostCreateHook(mainBase, info.path);
|
|
366
|
+
if (hookError) {
|
|
367
|
+
ctx.ui.notify(hookError, "warning");
|
|
368
|
+
}
|
|
369
|
+
|
|
363
370
|
// Track original cwd before switching
|
|
364
371
|
if (!originalCwd) originalCwd = basePath;
|
|
365
372
|
|
|
@@ -672,6 +679,17 @@ async function handleMerge(
|
|
|
672
679
|
// Try a direct squash-merge first. Only fall back to LLM on conflict.
|
|
673
680
|
const commitType = inferCommitType(name);
|
|
674
681
|
const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
|
|
682
|
+
|
|
683
|
+
// Reconcile worktree DB into main DB before squash merge
|
|
684
|
+
const wtDbPath = join(worktreePath(basePath, name), ".gsd", "gsd.db");
|
|
685
|
+
const mainDbPath = join(basePath, ".gsd", "gsd.db");
|
|
686
|
+
if (existsSync(wtDbPath) && existsSync(mainDbPath)) {
|
|
687
|
+
try {
|
|
688
|
+
const { reconcileWorktreeDb } = await import("./gsd-db.js");
|
|
689
|
+
reconcileWorktreeDb(mainDbPath, wtDbPath);
|
|
690
|
+
} catch { /* non-fatal */ }
|
|
691
|
+
}
|
|
692
|
+
|
|
675
693
|
try {
|
|
676
694
|
mergeWorktreeToMain(basePath, name, commitMessage);
|
|
677
695
|
ctx.ui.notify(
|
|
@@ -94,7 +94,7 @@ export function worktreeBranchName(name: string): string {
|
|
|
94
94
|
*
|
|
95
95
|
* @param opts.branch — override the default `worktree/<name>` branch name
|
|
96
96
|
*/
|
|
97
|
-
export function createWorktree(basePath: string, name: string, opts: { branch?: string; startPoint?: string } = {}): WorktreeInfo {
|
|
97
|
+
export function createWorktree(basePath: string, name: string, opts: { branch?: string; startPoint?: string; reuseExistingBranch?: boolean } = {}): WorktreeInfo {
|
|
98
98
|
// Validate name: alphanumeric, hyphens, underscores only
|
|
99
99
|
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
100
100
|
throw new Error(`Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
|
|
@@ -133,9 +133,16 @@ export function createWorktree(basePath: string, name: string, opts: { branch?:
|
|
|
133
133
|
);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
if (opts.reuseExistingBranch) {
|
|
137
|
+
// Attach worktree to the existing branch as-is (preserving commits).
|
|
138
|
+
// Used when resuming auto-mode: the milestone branch has valid work
|
|
139
|
+
// from prior sessions that must not be reset.
|
|
140
|
+
nativeWorktreeAdd(basePath, wtPath, branch);
|
|
141
|
+
} else {
|
|
142
|
+
// Reset the stale branch to the start point, then attach worktree to it
|
|
143
|
+
nativeBranchForceReset(basePath, branch, startPoint);
|
|
144
|
+
nativeWorktreeAdd(basePath, wtPath, branch);
|
|
145
|
+
}
|
|
139
146
|
} else {
|
|
140
147
|
nativeWorktreeAdd(basePath, wtPath, branch, true, startPoint);
|
|
141
148
|
}
|
|
@@ -16,12 +16,14 @@ export interface ResolvedConfig {
|
|
|
16
16
|
const ENV_KEYS: Record<RemoteChannel, string> = {
|
|
17
17
|
slack: "SLACK_BOT_TOKEN",
|
|
18
18
|
discord: "DISCORD_BOT_TOKEN",
|
|
19
|
+
telegram: "TELEGRAM_BOT_TOKEN",
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
// Channel ID format validation — prevents SSRF if preferences are attacker-controlled
|
|
22
23
|
const CHANNEL_ID_PATTERNS: Record<RemoteChannel, RegExp> = {
|
|
23
24
|
slack: /^[A-Z0-9]{9,12}$/,
|
|
24
25
|
discord: /^\d{17,20}$/,
|
|
26
|
+
telegram: /^-?\d{5,20}$/,
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
const DEFAULT_TIMEOUT_MINUTES = 5;
|
|
@@ -35,7 +37,7 @@ export function resolveRemoteConfig(): ResolvedConfig | null {
|
|
|
35
37
|
const prefs = loadEffectiveGSDPreferences();
|
|
36
38
|
const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
|
|
37
39
|
if (!rq || !rq.channel || !rq.channel_id) return null;
|
|
38
|
-
if (rq.channel !== "slack" && rq.channel !== "discord") return null;
|
|
40
|
+
if (rq.channel !== "slack" && rq.channel !== "discord" && rq.channel !== "telegram") return null;
|
|
39
41
|
|
|
40
42
|
const channelId = String(rq.channel_id);
|
|
41
43
|
if (!CHANNEL_ID_PATTERNS[rq.channel].test(channelId)) return null;
|
|
@@ -59,7 +61,7 @@ export function getRemoteConfigStatus(): string {
|
|
|
59
61
|
const prefs = loadEffectiveGSDPreferences();
|
|
60
62
|
const rq: RemoteQuestionsConfig | undefined = prefs?.preferences.remote_questions;
|
|
61
63
|
if (!rq || !rq.channel || !rq.channel_id) return "Remote questions: not configured";
|
|
62
|
-
if (rq.channel !== "slack" && rq.channel !== "discord") return `Remote questions: unknown channel type \"${rq.channel}\"`;
|
|
64
|
+
if (rq.channel !== "slack" && rq.channel !== "discord" && rq.channel !== "telegram") return `Remote questions: unknown channel type \"${rq.channel}\"`;
|
|
63
65
|
const channelId = String(rq.channel_id);
|
|
64
66
|
if (!CHANNEL_ID_PATTERNS[rq.channel].test(channelId)) return `Remote questions: invalid ${rq.channel} channel ID format`;
|
|
65
67
|
const envVar = ENV_KEYS[rq.channel];
|
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ChannelAdapter, RemotePrompt, RemoteDispatchResult, RemoteAnswer, RemotePromptRef } from "./types.js";
|
|
6
|
-
import { formatForDiscord, parseDiscordResponse } from "./format.js";
|
|
6
|
+
import { formatForDiscord, parseDiscordResponse, DISCORD_NUMBER_EMOJIS } from "./format.js";
|
|
7
7
|
|
|
8
8
|
const DISCORD_API = "https://discord.com/api/v10";
|
|
9
9
|
const PER_REQUEST_TIMEOUT_MS = 15_000;
|
|
10
|
-
const NUMBER_EMOJIS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣"];
|
|
11
|
-
|
|
12
10
|
export class DiscordAdapter implements ChannelAdapter {
|
|
13
11
|
readonly name = "discord" as const;
|
|
14
12
|
private botUserId: string | null = null;
|
|
13
|
+
private guildId: string | null = null;
|
|
15
14
|
private readonly token: string;
|
|
16
15
|
private readonly channelId: string;
|
|
17
16
|
|
|
@@ -24,6 +23,17 @@ export class DiscordAdapter implements ChannelAdapter {
|
|
|
24
23
|
const res = await this.discordApi("GET", "/users/@me");
|
|
25
24
|
if (!res.id) throw new Error("Discord auth failed: invalid token");
|
|
26
25
|
this.botUserId = String(res.id);
|
|
26
|
+
|
|
27
|
+
// Resolve guild ID for message URL generation.
|
|
28
|
+
// The channel belongs to a guild — fetch channel info to discover it.
|
|
29
|
+
try {
|
|
30
|
+
const channelInfo = await this.discordApi("GET", `/channels/${this.channelId}`);
|
|
31
|
+
if (channelInfo.guild_id) {
|
|
32
|
+
this.guildId = String(channelInfo.guild_id);
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Non-fatal — message URLs will be omitted if guild ID can't be resolved
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
async sendPrompt(prompt: RemotePrompt): Promise<RemoteDispatchResult> {
|
|
@@ -46,12 +56,18 @@ export class DiscordAdapter implements ChannelAdapter {
|
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
// Build message URL if guild ID is available
|
|
60
|
+
const messageUrl = this.guildId
|
|
61
|
+
? `https://discord.com/channels/${this.guildId}/${this.channelId}/${messageId}`
|
|
62
|
+
: undefined;
|
|
63
|
+
|
|
49
64
|
return {
|
|
50
65
|
ref: {
|
|
51
66
|
id: prompt.id,
|
|
52
67
|
channel: "discord",
|
|
53
68
|
messageId,
|
|
54
69
|
channelId: this.channelId,
|
|
70
|
+
threadUrl: messageUrl,
|
|
55
71
|
},
|
|
56
72
|
};
|
|
57
73
|
}
|
|
@@ -67,9 +83,24 @@ export class DiscordAdapter implements ChannelAdapter {
|
|
|
67
83
|
return this.checkReplies(prompt, ref);
|
|
68
84
|
}
|
|
69
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Acknowledge that an answer was received by adding a ✅ reaction to the
|
|
88
|
+
* original prompt message. Best-effort — failures are silently ignored.
|
|
89
|
+
*/
|
|
90
|
+
async acknowledgeAnswer(ref: RemotePromptRef): Promise<void> {
|
|
91
|
+
try {
|
|
92
|
+
await this.discordApi(
|
|
93
|
+
"PUT",
|
|
94
|
+
`/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent("✅")}/@me`,
|
|
95
|
+
);
|
|
96
|
+
} catch {
|
|
97
|
+
// Best-effort — don't let acknowledgement failures affect the flow
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
70
101
|
private async checkReactions(prompt: RemotePrompt, ref: RemotePromptRef): Promise<RemoteAnswer | null> {
|
|
71
102
|
const reactions: Array<{ emoji: string; count: number }> = [];
|
|
72
|
-
for (const emoji of
|
|
103
|
+
for (const emoji of DISCORD_NUMBER_EMOJIS) {
|
|
73
104
|
try {
|
|
74
105
|
const users = await this.discordApi("GET", `/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent(emoji)}`);
|
|
75
106
|
if (Array.isArray(users)) {
|
|
@@ -18,7 +18,8 @@ export interface DiscordEmbed {
|
|
|
18
18
|
footer?: { text: string };
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const
|
|
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,23 +94,29 @@ export function formatForDiscord(prompt: RemotePrompt): { embeds: DiscordEmbed[]
|
|
|
64
94
|
const embeds: DiscordEmbed[] = prompt.questions.map((q, questionIndex) => {
|
|
65
95
|
const supportsReactions = prompt.questions.length === 1;
|
|
66
96
|
const optionLines = q.options.map((opt, i) => {
|
|
67
|
-
const emoji =
|
|
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
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
102
|
+
const footerParts: string[] = [];
|
|
103
|
+
if (supportsReactions) {
|
|
104
|
+
footerParts.push(q.allowMultiple
|
|
105
|
+
? "Reply with comma-separated choices (`1,3`) or react with matching numbers"
|
|
106
|
+
: "Reply with a number or react with the matching number");
|
|
107
|
+
} else {
|
|
108
|
+
footerParts.push(`Question ${questionIndex + 1}/${prompt.questions.length} — reply with one line per question or use semicolons`);
|
|
109
|
+
}
|
|
110
|
+
if (prompt.context?.source) {
|
|
111
|
+
footerParts.push(`Source: ${prompt.context.source}`);
|
|
112
|
+
}
|
|
77
113
|
|
|
78
114
|
return {
|
|
79
115
|
title: q.header,
|
|
80
116
|
description: q.question,
|
|
81
117
|
color: 0x7c3aed,
|
|
82
118
|
fields: [{ name: "Options", value: optionLines.join("\n") }],
|
|
83
|
-
footer: { text:
|
|
119
|
+
footer: { text: footerParts.join(" · ") },
|
|
84
120
|
};
|
|
85
121
|
});
|
|
86
122
|
|
|
@@ -124,8 +160,8 @@ export function parseDiscordResponse(
|
|
|
124
160
|
|
|
125
161
|
const q = questions[0];
|
|
126
162
|
const picked = reactions
|
|
127
|
-
.filter((r) =>
|
|
128
|
-
.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)
|
|
129
165
|
.filter(Boolean) as string[];
|
|
130
166
|
|
|
131
167
|
answers[q.id] = picked.length > 0
|
|
@@ -135,6 +171,122 @@ export function parseDiscordResponse(
|
|
|
135
171
|
return { answers };
|
|
136
172
|
}
|
|
137
173
|
|
|
174
|
+
export function parseSlackReactionResponse(
|
|
175
|
+
reactionNames: string[],
|
|
176
|
+
questions: RemoteQuestion[],
|
|
177
|
+
): RemoteAnswer {
|
|
178
|
+
const answers: RemoteAnswer["answers"] = {};
|
|
179
|
+
if (questions.length !== 1) {
|
|
180
|
+
for (const q of questions) {
|
|
181
|
+
answers[q.id] = { answers: [], user_note: "Slack reactions are only supported for single-question prompts" };
|
|
182
|
+
}
|
|
183
|
+
return { answers };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const q = questions[0];
|
|
187
|
+
const picked = reactionNames
|
|
188
|
+
.filter((name) => SLACK_NUMBER_REACTION_NAMES.includes(name))
|
|
189
|
+
.map((name) => q.options[SLACK_NUMBER_REACTION_NAMES.indexOf(name)]?.label)
|
|
190
|
+
.filter(Boolean) as string[];
|
|
191
|
+
|
|
192
|
+
answers[q.id] = picked.length > 0
|
|
193
|
+
? { answers: q.allowMultiple ? picked : [picked[0]] }
|
|
194
|
+
: { answers: [], user_note: "No clear response via reactions" };
|
|
195
|
+
|
|
196
|
+
return { answers };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface TelegramInlineButton {
|
|
200
|
+
text: string;
|
|
201
|
+
callback_data: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface TelegramInlineKeyboardMarkup {
|
|
205
|
+
inline_keyboard: TelegramInlineButton[][];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface TelegramMessage {
|
|
209
|
+
text: string;
|
|
210
|
+
parse_mode: "HTML";
|
|
211
|
+
reply_markup?: TelegramInlineKeyboardMarkup;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function escapeHtml(s: string): string {
|
|
215
|
+
return s.replace(/&/g, "&").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
|
+
|
|
138
290
|
function parseAnswerForQuestion(text: string, q: RemoteQuestion): { answers: string[]; user_note?: string } {
|
|
139
291
|
if (!text) return { answers: [], user_note: "No response provided" };
|
|
140
292
|
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import type { ChannelAdapter, RemotePrompt, RemoteQuestion, RemoteAnswer } from "./types.js";
|
|
7
7
|
import { resolveRemoteConfig, type ResolvedConfig } from "./config.js";
|
|
8
|
-
import { SlackAdapter } from "./slack-adapter.js";
|
|
9
8
|
import { DiscordAdapter } from "./discord-adapter.js";
|
|
9
|
+
import { SlackAdapter } from "./slack-adapter.js";
|
|
10
|
+
import { TelegramAdapter } from "./telegram-adapter.js";
|
|
10
11
|
import { createPromptRecord, writePromptRecord, markPromptAnswered, markPromptDispatched, markPromptStatus, updatePromptRecord } from "./store.js";
|
|
11
12
|
|
|
12
13
|
interface ToolResult {
|
|
@@ -76,6 +77,14 @@ export async function tryRemoteQuestions(
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
markPromptAnswered(prompt.id, answer);
|
|
80
|
+
|
|
81
|
+
// Best-effort acknowledgement gives remote users a visible receipt signal.
|
|
82
|
+
if (dispatch.ref) {
|
|
83
|
+
try {
|
|
84
|
+
await adapter.acknowledgeAnswer?.(dispatch.ref);
|
|
85
|
+
} catch { /* best-effort */ }
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
return {
|
|
80
89
|
content: [{ type: "text", text: JSON.stringify({ answers: formatForTool(answer) }) }],
|
|
81
90
|
details: {
|
|
@@ -111,9 +120,9 @@ function createPrompt(questions: QuestionInput[], config: ResolvedConfig): Remot
|
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
function createAdapter(config: ResolvedConfig): ChannelAdapter {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
if (config.channel === "slack") return new SlackAdapter(config.token, config.channelId);
|
|
124
|
+
if (config.channel === "telegram") return new TelegramAdapter(config.token, config.channelId);
|
|
125
|
+
return new DiscordAdapter(config.token, config.channelId);
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
async function pollUntilDone(
|
|
@@ -173,6 +182,7 @@ const TOKEN_PATTERNS = [
|
|
|
173
182
|
/xoxb-[A-Za-z0-9\-]+/g, // Slack bot tokens
|
|
174
183
|
/xoxp-[A-Za-z0-9\-]+/g, // Slack user tokens
|
|
175
184
|
/xoxa-[A-Za-z0-9\-]+/g, // Slack app tokens
|
|
185
|
+
/\d{8,10}:[A-Za-z0-9_-]{35}/g, // Telegram bot tokens
|
|
176
186
|
/[A-Za-z0-9_\-.]{20,}/g, // Long opaque secrets (Discord tokens, etc.)
|
|
177
187
|
];
|
|
178
188
|
|
|
@@ -21,6 +21,7 @@ export async function handleRemote(
|
|
|
21
21
|
|
|
22
22
|
if (trimmed === "slack") return handleSetupSlack(ctx);
|
|
23
23
|
if (trimmed === "discord") return handleSetupDiscord(ctx);
|
|
24
|
+
if (trimmed === "telegram") return handleSetupTelegram(ctx);
|
|
24
25
|
if (trimmed === "status") return handleRemoteStatus(ctx);
|
|
25
26
|
if (trimmed === "disconnect") return handleDisconnect(ctx);
|
|
26
27
|
|
|
@@ -36,9 +37,28 @@ async function handleSetupSlack(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
36
37
|
const auth = await fetchJson("https://slack.com/api/auth.test", { headers: { Authorization: `Bearer ${token}` } });
|
|
37
38
|
if (!auth?.ok) return void ctx.ui.notify("Token validation failed — check the token and app install.", "error");
|
|
38
39
|
|
|
39
|
-
const
|
|
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:",
|