@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.
Files changed (57) hide show
  1. package/README.md +170 -170
  2. package/bin/kmhub.mjs +896 -896
  3. package/coach-book-output-guard.mjs +760 -760
  4. package/index.mjs +57 -57
  5. package/package.json +56 -56
  6. package/prompts/briefing.md +29 -29
  7. package/prompts/luxury.md +70 -70
  8. package/prompts/play.md +49 -49
  9. package/prompts/run.md +36 -36
  10. package/prompts/setup.md +33 -33
  11. package/prompts/vs-booked.md +46 -46
  12. package/prompts/what-can-you-do.md +40 -40
  13. package/prompts.mjs +110 -109
  14. package/read-only-tools.json +143 -142
  15. package/remote.mjs +929 -929
  16. package/tools/balloon-costing.mjs +80 -80
  17. package/tools/booking-equipment.mjs +110 -110
  18. package/tools/bridges.mjs +54 -54
  19. package/tools/briefing.mjs +91 -91
  20. package/tools/calendar.mjs +170 -170
  21. package/tools/capabilities.mjs +155 -155
  22. package/tools/catalog.mjs +288 -288
  23. package/tools/clubs.mjs +176 -176
  24. package/tools/coach.mjs +771 -771
  25. package/tools/compare.mjs +76 -76
  26. package/tools/core.mjs +244 -244
  27. package/tools/crm.mjs +209 -209
  28. package/tools/dubsado.mjs +137 -137
  29. package/tools/exports.mjs +128 -128
  30. package/tools/fact-review.mjs +125 -125
  31. package/tools/flows.mjs +261 -261
  32. package/tools/forms.mjs +158 -158
  33. package/tools/gols.mjs +134 -134
  34. package/tools/hr.mjs +162 -162
  35. package/tools/knowledge.mjs +125 -125
  36. package/tools/marketing.mjs +396 -396
  37. package/tools/meta.mjs +245 -245
  38. package/tools/military.mjs +244 -244
  39. package/tools/money.mjs +235 -197
  40. package/tools/outreach.mjs +238 -238
  41. package/tools/pending.mjs +122 -122
  42. package/tools/photos.mjs +140 -140
  43. package/tools/plays.mjs +244 -244
  44. package/tools/profile.mjs +118 -118
  45. package/tools/radar.mjs +173 -173
  46. package/tools/recurring-invoices.mjs +149 -149
  47. package/tools/reengage.mjs +434 -434
  48. package/tools/schedules.mjs +55 -55
  49. package/tools/setup.mjs +168 -168
  50. package/tools/sops-bridges.mjs +86 -86
  51. package/tools/sops.mjs +314 -314
  52. package/tools/sourcing.mjs +268 -268
  53. package/tools/strategy.mjs +146 -146
  54. package/tools/studio.mjs +132 -132
  55. package/tools/venueradar.mjs +151 -151
  56. package/tools/voice.mjs +134 -134
  57. package/tools.mjs +407 -407
@@ -1,91 +1,91 @@
1
- /**
2
- * Tool family: briefing - the morning handoff. One tool, one call, the whole day.
3
- *
4
- * WHY A WHOLE FAMILY FOR ONE TOOL
5
- * Every other family is a door onto one part of the workspace. This one is the
6
- * front door. A Terminal Mode session starts cold every single time: the model
7
- * knows the workspace exists and knows nothing about what is happening in it. The
8
- * old opening move was km_waiting, which hands back six counts, and then four or
9
- * five list calls to turn those counts back into things a person can act on. That
10
- * is five round trips before anyone has said a useful sentence, and a guess in the
11
- * middle of it about which of the six numbers actually mattered.
12
- *
13
- * km_briefing is that whole opening move as one call, with the guess replaced by a
14
- * ranking done on the server where the data already is. It comes back as ONE
15
- * ordered list, most-worth-doing first, every entry carrying the reason it is
16
- * there, plus the diary, plus the raw gate counts, plus one suggested play when
17
- * the shape of the day genuinely calls for one.
18
- *
19
- * WHAT IT DOES NOT DO
20
- * It reads. It cannot send, approve, complete, schedule or spend, and the play it
21
- * suggests is never one that can spend the client's money. Everything it surfaces
22
- * still ends where it ended before: a draft in the Approval Queue, a decision in
23
- * front of a human.
24
- *
25
- * The route behind it is GET /briefing. That route owns the ranking rule and the
26
- * suggestion rule, so this file passes two optional parameters through and
27
- * reshapes nothing.
28
- *
29
- * The family contract this file follows is documented in ./README.md.
30
- */
31
- import { z } from 'zod';
32
-
33
- export const FAMILY = 'briefing';
34
-
35
- export const TOOLS = ['km_briefing'];
36
-
37
- // Every profile. A session that can read invoices but cannot ask what needs doing
38
- // today is a session that starts every conversation from nothing. This is the one
39
- // tool whose absence is felt in every other family's work.
40
- export const PROFILES = ['*'];
41
-
42
- /** The API has not shipped this route yet (404 / 405 / 501 all mean the same here). */
43
- function routeMissing(r) {
44
- return r.status === 404 || r.status === 405 || r.status === 501;
45
- }
46
-
47
- /**
48
- * If the workspace is on an older kmhub-api, say so in one sentence and name the
49
- * fallback. This is the first tool of the session, so an unexplained JSON 404 here
50
- * would set the tone for everything after it.
51
- */
52
- const NOT_SUPPORTED =
53
- 'This KM Hub is on an older version of the API that does not have the morning briefing yet. Nothing is broken and no data is missing. Fall back to km_waiting for the gate counts, then km_list_outreach_drafts, km_list_outreach_replies, km_list_tasks with overdue true, km_list_invoices with overdue true, and km_my_schedule for the diary. Say plainly that you are stitching it together by hand rather than pretending you have the ranked version.';
54
-
55
- /**
56
- * @param {{ tool: Function }} server guarded registrar (see ./README.md)
57
- * @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
58
- * @param {{ out: Function, text: Function, qs: Function }} helpers shared response helpers from ../tools.mjs
59
- */
60
- export function register(server, call, { out, text, qs }) {
61
- server.tool(
62
- 'km_briefing',
63
- 'THE FIRST TOOL TO REACH FOR on any open question about the day. If the user says any of "what needs me today", "what have I got on", "morning", "catch me up", "where are we", "what should I do first", "anything urgent", "what did I miss", or opens a session without a specific task, call this before anything else and answer from what it returns. ' +
64
- 'It REPLACES the old opening sequence entirely: km_waiting plus km_list_outreach_drafts plus km_list_outreach_replies plus km_list_tasks plus km_list_invoices plus km_my_schedule, all in one call. Do not run those first and do not run them afterwards to double check. Go to a list tool only when the user asks for more of one specific thing than the briefing carried, for example the full text of a draft (km_read_outreach_draft) or every invoice rather than only the late ones (km_list_invoices). ' +
65
- 'What comes back, in one response: a headline sentence you can read straight out loud; `items`, ONE list ranked in the order a person should actually deal with them, each with a plain reason it is there; `calendar`, today and the next few days including any date that is only pencilled in and has nothing signed behind it; `waiting`, the same raw gate counts km_waiting would have given you; and `suggested_play` when the shape of the day genuinely calls for one. ' +
66
- 'The ranking is not a list of lists. A new enquiry nobody has answered and a reply nobody has handled sit near the top because they are the things that stop being worth anything if you leave them, and money that is already late and worth real amounts outranks something merely unread. A gig today is near the top too, but as a fact to absorb rather than a decision to make. Trust the order: work down it, and do not silently re-sort it into your own idea of importance. Each item carries `score` so you can see why it sits where it does, and `next_tool` naming the read tool that opens it. ' +
67
- 'Read the `notes` array and the `complete` flag before you summarise. When a part of the workspace could not be read, the briefing says which part rather than quietly showing an empty list, and you must pass that on rather than telling somebody they have a clear day when you do not know that. ' +
68
- 'This tool only reads. It never sends, approves, completes, schedules or spends anything, and a play it suggests is never one that can spend money.',
69
- {
70
- days: z
71
- .number()
72
- .int()
73
- .min(1)
74
- .max(14)
75
- .optional()
76
- .describe('How far ahead the calendar part looks, in days including today. Default 3. Raise it when the user asks about the week or the fortnight ahead.'),
77
- limit: z
78
- .number()
79
- .int()
80
- .min(1)
81
- .max(60)
82
- .optional()
83
- .describe('How many ranked items to return, most important first. Default 25, which is already more than a person will do in a morning. `counts.truncated` tells you when there were more.'),
84
- },
85
- async ({ days, limit }) => {
86
- const r = await call('GET', `/briefing${qs({ days, limit })}`);
87
- if (routeMissing(r)) return text(NOT_SUPPORTED, true);
88
- return out(r);
89
- },
90
- );
91
- }
1
+ /**
2
+ * Tool family: briefing - the morning handoff. One tool, one call, the whole day.
3
+ *
4
+ * WHY A WHOLE FAMILY FOR ONE TOOL
5
+ * Every other family is a door onto one part of the workspace. This one is the
6
+ * front door. A Terminal Mode session starts cold every single time: the model
7
+ * knows the workspace exists and knows nothing about what is happening in it. The
8
+ * old opening move was km_waiting, which hands back six counts, and then four or
9
+ * five list calls to turn those counts back into things a person can act on. That
10
+ * is five round trips before anyone has said a useful sentence, and a guess in the
11
+ * middle of it about which of the six numbers actually mattered.
12
+ *
13
+ * km_briefing is that whole opening move as one call, with the guess replaced by a
14
+ * ranking done on the server where the data already is. It comes back as ONE
15
+ * ordered list, most-worth-doing first, every entry carrying the reason it is
16
+ * there, plus the diary, plus the raw gate counts, plus one suggested play when
17
+ * the shape of the day genuinely calls for one.
18
+ *
19
+ * WHAT IT DOES NOT DO
20
+ * It reads. It cannot send, approve, complete, schedule or spend, and the play it
21
+ * suggests is never one that can spend the client's money. Everything it surfaces
22
+ * still ends where it ended before: a draft in the Approval Queue, a decision in
23
+ * front of a human.
24
+ *
25
+ * The route behind it is GET /briefing. That route owns the ranking rule and the
26
+ * suggestion rule, so this file passes two optional parameters through and
27
+ * reshapes nothing.
28
+ *
29
+ * The family contract this file follows is documented in ./README.md.
30
+ */
31
+ import { z } from 'zod';
32
+
33
+ export const FAMILY = 'briefing';
34
+
35
+ export const TOOLS = ['km_briefing'];
36
+
37
+ // Every profile. A session that can read invoices but cannot ask what needs doing
38
+ // today is a session that starts every conversation from nothing. This is the one
39
+ // tool whose absence is felt in every other family's work.
40
+ export const PROFILES = ['*'];
41
+
42
+ /** The API has not shipped this route yet (404 / 405 / 501 all mean the same here). */
43
+ function routeMissing(r) {
44
+ return r.status === 404 || r.status === 405 || r.status === 501;
45
+ }
46
+
47
+ /**
48
+ * If the workspace is on an older kmhub-api, say so in one sentence and name the
49
+ * fallback. This is the first tool of the session, so an unexplained JSON 404 here
50
+ * would set the tone for everything after it.
51
+ */
52
+ const NOT_SUPPORTED =
53
+ 'This KM Hub is on an older version of the API that does not have the morning briefing yet. Nothing is broken and no data is missing. Fall back to km_waiting for the gate counts, then km_list_outreach_drafts, km_list_outreach_replies, km_list_tasks with overdue true, km_list_invoices with overdue true, and km_my_schedule for the diary. Say plainly that you are stitching it together by hand rather than pretending you have the ranked version.';
54
+
55
+ /**
56
+ * @param {{ tool: Function }} server guarded registrar (see ./README.md)
57
+ * @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
58
+ * @param {{ out: Function, text: Function, qs: Function }} helpers shared response helpers from ../tools.mjs
59
+ */
60
+ export function register(server, call, { out, text, qs }) {
61
+ server.tool(
62
+ 'km_briefing',
63
+ 'THE FIRST TOOL TO REACH FOR on any open question about the day. If the user says any of "what needs me today", "what have I got on", "morning", "catch me up", "where are we", "what should I do first", "anything urgent", "what did I miss", or opens a session without a specific task, call this before anything else and answer from what it returns. ' +
64
+ 'It REPLACES the old opening sequence entirely: km_waiting plus km_list_outreach_drafts plus km_list_outreach_replies plus km_list_tasks plus km_list_invoices plus km_my_schedule, all in one call. Do not run those first and do not run them afterwards to double check. Go to a list tool only when the user asks for more of one specific thing than the briefing carried, for example the full text of a draft (km_read_outreach_draft) or every invoice rather than only the late ones (km_list_invoices). ' +
65
+ 'What comes back, in one response: a headline sentence you can read straight out loud; `items`, ONE list ranked in the order a person should actually deal with them, each with a plain reason it is there; `calendar`, today and the next few days including any date that is only pencilled in and has nothing signed behind it; `waiting`, the same raw gate counts km_waiting would have given you; and `suggested_play` when the shape of the day genuinely calls for one. ' +
66
+ 'The ranking is not a list of lists. A new enquiry nobody has answered and a reply nobody has handled sit near the top because they are the things that stop being worth anything if you leave them, and money that is already late and worth real amounts outranks something merely unread. A gig today is near the top too, but as a fact to absorb rather than a decision to make. Trust the order: work down it, and do not silently re-sort it into your own idea of importance. Each item carries `score` so you can see why it sits where it does, and `next_tool` naming the read tool that opens it. ' +
67
+ 'Read the `notes` array and the `complete` flag before you summarise. When a part of the workspace could not be read, the briefing says which part rather than quietly showing an empty list, and you must pass that on rather than telling somebody they have a clear day when you do not know that. ' +
68
+ 'This tool only reads. It never sends, approves, completes, schedules or spends anything, and a play it suggests is never one that can spend money.',
69
+ {
70
+ days: z
71
+ .number()
72
+ .int()
73
+ .min(1)
74
+ .max(14)
75
+ .optional()
76
+ .describe('How far ahead the calendar part looks, in days including today. Default 3. Raise it when the user asks about the week or the fortnight ahead.'),
77
+ limit: z
78
+ .number()
79
+ .int()
80
+ .min(1)
81
+ .max(60)
82
+ .optional()
83
+ .describe('How many ranked items to return, most important first. Default 25, which is already more than a person will do in a morning. `counts.truncated` tells you when there were more.'),
84
+ },
85
+ async ({ days, limit }) => {
86
+ const r = await call('GET', `/briefing${qs({ days, limit })}`);
87
+ if (routeMissing(r)) return text(NOT_SUPPORTED, true);
88
+ return out(r);
89
+ },
90
+ );
91
+ }
@@ -1,170 +1,170 @@
1
- /**
2
- * Tool family: calendar - the diary. What is on, what is free, and what one gig is.
3
- *
4
- * core.mjs already lists bookings and calendar events. This family is for the
5
- * questions a performer asks in a car park at 5pm, where the answer has to
6
- * arrive in one tool call and be readable out loud:
7
- *
8
- * km_my_schedule what have I got this week
9
- * km_check_availability am I free on that date, and at that hour
10
- * km_get_booking tell me everything about this gig
11
- * km_update_booking fix the details of a gig that is already agreed
12
- * km_blocked_dates which days are already spoken for
13
- *
14
- * The availability answer is computed by the same database functions that drive
15
- * the public booking widget and the lead gate, never re-derived on the way past,
16
- * because a second opinion about a free Saturday is how a real person ends up
17
- * double booked. When those functions decline to answer, the tool says it cannot
18
- * tell rather than guessing "free".
19
- *
20
- * km_update_booking is deliberately the narrowest write in the whole server. It
21
- * cannot move a date or a time, cannot touch money, and cannot cancel or
22
- * complete anything, because those commit the business to something a person
23
- * should be the one to commit to. It can confirm, and the description says in
24
- * plain words what confirming does to the calendar.
25
- *
26
- * The family contract this file follows is documented in ./README.md.
27
- */
28
- import { z } from 'zod';
29
-
30
- export const FAMILY = 'calendar';
31
-
32
- export const TOOLS = [
33
- 'km_my_schedule',
34
- 'km_check_availability',
35
- 'km_get_booking',
36
- 'km_update_booking',
37
- 'km_blocked_dates',
38
- ];
39
-
40
- // The diary is the context every other family reasons against: a money question
41
- // and a follow-up question both start with what is already on the books. It
42
- // carries five tools, so the cost of joining every profile is small.
43
- export const PROFILES = ['*'];
44
-
45
- /**
46
- * @param {{ tool: Function }} server guarded registrar (see ./README.md)
47
- * @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
48
- * @param {{ out: Function, qs: Function }} helpers shared response helpers from ../tools.mjs
49
- */
50
- export function register(server, call, { out, qs }) {
51
- server.tool(
52
- 'km_my_schedule',
53
- 'What is on in KM Hub, day by day, in one call. Reach for this when the question is specifically '
54
- + 'about the DIARY: a named week, a named day, or the next gig. "What have I got this week", '
55
- + '"am I busy Saturday", "when is my next gig". Do NOT answer the broad question about the day from '
56
- + 'here: if the person says "what needs me today", "catch me up", "what should I do first", or just '
57
- + 'opens a session with nothing specific, km_briefing is the tool. It carries this diary AND the '
58
- + 'enquiries, replies, tasks and money that are waiting, ranked against each other, none of which '
59
- + 'this tool can see. Running both means paying for the same diary twice. It returns a plain-English '
60
- + 'headline, one readable line per day, and the detail behind each line: the gigs with their times, '
61
- + 'client, venue, guest count and fee, anything else in the diary, days blocked off, and dates that '
62
- + 'are only pencilled in by a live lead. Days are counted in the workspace timezone, so "today" means '
63
- + 'today where the performer is. Defaults to the next 7 days starting today. Read only: it never '
64
- + 'changes anything. Use km_check_availability instead when the question is whether a specific date '
65
- + 'is free rather than what is already booked.',
66
- {
67
- from: z.string().optional().describe('First day, YYYY-MM-DD. Defaults to today in the workspace timezone.'),
68
- days: z.number().int().min(1).max(31).optional().describe('How many days to cover, 1 to 31. Default 7.'),
69
- },
70
- async ({ from, days }) => out(await call('GET', `/schedule${qs({ from, days })}`)),
71
- );
72
-
73
- server.tool(
74
- 'km_check_availability',
75
- 'Is a date free for a booking. Ask this before telling anyone a date might work. The verdict comes '
76
- + 'from the same availability check that the public booking page and the lead forms use, so it '
77
- + 'already counts confirmed bookings, days blocked off by hand, weekends the workspace does not work, '
78
- + 'and dates a live lead has pencilled in. Give start_time (and optionally end_time or '
79
- + 'duration_minutes, plus setup, packdown and travel padding) and it answers for that time window '
80
- + 'instead of the whole day, so two non-overlapping gigs on one day both read as possible. Alongside '
81
- + 'each verdict it lists what is actually in the diary that day, so a "taken" comes with the gig that '
82
- + 'is taking it. If the workspace has switched its availability check off, the tool reports that it '
83
- + 'cannot give a verdict and shows the diary instead: it will never call a day free when it does not '
84
- + 'know. Read only, and it never reserves or holds anything.',
85
- {
86
- from: z.string().optional().describe('First date to check, YYYY-MM-DD. Defaults to today in the workspace timezone.'),
87
- to: z.string().optional().describe('Last date to check, YYYY-MM-DD. Defaults to the same day as from. Up to 92 days for a whole-day check, 14 days when a start_time is given.'),
88
- start_time: z.string().optional().describe('Start of the slot, HH:MM in 24 hour time. Supply it to check a time window rather than the whole day.'),
89
- end_time: z.string().optional().describe('End of the slot, HH:MM in 24 hour time. Only meaningful with start_time; without it the workspace default duration is used.'),
90
- duration_minutes: z.number().int().min(1).max(1440).optional().describe('How long the gig runs, when end_time is unknown.'),
91
- buffer_before_minutes: z.number().int().min(0).max(1440).optional().describe('Setup time needed before the slot.'),
92
- buffer_after_minutes: z.number().int().min(0).max(1440).optional().describe('Packdown time needed after the slot.'),
93
- travel_minutes: z.number().int().min(0).max(1440).optional().describe('Travel time each way, added to both ends of the window.'),
94
- },
95
- async (args) => out(await call('GET', `/availability${qs(args)}`)),
96
- );
97
-
98
- server.tool(
99
- 'km_get_booking',
100
- 'Everything about one booking in KM Hub: the date, the start, end and setup times, the client with '
101
- + 'their phone and email, the venue with its address and on-site contact, the event type and guest '
102
- + 'count, the fee with deposit and outstanding balance, the client notes, internal notes and special '
103
- + 'requirements, the pipeline deal it belongs to, and any diary entries attached to it. This is the '
104
- + 'tool for "what is this gig", "where am I playing on Saturday", "who is the contact for the Kaplan '
105
- + 'wedding" and "what do I need to bring". Needs the booking id, which km_my_schedule and '
106
- + 'km_list_bookings both return. Read only.',
107
- {
108
- booking_id: z.string().describe('UUID of the booking (from km_my_schedule or km_list_bookings).'),
109
- },
110
- async ({ booking_id }) => out(await call('GET', `/bookings/${encodeURIComponent(String(booking_id || '').trim())}`)),
111
- );
112
-
113
- server.tool(
114
- 'km_update_booking',
115
- 'Correct the working details of a gig that is already agreed: its title, event type, guest count, '
116
- + 'setup time, venue, client notes, internal notes, special requirements, and its status. '
117
- + 'CONFIRMING IS CONSEQUENTIAL: setting status to confirmed marks that whole date as taken across '
118
- + 'KM Hub, so the public booking page, the lead forms and every availability check start turning '
119
- + 'other people away from it. Moving the status back to pending releases the date and offers it '
120
- + 'again. Only make that call when the performer has actually said the gig is on or off. '
121
- + 'WHAT THIS TOOL WILL NOT DO, by design, so do not try: it cannot move a booking to another date, '
122
- + 'cannot change the start or end time, cannot change the fee, deposit or balance, cannot mark money '
123
- + 'as paid, cannot mark a gig completed, and cannot cancel one. Those bind the business or free a '
124
- + 'date for someone else, so a person does them in KM Hub. It also refuses to touch a gig that is '
125
- + 'already completed or cancelled, and any booking owned by the meeting scheduler. Every change is '
126
- + 'written to the AI activity log so the owner can see it; booking edits are not auto-undoable, so '
127
- + 'get it right rather than counting on undo.',
128
- {
129
- booking_id: z.string().describe('UUID of the booking (from km_my_schedule or km_list_bookings).'),
130
- title: z.string().optional().describe('What the gig is called on the calendar.'),
131
- event_type: z.string().optional().describe('e.g. wedding, corporate, birthday. The value "meeting" is reserved by the scheduler and is refused.'),
132
- guest_count: z.number().int().min(0).nullable().optional().describe('How many people are expected. Send null to clear it.'),
133
- setup_start_time: z.string().nullable().optional().describe('Load-in time, HH:MM in 24 hour time. This is the performer\'s own logistics, not the event start. Send null to clear it.'),
134
- venue_id: z.string().optional().describe('UUID of a venue already in this workspace.'),
135
- client_notes: z.string().optional().describe('Notes the client would be shown.'),
136
- internal_notes: z.string().optional().describe('Notes only the team sees.'),
137
- special_requirements: z.string().optional().describe('Anything the gig needs: access, power, dress code, dietary.'),
138
- status: z
139
- .enum(['pending', 'confirmed', 'in_progress'])
140
- .optional()
141
- .describe('confirmed marks the date taken everywhere; pending releases it; in_progress means the gig is under way. Cancelled and completed are not available here.'),
142
- confirm_token: z
143
- .string()
144
- .optional()
145
- .describe(
146
- 'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
147
- + 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
148
- + 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
149
- + 'arguments. A yes for one action never authorises a different one.',
150
- ),
151
- },
152
- async ({ booking_id, ...changes }) =>
153
- out(await call('PATCH', `/bookings/${encodeURIComponent(String(booking_id || '').trim())}`, changes)),
154
- );
155
-
156
- server.tool(
157
- 'km_blocked_dates',
158
- 'The days that are already spoken for in KM Hub without being a gig: dates blocked off by hand with '
159
- + 'their reason (holiday, family, studio time), and active holds, including dates a live lead has '
160
- + 'pencilled in and that will expire on their own if the lead goes cold. Use it to answer "what am I '
161
- + 'blocked out for", "why does that weekend show as unavailable", or before suggesting dates to '
162
- + 'someone. Defaults to the next 90 days. Read only: blocking or unblocking a date decides whether '
163
- + 'real work can be booked, so a person does that in KM Hub.',
164
- {
165
- from: z.string().optional().describe('First date, YYYY-MM-DD. Defaults to today in the workspace timezone.'),
166
- to: z.string().optional().describe('Last date, YYYY-MM-DD. Defaults to 90 days out. At most a year at a time.'),
167
- },
168
- async ({ from, to }) => out(await call('GET', `/blocked-dates${qs({ from, to })}`)),
169
- );
170
- }
1
+ /**
2
+ * Tool family: calendar - the diary. What is on, what is free, and what one gig is.
3
+ *
4
+ * core.mjs already lists bookings and calendar events. This family is for the
5
+ * questions a performer asks in a car park at 5pm, where the answer has to
6
+ * arrive in one tool call and be readable out loud:
7
+ *
8
+ * km_my_schedule what have I got this week
9
+ * km_check_availability am I free on that date, and at that hour
10
+ * km_get_booking tell me everything about this gig
11
+ * km_update_booking fix the details of a gig that is already agreed
12
+ * km_blocked_dates which days are already spoken for
13
+ *
14
+ * The availability answer is computed by the same database functions that drive
15
+ * the public booking widget and the lead gate, never re-derived on the way past,
16
+ * because a second opinion about a free Saturday is how a real person ends up
17
+ * double booked. When those functions decline to answer, the tool says it cannot
18
+ * tell rather than guessing "free".
19
+ *
20
+ * km_update_booking is deliberately the narrowest write in the whole server. It
21
+ * cannot move a date or a time, cannot touch money, and cannot cancel or
22
+ * complete anything, because those commit the business to something a person
23
+ * should be the one to commit to. It can confirm, and the description says in
24
+ * plain words what confirming does to the calendar.
25
+ *
26
+ * The family contract this file follows is documented in ./README.md.
27
+ */
28
+ import { z } from 'zod';
29
+
30
+ export const FAMILY = 'calendar';
31
+
32
+ export const TOOLS = [
33
+ 'km_my_schedule',
34
+ 'km_check_availability',
35
+ 'km_get_booking',
36
+ 'km_update_booking',
37
+ 'km_blocked_dates',
38
+ ];
39
+
40
+ // The diary is the context every other family reasons against: a money question
41
+ // and a follow-up question both start with what is already on the books. It
42
+ // carries five tools, so the cost of joining every profile is small.
43
+ export const PROFILES = ['*'];
44
+
45
+ /**
46
+ * @param {{ tool: Function }} server guarded registrar (see ./README.md)
47
+ * @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
48
+ * @param {{ out: Function, qs: Function }} helpers shared response helpers from ../tools.mjs
49
+ */
50
+ export function register(server, call, { out, qs }) {
51
+ server.tool(
52
+ 'km_my_schedule',
53
+ 'What is on in KM Hub, day by day, in one call. Reach for this when the question is specifically '
54
+ + 'about the DIARY: a named week, a named day, or the next gig. "What have I got this week", '
55
+ + '"am I busy Saturday", "when is my next gig". Do NOT answer the broad question about the day from '
56
+ + 'here: if the person says "what needs me today", "catch me up", "what should I do first", or just '
57
+ + 'opens a session with nothing specific, km_briefing is the tool. It carries this diary AND the '
58
+ + 'enquiries, replies, tasks and money that are waiting, ranked against each other, none of which '
59
+ + 'this tool can see. Running both means paying for the same diary twice. It returns a plain-English '
60
+ + 'headline, one readable line per day, and the detail behind each line: the gigs with their times, '
61
+ + 'client, venue, guest count and fee, anything else in the diary, days blocked off, and dates that '
62
+ + 'are only pencilled in by a live lead. Days are counted in the workspace timezone, so "today" means '
63
+ + 'today where the performer is. Defaults to the next 7 days starting today. Read only: it never '
64
+ + 'changes anything. Use km_check_availability instead when the question is whether a specific date '
65
+ + 'is free rather than what is already booked.',
66
+ {
67
+ from: z.string().optional().describe('First day, YYYY-MM-DD. Defaults to today in the workspace timezone.'),
68
+ days: z.number().int().min(1).max(31).optional().describe('How many days to cover, 1 to 31. Default 7.'),
69
+ },
70
+ async ({ from, days }) => out(await call('GET', `/schedule${qs({ from, days })}`)),
71
+ );
72
+
73
+ server.tool(
74
+ 'km_check_availability',
75
+ 'Is a date free for a booking. Ask this before telling anyone a date might work. The verdict comes '
76
+ + 'from the same availability check that the public booking page and the lead forms use, so it '
77
+ + 'already counts confirmed bookings, days blocked off by hand, weekends the workspace does not work, '
78
+ + 'and dates a live lead has pencilled in. Give start_time (and optionally end_time or '
79
+ + 'duration_minutes, plus setup, packdown and travel padding) and it answers for that time window '
80
+ + 'instead of the whole day, so two non-overlapping gigs on one day both read as possible. Alongside '
81
+ + 'each verdict it lists what is actually in the diary that day, so a "taken" comes with the gig that '
82
+ + 'is taking it. If the workspace has switched its availability check off, the tool reports that it '
83
+ + 'cannot give a verdict and shows the diary instead: it will never call a day free when it does not '
84
+ + 'know. Read only, and it never reserves or holds anything.',
85
+ {
86
+ from: z.string().optional().describe('First date to check, YYYY-MM-DD. Defaults to today in the workspace timezone.'),
87
+ to: z.string().optional().describe('Last date to check, YYYY-MM-DD. Defaults to the same day as from. Up to 92 days for a whole-day check, 14 days when a start_time is given.'),
88
+ start_time: z.string().optional().describe('Start of the slot, HH:MM in 24 hour time. Supply it to check a time window rather than the whole day.'),
89
+ end_time: z.string().optional().describe('End of the slot, HH:MM in 24 hour time. Only meaningful with start_time; without it the workspace default duration is used.'),
90
+ duration_minutes: z.number().int().min(1).max(1440).optional().describe('How long the gig runs, when end_time is unknown.'),
91
+ buffer_before_minutes: z.number().int().min(0).max(1440).optional().describe('Setup time needed before the slot.'),
92
+ buffer_after_minutes: z.number().int().min(0).max(1440).optional().describe('Packdown time needed after the slot.'),
93
+ travel_minutes: z.number().int().min(0).max(1440).optional().describe('Travel time each way, added to both ends of the window.'),
94
+ },
95
+ async (args) => out(await call('GET', `/availability${qs(args)}`)),
96
+ );
97
+
98
+ server.tool(
99
+ 'km_get_booking',
100
+ 'Everything about one booking in KM Hub: the date, the start, end and setup times, the client with '
101
+ + 'their phone and email, the venue with its address and on-site contact, the event type and guest '
102
+ + 'count, the fee with deposit and outstanding balance, the client notes, internal notes and special '
103
+ + 'requirements, the pipeline deal it belongs to, and any diary entries attached to it. This is the '
104
+ + 'tool for "what is this gig", "where am I playing on Saturday", "who is the contact for the Kaplan '
105
+ + 'wedding" and "what do I need to bring". Needs the booking id, which km_my_schedule and '
106
+ + 'km_list_bookings both return. Read only.',
107
+ {
108
+ booking_id: z.string().describe('UUID of the booking (from km_my_schedule or km_list_bookings).'),
109
+ },
110
+ async ({ booking_id }) => out(await call('GET', `/bookings/${encodeURIComponent(String(booking_id || '').trim())}`)),
111
+ );
112
+
113
+ server.tool(
114
+ 'km_update_booking',
115
+ 'Correct the working details of a gig that is already agreed: its title, event type, guest count, '
116
+ + 'setup time, venue, client notes, internal notes, special requirements, and its status. '
117
+ + 'CONFIRMING IS CONSEQUENTIAL: setting status to confirmed marks that whole date as taken across '
118
+ + 'KM Hub, so the public booking page, the lead forms and every availability check start turning '
119
+ + 'other people away from it. Moving the status back to pending releases the date and offers it '
120
+ + 'again. Only make that call when the performer has actually said the gig is on or off. '
121
+ + 'WHAT THIS TOOL WILL NOT DO, by design, so do not try: it cannot move a booking to another date, '
122
+ + 'cannot change the start or end time, cannot change the fee, deposit or balance, cannot mark money '
123
+ + 'as paid, cannot mark a gig completed, and cannot cancel one. Those bind the business or free a '
124
+ + 'date for someone else, so a person does them in KM Hub. It also refuses to touch a gig that is '
125
+ + 'already completed or cancelled, and any booking owned by the meeting scheduler. Every change is '
126
+ + 'written to the AI activity log so the owner can see it; booking edits are not auto-undoable, so '
127
+ + 'get it right rather than counting on undo.',
128
+ {
129
+ booking_id: z.string().describe('UUID of the booking (from km_my_schedule or km_list_bookings).'),
130
+ title: z.string().optional().describe('What the gig is called on the calendar.'),
131
+ event_type: z.string().optional().describe('e.g. wedding, corporate, birthday. The value "meeting" is reserved by the scheduler and is refused.'),
132
+ guest_count: z.number().int().min(0).nullable().optional().describe('How many people are expected. Send null to clear it.'),
133
+ setup_start_time: z.string().nullable().optional().describe('Load-in time, HH:MM in 24 hour time. This is the performer\'s own logistics, not the event start. Send null to clear it.'),
134
+ venue_id: z.string().optional().describe('UUID of a venue already in this workspace.'),
135
+ client_notes: z.string().optional().describe('Notes the client would be shown.'),
136
+ internal_notes: z.string().optional().describe('Notes only the team sees.'),
137
+ special_requirements: z.string().optional().describe('Anything the gig needs: access, power, dress code, dietary.'),
138
+ status: z
139
+ .enum(['pending', 'confirmed', 'in_progress'])
140
+ .optional()
141
+ .describe('confirmed marks the date taken everywhere; pending releases it; in_progress means the gig is under way. Cancelled and completed are not available here.'),
142
+ confirm_token: z
143
+ .string()
144
+ .optional()
145
+ .describe(
146
+ 'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
147
+ + 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
148
+ + 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
149
+ + 'arguments. A yes for one action never authorises a different one.',
150
+ ),
151
+ },
152
+ async ({ booking_id, ...changes }) =>
153
+ out(await call('PATCH', `/bookings/${encodeURIComponent(String(booking_id || '').trim())}`, changes)),
154
+ );
155
+
156
+ server.tool(
157
+ 'km_blocked_dates',
158
+ 'The days that are already spoken for in KM Hub without being a gig: dates blocked off by hand with '
159
+ + 'their reason (holiday, family, studio time), and active holds, including dates a live lead has '
160
+ + 'pencilled in and that will expire on their own if the lead goes cold. Use it to answer "what am I '
161
+ + 'blocked out for", "why does that weekend show as unavailable", or before suggesting dates to '
162
+ + 'someone. Defaults to the next 90 days. Read only: blocking or unblocking a date decides whether '
163
+ + 'real work can be booked, so a person does that in KM Hub.',
164
+ {
165
+ from: z.string().optional().describe('First date, YYYY-MM-DD. Defaults to today in the workspace timezone.'),
166
+ to: z.string().optional().describe('Last date, YYYY-MM-DD. Defaults to 90 days out. At most a year at a time.'),
167
+ },
168
+ async ({ from, to }) => out(await call('GET', `/blocked-dates${qs({ from, to })}`)),
169
+ );
170
+ }