ofiere-openclaw-plugin 4.12.2 → 4.13.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.12.2",
3
+ "version": "4.13.0",
4
4
  "type": "module",
5
- "description": "OpenClaw plugin for Ofiere PM - 10 meta-tools with 13-action workflow mastery covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, and constellation agent architecture",
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"],
7
7
  "homepage": "https://github.com/gilanggemar/Ofiere",
8
8
  "repository": {
package/src/prompt.ts CHANGED
@@ -93,6 +93,20 @@ const TOOL_DOCS: Record<string, string> = {
93
93
  - list_agent_mesh: See the sovereignty map (which agent owns what domain)
94
94
  - New agents get workspace-<name>/ with IDENTITY.md, SOUL.md, AGENTS.md, TOOLS.md, <CODENAME>.md, and skills/
95
95
  - All changes sync to the Constellation dashboard automatically`,
96
+
97
+ OFIERE_FILE_OPS: `- **OFIERE_FILE_OPS** — Manage Space Files (action: "list_files", "list_folders", "create_folder", "create_text_file", "upload_file", "read_text_file", "rename_file", "rename_folder", "move_file", "move_folder", "delete_file", "delete_folder", "share_file", "unshare_file")
98
+ - list_files: Files in a space. Required: space_id. Optional: folder_id, shared_only
99
+ - list_folders: Folders in a space. Required: space_id. Optional: parent_folder_id
100
+ - create_folder: New folder. Required: space_id, name. Optional: parent_folder_id
101
+ - create_text_file: Create text file (md, txt, json, csv, etc.). Required: space_id, file_name, content. Optional: folder_id
102
+ - upload_file: Upload binary (base64). Required: space_id, file_name, content_base64. Optional: folder_id, file_type
103
+ - read_text_file: Read file content. Required: file_id. Use when task instructions reference a file with @[name](file:ID)
104
+ - rename_file / rename_folder: Rename. Required: file_id/folder_id + new_name
105
+ - move_file / move_folder: Move to target folder (null=root). Required: file_id/folder_id
106
+ - delete_file: Remove file + storage. Required: file_id
107
+ - delete_folder: Remove folder + all nested files recursively. Required: folder_id
108
+ - share_file / unshare_file: Toggle shared status. Required: file_id
109
+ - Files created here appear in the PM Space Files tab immediately`,
96
110
  };
97
111
 
98
112
  export function getSystemPrompt(state: {
@@ -148,6 +162,9 @@ ${toolDocs}
148
162
  - Use add_approval when a task needs sign-off from a human or another agent before proceeding. Approvers can be agents (by name) or humans.
149
163
  - Task approvals (OFIERE_TASK_OPS) are SEPARATE from workflow gate approvals (human_approval nodes). Do not confuse them.
150
164
  - When an agent completes critical work, consider adding an approval request for human review before marking the task DONE.
165
+ - When task instructions or system prompts contain file references like @[filename](file:FILE_ID), use OFIERE_FILE_OPS action:"read_text_file" file_id:"FILE_ID" to read the file content. Do NOT ask the user for the file — retrieve it yourself.
166
+ - Use OFIERE_FILE_OPS to create output files (reports, data, configs) in the Space Files explorer. Prefer create_text_file for text-based outputs.
167
+ - To save task output as a file, call OFIERE_FILE_OPS action:"create_text_file" with the space_id from the task context.
151
168
  </ofiere-pm>`;
152
169
  }
153
170
 
package/src/tools.ts CHANGED
@@ -293,8 +293,8 @@ function registerTaskOps(
293
293
  approval_id: { type: "string", description: "Approval ID for resolve_approval action" },
294
294
  approval_status: {
295
295
  type: "string",
296
- description: "Approval decision: approved or rejected. Used with resolve_approval.",
297
- enum: ["approved", "rejected"],
296
+ description: "Approval status filter (pending|approved|rejected) for list_approvals, or decision (approved|rejected) for resolve_approval.",
297
+ enum: ["pending", "approved", "rejected"],
298
298
  },
299
299
  comment: { type: "string", description: "Comment for approval (add_approval or resolve_approval)" },
300
300
  },
@@ -3132,6 +3132,526 @@ function registerConstellationOps(
3132
3132
  });
3133
3133
  }
3134
3134
 
3135
+ // ═══════════════════════════════════════════════════════════════════════════════
3136
+ // META-TOOL 11: OFIERE_FILE_OPS — Space File Management
3137
+ // ═══════════════════════════════════════════════════════════════════════════════
3138
+
3139
+ function registerFileOps(
3140
+ api: any,
3141
+ supabase: SupabaseClient,
3142
+ userId: string,
3143
+ ): void {
3144
+ api.registerTool({
3145
+ name: "OFIERE_FILE_OPS",
3146
+ label: "Ofiere File Operations",
3147
+ description:
3148
+ `Manage files and folders in the Ofiere PM Space Files explorer.\n\n` +
3149
+ `Actions:\n` +
3150
+ `- "list_files": List files. Required: space_id. Optional: folder_id, shared_only\n` +
3151
+ `- "list_folders": List folders. Required: space_id. Optional: parent_folder_id\n` +
3152
+ `- "create_folder": Create a folder. Required: space_id, name. Optional: parent_folder_id\n` +
3153
+ `- "create_text_file": Create a text file (md, txt, json, etc.). Required: space_id, file_name, content. Optional: folder_id\n` +
3154
+ `- "upload_file": Upload a binary file (base64-encoded). Required: space_id, file_name, content_base64. Optional: folder_id, file_type\n` +
3155
+ `- "read_text_file": Read the text content of a file. Required: file_id\n` +
3156
+ `- "rename_file": Rename a file. Required: file_id, new_name\n` +
3157
+ `- "rename_folder": Rename a folder. Required: folder_id, new_name\n` +
3158
+ `- "move_file": Move file to a folder (null=root). Required: file_id. Optional: target_folder_id\n` +
3159
+ `- "move_folder": Move folder. Required: folder_id. Optional: target_parent_id\n` +
3160
+ `- "delete_file": Delete file + storage blob. Required: file_id\n` +
3161
+ `- "delete_folder": Delete folder + all contents recursively. Required: folder_id\n` +
3162
+ `- "share_file": Mark file as shared (visible cross-space). Required: file_id\n` +
3163
+ `- "unshare_file": Remove shared status. Required: file_id\n\n` +
3164
+ `When task instructions contain @[filename](file:FILE_ID), use read_text_file to retrieve the content.`,
3165
+ parameters: {
3166
+ type: "object",
3167
+ required: ["action"],
3168
+ properties: {
3169
+ action: {
3170
+ type: "string",
3171
+ description: "The operation to perform",
3172
+ enum: [
3173
+ "list_files", "list_folders", "create_folder",
3174
+ "create_text_file", "upload_file", "read_text_file",
3175
+ "rename_file", "rename_folder",
3176
+ "move_file", "move_folder",
3177
+ "delete_file", "delete_folder",
3178
+ "share_file", "unshare_file",
3179
+ ],
3180
+ },
3181
+ space_id: { type: "string", description: "PM Space ID" },
3182
+ folder_id: { type: "string", description: "Folder ID for scoping or placement" },
3183
+ parent_folder_id: { type: "string", description: "Parent folder ID for nesting" },
3184
+ target_folder_id: { type: "string", description: "Target folder to move file into (null=root)" },
3185
+ target_parent_id: { type: "string", description: "Target parent folder to move folder into (null=root)" },
3186
+ file_id: { type: "string", description: "File ID for operations on existing files" },
3187
+ name: { type: "string", description: "Folder name (for create_folder)" },
3188
+ file_name: { type: "string", description: "File name including extension (e.g. 'report.md')" },
3189
+ new_name: { type: "string", description: "New name for rename operations" },
3190
+ content: { type: "string", description: "Text content for create_text_file" },
3191
+ content_base64: { type: "string", description: "Base64-encoded binary content for upload_file" },
3192
+ file_type: { type: "string", description: "MIME type (auto-detected if omitted)" },
3193
+ shared_only: { type: "boolean", description: "If true, only return shared files" },
3194
+ },
3195
+ },
3196
+ async execute(_id: string, params: Record<string, unknown>) {
3197
+ const action = params.action as string;
3198
+
3199
+ switch (action) {
3200
+ case "list_files":
3201
+ return handleListFiles(supabase, userId, params);
3202
+ case "list_folders":
3203
+ return handleListFolders(supabase, userId, params);
3204
+ case "create_folder":
3205
+ return handleCreateSpaceFolder(supabase, userId, params);
3206
+ case "create_text_file":
3207
+ return handleCreateTextFile(supabase, userId, params);
3208
+ case "upload_file":
3209
+ return handleUploadFile(supabase, userId, params);
3210
+ case "read_text_file":
3211
+ return handleReadTextFile(supabase, userId, params);
3212
+ case "rename_file":
3213
+ return handleRenameFile(supabase, userId, params);
3214
+ case "rename_folder":
3215
+ return handleRenameSpaceFolder(supabase, userId, params);
3216
+ case "move_file":
3217
+ return handleMoveFile(supabase, userId, params);
3218
+ case "move_folder":
3219
+ return handleMoveSpaceFolder(supabase, userId, params);
3220
+ case "delete_file":
3221
+ return handleDeleteSpaceFile(supabase, userId, params);
3222
+ case "delete_folder":
3223
+ return handleDeleteSpaceFolder(supabase, userId, params);
3224
+ case "share_file":
3225
+ return handleShareFile(supabase, userId, params, true);
3226
+ case "unshare_file":
3227
+ return handleShareFile(supabase, userId, params, false);
3228
+ default:
3229
+ return err(`Unknown action "${action}". Valid: list_files, list_folders, create_folder, create_text_file, upload_file, read_text_file, rename_file, rename_folder, move_file, move_folder, delete_file, delete_folder, share_file, unshare_file`);
3230
+ }
3231
+ },
3232
+ });
3233
+ }
3234
+
3235
+ // ── File Ops handlers ────────────────────────────────────────────────────────
3236
+
3237
+ async function handleListFiles(
3238
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3239
+ ): Promise<ToolResult> {
3240
+ try {
3241
+ const spaceId = params.space_id as string;
3242
+ if (!spaceId) return err("Missing required field: space_id");
3243
+
3244
+ let query = supabase
3245
+ .from("pm_space_files")
3246
+ .select("id, file_name, file_type, file_size, folder_id, is_shared, source, created_at")
3247
+ .eq("user_id", userId)
3248
+ .eq("space_id", spaceId)
3249
+ .order("created_at", { ascending: false });
3250
+
3251
+ if (params.shared_only) query = query.eq("is_shared", true);
3252
+ if (params.folder_id !== undefined) {
3253
+ if (params.folder_id === null || params.folder_id === "root") {
3254
+ query = query.is("folder_id", null);
3255
+ } else {
3256
+ query = query.eq("folder_id", params.folder_id as string);
3257
+ }
3258
+ }
3259
+
3260
+ const { data, error } = await query;
3261
+ if (error) return err(error.message);
3262
+ return ok({ files: data || [], count: (data || []).length });
3263
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3264
+ }
3265
+
3266
+ async function handleListFolders(
3267
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3268
+ ): Promise<ToolResult> {
3269
+ try {
3270
+ const spaceId = params.space_id as string;
3271
+ if (!spaceId) return err("Missing required field: space_id");
3272
+
3273
+ let query = supabase
3274
+ .from("pm_space_folders")
3275
+ .select("id, name, parent_folder_id, is_shared, created_at")
3276
+ .eq("user_id", userId)
3277
+ .eq("space_id", spaceId)
3278
+ .order("name", { ascending: true });
3279
+
3280
+ if (params.parent_folder_id !== undefined) {
3281
+ if (params.parent_folder_id === null || params.parent_folder_id === "root") {
3282
+ query = query.is("parent_folder_id", null);
3283
+ } else {
3284
+ query = query.eq("parent_folder_id", params.parent_folder_id as string);
3285
+ }
3286
+ }
3287
+
3288
+ const { data, error } = await query;
3289
+ if (error) return err(error.message);
3290
+ return ok({ folders: data || [], count: (data || []).length });
3291
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3292
+ }
3293
+
3294
+ async function handleCreateSpaceFolder(
3295
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3296
+ ): Promise<ToolResult> {
3297
+ try {
3298
+ const spaceId = params.space_id as string;
3299
+ const name = params.name as string;
3300
+ if (!spaceId) return err("Missing required field: space_id");
3301
+ if (!name) return err("Missing required field: name");
3302
+
3303
+ const { data, error } = await supabase
3304
+ .from("pm_space_folders")
3305
+ .insert({
3306
+ user_id: userId,
3307
+ space_id: spaceId,
3308
+ name,
3309
+ parent_folder_id: (params.parent_folder_id as string) || null,
3310
+ is_shared: false,
3311
+ })
3312
+ .select()
3313
+ .single();
3314
+
3315
+ if (error) return err(error.message);
3316
+ return ok({ message: `Folder "${name}" created`, folder: data });
3317
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3318
+ }
3319
+
3320
+ async function handleCreateTextFile(
3321
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3322
+ ): Promise<ToolResult> {
3323
+ try {
3324
+ const spaceId = params.space_id as string;
3325
+ const fileName = params.file_name as string;
3326
+ const content = params.content as string;
3327
+ if (!spaceId) return err("Missing required field: space_id");
3328
+ if (!fileName) return err("Missing required field: file_name");
3329
+ if (content === undefined || content === null) return err("Missing required field: content");
3330
+
3331
+ // Detect MIME type from extension
3332
+ const ext = fileName.split(".").pop()?.toLowerCase() || "";
3333
+ const mimeMap: Record<string, string> = {
3334
+ md: "text/markdown", txt: "text/plain", json: "application/json",
3335
+ csv: "text/csv", html: "text/html", css: "text/css",
3336
+ js: "text/javascript", ts: "text/typescript", yaml: "text/yaml",
3337
+ yml: "text/yaml", xml: "text/xml", svg: "image/svg+xml",
3338
+ sh: "text/x-shellscript", py: "text/x-python", toml: "text/toml",
3339
+ };
3340
+ const mimeType = mimeMap[ext] || "text/plain";
3341
+
3342
+ // Upload to storage
3343
+ const storagePath = `${userId}/${spaceId}/${Date.now()}-${fileName}`;
3344
+ const blob = new Blob([content], { type: mimeType });
3345
+ const buffer = await blob.arrayBuffer();
3346
+
3347
+ const { error: uploadErr } = await supabase.storage
3348
+ .from("space-files")
3349
+ .upload(storagePath, buffer, { contentType: mimeType, upsert: false });
3350
+
3351
+ if (uploadErr) return err(`Storage upload failed: ${uploadErr.message}`);
3352
+
3353
+ // Insert metadata
3354
+ const { data, error } = await supabase
3355
+ .from("pm_space_files")
3356
+ .insert({
3357
+ user_id: userId,
3358
+ space_id: spaceId,
3359
+ folder_id: (params.folder_id as string) || null,
3360
+ file_name: fileName,
3361
+ file_type: mimeType,
3362
+ file_size: content.length,
3363
+ storage_path: storagePath,
3364
+ is_shared: false,
3365
+ source: "agent",
3366
+ })
3367
+ .select()
3368
+ .single();
3369
+
3370
+ if (error) return err(error.message);
3371
+ return ok({ message: `File "${fileName}" created (${content.length} bytes)`, file: data });
3372
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3373
+ }
3374
+
3375
+ async function handleUploadFile(
3376
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3377
+ ): Promise<ToolResult> {
3378
+ try {
3379
+ const spaceId = params.space_id as string;
3380
+ const fileName = params.file_name as string;
3381
+ const b64 = params.content_base64 as string;
3382
+ if (!spaceId) return err("Missing required field: space_id");
3383
+ if (!fileName) return err("Missing required field: file_name");
3384
+ if (!b64) return err("Missing required field: content_base64");
3385
+
3386
+ // Decode base64
3387
+ const binaryStr = atob(b64);
3388
+ const bytes = new Uint8Array(binaryStr.length);
3389
+ for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
3390
+
3391
+ const mimeType = (params.file_type as string) || "application/octet-stream";
3392
+ const storagePath = `${userId}/${spaceId}/${Date.now()}-${fileName}`;
3393
+
3394
+ const { error: uploadErr } = await supabase.storage
3395
+ .from("space-files")
3396
+ .upload(storagePath, bytes.buffer, { contentType: mimeType, upsert: false });
3397
+
3398
+ if (uploadErr) return err(`Storage upload failed: ${uploadErr.message}`);
3399
+
3400
+ const { data, error } = await supabase
3401
+ .from("pm_space_files")
3402
+ .insert({
3403
+ user_id: userId,
3404
+ space_id: spaceId,
3405
+ folder_id: (params.folder_id as string) || null,
3406
+ file_name: fileName,
3407
+ file_type: mimeType,
3408
+ file_size: bytes.length,
3409
+ storage_path: storagePath,
3410
+ is_shared: false,
3411
+ source: "agent",
3412
+ })
3413
+ .select()
3414
+ .single();
3415
+
3416
+ if (error) return err(error.message);
3417
+ return ok({ message: `File "${fileName}" uploaded (${bytes.length} bytes)`, file: data });
3418
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3419
+ }
3420
+
3421
+ async function handleReadTextFile(
3422
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3423
+ ): Promise<ToolResult> {
3424
+ try {
3425
+ const fileId = params.file_id as string;
3426
+ if (!fileId) return err("Missing required field: file_id");
3427
+
3428
+ // Get metadata
3429
+ const { data: fileMeta, error: metaErr } = await supabase
3430
+ .from("pm_space_files")
3431
+ .select("storage_path, file_name, file_type, file_size")
3432
+ .eq("id", fileId)
3433
+ .eq("user_id", userId)
3434
+ .single();
3435
+
3436
+ if (metaErr || !fileMeta) return err(`File not found: ${fileId}`);
3437
+
3438
+ // Download from storage
3439
+ const { data: blob, error: dlErr } = await supabase.storage
3440
+ .from("space-files")
3441
+ .download(fileMeta.storage_path);
3442
+
3443
+ if (dlErr || !blob) return err(`Download failed: ${dlErr?.message || "unknown"}`);
3444
+
3445
+ const text = await blob.text();
3446
+
3447
+ return ok({
3448
+ file_id: fileId,
3449
+ file_name: fileMeta.file_name,
3450
+ file_type: fileMeta.file_type,
3451
+ file_size: fileMeta.file_size,
3452
+ content: text,
3453
+ });
3454
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3455
+ }
3456
+
3457
+ async function handleRenameFile(
3458
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3459
+ ): Promise<ToolResult> {
3460
+ try {
3461
+ const fileId = params.file_id as string;
3462
+ const newName = params.new_name as string;
3463
+ if (!fileId) return err("Missing required field: file_id");
3464
+ if (!newName) return err("Missing required field: new_name");
3465
+
3466
+ const { error } = await supabase
3467
+ .from("pm_space_files")
3468
+ .update({ file_name: newName, updated_at: new Date().toISOString() })
3469
+ .eq("id", fileId)
3470
+ .eq("user_id", userId);
3471
+
3472
+ if (error) return err(error.message);
3473
+ return ok({ message: `File renamed to "${newName}"` });
3474
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3475
+ }
3476
+
3477
+ async function handleRenameSpaceFolder(
3478
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3479
+ ): Promise<ToolResult> {
3480
+ try {
3481
+ const folderId = params.folder_id as string;
3482
+ const newName = params.new_name as string;
3483
+ if (!folderId) return err("Missing required field: folder_id");
3484
+ if (!newName) return err("Missing required field: new_name");
3485
+
3486
+ const { error } = await supabase
3487
+ .from("pm_space_folders")
3488
+ .update({ name: newName, updated_at: new Date().toISOString() })
3489
+ .eq("id", folderId)
3490
+ .eq("user_id", userId);
3491
+
3492
+ if (error) return err(error.message);
3493
+ return ok({ message: `Folder renamed to "${newName}"` });
3494
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3495
+ }
3496
+
3497
+ async function handleMoveFile(
3498
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3499
+ ): Promise<ToolResult> {
3500
+ try {
3501
+ const fileId = params.file_id as string;
3502
+ if (!fileId) return err("Missing required field: file_id");
3503
+
3504
+ const targetFolderId = params.target_folder_id !== undefined
3505
+ ? (params.target_folder_id as string | null)
3506
+ : null;
3507
+
3508
+ const { error } = await supabase
3509
+ .from("pm_space_files")
3510
+ .update({ folder_id: targetFolderId, updated_at: new Date().toISOString() })
3511
+ .eq("id", fileId)
3512
+ .eq("user_id", userId);
3513
+
3514
+ if (error) return err(error.message);
3515
+ return ok({ message: `File moved to ${targetFolderId || "root"}` });
3516
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3517
+ }
3518
+
3519
+ async function handleMoveSpaceFolder(
3520
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3521
+ ): Promise<ToolResult> {
3522
+ try {
3523
+ const folderId = params.folder_id as string;
3524
+ if (!folderId) return err("Missing required field: folder_id");
3525
+
3526
+ const targetParentId = params.target_parent_id !== undefined
3527
+ ? (params.target_parent_id as string | null)
3528
+ : null;
3529
+
3530
+ const { error } = await supabase
3531
+ .from("pm_space_folders")
3532
+ .update({ parent_folder_id: targetParentId, updated_at: new Date().toISOString() })
3533
+ .eq("id", folderId)
3534
+ .eq("user_id", userId);
3535
+
3536
+ if (error) return err(error.message);
3537
+ return ok({ message: `Folder moved to ${targetParentId || "root"}` });
3538
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3539
+ }
3540
+
3541
+ async function handleDeleteSpaceFile(
3542
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3543
+ ): Promise<ToolResult> {
3544
+ try {
3545
+ const fileId = params.file_id as string;
3546
+ if (!fileId) return err("Missing required field: file_id");
3547
+
3548
+ // Get storage path first
3549
+ const { data: fileMeta } = await supabase
3550
+ .from("pm_space_files")
3551
+ .select("storage_path, file_name")
3552
+ .eq("id", fileId)
3553
+ .eq("user_id", userId)
3554
+ .single();
3555
+
3556
+ if (!fileMeta) return err(`File not found: ${fileId}`);
3557
+
3558
+ // Delete from storage
3559
+ if (fileMeta.storage_path) {
3560
+ await supabase.storage.from("space-files").remove([fileMeta.storage_path]);
3561
+ }
3562
+
3563
+ // Delete metadata
3564
+ const { error } = await supabase
3565
+ .from("pm_space_files")
3566
+ .delete()
3567
+ .eq("id", fileId)
3568
+ .eq("user_id", userId);
3569
+
3570
+ if (error) return err(error.message);
3571
+ return ok({ message: `File "${fileMeta.file_name}" deleted`, deleted: true });
3572
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3573
+ }
3574
+
3575
+ async function handleDeleteSpaceFolder(
3576
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3577
+ ): Promise<ToolResult> {
3578
+ try {
3579
+ const folderId = params.folder_id as string;
3580
+ if (!folderId) return err("Missing required field: folder_id");
3581
+
3582
+ // Recursively collect all child folder IDs
3583
+ const allFolderIds: string[] = [folderId];
3584
+ const queue = [folderId];
3585
+ while (queue.length > 0) {
3586
+ const parentId = queue.shift()!;
3587
+ const { data: children } = await supabase
3588
+ .from("pm_space_folders")
3589
+ .select("id")
3590
+ .eq("parent_folder_id", parentId)
3591
+ .eq("user_id", userId);
3592
+ if (children) {
3593
+ for (const child of children) {
3594
+ allFolderIds.push(child.id);
3595
+ queue.push(child.id);
3596
+ }
3597
+ }
3598
+ }
3599
+
3600
+ // Delete all files in these folders (storage + metadata)
3601
+ const { data: files } = await supabase
3602
+ .from("pm_space_files")
3603
+ .select("id, storage_path")
3604
+ .in("folder_id", allFolderIds)
3605
+ .eq("user_id", userId);
3606
+
3607
+ if (files && files.length > 0) {
3608
+ const storagePaths = files.map((f: any) => f.storage_path).filter(Boolean);
3609
+ if (storagePaths.length > 0) {
3610
+ await supabase.storage.from("space-files").remove(storagePaths);
3611
+ }
3612
+ await supabase
3613
+ .from("pm_space_files")
3614
+ .delete()
3615
+ .in("id", files.map((f: any) => f.id))
3616
+ .eq("user_id", userId);
3617
+ }
3618
+
3619
+ // Delete all folders (deepest first)
3620
+ for (const id of allFolderIds.reverse()) {
3621
+ await supabase
3622
+ .from("pm_space_folders")
3623
+ .delete()
3624
+ .eq("id", id)
3625
+ .eq("user_id", userId);
3626
+ }
3627
+
3628
+ return ok({
3629
+ message: `Folder and all contents deleted`,
3630
+ folders_deleted: allFolderIds.length,
3631
+ files_deleted: files?.length || 0,
3632
+ });
3633
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3634
+ }
3635
+
3636
+ async function handleShareFile(
3637
+ supabase: SupabaseClient, userId: string, params: Record<string, unknown>,
3638
+ shared: boolean,
3639
+ ): Promise<ToolResult> {
3640
+ try {
3641
+ const fileId = params.file_id as string;
3642
+ if (!fileId) return err("Missing required field: file_id");
3643
+
3644
+ const { error } = await supabase
3645
+ .from("pm_space_files")
3646
+ .update({ is_shared: shared, updated_at: new Date().toISOString() })
3647
+ .eq("id", fileId)
3648
+ .eq("user_id", userId);
3649
+
3650
+ if (error) return err(error.message);
3651
+ return ok({ message: `File ${shared ? "shared" : "unshared"}` });
3652
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
3653
+ }
3654
+
3135
3655
  // ═══════════════════════════════════════════════════════════════════════════════
3136
3656
  // Public: Register All Meta-Tools
3137
3657
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -3159,9 +3679,10 @@ export function registerTools(
3159
3679
  registerMemoryOps(api, supabase, userId); // 8
3160
3680
  registerPromptOps(api, supabase, userId); // 9
3161
3681
  registerConstellationOps(api, supabase, userId); // 10
3682
+ registerFileOps(api, supabase, userId); // 11
3162
3683
 
3163
3684
  // ── Count and log ──
3164
- const toolCount = 10;
3685
+ const toolCount = 11;
3165
3686
  const callerName = getCallingAgentName(api);
3166
3687
  const agentLabel = fallbackAgentId || callerName || "auto-detect";
3167
3688
  api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);