@amplitude/ai 0.2.1 → 0.3.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.
Files changed (104) hide show
  1. package/.claude/commands/instrument-with-amplitude-ai.md +12 -0
  2. package/.cursor/skills/instrument-with-amplitude-ai/SKILL.md +4 -42
  3. package/AGENTS.md +86 -28
  4. package/README.md +190 -111
  5. package/amplitude-ai.md +463 -0
  6. package/bin/amplitude-ai-doctor.mjs +0 -0
  7. package/bin/amplitude-ai-instrument.mjs +0 -0
  8. package/bin/amplitude-ai-mcp.mjs +0 -0
  9. package/bin/amplitude-ai-register-catalog.mjs +0 -0
  10. package/bin/amplitude-ai-status.mjs +0 -0
  11. package/bin/amplitude-ai.mjs +14 -5
  12. package/data/agent_event_catalog.json +52 -2
  13. package/dist/bound-agent.d.ts +18 -14
  14. package/dist/bound-agent.d.ts.map +1 -1
  15. package/dist/bound-agent.js +4 -1
  16. package/dist/bound-agent.js.map +1 -1
  17. package/dist/cli/status.d.ts.map +1 -1
  18. package/dist/cli/status.js +31 -19
  19. package/dist/cli/status.js.map +1 -1
  20. package/dist/client.d.ts +14 -14
  21. package/dist/client.d.ts.map +1 -1
  22. package/dist/client.js +38 -0
  23. package/dist/client.js.map +1 -1
  24. package/dist/context.d.ts +5 -0
  25. package/dist/context.d.ts.map +1 -1
  26. package/dist/context.js +4 -0
  27. package/dist/context.js.map +1 -1
  28. package/dist/core/constants.d.ts +3 -1
  29. package/dist/core/constants.d.ts.map +1 -1
  30. package/dist/core/constants.js +3 -1
  31. package/dist/core/constants.js.map +1 -1
  32. package/dist/core/tracking.d.ts +12 -2
  33. package/dist/core/tracking.d.ts.map +1 -1
  34. package/dist/core/tracking.js +13 -2
  35. package/dist/core/tracking.js.map +1 -1
  36. package/dist/decorators.d.ts +1 -1
  37. package/dist/decorators.d.ts.map +1 -1
  38. package/dist/decorators.js +4 -3
  39. package/dist/decorators.js.map +1 -1
  40. package/dist/index.d.ts +7 -4
  41. package/dist/index.js +5 -2
  42. package/dist/mcp/contract.d.ts +4 -0
  43. package/dist/mcp/contract.d.ts.map +1 -1
  44. package/dist/mcp/contract.js +6 -2
  45. package/dist/mcp/contract.js.map +1 -1
  46. package/dist/mcp/generate-verify-test.d.ts +7 -0
  47. package/dist/mcp/generate-verify-test.d.ts.map +1 -0
  48. package/dist/mcp/generate-verify-test.js +25 -0
  49. package/dist/mcp/generate-verify-test.js.map +1 -0
  50. package/dist/mcp/index.d.ts +2 -1
  51. package/dist/mcp/index.js +2 -1
  52. package/dist/mcp/instrument-file.d.ts +14 -0
  53. package/dist/mcp/instrument-file.d.ts.map +1 -0
  54. package/dist/mcp/instrument-file.js +139 -0
  55. package/dist/mcp/instrument-file.js.map +1 -0
  56. package/dist/mcp/scan-project.d.ts +52 -0
  57. package/dist/mcp/scan-project.d.ts.map +1 -0
  58. package/dist/mcp/scan-project.js +309 -0
  59. package/dist/mcp/scan-project.js.map +1 -0
  60. package/dist/mcp/server.d.ts.map +1 -1
  61. package/dist/mcp/server.js +79 -4
  62. package/dist/mcp/server.js.map +1 -1
  63. package/dist/mcp/validate-file.d.ts +4 -0
  64. package/dist/mcp/validate-file.d.ts.map +1 -1
  65. package/dist/mcp/validate-file.js +559 -11
  66. package/dist/mcp/validate-file.js.map +1 -1
  67. package/dist/middleware.js +2 -1
  68. package/dist/middleware.js.map +1 -1
  69. package/dist/node_modules/.pnpm/acorn-typescript@1.4.13_acorn@8.16.0/node_modules/acorn-typescript/lib/index.js +2389 -0
  70. package/dist/node_modules/.pnpm/acorn-typescript@1.4.13_acorn@8.16.0/node_modules/acorn-typescript/lib/index.js.map +1 -0
  71. package/dist/node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/dist/acorn.js +5128 -0
  72. package/dist/node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/dist/acorn.js.map +1 -0
  73. package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js +1 -1
  74. package/dist/patching.d.ts.map +1 -1
  75. package/dist/providers/anthropic.d.ts.map +1 -1
  76. package/dist/providers/anthropic.js +1 -0
  77. package/dist/providers/anthropic.js.map +1 -1
  78. package/dist/providers/base.d.ts +2 -1
  79. package/dist/providers/base.d.ts.map +1 -1
  80. package/dist/providers/base.js +4 -0
  81. package/dist/providers/base.js.map +1 -1
  82. package/dist/providers/openai.d.ts.map +1 -1
  83. package/dist/providers/openai.js +2 -0
  84. package/dist/providers/openai.js.map +1 -1
  85. package/dist/serverless.d.ts +19 -0
  86. package/dist/serverless.d.ts.map +1 -0
  87. package/dist/serverless.js +35 -0
  88. package/dist/serverless.js.map +1 -0
  89. package/dist/session.d.ts +24 -8
  90. package/dist/session.d.ts.map +1 -1
  91. package/dist/session.js +20 -1
  92. package/dist/session.js.map +1 -1
  93. package/dist/types.d.ts +1 -0
  94. package/dist/types.d.ts.map +1 -1
  95. package/dist/types.js.map +1 -1
  96. package/llms-full.txt +353 -69
  97. package/llms.txt +6 -2
  98. package/mcp.schema.json +7 -3
  99. package/package.json +10 -5
  100. package/bin/amplitude-ai-init.mjs +0 -27
  101. package/dist/cli/init.d.ts +0 -14
  102. package/dist/cli/init.d.ts.map +0 -1
  103. package/dist/cli/init.js +0 -40
  104. package/dist/cli/init.js.map +0 -1
@@ -1,5 +1,451 @@
1
+ import { require_acorn } from "../node_modules/.pnpm/acorn@8.16.0/node_modules/acorn/dist/acorn.js";
2
+ import { require_lib } from "../node_modules/.pnpm/acorn-typescript@1.4.13_acorn@8.16.0/node_modules/acorn-typescript/lib/index.js";
3
+
1
4
  //#region src/mcp/validate-file.ts
2
- const llmPatterns = [
5
+ const LLM_METHOD_CHAINS = [
6
+ {
7
+ chain: [
8
+ "chat",
9
+ "completions",
10
+ "create"
11
+ ],
12
+ provider: "openai",
13
+ api: "chat.completions.create"
14
+ },
15
+ {
16
+ chain: [
17
+ "chat",
18
+ "completions",
19
+ "parse"
20
+ ],
21
+ provider: "openai",
22
+ api: "chat.completions.parse"
23
+ },
24
+ {
25
+ chain: ["responses", "create"],
26
+ provider: "openai",
27
+ api: "responses.create"
28
+ },
29
+ {
30
+ chain: ["messages", "create"],
31
+ provider: "anthropic",
32
+ api: "messages.create"
33
+ },
34
+ {
35
+ chain: ["generateContent"],
36
+ provider: "gemini",
37
+ api: "generateContent",
38
+ singleMethodHints: [
39
+ "gemini",
40
+ "google",
41
+ "generative",
42
+ "genai"
43
+ ]
44
+ },
45
+ {
46
+ chain: ["converse"],
47
+ provider: "bedrock",
48
+ api: "converse",
49
+ singleMethodHints: [
50
+ "bedrock",
51
+ "boto",
52
+ "runtime"
53
+ ]
54
+ },
55
+ {
56
+ chain: ["chat", "complete"],
57
+ provider: "mistral",
58
+ api: "chat.complete"
59
+ },
60
+ {
61
+ chain: ["getChatCompletions"],
62
+ provider: "azure-openai",
63
+ api: "getChatCompletions",
64
+ singleMethodHints: ["azure", "openai"]
65
+ },
66
+ {
67
+ chain: ["chat"],
68
+ provider: "cohere",
69
+ api: "chat",
70
+ singleMethodHints: ["cohere", "CohereClient"]
71
+ },
72
+ {
73
+ chain: ["generate"],
74
+ provider: "cohere",
75
+ api: "generate",
76
+ singleMethodHints: ["cohere", "CohereClient"]
77
+ }
78
+ ];
79
+ const VERCEL_AI_SDK_FUNCTIONS = new Set([
80
+ "streamText",
81
+ "generateText",
82
+ "streamObject",
83
+ "generateObject"
84
+ ]);
85
+ const JS_KEYWORDS = new Set([
86
+ "if",
87
+ "else",
88
+ "for",
89
+ "while",
90
+ "do",
91
+ "switch",
92
+ "case",
93
+ "try",
94
+ "catch",
95
+ "finally",
96
+ "return",
97
+ "throw",
98
+ "new",
99
+ "delete",
100
+ "typeof",
101
+ "void",
102
+ "in",
103
+ "of",
104
+ "with",
105
+ "class",
106
+ "extends",
107
+ "super",
108
+ "import",
109
+ "export",
110
+ "default",
111
+ "break",
112
+ "continue",
113
+ "debugger",
114
+ "yield",
115
+ "await"
116
+ ]);
117
+ const PROVIDER_CONSTRUCTOR_NAMES = new Set([
118
+ "OpenAI",
119
+ "Anthropic",
120
+ "Gemini",
121
+ "AzureOpenAI",
122
+ "Bedrock",
123
+ "Mistral",
124
+ "GoogleGenerativeAI",
125
+ "GoogleGenAI",
126
+ "CohereClient"
127
+ ]);
128
+ function extractMethodChain(node) {
129
+ const parts = [];
130
+ let current = node;
131
+ while (current.type === "MemberExpression") {
132
+ const mem = current;
133
+ if (!mem.computed && mem.property.type === "Identifier") parts.push(mem.property.name);
134
+ current = mem.object;
135
+ }
136
+ parts.reverse();
137
+ return {
138
+ chain: parts,
139
+ receiver: current.type === "Identifier" ? current.name : null
140
+ };
141
+ }
142
+ function chainEndsWith(chain, pattern) {
143
+ if (chain.length < pattern.length) return false;
144
+ const offset = chain.length - pattern.length;
145
+ return pattern.every((p, i) => chain[offset + i] === p);
146
+ }
147
+ function hasSingleMethodHint(chain, receiver, hints) {
148
+ const tokens = [...chain.slice(0, -1)];
149
+ if (receiver) tokens.push(receiver);
150
+ return tokens.some((t) => hints.some((h) => t.toLowerCase().includes(h.toLowerCase())));
151
+ }
152
+ function getPropertyValue(obj, key) {
153
+ for (const prop of obj.properties) {
154
+ if (prop.type !== "Property") continue;
155
+ const p = prop;
156
+ if (p.key.type === "Identifier" && p.key.name === key) return p.value;
157
+ if (p.key.type === "Literal" && p.key.value === key) return p.value;
158
+ }
159
+ return null;
160
+ }
161
+ function hasKeywordArg(args, keyword) {
162
+ for (const arg of args) if (arg.type === "ObjectExpression") {
163
+ if (getPropertyValue(arg, keyword)) return true;
164
+ }
165
+ return false;
166
+ }
167
+ function extractCodeContext(sourceLines, lineNum, radius = 4) {
168
+ const idx = lineNum - 1;
169
+ const start = Math.max(0, idx - radius);
170
+ const end = Math.min(sourceLines.length - 1, idx + radius);
171
+ return sourceLines.slice(start, end + 1).map((l, i) => {
172
+ const ln = start + i + 1;
173
+ return `${ln === lineNum ? ">>>" : " "} ${ln}: ${l}`;
174
+ }).join("\n");
175
+ }
176
+ function walkAST(node, callback, ancestors = []) {
177
+ callback(node, ancestors);
178
+ const next = [...ancestors, node];
179
+ for (const key of Object.keys(node)) {
180
+ if (key === "type" || key === "start" || key === "end" || key === "loc") continue;
181
+ const val = node[key];
182
+ if (val && typeof val === "object") {
183
+ if (Array.isArray(val)) {
184
+ for (const item of val) if (item && typeof item === "object" && "type" in item) walkAST(item, callback, next);
185
+ } else if ("type" in val) walkAST(val, callback, next);
186
+ }
187
+ }
188
+ }
189
+ function findContainingFunctionAST(ancestors) {
190
+ for (let i = ancestors.length - 1; i >= 0; i--) {
191
+ const node = ancestors[i];
192
+ if (!node) continue;
193
+ if (node.type === "FunctionDeclaration") {
194
+ const name = node.id?.name;
195
+ if (name && !JS_KEYWORDS.has(name)) return name;
196
+ }
197
+ if (node.type === "FunctionExpression") {
198
+ const fn = node;
199
+ if (fn.id?.name && !JS_KEYWORDS.has(fn.id.name)) return fn.id.name;
200
+ }
201
+ if (node.type === "VariableDeclarator") {
202
+ const decl = node;
203
+ if (decl.id.type === "Identifier" && decl.init && (decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")) {
204
+ const name = decl.id.name;
205
+ if (!JS_KEYWORDS.has(name)) return name;
206
+ }
207
+ }
208
+ if (node.type === "MethodDefinition" || node.type === "PropertyDefinition") {
209
+ const key = node.key;
210
+ if (key?.type === "Identifier") {
211
+ const name = key.name;
212
+ if (!JS_KEYWORDS.has(name)) return name;
213
+ }
214
+ }
215
+ }
216
+ return null;
217
+ }
218
+ function analyzeWithAST(source, sourceLines) {
219
+ let parse = null;
220
+ try {
221
+ const acorn = require_acorn();
222
+ const tsModule = require_lib();
223
+ const tsPlugin = typeof tsModule === "function" ? tsModule : tsModule.tsPlugin ?? tsModule.default;
224
+ if (typeof tsPlugin !== "function") return null;
225
+ parse = (src, opts) => acorn.Parser.extend(tsPlugin()).parse(src, opts);
226
+ } catch {
227
+ return null;
228
+ }
229
+ let tree;
230
+ try {
231
+ if (!parse) return null;
232
+ tree = parse(source, {
233
+ sourceType: "module",
234
+ ecmaVersion: "latest",
235
+ locations: true,
236
+ allowImportExportEverywhere: true,
237
+ allowReturnOutsideFunction: true
238
+ });
239
+ } catch {
240
+ return null;
241
+ }
242
+ const root = tree;
243
+ const hasPatch = /\bpatch\s*\(\s*\{/.test(source);
244
+ const hasSessionContext = /\.session\s*\(/.test(source) || /\bsession\.run\s*\(/.test(source) || /\.runAs\s*\(/.test(source) || /\bcreateAmplitudeAIMiddleware\s*\(/.test(source);
245
+ const hasAmplitudeImport = /from\s+['"]@amplitude\/ai['"]/.test(source) || /require\s*\(\s*['"]@amplitude\/ai['"]\s*\)/.test(source) || /\bAmplitudeAI\b/.test(source);
246
+ const wrappedClients = /* @__PURE__ */ new Set();
247
+ const localImportedBindings = /* @__PURE__ */ new Set();
248
+ const localImportedFunctions = /* @__PURE__ */ new Set();
249
+ const isLocalSpecifier = (spec) => spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("@/") || spec.startsWith("~/");
250
+ walkAST(root, (node) => {
251
+ if (node.type === "ImportDeclaration") {
252
+ const decl = node;
253
+ const source$1 = decl.source;
254
+ if (!source$1 || source$1.type !== "Literal") return;
255
+ const specifier = source$1.value;
256
+ if (typeof specifier !== "string" || !isLocalSpecifier(specifier)) return;
257
+ const specifiers = decl.specifiers;
258
+ if (!specifiers) return;
259
+ for (const s of specifiers) {
260
+ const local = s.local;
261
+ if (local?.type === "Identifier") {
262
+ const name = local.name;
263
+ localImportedBindings.add(name);
264
+ localImportedFunctions.add(name);
265
+ }
266
+ }
267
+ }
268
+ if (node.type === "NewExpression") {
269
+ const ne = node;
270
+ const calleeName = ne.callee.type === "Identifier" ? ne.callee.name : null;
271
+ if (calleeName && PROVIDER_CONSTRUCTOR_NAMES.has(calleeName)) {
272
+ if (hasKeywordArg(ne.arguments, "amplitude")) {}
273
+ }
274
+ }
275
+ });
276
+ walkAST(root, (node) => {
277
+ if (node.type === "VariableDeclarator") {
278
+ const decl = node;
279
+ if (decl.id.type !== "Identifier" || !decl.init) return;
280
+ const varName = decl.id.name;
281
+ if (decl.init.type === "NewExpression") {
282
+ const ne = decl.init;
283
+ const calleeName = ne.callee.type === "Identifier" ? ne.callee.name : null;
284
+ if (calleeName && PROVIDER_CONSTRUCTOR_NAMES.has(calleeName) && hasKeywordArg(ne.arguments, "amplitude")) wrappedClients.add(varName);
285
+ }
286
+ if (decl.init.type === "CallExpression") {
287
+ const ce = decl.init;
288
+ if ((ce.callee.type === "Identifier" && ce.callee.name === "wrap" || ce.callee.type === "MemberExpression" && !ce.callee.computed && ce.callee.property.type === "Identifier" && ce.callee.property.name === "wrap") && hasAmplitudeImport) wrappedClients.add(varName);
289
+ if (hasAmplitudeImport && ce.callee.type === "Identifier") {
290
+ const fnName = ce.callee.name;
291
+ if (localImportedFunctions.has(fnName)) localImportedBindings.add(varName);
292
+ }
293
+ }
294
+ }
295
+ });
296
+ const callSites = [];
297
+ const assistantsApiRe = /\.beta\.(?:threads|assistants)\./;
298
+ const isReceiverInstrumented = (receiver) => {
299
+ if (hasPatch) return true;
300
+ if (receiver !== null && wrappedClients.has(receiver)) return true;
301
+ if (hasAmplitudeImport && hasSessionContext && receiver !== null && localImportedBindings.has(receiver)) return true;
302
+ return false;
303
+ };
304
+ walkAST(root, (node, ancestors) => {
305
+ if (node.type !== "CallExpression") return;
306
+ const ce = node;
307
+ const lineNum = node.loc?.start.line ?? 0;
308
+ if (lineNum === 0) return;
309
+ if (ce.callee.type === "Identifier") {
310
+ const fnName = ce.callee.name;
311
+ if (VERCEL_AI_SDK_FUNCTIONS.has(fnName)) callSites.push({
312
+ line: lineNum,
313
+ provider: "vercel-ai-sdk",
314
+ api: fnName,
315
+ instrumented: hasPatch,
316
+ containing_function: findContainingFunctionAST(ancestors),
317
+ code_context: extractCodeContext(sourceLines, lineNum)
318
+ });
319
+ return;
320
+ }
321
+ if (ce.callee.type === "MemberExpression") {
322
+ const mem = ce.callee;
323
+ if (!mem.computed && mem.property.type === "Identifier" && mem.property.name === "send" && ce.arguments.length > 0 && ce.arguments[0]?.type === "NewExpression") {
324
+ const newExpr = ce.arguments[0];
325
+ const cmdName = newExpr.callee.type === "Identifier" ? newExpr.callee.name : null;
326
+ if (cmdName === "InvokeModelCommand" || cmdName === "InvokeModelWithResponseStreamCommand") {
327
+ const receiver = mem.object.type === "Identifier" ? mem.object.name : null;
328
+ callSites.push({
329
+ line: lineNum,
330
+ provider: "bedrock",
331
+ api: cmdName === "InvokeModelWithResponseStreamCommand" ? "invokeModelWithResponseStream" : "invokeModel",
332
+ instrumented: isReceiverInstrumented(receiver),
333
+ containing_function: findContainingFunctionAST(ancestors),
334
+ code_context: extractCodeContext(sourceLines, lineNum)
335
+ });
336
+ return;
337
+ }
338
+ if (cmdName === "ConverseCommand" || cmdName === "ConverseStreamCommand") {
339
+ const receiver = mem.object.type === "Identifier" ? mem.object.name : null;
340
+ callSites.push({
341
+ line: lineNum,
342
+ provider: "bedrock",
343
+ api: cmdName === "ConverseStreamCommand" ? "converseStream" : "converse",
344
+ instrumented: isReceiverInstrumented(receiver),
345
+ containing_function: findContainingFunctionAST(ancestors),
346
+ code_context: extractCodeContext(sourceLines, lineNum)
347
+ });
348
+ return;
349
+ }
350
+ }
351
+ }
352
+ if (ce.callee.type === "MemberExpression") {
353
+ const { chain, receiver } = extractMethodChain(ce.callee);
354
+ for (const pattern of LLM_METHOD_CHAINS) {
355
+ if (!chainEndsWith(chain, pattern.chain)) continue;
356
+ if (pattern.chain.length === 1 && pattern.singleMethodHints) {
357
+ if (!hasSingleMethodHint(chain, receiver, pattern.singleMethodHints)) continue;
358
+ }
359
+ let effectiveProvider = pattern.provider;
360
+ let effectiveApi = pattern.api;
361
+ if (pattern.provider === "anthropic") {
362
+ const lineText = sourceLines[lineNum - 1] ?? "";
363
+ if (assistantsApiRe.test(lineText) || chain.some((p) => p === "beta" || p === "threads")) {
364
+ effectiveProvider = "openai-assistants";
365
+ effectiveApi = "beta.threads.messages.create";
366
+ }
367
+ }
368
+ callSites.push({
369
+ line: lineNum,
370
+ provider: effectiveProvider,
371
+ api: effectiveApi,
372
+ instrumented: isReceiverInstrumented(receiver),
373
+ containing_function: findContainingFunctionAST(ancestors),
374
+ code_context: extractCodeContext(sourceLines, lineNum)
375
+ });
376
+ break;
377
+ }
378
+ }
379
+ });
380
+ const toolDefinitions = [];
381
+ walkAST(root, (node) => {
382
+ if (node.type !== "ObjectExpression") return;
383
+ const obj = node;
384
+ const typeVal = getPropertyValue(obj, "type");
385
+ if (!typeVal || typeVal.type !== "Literal" || typeVal.value !== "function") return;
386
+ const fnVal = getPropertyValue(obj, "function");
387
+ if (!fnVal || fnVal.type !== "ObjectExpression") return;
388
+ const nameVal = getPropertyValue(fnVal, "name");
389
+ if (nameVal?.type === "Literal" && typeof nameVal.value === "string") {
390
+ const name = nameVal.value;
391
+ if (!toolDefinitions.includes(name)) toolDefinitions.push(name);
392
+ }
393
+ });
394
+ walkAST(root, (node) => {
395
+ if (node.type !== "ObjectExpression") return;
396
+ const obj = node;
397
+ const nameVal = getPropertyValue(obj, "name");
398
+ if (!nameVal || nameVal.type !== "Literal" || typeof nameVal.value !== "string") return;
399
+ const hasInputSchema = getPropertyValue(obj, "input_schema") !== null;
400
+ const hasParameters = getPropertyValue(obj, "parameters") !== null;
401
+ if (!hasInputSchema && !hasParameters) return;
402
+ const name = nameVal.value;
403
+ if (!toolDefinitions.includes(name)) toolDefinitions.push(name);
404
+ });
405
+ const functionDefinitions = [];
406
+ walkAST(root, (node) => {
407
+ if (node.type === "FunctionDeclaration") {
408
+ const name = node.id?.name;
409
+ if (name && !functionDefinitions.includes(name)) functionDefinitions.push(name);
410
+ }
411
+ if (node.type === "VariableDeclarator") {
412
+ const decl = node;
413
+ if (decl.id.type === "Identifier" && decl.init && (decl.init.type === "ArrowFunctionExpression" || decl.init.type === "FunctionExpression")) {
414
+ const name = decl.id.name;
415
+ if (!functionDefinitions.includes(name)) functionDefinitions.push(name);
416
+ }
417
+ }
418
+ });
419
+ const uninstrumentedSites = callSites.filter((s) => !s.instrumented);
420
+ let suggestions;
421
+ if (uninstrumentedSites.length > 0) suggestions = [
422
+ "Now: instrument provider calls with wrapper/swap import (for example, `new OpenAI({ amplitude: ai, apiKey })`).",
423
+ "Next: add session lineage with `const session = ai.agent(...).session(...)` and wrap calls in `session.run(...)`.",
424
+ "Why: session context unlocks session-level scoring, enrichments, and reliable product-to-AI funnels.",
425
+ "Content tiers: choose `contentMode` (`full`, `metadata_only`, or `customer_enriched`) and prefer `redactPii: true` when using `full`.",
426
+ "Fallback: use `patch({ amplitudeAI: ai })` for migration speed, then graduate to wrapper + session context."
427
+ ];
428
+ else if (callSites.length > 0 && !hasSessionContext) suggestions = [
429
+ "Tracking is present, but session context is missing.",
430
+ "Now: add `const session = ai.agent(...).session(...)` and execute LLM calls inside `session.run(...)` (or use middleware).",
431
+ "Why: without session lineage you lose high-value outcomes like session enrichments, scoring, and dependable session funnels.",
432
+ "Content tiers: set `contentMode` intentionally and keep `redactPii: true` when using `full`."
433
+ ];
434
+ else if (callSites.length > 0) suggestions = ["File appears instrumented with session lineage.", "For privacy-by-default, set `contentMode` intentionally and enable `redactPii: true` when using `full`."];
435
+ else suggestions = ["No supported LLM call sites detected in this file.", "If this file should emit AI telemetry, add wrapped provider calls and session context."];
436
+ return {
437
+ total_call_sites: callSites.length,
438
+ instrumented: callSites.length - uninstrumentedSites.length,
439
+ uninstrumented: uninstrumentedSites.length,
440
+ has_amplitude_import: hasAmplitudeImport,
441
+ has_session_context: hasSessionContext,
442
+ call_sites: callSites,
443
+ suggestions,
444
+ tool_definitions: toolDefinitions,
445
+ function_definitions: functionDefinitions
446
+ };
447
+ }
448
+ const regexLlmPatterns = [
3
449
  {
4
450
  pattern: /\.chat\.completions\.create\s*\(/g,
5
451
  receiverRe: /(\w+)(?:\.\w+)*\.chat\.completions\.create\s*\(/,
@@ -41,11 +487,59 @@ const llmPatterns = [
41
487
  receiverRe: /(\w+)(?:\.\w+)*\.send\s*\(/,
42
488
  provider: "bedrock",
43
489
  api: "converse"
490
+ },
491
+ {
492
+ pattern: /\.send\s*\(\s*new\s+InvokeModelWithResponseStreamCommand\s*\(/g,
493
+ receiverRe: /(\w+)(?:\.\w+)*\.send\s*\(/,
494
+ provider: "bedrock",
495
+ api: "invokeModelWithResponseStream"
496
+ },
497
+ {
498
+ pattern: /\.send\s*\(\s*new\s+ConverseStreamCommand\s*\(/g,
499
+ receiverRe: /(\w+)(?:\.\w+)*\.send\s*\(/,
500
+ provider: "bedrock",
501
+ api: "converseStream"
502
+ },
503
+ {
504
+ pattern: /\.chat\.complete\s*\(/g,
505
+ receiverRe: /(\w+)(?:\.\w+)*\.chat\.complete\s*\(/,
506
+ provider: "mistral",
507
+ api: "chat.complete"
508
+ },
509
+ {
510
+ pattern: /\.getChatCompletions\s*\(/g,
511
+ receiverRe: /(\w+)(?:\.\w+)*\.getChatCompletions\s*\(/,
512
+ provider: "azure-openai",
513
+ api: "getChatCompletions"
514
+ },
515
+ {
516
+ pattern: /\bstreamText\s*\(/g,
517
+ receiverRe: null,
518
+ provider: "vercel-ai-sdk",
519
+ api: "streamText"
520
+ },
521
+ {
522
+ pattern: /\bgenerateText\s*\(/g,
523
+ receiverRe: null,
524
+ provider: "vercel-ai-sdk",
525
+ api: "generateText"
526
+ },
527
+ {
528
+ pattern: /\bstreamObject\s*\(/g,
529
+ receiverRe: null,
530
+ provider: "vercel-ai-sdk",
531
+ api: "streamObject"
532
+ },
533
+ {
534
+ pattern: /\bgenerateObject\s*\(/g,
535
+ receiverRe: null,
536
+ provider: "vercel-ai-sdk",
537
+ api: "generateObject"
44
538
  }
45
539
  ];
46
- function findWrappedConstructors(source) {
540
+ function findWrappedConstructorsRegex(source) {
47
541
  const result = /* @__PURE__ */ new Set();
48
- for (const m of source.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*new\s+(?:OpenAI|Anthropic|Gemini|AzureOpenAI|Bedrock|Mistral)\s*\(/g)) {
542
+ for (const m of source.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*new\s+(?:OpenAI|Anthropic|Gemini|AzureOpenAI|Bedrock|Mistral|GoogleGenerativeAI|GoogleGenAI|CohereClient)\s*\(/g)) {
49
543
  const varName = m[1] ?? "";
50
544
  if (!varName) continue;
51
545
  let depth = 1;
@@ -62,33 +556,80 @@ function findWrappedConstructors(source) {
62
556
  }
63
557
  return result;
64
558
  }
65
- function analyzeFileInstrumentation(source) {
559
+ function findContainingFunctionRegex(lines, lineIndex) {
560
+ for (let i = lineIndex; i >= 0; i--) {
561
+ const line = lines[i] ?? "";
562
+ const fnDeclMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/);
563
+ if (fnDeclMatch) {
564
+ const name = fnDeclMatch[1] ?? "";
565
+ if (name && !JS_KEYWORDS.has(name)) return name;
566
+ }
567
+ const arrowMatch = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(/);
568
+ if (arrowMatch) {
569
+ const name = arrowMatch[1] ?? "";
570
+ if (name && !JS_KEYWORDS.has(name)) return name;
571
+ }
572
+ }
573
+ return null;
574
+ }
575
+ function findToolDefinitionsRegex(source) {
576
+ const tools = [];
577
+ for (const m of source.matchAll(/function:\s*\{[^}]*name:\s*['"](\w+)['"]/g)) {
578
+ const name = m[1] ?? "";
579
+ if (name && !tools.includes(name)) tools.push(name);
580
+ }
581
+ for (const m of source.matchAll(/name:\s*['"](\w+)['"][^}]*input_schema\s*:/g)) {
582
+ const name = m[1] ?? "";
583
+ if (name && !tools.includes(name)) tools.push(name);
584
+ }
585
+ return tools;
586
+ }
587
+ function findFunctionDefinitionsRegex(source) {
588
+ const fns = [];
589
+ for (const m of source.matchAll(/(?:async\s+)?function\s+(\w+)|(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[^=])\s*=>/g)) {
590
+ const name = m[1] ?? m[2] ?? "";
591
+ if (name && !fns.includes(name)) fns.push(name);
592
+ }
593
+ return fns;
594
+ }
595
+ function analyzeWithRegex(source) {
66
596
  const hasPatch = /\bpatch\s*\(\s*\{/.test(source);
67
597
  const hasSessionContext = /\.session\s*\(/.test(source) || /\bsession\.run\s*\(/.test(source) || /\.runAs\s*\(/.test(source) || /\bcreateAmplitudeAIMiddleware\s*\(/.test(source);
68
598
  const hasAmplitudeImport = /from\s+['"]@amplitude\/ai['"]/.test(source) || /require\s*\(\s*['"]@amplitude\/ai['"]\s*\)/.test(source) || /\bAmplitudeAI\b/.test(source);
69
- const wrappedClients = findWrappedConstructors(source);
599
+ const wrappedClients = findWrappedConstructorsRegex(source);
70
600
  if (hasAmplitudeImport) for (const m of source.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*(?:\w+\.)*wrap\s*\(/g)) {
71
601
  const name = m[1] ?? "";
72
602
  if (name) wrappedClients.add(name);
73
603
  }
74
604
  const callSites = [];
75
605
  const lines = source.split("\n");
606
+ const assistantsApiRe = /\.beta\.(?:threads|assistants)\./;
76
607
  for (let i = 0; i < lines.length; i++) {
77
608
  const line = lines[i] ?? "";
78
- for (const { pattern, receiverRe, provider, api } of llmPatterns) {
609
+ for (const { pattern, receiverRe, provider, api } of regexLlmPatterns) {
79
610
  pattern.lastIndex = 0;
80
611
  if (pattern.test(line)) {
81
- const receiver = line.match(receiverRe)?.[1] ?? "";
612
+ let effectiveProvider = provider;
613
+ let effectiveApi = api;
614
+ if (provider === "anthropic" && assistantsApiRe.test(line)) {
615
+ effectiveProvider = "openai-assistants";
616
+ effectiveApi = "beta.threads.messages.create";
617
+ }
618
+ const receiver = (receiverRe ? line.match(receiverRe) : null)?.[1] ?? "";
82
619
  const instrumented = hasPatch || wrappedClients.has(receiver);
83
620
  callSites.push({
84
621
  line: i + 1,
85
- provider,
86
- api,
87
- instrumented
622
+ provider: effectiveProvider,
623
+ api: effectiveApi,
624
+ instrumented,
625
+ containing_function: findContainingFunctionRegex(lines, i),
626
+ code_context: extractCodeContext(lines, i + 1)
88
627
  });
89
628
  }
90
629
  }
91
630
  }
631
+ const toolDefs = findToolDefinitionsRegex(source);
632
+ const fnDefs = findFunctionDefinitionsRegex(source);
92
633
  const uninstrumentedSites = callSites.filter((s) => !s.instrumented);
93
634
  let suggestions;
94
635
  if (uninstrumentedSites.length > 0) suggestions = [
@@ -113,9 +654,16 @@ function analyzeFileInstrumentation(source) {
113
654
  has_amplitude_import: hasAmplitudeImport,
114
655
  has_session_context: hasSessionContext,
115
656
  call_sites: callSites,
116
- suggestions
657
+ suggestions,
658
+ tool_definitions: toolDefs,
659
+ function_definitions: fnDefs
117
660
  };
118
661
  }
662
+ function analyzeFileInstrumentation(source) {
663
+ const astResult = analyzeWithAST(source, source.split("\n"));
664
+ if (astResult) return astResult;
665
+ return analyzeWithRegex(source);
666
+ }
119
667
 
120
668
  //#endregion
121
669
  export { analyzeFileInstrumentation };