@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.
@@ -0,0 +1,192 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { existsSync, writeFileSync } from 'fs';
3
+ import path from 'path';
4
+ import { buildSystemPrompt as buildClaudeSystemPrompt } from './claude.js';
5
+
6
+ const KIMI_WIRE_PROTOCOL_VERSION = '1.3';
7
+ const KIMI_SYSTEM_PROMPT_FILE = '.lightcone-kimi-system.md';
8
+ const KIMI_AGENT_FILE = '.lightcone-kimi-agent.yaml';
9
+ const KIMI_MCP_FILE = '.lightcone-kimi-mcp.json';
10
+
11
+ /**
12
+ * Build Kimi CLI spawn args and config files.
13
+ * Returns { args, env, setupFiles() } ready for spawn('kimi', args, { env }).
14
+ */
15
+ export function buildKimiSpawn({ config, agentId, channelId, workspaceDir, chatBridgePath, serverUrl, machineApiKey }) {
16
+ const isResume = !!config.sessionId;
17
+ const sessionId = config.sessionId || randomUUID();
18
+
19
+ const systemPromptPath = path.join(workspaceDir, KIMI_SYSTEM_PROMPT_FILE);
20
+ const agentFilePath = path.join(workspaceDir, KIMI_AGENT_FILE);
21
+ const mcpConfigPath = path.join(workspaceDir, KIMI_MCP_FILE);
22
+
23
+ // Build system prompt (reuse claude's prompt builder)
24
+ const prompt = buildClaudeSystemPrompt(config, agentId);
25
+
26
+ // Write system prompt file (skip if resuming and file exists)
27
+ if (!isResume || !existsSync(systemPromptPath)) {
28
+ writeFileSync(systemPromptPath, prompt, 'utf8');
29
+ }
30
+
31
+ // Write agent YAML config
32
+ writeFileSync(agentFilePath, [
33
+ 'version: 1',
34
+ 'agent:',
35
+ ' extend: default',
36
+ ` system_prompt_path: ./${KIMI_SYSTEM_PROMPT_FILE}`,
37
+ '',
38
+ ].join('\n'), 'utf8');
39
+
40
+ // Build MCP config
41
+ const mcpServers = {
42
+ chat: {
43
+ command: 'node',
44
+ args: [chatBridgePath],
45
+ env: {
46
+ SERVER_URL: serverUrl,
47
+ MACHINE_API_KEY: machineApiKey,
48
+ AGENT_ID: agentId,
49
+ CHANNEL_ID: channelId ?? '',
50
+ },
51
+ },
52
+ };
53
+
54
+ if (config.browserAccess) {
55
+ mcpServers['chrome-devtools'] = {
56
+ command: 'npx',
57
+ args: ['chrome-devtools-mcp@latest', '--headless'],
58
+ env: {},
59
+ };
60
+ }
61
+
62
+ if (config.mysqlAccess) {
63
+ // Use relative path — caller provides the absolute path
64
+ const mysqlServerPath = new URL('../../mcp-servers/mysql/index.js', import.meta.url).pathname;
65
+ const agentEnv = config.envVars ?? {};
66
+ mcpServers['mysql'] = {
67
+ command: 'node',
68
+ args: [mysqlServerPath],
69
+ env: {
70
+ DB_HOST: process.env.DB_HOST ?? '',
71
+ DB_PORT: process.env.DB_PORT ?? '3306',
72
+ DB_USER: process.env.DB_USER ?? '',
73
+ DB_PASSWORD: process.env.DB_PASSWORD ?? '',
74
+ DB_NAME: agentEnv.MYSQL_DB ?? process.env.DB_NAME ?? '',
75
+ },
76
+ };
77
+ }
78
+
79
+ writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }), 'utf8');
80
+
81
+ // Build CLI args
82
+ const args = [
83
+ '--wire',
84
+ '--yolo',
85
+ '--agent-file', agentFilePath,
86
+ '--mcp-config-file', mcpConfigPath,
87
+ '--session', sessionId,
88
+ ];
89
+
90
+ if (config.model && config.model !== 'default') {
91
+ args.push('--model', config.model);
92
+ }
93
+
94
+ const spawnEnv = { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1', ...(config.envVars ?? {}) };
95
+
96
+ return { args, env: spawnEnv, sessionId, isResume, prompt };
97
+ }
98
+
99
+ /**
100
+ * Build the JSON-RPC initialize + prompt messages to send to stdin after spawn.
101
+ */
102
+ export function buildKimiInitMessages({ sessionId, isResume, prompt }) {
103
+ const initMsg = JSON.stringify({
104
+ jsonrpc: '2.0',
105
+ id: randomUUID(),
106
+ method: 'initialize',
107
+ params: {
108
+ protocol_version: KIMI_WIRE_PROTOCOL_VERSION,
109
+ client: { name: 'lightcone-daemon', version: '1.0.0' },
110
+ capabilities: { supports_question: false, supports_plan_mode: false },
111
+ },
112
+ });
113
+
114
+ const promptMsg = JSON.stringify({
115
+ jsonrpc: '2.0',
116
+ id: randomUUID(),
117
+ method: 'prompt',
118
+ params: {
119
+ user_input: isResume
120
+ ? prompt
121
+ : 'Your system prompt contains your standing instructions. Follow it now and begin listening for messages.',
122
+ },
123
+ });
124
+
125
+ return [initMsg, promptMsg];
126
+ }
127
+
128
+ /**
129
+ * Parse a line of Kimi wire-protocol JSON-RPC output.
130
+ * Returns an array of events: { kind, ... }
131
+ */
132
+ export function parseKimiLine(line, state) {
133
+ let message;
134
+ try { message = JSON.parse(line); }
135
+ catch { return []; }
136
+
137
+ const events = [];
138
+
139
+ // Announce session on first output
140
+ if (!state.sessionAnnounced && state.sessionId) {
141
+ events.push({ kind: 'session_init', sessionId: state.sessionId });
142
+ state.sessionAnnounced = true;
143
+ }
144
+
145
+ if ('method' in message && message.method === 'event') {
146
+ const eventType = message.params?.type;
147
+ const payload = message.params?.payload || {};
148
+
149
+ switch (eventType) {
150
+ case 'StepBegin':
151
+ events.push({ kind: 'thinking' });
152
+ break;
153
+ case 'ContentPart':
154
+ if (payload.type === 'think') {
155
+ events.push({ kind: 'thinking' });
156
+ } else if (payload.type === 'text') {
157
+ events.push({ kind: 'text', text: payload.text });
158
+ }
159
+ break;
160
+ case 'ToolCall':
161
+ events.push({ kind: 'tool_call', name: payload.function?.name || 'unknown_tool' });
162
+ break;
163
+ case 'TurnEnd':
164
+ events.push({ kind: 'turn_end' });
165
+ break;
166
+ case 'StepInterrupted':
167
+ events.push({ kind: 'turn_end' });
168
+ break;
169
+ }
170
+ return events;
171
+ }
172
+
173
+ if ('error' in message) {
174
+ events.push({ kind: 'error', message: message.error?.message || 'Unknown Kimi error' });
175
+ events.push({ kind: 'turn_end' });
176
+ }
177
+
178
+ return events;
179
+ }
180
+
181
+ /**
182
+ * Encode a message to send to Kimi via stdin.
183
+ * mode: 'idle' → prompt (new turn), 'busy' → steer (interrupt current turn)
184
+ */
185
+ export function encodeKimiStdin(text, mode = 'idle') {
186
+ return JSON.stringify({
187
+ jsonrpc: '2.0',
188
+ id: randomUUID(),
189
+ method: mode === 'idle' ? 'prompt' : 'steer',
190
+ params: { user_input: text },
191
+ });
192
+ }