@samrahimi/smol-js 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
  "use strict";
4
3
  var __create = Object.create;
5
4
  var __defProp = Object.defineProperty;
@@ -25,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
24
  ));
26
25
 
27
26
  // src/cli.ts
28
- var fs6 = __toESM(require("fs"));
29
- var path6 = __toESM(require("path"));
27
+ var fs8 = __toESM(require("fs"));
28
+ var path8 = __toESM(require("path"));
30
29
  var readline = __toESM(require("readline"));
31
30
  var import_chalk3 = __toESM(require("chalk"));
32
31
  var import_dotenv = __toESM(require("dotenv"));
@@ -742,9 +741,21 @@ Total time: ${(duration / 1e3).toFixed(2)}s`);
742
741
  getName() {
743
742
  return this.config.name;
744
743
  }
744
+ /** Get agent type identifier */
745
+ getType() {
746
+ return this.constructor.name;
747
+ }
748
+ /** Get max steps configuration */
749
+ getMaxSteps() {
750
+ return this.config.maxSteps;
751
+ }
752
+ /** Set the event callback (useful for orchestration) */
753
+ setOnEvent(callback) {
754
+ this.config.onEvent = callback;
755
+ }
745
756
  /** Sleep for a specified duration */
746
757
  sleep(ms) {
747
- return new Promise((resolve6) => setTimeout(resolve6, ms));
758
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
748
759
  }
749
760
  };
750
761
 
@@ -1847,6 +1858,10 @@ Please try a different approach.`
1847
1858
  memoryStep.tokenUsage = response.tokenUsage;
1848
1859
  if (response.content && response.content.trim()) {
1849
1860
  this.logger.reasoning(response.content.trim());
1861
+ this.emitEvent("agent_thinking", {
1862
+ step: this.currentStep,
1863
+ content: response.content.trim()
1864
+ });
1850
1865
  }
1851
1866
  if (!response.toolCalls || response.toolCalls.length === 0) {
1852
1867
  this.logger.warn("No tool calls in response. Prompting model to use tools.");
@@ -1879,42 +1894,84 @@ Please try a different approach.`
1879
1894
  async processToolCalls(toolCalls) {
1880
1895
  const results = [];
1881
1896
  const executeTool = async (tc) => {
1897
+ const startTime = Date.now();
1882
1898
  const toolName = tc.function.name;
1883
1899
  const tool = this.tools.get(toolName);
1884
1900
  if (!tool) {
1901
+ const error = `Unknown tool: ${toolName}. Available tools: ${Array.from(this.tools.keys()).join(", ")}`;
1902
+ this.emitEvent("agent_tool_result", {
1903
+ step: this.currentStep,
1904
+ toolCallId: tc.id,
1905
+ toolName,
1906
+ result: null,
1907
+ error,
1908
+ duration: Date.now() - startTime
1909
+ });
1885
1910
  return {
1886
1911
  toolCallId: tc.id,
1887
1912
  toolName,
1888
1913
  result: null,
1889
- error: `Unknown tool: ${toolName}. Available tools: ${Array.from(this.tools.keys()).join(", ")}`
1914
+ error
1890
1915
  };
1891
1916
  }
1892
1917
  let args;
1893
1918
  try {
1894
1919
  args = typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments;
1895
1920
  } catch {
1921
+ const error = `Failed to parse tool arguments: ${tc.function.arguments}`;
1922
+ this.emitEvent("agent_tool_result", {
1923
+ step: this.currentStep,
1924
+ toolCallId: tc.id,
1925
+ toolName,
1926
+ result: null,
1927
+ error,
1928
+ duration: Date.now() - startTime
1929
+ });
1896
1930
  return {
1897
1931
  toolCallId: tc.id,
1898
1932
  toolName,
1899
1933
  result: null,
1900
- error: `Failed to parse tool arguments: ${tc.function.arguments}`
1934
+ error
1901
1935
  };
1902
1936
  }
1903
1937
  this.logger.info(` Calling tool: ${toolName}(${JSON.stringify(args).slice(0, 100)}...)`);
1904
- this.emitEvent("agent_tool_call", { tool: toolName, args });
1938
+ this.emitEvent("agent_tool_call", {
1939
+ step: this.currentStep,
1940
+ toolCallId: tc.id,
1941
+ toolName,
1942
+ arguments: args
1943
+ });
1905
1944
  try {
1906
1945
  const result = await tool.call(args);
1946
+ const duration = Date.now() - startTime;
1947
+ this.emitEvent("agent_tool_result", {
1948
+ step: this.currentStep,
1949
+ toolCallId: tc.id,
1950
+ toolName,
1951
+ result,
1952
+ duration
1953
+ });
1907
1954
  return {
1908
1955
  toolCallId: tc.id,
1909
1956
  toolName,
1910
1957
  result
1911
1958
  };
1912
1959
  } catch (error) {
1960
+ const errorMsg = `Tool execution error: ${error.message}`;
1961
+ const duration = Date.now() - startTime;
1962
+ this.emitEvent("agent_tool_result", {
1963
+ step: this.currentStep,
1964
+ toolCallId: tc.id,
1965
+ toolName,
1966
+ result: null,
1967
+ error: errorMsg,
1968
+ duration
1969
+ });
1913
1970
  return {
1914
1971
  toolCallId: tc.id,
1915
1972
  toolName,
1916
1973
  result: null,
1917
- error: `Tool execution error: ${error.message}`
1974
+ error: errorMsg
1918
1975
  };
1919
1976
  }
1920
1977
  };
@@ -2037,7 +2094,7 @@ async function ${this.name}(task: string): Promise<string> { ... }
2037
2094
  };
2038
2095
 
2039
2096
  // src/models/OpenAIModel.ts
2040
- var import_openai = __toESM(require("openai"));
2097
+ var import_openai = __toESM(require("openai/index.mjs"));
2041
2098
 
2042
2099
  // src/models/Model.ts
2043
2100
  var Model = class {
@@ -2580,40 +2637,35 @@ var ExaGetContentsTool = class extends Tool {
2580
2637
  // src/tools/ExaResearchTool.ts
2581
2638
  var ExaResearchTool = class extends Tool {
2582
2639
  name = "exa_research";
2583
- description = "Perform deep research on a single topic using Exa.ai. Searches for relevant sources, retrieves their content, and finds similar pages for comprehensive coverage. Returns a structured research summary with sources. Use this for thorough research on any topic.";
2640
+ description = "Perform comprehensive web research using Exa.ai's agentic research system. Provide natural-language instructions about what to research, and the AI research agent will plan searches, gather sources, extract facts, and synthesize findings into a detailed markdown report with citations. Ideal for deep research on any topic. Results typically complete in 20-90 seconds.";
2584
2641
  inputs = {
2585
- topic: {
2642
+ instructions: {
2586
2643
  type: "string",
2587
- description: "The research topic or question to investigate",
2644
+ description: "Natural-language task description of what to research (max 4096 characters). Be clear about what information you want, how research should be conducted, and what the output should contain.",
2588
2645
  required: true
2589
2646
  },
2590
- numSources: {
2591
- type: "number",
2592
- description: "Number of primary sources to retrieve (default: 5, max: 10)",
2593
- required: false,
2594
- default: 5
2595
- },
2596
- category: {
2647
+ model: {
2597
2648
  type: "string",
2598
- description: 'Optional category: "research paper", "news", "blog", "company"',
2599
- required: false
2600
- },
2601
- includeDomains: {
2602
- type: "array",
2603
- description: "Only include results from these domains",
2649
+ description: 'Research model: "exa-research-fast" (faster, cheaper), "exa-research" (balanced, default), or "exa-research-pro" (most thorough)',
2604
2650
  required: false
2605
2651
  },
2606
- startPublishedDate: {
2607
- type: "string",
2608
- description: "Only include results published after this date (ISO 8601)",
2652
+ outputSchema: {
2653
+ type: "object",
2654
+ description: "Optional JSON Schema to enforce structured output format (max 8 root fields, 5 levels deep). If omitted, returns markdown report.",
2609
2655
  required: false
2610
2656
  }
2611
2657
  };
2612
2658
  outputType = "string";
2613
2659
  apiKey;
2660
+ defaultModel;
2661
+ pollInterval;
2662
+ maxPollTime;
2614
2663
  constructor(config) {
2615
2664
  super();
2616
2665
  this.apiKey = config?.apiKey ?? process.env.EXA_API_KEY ?? "";
2666
+ this.defaultModel = config?.model ?? "exa-research";
2667
+ this.pollInterval = config?.pollInterval ?? 2e3;
2668
+ this.maxPollTime = config?.maxPollTime ?? 3e5;
2617
2669
  }
2618
2670
  async setup() {
2619
2671
  if (!this.apiKey) {
@@ -2622,91 +2674,87 @@ var ExaResearchTool = class extends Tool {
2622
2674
  this.isSetup = true;
2623
2675
  }
2624
2676
  async execute(args) {
2625
- const topic = args.topic;
2626
- const numSources = Math.min(args.numSources ?? 5, 10);
2627
- const searchBody = {
2628
- query: topic,
2629
- numResults: numSources,
2630
- type: "auto",
2631
- text: { maxCharacters: 3e3 }
2677
+ const instructions = args.instructions;
2678
+ const model = args.model ?? this.defaultModel;
2679
+ const outputSchema = args.outputSchema;
2680
+ if (!instructions || instructions.length > 4096) {
2681
+ throw new Error("Instructions are required and must be <= 4096 characters");
2682
+ }
2683
+ const createBody = {
2684
+ instructions,
2685
+ model
2632
2686
  };
2633
- if (args.category) searchBody.category = args.category;
2634
- if (args.includeDomains) searchBody.includeDomains = args.includeDomains;
2635
- if (args.startPublishedDate) searchBody.startPublishedDate = args.startPublishedDate;
2636
- const searchResponse = await fetch("https://api.exa.ai/search", {
2687
+ if (outputSchema) {
2688
+ createBody.outputSchema = outputSchema;
2689
+ }
2690
+ const createResponse = await fetch("https://api.exa.ai/research/v1", {
2637
2691
  method: "POST",
2638
2692
  headers: {
2639
2693
  "x-api-key": this.apiKey,
2640
2694
  "Content-Type": "application/json"
2641
2695
  },
2642
- body: JSON.stringify(searchBody)
2696
+ body: JSON.stringify(createBody)
2643
2697
  });
2644
- if (!searchResponse.ok) {
2645
- const errorText = await searchResponse.text();
2646
- throw new Error(`Exa research search failed (${searchResponse.status}): ${errorText}`);
2698
+ if (!createResponse.ok) {
2699
+ const errorText = await createResponse.text();
2700
+ throw new Error(`Failed to create research task (${createResponse.status}): ${errorText}`);
2647
2701
  }
2648
- const searchData = await searchResponse.json();
2649
- if (!searchData.results || searchData.results.length === 0) {
2650
- return `No research sources found for topic: "${topic}"`;
2651
- }
2652
- let similarResults = [];
2653
- if (searchData.results.length > 0) {
2654
- try {
2655
- const similarBody = {
2656
- url: searchData.results[0].url,
2657
- numResults: 3,
2658
- text: { maxCharacters: 2e3 }
2659
- };
2660
- const similarResponse = await fetch("https://api.exa.ai/findSimilar", {
2661
- method: "POST",
2662
- headers: {
2663
- "x-api-key": this.apiKey,
2664
- "Content-Type": "application/json"
2665
- },
2666
- body: JSON.stringify(similarBody)
2667
- });
2668
- if (similarResponse.ok) {
2669
- const similarData = await similarResponse.json();
2670
- similarResults = similarData.results ?? [];
2702
+ const createData = await createResponse.json();
2703
+ const researchId = createData.researchId;
2704
+ console.log(`Research task created: ${researchId}. Polling for results...`);
2705
+ const startTime = Date.now();
2706
+ let attempts = 0;
2707
+ while (Date.now() - startTime < this.maxPollTime) {
2708
+ attempts++;
2709
+ if (attempts > 1) {
2710
+ await new Promise((resolve7) => setTimeout(resolve7, this.pollInterval));
2711
+ }
2712
+ const statusResponse = await fetch(`https://api.exa.ai/research/v1/${researchId}`, {
2713
+ method: "GET",
2714
+ headers: {
2715
+ "x-api-key": this.apiKey
2671
2716
  }
2672
- } catch {
2717
+ });
2718
+ if (!statusResponse.ok) {
2719
+ const errorText = await statusResponse.text();
2720
+ throw new Error(`Failed to check research status (${statusResponse.status}): ${errorText}`);
2673
2721
  }
2674
- }
2675
- const allSources = [...searchData.results, ...similarResults];
2676
- const seenUrls = /* @__PURE__ */ new Set();
2677
- const uniqueSources = allSources.filter((s) => {
2678
- if (seenUrls.has(s.url)) return false;
2679
- seenUrls.add(s.url);
2680
- return true;
2681
- });
2682
- const sections = [];
2683
- sections.push(`# Research: ${topic}
2684
- `);
2685
- sections.push(`Found ${uniqueSources.length} sources.
2686
- `);
2687
- sections.push("## Key Sources\n");
2688
- for (let i = 0; i < uniqueSources.length; i++) {
2689
- const source = uniqueSources[i];
2690
- sections.push(`### ${i + 1}. ${source.title ?? "Untitled"}`);
2691
- sections.push(`URL: ${source.url}`);
2692
- if ("publishedDate" in source && source.publishedDate) {
2693
- sections.push(`Date: ${source.publishedDate}`);
2722
+ const statusData = await statusResponse.json();
2723
+ if (statusData.status === "completed") {
2724
+ const output = statusData.output;
2725
+ if (!output) {
2726
+ throw new Error("Research completed but no output was returned");
2727
+ }
2728
+ const sections = [];
2729
+ if (outputSchema && output.parsed) {
2730
+ sections.push("# Research Results (Structured)\n");
2731
+ sections.push(JSON.stringify(output.parsed, null, 2));
2732
+ } else {
2733
+ sections.push(output.content);
2734
+ }
2735
+ if (statusData.costDollars) {
2736
+ const cost = statusData.costDollars;
2737
+ sections.push("\n---\n");
2738
+ sections.push("## Research Metrics\n");
2739
+ sections.push(`- **Cost**: $${cost.total.toFixed(4)}`);
2740
+ sections.push(`- **Searches**: ${cost.numSearches}`);
2741
+ sections.push(`- **Pages analyzed**: ${cost.numPages}`);
2742
+ sections.push(`- **Reasoning tokens**: ${cost.reasoningTokens.toLocaleString()}`);
2743
+ }
2744
+ console.log(`Research completed in ${attempts} polls (${((Date.now() - startTime) / 1e3).toFixed(1)}s)`);
2745
+ return sections.join("\n");
2694
2746
  }
2695
- if ("author" in source && source.author) {
2696
- sections.push(`Author: ${source.author}`);
2747
+ if (statusData.status === "failed") {
2748
+ throw new Error(`Research failed: ${statusData.error ?? "Unknown error"}`);
2697
2749
  }
2698
- if (source.text) {
2699
- sections.push(`
2700
- Content:
2701
- ${source.text.slice(0, 2e3)}`);
2750
+ if (statusData.status === "canceled") {
2751
+ throw new Error("Research was canceled");
2752
+ }
2753
+ if (attempts % 10 === 0) {
2754
+ console.log(`Still researching... (${attempts} polls, ${((Date.now() - startTime) / 1e3).toFixed(0)}s elapsed)`);
2702
2755
  }
2703
- sections.push("");
2704
2756
  }
2705
- sections.push("## Source URLs\n");
2706
- uniqueSources.forEach((s, i) => {
2707
- sections.push(`${i + 1}. ${s.url}`);
2708
- });
2709
- return sections.join("\n");
2757
+ throw new Error(`Research timed out after ${this.maxPollTime / 1e3}s. Task ID: ${researchId}`);
2710
2758
  }
2711
2759
  };
2712
2760
 
@@ -2722,12 +2770,21 @@ var TOOL_REGISTRY = {
2722
2770
  };
2723
2771
  var YAMLLoader = class {
2724
2772
  customTools = /* @__PURE__ */ new Map();
2773
+ toolInstances = /* @__PURE__ */ new Map();
2725
2774
  /**
2726
- * Register a custom tool type for use in YAML definitions.
2775
+ * Register a custom tool type (class) for use in YAML definitions.
2727
2776
  */
2728
2777
  registerToolType(typeName, toolClass) {
2729
2778
  this.customTools.set(typeName, toolClass);
2730
2779
  }
2780
+ /**
2781
+ * Register a pre-built tool instance for use in YAML definitions.
2782
+ * Used by the custom tool system to register ProxyTool instances
2783
+ * that are created by the scanner rather than by class instantiation.
2784
+ */
2785
+ registerToolInstance(typeName, tool) {
2786
+ this.toolInstances.set(typeName, tool);
2787
+ }
2731
2788
  /**
2732
2789
  * Load a workflow from a YAML file path.
2733
2790
  */
@@ -2811,9 +2868,16 @@ var YAMLLoader = class {
2811
2868
  * Build a tool instance from a type name and config.
2812
2869
  */
2813
2870
  buildTool(name, type, config) {
2871
+ const existingInstance = this.toolInstances.get(type);
2872
+ if (existingInstance) {
2873
+ if (name !== type && name !== existingInstance.name) {
2874
+ Object.defineProperty(existingInstance, "name", { value: name, writable: false });
2875
+ }
2876
+ return existingInstance;
2877
+ }
2814
2878
  const ToolClass = TOOL_REGISTRY[type] ?? this.customTools.get(type);
2815
2879
  if (!ToolClass) {
2816
- throw new Error(`Unknown tool type: ${type}. Available types: ${[...Object.keys(TOOL_REGISTRY), ...this.customTools.keys()].join(", ")}`);
2880
+ throw new Error(`Unknown tool type: ${type}. Available types: ${[...Object.keys(TOOL_REGISTRY), ...this.customTools.keys(), ...this.toolInstances.keys()].join(", ")}`);
2817
2881
  }
2818
2882
  const tool = new ToolClass(config);
2819
2883
  if (name !== type && name !== tool.name) {
@@ -2841,11 +2905,16 @@ var YAMLLoader = class {
2841
2905
  if (tool) {
2842
2906
  agentTools.push(tool);
2843
2907
  } else {
2844
- const ToolClass = TOOL_REGISTRY[toolName] ?? this.customTools.get(toolName);
2845
- if (ToolClass) {
2846
- agentTools.push(new ToolClass());
2908
+ const instance = this.toolInstances.get(toolName);
2909
+ if (instance) {
2910
+ agentTools.push(instance);
2847
2911
  } else {
2848
- throw new Error(`Tool "${toolName}" not found for agent "${name}"`);
2912
+ const ToolClass = TOOL_REGISTRY[toolName] ?? this.customTools.get(toolName);
2913
+ if (ToolClass) {
2914
+ agentTools.push(new ToolClass());
2915
+ } else {
2916
+ throw new Error(`Tool "${toolName}" not found for agent "${name}"`);
2917
+ }
2849
2918
  }
2850
2919
  }
2851
2920
  }
@@ -2894,40 +2963,116 @@ var YAMLLoader = class {
2894
2963
  }
2895
2964
  };
2896
2965
 
2966
+ // src/output/JSONOutputHandler.ts
2967
+ var JSONOutputHandler = class {
2968
+ runId;
2969
+ startTime;
2970
+ constructor(config) {
2971
+ this.runId = config.runId;
2972
+ this.startTime = Date.now();
2973
+ }
2974
+ emit(type, data, extra = {}) {
2975
+ console.log(JSON.stringify({
2976
+ runId: this.runId,
2977
+ timestamp: Date.now(),
2978
+ type,
2979
+ ...extra,
2980
+ data
2981
+ }));
2982
+ }
2983
+ emitRunStart(workflowPath, task, cwd) {
2984
+ this.emit("run_start", { workflowPath, task, cwd });
2985
+ }
2986
+ emitWorkflowLoaded(name, description, agents, tools, entrypoint) {
2987
+ this.emit("workflow_loaded", { name, description, agents, tools, entrypoint });
2988
+ }
2989
+ emitRunEnd(success, output, totalTokens, totalSteps) {
2990
+ this.emit("run_end", {
2991
+ success,
2992
+ output,
2993
+ totalDuration: Date.now() - this.startTime,
2994
+ totalTokens,
2995
+ totalSteps
2996
+ });
2997
+ }
2998
+ emitAgentStart(agentName, depth, task, agentType, maxSteps) {
2999
+ this.emit("agent_start", { task, agentType, maxSteps }, { agentName, depth });
3000
+ }
3001
+ emitAgentEnd(agentName, depth, output, totalSteps, tokenUsage, duration, success) {
3002
+ this.emit("agent_end", { output, totalSteps, tokenUsage, duration, success }, { agentName, depth });
3003
+ }
3004
+ emitAgentStep(agentName, depth, stepNumber, maxSteps, phase) {
3005
+ this.emit("agent_step", { stepNumber, maxSteps, phase }, { agentName, depth });
3006
+ }
3007
+ emitAgentThinking(agentName, depth, stepNumber, content, isPartial) {
3008
+ this.emit("agent_thinking", { stepNumber, content, isPartial }, { agentName, depth });
3009
+ }
3010
+ emitToolCall(agentName, depth, stepNumber, toolCallId, toolName, args) {
3011
+ this.emit("agent_tool_call", { stepNumber, toolCallId, toolName, arguments: args }, { agentName, depth });
3012
+ }
3013
+ emitToolResult(agentName, depth, stepNumber, toolCallId, toolName, result, error, duration) {
3014
+ this.emit("agent_tool_result", { stepNumber, toolCallId, toolName, result, error, duration }, { agentName, depth });
3015
+ }
3016
+ emitObservation(agentName, depth, stepNumber, observation, codeAction, logs) {
3017
+ this.emit("agent_observation", { stepNumber, observation, codeAction, logs }, { agentName, depth });
3018
+ }
3019
+ emitError(message, stack, agentName, depth, stepNumber) {
3020
+ this.emit("error", { message, stack, stepNumber }, {
3021
+ ...agentName ? { agentName } : {},
3022
+ ...depth !== void 0 ? { depth } : {}
3023
+ });
3024
+ }
3025
+ };
3026
+
2897
3027
  // src/orchestrator/Orchestrator.ts
2898
3028
  var Orchestrator = class {
2899
3029
  loader;
2900
3030
  config;
2901
3031
  activeAgents = /* @__PURE__ */ new Map();
2902
3032
  eventLog = [];
3033
+ jsonOutput = null;
3034
+ isJsonMode = false;
2903
3035
  constructor(config = {}) {
2904
3036
  this.loader = new YAMLLoader();
3037
+ this.isJsonMode = config.outputFormat === "json";
2905
3038
  this.config = {
2906
3039
  verbose: config.verbose ?? true,
2907
- onEvent: config.onEvent
3040
+ onEvent: config.onEvent,
3041
+ outputFormat: config.outputFormat ?? "text",
3042
+ runId: config.runId,
3043
+ cwd: config.cwd
2908
3044
  };
3045
+ if (this.isJsonMode) {
3046
+ if (!config.runId) {
3047
+ throw new Error("runId is required for JSON output mode");
3048
+ }
3049
+ this.jsonOutput = new JSONOutputHandler({
3050
+ runId: config.runId,
3051
+ verbose: config.verbose ?? true
3052
+ });
3053
+ }
2909
3054
  }
2910
3055
  /**
2911
3056
  * Load a workflow from a YAML file.
2912
3057
  */
2913
3058
  loadWorkflow(filePath) {
2914
3059
  const workflow = this.loader.loadFromFile(filePath);
2915
- this.displayWorkflowInfo(workflow);
3060
+ this.displayWorkflowInfo(workflow, filePath);
2916
3061
  return workflow;
2917
3062
  }
2918
3063
  /**
2919
3064
  * Load a workflow from YAML string.
2920
3065
  */
2921
- loadWorkflowFromString(yamlContent) {
3066
+ loadWorkflowFromString(yamlContent, sourcePath) {
2922
3067
  const workflow = this.loader.loadFromString(yamlContent);
2923
- this.displayWorkflowInfo(workflow);
3068
+ this.displayWorkflowInfo(workflow, sourcePath);
2924
3069
  return workflow;
2925
3070
  }
2926
3071
  /**
2927
3072
  * Run a loaded workflow with a task.
2928
3073
  */
2929
- async runWorkflow(workflow, task) {
2930
- this.displayRunStart(workflow.name, task);
3074
+ async runWorkflow(workflow, task, workflowPath) {
3075
+ this.displayRunStart(workflow.name, task, workflowPath);
2931
3076
  this.instrumentAgent(workflow.entrypointAgent, workflow.entrypointAgent.getName(), 0);
2932
3077
  for (const [name, agent] of workflow.agents) {
2933
3078
  if (agent !== workflow.entrypointAgent) {
@@ -2936,10 +3081,13 @@ var Orchestrator = class {
2936
3081
  }
2937
3082
  try {
2938
3083
  const result = await workflow.entrypointAgent.run(task);
2939
- this.displayRunEnd(result);
3084
+ this.displayRunEnd(result, true);
2940
3085
  return result;
2941
3086
  } catch (error) {
2942
3087
  this.displayError(error);
3088
+ if (this.isJsonMode && this.jsonOutput) {
3089
+ this.jsonOutput.emitRunEnd(false, null, 0, 0);
3090
+ }
2943
3091
  throw error;
2944
3092
  }
2945
3093
  }
@@ -2956,11 +3104,128 @@ var Orchestrator = class {
2956
3104
  */
2957
3105
  instrumentAgent(agent, name, depth) {
2958
3106
  this.activeAgents.set(name, { agent, depth });
3107
+ agent.setOnEvent((event) => {
3108
+ const orchestratorEvent = {
3109
+ type: event.type,
3110
+ agentName: name,
3111
+ depth,
3112
+ data: event.data,
3113
+ timestamp: Date.now()
3114
+ };
3115
+ this.logEvent(orchestratorEvent);
3116
+ if (this.isJsonMode && this.jsonOutput) {
3117
+ this.emitAgentEventAsJSON(event, name, depth);
3118
+ }
3119
+ });
3120
+ }
3121
+ /**
3122
+ * Emit an agent event as JSON.
3123
+ */
3124
+ emitAgentEventAsJSON(event, agentName, depth) {
3125
+ if (!this.jsonOutput) return;
3126
+ const data = event.data;
3127
+ switch (event.type) {
3128
+ case "agent_start":
3129
+ this.jsonOutput.emitAgentStart(
3130
+ agentName,
3131
+ depth,
3132
+ data.task,
3133
+ data.name ? "ToolUseAgent" : "CodeAgent",
3134
+ // Will be improved
3135
+ 20
3136
+ // Default maxSteps, could be passed
3137
+ );
3138
+ break;
3139
+ case "agent_step":
3140
+ this.jsonOutput.emitAgentStep(
3141
+ agentName,
3142
+ depth,
3143
+ data.step,
3144
+ data.maxSteps,
3145
+ "start"
3146
+ );
3147
+ break;
3148
+ case "agent_thinking":
3149
+ this.jsonOutput.emitAgentThinking(
3150
+ agentName,
3151
+ depth,
3152
+ data.step,
3153
+ data.content,
3154
+ false
3155
+ );
3156
+ break;
3157
+ case "agent_tool_call":
3158
+ this.jsonOutput.emitToolCall(
3159
+ agentName,
3160
+ depth,
3161
+ data.step,
3162
+ data.toolCallId,
3163
+ data.toolName,
3164
+ data.arguments
3165
+ );
3166
+ break;
3167
+ case "agent_tool_result":
3168
+ this.jsonOutput.emitToolResult(
3169
+ agentName,
3170
+ depth,
3171
+ data.step,
3172
+ data.toolCallId,
3173
+ data.toolName,
3174
+ data.result,
3175
+ data.error,
3176
+ data.duration
3177
+ );
3178
+ break;
3179
+ case "agent_observation":
3180
+ this.jsonOutput.emitObservation(
3181
+ agentName,
3182
+ depth,
3183
+ data.step,
3184
+ data.observation,
3185
+ data.codeAction,
3186
+ data.logs
3187
+ );
3188
+ break;
3189
+ case "agent_error":
3190
+ this.jsonOutput.emitError(
3191
+ data.error,
3192
+ void 0,
3193
+ agentName,
3194
+ depth,
3195
+ data.step
3196
+ );
3197
+ break;
3198
+ case "agent_end":
3199
+ this.jsonOutput.emitAgentEnd(
3200
+ agentName,
3201
+ depth,
3202
+ data.output,
3203
+ 0,
3204
+ // totalSteps - would need to track
3205
+ data.tokenUsage,
3206
+ data.duration,
3207
+ true
3208
+ );
3209
+ break;
3210
+ }
2959
3211
  }
2960
3212
  /**
2961
3213
  * Display workflow info at startup.
2962
3214
  */
2963
- displayWorkflowInfo(workflow) {
3215
+ displayWorkflowInfo(workflow, _sourcePath) {
3216
+ const agents = Array.from(workflow.agents.keys());
3217
+ const tools = Array.from(workflow.tools.keys());
3218
+ const entrypoint = workflow.entrypointAgent.getName();
3219
+ if (this.isJsonMode && this.jsonOutput) {
3220
+ this.jsonOutput.emitWorkflowLoaded(
3221
+ workflow.name,
3222
+ workflow.description,
3223
+ agents,
3224
+ tools,
3225
+ entrypoint
3226
+ );
3227
+ return;
3228
+ }
2964
3229
  if (!this.config.verbose) return;
2965
3230
  const line = "\u2550".repeat(70);
2966
3231
  console.log(import_chalk2.default.cyan(line));
@@ -2968,16 +3233,20 @@ var Orchestrator = class {
2968
3233
  if (workflow.description) {
2969
3234
  console.log(import_chalk2.default.cyan(` ${workflow.description}`));
2970
3235
  }
2971
- console.log(import_chalk2.default.cyan(` Agents: ${Array.from(workflow.agents.keys()).join(", ")}`));
2972
- console.log(import_chalk2.default.cyan(` Tools: ${Array.from(workflow.tools.keys()).join(", ") || "(none defined at workflow level)"}`));
2973
- console.log(import_chalk2.default.cyan(` Entrypoint: ${workflow.entrypointAgent.getName()}`));
3236
+ console.log(import_chalk2.default.cyan(` Agents: ${agents.join(", ")}`));
3237
+ console.log(import_chalk2.default.cyan(` Tools: ${tools.join(", ") || "(none defined at workflow level)"}`));
3238
+ console.log(import_chalk2.default.cyan(` Entrypoint: ${entrypoint}`));
2974
3239
  console.log(import_chalk2.default.cyan(line));
2975
3240
  console.log();
2976
3241
  }
2977
3242
  /**
2978
3243
  * Display run start info.
2979
3244
  */
2980
- displayRunStart(workflowName, task) {
3245
+ displayRunStart(workflowName, task, workflowPath) {
3246
+ if (this.isJsonMode && this.jsonOutput) {
3247
+ this.jsonOutput.emitRunStart(workflowPath || workflowName, task, this.config.cwd);
3248
+ return;
3249
+ }
2981
3250
  if (!this.config.verbose) return;
2982
3251
  console.log(import_chalk2.default.green.bold(`
2983
3252
  \u25B6 Running workflow "${workflowName}"`));
@@ -2987,7 +3256,16 @@ var Orchestrator = class {
2987
3256
  /**
2988
3257
  * Display run completion info.
2989
3258
  */
2990
- displayRunEnd(result) {
3259
+ displayRunEnd(result, success = true) {
3260
+ if (this.isJsonMode && this.jsonOutput) {
3261
+ this.jsonOutput.emitRunEnd(
3262
+ success,
3263
+ result.output,
3264
+ result.tokenUsage.totalTokens,
3265
+ result.steps.length
3266
+ );
3267
+ return;
3268
+ }
2991
3269
  if (!this.config.verbose) return;
2992
3270
  console.log(import_chalk2.default.gray("\n" + "\u2500".repeat(70)));
2993
3271
  console.log(import_chalk2.default.green.bold(`
@@ -3005,6 +3283,10 @@ var Orchestrator = class {
3005
3283
  * Display an error.
3006
3284
  */
3007
3285
  displayError(error) {
3286
+ if (this.isJsonMode && this.jsonOutput) {
3287
+ this.jsonOutput.emitError(error.message, error.stack);
3288
+ return;
3289
+ }
3008
3290
  if (!this.config.verbose) return;
3009
3291
  console.error(import_chalk2.default.red.bold(`
3010
3292
  \u274C Workflow failed: ${error.message}`));
@@ -3033,8 +3315,321 @@ var Orchestrator = class {
3033
3315
  getLoader() {
3034
3316
  return this.loader;
3035
3317
  }
3318
+ /**
3319
+ * Get the JSON output handler (if in JSON mode).
3320
+ */
3321
+ getJSONOutputHandler() {
3322
+ return this.jsonOutput;
3323
+ }
3324
+ /**
3325
+ * Check if in JSON output mode.
3326
+ */
3327
+ isJSONOutputMode() {
3328
+ return this.isJsonMode;
3329
+ }
3330
+ /**
3331
+ * Get the run ID.
3332
+ */
3333
+ getRunId() {
3334
+ return this.config.runId;
3335
+ }
3036
3336
  };
3037
3337
 
3338
+ // src/tools/CustomToolScanner.ts
3339
+ var fs7 = __toESM(require("fs"));
3340
+ var path7 = __toESM(require("path"));
3341
+
3342
+ // src/tools/ProxyTool.ts
3343
+ var import_child_process2 = require("child_process");
3344
+
3345
+ // src/utils/bunInstaller.ts
3346
+ var import_child_process = require("child_process");
3347
+ var path6 = __toESM(require("path"));
3348
+ var fs6 = __toESM(require("fs"));
3349
+ var os3 = __toESM(require("os"));
3350
+ var cachedBunPath = null;
3351
+ async function ensureBunAvailable() {
3352
+ if (cachedBunPath) return cachedBunPath;
3353
+ const fromPath = whichBun();
3354
+ if (fromPath) {
3355
+ cachedBunPath = fromPath;
3356
+ return fromPath;
3357
+ }
3358
+ const localPath = path6.join(os3.homedir(), ".bun", "bin", "bun");
3359
+ if (fs6.existsSync(localPath)) {
3360
+ cachedBunPath = localPath;
3361
+ return localPath;
3362
+ }
3363
+ console.log(
3364
+ "\n[smol-js] Bun is required to run custom tools but was not found. Installing Bun automatically...\n"
3365
+ );
3366
+ try {
3367
+ (0, import_child_process.execSync)("curl --proto =https --tlsv1.2 -sSf https://bun.sh | bash", {
3368
+ stdio: "inherit",
3369
+ shell: "/bin/bash",
3370
+ env: { ...process.env, HOME: os3.homedir() }
3371
+ });
3372
+ } catch (err) {
3373
+ throw new Error(
3374
+ `[smol-js] Failed to auto-install Bun. Please install it manually: https://bun.sh
3375
+ Details: ${err.message}`
3376
+ );
3377
+ }
3378
+ const afterInstall = whichBun() || (fs6.existsSync(localPath) ? localPath : null);
3379
+ if (!afterInstall) {
3380
+ throw new Error(
3381
+ "[smol-js] Bun installation appeared to succeed but the binary was not found. Please install manually: https://bun.sh"
3382
+ );
3383
+ }
3384
+ console.log(`[smol-js] Bun installed successfully at: ${afterInstall}
3385
+ `);
3386
+ cachedBunPath = afterInstall;
3387
+ return afterInstall;
3388
+ }
3389
+ function whichBun() {
3390
+ try {
3391
+ const cmd = process.platform === "win32" ? "where bun" : "which bun";
3392
+ const result = (0, import_child_process.execSync)(cmd, { encoding: "utf8", stdio: "pipe" }).trim();
3393
+ const first = result.split("\n")[0]?.trim();
3394
+ if (first && fs6.existsSync(first)) return first;
3395
+ return null;
3396
+ } catch {
3397
+ return null;
3398
+ }
3399
+ }
3400
+
3401
+ // src/tools/ProxyTool.ts
3402
+ var TOOL_OUTPUT_PREFIX = "[TOOL_OUTPUT]";
3403
+ var TOOL_RESULT_PREFIX = "[TOOL_RESULT]";
3404
+ var TOOL_ERROR_PREFIX = "[TOOL_ERROR]";
3405
+ var DEFAULT_TOOL_TIMEOUT_MS = 6e4;
3406
+ var ProxyTool = class extends Tool {
3407
+ name;
3408
+ description;
3409
+ inputs;
3410
+ outputType;
3411
+ toolPath;
3412
+ timeout;
3413
+ bunPath = null;
3414
+ constructor(config) {
3415
+ super();
3416
+ this.name = config.name;
3417
+ this.description = config.description;
3418
+ this.inputs = config.inputs;
3419
+ this.outputType = config.outputType;
3420
+ this.toolPath = config.toolPath;
3421
+ this.timeout = config.timeout ?? DEFAULT_TOOL_TIMEOUT_MS;
3422
+ }
3423
+ /**
3424
+ * Ensure Bun is available before first invocation.
3425
+ */
3426
+ async setup() {
3427
+ this.bunPath = await ensureBunAvailable();
3428
+ this.isSetup = true;
3429
+ }
3430
+ /**
3431
+ * Spawn the tool in a Bun child process, pass serialized args via CLI,
3432
+ * stream stdout back as log lines, and parse the final result.
3433
+ */
3434
+ async execute(args) {
3435
+ if (!this.bunPath) {
3436
+ await this.setup();
3437
+ }
3438
+ const serializedArgs = JSON.stringify(args);
3439
+ return new Promise((resolve7, reject) => {
3440
+ const child = (0, import_child_process2.spawn)(this.bunPath, ["run", this.toolPath, serializedArgs], {
3441
+ stdio: ["pipe", "pipe", "pipe"],
3442
+ env: { ...process.env }
3443
+ });
3444
+ let result = void 0;
3445
+ let resultReceived = false;
3446
+ let errorMessage = null;
3447
+ const logBuffer = [];
3448
+ let stderr = "";
3449
+ let partialLine = "";
3450
+ child.stdout.on("data", (chunk) => {
3451
+ partialLine += chunk.toString("utf8");
3452
+ const lines = partialLine.split("\n");
3453
+ partialLine = lines.pop();
3454
+ for (const line of lines) {
3455
+ this.processLine(line, {
3456
+ onOutput: (msg) => logBuffer.push(msg),
3457
+ onResult: (value) => {
3458
+ result = value;
3459
+ resultReceived = true;
3460
+ },
3461
+ onError: (msg) => {
3462
+ errorMessage = msg;
3463
+ }
3464
+ });
3465
+ }
3466
+ });
3467
+ child.stderr.on("data", (chunk) => {
3468
+ stderr += chunk.toString("utf8");
3469
+ });
3470
+ const timer = setTimeout(() => {
3471
+ child.kill("SIGTERM");
3472
+ reject(new Error(
3473
+ `Custom tool "${this.name}" timed out after ${this.timeout}ms. The process was terminated. Check the tool for infinite loops or slow operations.`
3474
+ ));
3475
+ }, this.timeout);
3476
+ timer.unref();
3477
+ child.on("close", (code) => {
3478
+ clearTimeout(timer);
3479
+ if (partialLine.trim()) {
3480
+ this.processLine(partialLine, {
3481
+ onOutput: (msg) => logBuffer.push(msg),
3482
+ onResult: (value) => {
3483
+ result = value;
3484
+ resultReceived = true;
3485
+ },
3486
+ onError: (msg) => {
3487
+ errorMessage = msg;
3488
+ }
3489
+ });
3490
+ }
3491
+ if (errorMessage) {
3492
+ reject(new Error(
3493
+ `Custom tool "${this.name}" reported an error: ${errorMessage}`
3494
+ ));
3495
+ return;
3496
+ }
3497
+ if (resultReceived) {
3498
+ if (logBuffer.length > 0) {
3499
+ const logPrefix = `[Tool output logs]
3500
+ ${logBuffer.join("\n")}
3501
+
3502
+ [Tool result]
3503
+ `;
3504
+ if (typeof result === "string") {
3505
+ resolve7(logPrefix + result);
3506
+ } else {
3507
+ resolve7({ logs: logBuffer.join("\n"), result });
3508
+ }
3509
+ } else {
3510
+ resolve7(result);
3511
+ }
3512
+ return;
3513
+ }
3514
+ const combined = (logBuffer.join("\n") + "\n" + stderr).trim();
3515
+ if (code !== 0) {
3516
+ reject(new Error(
3517
+ `Custom tool "${this.name}" exited with code ${code}. Output: ${combined || "(none)"}`
3518
+ ));
3519
+ } else {
3520
+ resolve7(combined || `Tool "${this.name}" produced no output.`);
3521
+ }
3522
+ });
3523
+ child.on("error", (err) => {
3524
+ clearTimeout(timer);
3525
+ reject(new Error(
3526
+ `Failed to spawn custom tool "${this.name}": ${err.message}`
3527
+ ));
3528
+ });
3529
+ });
3530
+ }
3531
+ // --- internal line parser ---
3532
+ processLine(line, handlers) {
3533
+ const trimmed = line.trimEnd();
3534
+ if (!trimmed) return;
3535
+ if (trimmed.startsWith(TOOL_RESULT_PREFIX)) {
3536
+ const json = trimmed.slice(TOOL_RESULT_PREFIX.length).trim();
3537
+ try {
3538
+ handlers.onResult(JSON.parse(json));
3539
+ } catch {
3540
+ handlers.onResult(json);
3541
+ }
3542
+ } else if (trimmed.startsWith(TOOL_ERROR_PREFIX)) {
3543
+ handlers.onError(trimmed.slice(TOOL_ERROR_PREFIX.length).trim());
3544
+ } else if (trimmed.startsWith(TOOL_OUTPUT_PREFIX)) {
3545
+ handlers.onOutput(trimmed.slice(TOOL_OUTPUT_PREFIX.length).trim());
3546
+ } else {
3547
+ handlers.onOutput(trimmed);
3548
+ }
3549
+ }
3550
+ };
3551
+
3552
+ // src/tools/CustomToolScanner.ts
3553
+ var METADATA_REGEX = /export\s+const\s+TOOL_METADATA\s*=\s*(\{[\s\S]*?\});\s*$/m;
3554
+ function scanCustomTools(folderPath) {
3555
+ if (!fs7.existsSync(folderPath)) {
3556
+ throw new Error(
3557
+ `Custom tools folder not found: ${folderPath}. Create the directory or check your --custom-tools-folder path.`
3558
+ );
3559
+ }
3560
+ const entries = fs7.readdirSync(folderPath, { withFileTypes: true });
3561
+ const discovered = [];
3562
+ for (const entry of entries) {
3563
+ if (entry.isDirectory()) continue;
3564
+ const ext = path7.extname(entry.name).toLowerCase();
3565
+ if (ext !== ".ts" && ext !== ".js") continue;
3566
+ const filePath = path7.resolve(folderPath, entry.name);
3567
+ const baseName = path7.basename(entry.name, ext);
3568
+ let metadata;
3569
+ try {
3570
+ metadata = extractMetadata(filePath);
3571
+ } catch (err) {
3572
+ throw new Error(
3573
+ `Failed to extract TOOL_METADATA from "${entry.name}": ${err.message}
3574
+ Ensure the file exports \`export const TOOL_METADATA = { name, description, inputs, outputType };\``
3575
+ );
3576
+ }
3577
+ if (metadata.name !== baseName) {
3578
+ throw new Error(
3579
+ `Tool metadata name mismatch in "${entry.name}": file is "${baseName}" but TOOL_METADATA.name is "${metadata.name}". They must match (Convention over Configuration).`
3580
+ );
3581
+ }
3582
+ discovered.push({ filePath, metadata });
3583
+ }
3584
+ return discovered;
3585
+ }
3586
+ function extractMetadata(filePath) {
3587
+ const source = fs7.readFileSync(filePath, "utf8");
3588
+ const match = source.match(METADATA_REGEX);
3589
+ if (!match) {
3590
+ throw new Error(
3591
+ "No `export const TOOL_METADATA = { ... };` block found. Add the metadata export at the bottom of your tool file."
3592
+ );
3593
+ }
3594
+ let parsed;
3595
+ try {
3596
+ parsed = new Function(`"use strict"; return (${match[1]});`)();
3597
+ } catch (err) {
3598
+ throw new Error(
3599
+ `Could not parse TOOL_METADATA object: ${err.message}. Ensure it is a valid JavaScript object literal.`
3600
+ );
3601
+ }
3602
+ if (!parsed.name || typeof parsed.name !== "string") {
3603
+ throw new Error("TOOL_METADATA.name must be a non-empty string.");
3604
+ }
3605
+ if (!parsed.description || typeof parsed.description !== "string") {
3606
+ throw new Error("TOOL_METADATA.description must be a non-empty string.");
3607
+ }
3608
+ if (!parsed.inputs || typeof parsed.inputs !== "object") {
3609
+ throw new Error("TOOL_METADATA.inputs must be an object mapping parameter names to their schemas.");
3610
+ }
3611
+ if (!parsed.outputType || typeof parsed.outputType !== "string") {
3612
+ throw new Error("TOOL_METADATA.outputType must be a non-empty string.");
3613
+ }
3614
+ return parsed;
3615
+ }
3616
+ function loadCustomTools(folderPath) {
3617
+ const discovered = scanCustomTools(folderPath);
3618
+ const tools = /* @__PURE__ */ new Map();
3619
+ for (const { filePath, metadata } of discovered) {
3620
+ const config = {
3621
+ toolPath: filePath,
3622
+ name: metadata.name,
3623
+ description: metadata.description,
3624
+ inputs: metadata.inputs,
3625
+ outputType: metadata.outputType,
3626
+ timeout: metadata.timeout
3627
+ };
3628
+ tools.set(metadata.name, new ProxyTool(config));
3629
+ }
3630
+ return tools;
3631
+ }
3632
+
3038
3633
  // src/cli.ts
3039
3634
  import_dotenv.default.config();
3040
3635
  async function main() {
@@ -3064,14 +3659,20 @@ function printUsage() {
3064
3659
  console.log(" smol-js validate <workflow.yaml> Validate a workflow file");
3065
3660
  console.log("");
3066
3661
  console.log("Options:");
3067
- console.log(" --task, -t <task> Task description (prompted if not provided)");
3068
- console.log(" --quiet, -q Reduce output verbosity");
3069
- console.log(" --help, -h Show this help message");
3662
+ console.log(" --task, -t <task> Task description (prompted if not provided)");
3663
+ console.log(" --quiet, -q Reduce output verbosity");
3664
+ console.log(" --output-format <format> Output format: text (default) or json");
3665
+ console.log(" --verbose Include full step details in output");
3666
+ console.log(" --run-id <id> Unique run identifier (auto-generated if not provided)");
3667
+ console.log(" --cwd <dir> Working directory for agent file operations");
3668
+ console.log(" --custom-tools-folder <path> Path to folder containing standalone custom tool files");
3669
+ console.log(" --help, -h Show this help message");
3070
3670
  console.log("");
3071
3671
  console.log("Examples:");
3072
3672
  console.log(' npx @samrahimi/smol-js workflow.yaml --task "Research AI safety"');
3073
3673
  console.log(' smol-js research-agent.yaml -t "Write a summary of quantum computing"');
3074
3674
  console.log(" smol-js validate my-workflow.yaml");
3675
+ console.log(' smol-js workflow.yaml --task "Task" --output-format json --run-id abc123');
3075
3676
  }
3076
3677
  async function runCommand(args) {
3077
3678
  if (args.length === 0) {
@@ -3081,39 +3682,130 @@ async function runCommand(args) {
3081
3682
  const filePath = args[0];
3082
3683
  let task = "";
3083
3684
  let quiet = false;
3685
+ let outputFormat = "text";
3686
+ let verbose = false;
3687
+ let runId = "";
3688
+ let cwd = "";
3689
+ let customToolsFolder = "";
3084
3690
  for (let i = 1; i < args.length; i++) {
3085
3691
  if (args[i] === "--task" || args[i] === "-t") {
3086
3692
  task = args[i + 1] ?? "";
3087
3693
  i++;
3088
3694
  } else if (args[i] === "--quiet" || args[i] === "-q") {
3089
3695
  quiet = true;
3696
+ } else if (args[i] === "--output-format") {
3697
+ const format = args[i + 1];
3698
+ if (format === "json" || format === "text") {
3699
+ outputFormat = format;
3700
+ } else {
3701
+ console.error(import_chalk3.default.red(`Invalid output format: ${format}. Use 'text' or 'json'.`));
3702
+ process.exit(1);
3703
+ }
3704
+ i++;
3705
+ } else if (args[i] === "--verbose") {
3706
+ verbose = true;
3707
+ } else if (args[i] === "--run-id") {
3708
+ runId = args[i + 1] ?? "";
3709
+ i++;
3710
+ } else if (args[i] === "--cwd") {
3711
+ cwd = args[i + 1] ?? "";
3712
+ i++;
3713
+ } else if (args[i] === "--custom-tools-folder") {
3714
+ customToolsFolder = args[i + 1] ?? "";
3715
+ i++;
3090
3716
  }
3091
3717
  }
3092
- const resolvedPath = path6.isAbsolute(filePath) ? filePath : path6.resolve(process.cwd(), filePath);
3093
- if (!fs6.existsSync(resolvedPath)) {
3094
- console.error(import_chalk3.default.red(`Error: file not found: ${resolvedPath}`));
3718
+ if (outputFormat === "json" && !runId) {
3719
+ runId = `run-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
3720
+ }
3721
+ const resolvedPath = path8.isAbsolute(filePath) ? filePath : path8.resolve(process.cwd(), filePath);
3722
+ if (!fs8.existsSync(resolvedPath)) {
3723
+ if (outputFormat === "json") {
3724
+ console.log(JSON.stringify({
3725
+ runId: runId || "unknown",
3726
+ timestamp: Date.now(),
3727
+ type: "error",
3728
+ data: { message: `File not found: ${resolvedPath}` }
3729
+ }));
3730
+ } else {
3731
+ console.error(import_chalk3.default.red(`Error: file not found: ${resolvedPath}`));
3732
+ }
3095
3733
  process.exit(1);
3096
3734
  }
3097
3735
  if (!task) {
3736
+ if (outputFormat === "json") {
3737
+ console.log(JSON.stringify({
3738
+ runId: runId || "unknown",
3739
+ timestamp: Date.now(),
3740
+ type: "error",
3741
+ data: { message: "Task is required in JSON output mode. Use --task flag." }
3742
+ }));
3743
+ process.exit(1);
3744
+ }
3098
3745
  task = await promptUser("Enter your task: ");
3099
3746
  if (!task.trim()) {
3100
3747
  console.error(import_chalk3.default.red("Error: task cannot be empty"));
3101
3748
  process.exit(1);
3102
3749
  }
3103
3750
  }
3104
- const orchestrator = new Orchestrator({ verbose: !quiet });
3751
+ if (cwd) {
3752
+ const resolvedCwd = path8.isAbsolute(cwd) ? cwd : path8.resolve(process.cwd(), cwd);
3753
+ if (!fs8.existsSync(resolvedCwd)) {
3754
+ fs8.mkdirSync(resolvedCwd, { recursive: true });
3755
+ }
3756
+ process.chdir(resolvedCwd);
3757
+ }
3758
+ const orchestrator = new Orchestrator({
3759
+ verbose: outputFormat === "json" ? verbose : !quiet,
3760
+ outputFormat,
3761
+ runId: runId || void 0,
3762
+ cwd: cwd || void 0
3763
+ });
3764
+ if (customToolsFolder) {
3765
+ const resolvedToolsFolder = path8.isAbsolute(customToolsFolder) ? customToolsFolder : path8.resolve(process.cwd(), customToolsFolder);
3766
+ if (outputFormat !== "json") {
3767
+ console.log(import_chalk3.default.gray(`
3768
+ Scanning custom tools in: ${resolvedToolsFolder}
3769
+ `));
3770
+ }
3771
+ const customTools = loadCustomTools(resolvedToolsFolder);
3772
+ const loader = orchestrator.getLoader();
3773
+ for (const [toolName, proxyTool] of customTools) {
3774
+ loader.registerToolInstance(toolName, proxyTool);
3775
+ if (outputFormat !== "json") {
3776
+ console.log(import_chalk3.default.green(` + Registered custom tool: ${toolName}`));
3777
+ }
3778
+ }
3779
+ }
3105
3780
  try {
3106
- console.log(import_chalk3.default.gray(`
3781
+ if (outputFormat !== "json") {
3782
+ console.log(import_chalk3.default.gray(`
3107
3783
  Loading workflow from: ${resolvedPath}
3108
3784
  `));
3785
+ }
3109
3786
  const workflow = orchestrator.loadWorkflow(resolvedPath);
3110
- await orchestrator.runWorkflow(workflow, task);
3787
+ await orchestrator.runWorkflow(workflow, task, resolvedPath);
3111
3788
  process.exit(0);
3112
3789
  } catch (error) {
3113
- console.error(import_chalk3.default.red(`
3790
+ if (outputFormat === "json") {
3791
+ const jsonHandler = orchestrator.getJSONOutputHandler();
3792
+ if (jsonHandler) {
3793
+ jsonHandler.emitError(error.message, error.stack);
3794
+ jsonHandler.emitRunEnd(false, null, 0, 0);
3795
+ } else {
3796
+ console.log(JSON.stringify({
3797
+ runId: runId || "unknown",
3798
+ timestamp: Date.now(),
3799
+ type: "error",
3800
+ data: { message: error.message, stack: error.stack }
3801
+ }));
3802
+ }
3803
+ } else {
3804
+ console.error(import_chalk3.default.red(`
3114
3805
  Error: ${error.message}`));
3115
- if (process.env.DEBUG) {
3116
- console.error(error.stack);
3806
+ if (process.env.DEBUG) {
3807
+ console.error(error.stack);
3808
+ }
3117
3809
  }
3118
3810
  process.exit(1);
3119
3811
  }
@@ -3124,8 +3816,8 @@ async function validateCommand(args) {
3124
3816
  process.exit(1);
3125
3817
  }
3126
3818
  const filePath = args[0];
3127
- const resolvedPath = path6.isAbsolute(filePath) ? filePath : path6.resolve(process.cwd(), filePath);
3128
- if (!fs6.existsSync(resolvedPath)) {
3819
+ const resolvedPath = path8.isAbsolute(filePath) ? filePath : path8.resolve(process.cwd(), filePath);
3820
+ if (!fs8.existsSync(resolvedPath)) {
3129
3821
  console.error(import_chalk3.default.red(`Error: file not found: ${resolvedPath}`));
3130
3822
  process.exit(1);
3131
3823
  }
@@ -3148,10 +3840,10 @@ function promptUser(question) {
3148
3840
  input: process.stdin,
3149
3841
  output: process.stdout
3150
3842
  });
3151
- return new Promise((resolve6) => {
3843
+ return new Promise((resolve7) => {
3152
3844
  rl.question(import_chalk3.default.cyan(question), (answer) => {
3153
3845
  rl.close();
3154
- resolve6(answer);
3846
+ resolve7(answer);
3155
3847
  });
3156
3848
  });
3157
3849
  }