@kivimedia/kmhub 2.9.1 → 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 -110
- 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
|
@@ -1,80 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,110 +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
|
-
}
|
|
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
|
+
}
|
package/tools/bridges.mjs
CHANGED
|
@@ -1,54 +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
|
-
}
|
|
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
|
+
}
|