@lightcone-ai/daemon 0.9.69 → 0.9.71

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.
@@ -7,6 +7,7 @@ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
7
7
  function randomInt(min, max) { return Math.floor(min + Math.random() * (max - min + 1)); }
8
8
 
9
9
  const PACE_SCALE = Math.max(0.2, Number(process.env.PUBLISHER_PACE_SCALE ?? '1') || 1);
10
+ const DRY_RUN = process.env.XHS_PUBLISH_DRY_RUN !== '0';
10
11
  async function humanPause(minMs, maxMs, label = '') {
11
12
  const ms = Math.round(randomInt(minMs, maxMs) * PACE_SCALE);
12
13
  if (label) console.error(`[XhsAdapter] pause ${label}: ${ms}ms`);
@@ -104,7 +105,11 @@ export class XhsAdapter {
104
105
  await this._assertNoBlockingErrors();
105
106
  await this._assertPublishButtonReady();
106
107
 
107
- // Click publish button
108
+ if (DRY_RUN) {
109
+ console.error('[XhsAdapter] XHS_PUBLISH_DRY_RUN enabled; skipping final publish click.');
110
+ return { success: true, dry_run: true, post_url: null };
111
+ }
112
+
108
113
  await humanPause(1200, 3000, 'before-publish-click');
109
114
  const clicked = await this._clickByText('发布');
110
115
  if (!clicked) throw new Error('PUBLISH_FAILED: 找不到发布按钮,页面结构可能已变化');
@@ -144,6 +149,11 @@ export class XhsAdapter {
144
149
  await this._assertNoBlockingErrors();
145
150
  await this._assertPublishButtonReady();
146
151
 
152
+ if (DRY_RUN) {
153
+ console.error('[XhsAdapter] XHS_PUBLISH_DRY_RUN enabled; skipping final publish click.');
154
+ return { success: true, dry_run: true, post_url: null };
155
+ }
156
+
147
157
  await humanPause(1200, 3000, 'before-publish-click');
148
158
  const clicked = await this._clickByText('发布');
149
159
  if (!clicked) throw new Error('PUBLISH_FAILED: 找不到发布按钮,页面结构可能已变化');
@@ -205,6 +205,11 @@ images/video 字段填写本地绝对路径(在 agent workspace 目录下)
205
205
  });
206
206
 
207
207
  await completeApproval(approval_action_id, true, result, null);
208
+ if (result?.dry_run) {
209
+ return {
210
+ content: [{ type: 'text', text: `✓ ${label}发布流程已完成到发布前一步,已跳过最终“发布”点击。` }],
211
+ };
212
+ }
208
213
  const postUrl = result.post_url ? `\n发布链接: ${result.post_url}` : '';
209
214
  return {
210
215
  content: [{ type: 'text', text: `✓ 已成功发布到${label}。${postUrl}` }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.9.69",
3
+ "version": "0.9.71",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -49,12 +49,14 @@ export class AgentManager {
49
49
  const dir = path.join(homedir(), '.lightcone', 'workspace', teamId ?? '_global');
50
50
  mkdirSync(path.join(dir, 'artifacts'), { recursive: true });
51
51
  mkdirSync(path.join(dir, 'notes'), { recursive: true });
52
+ mkdirSync(path.join(dir, 'tmp'), { recursive: true });
52
53
  return dir;
53
54
  }
54
55
 
55
56
  _workspaceDir(agentId, teamId) {
56
57
  const dir = path.join(homedir(), '.lightcone', 'workspace', teamId ?? '_global', agentId);
57
58
  mkdirSync(dir, { recursive: true });
59
+ mkdirSync(path.join(dir, 'tmp'), { recursive: true });
58
60
  return dir;
59
61
  }
60
62
 
@@ -226,6 +228,7 @@ export class AgentManager {
226
228
  MACHINE_API_KEY: config.authToken,
227
229
  AGENT_ID: agentId,
228
230
  TEAM_ID: teamId ?? '',
231
+ WORKSPACE_DIR: workspaceDir,
229
232
  },
230
233
  },
231
234
  };
@@ -3,7 +3,7 @@ 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
  import { readFileSync } from 'fs';
6
- import { extname } from 'path';
6
+ import path, { extname } from 'path';
7
7
 
8
8
  const cliArgs = process.argv.slice(2);
9
9
  function getArg(name) {
@@ -15,6 +15,8 @@ const SERVER_URL = process.env.SERVER_URL || getArg('--server-url') || 'htt
15
15
  const MACHINE_API_KEY = process.env.MACHINE_API_KEY || getArg('--auth-token') || '';
16
16
  const AGENT_ID = process.env.AGENT_ID || getArg('--agent-id') || '';
17
17
  const TEAM_ID = process.env.TEAM_ID || getArg('--team-id') || ''; // injected per-team at spawn time
18
+ const WORKSPACE_DIR = path.resolve(process.env.WORKSPACE_DIR || getArg('--workspace-dir') || process.cwd());
19
+ const TEAM_WORKSPACE_DIR = path.dirname(WORKSPACE_DIR);
18
20
 
19
21
  // Current active teamId for memory isolation (defaults to spawn-time TEAM_ID)
20
22
  let currentTeamId = TEAM_ID;
@@ -51,6 +53,21 @@ function formatBytes(bytes) {
51
53
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
52
54
  }
53
55
 
56
+ function isInsideDir(filePath, dir) {
57
+ const rel = path.relative(dir, filePath);
58
+ return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
59
+ }
60
+
61
+ function resolveLocalWorkspaceFile(filePath) {
62
+ const resolved = path.resolve(WORKSPACE_DIR, filePath);
63
+ if (isInsideDir(resolved, WORKSPACE_DIR)) return resolved;
64
+
65
+ const allowedTeamRoots = ['artifacts', 'notes', 'tmp'].map(dir => path.join(TEAM_WORKSPACE_DIR, dir));
66
+ if (allowedTeamRoots.some(root => isInsideDir(resolved, root))) return resolved;
67
+
68
+ throw new Error(`Local file must be inside the agent workspace or team shared artifacts/notes/tmp directories. Got: ${filePath}`);
69
+ }
70
+
54
71
  async function api(method, path, body) {
55
72
  const url = `${SERVER_URL}/internal/agent/${AGENT_ID}${path}`;
56
73
  const res = await fetch(url, {
@@ -316,7 +333,7 @@ server.tool('read_workspace', 'Read a file from the shared team workspace (e.g.
316
333
  return {
317
334
  content: [{
318
335
  type: 'text',
319
- text: `${path} is a binary workspace file (${summary.mime}, ${formatBytes(summary.bytes)}). Do not read it as text. Use it by path in the Files tab, or use upload_image/read_file_base64 if you need a public URL or local base64.`,
336
+ text: `${path} is a binary workspace file (${summary.mime}, ${formatBytes(summary.bytes)}). Do not read it as text. Use it by path in the Files tab. If a temporary public URL is needed for a chat preview or external platform, use upload_image separately.`,
320
337
  }],
321
338
  };
322
339
  }
@@ -337,14 +354,15 @@ server.tool('write_workspace', 'Write a file to the shared team workspace. Use t
337
354
  return { content: [{ type: 'text', text: `Saved to team workspace: ${path}` }] };
338
355
  });
339
356
 
340
- server.tool('write_workspace_file', 'Write a local file directly to the shared team workspace. Prefer this over write_workspace for images/PDFs/binary files so large base64 content never enters the model context.', {
341
- file_path: z.string().describe('Absolute path to the local file, e.g. "/home/ubuntu/lightcone/public/cover.png"'),
357
+ server.tool('write_workspace_file', 'Write a local file directly to the shared team workspace. Prefer this over write_workspace for images/PDFs/binary files so large base64 content never enters the model context. The source file may be a relative path under the current agent workspace, or an absolute path inside the agent workspace/team shared artifacts/notes/tmp directories.', {
358
+ file_path: z.string().describe('Local file path. Relative paths resolve from the current agent workspace. Absolute paths must stay inside the agent/team workspace.'),
342
359
  path: z.string().describe('Destination path relative to team workspace root, e.g. "artifacts/cover.png"'),
343
360
  }, async ({ file_path, path }) => {
344
361
  if (!currentTeamId) return { content: [{ type: 'text', text: 'No team context.' }] };
345
- const ext = extname(path || file_path).toLowerCase();
362
+ const localPath = resolveLocalWorkspaceFile(file_path);
363
+ const ext = extname(path || localPath).toLowerCase();
346
364
  const mime = WORKSPACE_BINARY_MIME[ext] ?? 'application/octet-stream';
347
- const buf = readFileSync(file_path);
365
+ const buf = readFileSync(localPath);
348
366
  const content = `data:${mime};base64,${buf.toString('base64')}`;
349
367
  await api('PUT', `/team-memory?path=${encodeURIComponent(path)}&teamId=${encodeURIComponent(currentTeamId)}`, { content });
350
368
  return { content: [{ type: 'text', text: `Saved local file to team workspace: ${path} (${mime}, ${formatBytes(buf.length)})` }] };
@@ -429,30 +447,30 @@ server.tool('skill_search', 'Search for skills by keyword across all accessible
429
447
  // ── read_file_base64 ──────────────────────────────────────────────────────────
430
448
  // Agent 需要在本机读取图片文件内容,转为 base64 后上传服务器
431
449
  server.tool('read_file_base64',
432
- '读取本机文件内容,返回 base64 编码。用于上传图片到服务器。',
450
+ '读取本机文件内容,返回 base64 编码。优先使用 write_workspace_file 保存正式产出;只有外部工具明确需要 base64 字符串时才使用。',
433
451
  {
434
- file_path: z.string().describe('本机文件的绝对路径'),
452
+ file_path: z.string().describe('本机文件路径。相对路径从当前 agent workspace 解析;绝对路径必须在 agent/team workspace 内。'),
435
453
  },
436
454
  async ({ file_path }) => {
437
- const { readFileSync } = await import('fs');
438
- const data = readFileSync(file_path).toString('base64');
455
+ const localPath = resolveLocalWorkspaceFile(file_path);
456
+ const data = readFileSync(localPath).toString('base64');
439
457
  return { content: [{ type: 'text', text: data }] };
440
458
  }
441
459
  );
442
460
 
443
461
  // ── upload_image ──────────────────────────────────────────────────────────────
444
462
  server.tool('upload_image',
445
- '将本机图片文件(绝对路径)上传到服务器,返回公开可访问的 URL。用于将 QR 码截图等发给用户。',
463
+ '将本机图片文件上传为临时公开 URL,用于聊天预览、二维码截图或外部平台临时访问。它不会保存正式产出;正式产出必须同时写入 artifacts/,优先使用 write_workspace_file。',
446
464
  {
447
- file_path: z.string().describe('本机图片文件的绝对路径'),
465
+ file_path: z.string().describe('本机图片文件路径。相对路径从当前 agent workspace 解析;绝对路径必须在 agent/team workspace 内。'),
448
466
  },
449
467
  async ({ file_path }) => {
450
- const { readFileSync } = await import('fs');
451
468
  const { extname, basename } = await import('path');
452
- const data = readFileSync(file_path).toString('base64');
453
- const filename = basename(file_path);
469
+ const localPath = resolveLocalWorkspaceFile(file_path);
470
+ const data = readFileSync(localPath).toString('base64');
471
+ const filename = basename(localPath);
454
472
  const result = await api('POST', '/upload', { filename, data });
455
- return { content: [{ type: 'text', text: `上传成功: ${result.url}` }] };
473
+ return { content: [{ type: 'text', text: `临时公开 URL: ${result.url}\n注意:这不是正式产出存储。如需保留文件,请同时使用 write_workspace_file 保存到 artifacts/。` }] };
456
474
  }
457
475
  );
458
476
 
@@ -22,7 +22,7 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
22
22
  8. **${t("claim_tasks")}** — Claim tasks by number (supports batch, handles conflicts).
23
23
  9. **${t("unclaim_task")}** — Release your claim on a task.
24
24
  10. **${t("update_task_status")}** — Change a task's status (e.g. to in_review or done).
25
- 11. **${t("upload_image")}** — Upload an image file to attach to a message. Returns an attachment ID to pass to send_message.
25
+ 11. **${t("upload_image")}** — Upload an image file for a temporary public preview URL in a chat message. This is not durable artifact storage.
26
26
  12. **${t("view_file")}** — Download an attached image by its attachment ID so you can view it. Use when messages contain image attachments.
27
27
 
28
28
  CRITICAL RULES:
@@ -189,8 +189,7 @@ When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.), alwa
189
189
  - **You MUST only read/write files inside your workspace directories.** Never modify files outside these paths:
190
190
  - Your personal workspace (shown at startup)
191
191
  - The team shared workspace (one level up)
192
- - \`/home/ubuntu/lightcone/public/\` shared web server, for serving completed web artifacts only
193
- - **NEVER touch other projects or directories** (e.g. \`/home/ubuntu/staircase/\`, \`/home/ubuntu/someproject/\`, etc.) without explicit permission from a human in this conversation.
192
+ - **NEVER touch other projects or directories outside your workspace roots** without explicit permission from a human in this conversation.
194
193
  - If a task requires modifying an external codebase, **ask for explicit authorization first**, stating exactly which files you intend to change.
195
194
 
196
195
  ## Workspace Structure
@@ -209,12 +208,14 @@ Located one level up from your personal workspace. Contains:
209
208
  - \`BRIEF.md\` — **read this on every startup**. Set by humans. Defines team mission, conventions, and background.
210
209
  - \`KNOWLEDGE.md\` — shared knowledge index. Use \`${t("write_workspace")}\` to record team-level learnings here.
211
210
  - \`notes/\` — shared research notes and decisions.
212
- - \`artifacts/\` — **ALL deliverables go here without exception**: code, scripts, HTML pages, data files, reports, images — everything you produce for a task. **Use \`${t("write_workspace")}({ path: "artifacts/filename.ext", content: "..." })\` to create every output file.** Never create deliverable files anywhere else.
211
+ - \`artifacts/\` — **ALL deliverables go here without exception**: code, scripts, HTML pages, data files, reports, images — everything you produce for a task. Use \`${t("write_workspace")}({ path: "artifacts/filename.ext", content: "..." })\` for text files and \`${t("write_workspace_file")}({ file_path: "tmp/local-file.png", path: "artifacts/file.png" })\` for local binary files. Never create deliverable files anywhere else.
213
212
 
214
213
  **Write rule:**
215
214
  - Personal learnings → \`${t("write_memory")}\`
216
215
  - Team-level knowledge → \`${t("write_workspace")}({ path: "KNOWLEDGE.md", ... })\`
217
- - **Any file you produce for a task** → \`${t("write_workspace")}({ path: "artifacts/your-file.ext", ... })\`
216
+ - **Any file you produce for a task** → \`${t("write_workspace")}({ path: "artifacts/your-file.ext", ... })\` or \`${t("write_workspace_file")}({ file_path, path: "artifacts/your-file.ext" })\`
217
+
218
+ Temporary local files belong under \`tmp/\` in your personal workspace. If you need to show an image in chat, first save the durable copy to \`artifacts/\`, then optionally call \`${t("upload_image")}\` for a temporary public preview URL.
218
219
 
219
220
  Example: writing a web page → \`${t("write_workspace")}({ path: "artifacts/job-query.html", content: "<!DOCTYPE html>..." })\`
220
221
 
@@ -228,6 +229,7 @@ Example: writing a web page → \`${t("write_workspace")}({ path: "artifacts/job
228
229
  **Team memory** (shared filesystem — visible to all agents in the team):
229
230
  - \`${t("read_workspace")}({ path })\` — read a team workspace file (e.g. \`"BRIEF.md"\`, \`"KNOWLEDGE.md"\`)
230
231
  - \`${t("write_workspace")}({ path, content })\` — write a team workspace file
232
+ - \`${t("write_workspace_file")}({ file_path, path })\` — write a local file from your workspace to a team workspace artifact without putting base64 in context
231
233
  - \`${t("list_workspace")}()\` — list all files in the team workspace
232
234
 
233
235
  ### Startup sequence (CRITICAL)
@@ -4,12 +4,13 @@ import path from 'path';
4
4
  import { buildSystemPrompt as buildClaudeSystemPrompt } from './claude.js';
5
5
  import { buildSkillMcpServers } from '../mcp-config.js';
6
6
 
7
- function buildChatBridgeArgs(chatBridgePath, { agentId, teamId, serverUrl, authToken }) {
7
+ function buildChatBridgeArgs(chatBridgePath, { agentId, teamId, serverUrl, authToken, workspaceDir }) {
8
8
  const args = [
9
9
  chatBridgePath,
10
10
  '--agent-id', agentId,
11
11
  '--server-url', serverUrl,
12
12
  '--auth-token', authToken,
13
+ '--workspace-dir', workspaceDir,
13
14
  ];
14
15
  if (teamId) args.push('--team-id', teamId);
15
16
  return args;
@@ -77,6 +78,7 @@ export function buildCodexSpawn({
77
78
  teamId,
78
79
  serverUrl,
79
80
  authToken: config.authToken || machineApiKey,
81
+ workspaceDir,
80
82
  });
81
83
 
82
84
  args.push(
@@ -48,6 +48,7 @@ export function buildKimiSpawn({ config, agentId, teamId, workspaceDir, chatBrid
48
48
  MACHINE_API_KEY: config.authToken,
49
49
  AGENT_ID: agentId,
50
50
  TEAM_ID: teamId ?? '',
51
+ WORKSPACE_DIR: workspaceDir,
51
52
  },
52
53
  },
53
54
  };