clientcoded 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "clientcoded",
3
+ "version": "0.1.0",
4
+ "description": "ClientCoded SDK - Trace your AI agent's tool calls for root cause analysis",
5
+ "main": "src/index.js",
6
+ "types": "src/index.d.ts",
7
+ "keywords": [
8
+ "ai",
9
+ "agent",
10
+ "testing",
11
+ "qa",
12
+ "tracing",
13
+ "evaluation",
14
+ "llm"
15
+ ],
16
+ "author": "ClientCoded <travis@clientcoded.com>",
17
+ "license": "MIT",
18
+ "homepage": "https://github.com/ClientCoded/clientcoded-js",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/ClientCoded/clientcoded-js.git"
22
+ },
23
+ "engines": {
24
+ "node": ">=16.0.0"
25
+ },
26
+ "files": [
27
+ "src/"
28
+ ]
29
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ declare module "clientcoded" {
2
+ interface ConfigOptions {
3
+ agentId: string;
4
+ apiKey: string;
5
+ traceUrl?: string;
6
+ }
7
+
8
+ interface TraceData {
9
+ input?: any;
10
+ output?: any;
11
+ error?: string;
12
+ latencyMs?: number;
13
+ }
14
+
15
+ export function configure(config: ConfigOptions): void;
16
+ export function setConversationId(conversationId: string): void;
17
+ export function setTurn(turnNumber: number): void;
18
+ export function incrementTurn(): void;
19
+ export function trace<T extends (...args: any[]) => Promise<any>>(
20
+ name: string,
21
+ fn: T
22
+ ): T;
23
+ export function traceSync<T extends (...args: any[]) => any>(
24
+ name: string,
25
+ fn: T
26
+ ): T;
27
+ export function logTrace(toolName: string, data?: TraceData): void;
28
+ export function traceLangChain<T extends (...args: any[]) => Promise<any>>(
29
+ name: string,
30
+ fn: T
31
+ ): T;
32
+ }
package/src/index.js ADDED
@@ -0,0 +1,258 @@
1
+ // ClientCoded JavaScript/TypeScript SDK
2
+ // npm install clientcoded
3
+
4
+ const DEFAULT_API = "https://clientcoded.app.n8n.cloud/webhook/ap-35-trace-ingest";
5
+
6
+ let _config = {
7
+ agentId: null,
8
+ apiKey: null,
9
+ traceUrl: DEFAULT_API,
10
+ };
11
+
12
+ let _conversationId = null;
13
+ let _turn = null;
14
+
15
+ /**
16
+ * Configure the SDK globally.
17
+ * @param {Object} config
18
+ * @param {string} config.agentId - Your agent's ID from ClientCoded dashboard
19
+ * @param {string} config.apiKey - Your API key from ClientCoded dashboard
20
+ * @param {string} [config.traceUrl] - Optional custom trace endpoint URL
21
+ */
22
+ function configure(config) {
23
+ _config.agentId = config.agentId;
24
+ _config.apiKey = config.apiKey;
25
+ if (config.traceUrl) {
26
+ _config.traceUrl = config.traceUrl;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Set the current conversation ID. Call at the start of each conversation.
32
+ * @param {string} conversationId
33
+ */
34
+ function setConversationId(conversationId) {
35
+ _conversationId = conversationId;
36
+ _turn = 0;
37
+ }
38
+
39
+ /**
40
+ * Set the current turn number.
41
+ * @param {number} turnNumber
42
+ */
43
+ function setTurn(turnNumber) {
44
+ _turn = turnNumber;
45
+ }
46
+
47
+ /**
48
+ * Increment the turn counter by 1.
49
+ */
50
+ function incrementTurn() {
51
+ _turn = (_turn || 0) + 1;
52
+ }
53
+
54
+ /**
55
+ * Wrap a function with tracing.
56
+ *
57
+ * Usage:
58
+ * const getInvoice = trace("get_invoice", async (invoiceId) => {
59
+ * return await stripe.invoices.retrieve(invoiceId);
60
+ * });
61
+ *
62
+ * @param {string} name - Name for this traced function
63
+ * @param {Function} fn - The function to trace
64
+ * @returns {Function} - Wrapped function with tracing
65
+ */
66
+ function trace(name, fn) {
67
+ return async function (...args) {
68
+ const traceId = _uuid();
69
+ const start = Date.now();
70
+ let error = null;
71
+ let result = null;
72
+
73
+ try {
74
+ result = await fn(...args);
75
+ return result;
76
+ } catch (e) {
77
+ error = e.message || String(e);
78
+ throw e;
79
+ } finally {
80
+ _sendTrace({
81
+ traceId,
82
+ toolName: name,
83
+ input: _safeSerializeInput(args),
84
+ output: _safeSerializeOutput(result),
85
+ error,
86
+ latencyMs: Date.now() - start,
87
+ });
88
+ }
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Wrap a synchronous function with tracing.
94
+ *
95
+ * Usage:
96
+ * const parseData = traceSync("parse_data", (raw) => {
97
+ * return JSON.parse(raw);
98
+ * });
99
+ *
100
+ * @param {string} name - Name for this traced function
101
+ * @param {Function} fn - The synchronous function to trace
102
+ * @returns {Function} - Wrapped function with tracing
103
+ */
104
+ function traceSync(name, fn) {
105
+ return function (...args) {
106
+ const traceId = _uuid();
107
+ const start = Date.now();
108
+ let error = null;
109
+ let result = null;
110
+
111
+ try {
112
+ result = fn(...args);
113
+ return result;
114
+ } catch (e) {
115
+ error = e.message || String(e);
116
+ throw e;
117
+ } finally {
118
+ _sendTrace({
119
+ traceId,
120
+ toolName: name,
121
+ input: _safeSerializeInput(args),
122
+ output: _safeSerializeOutput(result),
123
+ error,
124
+ latencyMs: Date.now() - start,
125
+ });
126
+ }
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Manual trace logging for cases where wrappers don't fit.
132
+ *
133
+ * Usage:
134
+ * const start = Date.now();
135
+ * try {
136
+ * const result = await complexOperation();
137
+ * logTrace("complex_op", { input: { key: "val" }, output: result });
138
+ * } catch (e) {
139
+ * logTrace("complex_op", { input: { key: "val" }, error: e.message });
140
+ * }
141
+ *
142
+ * @param {string} toolName
143
+ * @param {Object} data
144
+ * @param {*} [data.input]
145
+ * @param {*} [data.output]
146
+ * @param {string} [data.error]
147
+ * @param {number} [data.latencyMs]
148
+ */
149
+ function logTrace(toolName, data = {}) {
150
+ _sendTrace({
151
+ traceId: _uuid(),
152
+ toolName,
153
+ input: data.input ? _safeSerializeInput([data.input]) : null,
154
+ output: data.output ? _safeSerializeOutput(data.output) : null,
155
+ error: data.error || null,
156
+ latencyMs: data.latencyMs || null,
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Create a traced wrapper for a LangChain tool function.
162
+ *
163
+ * Usage:
164
+ * const searchDb = traceLangChain("search_database", async (query) => {
165
+ * return await db.execute(query);
166
+ * });
167
+ *
168
+ * @param {string} name
169
+ * @param {Function} fn
170
+ * @returns {Function}
171
+ */
172
+ function traceLangChain(name, fn) {
173
+ return trace(name, fn);
174
+ }
175
+
176
+ // === Internal functions ===
177
+
178
+ function _sendTrace({ traceId, toolName, input, output, error, latencyMs }) {
179
+ if (!_config.agentId || !_config.apiKey) return;
180
+
181
+ const payload = {
182
+ agent_id: _config.agentId,
183
+ api_key: _config.apiKey,
184
+ trace_id: traceId,
185
+ conversation_id: _conversationId,
186
+ turn: _turn,
187
+ tool_name: toolName,
188
+ input: input,
189
+ output: output,
190
+ error: error,
191
+ latency_ms: latencyMs,
192
+ timestamp: Date.now() / 1000,
193
+ };
194
+
195
+ // Fire and forget
196
+ try {
197
+ const controller = new AbortController();
198
+ const timeout = setTimeout(() => controller.abort(), 2000);
199
+
200
+ fetch(_config.traceUrl, {
201
+ method: "POST",
202
+ headers: { "Content-Type": "application/json" },
203
+ body: JSON.stringify(payload),
204
+ signal: controller.signal,
205
+ })
206
+ .catch(() => {})
207
+ .finally(() => clearTimeout(timeout));
208
+ } catch {
209
+ // Never break the customer's agent
210
+ }
211
+ }
212
+
213
+ function _safeSerializeInput(args) {
214
+ try {
215
+ const serialized = args.map((a) => _truncate(JSON.stringify(a)));
216
+ return { args: serialized };
217
+ } catch {
218
+ return { error: "unserializable" };
219
+ }
220
+ }
221
+
222
+ function _safeSerializeOutput(result) {
223
+ if (result === null || result === undefined) return null;
224
+ try {
225
+ return _truncate(JSON.stringify(result));
226
+ } catch {
227
+ return "unserializable";
228
+ }
229
+ }
230
+
231
+ function _truncate(s, maxLen = 2000) {
232
+ if (s.length > maxLen) {
233
+ return s.slice(0, maxLen) + "... (truncated)";
234
+ }
235
+ return s;
236
+ }
237
+
238
+ function _uuid() {
239
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
240
+ return crypto.randomUUID();
241
+ }
242
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
243
+ const r = (Math.random() * 16) | 0;
244
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
245
+ return v.toString(16);
246
+ });
247
+ }
248
+
249
+ module.exports = {
250
+ configure,
251
+ setConversationId,
252
+ setTurn,
253
+ incrementTurn,
254
+ trace,
255
+ traceSync,
256
+ logTrace,
257
+ traceLangChain,
258
+ };