@lightcone-ai/daemon 0.15.49 → 0.15.51

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.15.49",
3
+ "version": "0.15.51",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -6,11 +6,7 @@ import { createReadStream, existsSync, mkdirSync, readFileSync, writeFileSync }
6
6
  import { createHash, randomUUID } from 'crypto';
7
7
  import path, { extname } from 'path';
8
8
  import { recordUrlNarration } from './_vendor/video/recorder/index.js';
9
- import {
10
- VIDEO_EXT,
11
- VIDEO_MIME,
12
- writeLocalFileToWorkspace,
13
- } from './workspace-file-upload.js';
9
+ import { writeLocalFileToWorkspace } from './workspace-file-upload.js';
14
10
  import { runRecordUrlNarrationTool } from './record-url-narration-tool.js';
15
11
  import { runSubmitToLibraryTool } from './submit-to-library-tool.js';
16
12
  import { runRenderTextToImageTool } from './tools/render-text-to-image.js';
@@ -491,7 +487,7 @@ async function directApi(method, apiPath, body) {
491
487
  async function directApiVideoUpload(apiPath, {
492
488
  localPath,
493
489
  filename,
494
- contentType = VIDEO_MIME,
490
+ contentType = 'video/mp4',
495
491
  }) {
496
492
  const url = `${SERVER_URL}/internal/agent/${AGENT_ID}${apiPath}`;
497
493
  const headers = {
@@ -1090,44 +1086,22 @@ server.tool('write_workspace_file', 'Write a local file directly to the shared w
1090
1086
  uploadWorkspaceMemory: async ({ workspacePath, workspaceId, content }) => {
1091
1087
  await api('PUT', `/workspace-memory?path=${encodeURIComponent(workspacePath)}&workspaceId=${encodeURIComponent(workspaceId)}`, { content });
1092
1088
  },
1093
- uploadVideo: async ({
1094
- localPath: videoLocalPath,
1095
- workspacePath,
1096
- workspaceId,
1097
- filename,
1098
- mime,
1099
- }) => {
1100
- const uploadResult = await runMandatoryLocalTool({
1101
- toolName: 'write_workspace_file',
1102
- toolInput: {
1103
- file_path,
1104
- path: workspacePath,
1105
- upload_mode: 'upload_video',
1106
- },
1107
- executor: async () => directApiVideoUpload(
1108
- `/upload-video?workspaceId=${encodeURIComponent(workspaceId)}&filename=${encodeURIComponent(filename)}&path=${encodeURIComponent(workspacePath)}`,
1109
- {
1110
- localPath: videoLocalPath,
1111
- filename,
1112
- contentType: mime,
1113
- }
1114
- ),
1115
- });
1116
- return {
1117
- mode: 'upload-video',
1118
- mime: VIDEO_MIME,
1119
- bytes: Number(uploadResult?.sizeBytes) || 0,
1120
- };
1089
+ presign: async ({ workspaceId, path: filePath, size, mime, sha256 }) => {
1090
+ return api('POST', '/storage/presign', { workspaceId, path: filePath, size, mime, sha256 });
1091
+ },
1092
+ confirmUpload: async ({ workspaceId, path: filePath, objectKey }) => {
1093
+ await api('POST', '/storage/confirm', { workspaceId, path: filePath, objectKey });
1121
1094
  },
1122
1095
  });
1123
1096
 
1124
- const finalMime = result?.mime ?? (extname(path || localPath).toLowerCase() === VIDEO_EXT ? VIDEO_MIME : 'application/octet-stream');
1097
+ const finalMime = result?.mime ?? 'application/octet-stream';
1125
1098
  const finalBytes = Number.isFinite(result?.bytes) ? result.bytes : 0;
1126
1099
  const sizeText = finalBytes > 0 ? formatBytes(finalBytes) : 'unknown size';
1100
+ const asyncNote = result?.async ? ' (uploading in background)' : '';
1127
1101
  return {
1128
1102
  content: [{
1129
1103
  type: 'text',
1130
- text: `Saved local file to workspace: ${path} (${finalMime}, ${sizeText})`,
1104
+ text: `Saved local file to workspace: ${path} (${finalMime}, ${sizeText})${asyncNote}`,
1131
1105
  }],
1132
1106
  };
1133
1107
  });
@@ -229,37 +229,6 @@ export async function fetchPublishJobWorkspaceFile({ serverUrl, machineApiKey, j
229
229
  return res.json();
230
230
  }
231
231
 
232
- export async function streamPublishJobVideo({
233
- serverUrl,
234
- machineApiKey,
235
- jobId,
236
- videoId,
237
- localPath,
238
- writeBinaryFile = writeFileAtomically,
239
- }) {
240
- const normalizedJobId = String(jobId ?? '').trim();
241
- if (!normalizedJobId) {
242
- throw new Error('publish job id is required to fetch video files');
243
- }
244
- const normalizedVideoId = String(videoId ?? '').trim();
245
- if (!normalizedVideoId) {
246
- throw new Error('video id is required to fetch video files');
247
- }
248
- const url = `${String(serverUrl).replace(/\/$/, '')}/internal/agent/publish-jobs/${encodeURIComponent(normalizedJobId)}/video-files/${encodeURIComponent(normalizedVideoId)}`;
249
- const res = await fetch(url, {
250
- headers: {
251
- Authorization: `Bearer ${machineApiKey}`,
252
- },
253
- });
254
- if (!res.ok) {
255
- const text = await res.text().catch(() => '');
256
- throw new Error(`publish-jobs video-files GET failed (${res.status}): ${text}`);
257
- }
258
- const data = Buffer.from(await res.arrayBuffer());
259
- writeBinaryFile(localPath, data);
260
- return localPath;
261
- }
262
-
263
232
  export function workspacePathFromMediaPath(filePath, workspaceId) {
264
233
  if (!filePath) return null;
265
234
 
@@ -294,7 +263,6 @@ export async function materializeWorkspaceMedia({
294
263
  machineApiKey,
295
264
  jobId,
296
265
  fetchWorkspaceFile = fetchPublishJobWorkspaceFile,
297
- streamVideoFile = streamPublishJobVideo,
298
266
  writeBinaryFile = writeFileAtomically,
299
267
  }) {
300
268
  if (!filePath) return filePath;
@@ -317,20 +285,33 @@ export async function materializeWorkspaceMedia({
317
285
  jobId,
318
286
  relPath: workspacePath.relPath,
319
287
  });
320
- if (isWorkspaceCacheValid(localPath, data)) return localPath;
321
288
 
322
- if (data?.videoId) {
323
- await streamVideoFile({
324
- serverUrl,
325
- machineApiKey,
326
- jobId,
327
- videoId: data.videoId,
328
- localPath,
329
- });
330
- } else {
289
+ // New: signed URL from object storage
290
+ if (data?.signedUrl) {
291
+ // Use sha256-based cache key in tmp/cache to avoid duplicate downloads
292
+ const cacheDir = path.join(workspaceRootDir, 'tmp', 'cache');
293
+ const ext = path.extname(workspacePath.relPath) || '';
294
+ const cacheKey = data.object_hash ?? data.size_bytes?.toString() ?? 'nokey';
295
+ const cachePath = path.join(cacheDir, `${cacheKey}${ext}`);
296
+
297
+ if (existsSync(cachePath)) return cachePath;
298
+
299
+ mkdirSync(cacheDir, { recursive: true });
300
+ const res = await fetch(data.signedUrl);
301
+ if (!res.ok) throw new Error(`Failed to download from signed URL: ${res.status}`);
302
+ const buf = Buffer.from(await res.arrayBuffer());
303
+ writeFileAtomically(cachePath, buf);
304
+ return cachePath;
305
+ }
306
+
307
+ // Legacy: text content in DB
308
+ if (data?.content != null) {
309
+ if (isWorkspaceCacheValid(localPath, data)) return localPath;
331
310
  writeBinaryFile(localPath, decodeWorkspaceContent(data.content));
311
+ return localPath;
332
312
  }
333
- return localPath;
313
+
314
+ throw new Error(`No content or signedUrl in workspace file response for: ${filePath}`);
334
315
  }
335
316
 
336
317
  async function materializeMedia({
@@ -1,36 +1,89 @@
1
1
  import path, { extname } from 'node:path';
2
- import { readFileSync } from 'node:fs';
2
+ import { readFileSync, createReadStream } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
3
4
 
4
5
  export const WORKSPACE_BINARY_MIME = {
5
- '.png': 'image/png',
6
- '.jpg': 'image/jpeg',
6
+ '.mp4': 'video/mp4',
7
+ '.png': 'image/png',
8
+ '.jpg': 'image/jpeg',
7
9
  '.jpeg': 'image/jpeg',
8
10
  '.webp': 'image/webp',
9
- '.gif': 'image/gif',
10
- '.pdf': 'application/pdf',
11
+ '.gif': 'image/gif',
12
+ '.pdf': 'application/pdf',
13
+ '.mp3': 'audio/mpeg',
14
+ '.wav': 'audio/wav',
15
+ '.m4a': 'audio/mp4',
11
16
  };
12
17
 
13
- export const VIDEO_EXT = '.mp4';
14
- export const VIDEO_MIME = 'video/mp4';
18
+ const TEXT_EXTS = new Set([
19
+ '.md', '.txt', '.json', '.js', '.ts', '.html', '.css',
20
+ '.csv', '.xml', '.yaml', '.yml', '.sh',
21
+ ]);
22
+
23
+ const LARGE_FILE_THRESHOLD = 5 * 1024 * 1024; // 5MB
15
24
 
16
25
  export function resolveWorkspaceFileUploadPlan({ localPath, workspacePath }) {
17
26
  const target = String(workspacePath ?? '').trim() || String(localPath ?? '').trim();
18
27
  const ext = extname(target).toLowerCase();
19
28
  const filename = path.basename(target || localPath);
20
- if (ext === VIDEO_EXT) {
21
- return {
22
- mode: 'upload-video',
23
- ext,
24
- filename,
25
- mime: VIDEO_MIME,
26
- };
29
+ const mime = WORKSPACE_BINARY_MIME[ext] ?? 'application/octet-stream';
30
+ const isText = TEXT_EXTS.has(ext);
31
+ return { ext, filename, mime, isText };
32
+ }
33
+
34
+ function sha256ofBuffer(buf) {
35
+ return createHash('sha256').update(buf).digest('hex');
36
+ }
37
+
38
+ /**
39
+ * Upload a local file to the workspace via the new presign → PUT → confirm flow.
40
+ * For large files (>= 5MB), presign returns immediately (DB status = uploading)
41
+ * and the PUT + confirm happen in the background.
42
+ */
43
+ async function uploadBinaryFile({
44
+ localPath,
45
+ workspacePath,
46
+ workspaceId,
47
+ mime,
48
+ presign, // async fn({ workspaceId, path, size, mime, sha256 }) → { uploadUrl, objectKey, alreadyExists }
49
+ confirmUpload, // async fn({ workspaceId, path, objectKey }) → void
50
+ }) {
51
+ const buf = readFileSync(localPath);
52
+ const size = buf.length;
53
+ const sha256 = sha256ofBuffer(buf);
54
+
55
+ const { uploadUrl, objectKey, alreadyExists } = await presign({
56
+ workspaceId,
57
+ path: workspacePath,
58
+ size,
59
+ mime,
60
+ sha256,
61
+ });
62
+
63
+ if (alreadyExists) {
64
+ return { mode: 'object-storage', mime, bytes: size, objectKey, deduped: true };
27
65
  }
28
- return {
29
- mode: 'workspace-memory',
30
- ext,
31
- filename,
32
- mime: WORKSPACE_BINARY_MIME[ext] ?? 'application/octet-stream',
66
+
67
+ const isLarge = size >= LARGE_FILE_THRESHOLD;
68
+
69
+ const doPut = async () => {
70
+ const resp = await fetch(uploadUrl, {
71
+ method: 'PUT',
72
+ body: buf,
73
+ headers: { 'Content-Type': mime, 'Content-Length': String(size) },
74
+ });
75
+ if (!resp.ok) throw new Error(`COS PUT failed: ${resp.status} ${await resp.text()}`);
76
+ await confirmUpload({ workspaceId, path: workspacePath, objectKey });
33
77
  };
78
+
79
+ if (isLarge) {
80
+ // Fire-and-forget for large files; server already wrote `uploading` status
81
+ doPut().catch(err => console.error('[workspace-upload] large file background upload failed:', err?.message));
82
+ return { mode: 'object-storage', mime, bytes: size, objectKey, async: true };
83
+ }
84
+
85
+ await doPut();
86
+ return { mode: 'object-storage', mime, bytes: size, objectKey, async: false };
34
87
  }
35
88
 
36
89
  export async function writeLocalFileToWorkspace({
@@ -38,34 +91,33 @@ export async function writeLocalFileToWorkspace({
38
91
  workspacePath,
39
92
  workspaceId,
40
93
  readFileSyncFn = readFileSync,
41
- uploadWorkspaceMemory,
42
- uploadVideo,
94
+ uploadWorkspaceMemory, // legacy text path
95
+ presign,
96
+ confirmUpload,
43
97
  }) {
44
98
  const plan = resolveWorkspaceFileUploadPlan({ localPath, workspacePath });
45
- if (plan.mode === 'upload-video') {
46
- return uploadVideo({
99
+
100
+ if (!plan.isText) {
101
+ return uploadBinaryFile({
47
102
  localPath,
48
103
  workspacePath,
49
104
  workspaceId,
50
- filename: plan.filename,
51
105
  mime: plan.mime,
52
- mode: plan.mode,
106
+ presign,
107
+ confirmUpload,
53
108
  });
54
109
  }
55
110
 
111
+ // Text files: existing path via workspace-memory
56
112
  const buf = readFileSyncFn(localPath);
57
- const content = `data:${plan.mime};base64,${buf.toString('base64')}`;
113
+ const content = buf.toString('utf8');
58
114
  await uploadWorkspaceMemory({
59
115
  workspacePath,
60
116
  workspaceId,
61
117
  content,
62
118
  mime: plan.mime,
63
119
  bytes: buf.length,
64
- mode: plan.mode,
120
+ mode: 'workspace-memory',
65
121
  });
66
- return {
67
- mode: plan.mode,
68
- mime: plan.mime,
69
- bytes: buf.length,
70
- };
122
+ return { mode: 'workspace-memory', mime: plan.mime, bytes: buf.length };
71
123
  }