radar-cc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -0
- package/dist/cli/index.js +880 -0
- package/dist/cli/index.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Radar
|
|
2
|
+
|
|
3
|
+
A Claude Code sidecar that flags ambiguous prompts and checks whether Claude's actions matched your intent — displayed in a second terminal pane.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The problem
|
|
8
|
+
|
|
9
|
+
Claude Code's responses are only as good as the developer's intent, but intent is not often communicated precisely.
|
|
10
|
+
|
|
11
|
+
| You say | Claude does | You meant |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| "clean up this module" | Restructures imports, renames functions, splits files | Delete the 3 commented-out functions |
|
|
14
|
+
| "the API is slow" | Adds Redis caching across 4 endpoints | Profile this one DB query |
|
|
15
|
+
| "update the tests" | Rewrites the entire test suite | Add coverage for 2 new edge cases |
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Radar analyses at two moments: early in Claude's turn (pre-advisory, based on prompt text) and after Claude finishes (post-advisory, based on what tools Claude used and what it changed).
|
|
19
|
+
There's a limitiation in the current version: Sonnet is inferring intent alignment from side effects without seeing Claude's actual response. It knows Claude touched 5 files but not why Claude explained it did so. That's a reasonable proxy most of the time — scope creep and wrong-target are visible in tool activity — but it'll miss subtler misalignments where Claude did the right amount of work on the wrong thing within the same. The reason for that is that the OTel events Claude Code emits are operational telemetry — user_prompt, tool_result, api_request, api_error. They tell you what happened mechanically. The actual assistant message (what Claude wrote back to the developer) isn't an OTel event. In a future version we can fix that subtlety through parsing the jsonl files.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
The package isn't published to npm yet. To install locally from the repo:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/ahaydar/radar.git
|
|
30
|
+
cd radar
|
|
31
|
+
npm install
|
|
32
|
+
npm run build
|
|
33
|
+
npm link
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Add the OTel env vars to your shell profile:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cat >> ~/.zshrc << 'EOF'
|
|
40
|
+
|
|
41
|
+
# Radar: Claude Code OTel config
|
|
42
|
+
export CLAUDE_CODE_ENABLE_TELEMETRY=1
|
|
43
|
+
export OTEL_LOGS_EXPORTER=otlp
|
|
44
|
+
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
|
45
|
+
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://localhost:4820/v1/logs
|
|
46
|
+
export OTEL_LOG_USER_PROMPTS=1
|
|
47
|
+
export OTEL_LOG_TOOL_DETAILS=1
|
|
48
|
+
export OTEL_LOGS_EXPORT_INTERVAL=2000
|
|
49
|
+
EOF
|
|
50
|
+
source ~/.zshrc
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Replace `~/.zshrc` with `~/.bashrc` if using Bash. These are safe to have permanently — if Radar isn't running, Claude Code silently ignores them.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
Radar requires an Anthropic API key. Set it in your environment:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or pass it directly:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
radar watch --api-key sk-ant-...
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
In one terminal pane, start Radar:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
radar watch
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
In another pane, start Claude Code normally. Telemetry flows automatically.
|
|
76
|
+
|
|
77
|
+
Claude Code must be started (or restarted) after the env vars are set — it reads them at launch.
|
|
78
|
+
|
|
79
|
+
### Dev mode
|
|
80
|
+
|
|
81
|
+
`npm run dev` compiles TypeScript in watch mode and simultaneously runs `radar watch`, restarting it whenever the build changes:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
ANTHROPIC_API_KEY=sk-ant-... npm run dev
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## How it works
|
|
90
|
+
|
|
91
|
+
**Pre-advisory:** When you submit a prompt, Claude Code emits it via OTel. Radar's Haiku classifier scores its ambiguity. If risk is high, Sonnet prints a warning in the Radar pane — what Claude will likely misinterpret and how to rephrase. Claude is already working at this point; you can hit Esc to interrupt if the warning is serious.
|
|
92
|
+
|
|
93
|
+
**Post-advisory:** After Claude's turn ends (no new tool calls for ~5s), Sonnet reviews the accumulated activity — tools called, files edited, commands run, tokens spent — and checks whether it matched your likely intent. If misaligned, it prints a suggested re-prompt.
|
|
94
|
+
|
|
95
|
+
Radar stays silent when everything looks clear.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Tuning
|
|
100
|
+
|
|
101
|
+
Prompt templates are in `src/analysis/prompts.ts`. Key tuning dimensions:
|
|
102
|
+
|
|
103
|
+
- **Sensitivity:** Classifier flags prompts above a 0.6 ambiguity score. Raise to 0.7 if too noisy; lower to 0.5 if missing obvious cases.
|
|
104
|
+
- **Post-advisory context:** The advisor sees tool activity (files, commands, cost) but not Claude's response text. Tune the prompt to reference specific tools and costs.
|
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/receiver/otlp.ts
|
|
7
|
+
import { EventEmitter } from "events";
|
|
8
|
+
import * as http from "http";
|
|
9
|
+
function buildAttrMap(attrs) {
|
|
10
|
+
const map = /* @__PURE__ */ new Map();
|
|
11
|
+
if (!attrs) return map;
|
|
12
|
+
for (const a of attrs) {
|
|
13
|
+
const v = a.value;
|
|
14
|
+
if (v.stringValue !== void 0) map.set(a.key, v.stringValue);
|
|
15
|
+
else if (v.intValue !== void 0) map.set(a.key, v.intValue);
|
|
16
|
+
else if (v.doubleValue !== void 0) map.set(a.key, v.doubleValue);
|
|
17
|
+
else if (v.boolValue !== void 0) map.set(a.key, v.boolValue);
|
|
18
|
+
}
|
|
19
|
+
return map;
|
|
20
|
+
}
|
|
21
|
+
function getString(map, key) {
|
|
22
|
+
const v = map.get(key);
|
|
23
|
+
return typeof v === "string" ? v : "";
|
|
24
|
+
}
|
|
25
|
+
function getNumber(map, key) {
|
|
26
|
+
const v = map.get(key);
|
|
27
|
+
return typeof v === "number" ? v : 0;
|
|
28
|
+
}
|
|
29
|
+
function getBool(map, key) {
|
|
30
|
+
const v = map.get(key);
|
|
31
|
+
return typeof v === "boolean" ? v : false;
|
|
32
|
+
}
|
|
33
|
+
function parseLogRecord(record, sessionId) {
|
|
34
|
+
const eventName = record.body?.stringValue ?? "";
|
|
35
|
+
const timeNano = record.timeUnixNano ?? "0";
|
|
36
|
+
const timestampMs = Math.floor(Number(BigInt(timeNano) / 1000000n));
|
|
37
|
+
const attrs = buildAttrMap(record.attributes);
|
|
38
|
+
const promptId = getString(attrs, "prompt.id");
|
|
39
|
+
const base = { type: "unknown", promptId, sessionId, timestampMs };
|
|
40
|
+
switch (eventName) {
|
|
41
|
+
case "claude_code.user_prompt": {
|
|
42
|
+
const e = {
|
|
43
|
+
...base,
|
|
44
|
+
type: "user_prompt",
|
|
45
|
+
prompt: getString(attrs, "prompt"),
|
|
46
|
+
promptLength: getNumber(attrs, "prompt_length")
|
|
47
|
+
};
|
|
48
|
+
return e;
|
|
49
|
+
}
|
|
50
|
+
case "claude_code.tool_result": {
|
|
51
|
+
const e = {
|
|
52
|
+
...base,
|
|
53
|
+
type: "tool_result",
|
|
54
|
+
toolName: getString(attrs, "tool_name"),
|
|
55
|
+
success: getBool(attrs, "success"),
|
|
56
|
+
durationMs: getNumber(attrs, "duration_ms")
|
|
57
|
+
};
|
|
58
|
+
const toolParameters = getString(attrs, "tool_parameters");
|
|
59
|
+
if (toolParameters) e.toolParameters = toolParameters;
|
|
60
|
+
const resultSizeBytes = getNumber(attrs, "result_size_bytes");
|
|
61
|
+
if (resultSizeBytes) e.resultSizeBytes = resultSizeBytes;
|
|
62
|
+
return e;
|
|
63
|
+
}
|
|
64
|
+
case "claude_code.api_request": {
|
|
65
|
+
const e = {
|
|
66
|
+
...base,
|
|
67
|
+
type: "api_request",
|
|
68
|
+
model: getString(attrs, "model"),
|
|
69
|
+
costUsd: getNumber(attrs, "cost_usd"),
|
|
70
|
+
inputTokens: getNumber(attrs, "input_tokens"),
|
|
71
|
+
outputTokens: getNumber(attrs, "output_tokens"),
|
|
72
|
+
durationMs: getNumber(attrs, "duration_ms")
|
|
73
|
+
};
|
|
74
|
+
return e;
|
|
75
|
+
}
|
|
76
|
+
case "claude_code.api_error": {
|
|
77
|
+
const e = {
|
|
78
|
+
...base,
|
|
79
|
+
type: "api_error",
|
|
80
|
+
error: getString(attrs, "error")
|
|
81
|
+
};
|
|
82
|
+
const statusCode = getNumber(attrs, "status_code");
|
|
83
|
+
if (statusCode) e.statusCode = statusCode;
|
|
84
|
+
return e;
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var OtlpReceiver = class extends EventEmitter {
|
|
91
|
+
port;
|
|
92
|
+
fallbackSessionId;
|
|
93
|
+
server = null;
|
|
94
|
+
constructor(port = 4820) {
|
|
95
|
+
super();
|
|
96
|
+
this.port = port;
|
|
97
|
+
this.fallbackSessionId = `radar-${Math.random().toString(36).slice(2, 10)}`;
|
|
98
|
+
}
|
|
99
|
+
deriveSessionId(resourceAttrs) {
|
|
100
|
+
const sessionId = resourceAttrs.get("session.id");
|
|
101
|
+
if (typeof sessionId === "string" && sessionId) return sessionId;
|
|
102
|
+
const instanceId = resourceAttrs.get("service.instance.id");
|
|
103
|
+
if (typeof instanceId === "string" && instanceId) return instanceId;
|
|
104
|
+
const pid = resourceAttrs.get("process.pid");
|
|
105
|
+
if (pid !== void 0) return String(pid);
|
|
106
|
+
return this.fallbackSessionId;
|
|
107
|
+
}
|
|
108
|
+
start() {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const server = http.createServer((req, res) => {
|
|
111
|
+
this.handleRequest(req, res);
|
|
112
|
+
});
|
|
113
|
+
server.on("error", (err) => {
|
|
114
|
+
this.emit("error", err);
|
|
115
|
+
});
|
|
116
|
+
server.listen(this.port, () => {
|
|
117
|
+
this.server = server;
|
|
118
|
+
resolve();
|
|
119
|
+
});
|
|
120
|
+
server.once("error", reject);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
stop() {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
if (!this.server) {
|
|
126
|
+
resolve();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.server.close((err) => {
|
|
130
|
+
if (err) reject(err);
|
|
131
|
+
else resolve();
|
|
132
|
+
});
|
|
133
|
+
this.server = null;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
handleRequest(req, res) {
|
|
137
|
+
if (req.method !== "POST" || req.url !== "/v1/logs") {
|
|
138
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
139
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const chunks = [];
|
|
143
|
+
req.on("data", (chunk) => {
|
|
144
|
+
chunks.push(chunk);
|
|
145
|
+
});
|
|
146
|
+
req.on("end", () => {
|
|
147
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
148
|
+
res.end(JSON.stringify({ partialSuccess: {} }));
|
|
149
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
150
|
+
let payload;
|
|
151
|
+
try {
|
|
152
|
+
payload = JSON.parse(body);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
process.stderr.write(`[radar/otlp] malformed JSON: ${String(err)}
|
|
155
|
+
`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this.processPayload(payload);
|
|
159
|
+
});
|
|
160
|
+
req.on("error", (err) => {
|
|
161
|
+
process.stderr.write(`[radar/otlp] request error: ${String(err)}
|
|
162
|
+
`);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
processPayload(payload) {
|
|
166
|
+
for (const resourceLog of payload.resourceLogs ?? []) {
|
|
167
|
+
const resourceAttrs = buildAttrMap(resourceLog.resource?.attributes);
|
|
168
|
+
const sessionId = this.deriveSessionId(resourceAttrs);
|
|
169
|
+
for (const scopeLog of resourceLog.scopeLogs ?? []) {
|
|
170
|
+
for (const record of scopeLog.logRecords ?? []) {
|
|
171
|
+
const event = parseLogRecord(record, sessionId);
|
|
172
|
+
if (event) {
|
|
173
|
+
this.emit("event", event);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// src/aggregator/turn.ts
|
|
182
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
183
|
+
function extractBashCommand(toolParameters) {
|
|
184
|
+
if (toolParameters === void 0 || toolParameters === null) return void 0;
|
|
185
|
+
if (typeof toolParameters === "string") {
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(toolParameters);
|
|
188
|
+
if (typeof parsed === "object" && parsed !== null && "command" in parsed) {
|
|
189
|
+
const cmd = parsed.command;
|
|
190
|
+
if (typeof cmd === "string") {
|
|
191
|
+
return cmd.slice(0, 200);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
return toolParameters.slice(0, 200);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (typeof toolParameters === "object" && "command" in toolParameters) {
|
|
199
|
+
const cmd = toolParameters.command;
|
|
200
|
+
if (typeof cmd === "string") {
|
|
201
|
+
return cmd.slice(0, 200);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
function buildPublicContext(internal) {
|
|
207
|
+
const totalCostUsd = internal.apiRequests.reduce((sum, r) => sum + r.costUsd, 0);
|
|
208
|
+
const totalInputTokens = internal.apiRequests.reduce((sum, r) => sum + r.inputTokens, 0);
|
|
209
|
+
const totalOutputTokens = internal.apiRequests.reduce((sum, r) => sum + r.outputTokens, 0);
|
|
210
|
+
const toolNames = [...new Set(internal.toolResults.map((t) => t.toolName))];
|
|
211
|
+
return {
|
|
212
|
+
...internal,
|
|
213
|
+
totalCostUsd,
|
|
214
|
+
totalInputTokens,
|
|
215
|
+
totalOutputTokens,
|
|
216
|
+
toolNames
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
var TurnAggregator = class extends EventEmitter2 {
|
|
220
|
+
boundaryTimeoutMs;
|
|
221
|
+
cleanupAfterMs;
|
|
222
|
+
contexts = /* @__PURE__ */ new Map();
|
|
223
|
+
boundaryTimers = /* @__PURE__ */ new Map();
|
|
224
|
+
// Tracks pending context-cleanup timers so they can be cancelled if a promptId
|
|
225
|
+
// is reused before the cleanup window expires, preventing silent context deletion.
|
|
226
|
+
cleanupTimers = /* @__PURE__ */ new Map();
|
|
227
|
+
sessions = /* @__PURE__ */ new Map();
|
|
228
|
+
sessionCounter = 0;
|
|
229
|
+
constructor(options) {
|
|
230
|
+
super();
|
|
231
|
+
this.boundaryTimeoutMs = options?.boundaryTimeoutMs ?? 5e3;
|
|
232
|
+
this.cleanupAfterMs = options?.cleanupAfterMs ?? 3e5;
|
|
233
|
+
}
|
|
234
|
+
getSessions() {
|
|
235
|
+
return [...this.sessions.values()];
|
|
236
|
+
}
|
|
237
|
+
getSession(sessionId) {
|
|
238
|
+
return this.sessions.get(sessionId);
|
|
239
|
+
}
|
|
240
|
+
addEvent(event) {
|
|
241
|
+
const { promptId } = event;
|
|
242
|
+
const isNew = !this.contexts.has(promptId);
|
|
243
|
+
if (isNew) {
|
|
244
|
+
const pendingCleanup = this.cleanupTimers.get(promptId);
|
|
245
|
+
if (pendingCleanup !== void 0) {
|
|
246
|
+
clearTimeout(pendingCleanup);
|
|
247
|
+
this.cleanupTimers.delete(promptId);
|
|
248
|
+
}
|
|
249
|
+
const internal = {
|
|
250
|
+
promptId,
|
|
251
|
+
sessionId: event.sessionId,
|
|
252
|
+
prompt: "",
|
|
253
|
+
promptLength: 0,
|
|
254
|
+
startedAt: Date.now(),
|
|
255
|
+
toolResults: [],
|
|
256
|
+
apiRequests: [],
|
|
257
|
+
errors: []
|
|
258
|
+
};
|
|
259
|
+
this.contexts.set(promptId, internal);
|
|
260
|
+
if (!this.sessions.has(event.sessionId)) {
|
|
261
|
+
const session = {
|
|
262
|
+
sessionId: event.sessionId,
|
|
263
|
+
label: `S${++this.sessionCounter}`,
|
|
264
|
+
turnCount: 1,
|
|
265
|
+
completedTurns: 0,
|
|
266
|
+
startedAt: Date.now(),
|
|
267
|
+
lastSeenAt: Date.now(),
|
|
268
|
+
totalCostUsd: 0
|
|
269
|
+
};
|
|
270
|
+
this.sessions.set(event.sessionId, session);
|
|
271
|
+
this.emit("session_start", { ...session });
|
|
272
|
+
} else {
|
|
273
|
+
const session = this.sessions.get(event.sessionId);
|
|
274
|
+
session.lastSeenAt = Date.now();
|
|
275
|
+
session.turnCount++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const ctx = this.contexts.get(promptId);
|
|
279
|
+
switch (event.type) {
|
|
280
|
+
case "user_prompt": {
|
|
281
|
+
ctx.prompt = event.prompt ?? "";
|
|
282
|
+
ctx.promptLength = event.promptLength ?? ctx.prompt.length;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case "tool_result": {
|
|
286
|
+
const summary = {
|
|
287
|
+
toolName: event.toolName,
|
|
288
|
+
success: event.success,
|
|
289
|
+
durationMs: event.durationMs,
|
|
290
|
+
resultSizeBytes: event.resultSizeBytes
|
|
291
|
+
};
|
|
292
|
+
if (event.toolName === "Bash") {
|
|
293
|
+
summary.bashCommand = extractBashCommand(event.toolParameters);
|
|
294
|
+
}
|
|
295
|
+
ctx.toolResults.push(summary);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case "api_request": {
|
|
299
|
+
ctx.apiRequests.push({
|
|
300
|
+
model: event.model,
|
|
301
|
+
costUsd: event.costUsd,
|
|
302
|
+
inputTokens: event.inputTokens,
|
|
303
|
+
outputTokens: event.outputTokens,
|
|
304
|
+
durationMs: event.durationMs
|
|
305
|
+
});
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case "api_error": {
|
|
309
|
+
ctx.errors.push(event.error);
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (isNew) {
|
|
314
|
+
this.emit("turn_start", buildPublicContext(ctx));
|
|
315
|
+
}
|
|
316
|
+
this.resetBoundaryTimer(promptId);
|
|
317
|
+
}
|
|
318
|
+
getContext(promptId) {
|
|
319
|
+
const internal = this.contexts.get(promptId);
|
|
320
|
+
if (!internal) return void 0;
|
|
321
|
+
return buildPublicContext(internal);
|
|
322
|
+
}
|
|
323
|
+
resetBoundaryTimer(promptId) {
|
|
324
|
+
const existing = this.boundaryTimers.get(promptId);
|
|
325
|
+
if (existing !== void 0) {
|
|
326
|
+
clearTimeout(existing);
|
|
327
|
+
}
|
|
328
|
+
const timer = setTimeout(() => {
|
|
329
|
+
this.boundaryTimers.delete(promptId);
|
|
330
|
+
const internal = this.contexts.get(promptId);
|
|
331
|
+
if (internal) {
|
|
332
|
+
const session = this.sessions.get(internal.sessionId);
|
|
333
|
+
if (session) {
|
|
334
|
+
session.completedTurns++;
|
|
335
|
+
session.totalCostUsd += internal.apiRequests.reduce((s, r) => s + r.costUsd, 0);
|
|
336
|
+
}
|
|
337
|
+
this.emit("turn_complete", buildPublicContext(internal));
|
|
338
|
+
const cleanupTimer = setTimeout(() => {
|
|
339
|
+
this.contexts.delete(promptId);
|
|
340
|
+
this.cleanupTimers.delete(promptId);
|
|
341
|
+
}, this.cleanupAfterMs);
|
|
342
|
+
this.cleanupTimers.set(promptId, cleanupTimer);
|
|
343
|
+
}
|
|
344
|
+
}, this.boundaryTimeoutMs);
|
|
345
|
+
this.boundaryTimers.set(promptId, timer);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// src/analysis/classifier.ts
|
|
350
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
351
|
+
|
|
352
|
+
// src/analysis/prompts.ts
|
|
353
|
+
var CLASSIFIER_SYSTEM_PROMPT = `You are an intent-ambiguity classifier for Claude Code, an AI coding assistant.
|
|
354
|
+
|
|
355
|
+
Your job is to score how likely a user prompt will cause Claude to confidently execute a reasonable but WRONG interpretation \u2014 leading to wasted work, unintended changes, or the user having to undo what Claude did.
|
|
356
|
+
|
|
357
|
+
Be CONSERVATIVE. Only flag genuine ambiguity. Most prompts are clear enough.
|
|
358
|
+
|
|
359
|
+
Common failure modes to watch for:
|
|
360
|
+
- Scope ambiguity: "clean up this module", "refactor the service" \u2014 which files? what counts as clean?
|
|
361
|
+
- Target ambiguity: "the API is slow", "fix the tests" \u2014 which API? which tests?
|
|
362
|
+
- Intent ambiguity: "update the tests", "improve error handling" \u2014 add new tests? fix existing? what kind of improvement?
|
|
363
|
+
- Symptom vs cause: "auth isn't working" \u2014 fix the symptom or find the root cause?
|
|
364
|
+
|
|
365
|
+
Score guide:
|
|
366
|
+
- 0.0\u20130.3: Clear and specific. Claude knows exactly what to do.
|
|
367
|
+
- 0.4\u20130.59: Some ambiguity, but Claude will likely ask for clarification or make a safe default choice.
|
|
368
|
+
- 0.6\u20130.79: Real risk. Claude will pick an interpretation and run with it \u2014 the user might not like the result.
|
|
369
|
+
- 0.8\u20131.0: High risk. Multiple very different valid interpretations; high chance of wasted work.
|
|
370
|
+
|
|
371
|
+
Do NOT flag:
|
|
372
|
+
- Questions or requests for explanation ("how does X work?", "what is Y?")
|
|
373
|
+
- Conversational messages ("thanks", "ok", "sounds good")
|
|
374
|
+
- Read-only or low-stakes requests ("show me", "list", "describe")
|
|
375
|
+
|
|
376
|
+
Respond with ONLY a JSON object on a single line:
|
|
377
|
+
{"score": <0.0-1.0>, "reason": "<one sentence explaining the ambiguity or why it is clear>"}`;
|
|
378
|
+
var PRE_ADVISORY_SYSTEM_PROMPT = `You are a concise advisory assistant helping a developer clarify their intent before sending a prompt to Claude Code.
|
|
379
|
+
|
|
380
|
+
A classifier has flagged the prompt as potentially ambiguous. Your job is to help the user understand the risk and either rephrase or confirm their intent.
|
|
381
|
+
|
|
382
|
+
Output at most 4 lines of plain text. No markdown headers, no bullet symbols, no lists. Just plain sentences.
|
|
383
|
+
|
|
384
|
+
Cover:
|
|
385
|
+
1. What Claude will most likely do (the probable misinterpretation that could go wrong)
|
|
386
|
+
2. The specific scope or target risk (what is under-specified)
|
|
387
|
+
3. One clarifying question OR a concrete rephrasing that removes the ambiguity
|
|
388
|
+
|
|
389
|
+
Be direct and brief. Do not repeat the prompt back verbatim.`;
|
|
390
|
+
var PRE_ADVISORY_USER_TEMPLATE = `Prompt: {prompt}
|
|
391
|
+
|
|
392
|
+
Ambiguity score: {score}
|
|
393
|
+
Reason: {reason}`;
|
|
394
|
+
var POST_ADVISORY_SYSTEM_PROMPT = `You are a post-execution reviewer for Claude Code. You compare what the user asked for against what Claude actually did.
|
|
395
|
+
|
|
396
|
+
Given the original prompt and a summary of tool activity, determine alignment and give brief feedback.
|
|
397
|
+
|
|
398
|
+
If aligned: respond with exactly one line starting with "\u2713" \u2014 a brief confirmation \u2014 followed by a one-line tools/cost summary.
|
|
399
|
+
If misaligned: respond with a line starting with "\u2717" describing what went wrong, then a line starting with "\u2192" containing an exact re-prompt suggestion in quotes.
|
|
400
|
+
|
|
401
|
+
Format: plain text, no markdown. Maximum 5 lines total.`;
|
|
402
|
+
var POST_ADVISORY_USER_TEMPLATE = `Original prompt: {prompt}
|
|
403
|
+
|
|
404
|
+
Tool activity: {toolSummary}
|
|
405
|
+
Total cost: {totalCost}
|
|
406
|
+
Total tokens: {totalTokens}`;
|
|
407
|
+
|
|
408
|
+
// src/util/async.ts
|
|
409
|
+
function withTimeout(promise, ms, fallback) {
|
|
410
|
+
let timerId;
|
|
411
|
+
const timeout = new Promise((resolve) => {
|
|
412
|
+
timerId = setTimeout(() => resolve(fallback), ms);
|
|
413
|
+
});
|
|
414
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timerId));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/analysis/classifier.ts
|
|
418
|
+
var CLASSIFIER_TIMEOUT_MS = 3e3;
|
|
419
|
+
var CLASSIFIER_FALLBACK = {
|
|
420
|
+
score: 0.5,
|
|
421
|
+
reason: "Classification timed out"
|
|
422
|
+
};
|
|
423
|
+
var Classifier = class {
|
|
424
|
+
client;
|
|
425
|
+
constructor(apiKey) {
|
|
426
|
+
this.client = new Anthropic({ apiKey: apiKey ?? process.env.ANTHROPIC_API_KEY });
|
|
427
|
+
}
|
|
428
|
+
async classify(prompt) {
|
|
429
|
+
const classifyPromise = (async () => {
|
|
430
|
+
const message = await this.client.messages.create({
|
|
431
|
+
model: "claude-haiku-4-5",
|
|
432
|
+
max_tokens: 100,
|
|
433
|
+
system: CLASSIFIER_SYSTEM_PROMPT,
|
|
434
|
+
messages: [
|
|
435
|
+
{
|
|
436
|
+
role: "user",
|
|
437
|
+
content: `User prompt to classify:
|
|
438
|
+
${prompt}`
|
|
439
|
+
}
|
|
440
|
+
]
|
|
441
|
+
});
|
|
442
|
+
const content = message.content[0];
|
|
443
|
+
if (content.type !== "text") {
|
|
444
|
+
return CLASSIFIER_FALLBACK;
|
|
445
|
+
}
|
|
446
|
+
return parseClassifierResponse(content.text);
|
|
447
|
+
})();
|
|
448
|
+
return withTimeout(classifyPromise, CLASSIFIER_TIMEOUT_MS, CLASSIFIER_FALLBACK);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
function parseClassifierResponse(raw) {
|
|
452
|
+
try {
|
|
453
|
+
const jsonMatch = raw.match(/\{[^{}]*\}/);
|
|
454
|
+
if (!jsonMatch) {
|
|
455
|
+
return CLASSIFIER_FALLBACK;
|
|
456
|
+
}
|
|
457
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
458
|
+
if (typeof parsed !== "object" || parsed === null || !("score" in parsed) || !("reason" in parsed)) {
|
|
459
|
+
return CLASSIFIER_FALLBACK;
|
|
460
|
+
}
|
|
461
|
+
const obj = parsed;
|
|
462
|
+
const score = Number(obj.score);
|
|
463
|
+
const reason = String(obj.reason);
|
|
464
|
+
if (isNaN(score) || score < 0 || score > 1) {
|
|
465
|
+
return CLASSIFIER_FALLBACK;
|
|
466
|
+
}
|
|
467
|
+
return { score, reason };
|
|
468
|
+
} catch {
|
|
469
|
+
return CLASSIFIER_FALLBACK;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/analysis/advisor.ts
|
|
474
|
+
import Anthropic2 from "@anthropic-ai/sdk";
|
|
475
|
+
var ADVISORY_TIMEOUT_MS = 1e4;
|
|
476
|
+
var PRE_ADVISORY_FALLBACK = { text: "Advisory unavailable (timeout)" };
|
|
477
|
+
var POST_ADVISORY_FALLBACK_TIMEOUT = { text: "Advisory unavailable (timeout)", aligned: void 0 };
|
|
478
|
+
var POST_ADVISORY_FALLBACK_ERROR = { text: "Advisory unavailable (error)", aligned: void 0 };
|
|
479
|
+
var Advisor = class {
|
|
480
|
+
client;
|
|
481
|
+
constructor(apiKey) {
|
|
482
|
+
this.client = new Anthropic2({ apiKey: apiKey ?? process.env.ANTHROPIC_API_KEY });
|
|
483
|
+
}
|
|
484
|
+
// Pre-advisory: called when classifier score >= 0.6
|
|
485
|
+
async preAdvisory(prompt, classification) {
|
|
486
|
+
const userMessage = PRE_ADVISORY_USER_TEMPLATE.replace("{prompt}", prompt).replace("{score}", classification.score.toFixed(2)).replace("{reason}", classification.reason);
|
|
487
|
+
const advisoryPromise = (async () => {
|
|
488
|
+
const message = await this.client.messages.create({
|
|
489
|
+
model: "claude-sonnet-4-5",
|
|
490
|
+
max_tokens: 200,
|
|
491
|
+
system: PRE_ADVISORY_SYSTEM_PROMPT,
|
|
492
|
+
messages: [{ role: "user", content: userMessage }]
|
|
493
|
+
});
|
|
494
|
+
const content = message.content[0];
|
|
495
|
+
if (content.type !== "text") {
|
|
496
|
+
return PRE_ADVISORY_FALLBACK;
|
|
497
|
+
}
|
|
498
|
+
return { text: content.text.trim() };
|
|
499
|
+
})();
|
|
500
|
+
return withTimeout(advisoryPromise, ADVISORY_TIMEOUT_MS, PRE_ADVISORY_FALLBACK);
|
|
501
|
+
}
|
|
502
|
+
// Post-advisory: called on turn complete
|
|
503
|
+
async postAdvisory(context) {
|
|
504
|
+
const toolSummary = buildToolSummary(context);
|
|
505
|
+
const totalCost = `$${context.totalCostUsd.toFixed(3)}`;
|
|
506
|
+
const totalTokens = (context.totalInputTokens + context.totalOutputTokens).toLocaleString();
|
|
507
|
+
const userMessage = POST_ADVISORY_USER_TEMPLATE.replace("{prompt}", context.prompt).replace("{toolSummary}", toolSummary).replace("{totalCost}", totalCost).replace("{totalTokens}", totalTokens);
|
|
508
|
+
const advisoryPromise = (async () => {
|
|
509
|
+
const message = await this.client.messages.create({
|
|
510
|
+
model: "claude-sonnet-4-5",
|
|
511
|
+
max_tokens: 300,
|
|
512
|
+
system: POST_ADVISORY_SYSTEM_PROMPT,
|
|
513
|
+
messages: [{ role: "user", content: userMessage }]
|
|
514
|
+
});
|
|
515
|
+
const content = message.content[0];
|
|
516
|
+
if (content.type !== "text") {
|
|
517
|
+
return POST_ADVISORY_FALLBACK_ERROR;
|
|
518
|
+
}
|
|
519
|
+
const text = content.text.trim();
|
|
520
|
+
const aligned = text.startsWith("\u2713") ? true : text.startsWith("\u2717") ? false : void 0;
|
|
521
|
+
return { text, aligned };
|
|
522
|
+
})();
|
|
523
|
+
return withTimeout(advisoryPromise, ADVISORY_TIMEOUT_MS, POST_ADVISORY_FALLBACK_TIMEOUT);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
function buildToolSummary(context) {
|
|
527
|
+
const parts = [];
|
|
528
|
+
const toolCounts = /* @__PURE__ */ new Map();
|
|
529
|
+
const bashCommands = [];
|
|
530
|
+
let bashCallCount = 0;
|
|
531
|
+
for (const result of context.toolResults) {
|
|
532
|
+
if (result.toolName === "Bash") {
|
|
533
|
+
bashCallCount++;
|
|
534
|
+
if (result.bashCommand) {
|
|
535
|
+
bashCommands.push(`'${result.bashCommand}'`);
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
toolCounts.set(result.toolName, (toolCounts.get(result.toolName) ?? 0) + 1);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
for (const [toolName, count] of toolCounts.entries()) {
|
|
542
|
+
parts.push(count === 1 ? toolName : `${toolName} (${count} calls)`);
|
|
543
|
+
}
|
|
544
|
+
if (bashCallCount > 0) {
|
|
545
|
+
if (bashCommands.length > 0) {
|
|
546
|
+
const bashLabel = bashCommands.length <= 3 ? `Bash: ${bashCommands.join(", ")}` : `Bash: ${bashCommands.slice(0, 3).join(", ")} +${bashCommands.length - 3} more`;
|
|
547
|
+
parts.push(bashLabel);
|
|
548
|
+
} else {
|
|
549
|
+
parts.push(`Bash (${bashCallCount} calls)`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const totalTokens = context.totalInputTokens + context.totalOutputTokens;
|
|
553
|
+
if (totalTokens > 0) {
|
|
554
|
+
parts.push(`${totalTokens.toLocaleString()} tokens`);
|
|
555
|
+
}
|
|
556
|
+
if (context.totalCostUsd > 0) {
|
|
557
|
+
parts.push(`$${context.totalCostUsd.toFixed(3)}`);
|
|
558
|
+
}
|
|
559
|
+
return parts.join(" \xB7 ") || "No tools used";
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/output/formatter.ts
|
|
563
|
+
var RESET = "\x1B[0m";
|
|
564
|
+
var DIM = "\x1B[2m";
|
|
565
|
+
var BOLD = "\x1B[1m";
|
|
566
|
+
var GREEN = "\x1B[32m";
|
|
567
|
+
var YELLOW = "\x1B[33m";
|
|
568
|
+
var RED = "\x1B[31m";
|
|
569
|
+
var CYAN = "\x1B[36m";
|
|
570
|
+
var LINE_WIDTH = 52;
|
|
571
|
+
var WRAP_WIDTH = 50;
|
|
572
|
+
function formatTime(date) {
|
|
573
|
+
const hh = String(date.getHours()).padStart(2, "0");
|
|
574
|
+
const mm = String(date.getMinutes()).padStart(2, "0");
|
|
575
|
+
const ss = String(date.getSeconds()).padStart(2, "0");
|
|
576
|
+
return `${hh}:${mm}:${ss}`;
|
|
577
|
+
}
|
|
578
|
+
function separator(prefix = "") {
|
|
579
|
+
const dashes = "\u2500".repeat(Math.max(0, LINE_WIDTH - prefix.length));
|
|
580
|
+
return prefix + dashes;
|
|
581
|
+
}
|
|
582
|
+
function wrapText(text, maxWidth, indent) {
|
|
583
|
+
const rawLines = text.split("\n");
|
|
584
|
+
const result = [];
|
|
585
|
+
for (const rawLine of rawLines) {
|
|
586
|
+
const words = rawLine.split(" ");
|
|
587
|
+
let current = "";
|
|
588
|
+
for (const word of words) {
|
|
589
|
+
if (current === "") {
|
|
590
|
+
current = word;
|
|
591
|
+
} else if (current.length + 1 + word.length <= maxWidth) {
|
|
592
|
+
current += " " + word;
|
|
593
|
+
} else {
|
|
594
|
+
result.push(current);
|
|
595
|
+
current = indent + word;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (current !== "") {
|
|
599
|
+
result.push(current);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
function renderAdvisoryLines(advisory) {
|
|
605
|
+
return wrapText(advisory.trim(), WRAP_WIDTH, " ");
|
|
606
|
+
}
|
|
607
|
+
function writeln(text = "") {
|
|
608
|
+
process.stdout.write(text + "\n");
|
|
609
|
+
}
|
|
610
|
+
function printPreClear(score, sessionLabel) {
|
|
611
|
+
const time = formatTime(/* @__PURE__ */ new Date());
|
|
612
|
+
const sessionPart = sessionLabel ? ` [${sessionLabel}]` : "";
|
|
613
|
+
const prefix = `\u2500\u2500 PRE${sessionPart} \u2500\u2500 ${time} \u2500\u2500 score: ${score.toFixed(2)} \u2500\u2500 \u2713 Clear `;
|
|
614
|
+
const line = separator(prefix);
|
|
615
|
+
writeln(DIM + line + RESET);
|
|
616
|
+
}
|
|
617
|
+
function printPreAdvisory(score, advisory, sessionLabel) {
|
|
618
|
+
const time = formatTime(/* @__PURE__ */ new Date());
|
|
619
|
+
const sessionPart = sessionLabel ? ` [${sessionLabel}]` : "";
|
|
620
|
+
const headerPrefix = `\u2500\u2500 PRE${sessionPart} \u2500\u2500 ${time} \u2500\u2500 score: ${score.toFixed(2)} `;
|
|
621
|
+
const header = separator(headerPrefix);
|
|
622
|
+
writeln(YELLOW + BOLD + header + RESET);
|
|
623
|
+
const lines = renderAdvisoryLines(advisory);
|
|
624
|
+
for (const line of lines) {
|
|
625
|
+
writeln(line);
|
|
626
|
+
}
|
|
627
|
+
writeln(DIM + separator() + RESET);
|
|
628
|
+
}
|
|
629
|
+
function printPost(color, content, sessionLabel) {
|
|
630
|
+
const time = formatTime(/* @__PURE__ */ new Date());
|
|
631
|
+
const sessionPart = sessionLabel ? ` [${sessionLabel}]` : "";
|
|
632
|
+
const header = separator(`\u2500\u2500 POST${sessionPart} \u2500\u2500 ${time} `);
|
|
633
|
+
writeln(color + BOLD + header + RESET);
|
|
634
|
+
const lines = renderAdvisoryLines(content);
|
|
635
|
+
for (const line of lines) {
|
|
636
|
+
writeln(line);
|
|
637
|
+
}
|
|
638
|
+
writeln(DIM + separator() + RESET);
|
|
639
|
+
}
|
|
640
|
+
function printPostAligned(summary, sessionLabel) {
|
|
641
|
+
printPost(GREEN, summary, sessionLabel);
|
|
642
|
+
}
|
|
643
|
+
function printPostMisaligned(advisory, sessionLabel) {
|
|
644
|
+
printPost(RED, advisory, sessionLabel);
|
|
645
|
+
}
|
|
646
|
+
function printSessionStart(label, sessionId) {
|
|
647
|
+
const time = formatTime(/* @__PURE__ */ new Date());
|
|
648
|
+
const shortId = sessionId.slice(0, 8);
|
|
649
|
+
writeln(DIM + CYAN + `\u2500\u2500 ${label} connected (${shortId}\u2026) \u2500\u2500 ${time}` + RESET);
|
|
650
|
+
}
|
|
651
|
+
function printBanner(port) {
|
|
652
|
+
const headerPrefix = "\u2500\u2500 Radar v0.1.0 ";
|
|
653
|
+
const header = separator(headerPrefix);
|
|
654
|
+
writeln(CYAN + BOLD + header + RESET);
|
|
655
|
+
writeln(`Listening on localhost:${port}`);
|
|
656
|
+
writeln("Waiting for Claude Code telemetry...");
|
|
657
|
+
writeln("Set OTEL_LOG_USER_PROMPTS=1 for prompt content analysis.");
|
|
658
|
+
writeln(DIM + separator() + RESET);
|
|
659
|
+
}
|
|
660
|
+
function printWarning(message) {
|
|
661
|
+
writeln(YELLOW + "\u26A0 " + message + RESET);
|
|
662
|
+
}
|
|
663
|
+
function printError(message) {
|
|
664
|
+
writeln(RED + "\u2717 " + message + RESET);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// src/cli/watch.ts
|
|
668
|
+
function errMsg(err) {
|
|
669
|
+
return err instanceof Error ? err.message : String(err);
|
|
670
|
+
}
|
|
671
|
+
async function startWatch(options = {}) {
|
|
672
|
+
const port = options.port ?? 4820;
|
|
673
|
+
const scoreThreshold = options.scoreThreshold ?? 0.6;
|
|
674
|
+
const receiver = new OtlpReceiver(port);
|
|
675
|
+
const aggregator = new TurnAggregator({
|
|
676
|
+
boundaryTimeoutMs: options.boundaryTimeoutMs ?? 5e3
|
|
677
|
+
});
|
|
678
|
+
const classifier = new Classifier(options.apiKey);
|
|
679
|
+
const advisor = new Advisor(options.apiKey);
|
|
680
|
+
let warnedAboutMissingPrompt = false;
|
|
681
|
+
const classifying = /* @__PURE__ */ new Set();
|
|
682
|
+
const sessionLabels = /* @__PURE__ */ new Map();
|
|
683
|
+
aggregator.on("session_start", (s) => {
|
|
684
|
+
sessionLabels.set(s.sessionId, s.label);
|
|
685
|
+
printSessionStart(s.label, s.sessionId);
|
|
686
|
+
});
|
|
687
|
+
receiver.on("event", (event) => {
|
|
688
|
+
aggregator.addEvent(event);
|
|
689
|
+
if (event.type !== "user_prompt") return;
|
|
690
|
+
if (classifying.has(event.promptId)) return;
|
|
691
|
+
classifying.add(event.promptId);
|
|
692
|
+
if (!event.prompt && !warnedAboutMissingPrompt) {
|
|
693
|
+
warnedAboutMissingPrompt = true;
|
|
694
|
+
printWarning(
|
|
695
|
+
"Prompt content not available. Set OTEL_LOG_USER_PROMPTS=1 to enable intent analysis."
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
void runPreAdvisory(event.prompt, event.promptId, sessionLabels.get(event.sessionId));
|
|
699
|
+
});
|
|
700
|
+
receiver.on("error", (err) => {
|
|
701
|
+
printError(`OTLP server error: ${err.message}`);
|
|
702
|
+
});
|
|
703
|
+
aggregator.on("turn_complete", (ctx) => {
|
|
704
|
+
void runPostAdvisory(ctx, sessionLabels.get(ctx.sessionId));
|
|
705
|
+
});
|
|
706
|
+
async function runPreAdvisory(prompt, promptId, sessionLabel) {
|
|
707
|
+
try {
|
|
708
|
+
if (!prompt) return;
|
|
709
|
+
const result = await classifier.classify(prompt);
|
|
710
|
+
if (result.score < scoreThreshold) {
|
|
711
|
+
printPreClear(result.score, sessionLabel);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const advisory = await advisor.preAdvisory(prompt, result);
|
|
715
|
+
printPreAdvisory(result.score, advisory.text, sessionLabel);
|
|
716
|
+
} catch (err) {
|
|
717
|
+
printError(`Pre-advisory failed for prompt ${promptId}: ${errMsg(err)}`);
|
|
718
|
+
} finally {
|
|
719
|
+
classifying.delete(promptId);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async function runPostAdvisory(ctx, sessionLabel) {
|
|
723
|
+
if (!ctx.prompt) return;
|
|
724
|
+
try {
|
|
725
|
+
const result = await advisor.postAdvisory(ctx);
|
|
726
|
+
if (result.aligned === false) {
|
|
727
|
+
printPostMisaligned(result.text, sessionLabel);
|
|
728
|
+
} else {
|
|
729
|
+
printPostAligned(result.text, sessionLabel);
|
|
730
|
+
}
|
|
731
|
+
} catch (err) {
|
|
732
|
+
printError(`Post-advisory failed for prompt ${ctx.promptId}: ${errMsg(err)}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
await receiver.start();
|
|
737
|
+
} catch (err) {
|
|
738
|
+
const msg = errMsg(err);
|
|
739
|
+
if (msg.includes("EADDRINUSE")) {
|
|
740
|
+
printError(`Port ${port} is already in use. Use --port <n> to choose a different port.`);
|
|
741
|
+
} else {
|
|
742
|
+
printError(`Failed to start OTLP receiver: ${msg}`);
|
|
743
|
+
}
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
printBanner(port);
|
|
747
|
+
async function shutdown() {
|
|
748
|
+
process.stdout.write("\n");
|
|
749
|
+
printWarning("Shutting down Radar...");
|
|
750
|
+
await receiver.stop();
|
|
751
|
+
process.exit(0);
|
|
752
|
+
}
|
|
753
|
+
process.on("SIGINT", () => void shutdown());
|
|
754
|
+
process.on("SIGTERM", () => void shutdown());
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/cli/setup.ts
|
|
758
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
759
|
+
import { join } from "path";
|
|
760
|
+
import { homedir } from "os";
|
|
761
|
+
var CLAUDE_DIR = join(homedir(), ".claude");
|
|
762
|
+
var SETTINGS_PATH = join(CLAUDE_DIR, "settings.json");
|
|
763
|
+
var OTEL_VARS = {
|
|
764
|
+
CLAUDE_CODE_ENABLE_TELEMETRY: "1",
|
|
765
|
+
OTEL_LOGS_EXPORTER: "otlp",
|
|
766
|
+
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
|
|
767
|
+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "http://localhost:4820/v1/logs",
|
|
768
|
+
OTEL_LOG_USER_PROMPTS: "1",
|
|
769
|
+
OTEL_LOG_TOOL_DETAILS: "1",
|
|
770
|
+
OTEL_LOGS_EXPORT_INTERVAL: "2000"
|
|
771
|
+
};
|
|
772
|
+
var RESET2 = "\x1B[0m";
|
|
773
|
+
var DIM2 = "\x1B[2m";
|
|
774
|
+
var BOLD2 = "\x1B[1m";
|
|
775
|
+
var GREEN2 = "\x1B[32m";
|
|
776
|
+
var YELLOW2 = "\x1B[33m";
|
|
777
|
+
var CYAN2 = "\x1B[36m";
|
|
778
|
+
var LINE_WIDTH2 = 52;
|
|
779
|
+
function sep(prefix = "") {
|
|
780
|
+
return prefix + "\u2500".repeat(Math.max(0, LINE_WIDTH2 - prefix.length));
|
|
781
|
+
}
|
|
782
|
+
function writeln2(text = "") {
|
|
783
|
+
process.stdout.write(text + "\n");
|
|
784
|
+
}
|
|
785
|
+
function readSettings() {
|
|
786
|
+
if (!existsSync(SETTINGS_PATH)) return {};
|
|
787
|
+
try {
|
|
788
|
+
return JSON.parse(readFileSync(SETTINGS_PATH, "utf8"));
|
|
789
|
+
} catch {
|
|
790
|
+
throw new Error(`Could not parse ${SETTINGS_PATH}. Fix the JSON syntax and try again.`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function writeSettings(settings) {
|
|
794
|
+
if (!existsSync(CLAUDE_DIR)) {
|
|
795
|
+
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
796
|
+
}
|
|
797
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
798
|
+
}
|
|
799
|
+
function runSetup() {
|
|
800
|
+
writeln2(CYAN2 + BOLD2 + sep("\u2500\u2500 Radar Setup ") + RESET2);
|
|
801
|
+
let settings;
|
|
802
|
+
try {
|
|
803
|
+
settings = readSettings();
|
|
804
|
+
} catch (err) {
|
|
805
|
+
writeln2(YELLOW2 + "\u2717 " + (err instanceof Error ? err.message : String(err)) + RESET2);
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
const existingEnv = settings.env ?? {};
|
|
809
|
+
writeln2(`Writing OTel config to ${SETTINGS_PATH.replace(homedir(), "~")}...`);
|
|
810
|
+
writeln2();
|
|
811
|
+
for (const [key, value] of Object.entries(OTEL_VARS)) {
|
|
812
|
+
const alreadySet = key in existingEnv && existingEnv[key] === value;
|
|
813
|
+
const tag = alreadySet ? DIM2 + " (already set)" + RESET2 : "";
|
|
814
|
+
writeln2(` ${GREEN2}\u2713${RESET2} ${key}${tag}`);
|
|
815
|
+
}
|
|
816
|
+
settings.env = { ...existingEnv, ...OTEL_VARS };
|
|
817
|
+
try {
|
|
818
|
+
writeSettings(settings);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
writeln2();
|
|
821
|
+
writeln2(YELLOW2 + "\u2717 Failed to write settings: " + (err instanceof Error ? err.message : String(err)) + RESET2);
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
writeln2();
|
|
825
|
+
writeln2(`${GREEN2}\u2713${RESET2} Settings written to ${SETTINGS_PATH.replace(homedir(), "~")}`);
|
|
826
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
827
|
+
writeln2();
|
|
828
|
+
writeln2(`${YELLOW2}\u26A0${RESET2} ANTHROPIC_API_KEY is not set in your current shell.`);
|
|
829
|
+
writeln2(` Radar needs it to run analysis. Set it before starting:`);
|
|
830
|
+
writeln2(` ${DIM2}export ANTHROPIC_API_KEY=sk-ant-...${RESET2}`);
|
|
831
|
+
}
|
|
832
|
+
writeln2();
|
|
833
|
+
writeln2("Ready. Start Radar in a second terminal pane:");
|
|
834
|
+
writeln2(` ${BOLD2}radar watch${RESET2}`);
|
|
835
|
+
writeln2(DIM2 + sep() + RESET2);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/cli/index.ts
|
|
839
|
+
var program = new Command();
|
|
840
|
+
program.name("radar").description("Non-blocking intent alignment checker for Claude Code, powered by OpenTelemetry").version("0.1.0");
|
|
841
|
+
program.command("watch").description("Start listening for Claude Code telemetry and provide intent advisories").option("-p, --port <number>", "Port to listen on for OTLP log exports", "4820").option(
|
|
842
|
+
"-t, --timeout <ms>",
|
|
843
|
+
"Milliseconds of silence before a turn is considered complete",
|
|
844
|
+
"5000"
|
|
845
|
+
).option(
|
|
846
|
+
"-s, --threshold <score>",
|
|
847
|
+
"Ambiguity score threshold for triggering a pre-advisory (0.0\u20131.0)",
|
|
848
|
+
"0.6"
|
|
849
|
+
).option("-k, --api-key <key>", "Anthropic API key (defaults to ANTHROPIC_API_KEY env var)").action(async (opts) => {
|
|
850
|
+
const port = parseInt(opts.port, 10);
|
|
851
|
+
const boundaryTimeoutMs = parseInt(opts.timeout, 10);
|
|
852
|
+
const scoreThreshold = parseFloat(opts.threshold);
|
|
853
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
854
|
+
printError("--port must be a number between 1 and 65535");
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
if (isNaN(boundaryTimeoutMs) || boundaryTimeoutMs < 500) {
|
|
858
|
+
printError("--timeout must be a number >= 500 (ms)");
|
|
859
|
+
process.exit(1);
|
|
860
|
+
}
|
|
861
|
+
if (isNaN(scoreThreshold) || scoreThreshold < 0 || scoreThreshold > 1) {
|
|
862
|
+
printError("--threshold must be a number between 0.0 and 1.0");
|
|
863
|
+
process.exit(1);
|
|
864
|
+
}
|
|
865
|
+
const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
866
|
+
if (!apiKey) {
|
|
867
|
+
printError("Anthropic API key not found. Set ANTHROPIC_API_KEY or use --api-key <key>.");
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
await startWatch({ port, boundaryTimeoutMs, scoreThreshold, apiKey });
|
|
871
|
+
});
|
|
872
|
+
program.command("setup").description("Write OTel config to ~/.claude/settings.json so Claude Code streams telemetry to Radar").action(() => {
|
|
873
|
+
runSetup();
|
|
874
|
+
});
|
|
875
|
+
if (process.argv.length === 2) {
|
|
876
|
+
program.outputHelp();
|
|
877
|
+
process.exit(0);
|
|
878
|
+
}
|
|
879
|
+
program.parse(process.argv);
|
|
880
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cli/index.ts","../../src/receiver/otlp.ts","../../src/aggregator/turn.ts","../../src/analysis/classifier.ts","../../src/analysis/prompts.ts","../../src/util/async.ts","../../src/analysis/advisor.ts","../../src/output/formatter.ts","../../src/cli/watch.ts","../../src/cli/setup.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { startWatch } from './watch.js';\nimport { runSetup } from './setup.js';\nimport { printError } from '../output/formatter.js';\n\nconst program = new Command();\n\nprogram\n .name('radar')\n .description('Non-blocking intent alignment checker for Claude Code, powered by OpenTelemetry')\n .version('0.1.0');\n\nprogram\n .command('watch')\n .description('Start listening for Claude Code telemetry and provide intent advisories')\n .option('-p, --port <number>', 'Port to listen on for OTLP log exports', '4820')\n .option(\n '-t, --timeout <ms>',\n 'Milliseconds of silence before a turn is considered complete',\n '5000',\n )\n .option(\n '-s, --threshold <score>',\n 'Ambiguity score threshold for triggering a pre-advisory (0.0–1.0)',\n '0.6',\n )\n .option('-k, --api-key <key>', 'Anthropic API key (defaults to ANTHROPIC_API_KEY env var)')\n .action(async (opts: { port: string; timeout: string; threshold: string; apiKey?: string }) => {\n const port = parseInt(opts.port, 10);\n const boundaryTimeoutMs = parseInt(opts.timeout, 10);\n const scoreThreshold = parseFloat(opts.threshold);\n\n if (isNaN(port) || port < 1 || port > 65535) {\n printError('--port must be a number between 1 and 65535');\n process.exit(1);\n }\n\n if (isNaN(boundaryTimeoutMs) || boundaryTimeoutMs < 500) {\n printError('--timeout must be a number >= 500 (ms)');\n process.exit(1);\n }\n\n if (isNaN(scoreThreshold) || scoreThreshold < 0 || scoreThreshold > 1) {\n printError('--threshold must be a number between 0.0 and 1.0');\n process.exit(1);\n }\n\n const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;\n if (!apiKey) {\n printError('Anthropic API key not found. Set ANTHROPIC_API_KEY or use --api-key <key>.');\n process.exit(1);\n }\n\n await startWatch({ port, boundaryTimeoutMs, scoreThreshold, apiKey });\n });\n\nprogram\n .command('setup')\n .description('Write OTel config to ~/.claude/settings.json so Claude Code streams telemetry to Radar')\n .action(() => {\n runSetup();\n });\n\n// Show help if no command is given\nif (process.argv.length === 2) {\n program.outputHelp();\n process.exit(0);\n}\n\nprogram.parse(process.argv);\n","import { EventEmitter } from 'events';\nimport * as http from 'http';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type RadarEventType =\n | 'user_prompt'\n | 'tool_result'\n | 'api_request'\n | 'api_error'\n | 'unknown';\n\nexport interface BaseEvent {\n type: RadarEventType;\n promptId: string;\n sessionId: string;\n timestampMs: number;\n}\n\nexport interface UserPromptEvent extends BaseEvent {\n type: 'user_prompt';\n prompt: string;\n promptLength: number;\n}\n\nexport interface ToolResultEvent extends BaseEvent {\n type: 'tool_result';\n toolName: string;\n success: boolean;\n durationMs: number;\n toolParameters?: string;\n resultSizeBytes?: number;\n}\n\nexport interface ApiRequestEvent extends BaseEvent {\n type: 'api_request';\n model: string;\n costUsd: number;\n inputTokens: number;\n outputTokens: number;\n durationMs: number;\n}\n\nexport interface ApiErrorEvent extends BaseEvent {\n type: 'api_error';\n error: string;\n statusCode?: number;\n}\n\nexport type RadarEvent =\n | UserPromptEvent\n | ToolResultEvent\n | ApiRequestEvent\n | ApiErrorEvent;\n\n// ─── OTLP JSON shape (minimal) ────────────────────────────────────────────────\n\ninterface OtlpAttributeValue {\n stringValue?: string;\n intValue?: number;\n doubleValue?: number;\n boolValue?: boolean;\n}\n\ninterface OtlpAttribute {\n key: string;\n value: OtlpAttributeValue;\n}\n\ninterface OtlpLogRecord {\n timeUnixNano?: string;\n severityNumber?: number;\n body?: { stringValue?: string };\n attributes?: OtlpAttribute[];\n}\n\ninterface OtlpScopeLogs {\n scope?: { name?: string };\n logRecords?: OtlpLogRecord[];\n}\n\ninterface OtlpResourceLogs {\n resource?: { attributes?: OtlpAttribute[] };\n scopeLogs?: OtlpScopeLogs[];\n}\n\ninterface OtlpLogsPayload {\n resourceLogs?: OtlpResourceLogs[];\n}\n\n// ─── Attribute helpers ────────────────────────────────────────────────────────\n\ntype AttrValue = string | number | boolean | undefined;\n\n/** Build a lookup Map from an attribute array — O(n) once, then O(1) per key. */\nfunction buildAttrMap(attrs: OtlpAttribute[] | undefined): Map<string, AttrValue> {\n const map = new Map<string, AttrValue>();\n if (!attrs) return map;\n for (const a of attrs) {\n const v = a.value;\n if (v.stringValue !== undefined) map.set(a.key, v.stringValue);\n else if (v.intValue !== undefined) map.set(a.key, v.intValue);\n else if (v.doubleValue !== undefined) map.set(a.key, v.doubleValue);\n else if (v.boolValue !== undefined) map.set(a.key, v.boolValue);\n }\n return map;\n}\n\nfunction getString(map: Map<string, AttrValue>, key: string): string {\n const v = map.get(key);\n return typeof v === 'string' ? v : '';\n}\n\nfunction getNumber(map: Map<string, AttrValue>, key: string): number {\n const v = map.get(key);\n return typeof v === 'number' ? v : 0;\n}\n\nfunction getBool(map: Map<string, AttrValue>, key: string): boolean {\n const v = map.get(key);\n return typeof v === 'boolean' ? v : false;\n}\n\n// ─── Log record → RadarEvent ──────────────────────────────────────────────────\n\nfunction parseLogRecord(record: OtlpLogRecord, sessionId: string): RadarEvent | null {\n const eventName = record.body?.stringValue ?? '';\n\n // timeUnixNano is a string representing nanoseconds (may exceed JS safe int)\n const timeNano = record.timeUnixNano ?? '0';\n const timestampMs = Math.floor(Number(BigInt(timeNano) / 1_000_000n));\n\n // Build the attribute Map once — O(n) — then do O(1) lookups below\n const attrs = buildAttrMap(record.attributes);\n\n const promptId = getString(attrs, 'prompt.id');\n const base: BaseEvent = { type: 'unknown', promptId, sessionId, timestampMs };\n\n switch (eventName) {\n case 'claude_code.user_prompt': {\n const e: UserPromptEvent = {\n ...base,\n type: 'user_prompt',\n prompt: getString(attrs, 'prompt'),\n promptLength: getNumber(attrs, 'prompt_length'),\n };\n return e;\n }\n\n case 'claude_code.tool_result': {\n const e: ToolResultEvent = {\n ...base,\n type: 'tool_result',\n toolName: getString(attrs, 'tool_name'),\n success: getBool(attrs, 'success'),\n durationMs: getNumber(attrs, 'duration_ms'),\n };\n const toolParameters = getString(attrs, 'tool_parameters');\n if (toolParameters) e.toolParameters = toolParameters;\n const resultSizeBytes = getNumber(attrs, 'result_size_bytes');\n if (resultSizeBytes) e.resultSizeBytes = resultSizeBytes;\n return e;\n }\n\n case 'claude_code.api_request': {\n const e: ApiRequestEvent = {\n ...base,\n type: 'api_request',\n model: getString(attrs, 'model'),\n costUsd: getNumber(attrs, 'cost_usd'),\n inputTokens: getNumber(attrs, 'input_tokens'),\n outputTokens: getNumber(attrs, 'output_tokens'),\n durationMs: getNumber(attrs, 'duration_ms'),\n };\n return e;\n }\n\n case 'claude_code.api_error': {\n const e: ApiErrorEvent = {\n ...base,\n type: 'api_error',\n error: getString(attrs, 'error'),\n };\n const statusCode = getNumber(attrs, 'status_code');\n if (statusCode) e.statusCode = statusCode;\n return e;\n }\n\n default:\n return null;\n }\n}\n\n// ─── OtlpReceiver ─────────────────────────────────────────────────────────────\n\nexport class OtlpReceiver extends EventEmitter {\n private readonly port: number;\n private readonly fallbackSessionId: string;\n private server: http.Server | null = null;\n\n constructor(port = 4820) {\n super();\n this.port = port;\n this.fallbackSessionId = `radar-${Math.random().toString(36).slice(2, 10)}`;\n }\n\n private deriveSessionId(resourceAttrs: Map<string, AttrValue>): string {\n const sessionId = resourceAttrs.get('session.id');\n if (typeof sessionId === 'string' && sessionId) return sessionId;\n\n const instanceId = resourceAttrs.get('service.instance.id');\n if (typeof instanceId === 'string' && instanceId) return instanceId;\n\n const pid = resourceAttrs.get('process.pid');\n if (pid !== undefined) return String(pid);\n\n return this.fallbackSessionId;\n }\n\n start(): Promise<void> {\n return new Promise((resolve, reject) => {\n const server = http.createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n server.on('error', (err) => {\n this.emit('error', err);\n });\n\n server.listen(this.port, () => {\n this.server = server;\n resolve();\n });\n\n // If listen itself throws before the callback\n server.once('error', reject);\n });\n }\n\n stop(): Promise<void> {\n return new Promise((resolve, reject) => {\n if (!this.server) {\n resolve();\n return;\n }\n this.server.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n this.server = null;\n });\n }\n\n private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n if (req.method !== 'POST' || req.url !== '/v1/logs') {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Not found' }));\n return;\n }\n\n const chunks: Buffer[] = [];\n\n req.on('data', (chunk: Buffer) => {\n chunks.push(chunk);\n });\n\n req.on('end', () => {\n // Respond immediately — never block\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ partialSuccess: {} }));\n\n const body = Buffer.concat(chunks).toString('utf8');\n let payload: OtlpLogsPayload;\n\n try {\n payload = JSON.parse(body) as OtlpLogsPayload;\n } catch (err) {\n process.stderr.write(`[radar/otlp] malformed JSON: ${String(err)}\\n`);\n return;\n }\n\n this.processPayload(payload);\n });\n\n req.on('error', (err) => {\n process.stderr.write(`[radar/otlp] request error: ${String(err)}\\n`);\n });\n }\n\n private processPayload(payload: OtlpLogsPayload): void {\n for (const resourceLog of payload.resourceLogs ?? []) {\n const resourceAttrs = buildAttrMap(resourceLog.resource?.attributes);\n const sessionId = this.deriveSessionId(resourceAttrs);\n for (const scopeLog of resourceLog.scopeLogs ?? []) {\n for (const record of scopeLog.logRecords ?? []) {\n const event = parseLogRecord(record, sessionId);\n if (event) {\n this.emit('event', event);\n }\n }\n }\n }\n }\n}\n","import { EventEmitter } from 'events';\nimport type {\n UserPromptEvent,\n ToolResultEvent,\n ApiRequestEvent,\n ApiErrorEvent,\n} from '../receiver/otlp.js';\n\nexport interface ToolResultSummary {\n toolName: string;\n success: boolean;\n durationMs: number;\n bashCommand?: string;\n resultSizeBytes?: number;\n}\n\nexport interface ApiRequestSummary {\n model: string;\n costUsd: number;\n inputTokens: number;\n outputTokens: number;\n durationMs: number;\n}\n\nexport interface SessionSummary {\n sessionId: string;\n label: string; // \"S1\", \"S2\", etc.\n turnCount: number; // turns started\n completedTurns: number; // turns that hit boundary timeout\n startedAt: number; // ms since epoch\n lastSeenAt: number; // ms since epoch\n totalCostUsd: number; // sum across completed turns\n}\n\nexport interface TurnContext {\n promptId: string;\n sessionId: string;\n prompt: string;\n promptLength: number;\n startedAt: number;\n toolResults: ToolResultSummary[];\n apiRequests: ApiRequestSummary[];\n errors: string[];\n // Computed helpers:\n totalCostUsd: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n toolNames: string[];\n}\n\nexport interface TurnAggregatorOptions {\n boundaryTimeoutMs?: number;\n cleanupAfterMs?: number;\n}\n\ntype InternalTurnContext = Omit<\n TurnContext,\n 'totalCostUsd' | 'totalInputTokens' | 'totalOutputTokens' | 'toolNames'\n>;\n\n\nfunction extractBashCommand(toolParameters: unknown): string | undefined {\n if (toolParameters === undefined || toolParameters === null) return undefined;\n\n if (typeof toolParameters === 'string') {\n try {\n const parsed = JSON.parse(toolParameters) as unknown;\n if (typeof parsed === 'object' && parsed !== null && 'command' in parsed) {\n const cmd = (parsed as Record<string, unknown>).command;\n if (typeof cmd === 'string') {\n return cmd.slice(0, 200);\n }\n }\n } catch {\n // not JSON — fall back to raw string truncated\n return toolParameters.slice(0, 200);\n }\n }\n\n if (typeof toolParameters === 'object' && 'command' in (toolParameters as object)) {\n const cmd = (toolParameters as Record<string, unknown>).command;\n if (typeof cmd === 'string') {\n return cmd.slice(0, 200);\n }\n }\n\n return undefined;\n}\n\nfunction buildPublicContext(internal: InternalTurnContext): TurnContext {\n const totalCostUsd = internal.apiRequests.reduce((sum, r) => sum + r.costUsd, 0);\n const totalInputTokens = internal.apiRequests.reduce((sum, r) => sum + r.inputTokens, 0);\n const totalOutputTokens = internal.apiRequests.reduce((sum, r) => sum + r.outputTokens, 0);\n const toolNames = [...new Set(internal.toolResults.map((t) => t.toolName))];\n\n return {\n ...internal,\n totalCostUsd,\n totalInputTokens,\n totalOutputTokens,\n toolNames,\n };\n}\n\nexport class TurnAggregator extends EventEmitter {\n private readonly boundaryTimeoutMs: number;\n private readonly cleanupAfterMs: number;\n\n private readonly contexts = new Map<string, InternalTurnContext>();\n private readonly boundaryTimers = new Map<string, ReturnType<typeof setTimeout>>();\n // Tracks pending context-cleanup timers so they can be cancelled if a promptId\n // is reused before the cleanup window expires, preventing silent context deletion.\n private readonly cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\n private readonly sessions = new Map<string, SessionSummary>();\n private sessionCounter = 0;\n\n constructor(options?: TurnAggregatorOptions) {\n super();\n this.boundaryTimeoutMs = options?.boundaryTimeoutMs ?? 5000;\n this.cleanupAfterMs = options?.cleanupAfterMs ?? 300_000;\n }\n\n getSessions(): SessionSummary[] {\n return [...this.sessions.values()];\n }\n\n getSession(sessionId: string): SessionSummary | undefined {\n return this.sessions.get(sessionId);\n }\n\n addEvent(event: UserPromptEvent | ToolResultEvent | ApiRequestEvent | ApiErrorEvent): void {\n const { promptId } = event;\n\n const isNew = !this.contexts.has(promptId);\n\n if (isNew) {\n // Cancel any pending cleanup for this promptId (handles promptId reuse\n // within the cleanup window — prevents an orphaned timer from silently\n // deleting the freshly created context mid-flight).\n const pendingCleanup = this.cleanupTimers.get(promptId);\n if (pendingCleanup !== undefined) {\n clearTimeout(pendingCleanup);\n this.cleanupTimers.delete(promptId);\n }\n\n const internal: InternalTurnContext = {\n promptId,\n sessionId: event.sessionId,\n prompt: '',\n promptLength: 0,\n startedAt: Date.now(),\n toolResults: [],\n apiRequests: [],\n errors: [],\n };\n this.contexts.set(promptId, internal);\n\n // Session tracking\n if (!this.sessions.has(event.sessionId)) {\n const session: SessionSummary = {\n sessionId: event.sessionId,\n label: `S${++this.sessionCounter}`,\n turnCount: 1,\n completedTurns: 0,\n startedAt: Date.now(),\n lastSeenAt: Date.now(),\n totalCostUsd: 0,\n };\n this.sessions.set(event.sessionId, session);\n this.emit('session_start', { ...session });\n } else {\n const session = this.sessions.get(event.sessionId)!;\n session.lastSeenAt = Date.now();\n session.turnCount++;\n }\n }\n\n const ctx = this.contexts.get(promptId)!;\n\n switch (event.type) {\n case 'user_prompt': {\n ctx.prompt = event.prompt ?? '';\n ctx.promptLength = event.promptLength ?? ctx.prompt.length;\n break;\n }\n case 'tool_result': {\n const summary: ToolResultSummary = {\n toolName: event.toolName,\n success: event.success,\n durationMs: event.durationMs,\n resultSizeBytes: event.resultSizeBytes,\n };\n if (event.toolName === 'Bash') {\n summary.bashCommand = extractBashCommand(event.toolParameters);\n }\n ctx.toolResults.push(summary);\n break;\n }\n case 'api_request': {\n ctx.apiRequests.push({\n model: event.model,\n costUsd: event.costUsd,\n inputTokens: event.inputTokens,\n outputTokens: event.outputTokens,\n durationMs: event.durationMs,\n });\n break;\n }\n case 'api_error': {\n ctx.errors.push(event.error);\n break;\n }\n }\n\n if (isNew) {\n this.emit('turn_start', buildPublicContext(ctx));\n }\n\n this.resetBoundaryTimer(promptId);\n }\n\n getContext(promptId: string): TurnContext | undefined {\n const internal = this.contexts.get(promptId);\n if (!internal) return undefined;\n return buildPublicContext(internal);\n }\n\n private resetBoundaryTimer(promptId: string): void {\n const existing = this.boundaryTimers.get(promptId);\n if (existing !== undefined) {\n clearTimeout(existing);\n }\n\n const timer = setTimeout(() => {\n this.boundaryTimers.delete(promptId);\n const internal = this.contexts.get(promptId);\n if (internal) {\n // Update session stats\n const session = this.sessions.get(internal.sessionId);\n if (session) {\n session.completedTurns++;\n session.totalCostUsd += internal.apiRequests.reduce((s, r) => s + r.costUsd, 0);\n }\n this.emit('turn_complete', buildPublicContext(internal));\n // Schedule cleanup — store handle so it can be cancelled if the promptId\n // is reused before the window expires.\n const cleanupTimer = setTimeout(() => {\n this.contexts.delete(promptId);\n this.cleanupTimers.delete(promptId);\n }, this.cleanupAfterMs);\n this.cleanupTimers.set(promptId, cleanupTimer);\n }\n }, this.boundaryTimeoutMs);\n\n this.boundaryTimers.set(promptId, timer);\n }\n}\n","import Anthropic from '@anthropic-ai/sdk';\nimport { CLASSIFIER_SYSTEM_PROMPT } from './prompts.js';\nimport { withTimeout } from '../util/async.js';\n\nexport interface ClassifierResult {\n score: number; // 0.0 – 1.0\n reason: string;\n}\n\nconst CLASSIFIER_TIMEOUT_MS = 3000;\nconst CLASSIFIER_FALLBACK: ClassifierResult = {\n score: 0.5,\n reason: 'Classification timed out',\n};\n\nexport class Classifier {\n private readonly client: Anthropic;\n\n constructor(apiKey?: string) {\n this.client = new Anthropic({ apiKey: apiKey ?? process.env.ANTHROPIC_API_KEY });\n }\n\n async classify(prompt: string): Promise<ClassifierResult> {\n const classifyPromise = (async (): Promise<ClassifierResult> => {\n const message = await this.client.messages.create({\n model: 'claude-haiku-4-5',\n max_tokens: 100,\n system: CLASSIFIER_SYSTEM_PROMPT,\n messages: [\n {\n role: 'user',\n content: `User prompt to classify:\\n${prompt}`,\n },\n ],\n });\n\n const content = message.content[0];\n if (content.type !== 'text') {\n return CLASSIFIER_FALLBACK;\n }\n\n return parseClassifierResponse(content.text);\n })();\n\n return withTimeout(classifyPromise, CLASSIFIER_TIMEOUT_MS, CLASSIFIER_FALLBACK);\n }\n}\n\nfunction parseClassifierResponse(raw: string): ClassifierResult {\n try {\n // Extract JSON — handle cases where the model wraps it in markdown code blocks\n const jsonMatch = raw.match(/\\{[^{}]*\\}/);\n if (!jsonMatch) {\n return CLASSIFIER_FALLBACK;\n }\n\n const parsed = JSON.parse(jsonMatch[0]) as unknown;\n\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n !('score' in parsed) ||\n !('reason' in parsed)\n ) {\n return CLASSIFIER_FALLBACK;\n }\n\n const obj = parsed as Record<string, unknown>;\n const score = Number(obj.score);\n const reason = String(obj.reason);\n\n if (isNaN(score) || score < 0 || score > 1) {\n return CLASSIFIER_FALLBACK;\n }\n\n return { score, reason };\n } catch {\n return CLASSIFIER_FALLBACK;\n }\n}\n","// Classifier system prompt — used with Haiku (passed as `system:` field)\nexport const CLASSIFIER_SYSTEM_PROMPT: string = `You are an intent-ambiguity classifier for Claude Code, an AI coding assistant.\n\nYour job is to score how likely a user prompt will cause Claude to confidently execute a reasonable but WRONG interpretation — leading to wasted work, unintended changes, or the user having to undo what Claude did.\n\nBe CONSERVATIVE. Only flag genuine ambiguity. Most prompts are clear enough.\n\nCommon failure modes to watch for:\n- Scope ambiguity: \"clean up this module\", \"refactor the service\" — which files? what counts as clean?\n- Target ambiguity: \"the API is slow\", \"fix the tests\" — which API? which tests?\n- Intent ambiguity: \"update the tests\", \"improve error handling\" — add new tests? fix existing? what kind of improvement?\n- Symptom vs cause: \"auth isn't working\" — fix the symptom or find the root cause?\n\nScore guide:\n- 0.0–0.3: Clear and specific. Claude knows exactly what to do.\n- 0.4–0.59: Some ambiguity, but Claude will likely ask for clarification or make a safe default choice.\n- 0.6–0.79: Real risk. Claude will pick an interpretation and run with it — the user might not like the result.\n- 0.8–1.0: High risk. Multiple very different valid interpretations; high chance of wasted work.\n\nDo NOT flag:\n- Questions or requests for explanation (\"how does X work?\", \"what is Y?\")\n- Conversational messages (\"thanks\", \"ok\", \"sounds good\")\n- Read-only or low-stakes requests (\"show me\", \"list\", \"describe\")\n\nRespond with ONLY a JSON object on a single line:\n{\"score\": <0.0-1.0>, \"reason\": \"<one sentence explaining the ambiguity or why it is clear>\"}`;\n\n// Pre-advisory prompt — used with Sonnet\n// {prompt}, {score}, {reason} will be replaced\nexport const PRE_ADVISORY_SYSTEM_PROMPT: string = `You are a concise advisory assistant helping a developer clarify their intent before sending a prompt to Claude Code.\n\nA classifier has flagged the prompt as potentially ambiguous. Your job is to help the user understand the risk and either rephrase or confirm their intent.\n\nOutput at most 4 lines of plain text. No markdown headers, no bullet symbols, no lists. Just plain sentences.\n\nCover:\n1. What Claude will most likely do (the probable misinterpretation that could go wrong)\n2. The specific scope or target risk (what is under-specified)\n3. One clarifying question OR a concrete rephrasing that removes the ambiguity\n\nBe direct and brief. Do not repeat the prompt back verbatim.`;\n\nexport const PRE_ADVISORY_USER_TEMPLATE: string = `Prompt: {prompt}\n\nAmbiguity score: {score}\nReason: {reason}`;\n\n// Post-advisory prompt — used with Sonnet\n// {prompt}, {tools}, {cost}, {tokens} will be replaced\nexport const POST_ADVISORY_SYSTEM_PROMPT: string = `You are a post-execution reviewer for Claude Code. You compare what the user asked for against what Claude actually did.\n\nGiven the original prompt and a summary of tool activity, determine alignment and give brief feedback.\n\nIf aligned: respond with exactly one line starting with \"✓\" — a brief confirmation — followed by a one-line tools/cost summary.\nIf misaligned: respond with a line starting with \"✗\" describing what went wrong, then a line starting with \"→\" containing an exact re-prompt suggestion in quotes.\n\nFormat: plain text, no markdown. Maximum 5 lines total.`;\n\nexport const POST_ADVISORY_USER_TEMPLATE: string = `Original prompt: {prompt}\n\nTool activity: {toolSummary}\nTotal cost: {totalCost}\nTotal tokens: {totalTokens}`;\n","/**\n * Race a promise against a timeout. Cancels the timer when the promise settles,\n * preventing dangling timer handles in long-running processes.\n */\nexport function withTimeout<T>(promise: Promise<T>, ms: number, fallback: T): Promise<T> {\n let timerId: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<T>((resolve) => {\n timerId = setTimeout(() => resolve(fallback), ms);\n });\n return Promise.race([promise, timeout]).finally(() => clearTimeout(timerId));\n}\n","import Anthropic from '@anthropic-ai/sdk';\nimport type { TurnContext } from '../aggregator/turn.js';\nimport type { ClassifierResult } from './classifier.js';\nimport {\n PRE_ADVISORY_SYSTEM_PROMPT,\n PRE_ADVISORY_USER_TEMPLATE,\n POST_ADVISORY_SYSTEM_PROMPT,\n POST_ADVISORY_USER_TEMPLATE,\n} from './prompts.js';\nimport { withTimeout } from '../util/async.js';\n\nexport interface AdvisoryResult {\n text: string;\n aligned?: boolean; // only set for post-advisory\n}\n\nconst ADVISORY_TIMEOUT_MS = 10000;\n\nconst PRE_ADVISORY_FALLBACK: AdvisoryResult = { text: 'Advisory unavailable (timeout)' };\nconst POST_ADVISORY_FALLBACK_TIMEOUT: AdvisoryResult = { text: 'Advisory unavailable (timeout)', aligned: undefined };\nconst POST_ADVISORY_FALLBACK_ERROR: AdvisoryResult = { text: 'Advisory unavailable (error)', aligned: undefined };\n\nexport class Advisor {\n private readonly client: Anthropic;\n\n constructor(apiKey?: string) {\n this.client = new Anthropic({ apiKey: apiKey ?? process.env.ANTHROPIC_API_KEY });\n }\n\n // Pre-advisory: called when classifier score >= 0.6\n async preAdvisory(prompt: string, classification: ClassifierResult): Promise<AdvisoryResult> {\n const userMessage = PRE_ADVISORY_USER_TEMPLATE\n .replace('{prompt}', prompt)\n .replace('{score}', classification.score.toFixed(2))\n .replace('{reason}', classification.reason);\n\n const advisoryPromise = (async (): Promise<AdvisoryResult> => {\n const message = await this.client.messages.create({\n model: 'claude-sonnet-4-5',\n max_tokens: 200,\n system: PRE_ADVISORY_SYSTEM_PROMPT,\n messages: [{ role: 'user', content: userMessage }],\n });\n\n const content = message.content[0];\n if (content.type !== 'text') {\n return PRE_ADVISORY_FALLBACK;\n }\n\n return { text: content.text.trim() };\n })();\n\n return withTimeout(advisoryPromise, ADVISORY_TIMEOUT_MS, PRE_ADVISORY_FALLBACK);\n }\n\n // Post-advisory: called on turn complete\n async postAdvisory(context: TurnContext): Promise<AdvisoryResult> {\n const toolSummary = buildToolSummary(context);\n const totalCost = `$${context.totalCostUsd.toFixed(3)}`;\n const totalTokens = (context.totalInputTokens + context.totalOutputTokens).toLocaleString();\n\n const userMessage = POST_ADVISORY_USER_TEMPLATE\n .replace('{prompt}', context.prompt)\n .replace('{toolSummary}', toolSummary)\n .replace('{totalCost}', totalCost)\n .replace('{totalTokens}', totalTokens);\n\n const advisoryPromise = (async (): Promise<AdvisoryResult> => {\n const message = await this.client.messages.create({\n model: 'claude-sonnet-4-5',\n max_tokens: 300,\n system: POST_ADVISORY_SYSTEM_PROMPT,\n messages: [{ role: 'user', content: userMessage }],\n });\n\n const content = message.content[0];\n if (content.type !== 'text') {\n return POST_ADVISORY_FALLBACK_ERROR;\n }\n\n const text = content.text.trim();\n const aligned = text.startsWith('✓') ? true : text.startsWith('✗') ? false : undefined;\n\n return { text, aligned };\n })();\n\n return withTimeout(advisoryPromise, ADVISORY_TIMEOUT_MS, POST_ADVISORY_FALLBACK_TIMEOUT);\n }\n}\n\nfunction buildToolSummary(context: TurnContext): string {\n const parts: string[] = [];\n\n // Group tool calls by name, tracking Bash separately\n const toolCounts = new Map<string, number>();\n const bashCommands: string[] = [];\n let bashCallCount = 0;\n\n for (const result of context.toolResults) {\n if (result.toolName === 'Bash') {\n bashCallCount++;\n if (result.bashCommand) {\n bashCommands.push(`'${result.bashCommand}'`);\n }\n } else {\n toolCounts.set(result.toolName, (toolCounts.get(result.toolName) ?? 0) + 1);\n }\n }\n\n // Add non-bash tools\n for (const [toolName, count] of toolCounts.entries()) {\n parts.push(count === 1 ? toolName : `${toolName} (${count} calls)`);\n }\n\n // Add bash summary\n if (bashCallCount > 0) {\n if (bashCommands.length > 0) {\n const bashLabel = bashCommands.length <= 3\n ? `Bash: ${bashCommands.join(', ')}`\n : `Bash: ${bashCommands.slice(0, 3).join(', ')} +${bashCommands.length - 3} more`;\n parts.push(bashLabel);\n } else {\n parts.push(`Bash (${bashCallCount} calls)`);\n }\n }\n\n // Token and cost summary\n const totalTokens = context.totalInputTokens + context.totalOutputTokens;\n if (totalTokens > 0) {\n parts.push(`${totalTokens.toLocaleString()} tokens`);\n }\n if (context.totalCostUsd > 0) {\n parts.push(`$${context.totalCostUsd.toFixed(3)}`);\n }\n\n return parts.join(' · ') || 'No tools used';\n}\n","// ─── ANSI helpers ─────────────────────────────────────────────────────────────\n\nconst RESET = '\\x1b[0m';\nconst DIM = '\\x1b[2m';\nconst BOLD = '\\x1b[1m';\nconst GREEN = '\\x1b[32m';\nconst YELLOW = '\\x1b[33m';\nconst RED = '\\x1b[31m';\nconst CYAN = '\\x1b[36m';\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst LINE_WIDTH = 52;\nconst WRAP_WIDTH = 50;\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\n/**\n * Format a Date as \"HH:MM:SS\".\n */\nexport function formatTime(date: Date): string {\n const hh = String(date.getHours()).padStart(2, '0');\n const mm = String(date.getMinutes()).padStart(2, '0');\n const ss = String(date.getSeconds()).padStart(2, '0');\n return `${hh}:${mm}:${ss}`;\n}\n\n/**\n * Build a separator line of exactly LINE_WIDTH chars, padded with \"─\".\n * The prefix is included in the total width.\n */\nfunction separator(prefix = ''): string {\n const dashes = '─'.repeat(Math.max(0, LINE_WIDTH - prefix.length));\n return prefix + dashes;\n}\n\n/**\n * Wrap text to at most maxWidth characters per line, preserving existing newlines.\n * Continuation lines are indented with `indent` spaces.\n */\nfunction wrapText(text: string, maxWidth: number, indent: string): string[] {\n const rawLines = text.split('\\n');\n const result: string[] = [];\n\n for (const rawLine of rawLines) {\n const words = rawLine.split(' ');\n let current = '';\n\n for (const word of words) {\n if (current === '') {\n current = word;\n } else if (current.length + 1 + word.length <= maxWidth) {\n current += ' ' + word;\n } else {\n result.push(current);\n current = indent + word;\n }\n }\n\n if (current !== '') {\n result.push(current);\n }\n }\n\n return result;\n}\n\n/**\n * Render advisory text as output lines. The first line is left as-is (the\n * caller has already formatted it). Subsequent lines and long first lines are\n * word-wrapped at WRAP_WIDTH with a 2-space indent on continuations.\n */\nfunction renderAdvisoryLines(advisory: string): string[] {\n return wrapText(advisory.trim(), WRAP_WIDTH, ' ');\n}\n\nfunction writeln(text = ''): void {\n process.stdout.write(text + '\\n');\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Print a \"clear\" one-liner — dim, no box.\n *\n * Example:\n * ── PRE ── 14:23:07 ── score: 0.34 ── ✓ Clear ─────\n * ── PRE [S1] ── 14:23:07 ── score: 0.34 ── ✓ Clear ─────\n */\nexport function printPreClear(score: number, sessionLabel?: string): void {\n const time = formatTime(new Date());\n const sessionPart = sessionLabel ? ` [${sessionLabel}]` : '';\n const prefix = `── PRE${sessionPart} ── ${time} ── score: ${score.toFixed(2)} ── ✓ Clear `;\n const line = separator(prefix);\n writeln(DIM + line + RESET);\n}\n\n/**\n * Print a pre-advisory warning box with a yellow header.\n *\n * Example:\n * ── PRE ── 14:25:12 ── score: 0.78 ─────────────────\n * ── PRE [S1] ── 14:25:12 ── score: 0.78 ─────────────────\n */\nexport function printPreAdvisory(score: number, advisory: string, sessionLabel?: string): void {\n const time = formatTime(new Date());\n const sessionPart = sessionLabel ? ` [${sessionLabel}]` : '';\n const headerPrefix = `── PRE${sessionPart} ── ${time} ── score: ${score.toFixed(2)} `;\n const header = separator(headerPrefix);\n\n writeln(YELLOW + BOLD + header + RESET);\n\n const lines = renderAdvisoryLines(advisory);\n for (const line of lines) {\n writeln(line);\n }\n\n writeln(DIM + separator() + RESET);\n}\n\n/** Shared implementation for both post-advisory box variants. */\nfunction printPost(color: string, content: string, sessionLabel?: string): void {\n const time = formatTime(new Date());\n const sessionPart = sessionLabel ? ` [${sessionLabel}]` : '';\n const header = separator(`── POST${sessionPart} ── ${time} `);\n\n writeln(color + BOLD + header + RESET);\n\n const lines = renderAdvisoryLines(content);\n for (const line of lines) {\n writeln(line);\n }\n\n writeln(DIM + separator() + RESET);\n}\n\n/**\n * Print a post-advisory \"aligned\" box with a green header.\n *\n * Example:\n * ── POST ── 14:25:38 ────────────────────────────────\n * ── POST [S1] ── 14:25:38 ────────────────────────────────\n */\nexport function printPostAligned(summary: string, sessionLabel?: string): void {\n printPost(GREEN, summary, sessionLabel);\n}\n\n/**\n * Print a post-advisory \"misaligned\" box with a red header.\n *\n * Example:\n * ── POST ── 14:31:02 ────────────────────────────────\n * ── POST [S1] ── 14:31:02 ────────────────────────────────\n */\nexport function printPostMisaligned(advisory: string, sessionLabel?: string): void {\n printPost(RED, advisory, sessionLabel);\n}\n\n/**\n * Print a dim cyan session-connected line.\n *\n * Example:\n * ── S1 connected (abcd1234…) ── 14:23:07\n */\nexport function printSessionStart(label: string, sessionId: string): void {\n const time = formatTime(new Date());\n const shortId = sessionId.slice(0, 8);\n writeln(DIM + CYAN + `── ${label} connected (${shortId}…) ── ${time}` + RESET);\n}\n\n/**\n * Print the startup banner.\n *\n * Example:\n * ── Radar v0.1.0 ────────────────────────────────────\n * Listening on localhost:4820\n * Waiting for Claude Code telemetry...\n * Set OTEL_LOG_USER_PROMPTS=1 for prompt content analysis.\n * ────────────────────────────────────────────────────\n */\nexport function printBanner(port: number): void {\n const headerPrefix = '── Radar v0.1.0 ';\n const header = separator(headerPrefix);\n\n writeln(CYAN + BOLD + header + RESET);\n writeln(`Listening on localhost:${port}`);\n writeln('Waiting for Claude Code telemetry...');\n writeln('Set OTEL_LOG_USER_PROMPTS=1 for prompt content analysis.');\n writeln(DIM + separator() + RESET);\n}\n\n/**\n * Print a warning message in yellow.\n */\nexport function printWarning(message: string): void {\n writeln(YELLOW + '⚠ ' + message + RESET);\n}\n\n/**\n * Print an error message in red.\n */\nexport function printError(message: string): void {\n writeln(RED + '✗ ' + message + RESET);\n}\n","import { OtlpReceiver } from '../receiver/otlp.js';\nimport type { RadarEvent } from '../receiver/otlp.js';\nimport { TurnAggregator } from '../aggregator/turn.js';\nimport type { TurnContext, SessionSummary } from '../aggregator/turn.js';\nimport { Classifier } from '../analysis/classifier.js';\nimport { Advisor } from '../analysis/advisor.js';\nimport {\n printBanner,\n printPreClear,\n printPreAdvisory,\n printPostAligned,\n printPostMisaligned,\n printSessionStart,\n printWarning,\n printError,\n} from '../output/formatter.js';\n\nexport interface WatchOptions {\n port?: number;\n boundaryTimeoutMs?: number;\n scoreThreshold?: number;\n apiKey?: string;\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nexport async function startWatch(options: WatchOptions = {}): Promise<void> {\n const port = options.port ?? 4820;\n const scoreThreshold = options.scoreThreshold ?? 0.6;\n\n const receiver = new OtlpReceiver(port);\n const aggregator = new TurnAggregator({\n boundaryTimeoutMs: options.boundaryTimeoutMs ?? 5000,\n });\n const classifier = new Classifier(options.apiKey);\n const advisor = new Advisor(options.apiKey);\n\n // Track whether we've seen any events without prompt content — warn once\n let warnedAboutMissingPrompt = false;\n // Prevent double-classification if the same promptId is seen more than once\n const classifying = new Set<string>();\n // Map sessionId → display label (\"S1\", \"S2\", …)\n const sessionLabels = new Map<string, string>();\n\n // ── Wire: session_start → label tracking + display ────────────────────────\n aggregator.on('session_start', (s: SessionSummary) => {\n sessionLabels.set(s.sessionId, s.label);\n printSessionStart(s.label, s.sessionId);\n });\n\n // ── Wire: OtlpReceiver → TurnAggregator + classification ───────────────────\n //\n // A single listener handles both jobs in order: aggregation first so that\n // TurnContext exists by the time classification starts, then classification\n // for user_prompt events.\n receiver.on('event', (event: RadarEvent) => {\n // 1. Always feed the aggregator\n aggregator.addEvent(event);\n\n // 2. On user_prompt, trigger pre-advisory (fire-and-forget)\n if (event.type !== 'user_prompt') return;\n\n if (classifying.has(event.promptId)) return;\n classifying.add(event.promptId);\n\n if (!event.prompt && !warnedAboutMissingPrompt) {\n warnedAboutMissingPrompt = true;\n printWarning(\n 'Prompt content not available. Set OTEL_LOG_USER_PROMPTS=1 to enable intent analysis.',\n );\n }\n\n void runPreAdvisory(event.prompt, event.promptId, sessionLabels.get(event.sessionId));\n });\n\n receiver.on('error', (err: Error) => {\n printError(`OTLP server error: ${err.message}`);\n });\n\n // ── Wire: TurnAggregator → post-advisory ───────────────────────────────────\n aggregator.on('turn_complete', (ctx: TurnContext) => {\n void runPostAdvisory(ctx, sessionLabels.get(ctx.sessionId));\n });\n\n // ── Pre-advisory pipeline ───────────────────────────────────────────────────\n async function runPreAdvisory(prompt: string, promptId: string, sessionLabel?: string): Promise<void> {\n try {\n if (!prompt) return; // no prompt text — skip silently\n\n const result = await classifier.classify(prompt);\n\n if (result.score < scoreThreshold) {\n printPreClear(result.score, sessionLabel);\n return;\n }\n\n // Score >= threshold: escalate to Sonnet\n const advisory = await advisor.preAdvisory(prompt, result);\n printPreAdvisory(result.score, advisory.text, sessionLabel);\n } catch (err) {\n printError(`Pre-advisory failed for prompt ${promptId}: ${errMsg(err)}`);\n } finally {\n // Always release the deduplication guard once pre-advisory finishes,\n // whether it succeeded, failed, or was skipped due to missing prompt.\n classifying.delete(promptId);\n }\n }\n\n // ── Post-advisory pipeline ──────────────────────────────────────────────────\n async function runPostAdvisory(ctx: TurnContext, sessionLabel?: string): Promise<void> {\n if (!ctx.prompt) return; // no prompt text — skip silently\n\n try {\n const result = await advisor.postAdvisory(ctx);\n\n if (result.aligned === false) {\n printPostMisaligned(result.text, sessionLabel);\n } else {\n printPostAligned(result.text, sessionLabel);\n }\n } catch (err) {\n printError(`Post-advisory failed for prompt ${ctx.promptId}: ${errMsg(err)}`);\n }\n }\n\n // ── Start ───────────────────────────────────────────────────────────────────\n try {\n await receiver.start();\n } catch (err) {\n const msg = errMsg(err);\n if (msg.includes('EADDRINUSE')) {\n printError(`Port ${port} is already in use. Use --port <n> to choose a different port.`);\n } else {\n printError(`Failed to start OTLP receiver: ${msg}`);\n }\n process.exit(1);\n }\n\n printBanner(port);\n\n // ── Graceful shutdown ───────────────────────────────────────────────────────\n async function shutdown(): Promise<void> {\n process.stdout.write('\\n');\n printWarning('Shutting down Radar...');\n await receiver.stop();\n process.exit(0);\n }\n\n process.on('SIGINT', () => void shutdown());\n process.on('SIGTERM', () => void shutdown());\n}\n","import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst CLAUDE_DIR = join(homedir(), '.claude');\nconst SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json');\n\nconst OTEL_VARS: Record<string, string> = {\n CLAUDE_CODE_ENABLE_TELEMETRY: '1',\n OTEL_LOGS_EXPORTER: 'otlp',\n OTEL_EXPORTER_OTLP_PROTOCOL: 'http/json',\n OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: 'http://localhost:4820/v1/logs',\n OTEL_LOG_USER_PROMPTS: '1',\n OTEL_LOG_TOOL_DETAILS: '1',\n OTEL_LOGS_EXPORT_INTERVAL: '2000',\n};\n\n// ─── ANSI helpers (self-contained, no formatter dependency) ──────────────────\n\nconst RESET = '\\x1b[0m';\nconst DIM = '\\x1b[2m';\nconst BOLD = '\\x1b[1m';\nconst GREEN = '\\x1b[32m';\nconst YELLOW = '\\x1b[33m';\nconst CYAN = '\\x1b[36m';\n\nconst LINE_WIDTH = 52;\n\nfunction sep(prefix = ''): string {\n return prefix + '─'.repeat(Math.max(0, LINE_WIDTH - prefix.length));\n}\n\nfunction writeln(text = ''): void {\n process.stdout.write(text + '\\n');\n}\n\n// ─── Settings helpers ─────────────────────────────────────────────────────────\n\nfunction readSettings(): Record<string, unknown> {\n if (!existsSync(SETTINGS_PATH)) return {};\n try {\n return JSON.parse(readFileSync(SETTINGS_PATH, 'utf8')) as Record<string, unknown>;\n } catch {\n throw new Error(`Could not parse ${SETTINGS_PATH}. Fix the JSON syntax and try again.`);\n }\n}\n\nfunction writeSettings(settings: Record<string, unknown>): void {\n if (!existsSync(CLAUDE_DIR)) {\n mkdirSync(CLAUDE_DIR, { recursive: true });\n }\n writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\\n', 'utf8');\n}\n\n// ─── Main ─────────────────────────────────────────────────────────────────────\n\nexport function runSetup(): void {\n writeln(CYAN + BOLD + sep('── Radar Setup ') + RESET);\n\n // ── Read existing settings ────────────────────────────────────────────────\n let settings: Record<string, unknown>;\n try {\n settings = readSettings();\n } catch (err) {\n writeln(YELLOW + '✗ ' + (err instanceof Error ? err.message : String(err)) + RESET);\n process.exit(1);\n }\n\n const existingEnv = (settings.env as Record<string, string> | undefined) ?? {};\n\n // ── Merge OTel vars ───────────────────────────────────────────────────────\n writeln(`Writing OTel config to ${SETTINGS_PATH.replace(homedir(), '~')}...`);\n writeln();\n\n for (const [key, value] of Object.entries(OTEL_VARS)) {\n const alreadySet = key in existingEnv && existingEnv[key] === value;\n const tag = alreadySet ? DIM + ' (already set)' + RESET : '';\n writeln(` ${GREEN}✓${RESET} ${key}${tag}`);\n }\n\n settings.env = { ...existingEnv, ...OTEL_VARS };\n\n try {\n writeSettings(settings);\n } catch (err) {\n writeln();\n writeln(YELLOW + '✗ Failed to write settings: ' + (err instanceof Error ? err.message : String(err)) + RESET);\n process.exit(1);\n }\n\n writeln();\n writeln(`${GREEN}✓${RESET} Settings written to ${SETTINGS_PATH.replace(homedir(), '~')}`);\n\n // ── API key check ─────────────────────────────────────────────────────────\n if (!process.env.ANTHROPIC_API_KEY) {\n writeln();\n writeln(`${YELLOW}⚠${RESET} ANTHROPIC_API_KEY is not set in your current shell.`);\n writeln(` Radar needs it to run analysis. Set it before starting:`);\n writeln(` ${DIM}export ANTHROPIC_API_KEY=sk-ant-...${RESET}`);\n }\n\n // ── Done ──────────────────────────────────────────────────────────────────\n writeln();\n writeln('Ready. Start Radar in a second terminal pane:');\n writeln(` ${BOLD}radar watch${RESET}`);\n writeln(DIM + sep() + RESET);\n}\n"],"mappings":"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","names":["EventEmitter","Anthropic","Anthropic","RESET","DIM","BOLD","GREEN","YELLOW","CYAN","LINE_WIDTH","writeln"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "radar-cc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Non-blocking intent alignment checker for Claude Code, powered by OpenTelemetry",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"radar": "./dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/cli/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"dev": "tsup --watch & node --watch dist/cli/index.js watch",
|
|
16
|
+
"start": "node dist/cli/index.js",
|
|
17
|
+
"test": "npx tsx --test src/test/*.test.ts",
|
|
18
|
+
"typecheck": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
22
|
+
"commander": "^12.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"tsup": "^8.0.0",
|
|
26
|
+
"typescript": "^5.4.0",
|
|
27
|
+
"tsx": "^4.0.0",
|
|
28
|
+
"@types/node": "^20.0.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18.0.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"claude-code",
|
|
35
|
+
"opentelemetry",
|
|
36
|
+
"otel",
|
|
37
|
+
"intent",
|
|
38
|
+
"ambiguity",
|
|
39
|
+
"ai",
|
|
40
|
+
"developer-tools"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT"
|
|
43
|
+
}
|