bosun 0.41.0 → 0.41.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 +8 -0
- package/README.md +20 -0
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
statSync,
|
|
6
|
+
watch,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { basename, resolve } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
getNodeTypeMeta,
|
|
13
|
+
registerNodeType,
|
|
14
|
+
unregisterNodeType,
|
|
15
|
+
} from "../workflow-engine.mjs";
|
|
16
|
+
import { resolveRepoRoot } from "../../config/repo-root.mjs";
|
|
17
|
+
|
|
18
|
+
const TAG = "[workflow-custom-nodes]";
|
|
19
|
+
export const CUSTOM_NODE_DIR_NAME = "custom-nodes";
|
|
20
|
+
|
|
21
|
+
let activeRepoRoot = "";
|
|
22
|
+
let activeCustomDir = "";
|
|
23
|
+
let watcher = null;
|
|
24
|
+
let watcherTimer = null;
|
|
25
|
+
const fileVersions = new Map();
|
|
26
|
+
const fileNodeTypes = new Map();
|
|
27
|
+
|
|
28
|
+
function logWarn(message) {
|
|
29
|
+
console.warn(`${TAG} ${message}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sanitizeNodeName(name = "") {
|
|
33
|
+
const raw = String(name || "").trim().toLowerCase();
|
|
34
|
+
let normalized = "";
|
|
35
|
+
let lastWasDash = false;
|
|
36
|
+
for (const ch of raw) {
|
|
37
|
+
const isSafe =
|
|
38
|
+
(ch >= "a" && ch <= "z") ||
|
|
39
|
+
(ch >= "0" && ch <= "9") ||
|
|
40
|
+
ch === "." ||
|
|
41
|
+
ch === "_" ||
|
|
42
|
+
ch === "-";
|
|
43
|
+
const out = isSafe ? ch : "-";
|
|
44
|
+
if (out === "-") {
|
|
45
|
+
if (lastWasDash || normalized.length === 0) continue;
|
|
46
|
+
lastWasDash = true;
|
|
47
|
+
normalized += "-";
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
lastWasDash = false;
|
|
51
|
+
normalized += out;
|
|
52
|
+
}
|
|
53
|
+
return normalized.endsWith("-") ? normalized.slice(0, -1) : normalized;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toTypeName(name = "") {
|
|
57
|
+
const normalized = sanitizeNodeName(name).replace(/-/g, "_");
|
|
58
|
+
return normalized.startsWith("custom.") ? normalized : `custom.${normalized}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defaultRepoRoot(repoRoot = "") {
|
|
62
|
+
return resolve(repoRoot || resolveRepoRoot());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveCustomNodeDir(repoRoot = "") {
|
|
66
|
+
return resolve(defaultRepoRoot(repoRoot), CUSTOM_NODE_DIR_NAME);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isValidStringArray(value) {
|
|
70
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.trim());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeNodeExport(mod, filePath) {
|
|
74
|
+
const candidate = mod?.default && typeof mod.default === "object"
|
|
75
|
+
? { ...mod.default, ...mod }
|
|
76
|
+
: mod;
|
|
77
|
+
const type = String(candidate?.type || "").trim();
|
|
78
|
+
if (!type) throw new Error("missing string export: type");
|
|
79
|
+
if (typeof candidate.execute !== "function") throw new Error("missing execute(node, ctx, engine) function");
|
|
80
|
+
if (typeof candidate.describe !== "function") throw new Error("missing describe() function");
|
|
81
|
+
if (!isValidStringArray(candidate.inputs || [])) throw new Error("inputs must be an array of strings");
|
|
82
|
+
if (!isValidStringArray(candidate.outputs || [])) throw new Error("outputs must be an array of strings");
|
|
83
|
+
if (candidate.schema != null) {
|
|
84
|
+
if (typeof candidate.schema !== "object" || Array.isArray(candidate.schema)) {
|
|
85
|
+
throw new Error("schema must be an object when provided");
|
|
86
|
+
}
|
|
87
|
+
if (candidate.schema.type && candidate.schema.type !== "object") {
|
|
88
|
+
throw new Error("schema.type must be 'object' when provided");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
type,
|
|
93
|
+
inputs: [...(candidate.inputs || [])],
|
|
94
|
+
outputs: [...(candidate.outputs || [])],
|
|
95
|
+
execute: candidate.execute,
|
|
96
|
+
describe: candidate.describe,
|
|
97
|
+
schema: candidate.schema || {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: {},
|
|
100
|
+
additionalProperties: true,
|
|
101
|
+
},
|
|
102
|
+
filePath,
|
|
103
|
+
source: "custom",
|
|
104
|
+
badge: "custom",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function unloadFileTypes(filePath) {
|
|
109
|
+
const previousTypes = fileNodeTypes.get(filePath) || [];
|
|
110
|
+
for (const type of previousTypes) unregisterNodeType(type);
|
|
111
|
+
fileNodeTypes.delete(filePath);
|
|
112
|
+
fileVersions.delete(filePath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function loadCustomNodeFile(filePath) {
|
|
116
|
+
unloadFileTypes(filePath);
|
|
117
|
+
const version = statSync(filePath).mtimeMs;
|
|
118
|
+
const imported = await import(`${pathToFileURL(filePath).href}?v=${version}`);
|
|
119
|
+
const nodeDef = normalizeNodeExport(imported, filePath);
|
|
120
|
+
const existingMeta = getNodeTypeMeta(nodeDef.type);
|
|
121
|
+
if (existingMeta && existingMeta.filePath !== filePath) {
|
|
122
|
+
throw new Error(`duplicate node type '${nodeDef.type}' already registered from ${existingMeta.filePath || existingMeta.source}`);
|
|
123
|
+
}
|
|
124
|
+
registerNodeType(nodeDef.type, nodeDef, {
|
|
125
|
+
source: "custom",
|
|
126
|
+
badge: "custom",
|
|
127
|
+
inputs: nodeDef.inputs,
|
|
128
|
+
outputs: nodeDef.outputs,
|
|
129
|
+
filePath,
|
|
130
|
+
});
|
|
131
|
+
fileVersions.set(filePath, version);
|
|
132
|
+
fileNodeTypes.set(filePath, [nodeDef.type]);
|
|
133
|
+
return nodeDef;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function clearRemovedFiles(customDir) {
|
|
137
|
+
for (const filePath of [...fileNodeTypes.keys()]) {
|
|
138
|
+
if (!filePath.startsWith(customDir)) continue;
|
|
139
|
+
if (!existsSync(filePath)) unloadFileTypes(filePath);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function ensureCustomWorkflowNodesLoaded(options = {}) {
|
|
144
|
+
const repoRoot = defaultRepoRoot(options.repoRoot);
|
|
145
|
+
const customDir = resolveCustomNodeDir(repoRoot);
|
|
146
|
+
activeRepoRoot = repoRoot;
|
|
147
|
+
activeCustomDir = customDir;
|
|
148
|
+
if (!existsSync(customDir)) return [];
|
|
149
|
+
clearRemovedFiles(customDir);
|
|
150
|
+
const loaded = [];
|
|
151
|
+
for (const filePath of readdirSync(customDir)
|
|
152
|
+
.filter((name) => name.endsWith(".mjs"))
|
|
153
|
+
.map((name) => resolve(customDir, name))
|
|
154
|
+
.sort()) {
|
|
155
|
+
try {
|
|
156
|
+
const version = statSync(filePath).mtimeMs;
|
|
157
|
+
if (!options.forceReload && fileVersions.get(filePath) === version && fileNodeTypes.has(filePath)) {
|
|
158
|
+
loaded.push(...(fileNodeTypes.get(filePath) || []));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const def = await loadCustomNodeFile(filePath);
|
|
162
|
+
loaded.push(def.type);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
logWarn(`Skipping ${basename(filePath)}: ${error?.message || String(error)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return loaded;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isDevMode() {
|
|
171
|
+
if (process.env.VITEST) return false;
|
|
172
|
+
return String(process.env.NODE_ENV || "development").toLowerCase() !== "production";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function startCustomNodeDiscovery(options = {}) {
|
|
176
|
+
if (!isDevMode()) return null;
|
|
177
|
+
const repoRoot = defaultRepoRoot(options.repoRoot || activeRepoRoot);
|
|
178
|
+
const customDir = resolveCustomNodeDir(repoRoot);
|
|
179
|
+
activeRepoRoot = repoRoot;
|
|
180
|
+
activeCustomDir = customDir;
|
|
181
|
+
mkdirSync(customDir, { recursive: true });
|
|
182
|
+
if (watcher) return watcher;
|
|
183
|
+
watcher = watch(customDir, { persistent: true }, () => {
|
|
184
|
+
clearTimeout(watcherTimer);
|
|
185
|
+
watcherTimer = setTimeout(() => {
|
|
186
|
+
ensureCustomWorkflowNodesLoaded({ repoRoot, forceReload: true }).catch((error) => {
|
|
187
|
+
logWarn(`Hot reload failed: ${error?.message || String(error)}`);
|
|
188
|
+
});
|
|
189
|
+
}, 150);
|
|
190
|
+
});
|
|
191
|
+
watcher.on("error", (error) => logWarn(`watch error: ${error?.message || String(error)}`));
|
|
192
|
+
return watcher;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function stopCustomNodeDiscovery() {
|
|
196
|
+
if (watcher) watcher.close();
|
|
197
|
+
watcher = null;
|
|
198
|
+
if (watcherTimer) clearTimeout(watcherTimer);
|
|
199
|
+
watcherTimer = null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function getCustomNodeDir(repoRoot = "") {
|
|
203
|
+
return resolveCustomNodeDir(repoRoot || activeRepoRoot);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function scaffoldCustomNodeFile(name, options = {}) {
|
|
207
|
+
const repoRoot = defaultRepoRoot(options.repoRoot);
|
|
208
|
+
const customDir = resolveCustomNodeDir(repoRoot);
|
|
209
|
+
const safeName = sanitizeNodeName(name);
|
|
210
|
+
if (!safeName) throw new Error("Node name is required");
|
|
211
|
+
mkdirSync(customDir, { recursive: true });
|
|
212
|
+
const filePath = resolve(customDir, `${safeName}.mjs`);
|
|
213
|
+
if (existsSync(filePath)) throw new Error(`Custom node already exists: ${filePath}`);
|
|
214
|
+
const type = toTypeName(safeName);
|
|
215
|
+
const title = safeName.replace(/[-_]+/g, " ");
|
|
216
|
+
const contents = [
|
|
217
|
+
`export const type = ${JSON.stringify(type)};`,
|
|
218
|
+
'export const inputs = ["message"];',
|
|
219
|
+
'export const outputs = ["success", "error"];',
|
|
220
|
+
'export const schema = {',
|
|
221
|
+
' type: "object",',
|
|
222
|
+
' properties: {',
|
|
223
|
+
' message: {',
|
|
224
|
+
' type: "string",',
|
|
225
|
+
' description: "Message payload for this custom node.",',
|
|
226
|
+
' },',
|
|
227
|
+
' },',
|
|
228
|
+
' additionalProperties: true,',
|
|
229
|
+
'};',
|
|
230
|
+
'',
|
|
231
|
+
'export function describe() {',
|
|
232
|
+
` return ${JSON.stringify(`Custom node: ${title}`)};`,
|
|
233
|
+
'}',
|
|
234
|
+
'',
|
|
235
|
+
'export async function execute(node, ctx) {',
|
|
236
|
+
` const message = String(node?.config?.message || ${JSON.stringify(`hello from ${type}`)});`,
|
|
237
|
+
` ctx?.log?.(node?.id || type, "[custom-node] ${type}: " + message, "info");`,
|
|
238
|
+
' return {',
|
|
239
|
+
' success: true,',
|
|
240
|
+
' port: "success",',
|
|
241
|
+
' type,',
|
|
242
|
+
' message,',
|
|
243
|
+
' };',
|
|
244
|
+
'}',
|
|
245
|
+
'',
|
|
246
|
+
].join("\n");
|
|
247
|
+
writeFileSync(filePath, contents, "utf8");
|
|
248
|
+
return { filePath, type, customDir, repoRoot };
|
|
249
|
+
}
|
|
250
|
+
|