@kanvas/openclaw-plugin 0.1.2 → 0.1.4
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/dist/domains/crm/index.js +310 -0
- package/dist/index.js +23 -4
- package/dist/tools/crm.js +158 -3
- package/package.json +2 -1
|
@@ -1,4 +1,26 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
1
2
|
import { postGraphQLMultipart } from "../../client/multipart.js";
|
|
3
|
+
const CONTENT_TYPES = {
|
|
4
|
+
".pdf": "application/pdf",
|
|
5
|
+
".jpg": "image/jpeg",
|
|
6
|
+
".jpeg": "image/jpeg",
|
|
7
|
+
".png": "image/png",
|
|
8
|
+
".gif": "image/gif",
|
|
9
|
+
".webp": "image/webp",
|
|
10
|
+
".svg": "image/svg+xml",
|
|
11
|
+
".doc": "application/msword",
|
|
12
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
13
|
+
".xls": "application/vnd.ms-excel",
|
|
14
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
15
|
+
".csv": "text/csv",
|
|
16
|
+
".txt": "text/plain",
|
|
17
|
+
".zip": "application/zip",
|
|
18
|
+
".html": "text/html",
|
|
19
|
+
};
|
|
20
|
+
function guessContentType(fileName) {
|
|
21
|
+
const ext = fileName.slice(fileName.lastIndexOf(".")).toLowerCase();
|
|
22
|
+
return CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
23
|
+
}
|
|
2
24
|
export class CrmService {
|
|
3
25
|
client;
|
|
4
26
|
constructor(client) {
|
|
@@ -162,6 +184,17 @@ export class CrmService {
|
|
|
162
184
|
return this.client.query(mutation, { input });
|
|
163
185
|
}
|
|
164
186
|
async updateLead(id, input) {
|
|
187
|
+
// Auto-fetch branch_id and people_id if not provided (the API requires them)
|
|
188
|
+
if (!input.branch_id || !input.people_id) {
|
|
189
|
+
const current = await this.getLeadCore(id);
|
|
190
|
+
const lead = current.data?.leads?.data?.[0];
|
|
191
|
+
if (lead) {
|
|
192
|
+
if (!input.branch_id)
|
|
193
|
+
input.branch_id = lead.branch?.id;
|
|
194
|
+
if (!input.people_id)
|
|
195
|
+
input.people_id = lead.people?.id;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
165
198
|
const mutation = `
|
|
166
199
|
mutation UpdateLead($id: ID!, $input: LeadUpdateInput!) {
|
|
167
200
|
updateLead(id: $id, input: $input) {
|
|
@@ -180,6 +213,24 @@ export class CrmService {
|
|
|
180
213
|
`;
|
|
181
214
|
return this.client.query(mutation, { id, input });
|
|
182
215
|
}
|
|
216
|
+
/** Lightweight lead fetch for getting required fields (branch_id, people_id). */
|
|
217
|
+
async getLeadCore(id) {
|
|
218
|
+
const query = `
|
|
219
|
+
query GetLeadCore($first: Int!, $where: QueryLeadsWhereWhereConditions) {
|
|
220
|
+
leads(first: $first, where: $where) {
|
|
221
|
+
data {
|
|
222
|
+
id
|
|
223
|
+
branch { id }
|
|
224
|
+
people { id }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
return this.client.query(query, {
|
|
230
|
+
first: 1,
|
|
231
|
+
where: [{ column: "ID", operator: "EQ", value: id }],
|
|
232
|
+
});
|
|
233
|
+
}
|
|
183
234
|
async changeLeadOwner(input) {
|
|
184
235
|
return this.updateLead(String(input.leadId), {
|
|
185
236
|
branch_id: input.branch_id,
|
|
@@ -587,4 +638,263 @@ export class CrmService {
|
|
|
587
638
|
`;
|
|
588
639
|
return this.client.query(query, { first });
|
|
589
640
|
}
|
|
641
|
+
async attachFileToLeadByUrl(leadId, fileUrl, fileName) {
|
|
642
|
+
const mutation = `
|
|
643
|
+
mutation CreateFilesystem($input: FilesystemInputUrl!) {
|
|
644
|
+
createFileSystem(input: $input) {
|
|
645
|
+
id
|
|
646
|
+
uuid
|
|
647
|
+
name
|
|
648
|
+
url
|
|
649
|
+
type
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
`;
|
|
653
|
+
const fileResult = await this.client.query(mutation, {
|
|
654
|
+
input: { url: fileUrl, name: fileName },
|
|
655
|
+
});
|
|
656
|
+
const fileUuid = fileResult.data?.createFileSystem?.uuid;
|
|
657
|
+
if (!fileUuid) {
|
|
658
|
+
return fileResult; // return the error
|
|
659
|
+
}
|
|
660
|
+
// Now attach via updateLead with files
|
|
661
|
+
return this.updateLead(leadId, {
|
|
662
|
+
files: [{ url: fileUrl, name: fileName }],
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Resolve file content from base64, file path, or URL.
|
|
667
|
+
*/
|
|
668
|
+
async resolveFileContent(source) {
|
|
669
|
+
if (source.base64) {
|
|
670
|
+
return Buffer.from(source.base64, "base64");
|
|
671
|
+
}
|
|
672
|
+
if (source.filePath) {
|
|
673
|
+
return readFile(source.filePath);
|
|
674
|
+
}
|
|
675
|
+
if (source.url) {
|
|
676
|
+
const response = await fetch(source.url);
|
|
677
|
+
if (!response.ok) {
|
|
678
|
+
throw new Error(`Failed to download file from ${source.url}: ${response.status}`);
|
|
679
|
+
}
|
|
680
|
+
return Buffer.from(await response.arrayBuffer());
|
|
681
|
+
}
|
|
682
|
+
throw new Error("One of base64, filePath, or url must be provided");
|
|
683
|
+
}
|
|
684
|
+
async uploadFileToLead(leadId, fileName, source, contentType) {
|
|
685
|
+
const buffer = await this.resolveFileContent(source);
|
|
686
|
+
return postGraphQLMultipart({
|
|
687
|
+
config: this.client.getConfig(),
|
|
688
|
+
query: `
|
|
689
|
+
mutation AttachFileToLead($id: ID!, $file: Upload!) {
|
|
690
|
+
attachFileToLead(id: $id, file: $file) {
|
|
691
|
+
id
|
|
692
|
+
uuid
|
|
693
|
+
title
|
|
694
|
+
files {
|
|
695
|
+
data {
|
|
696
|
+
id
|
|
697
|
+
uuid
|
|
698
|
+
name
|
|
699
|
+
url
|
|
700
|
+
type
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
`,
|
|
706
|
+
variables: { id: leadId, file: null },
|
|
707
|
+
files: [{
|
|
708
|
+
key: "variables.file",
|
|
709
|
+
fileName,
|
|
710
|
+
contentType: contentType ?? guessContentType(fileName),
|
|
711
|
+
content: buffer,
|
|
712
|
+
}],
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
async uploadFileToMessage(messageId, fileName, source, contentType) {
|
|
716
|
+
const buffer = await this.resolveFileContent(source);
|
|
717
|
+
return postGraphQLMultipart({
|
|
718
|
+
config: this.client.getConfig(),
|
|
719
|
+
query: `
|
|
720
|
+
mutation AttachFileToMessage($message_id: ID!, $file: Upload!) {
|
|
721
|
+
attachFileToMessage(message_id: $message_id, file: $file) {
|
|
722
|
+
id
|
|
723
|
+
uuid
|
|
724
|
+
message
|
|
725
|
+
files {
|
|
726
|
+
data {
|
|
727
|
+
id
|
|
728
|
+
uuid
|
|
729
|
+
name
|
|
730
|
+
url
|
|
731
|
+
type
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
`,
|
|
737
|
+
variables: { message_id: messageId, file: null },
|
|
738
|
+
files: [{
|
|
739
|
+
key: "variables.file",
|
|
740
|
+
fileName,
|
|
741
|
+
contentType: contentType ?? guessContentType(fileName),
|
|
742
|
+
content: buffer,
|
|
743
|
+
}],
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
async updatePeople(id, input) {
|
|
747
|
+
const mutation = `
|
|
748
|
+
mutation UpdatePeople($id: ID!, $input: PeopleInput!) {
|
|
749
|
+
updatePeople(id: $id, input: $input) {
|
|
750
|
+
id
|
|
751
|
+
uuid
|
|
752
|
+
firstname
|
|
753
|
+
middlename
|
|
754
|
+
lastname
|
|
755
|
+
dob
|
|
756
|
+
contacts {
|
|
757
|
+
id
|
|
758
|
+
value
|
|
759
|
+
type { id name }
|
|
760
|
+
}
|
|
761
|
+
address {
|
|
762
|
+
id
|
|
763
|
+
address
|
|
764
|
+
city
|
|
765
|
+
state
|
|
766
|
+
zip
|
|
767
|
+
country
|
|
768
|
+
}
|
|
769
|
+
organizations { name }
|
|
770
|
+
custom_fields { name value }
|
|
771
|
+
tags { id name }
|
|
772
|
+
updated_at
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
`;
|
|
776
|
+
return this.client.query(mutation, { id, input });
|
|
777
|
+
}
|
|
778
|
+
async searchPeople(search, first = 10) {
|
|
779
|
+
const query = `
|
|
780
|
+
query SearchPeople($first: Int, $search: String) {
|
|
781
|
+
peoples(first: $first, search: $search) {
|
|
782
|
+
data {
|
|
783
|
+
id
|
|
784
|
+
uuid
|
|
785
|
+
firstname
|
|
786
|
+
lastname
|
|
787
|
+
contacts {
|
|
788
|
+
id
|
|
789
|
+
value
|
|
790
|
+
type { id name }
|
|
791
|
+
}
|
|
792
|
+
organizations { name }
|
|
793
|
+
created_at
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
`;
|
|
798
|
+
return this.client.query(query, { first, search });
|
|
799
|
+
}
|
|
800
|
+
async listPeopleRelationships(first = 50) {
|
|
801
|
+
const query = `
|
|
802
|
+
query PeopleRelationships($first: Int) {
|
|
803
|
+
peopleRelationships(first: $first) {
|
|
804
|
+
data {
|
|
805
|
+
id
|
|
806
|
+
name
|
|
807
|
+
description
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
`;
|
|
812
|
+
return this.client.query(query, { first });
|
|
813
|
+
}
|
|
814
|
+
async listContactTypes(first = 50) {
|
|
815
|
+
const query = `
|
|
816
|
+
query ContactTypes($first: Int) {
|
|
817
|
+
contactTypes(first: $first) {
|
|
818
|
+
data {
|
|
819
|
+
id
|
|
820
|
+
name
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
`;
|
|
825
|
+
return this.client.query(query, { first });
|
|
826
|
+
}
|
|
827
|
+
async createFollowUpEvent(input) {
|
|
828
|
+
const mutation = `
|
|
829
|
+
mutation CreateEvent($input: EventInput!) {
|
|
830
|
+
createEvent(input: $input) {
|
|
831
|
+
id
|
|
832
|
+
uuid
|
|
833
|
+
name
|
|
834
|
+
description
|
|
835
|
+
created_at
|
|
836
|
+
versions {
|
|
837
|
+
data {
|
|
838
|
+
id
|
|
839
|
+
start_at
|
|
840
|
+
end_at
|
|
841
|
+
dates {
|
|
842
|
+
id
|
|
843
|
+
date
|
|
844
|
+
start_time
|
|
845
|
+
end_time
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
`;
|
|
852
|
+
const eventInput = {
|
|
853
|
+
name: input.name,
|
|
854
|
+
description: input.description,
|
|
855
|
+
dates: [{
|
|
856
|
+
date: input.date,
|
|
857
|
+
start_time: input.start_time,
|
|
858
|
+
end_time: input.end_time,
|
|
859
|
+
}],
|
|
860
|
+
};
|
|
861
|
+
if (input.lead_id) {
|
|
862
|
+
eventInput.resources = [{
|
|
863
|
+
resources_id: String(input.lead_id),
|
|
864
|
+
resources_type: "lead",
|
|
865
|
+
}];
|
|
866
|
+
}
|
|
867
|
+
return this.client.query(mutation, { input: eventInput });
|
|
868
|
+
}
|
|
869
|
+
async listEvents(first = 25, where) {
|
|
870
|
+
const query = `
|
|
871
|
+
query Events($first: Int, $where: QueryEventsWhereWhereConditions) {
|
|
872
|
+
events(first: $first, where: $where, orderBy: [{ column: CREATED_AT, order: DESC }]) {
|
|
873
|
+
data {
|
|
874
|
+
id
|
|
875
|
+
uuid
|
|
876
|
+
name
|
|
877
|
+
description
|
|
878
|
+
created_at
|
|
879
|
+
type { name }
|
|
880
|
+
eventStatus { name }
|
|
881
|
+
versions {
|
|
882
|
+
data {
|
|
883
|
+
id
|
|
884
|
+
start_at
|
|
885
|
+
end_at
|
|
886
|
+
dates {
|
|
887
|
+
id
|
|
888
|
+
date
|
|
889
|
+
start_time
|
|
890
|
+
end_time
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
`;
|
|
898
|
+
return this.client.query(query, { first, where });
|
|
899
|
+
}
|
|
590
900
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
1
|
import { Type } from "@sinclair/typebox";
|
|
3
2
|
import { KanvasClient } from "./client/kanvas-client.js";
|
|
4
3
|
import { CrmService } from "./domains/crm/index.js";
|
|
@@ -62,10 +61,11 @@ function createAuthGuard(client, config, logger) {
|
|
|
62
61
|
return authPromise;
|
|
63
62
|
};
|
|
64
63
|
}
|
|
65
|
-
export default
|
|
64
|
+
export default {
|
|
66
65
|
id: "kanvas",
|
|
67
66
|
name: "Kanvas CRM",
|
|
68
67
|
description: "Connects agents to Kanvas — your company's nervous system for CRM, inventory, orders, and messaging.",
|
|
68
|
+
configSchema: { type: "object" },
|
|
69
69
|
register(api) {
|
|
70
70
|
const config = resolveConfig(api.pluginConfig);
|
|
71
71
|
const client = new KanvasClient(config);
|
|
@@ -103,9 +103,9 @@ export default definePluginEntry({
|
|
|
103
103
|
await runSetup();
|
|
104
104
|
});
|
|
105
105
|
}, { commands: ["setup"] });
|
|
106
|
-
api.logger.info("Kanvas plugin registered —
|
|
106
|
+
api.logger.info("Kanvas plugin registered — 50 tools loaded");
|
|
107
107
|
},
|
|
108
|
-
}
|
|
108
|
+
};
|
|
109
109
|
const KANVAS_SYSTEM_CONTEXT = `
|
|
110
110
|
## Kanvas Plugin
|
|
111
111
|
|
|
@@ -124,6 +124,16 @@ Use when the user asks about leads, prospects, sales pipeline, contacts, or foll
|
|
|
124
124
|
- \`kanvas_add_lead_participant\` / \`kanvas_remove_lead_participant\` → manage related people
|
|
125
125
|
- \`kanvas_follow_lead\` / \`kanvas_unfollow_lead\` → subscribe to lead updates
|
|
126
126
|
- \`kanvas_delete_lead\` / \`kanvas_restore_lead\` → soft-delete and restore
|
|
127
|
+
- \`kanvas_attach_file_to_lead_by_url\` → attach a file to a lead from a public URL
|
|
128
|
+
- \`kanvas_upload_file_to_lead\` → upload a file directly to a lead (base64-encoded content — for PDFs, images, generated docs)
|
|
129
|
+
- \`kanvas_upload_file_to_message\` → upload a file to a message (base64-encoded)
|
|
130
|
+
|
|
131
|
+
**People / Contacts**
|
|
132
|
+
Use when the user asks about updating contact info, adding phone numbers or emails.
|
|
133
|
+
- \`kanvas_search_people\` → search contacts by name, email, or phone
|
|
134
|
+
- \`kanvas_update_people\` → update a person's name, phone, email, address, tags (call kanvas_list_contact_types first for contact type IDs)
|
|
135
|
+
- \`kanvas_list_contact_types\` → get valid contact type IDs (email, phone, etc.)
|
|
136
|
+
- \`kanvas_list_people_relationships\` → list relationship types for lead participants
|
|
127
137
|
|
|
128
138
|
**CRM Support Data** — call these first when creating/updating leads to get valid IDs:
|
|
129
139
|
- \`kanvas_list_pipelines\` → pipelines and their stages (needed for pipeline_stage_id)
|
|
@@ -167,6 +177,12 @@ Use when the user asks about orders, purchases, or sales.
|
|
|
167
177
|
- \`kanvas_search_orders\` → find orders by number or keyword
|
|
168
178
|
- \`kanvas_get_order\` → full order detail with items, customer, status
|
|
169
179
|
|
|
180
|
+
**Follow-ups & Reminders**
|
|
181
|
+
ALWAYS schedule follow-ups in Kanvas so the human team can see them — NEVER store them only in local memory.
|
|
182
|
+
- \`kanvas_create_follow_up\` → create a calendar event for a future action (e.g. "Call Jane re: proposal"). Optionally link to a lead. Requires: name, date, start_time, end_time.
|
|
183
|
+
- \`kanvas_list_events\` → list scheduled events/follow-ups
|
|
184
|
+
- For structured follow-up data (tracking status, priority, custom fields), use \`kanvas_create_message\` with verb "follow_up" and a JSON payload containing { due_date, lead_id, action, status, priority }.
|
|
185
|
+
|
|
170
186
|
**Diagnostics**
|
|
171
187
|
- \`kanvas_test_connection\` → verify the API is reachable
|
|
172
188
|
|
|
@@ -176,4 +192,7 @@ Use when the user asks about orders, purchases, or sales.
|
|
|
176
192
|
- Messages use \`message_verb\` to define their type (e.g. "comment", "note", "sms"). New verbs are auto-created.
|
|
177
193
|
- The \`message\` field in social messages accepts arbitrary JSON — use it to store any structured data.
|
|
178
194
|
- Filtering uses \`where: [{ column, operator, value }]\` format. Common operators: "EQ", "LIKE", "IN".
|
|
195
|
+
- **Follow-ups and reminders MUST go in Kanvas** (events or messages), not local memory. The human team needs to see them in the dashboard.
|
|
196
|
+
- When updating a lead, you don't need to provide branch_id or people_id — they are auto-fetched.
|
|
197
|
+
- To add contacts to a person, call \`kanvas_list_contact_types\` first, then pass the contacts array to \`kanvas_update_people\`.
|
|
179
198
|
`.trim();
|
package/dist/tools/crm.js
CHANGED
|
@@ -61,12 +61,12 @@ export function registerCrmTools(api, service, ensureAuth) {
|
|
|
61
61
|
api.registerTool({
|
|
62
62
|
name: "kanvas_update_lead",
|
|
63
63
|
label: "Update Lead",
|
|
64
|
-
description: "Update an existing lead's fields.",
|
|
64
|
+
description: "Update an existing lead's fields. branch_id and people_id are auto-fetched if not provided.",
|
|
65
65
|
parameters: Type.Object({
|
|
66
66
|
id: Type.String({ description: "Lead ID" }),
|
|
67
67
|
input: Type.Object({
|
|
68
|
-
branch_id: Type.Union([Type.String(), Type.Number()]),
|
|
69
|
-
people_id: Type.Union([Type.String(), Type.Number()]),
|
|
68
|
+
branch_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: "Auto-fetched if omitted" })),
|
|
69
|
+
people_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: "Auto-fetched if omitted" })),
|
|
70
70
|
title: Type.Optional(Type.String()),
|
|
71
71
|
leads_owner_id: Type.Optional(Type.Union([Type.String(), Type.Number()])),
|
|
72
72
|
organization_id: Type.Optional(Type.Union([Type.String(), Type.Number()])),
|
|
@@ -340,4 +340,159 @@ export function registerCrmTools(api, service, ensureAuth) {
|
|
|
340
340
|
return toolResult(await service.listLeadTypes(params.first));
|
|
341
341
|
},
|
|
342
342
|
});
|
|
343
|
+
api.registerTool({
|
|
344
|
+
name: "kanvas_attach_file_to_lead_by_url",
|
|
345
|
+
label: "Attach File to Lead by URL",
|
|
346
|
+
description: "Attach a file (PDF, image, document) to a lead using a public URL. " +
|
|
347
|
+
"The file is downloaded by the server and attached to the lead.",
|
|
348
|
+
parameters: Type.Object({
|
|
349
|
+
leadId: Type.String({ description: "Lead ID" }),
|
|
350
|
+
fileUrl: Type.String({ description: "Public URL of the file to attach" }),
|
|
351
|
+
fileName: Type.String({ description: "File name (e.g. proposal.pdf, photo.jpg)" }),
|
|
352
|
+
}),
|
|
353
|
+
async execute(_id, params) {
|
|
354
|
+
await ensureAuth();
|
|
355
|
+
return toolResult(await service.attachFileToLeadByUrl(params.leadId, params.fileUrl, params.fileName));
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
api.registerTool({
|
|
359
|
+
name: "kanvas_upload_file_to_lead",
|
|
360
|
+
label: "Upload File to Lead",
|
|
361
|
+
description: "Upload a file to a lead. Provide the content as base64, a local file path, or a URL to download from. " +
|
|
362
|
+
"Exactly one of base64, filePath, or url must be provided.",
|
|
363
|
+
parameters: Type.Object({
|
|
364
|
+
leadId: Type.String({ description: "Lead ID" }),
|
|
365
|
+
fileName: Type.String({ description: "File name with extension (e.g. proposal.pdf, photo.jpg)" }),
|
|
366
|
+
base64: Type.Optional(Type.String({ description: "Base64-encoded file content" })),
|
|
367
|
+
filePath: Type.Optional(Type.String({ description: "Absolute path to a local file" })),
|
|
368
|
+
url: Type.Optional(Type.String({ description: "URL to download the file from" })),
|
|
369
|
+
contentType: Type.Optional(Type.String({ description: "MIME type (auto-detected from extension if omitted)" })),
|
|
370
|
+
}),
|
|
371
|
+
async execute(_id, params) {
|
|
372
|
+
await ensureAuth();
|
|
373
|
+
const { leadId, fileName, contentType, ...source } = params;
|
|
374
|
+
return toolResult(await service.uploadFileToLead(leadId, fileName, source, contentType));
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
api.registerTool({
|
|
378
|
+
name: "kanvas_upload_file_to_message",
|
|
379
|
+
label: "Upload File to Message",
|
|
380
|
+
description: "Upload a file to a message. Provide the content as base64, a local file path, or a URL to download from.",
|
|
381
|
+
parameters: Type.Object({
|
|
382
|
+
messageId: Type.String({ description: "Message ID" }),
|
|
383
|
+
fileName: Type.String({ description: "File name with extension" }),
|
|
384
|
+
base64: Type.Optional(Type.String({ description: "Base64-encoded file content" })),
|
|
385
|
+
filePath: Type.Optional(Type.String({ description: "Absolute path to a local file" })),
|
|
386
|
+
url: Type.Optional(Type.String({ description: "URL to download the file from" })),
|
|
387
|
+
contentType: Type.Optional(Type.String({ description: "MIME type (auto-detected if omitted)" })),
|
|
388
|
+
}),
|
|
389
|
+
async execute(_id, params) {
|
|
390
|
+
await ensureAuth();
|
|
391
|
+
const { messageId, fileName, contentType, ...source } = params;
|
|
392
|
+
return toolResult(await service.uploadFileToMessage(messageId, fileName, source, contentType));
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
// --- People / Contacts ---
|
|
396
|
+
api.registerTool({
|
|
397
|
+
name: "kanvas_search_people",
|
|
398
|
+
label: "Search People",
|
|
399
|
+
description: "Search contacts/people by name, email, or phone.",
|
|
400
|
+
parameters: Type.Object({
|
|
401
|
+
search: Type.String({ description: "Search keyword" }),
|
|
402
|
+
first: Type.Optional(Type.Number({ description: "Max results (default 10)" })),
|
|
403
|
+
}),
|
|
404
|
+
async execute(_id, params) {
|
|
405
|
+
await ensureAuth();
|
|
406
|
+
return toolResult(await service.searchPeople(params.search, params.first));
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
api.registerTool({
|
|
410
|
+
name: "kanvas_update_people",
|
|
411
|
+
label: "Update People/Contact",
|
|
412
|
+
description: "Update a person's profile — name, phone, email, address, tags, custom fields. " +
|
|
413
|
+
"To add contacts (phone/email), pass contacts array with value and contacts_types_id. " +
|
|
414
|
+
"Call kanvas_list_contact_types first to get valid contact type IDs.",
|
|
415
|
+
parameters: Type.Object({
|
|
416
|
+
id: Type.String({ description: "People ID" }),
|
|
417
|
+
input: Type.Object({
|
|
418
|
+
firstname: Type.Optional(Type.String()),
|
|
419
|
+
middlename: Type.Optional(Type.String()),
|
|
420
|
+
lastname: Type.Optional(Type.String()),
|
|
421
|
+
dob: Type.Optional(Type.String({ description: "Date of birth (YYYY-MM-DD)" })),
|
|
422
|
+
organization: Type.Optional(Type.String()),
|
|
423
|
+
contacts: Type.Optional(Type.Array(Type.Object({
|
|
424
|
+
value: Type.String({ description: "Phone number, email, etc." }),
|
|
425
|
+
contacts_types_id: Type.Union([Type.String(), Type.Number()], { description: "Contact type ID (call kanvas_list_contact_types)" }),
|
|
426
|
+
weight: Type.Optional(Type.Number()),
|
|
427
|
+
is_opt_out: Type.Optional(Type.Boolean()),
|
|
428
|
+
}))),
|
|
429
|
+
tags: Type.Optional(Type.Array(Type.Object({ name: Type.String() }))),
|
|
430
|
+
custom_fields: Type.Optional(Type.Array(Type.Record(Type.String(), Type.Unknown()))),
|
|
431
|
+
}),
|
|
432
|
+
}),
|
|
433
|
+
async execute(_id, params) {
|
|
434
|
+
await ensureAuth();
|
|
435
|
+
return toolResult(await service.updatePeople(params.id, params.input));
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
api.registerTool({
|
|
439
|
+
name: "kanvas_list_people_relationships",
|
|
440
|
+
label: "List People Relationships",
|
|
441
|
+
description: "List available relationship types for lead participants (e.g. Architect, Client, Consultant).",
|
|
442
|
+
parameters: Type.Object({
|
|
443
|
+
first: Type.Optional(Type.Number({ description: "Max results (default 50)" })),
|
|
444
|
+
}),
|
|
445
|
+
async execute(_id, params) {
|
|
446
|
+
await ensureAuth();
|
|
447
|
+
return toolResult(await service.listPeopleRelationships(params.first));
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
api.registerTool({
|
|
451
|
+
name: "kanvas_list_contact_types",
|
|
452
|
+
label: "List Contact Types",
|
|
453
|
+
description: "List available contact types (email, phone, etc.) needed for adding contacts to people.",
|
|
454
|
+
parameters: Type.Object({
|
|
455
|
+
first: Type.Optional(Type.Number({ description: "Max results (default 50)" })),
|
|
456
|
+
}),
|
|
457
|
+
async execute(_id, params) {
|
|
458
|
+
await ensureAuth();
|
|
459
|
+
return toolResult(await service.listContactTypes(params.first));
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
// --- Events / Follow-ups ---
|
|
463
|
+
api.registerTool({
|
|
464
|
+
name: "kanvas_create_follow_up",
|
|
465
|
+
label: "Create Follow-up",
|
|
466
|
+
description: "Schedule a follow-up reminder as a calendar event. Optionally link to a lead. " +
|
|
467
|
+
"Use this for any action that needs to happen on a future date — the team can see it in the Kanvas dashboard.",
|
|
468
|
+
parameters: Type.Object({
|
|
469
|
+
name: Type.String({ description: 'Follow-up name (e.g. "Call Jane Doe re: proposal")' }),
|
|
470
|
+
description: Type.Optional(Type.String({ description: "Details/notes about the follow-up" })),
|
|
471
|
+
date: Type.String({ description: "Date (YYYY-MM-DD)" }),
|
|
472
|
+
start_time: Type.String({ description: "Start time (HH:MM, 24h format)" }),
|
|
473
|
+
end_time: Type.String({ description: "End time (HH:MM, 24h format)" }),
|
|
474
|
+
lead_id: Type.Union([Type.String(), Type.Number()], { description: "Lead ID to link this follow-up to" }),
|
|
475
|
+
}),
|
|
476
|
+
async execute(_id, params) {
|
|
477
|
+
await ensureAuth();
|
|
478
|
+
return toolResult(await service.createFollowUpEvent(params));
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
api.registerTool({
|
|
482
|
+
name: "kanvas_list_events",
|
|
483
|
+
label: "List Events",
|
|
484
|
+
description: "List scheduled events/follow-ups with optional filtering.",
|
|
485
|
+
parameters: Type.Object({
|
|
486
|
+
first: Type.Optional(Type.Number({ description: "Max results (default 25)" })),
|
|
487
|
+
where: Type.Optional(Type.Array(Type.Object({
|
|
488
|
+
column: Type.String({ description: 'e.g. "ID", "NAME"' }),
|
|
489
|
+
operator: Type.String({ description: 'e.g. "EQ", "LIKE"' }),
|
|
490
|
+
value: Type.Unknown(),
|
|
491
|
+
}))),
|
|
492
|
+
}),
|
|
493
|
+
async execute(_id, params) {
|
|
494
|
+
await ensureAuth();
|
|
495
|
+
return toolResult(await service.listEvents(params.first, params.where));
|
|
496
|
+
},
|
|
497
|
+
});
|
|
343
498
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kanvas/openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Connects agents to Kanvas — your company's nervous system for CRM, inventory, orders, and messaging.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"keywords": ["openclaw", "kanvas", "crm", "plugin", "ai-agent"],
|
|
11
11
|
"type": "module",
|
|
12
12
|
"openclaw": {
|
|
13
|
+
"id": "kanvas",
|
|
13
14
|
"extensions": [
|
|
14
15
|
"./dist/index.js"
|
|
15
16
|
]
|