agent-muster 0.1.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.
package/dist/server.js ADDED
@@ -0,0 +1,283 @@
1
+ import express from 'express';
2
+ import { execFile } from 'node:child_process';
3
+ import { createServer } from 'node:http';
4
+ import { readdir, realpath, stat } from 'node:fs/promises';
5
+ import { homedir } from 'node:os';
6
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { promisify } from 'node:util';
9
+ const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../web/dist');
10
+ const execFileAsync = promisify(execFile);
11
+ async function chooseProjectDirectory() {
12
+ const platform = process.platform;
13
+ const command = platform === 'darwin' ? 'osascript' : platform === 'win32' ? 'powershell.exe' : 'zenity';
14
+ const args = platform === 'darwin'
15
+ ? ['-e', 'try\nPOSIX path of (choose folder with prompt "选择项目目录")\non error number -128\nreturn ""\nend try']
16
+ : platform === 'win32'
17
+ ? ['-NoProfile', '-STA', '-Command', 'Add-Type -AssemblyName System.Windows.Forms; $dialog = New-Object System.Windows.Forms.FolderBrowserDialog; if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Write($dialog.SelectedPath) }']
18
+ : ['--file-selection', '--directory', '--title=选择项目目录'];
19
+ try {
20
+ const { stdout } = await execFileAsync(command, args);
21
+ return stdout.trim() || null;
22
+ }
23
+ catch (error) {
24
+ if (platform === 'linux' && error.code === 1)
25
+ return null;
26
+ throw error;
27
+ }
28
+ }
29
+ export async function startServer(port, registry, projects, sessions, dingtalk) {
30
+ const app = express();
31
+ app.disable('x-powered-by');
32
+ app.use((request, response, next) => {
33
+ if (request.headers.host !== `127.0.0.1:${port}`) {
34
+ response.status(403).json({ error: 'Invalid host' });
35
+ return;
36
+ }
37
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
38
+ const origin = request.headers.origin;
39
+ if (origin && origin !== `http://127.0.0.1:${port}`) {
40
+ response.status(403).json({ error: 'Invalid origin' });
41
+ return;
42
+ }
43
+ }
44
+ next();
45
+ });
46
+ app.use('/api', express.json({ limit: '128kb' }));
47
+ app.get('/api/agents', (_request, response) => response.json(registry.list()));
48
+ app.get('/api/projects', (_request, response) => response.json(projects.list()));
49
+ app.post('/api/projects', async (request, response) => {
50
+ try {
51
+ if (typeof request.body?.path !== 'string')
52
+ throw new Error('Project directory is required');
53
+ response.status(201).json({ path: await projects.add(request.body.path) });
54
+ }
55
+ catch (error) {
56
+ response.status(400).json({ error: error.message });
57
+ }
58
+ });
59
+ app.post('/api/projects/pick', async (_request, response) => {
60
+ try {
61
+ const path = await chooseProjectDirectory();
62
+ response.json({ path: path ? await projects.add(path) : null });
63
+ }
64
+ catch (error) {
65
+ response.status(500).json({ error: error.message });
66
+ }
67
+ });
68
+ app.delete('/api/projects', async (request, response) => {
69
+ try {
70
+ const path = request.body?.path;
71
+ const projectSessions = typeof path === 'string' ? sessions.list().filter(item => item.cwd === path) : [];
72
+ const registered = typeof path === 'string' && projects.list().includes(path);
73
+ if (typeof path !== 'string' || (!registered && !projectSessions.length)) {
74
+ response.status(404).json({ error: 'Project not found' });
75
+ return;
76
+ }
77
+ await dingtalk.unbindProject(path);
78
+ for (const session of projectSessions) {
79
+ await dingtalk.unbindSession(session.id);
80
+ await sessions.remove(session.id);
81
+ }
82
+ if (registered)
83
+ await projects.remove(path);
84
+ response.json({ path });
85
+ }
86
+ catch (error) {
87
+ response.status(500).json({ error: error.message });
88
+ }
89
+ });
90
+ app.get('/api/dingtalk/apps', (_request, response) => response.json(dingtalk.list()));
91
+ app.get('/api/dingtalk/apps/:id/icon', async (request, response) => {
92
+ try {
93
+ const icon = await dingtalk.getIcon(request.params.id);
94
+ if (!icon) {
95
+ response.status(404).end();
96
+ return;
97
+ }
98
+ response.type(icon.mime).send(icon.data);
99
+ }
100
+ catch (error) {
101
+ response.status(500).json({ error: error.message });
102
+ }
103
+ });
104
+ app.post('/api/dingtalk/apps/:id/sync', async (request, response) => {
105
+ try {
106
+ response.json(await dingtalk.syncMetadata(request.params.id));
107
+ }
108
+ catch (error) {
109
+ response.status(502).json({ error: error.message });
110
+ }
111
+ });
112
+ app.post('/api/dingtalk/registration', async (_request, response) => {
113
+ try {
114
+ response.status(201).json(await dingtalk.start());
115
+ }
116
+ catch (error) {
117
+ response.status(502).json({ error: error.message });
118
+ }
119
+ });
120
+ app.get('/api/dingtalk/registration/:id', async (request, response) => {
121
+ try {
122
+ response.json(await dingtalk.poll(request.params.id));
123
+ }
124
+ catch (error) {
125
+ response.status(502).json({ error: error.message });
126
+ }
127
+ });
128
+ app.patch('/api/dingtalk/apps/:id', async (request, response) => {
129
+ try {
130
+ const { project, agent } = request.body || {};
131
+ if (project !== undefined && project !== null && typeof project !== 'string')
132
+ throw new Error('项目无效');
133
+ if (agent !== undefined && agent !== null && typeof agent !== 'string')
134
+ throw new Error('Agent 无效');
135
+ response.json(await dingtalk.bind(request.params.id, { project, agent }, projects.list(), registry.list().filter(item => item.installed).map(item => item.id)));
136
+ }
137
+ catch (error) {
138
+ response.status(400).json({ error: error.message });
139
+ }
140
+ });
141
+ app.delete('/api/dingtalk/apps/:id', async (request, response) => {
142
+ try {
143
+ if (!dingtalk.getBinding(request.params.id)) {
144
+ response.status(404).json({ error: '钉钉应用不存在' });
145
+ return;
146
+ }
147
+ await dingtalk.remove(request.params.id);
148
+ response.json({ id: request.params.id });
149
+ }
150
+ catch (error) {
151
+ response.status(500).json({ error: error.message });
152
+ }
153
+ });
154
+ app.get('/api/sessions', (_request, response) => response.json(sessions.list()));
155
+ app.get('/api/sessions/:id', (request, response) => {
156
+ const session = sessions.get(request.params.id);
157
+ if (!session) {
158
+ response.status(404).json({ error: 'Session not found' });
159
+ return;
160
+ }
161
+ response.json(session);
162
+ });
163
+ app.get('/api/directories', async (request, response) => {
164
+ try {
165
+ const requested = typeof request.query.path === 'string' ? request.query.path : process.cwd();
166
+ if (!isAbsolute(requested))
167
+ throw new Error('Absolute path required');
168
+ const path = await realpath(requested);
169
+ if (!(await stat(path)).isDirectory())
170
+ throw new Error('Directory required');
171
+ const entries = await readdir(path, { withFileTypes: true });
172
+ response.json({
173
+ path,
174
+ parent: dirname(path),
175
+ home: homedir(),
176
+ directories: entries.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
177
+ .map(entry => entry.name).sort((a, b) => a.localeCompare(b)),
178
+ });
179
+ }
180
+ catch (error) {
181
+ response.status(400).json({ error: error.message });
182
+ }
183
+ });
184
+ app.post('/api/sessions', async (request, response) => {
185
+ try {
186
+ const { agent, cwd, prompt, model } = request.body || {};
187
+ if (typeof agent !== 'string' || !registry.get(agent)?.installed)
188
+ throw new Error('Agent is not installed');
189
+ if (typeof cwd !== 'string' || typeof prompt !== 'string')
190
+ throw new Error('Project and prompt are required');
191
+ if (model !== undefined && (typeof model !== 'string' || (model && !registry.get(agent)?.models.some(item => item.id === model))))
192
+ throw new Error('Invalid model');
193
+ response.status(201).json(await sessions.create(agent, cwd, prompt, model || undefined));
194
+ }
195
+ catch (error) {
196
+ response.status(400).json({ error: error.message });
197
+ }
198
+ });
199
+ app.post('/api/sessions/:id/messages', (request, response) => {
200
+ try {
201
+ const { content, model } = request.body || {};
202
+ if (typeof content !== 'string')
203
+ throw new Error('Message is required');
204
+ const session = sessions.get(request.params.id);
205
+ if (!session)
206
+ throw new Error('Session not found');
207
+ if (model !== undefined && (typeof model !== 'string' || (model && !registry.get(session.agent)?.models.some(item => item.id === model))))
208
+ throw new Error('Invalid model');
209
+ response.json(sessions.send(request.params.id, content, model));
210
+ }
211
+ catch (error) {
212
+ response.status(400).json({ error: error.message });
213
+ }
214
+ });
215
+ app.post('/api/sessions/:id/dingtalk/messages', async (request, response) => {
216
+ try {
217
+ const { content } = request.body || {};
218
+ if (typeof content !== 'string' || !content.trim())
219
+ throw new Error('Message is required');
220
+ const session = sessions.get(request.params.id);
221
+ if (!session)
222
+ throw new Error('Session not found');
223
+ if (!session.dingtalkAppId)
224
+ throw new Error('此对话未关联钉钉应用');
225
+ await dingtalk.sendToSession(session.dingtalkAppId, session.id, content.trim());
226
+ response.json(sessions.recordDingTalkMessage(session.id, content.trim()));
227
+ }
228
+ catch (error) {
229
+ console.error(`DingTalk message sending failed for session ${request.params.id}:`, error);
230
+ response.status(400).json({ error: error.message });
231
+ }
232
+ });
233
+ app.post('/api/sessions/:id/stop', (request, response) => {
234
+ try {
235
+ response.json(sessions.stop(request.params.id));
236
+ }
237
+ catch (error) {
238
+ response.status(400).json({ error: error.message });
239
+ }
240
+ });
241
+ app.delete('/api/sessions/:id', async (request, response) => {
242
+ try {
243
+ if (!sessions.get(request.params.id)) {
244
+ response.status(404).json({ error: 'Session not found' });
245
+ return;
246
+ }
247
+ await dingtalk.unbindSession(request.params.id);
248
+ await sessions.remove(request.params.id);
249
+ response.json({ id: request.params.id });
250
+ }
251
+ catch (error) {
252
+ response.status(500).json({ error: error.message });
253
+ }
254
+ });
255
+ app.get('/api/sessions/:id/events', (request, response) => {
256
+ const session = sessions.get(request.params.id);
257
+ if (!session) {
258
+ response.status(404).json({ error: 'Session not found' });
259
+ return;
260
+ }
261
+ response.setHeader('Content-Type', 'text/event-stream');
262
+ response.setHeader('Cache-Control', 'no-cache');
263
+ response.setHeader('Connection', 'keep-alive');
264
+ response.flushHeaders();
265
+ const after = Math.max(Number(request.query.after || 0), Number(request.headers['last-event-id'] || 0));
266
+ const send = (event) => response.write(`id: ${event.id}\ndata: ${JSON.stringify(event)}\n\n`);
267
+ for (const event of session.events)
268
+ if (event.id > after)
269
+ send(event);
270
+ const listener = (event) => send(event);
271
+ sessions.on(session.id, listener);
272
+ const heartbeat = setInterval(() => response.write(': keepalive\n\n'), 15000);
273
+ request.on('close', () => { clearInterval(heartbeat); sessions.off(session.id, listener); });
274
+ });
275
+ app.use(express.static(webRoot));
276
+ app.get('/{*path}', (_request, response) => response.sendFile(join(webRoot, 'index.html')));
277
+ const server = createServer(app);
278
+ await new Promise((done, reject) => {
279
+ server.once('error', reject);
280
+ server.listen(port, '127.0.0.1', done);
281
+ });
282
+ return server;
283
+ }
@@ -0,0 +1,302 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { EventEmitter } from 'node:events';
3
+ import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { join, isAbsolute, relative, resolve } from 'node:path';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { adapters } from './adapters.js';
8
+ const sessionDirectory = join(homedir(), '.agent-muster', 'sessions');
9
+ const legacyDataFile = join(homedir(), '.agent-muster', 'sessions.json');
10
+ function sessionFile(id) {
11
+ return join(sessionDirectory, `${encodeURIComponent(id)}.json`);
12
+ }
13
+ async function writeSession(id, snapshot) {
14
+ await mkdir(sessionDirectory, { recursive: true });
15
+ const file = sessionFile(id);
16
+ const temporary = `${file}.${process.pid}.tmp`;
17
+ await writeFile(temporary, snapshot);
18
+ await rename(temporary, file);
19
+ }
20
+ function isWarning(text) {
21
+ return text.trim().split(/\r?\n/).every(line => /^\S+\s+WARN\b/.test(line));
22
+ }
23
+ function fileDiff(cwd, path) {
24
+ const file = resolve(cwd, path);
25
+ const localPath = relative(cwd, file);
26
+ if (localPath.startsWith('..') || isAbsolute(localPath))
27
+ return undefined;
28
+ const repository = spawnSync('git', ['-C', cwd, 'rev-parse', '--is-inside-work-tree'], { encoding: 'utf8', timeout: 2000 });
29
+ if (repository.status !== 0 || repository.stdout.trim() !== 'true')
30
+ return undefined;
31
+ const tracked = spawnSync('git', ['-C', cwd, 'ls-files', '--error-unmatch', '--', localPath], { encoding: 'utf8', timeout: 2000 });
32
+ const args = tracked.status === 0
33
+ ? ['-C', cwd, 'diff', '--no-ext-diff', 'HEAD', '--', localPath]
34
+ : ['-C', cwd, 'diff', '--no-ext-diff', '--no-index', '--', process.platform === 'win32' ? 'NUL' : '/dev/null', file];
35
+ const result = spawnSync('git', args, { encoding: 'utf8', timeout: 2000, maxBuffer: 1_000_000 });
36
+ return result.error ? undefined : result.stdout?.trim() || undefined;
37
+ }
38
+ export class SessionManager extends EventEmitter {
39
+ sessions = new Map();
40
+ processes = new Map();
41
+ writeQueue = Promise.resolve();
42
+ async load() {
43
+ const restore = (session) => {
44
+ if (session.status === 'running' || session.status === 'starting') {
45
+ session.status = 'stopped';
46
+ session.pid = undefined;
47
+ }
48
+ session.events ||= [];
49
+ for (const event of session.events) {
50
+ if (event.type === 'error' && isWarning(event.text))
51
+ event.type = 'warning';
52
+ }
53
+ this.sessions.set(session.id, session);
54
+ };
55
+ try {
56
+ const files = await readdir(sessionDirectory);
57
+ for (const file of files.filter(name => name.endsWith('.json'))) {
58
+ restore(JSON.parse(await readFile(join(sessionDirectory, file), 'utf8')));
59
+ }
60
+ }
61
+ catch (error) {
62
+ if (error.code !== 'ENOENT')
63
+ throw error;
64
+ }
65
+ try {
66
+ const stored = JSON.parse(await readFile(legacyDataFile, 'utf8'));
67
+ for (const session of stored) {
68
+ const existing = this.sessions.get(session.id);
69
+ if (existing && existing.updatedAt >= session.updatedAt)
70
+ continue;
71
+ restore(session);
72
+ await writeSession(session.id, JSON.stringify(session));
73
+ }
74
+ await rename(legacyDataFile, `${legacyDataFile}.${Date.now()}.bak`);
75
+ }
76
+ catch (error) {
77
+ if (error.code !== 'ENOENT')
78
+ throw error;
79
+ }
80
+ }
81
+ list() {
82
+ return [...this.sessions.values()].sort((a, b) => b.updatedAt - a.updatedAt);
83
+ }
84
+ get(id) {
85
+ return this.sessions.get(id);
86
+ }
87
+ linkDingTalkSession(id, appId) {
88
+ const session = this.sessions.get(id);
89
+ if (!session || session.dingtalkAppId === appId)
90
+ return;
91
+ session.dingtalkAppId = appId;
92
+ this.save(session);
93
+ }
94
+ isRunning(id) {
95
+ return this.processes.has(id);
96
+ }
97
+ save(session) {
98
+ const snapshot = JSON.stringify(session);
99
+ this.writeQueue = this.writeQueue.then(() => writeSession(session.id, snapshot))
100
+ .catch(error => console.error('Failed to save session:', error));
101
+ }
102
+ append(session, type, text, detail, kind, comparison, dingtalkConversation) {
103
+ const event = {
104
+ id: (session.events.at(-1)?.id || 0) + 1,
105
+ type,
106
+ text: text.slice(0, 50000),
107
+ ...(detail === undefined ? {} : { detail: detail.slice(0, 50000) }),
108
+ ...(kind === undefined ? {} : { kind }),
109
+ ...(comparison === undefined ? {} : { comparison }),
110
+ timestamp: Date.now(),
111
+ ...(dingtalkConversation ? { dingtalkConversation } : {}),
112
+ };
113
+ session.events.push(event);
114
+ if (session.events.length > 500)
115
+ session.events.splice(0, session.events.length - 500);
116
+ session.updatedAt = event.timestamp;
117
+ this.emit(session.id, event);
118
+ this.save(session);
119
+ }
120
+ status(session, status) {
121
+ session.status = status;
122
+ this.append(session, 'status', status);
123
+ }
124
+ addUsage(session, usage, total = false) {
125
+ session.usage = total ? usage : {
126
+ inputTokens: (session.usage?.inputTokens || 0) + usage.inputTokens,
127
+ outputTokens: (session.usage?.outputTokens || 0) + usage.outputTokens,
128
+ };
129
+ this.append(session, 'usage', JSON.stringify(session.usage));
130
+ }
131
+ async create(agent, cwd, prompt, model, dingtalkAppId, dingtalkConversation, images) {
132
+ if (!adapters[agent])
133
+ throw new Error('Unsupported agent');
134
+ if (!prompt.trim())
135
+ throw new Error('Prompt is required');
136
+ if (!isAbsolute(cwd) || !(await stat(cwd).then(value => value.isDirectory()).catch(() => false))) {
137
+ throw new Error('Project directory must be an existing absolute path');
138
+ }
139
+ const now = Date.now();
140
+ const session = {
141
+ id: `ses_${randomUUID()}`,
142
+ agent,
143
+ ...(dingtalkAppId ? { dingtalkAppId } : {}),
144
+ ...(dingtalkConversation ? { dingtalkConversation } : {}),
145
+ model,
146
+ cwd: resolve(cwd),
147
+ prompt: prompt.trim(),
148
+ status: 'starting',
149
+ createdAt: now,
150
+ updatedAt: now,
151
+ events: [],
152
+ };
153
+ this.sessions.set(session.id, session);
154
+ this.append(session, 'message', session.prompt, undefined, undefined, undefined, dingtalkConversation);
155
+ this.start(session, session.prompt, images);
156
+ return session;
157
+ }
158
+ send(id, message, model, dingtalkConversation, images) {
159
+ const session = this.sessions.get(id);
160
+ if (!session)
161
+ throw new Error('Session not found');
162
+ if (!message.trim())
163
+ throw new Error('Message is required');
164
+ if (this.processes.has(id))
165
+ throw new Error('Wait for the current task to finish or stop it');
166
+ if (!session.externalSessionId)
167
+ throw new Error('This agent did not create a resumable session');
168
+ if (model !== undefined)
169
+ session.model = model || undefined;
170
+ if (dingtalkConversation)
171
+ session.dingtalkConversation = dingtalkConversation;
172
+ this.append(session, 'message', message.trim(), undefined, undefined, undefined, dingtalkConversation);
173
+ this.start(session, message.trim(), images);
174
+ return session;
175
+ }
176
+ recordDingTalkMessage(id, content) {
177
+ const session = this.sessions.get(id);
178
+ if (!session)
179
+ throw new Error('Session not found');
180
+ this.append(session, 'dingtalk_message', content);
181
+ return session;
182
+ }
183
+ stop(id) {
184
+ const session = this.sessions.get(id);
185
+ if (!session)
186
+ throw new Error('Session not found');
187
+ const child = this.processes.get(id);
188
+ if (!child)
189
+ throw new Error('Session is not running');
190
+ this.status(session, 'stopped');
191
+ if (process.platform === 'win32')
192
+ child.kill('SIGTERM');
193
+ else if (child.pid)
194
+ process.kill(-child.pid, 'SIGTERM');
195
+ return session;
196
+ }
197
+ async remove(id) {
198
+ if (!this.sessions.has(id))
199
+ throw new Error('Session not found');
200
+ const child = this.processes.get(id);
201
+ if (child) {
202
+ const closed = new Promise(resolve => child.once('close', () => resolve()));
203
+ this.stop(id);
204
+ await closed;
205
+ }
206
+ await this.writeQueue;
207
+ await unlink(sessionFile(id));
208
+ this.sessions.delete(id);
209
+ }
210
+ shutdown() {
211
+ for (const id of this.processes.keys())
212
+ this.stop(id);
213
+ }
214
+ start(session, prompt, images) {
215
+ const adapter = adapters[session.agent];
216
+ const launch = adapter.launch(prompt, session.externalSessionId, session.model, images);
217
+ const child = spawn(launch.command, launch.args, {
218
+ cwd: session.cwd,
219
+ stdio: 'pipe',
220
+ detached: process.platform !== 'win32',
221
+ env: process.env,
222
+ });
223
+ this.processes.set(session.id, child);
224
+ session.pid = child.pid;
225
+ this.status(session, 'running');
226
+ child.stdin.end(launch.prompt === undefined ? undefined : `${launch.prompt}\n`);
227
+ let pending = '';
228
+ let stderrPending = '';
229
+ let failed = false;
230
+ const recordStderr = (line) => {
231
+ if (line.trim())
232
+ this.append(session, isWarning(line) ? 'warning' : 'error', line);
233
+ };
234
+ child.stdout.on('data', (chunk) => {
235
+ pending += chunk.toString('utf8');
236
+ const lines = pending.split('\n');
237
+ pending = lines.pop() || '';
238
+ for (const line of lines) {
239
+ const result = adapter.read(line.trim());
240
+ if (result.externalSessionId) {
241
+ session.externalSessionId = result.externalSessionId;
242
+ this.save(session);
243
+ }
244
+ if (result.text)
245
+ this.append(session, 'output', result.text);
246
+ if (result.usage)
247
+ this.addUsage(session, result.usage, result.usageIsTotal);
248
+ for (const tool of result.tools || [])
249
+ this.append(session, 'tool', tool.name, tool.detail);
250
+ for (const change of result.changes || []) {
251
+ const diff = change.diff || fileDiff(session.cwd, change.path);
252
+ this.append(session, 'file_change', change.path, diff, change.kind, change.diff || !diff ? undefined : 'HEAD');
253
+ }
254
+ if (result.error) {
255
+ failed = true;
256
+ this.append(session, 'error', result.error);
257
+ }
258
+ }
259
+ });
260
+ child.stderr.on('data', (chunk) => {
261
+ stderrPending += chunk.toString('utf8');
262
+ const lines = stderrPending.split('\n');
263
+ stderrPending = lines.pop() || '';
264
+ for (const line of lines)
265
+ recordStderr(line);
266
+ });
267
+ child.on('error', error => {
268
+ this.append(session, 'error', error.message);
269
+ this.processes.delete(session.id);
270
+ session.pid = undefined;
271
+ if (session.status !== 'stopped')
272
+ this.status(session, 'failed');
273
+ this.emit(`${session.id}:idle`);
274
+ });
275
+ child.on('close', code => {
276
+ recordStderr(stderrPending);
277
+ if (pending.trim()) {
278
+ const result = adapter.read(pending.trim());
279
+ if (result.text)
280
+ this.append(session, 'output', result.text);
281
+ if (result.usage)
282
+ this.addUsage(session, result.usage, result.usageIsTotal);
283
+ for (const tool of result.tools || [])
284
+ this.append(session, 'tool', tool.name, tool.detail);
285
+ for (const change of result.changes || []) {
286
+ const diff = change.diff || fileDiff(session.cwd, change.path);
287
+ this.append(session, 'file_change', change.path, diff, change.kind, change.diff || !diff ? undefined : 'HEAD');
288
+ }
289
+ if (result.error) {
290
+ failed = true;
291
+ this.append(session, 'error', result.error);
292
+ }
293
+ }
294
+ this.processes.delete(session.id);
295
+ session.pid = undefined;
296
+ if (session.status !== 'stopped' && session.status !== 'failed') {
297
+ this.status(session, code === 0 && !failed ? 'completed' : 'failed');
298
+ }
299
+ this.emit(`${session.id}:idle`);
300
+ });
301
+ }
302
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "agent-muster",
3
+ "version": "0.1.0",
4
+ "description": "A local web interface for managing coding agents, projects, sessions, and DingTalk integrations.",
5
+ "author": "sexyAmole",
6
+ "license": "UNLICENSED",
7
+ "homepage": "https://github.com/sexyAmole/agent-muster",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/sexyAmole/agent-muster.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/sexyAmole/agent-muster/issues"
14
+ },
15
+ "keywords": [
16
+ "cli",
17
+ "coding-agent",
18
+ "codex",
19
+ "claude-code",
20
+ "dingtalk"
21
+ ],
22
+ "type": "module",
23
+ "packageManager": "pnpm@10.33.4",
24
+ "bin": {
25
+ "agent-muster": "./bin/agent-muster.js"
26
+ },
27
+ "scripts": {
28
+ "dev": "pnpm run build && tsx watch src/cli.ts web --no-open",
29
+ "build": "tsc -p tsconfig.json && vite build --config web/vite.config.ts",
30
+ "prepack": "pnpm run build",
31
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p web/tsconfig.json --noEmit",
32
+ "start": "node bin/agent-muster.js web --no-open"
33
+ },
34
+ "files": [
35
+ "bin",
36
+ "dist",
37
+ "web/dist"
38
+ ],
39
+ "dependencies": {
40
+ "dingtalk-stream": "2.1.6-beta.1",
41
+ "express": "^5.1.0",
42
+ "qrcode": "^1.5.4",
43
+ "react": "^19.1.0",
44
+ "react-dom": "^19.1.0",
45
+ "react-markdown": "^10.1.0",
46
+ "remark-gfm": "^4.0.1",
47
+ "undici": "^8.11.2"
48
+ },
49
+ "devDependencies": {
50
+ "@types/express": "^5.0.2",
51
+ "@types/node": "^22.15.0",
52
+ "@types/qrcode": "^1.5.6",
53
+ "@types/react": "^19.1.0",
54
+ "@types/react-dom": "^19.1.0",
55
+ "@vitejs/plugin-react": "^4.7.0",
56
+ "tsx": "^4.20.0",
57
+ "typescript": "^5.8.0",
58
+ "vite": "^6.3.0"
59
+ },
60
+ "engines": {
61
+ "node": ">=22.19.0"
62
+ }
63
+ }