create-byan-agent 2.11.3 → 2.12.1

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.
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Kanban + stand-up registry for BYAN party-mode sessions.
3
+ *
4
+ * Kanban : _byan-output/party-mode-sessions/<session_id>/kanban.json
5
+ * columns : todo | doing | blocked | review | done
6
+ * cards : { id, title, assignee, priority, created_at, moved_at,
7
+ * column, blocker_reason? }
8
+ *
9
+ * Stand-up : _byan-output/party-mode-sessions/<session_id>/standup.jsonl
10
+ * entries : { agent, timestamp, did, blockers, next }
11
+ *
12
+ * Hermes watches stand-ups : an agent with 2+ consecutive "blocked"
13
+ * reports in the stand-up stream is flagged and their card is moved to
14
+ * `blocked` column in the kanban.
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+
20
+ const COLUMNS = ['todo', 'doing', 'blocked', 'review', 'done'];
21
+
22
+ function resolveRoot(projectRoot) {
23
+ return projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
24
+ }
25
+
26
+ function sessionDir(projectRoot, sessionId) {
27
+ return path.join(
28
+ resolveRoot(projectRoot),
29
+ '_byan-output',
30
+ 'party-mode-sessions',
31
+ sanitize(sessionId)
32
+ );
33
+ }
34
+
35
+ function sanitize(id) {
36
+ return String(id || 'default').replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 80);
37
+ }
38
+
39
+ function kanbanPath(projectRoot, sessionId) {
40
+ return path.join(sessionDir(projectRoot, sessionId), 'kanban.json');
41
+ }
42
+
43
+ function standupPath(projectRoot, sessionId) {
44
+ return path.join(sessionDir(projectRoot, sessionId), 'standup.jsonl');
45
+ }
46
+
47
+ function readKanban(projectRoot, sessionId) {
48
+ const p = kanbanPath(projectRoot, sessionId);
49
+ if (!fs.existsSync(p)) return null;
50
+ try {
51
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function writeKanban(projectRoot, sessionId, board) {
58
+ const p = kanbanPath(projectRoot, sessionId);
59
+ fs.mkdirSync(path.dirname(p), { recursive: true });
60
+ fs.writeFileSync(p, JSON.stringify(board, null, 2));
61
+ }
62
+
63
+ function emptyBoard(sessionId, now) {
64
+ return {
65
+ session_id: sessionId,
66
+ created_at: now.toISOString(),
67
+ updated_at: now.toISOString(),
68
+ columns: COLUMNS.slice(),
69
+ cards: {},
70
+ };
71
+ }
72
+
73
+ export function createBoard({ sessionId, projectRoot, now = new Date() } = {}) {
74
+ if (!sessionId) throw new Error('sessionId is required');
75
+ const existing = readKanban(projectRoot, sessionId);
76
+ if (existing) return existing;
77
+ const board = emptyBoard(sessionId, now);
78
+ writeKanban(projectRoot, sessionId, board);
79
+ return board;
80
+ }
81
+
82
+ export function addCard({
83
+ sessionId,
84
+ card,
85
+ projectRoot,
86
+ now = new Date(),
87
+ } = {}) {
88
+ if (!sessionId) throw new Error('sessionId is required');
89
+ if (!card || !card.id || !card.title) throw new Error('card.id and card.title required');
90
+
91
+ const board = readKanban(projectRoot, sessionId) || emptyBoard(sessionId, now);
92
+ if (board.cards[card.id]) throw new Error(`card ${card.id} already exists`);
93
+
94
+ board.cards[card.id] = {
95
+ id: card.id,
96
+ title: card.title,
97
+ assignee: card.assignee || null,
98
+ priority: card.priority || 'P2',
99
+ column: card.column || 'todo',
100
+ created_at: now.toISOString(),
101
+ moved_at: now.toISOString(),
102
+ blocker_reason: null,
103
+ };
104
+ board.updated_at = now.toISOString();
105
+
106
+ writeKanban(projectRoot, sessionId, board);
107
+ return board.cards[card.id];
108
+ }
109
+
110
+ export function moveCard({
111
+ sessionId,
112
+ cardId,
113
+ toColumn,
114
+ blocker_reason,
115
+ projectRoot,
116
+ now = new Date(),
117
+ } = {}) {
118
+ if (!COLUMNS.includes(toColumn)) {
119
+ throw new Error(`toColumn must be one of ${COLUMNS.join(', ')}, got ${toColumn}`);
120
+ }
121
+ const board = readKanban(projectRoot, sessionId);
122
+ if (!board) throw new Error(`no kanban for session ${sessionId}`);
123
+ if (!board.cards[cardId]) throw new Error(`card ${cardId} not found`);
124
+
125
+ const card = board.cards[cardId];
126
+ card.column = toColumn;
127
+ card.moved_at = now.toISOString();
128
+ card.blocker_reason = toColumn === 'blocked' ? blocker_reason || 'unspecified' : null;
129
+ board.updated_at = now.toISOString();
130
+
131
+ writeKanban(projectRoot, sessionId, board);
132
+ return card;
133
+ }
134
+
135
+ export function assignCard({
136
+ sessionId,
137
+ cardId,
138
+ assignee,
139
+ projectRoot,
140
+ now = new Date(),
141
+ } = {}) {
142
+ if (!assignee) throw new Error('assignee is required');
143
+ const board = readKanban(projectRoot, sessionId);
144
+ if (!board || !board.cards[cardId]) throw new Error(`card ${cardId} not found`);
145
+ board.cards[cardId].assignee = assignee;
146
+ board.cards[cardId].moved_at = now.toISOString();
147
+ board.updated_at = now.toISOString();
148
+ writeKanban(projectRoot, sessionId, board);
149
+ return board.cards[cardId];
150
+ }
151
+
152
+ export function getBoard({ sessionId, projectRoot } = {}) {
153
+ if (!sessionId) throw new Error('sessionId is required');
154
+ return readKanban(projectRoot, sessionId);
155
+ }
156
+
157
+ export function postStandup({
158
+ sessionId,
159
+ agent,
160
+ did,
161
+ blockers = [],
162
+ next,
163
+ projectRoot,
164
+ now = new Date(),
165
+ } = {}) {
166
+ if (!sessionId) throw new Error('sessionId is required');
167
+ if (!agent) throw new Error('agent is required');
168
+
169
+ const entry = {
170
+ agent,
171
+ timestamp: now.toISOString(),
172
+ did: did || '',
173
+ blockers: Array.isArray(blockers) ? blockers : [],
174
+ next: next || '',
175
+ };
176
+
177
+ const p = standupPath(projectRoot, sessionId);
178
+ fs.mkdirSync(path.dirname(p), { recursive: true });
179
+ fs.appendFileSync(p, JSON.stringify(entry) + '\n');
180
+ return entry;
181
+ }
182
+
183
+ export function readStandups({ sessionId, projectRoot, limit = 50 } = {}) {
184
+ const p = standupPath(projectRoot, sessionId);
185
+ if (!fs.existsSync(p)) return [];
186
+ const lines = fs.readFileSync(p, 'utf8').split('\n').filter(Boolean);
187
+ const out = [];
188
+ for (const line of lines) {
189
+ try {
190
+ out.push(JSON.parse(line));
191
+ } catch {
192
+ // skip malformed
193
+ }
194
+ }
195
+ return out.slice(-limit);
196
+ }
197
+
198
+ /**
199
+ * Detect agents with >= minStreak consecutive blocked stand-ups.
200
+ * Returns array of { agent, streak, lastAt }.
201
+ */
202
+ export function detectBlockedStreaks({ sessionId, minStreak = 2, projectRoot } = {}) {
203
+ const standups = readStandups({ sessionId, projectRoot, limit: 500 });
204
+ const streaks = {};
205
+ const agentLast = {};
206
+
207
+ for (const entry of standups) {
208
+ const isBlocked = Array.isArray(entry.blockers) && entry.blockers.length > 0;
209
+ if (isBlocked) {
210
+ streaks[entry.agent] = (streaks[entry.agent] || 0) + 1;
211
+ } else {
212
+ streaks[entry.agent] = 0;
213
+ }
214
+ agentLast[entry.agent] = entry.timestamp;
215
+ }
216
+
217
+ const flagged = [];
218
+ for (const [agent, n] of Object.entries(streaks)) {
219
+ if (n >= minStreak) {
220
+ flagged.push({ agent, streak: n, lastAt: agentLast[agent] });
221
+ }
222
+ }
223
+ return flagged;
224
+ }
225
+
226
+ export const KANBAN_COLUMNS = COLUMNS;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Peer review registry for BYAN agents working in party-mode.
3
+ *
4
+ * Contract :
5
+ * - An agent producing an artefact (commit, file change, spec) opens a
6
+ * review request via requestReview(). The request is persisted at
7
+ * _byan-output/reviews/<task_id>.json.
8
+ * - Another agent (must be ≠ author) issues a verdict via
9
+ * recordVerdict() with { verdict: approve | changes | block, comments,
10
+ * must_fix }.
11
+ * - listPending() returns all unresolved requests. pickReviewer()
12
+ * returns an alternative agent from the roster distinct from the
13
+ * author.
14
+ *
15
+ * Enforces the "reviewer must differ from author" invariant inside
16
+ * recordVerdict() and throws if violated.
17
+ */
18
+
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+
22
+ const DEFAULT_ROSTER = [
23
+ 'bmad-bmm-architect',
24
+ 'bmad-bmm-dev',
25
+ 'bmad-bmm-quinn',
26
+ 'bmad-bmm-pm',
27
+ 'bmad-bmm-sm',
28
+ 'bmad-bmm-analyst',
29
+ 'bmad-bmm-ux-designer',
30
+ 'bmad-bmm-tech-writer',
31
+ 'bmad-tea-tea',
32
+ 'bmad-compliance',
33
+ ];
34
+
35
+ function resolveRoot(projectRoot) {
36
+ return projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
37
+ }
38
+
39
+ function reviewsDir(projectRoot) {
40
+ return path.join(resolveRoot(projectRoot), '_byan-output', 'reviews');
41
+ }
42
+
43
+ function reviewPath(projectRoot, taskId) {
44
+ return path.join(reviewsDir(projectRoot), `${sanitizeId(taskId)}.json`);
45
+ }
46
+
47
+ function sanitizeId(id) {
48
+ return String(id).replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 80);
49
+ }
50
+
51
+ function readReview(projectRoot, taskId) {
52
+ const p = reviewPath(projectRoot, taskId);
53
+ if (!fs.existsSync(p)) return null;
54
+ try {
55
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ function writeReview(projectRoot, review) {
62
+ fs.mkdirSync(reviewsDir(projectRoot), { recursive: true });
63
+ fs.writeFileSync(reviewPath(projectRoot, review.task_id), JSON.stringify(review, null, 2));
64
+ }
65
+
66
+ export function requestReview({
67
+ task_id,
68
+ author,
69
+ artifact_paths = [],
70
+ description = '',
71
+ projectRoot,
72
+ now = new Date(),
73
+ } = {}) {
74
+ if (!task_id) throw new Error('task_id is required');
75
+ if (!author) throw new Error('author (agent name) is required');
76
+
77
+ const existing = readReview(projectRoot, task_id);
78
+ if (existing && existing.status === 'pending') {
79
+ throw new Error(`review for task ${task_id} already pending (author ${existing.author})`);
80
+ }
81
+
82
+ const review = {
83
+ task_id,
84
+ author,
85
+ artifact_paths: Array.isArray(artifact_paths) ? artifact_paths : [],
86
+ description: String(description || ''),
87
+ status: 'pending',
88
+ verdicts: [],
89
+ created_at: now.toISOString(),
90
+ updated_at: now.toISOString(),
91
+ };
92
+
93
+ writeReview(projectRoot, review);
94
+ return review;
95
+ }
96
+
97
+ export function recordVerdict({
98
+ task_id,
99
+ reviewer,
100
+ verdict,
101
+ comments = [],
102
+ must_fix = [],
103
+ projectRoot,
104
+ now = new Date(),
105
+ } = {}) {
106
+ if (!task_id) throw new Error('task_id is required');
107
+ if (!reviewer) throw new Error('reviewer (agent name) is required');
108
+ if (!['approve', 'changes', 'block'].includes(verdict)) {
109
+ throw new Error(`verdict must be approve | changes | block, got ${verdict}`);
110
+ }
111
+
112
+ const review = readReview(projectRoot, task_id);
113
+ if (!review) throw new Error(`no review found for task ${task_id} — call requestReview first`);
114
+
115
+ if (review.author === reviewer) {
116
+ throw new Error(
117
+ `reviewer (${reviewer}) cannot be the same as author (${review.author}). Pick a different agent.`
118
+ );
119
+ }
120
+
121
+ review.verdicts.push({
122
+ reviewer,
123
+ verdict,
124
+ comments: Array.isArray(comments) ? comments : [],
125
+ must_fix: Array.isArray(must_fix) ? must_fix : [],
126
+ at: now.toISOString(),
127
+ });
128
+
129
+ if (verdict === 'approve') review.status = 'approved';
130
+ else if (verdict === 'block') review.status = 'blocked';
131
+ else review.status = 'changes_requested';
132
+
133
+ review.updated_at = now.toISOString();
134
+ writeReview(projectRoot, review);
135
+ return review;
136
+ }
137
+
138
+ export function getReview({ task_id, projectRoot } = {}) {
139
+ if (!task_id) throw new Error('task_id is required');
140
+ return readReview(projectRoot, task_id);
141
+ }
142
+
143
+ export function listPending({ projectRoot } = {}) {
144
+ const dir = reviewsDir(projectRoot);
145
+ if (!fs.existsSync(dir)) return [];
146
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
147
+ const out = [];
148
+ for (const f of files) {
149
+ try {
150
+ const r = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
151
+ if (r.status === 'pending' || r.status === 'changes_requested') out.push(r);
152
+ } catch {
153
+ // skip malformed
154
+ }
155
+ }
156
+ out.sort((a, b) => (a.created_at > b.created_at ? -1 : 1));
157
+ return out;
158
+ }
159
+
160
+ export function pickReviewer({ author, preferredDomain, roster = DEFAULT_ROSTER } = {}) {
161
+ const domainPairs = {
162
+ dev: ['bmad-bmm-quinn', 'bmad-tea-tea'],
163
+ 'bmm-dev': ['bmad-bmm-quinn', 'bmad-tea-tea'],
164
+ 'bmad-bmm-dev': ['bmad-bmm-quinn', 'bmad-tea-tea'],
165
+ architect: ['bmad-tea-tea', 'bmad-bmm-quinn'],
166
+ 'bmad-bmm-architect': ['bmad-tea-tea', 'bmad-bmm-quinn'],
167
+ pm: ['bmad-bmm-sm', 'bmad-bmm-analyst'],
168
+ 'bmad-bmm-pm': ['bmad-bmm-sm', 'bmad-bmm-analyst'],
169
+ 'ux-designer': ['bmad-bmm-pm', 'bmad-bmm-analyst'],
170
+ 'bmad-bmm-ux-designer': ['bmad-bmm-pm', 'bmad-bmm-analyst'],
171
+ };
172
+
173
+ const keys = [preferredDomain, author].filter(Boolean);
174
+ for (const k of keys) {
175
+ const candidates = domainPairs[k] || [];
176
+ for (const c of candidates) {
177
+ if (c !== author) return c;
178
+ }
179
+ }
180
+
181
+ for (const r of roster) {
182
+ if (r !== author) return r;
183
+ }
184
+ return null;
185
+ }
186
+
187
+ export const DEFAULT_AGENT_ROSTER = DEFAULT_ROSTER;
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const DEFAULT_FILES = {
5
+ soul: '_byan/soul.md',
6
+ tao: '_byan/tao.md',
7
+ 'soul-memory': '_byan/soul-memory.md',
8
+ };
9
+
10
+ export function resolveProjectRoot(envRoot) {
11
+ return envRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
12
+ }
13
+
14
+ export function readSoul({ which = 'all', projectRoot }) {
15
+ const root = resolveProjectRoot(projectRoot);
16
+ const targets =
17
+ which === 'all'
18
+ ? Object.keys(DEFAULT_FILES)
19
+ : [which].filter((k) => DEFAULT_FILES[k]);
20
+
21
+ if (targets.length === 0) {
22
+ throw new Error(
23
+ `Unknown soul target: "${which}". Valid: ${Object.keys(DEFAULT_FILES).join(', ')} or "all".`
24
+ );
25
+ }
26
+
27
+ const result = {};
28
+ for (const key of targets) {
29
+ const p = path.join(root, DEFAULT_FILES[key]);
30
+ if (fs.existsSync(p)) {
31
+ result[key] = {
32
+ path: DEFAULT_FILES[key],
33
+ content: fs.readFileSync(p, 'utf8'),
34
+ };
35
+ } else {
36
+ result[key] = { path: DEFAULT_FILES[key], content: null, missing: true };
37
+ }
38
+ }
39
+ return result;
40
+ }
41
+
42
+ export function appendSoulMemory({ entry, projectRoot, validated = false, now = new Date() }) {
43
+ if (!entry || typeof entry !== 'string' || entry.trim().length === 0) {
44
+ throw new Error('entry must be a non-empty string');
45
+ }
46
+ if (!validated) {
47
+ throw new Error(
48
+ 'validated=true is required. Per BYAN rule, soul-memory entries must be confirmed by the user before append.'
49
+ );
50
+ }
51
+
52
+ const root = resolveProjectRoot(projectRoot);
53
+ const p = path.join(root, DEFAULT_FILES['soul-memory']);
54
+ const stamp = now.toISOString().slice(0, 10);
55
+ const block = `\n\n---\n\n## Entree ${stamp}\n\n${entry.trim()}\n`;
56
+
57
+ const dir = path.dirname(p);
58
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
59
+
60
+ const existing = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '# Soul-Memory — Journal vivant BYAN\n';
61
+ fs.writeFileSync(p, existing + block);
62
+
63
+ return { path: DEFAULT_FILES['soul-memory'], appended_chars: block.length };
64
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Workflow node scripts — thin wrapper over byan_web REST endpoints.
3
+ *
4
+ * Wraps :
5
+ * GET /api/workflow-nodes/:nodeId/scripts
6
+ * GET /api/scripts/:id
7
+ * POST /api/workflow-nodes/:nodeId/scripts
8
+ * PATCH /api/scripts/:id
9
+ * DELETE /api/scripts/:id
10
+ * GET /api/scripts/:id/history
11
+ * POST /api/scripts/:id/rollback
12
+ * POST /api/workflow-nodes/:nodeId/scripts/import
13
+ * POST /api/scripts/validate
14
+ *
15
+ * `apiRequest` is injected so tests can pass a mock without an HTTP round-trip.
16
+ */
17
+
18
+ export const ALLOWED_LANGUAGES = ['bash', 'python', 'javascript', 'typescript'];
19
+ export const ALLOWED_EXEC_MODES = ['context', 'local'];
20
+
21
+ function encodeId(id) {
22
+ if (!id || typeof id !== 'string') throw new Error('id must be a non-empty string');
23
+ return encodeURIComponent(id);
24
+ }
25
+
26
+ function unwrap(body) {
27
+ if (body && typeof body === 'object' && 'data' in body) return body.data;
28
+ return body;
29
+ }
30
+
31
+ export async function listScripts({ apiRequest, nodeId }) {
32
+ if (!nodeId) throw new Error('nodeId is required');
33
+ const body = await apiRequest(`/api/workflow-nodes/${encodeId(nodeId)}/scripts`);
34
+ return { data: body.data || [], total: body.total ?? (body.data || []).length };
35
+ }
36
+
37
+ export async function getScript({ apiRequest, id }) {
38
+ const body = await apiRequest(`/api/scripts/${encodeId(id)}`);
39
+ return unwrap(body);
40
+ }
41
+
42
+ export async function createScript({
43
+ apiRequest,
44
+ nodeId,
45
+ name,
46
+ language,
47
+ content,
48
+ execution_mode,
49
+ order_idx,
50
+ bypassValidation,
51
+ }) {
52
+ if (!nodeId) throw new Error('nodeId is required');
53
+ if (!name) throw new Error('name is required');
54
+ if (!ALLOWED_LANGUAGES.includes(language)) {
55
+ throw new Error(`language must be one of ${ALLOWED_LANGUAGES.join(', ')}, got ${language}`);
56
+ }
57
+ if (typeof content !== 'string') throw new Error('content (string) is required');
58
+
59
+ const payload = { name, language, content };
60
+ if (execution_mode !== undefined) payload.execution_mode = execution_mode;
61
+ if (order_idx !== undefined) payload.order_idx = order_idx;
62
+ if (bypassValidation === true) payload.bypassValidation = true;
63
+
64
+ const body = await apiRequest(`/api/workflow-nodes/${encodeId(nodeId)}/scripts`, {
65
+ method: 'POST',
66
+ body: JSON.stringify(payload),
67
+ });
68
+ return unwrap(body);
69
+ }
70
+
71
+ export async function updateScript({
72
+ apiRequest,
73
+ id,
74
+ expected_version,
75
+ name,
76
+ language,
77
+ content,
78
+ execution_mode,
79
+ order_idx,
80
+ bypassValidation,
81
+ }) {
82
+ const payload = {};
83
+ if (expected_version !== undefined) payload.expected_version = expected_version;
84
+ if (name !== undefined) payload.name = name;
85
+ if (language !== undefined) {
86
+ if (!ALLOWED_LANGUAGES.includes(language)) {
87
+ throw new Error(`language must be one of ${ALLOWED_LANGUAGES.join(', ')}, got ${language}`);
88
+ }
89
+ payload.language = language;
90
+ }
91
+ if (content !== undefined) payload.content = content;
92
+ if (execution_mode !== undefined) payload.execution_mode = execution_mode;
93
+ if (order_idx !== undefined) payload.order_idx = order_idx;
94
+ if (bypassValidation === true) payload.bypassValidation = true;
95
+
96
+ const body = await apiRequest(`/api/scripts/${encodeId(id)}`, {
97
+ method: 'PATCH',
98
+ body: JSON.stringify(payload),
99
+ });
100
+ return unwrap(body);
101
+ }
102
+
103
+ export async function deleteScript({ apiRequest, id }) {
104
+ const body = await apiRequest(`/api/scripts/${encodeId(id)}`, { method: 'DELETE' });
105
+ return unwrap(body);
106
+ }
107
+
108
+ export async function getScriptHistory({ apiRequest, id }) {
109
+ const body = await apiRequest(`/api/scripts/${encodeId(id)}/history`);
110
+ return { data: body.data || [], total: body.total ?? (body.data || []).length };
111
+ }
112
+
113
+ export async function rollbackScript({ apiRequest, id, version }) {
114
+ if (!Number.isInteger(version) || version < 1) {
115
+ throw new Error('version must be a positive integer');
116
+ }
117
+ const body = await apiRequest(`/api/scripts/${encodeId(id)}/rollback`, {
118
+ method: 'POST',
119
+ body: JSON.stringify({ version }),
120
+ });
121
+ return unwrap(body);
122
+ }
123
+
124
+ export async function importScript({
125
+ apiRequest,
126
+ nodeId,
127
+ url,
128
+ name,
129
+ language,
130
+ execution_mode,
131
+ }) {
132
+ if (!nodeId) throw new Error('nodeId is required');
133
+ if (!url) throw new Error('url is required');
134
+ const payload = { url };
135
+ if (name !== undefined) payload.name = name;
136
+ if (language !== undefined) payload.language = language;
137
+ if (execution_mode !== undefined) payload.execution_mode = execution_mode;
138
+
139
+ const body = await apiRequest(`/api/workflow-nodes/${encodeId(nodeId)}/scripts/import`, {
140
+ method: 'POST',
141
+ body: JSON.stringify(payload),
142
+ });
143
+ return unwrap(body);
144
+ }
145
+
146
+ export async function validateScriptSyntax({ apiRequest, language, content }) {
147
+ if (!ALLOWED_LANGUAGES.includes(language)) {
148
+ throw new Error(`language must be one of ${ALLOWED_LANGUAGES.join(', ')}, got ${language}`);
149
+ }
150
+ if (typeof content !== 'string') throw new Error('content (string) is required');
151
+ const body = await apiRequest('/api/scripts/validate', {
152
+ method: 'POST',
153
+ body: JSON.stringify({ language, content }),
154
+ });
155
+ return unwrap(body);
156
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "byan-mcp-server",
3
+ "version": "0.2.0",
4
+ "description": "BYAN MCP server — exposes byan_web REST API (projects, FD lifecycle, kanban, ELO, fact-check, workflow-node-scripts, peer-review) as Claude Code tools.",
5
+ "main": "server.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "byan-mcp": "./server.js"
9
+ },
10
+ "files": [
11
+ "server.js",
12
+ "lib/",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "start": "node server.js",
18
+ "test": "node --test test/*.test.js"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.29.0"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "byan",
29
+ "claude-code",
30
+ "workflow",
31
+ "fd-lifecycle",
32
+ "kanban",
33
+ "elo-trust",
34
+ "fact-check"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/Les-fous-du-bus/byan_web.git",
39
+ "directory": "_byan/mcp/byan-mcp-server"
40
+ },
41
+ "author": "Yan Acadenice",
42
+ "license": "MIT"
43
+ }