ccdeck 3.38.6 → 3.39.1

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.
@@ -0,0 +1,87 @@
1
+ import { open } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { claudeConfigDir } from "./claude-dir.mjs";
5
+
6
+ const BOOLEAN_KEYS = [
7
+ "CLAUDE_CODE_ENABLE_TELEMETRY", "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
8
+ "ENABLE_ENHANCED_TELEMETRY_BETA", "OTEL_LOG_USER_PROMPTS",
9
+ "OTEL_LOG_ASSISTANT_RESPONSES", "OTEL_LOG_TOOL_DETAILS",
10
+ "OTEL_LOG_TOOL_CONTENT", "OTEL_LOG_RAW_API_BODIES", "OTEL_LOG_MANAGED_SETTINGS",
11
+ ];
12
+ const SIGNALS = ["METRICS", "LOGS", "TRACES"];
13
+ const ENDPOINT_KEYS = ["OTEL_EXPORTER_OTLP_ENDPOINT", ...SIGNALS.map(s => `OTEL_EXPORTER_OTLP_${s}_ENDPOINT`)];
14
+ const EXPORTER_KEYS = SIGNALS.map(s => `OTEL_${s}_EXPORTER`);
15
+ const INTERVAL_KEYS = ["OTEL_METRIC_EXPORT_INTERVAL", "OTEL_LOGS_EXPORT_INTERVAL", "OTEL_TRACES_EXPORT_INTERVAL"];
16
+ const HEADER_KEYS = ["OTEL_EXPORTER_OTLP_HEADERS", ...SIGNALS.map(s => `OTEL_EXPORTER_OTLP_${s}_HEADERS`)];
17
+ const PROTOCOL_KEYS = ["OTEL_EXPORTER_OTLP_PROTOCOL", ...SIGNALS.map(s => `OTEL_EXPORTER_OTLP_${s}_PROTOCOL`)];
18
+ export const RADAR_KEYS = [...BOOLEAN_KEYS, ...ENDPOINT_KEYS, ...EXPORTER_KEYS, ...INTERVAL_KEYS, ...HEADER_KEYS, ...PROTOCOL_KEYS];
19
+
20
+ export function safeRadarValue(key, raw) {
21
+ if (HEADER_KEYS.includes(key)) return "Configured (hidden)";
22
+ if (typeof raw !== "string" && typeof raw !== "number" && typeof raw !== "boolean") return "Invalid (value hidden)";
23
+ const value = String(raw).trim();
24
+ if (PROTOCOL_KEYS.includes(key)) return ['grpc', 'http/protobuf', 'http/json'].includes(value) ? value : "Unknown (value hidden)";
25
+ if (BOOLEAN_KEYS.includes(key)) {
26
+ if (key === "OTEL_LOG_RAW_API_BODIES" && value.startsWith("file:")) return "Enabled (file destination hidden)";
27
+ if (["1", "true"].includes(value)) return "Enabled";
28
+ if (["0", "false"].includes(value)) return "Disabled";
29
+ return "Unknown (value hidden)";
30
+ }
31
+ if (ENDPOINT_KEYS.includes(key)) {
32
+ try {
33
+ const url = new URL(value);
34
+ if (!["http:", "https:"].includes(url.protocol) || !url.hostname) return "Invalid (value hidden)";
35
+ return `${url.protocol}//${url.hostname}${url.port ? `:${url.port}` : ""}`;
36
+ } catch { return "Invalid (value hidden)"; }
37
+ }
38
+ if (EXPORTER_KEYS.includes(key)) {
39
+ const exporters = value.split(",").map(s => s.trim());
40
+ return exporters.length && exporters.every(s => ["otlp", "console", "none", "prometheus"].includes(s))
41
+ ? exporters.join(", ") : "Unknown (value hidden)";
42
+ }
43
+ if (INTERVAL_KEYS.includes(key)) {
44
+ return /^\d+$/.test(value) && Number(value) > 0 && Number(value) <= 86_400_000
45
+ ? `${Number(value)} ms` : "Invalid (value hidden)";
46
+ }
47
+ return "Unknown (value hidden)";
48
+ }
49
+
50
+ export function radarVariables(settings, source) {
51
+ const env = settings?.env;
52
+ if (!env || typeof env !== "object" || Array.isArray(env)) return [];
53
+ return RADAR_KEYS.filter(key => Object.hasOwn(env, key)).map(key => ({
54
+ key, value: safeRadarValue(key, env[key]), source,
55
+ ...(BOOLEAN_KEYS.includes(key) && ["1", "0", "true", "false"].includes(String(env[key]).trim()) ? { rawValue: String(env[key]).trim() } : {}),
56
+ }));
57
+ }
58
+
59
+ async function readBoundedJson(path) {
60
+ const file = await open(path, "r");
61
+ try {
62
+ const stat = await file.stat();
63
+ if (!stat.isFile() || stat.size > 262_144) throw new Error("invalid_file");
64
+ const buffer = Buffer.alloc(262_145);
65
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
66
+ if (bytesRead > 262_144) throw new Error("oversize");
67
+ return JSON.parse(buffer.subarray(0, bytesRead).toString("utf8"));
68
+ } finally { await file.close(); }
69
+ }
70
+
71
+ export async function readRadarConfig({ home = homedir(), env = process.env, readJson = readBoundedJson } = {}) {
72
+ const dir = claudeConfigDir(env, home);
73
+ const files = [
74
+ ["User settings", join(dir, "settings.json")],
75
+ ["Cached organization settings", join(dir, "remote-settings.json")],
76
+ ["Managed settings", "/Library/Application Support/ClaudeCode/managed-settings.json"],
77
+ ];
78
+ const sources = await Promise.all(files.map(async ([name, path]) => {
79
+ try {
80
+ const settings = await readJson(path);
81
+ return { name, status: "read", variables: radarVariables(settings, name) };
82
+ } catch (error) {
83
+ return { name, status: error?.code === "ENOENT" ? "absent" : "unavailable", variables: [] };
84
+ }
85
+ }));
86
+ return { sources: sources.map(({ name, status }) => ({ name, status })), variables: sources.flatMap(s => s.variables) };
87
+ }
@@ -0,0 +1,43 @@
1
+ import { pathToFileURL } from "node:url";
2
+
3
+ export async function streamCapture({ base, token, source = 0, input = process.stdin, send = fetch, report = message => process.stderr.write(message + '\n') }) {
4
+ const url = new URL(base);
5
+ if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || url.username || url.password || url.pathname !== '/' || url.search || url.hash || !/^[a-f0-9]{64}$/.test(token)) throw new Error('Invalid local capture destination.');
6
+ if (!Number.isInteger(source) || source < 0 || source > 7) throw new Error('Invalid capture source.');
7
+ let stopped = false, sequence = Promise.resolve();
8
+ const finish = () => { stopped = true; input.destroy(); };
9
+ const post = bytes => {
10
+ sequence = sequence.then(async () => {
11
+ if (stopped) return;
12
+ try {
13
+ const response = await send(new URL('/api/system/traffic-radar/ingest', url), {
14
+ method: 'POST', headers: { 'x-radar-capture': token, 'x-radar-source': String(source), 'content-type': 'application/octet-stream' },
15
+ body: bytes, signal: AbortSignal.timeout(3000), redirect: 'error',
16
+ });
17
+ if (!response.ok) finish();
18
+ } catch { finish(); }
19
+ });
20
+ return sequence;
21
+ };
22
+ const heartbeat = setInterval(() => { if (!stopped) void post(Buffer.alloc(0)); }, 1000);
23
+ const deadline = setTimeout(finish, 600_000);
24
+ report('Telemetry Radar: sending capture bytes only to local ccdeck; no file is written. Press Ctrl+C to stop tcpdump.');
25
+ try {
26
+ for await (const chunk of input) {
27
+ for (let offset = 0; offset < chunk.length && !stopped; offset += 65_536) await post(chunk.subarray(offset, offset + 65_536));
28
+ if (stopped) break;
29
+ }
30
+ await sequence;
31
+ } catch {
32
+ if (!stopped) report('Capture input interrupted.');
33
+ } finally {
34
+ stopped = true; clearInterval(heartbeat); clearTimeout(deadline);
35
+ report('Telemetry Radar: local receiver stopped. Press Ctrl+C if tcpdump is still running.');
36
+ }
37
+ }
38
+
39
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
40
+ streamCapture({ base: process.argv[2], token: process.argv[3], source: Number(process.argv[4] ?? 0) }).catch(() => {
41
+ process.stderr.write('Could not start the local capture helper.\n'); process.exitCode = 1;
42
+ });
43
+ }
@@ -0,0 +1 @@
1
+ {"nested":{"opentelemetry":{"nested":{"proto":{"nested":{"collector":{"nested":{"logs":{"nested":{"v1":{"options":{"csharp_namespace":"OpenTelemetry.Proto.Collector.Logs.V1","java_multiple_files":true,"java_package":"io.opentelemetry.proto.collector.logs.v1","java_outer_classname":"LogsServiceProto","go_package":"go.opentelemetry.io/proto/otlp/collector/logs/v1"},"nested":{"LogsService":{"methods":{"Export":{"requestType":"ExportLogsServiceRequest","responseType":"ExportLogsServiceResponse"}}},"ExportLogsServiceRequest":{"fields":{"resourceLogs":{"rule":"repeated","type":"opentelemetry.proto.logs.v1.ResourceLogs","id":1,"protoName":"resource_logs"}}},"ExportLogsServiceResponse":{"fields":{"partialSuccess":{"type":"ExportLogsPartialSuccess","id":1,"protoName":"partial_success"}}},"ExportLogsPartialSuccess":{"fields":{"rejectedLogRecords":{"type":"int64","id":1,"protoName":"rejected_log_records"},"errorMessage":{"type":"string","id":2,"protoName":"error_message"}}}}}}},"trace":{"nested":{"v1":{"options":{"csharp_namespace":"OpenTelemetry.Proto.Collector.Trace.V1","java_multiple_files":true,"java_package":"io.opentelemetry.proto.collector.trace.v1","java_outer_classname":"TraceServiceProto","go_package":"go.opentelemetry.io/proto/otlp/collector/trace/v1"},"nested":{"TraceService":{"methods":{"Export":{"requestType":"ExportTraceServiceRequest","responseType":"ExportTraceServiceResponse"}}},"ExportTraceServiceRequest":{"fields":{"resourceSpans":{"rule":"repeated","type":"opentelemetry.proto.trace.v1.ResourceSpans","id":1,"protoName":"resource_spans"}}},"ExportTraceServiceResponse":{"fields":{"partialSuccess":{"type":"ExportTracePartialSuccess","id":1,"protoName":"partial_success"}}},"ExportTracePartialSuccess":{"fields":{"rejectedSpans":{"type":"int64","id":1,"protoName":"rejected_spans"},"errorMessage":{"type":"string","id":2,"protoName":"error_message"}}}}}}},"metrics":{"nested":{"v1":{"options":{"csharp_namespace":"OpenTelemetry.Proto.Collector.Metrics.V1","java_multiple_files":true,"java_package":"io.opentelemetry.proto.collector.metrics.v1","java_outer_classname":"MetricsServiceProto","go_package":"go.opentelemetry.io/proto/otlp/collector/metrics/v1"},"nested":{"MetricsService":{"methods":{"Export":{"requestType":"ExportMetricsServiceRequest","responseType":"ExportMetricsServiceResponse"}}},"ExportMetricsServiceRequest":{"fields":{"resourceMetrics":{"rule":"repeated","type":"opentelemetry.proto.metrics.v1.ResourceMetrics","id":1,"protoName":"resource_metrics"}}},"ExportMetricsServiceResponse":{"fields":{"partialSuccess":{"type":"ExportMetricsPartialSuccess","id":1,"protoName":"partial_success"}}},"ExportMetricsPartialSuccess":{"fields":{"rejectedDataPoints":{"type":"int64","id":1,"protoName":"rejected_data_points"},"errorMessage":{"type":"string","id":2,"protoName":"error_message"}}}}}}}}},"trace":{"nested":{"v1":{"options":{"csharp_namespace":"OpenTelemetry.Proto.Trace.V1","java_multiple_files":true,"java_package":"io.opentelemetry.proto.trace.v1","java_outer_classname":"TraceProto","go_package":"go.opentelemetry.io/proto/otlp/trace/v1"},"nested":{"TracesData":{"fields":{"resourceSpans":{"rule":"repeated","type":"ResourceSpans","id":1,"protoName":"resource_spans"}}},"ResourceSpans":{"fields":{"resource":{"type":"opentelemetry.proto.resource.v1.Resource","id":1},"scopeSpans":{"rule":"repeated","type":"ScopeSpans","id":2,"protoName":"scope_spans"},"schemaUrl":{"type":"string","id":3,"protoName":"schema_url"}},"reserved":[[1000,1000]]},"ScopeSpans":{"fields":{"scope":{"type":"opentelemetry.proto.common.v1.InstrumentationScope","id":1},"spans":{"rule":"repeated","type":"Span","id":2},"schemaUrl":{"type":"string","id":3,"protoName":"schema_url"}}},"Span":{"fields":{"traceId":{"type":"bytes","id":1,"protoName":"trace_id"},"spanId":{"type":"bytes","id":2,"protoName":"span_id"},"traceState":{"type":"string","id":3,"protoName":"trace_state"},"parentSpanId":{"type":"bytes","id":4,"protoName":"parent_span_id"},"flags":{"type":"fixed32","id":16},"name":{"type":"string","id":5},"kind":{"type":"SpanKind","id":6},"startTimeUnixNano":{"type":"fixed64","id":7,"protoName":"start_time_unix_nano"},"endTimeUnixNano":{"type":"fixed64","id":8,"protoName":"end_time_unix_nano"},"attributes":{"rule":"repeated","type":"opentelemetry.proto.common.v1.KeyValue","id":9},"droppedAttributesCount":{"type":"uint32","id":10,"protoName":"dropped_attributes_count"},"events":{"rule":"repeated","type":"Event","id":11},"droppedEventsCount":{"type":"uint32","id":12,"protoName":"dropped_events_count"},"links":{"rule":"repeated","type":"Link","id":13},"droppedLinksCount":{"type":"uint32","id":14,"protoName":"dropped_links_count"},"status":{"type":"Status","id":15}},"nested":{"SpanKind":{"values":{"SPAN_KIND_UNSPECIFIED":0,"SPAN_KIND_INTERNAL":1,"SPAN_KIND_SERVER":2,"SPAN_KIND_CLIENT":3,"SPAN_KIND_PRODUCER":4,"SPAN_KIND_CONSUMER":5}},"Event":{"fields":{"timeUnixNano":{"type":"fixed64","id":1,"protoName":"time_unix_nano"},"name":{"type":"string","id":2},"attributes":{"rule":"repeated","type":"opentelemetry.proto.common.v1.KeyValue","id":3},"droppedAttributesCount":{"type":"uint32","id":4,"protoName":"dropped_attributes_count"}}},"Link":{"fields":{"traceId":{"type":"bytes","id":1,"protoName":"trace_id"},"spanId":{"type":"bytes","id":2,"protoName":"span_id"},"traceState":{"type":"string","id":3,"protoName":"trace_state"},"attributes":{"rule":"repeated","type":"opentelemetry.proto.common.v1.KeyValue","id":4},"droppedAttributesCount":{"type":"uint32","id":5,"protoName":"dropped_attributes_count"},"flags":{"type":"fixed32","id":6}}}}},"Status":{"fields":{"message":{"type":"string","id":2},"code":{"type":"StatusCode","id":3}},"reserved":[[1,1]],"nested":{"StatusCode":{"values":{"STATUS_CODE_UNSET":0,"STATUS_CODE_OK":1,"STATUS_CODE_ERROR":2}}}},"SpanFlags":{"values":{"SPAN_FLAGS_DO_NOT_USE":0,"SPAN_FLAGS_TRACE_FLAGS_MASK":255,"SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK":256,"SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK":512}}}}}},"logs":{"nested":{"v1":{"options":{"csharp_namespace":"OpenTelemetry.Proto.Logs.V1","java_multiple_files":true,"java_package":"io.opentelemetry.proto.logs.v1","java_outer_classname":"LogsProto","go_package":"go.opentelemetry.io/proto/otlp/logs/v1"},"nested":{"LogsData":{"fields":{"resourceLogs":{"rule":"repeated","type":"ResourceLogs","id":1,"protoName":"resource_logs"}}},"ResourceLogs":{"fields":{"resource":{"type":"opentelemetry.proto.resource.v1.Resource","id":1},"scopeLogs":{"rule":"repeated","type":"ScopeLogs","id":2,"protoName":"scope_logs"},"schemaUrl":{"type":"string","id":3,"protoName":"schema_url"}},"reserved":[[1000,1000]]},"ScopeLogs":{"fields":{"scope":{"type":"opentelemetry.proto.common.v1.InstrumentationScope","id":1},"logRecords":{"rule":"repeated","type":"LogRecord","id":2,"protoName":"log_records"},"schemaUrl":{"type":"string","id":3,"protoName":"schema_url"}}},"SeverityNumber":{"values":{"SEVERITY_NUMBER_UNSPECIFIED":0,"SEVERITY_NUMBER_TRACE":1,"SEVERITY_NUMBER_TRACE2":2,"SEVERITY_NUMBER_TRACE3":3,"SEVERITY_NUMBER_TRACE4":4,"SEVERITY_NUMBER_DEBUG":5,"SEVERITY_NUMBER_DEBUG2":6,"SEVERITY_NUMBER_DEBUG3":7,"SEVERITY_NUMBER_DEBUG4":8,"SEVERITY_NUMBER_INFO":9,"SEVERITY_NUMBER_INFO2":10,"SEVERITY_NUMBER_INFO3":11,"SEVERITY_NUMBER_INFO4":12,"SEVERITY_NUMBER_WARN":13,"SEVERITY_NUMBER_WARN2":14,"SEVERITY_NUMBER_WARN3":15,"SEVERITY_NUMBER_WARN4":16,"SEVERITY_NUMBER_ERROR":17,"SEVERITY_NUMBER_ERROR2":18,"SEVERITY_NUMBER_ERROR3":19,"SEVERITY_NUMBER_ERROR4":20,"SEVERITY_NUMBER_FATAL":21,"SEVERITY_NUMBER_FATAL2":22,"SEVERITY_NUMBER_FATAL3":23,"SEVERITY_NUMBER_FATAL4":24}},"LogRecordFlags":{"values":{"LOG_RECORD_FLAGS_DO_NOT_USE":0,"LOG_RECORD_FLAGS_TRACE_FLAGS_MASK":255}},"LogRecord":{"fields":{"timeUnixNano":{"type":"fixed64","id":1,"protoName":"time_unix_nano"},"observedTimeUnixNano":{"type":"fixed64","id":11,"protoName":"observed_time_unix_nano"},"severityNumber":{"type":"SeverityNumber","id":2,"protoName":"severity_number"},"severityText":{"type":"string","id":3,"protoName":"severity_text"},"body":{"type":"opentelemetry.proto.common.v1.AnyValue","id":5},"attributes":{"rule":"repeated","type":"opentelemetry.proto.common.v1.KeyValue","id":6},"droppedAttributesCount":{"type":"uint32","id":7,"protoName":"dropped_attributes_count"},"flags":{"type":"fixed32","id":8},"traceId":{"type":"bytes","id":9,"protoName":"trace_id"},"spanId":{"type":"bytes","id":10,"protoName":"span_id"},"eventName":{"type":"string","id":12,"protoName":"event_name"}},"reserved":[[4,4]]}}}}},"metrics":{"nested":{"v1":{"options":{"csharp_namespace":"OpenTelemetry.Proto.Metrics.V1","java_multiple_files":true,"java_package":"io.opentelemetry.proto.metrics.v1","java_outer_classname":"MetricsProto","go_package":"go.opentelemetry.io/proto/otlp/metrics/v1"},"nested":{"MetricsData":{"fields":{"resourceMetrics":{"rule":"repeated","type":"ResourceMetrics","id":1,"protoName":"resource_metrics"}}},"ResourceMetrics":{"fields":{"resource":{"type":"opentelemetry.proto.resource.v1.Resource","id":1},"scopeMetrics":{"rule":"repeated","type":"ScopeMetrics","id":2,"protoName":"scope_metrics"},"schemaUrl":{"type":"string","id":3,"protoName":"schema_url"}},"reserved":[[1000,1000]]},"ScopeMetrics":{"fields":{"scope":{"type":"opentelemetry.proto.common.v1.InstrumentationScope","id":1},"metrics":{"rule":"repeated","type":"Metric","id":2},"schemaUrl":{"type":"string","id":3,"protoName":"schema_url"}}},"Metric":{"oneofs":{"data":{"oneof":["gauge","sum","histogram","exponentialHistogram","summary"]}},"fields":{"name":{"type":"string","id":1},"description":{"type":"string","id":2},"unit":{"type":"string","id":3},"gauge":{"type":"Gauge","id":5},"sum":{"type":"Sum","id":7},"histogram":{"type":"Histogram","id":9},"exponentialHistogram":{"type":"ExponentialHistogram","id":10,"protoName":"exponential_histogram"},"summary":{"type":"Summary","id":11},"metadata":{"rule":"repeated","type":"opentelemetry.proto.common.v1.KeyValue","id":12}},"reserved":[[4,4],[6,6],[8,8]]},"Gauge":{"fields":{"dataPoints":{"rule":"repeated","type":"NumberDataPoint","id":1,"protoName":"data_points"}}},"Sum":{"fields":{"dataPoints":{"rule":"repeated","type":"NumberDataPoint","id":1,"protoName":"data_points"},"aggregationTemporality":{"type":"AggregationTemporality","id":2,"protoName":"aggregation_temporality"},"isMonotonic":{"type":"bool","id":3,"protoName":"is_monotonic"}}},"Histogram":{"fields":{"dataPoints":{"rule":"repeated","type":"HistogramDataPoint","id":1,"protoName":"data_points"},"aggregationTemporality":{"type":"AggregationTemporality","id":2,"protoName":"aggregation_temporality"}}},"ExponentialHistogram":{"fields":{"dataPoints":{"rule":"repeated","type":"ExponentialHistogramDataPoint","id":1,"protoName":"data_points"},"aggregationTemporality":{"type":"AggregationTemporality","id":2,"protoName":"aggregation_temporality"}}},"Summary":{"fields":{"dataPoints":{"rule":"repeated","type":"SummaryDataPoint","id":1,"protoName":"data_points"}}},"AggregationTemporality":{"values":{"AGGREGATION_TEMPORALITY_UNSPECIFIED":0,"AGGREGATION_TEMPORALITY_DELTA":1,"AGGREGATION_TEMPORALITY_CUMULATIVE":2}},"DataPointFlags":{"values":{"DATA_POINT_FLAGS_DO_NOT_USE":0,"DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK":1}},"NumberDataPoint":{"oneofs":{"value":{"oneof":["asDouble","asInt"]}},"fields":{"attributes":{"rule":"repeated","type":"opentelemetry.proto.common.v1.KeyValue","id":7},"startTimeUnixNano":{"type":"fixed64","id":2,"protoName":"start_time_unix_nano"},"timeUnixNano":{"type":"fixed64","id":3,"protoName":"time_unix_nano"},"asDouble":{"type":"double","id":4,"protoName":"as_double"},"asInt":{"type":"sfixed64","id":6,"protoName":"as_int"},"exemplars":{"rule":"repeated","type":"Exemplar","id":5},"flags":{"type":"uint32","id":8}},"reserved":[[1,1]]},"HistogramDataPoint":{"oneofs":{"_sum":{"oneof":["sum"]},"_min":{"oneof":["min"]},"_max":{"oneof":["max"]}},"fields":{"attributes":{"rule":"repeated","type":"opentelemetry.proto.common.v1.KeyValue","id":9},"startTimeUnixNano":{"type":"fixed64","id":2,"protoName":"start_time_unix_nano"},"timeUnixNano":{"type":"fixed64","id":3,"protoName":"time_unix_nano"},"count":{"type":"fixed64","id":4},"sum":{"type":"double","id":5,"options":{"proto3_optional":true}},"bucketCounts":{"rule":"repeated","type":"fixed64","id":6,"protoName":"bucket_counts"},"explicitBounds":{"rule":"repeated","type":"double","id":7,"protoName":"explicit_bounds"},"exemplars":{"rule":"repeated","type":"Exemplar","id":8},"flags":{"type":"uint32","id":10},"min":{"type":"double","id":11,"options":{"proto3_optional":true}},"max":{"type":"double","id":12,"options":{"proto3_optional":true}}},"reserved":[[1,1]]},"ExponentialHistogramDataPoint":{"oneofs":{"_sum":{"oneof":["sum"]},"_min":{"oneof":["min"]},"_max":{"oneof":["max"]}},"fields":{"attributes":{"rule":"repeated","type":"opentelemetry.proto.common.v1.KeyValue","id":1},"startTimeUnixNano":{"type":"fixed64","id":2,"protoName":"start_time_unix_nano"},"timeUnixNano":{"type":"fixed64","id":3,"protoName":"time_unix_nano"},"count":{"type":"fixed64","id":4},"sum":{"type":"double","id":5,"options":{"proto3_optional":true}},"scale":{"type":"sint32","id":6},"zeroCount":{"type":"fixed64","id":7,"protoName":"zero_count"},"positive":{"type":"Buckets","id":8},"negative":{"type":"Buckets","id":9},"flags":{"type":"uint32","id":10},"exemplars":{"rule":"repeated","type":"Exemplar","id":11},"min":{"type":"double","id":12,"options":{"proto3_optional":true}},"max":{"type":"double","id":13,"options":{"proto3_optional":true}},"zeroThreshold":{"type":"double","id":14,"protoName":"zero_threshold"}},"nested":{"Buckets":{"fields":{"offset":{"type":"sint32","id":1},"bucketCounts":{"rule":"repeated","type":"uint64","id":2,"protoName":"bucket_counts"}}}}},"SummaryDataPoint":{"fields":{"attributes":{"rule":"repeated","type":"opentelemetry.proto.common.v1.KeyValue","id":7},"startTimeUnixNano":{"type":"fixed64","id":2,"protoName":"start_time_unix_nano"},"timeUnixNano":{"type":"fixed64","id":3,"protoName":"time_unix_nano"},"count":{"type":"fixed64","id":4},"sum":{"type":"double","id":5},"quantileValues":{"rule":"repeated","type":"ValueAtQuantile","id":6,"protoName":"quantile_values"},"flags":{"type":"uint32","id":8}},"reserved":[[1,1]],"nested":{"ValueAtQuantile":{"fields":{"quantile":{"type":"double","id":1},"value":{"type":"double","id":2}}}}},"Exemplar":{"oneofs":{"value":{"oneof":["asDouble","asInt"]}},"fields":{"filteredAttributes":{"rule":"repeated","type":"opentelemetry.proto.common.v1.KeyValue","id":7,"protoName":"filtered_attributes"},"timeUnixNano":{"type":"fixed64","id":2,"protoName":"time_unix_nano"},"asDouble":{"type":"double","id":3,"protoName":"as_double"},"asInt":{"type":"sfixed64","id":6,"protoName":"as_int"},"spanId":{"type":"bytes","id":4,"protoName":"span_id"},"traceId":{"type":"bytes","id":5,"protoName":"trace_id"}},"reserved":[[1,1]]}}}}},"common":{"nested":{"v1":{"options":{"csharp_namespace":"OpenTelemetry.Proto.Common.V1","java_multiple_files":true,"java_package":"io.opentelemetry.proto.common.v1","java_outer_classname":"CommonProto","go_package":"go.opentelemetry.io/proto/otlp/common/v1"},"nested":{"AnyValue":{"oneofs":{"value":{"oneof":["stringValue","boolValue","intValue","doubleValue","arrayValue","kvlistValue","bytesValue"]}},"fields":{"stringValue":{"type":"string","id":1,"protoName":"string_value"},"boolValue":{"type":"bool","id":2,"protoName":"bool_value"},"intValue":{"type":"int64","id":3,"protoName":"int_value"},"doubleValue":{"type":"double","id":4,"protoName":"double_value"},"arrayValue":{"type":"ArrayValue","id":5,"protoName":"array_value"},"kvlistValue":{"type":"KeyValueList","id":6,"protoName":"kvlist_value"},"bytesValue":{"type":"bytes","id":7,"protoName":"bytes_value"}}},"ArrayValue":{"fields":{"values":{"rule":"repeated","type":"AnyValue","id":1}}},"KeyValueList":{"fields":{"values":{"rule":"repeated","type":"KeyValue","id":1}}},"KeyValue":{"fields":{"key":{"type":"string","id":1},"value":{"type":"AnyValue","id":2}}},"InstrumentationScope":{"fields":{"name":{"type":"string","id":1},"version":{"type":"string","id":2},"attributes":{"rule":"repeated","type":"KeyValue","id":3},"droppedAttributesCount":{"type":"uint32","id":4,"protoName":"dropped_attributes_count"}}},"EntityRef":{"fields":{"schemaUrl":{"type":"string","id":1,"protoName":"schema_url"},"type":{"type":"string","id":2},"idKeys":{"rule":"repeated","type":"string","id":3,"protoName":"id_keys"},"descriptionKeys":{"rule":"repeated","type":"string","id":4,"protoName":"description_keys"}}}}}}},"resource":{"nested":{"v1":{"options":{"csharp_namespace":"OpenTelemetry.Proto.Resource.V1","java_multiple_files":true,"java_package":"io.opentelemetry.proto.resource.v1","java_outer_classname":"ResourceProto","go_package":"go.opentelemetry.io/proto/otlp/resource/v1"},"nested":{"Resource":{"fields":{"attributes":{"rule":"repeated","type":"opentelemetry.proto.common.v1.KeyValue","id":1},"droppedAttributesCount":{"type":"uint32","id":2,"protoName":"dropped_attributes_count"},"entityRefs":{"rule":"repeated","type":"opentelemetry.proto.common.v1.EntityRef","id":3,"protoName":"entity_refs"}}}}}}}}}}}}}
@@ -0,0 +1,40 @@
1
+ import { trafficCapture } from "./traffic-radar-capture.mjs";
2
+ const BASE = '/api/system/traffic-radar';
3
+ const loopback = req => ['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(req.socket.remoteAddress);
4
+ export function isAuthorizedTrafficIngest(req, url) {
5
+ return req.method === 'POST' && url.pathname === BASE + '/ingest' && loopback(req)
6
+ && trafficCapture.accepts(req.headers['x-radar-capture']);
7
+ }
8
+ function reply(res, status, body) {
9
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
10
+ res.end(JSON.stringify(body));
11
+ }
12
+ async function body(req, max) {
13
+ const chunks = []; let size = 0;
14
+ for await (const chunk of req) { size += chunk.length; if (size > max) throw new Error('Request too large.'); chunks.push(chunk); }
15
+ return Buffer.concat(chunks);
16
+ }
17
+ export async function handleTrafficCapture(req, res, url, capture = trafficCapture) {
18
+ if (!loopback(req)) return reply(res, 403, { error: 'Telemetry contents are available on this machine only.' });
19
+ const action = url.pathname.slice(BASE.length);
20
+ try {
21
+ if (req.method === 'GET' && action === '/capture') return reply(res, 200, capture.read());
22
+ if (req.method === 'GET' && action === '/capture/event') {
23
+ const detail = capture.detail(Number(url.searchParams.get('id')));
24
+ return reply(res, detail ? 200 : 404, detail ?? { error: 'This event is no longer retained.' });
25
+ }
26
+ if (req.method === 'POST' && action === '/ingest') {
27
+ const token = req.headers['x-radar-capture'];
28
+ if (!capture.accepts(token)) return reply(res, 401, { error: 'Capture session ended or unauthorized.' });
29
+ const data = await body(req, 262_144);
30
+ return reply(res, capture.ingest(token, data, Number(req.headers['x-radar-source'] ?? 0)) ? 200 : 409, { ok: capture.read().state === 'capturing' });
31
+ }
32
+ if (req.method === 'POST' && action === '/capture/prepare') {
33
+ const data = JSON.parse((await body(req, 4096)).toString());
34
+ return reply(res, 200, await capture.prepare(data.destination, req.socket.localPort));
35
+ }
36
+ if (req.method === 'POST' && action === '/capture/stop') { capture.stop(); return reply(res, 200, { ok: true }); }
37
+ if (req.method === 'POST' && action === '/capture/clear') { capture.clear(); return reply(res, 200, { ok: true }); }
38
+ return reply(res, 404, { error: 'Unknown capture action.' });
39
+ } catch { return reply(res, 400, { error: 'Could not complete capture setup. Use a valid IPv4 destination and check its network route.' }); }
40
+ }
@@ -0,0 +1,228 @@
1
+ import { gunzipSync } from "node:zlib";
2
+ import { decodeOtlp, headerDecoder } from "./traffic-radar-codec.mjs";
3
+
4
+ const LIMIT = 262_144;
5
+ const PREFACE = Buffer.from("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
6
+ const PATHS = Object.fromEntries([['logs', 'Logs'], ['metrics', 'Metrics'], ['traces', 'Trace']].map(([signal, name]) => [
7
+ `/opentelemetry.proto.collector.${signal === 'traces' ? 'trace' : signal}.v1.${name}Service/Export`, signal,
8
+ ]));
9
+
10
+ export function createWireCapture({ host, port, onExport, onResponse, onIssue, onObservation }) {
11
+ let pending = Buffer.alloc(0);
12
+ let read32, link, nanos;
13
+ const flows = new Map();
14
+ const issue = code => onIssue(code);
15
+ function direction() {
16
+ return { next: null, queue: new Map(), buffer: Buffer.alloc(0), headers: headerDecoder(), block: null };
17
+ }
18
+ function discard(key, code) { flows.delete(key); issue(code); }
19
+ function payload(stream, side, bytes, at, flow, id) {
20
+ const key = side === 'out' ? 'request' : 'response';
21
+ stream[key] = Buffer.concat([stream[key] ?? Buffer.alloc(0), bytes]);
22
+ if (stream[key].length > LIMIT) throw new Error('payload_limit');
23
+ while (stream[key].length >= 5) {
24
+ const size = stream[key].readUInt32BE(1);
25
+ if (size > LIMIT) throw new Error('payload_limit');
26
+ if (stream[key].length < size + 5) return;
27
+ const compressed = stream[key][0];
28
+ let body = stream[key].subarray(5, 5 + size);
29
+ stream[key] = stream[key].subarray(5 + size);
30
+ if (compressed > 1) throw new Error('unsupported_protocol');
31
+ if (compressed) {
32
+ if (stream[side + 'Encoding'] !== 'gzip') throw new Error('unsupported_compression');
33
+ body = gunzipSync(body, { maxOutputLength: LIMIT });
34
+ }
35
+ if (!stream.signal) throw new Error('unknown_export');
36
+ const data = decodeOtlp(stream.signal, key, body);
37
+ if (side === 'out') {
38
+ if (stream.eventId) throw new Error('unsupported_protocol');
39
+ stream.eventId = onExport({ at, signal: stream.signal, destination: `${host}:${port}`, bytes: size + 5, payload: data, protobufBase64: body.toString('base64'), flow: flow.key, stream: id });
40
+ } else {
41
+ stream.responseDecoded = true;
42
+ stream.partial = data.partialSuccess ?? null;
43
+ }
44
+ }
45
+ }
46
+ function finishResponse(stream, at) {
47
+ if (!stream.eventId) return;
48
+ const status = stream.grpcStatus;
49
+ const partial = stream.partial;
50
+ const rejected = partial && Object.entries(partial).find(([key]) => key.startsWith('rejected'))?.[1];
51
+ const outcome = status === '0' && stream.httpStatus === '200' && stream.responseDecoded ? (partial?.errorMessage || (rejected && rejected !== '0') ? 'partial' : 'accepted')
52
+ : status !== undefined && status !== '0' ? 'rejected' : 'unknown';
53
+ onResponse(stream.eventId, { outcome, at, grpcStatus: status ?? null, rejected: rejected ?? null, message: partial?.errorMessage ?? null });
54
+ }
55
+ function headers(flow, side, id, bytes, end, at) {
56
+ const values = flow[side].headers(bytes);
57
+ const stream = flow.streams.get(id) ?? {};
58
+ if (!flow.streams.has(id)) {
59
+ if (flow.streams.size >= 32) throw new Error('stream_limit');
60
+ flow.streams.set(id, stream);
61
+ }
62
+ if (side === 'out' && values[':path']) {
63
+ if (values[':method'] !== 'POST' || !values['content-type']?.startsWith('application/grpc')) throw new Error('unknown_export');
64
+ stream.signal = PATHS[values[':path']];
65
+ }
66
+ if (values['grpc-encoding']) stream[side + 'Encoding'] = values['grpc-encoding'];
67
+ if (side === 'in' && values['grpc-status'] !== undefined) stream.grpcStatus = values['grpc-status'];
68
+ if (side === 'in' && values[':status']) stream.httpStatus = values[':status'];
69
+ if (side === 'in' && end) { finishResponse(stream, at); flow.streams.delete(id); }
70
+ }
71
+ function frames(flow, side, bytes, at) {
72
+ const d = flow[side];
73
+ d.buffer = Buffer.concat([d.buffer, bytes]);
74
+ if (d.buffer.length > LIMIT * 2) throw new Error('stream_limit');
75
+ if (!flow.ready) {
76
+ if (side === 'in') return;
77
+ if (d.buffer[0] === 22) throw new Error('encrypted');
78
+ if (!PREFACE.subarray(0, Math.min(PREFACE.length, d.buffer.length)).equals(d.buffer.subarray(0, PREFACE.length))) throw new Error('joined_midstream');
79
+ if (d.buffer.length < PREFACE.length) return;
80
+ d.buffer = d.buffer.subarray(PREFACE.length);
81
+ flow.ready = true;
82
+ frames(flow, 'in', Buffer.alloc(0), at);
83
+ }
84
+ while (d.buffer.length >= 9) {
85
+ const length = d.buffer.readUIntBE(0, 3), type = d.buffer[3], flags = d.buffer[4], id = d.buffer.readUInt32BE(5) & 0x7fffffff;
86
+ if (length > LIMIT || type > 9) throw new Error('unsupported_protocol');
87
+ if (d.buffer.length < length + 9) return;
88
+ let body = d.buffer.subarray(9, 9 + length);
89
+ d.buffer = d.buffer.subarray(9 + length);
90
+ if (d.block && (type !== 9 || id !== d.block.id)) throw new Error('capture_gap');
91
+ if (type === 0 || type === 1) {
92
+ if (flags & 8) {
93
+ const pad = body[0];
94
+ if (pad === undefined || pad >= body.length) throw new Error('capture_gap');
95
+ body = body.subarray(1, body.length - pad);
96
+ }
97
+ if (type === 1 && (flags & 32)) {
98
+ if (body.length < 5) throw new Error('capture_gap');
99
+ body = body.subarray(5);
100
+ }
101
+ }
102
+ if (type === 1) {
103
+ if (flags & 4) headers(flow, side, id, body, flags & 1, at);
104
+ else d.block = { id, bytes: Buffer.from(body), end: flags & 1 };
105
+ } else if (type === 9) {
106
+ if (!d.block) throw new Error('capture_gap');
107
+ d.block.bytes = Buffer.concat([d.block.bytes, body]);
108
+ if (d.block.bytes.length > 65_536) throw new Error('stream_limit');
109
+ if (flags & 4) { const b = d.block; d.block = null; headers(flow, side, id, b.bytes, b.end, at); }
110
+ } else if (type === 0) {
111
+ const stream = flow.streams.get(id);
112
+ if (!stream) throw new Error('unknown_export');
113
+ payload(stream, side, body, at, flow, id);
114
+ if (side === 'in' && (flags & 1)) { finishResponse(stream, at); flow.streams.delete(id); }
115
+ } else if (type === 3) {
116
+ const stream = flow.streams.get(id);
117
+ if (stream?.eventId) onResponse(stream.eventId, { outcome: 'reset', at, grpcStatus: null, rejected: null, message: null });
118
+ flow.streams.delete(id);
119
+ } else if (type === 7) {
120
+ throw new Error('connection_closed');
121
+ }
122
+ }
123
+ let buffered = flow.out.buffer.length + flow.in.buffer.length;
124
+ for (const stream of flow.streams.values()) buffered += (stream.request?.length ?? 0) + (stream.response?.length ?? 0);
125
+ if (buffered > LIMIT * 4) throw new Error('stream_limit');
126
+ }
127
+ function tcp(packet, at) {
128
+ let offset = 0;
129
+ if (link === 1) {
130
+ if (packet.length < 14) return;
131
+ let protocol = packet.readUInt16BE(12); offset = 14;
132
+ for (let tag = 0; tag < 2 && [0x8100, 0x88a8].includes(protocol); tag++) {
133
+ if (packet.length < offset + 4) return;
134
+ protocol = packet.readUInt16BE(offset + 2); offset += 4;
135
+ }
136
+ if (protocol !== 0x800) { issue('unsupported_network'); return; }
137
+ } else if ([0, 108].includes(link)) offset = 4;
138
+ else if (link === 113) offset = 16;
139
+ else if (link === 276) offset = 20;
140
+ else if (link !== 101 && link !== 12) { issue('unsupported_link'); return; }
141
+ if (packet.length < offset + 20 || packet[offset] >> 4 !== 4) { issue('unsupported_network'); return; }
142
+ const ip = packet.subarray(offset);
143
+ const ihl = (ip[0] & 15) * 4, total = ip.readUInt16BE(2);
144
+ if (ihl < 20 || total < ihl || ip.length < total) { issue('capture_gap'); return; }
145
+ if (ip[9] !== 6) return;
146
+ if (ip.readUInt16BE(6) & 0x3fff) { issue('fragmented'); return; }
147
+ const src = [...ip.subarray(12, 16)].join('.'), dst = [...ip.subarray(16, 20)].join('.');
148
+ const t = ip.subarray(ihl, total);
149
+ if (t.length < 20) { issue('capture_gap'); return; }
150
+ const srcPort = t.readUInt16BE(0), dstPort = t.readUInt16BE(2);
151
+ const side = dst === host && dstPort === port ? 'out' : src === host && srcPort === port ? 'in' : null;
152
+ if (!side) return;
153
+ const key = side === 'out' ? `${src}:${srcPort}` : `${dst}:${dstPort}`;
154
+ for (const [k, f] of flows) if (at - f.at > 60_000) discard(k, 'idle_connection');
155
+ let flow = flows.get(key);
156
+ const syn = !!(t[13] & 2), ack = !!(t[13] & 16);
157
+ if (syn && !ack) flows.delete(key);
158
+ if (!flow || (syn && !ack)) {
159
+ if (flows.size >= 16) discard(flows.keys().next().value, 'connection_limit');
160
+ flow = { key, at, ready: false, out: direction(), in: direction(), streams: new Map(), ignored: false };
161
+ flows.set(key, flow);
162
+ }
163
+ flow.at = at;
164
+ const size = (t[12] >> 4) * 4;
165
+ if (size < 20 || size > t.length) { discard(key, 'capture_gap'); return; }
166
+ const body = t.subarray(size), d = flow[side];
167
+ let seq = (t.readUInt32BE(4) + (syn ? 1 : 0)) >>> 0;
168
+ if (syn && d.next === null) d.next = seq;
169
+ if (body.length && !flow.ignored) {
170
+ if (d.next === null) d.next = seq;
171
+ const delta = (seq - d.next) | 0;
172
+ if (delta > 0) {
173
+ d.queue.set(seq, Buffer.from(body));
174
+ let queued = 0; for (const b of d.queue.values()) queued += b.length;
175
+ if (d.queue.size > 32 || queued > LIMIT) discard(key, 'capture_gap');
176
+ return;
177
+ }
178
+ const part = delta < 0 ? body.subarray(-delta) : body;
179
+ if (part.length) {
180
+ d.next = (d.next + part.length) >>> 0;
181
+ try {
182
+ frames(flow, side, part, at);
183
+ while (d.queue.size) {
184
+ const next = [...d.queue.keys()].find(n => ((n - d.next) | 0) <= 0);
185
+ if (next === undefined) break;
186
+ const p = d.queue.get(next); d.queue.delete(next);
187
+ const remainder = p.subarray(Math.max(0, (d.next - next) | 0));
188
+ if (remainder.length) { d.next = (d.next + remainder.length) >>> 0; frames(flow, side, remainder, at); }
189
+ }
190
+ } catch (error) {
191
+ onObservation?.({ at, destination: `${host}:${port}`, reason: ["encrypted", "joined_midstream", "payload_limit", "stream_limit", "unsupported_compression", "unknown_export", "connection_closed"].includes(error.message) ? error.message : "decode_failed", source: flow.key });
192
+ issue(['encrypted', 'joined_midstream', 'payload_limit', 'stream_limit', 'unsupported_compression', 'unknown_export', 'connection_closed'].includes(error.message) ? error.message : 'decode_failed');
193
+ flow.ignored = true;
194
+ flow.out.buffer = flow.in.buffer = Buffer.alloc(0);
195
+ flow.out.queue.clear(); flow.in.queue.clear(); flow.streams.clear();
196
+ }
197
+ }
198
+ }
199
+ if (t[13] & 5) flows.delete(key);
200
+ }
201
+ return {
202
+ feed(chunk) {
203
+ if (chunk.length > LIMIT) throw new Error('capture_chunk_limit');
204
+ pending = Buffer.concat([pending, chunk]);
205
+ if (pending.length > LIMIT * 2) throw new Error('capture_buffer_limit');
206
+ if (!read32) {
207
+ if (pending.length < 24) return;
208
+ const magic = pending.subarray(0, 4).toString('hex');
209
+ if (!['d4c3b2a1', 'a1b2c3d4', '4d3cb2a1', 'a1b23c4d'].includes(magic)) throw new Error('pcap_required');
210
+ nanos = ['4d3cb2a1', 'a1b23c4d'].includes(magic);
211
+ read32 = magic.startsWith('a1') ? b => b.readUInt32BE() : b => b.readUInt32LE();
212
+ link = read32(pending.subarray(20));
213
+ if (![0, 1, 12, 101, 108, 113, 276].includes(link)) throw new Error('unsupported_link');
214
+ pending = pending.subarray(24);
215
+ }
216
+ while (pending.length >= 16) {
217
+ const size = read32(pending.subarray(8));
218
+ if (size > LIMIT) throw new Error('packet_limit');
219
+ if (pending.length < size + 16) return;
220
+ const at = read32(pending) * 1000 + read32(pending.subarray(4)) / (nanos ? 1e6 : 1000);
221
+ if (size !== read32(pending.subarray(12))) issue('capture_gap');
222
+ else tcp(pending.subarray(16, 16 + size), at);
223
+ pending = pending.subarray(size + 16);
224
+ }
225
+ },
226
+ close() { pending = Buffer.alloc(0); flows.clear(); },
227
+ };
228
+ }
@@ -0,0 +1,134 @@
1
+ import { execFile } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ import { basename } from "node:path";
4
+ import { readRadarConfig } from "./traffic-radar-config.mjs";
5
+
6
+ function systemCommand(file, args) {
7
+ return new Promise(resolve => {
8
+ execFile(file, args, { timeout: 2_000, maxBuffer: 262_144, encoding: "utf8", windowsHide: true }, (error, stdout, stderr) => {
9
+ if (!error) return resolve(stdout);
10
+ if (file === "/usr/sbin/lsof" && error.code === 1 && !stdout && !stderr) return resolve("");
11
+ resolve(null);
12
+ });
13
+ });
14
+ }
15
+
16
+ export function claudePids(text) {
17
+ const pids = [];
18
+ for (const line of String(text ?? "").split("\n")) {
19
+ const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(line);
20
+ if (match && ["claude", "claude-native"].includes(basename(match[2]))) {
21
+ const pid = Number(match[1]);
22
+ if (Number.isSafeInteger(pid) && pid > 0) pids.push(pid);
23
+ }
24
+ }
25
+ return [...new Set(pids)];
26
+ }
27
+
28
+ export function parseRadarSockets(text, pids) {
29
+ const allowed = new Set(pids);
30
+ const sockets = new Map();
31
+ let pid = null;
32
+ for (const line of String(text ?? "").split("\n")) {
33
+ if (/^p\d+$/.test(line)) pid = Number(line.slice(1));
34
+ if (!allowed.has(pid) || !line.startsWith("n")) continue;
35
+ const peer = line.slice(1).split("->")[1];
36
+ if (!peer) continue;
37
+ const match = /^(\[[0-9a-fA-F:.%]+\]|[0-9.]+):(\d+)$/.exec(peer);
38
+ if (!match) continue;
39
+ const port = Number(match[2]);
40
+ if (port < 1 || port > 65535) continue;
41
+ const destination = `${match[1]}:${port}`;
42
+ sockets.set(`${pid}|${destination}`, { pid, destination });
43
+ }
44
+ return [...sockets.values()];
45
+ }
46
+
47
+ export function parseRadarWorkspaces(text, pids, home = homedir()) {
48
+ const allowed = new Set(pids);
49
+ const workspaces = new Map();
50
+ let pid = null;
51
+ for (const line of String(text ?? "").split("\n")) {
52
+ if (/^p\d+$/.test(line)) pid = Number(line.slice(1));
53
+ if (allowed.has(pid) && line.startsWith("n/")) {
54
+ const path = line.slice(1);
55
+ workspaces.set(pid, path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path);
56
+ }
57
+ }
58
+ return workspaces;
59
+ }
60
+
61
+ export function createTrafficRadar({ platform = process.platform, now = Date.now, run = systemCommand, config = readRadarConfig } = {}) {
62
+ let pending = null;
63
+ let cached = null;
64
+ let previousAt = null;
65
+ let previousConfig = null;
66
+ const seen = new Map();
67
+ let alerts = [];
68
+ let serial = 0;
69
+ const addAlert = (message, at) => {
70
+ alerts.unshift({ id: ++serial, message, at });
71
+ alerts = alerts.slice(0, 8);
72
+ };
73
+
74
+ async function sample() {
75
+ const sampledAt = now();
76
+ if (previousAt !== null && sampledAt - previousAt > 30_000) {
77
+ seen.clear();
78
+ alerts = [];
79
+ previousConfig = null;
80
+ }
81
+ const base = { ok: true, sampledAt, platform, pollMs: 5_000, processCount: 0, connections: [], alerts: [] };
82
+ if (platform !== "darwin") return { ...base, status: "unsupported", config: null };
83
+ const [configuration, processes] = await Promise.all([config(), run("/bin/ps", ["-axo", "pid=,comm="])]);
84
+ const nextConfig = new Map(configuration.variables.filter(v => !v.key.endsWith("HEADERS")).map(v => [`${v.source}:${v.key}`, v.value]));
85
+ if (previousConfig) {
86
+ for (const [key, value] of nextConfig) {
87
+ if (previousConfig.get(key) === value) continue;
88
+ const variable = key.slice(key.indexOf(":") + 1);
89
+ if (variable.includes("ENDPOINT")) addAlert("Telemetry destination configuration changed. Review settings below.", sampledAt);
90
+ if (/^OTEL_LOG_/.test(variable) && value.startsWith("Enabled")) addAlert("Content capture configuration changed to enabled. Review settings below.", sampledAt);
91
+ }
92
+ }
93
+ const pids = claudePids(processes);
94
+ const selected = pids.slice(0, 64);
95
+ let sockets = "";
96
+ let cwd = "";
97
+ if (selected.length) [sockets, cwd] = await Promise.all([
98
+ run("/usr/sbin/lsof", ["-nP", "-a", "-p", selected.join(","), "-iTCP", "-sTCP:ESTABLISHED", "-Fpn"]),
99
+ run("/usr/sbin/lsof", ["-nP", "-a", "-p", selected.join(","), "-d", "cwd", "-Fpn"]),
100
+ ]);
101
+ const workspaces = parseRadarWorkspaces(cwd, selected);
102
+ const status = processes === null || sockets === null ? "unavailable" : "observing";
103
+ if (cached?.status === "observing" && status === "unavailable") addAlert("Connection visibility lost. A failed sample does not mean traffic stopped.", sampledAt);
104
+ const current = parseRadarSockets(sockets, selected);
105
+ const active = new Set(current.map(s => `${s.pid}|${s.destination}`));
106
+ const destinations = new Set([...seen.values()].map(s => s.destination));
107
+ for (const connection of current) {
108
+ const key = `${connection.pid}|${connection.destination}`;
109
+ if (!seen.has(key)) {
110
+ if (previousAt !== null && previousConfig && !destinations.has(connection.destination)) addAlert(`New Claude connection destination: ${connection.destination}`, sampledAt);
111
+ destinations.add(connection.destination);
112
+ seen.set(key, { ...connection, workspace: workspaces.get(connection.pid) ?? null, firstSeenAt: sampledAt, lastSeenAt: sampledAt });
113
+ } else seen.get(key).lastSeenAt = sampledAt;
114
+ }
115
+ for (const [key, connection] of seen) if (sampledAt - connection.lastSeenAt > 300_000) seen.delete(key);
116
+ while (seen.size > 100) seen.delete(seen.keys().next().value);
117
+ previousAt = sampledAt;
118
+ previousConfig = nextConfig;
119
+ return { ...base, status, config: configuration, processCount: pids.length, limited: pids.length > selected.length,
120
+ connections: [...seen.entries()].map(([key, value]) => ({ ...value, active: status === "observing" ? active.has(key) : null }))
121
+ .sort((a, b) => b.lastSeenAt - a.lastSeenAt), alerts };
122
+ }
123
+
124
+ return {
125
+ read() {
126
+ if (pending) return pending;
127
+ if (cached && now() - cached.sampledAt < 3_000) return Promise.resolve(cached);
128
+ pending = sample().then(result => { cached = result; return result; }).finally(() => { pending = null; });
129
+ return pending;
130
+ },
131
+ };
132
+ }
133
+
134
+ export const trafficRadar = createTrafficRadar();