@lightcone-ai/daemon 0.6.4 → 0.6.6
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/package.json +1 -1
- package/src/agent-manager.js +306 -90
- package/src/chat-bridge.js +96 -14
- package/src/connection.js +15 -1
- package/src/drivers/claude.js +232 -39
- package/src/drivers/codex.js +193 -0
- package/src/drivers/kimi.js +192 -0
package/package.json
CHANGED
package/src/agent-manager.js
CHANGED
|
@@ -3,6 +3,8 @@ import { mkdirSync, readdirSync, readFileSync, statSync, lstatSync } from 'fs';
|
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { buildSystemPrompt } from './drivers/claude.js';
|
|
6
|
+
import { buildCodexSpawn, buildCodexSystemPrompt, parseCodexLine } from './drivers/codex.js';
|
|
7
|
+
import { buildKimiSpawn, buildKimiInitMessages, parseKimiLine, encodeKimiStdin } from './drivers/kimi.js';
|
|
6
8
|
|
|
7
9
|
export class AgentManager {
|
|
8
10
|
constructor({ serverUrl, machineApiKey }) {
|
|
@@ -47,6 +49,19 @@ export class AgentManager {
|
|
|
47
49
|
return dir;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
_formatDeliveryText(message) {
|
|
53
|
+
return `New message in ${message.channel_type === 'dm' ? 'dm from' : `#${message.channel_name} from`} ${message.sender_name}: ${message.content}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_takePendingMessage(key) {
|
|
57
|
+
if (!this._pendingMessages) return null;
|
|
58
|
+
const pending = this._pendingMessages.get(key);
|
|
59
|
+
if (!pending?.length) return null;
|
|
60
|
+
const msg = pending.shift();
|
|
61
|
+
if (pending.length === 0) this._pendingMessages.delete(key);
|
|
62
|
+
return msg;
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
_startAgent({ agentId, channelId, config }, connection) {
|
|
51
66
|
const key = this._key(agentId, channelId);
|
|
52
67
|
if (this.agents.has(key) || this.starting.has(key)) {
|
|
@@ -55,88 +70,184 @@ export class AgentManager {
|
|
|
55
70
|
}
|
|
56
71
|
this.starting.add(key);
|
|
57
72
|
|
|
73
|
+
const runtime = config.runtime ?? 'claude';
|
|
58
74
|
const workspaceDir = this._workspaceDir(agentId, channelId);
|
|
59
75
|
const chatBridgePath = new URL('./chat-bridge.js', import.meta.url).pathname;
|
|
76
|
+
const startupMsg = runtime === 'codex' ? this._takePendingMessage(key) : null;
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
chat: {
|
|
63
|
-
command: 'node',
|
|
64
|
-
args: [chatBridgePath],
|
|
65
|
-
env: {
|
|
66
|
-
SERVER_URL: this.serverUrl,
|
|
67
|
-
MACHINE_API_KEY: this.machineApiKey,
|
|
68
|
-
AGENT_ID: agentId,
|
|
69
|
-
CHANNEL_ID: channelId ?? '',
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
};
|
|
78
|
+
let proc;
|
|
73
79
|
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
};
|
|
80
|
-
|
|
80
|
+
if (runtime === 'kimi') {
|
|
81
|
+
// ── Kimi CLI ──────────────────────────────────────────────────────────
|
|
82
|
+
const kimiSpawn = buildKimiSpawn({
|
|
83
|
+
config, agentId, channelId, workspaceDir, chatBridgePath,
|
|
84
|
+
serverUrl: this.serverUrl, machineApiKey: this.machineApiKey,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
console.log(`[AgentManager] Spawning kimi for ${agentId} channel=${channelId ?? 'none'} (session=${kimiSpawn.sessionId})`);
|
|
88
|
+
|
|
89
|
+
proc = spawn('kimi', kimiSpawn.args, {
|
|
90
|
+
cwd: workspaceDir,
|
|
91
|
+
env: kimiSpawn.env,
|
|
92
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Send initialize + prompt via stdin
|
|
96
|
+
const initMsgs = buildKimiInitMessages(kimiSpawn);
|
|
97
|
+
for (const msg of initMsgs) {
|
|
98
|
+
proc.stdin.write(msg + '\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Track Kimi-specific state for parsing
|
|
102
|
+
const kimiState = { sessionId: kimiSpawn.sessionId, sessionAnnounced: false };
|
|
103
|
+
|
|
104
|
+
this.agents.set(key, {
|
|
105
|
+
config, channelId, agentId, sessionId: kimiSpawn.sessionId, proc,
|
|
106
|
+
runtime: 'kimi', kimiState, kimiIdle: false,
|
|
107
|
+
});
|
|
108
|
+
this.starting.delete(key);
|
|
109
|
+
|
|
110
|
+
let buffer = '';
|
|
111
|
+
proc.stdout.on('data', (chunk) => {
|
|
112
|
+
buffer += chunk.toString();
|
|
113
|
+
const lines = buffer.split('\n');
|
|
114
|
+
buffer = lines.pop();
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
if (!line.trim()) continue;
|
|
117
|
+
this._parseKimiLine(key, agentId, channelId, line, connection);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
} else if (runtime === 'codex') {
|
|
121
|
+
const startupPrompt = startupMsg
|
|
122
|
+
? `${buildCodexSystemPrompt(config, agentId)}\n\nNew message received:\n\n${this._formatDeliveryText(startupMsg.message)}\n\nRespond as appropriate. Complete all required work before stopping.`
|
|
123
|
+
: buildCodexSystemPrompt(config, agentId);
|
|
124
|
+
|
|
125
|
+
const codexSpawn = buildCodexSpawn({
|
|
126
|
+
config,
|
|
127
|
+
agentId,
|
|
128
|
+
channelId,
|
|
129
|
+
workspaceDir,
|
|
130
|
+
chatBridgePath,
|
|
131
|
+
serverUrl: this.serverUrl,
|
|
132
|
+
machineApiKey: this.machineApiKey,
|
|
133
|
+
prompt: startupPrompt,
|
|
134
|
+
});
|
|
81
135
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
136
|
+
console.log(`[AgentManager] Spawning codex for ${agentId} channel=${channelId ?? 'none'} (session=${config.sessionId ?? 'new'})`);
|
|
137
|
+
|
|
138
|
+
proc = spawn('codex', codexSpawn.args, {
|
|
139
|
+
cwd: workspaceDir,
|
|
140
|
+
env: codexSpawn.env,
|
|
141
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
this.agents.set(key, {
|
|
145
|
+
config, channelId, agentId, sessionId: config.sessionId ?? null, proc,
|
|
146
|
+
runtime: 'codex',
|
|
147
|
+
});
|
|
148
|
+
this.starting.delete(key);
|
|
149
|
+
|
|
150
|
+
let buffer = '';
|
|
151
|
+
proc.stdout.on('data', (chunk) => {
|
|
152
|
+
buffer += chunk.toString();
|
|
153
|
+
const lines = buffer.split('\n');
|
|
154
|
+
buffer = lines.pop();
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
if (!line.trim()) continue;
|
|
157
|
+
this._parseCodexLine(key, agentId, channelId, line, connection);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
// ── Claude CLI (default) ──────────────────────────────────────────────
|
|
162
|
+
const mcpServers = {
|
|
163
|
+
chat: {
|
|
164
|
+
command: 'node',
|
|
165
|
+
args: [chatBridgePath],
|
|
166
|
+
env: {
|
|
167
|
+
SERVER_URL: this.serverUrl,
|
|
168
|
+
MACHINE_API_KEY: this.machineApiKey,
|
|
169
|
+
AGENT_ID: agentId,
|
|
170
|
+
CHANNEL_ID: channelId ?? '',
|
|
171
|
+
},
|
|
94
172
|
},
|
|
95
173
|
};
|
|
96
|
-
}
|
|
97
174
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'--input-format', 'stream-json',
|
|
106
|
-
'--mcp-config', JSON.stringify(mcpConfig),
|
|
107
|
-
'--system-prompt', buildSystemPrompt(config, agentId),
|
|
108
|
-
'--disallowed-tools', 'EnterPlanMode,ExitPlanMode',
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
if (config.sessionId) {
|
|
112
|
-
args.push('--resume', config.sessionId);
|
|
113
|
-
}
|
|
175
|
+
if (config.browserAccess) {
|
|
176
|
+
mcpServers['chrome-devtools'] = {
|
|
177
|
+
command: 'npx',
|
|
178
|
+
args: ['chrome-devtools-mcp@latest', '--headless'],
|
|
179
|
+
env: {},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
114
182
|
|
|
115
|
-
|
|
116
|
-
|
|
183
|
+
if (config.mysqlAccess) {
|
|
184
|
+
const mysqlServerPath = new URL('../../mcp-servers/mysql/index.js', import.meta.url).pathname;
|
|
185
|
+
const agentEnv = config.envVars ?? {};
|
|
186
|
+
mcpServers['mysql'] = {
|
|
187
|
+
command: 'node',
|
|
188
|
+
args: [mysqlServerPath],
|
|
189
|
+
env: {
|
|
190
|
+
DB_HOST: process.env.DB_HOST ?? '',
|
|
191
|
+
DB_PORT: process.env.DB_PORT ?? '3306',
|
|
192
|
+
DB_USER: process.env.DB_USER ?? '',
|
|
193
|
+
DB_PASSWORD: process.env.DB_PASSWORD ?? '',
|
|
194
|
+
DB_NAME: agentEnv.MYSQL_DB ?? process.env.DB_NAME ?? '',
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
117
198
|
|
|
118
|
-
|
|
199
|
+
const mcpConfig = { mcpServers };
|
|
200
|
+
|
|
201
|
+
const args = [
|
|
202
|
+
'--print',
|
|
203
|
+
'--allow-dangerously-skip-permissions',
|
|
204
|
+
'--dangerously-skip-permissions',
|
|
205
|
+
'--verbose',
|
|
206
|
+
'--output-format', 'stream-json',
|
|
207
|
+
'--input-format', 'stream-json',
|
|
208
|
+
'--mcp-config', JSON.stringify(mcpConfig),
|
|
209
|
+
'--system-prompt', buildSystemPrompt(config, agentId),
|
|
210
|
+
'--disallowed-tools', 'EnterPlanMode,ExitPlanMode',
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
if (config.sessionId) {
|
|
214
|
+
// Only resume if the session file exists locally
|
|
215
|
+
const projectSlug = workspaceDir.replace(/[\/\.]/g, '-');
|
|
216
|
+
const sessionFile = path.join(homedir(), '.claude', 'projects', projectSlug, `${config.sessionId}.jsonl`);
|
|
217
|
+
try {
|
|
218
|
+
statSync(sessionFile);
|
|
219
|
+
args.push('--resume', config.sessionId);
|
|
220
|
+
} catch {
|
|
221
|
+
console.log(`[AgentManager] Session ${config.sessionId} not found locally, starting fresh`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
119
224
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
env: spawnEnv,
|
|
123
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
124
|
-
});
|
|
225
|
+
const spawnEnv = { ...process.env, FORCE_COLOR: '0', ...(config.envVars ?? {}) };
|
|
226
|
+
delete spawnEnv.CLAUDECODE;
|
|
125
227
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
228
|
+
console.log(`[AgentManager] Spawning claude for ${agentId} channel=${channelId ?? 'none'} (session=${config.sessionId ?? 'new'})`);
|
|
229
|
+
|
|
230
|
+
proc = spawn('claude', args, {
|
|
231
|
+
cwd: workspaceDir,
|
|
232
|
+
env: spawnEnv,
|
|
233
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
this.agents.set(key, { config, channelId, agentId, sessionId: config.sessionId ?? null, proc, runtime: 'claude' });
|
|
237
|
+
this.starting.delete(key);
|
|
238
|
+
|
|
239
|
+
// Parse stdout stream for session ID and activity updates
|
|
240
|
+
let buffer = '';
|
|
241
|
+
proc.stdout.on('data', (chunk) => {
|
|
242
|
+
buffer += chunk.toString();
|
|
243
|
+
const lines = buffer.split('\n');
|
|
244
|
+
buffer = lines.pop();
|
|
245
|
+
for (const line of lines) {
|
|
246
|
+
if (!line.trim()) continue;
|
|
247
|
+
this._parseLine(key, agentId, channelId, line, connection);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
140
251
|
|
|
141
252
|
proc.stderr.on('data', (data) => {
|
|
142
253
|
const text = data.toString().trim();
|
|
@@ -144,9 +255,16 @@ export class AgentManager {
|
|
|
144
255
|
});
|
|
145
256
|
|
|
146
257
|
proc.on('exit', (code) => {
|
|
258
|
+
const agent = this.agents.get(key);
|
|
147
259
|
console.log(`[AgentManager] Agent ${agentId} channel=${channelId ?? 'none'} exited (code=${code})`);
|
|
148
260
|
this.agents.delete(key);
|
|
149
261
|
|
|
262
|
+
if (code === 0 && runtime === 'codex' && this._pendingMessages?.get(key)?.length) {
|
|
263
|
+
const restartConfig = { ...config, sessionId: agent?.sessionId ?? config.sessionId ?? null };
|
|
264
|
+
this._startAgent({ agentId, channelId, config: restartConfig }, connection);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
150
268
|
// If exited immediately with an error and had a session, retry without session (session file may not exist on this machine)
|
|
151
269
|
if (code !== 0 && config.sessionId && !this._retried?.has(key)) {
|
|
152
270
|
if (!this._retried) this._retried = new Set();
|
|
@@ -161,8 +279,12 @@ export class AgentManager {
|
|
|
161
279
|
connection.send({ type: 'agent:activity', agentId, channelId, activity: 'offline', detail: '', entries: [] });
|
|
162
280
|
});
|
|
163
281
|
|
|
164
|
-
// Send startup prompt
|
|
165
|
-
|
|
282
|
+
// Send startup prompt
|
|
283
|
+
if (runtime === 'kimi' || runtime === 'codex') {
|
|
284
|
+
// Kimi already received its prompt via the initialize + prompt JSON-RPC messages
|
|
285
|
+
} else {
|
|
286
|
+
this._write(key, 'You have just started. Follow your startup sequence: first call read_memory with path="MEMORY.md" to load your memory index, then call check_messages.');
|
|
287
|
+
}
|
|
166
288
|
|
|
167
289
|
connection.send({ type: 'agent:status', agentId, channelId, status: 'active' });
|
|
168
290
|
connection.send({ type: 'agent:activity', agentId, channelId, activity: 'online', detail: '', entries: [] });
|
|
@@ -206,36 +328,58 @@ export class AgentManager {
|
|
|
206
328
|
return;
|
|
207
329
|
}
|
|
208
330
|
|
|
209
|
-
const text =
|
|
331
|
+
const text = this._formatDeliveryText(message);
|
|
210
332
|
console.log(`[AgentManager] Delivering seq=${seq} to agent ${agentId} channel=${channelId}`);
|
|
333
|
+
const agent = this.agents.get(key);
|
|
334
|
+
if (agent?.runtime === 'codex') {
|
|
335
|
+
if (!this._pendingMessages) this._pendingMessages = new Map();
|
|
336
|
+
const pending = this._pendingMessages.get(key) ?? [];
|
|
337
|
+
pending.push(msg);
|
|
338
|
+
this._pendingMessages.set(key, pending);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
211
341
|
this._write(key, text);
|
|
212
342
|
}
|
|
213
343
|
|
|
214
344
|
_flushPending(key, connection) {
|
|
215
345
|
if (!this._pendingMessages) return;
|
|
346
|
+
if (this.agents.get(key)?.runtime === 'codex') return;
|
|
216
347
|
const pending = this._pendingMessages.get(key);
|
|
217
348
|
if (!pending || pending.length === 0) return;
|
|
218
349
|
this._pendingMessages.delete(key);
|
|
219
350
|
for (const msg of pending) {
|
|
220
351
|
const { agentId, channelId, seq, message } = msg;
|
|
221
|
-
const text =
|
|
352
|
+
const text = this._formatDeliveryText(message);
|
|
222
353
|
console.log(`[AgentManager] Flushing queued seq=${seq} to agent ${agentId} channel=${channelId}`);
|
|
223
354
|
this._write(key, text);
|
|
224
355
|
}
|
|
225
356
|
}
|
|
226
357
|
|
|
227
|
-
// Write a user message to the running
|
|
358
|
+
// Write a user message to the running agent process via stdin
|
|
228
359
|
_write(key, text) {
|
|
229
360
|
const agent = this.agents.get(key);
|
|
230
361
|
if (!agent?.proc) return;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
362
|
+
|
|
363
|
+
let line;
|
|
364
|
+
if (agent.runtime === 'kimi') {
|
|
365
|
+
// Kimi uses JSON-RPC: prompt (idle) or steer (busy)
|
|
366
|
+
const mode = agent.kimiIdle ? 'idle' : 'busy';
|
|
367
|
+
agent.kimiIdle = false; // will be set back to true on TurnEnd
|
|
368
|
+
line = encodeKimiStdin(text, mode) + '\n';
|
|
369
|
+
} else if (agent.runtime === 'codex') {
|
|
370
|
+
return;
|
|
371
|
+
} else {
|
|
372
|
+
// Claude uses stream-json
|
|
373
|
+
line = JSON.stringify({
|
|
374
|
+
type: 'user',
|
|
375
|
+
message: { role: 'user', content: [{ type: 'text', text }] },
|
|
376
|
+
}) + '\n';
|
|
377
|
+
}
|
|
378
|
+
|
|
235
379
|
try {
|
|
236
380
|
agent.proc.stdin.write(line);
|
|
237
381
|
} catch (err) {
|
|
238
|
-
console.error(`[AgentManager] stdin write error for ${
|
|
382
|
+
console.error(`[AgentManager] stdin write error for ${key}:`, err.message);
|
|
239
383
|
}
|
|
240
384
|
}
|
|
241
385
|
|
|
@@ -250,14 +394,29 @@ export class AgentManager {
|
|
|
250
394
|
? this._workspaceDir(agentId, agent.channelId)
|
|
251
395
|
: this._workspaceDir(agentId, channelId);
|
|
252
396
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
397
|
+
const runtime = agent?.config?.runtime ?? 'claude';
|
|
398
|
+
const skillDirs = runtime === 'codex'
|
|
399
|
+
? {
|
|
400
|
+
global: [
|
|
401
|
+
path.join(home, '.codex', 'skills'),
|
|
402
|
+
path.join(home, '.codex', 'skills', '.system'),
|
|
403
|
+
path.join(home, '.agents', 'skills'),
|
|
404
|
+
],
|
|
405
|
+
workspace: [
|
|
406
|
+
path.join(workspaceDir, '.codex', 'skills'),
|
|
407
|
+
path.join(workspaceDir, '.agents', 'skills'),
|
|
408
|
+
],
|
|
409
|
+
}
|
|
410
|
+
: {
|
|
411
|
+
global: [
|
|
412
|
+
path.join(home, '.claude', 'skills'),
|
|
413
|
+
path.join(home, '.claude', 'commands'),
|
|
414
|
+
],
|
|
415
|
+
workspace: [
|
|
416
|
+
path.join(workspaceDir, '.claude', 'skills'),
|
|
417
|
+
path.join(workspaceDir, '.claude', 'commands'),
|
|
418
|
+
],
|
|
419
|
+
};
|
|
261
420
|
|
|
262
421
|
const dedup = (skills) => {
|
|
263
422
|
const seen = new Set();
|
|
@@ -272,8 +431,8 @@ export class AgentManager {
|
|
|
272
431
|
sourcePath: s.sourcePath?.startsWith(home) ? '~' + s.sourcePath.slice(home.length) : s.sourcePath,
|
|
273
432
|
}));
|
|
274
433
|
|
|
275
|
-
const global = shorten(dedup(
|
|
276
|
-
const workspace = shorten(dedup(
|
|
434
|
+
const global = shorten(dedup(skillDirs.global.flatMap(d => this._scanSkillsDir(d))));
|
|
435
|
+
const workspace = shorten(dedup(skillDirs.workspace.flatMap(d => this._scanSkillsDir(d))));
|
|
277
436
|
|
|
278
437
|
connection.send({ type: 'agent:skills:list_result', agentId, requestId, global, workspace });
|
|
279
438
|
}
|
|
@@ -317,6 +476,63 @@ export class AgentManager {
|
|
|
317
476
|
return info;
|
|
318
477
|
}
|
|
319
478
|
|
|
479
|
+
_parseKimiLine(key, agentId, channelId, line, connection) {
|
|
480
|
+
const agent = this.agents.get(key);
|
|
481
|
+
if (!agent) return;
|
|
482
|
+
const events = parseKimiLine(line, agent.kimiState);
|
|
483
|
+
for (const evt of events) {
|
|
484
|
+
switch (evt.kind) {
|
|
485
|
+
case 'session_init':
|
|
486
|
+
if (agent.sessionId !== evt.sessionId) {
|
|
487
|
+
agent.sessionId = evt.sessionId;
|
|
488
|
+
connection.send({ type: 'agent:session', agentId, channelId, sessionId: evt.sessionId });
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
case 'thinking':
|
|
492
|
+
connection.send({ type: 'agent:activity', agentId, channelId, activity: 'thinking', detail: '', entries: [] });
|
|
493
|
+
break;
|
|
494
|
+
case 'tool_call':
|
|
495
|
+
connection.send({ type: 'agent:activity', agentId, channelId, activity: 'working', detail: evt.name, entries: [] });
|
|
496
|
+
break;
|
|
497
|
+
case 'turn_end':
|
|
498
|
+
agent.kimiIdle = true;
|
|
499
|
+
connection.send({ type: 'agent:activity', agentId, channelId, activity: 'online', detail: '', entries: [] });
|
|
500
|
+
break;
|
|
501
|
+
case 'error':
|
|
502
|
+
console.error(`[AgentManager][kimi][${agentId}] Error: ${evt.message}`);
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
_parseCodexLine(key, agentId, channelId, line, connection) {
|
|
509
|
+
const agent = this.agents.get(key);
|
|
510
|
+
if (!agent) return;
|
|
511
|
+
const events = parseCodexLine(line);
|
|
512
|
+
for (const evt of events) {
|
|
513
|
+
switch (evt.kind) {
|
|
514
|
+
case 'session_init':
|
|
515
|
+
if (agent.sessionId !== evt.sessionId) {
|
|
516
|
+
agent.sessionId = evt.sessionId;
|
|
517
|
+
connection.send({ type: 'agent:session', agentId, channelId, sessionId: evt.sessionId });
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
case 'thinking':
|
|
521
|
+
connection.send({ type: 'agent:activity', agentId, channelId, activity: 'thinking', detail: '', entries: [] });
|
|
522
|
+
break;
|
|
523
|
+
case 'tool_call':
|
|
524
|
+
connection.send({ type: 'agent:activity', agentId, channelId, activity: 'working', detail: evt.name, entries: [] });
|
|
525
|
+
break;
|
|
526
|
+
case 'turn_end':
|
|
527
|
+
connection.send({ type: 'agent:activity', agentId, channelId, activity: 'online', detail: '', entries: [] });
|
|
528
|
+
break;
|
|
529
|
+
case 'error':
|
|
530
|
+
console.error(`[AgentManager][codex][${agentId}] Error: ${evt.message}`);
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
320
536
|
_parseLine(key, agentId, channelId, line, connection) {
|
|
321
537
|
let event;
|
|
322
538
|
try { event = JSON.parse(line); }
|
package/src/chat-bridge.js
CHANGED
|
@@ -3,10 +3,16 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
6
|
+
const cliArgs = process.argv.slice(2);
|
|
7
|
+
function getArg(name) {
|
|
8
|
+
const idx = cliArgs.indexOf(name);
|
|
9
|
+
return idx !== -1 && cliArgs[idx + 1] ? cliArgs[idx + 1] : '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SERVER_URL = process.env.SERVER_URL || getArg('--server-url') || 'http://localhost:8777';
|
|
13
|
+
const MACHINE_API_KEY = process.env.MACHINE_API_KEY || getArg('--auth-token') || '';
|
|
14
|
+
const AGENT_ID = process.env.AGENT_ID || getArg('--agent-id') || '';
|
|
15
|
+
const CHANNEL_ID = process.env.CHANNEL_ID || getArg('--channel-id') || ''; // injected per-channel at spawn time
|
|
10
16
|
|
|
11
17
|
// Current active channelId for memory isolation (defaults to spawn-time CHANNEL_ID)
|
|
12
18
|
let currentChannelId = CHANNEL_ID;
|
|
@@ -57,24 +63,100 @@ server.tool('send_message', 'Send a message to a channel, DM, or thread', {
|
|
|
57
63
|
});
|
|
58
64
|
|
|
59
65
|
// ── read_history ──────────────────────────────────────────────────────────────
|
|
60
|
-
server.tool('read_history', 'Read message history from a channel', {
|
|
61
|
-
channel: z.string().describe('Target: #channel-name | dm:@agentName'),
|
|
62
|
-
limit: z.number().optional().describe('Max messages to return (default 50)'),
|
|
66
|
+
server.tool('read_history', 'Read message history from a channel, DM, or thread. Supports pagination via before/after and context jumps via around.', {
|
|
67
|
+
channel: z.string().describe('Target: #channel-name | dm:@agentName | #channel-name:shortMsgId'),
|
|
68
|
+
limit: z.number().optional().describe('Max messages to return (default 50, max 100)'),
|
|
69
|
+
around: z.union([z.string(), z.number()]).optional().describe('Center the result window around a messageId or seq number'),
|
|
63
70
|
before: z.number().optional().describe('Return messages before this seq'),
|
|
64
71
|
after: z.number().optional().describe('Return messages after this seq'),
|
|
65
|
-
}, async ({ channel, limit, before, after }) => {
|
|
66
|
-
const params = new URLSearchParams({ channel, limit: String(limit ?? 50) });
|
|
72
|
+
}, async ({ channel, limit, around, before, after }) => {
|
|
73
|
+
const params = new URLSearchParams({ channel, limit: String(Math.min(limit ?? 50, 100)) });
|
|
74
|
+
if (around != null) params.set('around', String(around));
|
|
67
75
|
if (before != null) params.set('before', String(before));
|
|
68
76
|
if (after != null) params.set('after', String(after));
|
|
69
77
|
const data = await api('GET', `/history?${params}`);
|
|
70
78
|
const msgs = data.messages ?? [];
|
|
71
79
|
if (msgs.length === 0) return { content: [{ type: 'text', text: 'No messages found.' }] };
|
|
72
80
|
|
|
73
|
-
const text = msgs.map(m =>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const text = msgs.map(m => {
|
|
82
|
+
const senderType = m.senderType === 'agent' ? ' type=agent' : '';
|
|
83
|
+
const taskSuffix = m.taskStatus ? ` [task #${m.taskNumber} status=${m.taskStatus}${m.taskAssigneeId ? ` assignee=${m.taskAssigneeType}:${m.taskAssigneeId}` : ''}]` : '';
|
|
84
|
+
return `[seq=${m.seq} msg=${m.id} time=${m.createdAt}${senderType}] @${m.senderName}: ${m.content}${taskSuffix}`;
|
|
85
|
+
}).join('\n');
|
|
86
|
+
|
|
87
|
+
let footer = '';
|
|
88
|
+
if (data.has_more && msgs.length > 0) {
|
|
89
|
+
const minSeq = msgs[0].seq;
|
|
90
|
+
const maxSeq = msgs[msgs.length - 1].seq;
|
|
91
|
+
if (around) {
|
|
92
|
+
footer = `\n\n--- Use before=${minSeq} to load older or after=${maxSeq} to load newer. ---`;
|
|
93
|
+
} else if (after) {
|
|
94
|
+
footer = `\n\n--- ${msgs.length} messages shown. Use after=${maxSeq} for more. ---`;
|
|
95
|
+
} else {
|
|
96
|
+
footer = `\n\n--- ${msgs.length} messages shown. Use before=${minSeq} for older. ---`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { content: [{ type: 'text', text: `## History for ${channel} (${msgs.length} messages)\n\n${text}${footer}` }] };
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── search_messages ──────────────────────────────────────────────────────────
|
|
104
|
+
server.tool('search_messages', 'Search messages visible to you. Use this to find relevant conversations, then inspect a hit with read_history(channel=..., around=messageId).', {
|
|
105
|
+
query: z.string().describe('Search query'),
|
|
106
|
+
channel: z.string().optional().describe('Optional target to scope the search, e.g. "#general", "dm:@richard"'),
|
|
107
|
+
limit: z.number().optional().describe('Max results (default 10, max 20)'),
|
|
108
|
+
}, async ({ query, channel, limit }) => {
|
|
109
|
+
const trimmed = query.trim();
|
|
110
|
+
if (!trimmed) return { content: [{ type: 'text', text: 'Search query cannot be empty.' }] };
|
|
111
|
+
const params = new URLSearchParams({ q: trimmed, limit: String(Math.min(limit ?? 10, 20)) });
|
|
112
|
+
if (channel) params.set('channel', channel);
|
|
113
|
+
try {
|
|
114
|
+
const data = await api('GET', `/search?${params}`);
|
|
115
|
+
if (!data.results || data.results.length === 0)
|
|
116
|
+
return { content: [{ type: 'text', text: 'No search results.' }] };
|
|
117
|
+
const formatted = data.results.map((r, i) => [
|
|
118
|
+
`[${i + 1}] msg=${r.id} seq=${r.seq} time=${r.createdAt}`,
|
|
119
|
+
`channel: #${r.channelName}`,
|
|
120
|
+
`sender: @${r.senderName}${r.senderType === 'agent' ? ' (agent)' : ''}`,
|
|
121
|
+
`content: ${r.snippet}`,
|
|
122
|
+
`next: read_history(channel="#${r.channelName}", around="${r.id}", limit=20)`,
|
|
123
|
+
].join('\n')).join('\n\n');
|
|
124
|
+
return { content: [{ type: 'text', text: `## Search Results for "${trimmed}" (${data.results.length} results)\n\n${formatted}` }] };
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── view_file ────────────────────────────────────────────────────────────────
|
|
131
|
+
server.tool('view_file', 'Download an attached image by its attachment ID and save it locally so you can view it. Use this when messages contain image attachments.', {
|
|
132
|
+
attachment_id: z.string().describe('The attachment UUID'),
|
|
133
|
+
}, async ({ attachment_id }) => {
|
|
134
|
+
try {
|
|
135
|
+
const fs = await import('fs');
|
|
136
|
+
const pathMod = await import('path');
|
|
137
|
+
const os = await import('os');
|
|
138
|
+
const cacheDir = pathMod.join(os.homedir(), '.lightcone', 'attachments');
|
|
139
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
140
|
+
// Check cache
|
|
141
|
+
const existing = fs.readdirSync(cacheDir).find(f => f.startsWith(attachment_id));
|
|
142
|
+
if (existing) {
|
|
143
|
+
return { content: [{ type: 'text', text: `File already cached at: ${pathMod.join(cacheDir, existing)}\n\nUse your Read tool to view this image.` }] };
|
|
144
|
+
}
|
|
145
|
+
const res = await fetch(`${SERVER_URL}/api/attachments/${attachment_id}`, {
|
|
146
|
+
headers: { 'Authorization': `Bearer ${MACHINE_API_KEY}` },
|
|
147
|
+
redirect: 'follow',
|
|
148
|
+
});
|
|
149
|
+
if (!res.ok) return { isError: true, content: [{ type: 'text', text: `Error: Failed to download attachment (${res.status})` }] };
|
|
150
|
+
const contentType = res.headers.get('content-type') || 'application/octet-stream';
|
|
151
|
+
const extMap = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/webp': '.webp' };
|
|
152
|
+
const ext = extMap[contentType] || '.bin';
|
|
153
|
+
const filePath = pathMod.join(cacheDir, `${attachment_id}${ext}`);
|
|
154
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
155
|
+
fs.writeFileSync(filePath, buffer);
|
|
156
|
+
return { content: [{ type: 'text', text: `Downloaded to: ${filePath}\n\nUse your Read tool to view this image.` }] };
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
159
|
+
}
|
|
78
160
|
});
|
|
79
161
|
|
|
80
162
|
// ── list_server ───────────────────────────────────────────────────────────────
|
package/src/connection.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
2
|
import os from 'os';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
3
4
|
|
|
4
5
|
const RECONNECT_INITIAL = 1000;
|
|
5
6
|
const RECONNECT_MAX = 30000;
|
|
7
|
+
const KNOWN_RUNTIMES = ['claude', 'codex', 'kimi'];
|
|
8
|
+
|
|
9
|
+
function detectRuntimes() {
|
|
10
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
11
|
+
const runtimes = [];
|
|
12
|
+
for (const runtime of KNOWN_RUNTIMES) {
|
|
13
|
+
try {
|
|
14
|
+
execSync(`${cmd} ${runtime}`, { stdio: 'pipe' });
|
|
15
|
+
runtimes.push(runtime);
|
|
16
|
+
} catch {}
|
|
17
|
+
}
|
|
18
|
+
return runtimes;
|
|
19
|
+
}
|
|
6
20
|
|
|
7
21
|
export class DaemonConnection {
|
|
8
22
|
constructor({ serverUrl, machineApiKey, onMessage }) {
|
|
@@ -58,7 +72,7 @@ export class DaemonConnection {
|
|
|
58
72
|
type: 'ready',
|
|
59
73
|
hostname: os.hostname(),
|
|
60
74
|
os: `${os.platform()} ${os.arch()}`,
|
|
61
|
-
runtimes:
|
|
75
|
+
runtimes: detectRuntimes(),
|
|
62
76
|
daemonVersion: '0.1.0',
|
|
63
77
|
});
|
|
64
78
|
}
|