radar-cc 0.1.0 → 0.1.2
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 +78 -71
- package/dist/cli/index.js +219 -21
- package/dist/cli/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,104 +1,111 @@
|
|
|
1
|
-
#
|
|
1
|
+
# radar-cc
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Intent alignment checker for Claude Code. Radar watches Claude Code's OpenTelemetry stream and tells you — in a second terminal pane — whether your prompt was clear before Claude starts, and whether Claude stayed on target after it finishes.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Requirements
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Node.js >= 22
|
|
8
|
+
- An Anthropic API key
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
## Install
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
12
|
+
```sh
|
|
13
|
+
npm install -g radar-cc
|
|
14
|
+
radar setup
|
|
15
|
+
```
|
|
25
16
|
|
|
26
|
-
|
|
17
|
+
`radar setup` writes the required OTel environment variables to `~/.claude/settings.json` and walks you through storing your Anthropic API key — either on local disk or in 1Password. Restart Claude Code after running it.
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
git clone https://github.com/ahaydar/radar.git
|
|
30
|
-
cd radar
|
|
31
|
-
npm install
|
|
32
|
-
npm run build
|
|
33
|
-
npm link
|
|
34
|
-
```
|
|
19
|
+
You can also pass the key directly to skip the prompt:
|
|
35
20
|
|
|
36
|
-
|
|
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
|
|
21
|
+
```sh
|
|
22
|
+
radar setup --api-key <your-key>
|
|
51
23
|
```
|
|
52
24
|
|
|
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
25
|
## Usage
|
|
56
26
|
|
|
57
|
-
|
|
27
|
+
Open a second terminal pane alongside Claude Code and run:
|
|
58
28
|
|
|
59
|
-
```
|
|
60
|
-
|
|
29
|
+
```sh
|
|
30
|
+
radar watch
|
|
61
31
|
```
|
|
62
32
|
|
|
63
|
-
|
|
33
|
+
Send prompts in your Claude Code pane as normal. Radar listens passively on `localhost:4820`.
|
|
64
34
|
|
|
65
|
-
|
|
66
|
-
|
|
35
|
+
## Output
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
── Radar v0.1.0 ─────────────────────────────────────
|
|
39
|
+
Listening on localhost:4820
|
|
40
|
+
Waiting for Claude Code telemetry...
|
|
41
|
+
─────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
── PRE ── 14:23:07 ── score: 0.34 ── ✓ Clear ────────
|
|
44
|
+
|
|
45
|
+
── PRE ── 14:25:12 ── score: 0.78 ───────────────────
|
|
46
|
+
⚠ "clean up this module" is ambiguous.
|
|
47
|
+
Claude will likely restructure imports and rename functions.
|
|
48
|
+
Did you mean: remove the 3 commented-out functions?
|
|
49
|
+
→ Try: "delete the dead code in auth.ts — the 3 commented
|
|
50
|
+
functions at the bottom"
|
|
51
|
+
─────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
── POST ── 14:25:38 ──────────────────────────────────
|
|
54
|
+
✓ Response aligned with intent.
|
|
55
|
+
Tools: Edit (2 files) · 847 tokens · $0.003
|
|
56
|
+
─────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
── POST ── 14:31:02 ──────────────────────────────────
|
|
59
|
+
✗ Scope exceeded likely intent.
|
|
60
|
+
Claude ran Edit on 5 files, Bash (3 commands), 12k tokens, $0.08.
|
|
61
|
+
Developer likely wanted: coverage for 2 new edge cases only.
|
|
62
|
+
→ "undo all changes. add test cases for the null input
|
|
63
|
+
and timeout edge cases in processOrder — nothing else"
|
|
64
|
+
─────────────────────────────────────────────────────
|
|
67
65
|
```
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
PRE advisories fire within ~2 seconds of your prompt. POST advisories fire after Claude's turn ends, based on what tools it used and what it changed — not Claude's response text, which OTel does not expose.
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
radar watch
|
|
73
|
-
```
|
|
69
|
+
## How it works
|
|
74
70
|
|
|
75
|
-
|
|
71
|
+
Claude Code emits structured OTel log events (`user_prompt`, `tool_result`, `api_request`) when telemetry is enabled. Radar runs a lightweight OTLP HTTP receiver on `localhost:4820` that collects these events without touching Claude's execution path.
|
|
76
72
|
|
|
77
|
-
|
|
73
|
+
- **Pre-advisory:** Haiku scores the prompt for ambiguity. If the score is >= 0.6, Sonnet prints a warning with the most likely misinterpretation and a suggested clarification.
|
|
74
|
+
- **Post-advisory:** After no new events for ~5 seconds (turn boundary), Sonnet reviews the accumulated tool activity against the original prompt intent and flags scope drift.
|
|
78
75
|
|
|
79
|
-
|
|
76
|
+
Claude is never blocked or interrupted.
|
|
80
77
|
|
|
81
|
-
|
|
78
|
+
## OTel variables (set by `radar setup`)
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
| Variable | Value |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `CLAUDE_CODE_ENABLE_TELEMETRY` | `1` |
|
|
83
|
+
| `OTEL_LOGS_EXPORTER` | `otlp` |
|
|
84
|
+
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `http/json` |
|
|
85
|
+
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | `http://localhost:4820/v1/logs` |
|
|
86
|
+
| `OTEL_LOG_USER_PROMPTS` | `1` |
|
|
87
|
+
| `OTEL_LOG_TOOL_DETAILS` | `1` |
|
|
88
|
+
| `OTEL_LOGS_EXPORT_INTERVAL` | `2000` |
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
`OTEL_LOG_USER_PROMPTS=1` is required for prompt content analysis. Without it, Radar can detect turn boundaries but cannot analyze intent.
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
These are written to the `env` block in `~/.claude/settings.json`. If Radar is not running, Claude Code silently ignores them.
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
## Options
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
```
|
|
97
|
+
radar setup [options]
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
-k, --api-key <key> API key to store (skips the interactive prompt)
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
radar watch [options]
|
|
98
102
|
|
|
99
|
-
|
|
103
|
+
-p, --port <number> OTLP listener port (default: 4820)
|
|
104
|
+
-t, --timeout <ms> Turn boundary silence window (default: 5000)
|
|
105
|
+
-s, --threshold <score> Ambiguity score threshold 0.0–1.0 (default: 0.6)
|
|
106
|
+
-k, --api-key <key> Anthropic API key (overrides all stored sources)
|
|
107
|
+
```
|
|
100
108
|
|
|
101
|
-
|
|
109
|
+
## License
|
|
102
110
|
|
|
103
|
-
|
|
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.
|
|
111
|
+
MIT
|
package/dist/cli/index.js
CHANGED
|
@@ -732,6 +732,15 @@ async function startWatch(options = {}) {
|
|
|
732
732
|
printError(`Post-advisory failed for prompt ${ctx.promptId}: ${errMsg(err)}`);
|
|
733
733
|
}
|
|
734
734
|
}
|
|
735
|
+
const requiredOtelVars = [
|
|
736
|
+
"CLAUDE_CODE_ENABLE_TELEMETRY",
|
|
737
|
+
"OTEL_LOGS_EXPORTER",
|
|
738
|
+
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"
|
|
739
|
+
];
|
|
740
|
+
const missingVars = requiredOtelVars.filter((v) => !process.env[v]);
|
|
741
|
+
if (missingVars.length > 0) {
|
|
742
|
+
printWarning("OTel env vars not configured. Run `radar setup` and restart Claude Code.");
|
|
743
|
+
}
|
|
735
744
|
try {
|
|
736
745
|
await receiver.start();
|
|
737
746
|
} catch (err) {
|
|
@@ -755,11 +764,76 @@ async function startWatch(options = {}) {
|
|
|
755
764
|
}
|
|
756
765
|
|
|
757
766
|
// src/cli/setup.ts
|
|
767
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
768
|
+
import { join as join2 } from "path";
|
|
769
|
+
import { homedir as homedir2 } from "os";
|
|
770
|
+
import { createInterface } from "readline";
|
|
771
|
+
|
|
772
|
+
// src/cli/config.ts
|
|
758
773
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
774
|
+
import { execSync } from "child_process";
|
|
759
775
|
import { join } from "path";
|
|
760
776
|
import { homedir } from "os";
|
|
761
|
-
var
|
|
762
|
-
var
|
|
777
|
+
var CONFIG_DIR = join(homedir(), ".config", "radar");
|
|
778
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
779
|
+
function readConfig() {
|
|
780
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
781
|
+
try {
|
|
782
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
783
|
+
} catch {
|
|
784
|
+
return {};
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function writeConfig(config) {
|
|
788
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
789
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
790
|
+
}
|
|
791
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
792
|
+
}
|
|
793
|
+
function configPath() {
|
|
794
|
+
return CONFIG_PATH;
|
|
795
|
+
}
|
|
796
|
+
function isOpAvailable() {
|
|
797
|
+
try {
|
|
798
|
+
execSync("op --version", { stdio: "ignore" });
|
|
799
|
+
return true;
|
|
800
|
+
} catch {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function createOpItem(apiKey) {
|
|
805
|
+
const title = "Radar Anthropic API Key";
|
|
806
|
+
const raw = execSync(
|
|
807
|
+
`op item create --category=login --title="${title}" "password=${apiKey}" --format json`,
|
|
808
|
+
{ encoding: "utf8" }
|
|
809
|
+
);
|
|
810
|
+
const item = JSON.parse(raw);
|
|
811
|
+
return `op://${item.vault.name}/${item.title}/password`;
|
|
812
|
+
}
|
|
813
|
+
function readOpItem(ref) {
|
|
814
|
+
return execSync(`op read "${ref}"`, { encoding: "utf8" }).trim();
|
|
815
|
+
}
|
|
816
|
+
function resolveStoredApiKey() {
|
|
817
|
+
const config = readConfig();
|
|
818
|
+
if (config.apiKey) return config.apiKey;
|
|
819
|
+
if (config.apiKeyRef) {
|
|
820
|
+
try {
|
|
821
|
+
return readOpItem(config.apiKeyRef);
|
|
822
|
+
} catch (err) {
|
|
823
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
824
|
+
throw new Error(
|
|
825
|
+
`Failed to read API key from 1Password (${config.apiKeyRef}).
|
|
826
|
+
Make sure you are signed in: op signin
|
|
827
|
+
Detail: ${detail}`
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return void 0;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/cli/setup.ts
|
|
835
|
+
var CLAUDE_DIR = join2(homedir2(), ".claude");
|
|
836
|
+
var SETTINGS_PATH = join2(CLAUDE_DIR, "settings.json");
|
|
763
837
|
var OTEL_VARS = {
|
|
764
838
|
CLAUDE_CODE_ENABLE_TELEMETRY: "1",
|
|
765
839
|
OTEL_LOGS_EXPORTER: "otlp",
|
|
@@ -783,20 +857,23 @@ function writeln2(text = "") {
|
|
|
783
857
|
process.stdout.write(text + "\n");
|
|
784
858
|
}
|
|
785
859
|
function readSettings() {
|
|
786
|
-
if (!
|
|
860
|
+
if (!existsSync2(SETTINGS_PATH)) return {};
|
|
787
861
|
try {
|
|
788
|
-
return JSON.parse(
|
|
862
|
+
return JSON.parse(readFileSync2(SETTINGS_PATH, "utf8"));
|
|
789
863
|
} catch {
|
|
790
864
|
throw new Error(`Could not parse ${SETTINGS_PATH}. Fix the JSON syntax and try again.`);
|
|
791
865
|
}
|
|
792
866
|
}
|
|
793
867
|
function writeSettings(settings) {
|
|
794
|
-
if (!
|
|
795
|
-
|
|
868
|
+
if (!existsSync2(CLAUDE_DIR)) {
|
|
869
|
+
mkdirSync2(CLAUDE_DIR, { recursive: true });
|
|
796
870
|
}
|
|
797
|
-
|
|
871
|
+
writeFileSync2(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
872
|
+
}
|
|
873
|
+
function question(rl, prompt) {
|
|
874
|
+
return new Promise((resolve) => rl.question(prompt, resolve));
|
|
798
875
|
}
|
|
799
|
-
function runSetup() {
|
|
876
|
+
async function runSetup(preEnteredKey) {
|
|
800
877
|
writeln2(CYAN2 + BOLD2 + sep("\u2500\u2500 Radar Setup ") + RESET2);
|
|
801
878
|
let settings;
|
|
802
879
|
try {
|
|
@@ -806,7 +883,7 @@ function runSetup() {
|
|
|
806
883
|
process.exit(1);
|
|
807
884
|
}
|
|
808
885
|
const existingEnv = settings.env ?? {};
|
|
809
|
-
writeln2(`Writing OTel config to ${SETTINGS_PATH.replace(
|
|
886
|
+
writeln2(`Writing OTel config to ${SETTINGS_PATH.replace(homedir2(), "~")}...`);
|
|
810
887
|
writeln2();
|
|
811
888
|
for (const [key, value] of Object.entries(OTEL_VARS)) {
|
|
812
889
|
const alreadySet = key in existingEnv && existingEnv[key] === value;
|
|
@@ -822,18 +899,133 @@ function runSetup() {
|
|
|
822
899
|
process.exit(1);
|
|
823
900
|
}
|
|
824
901
|
writeln2();
|
|
825
|
-
writeln2(`${GREEN2}\u2713${RESET2} Settings written to ${SETTINGS_PATH.replace(
|
|
826
|
-
|
|
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
|
-
}
|
|
902
|
+
writeln2(`${GREEN2}\u2713${RESET2} Settings written to ${SETTINGS_PATH.replace(homedir2(), "~")}`);
|
|
903
|
+
await promptApiKey(preEnteredKey);
|
|
832
904
|
writeln2();
|
|
833
905
|
writeln2("Ready. Start Radar in a second terminal pane:");
|
|
834
906
|
writeln2(` ${BOLD2}radar watch${RESET2}`);
|
|
835
907
|
writeln2(DIM2 + sep() + RESET2);
|
|
836
908
|
}
|
|
909
|
+
async function promptApiKey(preEnteredKey) {
|
|
910
|
+
writeln2();
|
|
911
|
+
let apiKey = preEnteredKey;
|
|
912
|
+
if (!apiKey) {
|
|
913
|
+
const config = readConfig();
|
|
914
|
+
const envKey = process.env.ANTHROPIC_API_KEY;
|
|
915
|
+
const hasStored = config.apiKey ?? config.apiKeyRef;
|
|
916
|
+
const activeKey = envKey ?? config.apiKey;
|
|
917
|
+
if (hasStored || envKey) {
|
|
918
|
+
if (envKey) {
|
|
919
|
+
const masked = envKey.slice(0, 10) + "\u2026" + envKey.slice(-4);
|
|
920
|
+
writeln2(`${GREEN2}\u2713${RESET2} API key found via ANTHROPIC_API_KEY env var: ${DIM2}${masked}${RESET2}`);
|
|
921
|
+
} else if (config.apiKeyRef) {
|
|
922
|
+
writeln2(`${GREEN2}\u2713${RESET2} API key linked via 1Password: ${DIM2}${config.apiKeyRef}${RESET2}`);
|
|
923
|
+
} else if (config.apiKey) {
|
|
924
|
+
const masked = config.apiKey.slice(0, 10) + "\u2026" + config.apiKey.slice(-4);
|
|
925
|
+
writeln2(`${GREEN2}\u2713${RESET2} API key stored locally: ${DIM2}${masked}${RESET2}`);
|
|
926
|
+
}
|
|
927
|
+
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
928
|
+
const answer = await question(rl2, " Replace it? [y/N] ");
|
|
929
|
+
rl2.close();
|
|
930
|
+
if (answer.trim().toLowerCase() !== "y") return;
|
|
931
|
+
void activeKey;
|
|
932
|
+
} else {
|
|
933
|
+
writeln2(`${YELLOW2}\u26A0${RESET2} No API key found. Radar needs one to run analysis.`);
|
|
934
|
+
}
|
|
935
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
936
|
+
const entered = await question(rl, " Enter your Anthropic API key: ");
|
|
937
|
+
rl.close();
|
|
938
|
+
apiKey = entered.trim();
|
|
939
|
+
if (!apiKey) {
|
|
940
|
+
writeln2(`${YELLOW2}\u26A0${RESET2} No key entered \u2014 skipping. Re-run setup or use --api-key <key>.`);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
await promptStorage(apiKey);
|
|
945
|
+
}
|
|
946
|
+
async function promptStorage(apiKey) {
|
|
947
|
+
const opAvailable = isOpAvailable();
|
|
948
|
+
writeln2();
|
|
949
|
+
writeln2("Where would you like to store the API key?");
|
|
950
|
+
writeln2(` ${BOLD2}1${RESET2} Local disk ${DIM2}(${configPath().replace(homedir2(), "~")})${RESET2}`);
|
|
951
|
+
if (opAvailable) {
|
|
952
|
+
writeln2(` ${BOLD2}2${RESET2} 1Password ${DIM2}(recommended)${RESET2}`);
|
|
953
|
+
} else {
|
|
954
|
+
writeln2(` ${DIM2}2 1Password (op CLI not found \u2014 see instructions below)${RESET2}`);
|
|
955
|
+
}
|
|
956
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
957
|
+
const answer = await question(rl, ` Choice [1${opAvailable ? "/2" : ""}]: `);
|
|
958
|
+
rl.close();
|
|
959
|
+
const choice = answer.trim();
|
|
960
|
+
if (choice === "2") {
|
|
961
|
+
if (!opAvailable) {
|
|
962
|
+
writeln2();
|
|
963
|
+
writeln2(`${YELLOW2}\u26A0${RESET2} 1Password CLI (op) is not installed.`);
|
|
964
|
+
writeln2(" To set it up:");
|
|
965
|
+
writeln2(` ${DIM2}1. Install: https://developer.1password.com/docs/cli/get-started${RESET2}`);
|
|
966
|
+
writeln2(` ${DIM2}2. Sign in: op signin${RESET2}`);
|
|
967
|
+
writeln2(` ${DIM2}3. Re-run: radar setup --api-key <key>${RESET2}`);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
await storeIn1Password(apiKey);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
storeLocally(apiKey);
|
|
974
|
+
}
|
|
975
|
+
function storeLocally(apiKey) {
|
|
976
|
+
const config = readConfig();
|
|
977
|
+
writeConfig({ ...config, apiKey, apiKeyRef: void 0 });
|
|
978
|
+
writeln2();
|
|
979
|
+
writeln2(`${GREEN2}\u2713${RESET2} API key saved to ${configPath().replace(homedir2(), "~")}`);
|
|
980
|
+
}
|
|
981
|
+
async function storeIn1Password(apiKey) {
|
|
982
|
+
writeln2();
|
|
983
|
+
writeln2("How would you like to store it in 1Password?");
|
|
984
|
+
writeln2(` ${BOLD2}1${RESET2} Create a new item ${DIM2}(radar will add it for you)${RESET2}`);
|
|
985
|
+
writeln2(` ${BOLD2}2${RESET2} Use an existing item ${DIM2}(enter an op:// reference)${RESET2}`);
|
|
986
|
+
const rl1 = createInterface({ input: process.stdin, output: process.stdout });
|
|
987
|
+
const choice = await question(rl1, " Choice [1/2]: ");
|
|
988
|
+
rl1.close();
|
|
989
|
+
if (choice.trim() === "2") {
|
|
990
|
+
await useExistingOpRef();
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
writeln2();
|
|
994
|
+
writeln2("Creating item in 1Password...");
|
|
995
|
+
try {
|
|
996
|
+
const ref = createOpItem(apiKey);
|
|
997
|
+
const config = readConfig();
|
|
998
|
+
writeConfig({ ...config, apiKey: void 0, apiKeyRef: ref });
|
|
999
|
+
writeln2(`${GREEN2}\u2713${RESET2} API key stored in 1Password`);
|
|
1000
|
+
writeln2(` Reference: ${DIM2}${ref}${RESET2}`);
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1003
|
+
writeln2(`${YELLOW2}\u2717${RESET2} Failed to store in 1Password: ${msg}`);
|
|
1004
|
+
writeln2(" Make sure you are signed in: " + DIM2 + "op signin" + RESET2);
|
|
1005
|
+
const rl2 = createInterface({ input: process.stdin, output: process.stdout });
|
|
1006
|
+
const fallback = await question(rl2, " Store on local disk instead? [Y/n] ");
|
|
1007
|
+
rl2.close();
|
|
1008
|
+
if (fallback.trim().toLowerCase() !== "n") {
|
|
1009
|
+
storeLocally(apiKey);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
async function useExistingOpRef() {
|
|
1014
|
+
writeln2();
|
|
1015
|
+
writeln2(` Enter the ${BOLD2}op://${RESET2} reference for your API key.`);
|
|
1016
|
+
writeln2(` ${DIM2}Example: op://Personal/Anthropic/credential${RESET2}`);
|
|
1017
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1018
|
+
const ref = await question(rl, " Reference: ");
|
|
1019
|
+
rl.close();
|
|
1020
|
+
const trimmed = ref.trim();
|
|
1021
|
+
if (!trimmed.startsWith("op://")) {
|
|
1022
|
+
writeln2(`${YELLOW2}\u26A0${RESET2} Invalid reference \u2014 must start with op://. Re-run setup to try again.`);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const config = readConfig();
|
|
1026
|
+
writeConfig({ ...config, apiKey: void 0, apiKeyRef: trimmed });
|
|
1027
|
+
writeln2(`${GREEN2}\u2713${RESET2} 1Password reference saved: ${DIM2}${trimmed}${RESET2}`);
|
|
1028
|
+
}
|
|
837
1029
|
|
|
838
1030
|
// src/cli/index.ts
|
|
839
1031
|
var program = new Command();
|
|
@@ -846,7 +1038,7 @@ program.command("watch").description("Start listening for Claude Code telemetry
|
|
|
846
1038
|
"-s, --threshold <score>",
|
|
847
1039
|
"Ambiguity score threshold for triggering a pre-advisory (0.0\u20131.0)",
|
|
848
1040
|
"0.6"
|
|
849
|
-
).option("-k, --api-key <key>", "Anthropic API key (
|
|
1041
|
+
).option("-k, --api-key <key>", "Anthropic API key (overrides all other sources)").action(async (opts) => {
|
|
850
1042
|
const port = parseInt(opts.port, 10);
|
|
851
1043
|
const boundaryTimeoutMs = parseInt(opts.timeout, 10);
|
|
852
1044
|
const scoreThreshold = parseFloat(opts.threshold);
|
|
@@ -862,15 +1054,21 @@ program.command("watch").description("Start listening for Claude Code telemetry
|
|
|
862
1054
|
printError("--threshold must be a number between 0.0 and 1.0");
|
|
863
1055
|
process.exit(1);
|
|
864
1056
|
}
|
|
865
|
-
|
|
1057
|
+
let apiKey;
|
|
1058
|
+
try {
|
|
1059
|
+
apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY ?? resolveStoredApiKey();
|
|
1060
|
+
} catch (err) {
|
|
1061
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
866
1064
|
if (!apiKey) {
|
|
867
|
-
printError("Anthropic API key not found.
|
|
1065
|
+
printError("Anthropic API key not found. Run `radar setup`, set ANTHROPIC_API_KEY, or use --api-key <key>.");
|
|
868
1066
|
process.exit(1);
|
|
869
1067
|
}
|
|
870
1068
|
await startWatch({ port, boundaryTimeoutMs, scoreThreshold, apiKey });
|
|
871
1069
|
});
|
|
872
|
-
program.command("setup").description("Write OTel config to ~/.claude/settings.json
|
|
873
|
-
runSetup();
|
|
1070
|
+
program.command("setup").description("Write OTel config to ~/.claude/settings.json and store your Anthropic API key").option("-k, --api-key <key>", "Anthropic API key to store (skips the interactive prompt)").action(async (opts) => {
|
|
1071
|
+
await runSetup(opts.apiKey);
|
|
874
1072
|
});
|
|
875
1073
|
if (process.argv.length === 2) {
|
|
876
1074
|
program.outputHelp();
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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","../../src/cli/config.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { startWatch } from './watch.js';\nimport { runSetup } from './setup.js';\nimport { resolveStoredApiKey } from './config.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 (overrides all other sources)')\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 // Resolution order: --api-key flag → ANTHROPIC_API_KEY env → stored config (local or 1Password)\n let apiKey: string | undefined;\n try {\n apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY ?? resolveStoredApiKey();\n } catch (err) {\n printError(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n if (!apiKey) {\n printError('Anthropic API key not found. Run `radar setup`, 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 and store your Anthropic API key')\n .option('-k, --api-key <key>', 'Anthropic API key to store (skips the interactive prompt)')\n .action(async (opts: { apiKey?: string }) => {\n await runSetup(opts.apiKey);\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 // ── OTel env var check ─────────────────────────────────────────────────────\n const requiredOtelVars = [\n 'CLAUDE_CODE_ENABLE_TELEMETRY',\n 'OTEL_LOGS_EXPORTER',\n 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT',\n ];\n const missingVars = requiredOtelVars.filter((v) => !process.env[v]);\n if (missingVars.length > 0) {\n printWarning('OTel env vars not configured. Run `radar setup` and restart Claude Code.');\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';\nimport { createInterface } from 'node:readline';\nimport {\n readConfig,\n writeConfig,\n configPath,\n isOpAvailable,\n createOpItem,\n} from './config.js';\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// ─── readline helper ──────────────────────────────────────────────────────────\n\nfunction question(rl: ReturnType<typeof createInterface>, prompt: string): Promise<string> {\n return new Promise((resolve) => rl.question(prompt, resolve));\n}\n\n// ─── Main ─────────────────────────────────────────────────────────────────────\n\nexport async function runSetup(preEnteredKey?: string): Promise<void> {\n writeln(CYAN + BOLD + sep('── Radar Setup ') + RESET);\n\n // ── Write OTel config ─────────────────────────────────────────────────────\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 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 ───────────────────────────────────────────────────────────────\n await promptApiKey(preEnteredKey);\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\n// ─── API key prompt ────────────────────────────────────────────────────────────\n\nasync function promptApiKey(preEnteredKey?: string): Promise<void> {\n writeln();\n\n let apiKey: string | undefined = preEnteredKey;\n\n // ── If no key was passed via --api-key, check for an existing one or prompt ─\n if (!apiKey) {\n const config = readConfig();\n const envKey = process.env.ANTHROPIC_API_KEY;\n const hasStored = config.apiKey ?? config.apiKeyRef;\n const activeKey = envKey ?? config.apiKey;\n\n if (hasStored || envKey) {\n // Show what's already configured\n if (envKey) {\n const masked = envKey.slice(0, 10) + '…' + envKey.slice(-4);\n writeln(`${GREEN}✓${RESET} API key found via ANTHROPIC_API_KEY env var: ${DIM}${masked}${RESET}`);\n } else if (config.apiKeyRef) {\n writeln(`${GREEN}✓${RESET} API key linked via 1Password: ${DIM}${config.apiKeyRef}${RESET}`);\n } else if (config.apiKey) {\n const masked = config.apiKey.slice(0, 10) + '…' + config.apiKey.slice(-4);\n writeln(`${GREEN}✓${RESET} API key stored locally: ${DIM}${masked}${RESET}`);\n }\n\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = await question(rl, ' Replace it? [y/N] ');\n rl.close();\n\n if (answer.trim().toLowerCase() !== 'y') return;\n\n // Fall through to prompt for new key\n void activeKey; // suppress unused warning\n } else {\n writeln(`${YELLOW}⚠${RESET} No API key found. Radar needs one to run analysis.`);\n }\n\n // Prompt for the key\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const entered = await question(rl, ' Enter your Anthropic API key: ');\n rl.close();\n\n apiKey = entered.trim();\n if (!apiKey) {\n writeln(`${YELLOW}⚠${RESET} No key entered — skipping. Re-run setup or use --api-key <key>.`);\n return;\n }\n }\n\n // ── Ask where to store it ─────────────────────────────────────────────────\n await promptStorage(apiKey);\n}\n\n// ─── Storage choice ────────────────────────────────────────────────────────────\n\nasync function promptStorage(apiKey: string): Promise<void> {\n const opAvailable = isOpAvailable();\n\n writeln();\n writeln('Where would you like to store the API key?');\n writeln(` ${BOLD}1${RESET} Local disk ${DIM}(${configPath().replace(homedir(), '~')})${RESET}`);\n\n if (opAvailable) {\n writeln(` ${BOLD}2${RESET} 1Password ${DIM}(recommended)${RESET}`);\n } else {\n writeln(` ${DIM}2 1Password (op CLI not found — see instructions below)${RESET}`);\n }\n\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = await question(rl, ` Choice [1${opAvailable ? '/2' : ''}]: `);\n rl.close();\n\n const choice = answer.trim();\n\n if (choice === '2') {\n if (!opAvailable) {\n writeln();\n writeln(`${YELLOW}⚠${RESET} 1Password CLI (op) is not installed.`);\n writeln(' To set it up:');\n writeln(` ${DIM}1. Install: https://developer.1password.com/docs/cli/get-started${RESET}`);\n writeln(` ${DIM}2. Sign in: op signin${RESET}`);\n writeln(` ${DIM}3. Re-run: radar setup --api-key <key>${RESET}`);\n return;\n }\n\n await storeIn1Password(apiKey);\n return;\n }\n\n // Default: local disk\n storeLocally(apiKey);\n}\n\nfunction storeLocally(apiKey: string): void {\n const config = readConfig();\n writeConfig({ ...config, apiKey, apiKeyRef: undefined });\n writeln();\n writeln(`${GREEN}✓${RESET} API key saved to ${configPath().replace(homedir(), '~')}`);\n}\n\nasync function storeIn1Password(apiKey: string): Promise<void> {\n writeln();\n writeln('How would you like to store it in 1Password?');\n writeln(` ${BOLD}1${RESET} Create a new item ${DIM}(radar will add it for you)${RESET}`);\n writeln(` ${BOLD}2${RESET} Use an existing item ${DIM}(enter an op:// reference)${RESET}`);\n\n const rl1 = createInterface({ input: process.stdin, output: process.stdout });\n const choice = await question(rl1, ' Choice [1/2]: ');\n rl1.close();\n\n if (choice.trim() === '2') {\n await useExistingOpRef();\n return;\n }\n\n // Create a new item\n writeln();\n writeln('Creating item in 1Password...');\n\n try {\n const ref = createOpItem(apiKey);\n const config = readConfig();\n writeConfig({ ...config, apiKey: undefined, apiKeyRef: ref });\n writeln(`${GREEN}✓${RESET} API key stored in 1Password`);\n writeln(` Reference: ${DIM}${ref}${RESET}`);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n writeln(`${YELLOW}✗${RESET} Failed to store in 1Password: ${msg}`);\n writeln(' Make sure you are signed in: ' + DIM + 'op signin' + RESET);\n\n // Offer local fallback\n const rl2 = createInterface({ input: process.stdin, output: process.stdout });\n const fallback = await question(rl2, ' Store on local disk instead? [Y/n] ');\n rl2.close();\n\n if (fallback.trim().toLowerCase() !== 'n') {\n storeLocally(apiKey);\n }\n }\n}\n\nasync function useExistingOpRef(): Promise<void> {\n writeln();\n writeln(` Enter the ${BOLD}op://${RESET} reference for your API key.`);\n writeln(` ${DIM}Example: op://Personal/Anthropic/credential${RESET}`);\n\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const ref = await question(rl, ' Reference: ');\n rl.close();\n\n const trimmed = ref.trim();\n if (!trimmed.startsWith('op://')) {\n writeln(`${YELLOW}⚠${RESET} Invalid reference — must start with op://. Re-run setup to try again.`);\n return;\n }\n\n const config = readConfig();\n writeConfig({ ...config, apiKey: undefined, apiKeyRef: trimmed });\n writeln(`${GREEN}✓${RESET} 1Password reference saved: ${DIM}${trimmed}${RESET}`);\n}\n","import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';\nimport { execSync } from 'node:child_process';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\n\n// ─── Paths ─────────────────────────────────────────────────────────────────────\n\nconst CONFIG_DIR = join(homedir(), '.config', 'radar');\nconst CONFIG_PATH = join(CONFIG_DIR, 'config.json');\n\n// ─── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface RadarConfig {\n apiKey?: string; // plaintext local storage\n apiKeyRef?: string; // 1Password reference e.g. op://vault/item/field\n}\n\n// ─── Read / write ──────────────────────────────────────────────────────────────\n\nexport function readConfig(): RadarConfig {\n if (!existsSync(CONFIG_PATH)) return {};\n try {\n return JSON.parse(readFileSync(CONFIG_PATH, 'utf8')) as RadarConfig;\n } catch {\n return {};\n }\n}\n\nexport function writeConfig(config: RadarConfig): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n }\n writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\\n', 'utf8');\n}\n\nexport function configPath(): string {\n return CONFIG_PATH;\n}\n\n// ─── 1Password helpers ─────────────────────────────────────────────────────────\n\nexport function isOpAvailable(): boolean {\n try {\n execSync('op --version', { stdio: 'ignore' });\n return true;\n } catch {\n return false;\n }\n}\n\n/** Run `op item create` and return the `op://` reference for the stored key. */\nexport function createOpItem(apiKey: string): string {\n const title = 'Radar Anthropic API Key';\n const raw = execSync(\n `op item create --category=login --title=\"${title}\" \"password=${apiKey}\" --format json`,\n { encoding: 'utf8' },\n );\n const item = JSON.parse(raw) as { title: string; vault: { name: string } };\n return `op://${item.vault.name}/${item.title}/password`;\n}\n\n/** Read a secret from 1Password by its `op://` reference. */\nexport function readOpItem(ref: string): string {\n return execSync(`op read \"${ref}\"`, { encoding: 'utf8' }).trim();\n}\n\n// ─── Key resolution ────────────────────────────────────────────────────────────\n\n/**\n * Resolve the API key from the stored config.\n * Tries plaintext `apiKey` first, then fetches via `op read` if `apiKeyRef` is set.\n * Returns undefined if no key is configured.\n * Throws if a 1Password reference is configured but the op read fails.\n */\nexport function resolveStoredApiKey(): string | undefined {\n const config = readConfig();\n if (config.apiKey) return config.apiKey;\n if (config.apiKeyRef) {\n try {\n return readOpItem(config.apiKeyRef);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(\n `Failed to read API key from 1Password (${config.apiKeyRef}).\\n` +\n `Make sure you are signed in: op signin\\n` +\n `Detail: ${detail}`,\n );\n }\n }\n return undefined;\n}\n"],"mappings":"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","names":["EventEmitter","Anthropic","Anthropic","readFileSync","writeFileSync","existsSync","mkdirSync","join","homedir","join","homedir","RESET","DIM","BOLD","GREEN","YELLOW","CYAN","LINE_WIDTH","writeln","existsSync","readFileSync","mkdirSync","writeFileSync","rl"]}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "radar-cc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Non-blocking intent alignment checker for Claude Code, powered by OpenTelemetry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"radar": "
|
|
7
|
+
"radar": "dist/cli/index.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/cli/index.js",
|
|
10
10
|
"files": [
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
"commander": "^12.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.0.0",
|
|
25
26
|
"tsup": "^8.0.0",
|
|
26
|
-
"typescript": "^5.4.0",
|
|
27
27
|
"tsx": "^4.0.0",
|
|
28
|
-
"
|
|
28
|
+
"typescript": "^5.4.0"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
|
-
"node": ">=
|
|
31
|
+
"node": ">=22.0.0"
|
|
32
32
|
},
|
|
33
33
|
"keywords": [
|
|
34
34
|
"claude-code",
|