@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
@@ -0,0 +1,25 @@
1
+ //#region src/mcp/generate-verify-test.ts
2
+ function generateVerifyTest(scanResult) {
3
+ const lines = [];
4
+ lines.push("import { describe, it, expect } from 'vitest';", "import { MockAmplitudeAI } from '@amplitude/ai/testing';", "", "describe('Amplitude AI verification', () => {");
5
+ for (const agent of scanResult.agents) lines.push(` it('${agent.inferred_id} emits correct event sequence', () => {`, " const mock = new MockAmplitudeAI();", ` const agent = mock.agent('${agent.inferred_id}');`, " const session = agent.session({ userId: 'verify-user', sessionId: 'verify-session' });", " session.runSync((s) => {", " s.trackUserMessage('test message');", " });", " const events = mock.getEvents();", " expect(events.length).toBeGreaterThanOrEqual(2);", ` mock.assertEventTracked('[Agent] User Message', { '[Agent] Agent ID': '${agent.inferred_id}' });`, " mock.assertSessionClosed('verify-session');", " });", "");
6
+ if ((scanResult.is_multi_agent || scanResult.multi_agent_signals.length > 0) && scanResult.agents.length >= 2) {
7
+ const parentAgent = scanResult.agents[0];
8
+ const childAgent = scanResult.agents[1];
9
+ if (parentAgent && childAgent) {
10
+ lines.push(" it('multi-agent delegation via runAs', () => {", " const mock = new MockAmplitudeAI();", ` const parent = mock.agent('${parentAgent.inferred_id}');`, ` const child = parent.child('${childAgent.inferred_id}');`, " const session = parent.session({ userId: 'verify-user', sessionId: 'verify-multi' });", " session.runSync((s) => {", " s.runAsSync(child, (cs) => {", " cs.trackUserMessage('delegated task');", " });", " });", ` const childEvents = mock.eventsForAgent('${childAgent.inferred_id}');`, " expect(childEvents.length).toBeGreaterThan(0);", " expect(childEvents[0].event_properties?.['[Agent] Session ID']).toBe('verify-multi');", " });", "");
11
+ const childAgents = scanResult.agents.slice(0, 2);
12
+ lines.push(" it('parallel fan-out shares session', async () => {", " const mock = new MockAmplitudeAI();", ` const orchestrator = mock.agent('${parentAgent.inferred_id}');`, " const children = [");
13
+ for (const child of childAgents) lines.push(` orchestrator.child('${child.inferred_id}'),`);
14
+ lines.push(" ];", " const session = orchestrator.session({ userId: 'verify-user', sessionId: 'verify-parallel' });", " await session.run(async (s) => {", " await Promise.all(children.map((child) =>", " s.runAs(child, async (cs) => {", " cs.trackUserMessage('parallel task');", " }),", " ));", " });", " for (const child of children) {", " const events = mock.eventsForAgent(child.agentId);", " expect(events.length).toBeGreaterThan(0);", " expect(events[0].event_properties?.['[Agent] Session ID']).toBe('verify-parallel');", " }", " });", "");
15
+ lines.push(" it('tool calls inside runAs are attributed to child agent', () => {", " const mock = new MockAmplitudeAI();", ` const parent = mock.agent('${parentAgent.inferred_id}');`, ` const child = parent.child('${childAgent.inferred_id}');`, " const session = parent.session({ userId: 'verify-user', sessionId: 'verify-tools' });", " session.runSync((s) => {", " s.runAsSync(child, (cs) => {", " cs.trackToolCall('search_knowledge_base', 150, true);", " });", " });", " const toolEvents = mock.getEvents().filter(e => e.event_type === '[Agent] Tool Call');", " expect(toolEvents.length).toBe(1);", ` expect(toolEvents[0].event_properties?.['[Agent] Agent ID']).toBe('${childAgent.inferred_id}');`, ` expect(toolEvents[0].event_properties?.['[Agent] Parent Agent ID']).toBe('${parentAgent.inferred_id}');`, " });", "");
16
+ lines.push(" it('runAs restores parent context after child throws', () => {", " const mock = new MockAmplitudeAI();", ` const parent = mock.agent('${parentAgent.inferred_id}');`, ` const faultyChild = parent.child('${childAgent.inferred_id}-faulty');`, " const session = parent.session({ userId: 'verify-user', sessionId: 'verify-error' });", " session.runSync((s) => {", " try {", " s.runAsSync(faultyChild, () => {", " throw new Error('child failed');", " });", " } catch {", " s.trackUserMessage('recovering from child failure');", " }", " });", ` const parentEvents = mock.eventsForAgent('${parentAgent.inferred_id}');`, " const recoveryMsg = parentEvents.find(e => e.event_type === '[Agent] User Message');", " expect(recoveryMsg).toBeDefined();", ` expect(recoveryMsg?.event_properties?.['[Agent] Agent ID']).toBe('${parentAgent.inferred_id}');`, " });", "");
17
+ }
18
+ }
19
+ lines.push("});", "");
20
+ return lines.join("\n");
21
+ }
22
+
23
+ //#endregion
24
+ export { generateVerifyTest };
25
+ //# sourceMappingURL=generate-verify-test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-verify-test.js","names":["lines: string[]"],"sources":["../../src/mcp/generate-verify-test.ts"],"sourcesContent":["import type { ScanResult } from './scan-project.js';\n\nexport function generateVerifyTest(scanResult: ScanResult): string {\n const lines: string[] = [];\n\n lines.push(\n \"import { describe, it, expect } from 'vitest';\",\n \"import { MockAmplitudeAI } from '@amplitude/ai/testing';\",\n '',\n \"describe('Amplitude AI verification', () => {\",\n );\n\n for (const agent of scanResult.agents) {\n lines.push(\n ` it('${agent.inferred_id} emits correct event sequence', () => {`,\n ' const mock = new MockAmplitudeAI();',\n ` const agent = mock.agent('${agent.inferred_id}');`,\n \" const session = agent.session({ userId: 'verify-user', sessionId: 'verify-session' });\",\n ' session.runSync((s) => {',\n \" s.trackUserMessage('test message');\",\n ' });',\n ' const events = mock.getEvents();',\n ' expect(events.length).toBeGreaterThanOrEqual(2);',\n ` mock.assertEventTracked('[Agent] User Message', { '[Agent] Agent ID': '${agent.inferred_id}' });`,\n \" mock.assertSessionClosed('verify-session');\",\n ' });',\n '',\n );\n }\n\n const shouldEmitMultiAgentTests =\n (scanResult.is_multi_agent || scanResult.multi_agent_signals.length > 0) &&\n scanResult.agents.length >= 2;\n\n if (shouldEmitMultiAgentTests) {\n const parentAgent = scanResult.agents[0];\n const childAgent = scanResult.agents[1];\n\n if (parentAgent && childAgent) {\n lines.push(\n \" it('multi-agent delegation via runAs', () => {\",\n ' const mock = new MockAmplitudeAI();',\n ` const parent = mock.agent('${parentAgent.inferred_id}');`,\n ` const child = parent.child('${childAgent.inferred_id}');`,\n \" const session = parent.session({ userId: 'verify-user', sessionId: 'verify-multi' });\",\n ' session.runSync((s) => {',\n ' s.runAsSync(child, (cs) => {',\n \" cs.trackUserMessage('delegated task');\",\n ' });',\n ' });',\n ` const childEvents = mock.eventsForAgent('${childAgent.inferred_id}');`,\n ' expect(childEvents.length).toBeGreaterThan(0);',\n \" expect(childEvents[0].event_properties?.['[Agent] Session ID']).toBe('verify-multi');\",\n ' });',\n '',\n );\n\n const childAgents = scanResult.agents.slice(0, 2);\n lines.push(\n \" it('parallel fan-out shares session', async () => {\",\n ' const mock = new MockAmplitudeAI();',\n ` const orchestrator = mock.agent('${parentAgent.inferred_id}');`,\n ' const children = [',\n );\n for (const child of childAgents) {\n lines.push(\n ` orchestrator.child('${child.inferred_id}'),`,\n );\n }\n lines.push(\n ' ];',\n \" const session = orchestrator.session({ userId: 'verify-user', sessionId: 'verify-parallel' });\",\n ' await session.run(async (s) => {',\n ' await Promise.all(children.map((child) =>',\n ' s.runAs(child, async (cs) => {',\n \" cs.trackUserMessage('parallel task');\",\n ' }),',\n ' ));',\n ' });',\n ' for (const child of children) {',\n ' const events = mock.eventsForAgent(child.agentId);',\n ' expect(events.length).toBeGreaterThan(0);',\n \" expect(events[0].event_properties?.['[Agent] Session ID']).toBe('verify-parallel');\",\n ' }',\n ' });',\n '',\n );\n\n lines.push(\n \" it('tool calls inside runAs are attributed to child agent', () => {\",\n ' const mock = new MockAmplitudeAI();',\n ` const parent = mock.agent('${parentAgent.inferred_id}');`,\n ` const child = parent.child('${childAgent.inferred_id}');`,\n \" const session = parent.session({ userId: 'verify-user', sessionId: 'verify-tools' });\",\n ' session.runSync((s) => {',\n ' s.runAsSync(child, (cs) => {',\n \" cs.trackToolCall('search_knowledge_base', 150, true);\",\n ' });',\n ' });',\n \" const toolEvents = mock.getEvents().filter(e => e.event_type === '[Agent] Tool Call');\",\n ' expect(toolEvents.length).toBe(1);',\n ` expect(toolEvents[0].event_properties?.['[Agent] Agent ID']).toBe('${childAgent.inferred_id}');`,\n ` expect(toolEvents[0].event_properties?.['[Agent] Parent Agent ID']).toBe('${parentAgent.inferred_id}');`,\n ' });',\n '',\n );\n\n lines.push(\n \" it('runAs restores parent context after child throws', () => {\",\n ' const mock = new MockAmplitudeAI();',\n ` const parent = mock.agent('${parentAgent.inferred_id}');`,\n ` const faultyChild = parent.child('${childAgent.inferred_id}-faulty');`,\n \" const session = parent.session({ userId: 'verify-user', sessionId: 'verify-error' });\",\n ' session.runSync((s) => {',\n ' try {',\n ' s.runAsSync(faultyChild, () => {',\n \" throw new Error('child failed');\",\n ' });',\n ' } catch {',\n \" s.trackUserMessage('recovering from child failure');\",\n ' }',\n ' });',\n ` const parentEvents = mock.eventsForAgent('${parentAgent.inferred_id}');`,\n \" const recoveryMsg = parentEvents.find(e => e.event_type === '[Agent] User Message');\",\n ' expect(recoveryMsg).toBeDefined();',\n ` expect(recoveryMsg?.event_properties?.['[Agent] Agent ID']).toBe('${parentAgent.inferred_id}');`,\n ' });',\n '',\n );\n }\n }\n\n lines.push('});', '');\n\n return lines.join('\\n');\n}\n"],"mappings":";AAEA,SAAgB,mBAAmB,YAAgC;CACjE,MAAMA,QAAkB,EAAE;AAE1B,OAAM,KACJ,kDACA,4DACA,IACA,gDACD;AAED,MAAK,MAAM,SAAS,WAAW,OAC7B,OAAM,KACJ,SAAS,MAAM,YAAY,0CAC3B,2CACA,iCAAiC,MAAM,YAAY,MACnD,8FACA,gCACA,6CACA,WACA,wCACA,wDACA,8EAA8E,MAAM,YAAY,QAChG,mDACA,SACA,GACD;AAOH,MAHG,WAAW,kBAAkB,WAAW,oBAAoB,SAAS,MACtE,WAAW,OAAO,UAAU,GAEC;EAC7B,MAAM,cAAc,WAAW,OAAO;EACtC,MAAM,aAAa,WAAW,OAAO;AAErC,MAAI,eAAe,YAAY;AAC7B,SAAM,KACJ,oDACA,2CACA,kCAAkC,YAAY,YAAY,MAC1D,mCAAmC,WAAW,YAAY,MAC1D,6FACA,gCACA,sCACA,kDACA,aACA,WACA,gDAAgD,WAAW,YAAY,MACvE,sDACA,6FACA,SACA,GACD;GAED,MAAM,cAAc,WAAW,OAAO,MAAM,GAAG,EAAE;AACjD,SAAM,KACJ,yDACA,2CACA,wCAAwC,YAAY,YAAY,MAChE,yBACD;AACD,QAAK,MAAM,SAAS,YAClB,OAAM,KACJ,6BAA6B,MAAM,YAAY,KAChD;AAEH,SAAM,KACJ,UACA,sGACA,wCACA,mDACA,0CACA,mDACA,eACA,aACA,WACA,uCACA,4DACA,mDACA,6FACA,SACA,SACA,GACD;AAED,SAAM,KACJ,yEACA,2CACA,kCAAkC,YAAY,YAAY,MAC1D,mCAAmC,WAAW,YAAY,MAC1D,6FACA,gCACA,sCACA,iEACA,aACA,WACA,8FACA,0CACA,0EAA0E,WAAW,YAAY,MACjG,iFAAiF,YAAY,YAAY,MACzG,SACA,GACD;AAED,SAAM,KACJ,oEACA,2CACA,kCAAkC,YAAY,YAAY,MAC1D,yCAAyC,WAAW,YAAY,aAChE,6FACA,gCACA,eACA,4CACA,8CACA,eACA,mBACA,gEACA,WACA,WACA,iDAAiD,YAAY,YAAY,MACzE,4FACA,0CACA,yEAAyE,YAAY,YAAY,MACjG,SACA,GACD;;;AAIL,OAAM,KAAK,OAAO,GAAG;AAErB,QAAO,MAAM,KAAK,KAAK"}
@@ -1,4 +1,5 @@
1
1
  import { GENERATED_FILES, MCP_PROMPTS, MCP_RESOURCES, MCP_SERVER_NAME, MCP_TOOLS, MToolName } from "./contract.js";
2
+ import { ScanResult, scanProject } from "./scan-project.js";
2
3
  import { IntegrationPattern, getIntegrationPatterns } from "./patterns.js";
3
4
  import { buildInstrumentationGuidance, createServer, runMcpServer } from "./server.js";
4
- export { GENERATED_FILES, IntegrationPattern, MCP_PROMPTS, MCP_RESOURCES, MCP_SERVER_NAME, MCP_TOOLS, MToolName, buildInstrumentationGuidance, createServer, getIntegrationPatterns, runMcpServer };
5
+ export { GENERATED_FILES, IntegrationPattern, MCP_PROMPTS, MCP_RESOURCES, MCP_SERVER_NAME, MCP_TOOLS, MToolName, type ScanResult, buildInstrumentationGuidance, createServer, getIntegrationPatterns, runMcpServer, scanProject };
package/dist/mcp/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { GENERATED_FILES, MCP_PROMPTS, MCP_RESOURCES, MCP_SERVER_NAME, MCP_TOOLS } from "./contract.js";
2
2
  import { getIntegrationPatterns } from "./patterns.js";
3
+ import { scanProject } from "./scan-project.js";
3
4
  import { buildInstrumentationGuidance, createServer, runMcpServer } from "./server.js";
4
5
 
5
- export { GENERATED_FILES, MCP_PROMPTS, MCP_RESOURCES, MCP_SERVER_NAME, MCP_TOOLS, buildInstrumentationGuidance, createServer, getIntegrationPatterns, runMcpServer };
6
+ export { GENERATED_FILES, MCP_PROMPTS, MCP_RESOURCES, MCP_SERVER_NAME, MCP_TOOLS, buildInstrumentationGuidance, createServer, getIntegrationPatterns, runMcpServer, scanProject };
@@ -0,0 +1,14 @@
1
+ //#region src/mcp/instrument-file.d.ts
2
+ interface InstrumentFileOptions {
3
+ source: string;
4
+ filePath: string;
5
+ tier: 'quick_start' | 'standard' | 'advanced';
6
+ bootstrapImportPath: string;
7
+ agentId: string;
8
+ description?: string | null;
9
+ providers: string[];
10
+ }
11
+ declare function instrumentFile(opts: InstrumentFileOptions): string;
12
+ //#endregion
13
+ export { InstrumentFileOptions, instrumentFile };
14
+ //# sourceMappingURL=instrument-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrument-file.d.ts","names":[],"sources":["../../src/mcp/instrument-file.ts"],"sourcesContent":[],"mappings":";UAAiB,qBAAA;EAAA,MAAA,EAAA,MAAA;EA0KD,QAAA,EAAA,MAAA;;;;;;;iBAAA,cAAA,OAAqB"}
@@ -0,0 +1,139 @@
1
+ //#region src/mcp/instrument-file.ts
2
+ const PROVIDER_IMPORT_MAP = {
3
+ openai: {
4
+ module: "openai",
5
+ defaultExport: "OpenAI",
6
+ namedExport: "openai"
7
+ },
8
+ "@anthropic-ai/sdk": {
9
+ module: "@anthropic-ai/sdk",
10
+ defaultExport: "Anthropic",
11
+ namedExport: "anthropic"
12
+ },
13
+ "@google/generative-ai": {
14
+ module: "@google/generative-ai",
15
+ defaultExport: "GoogleGenerativeAI",
16
+ namedExport: "gemini"
17
+ },
18
+ "@google/genai": {
19
+ module: "@google/genai",
20
+ defaultExport: "GoogleGenAI",
21
+ namedExport: "genai"
22
+ },
23
+ "@mistralai/mistralai": {
24
+ module: "@mistralai/mistralai",
25
+ defaultExport: "Mistral",
26
+ namedExport: "mistral"
27
+ },
28
+ "@azure/openai": {
29
+ module: "@azure/openai",
30
+ defaultExport: "AzureOpenAI",
31
+ namedExport: "azureOpenai"
32
+ },
33
+ "cohere-ai": {
34
+ module: "cohere-ai",
35
+ defaultExport: "CohereClient",
36
+ namedExport: "cohere"
37
+ }
38
+ };
39
+ function matchConstructor(source, constructorName) {
40
+ const results = [];
41
+ const re = new RegExp(`new\\s+${constructorName}\\s*\\(`, "g");
42
+ for (const m of source.matchAll(re)) {
43
+ const openIdx = (m.index ?? 0) + m[0].length - 1;
44
+ let depth = 1;
45
+ let i = openIdx + 1;
46
+ while (i < source.length && depth > 0) {
47
+ if (source[i] === "(") depth++;
48
+ else if (source[i] === ")") depth--;
49
+ i++;
50
+ }
51
+ results.push({
52
+ start: m.index ?? 0,
53
+ end: i,
54
+ fullMatch: source.slice(m.index ?? 0, i)
55
+ });
56
+ }
57
+ return results;
58
+ }
59
+ const ROUTE_HANDLER_RE = /export\s+async\s+function\s+(?:POST|GET|PUT|DELETE)\b/;
60
+ const EXPRESS_HANDLER_RE = /(?:app|router)\.\s*(?:get|post|put|delete)\s*\(\s*['"][^'"]+['"]\s*,\s*(?:async\s+)?\(/;
61
+ const HONO_HANDLER_RE = /(?:app|router)\.\s*(?:get|post|put|delete)\s*\(\s*['"][^'"]+['"]\s*,\s*(?:async\s+)?\(\s*c\b/;
62
+ function replaceProviderImports(source, providers, bootstrapImportPath) {
63
+ let result = source;
64
+ const namedImports = [];
65
+ for (const provider of providers) {
66
+ const mapping = PROVIDER_IMPORT_MAP[provider];
67
+ if (!mapping) continue;
68
+ const defaultImportRe = /* @__PURE__ */ new RegExp(`import\\s+${mapping.defaultExport}\\s+from\\s+['"]${mapping.module}['"];?`);
69
+ const namedImportRe = /* @__PURE__ */ new RegExp(`import\\s*\\{\\s*${mapping.defaultExport}\\s*\\}\\s*from\\s+['"]${mapping.module}['"];?`);
70
+ if (defaultImportRe.test(result)) {
71
+ result = result.replace(defaultImportRe, "");
72
+ namedImports.push(mapping.namedExport);
73
+ } else if (namedImportRe.test(result)) {
74
+ result = result.replace(namedImportRe, "");
75
+ namedImports.push(mapping.namedExport);
76
+ }
77
+ const matches = matchConstructor(result, mapping.defaultExport);
78
+ for (let i = matches.length - 1; i >= 0; i--) {
79
+ const match = matches[i];
80
+ if (match) result = result.slice(0, match.start) + mapping.namedExport + result.slice(match.end);
81
+ }
82
+ }
83
+ if (namedImports.length > 0) result = `import { ${namedImports.join(", ")} } from '${bootstrapImportPath}';\n` + result;
84
+ return result;
85
+ }
86
+ function addSessionWrapping(source, agentId, bootstrapImportPath) {
87
+ let result = source;
88
+ const existingImportMatch = (/* @__PURE__ */ new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${bootstrapImportPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"]`)).exec(result);
89
+ if (existingImportMatch) {
90
+ const existingNames = existingImportMatch[1] ?? "";
91
+ if (!existingNames.split(",").map((s) => s.trim()).includes("ai")) {
92
+ const newNames = existingNames.trim() ? `ai, ${existingNames.trim()}` : "ai";
93
+ result = result.replace(existingImportMatch[0], `import { ${newNames} } from '${bootstrapImportPath}'`);
94
+ }
95
+ } else if (!result.includes(`from '${bootstrapImportPath}'`) && !result.includes(`from "${bootstrapImportPath}"`)) result = `import { ai } from '${bootstrapImportPath}';\n${result}`;
96
+ const agentLine = `const agent = ai.agent('${agentId}');\n`;
97
+ if (ROUTE_HANDLER_RE.test(result)) {
98
+ const handlerMatch = result.match(/export\s+async\s+function\s+(?:POST|GET|PUT|DELETE)\s*\([^)]*\)\s*\{/);
99
+ result = result.replace(/(export\s+async\s+function\s+(?:POST|GET|PUT|DELETE)\s*\([^)]*\)\s*\{)/, `$1\n ${agentLine.trim()}\n const { messages, userId, sessionId } = await req.json();\n return agent.session({ userId, sessionId }).run(async (s) => {`);
100
+ if (handlerMatch?.index != null) {
101
+ const openBraceIdx = result.indexOf("{", handlerMatch.index);
102
+ if (openBraceIdx >= 0) {
103
+ let depth = 1;
104
+ let i = openBraceIdx + 1;
105
+ while (i < result.length && depth > 0) {
106
+ if (result[i] === "{") depth++;
107
+ else if (result[i] === "}") depth--;
108
+ i++;
109
+ }
110
+ const closingBraceIdx = i - 1;
111
+ result = `${result.slice(0, closingBraceIdx)} });\n${result.slice(closingBraceIdx)}`;
112
+ }
113
+ }
114
+ } else if (EXPRESS_HANDLER_RE.test(result) || HONO_HANDLER_RE.test(result)) result = result.replace(/((?:app|router)\.\s*(?:get|post|put|delete)\s*\(\s*['"][^'"]+['"]\s*,\s*(?:async\s+)?\([^)]*\)\s*(?:=>)?\s*\{)/, `$1\n ${agentLine.trim()}\n return agent.session({ userId: 'todo-extract-user-id', sessionId: 'todo-extract-session-id' }).run(async (s) => {`);
115
+ return result;
116
+ }
117
+ function addUserMessageTracking(source) {
118
+ const match = /(const\s+\{[^}]*\}\s*=\s*(?:await\s+)?(?:req\.body|request\.json\(\)|await\s+request\.json\(\)))/.exec(source);
119
+ if (match) return source.replace(match[0], `${match[0]};\n // TODO: extract user message and call s.trackUserMessage(userMessage)`);
120
+ return source;
121
+ }
122
+ function addFlushBeforeReturn(source) {
123
+ return source.replace(/(\n)([ \t]*)(return\s+(?:Response\.json|new\s+Response|NextResponse|res\.json|res\.send)\s*\()/g, "$1$2await ai.flush();\n$2$3");
124
+ }
125
+ function instrumentFile(opts) {
126
+ if (opts.tier === "quick_start") return opts.source;
127
+ let result = opts.source;
128
+ result = replaceProviderImports(result, opts.providers, opts.bootstrapImportPath);
129
+ if (opts.tier === "advanced") {
130
+ result = addSessionWrapping(result, opts.agentId, opts.bootstrapImportPath);
131
+ result = addUserMessageTracking(result);
132
+ result = addFlushBeforeReturn(result);
133
+ }
134
+ return result;
135
+ }
136
+
137
+ //#endregion
138
+ export { instrumentFile };
139
+ //# sourceMappingURL=instrument-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrument-file.js","names":["PROVIDER_IMPORT_MAP: Record<string, { module: string; defaultExport: string; namedExport: string }>","results: Array<{ start: number; end: number; fullMatch: string }>","namedImports: string[]"],"sources":["../../src/mcp/instrument-file.ts"],"sourcesContent":["export interface InstrumentFileOptions {\n source: string;\n filePath: string;\n tier: 'quick_start' | 'standard' | 'advanced';\n bootstrapImportPath: string;\n agentId: string;\n description?: string | null;\n providers: string[];\n}\n\nconst PROVIDER_IMPORT_MAP: Record<string, { module: string; defaultExport: string; namedExport: string }> = {\n openai: { module: 'openai', defaultExport: 'OpenAI', namedExport: 'openai' },\n '@anthropic-ai/sdk': { module: '@anthropic-ai/sdk', defaultExport: 'Anthropic', namedExport: 'anthropic' },\n '@google/generative-ai': { module: '@google/generative-ai', defaultExport: 'GoogleGenerativeAI', namedExport: 'gemini' },\n '@google/genai': { module: '@google/genai', defaultExport: 'GoogleGenAI', namedExport: 'genai' },\n '@mistralai/mistralai': { module: '@mistralai/mistralai', defaultExport: 'Mistral', namedExport: 'mistral' },\n '@azure/openai': { module: '@azure/openai', defaultExport: 'AzureOpenAI', namedExport: 'azureOpenai' },\n 'cohere-ai': { module: 'cohere-ai', defaultExport: 'CohereClient', namedExport: 'cohere' },\n};\n\n// Balanced-paren constructor matcher: handles nested parens like new OpenAI({ apiKey: getKey() })\nfunction matchConstructor(source: string, constructorName: string): Array<{ start: number; end: number; fullMatch: string }> {\n const results: Array<{ start: number; end: number; fullMatch: string }> = [];\n const re = new RegExp(`new\\\\s+${constructorName}\\\\s*\\\\(`, 'g');\n for (const m of source.matchAll(re)) {\n const openIdx = (m.index ?? 0) + m[0].length - 1;\n let depth = 1;\n let i = openIdx + 1;\n while (i < source.length && depth > 0) {\n if (source[i] === '(') depth++;\n else if (source[i] === ')') depth--;\n i++;\n }\n results.push({ start: m.index ?? 0, end: i, fullMatch: source.slice(m.index ?? 0, i) });\n }\n return results;\n}\n\nconst ROUTE_HANDLER_RE =\n /export\\s+async\\s+function\\s+(?:POST|GET|PUT|DELETE)\\b/;\nconst EXPRESS_HANDLER_RE =\n /(?:app|router)\\.\\s*(?:get|post|put|delete)\\s*\\(\\s*['\"][^'\"]+['\"]\\s*,\\s*(?:async\\s+)?\\(/;\nconst HONO_HANDLER_RE =\n /(?:app|router)\\.\\s*(?:get|post|put|delete)\\s*\\(\\s*['\"][^'\"]+['\"]\\s*,\\s*(?:async\\s+)?\\(\\s*c\\b/;\n\nfunction replaceProviderImports(\n source: string,\n providers: string[],\n bootstrapImportPath: string,\n): string {\n let result = source;\n const namedImports: string[] = [];\n\n for (const provider of providers) {\n const mapping = PROVIDER_IMPORT_MAP[provider];\n if (!mapping) continue;\n\n // Match default import: import OpenAI from 'openai'\n const defaultImportRe = new RegExp(\n `import\\\\s+${mapping.defaultExport}\\\\s+from\\\\s+['\"]${mapping.module}['\"];?`,\n );\n // Match named import: import { AzureOpenAI } from '@azure/openai'\n const namedImportRe = new RegExp(\n `import\\\\s*\\\\{\\\\s*${mapping.defaultExport}\\\\s*\\\\}\\\\s*from\\\\s+['\"]${mapping.module}['\"];?`,\n );\n if (defaultImportRe.test(result)) {\n result = result.replace(defaultImportRe, '');\n namedImports.push(mapping.namedExport);\n } else if (namedImportRe.test(result)) {\n result = result.replace(namedImportRe, '');\n namedImports.push(mapping.namedExport);\n }\n\n // Replace constructors with pre-wrapped named imports using balanced-paren matching\n const matches = matchConstructor(result, mapping.defaultExport);\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n if (match) {\n result = result.slice(0, match.start) + mapping.namedExport + result.slice(match.end);\n }\n }\n }\n\n if (namedImports.length > 0) {\n const importLine = `import { ${namedImports.join(', ')} } from '${bootstrapImportPath}';\\n`;\n result = importLine + result;\n }\n\n return result;\n}\n\nfunction addSessionWrapping(\n source: string,\n agentId: string,\n bootstrapImportPath: string,\n): string {\n let result = source;\n\n const importFromPath = new RegExp(\n `import\\\\s*\\\\{([^}]*)\\\\}\\\\s*from\\\\s*['\"]${bootstrapImportPath.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}['\"]`,\n );\n const existingImportMatch = importFromPath.exec(result);\n if (existingImportMatch) {\n const existingNames = existingImportMatch[1] ?? '';\n const importedNames = existingNames.split(',').map(s => s.trim());\n if (!importedNames.includes('ai')) {\n const newNames = existingNames.trim() ? `ai, ${existingNames.trim()}` : 'ai';\n result = result.replace(existingImportMatch[0], `import { ${newNames} } from '${bootstrapImportPath}'`);\n }\n } else if (!result.includes(`from '${bootstrapImportPath}'`) &&\n !result.includes(`from \"${bootstrapImportPath}\"`)) {\n result = `import { ai } from '${bootstrapImportPath}';\\n${result}`;\n }\n\n const agentLine = `const agent = ai.agent('${agentId}');\\n`;\n\n // Wrap route handler body inside session.run()\n if (ROUTE_HANDLER_RE.test(result)) {\n const handlerMatch = result.match(\n /export\\s+async\\s+function\\s+(?:POST|GET|PUT|DELETE)\\s*\\([^)]*\\)\\s*\\{/,\n );\n result = result.replace(\n /(export\\s+async\\s+function\\s+(?:POST|GET|PUT|DELETE)\\s*\\([^)]*\\)\\s*\\{)/,\n `$1\\n ${agentLine.trim()}\\n const { messages, userId, sessionId } = await req.json();\\n return agent.session({ userId, sessionId }).run(async (s) => {`,\n );\n // Find the handler's closing brace using balanced-brace matching from the handler opening\n if (handlerMatch?.index != null) {\n const openBraceIdx = result.indexOf('{', handlerMatch.index);\n if (openBraceIdx >= 0) {\n let depth = 1;\n let i = openBraceIdx + 1;\n while (i < result.length && depth > 0) {\n if (result[i] === '{') depth++;\n else if (result[i] === '}') depth--;\n i++;\n }\n const closingBraceIdx = i - 1;\n result = `${result.slice(0, closingBraceIdx)} });\\n${result.slice(closingBraceIdx)}`;\n }\n }\n } else if (EXPRESS_HANDLER_RE.test(result) || HONO_HANDLER_RE.test(result)) {\n result = result.replace(\n /((?:app|router)\\.\\s*(?:get|post|put|delete)\\s*\\(\\s*['\"][^'\"]+['\"]\\s*,\\s*(?:async\\s+)?\\([^)]*\\)\\s*(?:=>)?\\s*\\{)/,\n `$1\\n ${agentLine.trim()}\\n return agent.session({ userId: 'todo-extract-user-id', sessionId: 'todo-extract-session-id' }).run(async (s) => {`,\n );\n }\n\n return result;\n}\n\nfunction addUserMessageTracking(source: string): string {\n const requestBodyRe = /(const\\s+\\{[^}]*\\}\\s*=\\s*(?:await\\s+)?(?:req\\.body|request\\.json\\(\\)|await\\s+request\\.json\\(\\)))/;\n const match = requestBodyRe.exec(source);\n if (match) {\n return source.replace(\n match[0],\n `${match[0]};\\n // TODO: extract user message and call s.trackUserMessage(userMessage)`,\n );\n }\n return source;\n}\n\nfunction addFlushBeforeReturn(source: string): string {\n // Insert await ai.flush() before return statements in route handlers\n return source.replace(\n /(\\n)([ \\t]*)(return\\s+(?:Response\\.json|new\\s+Response|NextResponse|res\\.json|res\\.send)\\s*\\()/g,\n '$1$2await ai.flush();\\n$2$3',\n );\n}\n\nexport function instrumentFile(opts: InstrumentFileOptions): string {\n if (opts.tier === 'quick_start') {\n return opts.source;\n }\n\n let result = opts.source;\n\n result = replaceProviderImports(result, opts.providers, opts.bootstrapImportPath);\n\n if (opts.tier === 'advanced') {\n result = addSessionWrapping(result, opts.agentId, opts.bootstrapImportPath);\n result = addUserMessageTracking(result);\n result = addFlushBeforeReturn(result);\n }\n\n return result;\n}\n"],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ //#region src/mcp/scan-project.d.ts
2
+ interface ScanResult {
3
+ project_name: string | null;
4
+ runtime: 'node';
5
+ language: 'typescript' | 'javascript';
6
+ framework: string | null;
7
+ providers: string[];
8
+ agent_frameworks: string[];
9
+ package_manager: string | null;
10
+ existing_instrumentation: {
11
+ has_amplitude_ai: boolean;
12
+ has_patch: boolean;
13
+ has_wrappers: boolean;
14
+ has_session_context: boolean;
15
+ };
16
+ agents: Array<{
17
+ inferred_id: string;
18
+ file: string;
19
+ call_sites: number;
20
+ is_instrumented: boolean;
21
+ is_route_handler: boolean;
22
+ inferred_description: string | null;
23
+ call_site_details: Array<{
24
+ line: number;
25
+ provider: string;
26
+ api: string;
27
+ instrumented: boolean;
28
+ containing_function: string | null;
29
+ code_context: string;
30
+ }>;
31
+ tool_definitions: string[];
32
+ function_definitions: string[];
33
+ }>;
34
+ total_call_sites: number;
35
+ instrumented_call_sites: number;
36
+ uninstrumented_call_sites: number;
37
+ is_multi_agent: boolean;
38
+ multi_agent_signals: string[];
39
+ has_streaming: boolean;
40
+ has_vercel_ai_sdk: boolean;
41
+ has_edge_runtime: boolean;
42
+ has_assistants_api: boolean;
43
+ has_langgraph: boolean;
44
+ message_queue_deps: string[];
45
+ has_frontend_deps: boolean;
46
+ recommendations: string[];
47
+ recommended_tier: 'quick_start' | 'standard' | 'advanced';
48
+ }
49
+ declare function scanProject(rootPath: string): ScanResult;
50
+ //#endregion
51
+ export { ScanResult, scanProject };
52
+ //# sourceMappingURL=scan-project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan-project.d.ts","names":[],"sources":["../../src/mcp/scan-project.ts"],"sourcesContent":[],"mappings":";UASiB,UAAA;EAAA,YAAA,EAAU,MAAA,GAAA,IAqBJ;EAsLP,OAAA,EAAA,MAAW;;;;;;;;;;;;UA7LjB;;;;;;;uBAOa;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsLP,WAAA,oBAA+B"}
@@ -0,0 +1,309 @@
1
+ import { analyzeFileInstrumentation } from "./validate-file.js";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { basename, dirname, join, relative } from "node:path";
4
+
5
+ //#region src/mcp/scan-project.ts
6
+ const SOURCE_EXTENSIONS = new Set([
7
+ ".ts",
8
+ ".tsx",
9
+ ".js",
10
+ ".jsx"
11
+ ]);
12
+ const SKIP_DIRS = new Set([
13
+ "node_modules",
14
+ "dist",
15
+ ".next",
16
+ ".git",
17
+ "__tests__",
18
+ "test"
19
+ ]);
20
+ const MAX_DEPTH = 5;
21
+ const FRAMEWORK_DEPS = [
22
+ ["next", "nextjs"],
23
+ ["express", "express"],
24
+ ["fastify", "fastify"],
25
+ ["hono", "hono"],
26
+ ["@remix-run/node", "remix"]
27
+ ];
28
+ const PROVIDER_DEPS = [
29
+ "openai",
30
+ "@anthropic-ai/sdk",
31
+ "@google/generative-ai",
32
+ "@google/genai",
33
+ "@aws-sdk/client-bedrock-runtime",
34
+ "@mistralai/mistralai",
35
+ "@azure/openai",
36
+ "cohere-ai"
37
+ ];
38
+ const AGENT_FRAMEWORK_DEPS = [
39
+ "langchain",
40
+ "@langchain/core",
41
+ "llamaindex",
42
+ "@openai/agents",
43
+ "crewai"
44
+ ];
45
+ const MESSAGE_QUEUE_DEPS = [
46
+ "bullmq",
47
+ "bull",
48
+ "ioredis",
49
+ "amqplib",
50
+ "@aws-sdk/client-sqs",
51
+ "kafkajs",
52
+ "@google-cloud/pubsub"
53
+ ];
54
+ const FRONTEND_DEPS = [
55
+ "react",
56
+ "vue",
57
+ "svelte",
58
+ "@sveltejs/kit",
59
+ "angular",
60
+ "@angular/core"
61
+ ];
62
+ const STREAMING_RE = /streamText\s*\(|useChat\s*\(|stream\s*:\s*true/;
63
+ const VERCEL_AI_SDK_DEPS = [
64
+ "ai",
65
+ "@ai-sdk/openai",
66
+ "@ai-sdk/anthropic",
67
+ "@ai-sdk/google",
68
+ "@ai-sdk/mistral"
69
+ ];
70
+ const VERCEL_AI_SDK_RE = /\b(?:streamText|generateText|streamObject|generateObject|useChat|useCompletion|useAssistant)\s*\(/;
71
+ const EDGE_RUNTIME_RE = /runtime\s*=\s*['"]edge['"]/;
72
+ const ASSISTANTS_API_RE = /\.beta\.(?:threads|assistants)\./;
73
+ const LANGGRAPH_DEPS = ["@langchain/langgraph"];
74
+ const MULTI_AGENT_CODE_RE = /\.child\s*\(|\.runAs\s*\(|\.runAsSync\s*\(/;
75
+ const ROUTE_HANDLER_RE = /export\s+async\s+function\s+(?:POST|GET|PUT|DELETE)\b|app\.\s*(?:get|post|put|delete)\s*\(|router\./;
76
+ function collectSourceFiles(dir, rootPath, depth) {
77
+ if (depth > MAX_DEPTH) return [];
78
+ let entries;
79
+ try {
80
+ entries = readdirSync(dir);
81
+ } catch {
82
+ return [];
83
+ }
84
+ const files = [];
85
+ for (const entry of entries) {
86
+ if (SKIP_DIRS.has(entry)) continue;
87
+ const fullPath = join(dir, entry);
88
+ let stat;
89
+ try {
90
+ stat = statSync(fullPath);
91
+ } catch {
92
+ continue;
93
+ }
94
+ if (stat.isDirectory()) files.push(...collectSourceFiles(fullPath, rootPath, depth + 1));
95
+ else if (stat.isFile()) {
96
+ const ext = entry.slice(entry.lastIndexOf("."));
97
+ if (SOURCE_EXTENSIONS.has(ext)) files.push(fullPath);
98
+ }
99
+ }
100
+ return files;
101
+ }
102
+ function inferAgentId(filePath) {
103
+ const base = basename(filePath).replace(/\.[^.]+$/, "");
104
+ if (base === "route" || base === "index") return basename(dirname(filePath)) || base;
105
+ return base;
106
+ }
107
+ function inferDescription(filePath, isRouteHandler) {
108
+ const agentId = inferAgentId(filePath);
109
+ if (isRouteHandler) return `Route handler agent: ${agentId}`;
110
+ if (/agent|worker|service/i.test(filePath)) return `Background agent: ${agentId}`;
111
+ return null;
112
+ }
113
+ function readPackageJson(rootPath) {
114
+ const pkgPath = join(rootPath, "package.json");
115
+ if (!existsSync(pkgPath)) return {
116
+ name: null,
117
+ allDeps: /* @__PURE__ */ new Set()
118
+ };
119
+ try {
120
+ const raw = readFileSync(pkgPath, "utf8");
121
+ const pkg = JSON.parse(raw);
122
+ const name = typeof pkg.name === "string" ? pkg.name : null;
123
+ const deps = pkg.dependencies ?? {};
124
+ const devDeps = pkg.devDependencies ?? {};
125
+ return {
126
+ name,
127
+ allDeps: new Set([...Object.keys(deps), ...Object.keys(devDeps)])
128
+ };
129
+ } catch {
130
+ return {
131
+ name: null,
132
+ allDeps: /* @__PURE__ */ new Set()
133
+ };
134
+ }
135
+ }
136
+ function scanProject(rootPath) {
137
+ const { name: projectName, allDeps } = readPackageJson(rootPath);
138
+ let framework = null;
139
+ for (const [dep, label] of FRAMEWORK_DEPS) if (allDeps.has(dep)) {
140
+ framework = label;
141
+ break;
142
+ }
143
+ const providers = PROVIDER_DEPS.filter((dep) => allDeps.has(dep));
144
+ const agentFrameworks = AGENT_FRAMEWORK_DEPS.filter((dep) => allDeps.has(dep));
145
+ let packageManager = null;
146
+ if (existsSync(join(rootPath, "pnpm-lock.yaml"))) packageManager = "pnpm";
147
+ else if (existsSync(join(rootPath, "yarn.lock"))) packageManager = "yarn";
148
+ else if (existsSync(join(rootPath, "package-lock.json"))) packageManager = "npm";
149
+ const isTypeScript = existsSync(join(rootPath, "tsconfig.json"));
150
+ const hasAmplitudeAiDep = allDeps.has("@amplitude/ai");
151
+ const sourceFiles = collectSourceFiles(rootPath, rootPath, 0);
152
+ let totalCallSites = 0;
153
+ let instrumentedCallSites = 0;
154
+ let uninstrumentedCallSites = 0;
155
+ let globalHasPatch = false;
156
+ let globalHasSessionContext = false;
157
+ let globalHasAmplitudeImport = hasAmplitudeAiDep;
158
+ let hasStreaming = false;
159
+ let hasVercelAiSdkUsage = false;
160
+ let hasEdgeRuntime = false;
161
+ let hasAssistantsApi = false;
162
+ let hasMultiAgentCodePatterns = false;
163
+ const globalWrappedProviders = /* @__PURE__ */ new Set();
164
+ const CONSTRUCTOR_TO_PROVIDER = {
165
+ OpenAI: "openai",
166
+ Anthropic: "anthropic",
167
+ Gemini: "gemini",
168
+ GoogleGenerativeAI: "gemini",
169
+ GoogleGenAI: "gemini",
170
+ AzureOpenAI: "azure-openai",
171
+ Bedrock: "bedrock",
172
+ Mistral: "mistral",
173
+ CohereClient: "cohere"
174
+ };
175
+ const agents = [];
176
+ const filesWithCallSites = /* @__PURE__ */ new Set();
177
+ const providerImportsPerFile = /* @__PURE__ */ new Map();
178
+ for (const filePath of sourceFiles) {
179
+ let content;
180
+ try {
181
+ content = readFileSync(filePath, "utf8");
182
+ } catch {
183
+ continue;
184
+ }
185
+ const analysis = analyzeFileInstrumentation(content);
186
+ if (analysis.has_amplitude_import) globalHasAmplitudeImport = true;
187
+ if (analysis.has_session_context) globalHasSessionContext = true;
188
+ if (/\bpatch\s*\(\s*\{/.test(content)) globalHasPatch = true;
189
+ if (STREAMING_RE.test(content) && analysis.total_call_sites > 0) hasStreaming = true;
190
+ if (VERCEL_AI_SDK_RE.test(content)) hasVercelAiSdkUsage = true;
191
+ if (EDGE_RUNTIME_RE.test(content)) hasEdgeRuntime = true;
192
+ if (ASSISTANTS_API_RE.test(content)) hasAssistantsApi = true;
193
+ if (MULTI_AGENT_CODE_RE.test(content)) hasMultiAgentCodePatterns = true;
194
+ if (analysis.has_amplitude_import || /\bamplitude\s*:/.test(content)) {
195
+ for (const m of content.matchAll(/new\s+(OpenAI|Anthropic|Gemini|AzureOpenAI|Bedrock|Mistral|GoogleGenerativeAI|GoogleGenAI|CohereClient)\s*\(/g)) {
196
+ const provLabel = CONSTRUCTOR_TO_PROVIDER[m[1] ?? ""];
197
+ if (provLabel && /\bamplitude\s*:/.test(content)) globalWrappedProviders.add(provLabel);
198
+ }
199
+ if (/\.wrap\s*\(/.test(content)) for (const site of analysis.call_sites) globalWrappedProviders.add(site.provider);
200
+ }
201
+ const fileProviders = /* @__PURE__ */ new Set();
202
+ for (const site of analysis.call_sites) fileProviders.add(site.provider);
203
+ if (fileProviders.size > 0) {
204
+ const relPath = relative(rootPath, filePath);
205
+ providerImportsPerFile.set(relPath, fileProviders);
206
+ }
207
+ if (analysis.total_call_sites > 0) {
208
+ const relPath = relative(rootPath, filePath);
209
+ filesWithCallSites.add(relPath);
210
+ totalCallSites += analysis.total_call_sites;
211
+ instrumentedCallSites += analysis.instrumented;
212
+ uninstrumentedCallSites += analysis.uninstrumented;
213
+ const isRouteHandler = ROUTE_HANDLER_RE.test(content);
214
+ const inferredId = inferAgentId(filePath);
215
+ const allInstrumented = analysis.uninstrumented === 0;
216
+ agents.push({
217
+ inferred_id: inferredId,
218
+ file: relPath,
219
+ call_sites: analysis.total_call_sites,
220
+ is_instrumented: allInstrumented,
221
+ is_route_handler: isRouteHandler,
222
+ inferred_description: inferDescription(filePath, isRouteHandler),
223
+ call_site_details: analysis.call_sites.map((cs) => ({
224
+ line: cs.line,
225
+ provider: cs.provider,
226
+ api: cs.api,
227
+ instrumented: cs.instrumented,
228
+ containing_function: cs.containing_function,
229
+ code_context: cs.code_context
230
+ })),
231
+ tool_definitions: analysis.tool_definitions,
232
+ function_definitions: analysis.function_definitions
233
+ });
234
+ }
235
+ }
236
+ const globalHasWrappers = globalWrappedProviders.size > 0;
237
+ if (globalHasWrappers && globalHasAmplitudeImport) {
238
+ for (const agent of agents) if (!agent.is_instrumented) {
239
+ if ([...new Set(agent.call_site_details.map((cs) => cs.provider))].every((p) => globalWrappedProviders.has(p))) {
240
+ agent.is_instrumented = true;
241
+ const delta = agent.call_sites;
242
+ uninstrumentedCallSites -= delta;
243
+ instrumentedCallSites += delta;
244
+ }
245
+ }
246
+ }
247
+ const multiAgentSignals = [];
248
+ if (filesWithCallSites.size > 1) multiAgentSignals.push(`LLM call sites found in ${filesWithCallSites.size} separate files: ${[...filesWithCallSites].join(", ")}`);
249
+ if (agentFrameworks.length > 0) multiAgentSignals.push(`Agent framework dependencies detected: ${agentFrameworks.join(", ")}`);
250
+ const allProviders = /* @__PURE__ */ new Set();
251
+ for (const provSet of providerImportsPerFile.values()) for (const p of provSet) allProviders.add(p);
252
+ if (allProviders.size > 1) multiAgentSignals.push(`Multiple LLM providers in use: ${[...allProviders].join(", ")}`);
253
+ for (const agent of agents) {
254
+ const toolNames = agent.tool_definitions;
255
+ const callFunctions = agent.call_site_details.map((cs) => cs.containing_function).filter(Boolean);
256
+ const overlapping = toolNames.filter((t) => callFunctions.includes(t));
257
+ if (overlapping.length > 0) multiAgentSignals.push(`File ${agent.file}: tool definitions ${overlapping.join(", ")} also contain LLM calls — possible delegation-as-tools pattern`);
258
+ }
259
+ if (hasMultiAgentCodePatterns) multiAgentSignals.push("Amplitude AI SDK delegation APIs detected: .child() or .runAs() — multi-agent confirmed");
260
+ const isMultiAgent = hasMultiAgentCodePatterns;
261
+ const messageQueueDeps = MESSAGE_QUEUE_DEPS.filter((dep) => allDeps.has(dep));
262
+ const hasFrontendDeps = FRONTEND_DEPS.some((dep) => allDeps.has(dep));
263
+ const hasVercelAiSdk = VERCEL_AI_SDK_DEPS.some((dep) => allDeps.has(dep)) || hasVercelAiSdkUsage;
264
+ const hasLanggraph = LANGGRAPH_DEPS.some((dep) => allDeps.has(dep));
265
+ const recommendedTier = "advanced";
266
+ const recommendations = [];
267
+ if (hasStreaming) recommendations.push("Streaming detected: keep sessions open until stream is fully consumed. Use session.run() with an awaited stream, not a fire-and-forget pattern.");
268
+ if (messageQueueDeps.length > 0) recommendations.push(`Message queue deps detected (${messageQueueDeps.join(", ")}): enable propagateContext in the bootstrap file and use injectContext/extractContext to correlate events across services.`);
269
+ if (hasFrontendDeps) recommendations.push("Frontend framework detected alongside backend: pass browserSessionId and deviceId from frontend request headers to agent.session() for session replay linking.");
270
+ if (hasVercelAiSdk) recommendations.push("Vercel AI SDK detected. Provider wrappers instrument the underlying provider SDK (openai, @anthropic-ai/sdk), not the Vercel AI SDK abstraction layer (streamText, generateText). If you also have the underlying provider as a direct dependency, wrappers will work because Vercel AI SDK delegates to them internally. Otherwise, use Tier 1 (patch) which intercepts at the transport level, or add the underlying provider SDK as a direct dependency.");
271
+ if (hasEdgeRuntime) recommendations.push("Edge Runtime detected. session.run() relies on AsyncLocalStorage which may not be available in Edge Runtime or Cloudflare Workers. Use explicit context passing instead of session.run(): call agent.trackUserMessage() and agent.trackAiMessage() directly with sessionId parameter.");
272
+ if (hasAssistantsApi) recommendations.push("OpenAI Assistants API detected (client.beta.threads/assistants). Provider wrappers do not auto-instrument the Assistants API. Use manual tracking with trackUserMessage/trackAiMessage, or migrate to the OpenAI Agents SDK which supports AmplitudeTracingProcessor.");
273
+ if (hasLanggraph) recommendations.push("LangGraph detected. LLM calls within LangGraph are captured via the LangChain AmplitudeCallbackHandler, but graph orchestration events (node transitions, checkpoints, human-in-the-loop) are not yet instrumented.");
274
+ if (globalHasAmplitudeImport && globalHasWrappers && globalHasSessionContext) recommendations.push("Project already has @amplitude/ai instrumentation with wrappers and session context. No re-instrumentation needed. Consider upgrading contentMode tiers, adding scoring, or expanding multi-agent coverage if applicable.");
275
+ return {
276
+ project_name: projectName,
277
+ runtime: "node",
278
+ language: isTypeScript ? "typescript" : "javascript",
279
+ framework,
280
+ providers,
281
+ agent_frameworks: agentFrameworks,
282
+ package_manager: packageManager,
283
+ existing_instrumentation: {
284
+ has_amplitude_ai: globalHasAmplitudeImport,
285
+ has_patch: globalHasPatch,
286
+ has_wrappers: globalHasWrappers,
287
+ has_session_context: globalHasSessionContext
288
+ },
289
+ agents,
290
+ total_call_sites: totalCallSites,
291
+ instrumented_call_sites: instrumentedCallSites,
292
+ uninstrumented_call_sites: uninstrumentedCallSites,
293
+ is_multi_agent: isMultiAgent,
294
+ multi_agent_signals: multiAgentSignals,
295
+ has_streaming: hasStreaming,
296
+ has_vercel_ai_sdk: hasVercelAiSdk,
297
+ has_edge_runtime: hasEdgeRuntime,
298
+ has_assistants_api: hasAssistantsApi,
299
+ has_langgraph: hasLanggraph,
300
+ message_queue_deps: messageQueueDeps,
301
+ has_frontend_deps: hasFrontendDeps,
302
+ recommendations,
303
+ recommended_tier: recommendedTier
304
+ };
305
+ }
306
+
307
+ //#endregion
308
+ export { scanProject };
309
+ //# sourceMappingURL=scan-project.js.map