@keystrokehq/cli 0.1.38 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-DkLbeW8l.mjs → dist-BOhrc_Nv.mjs} +198 -561
- package/dist/dist-BOhrc_Nv.mjs.map +1 -0
- package/dist/{dist-B6z1wti6.mjs → dist-D-cLLjHv.mjs} +87 -2017
- package/dist/dist-D-cLLjHv.mjs.map +1 -0
- package/dist/{dist-GSI9JDuz.mjs → dist-DGKF3FGu.mjs} +31 -265
- package/dist/dist-DGKF3FGu.mjs.map +1 -0
- package/dist/dist-DMuIdus5.mjs +3 -0
- package/dist/{dist-gAvgHBlr.mjs → dist-Re6HHSqz.mjs} +2 -2
- package/dist/{dist-gAvgHBlr.mjs.map → dist-Re6HHSqz.mjs.map} +1 -1
- package/dist/index.mjs +177 -463
- package/dist/index.mjs.map +1 -1
- package/dist/{maybe-auto-update-Dv4MJvWb.mjs → maybe-auto-update-q5MthdI8.mjs} +2 -2
- package/dist/{maybe-auto-update-Dv4MJvWb.mjs.map → maybe-auto-update-q5MthdI8.mjs.map} +1 -1
- package/dist/skills-bundle/_AGENTS.mcp.md +5 -9
- package/dist/skills-bundle/_AGENTS.md +112 -243
- package/dist/skills-bundle/skills/keystroke-actions/SKILL.md +160 -0
- package/dist/skills-bundle/skills/keystroke-actions/references/catalog-and-imports.md +71 -0
- package/dist/skills-bundle/skills/keystroke-agents/SKILL.md +115 -0
- package/dist/skills-bundle/skills/keystroke-agents/references/models.md +23 -0
- package/dist/skills-bundle/skills/keystroke-agents/references/tools-mcp-codemode.md +73 -0
- package/dist/skills-bundle/skills/keystroke-agents/references/workflows-and-testing.md +26 -0
- package/dist/skills-bundle/skills/keystroke-apps/SKILL.md +151 -0
- package/dist/skills-bundle/skills/keystroke-apps/references/cli-and-catalog.md +104 -0
- package/dist/skills-bundle/skills/keystroke-channels/SKILL.md +66 -0
- package/dist/skills-bundle/skills/keystroke-channels/references/slack-setup.md +41 -0
- package/dist/skills-bundle/skills/keystroke-cli/SKILL.md +93 -0
- package/dist/skills-bundle/skills/keystroke-deploy/SKILL.md +93 -0
- package/dist/skills-bundle/skills/keystroke-deploy/references/build-and-full-deploy.md +30 -0
- package/dist/skills-bundle/skills/keystroke-deploy/references/filtered-deploy.md +50 -0
- package/dist/skills-bundle/skills/keystroke-deploy/references/wip-ignore.md +35 -0
- package/dist/skills-bundle/skills/keystroke-files/SKILL.md +43 -0
- package/dist/skills-bundle/skills/keystroke-skills/SKILL.md +42 -0
- package/dist/skills-bundle/skills/keystroke-triggers/SKILL.md +143 -0
- package/dist/skills-bundle/skills/keystroke-workflows/SKILL.md +78 -0
- package/dist/skills-bundle/skills/keystroke-workflows/references/authoring.md +168 -0
- package/dist/skills-bundle/skills/keystroke-workflows/references/testing.md +138 -0
- package/dist/templates/hello-world/.env.example +4 -0
- package/dist/templates/hello-world/README.md +3 -4
- package/dist/templates/hello-world/package.json +0 -1
- package/dist/templates/hello-world/src/actions/greet.ts +0 -1
- package/dist/templates/hello-world/src/agents/hello.ts +0 -2
- package/dist/templates/hello-world/src/workflows/greeting.ts +0 -1
- package/dist/{version-CiFlKPyE.mjs → version-DcR3O1UD.mjs} +3 -2
- package/dist/version-DcR3O1UD.mjs.map +1 -0
- package/package.json +5 -5
- package/dist/dist-B6z1wti6.mjs.map +0 -1
- package/dist/dist-CjWXZCN7.mjs +0 -3
- package/dist/dist-DkLbeW8l.mjs.map +0 -1
- package/dist/dist-GSI9JDuz.mjs.map +0 -1
- package/dist/version-CiFlKPyE.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 { Bn as credentialInputSchema, Cr as toJSONSchema, Gn as normalizeCredentialList, Gt as PromptResponseSchema, Kt as PublicModelsResponseSchema, W as DEFAULT_CLOUD_PLATFORM_ORIGIN, Yt as ROUTE_MANIFEST_REL_PATH, cr as custom, dr as literal, gr as preprocess, lr as discriminatedUnion, mr as object, nr as _enum, or as array, pr as number, rr as _function, sr as boolean$1, tr as ZodType, vr as string, yr as union } from "./dist-BOhrc_Nv.mjs";
|
|
4
|
+
import "./dist-Re6HHSqz.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,7 +24,6 @@ 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";
|
|
28
27
|
import { boolean, doublePrecision, index, integer, jsonb, pgEnum, pgTable, primaryKey, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
|
|
29
28
|
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";
|
|
30
29
|
import { sql } from "drizzle-orm";
|
|
@@ -338,1527 +337,6 @@ function hasAppLayout(dir) {
|
|
|
338
337
|
const dist = join(dir, "dist");
|
|
339
338
|
return existsSync(join(src, "agents")) || existsSync(join(src, "skills")) || existsSync(join(src, "files")) || existsSync(join(dist, "agents")) || existsSync(join(dist, ".keystroke", "assets.mjs"));
|
|
340
339
|
}
|
|
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
|
-
}
|
|
1862
340
|
const replaceEditSchema = object({
|
|
1863
341
|
oldText: string().describe("Exact text to replace. Must be unique in the file and must not overlap other edits in this call."),
|
|
1864
342
|
newText: string().describe("Replacement text")
|
|
@@ -3191,49 +1669,25 @@ const workflows = pgTable("workflows", {
|
|
|
3191
1669
|
id: text("id").primaryKey(),
|
|
3192
1670
|
projectId: text("project_id").notNull(),
|
|
3193
1671
|
slug: text("slug").notNull(),
|
|
3194
|
-
name: text("name")
|
|
3195
|
-
description: text("description")
|
|
1672
|
+
name: text("name"),
|
|
1673
|
+
description: text("description"),
|
|
3196
1674
|
moduleFile: text("module_file").notNull(),
|
|
3197
1675
|
subscribable: integer("subscribable").notNull(),
|
|
3198
1676
|
registeredAt: timestamp("registered_at", { withTimezone: true }).notNull(),
|
|
3199
1677
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
|
|
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 })
|
|
1678
|
+
deletedAt: timestamp("deleted_at", { withTimezone: true })
|
|
3213
1679
|
}, (table) => [uniqueIndex("workflows_project_id_slug_idx").on(table.projectId, table.slug)]);
|
|
3214
1680
|
const workflowsSqlite = sqliteTable("workflows", {
|
|
3215
1681
|
id: text$1("id").primaryKey(),
|
|
3216
1682
|
projectId: text$1("project_id").notNull(),
|
|
3217
1683
|
slug: text$1("slug").notNull(),
|
|
3218
|
-
name: text$1("name")
|
|
3219
|
-
description: text$1("description")
|
|
1684
|
+
name: text$1("name"),
|
|
1685
|
+
description: text$1("description"),
|
|
3220
1686
|
moduleFile: text$1("module_file").notNull(),
|
|
3221
1687
|
subscribable: integer$1("subscribable", { mode: "boolean" }).notNull(),
|
|
3222
1688
|
registeredAt: integer$1("registered_at", { mode: "timestamp_ms" }).notNull(),
|
|
3223
1689
|
updatedAt: integer$1("updated_at", { mode: "timestamp_ms" }).notNull(),
|
|
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" })
|
|
1690
|
+
deletedAt: integer$1("deleted_at", { mode: "timestamp_ms" })
|
|
3237
1691
|
}, (table) => [uniqueIndex$1("workflows_project_id_slug_idx").on(table.projectId, table.slug)]);
|
|
3238
1692
|
const resourceActivity = pgTable("resource_activity", {
|
|
3239
1693
|
id: text("id").primaryKey(),
|
|
@@ -3377,29 +1831,15 @@ object({
|
|
|
3377
1831
|
//#endregion
|
|
3378
1832
|
//#region ../../packages/action/dist/index.mjs
|
|
3379
1833
|
const zodSchema$3 = custom((v) => v instanceof ZodType, "must be a Zod schema");
|
|
3380
|
-
|
|
3381
|
-
const actionCoreSchema = object({
|
|
1834
|
+
object({
|
|
3382
1835
|
slug: string().trim().min(1),
|
|
3383
|
-
name:
|
|
3384
|
-
description:
|
|
1836
|
+
name: string().optional(),
|
|
1837
|
+
description: string().optional(),
|
|
3385
1838
|
input: zodSchema$3,
|
|
3386
1839
|
output: zodSchema$3,
|
|
3387
1840
|
credentials: array(credentialInputSchema).optional(),
|
|
3388
1841
|
run: _function()
|
|
3389
1842
|
});
|
|
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
|
-
}
|
|
3403
1843
|
new AsyncLocalStorage();
|
|
3404
1844
|
new AsyncLocalStorage();
|
|
3405
1845
|
new AbortController().signal;
|
|
@@ -20536,41 +18976,6 @@ object({
|
|
|
20536
18976
|
limit: number().int().min(1).max(50).optional(),
|
|
20537
18977
|
full: boolean$1().describe("When true, return complete archive note files for archive hits, ignored for session hits (search).").optional()
|
|
20538
18978
|
});
|
|
20539
|
-
//#endregion
|
|
20540
|
-
//#region ../../packages/workflow/dist/index.mjs
|
|
20541
|
-
const zodSchema$2 = custom((v) => v instanceof ZodType, "must be a Zod schema");
|
|
20542
|
-
/** Runtime validation for an unbranded workflow definition. */
|
|
20543
|
-
const workflowCoreSchema = object({
|
|
20544
|
-
slug: string().trim().min(1),
|
|
20545
|
-
name: requiredDisplayTextSchema,
|
|
20546
|
-
description: requiredDisplayTextSchema,
|
|
20547
|
-
subscription: object({ mode: _enum(["system", "subscribable"]).optional() }).optional(),
|
|
20548
|
-
input: zodSchema$2,
|
|
20549
|
-
output: zodSchema$2,
|
|
20550
|
-
run: _function()
|
|
20551
|
-
});
|
|
20552
|
-
const WORKFLOW$1 = Symbol.for("keystroke.workflow");
|
|
20553
|
-
/**
|
|
20554
|
-
* Validates brand + shape via `workflowCoreSchema` so discovery and guards
|
|
20555
|
-
* reject malformed definitions.
|
|
20556
|
-
*/
|
|
20557
|
-
function isWorkflow(value) {
|
|
20558
|
-
if (typeof value !== "object" || value === null) return false;
|
|
20559
|
-
if (!(WORKFLOW$1 in value) || value[WORKFLOW$1] !== true) return false;
|
|
20560
|
-
return workflowCoreSchema.safeParse(value).success;
|
|
20561
|
-
}
|
|
20562
|
-
new AsyncLocalStorage();
|
|
20563
|
-
const storage = new AsyncLocalStorage();
|
|
20564
|
-
registerWorkflowRunGetter(() => {
|
|
20565
|
-
const store = storage.getStore();
|
|
20566
|
-
if (!store) return;
|
|
20567
|
-
return {
|
|
20568
|
-
actionRunner: store.actionRunner,
|
|
20569
|
-
agentRunner: store.agentRunner,
|
|
20570
|
-
llmRunner: store.llmRunner,
|
|
20571
|
-
workflowRunner: store.workflowRunner
|
|
20572
|
-
};
|
|
20573
|
-
});
|
|
20574
18979
|
new AsyncLocalStorage();
|
|
20575
18980
|
[
|
|
20576
18981
|
"You are an expert chat title generator.",
|
|
@@ -20596,7 +19001,6 @@ new AsyncLocalStorage();
|
|
|
20596
19001
|
"User: Write a cold email sequence for fundraising from YC alums",
|
|
20597
19002
|
"→ YC Alumni Fundraising Cold Email Sequence"
|
|
20598
19003
|
].join("\n");
|
|
20599
|
-
object({ message: string().describe("Task or question for the subagent") });
|
|
20600
19004
|
const STATIC_MODEL_IDS = new Set([
|
|
20601
19005
|
"alibaba/qwen-3-14b",
|
|
20602
19006
|
"alibaba/qwen-3-235b",
|
|
@@ -21331,6 +19735,40 @@ function parseCronExpression(cronExpression) {
|
|
|
21331
19735
|
});
|
|
21332
19736
|
}
|
|
21333
19737
|
//#endregion
|
|
19738
|
+
//#region ../../packages/workflow/dist/index.mjs
|
|
19739
|
+
const zodSchema$2 = custom((v) => v instanceof ZodType, "must be a Zod schema");
|
|
19740
|
+
/** Runtime validation for an unbranded workflow definition. */
|
|
19741
|
+
const workflowCoreSchema = object({
|
|
19742
|
+
slug: string().trim().min(1),
|
|
19743
|
+
name: string().optional(),
|
|
19744
|
+
description: string().optional(),
|
|
19745
|
+
subscription: object({ mode: _enum(["system", "subscribable"]).optional() }).optional(),
|
|
19746
|
+
input: zodSchema$2,
|
|
19747
|
+
output: zodSchema$2,
|
|
19748
|
+
run: _function()
|
|
19749
|
+
});
|
|
19750
|
+
const WORKFLOW$1 = Symbol.for("keystroke.workflow");
|
|
19751
|
+
/**
|
|
19752
|
+
* Validates brand + shape via `workflowCoreSchema` so discovery and guards
|
|
19753
|
+
* reject malformed definitions.
|
|
19754
|
+
*/
|
|
19755
|
+
function isWorkflow(value) {
|
|
19756
|
+
if (typeof value !== "object" || value === null) return false;
|
|
19757
|
+
if (!(WORKFLOW$1 in value) || value[WORKFLOW$1] !== true) return false;
|
|
19758
|
+
return workflowCoreSchema.safeParse(value).success;
|
|
19759
|
+
}
|
|
19760
|
+
const storage = new AsyncLocalStorage();
|
|
19761
|
+
registerWorkflowRunGetter(() => {
|
|
19762
|
+
const store = storage.getStore();
|
|
19763
|
+
if (!store) return;
|
|
19764
|
+
return {
|
|
19765
|
+
actionRunner: store.actionRunner,
|
|
19766
|
+
agentRunner: store.agentRunner,
|
|
19767
|
+
llmRunner: store.llmRunner,
|
|
19768
|
+
workflowRunner: store.workflowRunner
|
|
19769
|
+
};
|
|
19770
|
+
});
|
|
19771
|
+
//#endregion
|
|
21334
19772
|
//#region ../../packages/trigger/dist/index.mjs
|
|
21335
19773
|
/**
|
|
21336
19774
|
* Validates and brands a cron expression. Accepts anything the `cron-schedule`
|
|
@@ -21369,12 +19807,11 @@ function normalizeLegacySlugFields$1(value) {
|
|
|
21369
19807
|
return next;
|
|
21370
19808
|
}
|
|
21371
19809
|
const slugField$1 = string().trim().min(1);
|
|
21372
|
-
const
|
|
21373
|
-
const optionalTextField$1 = string().trim().min(1).optional();
|
|
19810
|
+
const optionalTextField$1 = string().optional();
|
|
21374
19811
|
const baseSourceShape$1 = {
|
|
21375
19812
|
slug: slugField$1,
|
|
21376
|
-
name:
|
|
21377
|
-
description:
|
|
19813
|
+
name: optionalTextField$1,
|
|
19814
|
+
description: optionalTextField$1,
|
|
21378
19815
|
attach: _function()
|
|
21379
19816
|
};
|
|
21380
19817
|
/** Runtime validation for a trigger source (webhook | cron | poll). */
|
|
@@ -21449,9 +19886,11 @@ function attachmentSlugFromRecord(attachment) {
|
|
|
21449
19886
|
return slug;
|
|
21450
19887
|
}
|
|
21451
19888
|
function triggerMetaFromAttachment(attachment) {
|
|
19889
|
+
const name = attachment.name ?? attachment.source.name;
|
|
19890
|
+
const description = attachment.description ?? attachment.source.description;
|
|
21452
19891
|
return {
|
|
21453
|
-
name
|
|
21454
|
-
description
|
|
19892
|
+
...name !== void 0 ? { name } : {},
|
|
19893
|
+
...description !== void 0 ? { description } : {}
|
|
21455
19894
|
};
|
|
21456
19895
|
}
|
|
21457
19896
|
/**
|
|
@@ -21480,8 +19919,6 @@ function isManifestAgent(value) {
|
|
|
21480
19919
|
}
|
|
21481
19920
|
function validateManifestAgent(value, filePath) {
|
|
21482
19921
|
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`);
|
|
21485
19922
|
return value;
|
|
21486
19923
|
}
|
|
21487
19924
|
function normalizeWebhookEndpoint(endpoint) {
|
|
@@ -21540,30 +19977,6 @@ function actionSlug(tool) {
|
|
|
21540
19977
|
const record = tool;
|
|
21541
19978
|
return typeof record.slug === "string" ? record.slug : void 0;
|
|
21542
19979
|
}
|
|
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
|
-
}
|
|
21567
19980
|
/** App-kind slugs required by the agent's tools (same as `credential_instances.app_slug`). */
|
|
21568
19981
|
function collectAgentAppSlugs(agent) {
|
|
21569
19982
|
const slugs = /* @__PURE__ */ new Set();
|
|
@@ -21588,7 +20001,6 @@ function countAgentCredentials(agent) {
|
|
|
21588
20001
|
}
|
|
21589
20002
|
/** Single source of truth for the `kind: "agent"` route-manifest entry shape. */
|
|
21590
20003
|
function agentManifestEntry(agent, options) {
|
|
21591
|
-
const toolRequirements = collectAgentToolRequirements(agent);
|
|
21592
20004
|
return {
|
|
21593
20005
|
kind: "agent",
|
|
21594
20006
|
slug: agent.slug,
|
|
@@ -21600,69 +20012,7 @@ function agentManifestEntry(agent, options) {
|
|
|
21600
20012
|
toolCount: agent.tools?.length ?? 0,
|
|
21601
20013
|
credentialCount: countAgentCredentials(agent),
|
|
21602
20014
|
appSlugs: collectAgentAppSlugs(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 } : {}
|
|
20015
|
+
toolSlugs: collectAgentToolSlugs(agent)
|
|
21666
20016
|
};
|
|
21667
20017
|
}
|
|
21668
20018
|
function pollGroupId(discovered) {
|
|
@@ -21802,7 +20152,7 @@ function validateProjectModules(input) {
|
|
|
21802
20152
|
]);
|
|
21803
20153
|
}
|
|
21804
20154
|
const SKIP_DIRS = new Set([".git", "node_modules"]);
|
|
21805
|
-
function toPosix$
|
|
20155
|
+
function toPosix$1(path) {
|
|
21806
20156
|
return path.split(sep).join("/");
|
|
21807
20157
|
}
|
|
21808
20158
|
function parseSkillFrontmatter(raw) {
|
|
@@ -21823,26 +20173,23 @@ function parseSkillFrontmatter(raw) {
|
|
|
21823
20173
|
return out;
|
|
21824
20174
|
}
|
|
21825
20175
|
function walkSkillFiles(root, dir, out) {
|
|
21826
|
-
for (const
|
|
21827
|
-
const absolute = join(dir,
|
|
20176
|
+
for (const name of readdirSync(dir).sort()) {
|
|
20177
|
+
const absolute = join(dir, name);
|
|
21828
20178
|
const stats = statSync(absolute);
|
|
21829
20179
|
if (stats.isDirectory()) {
|
|
21830
|
-
if (SKIP_DIRS.has(
|
|
20180
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
21831
20181
|
walkSkillFiles(root, absolute, out);
|
|
21832
20182
|
continue;
|
|
21833
20183
|
}
|
|
21834
|
-
if (!stats.isFile() ||
|
|
21835
|
-
const moduleFile = toPosix$
|
|
21836
|
-
const slug = toPosix$
|
|
20184
|
+
if (!stats.isFile() || name !== "SKILL.md") continue;
|
|
20185
|
+
const moduleFile = toPosix$1(relative(root, absolute));
|
|
20186
|
+
const slug = toPosix$1(relative(join(root, "src", "skills"), absolute)).replace(/\/?SKILL\.md$/, "");
|
|
21837
20187
|
if (!slug) continue;
|
|
21838
20188
|
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`);
|
|
21842
20189
|
out.push({
|
|
21843
20190
|
slug,
|
|
21844
|
-
name,
|
|
21845
|
-
description,
|
|
20191
|
+
name: frontmatter.name,
|
|
20192
|
+
description: frontmatter.description,
|
|
21846
20193
|
moduleFile
|
|
21847
20194
|
});
|
|
21848
20195
|
}
|
|
@@ -21879,21 +20226,7 @@ function serializeRouteManifest(manifest) {
|
|
|
21879
20226
|
toolCount: entry.toolCount,
|
|
21880
20227
|
credentialCount: entry.credentialCount,
|
|
21881
20228
|
appSlugs: entry.appSlugs,
|
|
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 } : {}
|
|
20229
|
+
toolSlugs: entry.toolSlugs
|
|
21897
20230
|
});
|
|
21898
20231
|
break;
|
|
21899
20232
|
case "workflow":
|
|
@@ -21904,12 +20237,7 @@ function serializeRouteManifest(manifest) {
|
|
|
21904
20237
|
description: entry.description,
|
|
21905
20238
|
subscribable: entry.subscribable,
|
|
21906
20239
|
moduleFile: entry.moduleFile,
|
|
21907
|
-
requestSchema: schemaToJson(entry.request)
|
|
21908
|
-
responseSchema: schemaToJson(entry.response),
|
|
21909
|
-
...entry.flowGraph ? {
|
|
21910
|
-
flowGraph: entry.flowGraph,
|
|
21911
|
-
flowGraphSourceHash: entry.flowGraphSourceHash
|
|
21912
|
-
} : {}
|
|
20240
|
+
requestSchema: schemaToJson(entry.request)
|
|
21913
20241
|
});
|
|
21914
20242
|
break;
|
|
21915
20243
|
case "trigger-webhook":
|
|
@@ -21918,7 +20246,6 @@ function serializeRouteManifest(manifest) {
|
|
|
21918
20246
|
endpoint: entry.endpoint,
|
|
21919
20247
|
attachmentIds: entry.attachmentIds,
|
|
21920
20248
|
moduleFile: entry.moduleFile,
|
|
21921
|
-
...entry.sourceHash ? { sourceHash: entry.sourceHash } : {},
|
|
21922
20249
|
attachmentSchemas: Object.fromEntries(Object.entries(entry.attachmentSchemas).map(([attachmentSlug, schemas]) => [attachmentSlug, {
|
|
21923
20250
|
requestSchema: schemaToJson(schemas.request),
|
|
21924
20251
|
...schemas.filter ? { filterSchema: schemaToJson(schemas.filter) } : {}
|
|
@@ -21932,10 +20259,9 @@ function serializeRouteManifest(manifest) {
|
|
|
21932
20259
|
attachmentId: entry.attachmentId,
|
|
21933
20260
|
attachmentIds: entry.attachmentIds,
|
|
21934
20261
|
moduleFile: entry.moduleFile,
|
|
21935
|
-
...entry.sourceHash ? { sourceHash: entry.sourceHash } : {},
|
|
21936
20262
|
schedule: entry.schedule,
|
|
21937
|
-
name: entry.name,
|
|
21938
|
-
description: entry.description
|
|
20263
|
+
...entry.name !== void 0 ? { name: entry.name } : {},
|
|
20264
|
+
...entry.description !== void 0 ? { description: entry.description } : {}
|
|
21939
20265
|
});
|
|
21940
20266
|
break;
|
|
21941
20267
|
case "trigger-poll-group":
|
|
@@ -21986,8 +20312,6 @@ function isManifestWorkflow(value) {
|
|
|
21986
20312
|
}
|
|
21987
20313
|
function validateManifestWorkflow(value, filePath) {
|
|
21988
20314
|
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`);
|
|
21991
20315
|
return value;
|
|
21992
20316
|
}
|
|
21993
20317
|
const TRIGGER_ATTACHMENT = Symbol.for("keystroke.triggerAttachment");
|
|
@@ -22006,12 +20330,11 @@ function normalizeLegacySlugFields(value) {
|
|
|
22006
20330
|
return next;
|
|
22007
20331
|
}
|
|
22008
20332
|
const slugField = string().trim().min(1);
|
|
22009
|
-
const
|
|
22010
|
-
const optionalTextField = string().trim().min(1).optional();
|
|
20333
|
+
const optionalTextField = string().optional();
|
|
22011
20334
|
const baseSourceShape = {
|
|
22012
20335
|
slug: slugField,
|
|
22013
|
-
name:
|
|
22014
|
-
description:
|
|
20336
|
+
name: optionalTextField,
|
|
20337
|
+
description: optionalTextField,
|
|
22015
20338
|
attach: _function()
|
|
22016
20339
|
};
|
|
22017
20340
|
const triggerSourceSchema = preprocess(normalizeLegacySlugFields, discriminatedUnion("kind", [
|
|
@@ -22055,18 +20378,13 @@ const triggerAttachmentCoreSchema = preprocess(normalizeLegacySlugFields, discri
|
|
|
22055
20378
|
agent: agentSchema,
|
|
22056
20379
|
prompt: promptSchema
|
|
22057
20380
|
})]));
|
|
22058
|
-
function
|
|
22059
|
-
|
|
22060
|
-
|
|
22061
|
-
|
|
22062
|
-
return issues.map((issue) => {
|
|
22063
|
-
return `${issue.path.length > 0 ? `${issue.path.join(".")}: ` : ""}${issue.message}`;
|
|
22064
|
-
}).join("; ");
|
|
20381
|
+
function isManifestTriggerAttachment(value) {
|
|
20382
|
+
if (typeof value !== "object" || value === null) return false;
|
|
20383
|
+
if (!(TRIGGER_ATTACHMENT in value) || value[TRIGGER_ATTACHMENT] !== true) return false;
|
|
20384
|
+
return triggerAttachmentCoreSchema.safeParse(value).success;
|
|
22065
20385
|
}
|
|
22066
20386
|
function validateManifestTriggerAttachment(value, filePath) {
|
|
22067
|
-
if (!
|
|
22068
|
-
const result = triggerAttachmentCoreSchema.safeParse(value);
|
|
22069
|
-
if (!result.success) throw new Error(`${filePath}: invalid trigger attachment: ${formatIssues(result.error.issues)}`);
|
|
20387
|
+
if (!isManifestTriggerAttachment(value)) throw new Error(`${filePath} must default-export a trigger attachment from .attach({ workflow }) or .attach({ agent, prompt })`);
|
|
22070
20388
|
return value;
|
|
22071
20389
|
}
|
|
22072
20390
|
function shouldDiscoverTriggerFile(triggersDir, filePath) {
|
|
@@ -22182,181 +20500,11 @@ async function discoverWorkflows(workflowsDir, options) {
|
|
|
22182
20500
|
}
|
|
22183
20501
|
return workflows;
|
|
22184
20502
|
}
|
|
22185
|
-
function toPosix$1(path) {
|
|
22186
|
-
return path.split("\\").join("/");
|
|
22187
|
-
}
|
|
22188
|
-
function buildSymbolImportMap(source, fileName) {
|
|
22189
|
-
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
22190
|
-
const map = /* @__PURE__ */ new Map();
|
|
22191
|
-
for (const statement of sourceFile.statements) {
|
|
22192
|
-
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
22193
|
-
const specifier = statement.moduleSpecifier.text;
|
|
22194
|
-
const clause = statement.importClause;
|
|
22195
|
-
if (!clause) continue;
|
|
22196
|
-
if (clause.name) map.set(clause.name.text, {
|
|
22197
|
-
specifier,
|
|
22198
|
-
exportName: "default"
|
|
22199
|
-
});
|
|
22200
|
-
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) for (const element of clause.namedBindings.elements) {
|
|
22201
|
-
const exportName = (element.propertyName ?? element.name).text;
|
|
22202
|
-
map.set(element.name.text, {
|
|
22203
|
-
specifier,
|
|
22204
|
-
exportName
|
|
22205
|
-
});
|
|
22206
|
-
}
|
|
22207
|
-
}
|
|
22208
|
-
return map;
|
|
22209
|
-
}
|
|
22210
|
-
function resolveRelativeModuleFile(projectRoot, workflowModuleFile, specifier) {
|
|
22211
|
-
if (!specifier.startsWith(".")) return;
|
|
22212
|
-
const workflowDir = dirname(join(projectRoot, workflowModuleFile));
|
|
22213
|
-
const candidates = [
|
|
22214
|
-
join(workflowDir, specifier),
|
|
22215
|
-
`${join(workflowDir, specifier)}.ts`,
|
|
22216
|
-
join(workflowDir, `${specifier}.ts`),
|
|
22217
|
-
join(workflowDir, specifier, "index.ts")
|
|
22218
|
-
];
|
|
22219
|
-
for (const candidate of candidates) if (existsSync(candidate)) return toPosix$1(relative(projectRoot, candidate));
|
|
22220
|
-
}
|
|
22221
|
-
function toResolvedStepSlug(entry) {
|
|
22222
|
-
return {
|
|
22223
|
-
slug: entry.slug,
|
|
22224
|
-
name: entry.name,
|
|
22225
|
-
description: entry.description
|
|
22226
|
-
};
|
|
22227
|
-
}
|
|
22228
|
-
/** Build a deploy-time slug resolver from a workflow file's imports + definition registry. */
|
|
22229
|
-
function createResolveSlugFn(projectRoot, workflowModuleFile, workflowSource, registryByModuleFile, integrationRegistry) {
|
|
22230
|
-
const importMap = buildSymbolImportMap(workflowSource, workflowModuleFile);
|
|
22231
|
-
return (importName) => {
|
|
22232
|
-
const imported = importMap.get(importName);
|
|
22233
|
-
if (!imported) return;
|
|
22234
|
-
const { specifier, exportName } = imported;
|
|
22235
|
-
if (specifier.startsWith(".")) {
|
|
22236
|
-
const moduleFile = resolveRelativeModuleFile(projectRoot, workflowModuleFile, specifier);
|
|
22237
|
-
if (!moduleFile) return;
|
|
22238
|
-
const entry = registryByModuleFile.get(moduleFile);
|
|
22239
|
-
return entry ? toResolvedStepSlug(entry) : void 0;
|
|
22240
|
-
}
|
|
22241
|
-
const entry = integrationRegistry?.get(`${specifier}#${exportName}`);
|
|
22242
|
-
return entry ? toResolvedStepSlug(entry) : void 0;
|
|
22243
|
-
};
|
|
22244
|
-
}
|
|
22245
|
-
function buildSlugRegistry(entries) {
|
|
22246
|
-
return new Map(entries.map((entry) => [entry.moduleFile, entry]));
|
|
22247
|
-
}
|
|
22248
|
-
/** The framework package — never treated as an integration action source. */
|
|
22249
|
-
const FRAMEWORK_PACKAGE = "@keystrokehq/keystroke";
|
|
22250
|
-
/**
|
|
22251
|
-
* Map an integration import specifier to its app slug.
|
|
22252
|
-
*
|
|
22253
|
-
* `@keystrokehq/slackbot/actions` → `slackbot`, `@keystrokehq/github` → `github`.
|
|
22254
|
-
* Returns `undefined` for the framework package and any non-`@keystrokehq` scope.
|
|
22255
|
-
*/
|
|
22256
|
-
function integrationAppSlugFromSpecifier(specifier) {
|
|
22257
|
-
if (specifier === FRAMEWORK_PACKAGE || specifier.startsWith(`${FRAMEWORK_PACKAGE}/`)) return;
|
|
22258
|
-
return /^@keystrokehq\/([^/]+)(?:\/.*)?$/.exec(specifier)?.[1];
|
|
22259
|
-
}
|
|
22260
|
-
/**
|
|
22261
|
-
* The integration action subpath convention (`@keystrokehq/<app>/actions`). We only
|
|
22262
|
-
* import these — the real authoring convention — to avoid loading whole packages.
|
|
22263
|
-
*/
|
|
22264
|
-
function isIntegrationActionsSpecifier(specifier) {
|
|
22265
|
-
return /^@keystrokehq\/[^/]+\/actions$/.test(specifier) && specifier !== FRAMEWORK_PACKAGE;
|
|
22266
|
-
}
|
|
22267
|
-
/** Map each integration `/actions` specifier imported by a file to the exports it uses. */
|
|
22268
|
-
function collectIntegrationImports(source, fileName) {
|
|
22269
|
-
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
22270
|
-
const bySpecifier = /* @__PURE__ */ new Map();
|
|
22271
|
-
for (const statement of sourceFile.statements) {
|
|
22272
|
-
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
22273
|
-
const specifier = statement.moduleSpecifier.text;
|
|
22274
|
-
if (!isIntegrationActionsSpecifier(specifier)) continue;
|
|
22275
|
-
const usage = bySpecifier.get(specifier) ?? {
|
|
22276
|
-
names: /* @__PURE__ */ new Set(),
|
|
22277
|
-
importsAll: false
|
|
22278
|
-
};
|
|
22279
|
-
const clause = statement.importClause;
|
|
22280
|
-
if (clause?.name) usage.names.add("default");
|
|
22281
|
-
const bindings = clause?.namedBindings;
|
|
22282
|
-
if (bindings) if (ts.isNamespaceImport(bindings)) usage.importsAll = true;
|
|
22283
|
-
else for (const element of bindings.elements) usage.names.add(element.propertyName?.text ?? element.name.text);
|
|
22284
|
-
bySpecifier.set(specifier, usage);
|
|
22285
|
-
}
|
|
22286
|
-
return bySpecifier;
|
|
22287
|
-
}
|
|
22288
|
-
/**
|
|
22289
|
-
* Discover actions imported from integration packages (`@keystrokehq/<app>/actions`)
|
|
22290
|
-
* by the project's workflow/agent source files.
|
|
22291
|
-
*
|
|
22292
|
-
* Each `/actions` subpath is resolved from the project's `node_modules`, dynamically
|
|
22293
|
-
* imported, and its action exports enumerated. Best-effort: a missing or broken
|
|
22294
|
-
* integration package never fails the manifest build — it is logged and skipped.
|
|
22295
|
-
*/
|
|
22296
|
-
async function discoverIntegrationActions(projectRoot, sourceFiles, options) {
|
|
22297
|
-
const usageBySpecifier = /* @__PURE__ */ new Map();
|
|
22298
|
-
for (const sourceFile of sourceFiles) {
|
|
22299
|
-
let source;
|
|
22300
|
-
try {
|
|
22301
|
-
source = readFileSync(join(projectRoot, sourceFile), "utf8");
|
|
22302
|
-
} catch {
|
|
22303
|
-
continue;
|
|
22304
|
-
}
|
|
22305
|
-
for (const [specifier, usage] of collectIntegrationImports(source, sourceFile)) {
|
|
22306
|
-
const existing = usageBySpecifier.get(specifier) ?? {
|
|
22307
|
-
names: /* @__PURE__ */ new Set(),
|
|
22308
|
-
importsAll: false
|
|
22309
|
-
};
|
|
22310
|
-
for (const name of usage.names) existing.names.add(name);
|
|
22311
|
-
existing.importsAll = existing.importsAll || usage.importsAll;
|
|
22312
|
-
usageBySpecifier.set(specifier, existing);
|
|
22313
|
-
}
|
|
22314
|
-
}
|
|
22315
|
-
const entries = [];
|
|
22316
|
-
const registry = /* @__PURE__ */ new Map();
|
|
22317
|
-
const seenSlugs = new Set(options?.existingSlugs);
|
|
22318
|
-
const require = createRequire(join(projectRoot, "package.json"));
|
|
22319
|
-
for (const [specifier, usage] of usageBySpecifier) {
|
|
22320
|
-
const appSlug = integrationAppSlugFromSpecifier(specifier);
|
|
22321
|
-
if (!appSlug) continue;
|
|
22322
|
-
try {
|
|
22323
|
-
const resolvedPath = require.resolve(specifier);
|
|
22324
|
-
const href = pathToFileURL(resolvedPath).href;
|
|
22325
|
-
const mod = await runWithAppRoot(resolveAppRoot(resolvedPath), async () => await import(href));
|
|
22326
|
-
for (const [exportName, value] of Object.entries(mod)) {
|
|
22327
|
-
if (!isAction(value)) continue;
|
|
22328
|
-
if (!usage.importsAll && !usage.names.has(exportName)) continue;
|
|
22329
|
-
const definition = value;
|
|
22330
|
-
registry.set(`${specifier}#${exportName}`, {
|
|
22331
|
-
slug: definition.slug,
|
|
22332
|
-
name: definition.name,
|
|
22333
|
-
description: definition.description
|
|
22334
|
-
});
|
|
22335
|
-
if (seenSlugs.has(definition.slug)) continue;
|
|
22336
|
-
seenSlugs.add(definition.slug);
|
|
22337
|
-
entries.push(actionManifestEntry(definition, {
|
|
22338
|
-
moduleFile: specifier,
|
|
22339
|
-
appSlug,
|
|
22340
|
-
inputSchema: schemaToJson(definition.input),
|
|
22341
|
-
outputSchema: schemaToJson(definition.output)
|
|
22342
|
-
}));
|
|
22343
|
-
}
|
|
22344
|
-
} catch (error) {
|
|
22345
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
22346
|
-
console.warn(`[manifest] skipped integration actions for ${specifier}: ${message}`);
|
|
22347
|
-
}
|
|
22348
|
-
}
|
|
22349
|
-
return {
|
|
22350
|
-
entries,
|
|
22351
|
-
registry
|
|
22352
|
-
};
|
|
22353
|
-
}
|
|
22354
20503
|
function resolveDistModuleDirs(projectRoot) {
|
|
22355
20504
|
const distBase = join(projectRoot, "dist");
|
|
22356
20505
|
if (!existsSync(distBase)) throw new Error(`Build output missing at ${distBase}. Run keystroke build before emitting the route manifest.`);
|
|
22357
20506
|
return {
|
|
22358
20507
|
agentsDir: join(distBase, "agents"),
|
|
22359
|
-
actionsDir: join(distBase, "actions"),
|
|
22360
20508
|
workflowsDir: join(distBase, "workflows"),
|
|
22361
20509
|
triggersDir: join(distBase, "triggers")
|
|
22362
20510
|
};
|
|
@@ -22372,33 +20520,6 @@ function hashFileContents(absPath) {
|
|
|
22372
20520
|
}
|
|
22373
20521
|
}
|
|
22374
20522
|
/**
|
|
22375
|
-
* Run the deploy-time AST producer over a workflow's source file to attach its
|
|
22376
|
-
* control-flow skeleton + a content hash (for staleness). Best-effort: skips
|
|
22377
|
-
* when the resolved moduleFile is not a readable `.ts` source (e.g. dist-only
|
|
22378
|
-
* builds) or the source has no recognizable workflow, and never fails the build.
|
|
22379
|
-
*/
|
|
22380
|
-
function produceWorkflowFlowGraph(projectRoot, moduleFile, options) {
|
|
22381
|
-
if (!moduleFile.endsWith(".ts")) return;
|
|
22382
|
-
const absolute = join(projectRoot, moduleFile);
|
|
22383
|
-
if (!existsSync(absolute)) return;
|
|
22384
|
-
try {
|
|
22385
|
-
const source = readFileSync(absolute, "utf8");
|
|
22386
|
-
const flowGraph = buildWorkflowCanvasGraph(source, {
|
|
22387
|
-
fileName: moduleFile,
|
|
22388
|
-
resolveSlug: options?.slugRegistry ? createResolveSlugFn(projectRoot, moduleFile, source, options.slugRegistry, options.integrationRegistry) : void 0
|
|
22389
|
-
});
|
|
22390
|
-
if (!flowGraph) return;
|
|
22391
|
-
return {
|
|
22392
|
-
flowGraph,
|
|
22393
|
-
flowGraphSourceHash: createHash("sha256").update(source).digest("hex")
|
|
22394
|
-
};
|
|
22395
|
-
} catch (error) {
|
|
22396
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
22397
|
-
console.warn(`[manifest] skipped workflow flow graph for ${moduleFile}: ${message}`);
|
|
22398
|
-
return;
|
|
22399
|
-
}
|
|
22400
|
-
}
|
|
22401
|
-
/**
|
|
22402
20523
|
* Resolve manifest moduleFile values to project-root-relative source paths.
|
|
22403
20524
|
*
|
|
22404
20525
|
* Discovery runs over compiled `dist/` modules, so the raw moduleFile is a
|
|
@@ -22459,7 +20580,6 @@ async function buildStoredRouteManifestForProject(projectRoot, options) {
|
|
|
22459
20580
|
const dirs = resolveDistModuleDirs(projectRoot);
|
|
22460
20581
|
const sourcePaths = new SourceModuleFileResolver(projectRoot);
|
|
22461
20582
|
const manifest = [{ kind: "health" }];
|
|
22462
|
-
const slugRegistryEntries = [];
|
|
22463
20583
|
const agentEntries = await discoverAgentEntries(dirs.agentsDir, reload);
|
|
22464
20584
|
const workflows = await discoverWorkflows(dirs.workflowsDir, reload);
|
|
22465
20585
|
const attachments = await discoverTriggerAttachments(dirs.triggersDir, reload);
|
|
@@ -22473,72 +20593,22 @@ async function buildStoredRouteManifestForProject(projectRoot, options) {
|
|
|
22473
20593
|
agent: await importAgentDefinition(entry.filePath, reload)
|
|
22474
20594
|
})));
|
|
22475
20595
|
await validateAgentModelIds(loadedAgents.map(({ agent }) => agent.model));
|
|
22476
|
-
const resolvedAgents = [];
|
|
22477
20596
|
for (const { entry, agent } of loadedAgents) {
|
|
22478
20597
|
const moduleFile = await sourcePaths.resolve("agents", "agent", dirs.agentsDir, entry.filePath);
|
|
22479
|
-
|
|
22480
|
-
agent,
|
|
22481
|
-
moduleFile
|
|
22482
|
-
});
|
|
22483
|
-
slugRegistryEntries.push({
|
|
22484
|
-
slug: agent.slug,
|
|
22485
|
-
name: agent.name,
|
|
22486
|
-
description: agent.description,
|
|
22487
|
-
moduleFile
|
|
22488
|
-
});
|
|
22489
|
-
}
|
|
22490
|
-
const actions = await discoverActions(dirs.actionsDir, reload);
|
|
22491
|
-
const resolvedActions = [];
|
|
22492
|
-
for (const action of actions) {
|
|
22493
|
-
const moduleFile = await sourcePaths.resolve("actions", "action", dirs.actionsDir, action.filePath);
|
|
22494
|
-
resolvedActions.push({
|
|
22495
|
-
action,
|
|
22496
|
-
moduleFile
|
|
22497
|
-
});
|
|
22498
|
-
slugRegistryEntries.push({
|
|
22499
|
-
slug: action.definition.slug,
|
|
22500
|
-
name: action.definition.name,
|
|
22501
|
-
description: action.definition.description,
|
|
22502
|
-
moduleFile
|
|
22503
|
-
});
|
|
20598
|
+
manifest.push(agentManifestEntry(agent, { moduleFile }));
|
|
22504
20599
|
}
|
|
22505
|
-
const resolvedWorkflows = [];
|
|
22506
20600
|
for (const workflow of workflows) {
|
|
22507
20601
|
const moduleFile = await sourcePaths.resolve("workflows", "workflow", dirs.workflowsDir, workflow.filePath);
|
|
22508
|
-
|
|
22509
|
-
workflow,
|
|
22510
|
-
moduleFile
|
|
22511
|
-
});
|
|
22512
|
-
slugRegistryEntries.push({
|
|
20602
|
+
manifest.push({
|
|
20603
|
+
kind: "workflow",
|
|
22513
20604
|
slug: workflow.definition.slug,
|
|
22514
20605
|
name: workflow.definition.name,
|
|
22515
20606
|
description: workflow.definition.description,
|
|
22516
|
-
|
|
20607
|
+
subscribable: workflow.definition.subscription?.mode === "subscribable",
|
|
20608
|
+
moduleFile,
|
|
20609
|
+
request: workflow.definition.input
|
|
22517
20610
|
});
|
|
22518
20611
|
}
|
|
22519
|
-
const slugRegistry = buildSlugRegistry(slugRegistryEntries);
|
|
22520
|
-
const integration = await discoverIntegrationActions(projectRoot, [...resolvedWorkflows.map(({ moduleFile }) => moduleFile), ...resolvedAgents.map(({ moduleFile }) => moduleFile)], { existingSlugs: new Set(slugRegistryEntries.map((entry) => entry.slug)) });
|
|
22521
|
-
for (const { agent, moduleFile } of resolvedAgents) manifest.push(agentManifestEntry(agent, { moduleFile }));
|
|
22522
|
-
for (const { action, moduleFile } of resolvedActions) manifest.push(actionManifestEntry(action.definition, {
|
|
22523
|
-
moduleFile,
|
|
22524
|
-
inputSchema: schemaToJson(action.definition.input),
|
|
22525
|
-
outputSchema: schemaToJson(action.definition.output)
|
|
22526
|
-
}));
|
|
22527
|
-
for (const entry of integration.entries) manifest.push(entry);
|
|
22528
|
-
for (const { workflow, moduleFile } of resolvedWorkflows) manifest.push({
|
|
22529
|
-
kind: "workflow",
|
|
22530
|
-
slug: workflow.definition.slug,
|
|
22531
|
-
name: workflow.definition.name,
|
|
22532
|
-
description: workflow.definition.description,
|
|
22533
|
-
subscribable: workflow.definition.subscription?.mode === "subscribable",
|
|
22534
|
-
moduleFile,
|
|
22535
|
-
request: workflow.definition.input,
|
|
22536
|
-
response: workflow.definition.output,
|
|
22537
|
-
...produceWorkflowFlowGraph(projectRoot, moduleFile, {
|
|
22538
|
-
slugRegistry,
|
|
22539
|
-
integrationRegistry: integration.registry
|
|
22540
|
-
})
|
|
22541
|
-
});
|
|
22542
20612
|
const discoveredBySlug = new Map(attachments.map((attachment) => [attachment.slug, attachment]));
|
|
22543
20613
|
const cronByTriggerSlug = /* @__PURE__ */ new Map();
|
|
22544
20614
|
const pollByGroupId = /* @__PURE__ */ new Map();
|
|
@@ -22603,9 +20673,9 @@ async function buildStoredRouteManifestForProject(projectRoot, options) {
|
|
|
22603
20673
|
discovered,
|
|
22604
20674
|
options: { attachmentSlug: discovered.slug }
|
|
22605
20675
|
}];
|
|
22606
|
-
const attachmentMeta = Object.fromEntries(bindings.
|
|
20676
|
+
const attachmentMeta = Object.fromEntries(bindings.flatMap(({ discovered: row }) => {
|
|
22607
20677
|
const meta = triggerMetaFromAttachment(row.attachment);
|
|
22608
|
-
return [row.slug, meta];
|
|
20678
|
+
return Object.keys(meta).length > 0 ? [[row.slug, meta]] : [];
|
|
22609
20679
|
}));
|
|
22610
20680
|
manifest.push({
|
|
22611
20681
|
kind: "trigger-webhook",
|
|
@@ -22615,16 +20685,16 @@ async function buildStoredRouteManifestForProject(projectRoot, options) {
|
|
|
22615
20685
|
...sourceHash ? { sourceHash } : {},
|
|
22616
20686
|
request: webhookMatchSchemaForBindings(bindings),
|
|
22617
20687
|
attachmentSchemas: webhookManifestAttachmentSchemasFromBindings(bindings),
|
|
22618
|
-
attachmentMeta,
|
|
20688
|
+
...Object.keys(attachmentMeta).length > 0 ? { attachmentMeta } : {},
|
|
22619
20689
|
response: PromptResponseSchema
|
|
22620
20690
|
});
|
|
22621
20691
|
}
|
|
22622
|
-
return
|
|
20692
|
+
return buildStoredRouteManifestFromContext({
|
|
22623
20693
|
manifest,
|
|
22624
20694
|
options: {},
|
|
22625
20695
|
projectRoot,
|
|
22626
20696
|
skills: discoverSkillManifestEntries(projectRoot)
|
|
22627
|
-
})
|
|
20697
|
+
});
|
|
22628
20698
|
} finally {
|
|
22629
20699
|
if (previousRoot === void 0) delete process.env.KEYSTROKE_ROOT;
|
|
22630
20700
|
else process.env.KEYSTROKE_ROOT = previousRoot;
|
|
@@ -22635,6 +20705,6 @@ async function emitStoredRouteManifestForProject(projectRoot) {
|
|
|
22635
20705
|
persistStoredRouteManifest(projectRoot, await buildStoredRouteManifestForProject(projectRoot));
|
|
22636
20706
|
}
|
|
22637
20707
|
//#endregion
|
|
22638
|
-
export { validatePollGroups as A, attachmentSlugFromRecord as B, pollRouteFromSourceSlug as C, validateAttachmentTargets as D, toStoredRouteManifest as E, webhookManifestAttachmentSchemasFromBindings as F,
|
|
20708
|
+
export { validatePollGroups as A, attachmentSlugFromRecord as B, pollRouteFromSourceSlug as C, validateAttachmentTargets as D, toStoredRouteManifest as E, webhookManifestAttachmentSchemasFromBindings as F, readKeystrokeIgnoreDirective as G, discoverEntries as H, webhookMatchSchemaForBindings as I, walkTypeScriptFiles as J, shouldSkipKeystrokeModuleFile as K, webhookRouteFromEndpoint as L, validateTriggerAttachments as M, validateUniqueAttachmentSlugs as N, validateImportedTriggerAttachment as O, validateUniqueTriggerSourceSlugs as P, workflowKeyForAttachment as R, pollGroupRouteFromId as S, serializeRouteManifest as T, discoverModuleFileEntries as U, packAssetDirs as V, entryIdFromFile as W, 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, validateUniqueModuleKeys 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 };
|
|
22639
20709
|
|
|
22640
|
-
//# sourceMappingURL=dist-
|
|
20710
|
+
//# sourceMappingURL=dist-D-cLLjHv.mjs.map
|