@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 +187 -12
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
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
|
-
|
|
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
|
-
|
|
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: [/
|
|
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: [/
|
|
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: [/
|
|
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: [/
|
|
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: [/
|
|
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: [/
|
|
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: [/
|
|
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: [/
|
|
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
|
|
3014
|
+
const deps = pyprojectDependencyNames(file.text);
|
|
2887
3015
|
for (const name of packageNames) {
|
|
2888
|
-
if (
|
|
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