@moda-ai/cli 1.12.2 → 1.12.3

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/dist/harness.js CHANGED
@@ -2481,14 +2481,19 @@ function environmentFromAnnotation(annotation) {
2481
2481
  };
2482
2482
  }
2483
2483
  function applyHeuristics(graph, files) {
2484
+ const isApplicationCode = applicationCodePredicate(files);
2484
2485
  for (const file of files) {
2485
2486
  maybeAddPromptArtifact(graph, file);
2486
2487
  maybeAddSkillArtifact(graph, file);
2488
+ if (isApplicationCode(file.path))
2489
+ maybeAddToolArtifact(graph, file);
2487
2490
  maybeAddEvalArtifact(graph, file);
2488
2491
  maybeAddDeployment(graph, file);
2489
2492
  }
2490
2493
  if (graph.runtimeAgents.length === 0) {
2491
2494
  for (const file of files) {
2495
+ if (!isApplicationCode(file.path))
2496
+ continue;
2492
2497
  const agent = inferRuntimeAgent(file, graph);
2493
2498
  if (agent)
2494
2499
  upsertRuntimeAgent(graph, agent);
@@ -2587,7 +2592,10 @@ function inferRuntimeAgent(file, graph) {
2587
2592
  const loweredText = file.text.toLowerCase();
2588
2593
  const pathLooksLikeAgent = /(^|\/)(agent|agents|agent_harness)\//.test(loweredPath) ||
2589
2594
  /(^|\/)(agent|main|index)\.(ts|tsx|js|mjs|py)$/.test(loweredPath);
2590
- const codeLooksLikeAgent = /\b(run_agent|runagent|createagent|buildprompt|system prompt|openai|anthropic|model)\b/i.test(file.text);
2595
+ // Require evidence that the file actually constructs or runs an agent, not
2596
+ // merely that it mentions a model provider. Loose signals like "openai" or
2597
+ // "model" match library internals and utilities that are not runtime agents.
2598
+ const codeLooksLikeAgent = AGENT_RUNTIME_SIGNAL.test(file.text);
2591
2599
  if (!pathLooksLikeAgent || !codeLooksLikeAgent)
2592
2600
  return null;
2593
2601
  const channelIds = inferChannels(file);
@@ -2673,16 +2681,136 @@ function inferChannels(file) {
2673
2681
  .filter(([needle]) => text.includes(needle))
2674
2682
  .map(([, id]) => id);
2675
2683
  }
2684
+ // Trailing module names an import statement references, normalized to their last
2685
+ // path segment without extension. Covers ES/TS `from '...'` / `import(...)` /
2686
+ // `require(...)` and Python `from a.b.c import ...` / `import a.b.c`. Used to link
2687
+ // split-file tools whose module is imported by extensionless or dotted name
2688
+ // (e.g. `from tools import get_weather`, `import { getWeather } from './tools'`).
2689
+ function importedModuleNames(text) {
2690
+ const names = new Set();
2691
+ for (const match of text.matchAll(/(?:from|import|require)\s*\(?\s*["']([^"']+)["']/g)) {
2692
+ const segment = match[1].split('/').pop();
2693
+ if (segment)
2694
+ names.add(segment.replace(/\.(ts|tsx|js|mjs|py)$/i, ''));
2695
+ }
2696
+ for (const match of text.matchAll(/^\s*(?:from|import)\s+([.\w]+)/gm)) {
2697
+ const segment = match[1].replace(/^\.+/, '').split('.').pop();
2698
+ if (segment)
2699
+ names.add(segment);
2700
+ }
2701
+ return names;
2702
+ }
2676
2703
  function inferArtifactsForFile(file, graph) {
2677
2704
  const text = file.text ?? '';
2705
+ const imported = importedModuleNames(text);
2678
2706
  const referenced = graph.artifacts.filter((artifact) => {
2679
2707
  if (!artifact.sourcePath)
2680
2708
  return false;
2709
+ if (artifact.sourcePath === file.path)
2710
+ return true;
2681
2711
  const base = basename(artifact.sourcePath);
2682
- return text.includes(base) || text.includes(artifact.sourcePath);
2712
+ if (text.includes(base) || text.includes(artifact.sourcePath))
2713
+ return true;
2714
+ // Split-file tools: the agent imports the tool module by its extensionless or
2715
+ // dotted name, which contains neither the basename-with-extension nor the full
2716
+ // source path (e.g. `from tools import get_weather`, `from './tools'`).
2717
+ if (artifact.type === 'tool') {
2718
+ const moduleName = base.replace(/\.(ts|tsx|js|mjs|py)$/i, '');
2719
+ if (moduleName && imported.has(moduleName))
2720
+ return true;
2721
+ }
2722
+ return false;
2683
2723
  });
2684
2724
  return referenced.map((artifact) => artifact.id);
2685
2725
  }
2726
+ // Signals that a file constructs or runs an agent (as opposed to merely
2727
+ // importing a model SDK). Kept deliberately construction/run oriented so
2728
+ // library internals that only reference a provider are not misread as agents.
2729
+ // Covers the LangChain factory/executor shapes (create_tool_calling_agent,
2730
+ // create_openai_functions_agent, AgentExecutor, ...) and the `return`/`yield
2731
+ // Agent(...)` factory forms in addition to the direct construction/run signals.
2732
+ const AGENT_RUNTIME_SIGNAL = /(run_agent|runAgent|create_[a-z_]*agent\b|createAgent|initialize_agent|AgentExecutor\s*\(|new\s+Agent\s*\(|\bSwarm\s*\(|\bCrew\s*\(|\bRunner\s*\(|Runner\.run|\.run_sync\s*\(|\.run_streamed\s*\(|[=([,]\s*Agent\s*\(|(?:return|yield)\s+Agent\s*\(|^\s*Agent\s*\()/im;
2733
+ // Hosted/first-party tool classes that count as tool definitions when called.
2734
+ const HOSTED_TOOL_CLASSES = [
2735
+ 'WebSearchTool',
2736
+ 'FileSearchTool',
2737
+ 'ComputerTool',
2738
+ 'CodeInterpreterTool',
2739
+ 'HostedMCPTool',
2740
+ 'ImageGenerationTool',
2741
+ 'LocalShellTool',
2742
+ 'ShellTool',
2743
+ 'ApplyPatchTool',
2744
+ 'FunctionTool',
2745
+ ];
2746
+ function repoOwnPackageNames(files) {
2747
+ const packageJson = files.find((file) => file.path === 'package.json')?.text;
2748
+ const pyproject = files.find((file) => file.path === 'pyproject.toml')?.text;
2749
+ const names = [
2750
+ packageJson ? safePackageName(packageJson) : undefined,
2751
+ pyproject ? safeTomlName(pyproject) : undefined,
2752
+ ];
2753
+ return names.filter((name) => Boolean(name)).map((name) => name.toLowerCase());
2754
+ }
2755
+ function repoIsAgentFrameworkSource(files) {
2756
+ const own = new Set(repoOwnPackageNames(files));
2757
+ if (own.size === 0)
2758
+ return false;
2759
+ return FRAMEWORK_CATALOG.some((framework) => framework.packageNames.some((name) => own.has(name.toLowerCase())));
2760
+ }
2761
+ function isApplicationLikePath(path) {
2762
+ return /(^|\/)(examples?|apps?|demos?|samples?|cookbook|tutorials?|quickstart|playground)\//i.test(path);
2763
+ }
2764
+ // When the repository IS an agent framework/SDK (its own package name matches a
2765
+ // framework catalog entry), its library source is the framework implementation,
2766
+ // not customer application code: runtime agents and tools should only be
2767
+ // inferred from application-like directories (examples, apps, demos, ...).
2768
+ function applicationCodePredicate(files) {
2769
+ return repoIsAgentFrameworkSource(files) ? isApplicationLikePath : () => true;
2770
+ }
2771
+ const HOSTED_TOOL_CALL = new RegExp(`\\b(${HOSTED_TOOL_CLASSES.join('|')})\\s*\\(`, 'g');
2772
+ function detectToolDefinitions(file) {
2773
+ const text = file.text;
2774
+ if (!text)
2775
+ return [];
2776
+ const found = new Map();
2777
+ const pyDecorated = /@function_tool[^\n]*\n(?:[ \t]*@[^\n]*\n)*[ \t]*(?:async[ \t]+)?def[ \t]+([A-Za-z_]\w*)/g;
2778
+ for (const match of text.matchAll(pyDecorated))
2779
+ found.set(match[1], '@function_tool-decorated function');
2780
+ const tsFactory = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:[\w.]*\.)?(?:tool|functionTool|createTool|dynamicTool)\s*\(/g;
2781
+ for (const match of text.matchAll(tsFactory))
2782
+ found.set(match[1], 'tool factory assignment');
2783
+ const tsNewTool = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+(?:Dynamic|Structured)?Tool\b/g;
2784
+ for (const match of text.matchAll(tsNewTool))
2785
+ found.set(match[1], 'tool constructor assignment');
2786
+ for (const match of text.matchAll(HOSTED_TOOL_CALL))
2787
+ found.set(match[1], `hosted tool ${match[1]}`);
2788
+ return [...found.entries()].map(([name, reason]) => ({ name, reason }));
2789
+ }
2790
+ function maybeAddToolArtifact(graph, file) {
2791
+ if (!/\.(ts|tsx|js|mjs|py)$/i.test(file.path))
2792
+ return;
2793
+ // Skip test/eval fixtures — those tool definitions are fixtures, not a
2794
+ // customer's runtime tools. (Framework library internals are already
2795
+ // excluded by applyHeuristics's application-code predicate.)
2796
+ if (isEvalLikePath(file.path))
2797
+ return;
2798
+ const tools = detectToolDefinitions(file);
2799
+ if (tools.length === 0)
2800
+ return;
2801
+ for (const tool of tools) {
2802
+ upsertArtifact(graph, {
2803
+ id: stableId('tool', `${file.path}:${tool.name}`),
2804
+ type: 'tool',
2805
+ name: titleFromId(tool.name),
2806
+ scope: 'agent',
2807
+ sourcePath: file.path,
2808
+ usedByAgentIds: [],
2809
+ confidence: 0.6,
2810
+ evidence: [{ path: file.path, reason: `Tool definition detected (${tool.reason}).` }],
2811
+ });
2812
+ }
2813
+ }
2686
2814
  function detectDeveloperAgentAdapters(files) {
2687
2815
  const adapters = [];
2688
2816
  if (files.some((file) => file.path.startsWith('.claude/'))) {
@@ -2745,7 +2873,7 @@ const FRAMEWORK_CATALOG = [
2745
2873
  name: 'LangChain',
2746
2874
  kind: 'agent_framework',
2747
2875
  packageNames: ['langchain', '@langchain/core', '@langchain/langgraph', 'langgraph'],
2748
- patterns: [/langchain/i, /langgraph/i],
2876
+ patterns: [/\blangchain\b/i, /\blanggraph\b/i],
2749
2877
  },
2750
2878
  {
2751
2879
  id: 'framework_vercel_ai_sdk',
@@ -2759,7 +2887,7 @@ const FRAMEWORK_CATALOG = [
2759
2887
  name: 'Mastra',
2760
2888
  kind: 'agent_framework',
2761
2889
  packageNames: ['mastra', '@mastra/core'],
2762
- patterns: [/mastra/i],
2890
+ patterns: [/\bmastra\b/i],
2763
2891
  },
2764
2892
  {
2765
2893
  id: 'framework_openai_agents',
@@ -2773,7 +2901,7 @@ const FRAMEWORK_CATALOG = [
2773
2901
  name: 'CrewAI',
2774
2902
  kind: 'agent_framework',
2775
2903
  packageNames: ['crewai'],
2776
- patterns: [/crewai/i],
2904
+ patterns: [/\bcrewai\b/i],
2777
2905
  },
2778
2906
  {
2779
2907
  id: 'framework_pydantic_ai',
@@ -2798,7 +2926,7 @@ const PROVIDER_CATALOG = [
2798
2926
  kind: 'model_provider',
2799
2927
  packageNames: ['@anthropic-ai/sdk', '@ai-sdk/anthropic', 'anthropic'],
2800
2928
  envVars: ['ANTHROPIC_API_KEY'],
2801
- patterns: [/anthropic/i, /\bclaude-[\w.-]+/i, /ANTHROPIC_API_KEY/],
2929
+ patterns: [/\banthropic\b/i, /\bclaude-[\w.-]+/i, /ANTHROPIC_API_KEY/],
2802
2930
  },
2803
2931
  {
2804
2932
  id: 'provider_google',
@@ -2806,7 +2934,7 @@ const PROVIDER_CATALOG = [
2806
2934
  kind: 'model_provider',
2807
2935
  packageNames: ['@google/generative-ai', '@ai-sdk/google', 'google-generativeai'],
2808
2936
  envVars: ['GOOGLE_GENERATIVE_AI_API_KEY', 'GEMINI_API_KEY'],
2809
- patterns: [/gemini/i, /google.generativeai/i],
2937
+ patterns: [/\bgemini\b/i, /google\.generativeai/i],
2810
2938
  },
2811
2939
  {
2812
2940
  id: 'provider_cohere',
@@ -2814,7 +2942,7 @@ const PROVIDER_CATALOG = [
2814
2942
  kind: 'model_provider',
2815
2943
  packageNames: ['cohere-ai', 'cohere'],
2816
2944
  envVars: ['COHERE_API_KEY'],
2817
- patterns: [/cohere/i],
2945
+ patterns: [/\bcohere\b/i],
2818
2946
  },
2819
2947
  {
2820
2948
  id: 'provider_mistral',
@@ -2822,7 +2950,7 @@ const PROVIDER_CATALOG = [
2822
2950
  kind: 'model_provider',
2823
2951
  packageNames: ['@mistralai/mistralai', 'mistralai'],
2824
2952
  envVars: ['MISTRAL_API_KEY'],
2825
- patterns: [/mistral/i],
2953
+ patterns: [/\bmistral\b/i],
2826
2954
  },
2827
2955
  {
2828
2956
  id: 'provider_ollama',
@@ -2830,7 +2958,7 @@ const PROVIDER_CATALOG = [
2830
2958
  kind: 'model_provider',
2831
2959
  packageNames: ['ollama'],
2832
2960
  envVars: ['OLLAMA_HOST'],
2833
- patterns: [/ollama/i],
2961
+ patterns: [/\bollama\b/i],
2834
2962
  },
2835
2963
  ];
2836
2964
  function detectAgentFrameworks(files) {
@@ -2883,9 +3011,9 @@ function detectionEvidence(files, packageNames, patterns) {
2883
3011
  }
2884
3012
  }
2885
3013
  if (file.path === 'pyproject.toml' && file.text) {
2886
- const lowered = file.text.toLowerCase();
3014
+ const deps = pyprojectDependencyNames(file.text);
2887
3015
  for (const name of packageNames) {
2888
- if (lowered.includes(name.toLowerCase())) {
3016
+ if (deps.has(name.toLowerCase())) {
2889
3017
  evidence.push({ path: file.path, reason: `Python dependency ${name} detected.` });
2890
3018
  }
2891
3019
  }
@@ -2915,6 +3043,53 @@ function packageDependencyNames(text) {
2915
3043
  return new Set();
2916
3044
  }
2917
3045
  }
3046
+ function pyprojectDependencyNames(text) {
3047
+ // Only extract names from actual dependency contexts, never every quoted string
3048
+ // in the file: that would re-open the "ai"-inside-`keywords = ["ai"]` /
3049
+ // `name = "openai-agents"` false positives this parser exists to avoid. Two
3050
+ // dependency shapes are covered, and membership is tested by exact name:
3051
+ // - PEP 621 / PEP 518 requirement arrays (`dependencies`/`requires = [...]`,
3052
+ // `[project.optional-dependencies]`, `[dependency-groups]`): leading
3053
+ // distribution token of each quoted PEP 508 string (e.g. "openai>=2" -> openai).
3054
+ // - Poetry tables (`[tool.poetry.dependencies]`, `dev-dependencies`,
3055
+ // `group.<name>.dependencies`): the bare TOML key (e.g. `openai = "^1.0"`).
3056
+ const names = new Set();
3057
+ const addLeadingToken = (spec) => {
3058
+ const match = spec.match(/^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/);
3059
+ if (match)
3060
+ names.add(match[1].toLowerCase());
3061
+ };
3062
+ const isPoetryDepTable = (section) => /^tool\.poetry\.(dependencies|dev-dependencies|group\.[^.]+\.dependencies)$/.test(section);
3063
+ const isRequirementArrayTable = (section) => section === 'project.optional-dependencies' || section === 'dependency-groups';
3064
+ let section = '';
3065
+ let inDependencyArray = false;
3066
+ for (const raw of text.split(/\r?\n/)) {
3067
+ const line = raw.split('#')[0];
3068
+ const header = line.match(/^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*$/);
3069
+ if (header) {
3070
+ section = header[1].trim().toLowerCase();
3071
+ inDependencyArray = false;
3072
+ continue;
3073
+ }
3074
+ if (/^\s*(?:dependencies|requires)\s*=\s*\[/.test(line) ||
3075
+ (isRequirementArrayTable(section) && /=\s*\[/.test(line))) {
3076
+ inDependencyArray = true;
3077
+ }
3078
+ if (inDependencyArray) {
3079
+ for (const match of line.matchAll(/["']([^"']+)["']/g))
3080
+ addLeadingToken(match[1]);
3081
+ if (line.includes(']'))
3082
+ inDependencyArray = false;
3083
+ continue;
3084
+ }
3085
+ if (isPoetryDepTable(section)) {
3086
+ const key = line.match(/^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/);
3087
+ if (key && key[1].toLowerCase() !== 'python')
3088
+ names.add(key[1].toLowerCase());
3089
+ }
3090
+ }
3091
+ return names;
3092
+ }
2918
3093
  function modelHintsForProvider(providerId, files) {
2919
3094
  const hints = new Set();
2920
3095
  const pattern = providerId === 'provider_anthropic'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.12.2",
3
+ "version": "1.12.3",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-07-06T03:50:30.586Z",
4
- "cli_version": "1.12.2",
3
+ "bundled_at": "2026-07-06T05:27:59.799Z",
4
+ "cli_version": "1.12.3",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-node-anthropic",