@monoes/monobrowse 1.0.0 → 1.0.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.
Files changed (101) hide show
  1. package/dist/package.json +75 -0
  2. package/dist/src/browser/adapters/index.d.ts.map +1 -1
  3. package/dist/src/browser/adapters/index.js +4 -0
  4. package/dist/src/browser/adapters/index.js.map +1 -1
  5. package/dist/src/browser/browser.js +2 -2
  6. package/dist/src/browser/browser.js.map +1 -1
  7. package/dist/src/browser/cdp.d.ts +6 -0
  8. package/dist/src/browser/cdp.d.ts.map +1 -1
  9. package/dist/src/browser/cdp.js +56 -1
  10. package/dist/src/browser/cdp.js.map +1 -1
  11. package/dist/src/browser/dashboard/api-types.d.ts +1 -1
  12. package/dist/src/browser/dashboard/server.d.ts +1 -1
  13. package/dist/src/browser/dashboard/server.d.ts.map +1 -1
  14. package/dist/src/browser/dashboard/server.js +204 -15
  15. package/dist/src/browser/dashboard/server.js.map +1 -1
  16. package/dist/src/browser/dashboard/ui.html +1811 -0
  17. package/dist/src/browser/playbook/browser-handlers.d.ts +3 -0
  18. package/dist/src/browser/playbook/browser-handlers.d.ts.map +1 -0
  19. package/dist/src/browser/playbook/browser-handlers.js +377 -0
  20. package/dist/src/browser/playbook/browser-handlers.js.map +1 -0
  21. package/dist/src/browser/playbook/builtin-handlers.d.ts +3 -0
  22. package/dist/src/browser/playbook/builtin-handlers.d.ts.map +1 -0
  23. package/dist/src/browser/playbook/builtin-handlers.js +364 -0
  24. package/dist/src/browser/playbook/builtin-handlers.js.map +1 -0
  25. package/dist/src/browser/playbook/index.d.ts +3 -0
  26. package/dist/src/browser/playbook/index.d.ts.map +1 -0
  27. package/dist/src/browser/playbook/index.js +3 -0
  28. package/dist/src/browser/playbook/index.js.map +1 -0
  29. package/dist/src/browser/playbook/store.d.ts +8 -0
  30. package/dist/src/browser/playbook/store.d.ts.map +1 -0
  31. package/dist/src/browser/playbook/store.js +87 -0
  32. package/dist/src/browser/playbook/store.js.map +1 -0
  33. package/dist/src/browser/screenshot.d.ts +2 -0
  34. package/dist/src/browser/screenshot.d.ts.map +1 -1
  35. package/dist/src/browser/screenshot.js +69 -2
  36. package/dist/src/browser/screenshot.js.map +1 -1
  37. package/dist/src/browser/types.d.ts.map +1 -1
  38. package/dist/src/browser/types.js +11 -0
  39. package/dist/src/browser/types.js.map +1 -1
  40. package/dist/src/cli/action.d.ts +3 -0
  41. package/dist/src/cli/action.d.ts.map +1 -0
  42. package/dist/src/cli/action.js +139 -0
  43. package/dist/src/cli/action.js.map +1 -0
  44. package/dist/src/cli/commands.d.ts +8 -0
  45. package/dist/src/cli/commands.d.ts.map +1 -0
  46. package/dist/src/cli/commands.js +3021 -0
  47. package/dist/src/cli/commands.js.map +1 -0
  48. package/dist/src/cli/output.d.ts +11 -0
  49. package/dist/src/cli/output.d.ts.map +1 -0
  50. package/dist/src/cli/output.js +33 -0
  51. package/dist/src/cli/output.js.map +1 -0
  52. package/dist/src/cli/platform.d.ts +7 -0
  53. package/dist/src/cli/platform.d.ts.map +1 -0
  54. package/dist/src/cli/platform.js +193 -0
  55. package/dist/src/cli/platform.js.map +1 -0
  56. package/dist/src/cli/playbook.d.ts +3 -0
  57. package/dist/src/cli/playbook.d.ts.map +1 -0
  58. package/dist/src/cli/playbook.js +265 -0
  59. package/dist/src/cli/playbook.js.map +1 -0
  60. package/dist/src/cli/types.d.ts +42 -0
  61. package/dist/src/cli/types.d.ts.map +1 -0
  62. package/dist/src/cli/types.js +2 -0
  63. package/dist/src/cli/types.js.map +1 -0
  64. package/dist/src/cli/workflow.d.ts +2 -0
  65. package/dist/src/cli/workflow.d.ts.map +1 -0
  66. package/dist/src/cli/workflow.js +3 -0
  67. package/dist/src/cli/workflow.js.map +1 -0
  68. package/dist/src/cli.d.ts +7 -0
  69. package/dist/src/cli.d.ts.map +1 -0
  70. package/dist/src/cli.js +187 -0
  71. package/dist/src/cli.js.map +1 -0
  72. package/dist/src/index.d.ts +5 -1
  73. package/dist/src/index.d.ts.map +1 -1
  74. package/dist/src/index.js +16 -1
  75. package/dist/src/index.js.map +1 -1
  76. package/dist/tsconfig.tsbuildinfo +1 -1
  77. package/package.json +24 -5
  78. package/src/browser/adapters/index.ts +4 -0
  79. package/src/browser/browser.ts +2 -2
  80. package/src/browser/cdp.ts +68 -1
  81. package/src/browser/dashboard/api-types.ts +1 -1
  82. package/src/browser/dashboard/server.ts +210 -17
  83. package/src/browser/dashboard/ui.html +1412 -45
  84. package/src/browser/playbook/browser-handlers.ts +449 -0
  85. package/src/browser/{workflow → playbook}/builtin-handlers.ts +35 -14
  86. package/src/browser/playbook/index.ts +2 -0
  87. package/src/browser/{workflow → playbook}/store.ts +14 -14
  88. package/src/browser/screenshot.ts +78 -3
  89. package/src/browser/types.ts +11 -0
  90. package/src/cli/action.ts +145 -0
  91. package/src/cli/commands.ts +2895 -0
  92. package/src/cli/output.ts +37 -0
  93. package/src/cli/platform.ts +211 -0
  94. package/src/cli/playbook.ts +280 -0
  95. package/src/cli/types.ts +47 -0
  96. package/src/cli.ts +208 -0
  97. package/src/index.ts +20 -1
  98. package/src/browser/workflow/engine.ts +0 -202
  99. package/src/browser/workflow/expression.ts +0 -107
  100. package/src/browser/workflow/index.ts +0 -5
  101. package/src/browser/workflow/types.ts +0 -62
package/src/cli.ts ADDED
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * monobrowse — standalone CLI entry point
4
+ * Dispatches to the same command tree used by `monomind browse`
5
+ */
6
+
7
+ import { createRequire } from 'module';
8
+ import browseCommand from './cli/commands.js';
9
+ import { output } from './cli/output.js';
10
+ import type { Command, CommandContext, CommandOption, ParsedFlags } from './cli/types.js';
11
+
12
+ const _require = createRequire(import.meta.url);
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Subcommand lookup (supports aliases)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ function findSub(name: string, subs: Command[]): Command | undefined {
19
+ return subs.find(s => s.name === name || s.aliases?.includes(name));
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Minimal argv parser — resolves short flags from the target subcommand's opts
24
+ // ---------------------------------------------------------------------------
25
+
26
+ function buildShortMap(cmd: Command | undefined): Record<string, string> {
27
+ const m: Record<string, string> = {};
28
+ if (!cmd?.options) return m;
29
+ for (const opt of cmd.options) {
30
+ if (opt.short) m[opt.short] = opt.name;
31
+ }
32
+ return m;
33
+ }
34
+
35
+ function isBooleanOpt(name: string, cmd: Command | undefined): boolean {
36
+ const opt = cmd?.options?.find(o => o.name === name);
37
+ return opt?.type === 'boolean';
38
+ }
39
+
40
+ function parseArgv(argv: string[], sub: Command | undefined): { args: string[]; flags: ParsedFlags } {
41
+ const shortMap = buildShortMap(sub);
42
+ const flags: ParsedFlags = { _: [] };
43
+ const positional: string[] = [];
44
+ let i = 0;
45
+
46
+ while (i < argv.length) {
47
+ const arg = argv[i];
48
+
49
+ if (arg === '--') {
50
+ positional.push(...argv.slice(i + 1));
51
+ break;
52
+ }
53
+
54
+ if (arg.startsWith('--')) {
55
+ const raw = arg.slice(2);
56
+ if (raw.startsWith('no-')) {
57
+ flags[raw.slice(3)] = false;
58
+ } else if (raw.includes('=')) {
59
+ const eq = raw.indexOf('=');
60
+ flags[raw.slice(0, eq)] = raw.slice(eq + 1);
61
+ } else if (isBooleanOpt(raw, sub) || i + 1 >= argv.length || argv[i + 1].startsWith('-')) {
62
+ flags[raw] = true;
63
+ } else {
64
+ flags[raw] = argv[++i];
65
+ }
66
+ } else if (arg.startsWith('-') && arg.length >= 2) {
67
+ const short = arg[1];
68
+ const long = shortMap[short] ?? short;
69
+ if (arg.length > 2) {
70
+ flags[long] = arg.slice(2);
71
+ } else if (isBooleanOpt(long, sub) || i + 1 >= argv.length || argv[i + 1].startsWith('-')) {
72
+ flags[long] = true;
73
+ } else {
74
+ flags[long] = argv[++i];
75
+ }
76
+ } else {
77
+ positional.push(arg);
78
+ }
79
+
80
+ i++;
81
+ }
82
+
83
+ // Coerce number options
84
+ for (const opt of (sub?.options ?? [])) {
85
+ if (opt.type === 'number' && typeof flags[opt.name] === 'string') {
86
+ flags[opt.name] = Number(flags[opt.name]);
87
+ }
88
+ }
89
+
90
+ return { args: positional, flags };
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Help printer
95
+ // ---------------------------------------------------------------------------
96
+
97
+ function printHelp(cmd: Command, prefix = 'monobrowse'): void {
98
+ const usage = cmd.name === 'browse'
99
+ ? `${prefix} <subcommand> [options]`
100
+ : `${prefix} ${cmd.name} [subcommand] [options]`;
101
+ console.log(`\nUsage: ${usage}\n`);
102
+ console.log(cmd.description + '\n');
103
+
104
+ if (cmd.subcommands?.length) {
105
+ console.log('Subcommands:');
106
+ for (const s of cmd.subcommands) {
107
+ if (!s.hidden) console.log(` ${s.name.padEnd(20)} ${s.description}`);
108
+ }
109
+ console.log('');
110
+ }
111
+
112
+ if (cmd.options?.length) {
113
+ console.log('Options:');
114
+ for (const o of cmd.options) {
115
+ const shortPart = o.short ? `-${o.short}, ` : ' ';
116
+ const defaultPart = o.default !== undefined ? ` (default: ${o.default})` : '';
117
+ console.log(` ${shortPart}--${o.name.padEnd(18)} ${o.description}${defaultPart}`);
118
+ }
119
+ console.log('');
120
+ }
121
+
122
+ if (cmd.examples?.length) {
123
+ console.log('Examples:');
124
+ for (const ex of cmd.examples) {
125
+ const adjusted = ex.command.replace(/^monomind browse/, prefix);
126
+ console.log(` ${adjusted} ${ex.description}`);
127
+ }
128
+ console.log('');
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Dispatch: find action and build context
134
+ // ---------------------------------------------------------------------------
135
+
136
+ async function dispatch(cmd: Command, argv: string[]): Promise<void> {
137
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
138
+ printHelp(cmd);
139
+ return;
140
+ }
141
+
142
+ const subName = argv[0];
143
+ const rest = argv.slice(1);
144
+
145
+ const sub = findSub(subName, cmd.subcommands ?? []);
146
+ if (!sub) {
147
+ output.printError(`Unknown subcommand: ${subName}`);
148
+ output.printInfo('Run `monobrowse --help` to see available subcommands.');
149
+ process.exitCode = 1;
150
+ return;
151
+ }
152
+
153
+ if (rest[0] === '--help' || rest[0] === '-h') {
154
+ printHelp(sub, 'monobrowse');
155
+ return;
156
+ }
157
+
158
+ // Check if first remaining arg matches a nested subcommand
159
+ if (sub.subcommands?.length && rest.length > 0 && !rest[0].startsWith('-')) {
160
+ const nested = findSub(rest[0], sub.subcommands);
161
+ if (nested?.action) {
162
+ const { args, flags } = parseArgv(rest.slice(1), nested);
163
+ const ctx: CommandContext = { args, flags, cwd: process.cwd(), interactive: !!process.stdout.isTTY };
164
+ const result = await nested.action(ctx);
165
+ if (result && !result.success) process.exitCode = result.exitCode ?? 1;
166
+ return;
167
+ }
168
+ }
169
+
170
+ // No nested match — invoke the subcommand's own action
171
+ if (!sub.action) {
172
+ output.printInfo(`Run \`monobrowse ${subName} --help\` for usage.`);
173
+ return;
174
+ }
175
+
176
+ const { args, flags } = parseArgv(rest, sub);
177
+ const ctx: CommandContext = { args, flags, cwd: process.cwd(), interactive: !!process.stdout.isTTY };
178
+ const result = await sub.action(ctx);
179
+ if (result && !result.success) process.exitCode = result.exitCode ?? 1;
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Main
184
+ // ---------------------------------------------------------------------------
185
+
186
+ async function main(): Promise<void> {
187
+ const userArgs = process.argv.slice(2);
188
+
189
+ if (userArgs.length === 0 || userArgs[0] === '--help' || userArgs[0] === '-h') {
190
+ printHelp(browseCommand);
191
+ return;
192
+ }
193
+
194
+ if (userArgs[0] === '--version' || userArgs[0] === '-V') {
195
+ const pkg = _require('../package.json') as { version: string };
196
+ console.log(pkg.version);
197
+ return;
198
+ }
199
+
200
+ await dispatch(browseCommand, userArgs);
201
+ }
202
+
203
+ main().then(() => {
204
+ process.exit(process.exitCode ?? 0);
205
+ }).catch(err => {
206
+ output.printError(err instanceof Error ? err.message : String(err));
207
+ process.exit(1);
208
+ });
package/src/index.ts CHANGED
@@ -1,6 +1,25 @@
1
+ // Re-export the full monoplaybook engine so consumers only need @monoes/monobrowse
2
+ export * from '@monoes/monoplaybook';
3
+
1
4
  export * from './browser/index.js';
2
- export * from './browser/workflow/index.js';
3
5
  export * from './browser/action-builder/analyzer.js';
4
6
  export * from './browser/action-builder/types.js';
5
7
  export * from './browser/adapters/index.js';
6
8
  export { startDashboard, getDashboard } from './browser/dashboard/server.js';
9
+ export { createBuiltinHandlers, createBrowserHandlers } from './browser/playbook/index.js';
10
+ export { readAction } from './browser/playbook/store.js';
11
+
12
+ // Batteries-included handler factory: service nodes + browser automation + builtins.
13
+ // This is the recommended entry point when using monobrowse as the default executor.
14
+ import { createNodeHandlers } from '@monoes/monoplaybook';
15
+ import { createBrowserHandlers } from './browser/playbook/browser-handlers.js';
16
+ import { createBuiltinHandlers } from './browser/playbook/builtin-handlers.js';
17
+ import type { NodeHandler } from '@monoes/monoplaybook';
18
+
19
+ export function createDefaultHandlers(): Map<string, NodeHandler> {
20
+ return new Map<string, NodeHandler>([
21
+ ...createNodeHandlers(),
22
+ ...createBrowserHandlers(),
23
+ ...createBuiltinHandlers(),
24
+ ]);
25
+ }
@@ -1,202 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { resolveExpression, resolveConfig } from './expression.js';
3
- import { getDashboard } from '../dashboard/server.js';
4
- import type { WorkflowDef, NodeDef, Item, RunRecord, StepEvent, RunStatus } from './types.js';
5
-
6
- export type NodeHandler = (items: Item[], config: Record<string, unknown>) => Promise<Item[]>;
7
-
8
- export interface EngineOptions {
9
- handlers?: Map<string, NodeHandler>;
10
- onEvent?: (event: StepEvent) => void;
11
- signal?: AbortSignal;
12
- params?: Record<string, string>;
13
- }
14
-
15
- export async function runWorkflow(
16
- def: WorkflowDef,
17
- options: EngineOptions = {},
18
- ): Promise<RunRecord> {
19
- const { handlers = new Map(), onEvent = () => {}, signal, params = {} } = options;
20
- const runId = randomUUID();
21
- const startedAt = Date.now();
22
-
23
- // Build a unified abort controller that merges the caller's signal with dashboard stop-requests.
24
- const controller = new AbortController();
25
- if (signal) {
26
- if (signal.aborted) {
27
- controller.abort();
28
- } else {
29
- signal.addEventListener('abort', () => controller.abort(), { once: true });
30
- }
31
- }
32
- const dashboard = getDashboard();
33
- let stopPollTimer: ReturnType<typeof setInterval> | undefined;
34
- if (dashboard) {
35
- stopPollTimer = setInterval(() => {
36
- if (dashboard.isStopRequested(runId)) controller.abort();
37
- }, 500);
38
- // Avoid keeping Node.js event loop alive if nothing else is running
39
- stopPollTimer.unref?.();
40
- }
41
-
42
- const emit = (partial: Omit<StepEvent, 'runId' | 'workflowId' | 'workflowName' | 'timestamp'>) =>
43
- onEvent({ runId, workflowId: def.id, workflowName: def.name, timestamp: Date.now(), ...partial });
44
-
45
- emit({ nodeId: '', nodeName: '', eventType: 'run_started' });
46
-
47
- // Build adjacency structures
48
- const nodeMap = new Map(def.nodes.map(n => [n.id, n]));
49
- const inDegree = new Map<string, number>(def.nodes.map(n => [n.id, 0]));
50
- const outEdges = new Map<string, { to: string; handle?: string }[]>();
51
- const toEdges = new Map<string, { from: string; handle?: string }[]>();
52
-
53
- for (const conn of def.connections) {
54
- inDegree.set(conn.to, (inDegree.get(conn.to) ?? 0) + 1);
55
- const edges = outEdges.get(conn.from) ?? [];
56
- edges.push({ to: conn.to, handle: conn.handle });
57
- outEdges.set(conn.from, edges);
58
- const toList = toEdges.get(conn.to) ?? [];
59
- toList.push({ from: conn.from, handle: conn.handle });
60
- toEdges.set(conn.to, toList);
61
- }
62
-
63
- // Kahn's topological sort
64
- const queue: string[] = [];
65
- const sorted: string[] = [];
66
- for (const [id, deg] of inDegree) {
67
- if (deg === 0) queue.push(id);
68
- }
69
- while (queue.length > 0) {
70
- const id = queue.shift()!;
71
- sorted.push(id);
72
- for (const edge of outEdges.get(id) ?? []) {
73
- const newDeg = (inDegree.get(edge.to) ?? 0) - 1;
74
- inDegree.set(edge.to, newDeg);
75
- if (newDeg === 0) queue.push(edge.to);
76
- }
77
- }
78
- if (sorted.length !== def.nodes.length) {
79
- throw new Error('Workflow contains a cycle');
80
- }
81
-
82
- // Node output accumulator
83
- const nodeOutputs = new Map<string, Item[]>();
84
- let itemsProcessed = 0;
85
- let runStatus: RunStatus = 'completed';
86
- let runError: string | undefined;
87
-
88
- try {
89
- for (const nodeId of sorted) {
90
- if (controller.signal.aborted) {
91
- runStatus = 'stopped';
92
- emit({ nodeId, nodeName: nodeId, eventType: 'run_stopped' });
93
- break;
94
- }
95
-
96
- const node = nodeMap.get(nodeId)!;
97
- const nodeName = node.name ?? node.id;
98
- const t0 = Date.now();
99
-
100
- // Collect inputs from predecessor outputs
101
- const inputItems = collectInputs(nodeId, nodeOutputs, toEdges);
102
-
103
- emit({ nodeId, nodeName, eventType: 'step_started', itemTotal: inputItems.length });
104
-
105
- try {
106
- const outputs = await executeNode(node, inputItems, handlers, nodeOutputs, params, def.allowEnvAccess);
107
- nodeOutputs.set(nodeId, outputs);
108
- itemsProcessed += outputs.length;
109
- emit({ nodeId, nodeName, eventType: 'step_completed', durationMs: Date.now() - t0, itemTotal: outputs.length });
110
- } catch (err) {
111
- const error = err instanceof Error ? err.message : String(err);
112
- emit({ nodeId, nodeName, eventType: 'step_failed', error, durationMs: Date.now() - t0 });
113
- if (node.onError === 'skip') {
114
- nodeOutputs.set(nodeId, []);
115
- } else {
116
- runStatus = 'failed';
117
- runError = error;
118
- break;
119
- }
120
- }
121
- }
122
- } finally {
123
- if (stopPollTimer !== undefined) clearInterval(stopPollTimer);
124
- }
125
-
126
- const completedAt = Date.now();
127
- const record: RunRecord = {
128
- id: runId,
129
- workflowId: def.id,
130
- workflowName: def.name,
131
- status: runStatus,
132
- startedAt,
133
- completedAt,
134
- itemsProcessed,
135
- itemsTotal: itemsProcessed,
136
- error: runError,
137
- };
138
-
139
- emit({ nodeId: '', nodeName: '', eventType: runStatus === 'completed' ? 'run_completed' : 'run_stopped', error: runError });
140
- return record;
141
- }
142
-
143
- function collectInputs(nodeId: string, nodeOutputs: Map<string, Item[]>, toEdges: Map<string, { from: string; handle?: string }[]>): Item[] {
144
- const predecessors = toEdges.get(nodeId) ?? [];
145
- if (predecessors.length === 0) return [{ data: {} }];
146
- return predecessors.flatMap(({ from, handle }) => {
147
- const items = nodeOutputs.get(from) ?? [];
148
- if (handle === 'true') return items.filter(item => item.data['__ifResult'] === true);
149
- if (handle === 'false') return items.filter(item => item.data['__ifResult'] === false);
150
- return items;
151
- });
152
- }
153
-
154
- async function executeNode(
155
- node: NodeDef,
156
- inputs: Item[],
157
- handlers: Map<string, NodeHandler>,
158
- nodeOutputs: Map<string, Item[]>,
159
- params: Record<string, string>,
160
- allowEnvAccess?: boolean,
161
- ): Promise<Item[]> {
162
- const allOutputs: Record<string, Item[]> = Object.fromEntries(nodeOutputs);
163
- const { type, config } = node;
164
-
165
- if (type === 'trigger.manual') {
166
- const items = config['items'];
167
- if (Array.isArray(items)) return items as Item[];
168
- return inputs;
169
- }
170
-
171
- if (type === 'core.set') {
172
- return inputs.map(item => {
173
- const resolved = resolveConfig(config, item, allOutputs, params, allowEnvAccess);
174
- return { ...item, data: { ...item.data, ...resolved } };
175
- });
176
- }
177
-
178
- if (type === 'core.filter') {
179
- const predicate = config['expression'] as string;
180
- return inputs.filter(item => {
181
- try {
182
- return Boolean(resolveExpression(predicate, item, allOutputs, params, allowEnvAccess));
183
- } catch {
184
- return false;
185
- }
186
- });
187
- }
188
-
189
- if (type === 'core.if') {
190
- const predicate = config['expression'] as string;
191
- return inputs.map(item => {
192
- const result = Boolean(resolveExpression(predicate, item, allOutputs, params, allowEnvAccess));
193
- return { ...item, data: { ...item.data, __ifResult: result } };
194
- });
195
- }
196
-
197
- // action.* — delegate to registered handler
198
- const handler = handlers.get(type);
199
- if (!handler) throw new Error(`No handler registered for node type: ${type}`);
200
- const resolvedConfig = resolveConfig(config, inputs[0] ?? { data: {} }, allOutputs, params, allowEnvAccess);
201
- return handler(inputs, resolvedConfig);
202
- }
@@ -1,107 +0,0 @@
1
- import type { Item } from './types.js';
2
-
3
- const TEMPLATE_PATTERN = '\\{\\{([^}]+)\\}\\}';
4
-
5
- /** Env var names matching this pattern are blocked from $env expressions to prevent secret exfiltration. */
6
- const ENV_DENYLIST = /(_KEY|_TOKEN|_SECRET|_PASSWORD|_JWT|_API_KEY|_PRIVATE_KEY|_PASS|_PWD|_CERT|_PEM|_CREDENTIAL|_CREDENTIALS|_AUTH)$/i;
7
- type ParsedTemplate = RegExpMatchArray[];
8
- const cache = new Map<string, ParsedTemplate>();
9
-
10
- function extractTemplates(template: string): ParsedTemplate {
11
- if (cache.has(template)) return cache.get(template)!;
12
- // LRU eviction: drop the oldest entry rather than clearing all at once (thundering-herd).
13
- if (cache.size >= 500) {
14
- const oldest = cache.keys().next().value;
15
- if (oldest !== undefined) cache.delete(oldest);
16
- }
17
- const matches = [...template.matchAll(new RegExp(TEMPLATE_PATTERN, 'g'))];
18
- cache.set(template, matches);
19
- return matches;
20
- }
21
-
22
- export function resolveExpression(
23
- template: string,
24
- item: Item,
25
- nodeOutputs: Record<string, Item[]>,
26
- params: Record<string, string>,
27
- allowEnvAccess?: boolean,
28
- ): string {
29
- const matches = extractTemplates(template);
30
- if (matches.length === 0) return template;
31
-
32
- let result = template;
33
- for (const match of matches) {
34
- const expr = match[1].trim();
35
- const value = resolveToken(expr, item, nodeOutputs, params, allowEnvAccess);
36
- result = result.split(match[0]).join(String(value));
37
- }
38
- return result;
39
- }
40
-
41
- function resolveToken(
42
- expr: string,
43
- item: Item,
44
- nodeOutputs: Record<string, Item[]>,
45
- params: Record<string, string>,
46
- allowEnvAccess?: boolean,
47
- ): unknown {
48
- if (expr.startsWith('$json.')) {
49
- const key = expr.slice(6);
50
- if (!(key in item.data)) throw new Error(`Unresolved: $json.${key} not found in item data`);
51
- return item.data[key];
52
- }
53
- if (expr.startsWith('$env.')) {
54
- const key = expr.slice(5);
55
- if (!allowEnvAccess && ENV_DENYLIST.test(key)) {
56
- throw new Error(
57
- `Blocked: env var "${key}" matches secret pattern. Set allowEnvAccess: true in workflow config to override.`,
58
- );
59
- }
60
- const val = process.env[key];
61
- if (val === undefined) throw new Error(`Unresolved: $env.${key} not set`);
62
- return val;
63
- }
64
- if (expr.startsWith('params.')) {
65
- const key = expr.slice(7);
66
- if (!(key in params)) throw new Error(`Unresolved: params.${key} not provided`);
67
- return params[key];
68
- }
69
- if (expr.startsWith('$node.') || expr.startsWith('$node["')) {
70
- let nodeId: string;
71
- let field: string;
72
- if (expr.startsWith('$node["')) {
73
- // $node["NodeId"].field
74
- const bracketEnd = expr.indexOf('"].');
75
- if (bracketEnd === -1) throw new Error(`Unresolved: malformed $node bracket expression: ${expr}`);
76
- nodeId = expr.slice(7, bracketEnd); // slice off '$node["'
77
- field = expr.slice(bracketEnd + 3); // slice off '"].'
78
- } else {
79
- // $node.NodeId.field
80
- const parts = expr.slice(6).split('.');
81
- nodeId = parts[0];
82
- field = parts.slice(1).join('.');
83
- }
84
- const items = nodeOutputs[nodeId];
85
- if (!items || items.length === 0) throw new Error(`Unresolved: $node.${nodeId} has no output`);
86
- const val = items[0].data[field];
87
- if (val === undefined) throw new Error(`Unresolved: $node.${nodeId}.${field} not found`);
88
- return val;
89
- }
90
- // Named ref (e.g. {{box}} from a find step) — action executor resolves these using its element handle map
91
- return `{{${expr}}}`;
92
- }
93
-
94
- // Note: only resolves top-level string values. Nested objects/arrays are passed through unchanged.
95
- export function resolveConfig(
96
- config: Record<string, unknown>,
97
- item: Item,
98
- nodeOutputs: Record<string, Item[]>,
99
- params: Record<string, string>,
100
- allowEnvAccess?: boolean,
101
- ): Record<string, unknown> {
102
- const result: Record<string, unknown> = {};
103
- for (const [k, v] of Object.entries(config)) {
104
- result[k] = typeof v === 'string' ? resolveExpression(v, item, nodeOutputs, params, allowEnvAccess) : v;
105
- }
106
- return result;
107
- }
@@ -1,5 +0,0 @@
1
- export * from './types.js';
2
- export * from './store.js';
3
- export * from './engine.js';
4
- export * from './expression.js';
5
- export * from './builtin-handlers.js';
@@ -1,62 +0,0 @@
1
- export interface WorkflowDef {
2
- id: string;
3
- name: string;
4
- description?: string;
5
- params?: Record<string, { type?: string; required?: boolean; default?: unknown; description?: string }>;
6
- nodes: NodeDef[];
7
- connections: ConnectionDef[];
8
- /** Allow $env.* expressions to access env vars that match the secret denylist. Use with caution. */
9
- allowEnvAccess?: boolean;
10
- }
11
-
12
- export interface NodeDef {
13
- id: string;
14
- type: string;
15
- name?: string;
16
- config: Record<string, unknown>;
17
- onError?: 'skip' | 'stop';
18
- timeoutMs?: number;
19
- }
20
-
21
- export interface ConnectionDef {
22
- from: string;
23
- to: string;
24
- handle?: string; // port identifier for multi-output nodes (e.g. 'true'/'false' for core.if)
25
- }
26
-
27
- export interface Item {
28
- data: Record<string, unknown>;
29
- binaryBase64?: string; // base64-encoded binary payload
30
- }
31
-
32
- export type RunStatus = 'running' | 'completed' | 'failed' | 'stopped';
33
-
34
- export interface RunRecord {
35
- id: string;
36
- workflowId: string;
37
- workflowName: string;
38
- status: RunStatus;
39
- startedAt: number;
40
- completedAt?: number;
41
- itemsProcessed: number;
42
- itemsTotal: number;
43
- error?: string;
44
- }
45
-
46
- export interface StepEvent {
47
- runId: string;
48
- workflowId: string;
49
- workflowName: string;
50
- nodeId: string;
51
- nodeName: string;
52
- eventType: 'run_started' | 'step_started' | 'step_completed' | 'step_failed' | 'run_completed' | 'run_stopped';
53
- itemIndex?: number;
54
- itemTotal?: number;
55
- durationMs?: number;
56
- error?: string;
57
- timestamp: number;
58
- /** Absolute path of the project directory that emitted this event.
59
- * Clients may subscribe to a specific project by passing `?dir=<path>`.
60
- * Events without a projectDir are delivered to all clients (backward-compatible). */
61
- projectDir?: string;
62
- }