@kivimedia/kmhub 2.9.0 → 2.10.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 +170 -170
- package/bin/kmhub.mjs +896 -896
- package/coach-book-output-guard.mjs +760 -760
- package/index.mjs +57 -57
- package/package.json +56 -56
- package/prompts/briefing.md +29 -29
- package/prompts/luxury.md +70 -70
- package/prompts/play.md +49 -49
- package/prompts/run.md +36 -36
- package/prompts/setup.md +33 -33
- package/prompts/vs-booked.md +46 -46
- package/prompts/what-can-you-do.md +40 -40
- package/prompts.mjs +110 -109
- package/read-only-tools.json +143 -142
- package/remote.mjs +929 -929
- package/tools/balloon-costing.mjs +80 -80
- package/tools/booking-equipment.mjs +110 -110
- package/tools/bridges.mjs +54 -54
- package/tools/briefing.mjs +91 -91
- package/tools/calendar.mjs +170 -170
- package/tools/capabilities.mjs +155 -155
- package/tools/catalog.mjs +288 -288
- package/tools/clubs.mjs +176 -176
- package/tools/coach.mjs +771 -771
- package/tools/compare.mjs +76 -76
- package/tools/core.mjs +244 -244
- package/tools/crm.mjs +209 -209
- package/tools/dubsado.mjs +137 -137
- package/tools/exports.mjs +128 -128
- package/tools/fact-review.mjs +125 -125
- package/tools/flows.mjs +261 -261
- package/tools/forms.mjs +158 -158
- package/tools/gols.mjs +134 -134
- package/tools/hr.mjs +162 -162
- package/tools/knowledge.mjs +125 -125
- package/tools/marketing.mjs +396 -396
- package/tools/meta.mjs +245 -245
- package/tools/military.mjs +244 -244
- package/tools/money.mjs +235 -197
- package/tools/outreach.mjs +238 -238
- package/tools/pending.mjs +122 -122
- package/tools/photos.mjs +140 -140
- package/tools/plays.mjs +244 -244
- package/tools/profile.mjs +118 -118
- package/tools/radar.mjs +173 -173
- package/tools/recurring-invoices.mjs +149 -149
- package/tools/reengage.mjs +434 -434
- package/tools/schedules.mjs +55 -55
- package/tools/setup.mjs +168 -168
- package/tools/sops-bridges.mjs +86 -86
- package/tools/sops.mjs +314 -314
- package/tools/sourcing.mjs +268 -268
- package/tools/strategy.mjs +146 -146
- package/tools/studio.mjs +132 -132
- package/tools/venueradar.mjs +151 -151
- package/tools/voice.mjs +134 -134
- package/tools.mjs +407 -407
package/tools/core.mjs
CHANGED
|
@@ -1,244 +1,244 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool family: core - orientation plus the CRM essentials.
|
|
3
|
-
*
|
|
4
|
-
* These are the original ten kmhub-mcp tools (server 1.2.0), moved here verbatim
|
|
5
|
-
* when the tool set was split into families. Tool names, descriptions, zod schemas,
|
|
6
|
-
* call paths and registration order are all unchanged, so a client that was already
|
|
7
|
-
* connected sees exactly the same ten tools it saw before the split.
|
|
8
|
-
*
|
|
9
|
-
* The family contract this file follows is documented in ./README.md.
|
|
10
|
-
*/
|
|
11
|
-
import { z } from 'zod';
|
|
12
|
-
|
|
13
|
-
export const FAMILY = 'core';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Order preserved from the original tools.mjs TOOL_NAMES export, so the health
|
|
17
|
-
* endpoint and the startup log keep printing the same list in the same order.
|
|
18
|
-
*/
|
|
19
|
-
export const TOOLS = [
|
|
20
|
-
'km_me',
|
|
21
|
-
'km_workspaces',
|
|
22
|
-
'km_waiting',
|
|
23
|
-
'km_list_clients',
|
|
24
|
-
'km_create_lead',
|
|
25
|
-
'km_list_deals',
|
|
26
|
-
'km_list_events',
|
|
27
|
-
'km_list_bookings',
|
|
28
|
-
'km_create_inquiry',
|
|
29
|
-
'km_create_booking',
|
|
30
|
-
'km_create_outreach_draft',
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
// Orientation and the CRM floor belong in every profile: you always need to know
|
|
34
|
-
// whose workspace this is and what is waiting, whatever else you loaded.
|
|
35
|
-
export const PROFILES = ['*'];
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @param {{ tool: Function }} server guarded registrar (see ./README.md)
|
|
39
|
-
* @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
|
|
40
|
-
* @param {{ out: Function }} helpers shared response helpers from ../tools.mjs
|
|
41
|
-
*/
|
|
42
|
-
export function register(server, call, { out }) {
|
|
43
|
-
server.tool(
|
|
44
|
-
'km_me',
|
|
45
|
-
'Which KM Hub workspace this connection actually points at: its id, the business name on it, and the plan it is on. ' +
|
|
46
|
-
'Call it once at the start of a session so you know whose business you are speaking about, and call it again the moment anything looks wrong - an empty client list, a diary with nothing in it, figures that do not match what the user is describing - because the commonest cause of all three is a key pointing at a different workspace than the one they mean. ' +
|
|
47
|
-
'It does not tell you which human is at the keyboard, it does not list the users on the account, and the plan it reports is the subscription, not how much of this month\'s allowance is left. ' +
|
|
48
|
-
'It is cheap, it reads, and it changes nothing.',
|
|
49
|
-
{},
|
|
50
|
-
async () => out(await call('GET', '/me')),
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
server.tool(
|
|
54
|
-
'km_workspaces',
|
|
55
|
-
'Every KM Hub workspace the person at this keyboard belongs to, which single one this connector actually opens, and how to reach the others. ' +
|
|
56
|
-
'Call it the moment someone talks about a second brand, expects data that is not here, says they switched workspace in the web app, or asks why the numbers look like the wrong business. ' +
|
|
57
|
-
'The thing it exists to explain: an API key is welded to ONE workspace, so switching brand in the KM Hub web app does not move this connection and never will - the key carries the workspace, not the browser session. Someone can restart Claude all day and still be in the same brand. ' +
|
|
58
|
-
'It hands out no access and no keys: names and ids only. Reaching another workspace means installing a second connector with a key from that workspace, which the note in the response explains. ' +
|
|
59
|
-
'It is cheap, it reads, and it changes nothing.',
|
|
60
|
-
{},
|
|
61
|
-
async () => out(await call('GET', '/workspaces')),
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
server.tool(
|
|
65
|
-
'km_waiting',
|
|
66
|
-
'Raw gate counts and nothing else: how many approvals, new enquiries, open tasks, unpaid invoices and unsigned contracts are sitting in the workspace right now. It is a gauge, not an answer. ' +
|
|
67
|
-
'Do NOT reach for this on the broad question. If the user says "what needs me today", "catch me up", "where are we", "what should I do first", "anything urgent", "what did I miss", or opens a session with no specific task, km_briefing is the tool: it returns these same counts AND the actual items behind them, in one list ranked in the order a person should deal with them, each with the reason it is there. Answering the broad question from a set of counts means guessing which of the numbers mattered, which is exactly the guess km_briefing exists to remove. ' +
|
|
68
|
-
'Reach for km_waiting only when you genuinely want the bare numbers on their own: to say whether anything has changed since earlier in the same conversation, or when km_briefing has told you it is not available on this workspace. ' +
|
|
69
|
-
'A count tells you something is waiting. It never tells you which one is worth doing first, and it carries no client, no amount, no date and no id, so nothing here can be opened or acted on without a second call. Read only.',
|
|
70
|
-
{},
|
|
71
|
-
async () => out(await call('GET', '/waiting')),
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
server.tool(
|
|
75
|
-
'km_list_clients',
|
|
76
|
-
'The most recently ADDED clients and leads, newest first. This is a recency window, NOT a search. ' +
|
|
77
|
-
'It walks back from the newest record and stops, so a contact added a year ago will not appear whatever you set limit to, and even at the maximum of 100 it shows the newest 100 and nothing behind them. ' +
|
|
78
|
-
'That means it CANNOT answer "do I have X", "find me the person at Y" or "what is that magician called", and treating a miss here as proof somebody is not in the workspace is the easiest way to tell a user something false about their own business. km_find_contact is the tool for finding a named person, a company or part of an email, and it searches all of them rather than the newest few. ' +
|
|
79
|
-
'Reach for km_list_clients only for the genuinely recency-shaped question: who has come in lately, what do the last few enquiries look like, has anything landed since we spoke. ' +
|
|
80
|
-
'It returns the record as it stands, not the history behind it: km_get_client opens one in full. Read only.',
|
|
81
|
-
{ limit: z.number().int().min(1).max(100).optional() },
|
|
82
|
-
async ({ limit }) => out(await call('GET', `/clients?limit=${limit ?? 25}`)),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
server.tool(
|
|
86
|
-
'km_create_lead',
|
|
87
|
-
'Put a person on file as a contact with nothing attached to them: no enquiry, no deal, no date, no money. ' +
|
|
88
|
-
'Reach for it when the user has met somebody and wants them kept - a card from a convention, a name off a phone call - and there is nothing being sold yet. ' +
|
|
89
|
-
'Search with km_find_contact FIRST. This tool will happily create a second Sarah Jones alongside the one already there, and a split contact is worse than no contact: half her history sits on a record nobody opens. ' +
|
|
90
|
-
'It is the wrong tool when there is actually something to sell. If the person is asking about a date, use km_create_inquiry, which creates the contact AND opens the pipeline deal behind it. If the gig is already agreed, use km_create_booking. ' +
|
|
91
|
-
'Every field is optional, but a contact with no email and no phone can never be written to, so ask for one rather than saving a bare name. ' +
|
|
92
|
-
'This WRITES: it needs the API key to carry the write scope, and it lands in the AI activity log where the owner can see and undo it.',
|
|
93
|
-
{
|
|
94
|
-
first_name: z.string().optional(),
|
|
95
|
-
last_name: z.string().optional(),
|
|
96
|
-
company: z.string().optional(),
|
|
97
|
-
email: z.string().optional(),
|
|
98
|
-
phone: z.string().optional(),
|
|
99
|
-
notes: z.string().optional(),
|
|
100
|
-
},
|
|
101
|
-
async (args) => out(await call('POST', '/clients', args)),
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
server.tool(
|
|
105
|
-
'km_list_bookings',
|
|
106
|
-
'The real jobs: the bookings on this business, each with its client, its date, its venue and the money on it. ' +
|
|
107
|
-
'Reach for it for the book of business rather than for the diary - what has been booked lately, what is still only pending a confirmation, every gig inside a date range so you can see the shape of a season, what this business has actually done before. ' +
|
|
108
|
-
'from and to filter on the EVENT date, not on when the booking was taken, so from today forward is the work ahead and to yesterday is the history. Use status to separate the confirmed work from what is still pending. ' +
|
|
109
|
-
'It caps at 100 rows and does not report the true total, so a busy year needs a narrower window rather than a bigger limit, and if you asked for 100 and got 100, say the picture is partial instead of presenting a slice as the whole book. ' +
|
|
110
|
-
'It tells you what is BOOKED. It says nothing about whether a new date could be sold, because a free-looking day can still be blocked, out of area or already pencilled in by a live lead: km_check_availability is the only honest answer to that question. km_get_booking opens one booking in full, where that tool is loaded. Read only.',
|
|
111
|
-
{
|
|
112
|
-
limit: z.number().int().min(1).max(100).optional(),
|
|
113
|
-
status: z.string().optional().describe('e.g. confirmed, pending'),
|
|
114
|
-
from: z.string().optional().describe('Earliest event date, YYYY-MM-DD.'),
|
|
115
|
-
to: z.string().optional().describe('Latest event date, YYYY-MM-DD.'),
|
|
116
|
-
},
|
|
117
|
-
async ({ limit, status, from, to }) => {
|
|
118
|
-
const q = new URLSearchParams();
|
|
119
|
-
q.set('limit', String(limit ?? 25));
|
|
120
|
-
if (status) q.set('status', status);
|
|
121
|
-
if (from) q.set('from', from);
|
|
122
|
-
if (to) q.set('to', to);
|
|
123
|
-
return out(await call('GET', `/bookings?${q.toString()}`));
|
|
124
|
-
},
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
server.tool(
|
|
128
|
-
'km_list_deals',
|
|
129
|
-
'The pipeline: the live opportunities in this workspace and how far each one has got, newest first. ' +
|
|
130
|
-
'Reach for it for the shape of the funnel - what is open, what is stuck in quoting, what has been won, what was lost - and to get the deal_id that km_get_deal, km_create_outreach_draft and a play run all need before they can attach anything to the right opportunity. ' +
|
|
131
|
-
'stage narrows to one column of the board. status is the coarse open, won, lost, abandoned split. status lost, then km_get_deal on the big ones, is how you read WHY work was lost, which is almost always worth more than the count: a large deal lost on timing is a live target, a large deal lost on price is evidence about the rate. ' +
|
|
132
|
-
'It caps at 100 rows and does not report the true total, so if you asked for 100 and got 100, say the pipeline you are describing is partial. ' +
|
|
133
|
-
'It cannot search by a client\'s name: find the person with km_find_contact first, then work from their record. Read only.',
|
|
134
|
-
{
|
|
135
|
-
limit: z.number().int().min(1).max(100).optional(),
|
|
136
|
-
stage: z
|
|
137
|
-
.enum([
|
|
138
|
-
'inbound',
|
|
139
|
-
'qualifying',
|
|
140
|
-
'quoting',
|
|
141
|
-
'proposal_sent',
|
|
142
|
-
'booked',
|
|
143
|
-
'in_progress',
|
|
144
|
-
'completed',
|
|
145
|
-
'lost',
|
|
146
|
-
'abandoned',
|
|
147
|
-
])
|
|
148
|
-
.optional(),
|
|
149
|
-
status: z.enum(['open', 'won', 'lost', 'abandoned']).optional(),
|
|
150
|
-
},
|
|
151
|
-
async ({ limit, stage, status }) => {
|
|
152
|
-
const q = new URLSearchParams();
|
|
153
|
-
q.set('limit', String(limit ?? 25));
|
|
154
|
-
if (stage) q.set('stage', stage);
|
|
155
|
-
if (status) q.set('status', status);
|
|
156
|
-
return out(await call('GET', `/deals?${q.toString()}`));
|
|
157
|
-
},
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
server.tool(
|
|
161
|
-
'km_list_events',
|
|
162
|
-
'The raw contents of the KM Hub calendar over a date window, in start order: the gigs and everything else that has been put in the diary alongside them. ' +
|
|
163
|
-
'Always set from and to. With no window it returns whatever is most recent, which is almost never the question anybody asked. ' +
|
|
164
|
-
'Reach for it when the user wants the literal contents of a range: what is in September, is there anything at all on that weekend, how full does next month look. ' +
|
|
165
|
-
'For the ordinary "what have I got on" question, km_my_schedule is the better answer wherever it is loaded, because it groups the days and names the client, venue, times and fee on each gig rather than handing back a flat list. For whether ONE date can be sold, km_check_availability is the only honest source: this tool shows what already exists, and an empty-looking day is not the same as an available one. ' +
|
|
166
|
-
'Read only: it never adds, moves or cancels anything in the diary.',
|
|
167
|
-
{
|
|
168
|
-
from: z.string().optional().describe('Earliest start date, YYYY-MM-DD.'),
|
|
169
|
-
to: z.string().optional().describe('Latest start date, YYYY-MM-DD.'),
|
|
170
|
-
limit: z.number().int().min(1).max(200).optional(),
|
|
171
|
-
},
|
|
172
|
-
async ({ from, to, limit }) => {
|
|
173
|
-
const q = new URLSearchParams();
|
|
174
|
-
q.set('limit', String(limit ?? 50));
|
|
175
|
-
if (from) q.set('from', from);
|
|
176
|
-
if (to) q.set('to', to);
|
|
177
|
-
return out(await call('GET', `/events?${q.toString()}`));
|
|
178
|
-
},
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
server.tool(
|
|
182
|
-
'km_create_outreach_draft',
|
|
183
|
-
'Queue an outreach message as a DRAFT in KM Hub. This tool NEVER sends anything - the draft lands in the Approval Queue and a human approves it before any send happens. Audited in the AI activity log.',
|
|
184
|
-
{
|
|
185
|
-
body: z.string().describe('The message text (required).'),
|
|
186
|
-
subject: z.string().optional().describe('Subject line (email channel).'),
|
|
187
|
-
channel: z.enum(['email', 'linkedin', 'ig', 'sms_whatsapp']).optional().describe('Default email.'),
|
|
188
|
-
deal_id: z.string().optional().describe('UUID of the deal this draft belongs to (see km_list_deals).'),
|
|
189
|
-
contact_id: z.string().optional().describe('UUID of the client/contact (see km_find_contact).'),
|
|
190
|
-
},
|
|
191
|
-
async (args) => out(await call('POST', '/outreach-drafts', args)),
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
server.tool(
|
|
195
|
-
'km_create_inquiry',
|
|
196
|
-
'Log a new inquiry (lead + enquiry) in KM Hub - creates the client if needed and opens a pipeline deal. Audited and undoable from the AI activity log.',
|
|
197
|
-
{
|
|
198
|
-
client_name: z.string().describe('Full name of the person enquiring.'),
|
|
199
|
-
client_email: z.string().optional(),
|
|
200
|
-
client_phone: z.string().optional(),
|
|
201
|
-
event_type: z.string().optional().describe('e.g. wedding, corporate, birthday.'),
|
|
202
|
-
event_date: z.string().optional().describe('YYYY-MM-DD.'),
|
|
203
|
-
event_start_time: z.string().optional().describe('HH:MM 24h.'),
|
|
204
|
-
event_end_time: z.string().optional().describe('HH:MM 24h.'),
|
|
205
|
-
venue_name: z.string().optional(),
|
|
206
|
-
guest_count: z.number().optional(),
|
|
207
|
-
budget_cents: z.number().optional().describe('Budget in CENTS.'),
|
|
208
|
-
notes: z.string().optional(),
|
|
209
|
-
},
|
|
210
|
-
async (args) => out(await call('POST', '/inquiries', args)),
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
server.tool(
|
|
214
|
-
'km_create_booking',
|
|
215
|
-
'Create a REAL confirmed booking on the KM Hub calendar - creates the client if needed and opens a pipeline deal at booked/won. Requires an event_date. Only use when the gig is definitely booked; otherwise use km_create_inquiry.',
|
|
216
|
-
{
|
|
217
|
-
client_name: z.string().describe('Full name of the client.'),
|
|
218
|
-
client_email: z.string().optional(),
|
|
219
|
-
client_phone: z.string().optional(),
|
|
220
|
-
title: z.string().optional().describe('Booking title shown on the calendar.'),
|
|
221
|
-
event_type: z.string().optional(),
|
|
222
|
-
event_date: z.string().describe('YYYY-MM-DD (required for a real booking).'),
|
|
223
|
-
event_start_time: z.string().optional().describe('HH:MM 24h.'),
|
|
224
|
-
event_end_time: z.string().optional().describe('HH:MM 24h.'),
|
|
225
|
-
venue_name: z.string().optional(),
|
|
226
|
-
guest_count: z.number().optional(),
|
|
227
|
-
total_cents: z.number().optional().describe('Total in CENTS.'),
|
|
228
|
-
status: z
|
|
229
|
-
.enum(['pending', 'confirmed'])
|
|
230
|
-
.optional()
|
|
231
|
-
.describe('Default pending (owner reviews); confirmed puts it straight on the calendar.'),
|
|
232
|
-
confirm_token: z
|
|
233
|
-
.string()
|
|
234
|
-
.optional()
|
|
235
|
-
.describe(
|
|
236
|
-
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
237
|
-
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
238
|
-
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
239
|
-
+ 'arguments. A yes for one action never authorises a different one.',
|
|
240
|
-
),
|
|
241
|
-
},
|
|
242
|
-
async (args) => out(await call('POST', '/bookings', args)),
|
|
243
|
-
);
|
|
244
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: core - orientation plus the CRM essentials.
|
|
3
|
+
*
|
|
4
|
+
* These are the original ten kmhub-mcp tools (server 1.2.0), moved here verbatim
|
|
5
|
+
* when the tool set was split into families. Tool names, descriptions, zod schemas,
|
|
6
|
+
* call paths and registration order are all unchanged, so a client that was already
|
|
7
|
+
* connected sees exactly the same ten tools it saw before the split.
|
|
8
|
+
*
|
|
9
|
+
* The family contract this file follows is documented in ./README.md.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
13
|
+
export const FAMILY = 'core';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Order preserved from the original tools.mjs TOOL_NAMES export, so the health
|
|
17
|
+
* endpoint and the startup log keep printing the same list in the same order.
|
|
18
|
+
*/
|
|
19
|
+
export const TOOLS = [
|
|
20
|
+
'km_me',
|
|
21
|
+
'km_workspaces',
|
|
22
|
+
'km_waiting',
|
|
23
|
+
'km_list_clients',
|
|
24
|
+
'km_create_lead',
|
|
25
|
+
'km_list_deals',
|
|
26
|
+
'km_list_events',
|
|
27
|
+
'km_list_bookings',
|
|
28
|
+
'km_create_inquiry',
|
|
29
|
+
'km_create_booking',
|
|
30
|
+
'km_create_outreach_draft',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Orientation and the CRM floor belong in every profile: you always need to know
|
|
34
|
+
// whose workspace this is and what is waiting, whatever else you loaded.
|
|
35
|
+
export const PROFILES = ['*'];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {{ tool: Function }} server guarded registrar (see ./README.md)
|
|
39
|
+
* @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
|
|
40
|
+
* @param {{ out: Function }} helpers shared response helpers from ../tools.mjs
|
|
41
|
+
*/
|
|
42
|
+
export function register(server, call, { out }) {
|
|
43
|
+
server.tool(
|
|
44
|
+
'km_me',
|
|
45
|
+
'Which KM Hub workspace this connection actually points at: its id, the business name on it, and the plan it is on. ' +
|
|
46
|
+
'Call it once at the start of a session so you know whose business you are speaking about, and call it again the moment anything looks wrong - an empty client list, a diary with nothing in it, figures that do not match what the user is describing - because the commonest cause of all three is a key pointing at a different workspace than the one they mean. ' +
|
|
47
|
+
'It does not tell you which human is at the keyboard, it does not list the users on the account, and the plan it reports is the subscription, not how much of this month\'s allowance is left. ' +
|
|
48
|
+
'It is cheap, it reads, and it changes nothing.',
|
|
49
|
+
{},
|
|
50
|
+
async () => out(await call('GET', '/me')),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
server.tool(
|
|
54
|
+
'km_workspaces',
|
|
55
|
+
'Every KM Hub workspace the person at this keyboard belongs to, which single one this connector actually opens, and how to reach the others. ' +
|
|
56
|
+
'Call it the moment someone talks about a second brand, expects data that is not here, says they switched workspace in the web app, or asks why the numbers look like the wrong business. ' +
|
|
57
|
+
'The thing it exists to explain: an API key is welded to ONE workspace, so switching brand in the KM Hub web app does not move this connection and never will - the key carries the workspace, not the browser session. Someone can restart Claude all day and still be in the same brand. ' +
|
|
58
|
+
'It hands out no access and no keys: names and ids only. Reaching another workspace means installing a second connector with a key from that workspace, which the note in the response explains. ' +
|
|
59
|
+
'It is cheap, it reads, and it changes nothing.',
|
|
60
|
+
{},
|
|
61
|
+
async () => out(await call('GET', '/workspaces')),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
server.tool(
|
|
65
|
+
'km_waiting',
|
|
66
|
+
'Raw gate counts and nothing else: how many approvals, new enquiries, open tasks, unpaid invoices and unsigned contracts are sitting in the workspace right now. It is a gauge, not an answer. ' +
|
|
67
|
+
'Do NOT reach for this on the broad question. If the user says "what needs me today", "catch me up", "where are we", "what should I do first", "anything urgent", "what did I miss", or opens a session with no specific task, km_briefing is the tool: it returns these same counts AND the actual items behind them, in one list ranked in the order a person should deal with them, each with the reason it is there. Answering the broad question from a set of counts means guessing which of the numbers mattered, which is exactly the guess km_briefing exists to remove. ' +
|
|
68
|
+
'Reach for km_waiting only when you genuinely want the bare numbers on their own: to say whether anything has changed since earlier in the same conversation, or when km_briefing has told you it is not available on this workspace. ' +
|
|
69
|
+
'A count tells you something is waiting. It never tells you which one is worth doing first, and it carries no client, no amount, no date and no id, so nothing here can be opened or acted on without a second call. Read only.',
|
|
70
|
+
{},
|
|
71
|
+
async () => out(await call('GET', '/waiting')),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
server.tool(
|
|
75
|
+
'km_list_clients',
|
|
76
|
+
'The most recently ADDED clients and leads, newest first. This is a recency window, NOT a search. ' +
|
|
77
|
+
'It walks back from the newest record and stops, so a contact added a year ago will not appear whatever you set limit to, and even at the maximum of 100 it shows the newest 100 and nothing behind them. ' +
|
|
78
|
+
'That means it CANNOT answer "do I have X", "find me the person at Y" or "what is that magician called", and treating a miss here as proof somebody is not in the workspace is the easiest way to tell a user something false about their own business. km_find_contact is the tool for finding a named person, a company or part of an email, and it searches all of them rather than the newest few. ' +
|
|
79
|
+
'Reach for km_list_clients only for the genuinely recency-shaped question: who has come in lately, what do the last few enquiries look like, has anything landed since we spoke. ' +
|
|
80
|
+
'It returns the record as it stands, not the history behind it: km_get_client opens one in full. Read only.',
|
|
81
|
+
{ limit: z.number().int().min(1).max(100).optional() },
|
|
82
|
+
async ({ limit }) => out(await call('GET', `/clients?limit=${limit ?? 25}`)),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
server.tool(
|
|
86
|
+
'km_create_lead',
|
|
87
|
+
'Put a person on file as a contact with nothing attached to them: no enquiry, no deal, no date, no money. ' +
|
|
88
|
+
'Reach for it when the user has met somebody and wants them kept - a card from a convention, a name off a phone call - and there is nothing being sold yet. ' +
|
|
89
|
+
'Search with km_find_contact FIRST. This tool will happily create a second Sarah Jones alongside the one already there, and a split contact is worse than no contact: half her history sits on a record nobody opens. ' +
|
|
90
|
+
'It is the wrong tool when there is actually something to sell. If the person is asking about a date, use km_create_inquiry, which creates the contact AND opens the pipeline deal behind it. If the gig is already agreed, use km_create_booking. ' +
|
|
91
|
+
'Every field is optional, but a contact with no email and no phone can never be written to, so ask for one rather than saving a bare name. ' +
|
|
92
|
+
'This WRITES: it needs the API key to carry the write scope, and it lands in the AI activity log where the owner can see and undo it.',
|
|
93
|
+
{
|
|
94
|
+
first_name: z.string().optional(),
|
|
95
|
+
last_name: z.string().optional(),
|
|
96
|
+
company: z.string().optional(),
|
|
97
|
+
email: z.string().optional(),
|
|
98
|
+
phone: z.string().optional(),
|
|
99
|
+
notes: z.string().optional(),
|
|
100
|
+
},
|
|
101
|
+
async (args) => out(await call('POST', '/clients', args)),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
server.tool(
|
|
105
|
+
'km_list_bookings',
|
|
106
|
+
'The real jobs: the bookings on this business, each with its client, its date, its venue and the money on it. ' +
|
|
107
|
+
'Reach for it for the book of business rather than for the diary - what has been booked lately, what is still only pending a confirmation, every gig inside a date range so you can see the shape of a season, what this business has actually done before. ' +
|
|
108
|
+
'from and to filter on the EVENT date, not on when the booking was taken, so from today forward is the work ahead and to yesterday is the history. Use status to separate the confirmed work from what is still pending. ' +
|
|
109
|
+
'It caps at 100 rows and does not report the true total, so a busy year needs a narrower window rather than a bigger limit, and if you asked for 100 and got 100, say the picture is partial instead of presenting a slice as the whole book. ' +
|
|
110
|
+
'It tells you what is BOOKED. It says nothing about whether a new date could be sold, because a free-looking day can still be blocked, out of area or already pencilled in by a live lead: km_check_availability is the only honest answer to that question. km_get_booking opens one booking in full, where that tool is loaded. Read only.',
|
|
111
|
+
{
|
|
112
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
113
|
+
status: z.string().optional().describe('e.g. confirmed, pending'),
|
|
114
|
+
from: z.string().optional().describe('Earliest event date, YYYY-MM-DD.'),
|
|
115
|
+
to: z.string().optional().describe('Latest event date, YYYY-MM-DD.'),
|
|
116
|
+
},
|
|
117
|
+
async ({ limit, status, from, to }) => {
|
|
118
|
+
const q = new URLSearchParams();
|
|
119
|
+
q.set('limit', String(limit ?? 25));
|
|
120
|
+
if (status) q.set('status', status);
|
|
121
|
+
if (from) q.set('from', from);
|
|
122
|
+
if (to) q.set('to', to);
|
|
123
|
+
return out(await call('GET', `/bookings?${q.toString()}`));
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
server.tool(
|
|
128
|
+
'km_list_deals',
|
|
129
|
+
'The pipeline: the live opportunities in this workspace and how far each one has got, newest first. ' +
|
|
130
|
+
'Reach for it for the shape of the funnel - what is open, what is stuck in quoting, what has been won, what was lost - and to get the deal_id that km_get_deal, km_create_outreach_draft and a play run all need before they can attach anything to the right opportunity. ' +
|
|
131
|
+
'stage narrows to one column of the board. status is the coarse open, won, lost, abandoned split. status lost, then km_get_deal on the big ones, is how you read WHY work was lost, which is almost always worth more than the count: a large deal lost on timing is a live target, a large deal lost on price is evidence about the rate. ' +
|
|
132
|
+
'It caps at 100 rows and does not report the true total, so if you asked for 100 and got 100, say the pipeline you are describing is partial. ' +
|
|
133
|
+
'It cannot search by a client\'s name: find the person with km_find_contact first, then work from their record. Read only.',
|
|
134
|
+
{
|
|
135
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
136
|
+
stage: z
|
|
137
|
+
.enum([
|
|
138
|
+
'inbound',
|
|
139
|
+
'qualifying',
|
|
140
|
+
'quoting',
|
|
141
|
+
'proposal_sent',
|
|
142
|
+
'booked',
|
|
143
|
+
'in_progress',
|
|
144
|
+
'completed',
|
|
145
|
+
'lost',
|
|
146
|
+
'abandoned',
|
|
147
|
+
])
|
|
148
|
+
.optional(),
|
|
149
|
+
status: z.enum(['open', 'won', 'lost', 'abandoned']).optional(),
|
|
150
|
+
},
|
|
151
|
+
async ({ limit, stage, status }) => {
|
|
152
|
+
const q = new URLSearchParams();
|
|
153
|
+
q.set('limit', String(limit ?? 25));
|
|
154
|
+
if (stage) q.set('stage', stage);
|
|
155
|
+
if (status) q.set('status', status);
|
|
156
|
+
return out(await call('GET', `/deals?${q.toString()}`));
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
server.tool(
|
|
161
|
+
'km_list_events',
|
|
162
|
+
'The raw contents of the KM Hub calendar over a date window, in start order: the gigs and everything else that has been put in the diary alongside them. ' +
|
|
163
|
+
'Always set from and to. With no window it returns whatever is most recent, which is almost never the question anybody asked. ' +
|
|
164
|
+
'Reach for it when the user wants the literal contents of a range: what is in September, is there anything at all on that weekend, how full does next month look. ' +
|
|
165
|
+
'For the ordinary "what have I got on" question, km_my_schedule is the better answer wherever it is loaded, because it groups the days and names the client, venue, times and fee on each gig rather than handing back a flat list. For whether ONE date can be sold, km_check_availability is the only honest source: this tool shows what already exists, and an empty-looking day is not the same as an available one. ' +
|
|
166
|
+
'Read only: it never adds, moves or cancels anything in the diary.',
|
|
167
|
+
{
|
|
168
|
+
from: z.string().optional().describe('Earliest start date, YYYY-MM-DD.'),
|
|
169
|
+
to: z.string().optional().describe('Latest start date, YYYY-MM-DD.'),
|
|
170
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
171
|
+
},
|
|
172
|
+
async ({ from, to, limit }) => {
|
|
173
|
+
const q = new URLSearchParams();
|
|
174
|
+
q.set('limit', String(limit ?? 50));
|
|
175
|
+
if (from) q.set('from', from);
|
|
176
|
+
if (to) q.set('to', to);
|
|
177
|
+
return out(await call('GET', `/events?${q.toString()}`));
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
server.tool(
|
|
182
|
+
'km_create_outreach_draft',
|
|
183
|
+
'Queue an outreach message as a DRAFT in KM Hub. This tool NEVER sends anything - the draft lands in the Approval Queue and a human approves it before any send happens. Audited in the AI activity log.',
|
|
184
|
+
{
|
|
185
|
+
body: z.string().describe('The message text (required).'),
|
|
186
|
+
subject: z.string().optional().describe('Subject line (email channel).'),
|
|
187
|
+
channel: z.enum(['email', 'linkedin', 'ig', 'sms_whatsapp']).optional().describe('Default email.'),
|
|
188
|
+
deal_id: z.string().optional().describe('UUID of the deal this draft belongs to (see km_list_deals).'),
|
|
189
|
+
contact_id: z.string().optional().describe('UUID of the client/contact (see km_find_contact).'),
|
|
190
|
+
},
|
|
191
|
+
async (args) => out(await call('POST', '/outreach-drafts', args)),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
server.tool(
|
|
195
|
+
'km_create_inquiry',
|
|
196
|
+
'Log a new inquiry (lead + enquiry) in KM Hub - creates the client if needed and opens a pipeline deal. Audited and undoable from the AI activity log.',
|
|
197
|
+
{
|
|
198
|
+
client_name: z.string().describe('Full name of the person enquiring.'),
|
|
199
|
+
client_email: z.string().optional(),
|
|
200
|
+
client_phone: z.string().optional(),
|
|
201
|
+
event_type: z.string().optional().describe('e.g. wedding, corporate, birthday.'),
|
|
202
|
+
event_date: z.string().optional().describe('YYYY-MM-DD.'),
|
|
203
|
+
event_start_time: z.string().optional().describe('HH:MM 24h.'),
|
|
204
|
+
event_end_time: z.string().optional().describe('HH:MM 24h.'),
|
|
205
|
+
venue_name: z.string().optional(),
|
|
206
|
+
guest_count: z.number().optional(),
|
|
207
|
+
budget_cents: z.number().optional().describe('Budget in CENTS.'),
|
|
208
|
+
notes: z.string().optional(),
|
|
209
|
+
},
|
|
210
|
+
async (args) => out(await call('POST', '/inquiries', args)),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
server.tool(
|
|
214
|
+
'km_create_booking',
|
|
215
|
+
'Create a REAL confirmed booking on the KM Hub calendar - creates the client if needed and opens a pipeline deal at booked/won. Requires an event_date. Only use when the gig is definitely booked; otherwise use km_create_inquiry.',
|
|
216
|
+
{
|
|
217
|
+
client_name: z.string().describe('Full name of the client.'),
|
|
218
|
+
client_email: z.string().optional(),
|
|
219
|
+
client_phone: z.string().optional(),
|
|
220
|
+
title: z.string().optional().describe('Booking title shown on the calendar.'),
|
|
221
|
+
event_type: z.string().optional(),
|
|
222
|
+
event_date: z.string().describe('YYYY-MM-DD (required for a real booking).'),
|
|
223
|
+
event_start_time: z.string().optional().describe('HH:MM 24h.'),
|
|
224
|
+
event_end_time: z.string().optional().describe('HH:MM 24h.'),
|
|
225
|
+
venue_name: z.string().optional(),
|
|
226
|
+
guest_count: z.number().optional(),
|
|
227
|
+
total_cents: z.number().optional().describe('Total in CENTS.'),
|
|
228
|
+
status: z
|
|
229
|
+
.enum(['pending', 'confirmed'])
|
|
230
|
+
.optional()
|
|
231
|
+
.describe('Default pending (owner reviews); confirmed puts it straight on the calendar.'),
|
|
232
|
+
confirm_token: z
|
|
233
|
+
.string()
|
|
234
|
+
.optional()
|
|
235
|
+
.describe(
|
|
236
|
+
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
237
|
+
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
238
|
+
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
239
|
+
+ 'arguments. A yes for one action never authorises a different one.',
|
|
240
|
+
),
|
|
241
|
+
},
|
|
242
|
+
async (args) => out(await call('POST', '/bookings', args)),
|
|
243
|
+
);
|
|
244
|
+
}
|