@outputai/cli 0.3.2 → 0.3.3-dev.b4a190e.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.
@@ -77,7 +77,7 @@ services:
77
77
  condition: service_healthy
78
78
  worker:
79
79
  condition: service_healthy
80
- image: outputai/api:${OUTPUT_API_VERSION:-0.3.2}
80
+ image: outputai/api:${OUTPUT_API_VERSION:-0.3.3-dev.b4a190e.0}
81
81
  init: true
82
82
  networks:
83
83
  - main
@@ -1,3 +1,3 @@
1
1
  {
2
- "framework": "0.3.2"
2
+ "framework": "0.3.3-dev.b4a190e.0"
3
3
  }
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  provider: anthropic
3
- model: claude-haiku-4-5
3
+ # current as of 2026-05-04 — run output-dev-model-selection for the latest
4
+ model: claude-sonnet-4-6
4
5
  temperature: 0.3
5
6
  maxTokens: 4096
6
7
  ---
@@ -185,7 +185,8 @@ Example prompt file:
185
185
  ```
186
186
  ---
187
187
  provider: anthropic
188
- model: claude-haiku-4-5
188
+ # current as of 2026-05-04 — run output-dev-model-selection for the latest
189
+ model: claude-sonnet-4-6
189
190
  temperature: 0.3
190
191
  maxTokens: 1024
191
192
  ---
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  provider: anthropic
3
- model: claude-haiku-4-5
3
+ # current as of 2026-05-04 — run output-dev-model-selection for the latest
4
+ model: claude-sonnet-4-6
4
5
  temperature: 0.3
5
6
  maxTokens: 4096
6
7
  ---
@@ -4,6 +4,7 @@ export interface ScenarioResolutionResult {
4
4
  searchedPaths: string[];
5
5
  }
6
6
  export declare function extractWorkflowRelativePath(path: string): string | null;
7
- export declare function resolveScenarioPath(workflowName: string, scenarioName: string, basePath?: string): Promise<ScenarioResolutionResult>;
7
+ export declare function findWorkflowDirectoryFromPath(workflowPath: string | undefined, basePath?: string): string | null;
8
+ export declare function resolveScenarioPath(workflowName: string, scenarioName: string, basePath?: string, workflowPath?: string): Promise<ScenarioResolutionResult>;
8
9
  export declare function listScenariosForWorkflow(workflowName: string, workflowPath?: string, basePath?: string): string[];
9
10
  export declare function getScenarioNotFoundMessage(workflowName: string, scenarioName: string, searchedPaths: string[]): string;
@@ -1,14 +1,34 @@
1
1
  import { existsSync, readdirSync } from 'node:fs';
2
- import { resolve } from 'node:path';
2
+ import { dirname, resolve } from 'node:path';
3
3
  import { getWorkflowCatalog } from '#api/generated/api.js';
4
4
  import { getWorkflowsBasePath } from '#utils/paths.js';
5
5
  const SCENARIOS_DIR = 'scenarios';
6
6
  const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
7
7
  export function extractWorkflowRelativePath(path) {
8
- const match = path.match(/workflows\/(.+)\/workflow\.[jt]s$/);
8
+ const match = path.match(/(?:^|\/)workflows\/(.+)\/workflow\.[jt]s$/);
9
9
  return match ? match[1] : null;
10
10
  }
11
- async function fetchWorkflowDirectory(workflowName) {
11
+ function unique(values) {
12
+ return [...new Set(values)];
13
+ }
14
+ function workflowPathSuffixes(workflowPath) {
15
+ const parts = dirname(workflowPath).split(/[/\\]+/).filter(Boolean);
16
+ return parts.map((_, index) => parts.slice(index));
17
+ }
18
+ function candidateWorkflowDirsFromPath(workflowPath, basePath) {
19
+ return unique(workflowPathSuffixes(workflowPath).flatMap(suffix => WORKFLOWS_PATHS.map(workflowsDir => resolve(basePath, workflowsDir, ...suffix))));
20
+ }
21
+ function candidateScenarioDirsFromPath(workflowPath, basePath) {
22
+ return candidateWorkflowDirsFromPath(workflowPath, basePath)
23
+ .map(workflowDir => resolve(workflowDir, SCENARIOS_DIR));
24
+ }
25
+ export function findWorkflowDirectoryFromPath(workflowPath, basePath = getWorkflowsBasePath()) {
26
+ if (!workflowPath) {
27
+ return null;
28
+ }
29
+ return candidateWorkflowDirsFromPath(workflowPath, basePath).find(existsSync) ?? null;
30
+ }
31
+ async function fetchWorkflowPath(workflowName) {
12
32
  try {
13
33
  const response = await getWorkflowCatalog();
14
34
  const data = response?.data;
@@ -20,11 +40,7 @@ async function fetchWorkflowDirectory(workflowName) {
20
40
  if (!workflow) {
21
41
  return null;
22
42
  }
23
- const workflowPath = workflow.path;
24
- if (!workflowPath) {
25
- return null;
26
- }
27
- return extractWorkflowRelativePath(workflowPath);
43
+ return workflow.path ?? null;
28
44
  }
29
45
  catch {
30
46
  return null;
@@ -41,33 +57,48 @@ function resolveScenarioFromDirectory(relativeDir, scenarioFileName, basePath) {
41
57
  }
42
58
  return { found: false, searchedPaths };
43
59
  }
44
- export async function resolveScenarioPath(workflowName, scenarioName, basePath = getWorkflowsBasePath()) {
60
+ function resolveScenarioFromScenarioDirs(scenariosDirs, scenarioFileName) {
61
+ const searchedPaths = scenariosDirs.map(dir => resolve(dir, scenarioFileName));
62
+ const path = searchedPaths.find(existsSync);
63
+ return path ?
64
+ { found: true, path, searchedPaths } :
65
+ { found: false, searchedPaths };
66
+ }
67
+ export async function resolveScenarioPath(workflowName, scenarioName, basePath = getWorkflowsBasePath(), workflowPath) {
45
68
  const scenarioFileName = scenarioName.endsWith('.json') ?
46
69
  scenarioName :
47
70
  `${scenarioName}.json`;
48
- const catalogDir = await fetchWorkflowDirectory(workflowName);
49
- if (catalogDir) {
50
- const result = resolveScenarioFromDirectory(catalogDir, scenarioFileName, basePath);
71
+ if (workflowPath) {
72
+ const pathResult = resolveScenarioFromScenarioDirs(candidateScenarioDirsFromPath(workflowPath, basePath), scenarioFileName);
73
+ if (pathResult.found) {
74
+ return pathResult;
75
+ }
76
+ }
77
+ const catalogPath = workflowPath ? null : await fetchWorkflowPath(workflowName);
78
+ if (catalogPath) {
79
+ const result = resolveScenarioFromScenarioDirs(candidateScenarioDirsFromPath(catalogPath, basePath), scenarioFileName);
51
80
  if (result.found) {
52
81
  return result;
53
82
  }
54
- // Catalog resolved but scenario not found at that path — still try convention fallback
55
- // in case the catalog path differs from local source layout
56
- if (catalogDir !== workflowName) {
57
- const fallback = resolveScenarioFromDirectory(workflowName, scenarioFileName, basePath);
58
- return {
59
- found: fallback.found,
60
- path: fallback.path,
61
- searchedPaths: [...result.searchedPaths, ...fallback.searchedPaths]
62
- };
63
- }
64
- return result;
83
+ const fallback = resolveScenarioFromDirectory(workflowName, scenarioFileName, basePath);
84
+ return {
85
+ found: fallback.found,
86
+ path: fallback.path,
87
+ searchedPaths: [...result.searchedPaths, ...fallback.searchedPaths]
88
+ };
65
89
  }
66
90
  // API unavailable or workflow not in catalog — fall back to convention
67
91
  return resolveScenarioFromDirectory(workflowName, scenarioFileName, basePath);
68
92
  }
69
93
  export function listScenariosForWorkflow(workflowName, workflowPath, basePath = getWorkflowsBasePath()) {
70
- const relativeDir = (workflowPath && extractWorkflowRelativePath(workflowPath)) || workflowName;
94
+ const scenariosDirs = workflowPath ? candidateScenarioDirsFromPath(workflowPath, basePath) : [];
95
+ const scenariosDir = scenariosDirs.find(existsSync);
96
+ if (scenariosDir) {
97
+ return readdirSync(scenariosDir)
98
+ .filter(f => f.endsWith('.json'))
99
+ .map(f => f.replace(/\.json$/, ''));
100
+ }
101
+ const relativeDir = workflowName;
71
102
  for (const workflowsDir of WORKFLOWS_PATHS) {
72
103
  const scenariosDir = resolve(basePath, workflowsDir, relativeDir, SCENARIOS_DIR);
73
104
  if (existsSync(scenariosDir)) {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
- import { resolveScenarioPath, getScenarioNotFoundMessage, extractWorkflowRelativePath, listScenariosForWorkflow } from './scenario_resolver.js';
2
+ import { resolveScenarioPath, getScenarioNotFoundMessage, extractWorkflowRelativePath, findWorkflowDirectoryFromPath, listScenariosForWorkflow } from './scenario_resolver.js';
3
3
  import * as fs from 'node:fs';
4
4
  import * as api from '#api/generated/api.js';
5
5
  vi.mock('node:fs', () => ({
@@ -32,6 +32,10 @@ describe('extractWorkflowRelativePath', () => {
32
32
  expect(extractWorkflowRelativePath('/src/workflows/my_flow/workflow.ts'))
33
33
  .toBe('my_flow');
34
34
  });
35
+ it('should not match workflows inside a parent directory name', () => {
36
+ expect(extractWorkflowRelativePath('/app/test_workflows/dist/workflows/simple_sleep/workflow.js'))
37
+ .toBe('simple_sleep');
38
+ });
35
39
  it('should return null for non-matching paths', () => {
36
40
  expect(extractWorkflowRelativePath('/app/dist/other/workflow.js')).toBeNull();
37
41
  expect(extractWorkflowRelativePath('/app/dist/workflows/')).toBeNull();
@@ -100,6 +104,15 @@ describe('resolveScenarioPath', () => {
100
104
  expect(result.found).toBe(true);
101
105
  expect(result.path).toContain('src/workflows/my_workflow/scenarios/test_scenario.json');
102
106
  });
107
+ it('should resolve using a provided workflow path without requiring a workflows segment', async () => {
108
+ mockCatalogFailure();
109
+ vi.mocked(fs.existsSync).mockImplementation(path => {
110
+ return String(path) === '/project/src/workflows/writing/editor/scenarios/basic.json';
111
+ });
112
+ const result = await resolveScenarioPath('writing_editor', 'basic', '/project', '/app/build-output/writing/editor/workflow.js');
113
+ expect(result.found).toBe(true);
114
+ expect(result.path).toBe('/project/src/workflows/writing/editor/scenarios/basic.json');
115
+ });
103
116
  });
104
117
  describe('when workflow is not in catalog', () => {
105
118
  it('should fall back to convention-based lookup', async () => {
@@ -158,6 +171,12 @@ describe('listScenariosForWorkflow', () => {
158
171
  const result = listScenariosForWorkflow('my_workflow', undefined, '/project');
159
172
  expect(result).toEqual(['basic', 'advanced']);
160
173
  });
174
+ it('should list scenarios from a workflow path without requiring a workflows segment', () => {
175
+ vi.mocked(fs.existsSync).mockImplementation(path => String(path) === '/project/src/workflows/writing/editor/scenarios');
176
+ vi.mocked(fs.readdirSync).mockReturnValue(['basic.json', 'README.md']);
177
+ const result = listScenariosForWorkflow('writing_editor', '/app/build-output/writing/editor/workflow.js', '/project');
178
+ expect(result).toEqual(['basic']);
179
+ });
161
180
  it('should use workflowPath from catalog to derive directory', () => {
162
181
  vi.mocked(fs.existsSync).mockImplementation(path => String(path).includes('src/workflows/viz_examples/01_simple_linear/scenarios'));
163
182
  vi.mocked(fs.readdirSync).mockReturnValue(['test.json']);
@@ -197,6 +216,16 @@ describe('listScenariosForWorkflow', () => {
197
216
  expect(result).toEqual([]);
198
217
  });
199
218
  });
219
+ describe('findWorkflowDirectoryFromPath', () => {
220
+ beforeEach(() => {
221
+ vi.resetAllMocks();
222
+ });
223
+ it('should find the local workflow directory from a loaded workflow path suffix', () => {
224
+ vi.mocked(fs.existsSync).mockImplementation(path => String(path) === '/project/src/workflows/writing/editor');
225
+ const result = findWorkflowDirectoryFromPath('/app/build-output/writing/editor/workflow.js', '/project');
226
+ expect(result).toBe('/project/src/workflows/writing/editor');
227
+ });
228
+ });
200
229
  describe('getScenarioNotFoundMessage', () => {
201
230
  it('should return a helpful error message', () => {
202
231
  const searchedPaths = [
@@ -141,6 +141,6 @@ const Shell = ({ dockerComposePath, onCleanup }) => {
141
141
  if (ui.expandedJson.open) {
142
142
  return (_jsx(Box, { flexDirection: "column", height: rows, paddingX: 2, paddingTop: 1, children: _jsx(ExpandedJsonModal, {}) }));
143
143
  }
144
- return (_jsxs(Box, { flexDirection: "column", height: rows, paddingX: 2, paddingTop: 1, children: [_jsx(Header, { counters: counters }), _jsx(TabBar, { active: ui.tab }), _jsx(HorizontalRule, {}), _jsx(SearchBar, { active: ui.search.open }), _jsx(Toasts, {}), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [ui.tab === 'workflows' && !ui.runModal.open && _jsx(WorkflowsPanel, { workflows: workflows, runs: runs }), ui.tab === 'runs' && !ui.runModal.open && _jsx(RunsPanel, { runs: runs }), ui.tab === 'services' && !ui.runModal.open && (_jsx(ServicesPanel, { phase: phase, services: services, dockerComposePath: dockerComposePath })), ui.tab === 'help' && !ui.runModal.open && _jsx(HelpPanel, {}), ui.runModal.open && _jsx(RunModal, { workflowName: ui.runModal.workflowName })] })] }));
144
+ return (_jsxs(Box, { flexDirection: "column", height: rows, paddingX: 2, paddingTop: 1, children: [_jsx(Header, { counters: counters }), _jsx(TabBar, { active: ui.tab }), _jsx(HorizontalRule, {}), _jsx(SearchBar, { active: ui.search.open }), _jsx(Toasts, {}), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [ui.tab === 'workflows' && !ui.runModal.open && _jsx(WorkflowsPanel, { workflows: workflows, runs: runs }), ui.tab === 'runs' && !ui.runModal.open && _jsx(RunsPanel, { runs: runs }), ui.tab === 'services' && !ui.runModal.open && (_jsx(ServicesPanel, { phase: phase, services: services, dockerComposePath: dockerComposePath })), ui.tab === 'help' && !ui.runModal.open && _jsx(HelpPanel, {}), ui.runModal.open && _jsx(RunModal, { workflowName: ui.runModal.workflowName, workflowPath: ui.runModal.workflowPath })] })] }));
145
145
  };
146
146
  export const DevApp = ({ dockerComposePath, onCleanup }) => (_jsx(UiStateProvider, { children: _jsx(Shell, { dockerComposePath: dockerComposePath, onCleanup: onCleanup }) }));
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
2
  export declare const RunModal: React.FC<{
3
3
  workflowName: string;
4
+ workflowPath?: string;
4
5
  }>;
@@ -21,9 +21,9 @@ const buildEntries = (scenarios) => {
21
21
  };
22
22
  const Frame = ({ title, children }) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", paddingX: 1, paddingY: 0, children: [_jsx(Text, { bold: true, children: title }), children] }));
23
23
  const TextPrompt = ({ label, value }) => (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { children: [label, " "] }), _jsx(Text, { children: value }), _jsx(Text, { inverse: true, children: ' ' })] }));
24
- export const RunModal = ({ workflowName }) => {
24
+ export const RunModal = ({ workflowName, workflowPath }) => {
25
25
  const ui = useUiState();
26
- const scenarios = useMemo(() => listScenariosForWorkflow(workflowName), [workflowName]);
26
+ const scenarios = useMemo(() => listScenariosForWorkflow(workflowName, workflowPath), [workflowName, workflowPath]);
27
27
  const entries = useMemo(() => buildEntries(scenarios), [scenarios]);
28
28
  const [mode, setMode] = useState('select');
29
29
  const [index, setIndex] = useState(0);
@@ -54,7 +54,7 @@ export const RunModal = ({ workflowName }) => {
54
54
  };
55
55
  const runScenario = async (scenarioName) => {
56
56
  try {
57
- const input = await readScenario(workflowName, scenarioName);
57
+ const input = await readScenario(workflowName, scenarioName, workflowPath);
58
58
  await submit(input, scenarioName);
59
59
  }
60
60
  catch (err) {
@@ -64,7 +64,7 @@ export const RunModal = ({ workflowName }) => {
64
64
  };
65
65
  const startDuplicate = async (scenarioName) => {
66
66
  try {
67
- const sourceContent = await readScenario(workflowName, scenarioName);
67
+ const sourceContent = await readScenario(workflowName, scenarioName, workflowPath);
68
68
  setEditName(`${scenarioName}_copy`);
69
69
  setEditSeed(sourceContent);
70
70
  setEditFrameTitle(`Duplicate '${scenarioName}'`);
@@ -106,7 +106,7 @@ export const RunModal = ({ workflowName }) => {
106
106
  }
107
107
  setMode('submitting');
108
108
  try {
109
- const writtenPath = await writeScenario(workflowName, name, value);
109
+ const writtenPath = await writeScenario(workflowName, name, value, workflowPath);
110
110
  ui.pushToast(`Saved scenario at ${writtenPath}`, 'info');
111
111
  await submit(value, name);
112
112
  }
@@ -98,7 +98,7 @@ export const WorkflowsPanel = ({ workflows, runs }) => {
98
98
  ui.setTab('runs');
99
99
  }
100
100
  else if (input === 'r' && selectedWorkflow?.name) {
101
- ui.openRunModal(selectedWorkflow.name);
101
+ ui.openRunModal(selectedWorkflow.name, selectedWorkflow.path);
102
102
  }
103
103
  }, { isActive });
104
104
  if (workflows.length === 0) {
@@ -1,2 +1,2 @@
1
- export declare const readScenario: (workflowName: string, scenarioName: string) => Promise<unknown>;
2
- export declare const writeScenario: (workflowName: string, scenarioName: string, content: unknown) => Promise<string>;
1
+ export declare const readScenario: (workflowName: string, scenarioName: string, workflowPath?: string) => Promise<unknown>;
2
+ export declare const writeScenario: (workflowName: string, scenarioName: string, content: unknown, workflowPath?: string) => Promise<string>;
@@ -1,19 +1,23 @@
1
1
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { resolve, dirname } from 'node:path';
4
- import { resolveScenarioPath } from '#utils/scenario_resolver.js';
4
+ import { findWorkflowDirectoryFromPath, resolveScenarioPath } from '#utils/scenario_resolver.js';
5
5
  import { getWorkflowsBasePath } from '#utils/paths.js';
6
6
  const WORKFLOWS_PATHS = ['src/workflows', 'workflows'];
7
- export const readScenario = async (workflowName, scenarioName) => {
8
- const resolution = await resolveScenarioPath(workflowName, scenarioName);
7
+ export const readScenario = async (workflowName, scenarioName, workflowPath) => {
8
+ const resolution = await resolveScenarioPath(workflowName, scenarioName, getWorkflowsBasePath(), workflowPath);
9
9
  if (!resolution.found || !resolution.path) {
10
10
  throw new Error(`Scenario '${scenarioName}' not found for workflow '${workflowName}'.`);
11
11
  }
12
12
  const content = await readFile(resolution.path, 'utf-8');
13
13
  return JSON.parse(content);
14
14
  };
15
- const findWorkflowDirectory = (workflowName) => {
15
+ const findWorkflowDirectory = (workflowName, workflowPath) => {
16
16
  const basePath = getWorkflowsBasePath();
17
+ const pathDir = findWorkflowDirectoryFromPath(workflowPath, basePath);
18
+ if (pathDir) {
19
+ return pathDir;
20
+ }
17
21
  for (const wfDir of WORKFLOWS_PATHS) {
18
22
  const candidate = resolve(basePath, wfDir, workflowName);
19
23
  if (existsSync(candidate)) {
@@ -22,8 +26,8 @@ const findWorkflowDirectory = (workflowName) => {
22
26
  }
23
27
  return null;
24
28
  };
25
- export const writeScenario = async (workflowName, scenarioName, content) => {
26
- const dir = findWorkflowDirectory(workflowName);
29
+ export const writeScenario = async (workflowName, scenarioName, content, workflowPath) => {
30
+ const dir = findWorkflowDirectory(workflowName, workflowPath);
27
31
  if (!dir) {
28
32
  throw new Error(`Workflow directory for '${workflowName}' not found locally.`);
29
33
  }
@@ -17,6 +17,7 @@ export interface SearchState {
17
17
  export interface RunModalState {
18
18
  open: boolean;
19
19
  workflowName: string;
20
+ workflowPath?: string;
20
21
  }
21
22
  export interface ExpandedJsonState {
22
23
  open: boolean;
@@ -47,7 +48,7 @@ export interface UiState {
47
48
  setSelection: (selection: Selection) => void;
48
49
  setRightPaneTab: (tab: RightPaneTab) => void;
49
50
  setRunsView: (view: RunsView) => void;
50
- openRunModal: (workflowName: string) => void;
51
+ openRunModal: (workflowName: string, workflowPath?: string) => void;
51
52
  closeRunModal: () => void;
52
53
  openExpandedJson: (value: unknown, title: string) => void;
53
54
  closeExpandedJson: () => void;
@@ -43,7 +43,7 @@ export const UiStateProvider = ({ children }) => {
43
43
  setSelection,
44
44
  setRightPaneTab,
45
45
  setRunsView,
46
- openRunModal: (workflowName) => setRunModal({ open: true, workflowName }),
46
+ openRunModal: (workflowName, workflowPath) => setRunModal({ open: true, workflowName, workflowPath }),
47
47
  closeRunModal: () => setRunModal({ open: false, workflowName: '' }),
48
48
  openExpandedJson: (value, title) => setExpandedJson({ open: true, value, title }),
49
49
  closeExpandedJson: () => setExpandedJson({ open: false, value: null, title: '' }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.3-dev.b4a190e.0",
4
4
  "description": "CLI for Output.ai workflow generation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,9 +36,9 @@
36
36
  "semver": "7.7.4",
37
37
  "undici": "8.1.0",
38
38
  "yaml": "^2.8.3",
39
- "@outputai/credentials": "0.3.2",
40
- "@outputai/llm": "0.3.2",
41
- "@outputai/evals": "0.3.2"
39
+ "@outputai/credentials": "0.3.3-dev.b4a190e.0",
40
+ "@outputai/evals": "0.3.3-dev.b4a190e.0",
41
+ "@outputai/llm": "0.3.3-dev.b4a190e.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/cli-progress": "3.11.6",