create-byan-agent 2.8.1 → 2.9.0

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,174 @@
1
+ /**
2
+ * Claude Code CLI bridge adapter.
3
+ * Uses --print --output-format stream-json --input-format stream-json
4
+ * for persistent streaming sessions.
5
+ */
6
+
7
+ const { spawn } = require('child_process');
8
+ const { Bridge } = require('./bridge');
9
+
10
+ class ClaudeAdapter extends Bridge {
11
+ constructor(options) {
12
+ super(options);
13
+ this._buffer = '';
14
+ this._sessionId = null;
15
+ }
16
+
17
+ async start() {
18
+ const args = [
19
+ '--print',
20
+ '--output-format', 'stream-json',
21
+ '--input-format', 'stream-json',
22
+ ];
23
+
24
+ if (this.agent) {
25
+ const agentPath = this.resolveAgent(this.agent);
26
+ if (agentPath) {
27
+ args.push('--agent', agentPath);
28
+ }
29
+ }
30
+
31
+ if (this.model) {
32
+ args.push('--model', this.model);
33
+ }
34
+
35
+ if (this._sessionId) {
36
+ args.push('--session-id', this._sessionId);
37
+ }
38
+
39
+ this.process = spawn('claude', args, {
40
+ cwd: this.projectRoot,
41
+ env: { ...process.env },
42
+ stdio: ['pipe', 'pipe', 'pipe'],
43
+ });
44
+
45
+ this.active = true;
46
+
47
+ this.process.stdout.on('data', (data) => this._handleStdout(data));
48
+ this.process.stderr.on('data', (data) => this._handleStderr(data));
49
+
50
+ this.process.on('error', (err) => {
51
+ this.active = false;
52
+ this.onError(err);
53
+ });
54
+
55
+ this.process.on('exit', (code) => {
56
+ this.active = false;
57
+ this.onComplete({ code, sessionId: this._sessionId });
58
+ });
59
+ }
60
+
61
+ async send(message) {
62
+ if (!this.active || !this.process || !this.process.stdin.writable) {
63
+ if (!this.active) {
64
+ await this.start();
65
+ } else {
66
+ this.onError(new Error('Claude process stdin is not writable'));
67
+ return;
68
+ }
69
+ }
70
+
71
+ const payload = JSON.stringify({ type: 'user', content: message }) + '\n';
72
+
73
+ try {
74
+ this.process.stdin.write(payload);
75
+ } catch (err) {
76
+ this.onError(err);
77
+ }
78
+ }
79
+
80
+ async stop() {
81
+ this.active = false;
82
+ if (this.process) {
83
+ await this._killProcess(this.process);
84
+ this.process = null;
85
+ }
86
+ }
87
+
88
+ _handleStdout(data) {
89
+ this._buffer += data.toString();
90
+ const lines = this._buffer.split('\n');
91
+ this._buffer = lines.pop() || '';
92
+
93
+ for (const line of lines) {
94
+ const trimmed = line.trim();
95
+ if (!trimmed) continue;
96
+ this._parseLine(trimmed);
97
+ }
98
+ }
99
+
100
+ _handleStderr(data) {
101
+ const text = data.toString().trim();
102
+ if (text) {
103
+ this.onError(new Error(`claude stderr: ${text}`));
104
+ }
105
+ }
106
+
107
+ _parseLine(line) {
108
+ let event;
109
+ try {
110
+ event = JSON.parse(line);
111
+ } catch {
112
+ this.onChunk(line);
113
+ return;
114
+ }
115
+
116
+ switch (event.type) {
117
+ case 'assistant': {
118
+ const contents = event.message?.content || event.content || [];
119
+ const items = Array.isArray(contents) ? contents : [contents];
120
+ for (const item of items) {
121
+ if (typeof item === 'string') {
122
+ this.onChunk(item);
123
+ } else if (item.type === 'text') {
124
+ this.onChunk(item.text || '');
125
+ } else if (item.type === 'tool_use') {
126
+ this.onToolUse({
127
+ id: item.id,
128
+ name: item.name,
129
+ input: item.input,
130
+ });
131
+ }
132
+ }
133
+ break;
134
+ }
135
+
136
+ case 'content_block_delta': {
137
+ const delta = event.delta;
138
+ if (delta?.type === 'text_delta') {
139
+ this.onChunk(delta.text || '');
140
+ }
141
+ break;
142
+ }
143
+
144
+ case 'result': {
145
+ if (event.session_id) this._sessionId = event.session_id;
146
+ this.onComplete({
147
+ result: event.result,
148
+ cost: event.cost_usd,
149
+ sessionId: event.session_id,
150
+ });
151
+ break;
152
+ }
153
+
154
+ case 'tool_use': {
155
+ this.onToolUse({
156
+ id: event.id || event.tool_use_id,
157
+ name: event.name,
158
+ input: event.input,
159
+ });
160
+ break;
161
+ }
162
+
163
+ case 'error': {
164
+ this.onError(new Error(event.error || event.message || 'Unknown claude error'));
165
+ break;
166
+ }
167
+
168
+ default:
169
+ break;
170
+ }
171
+ }
172
+ }
173
+
174
+ module.exports = ClaudeAdapter;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * CLI auto-detection and BMAD agent scanner.
3
+ */
4
+
5
+ const { execFile } = require('child_process');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const CLI_DEFINITIONS = [
10
+ { name: 'claude', command: 'claude', versionArg: '--version' },
11
+ { name: 'copilot', command: 'copilot', versionArg: '--version' },
12
+ { name: 'codex', command: 'codex', versionArg: '--version' },
13
+ ];
14
+
15
+ function execPromise(cmd, args, timeoutMs = 5000) {
16
+ return new Promise((resolve) => {
17
+ try {
18
+ const proc = execFile(cmd, args, { timeout: timeoutMs }, (err, stdout, stderr) => {
19
+ if (err) {
20
+ resolve({ ok: false, output: '' });
21
+ return;
22
+ }
23
+ resolve({ ok: true, output: (stdout || stderr || '').trim() });
24
+ });
25
+ proc.on('error', () => resolve({ ok: false, output: '' }));
26
+ } catch {
27
+ resolve({ ok: false, output: '' });
28
+ }
29
+ });
30
+ }
31
+
32
+ function parseVersion(output) {
33
+ const match = output.match(/(\d+\.\d+[\w.-]*)/);
34
+ return match ? match[1] : null;
35
+ }
36
+
37
+ function whichSync(cmd) {
38
+ try {
39
+ const { execFileSync } = require('child_process');
40
+ return execFileSync('which', [cmd], { encoding: 'utf8', timeout: 3000 }).trim() || null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ async function detectCLIs() {
47
+ const results = [];
48
+
49
+ for (const def of CLI_DEFINITIONS) {
50
+ const cmdPath = whichSync(def.command);
51
+ if (!cmdPath) {
52
+ results.push({
53
+ name: def.name,
54
+ path: null,
55
+ version: null,
56
+ available: false,
57
+ preferred: false,
58
+ });
59
+ continue;
60
+ }
61
+
62
+ const versionResult = await execPromise(def.command, [def.versionArg]);
63
+ const version = versionResult.ok ? parseVersion(versionResult.output) : null;
64
+
65
+ results.push({
66
+ name: def.name,
67
+ path: cmdPath,
68
+ version,
69
+ available: true,
70
+ preferred: false,
71
+ });
72
+ }
73
+
74
+ const firstAvailable = results.find((r) => r.available);
75
+ if (firstAvailable) firstAvailable.preferred = true;
76
+
77
+ return results;
78
+ }
79
+
80
+ function parseFrontmatter(content) {
81
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
82
+ if (!match) return {};
83
+
84
+ const fm = {};
85
+ for (const line of match[1].split('\n')) {
86
+ const sep = line.indexOf(':');
87
+ if (sep === -1) continue;
88
+ const key = line.slice(0, sep).trim().replace(/^['"]|['"]$/g, '');
89
+ const val = line.slice(sep + 1).trim().replace(/^['"]|['"]$/g, '');
90
+ if (key && val) fm[key] = val;
91
+ }
92
+ return fm;
93
+ }
94
+
95
+ function scanDir(dirPath) {
96
+ try {
97
+ if (!fs.existsSync(dirPath)) return [];
98
+ return fs.readdirSync(dirPath).filter((f) => f.endsWith('.md'));
99
+ } catch {
100
+ return [];
101
+ }
102
+ }
103
+
104
+ async function detectAgents(projectRoot) {
105
+ const agents = [];
106
+ const seen = new Set();
107
+
108
+ const locations = [
109
+ { dir: path.join(projectRoot, '.github', 'agents'), source: 'copilot' },
110
+ { dir: path.join(projectRoot, '_byan', 'agents'), source: 'byan' },
111
+ ];
112
+
113
+ const bmadModules = ['core', 'bmm', 'bmb', 'tea', 'cis'];
114
+ for (const mod of bmadModules) {
115
+ locations.push({
116
+ dir: path.join(projectRoot, '_bmad', mod, 'agents'),
117
+ source: `bmad-${mod}`,
118
+ });
119
+ }
120
+
121
+ for (const loc of locations) {
122
+ const files = scanDir(loc.dir);
123
+ for (const file of files) {
124
+ const filePath = path.join(loc.dir, file);
125
+ const baseName = file.replace(/\.md$/, '');
126
+
127
+ const id = baseName
128
+ .replace(/^bmad-agent-/, '')
129
+ .replace(/\.backup\.\d+.*$/, '')
130
+ .replace(/\.optimized.*$/, '');
131
+
132
+ if (seen.has(id)) continue;
133
+ seen.add(id);
134
+
135
+ let fm = {};
136
+ try {
137
+ const content = fs.readFileSync(filePath, 'utf8').slice(0, 2000);
138
+ fm = parseFrontmatter(content);
139
+ } catch { /* skip unparseable */ }
140
+
141
+ agents.push({
142
+ id,
143
+ name: fm.name || id,
144
+ description: fm.description || '',
145
+ icon: fm.icon || null,
146
+ source: loc.source,
147
+ path: filePath,
148
+ });
149
+ }
150
+ }
151
+
152
+ agents.sort((a, b) => a.name.localeCompare(b.name));
153
+ return agents;
154
+ }
155
+
156
+ module.exports = { detectCLIs, detectAgents };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Codex CLI bridge adapter.
3
+ * Uses `codex exec` for non-interactive operation.
4
+ */
5
+
6
+ const { spawn } = require('child_process');
7
+ const { Bridge } = require('./bridge');
8
+
9
+ class CodexAdapter extends Bridge {
10
+ async start() {
11
+ this.active = true;
12
+ }
13
+
14
+ async send(message) {
15
+ if (!this.active) return;
16
+
17
+ const args = ['exec', '--prompt', message, '--approval-mode', 'auto-edit'];
18
+
19
+ this.process = spawn('codex', args, {
20
+ cwd: this.projectRoot,
21
+ env: { ...process.env },
22
+ stdio: ['pipe', 'pipe', 'pipe'],
23
+ });
24
+
25
+ let output = '';
26
+
27
+ this.process.stdout.on('data', (data) => {
28
+ const chunk = data.toString();
29
+ output += chunk;
30
+ this.onChunk(chunk);
31
+ });
32
+
33
+ this.process.stderr.on('data', (data) => {
34
+ const text = data.toString().trim();
35
+ if (text) this.onError(new Error(`codex stderr: ${text}`));
36
+ });
37
+
38
+ this.process.on('error', (err) => {
39
+ this.onError(err);
40
+ });
41
+
42
+ this.process.on('exit', (code) => {
43
+ this.process = null;
44
+ this.onComplete({ code, output });
45
+ });
46
+ }
47
+
48
+ async stop() {
49
+ this.active = false;
50
+ if (this.process) {
51
+ await this._killProcess(this.process);
52
+ this.process = null;
53
+ }
54
+ }
55
+ }
56
+
57
+ module.exports = CodexAdapter;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * GitHub Copilot CLI bridge adapter.
3
+ * Copilot CLI is TUI-based; this uses one-shot spawns per message.
4
+ * Full multi-turn PTY support deferred to P2.
5
+ */
6
+
7
+ const { spawn } = require('child_process');
8
+ const { Bridge } = require('./bridge');
9
+
10
+ class CopilotAdapter extends Bridge {
11
+ async start() {
12
+ this.active = true;
13
+ }
14
+
15
+ async send(message) {
16
+ if (!this.active) return;
17
+
18
+ const args = [];
19
+
20
+ if (this.agent) {
21
+ const agentPath = this.resolveAgent(this.agent);
22
+ if (agentPath) {
23
+ args.push(`--agent=${agentPath}`);
24
+ }
25
+ }
26
+
27
+ args.push('--prompt', message);
28
+
29
+ this.process = spawn('copilot', args, {
30
+ cwd: this.projectRoot,
31
+ env: { ...process.env },
32
+ stdio: ['pipe', 'pipe', 'pipe'],
33
+ });
34
+
35
+ let output = '';
36
+
37
+ this.process.stdout.on('data', (data) => {
38
+ const chunk = data.toString();
39
+ output += chunk;
40
+ this.onChunk(chunk);
41
+ });
42
+
43
+ this.process.stderr.on('data', (data) => {
44
+ const text = data.toString().trim();
45
+ if (text) this.onError(new Error(`copilot stderr: ${text}`));
46
+ });
47
+
48
+ this.process.on('error', (err) => {
49
+ this.onError(err);
50
+ });
51
+
52
+ this.process.on('exit', (code) => {
53
+ this.process = null;
54
+ this.onComplete({ code, output });
55
+ });
56
+ }
57
+
58
+ async stop() {
59
+ this.active = false;
60
+ if (this.process) {
61
+ await this._killProcess(this.process);
62
+ this.process = null;
63
+ }
64
+ }
65
+ }
66
+
67
+ module.exports = CopilotAdapter;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Chat session persistence -- save, load, list, export conversations.
3
+ * Stores under {projectRoot}/_byan/_memory/chat-sessions/
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const crypto = require('crypto');
9
+
10
+ class SessionManager {
11
+ constructor(projectRoot) {
12
+ this.projectRoot = projectRoot;
13
+ this.sessionsDir = path.join(projectRoot, '_byan', '_memory', 'chat-sessions');
14
+ this.sessions = new Map();
15
+ this._ensureDir();
16
+ }
17
+
18
+ _ensureDir() {
19
+ try {
20
+ if (!fs.existsSync(this.sessionsDir)) {
21
+ fs.mkdirSync(this.sessionsDir, { recursive: true });
22
+ }
23
+ } catch { /* best effort */ }
24
+ }
25
+
26
+ _generateId() {
27
+ const ts = Date.now().toString(36);
28
+ const rand = crypto.randomBytes(4).toString('hex');
29
+ return `chat-${ts}-${rand}`;
30
+ }
31
+
32
+ create(cliName, agentName) {
33
+ const id = this._generateId();
34
+ const session = {
35
+ id,
36
+ cli: cliName || 'claude',
37
+ agent: agentName || null,
38
+ created: new Date().toISOString(),
39
+ updated: new Date().toISOString(),
40
+ messages: [],
41
+ };
42
+
43
+ this.sessions.set(id, session);
44
+ this._saveToDisk(session);
45
+ return session;
46
+ }
47
+
48
+ addMessage(sessionId, role, content, metadata = {}) {
49
+ const session = this._getSession(sessionId);
50
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
51
+
52
+ session.messages.push({
53
+ role,
54
+ content,
55
+ timestamp: new Date().toISOString(),
56
+ ...metadata,
57
+ });
58
+
59
+ session.updated = new Date().toISOString();
60
+ this._saveToDisk(session);
61
+ }
62
+
63
+ save(sessionId) {
64
+ const session = this._getSession(sessionId);
65
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
66
+ this._saveToDisk(session);
67
+ }
68
+
69
+ load(sessionId) {
70
+ if (this.sessions.has(sessionId)) {
71
+ return this.sessions.get(sessionId);
72
+ }
73
+
74
+ const filePath = path.join(this.sessionsDir, `${sessionId}.json`);
75
+ try {
76
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
77
+ this.sessions.set(sessionId, data);
78
+ return data;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ list() {
85
+ this._loadAllFromDisk();
86
+
87
+ const summaries = [];
88
+ for (const session of this.sessions.values()) {
89
+ const lastMsg = session.messages[session.messages.length - 1];
90
+ summaries.push({
91
+ id: session.id,
92
+ cli: session.cli,
93
+ agent: session.agent,
94
+ created: session.created,
95
+ updated: session.updated,
96
+ messageCount: session.messages.length,
97
+ lastMessage: lastMsg
98
+ ? lastMsg.content.slice(0, 100)
99
+ : null,
100
+ });
101
+ }
102
+
103
+ summaries.sort((a, b) => b.updated.localeCompare(a.updated));
104
+ return summaries;
105
+ }
106
+
107
+ delete(sessionId) {
108
+ this.sessions.delete(sessionId);
109
+ const filePath = path.join(this.sessionsDir, `${sessionId}.json`);
110
+ try {
111
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
112
+ } catch { /* best effort */ }
113
+ }
114
+
115
+ exportJSON(sessionId) {
116
+ const session = this._getSession(sessionId);
117
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
118
+ return JSON.stringify(session, null, 2);
119
+ }
120
+
121
+ exportMarkdown(sessionId) {
122
+ const session = this._getSession(sessionId);
123
+ if (!session) throw new Error(`Session not found: ${sessionId}`);
124
+
125
+ const lines = [
126
+ `# Chat Session: ${session.id}`,
127
+ '',
128
+ `- **CLI:** ${session.cli}`,
129
+ `- **Agent:** ${session.agent || 'none'}`,
130
+ `- **Created:** ${session.created}`,
131
+ '',
132
+ '---',
133
+ '',
134
+ ];
135
+
136
+ for (const msg of session.messages) {
137
+ const label = msg.role === 'user' ? 'User' : 'Assistant';
138
+ lines.push(`### ${label} (${msg.timestamp})`);
139
+ lines.push('');
140
+ lines.push(msg.content);
141
+ lines.push('');
142
+ }
143
+
144
+ return lines.join('\n');
145
+ }
146
+
147
+ getSession(sessionId) {
148
+ return this._getSession(sessionId);
149
+ }
150
+
151
+ _getSession(sessionId) {
152
+ if (this.sessions.has(sessionId)) {
153
+ return this.sessions.get(sessionId);
154
+ }
155
+ return this.load(sessionId);
156
+ }
157
+
158
+ _saveToDisk(session) {
159
+ try {
160
+ this._ensureDir();
161
+ const filePath = path.join(this.sessionsDir, `${session.id}.json`);
162
+ fs.writeFileSync(filePath, JSON.stringify(session, null, 2), 'utf8');
163
+ } catch { /* best effort */ }
164
+ }
165
+
166
+ _loadAllFromDisk() {
167
+ try {
168
+ if (!fs.existsSync(this.sessionsDir)) return;
169
+ const files = fs.readdirSync(this.sessionsDir).filter((f) => f.endsWith('.json'));
170
+ for (const file of files) {
171
+ const id = file.replace(/\.json$/, '');
172
+ if (!this.sessions.has(id)) {
173
+ try {
174
+ const data = JSON.parse(
175
+ fs.readFileSync(path.join(this.sessionsDir, file), 'utf8')
176
+ );
177
+ this.sessions.set(id, data);
178
+ } catch { /* skip corrupted */ }
179
+ }
180
+ }
181
+ } catch { /* best effort */ }
182
+ }
183
+ }
184
+
185
+ module.exports = SessionManager;