viviscape-mcp 2.0.2 → 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 +43 -1
- package/dist/api-client.d.ts +151 -1
- package/dist/api-client.js +515 -71
- package/dist/auth/permissions.js +2 -0
- package/dist/enums.d.ts +48 -0
- package/dist/enums.js +67 -0
- package/dist/index.js +463 -34
- package/dist/projection.d.ts +47 -0
- package/dist/projection.js +132 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { config } from 'dotenv';
|
|
|
6
6
|
import { ViviScapeClient } from './api-client.js';
|
|
7
7
|
import { AuthService } from './auth/auth-service.js';
|
|
8
8
|
import { describe } from './auth/credentials.js';
|
|
9
|
+
import { PRIORITIES, PROJECT_STATUSES, PROSPECT_STATUSES, TASK_STATUSES, enumReference, } from './enums.js';
|
|
10
|
+
import { FILE_FIELDS, NOTE_FIELDS, PROJECT_FIELDS, TASK_FIELDS, filterTasks, shape, } from './projection.js';
|
|
9
11
|
import { baseUrl } from './config.js';
|
|
10
12
|
// quiet: dotenv's banner goes to stdout, which is the MCP protocol channel.
|
|
11
13
|
config({ quiet: true });
|
|
@@ -37,6 +39,39 @@ const server = new McpServer({
|
|
|
37
39
|
function json(data) {
|
|
38
40
|
return JSON.stringify(data, null, 2);
|
|
39
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Phase 2 (docs/AGENT-CAPABILITY-SPEC.md): list rows carry ~90 columns, so every
|
|
44
|
+
* list tool takes a page window and an optional field list. Defaults keep the
|
|
45
|
+
* response inside a tool-result budget; fields:["all"] opts out.
|
|
46
|
+
*/
|
|
47
|
+
const pageArgs = {
|
|
48
|
+
fields: z.array(z.string()).optional()
|
|
49
|
+
.describe('Columns to return. Omit for a PM-relevant default; ["all"] for every column.'),
|
|
50
|
+
limit: z.number().optional().describe('Max rows to return (default 50)'),
|
|
51
|
+
offset: z.number().optional().describe('Rows to skip, for paging (default 0)'),
|
|
52
|
+
};
|
|
53
|
+
const taskFilterArgs = {
|
|
54
|
+
company_id: z.number().optional().describe('Only tasks for this company'),
|
|
55
|
+
project_id: z.number().optional().describe('Only tasks in this project (group_id)'),
|
|
56
|
+
assignee_id: z.number().optional().describe('Only tasks assigned to this user id'),
|
|
57
|
+
status: z.string().optional().describe('Exact status match, e.g. new, inprogress'),
|
|
58
|
+
priority: z.string().optional().describe('Exact priority match, e.g. low, moderate, important, critical'),
|
|
59
|
+
due_before: z.string().optional().describe('Only tasks due on or before this date (ISO 8601)'),
|
|
60
|
+
due_after: z.string().optional().describe('Only tasks due on or after this date (ISO 8601)'),
|
|
61
|
+
search: z.string().optional().describe('Case-insensitive substring match on title, description, notes'),
|
|
62
|
+
};
|
|
63
|
+
// Variants that drop the key a given tool already takes as a required arg,
|
|
64
|
+
// so the spread cannot overwrite it with an optional one.
|
|
65
|
+
const { project_id: _omitProject, ...taskFilterArgsNoProject } = taskFilterArgs;
|
|
66
|
+
const { company_id: _omitCompany, ...taskFilterArgsNoCompany } = taskFilterArgs;
|
|
67
|
+
/** Split a tool's params into page options and everything else. */
|
|
68
|
+
function paging(p) {
|
|
69
|
+
return {
|
|
70
|
+
fields: p.fields,
|
|
71
|
+
limit: p.limit,
|
|
72
|
+
offset: p.offset,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
40
75
|
// ============================================================
|
|
41
76
|
// AUTH
|
|
42
77
|
// ============================================================
|
|
@@ -112,7 +147,7 @@ server.tool('prospect_add', 'Add a new prospect/lead to the ViviScape CRM', {
|
|
|
112
147
|
source: z.string().optional().describe('Lead source (e.g., "Website", "ROI Calculator")'),
|
|
113
148
|
source_url: z.string().optional().describe('URL where the lead came from'),
|
|
114
149
|
referred_by: z.string().optional().describe('Referral source'),
|
|
115
|
-
status: z.
|
|
150
|
+
status: z.enum(PROSPECT_STATUSES).optional().describe('Pipeline stage (platform values)'),
|
|
116
151
|
}, async (params) => {
|
|
117
152
|
const result = await requireClient().addProspect(params);
|
|
118
153
|
return { content: [{ type: 'text', text: json(result) }] };
|
|
@@ -127,7 +162,7 @@ server.tool('prospect_update', 'Update an existing prospect in the CRM', {
|
|
|
127
162
|
org: z.string().optional(),
|
|
128
163
|
note: z.string().optional(),
|
|
129
164
|
source: z.string().optional(),
|
|
130
|
-
status: z.
|
|
165
|
+
status: z.enum(PROSPECT_STATUSES).optional().describe('Pipeline stage (platform values)'),
|
|
131
166
|
priority: z.number().optional().describe('Priority level'),
|
|
132
167
|
}, async (params) => {
|
|
133
168
|
const result = await requireClient().updateProspect(params);
|
|
@@ -272,13 +307,17 @@ server.tool('clients_by_company', 'Get all clients for a company', { company_id:
|
|
|
272
307
|
// ============================================================
|
|
273
308
|
// PROJECTS & TASKS
|
|
274
309
|
// ============================================================
|
|
275
|
-
server.tool('project_list', 'Get all projects for current user
|
|
310
|
+
server.tool('project_list', 'Get all projects for current user. Returns a page of trimmed rows; see fields/limit/offset.', {
|
|
311
|
+
...pageArgs,
|
|
312
|
+
}, async (params) => {
|
|
276
313
|
const result = await requireClient().getMyProjects();
|
|
277
|
-
return { content: [{ type: 'text', text: json(result) }] };
|
|
314
|
+
return { content: [{ type: 'text', text: json(shape(result, PROJECT_FIELDS, paging(params))) }] };
|
|
278
315
|
});
|
|
279
|
-
server.tool('project_list_active', 'Get active projects for current user
|
|
316
|
+
server.tool('project_list_active', 'Get active projects for current user. Returns a page of trimmed rows; see fields/limit/offset.', {
|
|
317
|
+
...pageArgs,
|
|
318
|
+
}, async (params) => {
|
|
280
319
|
const result = await requireClient().getActiveProjects();
|
|
281
|
-
return { content: [{ type: 'text', text: json(result) }] };
|
|
320
|
+
return { content: [{ type: 'text', text: json(shape(result, PROJECT_FIELDS, paging(params))) }] };
|
|
282
321
|
});
|
|
283
322
|
server.tool('project_get', 'Get a project by ID', { project_id: z.number().describe('Project ID') }, async ({ project_id }) => {
|
|
284
323
|
const result = await requireClient().getProjectById(project_id);
|
|
@@ -288,23 +327,43 @@ server.tool('project_staff', 'Get staff members assigned to a project', { projec
|
|
|
288
327
|
const result = await requireClient().getProjectStaff(project_id);
|
|
289
328
|
return { content: [{ type: 'text', text: json(result) }] };
|
|
290
329
|
});
|
|
291
|
-
server.tool('project_tasks', 'Get all tasks for a project
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const result = await requireClient().
|
|
297
|
-
|
|
330
|
+
server.tool('project_tasks', 'Get all tasks for a project. Returns a page of trimmed rows; see fields/limit/offset.', {
|
|
331
|
+
project_id: z.number().describe('Project ID'),
|
|
332
|
+
...pageArgs,
|
|
333
|
+
...taskFilterArgsNoProject,
|
|
334
|
+
}, async (params) => {
|
|
335
|
+
const result = await requireClient().getProjectTasks(params.project_id);
|
|
336
|
+
// project_id is the query, not a filter -- re-filtering on it would drop
|
|
337
|
+
// every row if the response names the column differently.
|
|
338
|
+
const rows = Array.isArray(result)
|
|
339
|
+
? filterTasks(result, { ...params, project_id: undefined })
|
|
340
|
+
: result;
|
|
341
|
+
return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
|
|
342
|
+
});
|
|
343
|
+
server.tool('tasks_open', 'Get open tasks across projects. company_id and only_mine filter server-side; the rest filter in-process. Returns a page of trimmed rows.', {
|
|
344
|
+
only_mine: z.boolean().optional().describe('Only tasks assigned to the signed-in user (server-side)'),
|
|
345
|
+
team: z.string().optional().describe('Team filter passed through to the API'),
|
|
346
|
+
...taskFilterArgs,
|
|
347
|
+
...pageArgs,
|
|
348
|
+
}, async (params) => {
|
|
349
|
+
const result = await requireClient().getOpenTasks({
|
|
350
|
+
company_id: params.company_id,
|
|
351
|
+
only_mine: params.only_mine,
|
|
352
|
+
team: params.team,
|
|
353
|
+
});
|
|
354
|
+
const rows = Array.isArray(result) ? filterTasks(result, params) : result;
|
|
355
|
+
return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
|
|
298
356
|
});
|
|
299
357
|
server.tool('task_add', 'Add a task to a project. The API requires a full Group_Task payload; this tool builds it from simplified params.', {
|
|
300
358
|
project_id: z.number().describe('Project ID (group_id)'),
|
|
301
359
|
title: z.string().describe('Task title'),
|
|
302
360
|
description: z.string().optional().describe('Task description'),
|
|
303
361
|
notes: z.string().optional().describe('Task notes'),
|
|
304
|
-
assigned_to: z.number().optional().describe('User ID to assign to
|
|
362
|
+
assigned_to: z.number().optional().describe('User ID to assign the task to. The signed-in user is always recorded as creator.'),
|
|
305
363
|
company_id: z.number().optional().describe('Company ID associated with the project'),
|
|
306
364
|
service_id: z.number().optional().describe('Service ID classifying the work type'),
|
|
307
|
-
priority: z.
|
|
365
|
+
priority: z.enum(PRIORITIES).optional().describe('Priority (platform values)'),
|
|
366
|
+
status: z.enum(TASK_STATUSES).optional().describe('Task status (platform values)'),
|
|
308
367
|
estimated_hours: z.string().optional().describe('Estimated time in shorthand (e.g., 6h, 2h30m)'),
|
|
309
368
|
start_date: z.string().optional().describe('Start date (ISO 8601)'),
|
|
310
369
|
due_date: z.string().optional().describe('Due date (ISO 8601)'),
|
|
@@ -318,8 +377,8 @@ server.tool('task_update', 'Update a task. Fetches existing task first and merge
|
|
|
318
377
|
title: z.string().optional().describe('Task title'),
|
|
319
378
|
description: z.string().optional(),
|
|
320
379
|
assigned_to: z.number().optional().describe('User ID to assign to'),
|
|
321
|
-
priority: z.
|
|
322
|
-
status: z.
|
|
380
|
+
priority: z.enum(PRIORITIES).optional().describe('Priority (platform values)'),
|
|
381
|
+
status: z.enum(TASK_STATUSES).optional().describe('Task status (platform values)'),
|
|
323
382
|
due_date: z.string().optional().describe('Due date (ISO 8601)'),
|
|
324
383
|
}, async (params) => {
|
|
325
384
|
const result = await requireClient().updateTask(params);
|
|
@@ -329,34 +388,404 @@ server.tool('task_get', 'Get a single task by ID', { task_id: z.number().describ
|
|
|
329
388
|
const result = await requireClient().getTask(task_id);
|
|
330
389
|
return { content: [{ type: 'text', text: json(result) }] };
|
|
331
390
|
});
|
|
332
|
-
server.tool('tasks_pending', 'Get pending tasks for a user
|
|
333
|
-
|
|
391
|
+
server.tool('tasks_pending', 'Get pending tasks for a user. Returns a page of trimmed rows; see fields/limit/offset.', {
|
|
392
|
+
user_id: z.number().describe('User ID'),
|
|
393
|
+
...pageArgs,
|
|
394
|
+
...taskFilterArgs,
|
|
395
|
+
}, async (params) => {
|
|
396
|
+
const result = await requireClient().getPendingTasks(params.user_id);
|
|
397
|
+
const rows = Array.isArray(result) ? filterTasks(result, params) : result;
|
|
398
|
+
return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
|
|
399
|
+
});
|
|
400
|
+
server.tool('tasks_by_milestone', 'Get tasks for a milestone. Returns a page of trimmed rows; see fields/limit/offset.', {
|
|
401
|
+
milestone_id: z.number().describe('Milestone ID'),
|
|
402
|
+
...pageArgs,
|
|
403
|
+
...taskFilterArgs,
|
|
404
|
+
}, async (params) => {
|
|
405
|
+
const result = await requireClient().getTasksByMilestone(params.milestone_id);
|
|
406
|
+
const rows = Array.isArray(result) ? filterTasks(result, params) : result;
|
|
407
|
+
return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
|
|
408
|
+
});
|
|
409
|
+
server.tool('tasks_by_group_user', 'Get tasks by project group and user. Returns a page of trimmed rows; see fields/limit/offset.', {
|
|
410
|
+
group_id: z.number().describe('Project group ID'),
|
|
411
|
+
user_id: z.number().describe('User ID'),
|
|
412
|
+
...pageArgs,
|
|
413
|
+
...taskFilterArgs,
|
|
414
|
+
}, async (params) => {
|
|
415
|
+
const result = await requireClient().getTasksByGroupAndUser(params.group_id, params.user_id);
|
|
416
|
+
const rows = Array.isArray(result) ? filterTasks(result, params) : result;
|
|
417
|
+
return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
|
|
418
|
+
});
|
|
419
|
+
server.tool('tasks_by_company', 'Get tasks for a company. Returns a page of trimmed rows; see fields/limit/offset.', {
|
|
420
|
+
company_id: z.number().describe('Company ID'),
|
|
421
|
+
...pageArgs,
|
|
422
|
+
...taskFilterArgsNoCompany,
|
|
423
|
+
}, async (params) => {
|
|
424
|
+
const result = await requireClient().getCompanyTasks(params.company_id);
|
|
425
|
+
const rows = Array.isArray(result)
|
|
426
|
+
? filterTasks(result, { ...params, company_id: undefined })
|
|
427
|
+
: result;
|
|
428
|
+
return { content: [{ type: 'text', text: json(shape(rows, TASK_FIELDS, paging(params))) }] };
|
|
429
|
+
});
|
|
430
|
+
server.tool('projects_by_company', 'Get user projects filtered by company. Returns a page of trimmed rows; see fields/limit/offset.', {
|
|
431
|
+
user_id: z.number().describe('User ID'),
|
|
432
|
+
company_id: z.number().describe('Company ID'),
|
|
433
|
+
...pageArgs,
|
|
434
|
+
}, async (params) => {
|
|
435
|
+
const result = await requireClient().getProjectsByCompany(params.user_id, params.company_id);
|
|
436
|
+
return { content: [{ type: 'text', text: json(shape(result, PROJECT_FIELDS, paging(params))) }] };
|
|
437
|
+
});
|
|
438
|
+
server.tool('projects_active_by_user', 'Get active projects for a specific user. Returns a page of trimmed rows; see fields/limit/offset.', {
|
|
439
|
+
user_id: z.number().describe('User ID'),
|
|
440
|
+
...pageArgs,
|
|
441
|
+
}, async (params) => {
|
|
442
|
+
const result = await requireClient().getActiveProjectsByUser(params.user_id);
|
|
443
|
+
return { content: [{ type: 'text', text: json(shape(result, PROJECT_FIELDS, paging(params))) }] };
|
|
444
|
+
});
|
|
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));
|
|
334
560
|
return { content: [{ type: 'text', text: json(result) }] };
|
|
335
561
|
});
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
// ============================================================
|
|
602
|
+
// REFERENCE
|
|
603
|
+
// ============================================================
|
|
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()) }] }));
|
|
605
|
+
// ============================================================
|
|
606
|
+
// PROJECT WRITES
|
|
607
|
+
// ============================================================
|
|
608
|
+
server.tool('project_add', 'Create a project for a company. NOTE: the API has no project-delete route, so a project created here can only be archived or renamed afterward.', {
|
|
609
|
+
company_id: z.number().describe('Company ID the project belongs to'),
|
|
610
|
+
name: z.string().describe('Project name'),
|
|
611
|
+
short_description: z.string().optional().describe('One-line summary'),
|
|
612
|
+
description: z.string().optional().describe('Full description'),
|
|
613
|
+
status: z.enum(PROJECT_STATUSES).optional().describe('Initial status (default new)'),
|
|
614
|
+
is_public: z.boolean().optional().describe('Visible to clients (default false)'),
|
|
615
|
+
staff: z.array(z.number()).optional().describe('User ids to staff; defaults to the signed-in user'),
|
|
616
|
+
}, async (params) => {
|
|
617
|
+
const result = await requireClient().addProject(params);
|
|
338
618
|
return { content: [{ type: 'text', text: json(result) }] };
|
|
339
619
|
});
|
|
340
|
-
server.tool('
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}, async ({
|
|
344
|
-
const result = await requireClient().
|
|
620
|
+
server.tool('project_update', 'Update a project. Reads the current row and merges your changes onto it, so only pass what changes. Field names are the raw GEN_Group ones (strGroupName, strGroupDescription, bitPublic, target_date, ...).', {
|
|
621
|
+
project_id: z.number().describe('Project ID (intGroupId)'),
|
|
622
|
+
changes: z.record(z.string(), z.unknown()).describe('Fields to overwrite, e.g. {"strGroupName":"New name"}'),
|
|
623
|
+
}, async ({ project_id, changes }) => {
|
|
624
|
+
const result = await requireClient().updateProject(project_id, changes);
|
|
625
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
626
|
+
});
|
|
627
|
+
server.tool('project_set_status', 'Set a project status without touching its budget or approved time. Project status values are new, inprogress, complete -- note "complete", not "completed".', {
|
|
628
|
+
project_id: z.number().describe('Project ID'),
|
|
629
|
+
status: z.enum(PROJECT_STATUSES).describe('New status'),
|
|
630
|
+
billable: z.boolean().optional().describe('Billable flag (default true)'),
|
|
631
|
+
}, async ({ project_id, status, billable }) => {
|
|
632
|
+
const result = await requireClient().setProjectStatus(project_id, status, billable ?? true);
|
|
633
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
634
|
+
});
|
|
635
|
+
server.tool('project_user_add', 'Add a staff member to a project, optionally as lead or client contact', {
|
|
636
|
+
project_id: z.number().describe('Project ID'),
|
|
637
|
+
user_id: z.number().describe('User ID to add'),
|
|
638
|
+
is_lead: z.boolean().optional().describe('Make this user the project lead'),
|
|
639
|
+
is_contact: z.boolean().optional().describe('Mark as the client contact'),
|
|
640
|
+
}, async ({ project_id, user_id, is_lead, is_contact }) => {
|
|
641
|
+
const result = await requireClient().addProjectUser(project_id, user_id, { is_lead, is_contact });
|
|
345
642
|
return { content: [{ type: 'text', text: json(result) }] };
|
|
346
643
|
});
|
|
347
|
-
server.tool('
|
|
348
|
-
|
|
644
|
+
server.tool('project_user_remove', 'Remove a staff member from a project', {
|
|
645
|
+
project_id: z.number().describe('Project ID'),
|
|
646
|
+
user_id: z.number().describe('User ID to remove'),
|
|
647
|
+
}, async ({ project_id, user_id }) => {
|
|
648
|
+
const result = await requireClient().removeProjectUser(project_id, user_id);
|
|
649
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
650
|
+
});
|
|
651
|
+
// ============================================================
|
|
652
|
+
// MILESTONES
|
|
653
|
+
// ============================================================
|
|
654
|
+
server.tool('milestone_list', 'List milestones for a project', { project_id: z.number().describe('Project ID') }, async ({ project_id }) => {
|
|
655
|
+
const result = await requireClient().getMilestones(project_id);
|
|
656
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
657
|
+
});
|
|
658
|
+
server.tool('milestones_active', 'List active milestones across the account', {}, async () => {
|
|
659
|
+
const result = await requireClient().getActiveMilestones();
|
|
660
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
661
|
+
});
|
|
662
|
+
server.tool('milestones_by_company', 'List milestones for a company', { company_id: z.number().describe('Company ID') }, async ({ company_id }) => {
|
|
663
|
+
const result = await requireClient().getMilestonesByCompany(company_id);
|
|
664
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
665
|
+
});
|
|
666
|
+
server.tool('milestones_by_user', 'List active milestones for a user (defaults to the signed-in user)', { user_id: z.number().optional().describe('User ID') }, async ({ user_id }) => {
|
|
667
|
+
const result = await requireClient().getMilestonesByUser(user_id ?? 0);
|
|
668
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
669
|
+
});
|
|
670
|
+
server.tool('milestone_get', 'Get a milestone by ID', { milestone_id: z.number().describe('Milestone ID') }, async ({ milestone_id }) => {
|
|
671
|
+
const result = await requireClient().getMilestone(milestone_id);
|
|
672
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
673
|
+
});
|
|
674
|
+
server.tool('milestone_add', 'Create a milestone in a project. Use this to give tasks a phase to hang off; tasks reference it via milestone_id.', {
|
|
675
|
+
project_id: z.number().describe('Project ID the milestone belongs to'),
|
|
676
|
+
title: z.string().describe('Milestone name'),
|
|
677
|
+
sequence: z.number().optional().describe('Order within the project'),
|
|
678
|
+
start_date: z.string().optional().describe('Start date (ISO 8601)'),
|
|
679
|
+
due_date: z.string().optional().describe('Due date (ISO 8601)'),
|
|
680
|
+
estimated_hours: z.string().optional().describe('Estimate in shorthand (e.g., 40h, 2h30m)'),
|
|
681
|
+
}, async (params) => {
|
|
682
|
+
const result = await requireClient().addMilestone(params);
|
|
683
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
684
|
+
});
|
|
685
|
+
server.tool('milestone_update', 'Update a milestone. Reads the current record and merges your changes onto it.', {
|
|
686
|
+
milestone_id: z.number().describe('Milestone ID'),
|
|
687
|
+
title: z.string().optional().describe('New name'),
|
|
688
|
+
sequence: z.number().optional().describe('Order within the project'),
|
|
689
|
+
start_date: z.string().optional().describe('Start date (ISO 8601)'),
|
|
690
|
+
due_date: z.string().optional().describe('Due date (ISO 8601)'),
|
|
691
|
+
estimated_hours: z.string().optional().describe('Estimate in shorthand'),
|
|
692
|
+
progress: z.number().optional().describe('Percent complete (0-100)'),
|
|
693
|
+
complete: z.boolean().optional().describe('Mark complete'),
|
|
694
|
+
}, async (params) => {
|
|
695
|
+
const result = await requireClient().updateMilestone(params);
|
|
696
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
697
|
+
});
|
|
698
|
+
server.tool('milestone_clone', 'Copy an existing milestone into a project. Useful for repeatable delivery templates.', {
|
|
699
|
+
milestone_id: z.number().describe('Milestone ID to copy'),
|
|
700
|
+
project_id: z.number().describe('Project ID to copy it into'),
|
|
701
|
+
title: z.string().optional().describe('Name for the copy (defaults to the original name)'),
|
|
702
|
+
}, async ({ milestone_id, project_id, title }) => {
|
|
703
|
+
const result = await requireClient().cloneMilestone(milestone_id, project_id, title);
|
|
349
704
|
return { content: [{ type: 'text', text: json(result) }] };
|
|
350
705
|
});
|
|
351
|
-
server.tool('
|
|
706
|
+
server.tool('milestone_remove', 'Delete a milestone', { milestone_id: z.number().describe('Milestone ID') }, async ({ milestone_id }) => {
|
|
707
|
+
const result = await requireClient().removeMilestone(milestone_id);
|
|
708
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
709
|
+
});
|
|
710
|
+
// ============================================================
|
|
711
|
+
// TASK ASSIGNMENT AND LIFECYCLE
|
|
712
|
+
// ============================================================
|
|
713
|
+
server.tool('task_assignee_add', 'Assign a user to a task', {
|
|
714
|
+
task_id: z.number().describe('Task ID'),
|
|
715
|
+
user_id: z.number().describe('User ID to assign'),
|
|
716
|
+
leader: z.boolean().optional().describe('Make this user the task leader'),
|
|
717
|
+
attention: z.boolean().optional().describe('Flag the task for this user\'s attention'),
|
|
718
|
+
notify: z.boolean().optional().describe('Notify the user (default true)'),
|
|
719
|
+
}, async (params) => {
|
|
720
|
+
const result = await requireClient().addTaskAssignee(params);
|
|
721
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
722
|
+
});
|
|
723
|
+
server.tool('task_assignee_remove', 'Unassign a user from a task. Takes the assignee_id from the task staff_assignees list, NOT a user_id.', { assignee_id: z.number().describe('Assignee record ID (from staff_assignees)') }, async ({ assignee_id }) => {
|
|
724
|
+
const result = await requireClient().removeTaskAssignee(assignee_id);
|
|
725
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
726
|
+
});
|
|
727
|
+
server.tool('task_set_leader', 'Make a user the leader on a task', {
|
|
728
|
+
task_id: z.number().describe('Task ID'),
|
|
729
|
+
user_id: z.number().describe('User ID to make leader'),
|
|
730
|
+
}, async ({ task_id, user_id }) => {
|
|
731
|
+
const result = await requireClient().setTaskLeader(task_id, user_id);
|
|
732
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
733
|
+
});
|
|
734
|
+
server.tool('task_set_attention', 'Set or clear the attention flag for a user on a task', {
|
|
735
|
+
task_id: z.number().describe('Task ID'),
|
|
352
736
|
user_id: z.number().describe('User ID'),
|
|
353
|
-
|
|
354
|
-
}, async ({ user_id,
|
|
355
|
-
const result = await requireClient().
|
|
737
|
+
flag: z.boolean().describe('true to flag, false to clear'),
|
|
738
|
+
}, async ({ task_id, user_id, flag }) => {
|
|
739
|
+
const result = await requireClient().setTaskAttention(task_id, user_id, flag);
|
|
740
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
741
|
+
});
|
|
742
|
+
server.tool('task_delete', 'Delete a task permanently. There is no undo; prefer setting status to cancelled unless the task was created in error.', { task_id: z.number().describe('Task ID') }, async ({ task_id }) => {
|
|
743
|
+
const result = await requireClient().deleteTask(task_id);
|
|
744
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
745
|
+
});
|
|
746
|
+
server.tool('task_merge', 'Merge one task into another. The source task is consumed by the target; use this to clean up duplicates.', {
|
|
747
|
+
from_task_id: z.number().describe('Task to merge from (consumed)'),
|
|
748
|
+
to_task_id: z.number().describe('Task to merge into (kept)'),
|
|
749
|
+
}, async ({ from_task_id, to_task_id }) => {
|
|
750
|
+
const result = await requireClient().mergeTasks(from_task_id, to_task_id);
|
|
751
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
752
|
+
});
|
|
753
|
+
// ============================================================
|
|
754
|
+
// TASK COMMENTS
|
|
755
|
+
// ============================================================
|
|
756
|
+
server.tool('task_comments', 'Get all comments on a task. Task rows report total_comments and unread_for_me; this reads the thread itself.', { task_id: z.number().describe('Task ID') }, async ({ task_id }) => {
|
|
757
|
+
const result = await requireClient().getTaskComments(task_id);
|
|
758
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
759
|
+
});
|
|
760
|
+
server.tool('task_comment_get', 'Get a single task comment by ID', { comment_id: z.string().describe('Comment ID (GUID)') }, async ({ comment_id }) => {
|
|
761
|
+
const result = await requireClient().getTaskComment(comment_id);
|
|
762
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
763
|
+
});
|
|
764
|
+
server.tool('task_comment_add', 'Add a comment to a task. WARNING: on this platform account the backend fires a client-notification webhook for comments on tasks whose client_id > 0, so a comment can reach the customer. Check the task client_id first if that matters.', {
|
|
765
|
+
task_id: z.number().describe('Task ID'),
|
|
766
|
+
comment: z.string().describe('Comment text'),
|
|
767
|
+
client_id: z.number().optional().describe('Client ID to attribute the comment to (default 0 = staff comment)'),
|
|
768
|
+
}, async (params) => {
|
|
769
|
+
const result = await requireClient().addTaskComment(params);
|
|
770
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
771
|
+
});
|
|
772
|
+
server.tool('task_comment_update', 'Update the text of an existing task comment', {
|
|
773
|
+
comment_id: z.string().describe('Comment ID (GUID)'),
|
|
774
|
+
task_id: z.number().describe('Task ID the comment belongs to'),
|
|
775
|
+
comment: z.string().describe('Replacement comment text'),
|
|
776
|
+
}, async (params) => {
|
|
777
|
+
const result = await requireClient().updateTaskComment(params);
|
|
778
|
+
return { content: [{ type: 'text', text: json(result) }] };
|
|
779
|
+
});
|
|
780
|
+
server.tool('task_comment_remove', 'Remove a task comment by ID. Pass task_id: the backend answers 500 even when the delete succeeds, and task_id lets the removal be verified instead of reported as a failure.', {
|
|
781
|
+
comment_id: z.string().describe('Comment ID (GUID)'),
|
|
782
|
+
task_id: z.number().optional().describe('Task the comment belongs to, used to verify removal'),
|
|
783
|
+
}, async ({ comment_id, task_id }) => {
|
|
784
|
+
const result = await requireClient().removeTaskComment(comment_id, task_id);
|
|
356
785
|
return { content: [{ type: 'text', text: json(result) }] };
|
|
357
786
|
});
|
|
358
|
-
server.tool('
|
|
359
|
-
const result = await requireClient().
|
|
787
|
+
server.tool('task_mark_read', 'Mark a task read for the signed-in user, clearing unread_for_me', { task_id: z.number().describe('Task ID') }, async ({ task_id }) => {
|
|
788
|
+
const result = await requireClient().markTaskRead(task_id);
|
|
360
789
|
return { content: [{ type: 'text', text: json(result) }] };
|
|
361
790
|
});
|
|
362
791
|
// ============================================================
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response shaping for list tools -- phase 2 of docs/AGENT-CAPABILITY-SPEC.md.
|
|
3
|
+
*
|
|
4
|
+
* The core API returns every column it has: a task row carries ~90 fields
|
|
5
|
+
* (including fleet columns like vin and unit_class) and a project row carries 87
|
|
6
|
+
* (including video-conference config and welcome messages). Six open tasks came
|
|
7
|
+
* back as 102 KB; 211 active projects as 487 KB, which no caller can afford to
|
|
8
|
+
* read. These helpers trim to a PM-relevant default, page the result, and tell
|
|
9
|
+
* the caller what was left out so it can ask for more deliberately.
|
|
10
|
+
*/
|
|
11
|
+
/** Task fields worth returning by default, of roughly 90 available. */
|
|
12
|
+
export declare const TASK_FIELDS: string[];
|
|
13
|
+
/** Project (group) fields worth returning by default, of 87 available. */
|
|
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[];
|
|
23
|
+
export declare const DEFAULT_LIMIT = 50;
|
|
24
|
+
export interface ShapeOpts {
|
|
25
|
+
fields?: string[];
|
|
26
|
+
limit?: number;
|
|
27
|
+
offset?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface TaskFilters {
|
|
30
|
+
company_id?: number;
|
|
31
|
+
project_id?: number;
|
|
32
|
+
assignee_id?: number;
|
|
33
|
+
status?: string;
|
|
34
|
+
priority?: string;
|
|
35
|
+
due_before?: string;
|
|
36
|
+
due_after?: string;
|
|
37
|
+
search?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Apply the filters the backend does not support, in-process. */
|
|
40
|
+
export declare function filterTasks(rows: unknown[], f: TaskFilters): unknown[];
|
|
41
|
+
/**
|
|
42
|
+
* Trim, page, and annotate a list response. Non-array payloads (a single record,
|
|
43
|
+
* or an error object) pass through untouched.
|
|
44
|
+
*
|
|
45
|
+
* Pass fields: ['all'] to opt out of projection and get every column.
|
|
46
|
+
*/
|
|
47
|
+
export declare function shape(payload: unknown, defaults: string[], opts?: ShapeOpts): unknown;
|