bosun 0.36.0 → 0.36.2
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/.env.example +98 -16
- package/README.md +27 -0
- package/agent-event-bus.mjs +5 -5
- package/agent-pool.mjs +129 -12
- package/agent-prompts.mjs +7 -1
- package/agent-sdk.mjs +13 -2
- package/agent-supervisor.mjs +2 -2
- package/agent-work-report.mjs +1 -1
- package/anomaly-detector.mjs +6 -6
- package/autofix.mjs +15 -15
- package/bosun-skills.mjs +4 -4
- package/bosun.schema.json +160 -4
- package/claude-shell.mjs +11 -11
- package/cli.mjs +21 -21
- package/codex-config.mjs +19 -19
- package/codex-shell.mjs +180 -29
- package/config-doctor.mjs +27 -2
- package/config.mjs +60 -7
- package/copilot-shell.mjs +4 -4
- package/error-detector.mjs +1 -1
- package/fleet-coordinator.mjs +2 -2
- package/gemini-shell.mjs +692 -0
- package/github-oauth-portal.mjs +1 -1
- package/github-reconciler.mjs +2 -2
- package/kanban-adapter.mjs +741 -168
- package/merge-strategy.mjs +25 -25
- package/monitor.mjs +123 -105
- package/opencode-shell.mjs +22 -22
- package/package.json +7 -1
- package/postinstall.mjs +22 -22
- package/pr-cleanup-daemon.mjs +6 -6
- package/prepublish-check.mjs +4 -4
- package/presence.mjs +2 -2
- package/primary-agent.mjs +85 -7
- package/publish.mjs +1 -1
- package/review-agent.mjs +1 -1
- package/session-tracker.mjs +11 -0
- package/setup-web-server.mjs +429 -21
- package/setup.mjs +367 -12
- package/shared-knowledge.mjs +1 -1
- package/startup-service.mjs +9 -9
- package/stream-resilience.mjs +58 -4
- package/sync-engine.mjs +2 -2
- package/task-assessment.mjs +9 -9
- package/task-cli.mjs +1 -1
- package/task-complexity.mjs +71 -2
- package/task-context.mjs +1 -2
- package/task-executor.mjs +104 -41
- package/telegram-bot.mjs +825 -494
- package/telegram-sentinel.mjs +28 -28
- package/ui/app.js +256 -23
- package/ui/app.monolith.js +1 -1
- package/ui/components/agent-selector.js +4 -3
- package/ui/components/chat-view.js +101 -28
- package/ui/components/diff-viewer.js +3 -3
- package/ui/components/kanban-board.js +3 -3
- package/ui/components/session-list.js +255 -35
- package/ui/components/workspace-switcher.js +3 -3
- package/ui/demo.html +209 -194
- package/ui/index.html +3 -3
- package/ui/modules/icon-utils.js +206 -142
- package/ui/modules/icons.js +2 -27
- package/ui/modules/settings-schema.js +29 -5
- package/ui/modules/streaming.js +30 -2
- package/ui/modules/vision-stream.js +275 -0
- package/ui/modules/voice-client.js +102 -9
- package/ui/modules/voice-fallback.js +62 -6
- package/ui/modules/voice-overlay.js +594 -59
- package/ui/modules/voice.js +31 -38
- package/ui/setup.html +284 -34
- package/ui/styles/components.css +47 -0
- package/ui/styles/sessions.css +75 -0
- package/ui/tabs/agents.js +73 -43
- package/ui/tabs/chat.js +37 -40
- package/ui/tabs/control.js +2 -2
- package/ui/tabs/dashboard.js +1 -1
- package/ui/tabs/infra.js +10 -10
- package/ui/tabs/library.js +8 -8
- package/ui/tabs/logs.js +10 -10
- package/ui/tabs/settings.js +20 -20
- package/ui/tabs/tasks.js +76 -47
- package/ui-server.mjs +1761 -124
- package/update-check.mjs +13 -13
- package/ve-kanban.mjs +1 -1
- package/whatsapp-channel.mjs +5 -5
- package/workflow-engine.mjs +20 -1
- package/workflow-nodes.mjs +904 -4
- package/workflow-templates/agents.mjs +321 -7
- package/workflow-templates/ci-cd.mjs +6 -6
- package/workflow-templates/github.mjs +156 -84
- package/workflow-templates/planning.mjs +8 -8
- package/workflow-templates/reliability.mjs +8 -8
- package/workflow-templates/security.mjs +3 -3
- package/workflow-templates.mjs +15 -9
- package/workspace-manager.mjs +85 -1
- package/workspace-monitor.mjs +2 -2
- package/workspace-registry.mjs +2 -2
- package/worktree-manager.mjs +1 -1
package/workflow-nodes.mjs
CHANGED
|
@@ -194,6 +194,28 @@ function summarizeAssistantUsage(data = {}) {
|
|
|
194
194
|
return `Usage: ${parts.join(" · ")}`;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
async function createKanbanTaskWithProject(kanban, taskData = {}, projectIdValue = "") {
|
|
198
|
+
if (!kanban || typeof kanban.createTask !== "function") {
|
|
199
|
+
throw new Error("Kanban adapter not available");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const payload =
|
|
203
|
+
taskData && typeof taskData === "object" ? { ...taskData } : {};
|
|
204
|
+
const resolvedProjectId = String(projectIdValue || payload.projectId || "").trim();
|
|
205
|
+
|
|
206
|
+
if (resolvedProjectId) {
|
|
207
|
+
payload.projectId = resolvedProjectId;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (kanban.createTask.length >= 2) {
|
|
211
|
+
const taskPayload = { ...payload };
|
|
212
|
+
delete taskPayload.projectId;
|
|
213
|
+
return kanban.createTask(resolvedProjectId, taskPayload);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return kanban.createTask(payload);
|
|
217
|
+
}
|
|
218
|
+
|
|
197
219
|
function summarizeAssistantMessageData(data = {}) {
|
|
198
220
|
const messageText = normalizeNarrativeText(
|
|
199
221
|
extractStreamText(data?.content) ||
|
|
@@ -533,6 +555,162 @@ function normalizeLegacyWorkflowCommand(command) {
|
|
|
533
555
|
return normalized;
|
|
534
556
|
}
|
|
535
557
|
|
|
558
|
+
function resolveWorkflowNodeValue(value, ctx) {
|
|
559
|
+
if (typeof value === "string") return ctx.resolve(value);
|
|
560
|
+
if (Array.isArray(value)) {
|
|
561
|
+
return value.map((item) => resolveWorkflowNodeValue(item, ctx));
|
|
562
|
+
}
|
|
563
|
+
if (value && typeof value === "object") {
|
|
564
|
+
const resolved = {};
|
|
565
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
566
|
+
resolved[key] = resolveWorkflowNodeValue(entry, ctx);
|
|
567
|
+
}
|
|
568
|
+
return resolved;
|
|
569
|
+
}
|
|
570
|
+
return value;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function parseBooleanSetting(value, defaultValue = false) {
|
|
574
|
+
if (value == null || value === "") return defaultValue;
|
|
575
|
+
if (typeof value === "boolean") return value;
|
|
576
|
+
if (typeof value === "number") return value !== 0;
|
|
577
|
+
if (typeof value === "string") {
|
|
578
|
+
const normalized = value.trim().toLowerCase();
|
|
579
|
+
if (!normalized) return defaultValue;
|
|
580
|
+
if (["true", "1", "yes", "y", "on"].includes(normalized)) return true;
|
|
581
|
+
if (["false", "0", "no", "n", "off"].includes(normalized)) return false;
|
|
582
|
+
}
|
|
583
|
+
return defaultValue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function getPathValue(value, pathExpression) {
|
|
587
|
+
const path = String(pathExpression || "").trim();
|
|
588
|
+
if (!path) return undefined;
|
|
589
|
+
const parts = path
|
|
590
|
+
.split(".")
|
|
591
|
+
.map((part) => String(part || "").trim())
|
|
592
|
+
.filter(Boolean);
|
|
593
|
+
if (parts.length === 0) return undefined;
|
|
594
|
+
|
|
595
|
+
let cursor = value;
|
|
596
|
+
for (const part of parts) {
|
|
597
|
+
if (cursor == null) return undefined;
|
|
598
|
+
if (Array.isArray(cursor)) {
|
|
599
|
+
const idx = Number.parseInt(part, 10);
|
|
600
|
+
if (!Number.isFinite(idx)) return undefined;
|
|
601
|
+
cursor = cursor[idx];
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (typeof cursor !== "object") return undefined;
|
|
605
|
+
cursor = cursor[part];
|
|
606
|
+
}
|
|
607
|
+
return cursor;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function collectWakePhraseCandidates(payload, payloadField = "") {
|
|
611
|
+
const candidates = [];
|
|
612
|
+
const seen = new Set();
|
|
613
|
+
|
|
614
|
+
const appendCandidate = (field, rawValue) => {
|
|
615
|
+
if (rawValue == null) return;
|
|
616
|
+
if (Array.isArray(rawValue)) {
|
|
617
|
+
rawValue.forEach((entry, idx) => appendCandidate(`${field}[${idx}]`, entry));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (typeof rawValue === "object") {
|
|
621
|
+
if (typeof rawValue.content === "string") {
|
|
622
|
+
appendCandidate(`${field}.content`, rawValue.content);
|
|
623
|
+
}
|
|
624
|
+
if (typeof rawValue.text === "string") {
|
|
625
|
+
appendCandidate(`${field}.text`, rawValue.text);
|
|
626
|
+
}
|
|
627
|
+
if (typeof rawValue.transcript === "string") {
|
|
628
|
+
appendCandidate(`${field}.transcript`, rawValue.transcript);
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const text = String(rawValue).trim();
|
|
634
|
+
if (!text) return;
|
|
635
|
+
const key = `${field}::${text}`;
|
|
636
|
+
if (seen.has(key)) return;
|
|
637
|
+
seen.add(key);
|
|
638
|
+
candidates.push({ field, text });
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
if (payloadField) {
|
|
642
|
+
appendCandidate(payloadField, getPathValue(payload, payloadField));
|
|
643
|
+
return candidates;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const commonFields = [
|
|
647
|
+
"content",
|
|
648
|
+
"text",
|
|
649
|
+
"transcript",
|
|
650
|
+
"message",
|
|
651
|
+
"utterance",
|
|
652
|
+
"payload.content",
|
|
653
|
+
"payload.text",
|
|
654
|
+
"payload.transcript",
|
|
655
|
+
"event.content",
|
|
656
|
+
"event.text",
|
|
657
|
+
"event.transcript",
|
|
658
|
+
"voice.content",
|
|
659
|
+
"voice.transcript",
|
|
660
|
+
"meta.transcript",
|
|
661
|
+
];
|
|
662
|
+
for (const field of commonFields) {
|
|
663
|
+
appendCandidate(field, getPathValue(payload, field));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
667
|
+
messages.forEach((entry, idx) => appendCandidate(`messages[${idx}]`, entry));
|
|
668
|
+
|
|
669
|
+
const transcriptEvents = Array.isArray(payload?.transcriptEvents) ? payload.transcriptEvents : [];
|
|
670
|
+
transcriptEvents.forEach((entry, idx) => appendCandidate(`transcriptEvents[${idx}]`, entry));
|
|
671
|
+
|
|
672
|
+
return candidates;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function detectWakePhraseMatch(text, phrase, options = {}) {
|
|
676
|
+
const mode = String(options.mode || "contains").trim().toLowerCase() || "contains";
|
|
677
|
+
const caseSensitive = options.caseSensitive === true;
|
|
678
|
+
const source = String(text || "");
|
|
679
|
+
const target = String(phrase || "");
|
|
680
|
+
|
|
681
|
+
if (!source || !target) return { matched: false, mode };
|
|
682
|
+
|
|
683
|
+
const sourceNormalized = caseSensitive ? source : source.toLowerCase();
|
|
684
|
+
const targetNormalized = caseSensitive ? target : target.toLowerCase();
|
|
685
|
+
|
|
686
|
+
if (mode === "exact") {
|
|
687
|
+
return { matched: sourceNormalized.trim() === targetNormalized.trim(), mode };
|
|
688
|
+
}
|
|
689
|
+
if (mode === "starts_with") {
|
|
690
|
+
return { matched: sourceNormalized.trimStart().startsWith(targetNormalized), mode };
|
|
691
|
+
}
|
|
692
|
+
if (mode === "regex") {
|
|
693
|
+
try {
|
|
694
|
+
const regex = new RegExp(target, caseSensitive ? "" : "i");
|
|
695
|
+
return { matched: regex.test(source), mode };
|
|
696
|
+
} catch (err) {
|
|
697
|
+
return {
|
|
698
|
+
matched: false,
|
|
699
|
+
mode,
|
|
700
|
+
error: `invalid regex: ${err?.message || err}`,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return { matched: sourceNormalized.includes(targetNormalized), mode: "contains" };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function normalizeWorkflowStack(value) {
|
|
708
|
+
if (!Array.isArray(value)) return [];
|
|
709
|
+
return value
|
|
710
|
+
.map((entry) => String(entry || "").trim())
|
|
711
|
+
.filter(Boolean);
|
|
712
|
+
}
|
|
713
|
+
|
|
536
714
|
function isBosunStateComment(text) {
|
|
537
715
|
const raw = String(text || "").toLowerCase();
|
|
538
716
|
return raw.includes("bosun-state") || raw.includes("codex:ignore");
|
|
@@ -682,6 +860,165 @@ registerNodeType("trigger.event", {
|
|
|
682
860
|
},
|
|
683
861
|
});
|
|
684
862
|
|
|
863
|
+
registerNodeType("trigger.meeting.wake_phrase", {
|
|
864
|
+
describe: () => "Fires when a transcript/event payload contains the configured wake phrase",
|
|
865
|
+
schema: {
|
|
866
|
+
type: "object",
|
|
867
|
+
properties: {
|
|
868
|
+
wakePhrase: { type: "string", description: "Wake phrase to match (alias: phrase)" },
|
|
869
|
+
phrase: { type: "string", description: "Alias for wakePhrase" },
|
|
870
|
+
mode: {
|
|
871
|
+
type: "string",
|
|
872
|
+
enum: ["contains", "starts_with", "exact", "regex"],
|
|
873
|
+
default: "contains",
|
|
874
|
+
},
|
|
875
|
+
caseSensitive: { type: "boolean", default: false },
|
|
876
|
+
text: {
|
|
877
|
+
type: "string",
|
|
878
|
+
description: "Optional explicit text to inspect before payload-derived fields",
|
|
879
|
+
},
|
|
880
|
+
payloadField: {
|
|
881
|
+
type: "string",
|
|
882
|
+
description: "Optional payload path to inspect (e.g. content, payload.transcript)",
|
|
883
|
+
},
|
|
884
|
+
sessionId: { type: "string", description: "Optional sessionId filter" },
|
|
885
|
+
role: { type: "string", description: "Optional role filter (user|assistant|system)" },
|
|
886
|
+
failOnInvalidRegex: {
|
|
887
|
+
type: "boolean",
|
|
888
|
+
default: false,
|
|
889
|
+
description: "Throw when regex mode is invalid instead of soft-failing",
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
async execute(node, ctx) {
|
|
894
|
+
const eventData = ctx.data && typeof ctx.data === "object" ? ctx.data : {};
|
|
895
|
+
const resolveValue = (value) => (
|
|
896
|
+
typeof ctx?.resolve === "function" ? ctx.resolve(value) : value
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
const wakePhrase = String(
|
|
900
|
+
resolveValue(node.config?.wakePhrase || node.config?.phrase || eventData?.wakePhrase || ""),
|
|
901
|
+
).trim();
|
|
902
|
+
if (!wakePhrase) {
|
|
903
|
+
return { triggered: false, reason: "wake_phrase_missing" };
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const expectedSessionId = String(resolveValue(node.config?.sessionId || "")).trim();
|
|
907
|
+
const actualSessionId = String(
|
|
908
|
+
eventData?.sessionId || eventData?.meetingSessionId || eventData?.session?.id || "",
|
|
909
|
+
).trim();
|
|
910
|
+
if (expectedSessionId) {
|
|
911
|
+
if (!actualSessionId) {
|
|
912
|
+
return {
|
|
913
|
+
triggered: false,
|
|
914
|
+
reason: "session_missing",
|
|
915
|
+
expectedSessionId,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
if (expectedSessionId !== actualSessionId) {
|
|
919
|
+
return {
|
|
920
|
+
triggered: false,
|
|
921
|
+
reason: "session_mismatch",
|
|
922
|
+
expectedSessionId,
|
|
923
|
+
sessionId: actualSessionId,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const expectedRole = String(resolveValue(node.config?.role || "")).trim().toLowerCase();
|
|
929
|
+
const actualRole = String(
|
|
930
|
+
eventData?.role || eventData?.speakerRole || eventData?.participantRole || "",
|
|
931
|
+
).trim().toLowerCase();
|
|
932
|
+
if (expectedRole) {
|
|
933
|
+
if (!actualRole) {
|
|
934
|
+
return {
|
|
935
|
+
triggered: false,
|
|
936
|
+
reason: "role_missing",
|
|
937
|
+
expectedRole,
|
|
938
|
+
sessionId: actualSessionId || null,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
if (expectedRole !== actualRole) {
|
|
942
|
+
return {
|
|
943
|
+
triggered: false,
|
|
944
|
+
reason: "role_mismatch",
|
|
945
|
+
expectedRole,
|
|
946
|
+
role: actualRole,
|
|
947
|
+
sessionId: actualSessionId || null,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const payloadField = String(resolveValue(node.config?.payloadField || "")).trim();
|
|
953
|
+
const configuredText = String(resolveValue(node.config?.text || "") || "").trim();
|
|
954
|
+
const candidates = configuredText
|
|
955
|
+
? [{ field: "text", text: configuredText }]
|
|
956
|
+
: [];
|
|
957
|
+
candidates.push(...collectWakePhraseCandidates(eventData, payloadField));
|
|
958
|
+
if (!candidates.length) {
|
|
959
|
+
return {
|
|
960
|
+
triggered: false,
|
|
961
|
+
reason: "payload_missing",
|
|
962
|
+
wakePhrase,
|
|
963
|
+
sessionId: actualSessionId || null,
|
|
964
|
+
role: actualRole || null,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const mode = String(resolveValue(node.config?.mode || "contains")).trim().toLowerCase() || "contains";
|
|
969
|
+
const caseSensitive = parseBooleanSetting(
|
|
970
|
+
resolveValue(node.config?.caseSensitive ?? false),
|
|
971
|
+
false,
|
|
972
|
+
);
|
|
973
|
+
const failOnInvalidRegex = parseBooleanSetting(
|
|
974
|
+
resolveValue(node.config?.failOnInvalidRegex ?? false),
|
|
975
|
+
false,
|
|
976
|
+
);
|
|
977
|
+
|
|
978
|
+
for (const candidate of candidates) {
|
|
979
|
+
const matched = detectWakePhraseMatch(candidate.text, wakePhrase, {
|
|
980
|
+
mode,
|
|
981
|
+
caseSensitive,
|
|
982
|
+
});
|
|
983
|
+
if (matched.error) {
|
|
984
|
+
if (failOnInvalidRegex) {
|
|
985
|
+
throw new Error(`trigger.meeting.wake_phrase: ${matched.error}`);
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
triggered: false,
|
|
989
|
+
reason: "invalid_regex",
|
|
990
|
+
error: matched.error,
|
|
991
|
+
wakePhrase,
|
|
992
|
+
mode,
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
if (matched.matched) {
|
|
996
|
+
return {
|
|
997
|
+
triggered: true,
|
|
998
|
+
wakePhrase,
|
|
999
|
+
mode: matched.mode,
|
|
1000
|
+
sessionId: actualSessionId || null,
|
|
1001
|
+
role: actualRole || null,
|
|
1002
|
+
matchedField: candidate.field,
|
|
1003
|
+
matchedText: candidate.text.length > 240
|
|
1004
|
+
? `${candidate.text.slice(0, 237)}...`
|
|
1005
|
+
: candidate.text,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
return {
|
|
1011
|
+
triggered: false,
|
|
1012
|
+
reason: "wake_phrase_not_found",
|
|
1013
|
+
wakePhrase,
|
|
1014
|
+
mode,
|
|
1015
|
+
sessionId: actualSessionId || null,
|
|
1016
|
+
role: actualRole || null,
|
|
1017
|
+
inspectedFields: candidates.slice(0, 12).map((entry) => entry.field),
|
|
1018
|
+
};
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
1021
|
+
|
|
685
1022
|
registerNodeType("trigger.webhook", {
|
|
686
1023
|
describe: () => "Fires when a webhook is received at the workflow's endpoint",
|
|
687
1024
|
schema: {
|
|
@@ -1237,6 +1574,569 @@ registerNodeType("action.run_command", {
|
|
|
1237
1574
|
},
|
|
1238
1575
|
});
|
|
1239
1576
|
|
|
1577
|
+
registerNodeType("action.execute_workflow", {
|
|
1578
|
+
describe: () => "Execute another workflow by ID (synchronously or dispatch mode)",
|
|
1579
|
+
schema: {
|
|
1580
|
+
type: "object",
|
|
1581
|
+
properties: {
|
|
1582
|
+
workflowId: { type: "string", description: "Workflow ID to execute" },
|
|
1583
|
+
mode: { type: "string", enum: ["sync", "dispatch"], default: "sync" },
|
|
1584
|
+
input: {
|
|
1585
|
+
type: "object",
|
|
1586
|
+
description: "Input payload passed to the child workflow",
|
|
1587
|
+
additionalProperties: true,
|
|
1588
|
+
},
|
|
1589
|
+
inheritContext: {
|
|
1590
|
+
type: "boolean",
|
|
1591
|
+
default: false,
|
|
1592
|
+
description: "Copy parent workflow context data into child input before applying input overrides",
|
|
1593
|
+
},
|
|
1594
|
+
includeKeys: {
|
|
1595
|
+
type: "array",
|
|
1596
|
+
items: { type: "string" },
|
|
1597
|
+
description: "Optional allow-list of context keys to inherit when inheritContext=true",
|
|
1598
|
+
},
|
|
1599
|
+
outputVariable: {
|
|
1600
|
+
type: "string",
|
|
1601
|
+
description: "Optional context key to store execution summary output",
|
|
1602
|
+
},
|
|
1603
|
+
failOnChildError: {
|
|
1604
|
+
type: "boolean",
|
|
1605
|
+
default: true,
|
|
1606
|
+
description: "In sync mode, throw when child workflow completes with errors",
|
|
1607
|
+
},
|
|
1608
|
+
allowRecursive: {
|
|
1609
|
+
type: "boolean",
|
|
1610
|
+
default: false,
|
|
1611
|
+
description: "Allow recursive workflow execution when true",
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1614
|
+
required: ["workflowId"],
|
|
1615
|
+
},
|
|
1616
|
+
async execute(node, ctx, engine) {
|
|
1617
|
+
const workflowId = String(ctx.resolve(node.config?.workflowId || "") || "").trim();
|
|
1618
|
+
const modeRaw = String(ctx.resolve(node.config?.mode || "sync") || "sync")
|
|
1619
|
+
.trim()
|
|
1620
|
+
.toLowerCase();
|
|
1621
|
+
const mode = modeRaw || "sync";
|
|
1622
|
+
const outputVariable = String(ctx.resolve(node.config?.outputVariable || "") || "").trim();
|
|
1623
|
+
const inheritContext = parseBooleanSetting(
|
|
1624
|
+
resolveWorkflowNodeValue(node.config?.inheritContext ?? false, ctx),
|
|
1625
|
+
false,
|
|
1626
|
+
);
|
|
1627
|
+
const failOnChildError = parseBooleanSetting(
|
|
1628
|
+
resolveWorkflowNodeValue(node.config?.failOnChildError ?? true, ctx),
|
|
1629
|
+
true,
|
|
1630
|
+
);
|
|
1631
|
+
const allowRecursive = parseBooleanSetting(
|
|
1632
|
+
resolveWorkflowNodeValue(node.config?.allowRecursive ?? false, ctx),
|
|
1633
|
+
false,
|
|
1634
|
+
);
|
|
1635
|
+
const includeKeys = Array.isArray(node.config?.includeKeys)
|
|
1636
|
+
? node.config.includeKeys
|
|
1637
|
+
.map((value) => String(resolveWorkflowNodeValue(value, ctx) || "").trim())
|
|
1638
|
+
.filter(Boolean)
|
|
1639
|
+
: [];
|
|
1640
|
+
|
|
1641
|
+
if (!workflowId) {
|
|
1642
|
+
throw new Error("action.execute_workflow: 'workflowId' is required");
|
|
1643
|
+
}
|
|
1644
|
+
if (mode !== "sync" && mode !== "dispatch") {
|
|
1645
|
+
throw new Error(`action.execute_workflow: invalid mode "${mode}". Expected "sync" or "dispatch".`);
|
|
1646
|
+
}
|
|
1647
|
+
if (!engine || typeof engine.execute !== "function") {
|
|
1648
|
+
throw new Error("action.execute_workflow: workflow engine is not available");
|
|
1649
|
+
}
|
|
1650
|
+
if (typeof engine.get === "function" && !engine.get(workflowId)) {
|
|
1651
|
+
throw new Error(`action.execute_workflow: workflow "${workflowId}" not found`);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const resolvedInputConfig = resolveWorkflowNodeValue(node.config?.input ?? {}, ctx);
|
|
1655
|
+
if (
|
|
1656
|
+
resolvedInputConfig != null &&
|
|
1657
|
+
(typeof resolvedInputConfig !== "object" || Array.isArray(resolvedInputConfig))
|
|
1658
|
+
) {
|
|
1659
|
+
throw new Error("action.execute_workflow: 'input' must resolve to an object");
|
|
1660
|
+
}
|
|
1661
|
+
const configuredInput =
|
|
1662
|
+
resolvedInputConfig && typeof resolvedInputConfig === "object"
|
|
1663
|
+
? resolvedInputConfig
|
|
1664
|
+
: {};
|
|
1665
|
+
|
|
1666
|
+
const sourceData =
|
|
1667
|
+
ctx.data && typeof ctx.data === "object"
|
|
1668
|
+
? ctx.data
|
|
1669
|
+
: {};
|
|
1670
|
+
const inheritedInput = {};
|
|
1671
|
+
if (inheritContext) {
|
|
1672
|
+
if (includeKeys.length > 0) {
|
|
1673
|
+
for (const key of includeKeys) {
|
|
1674
|
+
if (Object.prototype.hasOwnProperty.call(sourceData, key)) {
|
|
1675
|
+
inheritedInput[key] = sourceData[key];
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
} else {
|
|
1679
|
+
Object.assign(inheritedInput, sourceData);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const parentWorkflowId = String(ctx.data?._workflowId || "").trim();
|
|
1684
|
+
const workflowStack = normalizeWorkflowStack(ctx.data?._workflowStack);
|
|
1685
|
+
if (parentWorkflowId && workflowStack[workflowStack.length - 1] !== parentWorkflowId) {
|
|
1686
|
+
workflowStack.push(parentWorkflowId);
|
|
1687
|
+
}
|
|
1688
|
+
if (!allowRecursive && workflowStack.includes(workflowId)) {
|
|
1689
|
+
const cyclePath = [...workflowStack, workflowId].join(" -> ");
|
|
1690
|
+
throw new Error(
|
|
1691
|
+
`action.execute_workflow: recursive workflow call blocked (${cyclePath}). ` +
|
|
1692
|
+
"Set allowRecursive=true to override.",
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
const childInput = {
|
|
1697
|
+
...inheritedInput,
|
|
1698
|
+
...configuredInput,
|
|
1699
|
+
_workflowStack: [...workflowStack, workflowId],
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
if (mode === "dispatch") {
|
|
1703
|
+
ctx.log(node.id, `Dispatching workflow "${workflowId}"`);
|
|
1704
|
+
const dispatched = engine.execute(workflowId, childInput);
|
|
1705
|
+
dispatched
|
|
1706
|
+
.then((childCtx) => {
|
|
1707
|
+
const status = childCtx?.errors?.length ? "failed" : "completed";
|
|
1708
|
+
ctx.log(node.id, `Dispatched workflow "${workflowId}" finished with status=${status}`);
|
|
1709
|
+
})
|
|
1710
|
+
.catch((err) => {
|
|
1711
|
+
ctx.log(node.id, `Dispatched workflow "${workflowId}" failed: ${err.message}`, "error");
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
const output = {
|
|
1715
|
+
success: true,
|
|
1716
|
+
queued: true,
|
|
1717
|
+
mode: "dispatch",
|
|
1718
|
+
workflowId,
|
|
1719
|
+
parentRunId: ctx.id,
|
|
1720
|
+
stackDepth: childInput._workflowStack.length,
|
|
1721
|
+
};
|
|
1722
|
+
if (outputVariable) {
|
|
1723
|
+
ctx.data[outputVariable] = output;
|
|
1724
|
+
}
|
|
1725
|
+
return output;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
ctx.log(node.id, `Executing workflow "${workflowId}" (sync)`);
|
|
1729
|
+
const childCtx = await engine.execute(workflowId, childInput);
|
|
1730
|
+
const childErrors = Array.isArray(childCtx?.errors)
|
|
1731
|
+
? childCtx.errors.map((entry) => ({
|
|
1732
|
+
nodeId: entry?.nodeId || null,
|
|
1733
|
+
error: String(entry?.error || "unknown child workflow error"),
|
|
1734
|
+
}))
|
|
1735
|
+
: [];
|
|
1736
|
+
const status = childErrors.length > 0 ? "failed" : "completed";
|
|
1737
|
+
const output = {
|
|
1738
|
+
success: status === "completed",
|
|
1739
|
+
queued: false,
|
|
1740
|
+
mode: "sync",
|
|
1741
|
+
workflowId,
|
|
1742
|
+
runId: childCtx?.id || null,
|
|
1743
|
+
status,
|
|
1744
|
+
errorCount: childErrors.length,
|
|
1745
|
+
errors: childErrors,
|
|
1746
|
+
};
|
|
1747
|
+
|
|
1748
|
+
if (outputVariable) {
|
|
1749
|
+
ctx.data[outputVariable] = output;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (status === "failed" && failOnChildError) {
|
|
1753
|
+
const reason = childErrors[0]?.error || "child workflow failed";
|
|
1754
|
+
const err = new Error(`action.execute_workflow: child workflow "${workflowId}" failed: ${reason}`);
|
|
1755
|
+
err.childWorkflow = output;
|
|
1756
|
+
throw err;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
return output;
|
|
1760
|
+
},
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
registerNodeType("meeting.start", {
|
|
1764
|
+
describe: () => "Create or reuse a meeting session for workflow-driven voice/video orchestration",
|
|
1765
|
+
schema: {
|
|
1766
|
+
type: "object",
|
|
1767
|
+
properties: {
|
|
1768
|
+
sessionId: { type: "string", description: "Optional session ID (auto-generated when empty)" },
|
|
1769
|
+
title: { type: "string", description: "Optional human-readable session title" },
|
|
1770
|
+
executor: { type: "string", description: "Preferred executor for this meeting session" },
|
|
1771
|
+
mode: { type: "string", description: "Preferred agent mode for this meeting session" },
|
|
1772
|
+
model: { type: "string", description: "Preferred model override for this meeting session" },
|
|
1773
|
+
wakePhrase: { type: "string", description: "Optional wake phrase metadata for downstream workflow logic" },
|
|
1774
|
+
metadata: { type: "object", description: "Additional metadata stored with the meeting session" },
|
|
1775
|
+
activate: { type: "boolean", default: true, description: "Mark meeting session active after creation/reuse" },
|
|
1776
|
+
maxMessages: { type: "number", description: "Optional session max message retention override" },
|
|
1777
|
+
failOnError: { type: "boolean", default: true, description: "Throw when meeting setup fails" },
|
|
1778
|
+
},
|
|
1779
|
+
},
|
|
1780
|
+
async execute(node, ctx, engine) {
|
|
1781
|
+
const meeting = engine.services?.meeting;
|
|
1782
|
+
if (!meeting || typeof meeting.startMeeting !== "function") {
|
|
1783
|
+
throw new Error("Meeting service is not available");
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
const failOnError = parseBooleanSetting(resolveWorkflowNodeValue(node.config?.failOnError ?? true, ctx), true);
|
|
1787
|
+
try {
|
|
1788
|
+
const sessionId = String(
|
|
1789
|
+
ctx.resolve(node.config?.sessionId || ctx.data?.meetingSessionId || ctx.data?.sessionId || ""),
|
|
1790
|
+
).trim() || undefined;
|
|
1791
|
+
const title = String(ctx.resolve(node.config?.title || "") || "").trim() || undefined;
|
|
1792
|
+
const executor = String(ctx.resolve(node.config?.executor || "") || "").trim() || undefined;
|
|
1793
|
+
const mode = String(ctx.resolve(node.config?.mode || "") || "").trim() || undefined;
|
|
1794
|
+
const model = String(ctx.resolve(node.config?.model || "") || "").trim() || undefined;
|
|
1795
|
+
const wakePhrase = String(ctx.resolve(node.config?.wakePhrase || "") || "").trim() || undefined;
|
|
1796
|
+
const metadataInput = resolveWorkflowNodeValue(node.config?.metadata || {}, ctx);
|
|
1797
|
+
const metadata =
|
|
1798
|
+
metadataInput && typeof metadataInput === "object" && !Array.isArray(metadataInput)
|
|
1799
|
+
? { ...metadataInput }
|
|
1800
|
+
: {};
|
|
1801
|
+
if (title) metadata.title = title;
|
|
1802
|
+
if (wakePhrase) metadata.wakePhrase = wakePhrase;
|
|
1803
|
+
|
|
1804
|
+
const activate = parseBooleanSetting(resolveWorkflowNodeValue(node.config?.activate ?? true, ctx), true);
|
|
1805
|
+
const maxMessagesRaw = Number(resolveWorkflowNodeValue(node.config?.maxMessages, ctx));
|
|
1806
|
+
const maxMessages = Number.isFinite(maxMessagesRaw) && maxMessagesRaw > 0
|
|
1807
|
+
? Math.trunc(maxMessagesRaw)
|
|
1808
|
+
: undefined;
|
|
1809
|
+
|
|
1810
|
+
const result = await meeting.startMeeting({
|
|
1811
|
+
sessionId,
|
|
1812
|
+
metadata,
|
|
1813
|
+
agent: executor,
|
|
1814
|
+
mode,
|
|
1815
|
+
model,
|
|
1816
|
+
activate,
|
|
1817
|
+
maxMessages,
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
const activeSessionId = String(result?.sessionId || sessionId || "").trim() || null;
|
|
1821
|
+
if (activeSessionId) {
|
|
1822
|
+
ctx.data.meetingSessionId = activeSessionId;
|
|
1823
|
+
ctx.data.sessionId = ctx.data.sessionId || activeSessionId;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
return {
|
|
1827
|
+
success: true,
|
|
1828
|
+
sessionId: activeSessionId,
|
|
1829
|
+
created: result?.created === true,
|
|
1830
|
+
session: result?.session || null,
|
|
1831
|
+
voice: result?.voice || null,
|
|
1832
|
+
};
|
|
1833
|
+
} catch (err) {
|
|
1834
|
+
if (failOnError) throw err;
|
|
1835
|
+
return {
|
|
1836
|
+
success: false,
|
|
1837
|
+
error: String(err?.message || err),
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
},
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
registerNodeType("meeting.send", {
|
|
1844
|
+
describe: () => "Send a meeting message through the meeting session dispatcher",
|
|
1845
|
+
schema: {
|
|
1846
|
+
type: "object",
|
|
1847
|
+
properties: {
|
|
1848
|
+
sessionId: { type: "string", description: "Meeting session ID (defaults to context session)" },
|
|
1849
|
+
message: { type: "string", description: "Message to send into the meeting session" },
|
|
1850
|
+
mode: { type: "string", description: "Optional per-message mode override" },
|
|
1851
|
+
model: { type: "string", description: "Optional per-message model override" },
|
|
1852
|
+
timeoutMs: { type: "number", description: "Optional per-message timeout in ms" },
|
|
1853
|
+
createIfMissing: { type: "boolean", default: true, description: "Create session automatically when missing" },
|
|
1854
|
+
allowInactive: { type: "boolean", default: false, description: "Allow sending when session is inactive" },
|
|
1855
|
+
failOnError: { type: "boolean", default: true, description: "Throw when sending fails" },
|
|
1856
|
+
},
|
|
1857
|
+
required: ["message"],
|
|
1858
|
+
},
|
|
1859
|
+
async execute(node, ctx, engine) {
|
|
1860
|
+
const meeting = engine.services?.meeting;
|
|
1861
|
+
if (!meeting || typeof meeting.sendMeetingMessage !== "function") {
|
|
1862
|
+
throw new Error("Meeting service is not available");
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const failOnError = parseBooleanSetting(resolveWorkflowNodeValue(node.config?.failOnError ?? true, ctx), true);
|
|
1866
|
+
try {
|
|
1867
|
+
const sessionId = String(
|
|
1868
|
+
ctx.resolve(node.config?.sessionId || ctx.data?.meetingSessionId || ctx.data?.sessionId || ""),
|
|
1869
|
+
).trim();
|
|
1870
|
+
if (!sessionId) {
|
|
1871
|
+
throw new Error("meeting.send requires sessionId (configure node.sessionId or run meeting.start first)");
|
|
1872
|
+
}
|
|
1873
|
+
const message = String(ctx.resolve(node.config?.message || "") || "").trim();
|
|
1874
|
+
if (!message) {
|
|
1875
|
+
throw new Error("meeting.send requires message");
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
const mode = String(ctx.resolve(node.config?.mode || "") || "").trim() || undefined;
|
|
1879
|
+
const model = String(ctx.resolve(node.config?.model || "") || "").trim() || undefined;
|
|
1880
|
+
const timeoutMsRaw = Number(resolveWorkflowNodeValue(node.config?.timeoutMs, ctx));
|
|
1881
|
+
const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0
|
|
1882
|
+
? Math.trunc(timeoutMsRaw)
|
|
1883
|
+
: undefined;
|
|
1884
|
+
const createIfMissing = parseBooleanSetting(resolveWorkflowNodeValue(node.config?.createIfMissing ?? true, ctx), true);
|
|
1885
|
+
const allowInactive = parseBooleanSetting(resolveWorkflowNodeValue(node.config?.allowInactive ?? false, ctx), false);
|
|
1886
|
+
|
|
1887
|
+
const result = await meeting.sendMeetingMessage(sessionId, message, {
|
|
1888
|
+
mode,
|
|
1889
|
+
model,
|
|
1890
|
+
timeoutMs,
|
|
1891
|
+
createIfMissing,
|
|
1892
|
+
allowInactive,
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
const nextSessionId = String(result?.sessionId || sessionId).trim();
|
|
1896
|
+
if (nextSessionId) {
|
|
1897
|
+
ctx.data.meetingSessionId = nextSessionId;
|
|
1898
|
+
ctx.data.sessionId = ctx.data.sessionId || nextSessionId;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
return {
|
|
1902
|
+
success: result?.ok !== false,
|
|
1903
|
+
sessionId: nextSessionId || null,
|
|
1904
|
+
messageId: result?.messageId || null,
|
|
1905
|
+
status: result?.status || null,
|
|
1906
|
+
responseText: result?.responseText || "",
|
|
1907
|
+
adapter: result?.adapter || null,
|
|
1908
|
+
observedEventCount: Number(result?.observedEventCount || 0),
|
|
1909
|
+
};
|
|
1910
|
+
} catch (err) {
|
|
1911
|
+
if (failOnError) throw err;
|
|
1912
|
+
return {
|
|
1913
|
+
success: false,
|
|
1914
|
+
error: String(err?.message || err),
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
registerNodeType("meeting.transcript", {
|
|
1921
|
+
describe: () => "Fetch meeting transcript pages and optionally project as plain text",
|
|
1922
|
+
schema: {
|
|
1923
|
+
type: "object",
|
|
1924
|
+
properties: {
|
|
1925
|
+
sessionId: { type: "string", description: "Meeting session ID (defaults to context session)" },
|
|
1926
|
+
page: { type: "number", default: 1 },
|
|
1927
|
+
pageSize: { type: "number", default: 200 },
|
|
1928
|
+
includeMessages: { type: "boolean", default: true, description: "Include structured message array in output" },
|
|
1929
|
+
failOnError: { type: "boolean", default: true, description: "Throw when transcript retrieval fails" },
|
|
1930
|
+
},
|
|
1931
|
+
},
|
|
1932
|
+
async execute(node, ctx, engine) {
|
|
1933
|
+
const meeting = engine.services?.meeting;
|
|
1934
|
+
if (!meeting || typeof meeting.fetchMeetingTranscript !== "function") {
|
|
1935
|
+
throw new Error("Meeting service is not available");
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
const failOnError = parseBooleanSetting(resolveWorkflowNodeValue(node.config?.failOnError ?? true, ctx), true);
|
|
1939
|
+
try {
|
|
1940
|
+
const sessionId = String(
|
|
1941
|
+
ctx.resolve(node.config?.sessionId || ctx.data?.meetingSessionId || ctx.data?.sessionId || ""),
|
|
1942
|
+
).trim();
|
|
1943
|
+
if (!sessionId) {
|
|
1944
|
+
throw new Error("meeting.transcript requires sessionId (configure node.sessionId or run meeting.start first)");
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
const pageRaw = Number(resolveWorkflowNodeValue(node.config?.page ?? 1, ctx));
|
|
1948
|
+
const page = Number.isFinite(pageRaw) && pageRaw > 0 ? Math.trunc(pageRaw) : 1;
|
|
1949
|
+
const pageSizeRaw = Number(resolveWorkflowNodeValue(node.config?.pageSize ?? 200, ctx));
|
|
1950
|
+
const pageSize = Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? Math.trunc(pageSizeRaw) : 200;
|
|
1951
|
+
const includeMessages = parseBooleanSetting(resolveWorkflowNodeValue(node.config?.includeMessages ?? true, ctx), true);
|
|
1952
|
+
|
|
1953
|
+
const transcript = await meeting.fetchMeetingTranscript(sessionId, {
|
|
1954
|
+
page,
|
|
1955
|
+
pageSize,
|
|
1956
|
+
});
|
|
1957
|
+
const messages = Array.isArray(transcript?.messages) ? transcript.messages : [];
|
|
1958
|
+
const transcriptText = messages
|
|
1959
|
+
.map((msg) => {
|
|
1960
|
+
const role = String(msg?.role || msg?.type || "system").trim().toLowerCase();
|
|
1961
|
+
const content = String(msg?.content || "").trim();
|
|
1962
|
+
if (!content) return "";
|
|
1963
|
+
return `${role}: ${content}`;
|
|
1964
|
+
})
|
|
1965
|
+
.filter(Boolean)
|
|
1966
|
+
.join("\n");
|
|
1967
|
+
|
|
1968
|
+
return {
|
|
1969
|
+
success: true,
|
|
1970
|
+
sessionId,
|
|
1971
|
+
status: transcript?.status || null,
|
|
1972
|
+
page: Number(transcript?.page || page),
|
|
1973
|
+
pageSize: Number(transcript?.pageSize || pageSize),
|
|
1974
|
+
totalMessages: Number(transcript?.totalMessages || messages.length),
|
|
1975
|
+
totalPages: Number(transcript?.totalPages || 0),
|
|
1976
|
+
hasNextPage: transcript?.hasNextPage === true,
|
|
1977
|
+
hasPreviousPage: transcript?.hasPreviousPage === true,
|
|
1978
|
+
transcript: transcriptText,
|
|
1979
|
+
messages: includeMessages ? messages : undefined,
|
|
1980
|
+
};
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
if (failOnError) throw err;
|
|
1983
|
+
return {
|
|
1984
|
+
success: false,
|
|
1985
|
+
error: String(err?.message || err),
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
},
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
registerNodeType("meeting.vision", {
|
|
1992
|
+
describe: () => "Analyze a meeting video frame and persist a vision summary",
|
|
1993
|
+
schema: {
|
|
1994
|
+
type: "object",
|
|
1995
|
+
properties: {
|
|
1996
|
+
sessionId: { type: "string", description: "Meeting session ID (defaults to context session)" },
|
|
1997
|
+
frameDataUrl: { type: "string", description: "Base64 data URL for the current frame" },
|
|
1998
|
+
source: { type: "string", enum: ["screen", "camera"], default: "screen" },
|
|
1999
|
+
prompt: { type: "string", description: "Optional per-frame vision prompt override" },
|
|
2000
|
+
visionModel: { type: "string", description: "Optional vision model override" },
|
|
2001
|
+
minIntervalMs: { type: "number", description: "Minimum analysis interval for this session" },
|
|
2002
|
+
forceAnalyze: { type: "boolean", default: false, description: "Bypass dedupe/throttle checks" },
|
|
2003
|
+
width: { type: "number", description: "Optional frame width for transcript context" },
|
|
2004
|
+
height: { type: "number", description: "Optional frame height for transcript context" },
|
|
2005
|
+
executor: { type: "string", description: "Optional executor hint for vision context" },
|
|
2006
|
+
mode: { type: "string", description: "Optional mode hint for vision context" },
|
|
2007
|
+
model: { type: "string", description: "Optional model hint for vision context" },
|
|
2008
|
+
failOnError: { type: "boolean", default: true, description: "Throw when vision analysis fails" },
|
|
2009
|
+
},
|
|
2010
|
+
},
|
|
2011
|
+
async execute(node, ctx, engine) {
|
|
2012
|
+
const meeting = engine.services?.meeting;
|
|
2013
|
+
if (!meeting || typeof meeting.analyzeMeetingFrame !== "function") {
|
|
2014
|
+
throw new Error("Meeting service is not available");
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const failOnError = parseBooleanSetting(resolveWorkflowNodeValue(node.config?.failOnError ?? true, ctx), true);
|
|
2018
|
+
try {
|
|
2019
|
+
const sessionId = String(
|
|
2020
|
+
ctx.resolve(node.config?.sessionId || ctx.data?.meetingSessionId || ctx.data?.sessionId || ""),
|
|
2021
|
+
).trim();
|
|
2022
|
+
if (!sessionId) {
|
|
2023
|
+
throw new Error("meeting.vision requires sessionId (configure node.sessionId or run meeting.start first)");
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
const frameDataUrl = String(
|
|
2027
|
+
ctx.resolve(node.config?.frameDataUrl || ctx.data?.frameDataUrl || ctx.data?.visionFrameDataUrl || ""),
|
|
2028
|
+
).trim();
|
|
2029
|
+
if (!frameDataUrl) {
|
|
2030
|
+
throw new Error("meeting.vision requires frameDataUrl");
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
const source = String(ctx.resolve(node.config?.source || "screen") || "screen").trim() || "screen";
|
|
2034
|
+
const prompt = String(ctx.resolve(node.config?.prompt || "") || "").trim() || undefined;
|
|
2035
|
+
const visionModel = String(ctx.resolve(node.config?.visionModel || "") || "").trim() || undefined;
|
|
2036
|
+
const minIntervalRaw = Number(resolveWorkflowNodeValue(node.config?.minIntervalMs, ctx));
|
|
2037
|
+
const minIntervalMs = Number.isFinite(minIntervalRaw) && minIntervalRaw > 0
|
|
2038
|
+
? Math.trunc(minIntervalRaw)
|
|
2039
|
+
: undefined;
|
|
2040
|
+
const forceAnalyze = parseBooleanSetting(resolveWorkflowNodeValue(node.config?.forceAnalyze ?? false, ctx), false);
|
|
2041
|
+
const widthRaw = Number(resolveWorkflowNodeValue(node.config?.width, ctx));
|
|
2042
|
+
const heightRaw = Number(resolveWorkflowNodeValue(node.config?.height, ctx));
|
|
2043
|
+
const width = Number.isFinite(widthRaw) && widthRaw > 0 ? Math.trunc(widthRaw) : undefined;
|
|
2044
|
+
const height = Number.isFinite(heightRaw) && heightRaw > 0 ? Math.trunc(heightRaw) : undefined;
|
|
2045
|
+
const executor = String(ctx.resolve(node.config?.executor || "") || "").trim() || undefined;
|
|
2046
|
+
const mode = String(ctx.resolve(node.config?.mode || "") || "").trim() || undefined;
|
|
2047
|
+
const model = String(ctx.resolve(node.config?.model || "") || "").trim() || undefined;
|
|
2048
|
+
|
|
2049
|
+
const result = await meeting.analyzeMeetingFrame(sessionId, frameDataUrl, {
|
|
2050
|
+
source,
|
|
2051
|
+
prompt,
|
|
2052
|
+
visionModel,
|
|
2053
|
+
minIntervalMs,
|
|
2054
|
+
forceAnalyze,
|
|
2055
|
+
width,
|
|
2056
|
+
height,
|
|
2057
|
+
executor,
|
|
2058
|
+
mode,
|
|
2059
|
+
model,
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
ctx.data.meetingSessionId = sessionId;
|
|
2063
|
+
if (result?.summary) {
|
|
2064
|
+
ctx.data.meetingVisionSummary = String(result.summary);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
return {
|
|
2068
|
+
success: result?.ok !== false,
|
|
2069
|
+
sessionId: String(result?.sessionId || sessionId).trim(),
|
|
2070
|
+
analyzed: result?.analyzed === true,
|
|
2071
|
+
skipped: result?.skipped === true,
|
|
2072
|
+
reason: result?.reason || null,
|
|
2073
|
+
summary: result?.summary || "",
|
|
2074
|
+
provider: result?.provider || null,
|
|
2075
|
+
model: result?.model || null,
|
|
2076
|
+
frameHash: result?.frameHash || null,
|
|
2077
|
+
};
|
|
2078
|
+
} catch (err) {
|
|
2079
|
+
if (failOnError) throw err;
|
|
2080
|
+
return {
|
|
2081
|
+
success: false,
|
|
2082
|
+
error: String(err?.message || err),
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
});
|
|
2087
|
+
|
|
2088
|
+
registerNodeType("meeting.finalize", {
|
|
2089
|
+
describe: () => "Finalize a meeting session with status and optional note",
|
|
2090
|
+
schema: {
|
|
2091
|
+
type: "object",
|
|
2092
|
+
properties: {
|
|
2093
|
+
sessionId: { type: "string", description: "Meeting session ID (defaults to context session)" },
|
|
2094
|
+
status: {
|
|
2095
|
+
type: "string",
|
|
2096
|
+
enum: ["active", "paused", "completed", "archived", "failed", "cancelled"],
|
|
2097
|
+
default: "completed",
|
|
2098
|
+
},
|
|
2099
|
+
note: { type: "string", description: "Optional note recorded in session history" },
|
|
2100
|
+
failOnError: { type: "boolean", default: true, description: "Throw when finalization fails" },
|
|
2101
|
+
},
|
|
2102
|
+
},
|
|
2103
|
+
async execute(node, ctx, engine) {
|
|
2104
|
+
const meeting = engine.services?.meeting;
|
|
2105
|
+
if (!meeting || typeof meeting.stopMeeting !== "function") {
|
|
2106
|
+
throw new Error("Meeting service is not available");
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
const failOnError = parseBooleanSetting(resolveWorkflowNodeValue(node.config?.failOnError ?? true, ctx), true);
|
|
2110
|
+
try {
|
|
2111
|
+
const sessionId = String(
|
|
2112
|
+
ctx.resolve(node.config?.sessionId || ctx.data?.meetingSessionId || ctx.data?.sessionId || ""),
|
|
2113
|
+
).trim();
|
|
2114
|
+
if (!sessionId) {
|
|
2115
|
+
throw new Error("meeting.finalize requires sessionId (configure node.sessionId or run meeting.start first)");
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
const status = String(
|
|
2119
|
+
ctx.resolve(node.config?.status || "completed") || "completed",
|
|
2120
|
+
).trim().toLowerCase() || "completed";
|
|
2121
|
+
const note = String(ctx.resolve(node.config?.note || "") || "").trim() || undefined;
|
|
2122
|
+
|
|
2123
|
+
const result = await meeting.stopMeeting(sessionId, { status, note });
|
|
2124
|
+
return {
|
|
2125
|
+
success: result?.ok !== false,
|
|
2126
|
+
sessionId: String(result?.sessionId || sessionId).trim(),
|
|
2127
|
+
status: result?.status || status,
|
|
2128
|
+
session: result?.session || null,
|
|
2129
|
+
};
|
|
2130
|
+
} catch (err) {
|
|
2131
|
+
if (failOnError) throw err;
|
|
2132
|
+
return {
|
|
2133
|
+
success: false,
|
|
2134
|
+
error: String(err?.message || err),
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
},
|
|
2138
|
+
});
|
|
2139
|
+
|
|
1240
2140
|
registerNodeType("action.create_task", {
|
|
1241
2141
|
describe: () => "Create a new task in the kanban board",
|
|
1242
2142
|
schema: {
|
|
@@ -1259,14 +2159,14 @@ registerNodeType("action.create_task", {
|
|
|
1259
2159
|
ctx.log(node.id, `Creating task: ${title}`);
|
|
1260
2160
|
|
|
1261
2161
|
if (kanban?.createTask) {
|
|
1262
|
-
const task = await kanban
|
|
2162
|
+
const task = await createKanbanTaskWithProject(kanban, {
|
|
1263
2163
|
title,
|
|
1264
2164
|
description,
|
|
1265
2165
|
status: node.config?.status || "todo",
|
|
1266
2166
|
priority: node.config?.priority,
|
|
1267
2167
|
tags: node.config?.tags,
|
|
1268
2168
|
projectId: node.config?.projectId,
|
|
1269
|
-
});
|
|
2169
|
+
}, node.config?.projectId);
|
|
1270
2170
|
return { success: true, taskId: task.id, title };
|
|
1271
2171
|
}
|
|
1272
2172
|
return { success: false, error: "Kanban adapter not available" };
|
|
@@ -2311,7 +3211,7 @@ registerNodeType("action.materialize_planner_tasks", {
|
|
|
2311
3211
|
status,
|
|
2312
3212
|
};
|
|
2313
3213
|
if (projectId) payload.projectId = projectId;
|
|
2314
|
-
const createdTask = await kanban
|
|
3214
|
+
const createdTask = await createKanbanTaskWithProject(kanban, payload, projectId);
|
|
2315
3215
|
created.push({
|
|
2316
3216
|
id: createdTask?.id || null,
|
|
2317
3217
|
title: task.title,
|
|
@@ -2841,7 +3741,7 @@ registerNodeType("action.ask_user", {
|
|
|
2841
3741
|
// Send via Telegram if configured
|
|
2842
3742
|
if ((channel === "telegram" || channel === "both") && engine.services?.telegram?.sendMessage) {
|
|
2843
3743
|
const optionsText = options.length ? `\n\nOptions: ${options.join(" | ")}` : "";
|
|
2844
|
-
await engine.services.telegram.sendMessage(undefined,
|
|
3744
|
+
await engine.services.telegram.sendMessage(undefined, `:help: **Workflow Question**\n\n${question}${optionsText}`);
|
|
2845
3745
|
}
|
|
2846
3746
|
|
|
2847
3747
|
// Store question for UI polling
|