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.
- package/package.json +1 -1
- 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.
|
|
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
|
|
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
|
-
|
|
3392
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|