@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
package/tools/pending.mjs CHANGED
@@ -1,122 +1,122 @@
1
- /**
2
- * Tool family: pending - what a scheduled run decided should happen, waiting on a person.
3
- *
4
- * Workstream D1, the design that survived review.
5
- *
6
- * A scheduled run has nobody at the keyboard, so it cannot give the confirmation a
7
- * money-spending or client-visible action requires. The first shipped answer to that
8
- * was "then schedules only read". This is the second answer: a scheduled run may
9
- * DECIDE something should happen, and may not DO it. The intent lands here carrying
10
- * the sentence the SERVER wrote about it, and a person approves.
11
- *
12
- * 🚨 THE SENTENCE IS NOT WRITTEN BY A MODEL. It comes from describeAction() in the
13
- * API, the same code that will carry the action out. That is the whole reason it can
14
- * be trusted as a description of what approving will do. Never paraphrase it into
15
- * something friendlier when presenting it: the paraphrase is what a person would be
16
- * consenting to, and it is not what will run.
17
- *
18
- * 🚨 NAMING. This family is deliberately NOT called proposals and its tools are
19
- * deliberately NOT km_list_proposals. That name is already taken by money.mjs for
20
- * SALES proposals sent to clients, and MCP tool names are a flat namespace: a second
21
- * km_list_proposals would have shadowed a shipped tool rather than erroring, so
22
- * "show me my proposals" would quietly have started answering a different question.
23
- * Same reason the routes are /pending-actions and not /proposals, which money.ts
24
- * already owns and, being registered first, would have won the match.
25
- *
26
- * The family contract this file follows is documented in ./README.md.
27
- */
28
- import { z } from 'zod';
29
-
30
- export const FAMILY = 'pending';
31
-
32
- export const TOOLS = ['km_list_pending_actions', 'km_approve_pending_action', 'km_reject_pending_action'];
33
-
34
- export const PROFILES = ['*'];
35
-
36
- function routeMissing(r) {
37
- return r.status === 404 || r.status === 405 || r.status === 501;
38
- }
39
-
40
- const NOT_SUPPORTED =
41
- 'Your KM Hub does not have the approvals queue in the terminal yet. That is not a fault: everything else works as ' +
42
- 'normal, and anything waiting for a decision is in the web app at https://hub.kivimedia.co.';
43
-
44
- export function register(server, call, { out, text }) {
45
- server.tool(
46
- 'km_list_pending_actions',
47
- 'Actions that are waiting for a person to say yes: things a scheduled run decided should happen and then stopped, ' +
48
- 'because there was nobody there to confirm them. Nothing in this list has happened yet. ' +
49
- 'Call it when the user asks what needs them, what is waiting, why something did not happen, or what their ' +
50
- 'overnight automation got up to. It is not about sales proposals sent to clients - that is km_list_proposals. ' +
51
- 'Read `what_would_happen` out as it is written. That sentence was produced by the API code that would carry the ' +
52
- 'action out, not by whatever asked for it, which is exactly why it can be relied on. Do not smooth it into your ' +
53
- 'own words first: a person agreeing to your paraphrase has not agreed to the action. ' +
54
- '`raised_by_a_schedule: true` means no human chose this, only a rule did. Say so, because a client reading a ' +
55
- 'queue of confident-sounding actions will assume somebody meant each one.',
56
- {
57
- status: z
58
- .string()
59
- .optional()
60
- .describe(
61
- 'Which to show: pending (the default, and almost always what is wanted), approved, rejected, executed, ' +
62
- 'failed or expired.',
63
- ),
64
- },
65
- async ({ status }) => {
66
- const r = await call('GET', '/pending-actions' + (status ? `?status=${encodeURIComponent(String(status))}` : ''));
67
- if (routeMissing(r)) return text(NOT_SUPPORTED, true);
68
- return out(r);
69
- },
70
- );
71
-
72
- server.tool(
73
- 'km_approve_pending_action',
74
- 'Say yes to one waiting action and let it happen. This is the only thing that carries one out. ' +
75
- '🚨 NEVER call this on your own judgement. The user has to have seen this specific action and said yes to THIS ' +
76
- 'one, in this conversation. "They asked me to keep on top of the overdue invoices" is not consent to a specific ' +
77
- 'action a schedule composed overnight, and neither is any general instruction to keep things moving. If you are ' +
78
- 'not sure they meant this one, show it to them and ask. ' +
79
- 'The first call returns a confirmation request carrying the action sentence and a token, exactly as any other ' +
80
- 'action needing a yes does. Show that sentence, get an answer, and only then call again with confirm_token. The ' +
81
- 'confirmation step is not paperwork to get through: it is the step where the person reads what will actually ' +
82
- 'happen. ' +
83
- 'A key belonging to something that runs on a schedule cannot call this at all - the API refuses it - because a ' +
84
- 'schedule approving its own work is just acting unsupervised with extra steps.',
85
- {
86
- pending_action_id: z.string().describe('The id from km_list_pending_actions.'),
87
- confirm_token: z
88
- .string()
89
- .optional()
90
- .describe(
91
- 'Only on the second call, after the user has read the action sentence and said yes. Never invent one, and ' +
92
- 'never carry one over from a different action.',
93
- ),
94
- },
95
- async ({ pending_action_id, confirm_token }) => {
96
- const id = String(pending_action_id || '').trim();
97
- if (!id) return text('I need the id. Run km_list_pending_actions to see what is waiting.', true);
98
- const body = {};
99
- if (confirm_token) body.confirm_token = String(confirm_token);
100
- const r = await call('POST', `/pending-actions/${encodeURIComponent(id)}/approve`, body);
101
- if (routeMissing(r)) return text(NOT_SUPPORTED, true);
102
- return out(r);
103
- },
104
- );
105
-
106
- server.tool(
107
- 'km_reject_pending_action',
108
- 'Decline a waiting action so it stops sitting in the queue. Nothing happens as a result, which is the point. ' +
109
- 'This needs no confirmation on purpose: putting friction in front of "no" nudges a tired person toward yes, and ' +
110
- 'declining is the reversible direction. Afterwards, say plainly that the action did not happen.',
111
- {
112
- pending_action_id: z.string().describe('The id from km_list_pending_actions.'),
113
- },
114
- async ({ pending_action_id }) => {
115
- const id = String(pending_action_id || '').trim();
116
- if (!id) return text('I need the id. Run km_list_pending_actions to see what is waiting.', true);
117
- const r = await call('POST', `/pending-actions/${encodeURIComponent(id)}/reject`, {});
118
- if (routeMissing(r)) return text(NOT_SUPPORTED, true);
119
- return out(r);
120
- },
121
- );
122
- }
1
+ /**
2
+ * Tool family: pending - what a scheduled run decided should happen, waiting on a person.
3
+ *
4
+ * Workstream D1, the design that survived review.
5
+ *
6
+ * A scheduled run has nobody at the keyboard, so it cannot give the confirmation a
7
+ * money-spending or client-visible action requires. The first shipped answer to that
8
+ * was "then schedules only read". This is the second answer: a scheduled run may
9
+ * DECIDE something should happen, and may not DO it. The intent lands here carrying
10
+ * the sentence the SERVER wrote about it, and a person approves.
11
+ *
12
+ * 🚨 THE SENTENCE IS NOT WRITTEN BY A MODEL. It comes from describeAction() in the
13
+ * API, the same code that will carry the action out. That is the whole reason it can
14
+ * be trusted as a description of what approving will do. Never paraphrase it into
15
+ * something friendlier when presenting it: the paraphrase is what a person would be
16
+ * consenting to, and it is not what will run.
17
+ *
18
+ * 🚨 NAMING. This family is deliberately NOT called proposals and its tools are
19
+ * deliberately NOT km_list_proposals. That name is already taken by money.mjs for
20
+ * SALES proposals sent to clients, and MCP tool names are a flat namespace: a second
21
+ * km_list_proposals would have shadowed a shipped tool rather than erroring, so
22
+ * "show me my proposals" would quietly have started answering a different question.
23
+ * Same reason the routes are /pending-actions and not /proposals, which money.ts
24
+ * already owns and, being registered first, would have won the match.
25
+ *
26
+ * The family contract this file follows is documented in ./README.md.
27
+ */
28
+ import { z } from 'zod';
29
+
30
+ export const FAMILY = 'pending';
31
+
32
+ export const TOOLS = ['km_list_pending_actions', 'km_approve_pending_action', 'km_reject_pending_action'];
33
+
34
+ export const PROFILES = ['*'];
35
+
36
+ function routeMissing(r) {
37
+ return r.status === 404 || r.status === 405 || r.status === 501;
38
+ }
39
+
40
+ const NOT_SUPPORTED =
41
+ 'Your KM Hub does not have the approvals queue in the terminal yet. That is not a fault: everything else works as ' +
42
+ 'normal, and anything waiting for a decision is in the web app at https://hub.kivimedia.co.';
43
+
44
+ export function register(server, call, { out, text }) {
45
+ server.tool(
46
+ 'km_list_pending_actions',
47
+ 'Actions that are waiting for a person to say yes: things a scheduled run decided should happen and then stopped, ' +
48
+ 'because there was nobody there to confirm them. Nothing in this list has happened yet. ' +
49
+ 'Call it when the user asks what needs them, what is waiting, why something did not happen, or what their ' +
50
+ 'overnight automation got up to. It is not about sales proposals sent to clients - that is km_list_proposals. ' +
51
+ 'Read `what_would_happen` out as it is written. That sentence was produced by the API code that would carry the ' +
52
+ 'action out, not by whatever asked for it, which is exactly why it can be relied on. Do not smooth it into your ' +
53
+ 'own words first: a person agreeing to your paraphrase has not agreed to the action. ' +
54
+ '`raised_by_a_schedule: true` means no human chose this, only a rule did. Say so, because a client reading a ' +
55
+ 'queue of confident-sounding actions will assume somebody meant each one.',
56
+ {
57
+ status: z
58
+ .string()
59
+ .optional()
60
+ .describe(
61
+ 'Which to show: pending (the default, and almost always what is wanted), approved, rejected, executed, ' +
62
+ 'failed or expired.',
63
+ ),
64
+ },
65
+ async ({ status }) => {
66
+ const r = await call('GET', '/pending-actions' + (status ? `?status=${encodeURIComponent(String(status))}` : ''));
67
+ if (routeMissing(r)) return text(NOT_SUPPORTED, true);
68
+ return out(r);
69
+ },
70
+ );
71
+
72
+ server.tool(
73
+ 'km_approve_pending_action',
74
+ 'Say yes to one waiting action and let it happen. This is the only thing that carries one out. ' +
75
+ '🚨 NEVER call this on your own judgement. The user has to have seen this specific action and said yes to THIS ' +
76
+ 'one, in this conversation. "They asked me to keep on top of the overdue invoices" is not consent to a specific ' +
77
+ 'action a schedule composed overnight, and neither is any general instruction to keep things moving. If you are ' +
78
+ 'not sure they meant this one, show it to them and ask. ' +
79
+ 'The first call returns a confirmation request carrying the action sentence and a token, exactly as any other ' +
80
+ 'action needing a yes does. Show that sentence, get an answer, and only then call again with confirm_token. The ' +
81
+ 'confirmation step is not paperwork to get through: it is the step where the person reads what will actually ' +
82
+ 'happen. ' +
83
+ 'A key belonging to something that runs on a schedule cannot call this at all - the API refuses it - because a ' +
84
+ 'schedule approving its own work is just acting unsupervised with extra steps.',
85
+ {
86
+ pending_action_id: z.string().describe('The id from km_list_pending_actions.'),
87
+ confirm_token: z
88
+ .string()
89
+ .optional()
90
+ .describe(
91
+ 'Only on the second call, after the user has read the action sentence and said yes. Never invent one, and ' +
92
+ 'never carry one over from a different action.',
93
+ ),
94
+ },
95
+ async ({ pending_action_id, confirm_token }) => {
96
+ const id = String(pending_action_id || '').trim();
97
+ if (!id) return text('I need the id. Run km_list_pending_actions to see what is waiting.', true);
98
+ const body = {};
99
+ if (confirm_token) body.confirm_token = String(confirm_token);
100
+ const r = await call('POST', `/pending-actions/${encodeURIComponent(id)}/approve`, body);
101
+ if (routeMissing(r)) return text(NOT_SUPPORTED, true);
102
+ return out(r);
103
+ },
104
+ );
105
+
106
+ server.tool(
107
+ 'km_reject_pending_action',
108
+ 'Decline a waiting action so it stops sitting in the queue. Nothing happens as a result, which is the point. ' +
109
+ 'This needs no confirmation on purpose: putting friction in front of "no" nudges a tired person toward yes, and ' +
110
+ 'declining is the reversible direction. Afterwards, say plainly that the action did not happen.',
111
+ {
112
+ pending_action_id: z.string().describe('The id from km_list_pending_actions.'),
113
+ },
114
+ async ({ pending_action_id }) => {
115
+ const id = String(pending_action_id || '').trim();
116
+ if (!id) return text('I need the id. Run km_list_pending_actions to see what is waiting.', true);
117
+ const r = await call('POST', `/pending-actions/${encodeURIComponent(id)}/reject`, {});
118
+ if (routeMissing(r)) return text(NOT_SUPPORTED, true);
119
+ return out(r);
120
+ },
121
+ );
122
+ }
package/tools/photos.mjs CHANGED
@@ -1,140 +1,140 @@
1
- /**
2
- * Tool family: photos - the work-photo library, and the photos on an outreach draft.
3
- *
4
- * Until now the terminal could see galleries and was told, in as many words, that
5
- * "adding images happens in KM Hub". So a session could write every word of an
6
- * email and then had to hand the visual half back to a person. These four tools
7
- * close that: get photos into the library, choose which ones ride an email, and
8
- * choose the shape they sit in.
9
- *
10
- * The safety line is the same as the rest of outreach: nothing here sends. A
11
- * draft with a gallery on it is still a draft, and a person still approves it.
12
- *
13
- * The family contract this file follows is documented in ./README.md.
14
- */
15
- import { z } from 'zod';
16
-
17
- export const FAMILY = 'photos';
18
-
19
- export const TOOLS = [
20
- 'km_list_work_photos',
21
- 'km_add_work_photos',
22
- 'km_attach_draft_photos',
23
- 'km_remove_draft_photos',
24
- ];
25
-
26
- export const PROFILES = ['outreach', 'content'];
27
-
28
- const LAYOUT = ['grid', 'pyramid'];
29
-
30
- export function register(server, call, { out }) {
31
- server.tool(
32
- 'km_list_work_photos',
33
- 'The real work photos this business has on file, grouped into the themed sets they belong to, each with its caption, ' +
34
- 'its hosted URL and the id you need to put it on an email. ' +
35
- 'Reach for it before attaching photos to anything, so you are choosing from what actually exists rather than guessing, ' +
36
- 'and when the user asks what imagery they have. ' +
37
- 'These are the photos that ride inside first-touch outreach emails, so they are also the honest answer to "what does a ' +
38
- 'prospect see when we write to them". ' +
39
- 'An empty library is worth saying out loud: it means every email this workspace sends is text only. Read only.',
40
- {},
41
- async () => out(await call('GET', '/photos')),
42
- );
43
-
44
- server.tool(
45
- 'km_add_work_photos',
46
- 'Add photos to the work-photo library, either by URL or as base64, into a named set. Once they are in, any outreach draft ' +
47
- 'in the workspace can use them. ' +
48
- 'Reach for it when the user hands you images, points you at some, or asks to get their work photos in. ' +
49
- 'Give each one a short caption where you can: the caption becomes the image alt text that a recipient with images turned ' +
50
- 'off actually reads, so a photo with no caption is a blank box to them. ' +
51
- '🚨 SIZE IS THE ONE THING THAT WILL BITE. Each photo must be under about 1.2MB, and a phone original is five times that. ' +
52
- 'KM Hub resizes in the browser before uploading; nothing here can, so an oversized photo is REFUSED rather than quietly ' +
53
- 'accepted, and the answer tells you which one and why. Resize to roughly 1600px on the longest edge as a JPEG around 82 ' +
54
- 'percent quality and send it again. Do that yourself rather than asking the user to. ' +
55
- 'JPEG, PNG and GIF only: HEIC and WebP render as a broken box in Gmail and Outlook, which is worse than sending no photo. ' +
56
- 'It adds to the library and nothing else. It does not attach anything to an email and it does not send.',
57
- {
58
- photos: z
59
- .array(
60
- z.object({
61
- url: z.string().optional().describe('Public http or https URL to fetch the image from.'),
62
- base64: z.string().optional().describe('The image itself, base64 encoded. A data: URL is fine. Use this for a local file.'),
63
- caption: z.string().optional().describe('Short human caption. Becomes the alt text, and helps the matcher pick this set for the right lead.'),
64
- filename: z.string().optional().describe('Original filename, used to name the stored file. Optional.'),
65
- }),
66
- )
67
- .min(1)
68
- .max(20)
69
- .describe('The photos to add. Each needs either a url or a base64. Twenty per call.'),
70
- theme: z
71
- .string()
72
- .optional()
73
- .describe(
74
- 'The set these belong to, in plain words, like "burning rubber" or "grand openings". Default "my best work". ' +
75
- 'Sets are how the right photos reach the right lead, so a name that describes the WORK beats a name that describes the date.',
76
- ),
77
- },
78
- async ({ photos, theme }) => out(await call('POST', '/photos', { photos, theme })),
79
- );
80
-
81
- server.tool(
82
- 'km_attach_draft_photos',
83
- 'Put a photo gallery on an outreach draft that is still waiting for approval, and choose the shape it sits in. ' +
84
- 'This is how "add some photos to that email" actually lands: the recipient opens the message and sees the words followed ' +
85
- 'by the pictures. ' +
86
- 'LAYOUT. "pyramid" is one big image across the top, then two side by side, then three, which is the shape that reads as ' +
87
- 'designed rather than dumped, and it is built around six photos. "grid" is the plainer two-across block and is what every ' +
88
- 'existing campaign already sends. ' +
89
- 'Pick the photos with photo_ids when the order matters, and put the strongest image FIRST, because in a pyramid the first ' +
90
- 'one is the big one. Pass a theme instead to take a whole set in its own order. ' +
91
- 'Attaching REPLACES any gallery already on the draft rather than adding a second one. ' +
92
- 'The wording stays yours afterwards: km_edit_outreach_draft rebuilds the message around these photos and keeps them, so ' +
93
- 'there is no need to finish the text before adding images. ' +
94
- 'It refuses on a message that is already approved, scheduled, sent or cancelled, on anything that is not email, and on a ' +
95
- 'draft built from a saved template, and it says which. It does NOT approve and does NOT send.',
96
- {
97
- id: z.string().describe('UUID of the draft (see km_list_outreach_drafts).'),
98
- layout: z
99
- .enum(LAYOUT)
100
- .optional()
101
- .describe('pyramid for one big then two then three. grid for the plain two-across block. Default grid.'),
102
- photo_ids: z
103
- .array(z.string())
104
- .optional()
105
- .describe('UUIDs from km_list_work_photos, in the order they should appear. The first one is the hero in a pyramid.'),
106
- theme: z
107
- .string()
108
- .optional()
109
- .describe('Take a whole themed set instead of naming photos. Ignored when photo_ids is given.'),
110
- count: z
111
- .number()
112
- .int()
113
- .min(1)
114
- .max(12)
115
- .optional()
116
- .describe('How many photos to use. Defaults to what the layout is built for: 6 for a pyramid, 10 for a grid.'),
117
- heading: z
118
- .string()
119
- .optional()
120
- .describe('The quiet line above the photos. Default "A few pieces of our recent work:".'),
121
- },
122
- async ({ id, layout, photo_ids, theme, count, heading }) =>
123
- out(await call('POST', `/outreach-drafts/${encodeURIComponent(String(id))}/photos`, {
124
- layout, photo_ids, theme, count, heading,
125
- })),
126
- );
127
-
128
- server.tool(
129
- 'km_remove_draft_photos',
130
- 'Take the photo gallery back off a draft, so it goes back to being a plain text email. ' +
131
- 'Reach for it when the user says the pictures are wrong, too many, or not right for this prospect, and you want the ' +
132
- 'message clean before choosing again. ' +
133
- 'It removes the photos from THIS ONE MESSAGE. The photos stay in the library and every other draft keeps its own. ' +
134
- 'It only works while the message is still a draft, and it does not send anything.',
135
- {
136
- id: z.string().describe('UUID of the draft (see km_list_outreach_drafts).'),
137
- },
138
- async ({ id }) => out(await call('DELETE', `/outreach-drafts/${encodeURIComponent(String(id))}/photos`)),
139
- );
140
- }
1
+ /**
2
+ * Tool family: photos - the work-photo library, and the photos on an outreach draft.
3
+ *
4
+ * Until now the terminal could see galleries and was told, in as many words, that
5
+ * "adding images happens in KM Hub". So a session could write every word of an
6
+ * email and then had to hand the visual half back to a person. These four tools
7
+ * close that: get photos into the library, choose which ones ride an email, and
8
+ * choose the shape they sit in.
9
+ *
10
+ * The safety line is the same as the rest of outreach: nothing here sends. A
11
+ * draft with a gallery on it is still a draft, and a person still approves it.
12
+ *
13
+ * The family contract this file follows is documented in ./README.md.
14
+ */
15
+ import { z } from 'zod';
16
+
17
+ export const FAMILY = 'photos';
18
+
19
+ export const TOOLS = [
20
+ 'km_list_work_photos',
21
+ 'km_add_work_photos',
22
+ 'km_attach_draft_photos',
23
+ 'km_remove_draft_photos',
24
+ ];
25
+
26
+ export const PROFILES = ['outreach', 'content'];
27
+
28
+ const LAYOUT = ['grid', 'pyramid'];
29
+
30
+ export function register(server, call, { out }) {
31
+ server.tool(
32
+ 'km_list_work_photos',
33
+ 'The real work photos this business has on file, grouped into the themed sets they belong to, each with its caption, ' +
34
+ 'its hosted URL and the id you need to put it on an email. ' +
35
+ 'Reach for it before attaching photos to anything, so you are choosing from what actually exists rather than guessing, ' +
36
+ 'and when the user asks what imagery they have. ' +
37
+ 'These are the photos that ride inside first-touch outreach emails, so they are also the honest answer to "what does a ' +
38
+ 'prospect see when we write to them". ' +
39
+ 'An empty library is worth saying out loud: it means every email this workspace sends is text only. Read only.',
40
+ {},
41
+ async () => out(await call('GET', '/photos')),
42
+ );
43
+
44
+ server.tool(
45
+ 'km_add_work_photos',
46
+ 'Add photos to the work-photo library, either by URL or as base64, into a named set. Once they are in, any outreach draft ' +
47
+ 'in the workspace can use them. ' +
48
+ 'Reach for it when the user hands you images, points you at some, or asks to get their work photos in. ' +
49
+ 'Give each one a short caption where you can: the caption becomes the image alt text that a recipient with images turned ' +
50
+ 'off actually reads, so a photo with no caption is a blank box to them. ' +
51
+ '🚨 SIZE IS THE ONE THING THAT WILL BITE. Each photo must be under about 1.2MB, and a phone original is five times that. ' +
52
+ 'KM Hub resizes in the browser before uploading; nothing here can, so an oversized photo is REFUSED rather than quietly ' +
53
+ 'accepted, and the answer tells you which one and why. Resize to roughly 1600px on the longest edge as a JPEG around 82 ' +
54
+ 'percent quality and send it again. Do that yourself rather than asking the user to. ' +
55
+ 'JPEG, PNG and GIF only: HEIC and WebP render as a broken box in Gmail and Outlook, which is worse than sending no photo. ' +
56
+ 'It adds to the library and nothing else. It does not attach anything to an email and it does not send.',
57
+ {
58
+ photos: z
59
+ .array(
60
+ z.object({
61
+ url: z.string().optional().describe('Public http or https URL to fetch the image from.'),
62
+ base64: z.string().optional().describe('The image itself, base64 encoded. A data: URL is fine. Use this for a local file.'),
63
+ caption: z.string().optional().describe('Short human caption. Becomes the alt text, and helps the matcher pick this set for the right lead.'),
64
+ filename: z.string().optional().describe('Original filename, used to name the stored file. Optional.'),
65
+ }),
66
+ )
67
+ .min(1)
68
+ .max(20)
69
+ .describe('The photos to add. Each needs either a url or a base64. Twenty per call.'),
70
+ theme: z
71
+ .string()
72
+ .optional()
73
+ .describe(
74
+ 'The set these belong to, in plain words, like "burning rubber" or "grand openings". Default "my best work". ' +
75
+ 'Sets are how the right photos reach the right lead, so a name that describes the WORK beats a name that describes the date.',
76
+ ),
77
+ },
78
+ async ({ photos, theme }) => out(await call('POST', '/photos', { photos, theme })),
79
+ );
80
+
81
+ server.tool(
82
+ 'km_attach_draft_photos',
83
+ 'Put a photo gallery on an outreach draft that is still waiting for approval, and choose the shape it sits in. ' +
84
+ 'This is how "add some photos to that email" actually lands: the recipient opens the message and sees the words followed ' +
85
+ 'by the pictures. ' +
86
+ 'LAYOUT. "pyramid" is one big image across the top, then two side by side, then three, which is the shape that reads as ' +
87
+ 'designed rather than dumped, and it is built around six photos. "grid" is the plainer two-across block and is what every ' +
88
+ 'existing campaign already sends. ' +
89
+ 'Pick the photos with photo_ids when the order matters, and put the strongest image FIRST, because in a pyramid the first ' +
90
+ 'one is the big one. Pass a theme instead to take a whole set in its own order. ' +
91
+ 'Attaching REPLACES any gallery already on the draft rather than adding a second one. ' +
92
+ 'The wording stays yours afterwards: km_edit_outreach_draft rebuilds the message around these photos and keeps them, so ' +
93
+ 'there is no need to finish the text before adding images. ' +
94
+ 'It refuses on a message that is already approved, scheduled, sent or cancelled, on anything that is not email, and on a ' +
95
+ 'draft built from a saved template, and it says which. It does NOT approve and does NOT send.',
96
+ {
97
+ id: z.string().describe('UUID of the draft (see km_list_outreach_drafts).'),
98
+ layout: z
99
+ .enum(LAYOUT)
100
+ .optional()
101
+ .describe('pyramid for one big then two then three. grid for the plain two-across block. Default grid.'),
102
+ photo_ids: z
103
+ .array(z.string())
104
+ .optional()
105
+ .describe('UUIDs from km_list_work_photos, in the order they should appear. The first one is the hero in a pyramid.'),
106
+ theme: z
107
+ .string()
108
+ .optional()
109
+ .describe('Take a whole themed set instead of naming photos. Ignored when photo_ids is given.'),
110
+ count: z
111
+ .number()
112
+ .int()
113
+ .min(1)
114
+ .max(12)
115
+ .optional()
116
+ .describe('How many photos to use. Defaults to what the layout is built for: 6 for a pyramid, 10 for a grid.'),
117
+ heading: z
118
+ .string()
119
+ .optional()
120
+ .describe('The quiet line above the photos. Default "A few pieces of our recent work:".'),
121
+ },
122
+ async ({ id, layout, photo_ids, theme, count, heading }) =>
123
+ out(await call('POST', `/outreach-drafts/${encodeURIComponent(String(id))}/photos`, {
124
+ layout, photo_ids, theme, count, heading,
125
+ })),
126
+ );
127
+
128
+ server.tool(
129
+ 'km_remove_draft_photos',
130
+ 'Take the photo gallery back off a draft, so it goes back to being a plain text email. ' +
131
+ 'Reach for it when the user says the pictures are wrong, too many, or not right for this prospect, and you want the ' +
132
+ 'message clean before choosing again. ' +
133
+ 'It removes the photos from THIS ONE MESSAGE. The photos stay in the library and every other draft keeps its own. ' +
134
+ 'It only works while the message is still a draft, and it does not send anything.',
135
+ {
136
+ id: z.string().describe('UUID of the draft (see km_list_outreach_drafts).'),
137
+ },
138
+ async ({ id }) => out(await call('DELETE', `/outreach-drafts/${encodeURIComponent(String(id))}/photos`)),
139
+ );
140
+ }