@kanvas/openclaw-plugin 0.1.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 ADDED
@@ -0,0 +1,193 @@
1
+ # Kanvas OpenClaw Plugin
2
+
3
+ OpenClaw plugin that connects AI agents to [Kanvas](https://kanvas.dev) — your company's nervous system. Kanvas is the operational engine that connects all your data, tools, and workflows. For AI agents, it's what lets them actually run your business — not just talk about it.
4
+
5
+ This plugin gives agents direct access to CRM, inventory, orders, and messaging so they can search leads, create contacts, check stock, track orders, store structured data, and send emails — all through 41 tools with auto-login and built-in system prompt context.
6
+
7
+ ## Quick Start
8
+
9
+ ### 1. Install the plugin
10
+
11
+ ```bash
12
+ # From local directory (development)
13
+ openclaw plugins install /path/to/kanvas-openclaw-plugin --link
14
+
15
+ # From npm (once published)
16
+ openclaw plugins install kanvas-openclaw-plugin
17
+ ```
18
+
19
+ ### 2. Configure credentials
20
+
21
+ In your OpenClaw config file:
22
+
23
+ ```json5
24
+ {
25
+ plugins: {
26
+ entries: {
27
+ "kanvas": {
28
+ enabled: true,
29
+ config: {
30
+ xKanvasApp: "your-app-id",
31
+ email: "agent@yourcompany.com",
32
+ password: "agent-password",
33
+ // Optional:
34
+ // xKanvasKey: "your-app-key" // needed for kanvas_send_anonymous_email
35
+ // xKanvasLocation: "branch-uuid"
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ The API URL defaults to `https://graphapi.kanvas.dev/graphql`. The plugin authenticates automatically on the first tool call using the Kanvas login mutation.
44
+
45
+ ### 3. Set up the agent's system prompt
46
+
47
+ The plugin injects tool documentation into the agent's context automatically. But the agent's **base system prompt** (configured in OpenClaw) should tell it to use Kanvas. Here's a sample:
48
+
49
+ ```
50
+ You are a sales and operations agent for [Company Name]. You manage leads,
51
+ inventory, and orders using Kanvas.
52
+
53
+ Your responsibilities:
54
+ - Register and manage leads in the CRM pipeline
55
+ - Look up products, stock levels, and pricing
56
+ - Check and track orders
57
+ - Add notes and messages to leads
58
+ - Send emails to customers and prospects
59
+
60
+ When users ask you to do any of these things, use the kanvas_* tools to act
61
+ on it directly. Don't just describe what you would do — actually do it.
62
+
63
+ Before creating a lead, always check kanvas_list_pipelines to get valid
64
+ pipeline stage IDs, and kanvas_list_lead_sources for source IDs.
65
+
66
+ When you don't have enough information to complete an action (e.g. missing
67
+ a required field), ask the user for the missing details before proceeding.
68
+ ```
69
+
70
+ Customize this for your business — add your company name, specific workflows, and any domain-specific instructions.
71
+
72
+ ## Tools Reference
73
+
74
+ ### CRM (22 tools)
75
+
76
+ | Tool | Description |
77
+ |------|-------------|
78
+ | `kanvas_search_leads` | Search leads by keyword |
79
+ | `kanvas_get_lead` | Full lead detail (pipeline, owner, participants, files, events) |
80
+ | `kanvas_create_lead` | Create a new lead |
81
+ | `kanvas_update_lead` | Update lead fields |
82
+ | `kanvas_change_lead_owner` | Reassign lead owner |
83
+ | `kanvas_change_lead_receiver` | Reassign lead receiver |
84
+ | `kanvas_add_lead_participant` | Add a person to a lead |
85
+ | `kanvas_remove_lead_participant` | Remove a person from a lead |
86
+ | `kanvas_follow_lead` | Subscribe to lead updates |
87
+ | `kanvas_unfollow_lead` | Unsubscribe from lead updates |
88
+ | `kanvas_delete_lead` | Soft-delete a lead |
89
+ | `kanvas_restore_lead` | Restore a deleted lead |
90
+ | `kanvas_mark_lead_outcome` | Mark as Won, Lost, or Close |
91
+ | `kanvas_create_lead_appointment` | Create a calendar event for a lead |
92
+ | `kanvas_add_lead_message` | Add note to a lead channel |
93
+ | `kanvas_add_lead_note_by_lead_id` | Add note by lead ID (auto-resolves channel) |
94
+ | `kanvas_list_lead_messages` | List messages in a lead channel |
95
+ | `kanvas_get_lead_primary_channel_slug` | Get the main channel slug for a lead |
96
+ | `kanvas_list_pipelines` | List pipelines and stages |
97
+ | `kanvas_list_lead_statuses` | List lead statuses |
98
+ | `kanvas_list_lead_sources` | List lead sources |
99
+ | `kanvas_list_lead_types` | List lead types |
100
+
101
+ ### Social / Messages (9 tools)
102
+
103
+ Messages act as NoSQL-like document storage — the `message` field accepts any JSON structure.
104
+
105
+ | Tool | Description |
106
+ |------|-------------|
107
+ | `kanvas_create_message` | Create a message with arbitrary JSON payload |
108
+ | `kanvas_get_message` | Get message with metadata, files, children |
109
+ | `kanvas_update_message` | Update message content or metadata |
110
+ | `kanvas_delete_message` | Soft-delete a message |
111
+ | `kanvas_list_channel_messages` | List messages by channel slug |
112
+ | `kanvas_search_messages` | Search/filter by type, channel, or entity |
113
+ | `kanvas_list_message_types` | List available message verbs |
114
+ | `kanvas_create_message_type` | Create a new verb with optional template |
115
+ | `kanvas_send_anonymous_email` | Send email via template (requires `xKanvasKey`) |
116
+
117
+ ### Inventory (7 tools)
118
+
119
+ | Tool | Description |
120
+ |------|-------------|
121
+ | `kanvas_search_products` | Search products by keyword |
122
+ | `kanvas_get_product` | Full product detail with variants and warehouses |
123
+ | `kanvas_list_variants` | List variants with pricing and stock |
124
+ | `kanvas_list_warehouses` | List stock locations |
125
+ | `kanvas_list_channels` | List sales channels |
126
+ | `kanvas_list_categories` | List product categories |
127
+ | `kanvas_list_inventory_statuses` | List product statuses |
128
+
129
+ ### Orders (2 tools)
130
+
131
+ | Tool | Description |
132
+ |------|-------------|
133
+ | `kanvas_search_orders` | Search orders by number or keyword |
134
+ | `kanvas_get_order` | Full order detail with items, customer, status |
135
+
136
+ ### Diagnostics (1 tool)
137
+
138
+ | Tool | Description |
139
+ |------|-------------|
140
+ | `kanvas_test_connection` | Verify API connectivity |
141
+
142
+ ## Authentication
143
+
144
+ The plugin supports three auth modes:
145
+
146
+ | Mode | Config fields | When to use |
147
+ |------|--------------|-------------|
148
+ | **Email/password** (recommended) | `email`, `password` | Agent logs in like a user. Token cached for the session. |
149
+ | **Bearer token** | `bearerToken` | Pre-authenticated service account. |
150
+ | **App key** | `authMode: "app-key"`, `xKanvasKey` | App-scoped access. Required for `kanvas_send_anonymous_email`. |
151
+
152
+ With email/password, the plugin calls the Kanvas `login` mutation on the first tool invocation and caches the bearer token — no hardcoded tokens needed.
153
+
154
+ ## Configuration Reference
155
+
156
+ | Field | Required | Default | Description |
157
+ |-------|----------|---------|-------------|
158
+ | `xKanvasApp` | Yes | — | App/tenant identifier |
159
+ | `email` | Yes* | — | Agent user email |
160
+ | `password` | Yes* | — | Agent user password |
161
+ | `apiUrl` | No | `https://graphapi.kanvas.dev/graphql` | GraphQL endpoint |
162
+ | `xKanvasLocation` | No | — | Branch/location UUID |
163
+ | `xKanvasKey` | No | — | App key (for anonymous email) |
164
+ | `bearerToken` | No | — | Pre-existing token (skips login) |
165
+ | `authMode` | No | `bearer` | `bearer` or `app-key` |
166
+ | `timeoutMs` | No | `15000` | Request timeout in ms |
167
+
168
+ *Required when using email/password auth (recommended). Not needed if using `bearerToken` or `app-key` mode.
169
+
170
+ All fields also fall back to `KANVAS_*` environment variables.
171
+
172
+ ## Development
173
+
174
+ ```bash
175
+ npm run build # Compile TypeScript to dist/
176
+ npm run check # Type-check without emitting
177
+ npm run dev # Watch mode
178
+ ```
179
+
180
+ ### Smoke Test
181
+
182
+ Test against the real API:
183
+
184
+ ```bash
185
+ KANVAS_X_APP=your-app-id \
186
+ KANVAS_EMAIL=your@email.com \
187
+ KANVAS_PASSWORD=yourpassword \
188
+ npx ts-node --esm scripts/smoke-test.ts
189
+ ```
190
+
191
+ ## License
192
+
193
+ Private — see package.json.
@@ -0,0 +1,75 @@
1
+ import * as readline from "node:readline/promises";
2
+ import { stdin, stdout } from "node:process";
3
+ import { KanvasClient } from "../client/kanvas-client.js";
4
+ const DEFAULT_API_URL = "https://graphapi.kanvas.dev/graphql";
5
+ async function prompt(rl, question, defaultValue) {
6
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
7
+ const answer = await rl.question(` ${question}${suffix}: `);
8
+ return answer.trim() || defaultValue || "";
9
+ }
10
+ export async function runSetup() {
11
+ const rl = readline.createInterface({ input: stdin, output: stdout });
12
+ try {
13
+ console.log("\n Kanvas Plugin Setup\n ───────────────────\n");
14
+ const apiUrl = await prompt(rl, "API URL", DEFAULT_API_URL);
15
+ const xKanvasApp = await prompt(rl, "App ID (X-Kanvas-App)");
16
+ if (!xKanvasApp) {
17
+ throw new Error("App ID is required");
18
+ }
19
+ const email = await prompt(rl, "Agent email");
20
+ if (!email) {
21
+ throw new Error("Email is required");
22
+ }
23
+ const password = await prompt(rl, "Agent password");
24
+ if (!password) {
25
+ throw new Error("Password is required");
26
+ }
27
+ const xKanvasLocation = await prompt(rl, "Branch/Location UUID (optional)");
28
+ const xKanvasKey = await prompt(rl, "App Key (optional, for anonymous email)");
29
+ // Test the credentials
30
+ console.log("\n Testing connection...");
31
+ const client = new KanvasClient({
32
+ apiUrl,
33
+ xKanvasApp,
34
+ authMode: "bearer",
35
+ });
36
+ const session = await client.login(email, password);
37
+ console.log(` Authenticated as ${session.uuid}`);
38
+ const conn = await client.testConnection();
39
+ if (!conn.ok) {
40
+ console.error(` Connection test failed: ${conn.errors.join(", ")}`);
41
+ }
42
+ else {
43
+ console.log(" Connection OK\n");
44
+ }
45
+ const result = { apiUrl, xKanvasApp, email, password };
46
+ if (xKanvasLocation)
47
+ result.xKanvasLocation = xKanvasLocation;
48
+ if (xKanvasKey)
49
+ result.xKanvasKey = xKanvasKey;
50
+ // Print the config for the user to copy
51
+ console.log(" Add this to your OpenClaw config:\n");
52
+ console.log(" plugins:");
53
+ console.log(" entries:");
54
+ console.log(" kanvas:");
55
+ console.log(" enabled: true");
56
+ console.log(" config:");
57
+ if (apiUrl !== DEFAULT_API_URL) {
58
+ console.log(` apiUrl: "${apiUrl}"`);
59
+ }
60
+ console.log(` xKanvasApp: "${xKanvasApp}"`);
61
+ console.log(` email: "${email}"`);
62
+ console.log(` password: "${password}"`);
63
+ if (xKanvasLocation) {
64
+ console.log(` xKanvasLocation: "${xKanvasLocation}"`);
65
+ }
66
+ if (xKanvasKey) {
67
+ console.log(` xKanvasKey: "${xKanvasKey}"`);
68
+ }
69
+ console.log("");
70
+ return result;
71
+ }
72
+ finally {
73
+ rl.close();
74
+ }
75
+ }
@@ -0,0 +1,25 @@
1
+ export function buildKanvasHeaders(config, override = {}) {
2
+ const headers = {
3
+ "Content-Type": "application/json",
4
+ "X-Kanvas-App": config.xKanvasApp,
5
+ };
6
+ const location = override.xKanvasLocation ?? config.xKanvasLocation;
7
+ if (location) {
8
+ headers["X-Kanvas-Location"] = location;
9
+ }
10
+ if (config.authMode === "bearer") {
11
+ const token = override.bearerToken ?? config.bearerToken;
12
+ if (!token) {
13
+ throw new Error("Bearer auth mode selected but no bearer token is configured");
14
+ }
15
+ headers.Authorization = `Bearer ${token}`;
16
+ }
17
+ if (config.authMode === "app-key") {
18
+ const appKey = override.xKanvasKey ?? config.xKanvasKey;
19
+ if (!appKey) {
20
+ throw new Error("App-key auth mode selected but no X-Kanvas-Key is configured");
21
+ }
22
+ headers["X-Kanvas-Key"] = appKey;
23
+ }
24
+ return headers;
25
+ }
@@ -0,0 +1,137 @@
1
+ import { buildKanvasHeaders } from "./headers.js";
2
+ export class KanvasClient {
3
+ config;
4
+ constructor(config) {
5
+ this.config = config;
6
+ }
7
+ getConfig() {
8
+ return this.config;
9
+ }
10
+ /**
11
+ * Authenticate with email/password and store the bearer token.
12
+ * The login mutation is public (no @guard), only needs X-Kanvas-App.
13
+ */
14
+ async login(email, password) {
15
+ const mutation = `
16
+ mutation Login($data: LoginInput!) {
17
+ login(data: $data) {
18
+ id
19
+ uuid
20
+ token
21
+ refresh_token
22
+ token_expires
23
+ refresh_token_expires
24
+ time
25
+ timezone
26
+ sessionId
27
+ }
28
+ }
29
+ `;
30
+ const response = await fetch(this.config.apiUrl, {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ "X-Kanvas-App": this.config.xKanvasApp,
35
+ },
36
+ body: JSON.stringify({
37
+ query: mutation,
38
+ variables: { data: { email, password } },
39
+ }),
40
+ });
41
+ const payload = (await response.json());
42
+ if (payload.errors?.length) {
43
+ throw new Error(`Kanvas login failed: ${payload.errors.map((e) => e.message).join(", ")}`);
44
+ }
45
+ if (!payload.data?.login?.token) {
46
+ throw new Error("Kanvas login failed: no token returned");
47
+ }
48
+ this.config = {
49
+ ...this.config,
50
+ authMode: "bearer",
51
+ bearerToken: payload.data.login.token,
52
+ };
53
+ return payload.data.login;
54
+ }
55
+ /**
56
+ * Execute a query using X-Kanvas-Key auth (for @guardByAppKey mutations).
57
+ * Requires xKanvasKey to be configured.
58
+ */
59
+ async queryWithAppKey(query, variables = {}) {
60
+ const appKey = this.config.xKanvasKey;
61
+ if (!appKey) {
62
+ throw new Error("xKanvasKey is required for this operation (app-key authenticated mutation)");
63
+ }
64
+ const headers = {
65
+ "Content-Type": "application/json",
66
+ "X-Kanvas-App": this.config.xKanvasApp,
67
+ "X-Kanvas-Key": appKey,
68
+ };
69
+ const location = this.config.xKanvasLocation;
70
+ if (location) {
71
+ headers["X-Kanvas-Location"] = location;
72
+ }
73
+ const controller = new AbortController();
74
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
75
+ try {
76
+ const response = await fetch(this.config.apiUrl, {
77
+ method: "POST",
78
+ headers,
79
+ body: JSON.stringify({ query, variables }),
80
+ signal: controller.signal,
81
+ });
82
+ return (await response.json());
83
+ }
84
+ finally {
85
+ clearTimeout(timeout);
86
+ }
87
+ }
88
+ async query(query, variables = {}, override = {}) {
89
+ const controller = new AbortController();
90
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
91
+ try {
92
+ const response = await fetch(this.config.apiUrl, {
93
+ method: "POST",
94
+ headers: buildKanvasHeaders(this.config, override),
95
+ body: JSON.stringify({ query, variables }),
96
+ signal: controller.signal,
97
+ });
98
+ return (await response.json());
99
+ }
100
+ finally {
101
+ clearTimeout(timeout);
102
+ }
103
+ }
104
+ async testConnection(override = {}) {
105
+ const query = `query PluginConnectionTest { __typename }`;
106
+ const controller = new AbortController();
107
+ const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs ?? 15000);
108
+ try {
109
+ const response = await fetch(this.config.apiUrl, {
110
+ method: "POST",
111
+ headers: buildKanvasHeaders(this.config, override),
112
+ body: JSON.stringify({ query, variables: {} }),
113
+ signal: controller.signal,
114
+ });
115
+ const payload = (await response.json());
116
+ return {
117
+ ok: response.ok && !payload.errors?.length,
118
+ status: response.status,
119
+ endpoint: this.config.apiUrl,
120
+ hasData: Boolean(payload.data),
121
+ errors: payload.errors?.map((error) => error.message) ?? [],
122
+ };
123
+ }
124
+ catch (error) {
125
+ return {
126
+ ok: false,
127
+ status: 0,
128
+ endpoint: this.config.apiUrl,
129
+ hasData: false,
130
+ errors: [error instanceof Error ? error.message : "Unknown connection error"],
131
+ };
132
+ }
133
+ finally {
134
+ clearTimeout(timeout);
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,37 @@
1
+ import { buildKanvasHeaders } from "./headers.js";
2
+ export async function postGraphQLMultipart(options) {
3
+ const form = new FormData();
4
+ form.append("operations", JSON.stringify({
5
+ query: options.query,
6
+ variables: options.variables,
7
+ }));
8
+ const map = {};
9
+ options.files.forEach((file, index) => {
10
+ map[String(index)] = [file.key];
11
+ });
12
+ form.append("map", JSON.stringify(map));
13
+ options.files.forEach((file, index) => {
14
+ let blob;
15
+ if (file.content instanceof Blob) {
16
+ blob = file.content;
17
+ }
18
+ else if (typeof file.content === "string") {
19
+ blob = new Blob([file.content], { type: file.contentType ?? "text/plain" });
20
+ }
21
+ else {
22
+ const uint8 = file.content instanceof Uint8Array ? file.content : new Uint8Array(file.content);
23
+ blob = new Blob([uint8], {
24
+ type: file.contentType ?? "application/octet-stream",
25
+ });
26
+ }
27
+ form.append(String(index), blob, file.fileName);
28
+ });
29
+ const headers = buildKanvasHeaders(options.config, options.override);
30
+ delete headers["Content-Type"];
31
+ const response = await fetch(options.config.apiUrl, {
32
+ method: "POST",
33
+ headers,
34
+ body: form,
35
+ });
36
+ return (await response.json());
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ function required(name, value) {
2
+ if (!value) {
3
+ throw new Error(`Missing required environment variable: ${name}`);
4
+ }
5
+ return value;
6
+ }
7
+ export function loadConfigFromEnv(env = process.env) {
8
+ const authMode = (env.KANVAS_AUTH_MODE ?? "bearer");
9
+ return {
10
+ apiUrl: required("KANVAS_API_URL", env.KANVAS_API_URL),
11
+ xKanvasApp: required("KANVAS_X_APP", env.KANVAS_X_APP),
12
+ xKanvasLocation: env.KANVAS_X_LOCATION,
13
+ authMode,
14
+ bearerToken: env.KANVAS_BEARER_TOKEN,
15
+ xKanvasKey: env.KANVAS_X_KEY,
16
+ timeoutMs: env.KANVAS_TIMEOUT_MS ? Number(env.KANVAS_TIMEOUT_MS) : 15000,
17
+ };
18
+ }
@@ -0,0 +1 @@
1
+ export {};