feat: project files MCP tools — list, get, download, upload, update description, delete
This commit is contained in:
parent
5e60e34c3f
commit
e4e604b049
112
src/tools/files.ts
Normal file
112
src/tools/files.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { Type } from '@sinclair/typebox';
|
||||||
|
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||||
|
import type { ToolContext } from './types.js';
|
||||||
|
|
||||||
|
const ListFilesParams = Type.Object({
|
||||||
|
project_slug: Type.String({ description: 'Project slug' }),
|
||||||
|
search: Type.Optional(Type.String({ description: 'Search by filename' })),
|
||||||
|
});
|
||||||
|
|
||||||
|
const GetFileParams = Type.Object({
|
||||||
|
project_slug: Type.String({ description: 'Project slug' }),
|
||||||
|
file_id: Type.String({ description: 'File UUID' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const DownloadFileParams = Type.Object({
|
||||||
|
project_slug: Type.String({ description: 'Project slug' }),
|
||||||
|
file_id: Type.String({ description: 'File UUID' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const UploadFileParams = Type.Object({
|
||||||
|
project_slug: Type.String({ description: 'Project slug' }),
|
||||||
|
filename: Type.String({ description: 'File name (e.g. "spec.md")' }),
|
||||||
|
content: Type.String({ description: 'File content as base64 string' }),
|
||||||
|
description: Type.Optional(Type.String({ description: 'File description' })),
|
||||||
|
});
|
||||||
|
|
||||||
|
const UpdateFileParams = Type.Object({
|
||||||
|
project_slug: Type.String({ description: 'Project slug' }),
|
||||||
|
file_id: Type.String({ description: 'File UUID' }),
|
||||||
|
description: Type.String({ description: 'New description' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const DeleteFileParams = Type.Object({
|
||||||
|
project_slug: Type.String({ description: 'Project slug' }),
|
||||||
|
file_id: Type.String({ description: 'File UUID' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
function ok(text: string) {
|
||||||
|
return { content: [{ type: 'text' as const, text }], details: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFileTools(ctx: ToolContext): ToolDefinition<any>[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'list_project_files',
|
||||||
|
label: 'List Project Files',
|
||||||
|
description: 'List files in a project. Optionally search by filename.',
|
||||||
|
parameters: ListFilesParams,
|
||||||
|
async execute(_id: string, params: any) {
|
||||||
|
const files = await ctx.trackerClient.listProjectFiles(params.project_slug, params.search);
|
||||||
|
if (files.length === 0) return ok('No files found.');
|
||||||
|
const list = files.map((f: any) => `- ${f.filename} (${f.size} bytes) ${f.description ? `— ${f.description}` : ''} [id: ${f.id}]`).join('\n');
|
||||||
|
return ok(`${files.length} file(s):\n${list}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_project_file',
|
||||||
|
label: 'Get Project File Info',
|
||||||
|
description: 'Get metadata of a project file by ID.',
|
||||||
|
parameters: GetFileParams,
|
||||||
|
async execute(_id: string, params: any) {
|
||||||
|
const file = await ctx.trackerClient.getProjectFile(params.project_slug, params.file_id);
|
||||||
|
return ok(JSON.stringify(file, null, 2));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'download_project_file',
|
||||||
|
label: 'Download Project File',
|
||||||
|
description: 'Download the content of a project file (returns text content).',
|
||||||
|
parameters: DownloadFileParams,
|
||||||
|
async execute(_id: string, params: any) {
|
||||||
|
const content = await ctx.trackerClient.downloadProjectFile(params.project_slug, params.file_id);
|
||||||
|
return ok(content);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'upload_project_file',
|
||||||
|
label: 'Upload Project File',
|
||||||
|
description: 'Upload a file to project. Content must be base64 encoded. If file with same name exists, it will be updated.',
|
||||||
|
parameters: UploadFileParams,
|
||||||
|
async execute(_id: string, params: any) {
|
||||||
|
const result = await ctx.trackerClient.uploadProjectFile(
|
||||||
|
params.project_slug,
|
||||||
|
params.filename,
|
||||||
|
params.content,
|
||||||
|
params.description,
|
||||||
|
);
|
||||||
|
return ok(`Uploaded: ${(result as any).filename} (${(result as any).size} bytes)`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'update_file_description',
|
||||||
|
label: 'Update File Description',
|
||||||
|
description: 'Update the description of a project file.',
|
||||||
|
parameters: UpdateFileParams,
|
||||||
|
async execute(_id: string, params: any) {
|
||||||
|
await ctx.trackerClient.updateProjectFile(params.project_slug, params.file_id, { description: params.description });
|
||||||
|
return ok('Description updated.');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete_project_file',
|
||||||
|
label: 'Delete Project File',
|
||||||
|
description: 'Delete a project file from disk and database.',
|
||||||
|
parameters: DeleteFileParams,
|
||||||
|
async execute(_id: string, params: any) {
|
||||||
|
await ctx.trackerClient.deleteProjectFile(params.project_slug, params.file_id);
|
||||||
|
return ok('File deleted.');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import { createStepTools } from './steps.js';
|
|||||||
import { createMessageTools } from './messages.js';
|
import { createMessageTools } from './messages.js';
|
||||||
import { createProjectTools } from './projects.js';
|
import { createProjectTools } from './projects.js';
|
||||||
import { createMemberTools } from './members.js';
|
import { createMemberTools } from './members.js';
|
||||||
|
import { createFileTools } from './files.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create all Team Board tracker tools for the agent.
|
* Create all Team Board tracker tools for the agent.
|
||||||
@ -17,6 +18,7 @@ export function createTrackerTools(ctx: ToolContext): ToolDefinition[] {
|
|||||||
...createMessageTools(ctx),
|
...createMessageTools(ctx),
|
||||||
...createProjectTools(ctx),
|
...createProjectTools(ctx),
|
||||||
...createMemberTools(ctx),
|
...createMemberTools(ctx),
|
||||||
|
...createFileTools(ctx),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -111,15 +111,51 @@ export class TrackerClient {
|
|||||||
return this.request('GET', `/api/v1/messages?${qs}`);
|
return this.request('GET', `/api/v1/messages?${qs}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Files (Attachments) ---
|
// --- Project Files ---
|
||||||
// Files are attachments on Messages. To attach a file to a task,
|
|
||||||
// send a message with task_id + attachment.
|
|
||||||
// Upload endpoint: POST /api/v1/messages/{id}/attachments (TODO in Tracker)
|
|
||||||
// Download endpoint: GET /api/v1/attachments/{id} (TODO in Tracker)
|
|
||||||
|
|
||||||
// async uploadAttachment(messageId: string, filename: string, data: Buffer): Promise<Record<string, unknown>> {
|
async listProjectFiles(projectSlug: string, search?: string): Promise<Record<string, unknown>[]> {
|
||||||
// return this.request('POST', `/api/v1/messages/${messageId}/attachments`, ...);
|
const qs = search ? `?search=${encodeURIComponent(search)}` : '';
|
||||||
// }
|
return this.request('GET', `/api/v1/projects/${projectSlug}/files${qs}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectFile(projectSlug: string, fileId: string): Promise<Record<string, unknown>> {
|
||||||
|
return this.request('GET', `/api/v1/projects/${projectSlug}/files/${fileId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadProjectFile(projectSlug: string, fileId: string): Promise<string> {
|
||||||
|
const url = `${this.baseUrl}/api/v1/projects/${projectSlug}/files/${fileId}/download`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { 'Authorization': `Bearer ${this.token}` },
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
||||||
|
return res.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadProjectFile(projectSlug: string, filename: string, content: string, description?: string): Promise<Record<string, unknown>> {
|
||||||
|
const url = `${this.baseUrl}/api/v1/projects/${projectSlug}/files`;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', new Blob([Buffer.from(content, 'base64')]), filename);
|
||||||
|
if (description) formData.append('description', description);
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Authorization': `Bearer ${this.token}` },
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => '');
|
||||||
|
throw new Error(`Upload failed: ${res.status} ${text}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProjectFile(projectSlug: string, fileId: string, data: { description?: string }): Promise<Record<string, unknown>> {
|
||||||
|
return this.request('PATCH', `/api/v1/projects/${projectSlug}/files/${fileId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteProjectFile(projectSlug: string, fileId: string): Promise<void> {
|
||||||
|
await this.request('DELETE', `/api/v1/projects/${projectSlug}/files/${fileId}`);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Projects ---
|
// --- Projects ---
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user