ofiere-openclaw-plugin 4.14.0 → 4.16.0

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tools.ts +125 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.14.0",
3
+ "version": "4.16.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin for Ofiere PM - 11 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, and space file management",
6
6
  "keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
package/src/tools.ts CHANGED
@@ -3232,6 +3232,66 @@ function registerFileOps(
3232
3232
  });
3233
3233
  }
3234
3234
 
3235
+ // ── File Ops helpers ─────────────────────────────────────────────────────────
3236
+
3237
+ /** MIME types allowed by the space-files storage bucket. */
3238
+ const ALLOWED_STORAGE_MIMES = new Set([
3239
+ "image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml",
3240
+ "video/mp4", "video/webm", "video/quicktime",
3241
+ "audio/mpeg", "audio/wav", "audio/ogg",
3242
+ "application/pdf", "text/plain", "text/markdown", "text/csv",
3243
+ "text/html", "text/css", "application/json",
3244
+ "application/javascript", "text/javascript", "application/typescript",
3245
+ "application/zip", "application/x-tar", "application/gzip",
3246
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
3247
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
3248
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
3249
+ "application/octet-stream",
3250
+ ]);
3251
+
3252
+ /**
3253
+ * Simple FNV-1a 32-bit hash → 8-char hex string.
3254
+ * Used to generate stable, short slugs from non-ASCII filenames.
3255
+ */
3256
+ function fnv1aHash(str: string): string {
3257
+ let hash = 0x811c9dc5;
3258
+ for (let i = 0; i < str.length; i++) {
3259
+ hash ^= str.charCodeAt(i);
3260
+ hash = (hash * 0x01000193) >>> 0;
3261
+ }
3262
+ return hash.toString(16).padStart(8, "0");
3263
+ }
3264
+
3265
+ /**
3266
+ * Sanitize a filename for safe use as a Supabase storage object key.
3267
+ * Strips characters that break storage APIs while preserving extension and readability.
3268
+ * For Unicode-heavy filenames, generates a hash-based slug to maintain uniqueness.
3269
+ * The original filename is always kept in DB metadata.
3270
+ */
3271
+ function sanitizeStorageFileName(fileName: string): string {
3272
+ // Split extension from stem
3273
+ const dotIdx = fileName.lastIndexOf(".");
3274
+ const stem = dotIdx > 0 ? fileName.slice(0, dotIdx) : fileName;
3275
+ const ext = dotIdx > 0 ? fileName.slice(dotIdx) : ""; // includes dot
3276
+
3277
+ // Sanitize stem: spaces → hyphens, strip non-ASCII/non-safe chars
3278
+ let safeStem = stem
3279
+ .replace(/\s+/g, "-")
3280
+ .replace(/[^a-zA-Z0-9_-]/g, "")
3281
+ .replace(/-{2,}/g, "-")
3282
+ .replace(/^-+|-+$/g, "");
3283
+
3284
+ // If stem collapsed (e.g. all Unicode), generate hash-based slug for traceability
3285
+ if (safeStem.length < 3) {
3286
+ safeStem = `f-${fnv1aHash(stem)}`;
3287
+ }
3288
+
3289
+ // Sanitize extension too (should be safe but belt-and-suspenders)
3290
+ const safeExt = ext.replace(/[^a-zA-Z0-9.]/g, "");
3291
+
3292
+ return `${safeStem}${safeExt}` || "file";
3293
+ }
3294
+
3235
3295
  // ── File Ops handlers ────────────────────────────────────────────────────────
3236
3296
 
3237
3297
  async function handleListFiles(
@@ -3339,8 +3399,9 @@ async function handleCreateTextFile(
3339
3399
  };
3340
3400
  const mimeType = mimeMap[ext] || "text/plain";
3341
3401
 
3342
- // Upload to storage
3343
- const storagePath = `${userId}/${spaceId}/${Date.now()}-${fileName}`;
3402
+ // Upload to storage — sanitize filename for storage key safety
3403
+ const safeFileName = sanitizeStorageFileName(fileName);
3404
+ const storagePath = `${userId}/${spaceId}/${Date.now()}-${safeFileName}`;
3344
3405
  const blob = new Blob([content], { type: mimeType });
3345
3406
  const buffer = await blob.arrayBuffer();
3346
3407
 
@@ -3388,8 +3449,37 @@ async function handleUploadFile(
3388
3449
  const bytes = new Uint8Array(binaryStr.length);
3389
3450
  for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
3390
3451
 
3391
- const mimeType = (params.file_type as string) || "application/octet-stream";
3392
- const storagePath = `${userId}/${spaceId}/${Date.now()}-${fileName}`;
3452
+ // Auto-detect MIME from extension if not explicitly provided
3453
+ let mimeType = params.file_type as string;
3454
+ let mimeWarning: string | undefined;
3455
+ if (!mimeType) {
3456
+ const ext = fileName.split(".").pop()?.toLowerCase() || "";
3457
+ const binaryMimeMap: Record<string, string> = {
3458
+ png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif",
3459
+ webp: "image/webp", bmp: "image/bmp", ico: "image/x-icon",
3460
+ svg: "image/svg+xml", avif: "image/avif",
3461
+ pdf: "application/pdf", zip: "application/zip", gz: "application/gzip",
3462
+ tar: "application/x-tar", "7z": "application/x-7z-compressed",
3463
+ mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", flac: "audio/flac",
3464
+ mp4: "video/mp4", webm: "video/webm", avi: "video/x-msvideo", mov: "video/quicktime",
3465
+ woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf",
3466
+ doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
3467
+ xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
3468
+ ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
3469
+ md: "text/markdown", txt: "text/plain", json: "application/json",
3470
+ csv: "text/csv", html: "text/html", css: "text/css",
3471
+ js: "text/javascript", ts: "text/typescript", yaml: "text/yaml",
3472
+ yml: "text/yaml", xml: "text/xml",
3473
+ };
3474
+ mimeType = binaryMimeMap[ext] || "application/octet-stream";
3475
+ } else if (!ALLOWED_STORAGE_MIMES.has(mimeType)) {
3476
+ // Explicit MIME not in bucket allowlist → fall back to octet-stream to prevent upload rejection
3477
+ mimeWarning = `Requested MIME "${mimeType}" is not in storage allowlist — stored as application/octet-stream. The original type is preserved in file metadata.`;
3478
+ mimeType = "application/octet-stream";
3479
+ }
3480
+
3481
+ const safeFileName = sanitizeStorageFileName(fileName);
3482
+ const storagePath = `${userId}/${spaceId}/${Date.now()}-${safeFileName}`;
3393
3483
 
3394
3484
  const { error: uploadErr } = await supabase.storage
3395
3485
  .from("space-files")
@@ -3397,6 +3487,10 @@ async function handleUploadFile(
3397
3487
 
3398
3488
  if (uploadErr) return err(`Storage upload failed: ${uploadErr.message}`);
3399
3489
 
3490
+ // Store the originally-requested MIME type in DB (for downstream consumers),
3491
+ // but use the storage-safe MIME for the actual upload
3492
+ const dbMimeType = (params.file_type as string) || mimeType;
3493
+
3400
3494
  const { data, error } = await supabase
3401
3495
  .from("pm_space_files")
3402
3496
  .insert({
@@ -3404,7 +3498,7 @@ async function handleUploadFile(
3404
3498
  space_id: spaceId,
3405
3499
  folder_id: (params.folder_id as string) || null,
3406
3500
  file_name: fileName,
3407
- file_type: mimeType,
3501
+ file_type: dbMimeType,
3408
3502
  file_size: bytes.length,
3409
3503
  storage_path: storagePath,
3410
3504
  is_shared: false,
@@ -3414,7 +3508,12 @@ async function handleUploadFile(
3414
3508
  .single();
3415
3509
 
3416
3510
  if (error) return err(error.message);
3417
- return ok({ message: `File "${fileName}" uploaded (${bytes.length} bytes)`, file: data });
3511
+ const result: Record<string, unknown> = {
3512
+ message: `File "${fileName}" uploaded (${bytes.length} bytes)`,
3513
+ file: data,
3514
+ };
3515
+ if (mimeWarning) result.warning = mimeWarning;
3516
+ return ok(result);
3418
3517
  } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3419
3518
  }
3420
3519
 
@@ -3463,13 +3562,16 @@ async function handleRenameFile(
3463
3562
  if (!fileId) return err("Missing required field: file_id");
3464
3563
  if (!newName) return err("Missing required field: new_name");
3465
3564
 
3466
- const { error } = await supabase
3565
+ const { data, error } = await supabase
3467
3566
  .from("pm_space_files")
3468
3567
  .update({ file_name: newName, updated_at: new Date().toISOString() })
3469
3568
  .eq("id", fileId)
3470
- .eq("user_id", userId);
3569
+ .eq("user_id", userId)
3570
+ .select("id")
3571
+ .maybeSingle();
3471
3572
 
3472
3573
  if (error) return err(error.message);
3574
+ if (!data) return err(`File not found: ${fileId}`);
3473
3575
  return ok({ message: `File renamed to "${newName}"` });
3474
3576
  } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3475
3577
  }
@@ -3483,13 +3585,16 @@ async function handleRenameSpaceFolder(
3483
3585
  if (!folderId) return err("Missing required field: folder_id");
3484
3586
  if (!newName) return err("Missing required field: new_name");
3485
3587
 
3486
- const { error } = await supabase
3588
+ const { data, error } = await supabase
3487
3589
  .from("pm_space_folders")
3488
3590
  .update({ name: newName, updated_at: new Date().toISOString() })
3489
3591
  .eq("id", folderId)
3490
- .eq("user_id", userId);
3592
+ .eq("user_id", userId)
3593
+ .select("id")
3594
+ .maybeSingle();
3491
3595
 
3492
3596
  if (error) return err(error.message);
3597
+ if (!data) return err(`Folder not found: ${folderId}`);
3493
3598
  return ok({ message: `Folder renamed to "${newName}"` });
3494
3599
  } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3495
3600
  }
@@ -3505,13 +3610,16 @@ async function handleMoveFile(
3505
3610
  ? (params.target_folder_id as string | null)
3506
3611
  : null;
3507
3612
 
3508
- const { error } = await supabase
3613
+ const { data, error } = await supabase
3509
3614
  .from("pm_space_files")
3510
3615
  .update({ folder_id: targetFolderId, updated_at: new Date().toISOString() })
3511
3616
  .eq("id", fileId)
3512
- .eq("user_id", userId);
3617
+ .eq("user_id", userId)
3618
+ .select("id")
3619
+ .maybeSingle();
3513
3620
 
3514
3621
  if (error) return err(error.message);
3622
+ if (!data) return err(`File not found: ${fileId}`);
3515
3623
  return ok({ message: `File moved to ${targetFolderId || "root"}` });
3516
3624
  } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3517
3625
  }
@@ -3527,13 +3635,16 @@ async function handleMoveSpaceFolder(
3527
3635
  ? (params.target_parent_id as string | null)
3528
3636
  : null;
3529
3637
 
3530
- const { error } = await supabase
3638
+ const { data, error } = await supabase
3531
3639
  .from("pm_space_folders")
3532
3640
  .update({ parent_folder_id: targetParentId, updated_at: new Date().toISOString() })
3533
3641
  .eq("id", folderId)
3534
- .eq("user_id", userId);
3642
+ .eq("user_id", userId)
3643
+ .select("id")
3644
+ .maybeSingle();
3535
3645
 
3536
3646
  if (error) return err(error.message);
3647
+ if (!data) return err(`Folder not found: ${folderId}`);
3537
3648
  return ok({ message: `Folder moved to ${targetParentId || "root"}` });
3538
3649
  } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3539
3650
  }