@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.
- package/CHANGELOG.md +165 -0
- package/README.md +117 -1
- package/bin/advisor-integration.js +93 -0
- package/bin/check-headless-sessions.js +140 -0
- package/bin/context-meter-config.cjs +101 -0
- package/bin/context-meter-config.test.cjs +101 -0
- package/bin/gsd-t-unattended-platform.js +381 -0
- package/bin/gsd-t-unattended-safety.js +766 -0
- package/bin/gsd-t-unattended.js +1259 -0
- package/bin/gsd-t.js +723 -19
- package/bin/handoff-lock.js +249 -0
- package/bin/headless-auto-spawn.js +328 -0
- package/bin/model-selector.js +224 -0
- package/bin/runway-estimator.js +242 -0
- package/bin/token-budget.js +96 -89
- package/bin/token-optimizer.js +471 -0
- package/bin/token-telemetry.js +246 -0
- package/commands/gsd-t-audit.md +3 -3
- package/commands/gsd-t-backlog-list.md +38 -0
- package/commands/gsd-t-brainstorm.md +3 -3
- package/commands/gsd-t-complete-milestone.md +24 -0
- package/commands/gsd-t-debug.md +124 -7
- package/commands/gsd-t-discuss.md +10 -3
- package/commands/gsd-t-doc-ripple.md +32 -4
- package/commands/gsd-t-execute.md +107 -52
- package/commands/gsd-t-help.md +22 -0
- package/commands/gsd-t-integrate.md +67 -4
- package/commands/gsd-t-optimization-apply.md +91 -0
- package/commands/gsd-t-optimization-reject.md +94 -0
- package/commands/gsd-t-partition.md +7 -0
- package/commands/gsd-t-pause.md +3 -0
- package/commands/gsd-t-plan.md +10 -3
- package/commands/gsd-t-prd.md +3 -3
- package/commands/gsd-t-quick.md +71 -9
- package/commands/gsd-t-reflect.md +3 -7
- package/commands/gsd-t-resume.md +86 -1
- package/commands/gsd-t-status.md +31 -0
- package/commands/gsd-t-test-sync.md +7 -0
- package/commands/gsd-t-unattended-stop.md +83 -0
- package/commands/gsd-t-unattended-watch.md +290 -0
- package/commands/gsd-t-unattended.md +414 -0
- package/commands/gsd-t-verify.md +12 -5
- package/commands/gsd-t-visualize.md +3 -7
- package/commands/gsd-t-wave.md +82 -18
- package/docs/GSD-T-README.md +69 -0
- package/docs/architecture.md +176 -4
- package/docs/infrastructure.md +221 -0
- package/docs/methodology.md +44 -0
- package/docs/prd-harness-evolution.md +51 -37
- package/docs/requirements.md +95 -0
- package/docs/unattended-windows-caveats.md +245 -0
- package/package.json +2 -2
- package/scripts/context-meter/count-tokens-client.js +221 -0
- package/scripts/context-meter/count-tokens-client.test.js +308 -0
- package/scripts/context-meter/test-injector.js +55 -0
- package/scripts/context-meter/threshold.js +88 -0
- package/scripts/context-meter/threshold.test.js +255 -0
- package/scripts/context-meter/transcript-parser.js +252 -0
- package/scripts/context-meter/transcript-parser.test.js +320 -0
- package/scripts/gsd-t-context-meter.e2e.test.js +415 -0
- package/scripts/gsd-t-context-meter.js +350 -0
- package/scripts/gsd-t-context-meter.test.js +417 -0
- package/scripts/gsd-t-heartbeat.js +2 -2
- package/scripts/gsd-t-statusline.js +23 -8
- package/templates/CLAUDE-global.md +17 -1
- package/templates/CLAUDE-project.md +26 -6
- package/templates/context-meter-config.json +10 -0
- package/templates/prompts/README.md +1 -1
- 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": "
|
|
4
|
-
"description": "GSD-T: Contract-Driven Development for Claude Code —
|
|
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 };
|