@lightcone-ai/daemon 0.6.6 → 0.6.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'child_process';
2
- import { mkdirSync, readdirSync, readFileSync, statSync, lstatSync } from 'fs';
2
+ import { mkdirSync, statSync } from 'fs';
3
3
  import { homedir } from 'os';
4
4
  import path from 'path';
5
5
  import { buildSystemPrompt } from './drivers/claude.js';
@@ -25,7 +25,6 @@ export class AgentManager {
25
25
  case 'agent:start': return this._startAgent(msg, connection);
26
26
  case 'agent:stop': return this._stopAgent(msg.agentId, msg.channelId, connection);
27
27
  case 'agent:deliver': return this._deliverMessage(msg, connection);
28
- case 'agent:skills:list': return this._listSkills(msg, connection);
29
28
  case 'ping': return connection.send({ type: 'pong' });
30
29
  default:
31
30
  console.log(`[AgentManager] Unhandled: ${msg.type}`);
@@ -62,7 +61,7 @@ export class AgentManager {
62
61
  return msg;
63
62
  }
64
63
 
65
- _startAgent({ agentId, channelId, config }, connection) {
64
+ async _startAgent({ agentId, channelId, config }, connection) {
66
65
  const key = this._key(agentId, channelId);
67
66
  if (this.agents.has(key) || this.starting.has(key)) {
68
67
  console.log(`[AgentManager] Agent ${agentId} in channel ${channelId} already registered`);
@@ -75,13 +74,25 @@ export class AgentManager {
75
74
  const chatBridgePath = new URL('./chat-bridge.js', import.meta.url).pathname;
76
75
  const startupMsg = runtime === 'codex' ? this._takePendingMessage(key) : null;
77
76
 
77
+ // Fetch skills index for system prompt + MCP derivation (non-blocking on failure)
78
+ let skills = [];
79
+ try {
80
+ const chParam = channelId ? `?channelId=${encodeURIComponent(channelId)}` : '';
81
+ const res = await fetch(`${this.serverUrl}/internal/agent/${agentId}/skills${chParam}`, {
82
+ headers: { 'Authorization': `Bearer ${this.machineApiKey}` },
83
+ });
84
+ if (res.ok) skills = await res.json();
85
+ } catch (err) {
86
+ console.log(`[AgentManager] Skills fetch failed for ${agentId} (non-fatal): ${err.message}`);
87
+ }
88
+
78
89
  let proc;
79
90
 
80
91
  if (runtime === 'kimi') {
81
92
  // ── Kimi CLI ──────────────────────────────────────────────────────────
82
93
  const kimiSpawn = buildKimiSpawn({
83
94
  config, agentId, channelId, workspaceDir, chatBridgePath,
84
- serverUrl: this.serverUrl, machineApiKey: this.machineApiKey,
95
+ serverUrl: this.serverUrl, machineApiKey: this.machineApiKey, skills,
85
96
  });
86
97
 
87
98
  console.log(`[AgentManager] Spawning kimi for ${agentId} channel=${channelId ?? 'none'} (session=${kimiSpawn.sessionId})`);
@@ -131,6 +142,7 @@ export class AgentManager {
131
142
  serverUrl: this.serverUrl,
132
143
  machineApiKey: this.machineApiKey,
133
144
  prompt: startupPrompt,
145
+ skills,
134
146
  });
135
147
 
136
148
  console.log(`[AgentManager] Spawning codex for ${agentId} channel=${channelId ?? 'none'} (session=${config.sessionId ?? 'new'})`);
@@ -172,27 +184,25 @@ export class AgentManager {
172
184
  },
173
185
  };
174
186
 
175
- if (config.browserAccess) {
176
- mcpServers['chrome-devtools'] = {
177
- command: 'npx',
178
- args: ['chrome-devtools-mcp@latest', '--headless'],
179
- env: {},
180
- };
181
- }
182
-
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
- },
187
+ // Derive MCP servers from skills with mcp_config
188
+ const agentEnv = config.envVars ?? {};
189
+ for (const skill of skills) {
190
+ if (!skill.mcpConfig) continue;
191
+ const mc = skill.mcpConfig;
192
+ if (mcpServers[mc.server]) continue; // already added
193
+ const resolvedArgs = (mc.args ?? []).map(a =>
194
+ a === '{mysql_mcp_path}'
195
+ ? new URL('../../mcp-servers/mysql/index.js', import.meta.url).pathname
196
+ : a
197
+ );
198
+ const resolvedEnv = {};
199
+ for (const envKey of (mc.env ?? [])) {
200
+ resolvedEnv[envKey] = agentEnv[envKey] ?? process.env[envKey] ?? '';
201
+ }
202
+ mcpServers[mc.server] = {
203
+ command: mc.command,
204
+ args: resolvedArgs,
205
+ env: resolvedEnv,
196
206
  };
197
207
  }
198
208
 
@@ -206,7 +216,7 @@ export class AgentManager {
206
216
  '--output-format', 'stream-json',
207
217
  '--input-format', 'stream-json',
208
218
  '--mcp-config', JSON.stringify(mcpConfig),
209
- '--system-prompt', buildSystemPrompt(config, agentId),
219
+ '--system-prompt', buildSystemPrompt(config, agentId, skills),
210
220
  '--disallowed-tools', 'EnterPlanMode,ExitPlanMode',
211
221
  ];
212
222
 
@@ -383,99 +393,6 @@ export class AgentManager {
383
393
  }
384
394
  }
385
395
 
386
- // ── skills ────────────────────────────────────────────────────────────────
387
-
388
- _listSkills({ agentId, channelId, requestId }, connection) {
389
- const home = homedir();
390
- // Find workspace dir for this agent (check running agents first, else derive)
391
- const key = this._key(agentId, channelId);
392
- const agent = this.agents.get(key);
393
- const workspaceDir = agent
394
- ? this._workspaceDir(agentId, agent.channelId)
395
- : this._workspaceDir(agentId, channelId);
396
-
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
- };
420
-
421
- const dedup = (skills) => {
422
- const seen = new Set();
423
- return skills.filter(s => {
424
- if (seen.has(s.name)) return false;
425
- seen.add(s.name);
426
- return true;
427
- });
428
- };
429
- const shorten = (skills) => skills.map(s => ({
430
- ...s,
431
- sourcePath: s.sourcePath?.startsWith(home) ? '~' + s.sourcePath.slice(home.length) : s.sourcePath,
432
- }));
433
-
434
- const global = shorten(dedup(skillDirs.global.flatMap(d => this._scanSkillsDir(d))));
435
- const workspace = shorten(dedup(skillDirs.workspace.flatMap(d => this._scanSkillsDir(d))));
436
-
437
- connection.send({ type: 'agent:skills:list_result', agentId, requestId, global, workspace });
438
- }
439
-
440
- _scanSkillsDir(dir) {
441
- let entries;
442
- try { entries = readdirSync(dir, { withFileTypes: true }); }
443
- catch { return []; }
444
- const skills = [];
445
- for (const entry of entries) {
446
- if (entry.isDirectory() || entry.isSymbolicLink()) {
447
- const skillMd = path.join(dir, entry.name, 'SKILL.md');
448
- try {
449
- const content = readFileSync(skillMd, 'utf-8');
450
- skills.push({ ...this._parseSkillMd(entry.name, content), sourcePath: dir });
451
- } catch {}
452
- } else if (entry.name.endsWith('.md')) {
453
- const cmdName = entry.name.replace(/\.md$/, '');
454
- try {
455
- const content = readFileSync(path.join(dir, entry.name), 'utf-8');
456
- skills.push({ ...this._parseSkillMd(cmdName, content), sourcePath: dir });
457
- } catch {}
458
- }
459
- }
460
- return skills;
461
- }
462
-
463
- _parseSkillMd(dirName, content) {
464
- const info = { name: dirName, displayName: dirName, description: '', userInvocable: false };
465
- const match = content.match(/^---\n([\s\S]*?)\n---/);
466
- if (!match) return info;
467
- for (const line of match[1].split('\n')) {
468
- const idx = line.indexOf(':');
469
- if (idx === -1) continue;
470
- const key = line.slice(0, idx).trim();
471
- const value = line.slice(idx + 1).trim();
472
- if (key === 'name') info.displayName = value;
473
- if (key === 'description') info.description = value;
474
- if (key === 'user-invocable') info.userInvocable = value === 'true';
475
- }
476
- return info;
477
- }
478
-
479
396
  _parseKimiLine(key, agentId, channelId, line, connection) {
480
397
  const agent = this.agents.get(key);
481
398
  if (!agent) return;
@@ -260,6 +260,66 @@ server.tool('write_memory', 'Write or update a memory file (full content replace
260
260
  return { content: [{ type: 'text', text: `Saved ${path}` }] };
261
261
  });
262
262
 
263
+ // ── skill_list ───────────────────────────────────────────────────────────────
264
+ server.tool('skill_list', 'List all skills available to you (platform + bound). Returns index only (name + description), not full content.', {}, async () => {
265
+ const chParam = currentChannelId ? `?channelId=${encodeURIComponent(currentChannelId)}` : '';
266
+ const skills = await api('GET', `/skills${chParam}`);
267
+ if (!skills || skills.length === 0) return { content: [{ type: 'text', text: 'No skills available.' }] };
268
+ const lines = skills.map(s => `- [${s.type}] **${s.name}** — ${s.description}`);
269
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
270
+ });
271
+
272
+ // ── skill_read ───────────────────────────────────────────────────────────────
273
+ server.tool('skill_read', 'Read the full content of a skill by name or ID', {
274
+ name: z.string().describe('Skill name or ID'),
275
+ }, async ({ name }) => {
276
+ try {
277
+ const skill = await api('GET', `/skills/${encodeURIComponent(name)}`);
278
+ return { content: [{ type: 'text', text: `# ${skill.name}\n\n${skill.content}` }] };
279
+ } catch (err) {
280
+ if (err.message.includes('404')) return { content: [{ type: 'text', text: `Skill "${name}" not found.` }] };
281
+ throw err;
282
+ }
283
+ });
284
+
285
+ // ── skill_create ─────────────────────────────────────────────────────────────
286
+ server.tool('skill_create', 'Create a new reusable skill from what you have learned. Auto-binds to the current channel.', {
287
+ name: z.string().describe('Short skill name (lowercase, hyphens ok), e.g. "xhs-posting"'),
288
+ description: z.string().describe('One-line description of what this skill covers'),
289
+ content: z.string().describe('Full skill content in markdown — procedures, steps, tips'),
290
+ tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
291
+ }, async ({ name, description, content, tags }) => {
292
+ const body = { name, description, content, tags: tags ?? [] };
293
+ if (currentChannelId) body.channelId = currentChannelId;
294
+ const result = await api('POST', '/skills', body);
295
+ return { content: [{ type: 'text', text: `Skill "${result.name}" created and bound to current channel.` }] };
296
+ });
297
+
298
+ // ── skill_update ─────────────────────────────────────────────────────────────
299
+ server.tool('skill_update', 'Update an existing skill content, description, or tags', {
300
+ name: z.string().describe('Skill name or ID to update'),
301
+ content: z.string().optional().describe('New full content (replaces existing)'),
302
+ description: z.string().optional().describe('New description'),
303
+ tags: z.array(z.string()).optional().describe('New tags'),
304
+ }, async ({ name, content, description, tags }) => {
305
+ const body = {};
306
+ if (content != null) body.content = content;
307
+ if (description != null) body.description = description;
308
+ if (tags != null) body.tags = tags;
309
+ const result = await api('PATCH', `/skills/${encodeURIComponent(name)}`, body);
310
+ return { content: [{ type: 'text', text: `Skill "${result.name}" updated.` }] };
311
+ });
312
+
313
+ // ── skill_search ─────────────────────────────────────────────────────────────
314
+ server.tool('skill_search', 'Search for skills by keyword across all accessible skills', {
315
+ query: z.string().describe('Search keyword'),
316
+ }, async ({ query }) => {
317
+ const skills = await api('GET', `/skills/search?q=${encodeURIComponent(query)}`);
318
+ if (!skills || skills.length === 0) return { content: [{ type: 'text', text: `No skills found for "${query}".` }] };
319
+ const lines = skills.map(s => `- [${s.type}] **${s.name}** — ${s.description}`);
320
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
321
+ });
322
+
263
323
  // ── read_file_base64 ──────────────────────────────────────────────────────────
264
324
  // Agent 需要在本机读取图片文件内容,转为 base64 后上传服务器
265
325
  server.tool('read_file_base64',
@@ -515,7 +515,44 @@ const PUBLISHER_PROMPT = `
515
515
 
516
516
  // ── 主函数 ────────────────────────────────────────────────────────────────────
517
517
 
518
- export function buildSystemPrompt(config, agentId) {
518
+ function buildSkillsPrompt(skills) {
519
+ if (!skills || skills.length === 0) return '';
520
+
521
+ const platform = skills.filter(s => s.type === 'platform');
522
+ const user = skills.filter(s => s.type === 'user');
523
+
524
+ let section = `
525
+
526
+ ## Skills — Reusable Knowledge & Procedures
527
+
528
+ You have access to reusable skills — proven procedures for common tasks.
529
+ Skills are loaded progressively: the index below shows what's available.
530
+ Use \`skill_read\` to load full content when needed.
531
+
532
+ ### How to use skills
533
+
534
+ - Before starting unfamiliar work, search for relevant skills: \`skill_search({ query: "..." })\`
535
+ - Load a skill's full procedure: \`skill_read({ name: "skill-name" })\`
536
+ - After completing a complex task (5+ tool calls), consider saving it as a skill: \`skill_create({ name, description, content })\`
537
+ - When using a skill and finding it outdated or wrong, update it immediately: \`skill_update({ name, content })\`
538
+ - Skills you create are automatically shared with other agents in the same channel
539
+
540
+ ### Available Skills
541
+ `;
542
+
543
+ if (platform.length > 0) {
544
+ section += `\n**Platform Skills:**\n`;
545
+ for (const s of platform) section += `- **${s.name}** — ${s.description}\n`;
546
+ }
547
+ if (user.length > 0) {
548
+ section += `\n**Bound Skills:**\n`;
549
+ for (const s of user) section += `- **${s.name}** — ${s.description}\n`;
550
+ }
551
+
552
+ return section;
553
+ }
554
+
555
+ export function buildSystemPrompt(config, agentId, skills) {
519
556
  const { name, displayName, description, feishuBotName } = config;
520
557
 
521
558
  const base = BASE_PROMPT(displayName, name, description, agentId, feishuBotName);
@@ -527,5 +564,7 @@ export function buildSystemPrompt(config, agentId) {
527
564
  'publisher': PUBLISHER_PROMPT,
528
565
  }[name] ?? '';
529
566
 
530
- return base + rolePrompt;
567
+ const skillsPrompt = buildSkillsPrompt(skills);
568
+
569
+ return base + skillsPrompt + rolePrompt;
531
570
  }
@@ -18,6 +18,15 @@ function quote(value) {
18
18
  return JSON.stringify(value);
19
19
  }
20
20
 
21
+ function normalizeCodexModel(model) {
22
+ if (!model) return 'gpt-5.2';
23
+ const normalized = String(model).trim().toLowerCase();
24
+ if (!normalized) return 'gpt-5.2';
25
+ if (['sonnet', 'opus', 'haiku'].includes(normalized)) return 'gpt-5.2';
26
+ if (normalized.startsWith('claude-')) return 'gpt-5.2';
27
+ return model;
28
+ }
29
+
21
30
  function ensureGitRepo(workspaceDir) {
22
31
  const gitDir = path.join(workspaceDir, '.git');
23
32
  if (existsSync(gitDir)) return;
@@ -53,7 +62,7 @@ export function buildCodexSystemPrompt(config, agentId) {
53
62
  }
54
63
 
55
64
  export function buildCodexSpawn({
56
- config, agentId, channelId, workspaceDir, chatBridgePath, serverUrl, machineApiKey, prompt,
65
+ config, agentId, channelId, workspaceDir, chatBridgePath, serverUrl, machineApiKey, prompt, skills,
57
66
  }) {
58
67
  ensureGitRepo(workspaceDir);
59
68
 
@@ -80,34 +89,36 @@ export function buildCodexSpawn({
80
89
  '-c', 'mcp_servers.chat.tool_timeout_sec=300',
81
90
  );
82
91
 
83
- if (config.browserAccess) {
84
- args.push(
85
- '-c', `mcp_servers.chrome-devtools.command=${quote('npx')}`,
86
- '-c', `mcp_servers.chrome-devtools.args=${quote(['chrome-devtools-mcp@latest', '--headless'])}`,
87
- '-c', 'mcp_servers.chrome-devtools.enabled=true'
88
- );
89
- }
90
-
91
- if (config.mysqlAccess) {
92
- const mysqlServerPath = new URL('../../mcp-servers/mysql/index.js', import.meta.url).pathname;
93
- const agentEnv = config.envVars ?? {};
94
- args.push(
95
- '-c', `mcp_servers.mysql.command=${quote('env')}`,
96
- '-c', `mcp_servers.mysql.args=${quote([
97
- `DB_HOST=${process.env.DB_HOST ?? ''}`,
98
- `DB_PORT=${process.env.DB_PORT ?? '3306'}`,
99
- `DB_USER=${process.env.DB_USER ?? ''}`,
100
- `DB_PASSWORD=${process.env.DB_PASSWORD ?? ''}`,
101
- `DB_NAME=${agentEnv.MYSQL_DB ?? process.env.DB_NAME ?? ''}`,
102
- 'node',
103
- mysqlServerPath,
104
- ])}`,
105
- '-c', 'mcp_servers.mysql.enabled=true'
92
+ // Derive MCP servers from skills with mcpConfig
93
+ const agentEnv = config.envVars ?? {};
94
+ for (const skill of (skills ?? [])) {
95
+ if (!skill.mcpConfig) continue;
96
+ const mc = skill.mcpConfig;
97
+ const resolvedArgs = (mc.args ?? []).map(a =>
98
+ a === '{mysql_mcp_path}'
99
+ ? new URL('../../mcp-servers/mysql/index.js', import.meta.url).pathname
100
+ : a
106
101
  );
102
+ // For codex, MCP servers use env wrapper for env vars
103
+ const envPairs = (mc.env ?? []).map(k => `${k}=${agentEnv[k] ?? process.env[k] ?? ''}`);
104
+ if (envPairs.length > 0) {
105
+ args.push(
106
+ '-c', `mcp_servers.${quote(mc.server)}.command=${quote('env')}`,
107
+ '-c', `mcp_servers.${quote(mc.server)}.args=${quote([...envPairs, mc.command, ...resolvedArgs])}`,
108
+ '-c', `mcp_servers.${quote(mc.server)}.enabled=true`
109
+ );
110
+ } else {
111
+ args.push(
112
+ '-c', `mcp_servers.${quote(mc.server)}.command=${quote(mc.command)}`,
113
+ '-c', `mcp_servers.${quote(mc.server)}.args=${quote(resolvedArgs)}`,
114
+ '-c', `mcp_servers.${quote(mc.server)}.enabled=true`
115
+ );
116
+ }
107
117
  }
108
118
 
109
- if (config.model) {
110
- args.push('-m', config.model);
119
+ const model = normalizeCodexModel(config.model);
120
+ if (model) {
121
+ args.push('-m', model);
111
122
  }
112
123
 
113
124
  if (config.reasoningEffort) {
@@ -12,7 +12,7 @@ const KIMI_MCP_FILE = '.lightcone-kimi-mcp.json';
12
12
  * Build Kimi CLI spawn args and config files.
13
13
  * Returns { args, env, setupFiles() } ready for spawn('kimi', args, { env }).
14
14
  */
15
- export function buildKimiSpawn({ config, agentId, channelId, workspaceDir, chatBridgePath, serverUrl, machineApiKey }) {
15
+ export function buildKimiSpawn({ config, agentId, channelId, workspaceDir, chatBridgePath, serverUrl, machineApiKey, skills }) {
16
16
  const isResume = !!config.sessionId;
17
17
  const sessionId = config.sessionId || randomUUID();
18
18
 
@@ -51,29 +51,22 @@ export function buildKimiSpawn({ config, agentId, channelId, workspaceDir, chatB
51
51
  },
52
52
  };
53
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
- };
54
+ // Derive MCP servers from skills with mcpConfig
55
+ const agentEnv = config.envVars ?? {};
56
+ for (const skill of (skills ?? [])) {
57
+ if (!skill.mcpConfig) continue;
58
+ const mc = skill.mcpConfig;
59
+ if (mcpServers[mc.server]) continue;
60
+ const resolvedArgs = (mc.args ?? []).map(a =>
61
+ a === '{mysql_mcp_path}'
62
+ ? new URL('../../mcp-servers/mysql/index.js', import.meta.url).pathname
63
+ : a
64
+ );
65
+ const resolvedEnv = {};
66
+ for (const envKey of (mc.env ?? [])) {
67
+ resolvedEnv[envKey] = agentEnv[envKey] ?? process.env[envKey] ?? '';
68
+ }
69
+ mcpServers[mc.server] = { command: mc.command, args: resolvedArgs, env: resolvedEnv };
77
70
  }
78
71
 
79
72
  writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }), 'utf8');