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.
@@ -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-budget.js";
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
 
@@ -30,7 +30,7 @@ import {
30
30
  getBudgetAlertLevel,
31
31
  getNewBudgetAlertLevel,
32
32
  getBudgetEnforcementAction,
33
- } from '../auto-budget.ts';
33
+ } from '../auto.ts';
34
34
  import {
35
35
  type UnitMetrics,
36
36
  type MetricsLedger,
@@ -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: "GET" | "POST" | "PUT" | "DELETE", path: string, body?: unknown): Promise<any> {
142
- return apiRequest(`${DISCORD_API}${path}`, method, body, {
143
- authScheme: "Bot",
144
- authToken: this.token,
145
- errorLabel: "Discord API",
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
- Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)])),
132
- ).toString();
133
- return apiRequest(`${SLACK_API}/${method}?${qs}`, "GET", undefined, opts);
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
- return apiRequest(`${SLACK_API}/${method}`, "POST", params, {
137
- ...opts,
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
- return apiRequest(
143
- `${TELEGRAM_API}/bot${this.token}/${method}`,
144
- "POST",
145
- params,
146
- { errorLabel: "Telegram API" },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.29.0-dev.7612840",
3
+ "version": "2.29.0-dev.953d788",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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-budget.js";
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
 
@@ -30,7 +30,7 @@ import {
30
30
  getBudgetAlertLevel,
31
31
  getNewBudgetAlertLevel,
32
32
  getBudgetEnforcementAction,
33
- } from '../auto-budget.ts';
33
+ } from '../auto.ts';
34
34
  import {
35
35
  type UnitMetrics,
36
36
  type MetricsLedger,
@@ -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: "GET" | "POST" | "PUT" | "DELETE", path: string, body?: unknown): Promise<any> {
142
- return apiRequest(`${DISCORD_API}${path}`, method, body, {
143
- authScheme: "Bot",
144
- authToken: this.token,
145
- errorLabel: "Discord API",
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
- Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)])),
132
- ).toString();
133
- return apiRequest(`${SLACK_API}/${method}?${qs}`, "GET", undefined, opts);
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
- return apiRequest(`${SLACK_API}/${method}`, "POST", params, {
137
- ...opts,
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
- return apiRequest(
143
- `${TELEGRAM_API}/bot${this.token}/${method}`,
144
- "POST",
145
- params,
146
- { errorLabel: "Telegram API" },
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
- }