ofiere-openclaw-plugin 4.12.3 → 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 +2 -2
- package/src/prompt.ts +17 -0
- package/src/tools.ts +522 -1
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OpenClaw plugin for Ofiere PM -
|
|
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
|
@@ -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 =
|
|
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})`);
|