@keystrokehq/cli 0.1.27 → 0.1.29
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/{dist-Re6HHSqz.mjs → dist-9VWONIo1.mjs} +2 -2
- package/dist/{dist-Re6HHSqz.mjs.map → dist-9VWONIo1.mjs.map} +1 -1
- package/dist/{dist-CUEVu120.mjs → dist-BGJ-Gw_w.mjs} +252 -24
- package/dist/dist-BGJ-Gw_w.mjs.map +1 -0
- package/dist/{dist-C1QOfWMM.mjs → dist-C0_71CpG.mjs} +1974 -50
- package/dist/dist-C0_71CpG.mjs.map +1 -0
- package/dist/{dist-BOhrc_Nv.mjs → dist-COhdZicI.mjs} +553 -196
- package/dist/dist-COhdZicI.mjs.map +1 -0
- package/dist/{dist-DsdMtFME.mjs → dist-ClUopraJ.mjs} +1 -1
- package/dist/index.mjs +128 -13
- package/dist/index.mjs.map +1 -1
- package/dist/{maybe-auto-update-BDvSKDZp.mjs → maybe-auto-update--WoIaE-P.mjs} +2 -2
- package/dist/{maybe-auto-update-BDvSKDZp.mjs.map → maybe-auto-update--WoIaE-P.mjs.map} +1 -1
- package/dist/templates/hello-world/package.json +1 -0
- package/dist/templates/hello-world/src/actions/greet.ts +1 -0
- package/dist/templates/hello-world/src/agents/hello.ts +2 -0
- package/dist/templates/hello-world/src/workflows/greeting.ts +1 -0
- package/dist/{version-pY9N8XlL.mjs → version-DtFg32hj.mjs} +2 -2
- package/dist/{version-pY9N8XlL.mjs.map → version-DtFg32hj.mjs.map} +1 -1
- package/package.json +5 -5
- package/dist/dist-BOhrc_Nv.mjs.map +0 -1
- package/dist/dist-C1QOfWMM.mjs.map +0 -1
- package/dist/dist-CUEVu120.mjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { n as __require, t as __commonJSMin } from "./chunk-DiodbrVj.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import "./dist-
|
|
3
|
+
import { Dr as union, Er as string, Gt as PromptResponseSchema, Kt as PublicModelsResponseSchema, Qn as normalizeCredentialList, Sr as object, W as DEFAULT_CLOUD_PLATFORM_ORIGIN, Yt as ROUTE_MANIFEST_REL_PATH, _r as discriminatedUnion, dr as _function, gr as custom, hr as boolean$1, jr as toJSONSchema, lr as ZodType, mr as array, nr as parseStoredRouteManifest, qn as credentialInputSchema, rr as requiredDisplayTextSchema, ur as _enum, wr as preprocess, xr as number, yr as literal } from "./dist-COhdZicI.mjs";
|
|
4
|
+
import "./dist-9VWONIo1.mjs";
|
|
5
5
|
import "./chunk-BZUGFHVS-CPWRFwK8.mjs";
|
|
6
6
|
import "./chunk-DLL7UR66-BUYgzxnR.mjs";
|
|
7
7
|
import "./chunk-TN7HHBQW-CSB_R-XD.mjs";
|
|
@@ -24,6 +24,7 @@ import { readdir, stat } from "node:fs/promises";
|
|
|
24
24
|
import { pathToFileURL } from "node:url";
|
|
25
25
|
import { createHash } from "node:crypto";
|
|
26
26
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
27
|
+
import ts from "typescript";
|
|
27
28
|
import { boolean, doublePrecision, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
|
|
28
29
|
import { index as index$1, integer as integer$1, primaryKey as primaryKey$1, real, sqliteTable, text as text$1, uniqueIndex as uniqueIndex$1 } from "drizzle-orm/sqlite-core";
|
|
29
30
|
import { sql } from "drizzle-orm";
|
|
@@ -337,6 +338,1527 @@ function hasAppLayout(dir) {
|
|
|
337
338
|
const dist = join(dir, "dist");
|
|
338
339
|
return existsSync(join(src, "agents")) || existsSync(join(src, "skills")) || existsSync(join(src, "files")) || existsSync(join(dist, "agents")) || existsSync(join(dist, ".keystroke", "assets.mjs"));
|
|
339
340
|
}
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region ../../packages/workflow-canvas/dist/index.mjs
|
|
343
|
+
/** Root segment for statements in the workflow `run` body. */
|
|
344
|
+
const RUN_BODY_SEGMENT = "run-body";
|
|
345
|
+
/** Join path segments for hashing / debugging. */
|
|
346
|
+
function formatCallSitePath(path) {
|
|
347
|
+
return path.join(">");
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Deterministic id for a durable call site from its structural path. The Phase 5
|
|
351
|
+
* build transform must compute the same path + id for each invocation.
|
|
352
|
+
*/
|
|
353
|
+
function callSiteId(path) {
|
|
354
|
+
return createHash("sha256").update(formatCallSitePath(path)).digest("hex").slice(0, 16);
|
|
355
|
+
}
|
|
356
|
+
/** Id for an opaque code-block node at a structural location. */
|
|
357
|
+
function codeBlockSiteId(path) {
|
|
358
|
+
return callSiteId([...path, "code-block"]);
|
|
359
|
+
}
|
|
360
|
+
/** Unwrap `await`/parenthesized wrappers down to the inner expression. */
|
|
361
|
+
function unwrap(expr) {
|
|
362
|
+
let current = expr;
|
|
363
|
+
while (ts.isAwaitExpression(current) || ts.isParenthesizedExpression(current)) current = current.expression;
|
|
364
|
+
return current;
|
|
365
|
+
}
|
|
366
|
+
/** Leftmost identifier text of a property-access / call chain (`a.b().c` → "a"). */
|
|
367
|
+
function baseIdentifierText(expr) {
|
|
368
|
+
let current = expr;
|
|
369
|
+
while (true) {
|
|
370
|
+
if (ts.isPropertyAccessExpression(current)) {
|
|
371
|
+
current = current.expression;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (ts.isCallExpression(current)) {
|
|
375
|
+
current = current.expression;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (ts.isNonNullExpression(current) || ts.isParenthesizedExpression(current)) {
|
|
379
|
+
current = current.expression;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
return ts.isIdentifier(current) ? current.text : void 0;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Classify an expression as a durable step invocation, or return null. Handles
|
|
388
|
+
* the closed grammar: `x.run(...)`, `x.scope(...).run(...)`,
|
|
389
|
+
* `x.run(...).scope(...)`, `x.prompt(...)`, `promptLlm(...)`, and
|
|
390
|
+
* `ctx.sleep(...)` / `ctx.hook(...)`. Step identity is the structural
|
|
391
|
+
* {@link callSiteId}; there is no user-authored step id.
|
|
392
|
+
*/
|
|
393
|
+
function classifyCall(expression, ctxParamName) {
|
|
394
|
+
const expr = unwrap(expression);
|
|
395
|
+
if (!ts.isCallExpression(expr)) return null;
|
|
396
|
+
const callee = expr.expression;
|
|
397
|
+
if (ts.isIdentifier(callee)) {
|
|
398
|
+
if (callee.text === "promptLlm") return {
|
|
399
|
+
key: "promptLlm",
|
|
400
|
+
callKind: "llm"
|
|
401
|
+
};
|
|
402
|
+
if (callee.text === "sleep") return {
|
|
403
|
+
key: "sleep",
|
|
404
|
+
callKind: "wait"
|
|
405
|
+
};
|
|
406
|
+
if (callee.text === "hook") return {
|
|
407
|
+
key: "hook",
|
|
408
|
+
callKind: "hook"
|
|
409
|
+
};
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
if (!ts.isPropertyAccessExpression(callee)) return null;
|
|
413
|
+
const method = callee.name.text;
|
|
414
|
+
const base = baseIdentifierText(callee.expression);
|
|
415
|
+
switch (method) {
|
|
416
|
+
case "run":
|
|
417
|
+
if (!base) return null;
|
|
418
|
+
return {
|
|
419
|
+
key: base,
|
|
420
|
+
callKind: "workflow-step",
|
|
421
|
+
importName: base
|
|
422
|
+
};
|
|
423
|
+
case "prompt":
|
|
424
|
+
if (!base) return null;
|
|
425
|
+
return {
|
|
426
|
+
key: base,
|
|
427
|
+
callKind: "agent",
|
|
428
|
+
importName: base
|
|
429
|
+
};
|
|
430
|
+
case "sleep":
|
|
431
|
+
if (base !== ctxParamName) return null;
|
|
432
|
+
return {
|
|
433
|
+
key: "sleep",
|
|
434
|
+
callKind: "wait"
|
|
435
|
+
};
|
|
436
|
+
case "hook":
|
|
437
|
+
if (base !== ctxParamName) return null;
|
|
438
|
+
return {
|
|
439
|
+
key: "hook",
|
|
440
|
+
callKind: "hook"
|
|
441
|
+
};
|
|
442
|
+
case "scope": return classifyCall(callee.expression, ctxParamName);
|
|
443
|
+
case "__site": return classifyCall(callee.expression, ctxParamName);
|
|
444
|
+
default: return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** Detect `Promise.all([...])` and return its element expressions. */
|
|
448
|
+
function asPromiseAll(expression) {
|
|
449
|
+
const expr = unwrap(expression);
|
|
450
|
+
if (!ts.isCallExpression(expr) || !ts.isPropertyAccessExpression(expr.expression)) return null;
|
|
451
|
+
const callee = expr.expression;
|
|
452
|
+
if (!ts.isIdentifier(callee.expression) || callee.expression.text !== "Promise" || callee.name.text !== "all") return null;
|
|
453
|
+
const [arg] = expr.arguments;
|
|
454
|
+
if (!arg || !ts.isArrayLiteralExpression(arg)) return null;
|
|
455
|
+
return [...arg.elements];
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* `const approval = ctx.hook(...)` / `ctx.sleep(...)` without an immediate `await` on
|
|
459
|
+
* the initializer — the durable step should appear at the later `await approval` site.
|
|
460
|
+
*/
|
|
461
|
+
function deferredHookSleepCall(initializer, ctxParamName) {
|
|
462
|
+
if (ts.isAwaitExpression(initializer)) return null;
|
|
463
|
+
const inner = unwrap(initializer);
|
|
464
|
+
if (!ts.isCallExpression(inner)) return null;
|
|
465
|
+
const step = classifyCall(initializer, ctxParamName);
|
|
466
|
+
if (!step || step.callKind !== "hook" && step.callKind !== "wait") return null;
|
|
467
|
+
return inner;
|
|
468
|
+
}
|
|
469
|
+
function isAwaitIdentifier(expression, varName) {
|
|
470
|
+
const inner = unwrap(expression);
|
|
471
|
+
return ts.isIdentifier(inner) && inner.text === varName;
|
|
472
|
+
}
|
|
473
|
+
/** Whether a statement is `await <varName>` (expr stmt or var decl initializer). */
|
|
474
|
+
function statementAwaitingIdentifier(statement, varName) {
|
|
475
|
+
if (ts.isExpressionStatement(statement)) return isAwaitIdentifier(statement.expression, varName);
|
|
476
|
+
if (ts.isVariableStatement(statement)) {
|
|
477
|
+
for (const decl of statement.declarationList.declarations) if (decl.initializer && isAwaitIdentifier(decl.initializer, varName)) return true;
|
|
478
|
+
}
|
|
479
|
+
if (ts.isIfStatement(statement)) return statementAwaitingIdentifier(statement.thenStatement, varName) || (statement.elseStatement ? statementAwaitingIdentifier(statement.elseStatement, varName) : false);
|
|
480
|
+
if (ts.isBlock(statement)) return statement.statements.some((inner) => statementAwaitingIdentifier(inner, varName));
|
|
481
|
+
if (ts.isForStatement(statement) || ts.isForOfStatement(statement) || ts.isForInStatement(statement) || ts.isWhileStatement(statement) || ts.isDoStatement(statement)) return statementAwaitingIdentifier(statement.statement, varName);
|
|
482
|
+
if (ts.isTryStatement(statement)) return statementAwaitingIdentifier(statement.tryBlock, varName) || (statement.catchClause ? statementAwaitingIdentifier(statement.catchClause.block, varName) : false) || (statement.finallyBlock ? statementAwaitingIdentifier(statement.finallyBlock, varName) : false);
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Path of the first later statement in the same block that `await`s `varName`, or null
|
|
487
|
+
* when the handle is never awaited (falls back to inline placement at the decl site).
|
|
488
|
+
*/
|
|
489
|
+
function findDeferredAwaitPath(statements, fromIndex, blockPath, varName) {
|
|
490
|
+
for (let index = fromIndex + 1; index < statements.length; index++) {
|
|
491
|
+
const statement = statements[index];
|
|
492
|
+
if (statementAwaitingIdentifier(statement, varName)) return [...blockPath, `stmt:${index}`];
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* The single structural traversal shared by every consumer of {@link callSiteId}:
|
|
498
|
+
* the graph producer's golden map ({@link computeCallSiteIds}), the contained-id
|
|
499
|
+
* collector ({@link collectContainedCallSiteIds}), and — mirrored, not shared —
|
|
500
|
+
* the graph builder itself. Assigning call paths in one place keeps the
|
|
501
|
+
* cross-package `callSiteId` invariant from drifting: any construct handled here
|
|
502
|
+
* (ternary prongs, deferred hook/sleep, loops, switch, try, `Promise.all`,
|
|
503
|
+
* same-file helper expansion) is handled identically for the map and the set.
|
|
504
|
+
*/
|
|
505
|
+
function walkCallSites(root, basePath, options, visit) {
|
|
506
|
+
const state = {
|
|
507
|
+
ctxParamName: options.ctxParamName,
|
|
508
|
+
localFunctions: options.localFunctions,
|
|
509
|
+
callCounters: /* @__PURE__ */ new Map(),
|
|
510
|
+
walked: /* @__PURE__ */ new Set(),
|
|
511
|
+
visit
|
|
512
|
+
};
|
|
513
|
+
if (ts.isBlock(root)) walkStatements([...root.statements], basePath, state);
|
|
514
|
+
else if (ts.isStatement(root)) walkStatement(root, basePath, state);
|
|
515
|
+
else if (ts.isExpression(root)) walkExpression(root, basePath, state);
|
|
516
|
+
else ts.forEachChild(root, (child) => {
|
|
517
|
+
if (ts.isStatement(child)) walkStatement(child, basePath, state);
|
|
518
|
+
else if (ts.isExpression(child)) walkExpression(child, basePath, state);
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
function nextCallPath(state, basePath) {
|
|
522
|
+
const key = basePath.join(">");
|
|
523
|
+
const index = state.callCounters.get(key) ?? 0;
|
|
524
|
+
state.callCounters.set(key, index + 1);
|
|
525
|
+
return [...basePath, `call:${index}`];
|
|
526
|
+
}
|
|
527
|
+
function peekCallPath(state, basePath) {
|
|
528
|
+
const key = basePath.join(">");
|
|
529
|
+
const index = state.callCounters.get(key) ?? 0;
|
|
530
|
+
return [...basePath, `call:${index}`];
|
|
531
|
+
}
|
|
532
|
+
function reserveCallPath(state, callPath) {
|
|
533
|
+
const callSegment = callPath.at(-1);
|
|
534
|
+
if (!callSegment?.startsWith("call:")) return;
|
|
535
|
+
const index = Number.parseInt(callSegment.slice(5), 10);
|
|
536
|
+
if (Number.isNaN(index)) return;
|
|
537
|
+
const key = callPath.slice(0, -1).join(">");
|
|
538
|
+
const current = state.callCounters.get(key) ?? 0;
|
|
539
|
+
state.callCounters.set(key, Math.max(current, index + 1));
|
|
540
|
+
}
|
|
541
|
+
function walkStatements(statements, path, state) {
|
|
542
|
+
statements.forEach((statement, index) => {
|
|
543
|
+
walkStatement(statement, [...path, `stmt:${index}`], state, statements, index);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
function walkStatement(statement, path, state, statements = [], statementIndex = 0) {
|
|
547
|
+
if (ts.isReturnStatement(statement)) {
|
|
548
|
+
if (statement.expression) walkExpression(statement.expression, path, state);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (ts.isExpressionStatement(statement)) {
|
|
552
|
+
walkExpression(statement.expression, path, state);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (ts.isVariableStatement(statement)) {
|
|
556
|
+
for (const decl of statement.declarationList.declarations) {
|
|
557
|
+
if (!decl.initializer) continue;
|
|
558
|
+
const deferredCall = deferredHookSleepCall(decl.initializer, state.ctxParamName);
|
|
559
|
+
if (deferredCall && ts.isIdentifier(decl.name) && statements.length > 0) {
|
|
560
|
+
const awaitPath = findDeferredAwaitPath(statements, statementIndex, path.slice(0, -1), decl.name.text);
|
|
561
|
+
if (awaitPath) {
|
|
562
|
+
const callPath = peekCallPath(state, awaitPath);
|
|
563
|
+
state.visit(deferredCall, callPath);
|
|
564
|
+
reserveCallPath(state, callPath);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
walkExpression(decl.initializer, path, state);
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (ts.isIfStatement(statement)) {
|
|
573
|
+
const conditions = [];
|
|
574
|
+
let elseBranch;
|
|
575
|
+
let current = statement;
|
|
576
|
+
while (current) {
|
|
577
|
+
conditions.push({
|
|
578
|
+
handle: `cond-${conditions.length}`,
|
|
579
|
+
branch: current.thenStatement
|
|
580
|
+
});
|
|
581
|
+
const next = current.elseStatement;
|
|
582
|
+
if (next && ts.isIfStatement(next)) {
|
|
583
|
+
current = next;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
elseBranch = next;
|
|
587
|
+
current = void 0;
|
|
588
|
+
}
|
|
589
|
+
for (const { handle, branch } of conditions) walkStatement(branch, [...path, `if:${handle}`], state);
|
|
590
|
+
if (elseBranch) walkStatement(elseBranch, [...path, "if:else"], state);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (ts.isForStatement(statement) || ts.isForOfStatement(statement) || ts.isForInStatement(statement) || ts.isWhileStatement(statement) || ts.isDoStatement(statement)) {
|
|
594
|
+
walkStatement(statement.statement, [...path, "loop"], state);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (ts.isSwitchStatement(statement)) {
|
|
598
|
+
for (const [index, clause] of statement.caseBlock.clauses.entries()) {
|
|
599
|
+
const handle = ts.isCaseClause(clause) ? `case-${index}` : "default";
|
|
600
|
+
walkStatements([...clause.statements], [...path, `switch:${handle}`], state);
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (ts.isBlock(statement)) {
|
|
605
|
+
walkStatements([...statement.statements], path, state);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (ts.isTryStatement(statement)) {
|
|
609
|
+
walkStatements([...statement.tryBlock.statements], [...path, "try"], state);
|
|
610
|
+
if (statement.catchClause?.block) walkStatements([...statement.catchClause.block.statements], [...path, "catch"], state);
|
|
611
|
+
if (statement.finallyBlock) walkStatements([...statement.finallyBlock.statements], [...path, "finally"], state);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function walkExpression(expression, path, state) {
|
|
615
|
+
const parallel = asPromiseAll(expression);
|
|
616
|
+
if (parallel) {
|
|
617
|
+
parallel.forEach((element, index) => {
|
|
618
|
+
walkExpression(element, [...path, `parallel:${index}`], state);
|
|
619
|
+
});
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (classifyCall(expression, state.ctxParamName)) {
|
|
623
|
+
const node = unwrap(expression);
|
|
624
|
+
if (ts.isCallExpression(node)) state.visit(node, nextCallPath(state, path));
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (ts.isConditionalExpression(expression)) {
|
|
628
|
+
walkExpression(expression.whenTrue, [...path, "cond:then"], state);
|
|
629
|
+
walkExpression(expression.whenFalse, [...path, "cond:else"], state);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
633
|
+
const helperName = expression.expression.text;
|
|
634
|
+
const helperBody = state.localFunctions.get(helperName);
|
|
635
|
+
if (helperBody) {
|
|
636
|
+
if (state.walked.has(helperName)) return;
|
|
637
|
+
state.walked.add(helperName);
|
|
638
|
+
const helperPath = [`helper:${helperName}`];
|
|
639
|
+
if (ts.isBlock(helperBody)) walkStatements([...helperBody.statements], helperPath, state);
|
|
640
|
+
else if (ts.isExpression(helperBody)) walkExpression(helperBody, helperPath, state);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
ts.forEachChild(expression, (child) => {
|
|
645
|
+
if (ts.isExpression(child)) walkExpression(child, path, state);
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
/** Convenience: collect all callSiteIds reachable from `root`. */
|
|
649
|
+
function collectCallSiteIds(root, basePath, options) {
|
|
650
|
+
const out = /* @__PURE__ */ new Set();
|
|
651
|
+
walkCallSites(root, basePath, options, (_call, callPath) => {
|
|
652
|
+
out.add(callSiteId(callPath));
|
|
653
|
+
});
|
|
654
|
+
return [...out];
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Collect durable call-site ids hidden inside an off-grammar subtree (a
|
|
658
|
+
* `code-block` node's `containedCallSiteIds`). Uses the same {@link walkCallSites}
|
|
659
|
+
* traversal as {@link computeCallSiteIds}, so the ids attributed to a code-block
|
|
660
|
+
* always match the ids the build transform injects at those call sites.
|
|
661
|
+
*/
|
|
662
|
+
function collectContainedCallSiteIds(node, ctxParamName, localFunctions, basePath = [RUN_BODY_SEGMENT]) {
|
|
663
|
+
return collectCallSiteIds(node, basePath, {
|
|
664
|
+
ctxParamName,
|
|
665
|
+
localFunctions
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Index same-file helper functions by name (top-level `function foo()` and
|
|
670
|
+
* `const foo = () => {}` / function-expression consts) so step detection can
|
|
671
|
+
* descend into a local helper a workflow calls. Cross-file helpers are
|
|
672
|
+
* intentionally absent (single-file boundary).
|
|
673
|
+
*/
|
|
674
|
+
function collectLocalFunctions(sourceFile) {
|
|
675
|
+
const byName = /* @__PURE__ */ new Map();
|
|
676
|
+
for (const statement of sourceFile.statements) {
|
|
677
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) {
|
|
678
|
+
byName.set(statement.name.text, statement.body);
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (ts.isVariableStatement(statement)) {
|
|
682
|
+
for (const decl of statement.declarationList.declarations) if (ts.isIdentifier(decl.name) && decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))) byName.set(decl.name.text, decl.initializer.body);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return byName;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Count textual invocations of each same-file helper across the workflow `run`
|
|
689
|
+
* body and every other same-file helper body. A helper invoked from more than
|
|
690
|
+
* one call site cannot be inlined as distinct real step nodes: the build injects
|
|
691
|
+
* one `callSiteId` per physical call site in the helper body, so every invocation
|
|
692
|
+
* shares those ids and folds occurrences at runtime (like a loop). Rendering it as
|
|
693
|
+
* one shared subgraph produces misleading cross-branch / self-loop edges, so a
|
|
694
|
+
* multiply-invoked helper collapses to a per-call-site `code-block` instead.
|
|
695
|
+
*/
|
|
696
|
+
function sharedHelperNames(located, localFunctions) {
|
|
697
|
+
const counts = /* @__PURE__ */ new Map();
|
|
698
|
+
const tally = (root) => {
|
|
699
|
+
const visit = (node) => {
|
|
700
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
701
|
+
const name = node.expression.text;
|
|
702
|
+
if (localFunctions.has(name)) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
703
|
+
}
|
|
704
|
+
ts.forEachChild(node, visit);
|
|
705
|
+
};
|
|
706
|
+
visit(root);
|
|
707
|
+
};
|
|
708
|
+
tally(located.body);
|
|
709
|
+
for (const body of localFunctions.values()) tally(body);
|
|
710
|
+
const shared = /* @__PURE__ */ new Set();
|
|
711
|
+
for (const [name, count] of counts) if (count >= 2) shared.add(name);
|
|
712
|
+
return shared;
|
|
713
|
+
}
|
|
714
|
+
/** Split camelCase identifiers into sentence case (`hasBreaking` → `Has breaking`). */
|
|
715
|
+
function prettifyCamelCaseIdentifier(name) {
|
|
716
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(/\s+/).map((word, index) => {
|
|
717
|
+
const lower = word.toLowerCase();
|
|
718
|
+
return index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower;
|
|
719
|
+
}).join(" ");
|
|
720
|
+
}
|
|
721
|
+
function unwrapParenthesized(expression) {
|
|
722
|
+
let current = expression;
|
|
723
|
+
while (ts.isParenthesizedExpression(current)) current = current.expression;
|
|
724
|
+
return current;
|
|
725
|
+
}
|
|
726
|
+
function identifierNameFromExpression(expression) {
|
|
727
|
+
const unwrapped = unwrapParenthesized(expression);
|
|
728
|
+
if (ts.isPropertyAccessExpression(unwrapped) || ts.isPropertyAccessChain(unwrapped)) return unwrapped.name.text;
|
|
729
|
+
if (ts.isIdentifier(unwrapped)) return unwrapped.text;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Human-readable label for an `if`/`switch` condition. Member access and bare
|
|
733
|
+
* identifiers prettify (`changes.hasBreaking` → `Has breaking`); everything
|
|
734
|
+
* else falls back to normalized source text.
|
|
735
|
+
*/
|
|
736
|
+
function formatConditionLabel(expression, sourceFile) {
|
|
737
|
+
const identifierName = identifierNameFromExpression(expression);
|
|
738
|
+
if (identifierName) return prettifyCamelCaseIdentifier(identifierName);
|
|
739
|
+
return expression.getText(sourceFile).replace(/\s+/g, " ").trim();
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Find the input-object argument of a durable call. Descends through the
|
|
743
|
+
* build-injected `.__site(...)` and user `.scope(...)` wrappers to the
|
|
744
|
+
* `.run({...})` / `.prompt({...})` / `promptLlm({...})` call and returns its
|
|
745
|
+
* first argument (the input object literal), or undefined.
|
|
746
|
+
*/
|
|
747
|
+
function extractStepInputArg(expression) {
|
|
748
|
+
return unwrapToCall(expression)?.arguments[0];
|
|
749
|
+
}
|
|
750
|
+
function combineField(base, path) {
|
|
751
|
+
if (base && path) return `${base}.${path}`;
|
|
752
|
+
return base ?? path;
|
|
753
|
+
}
|
|
754
|
+
/** Leftmost identifier + the dotted property path of a `a.b.c` chain (`{ base: "a", path: "b.c" }`). */
|
|
755
|
+
function propertyAccessParts(expr) {
|
|
756
|
+
const parts = [];
|
|
757
|
+
let current = expr;
|
|
758
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
759
|
+
parts.unshift(current.name.text);
|
|
760
|
+
current = current.expression;
|
|
761
|
+
}
|
|
762
|
+
if (!ts.isIdentifier(current)) return null;
|
|
763
|
+
return {
|
|
764
|
+
base: current.text,
|
|
765
|
+
path: parts.join(".")
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function referenceToken(source) {
|
|
769
|
+
return {
|
|
770
|
+
kind: "reference",
|
|
771
|
+
sourceNodeId: source.nodeId,
|
|
772
|
+
...source.field ? { field: source.field } : {}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function resolveIdentifier(name, scope) {
|
|
776
|
+
if (name === scope.inputParamName) return {
|
|
777
|
+
kind: "reference",
|
|
778
|
+
sourceNodeId: scope.entryNodeId
|
|
779
|
+
};
|
|
780
|
+
const source = scope.bindings.get(name);
|
|
781
|
+
if (source) return referenceToken(source);
|
|
782
|
+
return {
|
|
783
|
+
kind: "variable",
|
|
784
|
+
text: name
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
/** The callee's source name for a call expression (`buildPrompt`, `jobs.map`); never its args. */
|
|
788
|
+
function calleeName(call, sourceFile) {
|
|
789
|
+
const callee = call.expression;
|
|
790
|
+
if (ts.isIdentifier(callee)) return callee.text;
|
|
791
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
792
|
+
const parts = propertyAccessParts(callee);
|
|
793
|
+
if (parts) return `${parts.base}.${parts.path}`;
|
|
794
|
+
}
|
|
795
|
+
return callee.getText(sourceFile);
|
|
796
|
+
}
|
|
797
|
+
function resolvePropertyAccess(expr, scope, sourceFile) {
|
|
798
|
+
const parts = propertyAccessParts(expr);
|
|
799
|
+
if (!parts) return {
|
|
800
|
+
kind: "variable",
|
|
801
|
+
text: expr.getText(sourceFile)
|
|
802
|
+
};
|
|
803
|
+
if (parts.base === scope.inputParamName) return {
|
|
804
|
+
kind: "reference",
|
|
805
|
+
sourceNodeId: scope.entryNodeId,
|
|
806
|
+
field: parts.path
|
|
807
|
+
};
|
|
808
|
+
const source = scope.bindings.get(parts.base);
|
|
809
|
+
if (source) {
|
|
810
|
+
const field = combineField(source.field, parts.path);
|
|
811
|
+
return {
|
|
812
|
+
kind: "reference",
|
|
813
|
+
sourceNodeId: source.nodeId,
|
|
814
|
+
...field ? { field } : {}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
kind: "variable",
|
|
819
|
+
text: expr.getText(sourceFile)
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
/** Tokenize one value expression into a binding (literal text and/or upstream references). */
|
|
823
|
+
function tokensFromExpression(expr, scope, sourceFile) {
|
|
824
|
+
const value = unwrap(expr);
|
|
825
|
+
if (ts.isStringLiteralLike(value)) return { tokens: [{
|
|
826
|
+
kind: "text",
|
|
827
|
+
text: value.text
|
|
828
|
+
}] };
|
|
829
|
+
if (ts.isNumericLiteral(value)) return { tokens: [{
|
|
830
|
+
kind: "text",
|
|
831
|
+
text: value.text
|
|
832
|
+
}] };
|
|
833
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword) return { tokens: [{
|
|
834
|
+
kind: "text",
|
|
835
|
+
text: "true"
|
|
836
|
+
}] };
|
|
837
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword) return { tokens: [{
|
|
838
|
+
kind: "text",
|
|
839
|
+
text: "false"
|
|
840
|
+
}] };
|
|
841
|
+
if (value.kind === ts.SyntaxKind.NullKeyword) return { tokens: [{
|
|
842
|
+
kind: "text",
|
|
843
|
+
text: "null"
|
|
844
|
+
}] };
|
|
845
|
+
if (ts.isIdentifier(value)) return { tokens: [resolveIdentifier(value.text, scope)] };
|
|
846
|
+
if (ts.isPropertyAccessExpression(value)) return { tokens: [resolvePropertyAccess(value, scope, sourceFile)] };
|
|
847
|
+
if (ts.isTemplateExpression(value)) {
|
|
848
|
+
const tokens = [];
|
|
849
|
+
if (value.head.text) tokens.push({
|
|
850
|
+
kind: "text",
|
|
851
|
+
text: value.head.text
|
|
852
|
+
});
|
|
853
|
+
for (const span of value.templateSpans) {
|
|
854
|
+
tokens.push(...tokensFromExpression(span.expression, scope, sourceFile).tokens);
|
|
855
|
+
if (span.literal.text) tokens.push({
|
|
856
|
+
kind: "text",
|
|
857
|
+
text: span.literal.text
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
tokens,
|
|
862
|
+
...value.getText(sourceFile).includes("\n") ? { multiline: true } : {}
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
if (ts.isObjectLiteralExpression(value) || ts.isArrayLiteralExpression(value)) return {
|
|
866
|
+
tokens: [{
|
|
867
|
+
kind: "text",
|
|
868
|
+
text: value.getText(sourceFile)
|
|
869
|
+
}],
|
|
870
|
+
multiline: true
|
|
871
|
+
};
|
|
872
|
+
if (ts.isCallExpression(value)) return { tokens: [{
|
|
873
|
+
kind: "call",
|
|
874
|
+
name: calleeName(value, sourceFile)
|
|
875
|
+
}] };
|
|
876
|
+
return { tokens: [{
|
|
877
|
+
kind: "variable",
|
|
878
|
+
text: value.getText(sourceFile)
|
|
879
|
+
}] };
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Capture per-field call-site bindings from a step's input-object argument. Each
|
|
883
|
+
* property becomes a {@link WorkflowCanvasRawBinding} (literal text and/or
|
|
884
|
+
* references to upstream node outputs). Returns undefined when the argument isn't
|
|
885
|
+
* an object literal (e.g. `a.run(payload)`), or has no bindable properties.
|
|
886
|
+
*/
|
|
887
|
+
function inputBindingsFromCall(expression, scope, sourceFile) {
|
|
888
|
+
const arg = extractStepInputArg(expression);
|
|
889
|
+
if (!arg || !ts.isObjectLiteralExpression(arg)) return;
|
|
890
|
+
const bindings = {};
|
|
891
|
+
for (const prop of arg.properties) {
|
|
892
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
893
|
+
bindings[prop.name.text] = { tokens: [resolveIdentifier(prop.name.text, scope)] };
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
897
|
+
const key = ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
898
|
+
if (!key) continue;
|
|
899
|
+
bindings[key] = tokensFromExpression(prop.initializer, scope, sourceFile);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return Object.keys(bindings).length > 0 ? bindings : void 0;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Capture static `promptLlm(prompt, options)` config from an llm call site. Reads
|
|
906
|
+
* literal `model`/`thinkingLevel` (strings) and `maxTokens`/`temperature` (numbers),
|
|
907
|
+
* plus the PRESENCE of `system`/`outputSchema` (usually expressions, not literals).
|
|
908
|
+
* Returns undefined when no options object literal or no recognizable fields.
|
|
909
|
+
*/
|
|
910
|
+
function llmOptionsFromCall(expression) {
|
|
911
|
+
const optionsArg = unwrapToCall(expression)?.arguments[1];
|
|
912
|
+
if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg)) return;
|
|
913
|
+
const options = {};
|
|
914
|
+
for (const prop of optionsArg.properties) {
|
|
915
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
916
|
+
const key = ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
917
|
+
if (!key) continue;
|
|
918
|
+
const value = unwrap(prop.initializer);
|
|
919
|
+
switch (key) {
|
|
920
|
+
case "model":
|
|
921
|
+
if (ts.isStringLiteralLike(value)) options.model = value.text;
|
|
922
|
+
break;
|
|
923
|
+
case "thinkingLevel":
|
|
924
|
+
if (ts.isStringLiteralLike(value)) options.thinkingLevel = value.text;
|
|
925
|
+
break;
|
|
926
|
+
case "maxTokens":
|
|
927
|
+
if (ts.isNumericLiteral(value)) options.maxTokens = Number(value.text);
|
|
928
|
+
break;
|
|
929
|
+
case "temperature":
|
|
930
|
+
if (ts.isNumericLiteral(value)) options.temperature = Number(value.text);
|
|
931
|
+
break;
|
|
932
|
+
case "system":
|
|
933
|
+
options.hasSystemPrompt = true;
|
|
934
|
+
break;
|
|
935
|
+
case "outputSchema":
|
|
936
|
+
options.hasOutputSchema = true;
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return Object.keys(options).length > 0 ? options : void 0;
|
|
941
|
+
}
|
|
942
|
+
/** Descend wrapper calls (`.scope`/`.__site`) to the underlying durable call. */
|
|
943
|
+
function unwrapToCall(expression) {
|
|
944
|
+
let expr = unwrap(expression);
|
|
945
|
+
while (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) {
|
|
946
|
+
const method = expr.expression.name.text;
|
|
947
|
+
if (method === "scope" || method === "__site") {
|
|
948
|
+
expr = unwrap(expr.expression.expression);
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
return ts.isCallExpression(expr) ? expr : void 0;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Tokenize a `promptLlm(prompt, options)` call's options object into per-field
|
|
957
|
+
* bindings (model/system/outputSchema/maxTokens/…), the same token model used for
|
|
958
|
+
* action inputs — so a `model: CLASSIFY_MODEL` renders as a variable chip and a
|
|
959
|
+
* `system: dailySystem()` as a call chip, instead of being dropped. Returns
|
|
960
|
+
* undefined when there's no options object literal.
|
|
961
|
+
*/
|
|
962
|
+
function llmOptionBindingsFromCall(expression, scope, sourceFile) {
|
|
963
|
+
const optionsArg = unwrapToCall(expression)?.arguments[1];
|
|
964
|
+
if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg)) return;
|
|
965
|
+
const bindings = {};
|
|
966
|
+
for (const prop of optionsArg.properties) {
|
|
967
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
968
|
+
bindings[prop.name.text] = { tokens: [resolveIdentifier(prop.name.text, scope)] };
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
972
|
+
const key = ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
973
|
+
if (!key) continue;
|
|
974
|
+
bindings[key] = tokensFromExpression(prop.initializer, scope, sourceFile);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return Object.keys(bindings).length > 0 ? bindings : void 0;
|
|
978
|
+
}
|
|
979
|
+
/** Statically-evaluate a literal initializer to a scalar, or undefined if not literal. */
|
|
980
|
+
function literalConstantValue(initializer) {
|
|
981
|
+
const value = unwrap(initializer);
|
|
982
|
+
if (ts.isStringLiteralLike(value)) return value.text;
|
|
983
|
+
if (ts.isNumericLiteral(value)) return Number(value.text);
|
|
984
|
+
if (ts.isPrefixUnaryExpression(value) && value.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(value.operand)) return -Number(value.operand.text);
|
|
985
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
986
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Collect top-level `const NAME = <literal scalar>` declarations from the workflow
|
|
990
|
+
* source so the platform can resolve `variable` value tokens (e.g. a model pinned to
|
|
991
|
+
* a named const) to their concrete value. Scalars only — computed or non-literal
|
|
992
|
+
* consts (and anything function-local) are intentionally skipped.
|
|
993
|
+
*/
|
|
994
|
+
function collectTopLevelConstants(sourceFile) {
|
|
995
|
+
const constants = {};
|
|
996
|
+
for (const statement of sourceFile.statements) {
|
|
997
|
+
if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
|
|
998
|
+
for (const decl of statement.declarationList.declarations) {
|
|
999
|
+
if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
1000
|
+
const value = literalConstantValue(decl.initializer);
|
|
1001
|
+
if (value !== void 0) constants[decl.name.text] = value;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return constants;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Record the producer outputs a variable declaration binds, so later call sites
|
|
1008
|
+
* can reference them. Handles `const x = await a.run()` (→ `x` = the node) and
|
|
1009
|
+
* `const { text } = await a.prompt()` (→ `text` = the node's `text` field).
|
|
1010
|
+
*/
|
|
1011
|
+
function recordDeclarationBinding(name, nodeId, scope) {
|
|
1012
|
+
if (ts.isIdentifier(name)) {
|
|
1013
|
+
scope.bindings.set(name.text, { nodeId });
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (ts.isObjectBindingPattern(name)) for (const element of name.elements) {
|
|
1017
|
+
if (!ts.isIdentifier(element.name)) continue;
|
|
1018
|
+
const field = element.propertyName && ts.isIdentifier(element.propertyName) ? element.propertyName.text : element.name.text;
|
|
1019
|
+
scope.bindings.set(element.name.text, {
|
|
1020
|
+
nodeId,
|
|
1021
|
+
field
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const ENTRY_NODE_ID = "entry";
|
|
1026
|
+
const MAX_LABEL_LENGTH = 60;
|
|
1027
|
+
function truncate(text) {
|
|
1028
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
1029
|
+
return collapsed.length > MAX_LABEL_LENGTH ? `${collapsed.slice(0, MAX_LABEL_LENGTH - 1)}…` : collapsed;
|
|
1030
|
+
}
|
|
1031
|
+
/** First-party + integration packages live under this scope; `<app>` is the slug. */
|
|
1032
|
+
const APP_PACKAGE_SCOPE = "@keystrokehq/";
|
|
1033
|
+
function appSlugFromSpecifier(specifier) {
|
|
1034
|
+
if (!specifier.startsWith(APP_PACKAGE_SCOPE)) return;
|
|
1035
|
+
return specifier.slice(13).split("/")[0] || void 0;
|
|
1036
|
+
}
|
|
1037
|
+
function collectAppImportsBySymbol(sourceFile) {
|
|
1038
|
+
const bySymbol = /* @__PURE__ */ new Map();
|
|
1039
|
+
for (const statement of sourceFile.statements) {
|
|
1040
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
1041
|
+
const appSlug = appSlugFromSpecifier(statement.moduleSpecifier.text);
|
|
1042
|
+
const clause = statement.importClause;
|
|
1043
|
+
if (!appSlug || !clause) continue;
|
|
1044
|
+
if (clause.name) bySymbol.set(clause.name.text, appSlug);
|
|
1045
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) for (const element of clause.namedBindings.elements) bySymbol.set(element.name.text, appSlug);
|
|
1046
|
+
}
|
|
1047
|
+
return bySymbol;
|
|
1048
|
+
}
|
|
1049
|
+
function stepLabel(info) {
|
|
1050
|
+
switch (info.callKind) {
|
|
1051
|
+
case "wait": return "Sleep";
|
|
1052
|
+
case "hook": return "Wait for signal";
|
|
1053
|
+
case "llm": return info.key === "promptLlm" ? "LLM prompt" : info.key;
|
|
1054
|
+
case "agent":
|
|
1055
|
+
case "workflow-step": return info.key;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
var GraphBuilder = class {
|
|
1059
|
+
sourceFile;
|
|
1060
|
+
resolveSlug;
|
|
1061
|
+
nodes = [];
|
|
1062
|
+
edges = [];
|
|
1063
|
+
nodeIds = /* @__PURE__ */ new Set();
|
|
1064
|
+
callCounters = /* @__PURE__ */ new Map();
|
|
1065
|
+
structuralCount = 0;
|
|
1066
|
+
edgeCount = 0;
|
|
1067
|
+
exitId = null;
|
|
1068
|
+
/** >0 while walking inside a branch (if/switch/loop/try), so a `return` there
|
|
1069
|
+
* is modeled as its own local "Output" terminal instead of a long edge to the
|
|
1070
|
+
* single shared exit (which reads as crossing the whole graph). */
|
|
1071
|
+
inBranchDepth = 0;
|
|
1072
|
+
/** True while inlining a helper called in VALUE position (`const x = await f()`),
|
|
1073
|
+
* so a `return` inside it is a value handoff to the caller (its expression's
|
|
1074
|
+
* frontier), NOT the workflow's terminal output. False for the run body and for
|
|
1075
|
+
* helpers dispatched in tail position (`return f()`), whose returns ARE outputs. */
|
|
1076
|
+
returnAsValue = false;
|
|
1077
|
+
appImportsBySymbol;
|
|
1078
|
+
localFunctions;
|
|
1079
|
+
/** Same-file helpers invoked from >1 call site — collapsed to a code-block per site. */
|
|
1080
|
+
sharedHelpers = /* @__PURE__ */ new Set();
|
|
1081
|
+
/** First open endpoints per expanded helper (for deduped re-entry). */
|
|
1082
|
+
helperEntryByName = /* @__PURE__ */ new Map();
|
|
1083
|
+
helperExitByName = /* @__PURE__ */ new Map();
|
|
1084
|
+
/** Lexical symbol table for resolving call-site value references to upstream nodes. */
|
|
1085
|
+
bindingScope = {
|
|
1086
|
+
bindings: /* @__PURE__ */ new Map(),
|
|
1087
|
+
entryNodeId: ENTRY_NODE_ID
|
|
1088
|
+
};
|
|
1089
|
+
/** Hook/sleep handles created in a var decl but awaited later in the run body. */
|
|
1090
|
+
pendingDeferredSteps = /* @__PURE__ */ new Map();
|
|
1091
|
+
constructor(sourceFile, resolveSlug) {
|
|
1092
|
+
this.sourceFile = sourceFile;
|
|
1093
|
+
this.resolveSlug = resolveSlug;
|
|
1094
|
+
this.appImportsBySymbol = collectAppImportsBySymbol(sourceFile);
|
|
1095
|
+
this.localFunctions = collectLocalFunctions(sourceFile);
|
|
1096
|
+
}
|
|
1097
|
+
subtreeHasStep(node, ctxParamName, visiting = /* @__PURE__ */ new Set()) {
|
|
1098
|
+
let found = false;
|
|
1099
|
+
const visit = (current) => {
|
|
1100
|
+
if (found) return;
|
|
1101
|
+
if ((ts.isCallExpression(current) || ts.isAwaitExpression(current)) && classifyCall(current, ctxParamName)) {
|
|
1102
|
+
found = true;
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (ts.isCallExpression(current) && ts.isIdentifier(current.expression)) {
|
|
1106
|
+
const helperBody = this.localFunctions.get(current.expression.text);
|
|
1107
|
+
if (helperBody && !visiting.has(current.expression.text)) {
|
|
1108
|
+
visiting.add(current.expression.text);
|
|
1109
|
+
if (this.subtreeHasStep(helperBody, ctxParamName, visiting)) {
|
|
1110
|
+
found = true;
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
ts.forEachChild(current, visit);
|
|
1116
|
+
};
|
|
1117
|
+
visit(node);
|
|
1118
|
+
return found;
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Whether a helper called in VALUE position is safe to inline as real step nodes.
|
|
1122
|
+
* Only straight-line helpers qualify — a block of variable/expression statements
|
|
1123
|
+
* with at most a single trailing `return` (no `if`/`switch`/loop/`try`/`throw`),
|
|
1124
|
+
* or a concise non-ternary expression body. Helpers with internal control flow or
|
|
1125
|
+
* multiple returns are genuinely ambiguous as values (which `return` is "the"
|
|
1126
|
+
* result?), so they collapse to a single `code-block` instead (run attribution is
|
|
1127
|
+
* preserved via `containedCallSiteIds`). Helpers dispatched in tail position
|
|
1128
|
+
* (`return f()`) are inlined regardless — there their returns ARE the output.
|
|
1129
|
+
*/
|
|
1130
|
+
isLinearValueHelper(helperBody) {
|
|
1131
|
+
if (!ts.isBlock(helperBody)) return ts.isExpression(helperBody) && !ts.isConditionalExpression(helperBody);
|
|
1132
|
+
let returnCount = 0;
|
|
1133
|
+
const statements = helperBody.statements;
|
|
1134
|
+
for (const [index, statement] of statements.entries()) {
|
|
1135
|
+
if (ts.isVariableStatement(statement) || ts.isExpressionStatement(statement)) continue;
|
|
1136
|
+
if (ts.isReturnStatement(statement)) {
|
|
1137
|
+
returnCount += 1;
|
|
1138
|
+
if (returnCount > 1 || index !== statements.length - 1) return false;
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
return true;
|
|
1144
|
+
}
|
|
1145
|
+
build(located) {
|
|
1146
|
+
this.sharedHelpers = sharedHelperNames(located, this.localFunctions);
|
|
1147
|
+
this.bindingScope = {
|
|
1148
|
+
bindings: /* @__PURE__ */ new Map(),
|
|
1149
|
+
entryNodeId: ENTRY_NODE_ID,
|
|
1150
|
+
...located.inputParamName ? { inputParamName: located.inputParamName } : {}
|
|
1151
|
+
};
|
|
1152
|
+
const incoming = [{ nodeId: this.addNode("entry", { label: "Start" }, "entry") }];
|
|
1153
|
+
const runPath = [RUN_BODY_SEGMENT];
|
|
1154
|
+
const { body, ctxParamName } = located;
|
|
1155
|
+
const open = ts.isBlock(body) ? this.walkStatements([...body.statements], incoming, ctxParamName, void 0, runPath) : this.walkConcise(body, incoming, ctxParamName, runPath);
|
|
1156
|
+
if (open.length > 0) this.connect(open, this.getExit());
|
|
1157
|
+
const constants = collectTopLevelConstants(this.sourceFile);
|
|
1158
|
+
return {
|
|
1159
|
+
nodes: this.nodes,
|
|
1160
|
+
edges: this.edges,
|
|
1161
|
+
...Object.keys(constants).length > 0 ? { constants } : {}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
push(node) {
|
|
1165
|
+
this.nodes.push(node);
|
|
1166
|
+
this.nodeIds.add(node.id);
|
|
1167
|
+
return node.id;
|
|
1168
|
+
}
|
|
1169
|
+
addNode(nodeType, data, id, parentId) {
|
|
1170
|
+
const node = {
|
|
1171
|
+
id,
|
|
1172
|
+
nodeType,
|
|
1173
|
+
data
|
|
1174
|
+
};
|
|
1175
|
+
if (parentId) node.parentId = parentId;
|
|
1176
|
+
return this.push(node);
|
|
1177
|
+
}
|
|
1178
|
+
nextCallPath(basePath) {
|
|
1179
|
+
const key = formatCallSitePath(basePath);
|
|
1180
|
+
const index = this.callCounters.get(key) ?? 0;
|
|
1181
|
+
this.callCounters.set(key, index + 1);
|
|
1182
|
+
return [...basePath, `call:${index}`];
|
|
1183
|
+
}
|
|
1184
|
+
peekCallPath(basePath) {
|
|
1185
|
+
const key = formatCallSitePath(basePath);
|
|
1186
|
+
const index = this.callCounters.get(key) ?? 0;
|
|
1187
|
+
return [...basePath, `call:${index}`];
|
|
1188
|
+
}
|
|
1189
|
+
/** Reserve a precomputed call index so a later materialization does not shift ids. */
|
|
1190
|
+
reserveCallPath(callPath) {
|
|
1191
|
+
const callSegment = callPath.at(-1);
|
|
1192
|
+
if (!callSegment?.startsWith("call:")) return;
|
|
1193
|
+
const index = Number.parseInt(callSegment.slice(5), 10);
|
|
1194
|
+
if (Number.isNaN(index)) return;
|
|
1195
|
+
const key = formatCallSitePath(callPath.slice(0, -1));
|
|
1196
|
+
const current = this.callCounters.get(key) ?? 0;
|
|
1197
|
+
this.callCounters.set(key, Math.max(current, index + 1));
|
|
1198
|
+
}
|
|
1199
|
+
addStructural(label, parentId) {
|
|
1200
|
+
const id = `merge-${this.structuralCount++}`;
|
|
1201
|
+
return this.addNode("merge", { label }, id, parentId);
|
|
1202
|
+
}
|
|
1203
|
+
addErrorNode(label, parentId) {
|
|
1204
|
+
const id = `error-${this.structuralCount++}`;
|
|
1205
|
+
return this.addNode("error", { label }, id, parentId);
|
|
1206
|
+
}
|
|
1207
|
+
/** A local terminal output node for an early `return` inside a branch. */
|
|
1208
|
+
addOutputNode(parentId) {
|
|
1209
|
+
const id = `output-${this.structuralCount++}`;
|
|
1210
|
+
return this.addNode("exit", { label: "Output" }, id, parentId);
|
|
1211
|
+
}
|
|
1212
|
+
addDecision(label, outputs, parentId) {
|
|
1213
|
+
const id = `decision-${this.structuralCount++}`;
|
|
1214
|
+
return this.addNode("decision", {
|
|
1215
|
+
label,
|
|
1216
|
+
outputs
|
|
1217
|
+
}, id, parentId);
|
|
1218
|
+
}
|
|
1219
|
+
addCodeBlock(label, path, parentId, containedCallSiteIds) {
|
|
1220
|
+
const id = codeBlockSiteId(path);
|
|
1221
|
+
if (this.nodeIds.has(id)) return id;
|
|
1222
|
+
const data = {
|
|
1223
|
+
label,
|
|
1224
|
+
...containedCallSiteIds?.length ? { containedCallSiteIds } : {}
|
|
1225
|
+
};
|
|
1226
|
+
return this.addNode("code-block", data, id, parentId);
|
|
1227
|
+
}
|
|
1228
|
+
addStep(info, expression, callPath, parentId) {
|
|
1229
|
+
const id = callSiteId(callPath);
|
|
1230
|
+
if (this.nodeIds.has(id)) return id;
|
|
1231
|
+
const data = {
|
|
1232
|
+
label: stepLabel(info),
|
|
1233
|
+
callKind: info.callKind
|
|
1234
|
+
};
|
|
1235
|
+
const appSlug = info.importName ? this.appImportsBySymbol.get(info.importName) : void 0;
|
|
1236
|
+
if (appSlug) data.appSlug = appSlug;
|
|
1237
|
+
if (info.importName && this.resolveSlug) {
|
|
1238
|
+
const resolved = this.resolveSlug(info.importName, this.sourceFile);
|
|
1239
|
+
if (resolved) {
|
|
1240
|
+
data.slug = resolved.slug;
|
|
1241
|
+
if (resolved.description) data.description = resolved.description;
|
|
1242
|
+
if (resolved.name) data.label = resolved.name;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (info.callKind === "llm") {
|
|
1246
|
+
const llmOptions = llmOptionsFromCall(expression);
|
|
1247
|
+
if (llmOptions) data.llmOptions = llmOptions;
|
|
1248
|
+
const optionBindings = llmOptionBindingsFromCall(expression, this.bindingScope, this.sourceFile);
|
|
1249
|
+
if (optionBindings) data.inputBindings = optionBindings;
|
|
1250
|
+
} else if (info.callKind !== "wait" && info.callKind !== "hook") {
|
|
1251
|
+
const inputBindings = inputBindingsFromCall(expression, this.bindingScope, this.sourceFile);
|
|
1252
|
+
if (inputBindings) data.inputBindings = inputBindings;
|
|
1253
|
+
}
|
|
1254
|
+
return this.addNode("step", data, id, parentId);
|
|
1255
|
+
}
|
|
1256
|
+
addGroup(groupKind, label, parentId) {
|
|
1257
|
+
const nodeType = groupKind === "loop" ? "loop" : "parallel";
|
|
1258
|
+
const groupId = `${nodeType}-${this.structuralCount++}`;
|
|
1259
|
+
const groupNode = {
|
|
1260
|
+
id: groupId,
|
|
1261
|
+
nodeType,
|
|
1262
|
+
data: {
|
|
1263
|
+
label,
|
|
1264
|
+
groupKind
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
if (parentId) groupNode.parentId = parentId;
|
|
1268
|
+
this.push(groupNode);
|
|
1269
|
+
const groupStartId = `${groupId}-start`;
|
|
1270
|
+
this.push({
|
|
1271
|
+
id: groupStartId,
|
|
1272
|
+
nodeType: "loop-start",
|
|
1273
|
+
data: { label: "Start" },
|
|
1274
|
+
parentId: groupId
|
|
1275
|
+
});
|
|
1276
|
+
return {
|
|
1277
|
+
groupId,
|
|
1278
|
+
groupStartId
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
getExit() {
|
|
1282
|
+
if (this.exitId === null) this.exitId = this.addNode("exit", { label: "Output" }, "exit");
|
|
1283
|
+
return this.exitId;
|
|
1284
|
+
}
|
|
1285
|
+
addEdge(source, target, endpoint) {
|
|
1286
|
+
const edge = {
|
|
1287
|
+
id: `edge-${this.edgeCount++}`,
|
|
1288
|
+
source,
|
|
1289
|
+
target
|
|
1290
|
+
};
|
|
1291
|
+
if (endpoint?.label) edge.label = endpoint.label;
|
|
1292
|
+
if (endpoint?.sourceHandle) edge.sourceHandle = endpoint.sourceHandle;
|
|
1293
|
+
if (endpoint?.branchKind) edge.data = { branchKind: endpoint.branchKind };
|
|
1294
|
+
this.edges.push(edge);
|
|
1295
|
+
}
|
|
1296
|
+
connect(incoming, target) {
|
|
1297
|
+
for (const endpoint of incoming) this.addEdge(endpoint.nodeId, target, endpoint);
|
|
1298
|
+
}
|
|
1299
|
+
walkHelperCall(helperName, helperBody, incoming, ctxParamName, parentId, visiting, tailPosition) {
|
|
1300
|
+
const cachedExit = this.helperExitByName.get(helperName);
|
|
1301
|
+
const cachedEntry = this.helperEntryByName.get(helperName);
|
|
1302
|
+
if (cachedEntry && cachedExit) {
|
|
1303
|
+
this.connect(incoming, cachedEntry);
|
|
1304
|
+
return cachedExit.map((nodeId) => ({ nodeId }));
|
|
1305
|
+
}
|
|
1306
|
+
if (visiting.has(helperName)) return incoming;
|
|
1307
|
+
visiting.add(helperName);
|
|
1308
|
+
const helperPath = [`helper:${helperName}`];
|
|
1309
|
+
const nodesBefore = this.nodes.length;
|
|
1310
|
+
const previousReturnAsValue = this.returnAsValue;
|
|
1311
|
+
this.returnAsValue = !tailPosition;
|
|
1312
|
+
const result = ts.isBlock(helperBody) ? this.walkStatements([...helperBody.statements], incoming, ctxParamName, parentId, helperPath, visiting) : this.walkValue(helperBody, incoming, ctxParamName, parentId, helperPath, visiting);
|
|
1313
|
+
this.returnAsValue = previousReturnAsValue;
|
|
1314
|
+
const firstNewStep = this.nodes.slice(nodesBefore).find((n) => n.nodeType === "step");
|
|
1315
|
+
if (firstNewStep) {
|
|
1316
|
+
this.helperEntryByName.set(helperName, firstNewStep.id);
|
|
1317
|
+
this.helperExitByName.set(helperName, result.map((endpoint) => endpoint.nodeId));
|
|
1318
|
+
}
|
|
1319
|
+
visiting.delete(helperName);
|
|
1320
|
+
return result;
|
|
1321
|
+
}
|
|
1322
|
+
tryMaterializeDeferredAwait(expression, incoming, parentId) {
|
|
1323
|
+
let current = expression;
|
|
1324
|
+
while (ts.isAwaitExpression(current) || ts.isParenthesizedExpression(current)) current = current.expression;
|
|
1325
|
+
if (!ts.isIdentifier(current)) return null;
|
|
1326
|
+
const pending = this.pendingDeferredSteps.get(current.text);
|
|
1327
|
+
if (!pending) return null;
|
|
1328
|
+
this.pendingDeferredSteps.delete(current.text);
|
|
1329
|
+
const id = this.addStep(pending.step, pending.expression, pending.callPath, parentId);
|
|
1330
|
+
this.reserveCallPath(pending.callPath);
|
|
1331
|
+
this.connect(incoming, id);
|
|
1332
|
+
return [{ nodeId: id }];
|
|
1333
|
+
}
|
|
1334
|
+
tryWalkLocalHelper(expression, incoming, ctxParamName, parentId, path, visiting, tailPosition) {
|
|
1335
|
+
let current = expression;
|
|
1336
|
+
while (ts.isAwaitExpression(current) || ts.isParenthesizedExpression(current)) current = current.expression;
|
|
1337
|
+
if (!ts.isCallExpression(current) || !ts.isIdentifier(current.expression)) return null;
|
|
1338
|
+
const helperName = current.expression.text;
|
|
1339
|
+
const helperBody = this.localFunctions.get(helperName);
|
|
1340
|
+
if (!helperBody || !this.subtreeHasStep(helperBody, ctxParamName)) return null;
|
|
1341
|
+
if (this.sharedHelpers.has(helperName)) return null;
|
|
1342
|
+
if (!tailPosition && !this.isLinearValueHelper(helperBody)) return null;
|
|
1343
|
+
return this.walkHelperCall(helperName, helperBody, incoming, ctxParamName, parentId, visiting, tailPosition);
|
|
1344
|
+
}
|
|
1345
|
+
walkStatements(statements, incoming, ctxParamName, parentId, path, visiting = /* @__PURE__ */ new Set()) {
|
|
1346
|
+
let frontier = incoming;
|
|
1347
|
+
for (const [index, statement] of statements.entries()) frontier = this.walkStatement(statement, frontier, ctxParamName, parentId, [...path, `stmt:${index}`], visiting, statements, index);
|
|
1348
|
+
return frontier;
|
|
1349
|
+
}
|
|
1350
|
+
walkStatement(statement, incoming, ctxParamName, parentId, path, visiting = /* @__PURE__ */ new Set(), statements = [], statementIndex = 0) {
|
|
1351
|
+
if (ts.isReturnStatement(statement)) return this.walkReturn(statement, incoming, ctxParamName, parentId, path, visiting);
|
|
1352
|
+
if (ts.isThrowStatement(statement)) {
|
|
1353
|
+
const id = this.addErrorNode("Error", parentId);
|
|
1354
|
+
this.connect(incoming, id);
|
|
1355
|
+
return [];
|
|
1356
|
+
}
|
|
1357
|
+
if (ts.isExpressionStatement(statement)) return this.walkValue(statement.expression, incoming, ctxParamName, parentId, path, visiting);
|
|
1358
|
+
if (ts.isVariableStatement(statement)) {
|
|
1359
|
+
let frontier = incoming;
|
|
1360
|
+
for (const decl of statement.declarationList.declarations) {
|
|
1361
|
+
if (!decl.initializer) continue;
|
|
1362
|
+
const deferredCall = deferredHookSleepCall(decl.initializer, ctxParamName);
|
|
1363
|
+
if (deferredCall && ts.isIdentifier(decl.name) && statements.length > 0) {
|
|
1364
|
+
const awaitPath = findDeferredAwaitPath(statements, statementIndex, path.slice(0, -1), decl.name.text);
|
|
1365
|
+
if (awaitPath) {
|
|
1366
|
+
const step = classifyCall(decl.initializer, ctxParamName);
|
|
1367
|
+
if (step) {
|
|
1368
|
+
const callPath = this.peekCallPath(awaitPath);
|
|
1369
|
+
this.pendingDeferredSteps.set(decl.name.text, {
|
|
1370
|
+
step,
|
|
1371
|
+
expression: deferredCall,
|
|
1372
|
+
callPath
|
|
1373
|
+
});
|
|
1374
|
+
recordDeclarationBinding(decl.name, callSiteId(callPath), this.bindingScope);
|
|
1375
|
+
}
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
const isStep = classifyCall(decl.initializer, ctxParamName) !== null;
|
|
1380
|
+
frontier = this.walkValue(decl.initializer, frontier, ctxParamName, parentId, path, visiting);
|
|
1381
|
+
if (isStep && frontier.length === 1) recordDeclarationBinding(decl.name, frontier[0].nodeId, this.bindingScope);
|
|
1382
|
+
}
|
|
1383
|
+
return frontier;
|
|
1384
|
+
}
|
|
1385
|
+
if (ts.isIfStatement(statement)) return this.walkIf(statement, incoming, ctxParamName, parentId, path, visiting);
|
|
1386
|
+
if (ts.isForStatement(statement) || ts.isForOfStatement(statement) || ts.isForInStatement(statement) || ts.isWhileStatement(statement) || ts.isDoStatement(statement)) return this.walkLoop(statement, incoming, ctxParamName, parentId, path, visiting);
|
|
1387
|
+
if (ts.isSwitchStatement(statement)) return this.walkSwitch(statement, incoming, ctxParamName, parentId, path, visiting);
|
|
1388
|
+
if (ts.isBlock(statement)) return this.walkStatements([...statement.statements], incoming, ctxParamName, parentId, path, visiting);
|
|
1389
|
+
if (ts.isTryStatement(statement)) return this.walkTry(statement, incoming, ctxParamName, parentId, path, visiting);
|
|
1390
|
+
if (this.subtreeHasStep(statement, ctxParamName)) {
|
|
1391
|
+
const contained = collectContainedCallSiteIds(statement, ctxParamName, this.localFunctions, path);
|
|
1392
|
+
const id = this.addCodeBlock(truncate(statement.getText(this.sourceFile)), path, parentId, contained);
|
|
1393
|
+
this.connect(incoming, id);
|
|
1394
|
+
return [{ nodeId: id }];
|
|
1395
|
+
}
|
|
1396
|
+
return incoming;
|
|
1397
|
+
}
|
|
1398
|
+
walkValue(expression, incoming, ctxParamName, parentId, path, visiting = /* @__PURE__ */ new Set(), tailPosition = false) {
|
|
1399
|
+
const helperWalk = this.tryWalkLocalHelper(expression, incoming, ctxParamName, parentId, path, visiting, tailPosition);
|
|
1400
|
+
if (helperWalk) return helperWalk;
|
|
1401
|
+
if (ts.isConditionalExpression(expression) && this.subtreeHasStep(expression, ctxParamName)) return this.walkConditional(expression, incoming, ctxParamName, parentId, path, visiting, tailPosition);
|
|
1402
|
+
const parallel = asPromiseAll(expression);
|
|
1403
|
+
if (parallel) return this.walkParallel(parallel, incoming, ctxParamName, parentId, path, visiting);
|
|
1404
|
+
const deferredAwait = this.tryMaterializeDeferredAwait(expression, incoming, parentId);
|
|
1405
|
+
if (deferredAwait) return deferredAwait;
|
|
1406
|
+
const step = classifyCall(expression, ctxParamName);
|
|
1407
|
+
if (step) {
|
|
1408
|
+
const id = this.addStep(step, expression, this.nextCallPath(path), parentId);
|
|
1409
|
+
this.connect(incoming, id);
|
|
1410
|
+
return [{ nodeId: id }];
|
|
1411
|
+
}
|
|
1412
|
+
if (this.subtreeHasStep(expression, ctxParamName)) {
|
|
1413
|
+
const contained = collectContainedCallSiteIds(expression, ctxParamName, this.localFunctions, path);
|
|
1414
|
+
const id = this.addCodeBlock(truncate(expression.getText(this.sourceFile)), path, parentId, contained);
|
|
1415
|
+
this.connect(incoming, id);
|
|
1416
|
+
return [{ nodeId: id }];
|
|
1417
|
+
}
|
|
1418
|
+
return incoming;
|
|
1419
|
+
}
|
|
1420
|
+
walkConcise(expression, incoming, ctxParamName, path) {
|
|
1421
|
+
const frontier = this.walkValue(expression, incoming, ctxParamName, void 0, path, void 0, true);
|
|
1422
|
+
this.connect(frontier, this.getExit());
|
|
1423
|
+
return [];
|
|
1424
|
+
}
|
|
1425
|
+
walkReturn(statement, incoming, ctxParamName, parentId, path, visiting) {
|
|
1426
|
+
if (this.returnAsValue) return statement.expression ? this.walkValue(statement.expression, incoming, ctxParamName, parentId, path, visiting) : incoming;
|
|
1427
|
+
let frontier = incoming;
|
|
1428
|
+
if (statement.expression) frontier = this.walkValue(statement.expression, frontier, ctxParamName, parentId, path, visiting, true);
|
|
1429
|
+
const target = this.inBranchDepth > 0 ? this.addOutputNode(parentId) : this.getExit();
|
|
1430
|
+
this.connect(frontier, target);
|
|
1431
|
+
return [];
|
|
1432
|
+
}
|
|
1433
|
+
walkParallel(elements, incoming, ctxParamName, parentId, path, visiting) {
|
|
1434
|
+
const { groupId, groupStartId } = this.addGroup("parallel", "Parallel", parentId);
|
|
1435
|
+
this.connect(incoming, groupId);
|
|
1436
|
+
for (const [index, element] of elements.entries()) {
|
|
1437
|
+
const branchPath = [...path, `parallel:${index}`];
|
|
1438
|
+
if (!classifyCall(element, ctxParamName) && !this.subtreeHasStep(element, ctxParamName)) continue;
|
|
1439
|
+
this.walkValue(element, [{
|
|
1440
|
+
nodeId: groupStartId,
|
|
1441
|
+
branchKind: "parallel-branch"
|
|
1442
|
+
}], ctxParamName, groupId, branchPath, visiting);
|
|
1443
|
+
}
|
|
1444
|
+
return [{ nodeId: groupId }];
|
|
1445
|
+
}
|
|
1446
|
+
walkIf(statement, incoming, ctxParamName, parentId, path, visiting) {
|
|
1447
|
+
this.inBranchDepth++;
|
|
1448
|
+
const conditions = [];
|
|
1449
|
+
let elseBranch;
|
|
1450
|
+
let current = statement;
|
|
1451
|
+
while (current) {
|
|
1452
|
+
conditions.push({
|
|
1453
|
+
handle: `cond-${conditions.length}`,
|
|
1454
|
+
label: truncate(formatConditionLabel(current.expression, this.sourceFile)),
|
|
1455
|
+
branch: current.thenStatement
|
|
1456
|
+
});
|
|
1457
|
+
const next = current.elseStatement;
|
|
1458
|
+
if (next && ts.isIfStatement(next)) {
|
|
1459
|
+
current = next;
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
elseBranch = next;
|
|
1463
|
+
current = void 0;
|
|
1464
|
+
}
|
|
1465
|
+
const outputs = [...conditions.map(({ handle, label }) => ({
|
|
1466
|
+
id: handle,
|
|
1467
|
+
label
|
|
1468
|
+
})), {
|
|
1469
|
+
id: "else",
|
|
1470
|
+
label: "Else"
|
|
1471
|
+
}];
|
|
1472
|
+
const decisionId = this.addDecision(truncate(formatConditionLabel(statement.expression, this.sourceFile)), outputs, parentId);
|
|
1473
|
+
this.connect(incoming, decisionId);
|
|
1474
|
+
const branchOpens = conditions.map(({ handle, branch }) => this.walkStatement(branch, [{
|
|
1475
|
+
nodeId: decisionId,
|
|
1476
|
+
branchKind: "then",
|
|
1477
|
+
sourceHandle: handle
|
|
1478
|
+
}], ctxParamName, parentId, [...path, `if:${handle}`], visiting));
|
|
1479
|
+
const elseOpen = elseBranch ? this.walkStatement(elseBranch, [{
|
|
1480
|
+
nodeId: decisionId,
|
|
1481
|
+
branchKind: "else",
|
|
1482
|
+
sourceHandle: "else"
|
|
1483
|
+
}], ctxParamName, parentId, [...path, "if:else"], visiting) : [{
|
|
1484
|
+
nodeId: decisionId,
|
|
1485
|
+
branchKind: "else",
|
|
1486
|
+
sourceHandle: "else"
|
|
1487
|
+
}];
|
|
1488
|
+
this.inBranchDepth--;
|
|
1489
|
+
return this.mergeBranches([...branchOpens, elseOpen], parentId);
|
|
1490
|
+
}
|
|
1491
|
+
walkConditional(expression, incoming, ctxParamName, parentId, path, visiting, tailPosition) {
|
|
1492
|
+
const label = truncate(formatConditionLabel(expression.condition, this.sourceFile));
|
|
1493
|
+
const outputs = [{
|
|
1494
|
+
id: "cond-0",
|
|
1495
|
+
label
|
|
1496
|
+
}, {
|
|
1497
|
+
id: "else",
|
|
1498
|
+
label: "Else"
|
|
1499
|
+
}];
|
|
1500
|
+
const decisionId = this.addDecision(label, outputs, parentId);
|
|
1501
|
+
this.connect(incoming, decisionId);
|
|
1502
|
+
const thenOpen = this.walkValue(expression.whenTrue, [{
|
|
1503
|
+
nodeId: decisionId,
|
|
1504
|
+
branchKind: "then",
|
|
1505
|
+
sourceHandle: "cond-0"
|
|
1506
|
+
}], ctxParamName, parentId, [...path, "cond:then"], visiting, tailPosition);
|
|
1507
|
+
const elseOpen = this.walkValue(expression.whenFalse, [{
|
|
1508
|
+
nodeId: decisionId,
|
|
1509
|
+
branchKind: "else",
|
|
1510
|
+
sourceHandle: "else"
|
|
1511
|
+
}], ctxParamName, parentId, [...path, "cond:else"], visiting, tailPosition);
|
|
1512
|
+
return this.mergeBranches([thenOpen, elseOpen], parentId);
|
|
1513
|
+
}
|
|
1514
|
+
walkLoop(statement, incoming, ctxParamName, parentId, path, visiting) {
|
|
1515
|
+
this.inBranchDepth++;
|
|
1516
|
+
const label = ts.isForOfStatement(statement) ? "For each item" : "Loop";
|
|
1517
|
+
const { groupId, groupStartId } = this.addGroup("loop", label, parentId);
|
|
1518
|
+
this.connect(incoming, groupId);
|
|
1519
|
+
this.walkStatement(statement.statement, [{ nodeId: groupStartId }], ctxParamName, groupId, [...path, "loop"], visiting);
|
|
1520
|
+
this.inBranchDepth--;
|
|
1521
|
+
return [{ nodeId: groupId }];
|
|
1522
|
+
}
|
|
1523
|
+
walkSwitch(statement, incoming, ctxParamName, parentId, path, visiting) {
|
|
1524
|
+
this.inBranchDepth++;
|
|
1525
|
+
const clauses = statement.caseBlock.clauses;
|
|
1526
|
+
const outputs = clauses.map((clause, index) => ts.isCaseClause(clause) ? {
|
|
1527
|
+
id: `case-${index}`,
|
|
1528
|
+
label: truncate(clause.expression.getText(this.sourceFile))
|
|
1529
|
+
} : {
|
|
1530
|
+
id: "default",
|
|
1531
|
+
label: "default"
|
|
1532
|
+
});
|
|
1533
|
+
const decisionId = this.addDecision(truncate(formatConditionLabel(statement.expression, this.sourceFile)), outputs, parentId);
|
|
1534
|
+
this.connect(incoming, decisionId);
|
|
1535
|
+
const branches = clauses.map((clause, index) => {
|
|
1536
|
+
const handle = ts.isCaseClause(clause) ? `case-${index}` : "default";
|
|
1537
|
+
return this.walkStatements([...clause.statements], [{
|
|
1538
|
+
nodeId: decisionId,
|
|
1539
|
+
sourceHandle: outputs[index].id,
|
|
1540
|
+
branchKind: ts.isCaseClause(clause) ? "then" : "else"
|
|
1541
|
+
}], ctxParamName, parentId, [...path, `switch:${handle}`], visiting);
|
|
1542
|
+
});
|
|
1543
|
+
this.inBranchDepth--;
|
|
1544
|
+
return this.mergeBranches(branches, parentId);
|
|
1545
|
+
}
|
|
1546
|
+
walkTry(statement, incoming, ctxParamName, parentId, path, visiting) {
|
|
1547
|
+
this.inBranchDepth++;
|
|
1548
|
+
const beforeTry = this.nodes.length;
|
|
1549
|
+
const tryOpen = this.walkStatements([...statement.tryBlock.statements], incoming, ctxParamName, parentId, [...path, "try"], visiting);
|
|
1550
|
+
const protectedEntry = this.nodes[beforeTry];
|
|
1551
|
+
const protectedEntryId = protectedEntry?.id;
|
|
1552
|
+
const protectedEntryIsStep = protectedEntry?.nodeType === "step";
|
|
1553
|
+
let frontier = tryOpen;
|
|
1554
|
+
const catchBlock = statement.catchClause?.block;
|
|
1555
|
+
if (catchBlock && this.subtreeHasStep(catchBlock, ctxParamName)) if (protectedEntryId && protectedEntryIsStep) {
|
|
1556
|
+
const errorOpen = this.walkStatements([...catchBlock.statements], [{
|
|
1557
|
+
nodeId: protectedEntryId,
|
|
1558
|
+
sourceHandle: "error",
|
|
1559
|
+
branchKind: "error",
|
|
1560
|
+
label: "on error"
|
|
1561
|
+
}], ctxParamName, parentId, [...path, "catch"], visiting);
|
|
1562
|
+
frontier = this.mergeBranches([tryOpen, errorOpen], parentId);
|
|
1563
|
+
} else frontier = this.walkStatements([...catchBlock.statements], tryOpen, ctxParamName, parentId, [...path, "catch"], visiting);
|
|
1564
|
+
if (statement.finallyBlock) frontier = this.walkStatements([...statement.finallyBlock.statements], frontier, ctxParamName, parentId, [...path, "finally"], visiting);
|
|
1565
|
+
this.inBranchDepth--;
|
|
1566
|
+
return frontier;
|
|
1567
|
+
}
|
|
1568
|
+
mergeBranches(branches, parentId) {
|
|
1569
|
+
const open = branches.flat();
|
|
1570
|
+
if (open.length === 0) return [];
|
|
1571
|
+
const mergeId = this.addStructural("Merge", parentId);
|
|
1572
|
+
this.connect(open, mergeId);
|
|
1573
|
+
return [{ nodeId: mergeId }];
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
/** Build a workflow-canvas graph (entry → control flow → exit) from a located workflow. */
|
|
1577
|
+
function buildGraph(sourceFile, located, options) {
|
|
1578
|
+
return new GraphBuilder(sourceFile, options?.resolveSlug).build(located);
|
|
1579
|
+
}
|
|
1580
|
+
function isDefineWorkflowCallee(expr) {
|
|
1581
|
+
if (ts.isIdentifier(expr)) return expr.text === "defineWorkflow";
|
|
1582
|
+
if (ts.isPropertyAccessExpression(expr)) return expr.name.text === "defineWorkflow";
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
function readRunParamCtxName(params) {
|
|
1586
|
+
const ctxParam = params[1];
|
|
1587
|
+
if (ctxParam && ts.isIdentifier(ctxParam.name)) return ctxParam.name.text;
|
|
1588
|
+
}
|
|
1589
|
+
function readRunParamInputName(params) {
|
|
1590
|
+
const inputParam = params[0];
|
|
1591
|
+
if (inputParam && ts.isIdentifier(inputParam.name)) return inputParam.name.text;
|
|
1592
|
+
}
|
|
1593
|
+
function readWorkflowObject(obj) {
|
|
1594
|
+
let slug;
|
|
1595
|
+
let body;
|
|
1596
|
+
let ctxParamName;
|
|
1597
|
+
let inputParamName;
|
|
1598
|
+
for (const prop of obj.properties) {
|
|
1599
|
+
const name = prop.name && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) ? prop.name.text : void 0;
|
|
1600
|
+
if (name === "slug" && ts.isPropertyAssignment(prop) && ts.isStringLiteral(prop.initializer)) {
|
|
1601
|
+
slug = prop.initializer.text;
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
if (name !== "run") continue;
|
|
1605
|
+
if (ts.isMethodDeclaration(prop) && prop.body) {
|
|
1606
|
+
body = prop.body;
|
|
1607
|
+
inputParamName = readRunParamInputName(prop.parameters);
|
|
1608
|
+
ctxParamName = readRunParamCtxName(prop.parameters);
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
1612
|
+
const init = prop.initializer;
|
|
1613
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
1614
|
+
body = init.body;
|
|
1615
|
+
inputParamName = readRunParamInputName(init.parameters);
|
|
1616
|
+
ctxParamName = readRunParamCtxName(init.parameters);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (!body) return null;
|
|
1621
|
+
return {
|
|
1622
|
+
slug,
|
|
1623
|
+
body,
|
|
1624
|
+
inputParamName,
|
|
1625
|
+
ctxParamName
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Find the `defineWorkflow({ ... })` call in a source file and extract its `run`
|
|
1630
|
+
* body. Returns null when the file has no recognizable workflow definition.
|
|
1631
|
+
*/
|
|
1632
|
+
function locateWorkflow(sourceFile) {
|
|
1633
|
+
let located = null;
|
|
1634
|
+
const visit = (node) => {
|
|
1635
|
+
if (located) return;
|
|
1636
|
+
if (ts.isCallExpression(node) && isDefineWorkflowCallee(node.expression)) {
|
|
1637
|
+
const [arg] = node.arguments;
|
|
1638
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
1639
|
+
located = readWorkflowObject(arg);
|
|
1640
|
+
if (located) return;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
ts.forEachChild(node, visit);
|
|
1644
|
+
};
|
|
1645
|
+
visit(sourceFile);
|
|
1646
|
+
return located;
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Maps each durable call site in a workflow `run` body to its deterministic
|
|
1650
|
+
* {@link callSiteId} — the SAME id the graph producer mints for the corresponding
|
|
1651
|
+
* step node. The Phase 5 build transform consumes this to inject the id into the
|
|
1652
|
+
* compiled call so the runtime emits `step:<callSiteId>#<occurrence>`, lining run
|
|
1653
|
+
* events and credential consumers up 1:1 with canvas nodes.
|
|
1654
|
+
*
|
|
1655
|
+
* Keyed by the outermost durable {@link ts.CallExpression} (after unwrapping
|
|
1656
|
+
* `await`/parens) — i.e. the node the transform rewrites: `x.run(i)`,
|
|
1657
|
+
* `x.scope(s).run(i)`, `x.run(i).scope(s)`, `promptLlm(i, opts)`,
|
|
1658
|
+
* `ctx.sleep(d)`, `ctx.hook(opts)`.
|
|
1659
|
+
*
|
|
1660
|
+
* Shares the {@link walkCallSites} traversal with the contained-id collector so
|
|
1661
|
+
* the structural `callSiteId` invariant cannot drift between the two.
|
|
1662
|
+
*/
|
|
1663
|
+
function computeCallSiteIds(sourceFile) {
|
|
1664
|
+
const located = locateWorkflow(sourceFile);
|
|
1665
|
+
const map = /* @__PURE__ */ new Map();
|
|
1666
|
+
if (!located) return map;
|
|
1667
|
+
walkCallSites(located.body, [RUN_BODY_SEGMENT], {
|
|
1668
|
+
ctxParamName: located.ctxParamName,
|
|
1669
|
+
localFunctions: collectLocalFunctions(sourceFile)
|
|
1670
|
+
}, (call, callPath) => {
|
|
1671
|
+
map.set(call, callSiteId(callPath));
|
|
1672
|
+
});
|
|
1673
|
+
return map;
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Static reliability checks over a single workflow (or would-be helper) source
|
|
1677
|
+
* file. ERRORS block a deploy (they produce an invisible/misleading canvas):
|
|
1678
|
+
* durable steps outside a workflow file, and steps nested as call arguments.
|
|
1679
|
+
* WARNINGS explain each degraded (opaque code-block) construct so the author can
|
|
1680
|
+
* opt into the supported grammar.
|
|
1681
|
+
*/
|
|
1682
|
+
function diagnoseWorkflowSource(source, fileName) {
|
|
1683
|
+
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
1684
|
+
const located = locateWorkflow(sourceFile);
|
|
1685
|
+
if (!located) return crossFileStepDiagnostics(sourceFile, fileName);
|
|
1686
|
+
const shared = sharedHelperNames(located, collectLocalFunctions(sourceFile));
|
|
1687
|
+
return [
|
|
1688
|
+
...nestedStepArgDiagnostics(sourceFile, fileName, located.ctxParamName),
|
|
1689
|
+
...stepInputSpreadDiagnostics(sourceFile, fileName, located.ctxParamName),
|
|
1690
|
+
...opaqueCodeBlockDiagnostics(sourceFile, located, fileName, shared)
|
|
1691
|
+
];
|
|
1692
|
+
}
|
|
1693
|
+
function lineColumn(sourceFile, node) {
|
|
1694
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
1695
|
+
return {
|
|
1696
|
+
line: line + 1,
|
|
1697
|
+
column: character + 1
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
function snippet(sourceFile, node) {
|
|
1701
|
+
const text = node.getText(sourceFile).replace(/\s+/g, " ").trim();
|
|
1702
|
+
return text.length > 80 ? `${text.slice(0, 79)}…` : text;
|
|
1703
|
+
}
|
|
1704
|
+
/** Imported identifier name → its module specifier (`@keystrokehq/gmail/actions`, `../actions/x`). */
|
|
1705
|
+
function collectImportsBySymbol(sourceFile) {
|
|
1706
|
+
const bySymbol = /* @__PURE__ */ new Map();
|
|
1707
|
+
for (const statement of sourceFile.statements) {
|
|
1708
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
1709
|
+
const specifier = statement.moduleSpecifier.text;
|
|
1710
|
+
const clause = statement.importClause;
|
|
1711
|
+
if (!clause) continue;
|
|
1712
|
+
if (clause.name) bySymbol.set(clause.name.text, specifier);
|
|
1713
|
+
if (clause.namedBindings) {
|
|
1714
|
+
if (ts.isNamedImports(clause.namedBindings)) for (const element of clause.namedBindings.elements) bySymbol.set(element.name.text, specifier);
|
|
1715
|
+
else if (ts.isNamespaceImport(clause.namedBindings)) bySymbol.set(clause.namedBindings.name.text, specifier);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
return bySymbol;
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Durable steps come from either an integration package (`@keystrokehq/<app>/...`)
|
|
1722
|
+
* or the project's own actions/agents (imported by relative path). Third-party
|
|
1723
|
+
* packages (drizzle `db.run()`, etc.) use bare, non-`@keystrokehq` specifiers, so
|
|
1724
|
+
* excluding those avoids false positives while still catching real project steps.
|
|
1725
|
+
*/
|
|
1726
|
+
function isStepBearingSpecifier(specifier) {
|
|
1727
|
+
if (!specifier) return false;
|
|
1728
|
+
return specifier.startsWith("@keystrokehq/") || specifier.startsWith(".");
|
|
1729
|
+
}
|
|
1730
|
+
/** Whether `call` is the object of a `.scope(...)`/`.__site(...)` chain (an inner link). */
|
|
1731
|
+
function isChainedInnerCall(call) {
|
|
1732
|
+
const parent = call.parent;
|
|
1733
|
+
return ts.isPropertyAccessExpression(parent) && (parent.name.text === "scope" || parent.name.text === "__site") && parent.expression === call;
|
|
1734
|
+
}
|
|
1735
|
+
function forEachDurableRoot(sourceFile, ctxParamName, visit) {
|
|
1736
|
+
const walk = (node) => {
|
|
1737
|
+
if (ts.isCallExpression(node) && classifyCall(node, ctxParamName) && !isChainedInnerCall(node)) visit(node);
|
|
1738
|
+
ts.forEachChild(node, walk);
|
|
1739
|
+
};
|
|
1740
|
+
walk(sourceFile);
|
|
1741
|
+
}
|
|
1742
|
+
function isFunctionLike(node) {
|
|
1743
|
+
return ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node);
|
|
1744
|
+
}
|
|
1745
|
+
/** Durable step calls in a file that is not a workflow — invisible + un-injected. */
|
|
1746
|
+
function crossFileStepDiagnostics(sourceFile, fileName) {
|
|
1747
|
+
const importsBySymbol = collectImportsBySymbol(sourceFile);
|
|
1748
|
+
const out = [];
|
|
1749
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1750
|
+
forEachDurableRoot(sourceFile, void 0, (call) => {
|
|
1751
|
+
const info = classifyCall(call, void 0);
|
|
1752
|
+
if (!info) return;
|
|
1753
|
+
if (!(info.callKind === "llm" || (info.callKind === "workflow-step" || info.callKind === "agent") && info.importName !== void 0 && isStepBearingSpecifier(importsBySymbol.get(info.importName)))) return;
|
|
1754
|
+
const { line, column } = lineColumn(sourceFile, call);
|
|
1755
|
+
const dedupe = `${line}:${column}`;
|
|
1756
|
+
if (seen.has(dedupe)) return;
|
|
1757
|
+
seen.add(dedupe);
|
|
1758
|
+
out.push({
|
|
1759
|
+
severity: "error",
|
|
1760
|
+
code: "cross-file-step",
|
|
1761
|
+
fileName,
|
|
1762
|
+
line,
|
|
1763
|
+
column,
|
|
1764
|
+
message: `Durable step call \`${snippet(sourceFile, call)}\` is outside a workflow file. Steps only run and render on the canvas when they live in a \`defineWorkflow\` file (its \`run\` body or a same-file helper). This step is invisible to the canvas and cannot be attributed to runs. Move it into the workflow, or keep this module pure (return data; let the workflow call the step).`
|
|
1765
|
+
});
|
|
1766
|
+
});
|
|
1767
|
+
return out;
|
|
1768
|
+
}
|
|
1769
|
+
/** Durable step calls nested as an argument to another call — the inner step is dropped. */
|
|
1770
|
+
function nestedStepArgDiagnostics(sourceFile, fileName, ctxParamName) {
|
|
1771
|
+
const out = [];
|
|
1772
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1773
|
+
forEachDurableRoot(sourceFile, ctxParamName, (call) => {
|
|
1774
|
+
let node = call;
|
|
1775
|
+
let parent = node.parent;
|
|
1776
|
+
while (parent && !ts.isSourceFile(parent)) {
|
|
1777
|
+
if (isFunctionLike(parent)) return;
|
|
1778
|
+
if (ts.isCallExpression(parent) && parent.arguments.includes(node)) {
|
|
1779
|
+
if (asPromiseAll(parent)) return;
|
|
1780
|
+
const { line, column } = lineColumn(sourceFile, call);
|
|
1781
|
+
const dedupe = `${line}:${column}`;
|
|
1782
|
+
if (!seen.has(dedupe)) {
|
|
1783
|
+
seen.add(dedupe);
|
|
1784
|
+
out.push({
|
|
1785
|
+
severity: "error",
|
|
1786
|
+
code: "nested-step-arg",
|
|
1787
|
+
fileName,
|
|
1788
|
+
line,
|
|
1789
|
+
column,
|
|
1790
|
+
message: `Durable step call \`${snippet(sourceFile, call)}\` is nested as an argument to \`${snippet(sourceFile, parent.expression)}(…)\`. The nested step is dropped from the canvas (and un-attributable in runs). Assign it to a variable first, then pass the variable: \`const r = await step.run(…); other.run({ x: r });\`.`
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
if (ts.isStatement(parent)) return;
|
|
1796
|
+
node = parent;
|
|
1797
|
+
parent = parent.parent;
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
return out;
|
|
1801
|
+
}
|
|
1802
|
+
/** Spread / computed keys in a step input object — silently missing from the inspector. */
|
|
1803
|
+
function stepInputSpreadDiagnostics(sourceFile, fileName, ctxParamName) {
|
|
1804
|
+
const out = [];
|
|
1805
|
+
forEachDurableRoot(sourceFile, ctxParamName, (call) => {
|
|
1806
|
+
const arg = extractStepInputArg(call);
|
|
1807
|
+
if (!arg || !ts.isObjectLiteralExpression(arg)) return;
|
|
1808
|
+
for (const prop of arg.properties) {
|
|
1809
|
+
const isSpread = ts.isSpreadAssignment(prop);
|
|
1810
|
+
const isComputed = ts.isPropertyAssignment(prop) && ts.isComputedPropertyName(prop.name);
|
|
1811
|
+
if (!isSpread && !isComputed) continue;
|
|
1812
|
+
const { line, column } = lineColumn(sourceFile, prop);
|
|
1813
|
+
out.push({
|
|
1814
|
+
severity: "warning",
|
|
1815
|
+
code: "step-input-spread",
|
|
1816
|
+
fileName,
|
|
1817
|
+
line,
|
|
1818
|
+
column,
|
|
1819
|
+
message: `${isSpread ? "Spread" : "Computed-key"} property \`${snippet(sourceFile, prop)}\` in a step input isn't shown in the step inspector. List the fields explicitly so each input renders as a value chip.`
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
return out;
|
|
1824
|
+
}
|
|
1825
|
+
function opaqueCodeBlockReason(label, shared) {
|
|
1826
|
+
if (/\bPromise\.(race|allSettled|any)\b/.test(label)) return "Promise.race/allSettled/any isn't modeled as branches; it renders opaque.";
|
|
1827
|
+
if (/\bPromise\.all\b/.test(label)) return "Promise.all over a computed array (e.g. `.map(...)`) can't be shown as parallel branches. Pass an array literal of explicit step calls, or use a for-of loop.";
|
|
1828
|
+
if (/\.(map|forEach|filter|reduce|flatMap)\s*\(/.test(label)) return "Array-method iteration (`.map`/`.filter`/`.forEach`) containing steps renders as one opaque block. Use a for-of loop to render an explicit loop.";
|
|
1829
|
+
const helperMatch = label.match(/(?:await\s+)?([A-Za-z0-9_$]+)\s*\(/);
|
|
1830
|
+
if (helperMatch && shared.has(helperMatch[1])) return `Helper \`${helperMatch[1]}\` is called from more than one site, so each call renders as an opaque block (its steps share one id and can't be shown individually). Inline it, or call the steps directly in \`run()\`.`;
|
|
1831
|
+
return "This construct is outside the supported grammar and renders as an opaque block; the steps inside it aren't shown individually.";
|
|
1832
|
+
}
|
|
1833
|
+
/** One warning per opaque code-block the producer emits, with an explicit reason. */
|
|
1834
|
+
function opaqueCodeBlockDiagnostics(sourceFile, located, fileName, shared) {
|
|
1835
|
+
let graph;
|
|
1836
|
+
try {
|
|
1837
|
+
graph = buildGraph(sourceFile, located);
|
|
1838
|
+
} catch {
|
|
1839
|
+
return [];
|
|
1840
|
+
}
|
|
1841
|
+
return graph.nodes.filter((node) => node.nodeType === "code-block").map((node) => {
|
|
1842
|
+
const label = node.data.label ?? "";
|
|
1843
|
+
return {
|
|
1844
|
+
severity: "warning",
|
|
1845
|
+
code: "opaque-code-block",
|
|
1846
|
+
fileName,
|
|
1847
|
+
message: `Renders as an opaque code block: \`${label}\`. ${opaqueCodeBlockReason(label, shared)}`
|
|
1848
|
+
};
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Deterministic deploy-time producer: parse a workflow's TypeScript source and
|
|
1853
|
+
* emit its control-flow skeleton (entry → steps/branches/loops → exit) as a
|
|
1854
|
+
* {@link WorkflowCanvasGraph}.
|
|
1855
|
+
*/
|
|
1856
|
+
function buildWorkflowCanvasGraph(source, options = {}) {
|
|
1857
|
+
const sourceFile = ts.createSourceFile(options.fileName ?? "workflow.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
1858
|
+
const located = locateWorkflow(sourceFile);
|
|
1859
|
+
if (!located) return null;
|
|
1860
|
+
return buildGraph(sourceFile, located, { resolveSlug: options.resolveSlug });
|
|
1861
|
+
}
|
|
340
1862
|
const replaceEditSchema = object({
|
|
341
1863
|
oldText: string().describe("Exact text to replace. Must be unique in the file and must not overlap other edits in this call."),
|
|
342
1864
|
newText: string().describe("Replacement text")
|
|
@@ -1669,25 +3191,49 @@ const workflows = pgTable("workflows", {
|
|
|
1669
3191
|
id: text("id").primaryKey(),
|
|
1670
3192
|
projectId: text("project_id").notNull(),
|
|
1671
3193
|
slug: text("slug").notNull(),
|
|
1672
|
-
name: text("name"),
|
|
1673
|
-
description: text("description"),
|
|
3194
|
+
name: text("name").notNull(),
|
|
3195
|
+
description: text("description").notNull(),
|
|
1674
3196
|
moduleFile: text("module_file").notNull(),
|
|
1675
3197
|
subscribable: integer("subscribable").notNull(),
|
|
1676
3198
|
registeredAt: timestamp("registered_at", { withTimezone: true }).notNull(),
|
|
1677
3199
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
|
|
1678
|
-
deletedAt: timestamp("deleted_at", { withTimezone: true })
|
|
3200
|
+
deletedAt: timestamp("deleted_at", { withTimezone: true }),
|
|
3201
|
+
runInputValues: jsonb("run_input_values").$type(),
|
|
3202
|
+
canvasAnnotationsJson: jsonb("canvas_annotations_json").$type(),
|
|
3203
|
+
canvasAnnotationsHash: text("canvas_annotations_hash"),
|
|
3204
|
+
canvasAnnotationsModel: text("canvas_annotations_model"),
|
|
3205
|
+
canvasAnnotationsStatus: text("canvas_annotations_status").$type().notNull().default("idle"),
|
|
3206
|
+
canvasAnnotationsGeneratedAt: timestamp("canvas_annotations_generated_at", { withTimezone: true }),
|
|
3207
|
+
overviewSummary: text("overview_summary"),
|
|
3208
|
+
overviewPhasesJson: jsonb("overview_phases_json").$type(),
|
|
3209
|
+
overviewHash: text("overview_hash"),
|
|
3210
|
+
overviewModel: text("overview_model"),
|
|
3211
|
+
overviewStatus: text("overview_status").$type().notNull().default("idle"),
|
|
3212
|
+
overviewGeneratedAt: timestamp("overview_generated_at", { withTimezone: true })
|
|
1679
3213
|
}, (table) => [uniqueIndex("workflows_project_id_slug_idx").on(table.projectId, table.slug)]);
|
|
1680
3214
|
const workflowsSqlite = sqliteTable("workflows", {
|
|
1681
3215
|
id: text$1("id").primaryKey(),
|
|
1682
3216
|
projectId: text$1("project_id").notNull(),
|
|
1683
3217
|
slug: text$1("slug").notNull(),
|
|
1684
|
-
name: text$1("name"),
|
|
1685
|
-
description: text$1("description"),
|
|
3218
|
+
name: text$1("name").notNull(),
|
|
3219
|
+
description: text$1("description").notNull(),
|
|
1686
3220
|
moduleFile: text$1("module_file").notNull(),
|
|
1687
3221
|
subscribable: integer$1("subscribable", { mode: "boolean" }).notNull(),
|
|
1688
3222
|
registeredAt: integer$1("registered_at", { mode: "timestamp_ms" }).notNull(),
|
|
1689
3223
|
updatedAt: integer$1("updated_at", { mode: "timestamp_ms" }).notNull(),
|
|
1690
|
-
deletedAt: integer$1("deleted_at", { mode: "timestamp_ms" })
|
|
3224
|
+
deletedAt: integer$1("deleted_at", { mode: "timestamp_ms" }),
|
|
3225
|
+
runInputValues: text$1("run_input_values", { mode: "json" }).$type(),
|
|
3226
|
+
canvasAnnotationsJson: text$1("canvas_annotations_json", { mode: "json" }).$type(),
|
|
3227
|
+
canvasAnnotationsHash: text$1("canvas_annotations_hash"),
|
|
3228
|
+
canvasAnnotationsModel: text$1("canvas_annotations_model"),
|
|
3229
|
+
canvasAnnotationsStatus: text$1("canvas_annotations_status").$type().notNull().default("idle"),
|
|
3230
|
+
canvasAnnotationsGeneratedAt: integer$1("canvas_annotations_generated_at", { mode: "timestamp_ms" }),
|
|
3231
|
+
overviewSummary: text$1("overview_summary"),
|
|
3232
|
+
overviewPhasesJson: text$1("overview_phases_json", { mode: "json" }).$type(),
|
|
3233
|
+
overviewHash: text$1("overview_hash"),
|
|
3234
|
+
overviewModel: text$1("overview_model"),
|
|
3235
|
+
overviewStatus: text$1("overview_status").$type().notNull().default("idle"),
|
|
3236
|
+
overviewGeneratedAt: integer$1("overview_generated_at", { mode: "timestamp_ms" })
|
|
1691
3237
|
}, (table) => [uniqueIndex$1("workflows_project_id_slug_idx").on(table.projectId, table.slug)]);
|
|
1692
3238
|
const resourceActivity = pgTable("resource_activity", {
|
|
1693
3239
|
id: text("id").primaryKey(),
|
|
@@ -1831,15 +3377,29 @@ object({
|
|
|
1831
3377
|
//#endregion
|
|
1832
3378
|
//#region ../../packages/action/dist/index.mjs
|
|
1833
3379
|
const zodSchema$3 = custom((v) => v instanceof ZodType, "must be a Zod schema");
|
|
1834
|
-
|
|
3380
|
+
/** Runtime validation for an unbranded action definition. */
|
|
3381
|
+
const actionCoreSchema = object({
|
|
1835
3382
|
slug: string().trim().min(1),
|
|
1836
|
-
name:
|
|
1837
|
-
description:
|
|
3383
|
+
name: requiredDisplayTextSchema,
|
|
3384
|
+
description: requiredDisplayTextSchema,
|
|
1838
3385
|
input: zodSchema$3,
|
|
1839
3386
|
output: zodSchema$3,
|
|
1840
3387
|
credentials: array(credentialInputSchema).optional(),
|
|
1841
3388
|
run: _function()
|
|
1842
3389
|
});
|
|
3390
|
+
const ACTION$1 = Symbol.for("keystroke.action");
|
|
3391
|
+
/**
|
|
3392
|
+
* Validates brand + shape via `actionCoreSchema` so discovery and guards reject
|
|
3393
|
+
* malformed definitions.
|
|
3394
|
+
*/
|
|
3395
|
+
function isAction(value) {
|
|
3396
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3397
|
+
if (!(ACTION$1 in value) || value[ACTION$1] !== true) return false;
|
|
3398
|
+
return actionCoreSchema.safeParse(value).success;
|
|
3399
|
+
}
|
|
3400
|
+
function getActionCredentialRequirements(action) {
|
|
3401
|
+
return action.credentials;
|
|
3402
|
+
}
|
|
1843
3403
|
new AsyncLocalStorage();
|
|
1844
3404
|
new AsyncLocalStorage();
|
|
1845
3405
|
new AbortController().signal;
|
|
@@ -18982,8 +20542,8 @@ const zodSchema$2 = custom((v) => v instanceof ZodType, "must be a Zod schema");
|
|
|
18982
20542
|
/** Runtime validation for an unbranded workflow definition. */
|
|
18983
20543
|
const workflowCoreSchema = object({
|
|
18984
20544
|
slug: string().trim().min(1),
|
|
18985
|
-
name:
|
|
18986
|
-
description:
|
|
20545
|
+
name: requiredDisplayTextSchema,
|
|
20546
|
+
description: requiredDisplayTextSchema,
|
|
18987
20547
|
subscription: object({ mode: _enum(["system", "subscribable"]).optional() }).optional(),
|
|
18988
20548
|
input: zodSchema$2,
|
|
18989
20549
|
output: zodSchema$2,
|
|
@@ -18999,6 +20559,7 @@ function isWorkflow(value) {
|
|
|
18999
20559
|
if (!(WORKFLOW$1 in value) || value[WORKFLOW$1] !== true) return false;
|
|
19000
20560
|
return workflowCoreSchema.safeParse(value).success;
|
|
19001
20561
|
}
|
|
20562
|
+
new AsyncLocalStorage();
|
|
19002
20563
|
const storage = new AsyncLocalStorage();
|
|
19003
20564
|
registerWorkflowRunGetter(() => {
|
|
19004
20565
|
const store = storage.getStore();
|
|
@@ -19808,11 +21369,12 @@ function normalizeLegacySlugFields$1(value) {
|
|
|
19808
21369
|
return next;
|
|
19809
21370
|
}
|
|
19810
21371
|
const slugField$1 = string().trim().min(1);
|
|
19811
|
-
const
|
|
21372
|
+
const requiredTextField$1 = string().trim().min(1);
|
|
21373
|
+
const optionalTextField$1 = string().trim().min(1).optional();
|
|
19812
21374
|
const baseSourceShape$1 = {
|
|
19813
21375
|
slug: slugField$1,
|
|
19814
|
-
name:
|
|
19815
|
-
description:
|
|
21376
|
+
name: requiredTextField$1,
|
|
21377
|
+
description: requiredTextField$1,
|
|
19816
21378
|
attach: _function()
|
|
19817
21379
|
};
|
|
19818
21380
|
/** Runtime validation for a trigger source (webhook | cron | poll). */
|
|
@@ -19887,11 +21449,9 @@ function attachmentSlugFromRecord(attachment) {
|
|
|
19887
21449
|
return slug;
|
|
19888
21450
|
}
|
|
19889
21451
|
function triggerMetaFromAttachment(attachment) {
|
|
19890
|
-
const name = attachment.name ?? attachment.source.name;
|
|
19891
|
-
const description = attachment.description ?? attachment.source.description;
|
|
19892
21452
|
return {
|
|
19893
|
-
|
|
19894
|
-
|
|
21453
|
+
name: attachment.name ?? attachment.source.name,
|
|
21454
|
+
description: attachment.description ?? attachment.source.description
|
|
19895
21455
|
};
|
|
19896
21456
|
}
|
|
19897
21457
|
/**
|
|
@@ -19920,6 +21480,8 @@ function isManifestAgent(value) {
|
|
|
19920
21480
|
}
|
|
19921
21481
|
function validateManifestAgent(value, filePath) {
|
|
19922
21482
|
if (!isManifestAgent(value)) throw new Error(`${filePath} must default-export defineAgent(...)`);
|
|
21483
|
+
if (!value.name?.trim()) throw new Error(`${filePath} agent must include a non-empty name`);
|
|
21484
|
+
if (!value.description?.trim()) throw new Error(`${filePath} agent must include a non-empty description`);
|
|
19923
21485
|
return value;
|
|
19924
21486
|
}
|
|
19925
21487
|
function normalizeWebhookEndpoint(endpoint) {
|
|
@@ -19978,6 +21540,30 @@ function actionSlug(tool) {
|
|
|
19978
21540
|
const record = tool;
|
|
19979
21541
|
return typeof record.slug === "string" ? record.slug : void 0;
|
|
19980
21542
|
}
|
|
21543
|
+
function toolCredentialRequirements(tool) {
|
|
21544
|
+
const record = tool;
|
|
21545
|
+
if (isManifestAction(tool)) return getManifestActionCredentialRequirements(tool);
|
|
21546
|
+
return "credentials" in record && Array.isArray(record.credentials) ? record.credentials : void 0;
|
|
21547
|
+
}
|
|
21548
|
+
/**
|
|
21549
|
+
* Credential requirements per tool slug — the agent's credential consumer key.
|
|
21550
|
+
* Powers the workflow-canvas credential overlay for agent nodes (which resolve
|
|
21551
|
+
* per tool, not per call site).
|
|
21552
|
+
*/
|
|
21553
|
+
function collectAgentToolRequirements(agent) {
|
|
21554
|
+
const byTool = {};
|
|
21555
|
+
for (const tool of agent.tools ?? []) {
|
|
21556
|
+
const slug = actionSlug(tool);
|
|
21557
|
+
const requirements = toolCredentialRequirements(tool);
|
|
21558
|
+
if (!slug || !requirements?.length) continue;
|
|
21559
|
+
byTool[slug] = normalizeCredentialList(requirements).map((requirement) => ({
|
|
21560
|
+
key: requirement.key,
|
|
21561
|
+
kind: requirement.kind,
|
|
21562
|
+
...requirement.scope ? { scope: requirement.scope } : {}
|
|
21563
|
+
}));
|
|
21564
|
+
}
|
|
21565
|
+
return byTool;
|
|
21566
|
+
}
|
|
19981
21567
|
/** App-kind slugs required by the agent's tools (same as `credential_instances.app_slug`). */
|
|
19982
21568
|
function collectAgentAppSlugs(agent) {
|
|
19983
21569
|
const slugs = /* @__PURE__ */ new Set();
|
|
@@ -20002,6 +21588,7 @@ function countAgentCredentials(agent) {
|
|
|
20002
21588
|
}
|
|
20003
21589
|
/** Single source of truth for the `kind: "agent"` route-manifest entry shape. */
|
|
20004
21590
|
function agentManifestEntry(agent, options) {
|
|
21591
|
+
const toolRequirements = collectAgentToolRequirements(agent);
|
|
20005
21592
|
return {
|
|
20006
21593
|
kind: "agent",
|
|
20007
21594
|
slug: agent.slug,
|
|
@@ -20013,7 +21600,69 @@ function agentManifestEntry(agent, options) {
|
|
|
20013
21600
|
toolCount: agent.tools?.length ?? 0,
|
|
20014
21601
|
credentialCount: countAgentCredentials(agent),
|
|
20015
21602
|
appSlugs: collectAgentAppSlugs(agent),
|
|
20016
|
-
toolSlugs: collectAgentToolSlugs(agent)
|
|
21603
|
+
toolSlugs: collectAgentToolSlugs(agent),
|
|
21604
|
+
...Object.keys(toolRequirements).length ? { toolRequirements } : {}
|
|
21605
|
+
};
|
|
21606
|
+
}
|
|
21607
|
+
function resolveActionFromModule(mod, filePath) {
|
|
21608
|
+
if (isAction(mod.default)) return mod.default;
|
|
21609
|
+
const named = Object.values(mod).filter(isAction);
|
|
21610
|
+
if (named.length === 1) return named[0];
|
|
21611
|
+
if (named.length > 1) throw new Error(`${filePath} exports multiple actions; use one action per file`);
|
|
21612
|
+
throw new Error(`${filePath} must export defineAction(...) (default or single named export)`);
|
|
21613
|
+
}
|
|
21614
|
+
async function importActionDefinition(filePath, options) {
|
|
21615
|
+
const appRoot = resolveAppRoot(filePath);
|
|
21616
|
+
const href = pathToFileURL(filePath).href;
|
|
21617
|
+
return runWithAppRoot(appRoot, async () => {
|
|
21618
|
+
return resolveActionFromModule(await (options?.reload ? import(`${href}?keystroke=${Date.now()}`) : import(href)), filePath);
|
|
21619
|
+
});
|
|
21620
|
+
}
|
|
21621
|
+
async function discoverActions(actionsDir, options) {
|
|
21622
|
+
const files = await discoverModuleFileEntries(actionsDir, {
|
|
21623
|
+
nestedEntry: "action",
|
|
21624
|
+
duplicateLabel: "action module file"
|
|
21625
|
+
});
|
|
21626
|
+
const actions = [];
|
|
21627
|
+
for (const { filePath, moduleFile } of files) {
|
|
21628
|
+
const definition = await importActionDefinition(filePath, options);
|
|
21629
|
+
const key = definition.slug.trim();
|
|
21630
|
+
if (!key) throw new Error(`${filePath} action must define a non-empty slug`);
|
|
21631
|
+
actions.push({
|
|
21632
|
+
key,
|
|
21633
|
+
filePath,
|
|
21634
|
+
moduleFile,
|
|
21635
|
+
definition
|
|
21636
|
+
});
|
|
21637
|
+
}
|
|
21638
|
+
return actions;
|
|
21639
|
+
}
|
|
21640
|
+
/**
|
|
21641
|
+
* Reduce a definition's `.credentials` list to the deploy-time summaries the
|
|
21642
|
+
* workflow-canvas credential overlay needs (`key` / `kind` / pinned `scope`).
|
|
21643
|
+
* The Zod schema is intentionally dropped — only the resolution metadata ships.
|
|
21644
|
+
*/
|
|
21645
|
+
function credentialRequirementSummaries(list) {
|
|
21646
|
+
if (!list?.length) return [];
|
|
21647
|
+
return normalizeCredentialList(list).map((requirement) => ({
|
|
21648
|
+
key: requirement.key,
|
|
21649
|
+
kind: requirement.kind,
|
|
21650
|
+
...requirement.scope ? { scope: requirement.scope } : {}
|
|
21651
|
+
}));
|
|
21652
|
+
}
|
|
21653
|
+
/** Single source of truth for the `kind: "action"` route-manifest entry shape. */
|
|
21654
|
+
function actionManifestEntry(action, options) {
|
|
21655
|
+
const requirements = credentialRequirementSummaries(getActionCredentialRequirements(action));
|
|
21656
|
+
return {
|
|
21657
|
+
kind: "action",
|
|
21658
|
+
slug: action.slug,
|
|
21659
|
+
moduleFile: options.moduleFile,
|
|
21660
|
+
name: action.name,
|
|
21661
|
+
description: action.description,
|
|
21662
|
+
...options.appSlug ? { appSlug: options.appSlug } : {},
|
|
21663
|
+
inputSchema: options.inputSchema,
|
|
21664
|
+
outputSchema: options.outputSchema,
|
|
21665
|
+
...requirements.length ? { requirements } : {}
|
|
20017
21666
|
};
|
|
20018
21667
|
}
|
|
20019
21668
|
function pollGroupId(discovered) {
|
|
@@ -20153,7 +21802,7 @@ function validateProjectModules(input) {
|
|
|
20153
21802
|
]);
|
|
20154
21803
|
}
|
|
20155
21804
|
const SKIP_DIRS = new Set([".git", "node_modules"]);
|
|
20156
|
-
function toPosix$
|
|
21805
|
+
function toPosix$2(path) {
|
|
20157
21806
|
return path.split(sep).join("/");
|
|
20158
21807
|
}
|
|
20159
21808
|
function parseSkillFrontmatter(raw) {
|
|
@@ -20174,23 +21823,26 @@ function parseSkillFrontmatter(raw) {
|
|
|
20174
21823
|
return out;
|
|
20175
21824
|
}
|
|
20176
21825
|
function walkSkillFiles(root, dir, out) {
|
|
20177
|
-
for (const
|
|
20178
|
-
const absolute = join(dir,
|
|
21826
|
+
for (const entry of readdirSync(dir).sort()) {
|
|
21827
|
+
const absolute = join(dir, entry);
|
|
20179
21828
|
const stats = statSync(absolute);
|
|
20180
21829
|
if (stats.isDirectory()) {
|
|
20181
|
-
if (SKIP_DIRS.has(
|
|
21830
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
20182
21831
|
walkSkillFiles(root, absolute, out);
|
|
20183
21832
|
continue;
|
|
20184
21833
|
}
|
|
20185
|
-
if (!stats.isFile() ||
|
|
20186
|
-
const moduleFile = toPosix$
|
|
20187
|
-
const slug = toPosix$
|
|
21834
|
+
if (!stats.isFile() || entry !== "SKILL.md") continue;
|
|
21835
|
+
const moduleFile = toPosix$2(relative(root, absolute));
|
|
21836
|
+
const slug = toPosix$2(relative(join(root, "src", "skills"), absolute)).replace(/\/?SKILL\.md$/, "");
|
|
20188
21837
|
if (!slug) continue;
|
|
20189
21838
|
const frontmatter = parseSkillFrontmatter(readFileSync(absolute, "utf8"));
|
|
21839
|
+
const name = frontmatter.name?.trim();
|
|
21840
|
+
const description = frontmatter.description?.trim();
|
|
21841
|
+
if (!name || !description) throw new Error(`${moduleFile} skill frontmatter must include non-empty name and description`);
|
|
20190
21842
|
out.push({
|
|
20191
21843
|
slug,
|
|
20192
|
-
name
|
|
20193
|
-
description
|
|
21844
|
+
name,
|
|
21845
|
+
description,
|
|
20194
21846
|
moduleFile
|
|
20195
21847
|
});
|
|
20196
21848
|
}
|
|
@@ -20227,7 +21879,21 @@ function serializeRouteManifest(manifest) {
|
|
|
20227
21879
|
toolCount: entry.toolCount,
|
|
20228
21880
|
credentialCount: entry.credentialCount,
|
|
20229
21881
|
appSlugs: entry.appSlugs,
|
|
20230
|
-
toolSlugs: entry.toolSlugs
|
|
21882
|
+
toolSlugs: entry.toolSlugs,
|
|
21883
|
+
...entry.toolRequirements ? { toolRequirements: entry.toolRequirements } : {}
|
|
21884
|
+
});
|
|
21885
|
+
break;
|
|
21886
|
+
case "action":
|
|
21887
|
+
entries.push({
|
|
21888
|
+
kind: entry.kind,
|
|
21889
|
+
slug: entry.slug,
|
|
21890
|
+
moduleFile: entry.moduleFile,
|
|
21891
|
+
...entry.appSlug !== void 0 ? { appSlug: entry.appSlug } : {},
|
|
21892
|
+
name: entry.name,
|
|
21893
|
+
description: entry.description,
|
|
21894
|
+
inputSchema: entry.inputSchema,
|
|
21895
|
+
outputSchema: entry.outputSchema,
|
|
21896
|
+
...entry.requirements ? { requirements: entry.requirements } : {}
|
|
20231
21897
|
});
|
|
20232
21898
|
break;
|
|
20233
21899
|
case "workflow":
|
|
@@ -20238,7 +21904,12 @@ function serializeRouteManifest(manifest) {
|
|
|
20238
21904
|
description: entry.description,
|
|
20239
21905
|
subscribable: entry.subscribable,
|
|
20240
21906
|
moduleFile: entry.moduleFile,
|
|
20241
|
-
requestSchema: schemaToJson(entry.request)
|
|
21907
|
+
requestSchema: schemaToJson(entry.request),
|
|
21908
|
+
responseSchema: schemaToJson(entry.response),
|
|
21909
|
+
...entry.flowGraph ? {
|
|
21910
|
+
flowGraph: entry.flowGraph,
|
|
21911
|
+
flowGraphSourceHash: entry.flowGraphSourceHash
|
|
21912
|
+
} : {}
|
|
20242
21913
|
});
|
|
20243
21914
|
break;
|
|
20244
21915
|
case "trigger-webhook":
|
|
@@ -20247,6 +21918,7 @@ function serializeRouteManifest(manifest) {
|
|
|
20247
21918
|
endpoint: entry.endpoint,
|
|
20248
21919
|
attachmentIds: entry.attachmentIds,
|
|
20249
21920
|
moduleFile: entry.moduleFile,
|
|
21921
|
+
...entry.sourceHash ? { sourceHash: entry.sourceHash } : {},
|
|
20250
21922
|
attachmentSchemas: Object.fromEntries(Object.entries(entry.attachmentSchemas).map(([attachmentSlug, schemas]) => [attachmentSlug, {
|
|
20251
21923
|
requestSchema: schemaToJson(schemas.request),
|
|
20252
21924
|
...schemas.filter ? { filterSchema: schemaToJson(schemas.filter) } : {}
|
|
@@ -20260,9 +21932,10 @@ function serializeRouteManifest(manifest) {
|
|
|
20260
21932
|
attachmentId: entry.attachmentId,
|
|
20261
21933
|
attachmentIds: entry.attachmentIds,
|
|
20262
21934
|
moduleFile: entry.moduleFile,
|
|
21935
|
+
...entry.sourceHash ? { sourceHash: entry.sourceHash } : {},
|
|
20263
21936
|
schedule: entry.schedule,
|
|
20264
|
-
|
|
20265
|
-
|
|
21937
|
+
name: entry.name,
|
|
21938
|
+
description: entry.description
|
|
20266
21939
|
});
|
|
20267
21940
|
break;
|
|
20268
21941
|
case "trigger-poll-group":
|
|
@@ -20313,6 +21986,8 @@ function isManifestWorkflow(value) {
|
|
|
20313
21986
|
}
|
|
20314
21987
|
function validateManifestWorkflow(value, filePath) {
|
|
20315
21988
|
if (!isManifestWorkflow(value)) throw new Error(`${filePath} must default-export defineWorkflow(...)`);
|
|
21989
|
+
if (!value.name?.trim()) throw new Error(`${filePath} workflow must include a non-empty name`);
|
|
21990
|
+
if (!value.description?.trim()) throw new Error(`${filePath} workflow must include a non-empty description`);
|
|
20316
21991
|
return value;
|
|
20317
21992
|
}
|
|
20318
21993
|
const TRIGGER_ATTACHMENT = Symbol.for("keystroke.triggerAttachment");
|
|
@@ -20331,11 +22006,12 @@ function normalizeLegacySlugFields(value) {
|
|
|
20331
22006
|
return next;
|
|
20332
22007
|
}
|
|
20333
22008
|
const slugField = string().trim().min(1);
|
|
20334
|
-
const
|
|
22009
|
+
const requiredTextField = string().trim().min(1);
|
|
22010
|
+
const optionalTextField = string().trim().min(1).optional();
|
|
20335
22011
|
const baseSourceShape = {
|
|
20336
22012
|
slug: slugField,
|
|
20337
|
-
name:
|
|
20338
|
-
description:
|
|
22013
|
+
name: requiredTextField,
|
|
22014
|
+
description: requiredTextField,
|
|
20339
22015
|
attach: _function()
|
|
20340
22016
|
};
|
|
20341
22017
|
const triggerSourceSchema = preprocess(normalizeLegacySlugFields, discriminatedUnion("kind", [
|
|
@@ -20501,11 +22177,181 @@ async function discoverWorkflows(workflowsDir, options) {
|
|
|
20501
22177
|
}
|
|
20502
22178
|
return workflows;
|
|
20503
22179
|
}
|
|
22180
|
+
function toPosix$1(path) {
|
|
22181
|
+
return path.split("\\").join("/");
|
|
22182
|
+
}
|
|
22183
|
+
function buildSymbolImportMap(source, fileName) {
|
|
22184
|
+
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
22185
|
+
const map = /* @__PURE__ */ new Map();
|
|
22186
|
+
for (const statement of sourceFile.statements) {
|
|
22187
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
22188
|
+
const specifier = statement.moduleSpecifier.text;
|
|
22189
|
+
const clause = statement.importClause;
|
|
22190
|
+
if (!clause) continue;
|
|
22191
|
+
if (clause.name) map.set(clause.name.text, {
|
|
22192
|
+
specifier,
|
|
22193
|
+
exportName: "default"
|
|
22194
|
+
});
|
|
22195
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) for (const element of clause.namedBindings.elements) {
|
|
22196
|
+
const exportName = (element.propertyName ?? element.name).text;
|
|
22197
|
+
map.set(element.name.text, {
|
|
22198
|
+
specifier,
|
|
22199
|
+
exportName
|
|
22200
|
+
});
|
|
22201
|
+
}
|
|
22202
|
+
}
|
|
22203
|
+
return map;
|
|
22204
|
+
}
|
|
22205
|
+
function resolveRelativeModuleFile(projectRoot, workflowModuleFile, specifier) {
|
|
22206
|
+
if (!specifier.startsWith(".")) return;
|
|
22207
|
+
const workflowDir = dirname(join(projectRoot, workflowModuleFile));
|
|
22208
|
+
const candidates = [
|
|
22209
|
+
join(workflowDir, specifier),
|
|
22210
|
+
`${join(workflowDir, specifier)}.ts`,
|
|
22211
|
+
join(workflowDir, `${specifier}.ts`),
|
|
22212
|
+
join(workflowDir, specifier, "index.ts")
|
|
22213
|
+
];
|
|
22214
|
+
for (const candidate of candidates) if (existsSync(candidate)) return toPosix$1(relative(projectRoot, candidate));
|
|
22215
|
+
}
|
|
22216
|
+
function toResolvedStepSlug(entry) {
|
|
22217
|
+
return {
|
|
22218
|
+
slug: entry.slug,
|
|
22219
|
+
name: entry.name,
|
|
22220
|
+
description: entry.description
|
|
22221
|
+
};
|
|
22222
|
+
}
|
|
22223
|
+
/** Build a deploy-time slug resolver from a workflow file's imports + definition registry. */
|
|
22224
|
+
function createResolveSlugFn(projectRoot, workflowModuleFile, workflowSource, registryByModuleFile, integrationRegistry) {
|
|
22225
|
+
const importMap = buildSymbolImportMap(workflowSource, workflowModuleFile);
|
|
22226
|
+
return (importName) => {
|
|
22227
|
+
const imported = importMap.get(importName);
|
|
22228
|
+
if (!imported) return;
|
|
22229
|
+
const { specifier, exportName } = imported;
|
|
22230
|
+
if (specifier.startsWith(".")) {
|
|
22231
|
+
const moduleFile = resolveRelativeModuleFile(projectRoot, workflowModuleFile, specifier);
|
|
22232
|
+
if (!moduleFile) return;
|
|
22233
|
+
const entry = registryByModuleFile.get(moduleFile);
|
|
22234
|
+
return entry ? toResolvedStepSlug(entry) : void 0;
|
|
22235
|
+
}
|
|
22236
|
+
const entry = integrationRegistry?.get(`${specifier}#${exportName}`);
|
|
22237
|
+
return entry ? toResolvedStepSlug(entry) : void 0;
|
|
22238
|
+
};
|
|
22239
|
+
}
|
|
22240
|
+
function buildSlugRegistry(entries) {
|
|
22241
|
+
return new Map(entries.map((entry) => [entry.moduleFile, entry]));
|
|
22242
|
+
}
|
|
22243
|
+
/** The framework package — never treated as an integration action source. */
|
|
22244
|
+
const FRAMEWORK_PACKAGE = "@keystrokehq/keystroke";
|
|
22245
|
+
/**
|
|
22246
|
+
* Map an integration import specifier to its app slug.
|
|
22247
|
+
*
|
|
22248
|
+
* `@keystrokehq/slackbot/actions` → `slackbot`, `@keystrokehq/github` → `github`.
|
|
22249
|
+
* Returns `undefined` for the framework package and any non-`@keystrokehq` scope.
|
|
22250
|
+
*/
|
|
22251
|
+
function integrationAppSlugFromSpecifier(specifier) {
|
|
22252
|
+
if (specifier === FRAMEWORK_PACKAGE || specifier.startsWith(`${FRAMEWORK_PACKAGE}/`)) return;
|
|
22253
|
+
return /^@keystrokehq\/([^/]+)(?:\/.*)?$/.exec(specifier)?.[1];
|
|
22254
|
+
}
|
|
22255
|
+
/**
|
|
22256
|
+
* The integration action subpath convention (`@keystrokehq/<app>/actions`). We only
|
|
22257
|
+
* import these — the real authoring convention — to avoid loading whole packages.
|
|
22258
|
+
*/
|
|
22259
|
+
function isIntegrationActionsSpecifier(specifier) {
|
|
22260
|
+
return /^@keystrokehq\/[^/]+\/actions$/.test(specifier) && specifier !== FRAMEWORK_PACKAGE;
|
|
22261
|
+
}
|
|
22262
|
+
/** Map each integration `/actions` specifier imported by a file to the exports it uses. */
|
|
22263
|
+
function collectIntegrationImports(source, fileName) {
|
|
22264
|
+
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
22265
|
+
const bySpecifier = /* @__PURE__ */ new Map();
|
|
22266
|
+
for (const statement of sourceFile.statements) {
|
|
22267
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
22268
|
+
const specifier = statement.moduleSpecifier.text;
|
|
22269
|
+
if (!isIntegrationActionsSpecifier(specifier)) continue;
|
|
22270
|
+
const usage = bySpecifier.get(specifier) ?? {
|
|
22271
|
+
names: /* @__PURE__ */ new Set(),
|
|
22272
|
+
importsAll: false
|
|
22273
|
+
};
|
|
22274
|
+
const clause = statement.importClause;
|
|
22275
|
+
if (clause?.name) usage.names.add("default");
|
|
22276
|
+
const bindings = clause?.namedBindings;
|
|
22277
|
+
if (bindings) if (ts.isNamespaceImport(bindings)) usage.importsAll = true;
|
|
22278
|
+
else for (const element of bindings.elements) usage.names.add(element.propertyName?.text ?? element.name.text);
|
|
22279
|
+
bySpecifier.set(specifier, usage);
|
|
22280
|
+
}
|
|
22281
|
+
return bySpecifier;
|
|
22282
|
+
}
|
|
22283
|
+
/**
|
|
22284
|
+
* Discover actions imported from integration packages (`@keystrokehq/<app>/actions`)
|
|
22285
|
+
* by the project's workflow/agent source files.
|
|
22286
|
+
*
|
|
22287
|
+
* Each `/actions` subpath is resolved from the project's `node_modules`, dynamically
|
|
22288
|
+
* imported, and its action exports enumerated. Best-effort: a missing or broken
|
|
22289
|
+
* integration package never fails the manifest build — it is logged and skipped.
|
|
22290
|
+
*/
|
|
22291
|
+
async function discoverIntegrationActions(projectRoot, sourceFiles, options) {
|
|
22292
|
+
const usageBySpecifier = /* @__PURE__ */ new Map();
|
|
22293
|
+
for (const sourceFile of sourceFiles) {
|
|
22294
|
+
let source;
|
|
22295
|
+
try {
|
|
22296
|
+
source = readFileSync(join(projectRoot, sourceFile), "utf8");
|
|
22297
|
+
} catch {
|
|
22298
|
+
continue;
|
|
22299
|
+
}
|
|
22300
|
+
for (const [specifier, usage] of collectIntegrationImports(source, sourceFile)) {
|
|
22301
|
+
const existing = usageBySpecifier.get(specifier) ?? {
|
|
22302
|
+
names: /* @__PURE__ */ new Set(),
|
|
22303
|
+
importsAll: false
|
|
22304
|
+
};
|
|
22305
|
+
for (const name of usage.names) existing.names.add(name);
|
|
22306
|
+
existing.importsAll = existing.importsAll || usage.importsAll;
|
|
22307
|
+
usageBySpecifier.set(specifier, existing);
|
|
22308
|
+
}
|
|
22309
|
+
}
|
|
22310
|
+
const entries = [];
|
|
22311
|
+
const registry = /* @__PURE__ */ new Map();
|
|
22312
|
+
const seenSlugs = new Set(options?.existingSlugs);
|
|
22313
|
+
const require = createRequire(join(projectRoot, "package.json"));
|
|
22314
|
+
for (const [specifier, usage] of usageBySpecifier) {
|
|
22315
|
+
const appSlug = integrationAppSlugFromSpecifier(specifier);
|
|
22316
|
+
if (!appSlug) continue;
|
|
22317
|
+
try {
|
|
22318
|
+
const resolvedPath = require.resolve(specifier);
|
|
22319
|
+
const href = pathToFileURL(resolvedPath).href;
|
|
22320
|
+
const mod = await runWithAppRoot(resolveAppRoot(resolvedPath), async () => await import(href));
|
|
22321
|
+
for (const [exportName, value] of Object.entries(mod)) {
|
|
22322
|
+
if (!isAction(value)) continue;
|
|
22323
|
+
if (!usage.importsAll && !usage.names.has(exportName)) continue;
|
|
22324
|
+
const definition = value;
|
|
22325
|
+
registry.set(`${specifier}#${exportName}`, {
|
|
22326
|
+
slug: definition.slug,
|
|
22327
|
+
name: definition.name,
|
|
22328
|
+
description: definition.description
|
|
22329
|
+
});
|
|
22330
|
+
if (seenSlugs.has(definition.slug)) continue;
|
|
22331
|
+
seenSlugs.add(definition.slug);
|
|
22332
|
+
entries.push(actionManifestEntry(definition, {
|
|
22333
|
+
moduleFile: specifier,
|
|
22334
|
+
appSlug,
|
|
22335
|
+
inputSchema: schemaToJson(definition.input),
|
|
22336
|
+
outputSchema: schemaToJson(definition.output)
|
|
22337
|
+
}));
|
|
22338
|
+
}
|
|
22339
|
+
} catch (error) {
|
|
22340
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22341
|
+
console.warn(`[manifest] skipped integration actions for ${specifier}: ${message}`);
|
|
22342
|
+
}
|
|
22343
|
+
}
|
|
22344
|
+
return {
|
|
22345
|
+
entries,
|
|
22346
|
+
registry
|
|
22347
|
+
};
|
|
22348
|
+
}
|
|
20504
22349
|
function resolveDistModuleDirs(projectRoot) {
|
|
20505
22350
|
const distBase = join(projectRoot, "dist");
|
|
20506
22351
|
if (!existsSync(distBase)) throw new Error(`Build output missing at ${distBase}. Run keystroke build before emitting the route manifest.`);
|
|
20507
22352
|
return {
|
|
20508
22353
|
agentsDir: join(distBase, "agents"),
|
|
22354
|
+
actionsDir: join(distBase, "actions"),
|
|
20509
22355
|
workflowsDir: join(distBase, "workflows"),
|
|
20510
22356
|
triggersDir: join(distBase, "triggers")
|
|
20511
22357
|
};
|
|
@@ -20521,6 +22367,33 @@ function hashFileContents(absPath) {
|
|
|
20521
22367
|
}
|
|
20522
22368
|
}
|
|
20523
22369
|
/**
|
|
22370
|
+
* Run the deploy-time AST producer over a workflow's source file to attach its
|
|
22371
|
+
* control-flow skeleton + a content hash (for staleness). Best-effort: skips
|
|
22372
|
+
* when the resolved moduleFile is not a readable `.ts` source (e.g. dist-only
|
|
22373
|
+
* builds) or the source has no recognizable workflow, and never fails the build.
|
|
22374
|
+
*/
|
|
22375
|
+
function produceWorkflowFlowGraph(projectRoot, moduleFile, options) {
|
|
22376
|
+
if (!moduleFile.endsWith(".ts")) return;
|
|
22377
|
+
const absolute = join(projectRoot, moduleFile);
|
|
22378
|
+
if (!existsSync(absolute)) return;
|
|
22379
|
+
try {
|
|
22380
|
+
const source = readFileSync(absolute, "utf8");
|
|
22381
|
+
const flowGraph = buildWorkflowCanvasGraph(source, {
|
|
22382
|
+
fileName: moduleFile,
|
|
22383
|
+
resolveSlug: options?.slugRegistry ? createResolveSlugFn(projectRoot, moduleFile, source, options.slugRegistry, options.integrationRegistry) : void 0
|
|
22384
|
+
});
|
|
22385
|
+
if (!flowGraph) return;
|
|
22386
|
+
return {
|
|
22387
|
+
flowGraph,
|
|
22388
|
+
flowGraphSourceHash: createHash("sha256").update(source).digest("hex")
|
|
22389
|
+
};
|
|
22390
|
+
} catch (error) {
|
|
22391
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22392
|
+
console.warn(`[manifest] skipped workflow flow graph for ${moduleFile}: ${message}`);
|
|
22393
|
+
return;
|
|
22394
|
+
}
|
|
22395
|
+
}
|
|
22396
|
+
/**
|
|
20524
22397
|
* Resolve manifest moduleFile values to project-root-relative source paths.
|
|
20525
22398
|
*
|
|
20526
22399
|
* Discovery runs over compiled `dist/` modules, so the raw moduleFile is a
|
|
@@ -20581,6 +22454,7 @@ async function buildStoredRouteManifestForProject(projectRoot, options) {
|
|
|
20581
22454
|
const dirs = resolveDistModuleDirs(projectRoot);
|
|
20582
22455
|
const sourcePaths = new SourceModuleFileResolver(projectRoot);
|
|
20583
22456
|
const manifest = [{ kind: "health" }];
|
|
22457
|
+
const slugRegistryEntries = [];
|
|
20584
22458
|
const agentEntries = await discoverAgentEntries(dirs.agentsDir, reload);
|
|
20585
22459
|
const workflows = await discoverWorkflows(dirs.workflowsDir, reload);
|
|
20586
22460
|
const attachments = await discoverTriggerAttachments(dirs.triggersDir, reload);
|
|
@@ -20594,22 +22468,72 @@ async function buildStoredRouteManifestForProject(projectRoot, options) {
|
|
|
20594
22468
|
agent: await importAgentDefinition(entry.filePath, reload)
|
|
20595
22469
|
})));
|
|
20596
22470
|
await validateAgentModelIds(loadedAgents.map(({ agent }) => agent.model));
|
|
22471
|
+
const resolvedAgents = [];
|
|
20597
22472
|
for (const { entry, agent } of loadedAgents) {
|
|
20598
22473
|
const moduleFile = await sourcePaths.resolve("agents", "agent", dirs.agentsDir, entry.filePath);
|
|
20599
|
-
|
|
22474
|
+
resolvedAgents.push({
|
|
22475
|
+
agent,
|
|
22476
|
+
moduleFile
|
|
22477
|
+
});
|
|
22478
|
+
slugRegistryEntries.push({
|
|
22479
|
+
slug: agent.slug,
|
|
22480
|
+
name: agent.name,
|
|
22481
|
+
description: agent.description,
|
|
22482
|
+
moduleFile
|
|
22483
|
+
});
|
|
22484
|
+
}
|
|
22485
|
+
const actions = await discoverActions(dirs.actionsDir, reload);
|
|
22486
|
+
const resolvedActions = [];
|
|
22487
|
+
for (const action of actions) {
|
|
22488
|
+
const moduleFile = await sourcePaths.resolve("actions", "action", dirs.actionsDir, action.filePath);
|
|
22489
|
+
resolvedActions.push({
|
|
22490
|
+
action,
|
|
22491
|
+
moduleFile
|
|
22492
|
+
});
|
|
22493
|
+
slugRegistryEntries.push({
|
|
22494
|
+
slug: action.definition.slug,
|
|
22495
|
+
name: action.definition.name,
|
|
22496
|
+
description: action.definition.description,
|
|
22497
|
+
moduleFile
|
|
22498
|
+
});
|
|
20600
22499
|
}
|
|
22500
|
+
const resolvedWorkflows = [];
|
|
20601
22501
|
for (const workflow of workflows) {
|
|
20602
22502
|
const moduleFile = await sourcePaths.resolve("workflows", "workflow", dirs.workflowsDir, workflow.filePath);
|
|
20603
|
-
|
|
20604
|
-
|
|
22503
|
+
resolvedWorkflows.push({
|
|
22504
|
+
workflow,
|
|
22505
|
+
moduleFile
|
|
22506
|
+
});
|
|
22507
|
+
slugRegistryEntries.push({
|
|
20605
22508
|
slug: workflow.definition.slug,
|
|
20606
22509
|
name: workflow.definition.name,
|
|
20607
22510
|
description: workflow.definition.description,
|
|
20608
|
-
|
|
20609
|
-
moduleFile,
|
|
20610
|
-
request: workflow.definition.input
|
|
22511
|
+
moduleFile
|
|
20611
22512
|
});
|
|
20612
22513
|
}
|
|
22514
|
+
const slugRegistry = buildSlugRegistry(slugRegistryEntries);
|
|
22515
|
+
const integration = await discoverIntegrationActions(projectRoot, [...resolvedWorkflows.map(({ moduleFile }) => moduleFile), ...resolvedAgents.map(({ moduleFile }) => moduleFile)], { existingSlugs: new Set(slugRegistryEntries.map((entry) => entry.slug)) });
|
|
22516
|
+
for (const { agent, moduleFile } of resolvedAgents) manifest.push(agentManifestEntry(agent, { moduleFile }));
|
|
22517
|
+
for (const { action, moduleFile } of resolvedActions) manifest.push(actionManifestEntry(action.definition, {
|
|
22518
|
+
moduleFile,
|
|
22519
|
+
inputSchema: schemaToJson(action.definition.input),
|
|
22520
|
+
outputSchema: schemaToJson(action.definition.output)
|
|
22521
|
+
}));
|
|
22522
|
+
for (const entry of integration.entries) manifest.push(entry);
|
|
22523
|
+
for (const { workflow, moduleFile } of resolvedWorkflows) manifest.push({
|
|
22524
|
+
kind: "workflow",
|
|
22525
|
+
slug: workflow.definition.slug,
|
|
22526
|
+
name: workflow.definition.name,
|
|
22527
|
+
description: workflow.definition.description,
|
|
22528
|
+
subscribable: workflow.definition.subscription?.mode === "subscribable",
|
|
22529
|
+
moduleFile,
|
|
22530
|
+
request: workflow.definition.input,
|
|
22531
|
+
response: workflow.definition.output,
|
|
22532
|
+
...produceWorkflowFlowGraph(projectRoot, moduleFile, {
|
|
22533
|
+
slugRegistry,
|
|
22534
|
+
integrationRegistry: integration.registry
|
|
22535
|
+
})
|
|
22536
|
+
});
|
|
20613
22537
|
const discoveredBySlug = new Map(attachments.map((attachment) => [attachment.slug, attachment]));
|
|
20614
22538
|
const cronByTriggerSlug = /* @__PURE__ */ new Map();
|
|
20615
22539
|
const pollByGroupId = /* @__PURE__ */ new Map();
|
|
@@ -20674,9 +22598,9 @@ async function buildStoredRouteManifestForProject(projectRoot, options) {
|
|
|
20674
22598
|
discovered,
|
|
20675
22599
|
options: { attachmentSlug: discovered.slug }
|
|
20676
22600
|
}];
|
|
20677
|
-
const attachmentMeta = Object.fromEntries(bindings.
|
|
22601
|
+
const attachmentMeta = Object.fromEntries(bindings.map(({ discovered: row }) => {
|
|
20678
22602
|
const meta = triggerMetaFromAttachment(row.attachment);
|
|
20679
|
-
return
|
|
22603
|
+
return [row.slug, meta];
|
|
20680
22604
|
}));
|
|
20681
22605
|
manifest.push({
|
|
20682
22606
|
kind: "trigger-webhook",
|
|
@@ -20686,16 +22610,16 @@ async function buildStoredRouteManifestForProject(projectRoot, options) {
|
|
|
20686
22610
|
...sourceHash ? { sourceHash } : {},
|
|
20687
22611
|
request: webhookMatchSchemaForBindings(bindings),
|
|
20688
22612
|
attachmentSchemas: webhookManifestAttachmentSchemasFromBindings(bindings),
|
|
20689
|
-
|
|
22613
|
+
attachmentMeta,
|
|
20690
22614
|
response: PromptResponseSchema
|
|
20691
22615
|
});
|
|
20692
22616
|
}
|
|
20693
|
-
return buildStoredRouteManifestFromContext({
|
|
22617
|
+
return parseStoredRouteManifest(buildStoredRouteManifestFromContext({
|
|
20694
22618
|
manifest,
|
|
20695
22619
|
options: {},
|
|
20696
22620
|
projectRoot,
|
|
20697
22621
|
skills: discoverSkillManifestEntries(projectRoot)
|
|
20698
|
-
});
|
|
22622
|
+
}));
|
|
20699
22623
|
} finally {
|
|
20700
22624
|
if (previousRoot === void 0) delete process.env.KEYSTROKE_ROOT;
|
|
20701
22625
|
else process.env.KEYSTROKE_ROOT = previousRoot;
|
|
@@ -20706,6 +22630,6 @@ async function emitStoredRouteManifestForProject(projectRoot) {
|
|
|
20706
22630
|
persistStoredRouteManifest(projectRoot, await buildStoredRouteManifestForProject(projectRoot));
|
|
20707
22631
|
}
|
|
20708
22632
|
//#endregion
|
|
20709
|
-
export { validatePollGroups as A, attachmentSlugFromRecord as B, pollRouteFromSourceSlug as C, validateAttachmentTargets as D, toStoredRouteManifest as E, webhookManifestAttachmentSchemasFromBindings as F,
|
|
22633
|
+
export { validatePollGroups as A, attachmentSlugFromRecord as B, pollRouteFromSourceSlug as C, validateAttachmentTargets as D, toStoredRouteManifest as E, webhookManifestAttachmentSchemasFromBindings as F, packAssetDirs as G, computeCallSiteIds as H, webhookMatchSchemaForBindings as I, entryIdFromFile as J, discoverEntries as K, webhookRouteFromEndpoint as L, validateTriggerAttachments as M, validateUniqueAttachmentSlugs as N, validateImportedTriggerAttachment as O, validateUniqueTriggerSourceSlugs as P, walkTypeScriptFiles as Q, workflowKeyForAttachment as R, pollGroupRouteFromId as S, serializeRouteManifest as T, diagnoseWorkflowSource as U, classifyCall as V, locateWorkflow as W, shouldSkipKeystrokeModuleFile as X, readKeystrokeIgnoreDirective as Y, validateUniqueModuleKeys as Z, importAgentDefinition as _, buildStoredRouteManifestForProject as a, persistStoredRouteManifest as b, collectAgentAppSlugs as c, discoverAgentEntries as d, discoverSkillManifestEntries as f, emitStoredRouteManifestForProject as g, discoverWorkflows as h, buildPollGroups as i, validateProjectModules as j, validateImportedWorkflowDefinition as k, collectAgentToolSlugs as l, discoverWorkflowEntries as m, agentManifestEntry as n, buildStoredRouteManifestFromContext as o, discoverTriggerAttachments as p, discoverModuleFileEntries as q, agentRouteFromKey as r, buildWebhookBindingsByRoute as s, agentKeyForAttachment as t, countAgentCredentials as u, importTriggerAttachments as v, schemaToJson as w, pollGroupId as x, importWorkflowDefinition as y, workflowRouteFromKey as z };
|
|
20710
22634
|
|
|
20711
|
-
//# sourceMappingURL=dist-
|
|
22635
|
+
//# sourceMappingURL=dist-C0_71CpG.mjs.map
|