@kivimedia/kmhub 2.0.0 → 2.9.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 +6 -5
- package/bin/kmhub.mjs +20 -7
- package/coach-book-output-guard.mjs +760 -0
- package/index.mjs +2 -0
- package/package.json +8 -3
- package/prompts/briefing.md +29 -0
- package/prompts/luxury.md +70 -0
- package/prompts/play.md +49 -0
- package/prompts/run.md +36 -0
- package/prompts/setup.md +33 -0
- package/prompts/vs-booked.md +46 -0
- package/prompts/what-can-you-do.md +40 -0
- package/prompts.mjs +109 -0
- package/read-only-tools.json +142 -0
- package/remote.mjs +815 -99
- package/tools/balloon-costing.mjs +80 -0
- package/tools/booking-equipment.mjs +110 -0
- package/tools/bridges.mjs +54 -0
- package/tools/calendar.mjs +9 -0
- package/tools/capabilities.mjs +155 -0
- package/tools/catalog.mjs +288 -0
- package/tools/clubs.mjs +176 -0
- package/tools/coach.mjs +771 -0
- package/tools/compare.mjs +76 -0
- package/tools/core.mjs +21 -0
- package/tools/crm.mjs +12 -3
- package/tools/dubsado.mjs +137 -0
- package/tools/exports.mjs +128 -0
- package/tools/fact-review.mjs +125 -0
- package/tools/flows.mjs +261 -0
- package/tools/forms.mjs +158 -0
- package/tools/gols.mjs +134 -0
- package/tools/hr.mjs +162 -0
- package/tools/knowledge.mjs +4 -3
- package/tools/marketing.mjs +396 -0
- package/tools/meta.mjs +2 -2
- package/tools/military.mjs +244 -0
- package/tools/outreach.mjs +27 -4
- package/tools/pending.mjs +122 -0
- package/tools/photos.mjs +140 -0
- package/tools/plays.mjs +1 -1
- package/tools/profile.mjs +118 -0
- package/tools/radar.mjs +173 -0
- package/tools/recurring-invoices.mjs +149 -0
- package/tools/reengage.mjs +434 -0
- package/tools/schedules.mjs +55 -0
- package/tools/setup.mjs +168 -0
- package/tools/sops-bridges.mjs +86 -0
- package/tools/sops.mjs +314 -0
- package/tools/sourcing.mjs +50 -2
- package/tools/strategy.mjs +146 -0
- package/tools/studio.mjs +132 -0
- package/tools/venueradar.mjs +151 -0
- package/tools/voice.mjs +134 -0
- package/tools.mjs +70 -12
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: balloon-costing - what a balloon build actually costs, and the way
|
|
3
|
+
* back to the window that holds it.
|
|
4
|
+
*
|
|
5
|
+
* Freshdesk #271. Jackie went looking for the price of her balloon COLUMNS to check
|
|
6
|
+
* her margin, closed the window by accident, and could not find her way back: "I
|
|
7
|
+
* always have this problem of not getting back to the correct window using Claude and
|
|
8
|
+
* KM Hub once it is closed."
|
|
9
|
+
*
|
|
10
|
+
* Two things were wrong, and this family fixes the first while the shared `out()`
|
|
11
|
+
* helper fixes the second.
|
|
12
|
+
*
|
|
13
|
+
* 1. km_list_job_costs reads `cost_jobs`, the GENERAL costing table. The balloon
|
|
14
|
+
* calculator writes balloon_jobs and balloon_inventory (mig 393), which no route
|
|
15
|
+
* had ever read. So the one tool that sounded like the answer was reading a
|
|
16
|
+
* different set of books.
|
|
17
|
+
* 2. Every row this returns carries `open_url`, and the body carries
|
|
18
|
+
* `open_in_km_hub`, which `out()` appends as a plain "Open in KM Hub" line.
|
|
19
|
+
*
|
|
20
|
+
* 🚨 READ ONLY. Costing a job, importing a price list and editing a line item act on
|
|
21
|
+
* the owner's own pricing and stay in the web app.
|
|
22
|
+
*
|
|
23
|
+
* The family contract this file follows is documented in ./README.md.
|
|
24
|
+
*/
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
|
|
27
|
+
export const FAMILY = 'balloon-costing';
|
|
28
|
+
|
|
29
|
+
export const TOOLS = ['km_list_balloon_costs'];
|
|
30
|
+
|
|
31
|
+
export const PROFILES = ['money'];
|
|
32
|
+
|
|
33
|
+
function routeMissing(r) {
|
|
34
|
+
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const NOT_SUPPORTED =
|
|
38
|
+
'Your KM Hub does not expose balloon job costing to the terminal yet. That is not a fault: the workspace is fine and ' +
|
|
39
|
+
'every other tool works as normal. The calculator is there in the web app at ' +
|
|
40
|
+
'https://hub.kivimedia.co/?page=job-cost.';
|
|
41
|
+
|
|
42
|
+
export function register(server, call, { out, text, qs }) {
|
|
43
|
+
server.tool(
|
|
44
|
+
'km_list_balloon_costs',
|
|
45
|
+
'What a balloon build costs and what it was quoted at: the priced balloon catalog (pack price, pack count and the ' +
|
|
46
|
+
'per-each price derived from them) and the costed balloon jobs with their material, labor and expense lines, the ' +
|
|
47
|
+
'overhead and profit percentages behind each, and the margin those percentages imply. ' +
|
|
48
|
+
'Reach for this for any question about balloon pricing or balloon margin, for example "what do my columns cost ' +
|
|
49
|
+
'me" or "what margin did I build into the Saturday job". ' +
|
|
50
|
+
'🚨 This is a DIFFERENT set of books from km_list_job_costs, which reads the general costing table and knows ' +
|
|
51
|
+
'nothing about balloons. Use this one for a balloon question. ' +
|
|
52
|
+
'🚨 indirect_expense_percentage and profit_percentage are ASSUMPTIONS the owner typed when costing the job, not ' +
|
|
53
|
+
'margin achieved, so `targeted_margin_percentage` is targeted and never "made". `actual_margin_percentage` is ' +
|
|
54
|
+
'null until somebody has filled in the actual columns. ' +
|
|
55
|
+
'Every row carries `open_url` and the answer carries a link back to the page it came from: show it, because ' +
|
|
56
|
+
'losing the window is the reason this tool exists. Read only: pricing is edited in the web app.',
|
|
57
|
+
{
|
|
58
|
+
search: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe(
|
|
62
|
+
'Narrow to one thing by name, for example "column", "garland" or a client name. Matches catalog item names, ' +
|
|
63
|
+
'part numbers and descriptions, and matches jobs on client name, job number, description and the names of ' +
|
|
64
|
+
'the material lines inside them.',
|
|
65
|
+
),
|
|
66
|
+
limit: z
|
|
67
|
+
.number()
|
|
68
|
+
.int()
|
|
69
|
+
.min(1)
|
|
70
|
+
.max(100)
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('How many catalog items and how many jobs, newest job first. Default 25.'),
|
|
73
|
+
},
|
|
74
|
+
async ({ search, limit }) => {
|
|
75
|
+
const r = await call('GET', `/balloon-costing${qs({ search, limit })}`);
|
|
76
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
77
|
+
return out(r);
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: booking-equipment - what gear is going out on a job.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* This is the row that makes an equipment SOP reach a human being. A procedure
|
|
6
|
+
* attached to the Roland V160 is a document nobody opens until something says
|
|
7
|
+
* the V160 is on Tuesday's job; then it arrives in the brief on its own.
|
|
8
|
+
*
|
|
9
|
+
* Before migration 677 the only way to say that ran through a performer's shift,
|
|
10
|
+
* which is the right model for a DJ and the wrong one for production AV, where
|
|
11
|
+
* the pull sheet exists before anybody is rostered and a projector belongs to a
|
|
12
|
+
* ballroom rather than to a person.
|
|
13
|
+
*
|
|
14
|
+
* SAFETY. Nothing here contacts anybody or moves money. It does decide what a
|
|
15
|
+
* crew member is told to bring, which is why every line records where it came
|
|
16
|
+
* from: a demonstration row can never be mistaken for a real pull sheet.
|
|
17
|
+
*
|
|
18
|
+
* The family contract this file follows is documented in ./README.md.
|
|
19
|
+
*/
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
|
|
22
|
+
export const FAMILY = 'booking-equipment';
|
|
23
|
+
|
|
24
|
+
export const TOOLS = ['km_list_equipment', 'km_booking_gear', 'km_add_booking_gear', 'km_remove_booking_gear'];
|
|
25
|
+
|
|
26
|
+
// Lands in `full` only. An EMPTY list means exactly that - ['*'] would opt into
|
|
27
|
+
// every profile, which is the mistake tools.mjs documents six families making.
|
|
28
|
+
export const PROFILES = [];
|
|
29
|
+
|
|
30
|
+
export function register(server, call, { out }) {
|
|
31
|
+
server.tool(
|
|
32
|
+
'km_list_equipment',
|
|
33
|
+
'The gear this workspace owns: name, category, how many, condition and day rate. ' +
|
|
34
|
+
'REACH FOR THIS FIRST whenever gear is involved, because every other tool here needs an item id and this is the only ' +
|
|
35
|
+
'way to find one. Search by name to find the item an SOP is about, or filter by category to see what the business ' +
|
|
36
|
+
'actually has. ' +
|
|
37
|
+
'An empty catalogue is worth saying out loud: it means no equipment procedure can ever surface on a job, however ' +
|
|
38
|
+
'many are written, because nothing can say which job the gear is on. Read only.',
|
|
39
|
+
{
|
|
40
|
+
q: z.string().optional().describe('Free text against the item name, e.g. Roland or projector.'),
|
|
41
|
+
category: z.string().optional().describe('Only this category, e.g. Video, Audio, Lighting, Staging.'),
|
|
42
|
+
limit: z.number().int().positive().max(500).optional().describe('Page size, default 200.'),
|
|
43
|
+
offset: z.number().int().min(0).optional().describe('Skip this many. Use it when a page comes back full.'),
|
|
44
|
+
},
|
|
45
|
+
async (args) => {
|
|
46
|
+
const p = new URLSearchParams();
|
|
47
|
+
for (const [k, v] of Object.entries(args)) if (v !== undefined && v !== '') p.set(k, String(v));
|
|
48
|
+
const qs = p.toString();
|
|
49
|
+
return out(await call('GET', `/equipment${qs ? `?${qs}` : ''}`));
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
server.tool(
|
|
54
|
+
'km_booking_gear',
|
|
55
|
+
'The gear going out on a job, room by room, with each item named and counted. ' +
|
|
56
|
+
'Reach for it when preparing a job, when somebody asks what is on the truck, or before briefing crew - and reach ' +
|
|
57
|
+
'for it whenever km_sop_brief comes back with no equipment procedures, because an empty gear list is almost always ' +
|
|
58
|
+
'the reason. A booking with nothing recorded here cannot surface a single equipment SOP, however many are written. ' +
|
|
59
|
+
'Lines marked source "seed" are demonstrations rather than a real pull sheet; say so rather than reading them out ' +
|
|
60
|
+
'as fact. Read only.',
|
|
61
|
+
{ booking_id: z.string().describe('The booking id from km_list_bookings or km_get_booking.') },
|
|
62
|
+
async ({ booking_id }) => out(await call('GET', `/bookings/${encodeURIComponent(booking_id)}/equipment`)),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
server.tool(
|
|
66
|
+
'km_add_booking_gear',
|
|
67
|
+
'Put gear on a job, as a list. This is what makes equipment procedures surface: attach an SOP to a projector with ' +
|
|
68
|
+
'km_attach_sop, record that the projector is on this job here, and it appears in that job\'s brief from then on. ' +
|
|
69
|
+
'Reach for it when a pull sheet is being built, when somebody says what is going out, or when a proposal has been ' +
|
|
70
|
+
'won and the gear on it is now real. ' +
|
|
71
|
+
'Send the whole list in one call rather than one item at a time - a half-built pull sheet is worse than none. ' +
|
|
72
|
+
'Put the room on every line where the job has rooms: "the V160, Valencia Ballroom" tells somebody where to walk, ' +
|
|
73
|
+
'and the same item in two rooms is two lines because two people set it up. ' +
|
|
74
|
+
'Re-sending the same list updates quantities instead of duplicating, so it is safe to run again. ' +
|
|
75
|
+
'Set source honestly: "seed" for anything you are adding to demonstrate the system, never for gear you were told ' +
|
|
76
|
+
'is actually going out.',
|
|
77
|
+
{
|
|
78
|
+
booking_id: z.string(),
|
|
79
|
+
items: z.array(z.object({
|
|
80
|
+
equipment_item_id: z.string().describe('The gear id. Look it up first; an id that points at nothing is refused rather than stored.'),
|
|
81
|
+
quantity: z.number().int().positive().optional().describe('How many. Defaults to 1.'),
|
|
82
|
+
room: z.string().optional().describe('Which room this one is for, named the way the client says it, e.g. "Valencia Ballroom".'),
|
|
83
|
+
notes: z.string().optional().describe('Anything the crew needs about this line specifically.'),
|
|
84
|
+
})).describe('The whole pull sheet, or the part of it being added.'),
|
|
85
|
+
source: z.enum(['manual', 'proposal', 'djep', 'import', 'seed']).optional()
|
|
86
|
+
.describe('Where this came from. "proposal" or "djep" when it was copied from one, with the number in source_ref. "seed" for a demonstration.'),
|
|
87
|
+
source_ref: z.string().optional().describe('The proposal number or DJEP job this was taken from, so the line can be traced back.'),
|
|
88
|
+
},
|
|
89
|
+
async ({ booking_id, ...body }) =>
|
|
90
|
+
out(await call('POST', `/bookings/${encodeURIComponent(booking_id)}/equipment`, body)),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
server.tool(
|
|
94
|
+
'km_remove_booking_gear',
|
|
95
|
+
'Take one piece of gear off a job. The equipment itself and any SOPs attached to it are untouched; this only says it ' +
|
|
96
|
+
'is no longer going out on this booking. ' +
|
|
97
|
+
'If the job has rooms, name the room - a line with no room is a different line from the one in the Valencia ' +
|
|
98
|
+
'Ballroom, and leaving it out removes every room\'s copy.',
|
|
99
|
+
{
|
|
100
|
+
booking_id: z.string(),
|
|
101
|
+
equipment_item_id: z.string(),
|
|
102
|
+
room: z.string().optional().describe('The room to remove it from. Leave it out to remove every copy across all rooms.'),
|
|
103
|
+
},
|
|
104
|
+
async ({ booking_id, equipment_item_id, room }) => {
|
|
105
|
+
const p = new URLSearchParams({ equipment_item_id });
|
|
106
|
+
if (room !== undefined) p.set('room', room);
|
|
107
|
+
return out(await call('DELETE', `/bookings/${encodeURIComponent(booking_id)}/equipment?${p.toString()}`));
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: bridges - which CRM this workspace sends its lead replies through, and
|
|
3
|
+
* whether that connection is alive right now.
|
|
4
|
+
*
|
|
5
|
+
* Some businesses run their inquiries out of a CRM with no usable API (Dubsado, SMPL).
|
|
6
|
+
* Kivi Media keeps a live session to it on a server, and KM Hub sends replies to those
|
|
7
|
+
* leads THROUGH the CRM, so the answer lands in the client's own record, from the
|
|
8
|
+
* business's own address, instead of arriving as a stray email from somewhere else.
|
|
9
|
+
*
|
|
10
|
+
* This family answers "is that connected and working". Reading and answering the leads
|
|
11
|
+
* themselves is the `dubsado` family. Same table behind both, and behind the card in
|
|
12
|
+
* Settings > Email Senders, so the terminal and the app never disagree.
|
|
13
|
+
*
|
|
14
|
+
* READ ONLY, on purpose. A `km_bridge_set_sending` shipped on 02-Sep and was removed the
|
|
15
|
+
* next morning: turning the routing off is a Kivi Media action now, and a KM Hub API key
|
|
16
|
+
* is welded to one workspace and carries no user, so nothing in a call from here can tell
|
|
17
|
+
* Kivi apart from the client. If somebody asks to switch it off, say that Kivi Media does
|
|
18
|
+
* it and usually the same day - do not go looking for another route.
|
|
19
|
+
*
|
|
20
|
+
* The family contract this file follows is documented in ./README.md.
|
|
21
|
+
*/
|
|
22
|
+
export const FAMILY = 'bridges';
|
|
23
|
+
|
|
24
|
+
export const TOOLS = ['km_bridges'];
|
|
25
|
+
|
|
26
|
+
export const PROFILES = ['*'];
|
|
27
|
+
|
|
28
|
+
function routeMissing(r) {
|
|
29
|
+
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const NOT_SUPPORTED =
|
|
33
|
+
'This KM Hub server does not report CRM connections yet. Nothing is wrong: it just means the ' +
|
|
34
|
+
'workspace sends email the ordinary way.';
|
|
35
|
+
|
|
36
|
+
export function register(server, call, { out, text }) {
|
|
37
|
+
server.tool(
|
|
38
|
+
'km_bridges',
|
|
39
|
+
'Whether this workspace sends its lead replies through a CRM (Dubsado, SMPL) rather than by ' +
|
|
40
|
+
'ordinary email, and whether that connection is alive RIGHT NOW. Call it before promising that ' +
|
|
41
|
+
'a reply will reach a lead, whenever somebody says a client never heard back, and any time you ' +
|
|
42
|
+
'are about to explain where their email comes from. A bridge shown as down is not something ' +
|
|
43
|
+
'the person can fix: say plainly that new inquiries are not arriving and replies to them cannot ' +
|
|
44
|
+
'send until the Kivi Media team revives it, and do not attempt a send that depends on it. Most ' +
|
|
45
|
+
'workspaces have no bridge at all, which is normal and worth saying just as plainly.',
|
|
46
|
+
{},
|
|
47
|
+
async () => {
|
|
48
|
+
const r = await call('GET', '/bridges');
|
|
49
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
50
|
+
return out(r);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
}
|
package/tools/calendar.mjs
CHANGED
|
@@ -139,6 +139,15 @@ export function register(server, call, { out, qs }) {
|
|
|
139
139
|
.enum(['pending', 'confirmed', 'in_progress'])
|
|
140
140
|
.optional()
|
|
141
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
|
+
),
|
|
142
151
|
},
|
|
143
152
|
async ({ booking_id, ...changes }) =>
|
|
144
153
|
out(await call('PATCH', `/bookings/${encodeURIComponent(String(booking_id || '').trim())}`, changes)),
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: capabilities - what KM Hub can do, and honestly where.
|
|
3
|
+
*
|
|
4
|
+
* km_capabilities answers "what is KM Hub" from ./capabilities.json, which is
|
|
5
|
+
* GENERATED from the web app's own nav registry (scripts/generate-capabilities.mjs).
|
|
6
|
+
* That generation is the whole point: a hand-written feature list is wrong within a
|
|
7
|
+
* month, and a connector that confidently describes a product that has moved is
|
|
8
|
+
* worse than one that says nothing.
|
|
9
|
+
*
|
|
10
|
+
* WHY THIS READS A LOCAL FILE AND NOT THE API. The answer is the same for every
|
|
11
|
+
* workspace on a given build, it is needed at the START of a session when the model
|
|
12
|
+
* is deciding what is even possible, and it must work when the API is slow. A round
|
|
13
|
+
* trip would buy nothing. Per-workspace filtering (which pillars this org has switched
|
|
14
|
+
* on) is a later addition and belongs on the API when it comes.
|
|
15
|
+
*
|
|
16
|
+
* 🚨 THE `reach` FIELD IS THE HONEST PART. Every page reports `terminal`,
|
|
17
|
+
* `terminal_read` or `web_only`. Claude MUST NOT promise to do a web_only thing
|
|
18
|
+
* from the terminal, and MUST NOT promise to EDIT a terminal_read page: those can
|
|
19
|
+
* be looked at from here and changed only in the web app. Say KM Hub does it, name
|
|
20
|
+
* the page, and point at the web app. Telling a client "yes I can" and then
|
|
21
|
+
* failing is worse than "KM Hub does that, here is where". The read distinction
|
|
22
|
+
* exists because Mark Fuller's 01-Sep-26 gap report caught two read-only pages
|
|
23
|
+
* reporting plain `terminal`, which reads as "you can work on this from here".
|
|
24
|
+
*
|
|
25
|
+
* The family contract this file follows is documented in ./README.md.
|
|
26
|
+
*/
|
|
27
|
+
import { readFileSync } from 'node:fs';
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
|
|
30
|
+
export const FAMILY = 'capabilities';
|
|
31
|
+
|
|
32
|
+
export const TOOLS = ['km_capabilities'];
|
|
33
|
+
|
|
34
|
+
// Knowing what the product is costs one small file and changes every other answer,
|
|
35
|
+
// so it loads in every profile.
|
|
36
|
+
export const PROFILES = ['*'];
|
|
37
|
+
|
|
38
|
+
let CACHE = null;
|
|
39
|
+
|
|
40
|
+
function load() {
|
|
41
|
+
if (CACHE) return CACHE;
|
|
42
|
+
try {
|
|
43
|
+
CACHE = JSON.parse(readFileSync(new URL('../capabilities.json', import.meta.url), 'utf8'));
|
|
44
|
+
} catch (e) {
|
|
45
|
+
CACHE = { error: String(e?.message || e) };
|
|
46
|
+
}
|
|
47
|
+
return CACHE;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Trim a page down to what a model needs to decide, dropping the empty fields. */
|
|
51
|
+
function slim(p, withTools) {
|
|
52
|
+
const o = { id: p.id, label: p.label, reach: p.reach };
|
|
53
|
+
// A page can be web-only because nobody built it yet, or because it should never
|
|
54
|
+
// be driven from a terminal at all. Saying which is the difference between "not
|
|
55
|
+
// yet" and "not ever", and a model that blurs them will keep offering to try.
|
|
56
|
+
if (p.web_only_by_design) o.web_only_by_design = true;
|
|
57
|
+
if (p.benefit) o.what_it_does = p.benefit;
|
|
58
|
+
if (withTools && p.tools?.length) o.tools = p.tools;
|
|
59
|
+
if (withTools && p.plays?.length) o.plays = p.plays;
|
|
60
|
+
return o;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function register(server, call, { text }) {
|
|
64
|
+
server.tool(
|
|
65
|
+
'km_capabilities',
|
|
66
|
+
'The map of everything KM Hub does, and honestly which parts you can do from here. ' +
|
|
67
|
+
'CALL THIS EARLY in any session where the user is deciding what to work on, asks what KM Hub can do, ' +
|
|
68
|
+
'asks whether it can handle some area of their business, wonders if it does something a different tool does, ' +
|
|
69
|
+
'or asks a question you are about to answer with "I do not think KM Hub does that". You very likely have not ' +
|
|
70
|
+
'seen the whole product: it is 7 pillars and over 100 feature pages, far more than the tools in front of you, ' +
|
|
71
|
+
'so an answer based only on your tool list will understate it badly. ' +
|
|
72
|
+
'What comes back is the same nav the web app renders - Marketing, Sales, Operations, HR, Clients, AI Team, ' +
|
|
73
|
+
'Settings - each with its groups, its pages, and one plain sentence per page about what that page is FOR. ' +
|
|
74
|
+
'🚨 Every page carries `reach`. `terminal` means tools or plays here can do it. `terminal_read` means you can ' +
|
|
75
|
+
'look from here but every change happens in the web app, so say where the editing lives up front. `web_only` ' +
|
|
76
|
+
'means KM Hub does it but this connector cannot reach it at all. NEVER offer to do a web_only thing or to ' +
|
|
77
|
+
'edit a terminal_read one. Say KM Hub does it, name the page, and ' +
|
|
78
|
+
'point them at https://hub.kivimedia.co. Claiming a capability you do not have and then failing costs more ' +
|
|
79
|
+
'trust than saying where it lives. ' +
|
|
80
|
+
'Use it to answer the whole question rather than the part you happen to hold: someone asking about follow-up ' +
|
|
81
|
+
'should hear that KM Hub also runs their newsletter, their SEO and their reviews. It reads a file, costs ' +
|
|
82
|
+
'nothing, sends nothing and changes nothing.',
|
|
83
|
+
{
|
|
84
|
+
pillar: z
|
|
85
|
+
.enum(['marketing', 'sales', 'operations', 'hr', 'support', 'strategy', 'setup'])
|
|
86
|
+
.optional()
|
|
87
|
+
.describe(
|
|
88
|
+
'Narrow to one pillar when the user asked about one area. marketing = being found and reaching out, ' +
|
|
89
|
+
'sales = pipeline through to close, operations = diary, money and delivery, hr = team, pay and time, ' +
|
|
90
|
+
'support = clients, conversations and reputation, strategy = the AI officers, decisions and reporting, ' +
|
|
91
|
+
'setup = account, billing and connections. Leave it out for the whole product.',
|
|
92
|
+
),
|
|
93
|
+
reach: z
|
|
94
|
+
.enum(['terminal', 'terminal_read', 'web_only', 'all'])
|
|
95
|
+
.optional()
|
|
96
|
+
.describe(
|
|
97
|
+
"Default 'all'. Use 'terminal' when you want what you can reach from here; it includes 'terminal_read' " +
|
|
98
|
+
"pages, so check each page's own reach before offering to CHANGE anything on it. Use 'terminal_read' " +
|
|
99
|
+
"for only the look-but-not-touch pages, and 'web_only' to answer \"what else is in there\" honestly.",
|
|
100
|
+
),
|
|
101
|
+
detail: z
|
|
102
|
+
.enum(['summary', 'full'])
|
|
103
|
+
.optional()
|
|
104
|
+
.describe(
|
|
105
|
+
"Default 'summary': pillars, groups, pages and what each is for. 'full' adds the exact tool and play " +
|
|
106
|
+
'names behind each page, which you want when planning a piece of work rather than describing the product.',
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
async ({ pillar, reach = 'all', detail = 'summary' }) => {
|
|
110
|
+
const doc = load();
|
|
111
|
+
if (doc.error) {
|
|
112
|
+
return text(
|
|
113
|
+
'The capability map did not load on this connector, so I cannot list what KM Hub does. ' +
|
|
114
|
+
'The workspace itself is fine and every other tool works as normal. Detail: ' + doc.error,
|
|
115
|
+
true,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const withTools = detail === 'full';
|
|
120
|
+
const pillars = [];
|
|
121
|
+
for (const p of doc.pillars) {
|
|
122
|
+
if (pillar && p.id !== pillar) continue;
|
|
123
|
+
const groups = [];
|
|
124
|
+
for (const g of p.groups) {
|
|
125
|
+
const pages = g.pages
|
|
126
|
+
// 'terminal' includes 'terminal_read': both are reachable, and a
|
|
127
|
+
// caller narrowing to what it can reach must not lose the read side.
|
|
128
|
+
.filter((x) => reach === 'all' || x.reach === reach || (reach === 'terminal' && x.reach === 'terminal_read'))
|
|
129
|
+
.map((x) => slim(x, withTools));
|
|
130
|
+
if (pages.length) groups.push({ group: g.header, pages });
|
|
131
|
+
}
|
|
132
|
+
if (groups.length) pillars.push({ id: p.id, label: p.label, tagline: p.tagline, groups });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const shown = pillars.reduce((n, p) => n + p.groups.reduce((m, g) => m + g.pages.length, 0), 0);
|
|
136
|
+
|
|
137
|
+
const body = {
|
|
138
|
+
product: 'KM Hub',
|
|
139
|
+
web_app: 'https://hub.kivimedia.co',
|
|
140
|
+
totals: doc.counts,
|
|
141
|
+
showing: { pillar: pillar || 'all', reach, pages: shown },
|
|
142
|
+
how_to_read:
|
|
143
|
+
"reach 'terminal' means you can do it from here. reach 'terminal_read' means you can LOOK at it from " +
|
|
144
|
+
'here but every change happens in the web app: say where the editing lives up front, never offer the ' +
|
|
145
|
+
"edit and fail at it. reach 'web_only' means KM Hub does it but this connector cannot yet, so name the " +
|
|
146
|
+
'page and point at the web app rather than offering to do it. ' +
|
|
147
|
+
"A page also carrying web_only_by_design will NEVER be reachable from a terminal: it is a deliberate " +
|
|
148
|
+
'boundary, not a backlog item, so do not imply it is coming.',
|
|
149
|
+
pillars,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return { content: [{ type: 'text', text: JSON.stringify(body, null, 2) }] };
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
}
|