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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,56 @@
1
+ # byan-mcp-server
2
+
3
+ MCP (Model Context Protocol) server that exposes the [byan_web](https://github.com/Les-fous-du-bus/byan_web) REST API as tools usable from Claude Code, Codex, or any MCP-aware client.
4
+
5
+ ## What it exposes
6
+
7
+ - `byan_ping`, `byan_list_projects`, `byan_import_project` — project inventory
8
+ - `byan_dispatch` — task routing heuristic
9
+ - `byan_soul_read`, `byan_soul_memory_append` — BYAN soul system
10
+ - `byan_elo_summary` / `byan_elo_context` / `byan_elo_record` — ELO trust calibration
11
+ - `byan_fc_check` / `byan_fc_parse` — fact-check engine
12
+ - `byan_fd_start` / `byan_fd_status` / `byan_fd_advance` / `byan_fd_update` / `byan_fd_abort` — Feature Development (FD) lifecycle
13
+ - `byan_review_request` / `byan_review_verdict` / `byan_review_get` / `byan_review_pending` / `byan_review_pick_reviewer` — peer review
14
+ - `byan_kanban_create` / `byan_kanban_add` / `byan_kanban_move` / `byan_kanban_assign` / `byan_kanban_get` — party-mode kanban
15
+ - `byan_standup_post` / `byan_standup_read` / `byan_standup_blocked` — stand-up feed
16
+ - `byan_copilot_sessions` / `byan_copilot_session_events` / `byan_copilot_search` — Copilot CLI session inspector
17
+ - `byan_workflow_script_list` / `_get` / `_create` / `_update` / `_delete` / `_history` / `_rollback` / `_import` / `_validate` — workflow-node scripts CRUD
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install -g byan-mcp-server
23
+ ```
24
+
25
+ ## Configure
26
+
27
+ Register the server in your MCP client config. Example for Claude Code (`~/.config/claude/mcp.json` or project `.mcp.json`):
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "byan": {
33
+ "command": "byan-mcp",
34
+ "env": {
35
+ "BYAN_API_URL": "https://byan-api.example.com",
36
+ "BYAN_API_TOKEN": "byan_xxx_or_jwt"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ ## Env vars
44
+
45
+ - `BYAN_API_URL` — byan_web API base URL (default `http://localhost:3737`)
46
+ - `BYAN_API_TOKEN` — API key (prefix `byan_` uses `ApiKey` scheme) or JWT (uses `Bearer`)
47
+ - `CLAUDE_PROJECT_DIR` — project root for filesystem-backed tools (soul, FD state, kanban)
48
+
49
+ ## Requirements
50
+
51
+ - Node.js >= 18
52
+ - A running byan_web API (for auth-gated tools)
53
+
54
+ ## License
55
+
56
+ MIT
@@ -0,0 +1,108 @@
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+
4
+ const CLI_REL_PATH = 'bin/byan-v2-cli.js';
5
+ const DEFAULT_TIMEOUT_MS = 10_000;
6
+
7
+ export function parseCliOutput(stdout) {
8
+ const trimmed = stdout.trim();
9
+ if (!trimmed) return { raw: '' };
10
+
11
+ try {
12
+ return JSON.parse(trimmed);
13
+ } catch {
14
+ // fall through
15
+ }
16
+
17
+ const firstBracket = trimmed.search(/[\[{]/);
18
+ if (firstBracket >= 0) {
19
+ const candidate = trimmed.slice(firstBracket);
20
+ try {
21
+ return JSON.parse(candidate);
22
+ } catch {
23
+ // still not valid JSON
24
+ }
25
+ }
26
+
27
+ return { raw: trimmed };
28
+ }
29
+
30
+ function resolveProjectRoot(envRoot) {
31
+ return envRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
32
+ }
33
+
34
+ export function runByanCli(args, options = {}) {
35
+ const root = resolveProjectRoot(options.projectRoot);
36
+ const script = path.join(root, CLI_REL_PATH);
37
+ const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
38
+
39
+ return new Promise((resolve, reject) => {
40
+ const child = spawn('node', [script, ...args], {
41
+ cwd: root,
42
+ env: { ...process.env },
43
+ });
44
+
45
+ let stdout = '';
46
+ let stderr = '';
47
+ const timer = setTimeout(() => {
48
+ child.kill('SIGKILL');
49
+ reject(new Error(`byan-v2-cli timed out after ${timeoutMs}ms: ${args.join(' ')}`));
50
+ }, timeoutMs);
51
+
52
+ child.stdout.on('data', (d) => (stdout += d.toString()));
53
+ child.stderr.on('data', (d) => (stderr += d.toString()));
54
+
55
+ child.on('error', (err) => {
56
+ clearTimeout(timer);
57
+ reject(err);
58
+ });
59
+
60
+ child.on('close', (code) => {
61
+ clearTimeout(timer);
62
+ if (code !== 0) {
63
+ const err = new Error(
64
+ `byan-v2-cli exited ${code}: ${stderr.trim() || stdout.trim()}`
65
+ );
66
+ err.code = code;
67
+ err.stderr = stderr;
68
+ err.stdout = stdout;
69
+ reject(err);
70
+ return;
71
+ }
72
+ resolve(parseCliOutput(stdout));
73
+ });
74
+ });
75
+ }
76
+
77
+ export async function eloSummary(opts) {
78
+ return runByanCli(['elo', 'summary'], opts);
79
+ }
80
+
81
+ export async function eloContext({ domain, ...opts }) {
82
+ if (!domain) throw new Error('domain is required');
83
+ return runByanCli(['elo', 'context', domain], opts);
84
+ }
85
+
86
+ export async function eloDashboard({ domain, ...opts }) {
87
+ return runByanCli(domain ? ['elo', 'dashboard', domain] : ['elo', 'dashboard'], opts);
88
+ }
89
+
90
+ export async function eloRecord({ domain, result, reason, ...opts }) {
91
+ if (!domain) throw new Error('domain is required');
92
+ if (!['VALIDATED', 'BLOCKED', 'PARTIAL'].includes(result)) {
93
+ throw new Error(`result must be VALIDATED|BLOCKED|PARTIAL, got: ${result}`);
94
+ }
95
+ const args = ['elo', 'record', domain, result];
96
+ if (reason) args.push(reason);
97
+ return runByanCli(args, opts);
98
+ }
99
+
100
+ export async function fcCheck({ text, ...opts }) {
101
+ if (!text || typeof text !== 'string') throw new Error('text is required');
102
+ return runByanCli(['fc', 'check', text], opts);
103
+ }
104
+
105
+ export async function fcParse({ text, ...opts }) {
106
+ if (!text || typeof text !== 'string') throw new Error('text is required');
107
+ return runByanCli(['fc', 'parse', text], opts);
108
+ }
@@ -0,0 +1,148 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ const COPILOT_ROOT = process.env.BYAN_COPILOT_ROOT || path.join(os.homedir(), '.copilot', 'session-state');
6
+
7
+ function readJsonl(filePath, limit) {
8
+ if (!fs.existsSync(filePath)) return [];
9
+ const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean);
10
+ const out = [];
11
+ for (const line of lines) {
12
+ try {
13
+ out.push(JSON.parse(line));
14
+ } catch {
15
+ // skip malformed
16
+ }
17
+ if (typeof limit === 'number' && out.length >= limit) break;
18
+ }
19
+ return out;
20
+ }
21
+
22
+ function summarizeSession(sessionId) {
23
+ const eventsPath = path.join(COPILOT_ROOT, sessionId, 'events.jsonl');
24
+ if (!fs.existsSync(eventsPath)) return null;
25
+
26
+ const events = readJsonl(eventsPath);
27
+ if (events.length === 0) return null;
28
+
29
+ const start = events.find((e) => e.type === 'session.start');
30
+ const shutdown = events.find((e) => e.type === 'session.shutdown');
31
+ const agent = events.find((e) => e.type === 'subagent.selected');
32
+
33
+ const counts = {};
34
+ for (const e of events) {
35
+ counts[e.type] = (counts[e.type] || 0) + 1;
36
+ }
37
+
38
+ const userMessages = events.filter((e) => e.type === 'user.message');
39
+ const assistantMessages = events.filter((e) => e.type === 'assistant.message');
40
+
41
+ return {
42
+ sessionId,
43
+ startTime: start?.data?.startTime || null,
44
+ endTime: shutdown?.timestamp || null,
45
+ cwd: start?.data?.context?.cwd || null,
46
+ branch: start?.data?.context?.branch || null,
47
+ agent: agent?.data?.agentName || null,
48
+ event_count: events.length,
49
+ user_messages: userMessages.length,
50
+ assistant_messages: assistantMessages.length,
51
+ tool_calls: counts['tool.execution_start'] || 0,
52
+ event_type_counts: counts,
53
+ };
54
+ }
55
+
56
+ export function listSessions({ limit = 20, sinceIso = null, cwdFilter = null } = {}) {
57
+ if (!fs.existsSync(COPILOT_ROOT)) return { root: COPILOT_ROOT, sessions: [], total: 0, exists: false };
58
+
59
+ const dirs = fs
60
+ .readdirSync(COPILOT_ROOT, { withFileTypes: true })
61
+ .filter((d) => d.isDirectory())
62
+ .map((d) => d.name);
63
+
64
+ const summaries = [];
65
+ for (const id of dirs) {
66
+ const s = summarizeSession(id);
67
+ if (!s) continue;
68
+ if (sinceIso && s.startTime && Date.parse(s.startTime) < Date.parse(sinceIso)) continue;
69
+ if (cwdFilter && s.cwd && !s.cwd.includes(cwdFilter)) continue;
70
+ summaries.push(s);
71
+ }
72
+
73
+ summaries.sort((a, b) => {
74
+ const at = Date.parse(a.startTime || 0);
75
+ const bt = Date.parse(b.startTime || 0);
76
+ return bt - at;
77
+ });
78
+
79
+ return {
80
+ root: COPILOT_ROOT,
81
+ total: summaries.length,
82
+ exists: true,
83
+ sessions: summaries.slice(0, limit),
84
+ };
85
+ }
86
+
87
+ export function readSessionEvents({ sessionId, types = null, limit = 200 } = {}) {
88
+ if (!sessionId || typeof sessionId !== 'string') {
89
+ throw new Error('sessionId is required');
90
+ }
91
+ const eventsPath = path.join(COPILOT_ROOT, sessionId, 'events.jsonl');
92
+ if (!fs.existsSync(eventsPath)) {
93
+ throw new Error(`events.jsonl not found for session ${sessionId}`);
94
+ }
95
+
96
+ const allEvents = readJsonl(eventsPath);
97
+ const filtered = Array.isArray(types) && types.length > 0
98
+ ? allEvents.filter((e) => types.includes(e.type))
99
+ : allEvents;
100
+
101
+ return {
102
+ sessionId,
103
+ total: allEvents.length,
104
+ returned: Math.min(filtered.length, limit),
105
+ filtered_by_type: Array.isArray(types) ? types : null,
106
+ events: filtered.slice(0, limit),
107
+ };
108
+ }
109
+
110
+ export function searchSessions({ query, types = ['user.message', 'assistant.message'], limit = 50 } = {}) {
111
+ if (!query || typeof query !== 'string') {
112
+ throw new Error('query is required');
113
+ }
114
+ if (!fs.existsSync(COPILOT_ROOT)) return { matches: [], total: 0 };
115
+
116
+ const q = query.toLowerCase();
117
+ const dirs = fs
118
+ .readdirSync(COPILOT_ROOT, { withFileTypes: true })
119
+ .filter((d) => d.isDirectory())
120
+ .map((d) => d.name);
121
+
122
+ const matches = [];
123
+ for (const sessionId of dirs) {
124
+ const eventsPath = path.join(COPILOT_ROOT, sessionId, 'events.jsonl');
125
+ if (!fs.existsSync(eventsPath)) continue;
126
+ const events = readJsonl(eventsPath);
127
+ for (const e of events) {
128
+ if (!types.includes(e.type)) continue;
129
+ const text = typeof e.data?.content === 'string'
130
+ ? e.data.content
131
+ : typeof e.data?.text === 'string'
132
+ ? e.data.text
133
+ : JSON.stringify(e.data || {});
134
+ if (text.toLowerCase().includes(q)) {
135
+ matches.push({
136
+ sessionId,
137
+ timestamp: e.timestamp,
138
+ type: e.type,
139
+ excerpt: text.slice(0, 300),
140
+ });
141
+ if (matches.length >= limit) break;
142
+ }
143
+ }
144
+ if (matches.length >= limit) break;
145
+ }
146
+
147
+ return { query, total: matches.length, matches };
148
+ }
@@ -0,0 +1,23 @@
1
+ export function dispatch({ task, complexity, parallelizable }) {
2
+ const score =
3
+ typeof complexity === 'number'
4
+ ? complexity
5
+ : Math.min(100, Math.floor((task?.length || 0) / 10));
6
+ const isPar = parallelizable === true;
7
+
8
+ let route, reasoning;
9
+ if (score < 15) {
10
+ route = 'main-thread';
11
+ reasoning = `Score ${score} < 15. Inline in current context, no delegation overhead.`;
12
+ } else if (score < 40 && isPar) {
13
+ route = 'agent-subagent-worktree';
14
+ reasoning = `Score ${score} + parallelizable. Spawn Claude Code Agent tool with worktree isolation.`;
15
+ } else if (score < 40) {
16
+ route = 'mcp-worker-haiku';
17
+ reasoning = `Score ${score}, sequential. Delegate to lightweight Haiku worker via MCP.`;
18
+ } else {
19
+ route = 'main-thread-opus';
20
+ reasoning = `Score ${score} >= 40. Complex task, keep in main thread with Opus reasoning.`;
21
+ }
22
+ return { score, route, reasoning, parallelizable: isPar };
23
+ }
@@ -0,0 +1,163 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const PHASES = ['BRAINSTORM', 'PRUNE', 'DISPATCH', 'BUILD', 'VALIDATE', 'COMPLETED', 'ABORTED'];
5
+
6
+ function resolveRoot(projectRoot) {
7
+ return projectRoot || process.env.CLAUDE_PROJECT_DIR || process.cwd();
8
+ }
9
+
10
+ function statePath(projectRoot) {
11
+ return path.join(resolveRoot(projectRoot), '_byan-output', 'fd-state.json');
12
+ }
13
+
14
+ function ensureDir(filePath) {
15
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
16
+ }
17
+
18
+ function readState(projectRoot) {
19
+ const p = statePath(projectRoot);
20
+ if (!fs.existsSync(p)) return null;
21
+ try {
22
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ function writeState(state, projectRoot) {
29
+ const p = statePath(projectRoot);
30
+ ensureDir(p);
31
+ fs.writeFileSync(p, JSON.stringify(state, null, 2));
32
+ return p;
33
+ }
34
+
35
+ function slugify(s) {
36
+ return String(s || 'feature')
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9]+/g, '-')
39
+ .replace(/(^-|-$)/g, '')
40
+ .slice(0, 40);
41
+ }
42
+
43
+ function stampId(now = new Date(), slug) {
44
+ const pad = (n) => String(n).padStart(2, '0');
45
+ const s =
46
+ now.getFullYear().toString() +
47
+ pad(now.getMonth() + 1) +
48
+ pad(now.getDate()) +
49
+ '-' +
50
+ pad(now.getHours()) +
51
+ pad(now.getMinutes()) +
52
+ pad(now.getSeconds());
53
+ return `${s}-${slugify(slug)}`;
54
+ }
55
+
56
+ export function start({ featureName, projectRoot, now = new Date(), force = false } = {}) {
57
+ const existing = readState(projectRoot);
58
+ if (existing && !['COMPLETED', 'ABORTED'].includes(existing.phase) && !force) {
59
+ throw new Error(
60
+ `FD already in progress (phase ${existing.phase}, fd_id ${existing.fd_id}). Abort or complete it first, or pass force=true.`
61
+ );
62
+ }
63
+
64
+ const state = {
65
+ fd_id: stampId(now, featureName),
66
+ feature_name: featureName || 'unnamed',
67
+ phase: 'BRAINSTORM',
68
+ started_at: now.toISOString(),
69
+ updated_at: now.toISOString(),
70
+ phase_history: [{ phase: 'BRAINSTORM', entered_at: now.toISOString() }],
71
+ raw_ideas: [],
72
+ backlog: [],
73
+ dispatch_table: [],
74
+ commits: [],
75
+ notes: [],
76
+ };
77
+ writeState(state, projectRoot);
78
+ return state;
79
+ }
80
+
81
+ export function status({ projectRoot } = {}) {
82
+ const state = readState(projectRoot);
83
+ if (!state) {
84
+ return { active: false, phase: null, fd_id: null };
85
+ }
86
+ return {
87
+ active: !['COMPLETED', 'ABORTED'].includes(state.phase),
88
+ ...state,
89
+ };
90
+ }
91
+
92
+ const BRAINSTORM_MIN_IDEAS = 10;
93
+
94
+ export function advance({ to, note, projectRoot, now = new Date(), force = false } = {}) {
95
+ if (!PHASES.includes(to)) {
96
+ throw new Error(`Invalid target phase ${to}. Must be one of ${PHASES.join(', ')}`);
97
+ }
98
+ const state = readState(projectRoot);
99
+ if (!state) throw new Error('No active FD session. Call start() first.');
100
+ if (['COMPLETED', 'ABORTED'].includes(state.phase)) {
101
+ throw new Error(`Current FD session is ${state.phase} and cannot advance.`);
102
+ }
103
+
104
+ const order = PHASES.indexOf(state.phase);
105
+ const target = PHASES.indexOf(to);
106
+ if (target < order && !['ABORTED', 'COMPLETED'].includes(to)) {
107
+ throw new Error(
108
+ `Cannot move backwards from ${state.phase} to ${to}. Use abort() or fix the workflow.`
109
+ );
110
+ }
111
+
112
+ // BRAINSTORM exit gate : need >= BRAINSTORM_MIN_IDEAS raw ideas
113
+ if (
114
+ state.phase === 'BRAINSTORM' &&
115
+ to !== 'BRAINSTORM' &&
116
+ !['ABORTED'].includes(to) &&
117
+ !force
118
+ ) {
119
+ const n = Array.isArray(state.raw_ideas) ? state.raw_ideas.length : 0;
120
+ if (n < BRAINSTORM_MIN_IDEAS) {
121
+ throw new Error(
122
+ `BRAINSTORM requires at least ${BRAINSTORM_MIN_IDEAS} raw ideas before advancing (currently ${n}). Add more via update({ patch: { raw_ideas: [...] } }), or pass force=true to skip.`
123
+ );
124
+ }
125
+ }
126
+
127
+ state.phase = to;
128
+ state.updated_at = now.toISOString();
129
+ state.phase_history.push({ phase: to, entered_at: now.toISOString(), note: note || null });
130
+
131
+ writeState(state, projectRoot);
132
+ return state;
133
+ }
134
+
135
+ export function update({ patch = {}, projectRoot, now = new Date() } = {}) {
136
+ const state = readState(projectRoot);
137
+ if (!state) throw new Error('No active FD session.');
138
+
139
+ const allowed = ['raw_ideas', 'backlog', 'dispatch_table', 'commits', 'notes', 'feature_name'];
140
+ for (const key of Object.keys(patch)) {
141
+ if (!allowed.includes(key)) {
142
+ throw new Error(`Field "${key}" is not patchable. Allowed: ${allowed.join(', ')}`);
143
+ }
144
+ state[key] = patch[key];
145
+ }
146
+ state.updated_at = now.toISOString();
147
+
148
+ writeState(state, projectRoot);
149
+ return state;
150
+ }
151
+
152
+ export function abort({ reason, projectRoot, now = new Date() } = {}) {
153
+ const state = readState(projectRoot);
154
+ if (!state) throw new Error('No FD session to abort.');
155
+ state.phase = 'ABORTED';
156
+ state.updated_at = now.toISOString();
157
+ state.phase_history.push({ phase: 'ABORTED', entered_at: now.toISOString(), note: reason || null });
158
+ writeState(state, projectRoot);
159
+ return state;
160
+ }
161
+
162
+ export const ALL_PHASES = PHASES;
163
+ export const BRAINSTORM_MIN = BRAINSTORM_MIN_IDEAS;