@samrahimi/smol-js 0.6.5 → 0.7.1

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
@@ -24,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
- var fs6 = __toESM(require("fs"));
28
- var path6 = __toESM(require("path"));
27
+ var fs8 = __toESM(require("fs"));
28
+ var path9 = __toESM(require("path"));
29
29
  var readline = __toESM(require("readline"));
30
30
  var import_chalk3 = __toESM(require("chalk"));
31
31
  var import_dotenv = __toESM(require("dotenv"));
@@ -741,9 +741,21 @@ Total time: ${(duration / 1e3).toFixed(2)}s`);
741
741
  getName() {
742
742
  return this.config.name;
743
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
+ }
744
756
  /** Sleep for a specified duration */
745
757
  sleep(ms) {
746
- return new Promise((resolve6) => setTimeout(resolve6, ms));
758
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
747
759
  }
748
760
  };
749
761
 
@@ -1846,6 +1858,10 @@ Please try a different approach.`
1846
1858
  memoryStep.tokenUsage = response.tokenUsage;
1847
1859
  if (response.content && response.content.trim()) {
1848
1860
  this.logger.reasoning(response.content.trim());
1861
+ this.emitEvent("agent_thinking", {
1862
+ step: this.currentStep,
1863
+ content: response.content.trim()
1864
+ });
1849
1865
  }
1850
1866
  if (!response.toolCalls || response.toolCalls.length === 0) {
1851
1867
  this.logger.warn("No tool calls in response. Prompting model to use tools.");
@@ -1878,42 +1894,84 @@ Please try a different approach.`
1878
1894
  async processToolCalls(toolCalls) {
1879
1895
  const results = [];
1880
1896
  const executeTool = async (tc) => {
1897
+ const startTime = Date.now();
1881
1898
  const toolName = tc.function.name;
1882
1899
  const tool = this.tools.get(toolName);
1883
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
+ });
1884
1910
  return {
1885
1911
  toolCallId: tc.id,
1886
1912
  toolName,
1887
1913
  result: null,
1888
- error: `Unknown tool: ${toolName}. Available tools: ${Array.from(this.tools.keys()).join(", ")}`
1914
+ error
1889
1915
  };
1890
1916
  }
1891
1917
  let args;
1892
1918
  try {
1893
1919
  args = typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments;
1894
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
+ });
1895
1930
  return {
1896
1931
  toolCallId: tc.id,
1897
1932
  toolName,
1898
1933
  result: null,
1899
- error: `Failed to parse tool arguments: ${tc.function.arguments}`
1934
+ error
1900
1935
  };
1901
1936
  }
1902
1937
  this.logger.info(` Calling tool: ${toolName}(${JSON.stringify(args).slice(0, 100)}...)`);
1903
- 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
+ });
1904
1944
  try {
1905
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
+ });
1906
1954
  return {
1907
1955
  toolCallId: tc.id,
1908
1956
  toolName,
1909
1957
  result
1910
1958
  };
1911
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
+ });
1912
1970
  return {
1913
1971
  toolCallId: tc.id,
1914
1972
  toolName,
1915
1973
  result: null,
1916
- error: `Tool execution error: ${error.message}`
1974
+ error: errorMsg
1917
1975
  };
1918
1976
  }
1919
1977
  };
@@ -2036,7 +2094,7 @@ async function ${this.name}(task: string): Promise<string> { ... }
2036
2094
  };
2037
2095
 
2038
2096
  // src/models/OpenAIModel.ts
2039
- var import_openai = __toESM(require("openai"));
2097
+ var import_openai = __toESM(require("openai/index.mjs"));
2040
2098
 
2041
2099
  // src/models/Model.ts
2042
2100
  var Model = class {
@@ -2579,40 +2637,35 @@ var ExaGetContentsTool = class extends Tool {
2579
2637
  // src/tools/ExaResearchTool.ts
2580
2638
  var ExaResearchTool = class extends Tool {
2581
2639
  name = "exa_research";
2582
- 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.";
2583
2641
  inputs = {
2584
- topic: {
2642
+ instructions: {
2585
2643
  type: "string",
2586
- 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.",
2587
2645
  required: true
2588
2646
  },
2589
- numSources: {
2590
- type: "number",
2591
- description: "Number of primary sources to retrieve (default: 5, max: 10)",
2592
- required: false,
2593
- default: 5
2594
- },
2595
- category: {
2647
+ model: {
2596
2648
  type: "string",
2597
- description: 'Optional category: "research paper", "news", "blog", "company"',
2598
- required: false
2599
- },
2600
- includeDomains: {
2601
- type: "array",
2602
- 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)',
2603
2650
  required: false
2604
2651
  },
2605
- startPublishedDate: {
2606
- type: "string",
2607
- 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.",
2608
2655
  required: false
2609
2656
  }
2610
2657
  };
2611
2658
  outputType = "string";
2612
2659
  apiKey;
2660
+ defaultModel;
2661
+ pollInterval;
2662
+ maxPollTime;
2613
2663
  constructor(config) {
2614
2664
  super();
2615
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;
2616
2669
  }
2617
2670
  async setup() {
2618
2671
  if (!this.apiKey) {
@@ -2621,91 +2674,87 @@ var ExaResearchTool = class extends Tool {
2621
2674
  this.isSetup = true;
2622
2675
  }
2623
2676
  async execute(args) {
2624
- const topic = args.topic;
2625
- const numSources = Math.min(args.numSources ?? 5, 10);
2626
- const searchBody = {
2627
- query: topic,
2628
- numResults: numSources,
2629
- type: "auto",
2630
- 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
2631
2686
  };
2632
- if (args.category) searchBody.category = args.category;
2633
- if (args.includeDomains) searchBody.includeDomains = args.includeDomains;
2634
- if (args.startPublishedDate) searchBody.startPublishedDate = args.startPublishedDate;
2635
- 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", {
2636
2691
  method: "POST",
2637
2692
  headers: {
2638
2693
  "x-api-key": this.apiKey,
2639
2694
  "Content-Type": "application/json"
2640
2695
  },
2641
- body: JSON.stringify(searchBody)
2696
+ body: JSON.stringify(createBody)
2642
2697
  });
2643
- if (!searchResponse.ok) {
2644
- const errorText = await searchResponse.text();
2645
- throw new Error(`Exa research search failed (${searchResponse.status}): ${errorText}`);
2646
- }
2647
- const searchData = await searchResponse.json();
2648
- if (!searchData.results || searchData.results.length === 0) {
2649
- return `No research sources found for topic: "${topic}"`;
2698
+ if (!createResponse.ok) {
2699
+ const errorText = await createResponse.text();
2700
+ throw new Error(`Failed to create research task (${createResponse.status}): ${errorText}`);
2650
2701
  }
2651
- let similarResults = [];
2652
- if (searchData.results.length > 0) {
2653
- try {
2654
- const similarBody = {
2655
- url: searchData.results[0].url,
2656
- numResults: 3,
2657
- text: { maxCharacters: 2e3 }
2658
- };
2659
- const similarResponse = await fetch("https://api.exa.ai/findSimilar", {
2660
- method: "POST",
2661
- headers: {
2662
- "x-api-key": this.apiKey,
2663
- "Content-Type": "application/json"
2664
- },
2665
- body: JSON.stringify(similarBody)
2666
- });
2667
- if (similarResponse.ok) {
2668
- const similarData = await similarResponse.json();
2669
- 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((resolve8) => setTimeout(resolve8, 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
2670
2716
  }
2671
- } catch {
2717
+ });
2718
+ if (!statusResponse.ok) {
2719
+ const errorText = await statusResponse.text();
2720
+ throw new Error(`Failed to check research status (${statusResponse.status}): ${errorText}`);
2672
2721
  }
2673
- }
2674
- const allSources = [...searchData.results, ...similarResults];
2675
- const seenUrls = /* @__PURE__ */ new Set();
2676
- const uniqueSources = allSources.filter((s) => {
2677
- if (seenUrls.has(s.url)) return false;
2678
- seenUrls.add(s.url);
2679
- return true;
2680
- });
2681
- const sections = [];
2682
- sections.push(`# Research: ${topic}
2683
- `);
2684
- sections.push(`Found ${uniqueSources.length} sources.
2685
- `);
2686
- sections.push("## Key Sources\n");
2687
- for (let i = 0; i < uniqueSources.length; i++) {
2688
- const source = uniqueSources[i];
2689
- sections.push(`### ${i + 1}. ${source.title ?? "Untitled"}`);
2690
- sections.push(`URL: ${source.url}`);
2691
- if ("publishedDate" in source && source.publishedDate) {
2692
- 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");
2746
+ }
2747
+ if (statusData.status === "failed") {
2748
+ throw new Error(`Research failed: ${statusData.error ?? "Unknown error"}`);
2693
2749
  }
2694
- if ("author" in source && source.author) {
2695
- sections.push(`Author: ${source.author}`);
2750
+ if (statusData.status === "canceled") {
2751
+ throw new Error("Research was canceled");
2696
2752
  }
2697
- if (source.text) {
2698
- sections.push(`
2699
- Content:
2700
- ${source.text.slice(0, 2e3)}`);
2753
+ if (attempts % 10 === 0) {
2754
+ console.log(`Still researching... (${attempts} polls, ${((Date.now() - startTime) / 1e3).toFixed(0)}s elapsed)`);
2701
2755
  }
2702
- sections.push("");
2703
2756
  }
2704
- sections.push("## Source URLs\n");
2705
- uniqueSources.forEach((s, i) => {
2706
- sections.push(`${i + 1}. ${s.url}`);
2707
- });
2708
- return sections.join("\n");
2757
+ throw new Error(`Research timed out after ${this.maxPollTime / 1e3}s. Task ID: ${researchId}`);
2709
2758
  }
2710
2759
  };
2711
2760
 
@@ -2721,12 +2770,21 @@ var TOOL_REGISTRY = {
2721
2770
  };
2722
2771
  var YAMLLoader = class {
2723
2772
  customTools = /* @__PURE__ */ new Map();
2773
+ toolInstances = /* @__PURE__ */ new Map();
2724
2774
  /**
2725
- * Register a custom tool type for use in YAML definitions.
2775
+ * Register a custom tool type (class) for use in YAML definitions.
2726
2776
  */
2727
2777
  registerToolType(typeName, toolClass) {
2728
2778
  this.customTools.set(typeName, toolClass);
2729
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
+ }
2730
2788
  /**
2731
2789
  * Load a workflow from a YAML file path.
2732
2790
  */
@@ -2810,9 +2868,16 @@ var YAMLLoader = class {
2810
2868
  * Build a tool instance from a type name and config.
2811
2869
  */
2812
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
+ }
2813
2878
  const ToolClass = TOOL_REGISTRY[type] ?? this.customTools.get(type);
2814
2879
  if (!ToolClass) {
2815
- 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(", ")}`);
2816
2881
  }
2817
2882
  const tool = new ToolClass(config);
2818
2883
  if (name !== type && name !== tool.name) {
@@ -2840,11 +2905,16 @@ var YAMLLoader = class {
2840
2905
  if (tool) {
2841
2906
  agentTools.push(tool);
2842
2907
  } else {
2843
- const ToolClass = TOOL_REGISTRY[toolName] ?? this.customTools.get(toolName);
2844
- if (ToolClass) {
2845
- agentTools.push(new ToolClass());
2908
+ const instance = this.toolInstances.get(toolName);
2909
+ if (instance) {
2910
+ agentTools.push(instance);
2846
2911
  } else {
2847
- 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
+ }
2848
2918
  }
2849
2919
  }
2850
2920
  }
@@ -2893,40 +2963,116 @@ var YAMLLoader = class {
2893
2963
  }
2894
2964
  };
2895
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
+
2896
3027
  // src/orchestrator/Orchestrator.ts
2897
3028
  var Orchestrator = class {
2898
3029
  loader;
2899
3030
  config;
2900
3031
  activeAgents = /* @__PURE__ */ new Map();
2901
3032
  eventLog = [];
3033
+ jsonOutput = null;
3034
+ isJsonMode = false;
2902
3035
  constructor(config = {}) {
2903
3036
  this.loader = new YAMLLoader();
3037
+ this.isJsonMode = config.outputFormat === "json";
2904
3038
  this.config = {
2905
3039
  verbose: config.verbose ?? true,
2906
- onEvent: config.onEvent
3040
+ onEvent: config.onEvent,
3041
+ outputFormat: config.outputFormat ?? "text",
3042
+ runId: config.runId,
3043
+ cwd: config.cwd
2907
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
+ }
2908
3054
  }
2909
3055
  /**
2910
3056
  * Load a workflow from a YAML file.
2911
3057
  */
2912
3058
  loadWorkflow(filePath) {
2913
3059
  const workflow = this.loader.loadFromFile(filePath);
2914
- this.displayWorkflowInfo(workflow);
3060
+ this.displayWorkflowInfo(workflow, filePath);
2915
3061
  return workflow;
2916
3062
  }
2917
3063
  /**
2918
3064
  * Load a workflow from YAML string.
2919
3065
  */
2920
- loadWorkflowFromString(yamlContent) {
3066
+ loadWorkflowFromString(yamlContent, sourcePath) {
2921
3067
  const workflow = this.loader.loadFromString(yamlContent);
2922
- this.displayWorkflowInfo(workflow);
3068
+ this.displayWorkflowInfo(workflow, sourcePath);
2923
3069
  return workflow;
2924
3070
  }
2925
3071
  /**
2926
3072
  * Run a loaded workflow with a task.
2927
3073
  */
2928
- async runWorkflow(workflow, task) {
2929
- this.displayRunStart(workflow.name, task);
3074
+ async runWorkflow(workflow, task, workflowPath) {
3075
+ this.displayRunStart(workflow.name, task, workflowPath);
2930
3076
  this.instrumentAgent(workflow.entrypointAgent, workflow.entrypointAgent.getName(), 0);
2931
3077
  for (const [name, agent] of workflow.agents) {
2932
3078
  if (agent !== workflow.entrypointAgent) {
@@ -2935,10 +3081,13 @@ var Orchestrator = class {
2935
3081
  }
2936
3082
  try {
2937
3083
  const result = await workflow.entrypointAgent.run(task);
2938
- this.displayRunEnd(result);
3084
+ this.displayRunEnd(result, true);
2939
3085
  return result;
2940
3086
  } catch (error) {
2941
3087
  this.displayError(error);
3088
+ if (this.isJsonMode && this.jsonOutput) {
3089
+ this.jsonOutput.emitRunEnd(false, null, 0, 0);
3090
+ }
2942
3091
  throw error;
2943
3092
  }
2944
3093
  }
@@ -2955,11 +3104,128 @@ var Orchestrator = class {
2955
3104
  */
2956
3105
  instrumentAgent(agent, name, depth) {
2957
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
+ }
2958
3211
  }
2959
3212
  /**
2960
3213
  * Display workflow info at startup.
2961
3214
  */
2962
- 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
+ }
2963
3229
  if (!this.config.verbose) return;
2964
3230
  const line = "\u2550".repeat(70);
2965
3231
  console.log(import_chalk2.default.cyan(line));
@@ -2967,16 +3233,20 @@ var Orchestrator = class {
2967
3233
  if (workflow.description) {
2968
3234
  console.log(import_chalk2.default.cyan(` ${workflow.description}`));
2969
3235
  }
2970
- console.log(import_chalk2.default.cyan(` Agents: ${Array.from(workflow.agents.keys()).join(", ")}`));
2971
- console.log(import_chalk2.default.cyan(` Tools: ${Array.from(workflow.tools.keys()).join(", ") || "(none defined at workflow level)"}`));
2972
- 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}`));
2973
3239
  console.log(import_chalk2.default.cyan(line));
2974
3240
  console.log();
2975
3241
  }
2976
3242
  /**
2977
3243
  * Display run start info.
2978
3244
  */
2979
- 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
+ }
2980
3250
  if (!this.config.verbose) return;
2981
3251
  console.log(import_chalk2.default.green.bold(`
2982
3252
  \u25B6 Running workflow "${workflowName}"`));
@@ -2986,7 +3256,16 @@ var Orchestrator = class {
2986
3256
  /**
2987
3257
  * Display run completion info.
2988
3258
  */
2989
- 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
+ }
2990
3269
  if (!this.config.verbose) return;
2991
3270
  console.log(import_chalk2.default.gray("\n" + "\u2500".repeat(70)));
2992
3271
  console.log(import_chalk2.default.green.bold(`
@@ -3004,6 +3283,10 @@ var Orchestrator = class {
3004
3283
  * Display an error.
3005
3284
  */
3006
3285
  displayError(error) {
3286
+ if (this.isJsonMode && this.jsonOutput) {
3287
+ this.jsonOutput.emitError(error.message, error.stack);
3288
+ return;
3289
+ }
3007
3290
  if (!this.config.verbose) return;
3008
3291
  console.error(import_chalk2.default.red.bold(`
3009
3292
  \u274C Workflow failed: ${error.message}`));
@@ -3032,8 +3315,325 @@ var Orchestrator = class {
3032
3315
  getLoader() {
3033
3316
  return this.loader;
3034
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
+ }
3035
3336
  };
3036
3337
 
3338
+ // src/tools/CustomToolScanner.ts
3339
+ var fs7 = __toESM(require("fs"));
3340
+ var path8 = __toESM(require("path"));
3341
+
3342
+ // src/tools/ProxyTool.ts
3343
+ var import_child_process2 = require("child_process");
3344
+ var path7 = __toESM(require("path"));
3345
+
3346
+ // src/utils/bunInstaller.ts
3347
+ var import_child_process = require("child_process");
3348
+ var path6 = __toESM(require("path"));
3349
+ var fs6 = __toESM(require("fs"));
3350
+ var os3 = __toESM(require("os"));
3351
+ var cachedBunPath = null;
3352
+ async function ensureBunAvailable() {
3353
+ if (cachedBunPath) return cachedBunPath;
3354
+ const fromPath = whichBun();
3355
+ if (fromPath) {
3356
+ cachedBunPath = fromPath;
3357
+ return fromPath;
3358
+ }
3359
+ const localPath = path6.join(os3.homedir(), ".bun", "bin", "bun");
3360
+ if (fs6.existsSync(localPath)) {
3361
+ cachedBunPath = localPath;
3362
+ return localPath;
3363
+ }
3364
+ console.log(
3365
+ "\n[smol-js] Bun is required to run custom tools but was not found. Installing Bun automatically...\n"
3366
+ );
3367
+ try {
3368
+ (0, import_child_process.execSync)("curl --proto =https --tlsv1.2 -sSf https://bun.sh | bash", {
3369
+ stdio: "inherit",
3370
+ shell: "/bin/bash",
3371
+ env: { ...process.env, HOME: os3.homedir() }
3372
+ });
3373
+ } catch (err) {
3374
+ throw new Error(
3375
+ `[smol-js] Failed to auto-install Bun. Please install it manually: https://bun.sh
3376
+ Details: ${err.message}`
3377
+ );
3378
+ }
3379
+ const afterInstall = whichBun() || (fs6.existsSync(localPath) ? localPath : null);
3380
+ if (!afterInstall) {
3381
+ throw new Error(
3382
+ "[smol-js] Bun installation appeared to succeed but the binary was not found. Please install manually: https://bun.sh"
3383
+ );
3384
+ }
3385
+ console.log(`[smol-js] Bun installed successfully at: ${afterInstall}
3386
+ `);
3387
+ cachedBunPath = afterInstall;
3388
+ return afterInstall;
3389
+ }
3390
+ function whichBun() {
3391
+ try {
3392
+ const cmd = process.platform === "win32" ? "where bun" : "which bun";
3393
+ const result = (0, import_child_process.execSync)(cmd, { encoding: "utf8", stdio: "pipe" }).trim();
3394
+ const first = result.split("\n")[0]?.trim();
3395
+ if (first && fs6.existsSync(first)) return first;
3396
+ return null;
3397
+ } catch {
3398
+ return null;
3399
+ }
3400
+ }
3401
+
3402
+ // src/tools/ProxyTool.ts
3403
+ var TOOL_RESULT_PREFIX = "[TOOL_RESULT]";
3404
+ var TOOL_ERROR_PREFIX = "[TOOL_ERROR]";
3405
+ var DEFAULT_TOOL_TIMEOUT_MS = 6e4;
3406
+ function resolveHarnessPath() {
3407
+ return path7.resolve(__dirname, "..", "toolHarness.ts");
3408
+ }
3409
+ var ProxyTool = class extends Tool {
3410
+ name;
3411
+ description;
3412
+ inputs;
3413
+ outputType;
3414
+ toolPath;
3415
+ timeout;
3416
+ bunPath = null;
3417
+ harnessPath = null;
3418
+ constructor(config) {
3419
+ super();
3420
+ this.name = config.name;
3421
+ this.description = config.description;
3422
+ this.inputs = config.inputs;
3423
+ this.outputType = config.outputType;
3424
+ this.toolPath = config.toolPath;
3425
+ this.timeout = config.timeout ?? DEFAULT_TOOL_TIMEOUT_MS;
3426
+ }
3427
+ /**
3428
+ * Ensure Bun is available and locate the harness before first invocation.
3429
+ */
3430
+ async setup() {
3431
+ this.bunPath = await ensureBunAvailable();
3432
+ this.harnessPath = resolveHarnessPath();
3433
+ this.isSetup = true;
3434
+ }
3435
+ /**
3436
+ * Spawn the harness in a Bun child process. The harness imports the tool,
3437
+ * calls execute(args), and writes the protocol lines. Any console.log from
3438
+ * the tool flows through stdout as plain lines.
3439
+ */
3440
+ async execute(args) {
3441
+ if (!this.bunPath || !this.harnessPath) {
3442
+ await this.setup();
3443
+ }
3444
+ const serializedArgs = JSON.stringify(args);
3445
+ return new Promise((resolve8, reject) => {
3446
+ const child = (0, import_child_process2.spawn)(this.bunPath, ["run", this.harnessPath, this.toolPath, serializedArgs], {
3447
+ stdio: ["pipe", "pipe", "pipe"],
3448
+ env: { ...process.env }
3449
+ });
3450
+ let result = void 0;
3451
+ let resultReceived = false;
3452
+ let errorMessage = null;
3453
+ const logBuffer = [];
3454
+ let stderr = "";
3455
+ let partialLine = "";
3456
+ child.stdout.on("data", (chunk) => {
3457
+ partialLine += chunk.toString("utf8");
3458
+ const lines = partialLine.split("\n");
3459
+ partialLine = lines.pop();
3460
+ for (const line of lines) {
3461
+ this.processLine(line, {
3462
+ onOutput: (msg) => logBuffer.push(msg),
3463
+ onResult: (value) => {
3464
+ result = value;
3465
+ resultReceived = true;
3466
+ },
3467
+ onError: (msg) => {
3468
+ errorMessage = msg;
3469
+ }
3470
+ });
3471
+ }
3472
+ });
3473
+ child.stderr.on("data", (chunk) => {
3474
+ stderr += chunk.toString("utf8");
3475
+ });
3476
+ const timer = setTimeout(() => {
3477
+ child.kill("SIGTERM");
3478
+ reject(new Error(
3479
+ `Custom tool "${this.name}" timed out after ${this.timeout}ms. The process was terminated. Check the tool for infinite loops or slow operations.`
3480
+ ));
3481
+ }, this.timeout);
3482
+ timer.unref();
3483
+ child.on("close", (code) => {
3484
+ clearTimeout(timer);
3485
+ if (partialLine.trim()) {
3486
+ this.processLine(partialLine, {
3487
+ onOutput: (msg) => logBuffer.push(msg),
3488
+ onResult: (value) => {
3489
+ result = value;
3490
+ resultReceived = true;
3491
+ },
3492
+ onError: (msg) => {
3493
+ errorMessage = msg;
3494
+ }
3495
+ });
3496
+ }
3497
+ if (errorMessage) {
3498
+ reject(new Error(
3499
+ `Custom tool "${this.name}" reported an error: ${errorMessage}`
3500
+ ));
3501
+ return;
3502
+ }
3503
+ if (resultReceived) {
3504
+ if (logBuffer.length > 0) {
3505
+ const logPrefix = `[Tool output logs]
3506
+ ${logBuffer.join("\n")}
3507
+
3508
+ [Tool result]
3509
+ `;
3510
+ if (typeof result === "string") {
3511
+ resolve8(logPrefix + result);
3512
+ } else {
3513
+ resolve8({ logs: logBuffer.join("\n"), result });
3514
+ }
3515
+ } else {
3516
+ resolve8(result);
3517
+ }
3518
+ return;
3519
+ }
3520
+ const combined = (logBuffer.join("\n") + "\n" + stderr).trim();
3521
+ if (code !== 0) {
3522
+ reject(new Error(
3523
+ `Custom tool "${this.name}" exited with code ${code}. Output: ${combined || "(none)"}`
3524
+ ));
3525
+ } else {
3526
+ resolve8(combined || `Tool "${this.name}" produced no output.`);
3527
+ }
3528
+ });
3529
+ child.on("error", (err) => {
3530
+ clearTimeout(timer);
3531
+ reject(new Error(
3532
+ `Failed to spawn custom tool "${this.name}": ${err.message}`
3533
+ ));
3534
+ });
3535
+ });
3536
+ }
3537
+ // --- line parser: protocol is spoken by harness, interpreted here ---
3538
+ processLine(line, handlers) {
3539
+ const trimmed = line.trimEnd();
3540
+ if (!trimmed) return;
3541
+ if (trimmed.startsWith(TOOL_RESULT_PREFIX)) {
3542
+ const json = trimmed.slice(TOOL_RESULT_PREFIX.length).trim();
3543
+ try {
3544
+ handlers.onResult(JSON.parse(json));
3545
+ } catch {
3546
+ handlers.onResult(json);
3547
+ }
3548
+ } else if (trimmed.startsWith(TOOL_ERROR_PREFIX)) {
3549
+ handlers.onError(trimmed.slice(TOOL_ERROR_PREFIX.length).trim());
3550
+ } else {
3551
+ handlers.onOutput(trimmed);
3552
+ }
3553
+ }
3554
+ };
3555
+
3556
+ // src/tools/CustomToolScanner.ts
3557
+ var METADATA_REGEX = /export\s+const\s+TOOL_METADATA\s*=\s*(\{[\s\S]*?\});\s*$/m;
3558
+ function scanCustomTools(folderPath) {
3559
+ if (!fs7.existsSync(folderPath)) {
3560
+ throw new Error(
3561
+ `Custom tools folder not found: ${folderPath}. Create the directory or check your --custom-tools-folder path.`
3562
+ );
3563
+ }
3564
+ const entries = fs7.readdirSync(folderPath, { withFileTypes: true });
3565
+ const discovered = [];
3566
+ for (const entry of entries) {
3567
+ if (entry.isDirectory()) continue;
3568
+ const ext = path8.extname(entry.name).toLowerCase();
3569
+ if (ext !== ".ts" && ext !== ".js") continue;
3570
+ const filePath = path8.resolve(folderPath, entry.name);
3571
+ const baseName = path8.basename(entry.name, ext);
3572
+ let metadata;
3573
+ try {
3574
+ metadata = extractMetadata(filePath);
3575
+ } catch (err) {
3576
+ throw new Error(
3577
+ `Failed to extract TOOL_METADATA from "${entry.name}": ${err.message}
3578
+ Ensure the file exports \`export const TOOL_METADATA = { name, description, inputs, outputType };\``
3579
+ );
3580
+ }
3581
+ if (metadata.name !== baseName) {
3582
+ throw new Error(
3583
+ `Tool metadata name mismatch in "${entry.name}": file is "${baseName}" but TOOL_METADATA.name is "${metadata.name}". They must match (Convention over Configuration).`
3584
+ );
3585
+ }
3586
+ discovered.push({ filePath, metadata });
3587
+ }
3588
+ return discovered;
3589
+ }
3590
+ function extractMetadata(filePath) {
3591
+ const source = fs7.readFileSync(filePath, "utf8");
3592
+ const match = source.match(METADATA_REGEX);
3593
+ if (!match) {
3594
+ throw new Error(
3595
+ "No `export const TOOL_METADATA = { ... };` block found. Add the metadata export at the bottom of your tool file."
3596
+ );
3597
+ }
3598
+ let parsed;
3599
+ try {
3600
+ parsed = new Function(`"use strict"; return (${match[1]});`)();
3601
+ } catch (err) {
3602
+ throw new Error(
3603
+ `Could not parse TOOL_METADATA object: ${err.message}. Ensure it is a valid JavaScript object literal.`
3604
+ );
3605
+ }
3606
+ if (!parsed.name || typeof parsed.name !== "string") {
3607
+ throw new Error("TOOL_METADATA.name must be a non-empty string.");
3608
+ }
3609
+ if (!parsed.description || typeof parsed.description !== "string") {
3610
+ throw new Error("TOOL_METADATA.description must be a non-empty string.");
3611
+ }
3612
+ if (!parsed.inputs || typeof parsed.inputs !== "object") {
3613
+ throw new Error("TOOL_METADATA.inputs must be an object mapping parameter names to their schemas.");
3614
+ }
3615
+ if (!parsed.outputType || typeof parsed.outputType !== "string") {
3616
+ throw new Error("TOOL_METADATA.outputType must be a non-empty string.");
3617
+ }
3618
+ return parsed;
3619
+ }
3620
+ function loadCustomTools(folderPath) {
3621
+ const discovered = scanCustomTools(folderPath);
3622
+ const tools = /* @__PURE__ */ new Map();
3623
+ for (const { filePath, metadata } of discovered) {
3624
+ const config = {
3625
+ toolPath: filePath,
3626
+ name: metadata.name,
3627
+ description: metadata.description,
3628
+ inputs: metadata.inputs,
3629
+ outputType: metadata.outputType,
3630
+ timeout: metadata.timeout
3631
+ };
3632
+ tools.set(metadata.name, new ProxyTool(config));
3633
+ }
3634
+ return tools;
3635
+ }
3636
+
3037
3637
  // src/cli.ts
3038
3638
  import_dotenv.default.config();
3039
3639
  async function main() {
@@ -3063,14 +3663,20 @@ function printUsage() {
3063
3663
  console.log(" smol-js validate <workflow.yaml> Validate a workflow file");
3064
3664
  console.log("");
3065
3665
  console.log("Options:");
3066
- console.log(" --task, -t <task> Task description (prompted if not provided)");
3067
- console.log(" --quiet, -q Reduce output verbosity");
3068
- console.log(" --help, -h Show this help message");
3666
+ console.log(" --task, -t <task> Task description (prompted if not provided)");
3667
+ console.log(" --quiet, -q Reduce output verbosity");
3668
+ console.log(" --output-format <format> Output format: text (default) or json");
3669
+ console.log(" --verbose Include full step details in output");
3670
+ console.log(" --run-id <id> Unique run identifier (auto-generated if not provided)");
3671
+ console.log(" --cwd <dir> Working directory for agent file operations");
3672
+ console.log(" --custom-tools-folder <path> Path to folder containing standalone custom tool files");
3673
+ console.log(" --help, -h Show this help message");
3069
3674
  console.log("");
3070
3675
  console.log("Examples:");
3071
3676
  console.log(' npx @samrahimi/smol-js workflow.yaml --task "Research AI safety"');
3072
3677
  console.log(' smol-js research-agent.yaml -t "Write a summary of quantum computing"');
3073
3678
  console.log(" smol-js validate my-workflow.yaml");
3679
+ console.log(' smol-js workflow.yaml --task "Task" --output-format json --run-id abc123');
3074
3680
  }
3075
3681
  async function runCommand(args) {
3076
3682
  if (args.length === 0) {
@@ -3080,39 +3686,130 @@ async function runCommand(args) {
3080
3686
  const filePath = args[0];
3081
3687
  let task = "";
3082
3688
  let quiet = false;
3689
+ let outputFormat = "text";
3690
+ let verbose = false;
3691
+ let runId = "";
3692
+ let cwd = "";
3693
+ let customToolsFolder = "";
3083
3694
  for (let i = 1; i < args.length; i++) {
3084
3695
  if (args[i] === "--task" || args[i] === "-t") {
3085
3696
  task = args[i + 1] ?? "";
3086
3697
  i++;
3087
3698
  } else if (args[i] === "--quiet" || args[i] === "-q") {
3088
3699
  quiet = true;
3700
+ } else if (args[i] === "--output-format") {
3701
+ const format = args[i + 1];
3702
+ if (format === "json" || format === "text") {
3703
+ outputFormat = format;
3704
+ } else {
3705
+ console.error(import_chalk3.default.red(`Invalid output format: ${format}. Use 'text' or 'json'.`));
3706
+ process.exit(1);
3707
+ }
3708
+ i++;
3709
+ } else if (args[i] === "--verbose") {
3710
+ verbose = true;
3711
+ } else if (args[i] === "--run-id") {
3712
+ runId = args[i + 1] ?? "";
3713
+ i++;
3714
+ } else if (args[i] === "--cwd") {
3715
+ cwd = args[i + 1] ?? "";
3716
+ i++;
3717
+ } else if (args[i] === "--custom-tools-folder") {
3718
+ customToolsFolder = args[i + 1] ?? "";
3719
+ i++;
3089
3720
  }
3090
3721
  }
3091
- const resolvedPath = path6.isAbsolute(filePath) ? filePath : path6.resolve(process.cwd(), filePath);
3092
- if (!fs6.existsSync(resolvedPath)) {
3093
- console.error(import_chalk3.default.red(`Error: file not found: ${resolvedPath}`));
3722
+ if (outputFormat === "json" && !runId) {
3723
+ runId = `run-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
3724
+ }
3725
+ const resolvedPath = path9.isAbsolute(filePath) ? filePath : path9.resolve(process.cwd(), filePath);
3726
+ if (!fs8.existsSync(resolvedPath)) {
3727
+ if (outputFormat === "json") {
3728
+ console.log(JSON.stringify({
3729
+ runId: runId || "unknown",
3730
+ timestamp: Date.now(),
3731
+ type: "error",
3732
+ data: { message: `File not found: ${resolvedPath}` }
3733
+ }));
3734
+ } else {
3735
+ console.error(import_chalk3.default.red(`Error: file not found: ${resolvedPath}`));
3736
+ }
3094
3737
  process.exit(1);
3095
3738
  }
3096
3739
  if (!task) {
3740
+ if (outputFormat === "json") {
3741
+ console.log(JSON.stringify({
3742
+ runId: runId || "unknown",
3743
+ timestamp: Date.now(),
3744
+ type: "error",
3745
+ data: { message: "Task is required in JSON output mode. Use --task flag." }
3746
+ }));
3747
+ process.exit(1);
3748
+ }
3097
3749
  task = await promptUser("Enter your task: ");
3098
3750
  if (!task.trim()) {
3099
3751
  console.error(import_chalk3.default.red("Error: task cannot be empty"));
3100
3752
  process.exit(1);
3101
3753
  }
3102
3754
  }
3103
- const orchestrator = new Orchestrator({ verbose: !quiet });
3755
+ if (cwd) {
3756
+ const resolvedCwd = path9.isAbsolute(cwd) ? cwd : path9.resolve(process.cwd(), cwd);
3757
+ if (!fs8.existsSync(resolvedCwd)) {
3758
+ fs8.mkdirSync(resolvedCwd, { recursive: true });
3759
+ }
3760
+ process.chdir(resolvedCwd);
3761
+ }
3762
+ const orchestrator = new Orchestrator({
3763
+ verbose: outputFormat === "json" ? verbose : !quiet,
3764
+ outputFormat,
3765
+ runId: runId || void 0,
3766
+ cwd: cwd || void 0
3767
+ });
3768
+ if (customToolsFolder) {
3769
+ const resolvedToolsFolder = path9.isAbsolute(customToolsFolder) ? customToolsFolder : path9.resolve(process.cwd(), customToolsFolder);
3770
+ if (outputFormat !== "json") {
3771
+ console.log(import_chalk3.default.gray(`
3772
+ Scanning custom tools in: ${resolvedToolsFolder}
3773
+ `));
3774
+ }
3775
+ const customTools = loadCustomTools(resolvedToolsFolder);
3776
+ const loader = orchestrator.getLoader();
3777
+ for (const [toolName, proxyTool] of customTools) {
3778
+ loader.registerToolInstance(toolName, proxyTool);
3779
+ if (outputFormat !== "json") {
3780
+ console.log(import_chalk3.default.green(` + Registered custom tool: ${toolName}`));
3781
+ }
3782
+ }
3783
+ }
3104
3784
  try {
3105
- console.log(import_chalk3.default.gray(`
3785
+ if (outputFormat !== "json") {
3786
+ console.log(import_chalk3.default.gray(`
3106
3787
  Loading workflow from: ${resolvedPath}
3107
3788
  `));
3789
+ }
3108
3790
  const workflow = orchestrator.loadWorkflow(resolvedPath);
3109
- await orchestrator.runWorkflow(workflow, task);
3791
+ await orchestrator.runWorkflow(workflow, task, resolvedPath);
3110
3792
  process.exit(0);
3111
3793
  } catch (error) {
3112
- console.error(import_chalk3.default.red(`
3794
+ if (outputFormat === "json") {
3795
+ const jsonHandler = orchestrator.getJSONOutputHandler();
3796
+ if (jsonHandler) {
3797
+ jsonHandler.emitError(error.message, error.stack);
3798
+ jsonHandler.emitRunEnd(false, null, 0, 0);
3799
+ } else {
3800
+ console.log(JSON.stringify({
3801
+ runId: runId || "unknown",
3802
+ timestamp: Date.now(),
3803
+ type: "error",
3804
+ data: { message: error.message, stack: error.stack }
3805
+ }));
3806
+ }
3807
+ } else {
3808
+ console.error(import_chalk3.default.red(`
3113
3809
  Error: ${error.message}`));
3114
- if (process.env.DEBUG) {
3115
- console.error(error.stack);
3810
+ if (process.env.DEBUG) {
3811
+ console.error(error.stack);
3812
+ }
3116
3813
  }
3117
3814
  process.exit(1);
3118
3815
  }
@@ -3123,8 +3820,8 @@ async function validateCommand(args) {
3123
3820
  process.exit(1);
3124
3821
  }
3125
3822
  const filePath = args[0];
3126
- const resolvedPath = path6.isAbsolute(filePath) ? filePath : path6.resolve(process.cwd(), filePath);
3127
- if (!fs6.existsSync(resolvedPath)) {
3823
+ const resolvedPath = path9.isAbsolute(filePath) ? filePath : path9.resolve(process.cwd(), filePath);
3824
+ if (!fs8.existsSync(resolvedPath)) {
3128
3825
  console.error(import_chalk3.default.red(`Error: file not found: ${resolvedPath}`));
3129
3826
  process.exit(1);
3130
3827
  }
@@ -3147,10 +3844,10 @@ function promptUser(question) {
3147
3844
  input: process.stdin,
3148
3845
  output: process.stdout
3149
3846
  });
3150
- return new Promise((resolve6) => {
3847
+ return new Promise((resolve8) => {
3151
3848
  rl.question(import_chalk3.default.cyan(question), (answer) => {
3152
3849
  rl.close();
3153
- resolve6(answer);
3850
+ resolve8(answer);
3154
3851
  });
3155
3852
  });
3156
3853
  }