@sweny-ai/core 0.1.122 → 0.1.124
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/agent-env.d.ts +171 -0
- package/dist/agent-env.js +393 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +1 -1
- package/dist/claude.d.ts +61 -3
- package/dist/claude.js +191 -27
- package/dist/cli/check.d.ts +42 -1
- package/dist/cli/check.js +130 -8
- package/dist/cli/cloud-lifecycle.d.ts +24 -29
- package/dist/cli/cloud-lifecycle.js +187 -12
- package/dist/cli/cloud-report.js +45 -2
- package/dist/cli/config-file.d.ts +8 -1
- package/dist/cli/config-file.js +42 -1
- package/dist/cli/config.js +11 -14
- package/dist/cli/e2e.d.ts +22 -0
- package/dist/cli/e2e.js +31 -4
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +138 -46
- package/dist/cli/marketplace.d.ts +3 -1
- package/dist/cli/marketplace.js +2 -2
- package/dist/cli/new.d.ts +15 -0
- package/dist/cli/new.js +139 -65
- package/dist/cli/publish.js +9 -0
- package/dist/cli/run-output.d.ts +59 -0
- package/dist/cli/run-output.js +189 -0
- package/dist/cli/skill-env.d.ts +34 -0
- package/dist/cli/skill-env.js +64 -0
- package/dist/cli/templates.js +587 -0
- package/dist/cli/workflow-input.d.ts +41 -0
- package/dist/cli/workflow-input.js +56 -0
- package/dist/executor.d.ts +17 -1
- package/dist/executor.js +119 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mermaid.d.ts +6 -0
- package/dist/mermaid.js +10 -5
- package/dist/schema.d.ts +218 -188
- package/dist/schema.js +24 -3
- package/dist/skills/betterstack.js +4 -0
- package/dist/skills/datadog.js +3 -0
- package/dist/skills/github.js +8 -0
- package/dist/skills/linear.js +8 -0
- package/dist/skills/notification.js +3 -0
- package/dist/skills/sentry.js +4 -0
- package/dist/skills/slack.js +2 -0
- package/dist/skills/supabase.js +9 -0
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +9 -3
- package/dist/types.d.ts +99 -2
- package/dist/types.js +17 -0
- package/dist/untrusted.d.ts +32 -0
- package/dist/untrusted.js +46 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +2 -0
- package/dist/workflows/triage.yml +3 -1
- package/package.json +2 -2
package/dist/cli/new.js
CHANGED
|
@@ -355,11 +355,12 @@ export function buildEnvTemplate(credentials) {
|
|
|
355
355
|
if (cred.hint) {
|
|
356
356
|
lines.push(`# ${cred.hint}`);
|
|
357
357
|
}
|
|
358
|
+
const prefix = cred.optional ? "# " : "";
|
|
358
359
|
if (cred.default !== undefined) {
|
|
359
|
-
lines.push(`${cred.key}=${cred.default}`);
|
|
360
|
+
lines.push(`${prefix}${cred.key}=${cred.default}`);
|
|
360
361
|
}
|
|
361
362
|
else {
|
|
362
|
-
lines.push(`${cred.key}=`);
|
|
363
|
+
lines.push(`${prefix}${cred.key}=`);
|
|
363
364
|
}
|
|
364
365
|
lines.push("");
|
|
365
366
|
}
|
|
@@ -441,6 +442,47 @@ export function writeWorkflowFile(cwd, id, yaml, options) {
|
|
|
441
442
|
fs.writeFileSync(target, yaml, "utf-8");
|
|
442
443
|
return { written: true, path: target };
|
|
443
444
|
}
|
|
445
|
+
/**
|
|
446
|
+
* Make sure `.env` is gitignored: create `.gitignore` if missing, append
|
|
447
|
+
* `.env` if absent. Returns what happened.
|
|
448
|
+
*/
|
|
449
|
+
export function ensureGitignoreEnv(cwd) {
|
|
450
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
451
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
452
|
+
fs.writeFileSync(gitignorePath, ".env\n", "utf-8");
|
|
453
|
+
return "created";
|
|
454
|
+
}
|
|
455
|
+
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
456
|
+
if (content.split(/\r?\n/).some((line) => line.trim() === ".env"))
|
|
457
|
+
return "present";
|
|
458
|
+
const sep = content.length === 0 || content.endsWith("\n") ? "" : "\n";
|
|
459
|
+
fs.appendFileSync(gitignorePath, `${sep}.env\n`, "utf-8");
|
|
460
|
+
return "appended";
|
|
461
|
+
}
|
|
462
|
+
/** The command to show in next steps: `npx` users have no `sweny` on PATH. */
|
|
463
|
+
function cliCommand() {
|
|
464
|
+
return process.env.npm_command === "exec" ? "npx @sweny-ai/core" : "sweny";
|
|
465
|
+
}
|
|
466
|
+
/** Usage printed (exit 2) when `sweny new` has no terminal and no --yes. */
|
|
467
|
+
export function nonInteractiveUsage() {
|
|
468
|
+
return [
|
|
469
|
+
"sweny new needs a terminal to ask questions. Non-interactive form:",
|
|
470
|
+
"",
|
|
471
|
+
" sweny new --template <id> --yes",
|
|
472
|
+
"",
|
|
473
|
+
`Templates: ${WORKFLOW_TEMPLATES.map((t) => t.id).join(", ")}`,
|
|
474
|
+
`Zero-credential starter: sweny new --template ${WORKFLOW_TEMPLATES[0].id} --yes`,
|
|
475
|
+
].join("\n");
|
|
476
|
+
}
|
|
477
|
+
/** Credentials to list for a workflow: a no-skill starter only needs Claude auth, which a Claude Code login covers. */
|
|
478
|
+
function credentialsForWorkflow(skillIds, allSkills) {
|
|
479
|
+
const creds = collectCredentialsForSkills(skillIds, allSkills);
|
|
480
|
+
if (skillIds.length > 0)
|
|
481
|
+
return creds;
|
|
482
|
+
return creds.map((c) => c.key === "ANTHROPIC_API_KEY"
|
|
483
|
+
? { ...c, optional: true, hint: "Not needed if you are logged in to Claude Code (run `claude` once)" }
|
|
484
|
+
: c);
|
|
485
|
+
}
|
|
444
486
|
// ── Interactive wizard ────────────────────────────────────────────────
|
|
445
487
|
function cancel() {
|
|
446
488
|
p.cancel("Setup cancelled.");
|
|
@@ -468,6 +510,17 @@ function cancel() {
|
|
|
468
510
|
*/
|
|
469
511
|
export async function runNew(options) {
|
|
470
512
|
const cwd = process.cwd();
|
|
513
|
+
// `sweny new <id>`: a built-in template id wins over a marketplace lookup,
|
|
514
|
+
// so it works with no network. Only other ids go to the marketplace.
|
|
515
|
+
if (options?.marketplaceId && options.marketplaceId !== "e2e" && !options.forceMarketplace) {
|
|
516
|
+
if (WORKFLOW_TEMPLATES.some((t) => t.id === options.marketplaceId)) {
|
|
517
|
+
return runNew({ template: options.marketplaceId, skipIntro: options.skipIntro, yes: options.yes });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (options?.template && !WORKFLOW_TEMPLATES.some((t) => t.id === options.template)) {
|
|
521
|
+
p.log.error(`Unknown template "${options.template}". Available: ${WORKFLOW_TEMPLATES.map((t) => t.id).join(", ")}`);
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
471
524
|
// ── E2E shortcut: `sweny new e2e` jumps straight into the e2e wizard ──
|
|
472
525
|
// `new` is the single entry for all workflow creation; this is a named
|
|
473
526
|
// shortcut into the same flow the interactive picker reaches via "__e2e".
|
|
@@ -491,6 +544,10 @@ export async function runNew(options) {
|
|
|
491
544
|
// Check for existing workflow file and prompt before fetching.
|
|
492
545
|
const existingWorkflowPath = path.join(cwd, ".sweny", "workflows", `${options.marketplaceId}.yml`);
|
|
493
546
|
let overwrite = false;
|
|
547
|
+
if (fs.existsSync(existingWorkflowPath) && options.yes) {
|
|
548
|
+
p.log.info(`Workflow already exists at ${path.relative(cwd, existingWorkflowPath)}. Not overwritten.`);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
494
551
|
if (fs.existsSync(existingWorkflowPath)) {
|
|
495
552
|
const confirmed = await p.confirm({
|
|
496
553
|
message: `.sweny/workflows/${options.marketplaceId}.yml already exists. Overwrite?`,
|
|
@@ -515,7 +572,8 @@ export async function runNew(options) {
|
|
|
515
572
|
});
|
|
516
573
|
}
|
|
517
574
|
catch (err) {
|
|
518
|
-
|
|
575
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
576
|
+
p.log.error(`${reason}\nNot a built-in template either. Built-in templates (work offline): ${WORKFLOW_TEMPLATES.map((t) => t.id).join(", ")}`);
|
|
519
577
|
process.exit(1);
|
|
520
578
|
}
|
|
521
579
|
if (!result.installed) {
|
|
@@ -546,44 +604,57 @@ export async function runNew(options) {
|
|
|
546
604
|
p.log.info(`Found ${customSkills.length} custom skill(s): ${chalk.cyan(customSkills.map((s) => s.id).join(", "))}`);
|
|
547
605
|
}
|
|
548
606
|
// ── Step 1: Pick a workflow ─────────────────────────────────────────
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
...WORKFLOW_TEMPLATES.map((t) => ({
|
|
558
|
-
value: t.id,
|
|
559
|
-
label: t.name,
|
|
560
|
-
hint: t.description,
|
|
561
|
-
})),
|
|
562
|
-
{ value: "__e2e", label: "End-to-end browser testing", hint: "Automated browser tests for your app" },
|
|
563
|
-
{ value: "__custom", label: "Describe your own", hint: "AI-generated from your description" },
|
|
564
|
-
{ value: "__blank", label: "Start blank", hint: "just set up config, I'll create workflows later" },
|
|
565
|
-
],
|
|
566
|
-
});
|
|
567
|
-
if (p.isCancel(templateChoice))
|
|
568
|
-
cancel();
|
|
569
|
-
// ── Marketplace short-circuit: fetch index, pick, delegate ──────────
|
|
570
|
-
if (templateChoice === "__marketplace") {
|
|
571
|
-
const { fetchMarketplaceIndex } = await import("./marketplace.js");
|
|
572
|
-
const spinner = p.spinner();
|
|
573
|
-
spinner.start("Fetching marketplace index…");
|
|
574
|
-
let entries;
|
|
607
|
+
// Marketplace is offered only when its index is reachable. Offline or a
|
|
608
|
+
// missing index must never leave the default option broken.
|
|
609
|
+
let marketplaceEntries = null;
|
|
610
|
+
let templateChoice;
|
|
611
|
+
if (options?.template) {
|
|
612
|
+
templateChoice = options.template;
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
575
615
|
try {
|
|
576
|
-
|
|
577
|
-
|
|
616
|
+
const { fetchMarketplaceIndex } = await import("./marketplace.js");
|
|
617
|
+
const entries = await fetchMarketplaceIndex({ timeoutMs: 3000 });
|
|
618
|
+
if (Array.isArray(entries) && entries.length > 0)
|
|
619
|
+
marketplaceEntries = entries;
|
|
620
|
+
else
|
|
621
|
+
p.log.info("Marketplace has no workflows yet. Showing built-in templates.");
|
|
578
622
|
}
|
|
579
623
|
catch (err) {
|
|
580
|
-
|
|
581
|
-
p.log.
|
|
582
|
-
process.exit(1);
|
|
624
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
625
|
+
p.log.info(`Marketplace unavailable (${reason}). Showing built-in templates.`);
|
|
583
626
|
}
|
|
627
|
+
templateChoice = await p.select({
|
|
628
|
+
message: "What do you want to do?",
|
|
629
|
+
initialValue: WORKFLOW_TEMPLATES[0].id,
|
|
630
|
+
options: [
|
|
631
|
+
...WORKFLOW_TEMPLATES.map((t) => ({
|
|
632
|
+
value: t.id,
|
|
633
|
+
label: t.name,
|
|
634
|
+
hint: t.description,
|
|
635
|
+
})),
|
|
636
|
+
...(marketplaceEntries
|
|
637
|
+
? [
|
|
638
|
+
{
|
|
639
|
+
value: "__marketplace",
|
|
640
|
+
label: "Browse marketplace",
|
|
641
|
+
hint: "install a published workflow from swenyai/workflows",
|
|
642
|
+
},
|
|
643
|
+
]
|
|
644
|
+
: []),
|
|
645
|
+
{ value: "__e2e", label: "End-to-end browser testing", hint: "Automated browser tests for your app" },
|
|
646
|
+
{ value: "__custom", label: "Describe your own", hint: "AI-generated from your description" },
|
|
647
|
+
{ value: "__blank", label: "Start blank", hint: "just set up config, I'll create workflows later" },
|
|
648
|
+
],
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
if (p.isCancel(templateChoice))
|
|
652
|
+
cancel();
|
|
653
|
+
// ── Marketplace short-circuit: pick from the fetched index, delegate ──
|
|
654
|
+
if (templateChoice === "__marketplace" && marketplaceEntries) {
|
|
584
655
|
const pick = await p.select({
|
|
585
656
|
message: "Which workflow?",
|
|
586
|
-
options:
|
|
657
|
+
options: marketplaceEntries.map((e) => ({
|
|
587
658
|
value: e.id,
|
|
588
659
|
label: e.name,
|
|
589
660
|
hint: e.description,
|
|
@@ -593,7 +664,7 @@ export async function runNew(options) {
|
|
|
593
664
|
cancel();
|
|
594
665
|
// Delegate to the marketplace install path
|
|
595
666
|
// Wizard already showed its intro — skip the fast-path intro to avoid doubling.
|
|
596
|
-
return runNew({ marketplaceId: pick, skipIntro: true });
|
|
667
|
+
return runNew({ marketplaceId: pick, skipIntro: true, forceMarketplace: true });
|
|
597
668
|
}
|
|
598
669
|
// ── E2E short-circuit: delegate to the e2e wizard ────────────────────
|
|
599
670
|
if (templateChoice === "__e2e") {
|
|
@@ -617,7 +688,7 @@ export async function runNew(options) {
|
|
|
617
688
|
const issueTracker = inferIssueTracker(workflowSkills, sourceControl);
|
|
618
689
|
const observability = inferObservability(workflowSkills);
|
|
619
690
|
// Collect credentials for the workflow's skills (+ always ANTHROPIC_API_KEY)
|
|
620
|
-
const credentials =
|
|
691
|
+
const credentials = credentialsForWorkflow(workflowSkills, allSkills);
|
|
621
692
|
// ── Step 3: Summary + confirm ───────────────────────────────────────
|
|
622
693
|
const files = [];
|
|
623
694
|
if (!hasExistingConfig)
|
|
@@ -647,18 +718,20 @@ export async function runNew(options) {
|
|
|
647
718
|
summaryLines.push(chalk.bold(hasExistingConfig ? "Workflow uses:" : "Inferred from workflow:"));
|
|
648
719
|
summaryLines.push(...inferred);
|
|
649
720
|
summaryLines.push("");
|
|
650
|
-
summaryLines.push(chalk.bold(`Credentials
|
|
651
|
-
summaryLines.push(...credentials.map((c) => ` ${chalk.dim(c.key)}`));
|
|
721
|
+
summaryLines.push(chalk.bold(`Credentials: ${credentials.filter((c) => !c.optional).length} required`));
|
|
722
|
+
summaryLines.push(...credentials.map((c) => ` ${chalk.dim(c.key)}${c.optional ? chalk.dim(" (optional with a Claude Code login)") : ""}`));
|
|
652
723
|
p.log.message(summaryLines.join("\n"));
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
724
|
+
if (!options?.yes) {
|
|
725
|
+
const confirmed = await p.confirm({
|
|
726
|
+
message: hasExistingConfig ? "Add this workflow?" : "Create these files?",
|
|
727
|
+
initialValue: true,
|
|
728
|
+
});
|
|
729
|
+
if (p.isCancel(confirmed))
|
|
730
|
+
cancel();
|
|
731
|
+
if (!confirmed) {
|
|
732
|
+
p.cancel("Setup cancelled.");
|
|
733
|
+
process.exit(0);
|
|
734
|
+
}
|
|
662
735
|
}
|
|
663
736
|
// ── Step 4: Write files ─────────────────────────────────────────────
|
|
664
737
|
// 1. Write .sweny.yml (only if fresh)
|
|
@@ -684,6 +757,11 @@ export async function runNew(options) {
|
|
|
684
757
|
// 3. Workflow template
|
|
685
758
|
if (template) {
|
|
686
759
|
const firstAttempt = writeWorkflowFile(cwd, template.id, template.yaml, { overwrite: false });
|
|
760
|
+
if (firstAttempt.exists && options?.yes) {
|
|
761
|
+
p.log.info(`.sweny/workflows/${template.id}.yml already exists, not overwritten`);
|
|
762
|
+
p.outro("Done.");
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
687
765
|
if (firstAttempt.exists) {
|
|
688
766
|
const overwrite = await p.confirm({
|
|
689
767
|
message: `.sweny/workflows/${template.id}.yml already exists. Overwrite?`,
|
|
@@ -700,30 +778,26 @@ export async function runNew(options) {
|
|
|
700
778
|
}
|
|
701
779
|
p.log.success(`Created .sweny/workflows/${template.id}.yml`);
|
|
702
780
|
}
|
|
703
|
-
// 4. .
|
|
704
|
-
const
|
|
705
|
-
if (
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
p.log.warn(chalk.yellow(".env is not in .gitignore — add it to avoid committing secrets"));
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
else {
|
|
713
|
-
p.log.warn(chalk.yellow("No .gitignore found — make sure .env is not committed"));
|
|
714
|
-
}
|
|
781
|
+
// 4. Keep .env out of version control
|
|
782
|
+
const ignoreResult = ensureGitignoreEnv(cwd);
|
|
783
|
+
if (ignoreResult === "created")
|
|
784
|
+
p.log.success("Created .gitignore with .env");
|
|
785
|
+
else if (ignoreResult === "appended")
|
|
786
|
+
p.log.success("Added .env to .gitignore");
|
|
715
787
|
// 5. Next steps
|
|
716
|
-
const
|
|
788
|
+
const requiredCreds = credentials.filter((c) => !c.optional);
|
|
789
|
+
const credUrls = requiredCreds.filter((c) => c.url).map((c) => ` ${c.key}: ${c.url}`);
|
|
717
790
|
const steps = [];
|
|
718
791
|
let stepNum = 1;
|
|
719
|
-
|
|
720
|
-
|
|
792
|
+
const cli = cliCommand();
|
|
793
|
+
if (addedNewKeys && requiredCreds.length > 0) {
|
|
794
|
+
steps.push(`${stepNum++}. Fill in your API keys in .env:`, ...credUrls, "", `${stepNum++}. Verify connectivity:`, ` ${cli} check`, "");
|
|
721
795
|
}
|
|
722
796
|
if (template) {
|
|
723
|
-
steps.push(`${stepNum++}. Run your workflow:`, `
|
|
797
|
+
steps.push(`${stepNum++}. Run your workflow:`, ` ${cli} workflow run .sweny/workflows/${template.id}.yml`);
|
|
724
798
|
}
|
|
725
799
|
else {
|
|
726
|
-
steps.push(`${stepNum++}. Create your first workflow:`,
|
|
800
|
+
steps.push(`${stepNum++}. Create your first workflow:`, ` ${cli} new`);
|
|
727
801
|
}
|
|
728
802
|
p.note(steps.join("\n"), "Next steps");
|
|
729
803
|
p.outro(hasExistingConfig ? "Workflow added!" : "You're all set!");
|
package/dist/cli/publish.js
CHANGED
|
@@ -58,6 +58,15 @@ export function validateWorkflowFile(filePath) {
|
|
|
58
58
|
warnings,
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
// Marketplace templates are required to declare workflow_type (spec:
|
|
62
|
+
// https://spec.sweny.ai/nodes/#workflow_type). Absence defaults to
|
|
63
|
+
// "generic" at runtime, but a marketplace submission that never chose a
|
|
64
|
+
// type is a template that never got the cross-type renderer it should
|
|
65
|
+
// have. Warn, don't block: this stays a warning so existing community
|
|
66
|
+
// submissions aren't hard-rejected by `sweny publish`.
|
|
67
|
+
if (!workflow.workflow_type) {
|
|
68
|
+
warnings.push('Workflow does not declare "workflow_type" (pr_review, e2e_test, content_generation, monitor, data_sync, generic); defaults to "generic". Marketplace templates should declare one explicitly.');
|
|
69
|
+
}
|
|
61
70
|
// Config completeness warnings
|
|
62
71
|
const allSkills = [...builtinSkills, ...customSkills];
|
|
63
72
|
const skillMap = new Map(allSkills.map((s) => [s.id, s]));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run-end output for `sweny workflow run`: the receipt line, the
|
|
3
|
+
* GITHUB_STEP_SUMMARY writer, the quiet-by-default logger, and the help text.
|
|
4
|
+
*
|
|
5
|
+
* METADATA ONLY. Nothing here reads prompts, node outputs, tool inputs, or
|
|
6
|
+
* model prose: only counts, durations, token totals, and cost.
|
|
7
|
+
*/
|
|
8
|
+
import { type ExecutionTrace, type Logger, type NodeResult, type Workflow } from "../types.js";
|
|
9
|
+
export interface RunSummary {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
nodesOk: number;
|
|
12
|
+
nodesTotal: number;
|
|
13
|
+
nodesSkipped: number;
|
|
14
|
+
toolCalls: number;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
/** input + output tokens. Undefined when no node reported usage. */
|
|
17
|
+
tokens?: number;
|
|
18
|
+
/** Sum of SDK-reported cost. Undefined when the SDK reported none (never estimated). */
|
|
19
|
+
costUsd?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Sum the per-node results into one run summary. */
|
|
22
|
+
export declare function summarizeRun(results: Map<string, NodeResult>, durationMs: number, crashed?: boolean): RunSummary;
|
|
23
|
+
export declare function formatReceiptDuration(ms: number): string;
|
|
24
|
+
export declare function formatTokenCount(n: number): string;
|
|
25
|
+
export declare function formatCost(usd: number): string;
|
|
26
|
+
/**
|
|
27
|
+
* One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
|
|
28
|
+
* Token and cost segments are omitted when the SDK reported none. No ANSI;
|
|
29
|
+
* callers colorize only when writing to a TTY.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatReceipt(s: RunSummary): string;
|
|
32
|
+
/** Receipt for a terminal: colored only when `color` is true. */
|
|
33
|
+
export declare function renderReceiptLine(s: RunSummary, color: boolean): string;
|
|
34
|
+
/** Markdown for `$GITHUB_STEP_SUMMARY`: receipt + status-colored Mermaid DAG. */
|
|
35
|
+
export declare function formatStepSummary(workflow: Workflow, results: Map<string, NodeResult>, summary: RunSummary, trace?: ExecutionTrace): string;
|
|
36
|
+
/**
|
|
37
|
+
* Append the step summary to the file named by GITHUB_STEP_SUMMARY.
|
|
38
|
+
* Runner-agnostic: does nothing when the variable is unset or empty.
|
|
39
|
+
* Never throws; returns true only when something was written.
|
|
40
|
+
*/
|
|
41
|
+
export declare function writeStepSummary(workflow: Workflow, results: Map<string, NodeResult>, summary: RunSummary, trace?: ExecutionTrace, env?: NodeJS.ProcessEnv): boolean;
|
|
42
|
+
export interface RunLogger extends Logger {
|
|
43
|
+
/** Write any warnings held back while a node line was live. */
|
|
44
|
+
flush(): void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Logger for `workflow run`. With `verbose`, the raw `[info]`/`[debug]` lines
|
|
48
|
+
* pass through unchanged. Otherwise info/debug are dropped and warnings and
|
|
49
|
+
* errors are rendered as ` ⚠ msg` / ` ✗ msg` on stderr. On a TTY they are held
|
|
50
|
+
* while a node line is live (the progress line is redrawn in place) and written
|
|
51
|
+
* by `flush()` after the node exits.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createRunLogger(opts: {
|
|
54
|
+
verbose: boolean;
|
|
55
|
+
tty: boolean;
|
|
56
|
+
write?: (s: string) => void;
|
|
57
|
+
}): RunLogger;
|
|
58
|
+
export declare const WORKFLOW_RUN_DESCRIPTION = "Run a workflow from a YAML or JSON file. With no file, batch-runs every workflow in .sweny/e2e/ (lists and confirms first; use --yes to skip the prompt).";
|
|
59
|
+
export declare const WORKFLOW_RUN_OPTIONS: ReadonlyArray<readonly [flags: string, description: string]>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run-end output for `sweny workflow run`: the receipt line, the
|
|
3
|
+
* GITHUB_STEP_SUMMARY writer, the quiet-by-default logger, and the help text.
|
|
4
|
+
*
|
|
5
|
+
* METADATA ONLY. Nothing here reads prompts, node outputs, tool inputs, or
|
|
6
|
+
* model prose: only counts, durations, token totals, and cost.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { consoleLogger } from "../types.js";
|
|
11
|
+
import { toMermaidBlock } from "../mermaid.js";
|
|
12
|
+
/** Sum the per-node results into one run summary. */
|
|
13
|
+
export function summarizeRun(results, durationMs, crashed = false) {
|
|
14
|
+
let nodesOk = 0;
|
|
15
|
+
let nodesSkipped = 0;
|
|
16
|
+
let failed = crashed;
|
|
17
|
+
let toolCalls = 0;
|
|
18
|
+
let tokens;
|
|
19
|
+
let costUsd;
|
|
20
|
+
const isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
21
|
+
for (const r of results.values()) {
|
|
22
|
+
if (r.status === "success")
|
|
23
|
+
nodesOk++;
|
|
24
|
+
else if (r.status === "skipped")
|
|
25
|
+
nodesSkipped++;
|
|
26
|
+
else
|
|
27
|
+
failed = true;
|
|
28
|
+
toolCalls += r.toolCalls?.length ?? 0;
|
|
29
|
+
const u = r.usage;
|
|
30
|
+
if (u) {
|
|
31
|
+
if (isNum(u.inputTokens) || isNum(u.outputTokens)) {
|
|
32
|
+
tokens =
|
|
33
|
+
(tokens ?? 0) + (isNum(u.inputTokens) ? u.inputTokens : 0) + (isNum(u.outputTokens) ? u.outputTokens : 0);
|
|
34
|
+
}
|
|
35
|
+
if (isNum(u.costUsd))
|
|
36
|
+
costUsd = (costUsd ?? 0) + u.costUsd;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
ok: !failed,
|
|
41
|
+
nodesOk,
|
|
42
|
+
nodesTotal: results.size,
|
|
43
|
+
nodesSkipped,
|
|
44
|
+
toolCalls,
|
|
45
|
+
durationMs,
|
|
46
|
+
...(tokens !== undefined ? { tokens } : {}),
|
|
47
|
+
...(costUsd !== undefined ? { costUsd } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function formatReceiptDuration(ms) {
|
|
51
|
+
if (ms < 1000)
|
|
52
|
+
return `${Math.max(0, Math.round(ms))}ms`;
|
|
53
|
+
const s = Math.round(ms / 1000);
|
|
54
|
+
if (s < 60)
|
|
55
|
+
return `${s}s`;
|
|
56
|
+
return `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
57
|
+
}
|
|
58
|
+
export function formatTokenCount(n) {
|
|
59
|
+
if (n < 1000)
|
|
60
|
+
return String(Math.round(n));
|
|
61
|
+
if (n < 10_000)
|
|
62
|
+
return `${Math.round(n / 100) / 10}k`;
|
|
63
|
+
if (n < 1_000_000)
|
|
64
|
+
return `${Math.round(n / 1000)}k`;
|
|
65
|
+
return `${Math.round(n / 100_000) / 10}M`;
|
|
66
|
+
}
|
|
67
|
+
export function formatCost(usd) {
|
|
68
|
+
if (usd > 0 && usd < 0.01)
|
|
69
|
+
return "<$0.01";
|
|
70
|
+
return `$${usd.toFixed(2)}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* One plain line: `✓ 3/3 nodes · 41 tool calls · 2m10s · 12k tokens · $0.18`.
|
|
74
|
+
* Token and cost segments are omitted when the SDK reported none. No ANSI;
|
|
75
|
+
* callers colorize only when writing to a TTY.
|
|
76
|
+
*/
|
|
77
|
+
export function formatReceipt(s) {
|
|
78
|
+
const parts = [
|
|
79
|
+
`${s.ok ? "✓" : "✗"} ${s.nodesOk}/${s.nodesTotal} nodes`,
|
|
80
|
+
...(s.nodesSkipped > 0 ? [`${s.nodesSkipped} skipped`] : []),
|
|
81
|
+
`${s.toolCalls} tool ${s.toolCalls === 1 ? "call" : "calls"}`,
|
|
82
|
+
formatReceiptDuration(s.durationMs),
|
|
83
|
+
...(s.tokens !== undefined ? [`${formatTokenCount(s.tokens)} tokens`] : []),
|
|
84
|
+
...(s.costUsd !== undefined ? [formatCost(s.costUsd)] : []),
|
|
85
|
+
];
|
|
86
|
+
return parts.join(" · ");
|
|
87
|
+
}
|
|
88
|
+
/** Receipt for a terminal: colored only when `color` is true. */
|
|
89
|
+
export function renderReceiptLine(s, color) {
|
|
90
|
+
const line = formatReceipt(s);
|
|
91
|
+
if (!color)
|
|
92
|
+
return line;
|
|
93
|
+
return s.ok ? chalk.green(line) : chalk.red(line);
|
|
94
|
+
}
|
|
95
|
+
// ── Step summary ────────────────────────────────────────────────
|
|
96
|
+
/** Brand blue (blue-600 / blue-700), not indigo. Applied to successful nodes. */
|
|
97
|
+
const STEP_SUMMARY_CLASS_DEFS = {
|
|
98
|
+
success: "fill:#2563eb,stroke:#1d4ed8,color:#fff,stroke-width:2px",
|
|
99
|
+
};
|
|
100
|
+
/** Markdown for `$GITHUB_STEP_SUMMARY`: receipt + status-colored Mermaid DAG. */
|
|
101
|
+
export function formatStepSummary(workflow, results, summary, trace) {
|
|
102
|
+
const state = {};
|
|
103
|
+
for (const [id, r] of results) {
|
|
104
|
+
state[id] = r.status === "success" ? "success" : r.status === "failed" ? "failed" : "skipped";
|
|
105
|
+
}
|
|
106
|
+
return [
|
|
107
|
+
`## ${summary.ok ? "✅" : "❌"} ${workflow.name}`,
|
|
108
|
+
"",
|
|
109
|
+
`\`${formatReceipt(summary)}\``,
|
|
110
|
+
"",
|
|
111
|
+
toMermaidBlock(workflow, { state, trace, classDefs: STEP_SUMMARY_CLASS_DEFS }),
|
|
112
|
+
"",
|
|
113
|
+
"",
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Append the step summary to the file named by GITHUB_STEP_SUMMARY.
|
|
118
|
+
* Runner-agnostic: does nothing when the variable is unset or empty.
|
|
119
|
+
* Never throws; returns true only when something was written.
|
|
120
|
+
*/
|
|
121
|
+
export function writeStepSummary(workflow, results, summary, trace, env = process.env) {
|
|
122
|
+
const file = env.GITHUB_STEP_SUMMARY;
|
|
123
|
+
if (!file)
|
|
124
|
+
return false;
|
|
125
|
+
try {
|
|
126
|
+
fs.appendFileSync(file, formatStepSummary(workflow, results, summary, trace));
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
process.stderr.write(` ⚠ could not write GITHUB_STEP_SUMMARY: ${err instanceof Error ? err.message : err}\n`);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Logger for `workflow run`. With `verbose`, the raw `[info]`/`[debug]` lines
|
|
136
|
+
* pass through unchanged. Otherwise info/debug are dropped and warnings and
|
|
137
|
+
* errors are rendered as ` ⚠ msg` / ` ✗ msg` on stderr. On a TTY they are held
|
|
138
|
+
* while a node line is live (the progress line is redrawn in place) and written
|
|
139
|
+
* by `flush()` after the node exits.
|
|
140
|
+
*/
|
|
141
|
+
export function createRunLogger(opts) {
|
|
142
|
+
const write = opts.write ?? ((s) => void process.stderr.write(s));
|
|
143
|
+
if (opts.verbose)
|
|
144
|
+
return { ...consoleLogger, flush() { } };
|
|
145
|
+
const held = [];
|
|
146
|
+
const emit = (line) => {
|
|
147
|
+
if (opts.tty)
|
|
148
|
+
held.push(line);
|
|
149
|
+
else
|
|
150
|
+
write(line);
|
|
151
|
+
};
|
|
152
|
+
const noop = () => { };
|
|
153
|
+
return {
|
|
154
|
+
info: noop,
|
|
155
|
+
debug: noop,
|
|
156
|
+
warn: (msg) => emit(` ${chalk.yellow("⚠")} ${msg.trim()}\n`),
|
|
157
|
+
error: (msg) => emit(` ${chalk.red("✗")} ${msg.trim()}\n`),
|
|
158
|
+
flush() {
|
|
159
|
+
while (held.length)
|
|
160
|
+
write(held.shift());
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ── Help text ───────────────────────────────────────────────────
|
|
165
|
+
export const WORKFLOW_RUN_DESCRIPTION = "Run a workflow from a YAML or JSON file. With no file, batch-runs every workflow in .sweny/e2e/ (lists and confirms first; use --yes to skip the prompt).";
|
|
166
|
+
export const WORKFLOW_RUN_OPTIONS = [
|
|
167
|
+
[
|
|
168
|
+
"--timeout <ms>",
|
|
169
|
+
"Whole-run wall-clock timeout in ms. Applies to batch runs (.sweny/e2e/) and to a single workflow file (default: 3600000 = 60 min; 0 = no wall-clock budget)",
|
|
170
|
+
],
|
|
171
|
+
[
|
|
172
|
+
"--max-steps <n>",
|
|
173
|
+
"Hard cap on total node executions for a single workflow file, including eval-failure retries (default: 200)",
|
|
174
|
+
],
|
|
175
|
+
["-y, --yes", "Skip the batch confirmation prompt (for CI)"],
|
|
176
|
+
[
|
|
177
|
+
"--dry-run",
|
|
178
|
+
"Run with read-only tools only: write tools, external MCP servers, and shell/file-edit tools are withheld, so nothing is created, posted, or sent. Stops at the first conditional edge. To inspect nodes without running, use --list-nodes.",
|
|
179
|
+
],
|
|
180
|
+
["--list-nodes", "Validate, print nodes and skills, and exit without running"],
|
|
181
|
+
["--json", "Output result as JSON on stdout; suppress progress output"],
|
|
182
|
+
["--stream", "Stream NDJSON events to stdout (for Studio / automation)"],
|
|
183
|
+
[
|
|
184
|
+
"--verbose",
|
|
185
|
+
"Show raw log lines and each tool call's input and output inline (human-readable, truncated). Use --stream for full untruncated NDJSON.",
|
|
186
|
+
],
|
|
187
|
+
["--mermaid", "Output a Mermaid diagram with execution state after run"],
|
|
188
|
+
["--input <json>", "JSON string of input data to pass to the workflow"],
|
|
189
|
+
];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Missing-credential reporting for skills.
|
|
3
|
+
*
|
|
4
|
+
* A built-in skill whose required env var is unset is NOT an unknown skill.
|
|
5
|
+
* These helpers turn that state into one actionable line per skill
|
|
6
|
+
* (`skill "github" needs GITHUB_TOKEN (set it in .env)`), shared by
|
|
7
|
+
* `workflow run` (hard fail) and `workflow validate` (warning).
|
|
8
|
+
*/
|
|
9
|
+
import type { SkillValidationResult } from "../skills/index.js";
|
|
10
|
+
import type { Skill } from "../types.js";
|
|
11
|
+
export interface SkillEnvGap {
|
|
12
|
+
id: string;
|
|
13
|
+
missingEnv: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Built-in skills referenced by a workflow that are not usable because a
|
|
17
|
+
* required env var is unset. `availableIds` is the configured skill set;
|
|
18
|
+
* inline and unknown ids never produce a gap.
|
|
19
|
+
*/
|
|
20
|
+
export declare function findSkillEnvGaps(skillIds: Iterable<string>, availableIds: ReadonlySet<string>, inlineIds?: ReadonlySet<string>): SkillEnvGap[];
|
|
21
|
+
export declare function formatSkillEnvGap(gap: SkillEnvGap): string;
|
|
22
|
+
/**
|
|
23
|
+
* Lines to print for a failed `validateWorkflowSkills`: one env line per
|
|
24
|
+
* built-in skill with missing env, plus node-level errors that carry extra
|
|
25
|
+
* information (alternatives among several skills, or unknown ids).
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatMissingSkillLines(validation: SkillValidationResult): string[];
|
|
28
|
+
/** Warnings for `workflow validate`: referenced skills whose env is missing. */
|
|
29
|
+
export declare function skillEnvWarnings(workflow: {
|
|
30
|
+
nodes: Record<string, {
|
|
31
|
+
skills: string[];
|
|
32
|
+
}>;
|
|
33
|
+
skills?: Record<string, unknown>;
|
|
34
|
+
}, env: Record<string, string | undefined>, available: Skill[]): string[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Missing-credential reporting for skills.
|
|
3
|
+
*
|
|
4
|
+
* A built-in skill whose required env var is unset is NOT an unknown skill.
|
|
5
|
+
* These helpers turn that state into one actionable line per skill
|
|
6
|
+
* (`skill "github" needs GITHUB_TOKEN (set it in .env)`), shared by
|
|
7
|
+
* `workflow run` (hard fail) and `workflow validate` (warning).
|
|
8
|
+
*/
|
|
9
|
+
import { builtinSkills, isSkillConfigured } from "../skills/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Built-in skills referenced by a workflow that are not usable because a
|
|
12
|
+
* required env var is unset. `availableIds` is the configured skill set;
|
|
13
|
+
* inline and unknown ids never produce a gap.
|
|
14
|
+
*/
|
|
15
|
+
export function findSkillEnvGaps(skillIds, availableIds, inlineIds = new Set()) {
|
|
16
|
+
const gaps = [];
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
for (const id of skillIds) {
|
|
19
|
+
if (seen.has(id))
|
|
20
|
+
continue;
|
|
21
|
+
seen.add(id);
|
|
22
|
+
if (availableIds.has(id) || inlineIds.has(id))
|
|
23
|
+
continue;
|
|
24
|
+
const builtin = builtinSkills.find((s) => s.id === id);
|
|
25
|
+
if (!builtin)
|
|
26
|
+
continue;
|
|
27
|
+
const fields = Object.values(builtin.config).filter((f) => f.env);
|
|
28
|
+
const required = fields.filter((f) => f.required).map((f) => f.env);
|
|
29
|
+
const missingEnv = required.length > 0 ? required : fields.map((f) => f.env);
|
|
30
|
+
if (missingEnv.length === 0)
|
|
31
|
+
continue;
|
|
32
|
+
gaps.push({ id, missingEnv });
|
|
33
|
+
}
|
|
34
|
+
return gaps;
|
|
35
|
+
}
|
|
36
|
+
export function formatSkillEnvGap(gap) {
|
|
37
|
+
return `skill "${gap.id}" needs ${gap.missingEnv.join(", ")} (set it in .env)`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Lines to print for a failed `validateWorkflowSkills`: one env line per
|
|
41
|
+
* built-in skill with missing env, plus node-level errors that carry extra
|
|
42
|
+
* information (alternatives among several skills, or unknown ids).
|
|
43
|
+
*/
|
|
44
|
+
export function formatMissingSkillLines(validation) {
|
|
45
|
+
const lines = [];
|
|
46
|
+
const envGaps = validation.missing.filter((m) => m.category !== "unknown" && m.missingEnv.length > 0);
|
|
47
|
+
const unknownIds = validation.missing.filter((m) => m.category === "unknown").map((m) => m.id);
|
|
48
|
+
for (const m of envGaps)
|
|
49
|
+
lines.push(formatSkillEnvGap({ id: m.id, missingEnv: m.missingEnv }));
|
|
50
|
+
for (const err of validation.errors) {
|
|
51
|
+
const listed = /needs one of: (.+)\)$/.exec(err)?.[1]?.split(", ") ?? [];
|
|
52
|
+
const involvesUnknown = listed.some((id) => unknownIds.includes(id));
|
|
53
|
+
if (involvesUnknown || listed.length > 1)
|
|
54
|
+
lines.push(err);
|
|
55
|
+
}
|
|
56
|
+
return lines;
|
|
57
|
+
}
|
|
58
|
+
/** Warnings for `workflow validate`: referenced skills whose env is missing. */
|
|
59
|
+
export function skillEnvWarnings(workflow, env, available) {
|
|
60
|
+
const availableIds = new Set(available.filter((s) => isSkillConfigured(s, env)).map((s) => s.id));
|
|
61
|
+
const ids = Object.values(workflow.nodes).flatMap((n) => n.skills);
|
|
62
|
+
const inline = new Set(Object.keys(workflow.skills ?? {}));
|
|
63
|
+
return findSkillEnvGaps(ids, availableIds, inline).map(formatSkillEnvGap);
|
|
64
|
+
}
|