@xdelivered/emberflow 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands.ts +7 -3
- package/bin/init.test.ts +94 -0
- package/bin/init.ts +108 -2
- package/dist/bin/commands.d.ts +1 -0
- package/dist/bin/commands.js +6 -3
- package/dist/bin/init.d.ts +4 -0
- package/dist/bin/init.js +96 -2
- package/dist/server/agents/codexParse.js +31 -0
- package/dist/server/agents/detect.d.ts +18 -8
- package/dist/server/agents/detect.js +78 -13
- package/dist/server/agents/modelRejectionHint.js +1 -1
- package/dist/server/agents/prompt.d.ts +15 -1
- package/dist/server/agents/prompt.js +59 -37
- package/dist/server/agents/runManager.d.ts +23 -2
- package/dist/server/agents/runManager.js +36 -8
- package/dist/server/client.d.ts +1 -0
- package/dist/server/client.js +7 -2
- package/dist/server/index.js +208 -16
- package/dist/server/nodesPayload.d.ts +14 -1
- package/dist/server/nodesPayload.js +15 -2
- package/dist/server/projectMode.js +5 -2
- package/dist/server/runRegistry.d.ts +52 -1
- package/dist/server/runRegistry.js +170 -1
- package/dist/server/sourceNav.d.ts +77 -0
- package/dist/server/sourceNav.js +503 -0
- package/dist/server/subflowRunner.d.ts +48 -1
- package/dist/server/subflowRunner.js +34 -7
- package/dist/server/updateCheck.d.ts +68 -0
- package/dist/server/updateCheck.js +142 -0
- package/dist/src/engine/executor.js +4 -3
- package/dist/src/engine/registry.d.ts +27 -2
- package/dist/src/engine/registry.js +84 -2
- package/dist/src/nodes/index.d.ts +8 -2
- package/dist/src/nodes/index.js +7 -3
- package/dist/src/nodes/login.d.ts +3 -1
- package/dist/src/nodes/login.js +2 -2
- package/package.json +28 -7
- package/server/agentRoute.test.ts +20 -0
- package/server/agents/codexParse.test.ts +37 -0
- package/server/agents/codexParse.ts +34 -0
- package/server/agents/detect.test.ts +26 -7
- package/server/agents/detect.ts +82 -14
- package/server/agents/modelRejectionHint.ts +2 -1
- package/server/agents/prompt.test.ts +128 -0
- package/server/agents/prompt.ts +102 -3
- package/server/agents/runManager.test.ts +9 -0
- package/server/agents/runManager.ts +37 -7
- package/server/client.ts +10 -4
- package/server/index.ts +213 -15
- package/server/nodeRun.test.ts +189 -0
- package/server/nodesPayload.test.ts +38 -0
- package/server/nodesPayload.ts +28 -3
- package/server/projectMode.ts +5 -2
- package/server/runRegistry.ts +200 -3
- package/server/setupStatus.test.ts +3 -0
- package/server/sourceNav.test.ts +382 -0
- package/server/sourceNav.ts +644 -0
- package/server/sourceNavRoute.test.ts +163 -0
- package/server/stepDrill.test.ts +380 -0
- package/server/subflowRunner.ts +92 -11
- package/server/updateCheck.test.ts +209 -0
- package/server/updateCheck.ts +175 -0
- package/server/workflowTestRoute.test.ts +1 -1
- package/src/App.tsx +82 -2
- package/src/components/AgentConsole.tsx +7 -280
- package/src/components/AgentStream.test.tsx +88 -0
- package/src/components/AgentStream.tsx +303 -0
- package/src/components/CreateModal.tsx +43 -9
- package/src/components/Dock.tsx +1 -1
- package/src/components/EmptyState.test.tsx +49 -0
- package/src/components/EmptyState.tsx +61 -0
- package/src/components/EnvironmentDialog.tsx +4 -1
- package/src/components/EnvironmentPicker.tsx +8 -3
- package/src/components/EnvironmentsDialog.tsx +4 -1
- package/src/components/InfraPanel.tsx +30 -2
- package/src/components/InfrastructureDialog.test.tsx +97 -0
- package/src/components/InfrastructureDialog.tsx +111 -0
- package/src/components/Inspector.tsx +9 -26
- package/src/components/RunbookView.tsx +70 -6
- package/src/components/SettingsDialog.tsx +4 -59
- package/src/components/Sidebar.tsx +6 -26
- package/src/components/SourceNavigator.test.tsx +226 -0
- package/src/components/SourceNavigator.tsx +520 -0
- package/src/components/StatusBar.tsx +227 -26
- package/src/components/Toolbar.tsx +28 -16
- package/src/components/UpdateChip.test.tsx +31 -0
- package/src/components/WelcomeDialog.test.tsx +384 -13
- package/src/components/WelcomeDialog.tsx +996 -177
- package/src/engine/executor.ts +4 -3
- package/src/engine/registry.sourceRef.test.ts +112 -0
- package/src/engine/registry.ts +103 -2
- package/src/lib/guidedQuestions.test.ts +224 -0
- package/src/lib/guidedQuestions.ts +181 -0
- package/src/lib/runbookModel.ts +3 -3
- package/src/lib/runbookProjection.test.ts +43 -0
- package/src/lib/runbookProjection.ts +26 -6
- package/src/nodes/index.ts +10 -3
- package/src/nodes/login.ts +5 -2
- package/src/store/agentClient.ts +27 -8
- package/src/store/apiTree.ts +12 -0
- package/src/store/builderStore.test.ts +441 -99
- package/src/store/builderStore.ts +385 -314
- package/src/store/nodeMeta.ts +10 -2
- package/src/store/serverRunner.ts +56 -5
- package/src/store/setupClient.ts +7 -2
- package/src/store/sourceNavClient.ts +48 -0
- package/src/store/updateClient.ts +50 -0
- package/studio-dist/assets/index-BbzppDpt.js +65 -0
- package/studio-dist/assets/index-CLf6blcA.css +1 -0
- package/studio-dist/index.html +2 -2
- package/templates/skills/emberflow-basics/SKILL.md +29 -1
- package/templates/skills/emberflow-model-process/SKILL.md +92 -9
- package/templates/skills/emberflow-new-workflow/SKILL.md +8 -1
- package/templates/skills/emberflow-review-workflow/SKILL.md +64 -1
- package/studio-dist/assets/index-DNJwW-hM.css +0 -1
- package/studio-dist/assets/index-O26dKRjW.js +0 -65
package/src/lib/runbookModel.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { WorkflowDefinition, WorkflowEdge } from '../engine';
|
|
2
|
-
import { computeLoopRegions, topoSort, type LoopRegion } from '../engine';
|
|
3
|
-
import type { NodeRegistry } from '../engine';
|
|
1
|
+
import type { WorkflowDefinition, WorkflowEdge } from '../engine/types';
|
|
2
|
+
import { computeLoopRegions, topoSort, type LoopRegion } from '../engine/validation';
|
|
3
|
+
import type { NodeRegistry } from '../engine/registry';
|
|
4
4
|
import { firstSentence, simpleNodeDescription } from './registerLens';
|
|
5
5
|
|
|
6
6
|
export interface RunbookStep {
|
|
@@ -209,6 +209,49 @@ describe('projectRunbook', () => {
|
|
|
209
209
|
expect(proj.steps.get('onStep')!.tech).toBe(`#2 Http · 50ms · ERROR: ${'x'.repeat(80)}`);
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
+
it('drilled view: outcome and receipt match caller-prefixed child log ids (`caller/nodeId`); undrilled does not', () => {
|
|
213
|
+
const doc = makeDoc();
|
|
214
|
+
const run = makeRun({
|
|
215
|
+
nodeStates: {
|
|
216
|
+
onStep: {
|
|
217
|
+
status: 'succeeded',
|
|
218
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
219
|
+
completedAt: '2026-01-01T00:00:00.100Z',
|
|
220
|
+
input: { a: 1 },
|
|
221
|
+
output: { x: 1 },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
// How a drilled subflow child's logs arrive: nodeId prefixed with the
|
|
226
|
+
// caller chain (`sub/onStep`, nested per level).
|
|
227
|
+
const logs: LogLine[] = [
|
|
228
|
+
{ timestamp: 't1', level: 'debug', runId: 'run-1', nodeId: 'sub/onStep', message: '#4 ▶ execute' },
|
|
229
|
+
{ timestamp: 't2', level: 'info', runId: 'run-1', nodeId: 'sub/onStep', message: 'child outcome' },
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const drilled = projectRunbook(doc, run, logs, [], FLOW_ID, true);
|
|
233
|
+
expect(drilled.steps.get('onStep')!.outcome).toBe('child outcome');
|
|
234
|
+
expect(drilled.steps.get('onStep')!.tech).toBe('#4 Http · 100ms · in[a] → out[x]');
|
|
235
|
+
|
|
236
|
+
// Undrilled: suffix matching stays off so a parent node can never pick
|
|
237
|
+
// up a same-named child's prefixed lines.
|
|
238
|
+
const plain = projectRunbook(doc, run, logs, [], FLOW_ID);
|
|
239
|
+
expect(plain.steps.get('onStep')!.outcome).toBe('');
|
|
240
|
+
expect(plain.steps.get('onStep')!.tech).toBe('Http · 100ms · in[a] → out[x]');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('drilled view: a plain (unprefixed) id must not suffix-match another node ending with the same segment', () => {
|
|
244
|
+
const doc = makeDoc();
|
|
245
|
+
const logs: LogLine[] = [
|
|
246
|
+
// `sub/onStep` ends with `/onStep` but NOT with `/Step` etc.; also an
|
|
247
|
+
// exact-id line for a different node must not bleed over.
|
|
248
|
+
{ timestamp: 't1', level: 'info', runId: 'run-1', nodeId: 'cond', message: 'cond outcome' },
|
|
249
|
+
];
|
|
250
|
+
const drilled = projectRunbook(doc, makeRun(), logs, [], FLOW_ID, true);
|
|
251
|
+
expect(drilled.steps.get('onStep')!.outcome).toBe('');
|
|
252
|
+
expect(drilled.steps.get('cond')!.outcome).toBe('cond outcome');
|
|
253
|
+
});
|
|
254
|
+
|
|
212
255
|
it('tech line: no receipt yet means no #N prefix', () => {
|
|
213
256
|
const doc = makeDoc();
|
|
214
257
|
const run = makeRun({ nodeStates: { onStep: { status: 'idle' } } });
|
|
@@ -56,10 +56,24 @@ function fieldList(value: unknown, label: string): string {
|
|
|
56
56
|
return `${label}[${shown.join(',')}]`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Whether a log line belongs to the given node. A drilled-in subflow child's
|
|
61
|
+
* log lines carry nodeIds prefixed with the caller chain
|
|
62
|
+
* (`${callerNodeId}/${childNodeId}`, nested per level — see subflowRunner),
|
|
63
|
+
* while the drilled view's runbook uses the raw child ids, so when projecting
|
|
64
|
+
* a drilled child flow a suffix match after a slash also counts. Kept behind
|
|
65
|
+
* `drilled` so an undriled parent view can never match a child's prefixed
|
|
66
|
+
* line against a same-named parent node.
|
|
67
|
+
*/
|
|
68
|
+
function lineIsFor(line: LogLine, nodeId: string, drilled: boolean): boolean {
|
|
69
|
+
if (line.nodeId === nodeId) return true;
|
|
70
|
+
return drilled && !!line.nodeId && line.nodeId.endsWith(`/${nodeId}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function lastInfoOutcome(logs: LogLine[], nodeId: string, drilled: boolean): string {
|
|
60
74
|
for (let i = logs.length - 1; i >= 0; i--) {
|
|
61
75
|
const line = logs[i];
|
|
62
|
-
if (line
|
|
76
|
+
if (lineIsFor(line, nodeId, drilled) && line.level === 'info') return line.message;
|
|
63
77
|
}
|
|
64
78
|
return '';
|
|
65
79
|
}
|
|
@@ -72,10 +86,10 @@ function lastInfoOutcome(logs: LogLine[], nodeId: string): string {
|
|
|
72
86
|
* iteration); null when the node has no receipt yet (never executed).
|
|
73
87
|
*/
|
|
74
88
|
const RECEIPT_RE = /^#(\d+) ▶ execute/;
|
|
75
|
-
function receiptSeq(logs: LogLine[], nodeId: string): string | null {
|
|
89
|
+
function receiptSeq(logs: LogLine[], nodeId: string, drilled: boolean): string | null {
|
|
76
90
|
for (let i = logs.length - 1; i >= 0; i--) {
|
|
77
91
|
const line = logs[i];
|
|
78
|
-
if (line
|
|
92
|
+
if (lineIsFor(line, nodeId, drilled) && line.level === 'debug') {
|
|
79
93
|
const match = RECEIPT_RE.exec(line.message);
|
|
80
94
|
if (match) return match[1];
|
|
81
95
|
}
|
|
@@ -152,6 +166,9 @@ export function projectRunbook(
|
|
|
152
166
|
logs: LogLine[],
|
|
153
167
|
history: RunHistoryEntry[],
|
|
154
168
|
flowId: string,
|
|
169
|
+
/** True when projecting a drilled-in subflow child view: log lines then
|
|
170
|
+
* also match by `…/${nodeId}` suffix (child log ids are caller-prefixed). */
|
|
171
|
+
drilled = false,
|
|
155
172
|
): RunbookProjection {
|
|
156
173
|
// ── steps ──
|
|
157
174
|
const steps = new Map<string, StepProjection>();
|
|
@@ -160,10 +177,13 @@ export function projectRunbook(
|
|
|
160
177
|
const status = statusOf(state);
|
|
161
178
|
// A failed node's outcome line is its error, not the last info log (which
|
|
162
179
|
// may be stale from a prior successful attempt or simply absent).
|
|
163
|
-
const outcome =
|
|
180
|
+
const outcome =
|
|
181
|
+
status === 'failed'
|
|
182
|
+
? state?.error || lastInfoOutcome(logs, nodeId, drilled)
|
|
183
|
+
: lastInfoOutcome(logs, nodeId, drilled);
|
|
164
184
|
const durationMs = durationOf(state);
|
|
165
185
|
// `#N` receipt prefix once the node has executed; bare type name before that.
|
|
166
|
-
const seq = receiptSeq(logs, nodeId);
|
|
186
|
+
const seq = receiptSeq(logs, nodeId, drilled);
|
|
167
187
|
const prefix = seq ? `#${seq} ` : '';
|
|
168
188
|
const ms = `${durationMs ?? 0}ms`;
|
|
169
189
|
let tech: string;
|
package/src/nodes/index.ts
CHANGED
|
@@ -8,9 +8,16 @@ import { registerPradarNodesModule } from './pradar';
|
|
|
8
8
|
import { registerResponseNodes } from './response';
|
|
9
9
|
import { registerRequireAuthNode } from './requireAuth';
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
/**
|
|
12
|
+
* All built-in nodes: login examples, Open-Meteo weather, anomaly-detection API, EV charging demo, HTTP Response, requireAuth.
|
|
13
|
+
* `opts.captureSourceRefs` is threaded from SERVER callers only (buildRegistries)
|
|
14
|
+
* — this function is shared with the browser bundle, where capture stays off.
|
|
15
|
+
*/
|
|
16
|
+
export function createDefaultRegistry(
|
|
17
|
+
delayMs?: number,
|
|
18
|
+
opts?: { captureSourceRefs?: boolean },
|
|
19
|
+
): NodeRegistry {
|
|
20
|
+
const registry = createLoginRegistry(delayMs, opts);
|
|
14
21
|
registerWeatherNodes(registry);
|
|
15
22
|
registerAnomalyNodes(registry);
|
|
16
23
|
registerFlowControlNodes(registry);
|
package/src/nodes/login.ts
CHANGED
|
@@ -12,8 +12,11 @@ function usernamePart(userId: string): string {
|
|
|
12
12
|
* `delayMs` is injected into every implementation via `await sleep(delayMs)`
|
|
13
13
|
* so tests can run with no delay by passing `0`.
|
|
14
14
|
*/
|
|
15
|
-
export function createLoginRegistry(
|
|
16
|
-
|
|
15
|
+
export function createLoginRegistry(
|
|
16
|
+
delayMs = 300,
|
|
17
|
+
opts?: { captureSourceRefs?: boolean },
|
|
18
|
+
): NodeRegistry {
|
|
19
|
+
const registry = new NodeRegistry(opts);
|
|
17
20
|
|
|
18
21
|
registry.register(
|
|
19
22
|
{
|
package/src/store/agentClient.ts
CHANGED
|
@@ -40,6 +40,7 @@ export type AgentIntent =
|
|
|
40
40
|
| { action: 'setup-auth'; environment: string; instruction: string }
|
|
41
41
|
| { action: 'setup-environments'; instruction: string }
|
|
42
42
|
| { action: 'scout-infrastructure'; instruction: string }
|
|
43
|
+
| { action: 'guided-setup'; instruction: string }
|
|
43
44
|
| { action: 'cover-operation'; flowId: string; instruction: string }
|
|
44
45
|
| { action: 'ask'; flowId?: string; instruction: string };
|
|
45
46
|
|
|
@@ -63,11 +64,18 @@ export async function fetchAvailableAgents(): Promise<DetectedAgent[]> {
|
|
|
63
64
|
|
|
64
65
|
/** POST /agent — start an agent run, returns its id. */
|
|
65
66
|
export async function startAgent(intent: AgentIntent, opts?: StartAgentOptions): Promise<string> {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
let response: Response;
|
|
68
|
+
try {
|
|
69
|
+
response = await fetch(`${BASE}/agent`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'content-type': 'application/json' },
|
|
72
|
+
body: JSON.stringify({ intent, ...opts }),
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
// fetch rejects with a bare "Failed to fetch" on network errors — say what
|
|
76
|
+
// that actually means here and what to do about it.
|
|
77
|
+
throw new Error('Could not reach the runner — check it is still running, then try again.');
|
|
78
|
+
}
|
|
71
79
|
if (!response.ok) {
|
|
72
80
|
const body = (await response.json().catch(() => ({}))) as { error?: string };
|
|
73
81
|
throw new Error(body.error ?? `Failed to start agent (HTTP ${response.status})`);
|
|
@@ -84,16 +92,27 @@ export async function startAgent(intent: AgentIntent, opts?: StartAgentOptions):
|
|
|
84
92
|
*/
|
|
85
93
|
export function streamAgent(agentRunId: string, onEvent: (event: AgentEvent) => void): () => void {
|
|
86
94
|
const source = new EventSource(`${BASE}/agent/${agentRunId}/events`);
|
|
95
|
+
// Set once the server delivers the run's terminal event — from then on any
|
|
96
|
+
// connection-level error (the server closing the response right after) is
|
|
97
|
+
// expected, not a lost stream, so onerror must stay quiet.
|
|
98
|
+
let sawTerminal = false;
|
|
87
99
|
const types: AgentEvent['type'][] = ['started', 'message', 'command', 'mcp', 'approval-request', 'done', 'error'];
|
|
88
100
|
for (const type of types) {
|
|
89
101
|
source.addEventListener(type, (event) => {
|
|
90
|
-
|
|
102
|
+
// EventSource dispatches its CONNECTION errors as `error` events too —
|
|
103
|
+
// those carry no data, unlike the server's named `event: error` payloads.
|
|
104
|
+
const raw = (event as MessageEvent).data as string | undefined;
|
|
105
|
+
if (raw === undefined) return;
|
|
106
|
+
const data = JSON.parse(raw) as AgentEvent;
|
|
91
107
|
onEvent(data);
|
|
92
|
-
if (type === 'done' || type === 'error')
|
|
108
|
+
if (type === 'done' || type === 'error') {
|
|
109
|
+
sawTerminal = true;
|
|
110
|
+
source.close();
|
|
111
|
+
}
|
|
93
112
|
});
|
|
94
113
|
}
|
|
95
114
|
source.onerror = () => {
|
|
96
|
-
if (source.readyState === EventSource.CLOSED) {
|
|
115
|
+
if (!sawTerminal && source.readyState === EventSource.CLOSED) {
|
|
97
116
|
onEvent({ type: 'error', text: 'Lost connection to the agent event stream' });
|
|
98
117
|
}
|
|
99
118
|
};
|
package/src/store/apiTree.ts
CHANGED
|
@@ -71,3 +71,15 @@ export function buildApiTree(ops: OpInput[]): ApiTreeNode[] {
|
|
|
71
71
|
tree.sort((a, b) => a.name.localeCompare(b.name));
|
|
72
72
|
return tree;
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
/** Flatten an API tree into every API/folder path (e.g. 'default', 'billing', 'billing/charges'). */
|
|
76
|
+
export function flattenLocations(tree: ApiTreeNode[]): string[] {
|
|
77
|
+
const paths: string[] = [];
|
|
78
|
+
const walk = (node: ApiTreeNode, prefix: string) => {
|
|
79
|
+
const path = prefix ? `${prefix}/${node.name}` : node.name;
|
|
80
|
+
paths.push(path);
|
|
81
|
+
for (const folder of node.folders) walk(folder, path);
|
|
82
|
+
};
|
|
83
|
+
for (const api of tree) walk(api, '');
|
|
84
|
+
return paths;
|
|
85
|
+
}
|