gsd-pi 2.29.0-dev.7612840 → 2.29.0-dev.953d788
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/dist/resources/extensions/gsd/auto.ts +15 -0
- package/dist/resources/extensions/gsd/commands.ts +1 -2
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +1 -2
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +19 -8
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +17 -11
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +19 -8
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto.ts +15 -0
- package/src/resources/extensions/gsd/commands.ts +1 -2
- package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -2
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/src/resources/extensions/remote-questions/discord-adapter.ts +19 -8
- package/src/resources/extensions/remote-questions/slack-adapter.ts +17 -11
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +19 -8
- package/dist/resources/extensions/remote-questions/http-client.ts +0 -76
- package/src/resources/extensions/remote-questions/http-client.ts +0 -76
|
@@ -22,6 +22,7 @@ import { loadFile, getManifestStatus, resolveAllOverrides, parsePlan, parseSumma
|
|
|
22
22
|
import { loadPrompt } from "./prompt-loader.js";
|
|
23
23
|
import { runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit } from "./verification-gate.js";
|
|
24
24
|
import { writeVerificationJSON } from "./verification-evidence.js";
|
|
25
|
+
export { inlinePriorMilestoneSummary } from "./files.js";
|
|
25
26
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
26
27
|
import {
|
|
27
28
|
gsdRoot, resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
|
|
@@ -191,6 +192,12 @@ import {
|
|
|
191
192
|
NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
|
|
192
193
|
} from "./auto/session.js";
|
|
193
194
|
import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerificationRetry } from "./auto/session.js";
|
|
195
|
+
export {
|
|
196
|
+
MAX_UNIT_DISPATCHES, STUB_RECOVERY_THRESHOLD, MAX_LIFETIME_DISPATCHES,
|
|
197
|
+
MAX_CONSECUTIVE_SKIPS, DISPATCH_GAP_TIMEOUT_MS, MAX_SKIP_DEPTH,
|
|
198
|
+
NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
|
|
199
|
+
} from "./auto/session.js";
|
|
200
|
+
export type { CompletedUnit, CurrentUnit, UnitRouting, StartModel } from "./auto/session.js";
|
|
194
201
|
|
|
195
202
|
// ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
|
|
196
203
|
// ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
|
|
@@ -261,6 +268,8 @@ export function shouldUseWorktreeIsolation(): boolean {
|
|
|
261
268
|
* Maps toolCallId → start timestamp (ms) so the idle watchdog can detect tools that have been
|
|
262
269
|
* running suspiciously long (e.g., a Bash command hung because `&` kept stdout open).
|
|
263
270
|
*/
|
|
271
|
+
// Re-export budget utilities for external consumers
|
|
272
|
+
export { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction } from "./auto-budget.js";
|
|
264
273
|
|
|
265
274
|
/** Wrapper: register SIGTERM handler and store reference. */
|
|
266
275
|
function registerSigtermHandler(currentBasePath: string): void {
|
|
@@ -273,6 +282,8 @@ function deregisterSigtermHandler(): void {
|
|
|
273
282
|
s.sigtermHandler = null;
|
|
274
283
|
}
|
|
275
284
|
|
|
285
|
+
export { type AutoDashboardData } from "./auto-dashboard.js";
|
|
286
|
+
|
|
276
287
|
export function getAutoDashboardData(): AutoDashboardData {
|
|
277
288
|
const ledger = getLedger();
|
|
278
289
|
const totals = ledger ? getProjectTotals(ledger.units) : null;
|
|
@@ -923,6 +934,8 @@ async function showStepWizard(
|
|
|
923
934
|
}
|
|
924
935
|
}
|
|
925
936
|
|
|
937
|
+
// describeNextUnit is imported from auto-dashboard.ts and re-exported
|
|
938
|
+
export { describeNextUnit } from "./auto-dashboard.js";
|
|
926
939
|
|
|
927
940
|
/** Thin wrapper: delegates to auto-dashboard.ts, passing state accessors. */
|
|
928
941
|
function updateProgressWidget(
|
|
@@ -1892,3 +1905,5 @@ export async function dispatchHookUnit(
|
|
|
1892
1905
|
}
|
|
1893
1906
|
|
|
1894
1907
|
|
|
1908
|
+
// Direct phase dispatch → auto-direct-dispatch.ts
|
|
1909
|
+
export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
|
|
@@ -13,8 +13,7 @@ import { deriveState } from "./state.js";
|
|
|
13
13
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
14
14
|
import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
|
|
15
15
|
import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
|
|
16
|
-
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
|
|
17
|
-
import { dispatchDirectPhase } from "./auto-direct-dispatch.js";
|
|
16
|
+
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote, dispatchDirectPhase } from "./auto.js";
|
|
18
17
|
import { resolveProjectRoot } from "./worktree.js";
|
|
19
18
|
import { assertSafeDirectory } from "./validate-directory.js";
|
|
20
19
|
import {
|
|
@@ -11,8 +11,7 @@ import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
|
11
11
|
import { deriveState } from "./state.js";
|
|
12
12
|
import { loadFile, parseRoadmap, parsePlan } from "./files.js";
|
|
13
13
|
import { resolveMilestoneFile, resolveSliceFile } from "./paths.js";
|
|
14
|
-
import { getAutoDashboardData } from "./auto.js";
|
|
15
|
-
import type { AutoDashboardData } from "./auto-dashboard.js";
|
|
14
|
+
import { getAutoDashboardData, type AutoDashboardData } from "./auto.js";
|
|
16
15
|
import {
|
|
17
16
|
getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice,
|
|
18
17
|
aggregateByModel, aggregateCacheHitRate, formatCost, formatTokenCount, formatCostProjection,
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getBudgetAlertLevel,
|
|
6
6
|
getBudgetEnforcementAction,
|
|
7
7
|
getNewBudgetAlertLevel,
|
|
8
|
-
} from "../auto
|
|
8
|
+
} from "../auto.js";
|
|
9
9
|
|
|
10
10
|
test("getBudgetAlertLevel returns the expected threshold bucket", () => {
|
|
11
11
|
assert.equal(getBudgetAlertLevel(0.10), 0);
|
|
@@ -17,8 +17,8 @@ import { tmpdir } from "node:os";
|
|
|
17
17
|
import {
|
|
18
18
|
_getUnitConsecutiveSkips,
|
|
19
19
|
_resetUnitConsecutiveSkips,
|
|
20
|
+
MAX_CONSECUTIVE_SKIPS,
|
|
20
21
|
} from "../auto.ts";
|
|
21
|
-
import { MAX_CONSECUTIVE_SKIPS } from "../auto/session.ts";
|
|
22
22
|
import { persistCompletedKey, removePersistedKey, loadPersistedKeys } from "../auto-recovery.ts";
|
|
23
23
|
import { createTestContext } from "./test-helpers.ts";
|
|
24
24
|
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Remote Questions — Discord adapter
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
5
|
+
import { PER_REQUEST_TIMEOUT_MS, type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
6
6
|
import { formatForDiscord, parseDiscordResponse, DISCORD_NUMBER_EMOJIS } from "./format.js";
|
|
7
|
-
import { apiRequest } from "./http-client.js";
|
|
8
7
|
|
|
9
8
|
const DISCORD_API = "https://discord.com/api/v10";
|
|
10
9
|
|
|
@@ -138,11 +137,23 @@ export class DiscordAdapter implements ChannelAdapter {
|
|
|
138
137
|
return parseDiscordResponse([], String(replies[0].content), prompt.questions);
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
private async discordApi(method:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
private async discordApi(method: string, path: string, body?: unknown): Promise<any> {
|
|
141
|
+
const headers: Record<string, string> = { Authorization: `Bot ${this.token}` };
|
|
142
|
+
const init: RequestInit = { method, headers };
|
|
143
|
+
if (body) {
|
|
144
|
+
headers["Content-Type"] = "application/json";
|
|
145
|
+
init.body = JSON.stringify(body);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
init.signal = AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS);
|
|
149
|
+
const response = await fetch(`${DISCORD_API}${path}`, init);
|
|
150
|
+
if (response.status === 204) return {};
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
const text = await response.text().catch(() => "");
|
|
153
|
+
// Limit error body length to avoid leaking verbose Discord error responses
|
|
154
|
+
const safeText = text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
155
|
+
throw new Error(`Discord API HTTP ${response.status}: ${safeText}`);
|
|
156
|
+
}
|
|
157
|
+
return response.json();
|
|
147
158
|
}
|
|
148
159
|
}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Remote Questions — Slack adapter
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
5
|
+
import { PER_REQUEST_TIMEOUT_MS, type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
6
6
|
import { formatForSlack, parseSlackReply, parseSlackReactionResponse, SLACK_NUMBER_REACTION_NAMES } from "./format.js";
|
|
7
|
-
import { apiRequest } from "./http-client.js";
|
|
8
7
|
|
|
9
8
|
const SLACK_API = "https://slack.com/api";
|
|
10
9
|
const SLACK_ACK_REACTION = "white_check_mark";
|
|
@@ -123,19 +122,26 @@ export class SlackAdapter implements ChannelAdapter {
|
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
private async slackApi(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
125
|
+
const url = `${SLACK_API}/${method}`;
|
|
126
126
|
const isGet = method === "conversations.replies" || method === "auth.test" || method === "reactions.get";
|
|
127
|
-
const opts = { authScheme: "Bearer" as const, authToken: this.token, errorLabel: "Slack API" };
|
|
128
127
|
|
|
128
|
+
let response: Response;
|
|
129
129
|
if (isGet) {
|
|
130
|
-
const qs = new URLSearchParams(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
const qs = new URLSearchParams(Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)]))).toString();
|
|
131
|
+
response = await fetch(`${url}?${qs}`, { method: "GET", headers: { Authorization: `Bearer ${this.token}` }, signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS) });
|
|
132
|
+
} else {
|
|
133
|
+
response = await fetch(url, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
Authorization: `Bearer ${this.token}`,
|
|
137
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify(params),
|
|
140
|
+
signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
|
|
141
|
+
});
|
|
134
142
|
}
|
|
135
143
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
contentType: "application/json; charset=utf-8",
|
|
139
|
-
});
|
|
144
|
+
if (!response.ok) throw new Error(`Slack API HTTP ${response.status}: ${response.statusText}`);
|
|
145
|
+
return (await response.json()) as Record<string, unknown>;
|
|
140
146
|
}
|
|
141
147
|
}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Remote Questions — Telegram adapter
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
5
|
+
import { PER_REQUEST_TIMEOUT_MS, type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
6
6
|
import { formatForTelegram, parseTelegramResponse } from "./format.js";
|
|
7
|
-
import { apiRequest } from "./http-client.js";
|
|
8
7
|
|
|
9
8
|
const TELEGRAM_API = "https://api.telegram.org";
|
|
10
9
|
|
|
@@ -139,11 +138,23 @@ export class TelegramAdapter implements ChannelAdapter {
|
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
private async telegramApi(method: string, params?: Record<string, unknown>): Promise<any> {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"POST",
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
const url = `${TELEGRAM_API}/bot${this.token}/${method}`;
|
|
142
|
+
const init: RequestInit = {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (params) {
|
|
149
|
+
init.body = JSON.stringify(params);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const response = await fetch(url, init);
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const text = await response.text().catch(() => "");
|
|
155
|
+
const safeText = text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
156
|
+
throw new Error(`Telegram API HTTP ${response.status}: ${safeText}`);
|
|
157
|
+
}
|
|
158
|
+
return response.json();
|
|
148
159
|
}
|
|
149
160
|
}
|
package/package.json
CHANGED
|
@@ -22,6 +22,7 @@ import { loadFile, getManifestStatus, resolveAllOverrides, parsePlan, parseSumma
|
|
|
22
22
|
import { loadPrompt } from "./prompt-loader.js";
|
|
23
23
|
import { runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit } from "./verification-gate.js";
|
|
24
24
|
import { writeVerificationJSON } from "./verification-evidence.js";
|
|
25
|
+
export { inlinePriorMilestoneSummary } from "./files.js";
|
|
25
26
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
26
27
|
import {
|
|
27
28
|
gsdRoot, resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
|
|
@@ -191,6 +192,12 @@ import {
|
|
|
191
192
|
NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
|
|
192
193
|
} from "./auto/session.js";
|
|
193
194
|
import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerificationRetry } from "./auto/session.js";
|
|
195
|
+
export {
|
|
196
|
+
MAX_UNIT_DISPATCHES, STUB_RECOVERY_THRESHOLD, MAX_LIFETIME_DISPATCHES,
|
|
197
|
+
MAX_CONSECUTIVE_SKIPS, DISPATCH_GAP_TIMEOUT_MS, MAX_SKIP_DEPTH,
|
|
198
|
+
NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
|
|
199
|
+
} from "./auto/session.js";
|
|
200
|
+
export type { CompletedUnit, CurrentUnit, UnitRouting, StartModel } from "./auto/session.js";
|
|
194
201
|
|
|
195
202
|
// ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
|
|
196
203
|
// ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
|
|
@@ -261,6 +268,8 @@ export function shouldUseWorktreeIsolation(): boolean {
|
|
|
261
268
|
* Maps toolCallId → start timestamp (ms) so the idle watchdog can detect tools that have been
|
|
262
269
|
* running suspiciously long (e.g., a Bash command hung because `&` kept stdout open).
|
|
263
270
|
*/
|
|
271
|
+
// Re-export budget utilities for external consumers
|
|
272
|
+
export { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction } from "./auto-budget.js";
|
|
264
273
|
|
|
265
274
|
/** Wrapper: register SIGTERM handler and store reference. */
|
|
266
275
|
function registerSigtermHandler(currentBasePath: string): void {
|
|
@@ -273,6 +282,8 @@ function deregisterSigtermHandler(): void {
|
|
|
273
282
|
s.sigtermHandler = null;
|
|
274
283
|
}
|
|
275
284
|
|
|
285
|
+
export { type AutoDashboardData } from "./auto-dashboard.js";
|
|
286
|
+
|
|
276
287
|
export function getAutoDashboardData(): AutoDashboardData {
|
|
277
288
|
const ledger = getLedger();
|
|
278
289
|
const totals = ledger ? getProjectTotals(ledger.units) : null;
|
|
@@ -923,6 +934,8 @@ async function showStepWizard(
|
|
|
923
934
|
}
|
|
924
935
|
}
|
|
925
936
|
|
|
937
|
+
// describeNextUnit is imported from auto-dashboard.ts and re-exported
|
|
938
|
+
export { describeNextUnit } from "./auto-dashboard.js";
|
|
926
939
|
|
|
927
940
|
/** Thin wrapper: delegates to auto-dashboard.ts, passing state accessors. */
|
|
928
941
|
function updateProgressWidget(
|
|
@@ -1892,3 +1905,5 @@ export async function dispatchHookUnit(
|
|
|
1892
1905
|
}
|
|
1893
1906
|
|
|
1894
1907
|
|
|
1908
|
+
// Direct phase dispatch → auto-direct-dispatch.ts
|
|
1909
|
+
export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
|
|
@@ -13,8 +13,7 @@ import { deriveState } from "./state.js";
|
|
|
13
13
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
14
14
|
import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
|
|
15
15
|
import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
|
|
16
|
-
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
|
|
17
|
-
import { dispatchDirectPhase } from "./auto-direct-dispatch.js";
|
|
16
|
+
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote, dispatchDirectPhase } from "./auto.js";
|
|
18
17
|
import { resolveProjectRoot } from "./worktree.js";
|
|
19
18
|
import { assertSafeDirectory } from "./validate-directory.js";
|
|
20
19
|
import {
|
|
@@ -11,8 +11,7 @@ import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
|
11
11
|
import { deriveState } from "./state.js";
|
|
12
12
|
import { loadFile, parseRoadmap, parsePlan } from "./files.js";
|
|
13
13
|
import { resolveMilestoneFile, resolveSliceFile } from "./paths.js";
|
|
14
|
-
import { getAutoDashboardData } from "./auto.js";
|
|
15
|
-
import type { AutoDashboardData } from "./auto-dashboard.js";
|
|
14
|
+
import { getAutoDashboardData, type AutoDashboardData } from "./auto.js";
|
|
16
15
|
import {
|
|
17
16
|
getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice,
|
|
18
17
|
aggregateByModel, aggregateCacheHitRate, formatCost, formatTokenCount, formatCostProjection,
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getBudgetAlertLevel,
|
|
6
6
|
getBudgetEnforcementAction,
|
|
7
7
|
getNewBudgetAlertLevel,
|
|
8
|
-
} from "../auto
|
|
8
|
+
} from "../auto.js";
|
|
9
9
|
|
|
10
10
|
test("getBudgetAlertLevel returns the expected threshold bucket", () => {
|
|
11
11
|
assert.equal(getBudgetAlertLevel(0.10), 0);
|
|
@@ -17,8 +17,8 @@ import { tmpdir } from "node:os";
|
|
|
17
17
|
import {
|
|
18
18
|
_getUnitConsecutiveSkips,
|
|
19
19
|
_resetUnitConsecutiveSkips,
|
|
20
|
+
MAX_CONSECUTIVE_SKIPS,
|
|
20
21
|
} from "../auto.ts";
|
|
21
|
-
import { MAX_CONSECUTIVE_SKIPS } from "../auto/session.ts";
|
|
22
22
|
import { persistCompletedKey, removePersistedKey, loadPersistedKeys } from "../auto-recovery.ts";
|
|
23
23
|
import { createTestContext } from "./test-helpers.ts";
|
|
24
24
|
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Remote Questions — Discord adapter
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
5
|
+
import { PER_REQUEST_TIMEOUT_MS, type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
6
6
|
import { formatForDiscord, parseDiscordResponse, DISCORD_NUMBER_EMOJIS } from "./format.js";
|
|
7
|
-
import { apiRequest } from "./http-client.js";
|
|
8
7
|
|
|
9
8
|
const DISCORD_API = "https://discord.com/api/v10";
|
|
10
9
|
|
|
@@ -138,11 +137,23 @@ export class DiscordAdapter implements ChannelAdapter {
|
|
|
138
137
|
return parseDiscordResponse([], String(replies[0].content), prompt.questions);
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
private async discordApi(method:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
private async discordApi(method: string, path: string, body?: unknown): Promise<any> {
|
|
141
|
+
const headers: Record<string, string> = { Authorization: `Bot ${this.token}` };
|
|
142
|
+
const init: RequestInit = { method, headers };
|
|
143
|
+
if (body) {
|
|
144
|
+
headers["Content-Type"] = "application/json";
|
|
145
|
+
init.body = JSON.stringify(body);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
init.signal = AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS);
|
|
149
|
+
const response = await fetch(`${DISCORD_API}${path}`, init);
|
|
150
|
+
if (response.status === 204) return {};
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
const text = await response.text().catch(() => "");
|
|
153
|
+
// Limit error body length to avoid leaking verbose Discord error responses
|
|
154
|
+
const safeText = text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
155
|
+
throw new Error(`Discord API HTTP ${response.status}: ${safeText}`);
|
|
156
|
+
}
|
|
157
|
+
return response.json();
|
|
147
158
|
}
|
|
148
159
|
}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Remote Questions — Slack adapter
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
5
|
+
import { PER_REQUEST_TIMEOUT_MS, type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
6
6
|
import { formatForSlack, parseSlackReply, parseSlackReactionResponse, SLACK_NUMBER_REACTION_NAMES } from "./format.js";
|
|
7
|
-
import { apiRequest } from "./http-client.js";
|
|
8
7
|
|
|
9
8
|
const SLACK_API = "https://slack.com/api";
|
|
10
9
|
const SLACK_ACK_REACTION = "white_check_mark";
|
|
@@ -123,19 +122,26 @@ export class SlackAdapter implements ChannelAdapter {
|
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
private async slackApi(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
125
|
+
const url = `${SLACK_API}/${method}`;
|
|
126
126
|
const isGet = method === "conversations.replies" || method === "auth.test" || method === "reactions.get";
|
|
127
|
-
const opts = { authScheme: "Bearer" as const, authToken: this.token, errorLabel: "Slack API" };
|
|
128
127
|
|
|
128
|
+
let response: Response;
|
|
129
129
|
if (isGet) {
|
|
130
|
-
const qs = new URLSearchParams(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
const qs = new URLSearchParams(Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)]))).toString();
|
|
131
|
+
response = await fetch(`${url}?${qs}`, { method: "GET", headers: { Authorization: `Bearer ${this.token}` }, signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS) });
|
|
132
|
+
} else {
|
|
133
|
+
response = await fetch(url, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
Authorization: `Bearer ${this.token}`,
|
|
137
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify(params),
|
|
140
|
+
signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
|
|
141
|
+
});
|
|
134
142
|
}
|
|
135
143
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
contentType: "application/json; charset=utf-8",
|
|
139
|
-
});
|
|
144
|
+
if (!response.ok) throw new Error(`Slack API HTTP ${response.status}: ${response.statusText}`);
|
|
145
|
+
return (await response.json()) as Record<string, unknown>;
|
|
140
146
|
}
|
|
141
147
|
}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Remote Questions — Telegram adapter
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
5
|
+
import { PER_REQUEST_TIMEOUT_MS, type ChannelAdapter, type RemotePrompt, type RemoteDispatchResult, type RemoteAnswer, type RemotePromptRef } from "./types.js";
|
|
6
6
|
import { formatForTelegram, parseTelegramResponse } from "./format.js";
|
|
7
|
-
import { apiRequest } from "./http-client.js";
|
|
8
7
|
|
|
9
8
|
const TELEGRAM_API = "https://api.telegram.org";
|
|
10
9
|
|
|
@@ -139,11 +138,23 @@ export class TelegramAdapter implements ChannelAdapter {
|
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
private async telegramApi(method: string, params?: Record<string, unknown>): Promise<any> {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"POST",
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
const url = `${TELEGRAM_API}/bot${this.token}/${method}`;
|
|
142
|
+
const init: RequestInit = {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (params) {
|
|
149
|
+
init.body = JSON.stringify(params);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const response = await fetch(url, init);
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const text = await response.text().catch(() => "");
|
|
155
|
+
const safeText = text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
156
|
+
throw new Error(`Telegram API HTTP ${response.status}: ${safeText}`);
|
|
157
|
+
}
|
|
158
|
+
return response.json();
|
|
148
159
|
}
|
|
149
160
|
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remote Questions — shared HTTP client
|
|
3
|
-
*
|
|
4
|
-
* Centralizes timeout, error handling, and JSON serialization logic
|
|
5
|
-
* used by all channel adapters (Discord, Slack, Telegram).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { PER_REQUEST_TIMEOUT_MS } from "./types.js";
|
|
9
|
-
|
|
10
|
-
export interface ApiRequestOptions {
|
|
11
|
-
/** Authorization header scheme. Omit to skip the Authorization header entirely. */
|
|
12
|
-
authScheme?: "Bearer" | "Bot";
|
|
13
|
-
/** Token for the Authorization header. Ignored when authScheme is omitted. */
|
|
14
|
-
authToken?: string;
|
|
15
|
-
/** Max chars of error body to include in thrown Error. Default 200. */
|
|
16
|
-
safeErrorLength?: number;
|
|
17
|
-
/** Label used in error messages (e.g. "Discord API", "Slack API"). Default "HTTP". */
|
|
18
|
-
errorLabel?: string;
|
|
19
|
-
/** Content-Type override. Default "application/json" when body is present. */
|
|
20
|
-
contentType?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Makes an HTTP request with standardized timeout, error handling, and JSON
|
|
25
|
-
* serialization.
|
|
26
|
-
*
|
|
27
|
-
* - Sets `AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS)` on every request.
|
|
28
|
-
* - Serializes `body` as JSON and sets Content-Type when provided.
|
|
29
|
-
* - Returns `{}` for 204 No Content responses.
|
|
30
|
-
* - Truncates error response bodies to `safeErrorLength` chars (default 200).
|
|
31
|
-
*/
|
|
32
|
-
export async function apiRequest(
|
|
33
|
-
url: string,
|
|
34
|
-
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
35
|
-
body?: unknown,
|
|
36
|
-
options: ApiRequestOptions = {},
|
|
37
|
-
): Promise<any> {
|
|
38
|
-
const {
|
|
39
|
-
authScheme,
|
|
40
|
-
authToken,
|
|
41
|
-
safeErrorLength = 200,
|
|
42
|
-
errorLabel = "HTTP",
|
|
43
|
-
contentType,
|
|
44
|
-
} = options;
|
|
45
|
-
|
|
46
|
-
const headers: Record<string, string> = {};
|
|
47
|
-
if (authScheme && authToken) {
|
|
48
|
-
headers["Authorization"] = `${authScheme} ${authToken}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const init: RequestInit = {
|
|
52
|
-
method,
|
|
53
|
-
headers,
|
|
54
|
-
signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
if (body !== undefined) {
|
|
58
|
-
headers["Content-Type"] = contentType ?? "application/json";
|
|
59
|
-
init.body = JSON.stringify(body);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const response = await fetch(url, init);
|
|
63
|
-
|
|
64
|
-
if (response.status === 204) return {};
|
|
65
|
-
|
|
66
|
-
if (!response.ok) {
|
|
67
|
-
const text = await response.text().catch(() => "");
|
|
68
|
-
const safeText =
|
|
69
|
-
text.length > safeErrorLength
|
|
70
|
-
? text.slice(0, safeErrorLength) + "\u2026"
|
|
71
|
-
: text;
|
|
72
|
-
throw new Error(`${errorLabel} HTTP ${response.status}: ${safeText}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return response.json();
|
|
76
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remote Questions — shared HTTP client
|
|
3
|
-
*
|
|
4
|
-
* Centralizes timeout, error handling, and JSON serialization logic
|
|
5
|
-
* used by all channel adapters (Discord, Slack, Telegram).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { PER_REQUEST_TIMEOUT_MS } from "./types.js";
|
|
9
|
-
|
|
10
|
-
export interface ApiRequestOptions {
|
|
11
|
-
/** Authorization header scheme. Omit to skip the Authorization header entirely. */
|
|
12
|
-
authScheme?: "Bearer" | "Bot";
|
|
13
|
-
/** Token for the Authorization header. Ignored when authScheme is omitted. */
|
|
14
|
-
authToken?: string;
|
|
15
|
-
/** Max chars of error body to include in thrown Error. Default 200. */
|
|
16
|
-
safeErrorLength?: number;
|
|
17
|
-
/** Label used in error messages (e.g. "Discord API", "Slack API"). Default "HTTP". */
|
|
18
|
-
errorLabel?: string;
|
|
19
|
-
/** Content-Type override. Default "application/json" when body is present. */
|
|
20
|
-
contentType?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Makes an HTTP request with standardized timeout, error handling, and JSON
|
|
25
|
-
* serialization.
|
|
26
|
-
*
|
|
27
|
-
* - Sets `AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS)` on every request.
|
|
28
|
-
* - Serializes `body` as JSON and sets Content-Type when provided.
|
|
29
|
-
* - Returns `{}` for 204 No Content responses.
|
|
30
|
-
* - Truncates error response bodies to `safeErrorLength` chars (default 200).
|
|
31
|
-
*/
|
|
32
|
-
export async function apiRequest(
|
|
33
|
-
url: string,
|
|
34
|
-
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
35
|
-
body?: unknown,
|
|
36
|
-
options: ApiRequestOptions = {},
|
|
37
|
-
): Promise<any> {
|
|
38
|
-
const {
|
|
39
|
-
authScheme,
|
|
40
|
-
authToken,
|
|
41
|
-
safeErrorLength = 200,
|
|
42
|
-
errorLabel = "HTTP",
|
|
43
|
-
contentType,
|
|
44
|
-
} = options;
|
|
45
|
-
|
|
46
|
-
const headers: Record<string, string> = {};
|
|
47
|
-
if (authScheme && authToken) {
|
|
48
|
-
headers["Authorization"] = `${authScheme} ${authToken}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const init: RequestInit = {
|
|
52
|
-
method,
|
|
53
|
-
headers,
|
|
54
|
-
signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
if (body !== undefined) {
|
|
58
|
-
headers["Content-Type"] = contentType ?? "application/json";
|
|
59
|
-
init.body = JSON.stringify(body);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const response = await fetch(url, init);
|
|
63
|
-
|
|
64
|
-
if (response.status === 204) return {};
|
|
65
|
-
|
|
66
|
-
if (!response.ok) {
|
|
67
|
-
const text = await response.text().catch(() => "");
|
|
68
|
-
const safeText =
|
|
69
|
-
text.length > safeErrorLength
|
|
70
|
-
? text.slice(0, safeErrorLength) + "\u2026"
|
|
71
|
-
: text;
|
|
72
|
-
throw new Error(`${errorLabel} HTTP ${response.status}: ${safeText}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return response.json();
|
|
76
|
-
}
|