@tekyzinc/gsd-t 2.74.13 → 3.10.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +117 -1
  3. package/bin/advisor-integration.js +93 -0
  4. package/bin/check-headless-sessions.js +140 -0
  5. package/bin/context-meter-config.cjs +101 -0
  6. package/bin/context-meter-config.test.cjs +101 -0
  7. package/bin/gsd-t-unattended-platform.js +381 -0
  8. package/bin/gsd-t-unattended-safety.js +766 -0
  9. package/bin/gsd-t-unattended.js +1259 -0
  10. package/bin/gsd-t.js +723 -19
  11. package/bin/handoff-lock.js +249 -0
  12. package/bin/headless-auto-spawn.js +328 -0
  13. package/bin/model-selector.js +224 -0
  14. package/bin/runway-estimator.js +242 -0
  15. package/bin/token-budget.js +96 -89
  16. package/bin/token-optimizer.js +471 -0
  17. package/bin/token-telemetry.js +246 -0
  18. package/commands/gsd-t-audit.md +3 -3
  19. package/commands/gsd-t-backlog-list.md +38 -0
  20. package/commands/gsd-t-brainstorm.md +3 -3
  21. package/commands/gsd-t-complete-milestone.md +24 -0
  22. package/commands/gsd-t-debug.md +124 -7
  23. package/commands/gsd-t-discuss.md +10 -3
  24. package/commands/gsd-t-doc-ripple.md +32 -4
  25. package/commands/gsd-t-execute.md +107 -52
  26. package/commands/gsd-t-help.md +22 -0
  27. package/commands/gsd-t-integrate.md +67 -4
  28. package/commands/gsd-t-optimization-apply.md +91 -0
  29. package/commands/gsd-t-optimization-reject.md +94 -0
  30. package/commands/gsd-t-partition.md +7 -0
  31. package/commands/gsd-t-pause.md +3 -0
  32. package/commands/gsd-t-plan.md +10 -3
  33. package/commands/gsd-t-prd.md +3 -3
  34. package/commands/gsd-t-quick.md +71 -9
  35. package/commands/gsd-t-reflect.md +3 -7
  36. package/commands/gsd-t-resume.md +86 -1
  37. package/commands/gsd-t-status.md +31 -0
  38. package/commands/gsd-t-test-sync.md +7 -0
  39. package/commands/gsd-t-unattended-stop.md +83 -0
  40. package/commands/gsd-t-unattended-watch.md +290 -0
  41. package/commands/gsd-t-unattended.md +414 -0
  42. package/commands/gsd-t-verify.md +12 -5
  43. package/commands/gsd-t-visualize.md +3 -7
  44. package/commands/gsd-t-wave.md +82 -18
  45. package/docs/GSD-T-README.md +69 -0
  46. package/docs/architecture.md +176 -4
  47. package/docs/infrastructure.md +221 -0
  48. package/docs/methodology.md +44 -0
  49. package/docs/prd-harness-evolution.md +51 -37
  50. package/docs/requirements.md +95 -0
  51. package/docs/unattended-windows-caveats.md +245 -0
  52. package/package.json +2 -2
  53. package/scripts/context-meter/count-tokens-client.js +221 -0
  54. package/scripts/context-meter/count-tokens-client.test.js +308 -0
  55. package/scripts/context-meter/test-injector.js +55 -0
  56. package/scripts/context-meter/threshold.js +88 -0
  57. package/scripts/context-meter/threshold.test.js +255 -0
  58. package/scripts/context-meter/transcript-parser.js +252 -0
  59. package/scripts/context-meter/transcript-parser.test.js +320 -0
  60. package/scripts/gsd-t-context-meter.e2e.test.js +415 -0
  61. package/scripts/gsd-t-context-meter.js +350 -0
  62. package/scripts/gsd-t-context-meter.test.js +417 -0
  63. package/scripts/gsd-t-heartbeat.js +2 -2
  64. package/scripts/gsd-t-statusline.js +23 -8
  65. package/templates/CLAUDE-global.md +17 -1
  66. package/templates/CLAUDE-project.md +26 -6
  67. package/templates/context-meter-config.json +10 -0
  68. package/templates/prompts/README.md +1 -1
  69. package/bin/task-counter.cjs +0 -161
@@ -0,0 +1,245 @@
1
+ # Unattended Supervisor — Windows Caveats (v1.0.0)
2
+
3
+ **Status**: Windows support is **shipping but caveated** in v1.0.0.
4
+
5
+ **Contract reference**: `.gsd-t/contracts/unattended-supervisor-contract.md` v1.0.0
6
+ **Platform module**: `bin/gsd-t-unattended-platform.js`
7
+
8
+ ---
9
+
10
+ ## 0. Required Software (All Platforms)
11
+
12
+ The launch command (`/user:gsd-t-unattended`) pre-flights required software in
13
+ Step 1e and refuses to spawn if anything is missing. Install these before
14
+ launching:
15
+
16
+ **Required everywhere (hard-fail):**
17
+
18
+ | Tool | Install |
19
+ |---------|---------------------------------------------------------------|
20
+ | node | https://nodejs.org (>= 16) |
21
+ | claude | `npm install -g @anthropic-ai/claude-code` |
22
+ | git | https://git-scm.com/downloads |
23
+
24
+ **macOS — soft warnings (install for best reliability):**
25
+
26
+ | Tool | Purpose | Install |
27
+ |-------------|--------------------------|---------------------|
28
+ | caffeinate | sleep prevention | built-in (Apple) |
29
+
30
+ **Linux — soft warnings:**
31
+
32
+ | Tool | Purpose | Install |
33
+ |---------------------------|-----------------------------|--------------------------------------------|
34
+ | systemd-inhibit OR caffeine | sleep/screen-lock prevention | usually preinstalled; else `sudo apt install caffeine` |
35
+ | notify-send | desktop notifications | `sudo apt install libnotify-bin` |
36
+
37
+ **Windows — soft warnings:**
38
+
39
+ | Tool | Purpose | Install |
40
+ |--------------------|------------------------------|----------------------------------------|
41
+ | PowerShell | sleep prevention (built-in) | ships with Windows |
42
+ | BurntToast | real toast notifications | `Install-Module BurntToast` (PowerShell) |
43
+
44
+ If the pre-flight fails, the launcher prints each missing tool with its
45
+ install instructions and refuses to spawn. Soft warnings are advisory only —
46
+ the supervisor still launches without them, but with degraded resilience.
47
+
48
+ ---
49
+
50
+ ## 1. Overview
51
+
52
+ The GSD-T unattended supervisor (M36) ships cross-platform. All core supervisor
53
+ mechanics — the watch loop, state-file lifecycle, worker spawning, stop
54
+ sentinel, and safety rails — run unchanged on Windows. The darwin and linux
55
+ code paths are runtime-tested; the win32 code paths are
56
+ **implementation-complete but not runtime-tested on the dev host (macOS)**.
57
+
58
+ Three OS-integration surfaces have known limitations on Windows:
59
+
60
+ 1. **Sleep prevention** — no stock `caffeinate` equivalent ships with Windows.
61
+ 2. **Notifications** — `msg.exe` is a minimal fire-and-forget shim with real
62
+ delivery restrictions.
63
+ 3. **Process detach** — `spawn(..., { detached: true })` behaves differently
64
+ from POSIX and is not a true daemonization primitive.
65
+
66
+ This document covers what works, what doesn't, the Spike C disposition, and
67
+ the recommended usage pattern. Everything below applies to `win32` only;
68
+ darwin and linux are unaffected.
69
+
70
+ ---
71
+
72
+ ## 2. Known Gaps
73
+
74
+ ### 2.1 Sleep prevention (no-op on win32)
75
+
76
+ `preventSleep(reason)` in `bin/gsd-t-unattended-platform.js` explicitly
77
+ **returns `null` on win32** and writes a one-line notice to stderr:
78
+
79
+ ```
80
+ [platform] sleep prevention not implemented on win32 (see docs/unattended-windows-caveats.md)
81
+ ```
82
+
83
+ Windows has no stock command-line equivalent of macOS `caffeinate`. The
84
+ native API (`SetThreadExecutionState`) requires a C binding, which violates
85
+ the GSD-T zero-external-dependency constraint for `bin/`. `releaseSleep(null)`
86
+ is a safe no-op, so the supervisor still shuts down cleanly.
87
+
88
+ **Consequence**: if a Windows machine is configured to sleep on its default
89
+ power schedule, a long unattended run will pause (or outright fail) when the
90
+ machine sleeps. The supervisor has no way to prevent this.
91
+
92
+ **Workaround (v1)**: the user must configure Windows power settings manually
93
+ to keep the machine awake for the duration of the run. Microsoft's official
94
+ guidance covers both GUI and command-line approaches:
95
+
96
+ - [powercfg command-line options](https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/powercfg-command-line-options)
97
+ - [Change power plan settings](https://support.microsoft.com/en-us/windows/change-power-mode-for-your-windows-pc-c2aff038-22f9-f46a-8ca1-ba5be2b6a7b9)
98
+
99
+ Useful examples:
100
+
101
+ ```powershell
102
+ # Disable sleep while on AC power (requires admin)
103
+ powercfg /change standby-timeout-ac 0
104
+
105
+ # Disable sleep while on battery (requires admin)
106
+ powercfg /change standby-timeout-dc 0
107
+ ```
108
+
109
+ ### 2.2 Notifications (`msg.exe`, interactive-session only)
110
+
111
+ `notify(title, message, level)` in `bin/gsd-t-unattended-platform.js` uses
112
+ `msg.exe * "title: message"` on win32 with `windowsHide: true`. This is a
113
+ deliberate minimal fire-and-forget dependency: `msg.exe` ships with Windows
114
+ and has zero install cost.
115
+
116
+ **Restrictions**:
117
+
118
+ - `msg.exe` only delivers to **interactive Windows sessions**. If the
119
+ supervisor is launched under a Windows Service, a non-interactive ssh
120
+ session, or any other session without a desktop attached, `msg.exe` will
121
+ silently drop the notification (or fail with an access error). The
122
+ supervisor core is unaffected — the `child.on('error', ...)` handler logs
123
+ the failure to stderr and the relay loop continues.
124
+ - `msg.exe` is not a true toast / Action Center notification. It pops a
125
+ blocking message-box dialog into the active session. This is visible and
126
+ audible but is not the modern Windows notification experience.
127
+
128
+ **Recommended v2 enhancement**: integrate a real toast API such as
129
+ [`BurntToast`](https://github.com/Windos/BurntToast) (a PowerShell module).
130
+ `BurntToast` requires an opt-in install, so it will remain behind a capability
131
+ check; stock installs will fall back to `msg.exe`.
132
+
133
+ ### 2.3 Process detach (`detached: true` is not full daemonization)
134
+
135
+ `spawnSupervisor({ binPath, args, cwd })` uses
136
+ `spawn('node', [...], { detached: true, stdio: 'ignore', windowsHide: true })`
137
+ followed by `child.unref()`. On **POSIX** (darwin / linux) this makes the
138
+ child a new process-group leader and the supervisor survives the parent
139
+ terminal closing. On **win32** the same options produce a separate process
140
+ tree but do **not** fully detach the child from the launching console:
141
+
142
+ - Closing the launching terminal window may still deliver a console
143
+ `CTRL_CLOSE_EVENT` to the child and terminate the supervisor.
144
+ - Signing out of the user session will terminate the child along with every
145
+ other user process.
146
+ - There is no equivalent of POSIX `setsid()` purely from Node's `spawn`
147
+ options.
148
+
149
+ **Workarounds**:
150
+
151
+ - Use `start /B` via a shell wrapper to launch the supervisor in a fully
152
+ background-detached process on the current session.
153
+ - For truly-outlive-the-session runs, register the supervisor as a **Windows
154
+ Task Scheduler** task. Task Scheduler manages its own process lifetime
155
+ independent of the interactive session.
156
+ - Run inside **WSL2** (see §5) to get POSIX detach semantics end-to-end.
157
+
158
+ ---
159
+
160
+ ## 3. Spike C Disposition — Sleep Prevention Alternatives
161
+
162
+ Spike C was an exploratory investigation of Windows-native alternatives to
163
+ `caffeinate`. The question: can we keep the zero-dependency constraint and
164
+ still prevent sleep?
165
+
166
+ Three candidates were evaluated:
167
+
168
+ | Option | Mechanism | Verdict |
169
+ |--------|-----------|---------|
170
+ | `SetThreadExecutionState` Win32 API via `ffi` | Native API, exactly what caffeinate maps to. | **Reject.** Requires `ffi-napi` or equivalent — a C binding and a compiled native dep. Violates the zero-external-dependency constraint for `bin/`. |
171
+ | `powercfg` toggle | CLI that ships with Windows. `powercfg /change standby-timeout-ac 0` before run, restore after. | **Reject.** Requires administrator privileges. Persists across runs if the supervisor crashes mid-run, leaving the machine in a modified power state. Hard to guarantee restoration. |
172
+ | Task Scheduler "wake to run" / wake events | Scheduled task can force the machine to wake at interval. | **Defer.** Best user-space path but adds non-trivial scheduler management code (create task, tear down on exit, handle orphaned tasks). |
173
+
174
+ **Verdict**: **defer to v2.** v1.0.0 ships with the `null`-handle + documentation
175
+ approach described in §2.1. v2 will consider Task Scheduler integration as the
176
+ primary path, since it is the only candidate that is both user-space and
177
+ compatible with the zero-dependency constraint.
178
+
179
+ ---
180
+
181
+ ## 4. What Works Today on Windows
182
+
183
+ All of the following run on Windows with no caveats (pending runtime testing
184
+ on a real Windows host; implementation is complete):
185
+
186
+ - **Core supervisor process** — runs to completion, observes the state file,
187
+ dispatches workers, honours the watch-loop cadence, and exits cleanly.
188
+ - **State file lifecycle** — atomic write, lockfile, heartbeat updates, and
189
+ teardown all use Node built-ins and behave identically on win32.
190
+ - **Worker spawning** — `spawnWorker(projectDir, timeoutMs)` uses `claude.cmd`
191
+ via `resolveClaudePath()` with `shell: false` and `windowsHide: true`, which
192
+ avoids the Spike C PowerShell quoting hazard.
193
+ - **Stop sentinel** — sentinel-file detection is filesystem-based and is
194
+ cross-platform by construction.
195
+ - **Safety rails** — `isAlive(pid)` uses Node's `process.kill(pid, 0)`, which
196
+ implements POSIX signal-0 semantics on Windows. Liveness checks, watchdog
197
+ timers, and stuck-worker detection all work unchanged.
198
+
199
+ Exit code table (contract §5), heartbeat contract, and the launch-handshake
200
+ file contract are all platform-agnostic and fully honoured on win32.
201
+
202
+ ---
203
+
204
+ ## 5. Recommended Usage Pattern on Windows
205
+
206
+ Given the gaps above, the recommended way to run the unattended supervisor on
207
+ Windows for v1.0.0 is:
208
+
209
+ 1. **Run on a desktop that never sleeps.** Configure Windows power settings so
210
+ the machine will not sleep, hibernate, or turn off the display for the
211
+ expected duration of the run. Use `powercfg` or the Settings UI (see §2.1).
212
+ 2. **Launch from an interactive PowerShell session** (not a Windows Service,
213
+ not a non-interactive ssh session). This ensures `msg.exe` notifications
214
+ can be delivered and that the supervisor's stderr diagnostics are visible.
215
+ 3. **Monitor via the watch-tick command from the same interactive session.**
216
+ Do not close the launching terminal until the supervisor exits — see §2.3
217
+ for why.
218
+ 4. **Consider running inside WSL2** instead of native Windows. WSL2 provides a
219
+ full Linux userland where `bin/gsd-t-unattended-platform.js` takes the
220
+ `linux` branch everywhere: `isAlive`, `spawnWorker`, `spawnSupervisor`, and
221
+ `notify` all behave with POSIX semantics end-to-end. The only remaining gap
222
+ in WSL2 is sleep prevention (linux also returns `null` from `preventSleep`
223
+ in v1.0.0), which is still governed by the host Windows machine's power
224
+ settings. WSL2 is currently the closest thing to the full darwin / linux
225
+ feature set on a Windows box.
226
+
227
+ ---
228
+
229
+ ## 6. Summary Matrix
230
+
231
+ | Feature | darwin | linux | win32 |
232
+ |--------------------------|-----------|-----------|------------------------------------------------|
233
+ | `resolveClaudePath` | `claude` | `claude` | `claude.cmd` |
234
+ | `isAlive(pid)` | works | works | works (POSIX signal-0 semantics) |
235
+ | `spawnWorker` | works | works | implementation-complete, untested on real host |
236
+ | `spawnSupervisor` detach | full | full | partial — console-close may kill child |
237
+ | `preventSleep` | works | null (v1) | null (v1) — see §2.1 and §3 |
238
+ | `releaseSleep` | works | no-op | no-op |
239
+ | `notify` | works | works | works only in interactive sessions (§2.2) |
240
+
241
+ **v2 roadmap (non-binding)**:
242
+ - Linux: opt-in `systemd-inhibit` for sleep prevention.
243
+ - Windows: Task Scheduler integration for sleep prevention; `BurntToast`
244
+ capability check for real toast notifications; optional Task-Scheduler-based
245
+ daemonization path for true detachment.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "2.74.13",
4
- "description": "GSD-T: Contract-Driven Development for Claude Code — 56 slash commands with headless CI/CD mode, graph-powered code analysis, real-time agent dashboard, execution intelligence, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
3
+ "version": "3.10.10",
4
+ "description": "GSD-T: Contract-Driven Development for Claude Code — 61 slash commands with unattended supervisor relay, headless CI/CD mode, graph-powered code analysis, real-time agent dashboard, execution intelligence, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -0,0 +1,221 @@
1
+ /**
2
+ * count-tokens-client.js
3
+ *
4
+ * Zero-dependency Node.js client for Anthropic's `POST /v1/messages/count_tokens`
5
+ * endpoint. Built on the built-in `https` / `http` modules so the Context Meter
6
+ * hook ships with no runtime dependencies.
7
+ *
8
+ * Contract: `.gsd-t/contracts/context-meter-contract.md` — "count_tokens API usage"
9
+ *
10
+ * Design notes:
11
+ *
12
+ * - Every failure mode returns `null`. The caller (the hook in Task 4) treats
13
+ * `null` as "fail open" — it simply emits `{}` on stdout and Claude is never
14
+ * blocked. This function NEVER throws.
15
+ *
16
+ * - The `system` field on the Messages API rejects an empty string
17
+ * (`system: ""` → 400). The `{ system, messages }` shape produced by
18
+ * `transcript-parser.js` starts with an empty system string when the
19
+ * transcript has no system blocks, so this client DROPS the `system` key
20
+ * from the request body when the input is an empty string. Any non-empty
21
+ * system is forwarded as-is.
22
+ *
23
+ * - The hard timeout uses `req.setTimeout(ms)`; on fire we `req.destroy()` to
24
+ * release the socket and return `null`. Without the explicit destroy the
25
+ * socket can linger for the OS-level keep-alive window, which matters when
26
+ * the hook is already at its ~200ms latency budget.
27
+ *
28
+ * - We NEVER log the request body. The only diagnostic signal this module
29
+ * produces is the returned value itself (`null` on failure). Any logging
30
+ * is the caller's responsibility — the hook writes to `logPath` per config.
31
+ *
32
+ * - A hidden `_baseUrl` option lets the tests point the client at a local
33
+ * stub HTTP server bound to `127.0.0.1:0`. Production callers never pass
34
+ * `_baseUrl`. Parsing uses `URL` so either http or https works transparently.
35
+ *
36
+ * @module scripts/context-meter/count-tokens-client
37
+ */
38
+
39
+ "use strict";
40
+
41
+ const https = require("https");
42
+ const http = require("http");
43
+ const { URL } = require("url");
44
+
45
+ const DEFAULT_BASE_URL = "https://api.anthropic.com";
46
+ const COUNT_TOKENS_PATH = "/v1/messages/count_tokens";
47
+ const ANTHROPIC_VERSION = "2023-06-01";
48
+
49
+ /**
50
+ * Call Anthropic count_tokens.
51
+ *
52
+ * @param {object} opts
53
+ * @param {string} opts.apiKey - Anthropic API key (from env var named in config)
54
+ * @param {string} opts.model - model id, e.g. "claude-opus-4-6"
55
+ * @param {string} opts.system - system prompt text; dropped from body if ""
56
+ * @param {Array} opts.messages - messages array from transcript-parser.js
57
+ * @param {number} opts.timeoutMs - hard timeout for the whole request
58
+ * @param {string} [opts._baseUrl] - TEST ONLY: override the base URL
59
+ * @returns {Promise<{inputTokens: number} | null>} tokens on success, null on any failure
60
+ */
61
+ function countTokens(opts) {
62
+ return new Promise((resolve) => {
63
+ // Single outer try/catch — any synchronous throw below becomes `null`.
64
+ try {
65
+ if (!opts || typeof opts !== "object") {
66
+ resolve(null);
67
+ return;
68
+ }
69
+
70
+ const {
71
+ apiKey,
72
+ model,
73
+ system,
74
+ messages,
75
+ timeoutMs,
76
+ _baseUrl,
77
+ } = opts;
78
+
79
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
80
+ resolve(null);
81
+ return;
82
+ }
83
+ if (typeof model !== "string" || model.length === 0) {
84
+ resolve(null);
85
+ return;
86
+ }
87
+ if (!Array.isArray(messages)) {
88
+ resolve(null);
89
+ return;
90
+ }
91
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
92
+ resolve(null);
93
+ return;
94
+ }
95
+
96
+ // Build request body. Drop `system` when it's an empty string —
97
+ // the endpoint rejects `system: ""` with a 400.
98
+ const body = { model, messages };
99
+ if (typeof system === "string" && system.length > 0) {
100
+ body.system = system;
101
+ } else if (system != null && typeof system !== "string") {
102
+ // Unusual shape — do not forward.
103
+ resolve(null);
104
+ return;
105
+ }
106
+
107
+ let payload;
108
+ try {
109
+ payload = JSON.stringify(body);
110
+ } catch (_) {
111
+ resolve(null);
112
+ return;
113
+ }
114
+
115
+ // Parse base URL — test code passes http://127.0.0.1:<port>, prod uses https.
116
+ let parsed;
117
+ try {
118
+ parsed = new URL(COUNT_TOKENS_PATH, _baseUrl || DEFAULT_BASE_URL);
119
+ } catch (_) {
120
+ resolve(null);
121
+ return;
122
+ }
123
+
124
+ const isHttps = parsed.protocol === "https:";
125
+ const transport = isHttps ? https : http;
126
+
127
+ const reqOptions = {
128
+ method: "POST",
129
+ hostname: parsed.hostname,
130
+ port: parsed.port || (isHttps ? 443 : 80),
131
+ path: parsed.pathname + parsed.search,
132
+ headers: {
133
+ "x-api-key": apiKey,
134
+ "anthropic-version": ANTHROPIC_VERSION,
135
+ "content-type": "application/json",
136
+ "content-length": Buffer.byteLength(payload),
137
+ },
138
+ };
139
+
140
+ let settled = false;
141
+ const settle = (value) => {
142
+ if (settled) return;
143
+ settled = true;
144
+ resolve(value);
145
+ };
146
+
147
+ let req;
148
+ try {
149
+ req = transport.request(reqOptions, (res) => {
150
+ const status = res.statusCode || 0;
151
+ const chunks = [];
152
+ res.on("data", (chunk) => {
153
+ chunks.push(chunk);
154
+ });
155
+ res.on("end", () => {
156
+ if (status !== 200) {
157
+ // 401 / 403 / 429 / 5xx — fail open silently.
158
+ settle(null);
159
+ return;
160
+ }
161
+ let text;
162
+ try {
163
+ text = Buffer.concat(chunks).toString("utf8");
164
+ } catch (_) {
165
+ settle(null);
166
+ return;
167
+ }
168
+ let parsedBody;
169
+ try {
170
+ parsedBody = JSON.parse(text);
171
+ } catch (_) {
172
+ settle(null);
173
+ return;
174
+ }
175
+ if (!parsedBody || typeof parsedBody !== "object") {
176
+ settle(null);
177
+ return;
178
+ }
179
+ const n = Number(parsedBody.input_tokens);
180
+ if (!Number.isFinite(n)) {
181
+ settle(null);
182
+ return;
183
+ }
184
+ settle({ inputTokens: n });
185
+ });
186
+ res.on("error", () => {
187
+ settle(null);
188
+ });
189
+ });
190
+ } catch (_) {
191
+ settle(null);
192
+ return;
193
+ }
194
+
195
+ req.on("error", () => {
196
+ settle(null);
197
+ });
198
+
199
+ req.setTimeout(timeoutMs, () => {
200
+ // Destroy the socket so it doesn't linger beyond the hook's latency budget.
201
+ try {
202
+ req.destroy();
203
+ } catch (_) {
204
+ /* ignore */
205
+ }
206
+ settle(null);
207
+ });
208
+
209
+ try {
210
+ req.write(payload);
211
+ req.end();
212
+ } catch (_) {
213
+ settle(null);
214
+ }
215
+ } catch (_) {
216
+ resolve(null);
217
+ }
218
+ });
219
+ }
220
+
221
+ module.exports = { countTokens };