kindred-tracer-node 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # kindred-tracer-node
2
+
3
+ Kindred Tracer SDK for Node.js - Auto-instrumentation for AI agents.
4
+
5
+ This package automatically intercepts HTTP/HTTPS requests from your AI agent, categorizes them as LLM calls or tool executions, and exports logs to the Kindred log-search system.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install kindred-tracer-node
11
+ # or
12
+ pnpm add kindred-tracer-node
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Basic Usage
18
+
19
+ Just call `kindredTracer()` once at startup, and all HTTP requests will be automatically intercepted and logged:
20
+
21
+ ```typescript
22
+ import { kindredTracer } from 'kindred-tracer-node';
23
+
24
+ // At startup - initialize the tracer
25
+ kindredTracer();
26
+
27
+ // Your agent code here - no wrapping needed!
28
+ // All HTTP requests will be automatically logged
29
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
30
+ method: 'POST',
31
+ body: JSON.stringify({ /* ... */ })
32
+ });
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ Set the following environment variables:
38
+
39
+ - `KINDRED_API_KEY` (required) - Your Kindred API key for authentication
40
+ - `KINDRED_API_URL` (optional) - Base URL for Kindred API, defaults to `https://api.usekindred.dev`
41
+ - `KINDRED_SESSION_ID` (optional) - Session identifier. If not set, a UUID will be auto-generated
42
+ - `KINDRED_AGENT_ID` (optional) - Agent identifier
43
+ - `KINDRED_RUN_ID` (optional) - Run identifier
44
+
45
+ You can also pass these values directly to `kindredTracer()`:
46
+
47
+ ```typescript
48
+ import { kindredTracer } from 'kindred-tracer-node';
49
+
50
+ // Initialize with explicit values
51
+ kindredTracer('session-123', 'agent-456', 'run-789');
52
+ ```
53
+
54
+ ## How It Works
55
+
56
+ 1. **Simple Initialization**: Call `kindredTracer()` once at startup to set up global context and enable interception.
57
+
58
+ 2. **Auto-instrumentation**: The tracer automatically patches Node.js's `https.request` and `http.request` when initialized.
59
+
60
+ 3. **Global Context**: Uses a global context that applies to all HTTP requests after initialization.
61
+
62
+ 4. **Request Detection**:
63
+ - **LLM Calls**: Detected by hostname (e.g., `api.openai.com`, `api.anthropic.com`) → logged as `role: "agent"`
64
+ - **Tool Calls**: Any other hostname → logged as `role: "tool"`
65
+
66
+ 5. **Streaming Support**: Handles streaming responses correctly - chunks are passed through immediately (zero latency) while being buffered for logging.
67
+
68
+ 6. **Non-blocking Export**: Logs are batched and exported asynchronously to avoid slowing down your agent.
69
+
70
+ ## Log Format
71
+
72
+ Logs are automatically formatted and sent to `${KINDRED_API_URL}/api/logs/ingest` with the following structure:
73
+
74
+ ```typescript
75
+ {
76
+ session_id: string;
77
+ timestamp: string; // ISO 8601
78
+ role: "user" | "agent" | "tool" | "system";
79
+ content: string;
80
+ agent_id?: string;
81
+ run_id?: string;
82
+ meta?: {
83
+ type: "llm_generation" | "tool_execution";
84
+ request_id: string;
85
+ host: string;
86
+ method: string;
87
+ path: string;
88
+ request_headers: Record<string, unknown>;
89
+ request_body: string | null;
90
+ response_status: number;
91
+ response_headers: Record<string, unknown>;
92
+ response_body: string | null;
93
+ duration_ms: number;
94
+ tool_calls?: Array<{...}>; // Extracted from OpenAI responses
95
+ };
96
+ }
97
+ ```
98
+
99
+ ## Flushing Logs
100
+
101
+ Before shutting down your application, you can flush any pending logs:
102
+
103
+ ```typescript
104
+ import { flushLogs } from 'kindred-tracer-node';
105
+
106
+ // On shutdown
107
+ await flushLogs();
108
+ ```
109
+
110
+ ## Security
111
+
112
+ The tracer automatically sanitizes sensitive headers before logging:
113
+ - `Authorization`
114
+ - `x-api-key`
115
+ - `api-key`
116
+ - `x-auth-token`
117
+ - `cookie`
118
+
119
+ ## Supported LLM Providers
120
+
121
+ The tracer automatically detects requests to:
122
+ - OpenAI (`api.openai.com`)
123
+ - Anthropic (`api.anthropic.com`)
124
+ - Google Gemini (`generativelanguage.googleapis.com`)
125
+ - Cohere (`api.cohere.com`)
126
+ - Mistral (`api.mistral.ai`)
127
+
128
+ ## Example
129
+
130
+ Here's a complete example:
131
+
132
+ ```typescript
133
+ import { kindredTracer, flushLogs } from 'kindred-tracer-node';
134
+
135
+ // Set your API key
136
+ process.env.KINDRED_API_KEY = 'your-api-key-here';
137
+
138
+ // Initialize the tracer (reads sessionId from KINDRED_SESSION_ID env var, or auto-generates)
139
+ kindredTracer();
140
+
141
+ // Your agent code - all HTTP requests are automatically logged
142
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json' },
145
+ body: JSON.stringify({
146
+ model: 'gpt-4',
147
+ messages: [{ role: 'user', content: 'Hello!' }]
148
+ })
149
+ });
150
+
151
+ // Before shutdown, flush any pending logs
152
+ await flushLogs();
153
+ ```
154
+
155
+ ## License
156
+
157
+ MIT
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Context management using global context for simple initialization pattern
3
+ */
4
+ export interface TraceContext {
5
+ sessionId: string;
6
+ agentId?: string;
7
+ runId?: string;
8
+ traceId: string;
9
+ }
10
+ /**
11
+ * Get the current trace context
12
+ */
13
+ export declare function getContext(): TraceContext | undefined;
14
+ /**
15
+ * Set the global trace context
16
+ */
17
+ export declare function setGlobalContext(sessionId: string, agentId?: string, runId?: string, traceId?: string): void;
18
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD;;GAEG;AACH,wBAAgB,UAAU,IAAI,YAAY,GAAG,SAAS,CAErD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,GACf,IAAI,CAON"}
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * Context management using global context for simple initialization pattern
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getContext = getContext;
7
+ exports.setGlobalContext = setGlobalContext;
8
+ // Global context storage
9
+ let globalContext = undefined;
10
+ /**
11
+ * Get the current trace context
12
+ */
13
+ function getContext() {
14
+ return globalContext;
15
+ }
16
+ /**
17
+ * Set the global trace context
18
+ */
19
+ function setGlobalContext(sessionId, agentId, runId, traceId) {
20
+ globalContext = {
21
+ sessionId,
22
+ agentId,
23
+ runId,
24
+ traceId: traceId || sessionId, // Use sessionId as traceId if not provided
25
+ };
26
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Non-blocking log exporter to Kindred API
3
+ */
4
+ import type { LogEntry } from "./types";
5
+ /**
6
+ * Add a log entry to the buffer
7
+ */
8
+ export declare function addLog(log: LogEntry): void;
9
+ /**
10
+ * Flush any remaining logs (call on shutdown)
11
+ * Waits for all pending logs to be sent
12
+ */
13
+ export declare function flush(): Promise<void>;
14
+ //# sourceMappingURL=exporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAcxC;;GAEG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAgB1C;AA2KD;;;GAGG;AACH,wBAAsB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CA6C3C"}
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ /**
3
+ * Non-blocking log exporter to Kindred API
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.addLog = addLog;
10
+ exports.flush = flush;
11
+ const https_1 = __importDefault(require("https"));
12
+ const http_1 = __importDefault(require("http"));
13
+ const url_1 = require("url");
14
+ const API_URL = process.env.KINDRED_API_URL || "https://api.usekindred.dev";
15
+ const API_KEY = process.env.KINDRED_API_KEY;
16
+ // In-memory buffer for batching logs
17
+ const logBuffer = [];
18
+ const BATCH_SIZE = 5; // Send logs in smaller batches to avoid payload size limits
19
+ const FLUSH_INTERVAL_MS = 5000; // Flush every 5 seconds
20
+ const MAX_PAYLOAD_SIZE = 100 * 1024; // 100KB max payload size
21
+ let flushTimer = null;
22
+ /**
23
+ * Add a log entry to the buffer
24
+ */
25
+ function addLog(log) {
26
+ logBuffer.push(log);
27
+ // Flush if buffer is full
28
+ if (logBuffer.length >= BATCH_SIZE) {
29
+ flushLogs().catch((error) => {
30
+ console.error("[kindred-tracer] Failed to flush logs:", error);
31
+ });
32
+ }
33
+ else if (!flushTimer) {
34
+ // Start flush timer if not already running
35
+ flushTimer = setTimeout(() => {
36
+ flushLogs().catch((error) => {
37
+ console.error("[kindred-tracer] Failed to flush logs:", error);
38
+ });
39
+ }, FLUSH_INTERVAL_MS);
40
+ }
41
+ }
42
+ /**
43
+ * Truncate large strings in log metadata to prevent payload size issues
44
+ */
45
+ function truncateLogEntry(log) {
46
+ const MAX_STRING_LENGTH = 10000; // 10KB max per string field
47
+ if (!log.meta) {
48
+ return log;
49
+ }
50
+ const truncatedMeta = { ...log.meta };
51
+ // Truncate large strings in metadata
52
+ const truncateString = (str) => {
53
+ if (!str || typeof str !== "string")
54
+ return str || null;
55
+ if (str.length <= MAX_STRING_LENGTH)
56
+ return str;
57
+ return str.substring(0, MAX_STRING_LENGTH) + `... [truncated ${str.length - MAX_STRING_LENGTH} chars]`;
58
+ };
59
+ if (typeof truncatedMeta.request_body === "string") {
60
+ truncatedMeta.request_body = truncateString(truncatedMeta.request_body);
61
+ }
62
+ if (typeof truncatedMeta.response_body === "string") {
63
+ truncatedMeta.response_body = truncateString(truncatedMeta.response_body);
64
+ }
65
+ return {
66
+ ...log,
67
+ meta: truncatedMeta,
68
+ };
69
+ }
70
+ /**
71
+ * Flush logs to the API (non-blocking)
72
+ */
73
+ function flushLogs() {
74
+ if (logBuffer.length === 0) {
75
+ if (flushTimer) {
76
+ clearTimeout(flushTimer);
77
+ flushTimer = null;
78
+ }
79
+ return Promise.resolve();
80
+ }
81
+ // Copy and clear buffer
82
+ const logsToSend = [...logBuffer];
83
+ logBuffer.length = 0;
84
+ if (flushTimer) {
85
+ clearTimeout(flushTimer);
86
+ flushTimer = null;
87
+ }
88
+ // Truncate large payloads
89
+ const truncatedLogs = logsToSend.map(truncateLogEntry);
90
+ // Send and return promise so caller can wait
91
+ return sendLogs(truncatedLogs).catch((error) => {
92
+ // Log error but don't throw - we don't want to break the agent
93
+ console.error("[kindred-tracer] Failed to send logs:", error);
94
+ });
95
+ }
96
+ /**
97
+ * Send logs to Kindred API
98
+ */
99
+ async function sendLogs(logs) {
100
+ if (!API_KEY) {
101
+ console.warn("[kindred-tracer] KINDRED_API_KEY not set, skipping log export");
102
+ return;
103
+ }
104
+ if (logs.length === 0) {
105
+ return;
106
+ }
107
+ const ingestUrl = new url_1.URL(`${API_URL}/api/logs/ingest`);
108
+ const isHttps = ingestUrl.protocol === "https:";
109
+ const client = isHttps ? https_1.default : http_1.default;
110
+ // Try to stringify, but split into smaller batches if too large
111
+ let postData;
112
+ let logsToSend = logs;
113
+ try {
114
+ postData = JSON.stringify({ logs });
115
+ // If payload is too large, split into smaller batches
116
+ if (postData.length > MAX_PAYLOAD_SIZE && logs.length > 1) {
117
+ const batchSize = Math.max(1, Math.floor(logs.length / 2));
118
+ const firstBatch = logs.slice(0, batchSize);
119
+ const secondBatch = logs.slice(batchSize);
120
+ // Send first batch
121
+ await sendLogs(firstBatch);
122
+ // Send second batch
123
+ await sendLogs(secondBatch);
124
+ return;
125
+ }
126
+ }
127
+ catch (error) {
128
+ // If stringify fails (too large), try sending fewer logs
129
+ if (logs.length > 1) {
130
+ const half = Math.floor(logs.length / 2);
131
+ await sendLogs(logs.slice(0, half));
132
+ await sendLogs(logs.slice(half));
133
+ return;
134
+ }
135
+ // If even one log is too large, skip it
136
+ console.error("[kindred-tracer] Log entry too large to serialize, skipping");
137
+ return;
138
+ }
139
+ const options = {
140
+ hostname: ingestUrl.hostname,
141
+ port: ingestUrl.port || (isHttps ? 443 : 80),
142
+ path: ingestUrl.pathname,
143
+ method: "POST",
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ "Content-Length": Buffer.byteLength(postData),
147
+ Authorization: `Bearer ${API_KEY}`,
148
+ },
149
+ timeout: 10000, // 10 second timeout
150
+ };
151
+ return new Promise((resolve, reject) => {
152
+ const req = client.request(options, (res) => {
153
+ let responseData = "";
154
+ res.on("data", (chunk) => {
155
+ responseData += chunk;
156
+ });
157
+ res.on("end", () => {
158
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
159
+ if (process.env.KINDRED_TRACER_VERBOSE) {
160
+ console.log(`[kindred-tracer] Successfully sent ${logs.length} log(s) to API`);
161
+ }
162
+ resolve();
163
+ }
164
+ else {
165
+ const errorMsg = `API returned ${res.statusCode}: ${responseData || "unknown error"}`;
166
+ console.error(`[kindred-tracer] ${errorMsg}`);
167
+ reject(new Error(errorMsg));
168
+ }
169
+ });
170
+ res.on("error", (error) => {
171
+ reject(error);
172
+ });
173
+ });
174
+ req.on("error", (error) => {
175
+ reject(error);
176
+ });
177
+ req.on("timeout", () => {
178
+ req.destroy();
179
+ reject(new Error("Request timeout"));
180
+ });
181
+ req.write(postData);
182
+ req.end();
183
+ });
184
+ }
185
+ /**
186
+ * Flush any remaining logs (call on shutdown)
187
+ * Waits for all pending logs to be sent
188
+ */
189
+ async function flush() {
190
+ // Clear any pending timer
191
+ if (flushTimer) {
192
+ clearTimeout(flushTimer);
193
+ flushTimer = null;
194
+ }
195
+ // Send all logs currently in buffer (don't wait for new ones)
196
+ if (logBuffer.length === 0) {
197
+ if (process.env.KINDRED_TRACER_VERBOSE) {
198
+ console.log("[kindred-tracer] No logs to flush");
199
+ }
200
+ return;
201
+ }
202
+ // Copy buffer and clear it immediately
203
+ const logsToSend = [...logBuffer];
204
+ logBuffer.length = 0;
205
+ if (process.env.KINDRED_TRACER_VERBOSE) {
206
+ console.log(`[kindred-tracer] Flushing ${logsToSend.length} log(s)`);
207
+ }
208
+ if (logsToSend.length > 0) {
209
+ // Truncate and send with timeout
210
+ const truncatedLogs = logsToSend.map(truncateLogEntry);
211
+ try {
212
+ // Add a timeout wrapper to prevent hanging
213
+ await Promise.race([
214
+ sendLogs(truncatedLogs),
215
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Flush timeout after 15 seconds")), 15000)),
216
+ ]);
217
+ if (process.env.KINDRED_TRACER_VERBOSE) {
218
+ console.log("[kindred-tracer] Flush completed successfully");
219
+ }
220
+ }
221
+ catch (error) {
222
+ console.error("[kindred-tracer] Failed to send logs during flush:", error.message || error);
223
+ // Don't throw - we want flush to complete even if send fails
224
+ }
225
+ }
226
+ // Give a moment for the HTTP request to complete
227
+ await new Promise((resolve) => setTimeout(resolve, 500));
228
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Kindred Tracer SDK for Node.js
3
+ * Auto-instrumentation for AI agents
4
+ */
5
+ /**
6
+ * Initialize the Kindred tracer.
7
+ *
8
+ * Reads sessionId, agentId, and runId from environment variables or parameters.
9
+ * Auto-generates sessionId if not provided.
10
+ *
11
+ * @param sessionId - Session identifier. If not provided, reads from KINDRED_SESSION_ID env var.
12
+ * If still not set, auto-generates a UUID.
13
+ * @param agentId - Agent identifier. If not provided, reads from KINDRED_AGENT_ID env var.
14
+ * @param runId - Run identifier. If not provided, reads from KINDRED_RUN_ID env var.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * import { kindredTracer } from 'kindred-tracer-node';
19
+ *
20
+ * // At startup - reads from environment variables
21
+ * kindredTracer();
22
+ *
23
+ * // Or specify explicitly
24
+ * kindredTracer('session-123', 'agent-456');
25
+ * ```
26
+ */
27
+ export declare function kindredTracer(sessionId?: string, agentId?: string, runId?: string): void;
28
+ /**
29
+ * Flush any pending logs to the API.
30
+ * Call this before shutting down your application.
31
+ */
32
+ export declare function flushLogs(): Promise<void>;
33
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,aAAa,CAC3B,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,GACb,IAAI,CAeN;AAED;;;GAGG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/C"}
package/dist/index.js ADDED
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ /**
3
+ * Kindred Tracer SDK for Node.js
4
+ * Auto-instrumentation for AI agents
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.kindredTracer = kindredTracer;
8
+ exports.flushLogs = flushLogs;
9
+ const uuid_1 = require("uuid");
10
+ const context_1 = require("./context");
11
+ const interceptor_1 = require("./interceptor");
12
+ const exporter_1 = require("./exporter");
13
+ // Track if interceptor has been initialized
14
+ let isInterceptorInitialized = false;
15
+ /**
16
+ * Initialize the Kindred tracer.
17
+ *
18
+ * Reads sessionId, agentId, and runId from environment variables or parameters.
19
+ * Auto-generates sessionId if not provided.
20
+ *
21
+ * @param sessionId - Session identifier. If not provided, reads from KINDRED_SESSION_ID env var.
22
+ * If still not set, auto-generates a UUID.
23
+ * @param agentId - Agent identifier. If not provided, reads from KINDRED_AGENT_ID env var.
24
+ * @param runId - Run identifier. If not provided, reads from KINDRED_RUN_ID env var.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { kindredTracer } from 'kindred-tracer-node';
29
+ *
30
+ * // At startup - reads from environment variables
31
+ * kindredTracer();
32
+ *
33
+ * // Or specify explicitly
34
+ * kindredTracer('session-123', 'agent-456');
35
+ * ```
36
+ */
37
+ function kindredTracer(sessionId, agentId, runId) {
38
+ // Read from parameters or environment variables
39
+ const finalSessionId = sessionId || process.env.KINDRED_SESSION_ID || (0, uuid_1.v4)();
40
+ const finalAgentId = agentId || process.env.KINDRED_AGENT_ID;
41
+ const finalRunId = runId || process.env.KINDRED_RUN_ID;
42
+ // Set global context
43
+ (0, context_1.setGlobalContext)(finalSessionId, finalAgentId, finalRunId);
44
+ // Initialize interceptor if not already done
45
+ if (!isInterceptorInitialized) {
46
+ (0, interceptor_1.initializeInterceptor)();
47
+ isInterceptorInitialized = true;
48
+ }
49
+ }
50
+ /**
51
+ * Flush any pending logs to the API.
52
+ * Call this before shutting down your application.
53
+ */
54
+ async function flushLogs() {
55
+ return (0, exporter_1.flush)();
56
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * HTTP/HTTPS request interceptor
3
+ * Handles streaming responses correctly: passes through chunks immediately while buffering for logging
4
+ */
5
+ /**
6
+ * Initialize the HTTP interceptor
7
+ */
8
+ export declare function initializeInterceptor(): void;
9
+ //# sourceMappingURL=interceptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4EH;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAsB5C"}
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ /**
3
+ * HTTP/HTTPS request interceptor
4
+ * Handles streaming responses correctly: passes through chunks immediately while buffering for logging
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.initializeInterceptor = initializeInterceptor;
11
+ const https_1 = __importDefault(require("https"));
12
+ const http_1 = __importDefault(require("http"));
13
+ const url_1 = require("url");
14
+ const context_1 = require("./context");
15
+ const exporter_1 = require("./exporter");
16
+ const parser_1 = require("./parser");
17
+ const types_1 = require("./types");
18
+ const uuid_1 = require("uuid");
19
+ // Store original request functions
20
+ const originalHttpsRequest = https_1.default.request;
21
+ const originalHttpRequest = http_1.default.request;
22
+ // Track if we've already patched
23
+ let isPatched = false;
24
+ /**
25
+ * Sanitize headers by removing sensitive information
26
+ */
27
+ function sanitizeHeaders(headers) {
28
+ const sanitized = { ...headers };
29
+ const sensitiveKeys = [
30
+ "authorization",
31
+ "x-api-key",
32
+ "api-key",
33
+ "x-auth-token",
34
+ "cookie",
35
+ ];
36
+ for (const key of sensitiveKeys) {
37
+ const lowerKey = key.toLowerCase();
38
+ for (const headerKey of Object.keys(sanitized)) {
39
+ if (headerKey.toLowerCase() === lowerKey) {
40
+ delete sanitized[headerKey];
41
+ }
42
+ }
43
+ }
44
+ return sanitized;
45
+ }
46
+ /**
47
+ * Extract request body from options or data
48
+ */
49
+ async function extractRequestBody(options, data) {
50
+ if (data !== undefined) {
51
+ if (typeof data === "string") {
52
+ return data;
53
+ }
54
+ if (Buffer.isBuffer(data)) {
55
+ return data.toString("utf-8");
56
+ }
57
+ return JSON.stringify(data);
58
+ }
59
+ // If body is in options, try to extract it
60
+ if ("body" in options && options.body !== undefined) {
61
+ if (typeof options.body === "string") {
62
+ return options.body;
63
+ }
64
+ if (Buffer.isBuffer(options.body)) {
65
+ return options.body.toString("utf-8");
66
+ }
67
+ return JSON.stringify(options.body);
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Initialize the HTTP interceptor
73
+ */
74
+ function initializeInterceptor() {
75
+ if (isPatched) {
76
+ return;
77
+ }
78
+ isPatched = true;
79
+ // Patch https.request
80
+ https_1.default.request = function (options, callback) {
81
+ return interceptRequest(originalHttpsRequest, options, callback, true);
82
+ };
83
+ // Patch http.request
84
+ http_1.default.request = function (options, callback) {
85
+ return interceptRequest(originalHttpRequest, options, callback, false);
86
+ };
87
+ }
88
+ /**
89
+ * Intercept HTTP/HTTPS requests
90
+ */
91
+ function interceptRequest(originalRequest, options, callback, isHttps = true) {
92
+ const context = (0, context_1.getContext)();
93
+ // If no context, just pass through
94
+ if (!context) {
95
+ if (typeof options === "string" || options instanceof url_1.URL) {
96
+ return originalRequest(options, callback);
97
+ }
98
+ return originalRequest(options, callback);
99
+ }
100
+ // Normalize options
101
+ let normalizedOptions;
102
+ if (typeof options === "string" || options instanceof url_1.URL) {
103
+ const url = new url_1.URL(options.toString());
104
+ normalizedOptions = {
105
+ hostname: url.hostname,
106
+ port: url.port || (isHttps ? 443 : 80),
107
+ path: url.pathname + url.search,
108
+ protocol: url.protocol,
109
+ };
110
+ }
111
+ else {
112
+ normalizedOptions = { ...options };
113
+ }
114
+ const hostname = normalizedOptions.hostname ||
115
+ (normalizedOptions.host ? normalizedOptions.host.split(":")[0] : null);
116
+ if (!hostname) {
117
+ // Can't determine host, pass through
118
+ return originalRequest(normalizedOptions, callback);
119
+ }
120
+ // Determine if this is an LLM call or tool call
121
+ const isLLM = (0, types_1.isLLMHost)(hostname);
122
+ const role = isLLM ? "agent" : "tool";
123
+ // Extract request data
124
+ const requestId = (0, uuid_1.v4)();
125
+ const startTime = Date.now();
126
+ let requestBody = null;
127
+ const requestHeaders = sanitizeHeaders(normalizedOptions.headers || {});
128
+ // Create the request
129
+ const req = originalRequest(normalizedOptions, (res) => {
130
+ // Buffer for response body (for logging) - accumulates chunks
131
+ const responseChunks = [];
132
+ // Intercept data events to buffer while passing through immediately
133
+ // This ensures zero latency - chunks go to user immediately
134
+ const originalEmit = res.emit.bind(res);
135
+ res.emit = function (event, ...args) {
136
+ // Buffer data chunks for logging
137
+ if (event === "data" && args[0] instanceof Buffer) {
138
+ responseChunks.push(args[0]);
139
+ }
140
+ // Always emit the event (pass through) - this is critical for zero latency
141
+ return originalEmit(event, ...args);
142
+ };
143
+ // When response ends, log it
144
+ res.once("end", () => {
145
+ // Reconstruct response body from buffered chunks
146
+ // Truncate to prevent huge payloads (max 50KB per response)
147
+ const MAX_RESPONSE_BODY_SIZE = 50 * 1024; // 50KB
148
+ let responseBody = null;
149
+ if (responseChunks.length > 0) {
150
+ const fullBody = Buffer.concat(responseChunks).toString("utf-8");
151
+ if (fullBody.length > MAX_RESPONSE_BODY_SIZE) {
152
+ responseBody =
153
+ fullBody.substring(0, MAX_RESPONSE_BODY_SIZE) +
154
+ `... [truncated ${fullBody.length - MAX_RESPONSE_BODY_SIZE} chars]`;
155
+ }
156
+ else {
157
+ responseBody = fullBody;
158
+ }
159
+ }
160
+ // Parse response if it's JSON
161
+ let parsedResponse = null;
162
+ let toolCalls = null;
163
+ if (responseBody) {
164
+ try {
165
+ parsedResponse = JSON.parse(responseBody);
166
+ if (isLLM) {
167
+ const parsed = (0, parser_1.parseOpenAIResponse)(parsedResponse);
168
+ if (parsed.toolCalls) {
169
+ toolCalls = parsed.toolCalls;
170
+ }
171
+ }
172
+ }
173
+ catch {
174
+ // Not JSON, ignore
175
+ }
176
+ }
177
+ const endTime = Date.now();
178
+ const duration = endTime - startTime;
179
+ // Build log entry
180
+ const logEntry = {
181
+ session_id: context.sessionId,
182
+ timestamp: new Date(startTime).toISOString(),
183
+ role,
184
+ content: isLLM
185
+ ? `LLM request to ${hostname}${normalizedOptions.path || ""}`
186
+ : `Tool request to ${hostname}${normalizedOptions.path || ""}`,
187
+ agent_id: context.agentId,
188
+ run_id: context.runId,
189
+ meta: {
190
+ type: isLLM ? "llm_generation" : "tool_execution",
191
+ request_id: requestId,
192
+ host: hostname,
193
+ method: normalizedOptions.method || "GET",
194
+ path: normalizedOptions.path || "",
195
+ request_headers: requestHeaders,
196
+ request_body: requestBody,
197
+ response_status: res.statusCode,
198
+ response_headers: sanitizeHeaders(res.headers || {}),
199
+ response_body: responseBody,
200
+ duration_ms: duration,
201
+ ...(toolCalls ? { tool_calls: toolCalls } : {}),
202
+ },
203
+ };
204
+ // Export log (non-blocking)
205
+ (0, exporter_1.addLog)(logEntry);
206
+ });
207
+ // Call original callback if provided
208
+ if (callback) {
209
+ callback(res);
210
+ }
211
+ });
212
+ // Intercept request body if data is written
213
+ // Accumulate request body chunks (with size limit)
214
+ const requestChunks = [];
215
+ const MAX_REQUEST_BODY_SIZE = 50 * 1024; // 50KB
216
+ let requestBodySize = 0;
217
+ const originalWrite = req.write.bind(req);
218
+ const originalEnd = req.end.bind(req);
219
+ req.write = function (chunk, encoding, cb) {
220
+ // Buffer request body chunks (up to size limit)
221
+ if (requestBodySize < MAX_REQUEST_BODY_SIZE) {
222
+ let chunkBuffer;
223
+ if (typeof chunk === "string") {
224
+ const enc = typeof encoding === "string" ? encoding : undefined;
225
+ chunkBuffer = Buffer.from(chunk, enc);
226
+ }
227
+ else if (Buffer.isBuffer(chunk)) {
228
+ chunkBuffer = chunk;
229
+ }
230
+ else {
231
+ return originalWrite(chunk, encoding, cb);
232
+ }
233
+ if (requestBodySize + chunkBuffer.length <= MAX_REQUEST_BODY_SIZE) {
234
+ requestChunks.push(chunkBuffer);
235
+ requestBodySize += chunkBuffer.length;
236
+ }
237
+ else {
238
+ // Truncate at limit
239
+ const remaining = MAX_REQUEST_BODY_SIZE - requestBodySize;
240
+ requestChunks.push(chunkBuffer.subarray(0, remaining));
241
+ requestBodySize = MAX_REQUEST_BODY_SIZE;
242
+ }
243
+ }
244
+ return originalWrite(chunk, encoding, cb);
245
+ };
246
+ req.end = function (chunk, encoding, cb) {
247
+ // Buffer final chunk if present and under limit
248
+ if (chunk !== undefined && requestBodySize < MAX_REQUEST_BODY_SIZE) {
249
+ let chunkBuffer;
250
+ if (typeof chunk === "string") {
251
+ const enc = typeof encoding === "string" ? encoding : undefined;
252
+ chunkBuffer = Buffer.from(chunk, enc);
253
+ }
254
+ else if (Buffer.isBuffer(chunk)) {
255
+ chunkBuffer = chunk;
256
+ }
257
+ else {
258
+ return originalEnd(chunk, encoding, cb);
259
+ }
260
+ if (requestBodySize + chunkBuffer.length <= MAX_REQUEST_BODY_SIZE) {
261
+ requestChunks.push(chunkBuffer);
262
+ requestBodySize += chunkBuffer.length;
263
+ }
264
+ else {
265
+ const remaining = MAX_REQUEST_BODY_SIZE - requestBodySize;
266
+ requestChunks.push(chunkBuffer.subarray(0, remaining));
267
+ requestBodySize = MAX_REQUEST_BODY_SIZE;
268
+ }
269
+ }
270
+ // Reconstruct request body (may be truncated)
271
+ if (requestChunks.length > 0) {
272
+ requestBody = Buffer.concat(requestChunks).toString("utf-8");
273
+ if (requestBodySize >= MAX_REQUEST_BODY_SIZE) {
274
+ requestBody += "... [truncated]";
275
+ }
276
+ }
277
+ return originalEnd(chunk, encoding, cb);
278
+ };
279
+ return req;
280
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Response parser for extracting structured data from LLM responses
3
+ */
4
+ export interface ToolCall {
5
+ id?: string;
6
+ type: string;
7
+ function: {
8
+ name: string;
9
+ arguments: string;
10
+ };
11
+ }
12
+ export interface ParsedResponse {
13
+ toolCalls?: ToolCall[];
14
+ content?: string;
15
+ role?: string;
16
+ }
17
+ /**
18
+ * Parse OpenAI JSON response and extract tool calls
19
+ */
20
+ export declare function parseOpenAIResponse(body: unknown): ParsedResponse;
21
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,QAAQ;IACvB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CA4CjE"}
package/dist/parser.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ /**
3
+ * Response parser for extracting structured data from LLM responses
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseOpenAIResponse = parseOpenAIResponse;
7
+ /**
8
+ * Parse OpenAI JSON response and extract tool calls
9
+ */
10
+ function parseOpenAIResponse(body) {
11
+ const result = {};
12
+ if (typeof body !== "object" || body === null) {
13
+ return result;
14
+ }
15
+ const obj = body;
16
+ // Handle chat completion response
17
+ if (Array.isArray(obj.choices)) {
18
+ const firstChoice = obj.choices[0];
19
+ if (firstChoice?.message) {
20
+ const message = firstChoice.message;
21
+ // Extract tool calls
22
+ if (Array.isArray(message.tool_calls)) {
23
+ result.toolCalls = message.tool_calls.map((tc) => {
24
+ const toolCall = tc;
25
+ const fn = toolCall.function;
26
+ return {
27
+ id: typeof toolCall.id === "string" ? toolCall.id : undefined,
28
+ type: typeof toolCall.type === "string" ? toolCall.type : "function",
29
+ function: {
30
+ name: typeof fn?.name === "string" ? fn.name : "",
31
+ arguments: typeof fn?.arguments === "string" ? fn.arguments : "{}",
32
+ },
33
+ };
34
+ });
35
+ }
36
+ // Extract content
37
+ if (typeof message.content === "string") {
38
+ result.content = message.content;
39
+ }
40
+ // Extract role
41
+ if (typeof message.role === "string") {
42
+ result.role = message.role;
43
+ }
44
+ }
45
+ }
46
+ return result;
47
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Type definitions for kindred-tracer
3
+ */
4
+ export interface LogEntry {
5
+ session_id: string;
6
+ timestamp: string;
7
+ role: "user" | "agent" | "tool" | "system";
8
+ content: string;
9
+ agent_id?: string;
10
+ run_id?: string;
11
+ meta?: Record<string, unknown>;
12
+ }
13
+ /**
14
+ * LLM provider hosts
15
+ */
16
+ export declare const LLM_HOSTS: Set<string>;
17
+ /**
18
+ * Check if a host is an LLM provider
19
+ */
20
+ export declare function isLLMHost(host: string): boolean;
21
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,aAMpB,CAAC;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/C"}
package/dist/types.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ /**
3
+ * Type definitions for kindred-tracer
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LLM_HOSTS = void 0;
7
+ exports.isLLMHost = isLLMHost;
8
+ /**
9
+ * LLM provider hosts
10
+ */
11
+ exports.LLM_HOSTS = new Set([
12
+ "api.openai.com",
13
+ "api.anthropic.com",
14
+ "generativelanguage.googleapis.com", // Google Gemini
15
+ "api.cohere.com",
16
+ "api.mistral.ai",
17
+ ]);
18
+ /**
19
+ * Check if a host is an LLM provider
20
+ */
21
+ function isLLMHost(host) {
22
+ return exports.LLM_HOSTS.has(host.toLowerCase());
23
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "kindred-tracer-node",
3
+ "version": "1.0.0",
4
+ "description": "Kindred Tracer SDK for Node.js - Auto-instrumentation for AI agents",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/aryanmundre/Kindred.git",
10
+ "directory": "packages/kindred-tracer/node"
11
+ },
12
+ "homepage": "https://github.com/aryanmundre/Kindred#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/aryanmundre/Kindred/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "clean": "node -e \"require('fs').rmSync('dist', {recursive: true, force: true}); require('fs').rmSync('tsconfig.tsbuildinfo', {force: true});\"",
21
+ "prebuild": "pnpm run clean",
22
+ "build": "tsc --build",
23
+ "prepublishOnly": "pnpm run build"
24
+ },
25
+ "dependencies": {
26
+ "uuid": "^9.0.1"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^20.10.6",
30
+ "@types/uuid": "^9.0.7",
31
+ "typescript": "^5.3.0"
32
+ },
33
+ "keywords": ["kindred", "tracer", "instrumentation", "ai", "agent"],
34
+ "license": "MIT"
35
+ }