ofiere-openclaw-plugin 4.15.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 +67 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.15.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
@@ -3234,19 +3234,62 @@ function registerFileOps(
3234
3234
 
3235
3235
  // ── File Ops helpers ─────────────────────────────────────────────────────────
3236
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
+
3237
3265
  /**
3238
3266
  * Sanitize a filename for safe use as a Supabase storage object key.
3239
- * Strips characters that break storage APIs (parens, em-dashes, exclamation marks, etc.)
3240
- * while preserving extension and readability. The original filename is kept in DB metadata.
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.
3241
3270
  */
3242
3271
  function sanitizeStorageFileName(fileName: string): string {
3243
- // Replace spaces with hyphens, strip anything not alphanumeric, hyphen, underscore, or dot
3244
- return fileName
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
3245
3279
  .replace(/\s+/g, "-")
3246
- .replace(/[^a-zA-Z0-9._-]/g, "")
3247
- .replace(/-{2,}/g, "-") // collapse multiple hyphens
3248
- .replace(/^-+|-+$/g, "") // trim leading/trailing hyphens
3249
- || "file"; // fallback if everything was stripped
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";
3250
3293
  }
3251
3294
 
3252
3295
  // ── File Ops handlers ────────────────────────────────────────────────────────
@@ -3408,6 +3451,7 @@ async function handleUploadFile(
3408
3451
 
3409
3452
  // Auto-detect MIME from extension if not explicitly provided
3410
3453
  let mimeType = params.file_type as string;
3454
+ let mimeWarning: string | undefined;
3411
3455
  if (!mimeType) {
3412
3456
  const ext = fileName.split(".").pop()?.toLowerCase() || "";
3413
3457
  const binaryMimeMap: Record<string, string> = {
@@ -3428,6 +3472,10 @@ async function handleUploadFile(
3428
3472
  yml: "text/yaml", xml: "text/xml",
3429
3473
  };
3430
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";
3431
3479
  }
3432
3480
 
3433
3481
  const safeFileName = sanitizeStorageFileName(fileName);
@@ -3439,6 +3487,10 @@ async function handleUploadFile(
3439
3487
 
3440
3488
  if (uploadErr) return err(`Storage upload failed: ${uploadErr.message}`);
3441
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
+
3442
3494
  const { data, error } = await supabase
3443
3495
  .from("pm_space_files")
3444
3496
  .insert({
@@ -3446,7 +3498,7 @@ async function handleUploadFile(
3446
3498
  space_id: spaceId,
3447
3499
  folder_id: (params.folder_id as string) || null,
3448
3500
  file_name: fileName,
3449
- file_type: mimeType,
3501
+ file_type: dbMimeType,
3450
3502
  file_size: bytes.length,
3451
3503
  storage_path: storagePath,
3452
3504
  is_shared: false,
@@ -3456,7 +3508,12 @@ async function handleUploadFile(
3456
3508
  .single();
3457
3509
 
3458
3510
  if (error) return err(error.message);
3459
- 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);
3460
3517
  } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3461
3518
  }
3462
3519