@yeaft/webchat-agent 0.1.247 → 0.1.249

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.
@@ -1,170 +0,0 @@
1
- /**
2
- * Conductor — UI Message Helpers (V5)
3
- *
4
- * Sends WebSocket messages to server/frontend.
5
- * Maintains uiMessages persistence list on the conductor instance.
6
- *
7
- * No sessionId — Conductor is a singleton per Agent.
8
- */
9
- import ctx from '../context.js';
10
- import { saveConductorMeta } from './persistence.js';
11
-
12
- /**
13
- * Send conductor message to server (forwarded to Web frontend)
14
- */
15
- export function sendConductorMessage(msg) {
16
- if (ctx.sendToServer) {
17
- ctx.sendToServer(msg);
18
- }
19
- }
20
-
21
- /**
22
- * Send Conductor Claude output to frontend
23
- * outputType: 'text' | 'tool_use' | 'tool_result' | 'system' | 'task_created' | 'task_forwarded'
24
- */
25
- export function sendConductorOutput(conductor, outputType, rawMessage, extra = {}) {
26
- sendConductorMessage({
27
- type: 'conductor_output',
28
- outputType,
29
- data: rawMessage,
30
- ...extra
31
- });
32
-
33
- // Record trimmed UI messages
34
- if (outputType === 'text') {
35
- const content = rawMessage?.message?.content;
36
- let text = '';
37
- if (typeof content === 'string') {
38
- text = content;
39
- } else if (Array.isArray(content)) {
40
- text = content.filter(b => b.type === 'text').map(b => b.text).join('');
41
- }
42
- if (!text) return;
43
- let found = false;
44
- for (let i = conductor.uiMessages.length - 1; i >= 0; i--) {
45
- const msg = conductor.uiMessages[i];
46
- if (msg.source === 'conductor' && msg.type === 'text' && msg._streaming) {
47
- msg.content += text;
48
- found = true;
49
- break;
50
- }
51
- }
52
- if (!found) {
53
- conductor.uiMessages.push({
54
- source: 'conductor', type: 'text', content: text,
55
- _streaming: true, timestamp: Date.now()
56
- });
57
- }
58
- } else if (outputType === 'system') {
59
- const content = rawMessage?.message?.content;
60
- let text = '';
61
- if (typeof content === 'string') text = content;
62
- else if (Array.isArray(content)) text = content.filter(b => b.type === 'text').map(b => b.text).join('');
63
- if (!text) return;
64
- conductor.uiMessages.push({
65
- source: 'conductor', type: 'system', content: text, timestamp: Date.now()
66
- });
67
- } else if (outputType === 'task_created') {
68
- conductor.uiMessages.push({
69
- source: 'conductor', type: 'task_created',
70
- taskId: extra.taskId, taskTitle: extra.taskTitle,
71
- content: `Created task: ${extra.taskTitle}`,
72
- timestamp: Date.now()
73
- });
74
- } else if (outputType === 'task_forwarded') {
75
- conductor.uiMessages.push({
76
- source: 'conductor', type: 'task_forwarded',
77
- taskId: extra.taskId,
78
- content: `Forwarded message to task: ${extra.taskId}`,
79
- timestamp: Date.now()
80
- });
81
- } else if (outputType === 'tool_use') {
82
- endConductorStreaming(conductor);
83
- const content = rawMessage?.message?.content;
84
- if (Array.isArray(content)) {
85
- for (const block of content) {
86
- if (block.type === 'tool_use') {
87
- const input = block.input || {};
88
- const trimmedInput = {};
89
- if (input.file_path) trimmedInput.file_path = input.file_path;
90
- if (input.command) trimmedInput.command = input.command.substring(0, 200);
91
- if (input.pattern) trimmedInput.pattern = input.pattern;
92
- conductor.uiMessages.push({
93
- source: 'conductor', type: 'tool',
94
- toolName: block.name, toolId: block.id,
95
- toolInput: Object.keys(trimmedInput).length > 0 ? trimmedInput : null,
96
- content: `${block.name} ${input.file_path || input.command?.substring(0, 60) || ''}`,
97
- hasResult: false,
98
- timestamp: Date.now()
99
- });
100
- }
101
- }
102
- }
103
- } else if (outputType === 'tool_result') {
104
- const toolId = rawMessage?.message?.tool_use_id;
105
- if (toolId) {
106
- for (let i = conductor.uiMessages.length - 1; i >= 0; i--) {
107
- if (conductor.uiMessages[i].type === 'tool' && conductor.uiMessages[i].toolId === toolId) {
108
- conductor.uiMessages[i].hasResult = true;
109
- break;
110
- }
111
- }
112
- }
113
- }
114
- }
115
-
116
- /**
117
- * End Conductor streaming message
118
- */
119
- export function endConductorStreaming(conductor) {
120
- for (let i = conductor.uiMessages.length - 1; i >= 0; i--) {
121
- if (conductor.uiMessages[i].source === 'conductor' && conductor.uiMessages[i]._streaming) {
122
- delete conductor.uiMessages[i]._streaming;
123
- break;
124
- }
125
- }
126
- }
127
-
128
- /**
129
- * Record user message to uiMessages
130
- */
131
- export function recordUserMessage(conductor, content) {
132
- conductor.uiMessages.push({
133
- source: 'user', type: 'text', content,
134
- timestamp: Date.now()
135
- });
136
- }
137
-
138
- /**
139
- * Send conductor status update to frontend
140
- */
141
- export function sendStatusUpdate(conductor) {
142
- const tasks = {};
143
- for (const [taskId, t] of conductor.tasks) {
144
- tasks[taskId] = {
145
- taskId: t.taskId,
146
- title: t.title,
147
- workDir: t.workDir,
148
- scenario: t.scenario,
149
- status: t.status,
150
- activeActors: t.activeActors || [],
151
- currentStep: t.currentStep || '',
152
- lastUpdate: t.lastUpdate || t.updatedAt
153
- };
154
- }
155
-
156
- sendConductorMessage({
157
- type: 'conductor_status',
158
- status: conductor.status,
159
- tasks,
160
- costUsd: conductor.costUsd,
161
- totalInputTokens: conductor.totalInputTokens,
162
- totalOutputTokens: conductor.totalOutputTokens,
163
- activeClaudes: conductor.activeClaudes || 0
164
- });
165
-
166
- // Async persist
167
- saveConductorMeta(conductor).catch(e =>
168
- console.warn('[Conductor] Failed to save meta:', e.message)
169
- );
170
- }
@@ -1,182 +0,0 @@
1
- /**
2
- * Conductor — Worktree Management (V5)
3
- *
4
- * V5 strategy: each task gets ONE worktree at creation time.
5
- * Path: {workDir}/.conductor/task-N/worktree/
6
- *
7
- * This prevents file conflicts when multiple tasks work on the same project.
8
- * - Read-write actors (coding/testing/review) → cwd = worktree
9
- * - Read-only actors (planning/discussion/design) → cwd = {workDir}
10
- */
11
- import { promises as fs } from 'fs';
12
- import { join, resolve } from 'path';
13
- import { execFile as execFileCb } from 'child_process';
14
- import { promisify } from 'util';
15
-
16
- const execFile = promisify(execFileCb);
17
-
18
- /**
19
- * Create a worktree for a task.
20
- * Called at task creation time.
21
- *
22
- * @param {string} workDir - Project root directory (git repo root)
23
- * @param {string} taskId - Task ID (e.g. 'task-m1234-abcd')
24
- * @param {string} taskDir - Full task directory path ({workDir}/.conductor/{taskId})
25
- * @param {object} [options]
26
- * @param {string} [options.baseBranch='HEAD'] - Base ref for the worktree
27
- * @returns {Promise<string>} worktree path
28
- */
29
- export async function createTaskWorktree(workDir, taskId, taskDir, options = {}) {
30
- const { baseBranch = 'HEAD' } = options;
31
-
32
- const wtDir = join(taskDir, 'worktree');
33
- const branch = `conductor/${taskId}`;
34
-
35
- // Check if worktree already exists
36
- const exists = await directoryExists(wtDir);
37
- if (exists) {
38
- const known = await isKnownWorktree(workDir, wtDir);
39
- if (known) {
40
- console.log(`[Conductor/Worktree] Already exists: ${wtDir}`);
41
- return wtDir;
42
- }
43
- // Orphaned directory, clean up and recreate
44
- console.warn(`[Conductor/Worktree] Orphaned dir, removing: ${wtDir}`);
45
- await fs.rm(wtDir, { recursive: true, force: true }).catch(() => {});
46
- }
47
-
48
- // Check if this is a git repo
49
- const isGitRepo = await directoryExists(join(workDir, '.git'));
50
- if (!isGitRepo) {
51
- // Not a git repo — just create a regular directory (no worktree isolation possible)
52
- console.warn(`[Conductor/Worktree] ${workDir} is not a git repo, creating plain directory`);
53
- await fs.mkdir(wtDir, { recursive: true });
54
- return wtDir;
55
- }
56
-
57
- try {
58
- // Create branch (ignore if already exists)
59
- try {
60
- await execFile('git', ['branch', branch, baseBranch], { cwd: workDir });
61
- } catch {
62
- // Branch already exists, ignore
63
- }
64
-
65
- // Create worktree
66
- await execFile('git', ['worktree', 'add', wtDir, branch], { cwd: workDir });
67
- console.log(`[Conductor/Worktree] Created: ${wtDir} on branch ${branch}`);
68
- return wtDir;
69
- } catch (e) {
70
- console.error(`[Conductor/Worktree] Failed to create worktree for ${taskId}:`, e.message);
71
- // Fallback: create plain directory so task can still function
72
- await fs.mkdir(wtDir, { recursive: true }).catch(() => {});
73
- return wtDir;
74
- }
75
- }
76
-
77
- /**
78
- * Clean up a task's worktree
79
- *
80
- * @param {string} workDir - Project root directory
81
- * @param {string} taskId - Task ID
82
- * @param {string} taskDir - Full task directory path
83
- */
84
- export async function cleanupTaskWorktree(workDir, taskId, taskDir) {
85
- const wtDir = join(taskDir, 'worktree');
86
- const branch = `conductor/${taskId}`;
87
-
88
- if (!(await directoryExists(wtDir))) return;
89
-
90
- const isGitRepo = await directoryExists(join(workDir, '.git'));
91
- if (!isGitRepo) {
92
- // Plain directory, just remove
93
- await fs.rm(wtDir, { recursive: true, force: true }).catch(() => {});
94
- return;
95
- }
96
-
97
- try {
98
- await execFile('git', ['worktree', 'remove', wtDir, '--force'], { cwd: workDir });
99
- console.log(`[Conductor/Worktree] Removed: ${wtDir}`);
100
- } catch (e) {
101
- console.warn(`[Conductor/Worktree] Failed to remove ${wtDir}:`, e.message);
102
- }
103
-
104
- try {
105
- await execFile('git', ['branch', '-D', branch], { cwd: workDir });
106
- console.log(`[Conductor/Worktree] Deleted branch: ${branch}`);
107
- } catch (e) {
108
- console.warn(`[Conductor/Worktree] Failed to delete branch ${branch}:`, e.message);
109
- }
110
- }
111
-
112
- /**
113
- * Clean up all conductor worktrees under a project
114
- *
115
- * @param {string} workDir - Project root directory
116
- */
117
- export async function cleanupAllConductorWorktrees(workDir) {
118
- const conductorDir = join(workDir, '.conductor');
119
-
120
- if (!(await directoryExists(conductorDir))) return;
121
-
122
- try {
123
- const entries = await fs.readdir(conductorDir);
124
- for (const entry of entries) {
125
- if (!entry.startsWith('task-')) continue;
126
- const taskDir = join(conductorDir, entry);
127
- const stat = await fs.stat(taskDir).catch(() => null);
128
- if (stat?.isDirectory()) {
129
- await cleanupTaskWorktree(workDir, entry, taskDir);
130
- }
131
- }
132
- } catch (e) {
133
- console.error(`[Conductor/Worktree] Failed to cleanup all:`, e.message);
134
- }
135
- }
136
-
137
- /**
138
- * Get the worktree path for a task (for cwd routing)
139
- */
140
- export function getTaskWorktreePath(taskDir) {
141
- return join(taskDir, 'worktree');
142
- }
143
-
144
- /**
145
- * Determine actor cwd based on specialty:
146
- * - Read-write actors → worktree
147
- * - Read-only actors → workDir (main project directory)
148
- */
149
- export function getActorCwd(workDir, taskDir, specialty) {
150
- const readWriteSpecialties = new Set(['coding', 'testing', 'review']);
151
- if (readWriteSpecialties.has(specialty)) {
152
- return getTaskWorktreePath(taskDir);
153
- }
154
- return workDir;
155
- }
156
-
157
- // =====================================================================
158
- // Helpers
159
- // =====================================================================
160
-
161
- async function directoryExists(dir) {
162
- try {
163
- await fs.access(dir);
164
- return true;
165
- } catch {
166
- return false;
167
- }
168
- }
169
-
170
- async function isKnownWorktree(projectDir, wtDir) {
171
- const normalizedWtDir = resolve(wtDir);
172
- try {
173
- const { stdout } = await execFile('git', ['worktree', 'list', '--porcelain'], { cwd: projectDir });
174
- for (const line of stdout.split('\n')) {
175
- if (line.startsWith('worktree ')) {
176
- const knownPath = resolve(line.slice('worktree '.length).trim());
177
- if (knownPath === normalizedWtDir) return true;
178
- }
179
- }
180
- } catch { /* ignore */ }
181
- return false;
182
- }
package/conductor.js DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * Conductor Mode — V5 Entry Point
3
- *
4
- * Aggregates and re-exports conductor sub-module public API.
5
- * Singleton model: one Conductor per Agent.
6
- */
7
-
8
- // Session core (singleton)
9
- export {
10
- getConductor,
11
- initConductor,
12
- handleConductorUserInput,
13
- stopConductor,
14
- clearConductor
15
- } from './conductor/session.js';
16
-
17
- // Persistence
18
- export {
19
- handleLoadConductorHistory,
20
- loadState as loadConductorState,
21
- ensureConductorHome
22
- } from './conductor/persistence.js';
23
-
24
- // UI messages
25
- import { sendConductorMessage } from './conductor/ui-messages.js';
26
- import { getConductor } from './conductor/session.js';
27
- import { handleLoadConductorHistory as _handleLoadConductorHistory } from './conductor/persistence.js';
28
-
29
- // Dependency injection wrapper for history loading
30
- export async function handleConductorHistory(msg) {
31
- return _handleLoadConductorHistory(msg, getConductor(), sendConductorMessage);
32
- }
33
-
34
- // Semaphore
35
- export { globalSemaphore } from './conductor/semaphore.js';