viviscape-mcp 2.1.0 → 2.2.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/README.md +3 -1
- package/dist/api-client.d.ts +57 -0
- package/dist/api-client.js +212 -0
- package/dist/index.js +157 -1
- package/dist/projection.d.ts +8 -0
- package/dist/projection.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -111,7 +111,9 @@ The server exposes tools across these domains:
|
|
|
111
111
|
- **Project writes** - `project_add`, `project_update`, `project_set_status`, `project_user_add`, `project_user_remove`
|
|
112
112
|
- **Reference** - `enums` (the status, priority, and group-type values the platform accepts)
|
|
113
113
|
- **Time logs** - `timelog_add`, `timelog_update`
|
|
114
|
-
- **Notes** - `note_add`, `note_get`, `note_update`, `note_remove`, `notes_mine`, `notes_query`
|
|
114
|
+
- **Notes** - `note_add`, `note_get`, `note_update`, `note_remove`, `notes_mine`, `notes_query`, `notes_account`, `note_revisions`
|
|
115
|
+
- **Note filing** - `note_companies`, `note_users`, `note_tags`, `note_attachments`, `notebook`, `notebook_users` (each takes an `action`)
|
|
116
|
+
- **Files** - `project_files` (list/upload/update/delete/download), `task_files`
|
|
115
117
|
- **Insights** - hours by person/service/project, AI summary, person stats, time totals
|
|
116
118
|
- **Account** - `account_info`, `account_services`, `account_users`
|
|
117
119
|
- **Auth** - `auth_login`, `auth_status`, `auth_logout`
|
package/dist/api-client.d.ts
CHANGED
|
@@ -21,6 +21,16 @@ export declare class ViviScapeClient {
|
|
|
21
21
|
get session(): Credentials;
|
|
22
22
|
private get ctx();
|
|
23
23
|
private request;
|
|
24
|
+
/**
|
|
25
|
+
* Shared response handling for the binary upload paths, which cannot go
|
|
26
|
+
* through request() because that always sends JSON.
|
|
27
|
+
*/
|
|
28
|
+
private send;
|
|
29
|
+
private authHeaders;
|
|
30
|
+
/** multipart/form-data upload; fetch sets the boundary, so no Content-Type here. */
|
|
31
|
+
private postMultipart;
|
|
32
|
+
/** Raw-body upload with metadata in headers (the groups/file/add contract). */
|
|
33
|
+
private postBytes;
|
|
24
34
|
private get;
|
|
25
35
|
private post;
|
|
26
36
|
addProspect(data: Record<string, unknown>): Promise<unknown>;
|
|
@@ -162,6 +172,53 @@ export declare class ViviScapeClient {
|
|
|
162
172
|
updateNote(data: Record<string, unknown>): Promise<unknown>;
|
|
163
173
|
removeNote(noteId: number | string): Promise<unknown>;
|
|
164
174
|
queryNotes(data: Record<string, unknown>): Promise<unknown>;
|
|
175
|
+
getAccountNotes(companyId?: number, userId?: number): Promise<unknown>;
|
|
176
|
+
getNoteRevisions(noteId: string): Promise<unknown>;
|
|
177
|
+
getNoteCompanies(noteId: string): Promise<unknown>;
|
|
178
|
+
addNoteCompany(noteId: string, companyId: number): Promise<unknown>;
|
|
179
|
+
removeNoteCompany(assignmentId: string): Promise<unknown>;
|
|
180
|
+
getNoteUsers(noteId: string): Promise<unknown>;
|
|
181
|
+
addNoteUser(noteId: string, userId: number, permission?: string): Promise<unknown>;
|
|
182
|
+
updateNoteUser(assignmentId: string, noteId: string, userId: number, permission: string): Promise<unknown>;
|
|
183
|
+
removeNoteUser(assignmentId: string): Promise<unknown>;
|
|
184
|
+
getNoteTags(noteId: string): Promise<unknown>;
|
|
185
|
+
addNoteTag(noteId: string, tag: string): Promise<unknown>;
|
|
186
|
+
removeNoteTag(tagId: string): Promise<unknown>;
|
|
187
|
+
getNoteAttachments(noteId: string): Promise<unknown>;
|
|
188
|
+
/** Uploads a local file as a note attachment. knowledge=true marks it for extraction. */
|
|
189
|
+
uploadNoteAttachment(noteId: string, filePath: string, knowledge?: boolean): Promise<unknown>;
|
|
190
|
+
removeNoteAttachment(attachmentId: string): Promise<unknown>;
|
|
191
|
+
/** Flag an attachment as knowledge-base material (drives text extraction). */
|
|
192
|
+
setNoteAttachmentKnowledge(attachment: Record<string, unknown>): Promise<unknown>;
|
|
193
|
+
getNotebooks(userId?: number): Promise<unknown>;
|
|
194
|
+
getNotebookNotes(bookId: string): Promise<unknown>;
|
|
195
|
+
addNotebook(name: string, description?: string): Promise<unknown>;
|
|
196
|
+
updateNotebook(bookId: string, changes: Record<string, unknown>): Promise<unknown>;
|
|
197
|
+
removeNotebook(bookId: string): Promise<unknown>;
|
|
198
|
+
/** Add or remove notes from a notebook in one call. */
|
|
199
|
+
assignNotesToBook(bookId: string, noteIds: string[], assign?: boolean): Promise<unknown>;
|
|
200
|
+
getNotebookUsers(bookId: string): Promise<unknown>;
|
|
201
|
+
addNotebookUser(bookId: string, userId: number, permission?: string): Promise<unknown>;
|
|
202
|
+
updateNotebookUser(assignmentId: string, bookId: string, userId: number, permission: string): Promise<unknown>;
|
|
203
|
+
removeNotebookUser(assignmentId: string): Promise<unknown>;
|
|
204
|
+
getProjectFiles(projectId: number, userId?: number): Promise<unknown>;
|
|
205
|
+
getTaskFiles(taskId: number): Promise<unknown>;
|
|
206
|
+
/**
|
|
207
|
+
* Upload a file to a project. This route takes the raw bytes as the body and
|
|
208
|
+
* every piece of metadata as a header, and the handler reads each header
|
|
209
|
+
* without a null check -- so all of them must be sent, even when empty.
|
|
210
|
+
*/
|
|
211
|
+
uploadProjectFile(opts: {
|
|
212
|
+
project_id: number;
|
|
213
|
+
file_path: string;
|
|
214
|
+
title?: string;
|
|
215
|
+
task_id?: number;
|
|
216
|
+
notify?: boolean;
|
|
217
|
+
}): Promise<unknown>;
|
|
218
|
+
updateProjectFile(file: Record<string, unknown>): Promise<unknown>;
|
|
219
|
+
deleteProjectFile(fileId: number): Promise<unknown>;
|
|
220
|
+
/** Returns whatever the download route serves; usually a redirect target or URL. */
|
|
221
|
+
downloadProjectFile(fileId: number): Promise<unknown>;
|
|
165
222
|
getAccountInfo(): Promise<unknown>;
|
|
166
223
|
getActiveServices(): Promise<unknown>;
|
|
167
224
|
getAllServices(): Promise<unknown>;
|
package/dist/api-client.js
CHANGED
|
@@ -255,6 +255,51 @@ export class ViviScapeClient {
|
|
|
255
255
|
return text;
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Shared response handling for the binary upload paths, which cannot go
|
|
260
|
+
* through request() because that always sends JSON.
|
|
261
|
+
*/
|
|
262
|
+
async send(method, path, init) {
|
|
263
|
+
requireFeature(featureForPath(path), this.creds);
|
|
264
|
+
const url = `${this.baseUrl}${API_PREFIX}/${path.replace(/^\//, '')}`;
|
|
265
|
+
const res = await fetch(url, { method, redirect: 'manual', ...init });
|
|
266
|
+
const text = await res.text();
|
|
267
|
+
if (res.status === 401 || res.status === 403)
|
|
268
|
+
throw new SessionExpiredError();
|
|
269
|
+
if (res.status >= 300 && res.status < 400)
|
|
270
|
+
throw new SessionExpiredError();
|
|
271
|
+
if (!res.ok) {
|
|
272
|
+
const snippet = text.length > 400 ? `${text.slice(0, 400)}...` : text;
|
|
273
|
+
throw new ApiError(res.status, `API ${method} ${path} returned ${res.status}: ${snippet}`);
|
|
274
|
+
}
|
|
275
|
+
if (!text.trim())
|
|
276
|
+
return null;
|
|
277
|
+
try {
|
|
278
|
+
return JSON.parse(text);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return text;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
authHeaders(extra = {}) {
|
|
285
|
+
return {
|
|
286
|
+
'Authorization': `Bearer ${this.creds.access_token}`,
|
|
287
|
+
'user_id': String(this.creds.user_id),
|
|
288
|
+
'pid': String(this.creds.pid),
|
|
289
|
+
...extra,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/** multipart/form-data upload; fetch sets the boundary, so no Content-Type here. */
|
|
293
|
+
async postMultipart(path, form) {
|
|
294
|
+
return this.send('POST', path, { headers: this.authHeaders(), body: form });
|
|
295
|
+
}
|
|
296
|
+
/** Raw-body upload with metadata in headers (the groups/file/add contract). */
|
|
297
|
+
async postBytes(path, bytes, extra) {
|
|
298
|
+
return this.send('POST', path, {
|
|
299
|
+
headers: this.authHeaders({ 'Content-Type': 'application/octet-stream', ...extra }),
|
|
300
|
+
body: bytes,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
258
303
|
get(path) {
|
|
259
304
|
return this.request('GET', path);
|
|
260
305
|
}
|
|
@@ -683,6 +728,173 @@ export class ViviScapeClient {
|
|
|
683
728
|
module: 'notes',
|
|
684
729
|
});
|
|
685
730
|
}
|
|
731
|
+
// -- Note relations, tags, revisions -------------------------
|
|
732
|
+
async getAccountNotes(companyId = 0, userId = 0) {
|
|
733
|
+
return this.post('notes/account', {
|
|
734
|
+
account_id: this.accountId,
|
|
735
|
+
company_id: companyId,
|
|
736
|
+
user_id: userId,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
async getNoteRevisions(noteId) {
|
|
740
|
+
return this.get(`note/revisions/${noteId}`);
|
|
741
|
+
}
|
|
742
|
+
async getNoteCompanies(noteId) {
|
|
743
|
+
return this.get(`note/companies/${noteId}`);
|
|
744
|
+
}
|
|
745
|
+
async addNoteCompany(noteId, companyId) {
|
|
746
|
+
return this.post('note/company/add', {
|
|
747
|
+
assignment_id: EMPTY_GUID,
|
|
748
|
+
note_id: noteId,
|
|
749
|
+
assign_user_id: this.userId,
|
|
750
|
+
company_id: companyId,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
async removeNoteCompany(assignmentId) {
|
|
754
|
+
return this.get(`note/company/remove/${assignmentId}`);
|
|
755
|
+
}
|
|
756
|
+
async getNoteUsers(noteId) {
|
|
757
|
+
return this.get(`note/users/${noteId}`);
|
|
758
|
+
}
|
|
759
|
+
async addNoteUser(noteId, userId, permission = 'read') {
|
|
760
|
+
return this.post('note/user/add', {
|
|
761
|
+
assignment_id: EMPTY_GUID,
|
|
762
|
+
note_id: noteId,
|
|
763
|
+
user_id: userId,
|
|
764
|
+
permission,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
async updateNoteUser(assignmentId, noteId, userId, permission) {
|
|
768
|
+
return this.post('note/user/update', {
|
|
769
|
+
assignment_id: assignmentId,
|
|
770
|
+
note_id: noteId,
|
|
771
|
+
user_id: userId,
|
|
772
|
+
permission,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
async removeNoteUser(assignmentId) {
|
|
776
|
+
return this.get(`note/user/remove/${assignmentId}`);
|
|
777
|
+
}
|
|
778
|
+
async getNoteTags(noteId) {
|
|
779
|
+
return this.get(`note/tags/${noteId}`);
|
|
780
|
+
}
|
|
781
|
+
async addNoteTag(noteId, tag) {
|
|
782
|
+
return this.post('note/tags/add', { tag_id: EMPTY_GUID, note_id: noteId, tag });
|
|
783
|
+
}
|
|
784
|
+
async removeNoteTag(tagId) {
|
|
785
|
+
return this.get(`note/tag/remove/${tagId}`);
|
|
786
|
+
}
|
|
787
|
+
// -- Note attachments ----------------------------------------
|
|
788
|
+
async getNoteAttachments(noteId) {
|
|
789
|
+
return this.get(`note/attachments/${noteId}`);
|
|
790
|
+
}
|
|
791
|
+
/** Uploads a local file as a note attachment. knowledge=true marks it for extraction. */
|
|
792
|
+
async uploadNoteAttachment(noteId, filePath, knowledge = false) {
|
|
793
|
+
const { readFileSync } = await import('node:fs');
|
|
794
|
+
const { basename } = await import('node:path');
|
|
795
|
+
const form = new FormData();
|
|
796
|
+
form.append('knowledge', String(knowledge));
|
|
797
|
+
form.append('file', new Blob([readFileSync(filePath)]), basename(filePath));
|
|
798
|
+
return this.postMultipart(`note/attachment/upload/${noteId}`, form);
|
|
799
|
+
}
|
|
800
|
+
async removeNoteAttachment(attachmentId) {
|
|
801
|
+
return this.get(`note/attachment/remove/${attachmentId}`);
|
|
802
|
+
}
|
|
803
|
+
/** Flag an attachment as knowledge-base material (drives text extraction). */
|
|
804
|
+
async setNoteAttachmentKnowledge(attachment) {
|
|
805
|
+
return this.post('note/attachment/knowledge', attachment);
|
|
806
|
+
}
|
|
807
|
+
// -- Notebooks -----------------------------------------------
|
|
808
|
+
async getNotebooks(userId = 0) {
|
|
809
|
+
return this.get(`note/books/user/${userId || this.userId}`);
|
|
810
|
+
}
|
|
811
|
+
async getNotebookNotes(bookId) {
|
|
812
|
+
return this.get(`note/book/notes/${bookId}`);
|
|
813
|
+
}
|
|
814
|
+
async addNotebook(name, description = '') {
|
|
815
|
+
return this.post('note/book/add', {
|
|
816
|
+
book_id: EMPTY_GUID,
|
|
817
|
+
name,
|
|
818
|
+
description,
|
|
819
|
+
creator_id: this.userId,
|
|
820
|
+
account_id: this.accountId,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
async updateNotebook(bookId, changes) {
|
|
824
|
+
return this.post('note/book/update', {
|
|
825
|
+
book_id: bookId,
|
|
826
|
+
creator_id: this.userId,
|
|
827
|
+
account_id: this.accountId,
|
|
828
|
+
...changes,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
async removeNotebook(bookId) {
|
|
832
|
+
return this.get(`note/book/remove/${bookId}`);
|
|
833
|
+
}
|
|
834
|
+
/** Add or remove notes from a notebook in one call. */
|
|
835
|
+
async assignNotesToBook(bookId, noteIds, assign = true) {
|
|
836
|
+
return this.post('note/book/assign', { book_id: bookId, note_ids: noteIds, assign });
|
|
837
|
+
}
|
|
838
|
+
async getNotebookUsers(bookId) {
|
|
839
|
+
return this.get(`note/book/users/${bookId}`);
|
|
840
|
+
}
|
|
841
|
+
async addNotebookUser(bookId, userId, permission = 'read') {
|
|
842
|
+
return this.post('note/book/user/add', {
|
|
843
|
+
assignment_id: EMPTY_GUID,
|
|
844
|
+
book_id: bookId,
|
|
845
|
+
user_id: userId,
|
|
846
|
+
permission,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
async updateNotebookUser(assignmentId, bookId, userId, permission) {
|
|
850
|
+
return this.post('note/book/user/update', {
|
|
851
|
+
assignment_id: assignmentId,
|
|
852
|
+
book_id: bookId,
|
|
853
|
+
user_id: userId,
|
|
854
|
+
permission,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
async removeNotebookUser(assignmentId) {
|
|
858
|
+
return this.get(`note/book/user/remove/${assignmentId}`);
|
|
859
|
+
}
|
|
860
|
+
// -- Project and task files ----------------------------------
|
|
861
|
+
async getProjectFiles(projectId, userId = 0) {
|
|
862
|
+
return this.get(`groups/files?groupid=${projectId}&userid=${userId || this.userId}`);
|
|
863
|
+
}
|
|
864
|
+
async getTaskFiles(taskId) {
|
|
865
|
+
return this.get(`groups/task/files?taskid=${taskId}`);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Upload a file to a project. This route takes the raw bytes as the body and
|
|
869
|
+
* every piece of metadata as a header, and the handler reads each header
|
|
870
|
+
* without a null check -- so all of them must be sent, even when empty.
|
|
871
|
+
*/
|
|
872
|
+
async uploadProjectFile(opts) {
|
|
873
|
+
const { readFileSync } = await import('node:fs');
|
|
874
|
+
const { basename } = await import('node:path');
|
|
875
|
+
const name = basename(opts.file_path);
|
|
876
|
+
return this.postBytes('groups/file/add', readFileSync(opts.file_path), {
|
|
877
|
+
userid: String(this.userId),
|
|
878
|
+
groupid: String(opts.project_id),
|
|
879
|
+
filename: name,
|
|
880
|
+
title: opts.title || name,
|
|
881
|
+
youtube: '',
|
|
882
|
+
notify: String(opts.notify ?? false),
|
|
883
|
+
taskid: String(opts.task_id ?? 0),
|
|
884
|
+
lat: '0',
|
|
885
|
+
lng: '0',
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
async updateProjectFile(file) {
|
|
889
|
+
return this.post('groups/file/update', file);
|
|
890
|
+
}
|
|
891
|
+
async deleteProjectFile(fileId) {
|
|
892
|
+
return this.get(`groups/file/delete?fileid=${fileId}`);
|
|
893
|
+
}
|
|
894
|
+
/** Returns whatever the download route serves; usually a redirect target or URL. */
|
|
895
|
+
async downloadProjectFile(fileId) {
|
|
896
|
+
return this.get(`groups/file/download/${fileId}`);
|
|
897
|
+
}
|
|
686
898
|
// -- Account ------------------------------------------------
|
|
687
899
|
async getAccountInfo() {
|
|
688
900
|
return this.get(`account/id/${this.accountId}`);
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { ViviScapeClient } from './api-client.js';
|
|
|
7
7
|
import { AuthService } from './auth/auth-service.js';
|
|
8
8
|
import { describe } from './auth/credentials.js';
|
|
9
9
|
import { PRIORITIES, PROJECT_STATUSES, PROSPECT_STATUSES, TASK_STATUSES, enumReference, } from './enums.js';
|
|
10
|
-
import { PROJECT_FIELDS, TASK_FIELDS, filterTasks, shape, } from './projection.js';
|
|
10
|
+
import { FILE_FIELDS, NOTE_FIELDS, PROJECT_FIELDS, TASK_FIELDS, filterTasks, shape, } from './projection.js';
|
|
11
11
|
import { baseUrl } from './config.js';
|
|
12
12
|
// quiet: dotenv's banner goes to stdout, which is the MCP protocol channel.
|
|
13
13
|
config({ quiet: true });
|
|
@@ -443,6 +443,162 @@ server.tool('projects_active_by_user', 'Get active projects for a specific user.
|
|
|
443
443
|
return { content: [{ type: 'text', text: json(shape(result, PROJECT_FIELDS, paging(params))) }] };
|
|
444
444
|
});
|
|
445
445
|
// ============================================================
|
|
446
|
+
// DOCUMENTATION: NOTE RELATIONS, TAGS, REVISIONS, ATTACHMENTS
|
|
447
|
+
// ============================================================
|
|
448
|
+
//
|
|
449
|
+
// These tools take an `action` rather than splitting every verb into its own
|
|
450
|
+
// tool: the server already registers ~90 tools, and past roughly 100 the tool
|
|
451
|
+
// list itself starts costing more than it returns. Each action names exactly
|
|
452
|
+
// which arguments it needs, and a missing one is reported by name.
|
|
453
|
+
/** Fail with a message that names the action and the argument it wanted. */
|
|
454
|
+
function need(action, name, value) {
|
|
455
|
+
if (value === undefined || value === null || value === '') {
|
|
456
|
+
throw new Error(`action "${action}" requires ${name}`);
|
|
457
|
+
}
|
|
458
|
+
return value;
|
|
459
|
+
}
|
|
460
|
+
server.tool('notes_account', 'List notes across the account, newest-first as the API returns them. Bodies are omitted by default (hundreds of rows); pass fields:["note_id","title","note"] to read content.', {
|
|
461
|
+
company_id: z.number().optional().describe('Restrict to notes linked to this company'),
|
|
462
|
+
user_id: z.number().optional().describe('Restrict to notes for this user'),
|
|
463
|
+
...pageArgs,
|
|
464
|
+
}, async (params) => {
|
|
465
|
+
const result = await requireClient().getAccountNotes(params.company_id ?? 0, params.user_id ?? 0);
|
|
466
|
+
return { content: [{ type: 'text', text: json(shape(result, NOTE_FIELDS, paging(params))) }] };
|
|
467
|
+
});
|
|
468
|
+
server.tool('note_revisions', 'Get the edit history of a note. Use before rewriting a shared note so human edits are not silently clobbered.', { note_id: z.string().describe('Note ID (GUID)') }, async ({ note_id }) => {
|
|
469
|
+
const result = await requireClient().getNoteRevisions(note_id);
|
|
470
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
471
|
+
});
|
|
472
|
+
server.tool('note_companies', 'Link a note to a company, list its links, or remove one. This is how a generated document gets filed against a client. NOTE: add returns an echo with zeroed ids, so call list afterward to get the assignment_id you need for remove.', {
|
|
473
|
+
action: z.enum(['list', 'add', 'remove']).describe('What to do'),
|
|
474
|
+
note_id: z.string().optional().describe('Note ID (list, add)'),
|
|
475
|
+
company_id: z.number().optional().describe('Company ID (add)'),
|
|
476
|
+
assignment_id: z.string().optional().describe('Link ID from list (remove)'),
|
|
477
|
+
}, async ({ action, note_id, company_id, assignment_id }) => {
|
|
478
|
+
const c = requireClient();
|
|
479
|
+
const result = action === 'list' ? await c.getNoteCompanies(need(action, 'note_id', note_id))
|
|
480
|
+
: action === 'add' ? await c.addNoteCompany(need(action, 'note_id', note_id), need(action, 'company_id', company_id))
|
|
481
|
+
: await c.removeNoteCompany(need(action, 'assignment_id', assignment_id));
|
|
482
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
483
|
+
});
|
|
484
|
+
server.tool('note_users', 'Share a note with a user, list who it is shared with, change a permission, or unshare. NOTE: add returns an echo with zeroed ids; call list to get the assignment_id.', {
|
|
485
|
+
action: z.enum(['list', 'add', 'update', 'remove']).describe('What to do'),
|
|
486
|
+
note_id: z.string().optional().describe('Note ID (list, add, update)'),
|
|
487
|
+
user_id: z.number().optional().describe('User ID (add, update)'),
|
|
488
|
+
permission: z.string().optional().describe('Permission, e.g. read or write (add, update)'),
|
|
489
|
+
assignment_id: z.string().optional().describe('Share ID from list (update, remove)'),
|
|
490
|
+
}, async ({ action, note_id, user_id, permission, assignment_id }) => {
|
|
491
|
+
const c = requireClient();
|
|
492
|
+
const result = action === 'list' ? await c.getNoteUsers(need(action, 'note_id', note_id))
|
|
493
|
+
: action === 'add' ? await c.addNoteUser(need(action, 'note_id', note_id), need(action, 'user_id', user_id), permission || 'read')
|
|
494
|
+
: action === 'update' ? await c.updateNoteUser(need(action, 'assignment_id', assignment_id), need(action, 'note_id', note_id), need(action, 'user_id', user_id), need(action, 'permission', permission))
|
|
495
|
+
: await c.removeNoteUser(need(action, 'assignment_id', assignment_id));
|
|
496
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
497
|
+
});
|
|
498
|
+
server.tool('note_tags', 'List, add, or remove tags on a note -- the only way to categorise notes for later retrieval. NOTE: remove answers false even when it succeeds; re-list to confirm.', {
|
|
499
|
+
action: z.enum(['list', 'add', 'remove']).describe('What to do'),
|
|
500
|
+
note_id: z.string().optional().describe('Note ID (list, add)'),
|
|
501
|
+
tag: z.string().optional().describe('Tag text (add)'),
|
|
502
|
+
tag_id: z.string().optional().describe('Tag ID from list (remove)'),
|
|
503
|
+
}, async ({ action, note_id, tag, tag_id }) => {
|
|
504
|
+
const c = requireClient();
|
|
505
|
+
const result = action === 'list' ? await c.getNoteTags(need(action, 'note_id', note_id))
|
|
506
|
+
: action === 'add' ? await c.addNoteTag(need(action, 'note_id', note_id), need(action, 'tag', tag))
|
|
507
|
+
: await c.removeNoteTag(need(action, 'tag_id', tag_id));
|
|
508
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
509
|
+
});
|
|
510
|
+
server.tool('note_attachments', 'List, upload, or remove note attachments, or flag one as knowledge-base material. upload takes a path to a local file.', {
|
|
511
|
+
action: z.enum(['list', 'upload', 'remove', 'set_knowledge']).describe('What to do'),
|
|
512
|
+
note_id: z.string().optional().describe('Note ID (list, upload)'),
|
|
513
|
+
file_path: z.string().optional().describe('Local file to upload (upload)'),
|
|
514
|
+
knowledge: z.boolean().optional().describe('Mark for text extraction (upload, set_knowledge)'),
|
|
515
|
+
attachment_id: z.string().optional().describe('Attachment ID from list (remove, set_knowledge)'),
|
|
516
|
+
}, async ({ action, note_id, file_path, knowledge, attachment_id }) => {
|
|
517
|
+
const c = requireClient();
|
|
518
|
+
const result = action === 'list' ? await c.getNoteAttachments(need(action, 'note_id', note_id))
|
|
519
|
+
: action === 'upload' ? await c.uploadNoteAttachment(need(action, 'note_id', note_id), need(action, 'file_path', file_path), knowledge ?? false)
|
|
520
|
+
: action === 'remove' ? await c.removeNoteAttachment(need(action, 'attachment_id', attachment_id))
|
|
521
|
+
: await c.setNoteAttachmentKnowledge({
|
|
522
|
+
attachment_id: need(action, 'attachment_id', attachment_id),
|
|
523
|
+
note_id: note_id ?? undefined,
|
|
524
|
+
knowledge: knowledge ?? true,
|
|
525
|
+
});
|
|
526
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
527
|
+
});
|
|
528
|
+
server.tool('notebook', 'Notebooks group notes into a per-client or per-project documentation space. Actions: list, add, update, remove, notes (list a book contents), assign / unassign notes. WARNING: remove is broken server-side (400 even for an empty book) -- delete notebooks in the ViviScape Work UI.', {
|
|
529
|
+
action: z.enum(['list', 'add', 'update', 'remove', 'notes', 'assign', 'unassign']).describe('What to do'),
|
|
530
|
+
book_id: z.string().optional().describe('Notebook ID (update, remove, notes, assign, unassign)'),
|
|
531
|
+
name: z.string().optional().describe('Notebook name (add, update)'),
|
|
532
|
+
description: z.string().optional().describe('Notebook description (add, update)'),
|
|
533
|
+
note_ids: z.array(z.string()).optional().describe('Note IDs (assign, unassign)'),
|
|
534
|
+
user_id: z.number().optional().describe('Owner to list for (list; defaults to signed-in user)'),
|
|
535
|
+
}, async ({ action, book_id, name, description, note_ids, user_id }) => {
|
|
536
|
+
const c = requireClient();
|
|
537
|
+
const result = action === 'list' ? await c.getNotebooks(user_id ?? 0)
|
|
538
|
+
: action === 'add' ? await c.addNotebook(need(action, 'name', name), description || '')
|
|
539
|
+
: action === 'update' ? await c.updateNotebook(need(action, 'book_id', book_id), {
|
|
540
|
+
...(name ? { name } : {}),
|
|
541
|
+
...(description !== undefined ? { description } : {}),
|
|
542
|
+
})
|
|
543
|
+
: action === 'remove' ? await c.removeNotebook(need(action, 'book_id', book_id))
|
|
544
|
+
: action === 'notes' ? await c.getNotebookNotes(need(action, 'book_id', book_id))
|
|
545
|
+
: await c.assignNotesToBook(need(action, 'book_id', book_id), need(action, 'note_ids', note_ids), action === 'assign');
|
|
546
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
547
|
+
});
|
|
548
|
+
server.tool('notebook_users', 'List, add, update, or remove the users a notebook is shared with', {
|
|
549
|
+
action: z.enum(['list', 'add', 'update', 'remove']).describe('What to do'),
|
|
550
|
+
book_id: z.string().optional().describe('Notebook ID (list, add, update)'),
|
|
551
|
+
user_id: z.number().optional().describe('User ID (add, update)'),
|
|
552
|
+
permission: z.string().optional().describe('Permission, e.g. read, write, owner (add, update)'),
|
|
553
|
+
assignment_id: z.string().optional().describe('Share ID from list (update, remove)'),
|
|
554
|
+
}, async ({ action, book_id, user_id, permission, assignment_id }) => {
|
|
555
|
+
const c = requireClient();
|
|
556
|
+
const result = action === 'list' ? await c.getNotebookUsers(need(action, 'book_id', book_id))
|
|
557
|
+
: action === 'add' ? await c.addNotebookUser(need(action, 'book_id', book_id), need(action, 'user_id', user_id), permission || 'read')
|
|
558
|
+
: action === 'update' ? await c.updateNotebookUser(need(action, 'assignment_id', assignment_id), need(action, 'book_id', book_id), need(action, 'user_id', user_id), need(action, 'permission', permission))
|
|
559
|
+
: await c.removeNotebookUser(need(action, 'assignment_id', assignment_id));
|
|
560
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
561
|
+
});
|
|
562
|
+
// ============================================================
|
|
563
|
+
// PROJECT AND TASK FILES
|
|
564
|
+
// ============================================================
|
|
565
|
+
server.tool('project_files', 'List, upload, update, delete, or resolve the download URL of files attached to a project. upload takes a path to a local file; delete is by file_id from list.', {
|
|
566
|
+
action: z.enum(['list', 'upload', 'update', 'delete', 'download']).describe('What to do'),
|
|
567
|
+
project_id: z.number().optional().describe('Project ID (list, upload)'),
|
|
568
|
+
file_path: z.string().optional().describe('Local file to upload (upload)'),
|
|
569
|
+
title: z.string().optional().describe('Title for the uploaded file (upload; defaults to the filename)'),
|
|
570
|
+
task_id: z.number().optional().describe('Attach the upload to a task as well (upload)'),
|
|
571
|
+
notify: z.boolean().optional().describe('Notify project members (upload; default false)'),
|
|
572
|
+
file_id: z.number().optional().describe('File ID from list (delete, download)'),
|
|
573
|
+
file: z.record(z.string(), z.unknown()).optional().describe('Full file record with changes applied (update)'),
|
|
574
|
+
...pageArgs,
|
|
575
|
+
}, async (params) => {
|
|
576
|
+
const c = requireClient();
|
|
577
|
+
const { action } = params;
|
|
578
|
+
if (action === 'list') {
|
|
579
|
+
const result = await c.getProjectFiles(need(action, 'project_id', params.project_id));
|
|
580
|
+
return { content: [{ type: 'text', text: json(shape(result, FILE_FIELDS, paging(params))) }] };
|
|
581
|
+
}
|
|
582
|
+
const result = action === 'upload' ? await c.uploadProjectFile({
|
|
583
|
+
project_id: need(action, 'project_id', params.project_id),
|
|
584
|
+
file_path: need(action, 'file_path', params.file_path),
|
|
585
|
+
title: params.title,
|
|
586
|
+
task_id: params.task_id,
|
|
587
|
+
notify: params.notify,
|
|
588
|
+
})
|
|
589
|
+
: action === 'update' ? await c.updateProjectFile(need(action, 'file', params.file))
|
|
590
|
+
: action === 'delete' ? await c.deleteProjectFile(need(action, 'file_id', params.file_id))
|
|
591
|
+
: await c.downloadProjectFile(need(action, 'file_id', params.file_id));
|
|
592
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
593
|
+
});
|
|
594
|
+
server.tool('task_files', 'List files attached to a task. Use this to read context humans already attached before generating anything new.', {
|
|
595
|
+
task_id: z.number().describe('Task ID'),
|
|
596
|
+
...pageArgs,
|
|
597
|
+
}, async (params) => {
|
|
598
|
+
const result = await requireClient().getTaskFiles(params.task_id);
|
|
599
|
+
return { content: [{ type: 'text', text: json(shape(result, FILE_FIELDS, paging(params))) }] };
|
|
600
|
+
});
|
|
601
|
+
// ============================================================
|
|
446
602
|
// REFERENCE
|
|
447
603
|
// ============================================================
|
|
448
604
|
server.tool('enums', 'List the status, priority, and group-type values the platform actually accepts. Read this before setting a status or priority; tasks complete as "completed" while projects complete as "complete", and priority is low/moderate/important/urgent/critical.', {}, async () => ({ content: [{ type: 'text', text: json(enumReference()) }] }));
|
package/dist/projection.d.ts
CHANGED
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
export declare const TASK_FIELDS: string[];
|
|
13
13
|
/** Project (group) fields worth returning by default, of 87 available. */
|
|
14
14
|
export declare const PROJECT_FIELDS: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Note fields worth returning by default. Excludes `note` (the full body) and
|
|
17
|
+
* `firefly_transcript_id`: the account list runs to hundreds of rows and the
|
|
18
|
+
* bodies dominate the payload. Ask for fields:["note"] to read one.
|
|
19
|
+
*/
|
|
20
|
+
export declare const NOTE_FIELDS: string[];
|
|
21
|
+
/** Project/task file fields worth returning by default. */
|
|
22
|
+
export declare const FILE_FIELDS: string[];
|
|
15
23
|
export declare const DEFAULT_LIMIT = 50;
|
|
16
24
|
export interface ShapeOpts {
|
|
17
25
|
fields?: string[];
|
package/dist/projection.js
CHANGED
|
@@ -22,6 +22,22 @@ export const PROJECT_FIELDS = [
|
|
|
22
22
|
'total_tasks', 'total_tasks_open', 'total_tasks_completed',
|
|
23
23
|
'target_date', 'complete_date', 'total_estimated_str', 'total_duration_str',
|
|
24
24
|
];
|
|
25
|
+
/**
|
|
26
|
+
* Note fields worth returning by default. Excludes `note` (the full body) and
|
|
27
|
+
* `firefly_transcript_id`: the account list runs to hundreds of rows and the
|
|
28
|
+
* bodies dominate the payload. Ask for fields:["note"] to read one.
|
|
29
|
+
*/
|
|
30
|
+
export const NOTE_FIELDS = [
|
|
31
|
+
'note_id', 'title', 'book_id', 'creator', 'creator_id',
|
|
32
|
+
'summary', 'tags', 'assigned_users', 'knowledge', 'is_public',
|
|
33
|
+
'last_update', 'create_date',
|
|
34
|
+
];
|
|
35
|
+
/** Project/task file fields worth returning by default. */
|
|
36
|
+
export const FILE_FIELDS = [
|
|
37
|
+
'file_id', 'group_id', 'task_id', 'title', 'file_name', 'file_type',
|
|
38
|
+
'file_size_friendly', 'author', 'user_id', 'post_date', 'url_path',
|
|
39
|
+
'is_public', 'is_private',
|
|
40
|
+
];
|
|
25
41
|
export const DEFAULT_LIMIT = 50;
|
|
26
42
|
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
27
43
|
/** Parse the staff_assignees blob, which arrives as JSON text or as an array. */
|
package/package.json
CHANGED