@kivimedia/kmhub 2.9.1 → 2.11.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 +37 -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 -110
  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 +144 -134
  34. package/tools/hr.mjs +162 -162
  35. package/tools/knowledge.mjs +129 -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 +137 -134
  57. package/tools.mjs +407 -407
@@ -1,238 +1,238 @@
1
- /**
2
- * Tool family: outreach - the approval queue, the campaigns, the replies and the
3
- * email log.
4
- *
5
- * The safety line runs straight through this file and never moves: KM Hub writes
6
- * every outgoing message as a DRAFT, and a person approves it in KM Hub or the
7
- * Booth before anything leaves the building. So there is no approve tool here, no
8
- * send tool, no schedule tool and no delete tool, and every description says so
9
- * out loud, because the model will be asked to send and needs to know the answer
10
- * before it starts looking for a way.
11
- *
12
- * What IS here is the work a person actually wants help with: reading the
13
- * backlog, rewriting a message that is not right yet, seeing how a campaign is
14
- * doing, stopping one that should stop, triaging replies, and checking whether
15
- * anything landed.
16
- *
17
- * The family contract this file follows is documented in ./README.md.
18
- */
19
- import { z } from 'zod';
20
-
21
- export const FAMILY = 'outreach';
22
-
23
- export const TOOLS = [
24
- 'km_list_outreach_drafts',
25
- 'km_read_outreach_draft',
26
- 'km_edit_outreach_draft',
27
- 'km_list_outreach_campaigns',
28
- 'km_outreach_campaign_stats',
29
- 'km_pause_outreach_campaign',
30
- 'km_resume_outreach_campaign',
31
- 'km_list_outreach_replies',
32
- 'km_outreach_email_log',
33
- 'km_outreach_email_stats',
34
- ];
35
-
36
- // The cold outreach machine. tools.mjs already maps this family into the
37
- // 'outreach' profile; declaring it here keeps the file true on its own.
38
- export const PROFILES = ['outreach'];
39
-
40
- const DRAFT_STATUS = ['draft', 'approved', 'scheduled', 'sent', 'cancelled', 'all'];
41
- const CHANNEL = ['email', 'linkedin', 'ig', 'sms_whatsapp'];
42
- const CAMPAIGN_STATUS = ['draft', 'running', 'paused', 'completed', 'archived'];
43
- const REPLY_CLASS = [
44
- 'interested', 'not_interested', 'objection', 'ooo',
45
- 'wrong_person', 'unsubscribe', 'question', 'auto_reply', 'neutral',
46
- ];
47
-
48
- /**
49
- * @param {{ tool: Function }} server guarded registrar (see ./README.md)
50
- * @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
51
- * @param {{ out: Function, qs: Function }} helpers shared response helpers from ../tools.mjs
52
- */
53
- export function register(server, call, { out, qs }) {
54
- server.tool(
55
- 'km_list_outreach_drafts',
56
- 'Show the outreach messages waiting for approval: the emails and DMs that have been written for the user but have not gone anywhere yet. ' +
57
- 'Reach for it when the question is specifically about THIS queue: how many messages are pending, what is in the queue, or when they want to work through it one message at a time. ' +
58
- 'Do not answer the broad "what is waiting on me" from here. This is one channel of one kind of work, and km_briefing already carries the top of this queue ranked against the replies, tasks, invoices, enquiries and gigs that this tool cannot see, so a session that answers the whole day from this list reports a quarter of it as if it were all of it. ' +
59
- 'Each one comes back with the opening lines, who it is for, which campaign it belongs to and whether anyone has already edited it; ' +
60
- 'read one in full with km_read_outreach_draft. A backlog of a few hundred is normal, so this pages: the answer carries the exact ' +
61
- 'total and tells you whether there is more, so never report a count from the rows you can see. ' +
62
- 'This only reads. It cannot approve or send anything - a person does that in KM Hub.',
63
- {
64
- status: z.enum(DRAFT_STATUS).optional().describe('Default draft, which is the queue actually waiting on a human. Use all for the whole history.'),
65
- channel: z.enum(CHANNEL).optional().describe('Narrow to one channel.'),
66
- campaign_id: z.string().optional().describe('UUID of a campaign (see km_list_outreach_campaigns).'),
67
- contact_id: z.string().optional().describe('UUID of a contact (see km_find_contact).'),
68
- deal_id: z.string().optional().describe('UUID of a deal (see km_list_deals).'),
69
- limit: z.number().int().min(1).max(100).optional().describe('Default 20.'),
70
- offset: z.number().int().min(0).optional().describe('Skip this many before reading, for paging through a long backlog.'),
71
- },
72
- async ({ status, channel, campaign_id, contact_id, deal_id, limit, offset }) =>
73
- out(await call('GET', `/outreach-drafts${qs({ status, channel, campaign_id, contact_id, deal_id, limit, offset })}`)),
74
- );
75
-
76
- server.tool(
77
- 'km_read_outreach_draft',
78
- 'Read one waiting outreach message in full: the whole body, the subject, who it is addressed to, which campaign and which step of the ' +
79
- 'sequence it belongs to, and where it stands. Reach for it before you quote a message back to the user, judge it, or rewrite it, so you ' +
80
- 'are working from the real wording rather than the preview. It also reports whether the message can still be rewritten from here, ' +
81
- 'so you know before you try. Read only: it never approves and never sends.',
82
- {
83
- id: z.string().describe('UUID of the draft (see km_list_outreach_drafts).'),
84
- },
85
- async ({ id }) => out(await call('GET', `/outreach-drafts/${encodeURIComponent(String(id))}`)),
86
- );
87
-
88
- server.tool(
89
- 'km_edit_outreach_draft',
90
- 'Rewrite a message that is still waiting for approval. This is how "make it shorter", "lead with the fire" or "drop the exclamation marks" ' +
91
- 'actually lands: the wording in the Approval Queue is replaced with yours, so the user opens KM Hub and reads the better version. ' +
92
- 'Send the FULL replacement text, never a description of the change, never a diff, never only the paragraph you touched. ' +
93
- 'Read the message first with km_read_outreach_draft so you are rewriting what is really there. ' +
94
- 'It refuses, and says why, once a person has approved, scheduled, sent or cancelled the message: wording someone signed off on is the ' +
95
- 'wording that goes out. ' +
96
- 'A message carrying PHOTOS rewrites normally: the rich version is rebuilt around your new words and the photos stay exactly where they ' +
97
- 'are, so there is no need to finish the wording before adding images. ' +
98
- 'The one thing it will not rewrite is a draft built from a saved TEMPLATE, where the markup is hand built around this copy and the plain ' +
99
- 'text is only half of what the recipient sees; the subject can still be changed there. ' +
100
- 'Editing does NOT approve and does NOT send. The message stays in the queue and a person still approves it. ' +
101
- 'The wording you replaced is kept in the AI activity log so nothing is lost, but there is no one-click undo for an edit: ' +
102
- 'putting the old words back means editing again, so do not promise the user a revert button.',
103
- {
104
- id: z.string().describe('UUID of the draft (see km_list_outreach_drafts).'),
105
- body: z.string().optional().describe('The complete new message text, replacing what is there now.'),
106
- subject: z.string().optional().describe('The complete new subject line (email). Leave it out to keep the current one.'),
107
- },
108
- async ({ id, body, subject }) => {
109
- const payload = {};
110
- if (body !== undefined) payload.body = body;
111
- if (subject !== undefined) payload.subject = subject;
112
- return out(await call('POST', `/outreach-drafts/${encodeURIComponent(String(id))}/edit`, payload));
113
- },
114
- );
115
-
116
- server.tool(
117
- 'km_list_outreach_campaigns',
118
- 'List the cold outreach campaigns in this workspace: the name of each one, whether it is running, paused, finished, archived or still ' +
119
- 'being built, how many prospects are on it and when it last sent. Reach for it when the user asks what campaigns are live, which one is ' +
120
- 'paused, or as the step before pausing or resuming one so you are certain you have the right campaign and can name it back to them. ' +
121
- 'Read only.',
122
- {
123
- status: z.enum(CAMPAIGN_STATUS).optional().describe('Narrow to one status.'),
124
- limit: z.number().int().min(1).max(100).optional().describe('Default 25.'),
125
- },
126
- async ({ status, limit }) => out(await call('GET', `/outreach-campaigns${qs({ status, limit })}`)),
127
- );
128
-
129
- server.tool(
130
- 'km_outreach_campaign_stats',
131
- 'How one campaign is actually doing. Returns the campaign settings plus a headcount of its prospects by state (still pending, queued, ' +
132
- 'sent to, replied, stopped, completed, suppressed), how many people opened or clicked, how many bounced, and how many of its messages ' +
133
- 'are still sitting in the approval queue. Reach for it when the user asks how a campaign is going, whether it is worth continuing, or ' +
134
- 'why nothing seems to be going out - a big awaiting_approval number usually IS the answer to that last one. ' +
135
- 'The opened and clicked numbers count PEOPLE, not events: 40 opened means 40 different prospects opened at least once. Read only.',
136
- {
137
- campaign_id: z.string().describe('UUID of the campaign (see km_list_outreach_campaigns).'),
138
- },
139
- async ({ campaign_id }) => out(await call('GET', `/outreach-campaigns/${encodeURIComponent(String(campaign_id))}/stats`)),
140
- );
141
-
142
- server.tool(
143
- 'km_pause_outreach_campaign',
144
- 'Pause a running campaign. Nothing more goes out on it until someone resumes, and every prospect keeps their place in the sequence, so ' +
145
- 'this is fully reversible and nothing is lost. ' +
146
- 'Pausing a live campaign stops mail reaching real people, so CONFIRM FIRST: name the campaign back to the user and get a yes before ' +
147
- 'you call this. Use km_list_outreach_campaigns to get the id and the exact name. ' +
148
- 'It will not pause anything that is not currently running, and it will say so plainly rather than guess. It does not delete, archive ' +
149
- 'or unenroll anybody.',
150
- {
151
- campaign_id: z.string().describe('UUID of the campaign (see km_list_outreach_campaigns).'),
152
- confirm_token: z
153
- .string()
154
- .optional()
155
- .describe(
156
- 'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
157
- + 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
158
- + 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
159
- + 'arguments. A yes for one action never authorises a different one.',
160
- ),
161
- },
162
- async ({ campaign_id, confirm_token }) =>
163
- out(await call('POST', `/outreach-campaigns/${encodeURIComponent(String(campaign_id))}/pause`, { confirm_token })),
164
- );
165
-
166
- server.tool(
167
- 'km_resume_outreach_campaign',
168
- 'Un-pause a campaign that someone paused. It picks up exactly where it stopped. ' +
169
- 'Resuming starts messages flowing toward real people again, so CONFIRM FIRST: name the campaign back to the user and get a yes. ' +
170
- 'This only un-pauses a PAUSED campaign. It will not start a campaign for the first time and will not reopen a completed or archived ' +
171
- 'one; those are decisions a person makes in KM Hub. Every message on the campaign still waits for a human approval before it sends.',
172
- {
173
- campaign_id: z.string().describe('UUID of the campaign (see km_list_outreach_campaigns).'),
174
- confirm_token: z
175
- .string()
176
- .optional()
177
- .describe(
178
- 'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
179
- + 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
180
- + 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
181
- + 'arguments. A yes for one action never authorises a different one.',
182
- ),
183
- },
184
- async ({ campaign_id, confirm_token }) =>
185
- out(await call('POST', `/outreach-campaigns/${encodeURIComponent(String(campaign_id))}/resume`, { confirm_token })),
186
- );
187
-
188
- server.tool(
189
- 'km_list_outreach_replies',
190
- 'The replies that have come back from outreach, newest first, each with what it appears to be: interested, a question, an objection, not ' +
191
- 'interested, an out of office, the wrong person, or an unsubscribe. Reach for it when the user asks who replied, wants to work through ' +
192
- 'the responses, or asks whether anyone bit. Filter by classification to pull out the warm ones, or handled=false to see only what ' +
193
- 'nobody has dealt with yet. Long quoted email threads come back trimmed to a readable preview. ' +
194
- 'Read only: it cannot answer anybody. To reply, write the answer with km_create_outreach_draft and a person approves it in KM Hub.',
195
- {
196
- classification: z.enum(REPLY_CLASS).optional().describe('Narrow to one kind of reply. interested is the one worth surfacing first.'),
197
- handled: z.boolean().optional().describe('false shows only replies nobody has dealt with yet.'),
198
- contact_id: z.string().optional().describe('UUID of a contact (see km_find_contact).'),
199
- deal_id: z.string().optional().describe('UUID of a deal (see km_list_deals).'),
200
- from: z.string().optional().describe('Earliest received date, YYYY-MM-DD.'),
201
- to: z.string().optional().describe('Latest received date, YYYY-MM-DD.'),
202
- limit: z.number().int().min(1).max(100).optional().describe('Default 25.'),
203
- },
204
- async ({ classification, handled, contact_id, deal_id, from, to, limit }) =>
205
- out(await call('GET', `/outreach-replies${qs({ classification, handled, contact_id, deal_id, from, to, limit })}`)),
206
- );
207
-
208
- server.tool(
209
- 'km_outreach_email_log',
210
- 'What actually went out by email and what happened to it: who it went to, the subject, when it sent, whether it was delivered, when it ' +
211
- 'was first opened and how many times, whether any links were clicked, and whether it bounced. Reach for it when the user asks "did that ' +
212
- 'reach her", "has anyone opened it", "what have we sent this person" (pass contact_id) or wants the recent sending history. ' +
213
- 'Defaults to messages that have already sent; pass status=all to include the ones still waiting or cancelled. ' +
214
- 'It returns the envelope and the engagement, not the message text - use km_read_outreach_draft for the wording. Read only.',
215
- {
216
- contact_id: z.string().optional().describe('UUID of a contact (see km_find_contact), for one person history.'),
217
- campaign_id: z.string().optional().describe('UUID of a campaign (see km_list_outreach_campaigns).'),
218
- status: z.enum(DRAFT_STATUS).optional().describe('Default sent, which is the log of what really went out.'),
219
- from: z.string().optional().describe('Earliest date, YYYY-MM-DD.'),
220
- to: z.string().optional().describe('Latest date, YYYY-MM-DD.'),
221
- limit: z.number().int().min(1).max(100).optional().describe('Default 25.'),
222
- },
223
- async ({ contact_id, campaign_id, status, from, to, limit }) =>
224
- out(await call('GET', `/outreach-emails${qs({ contact_id, campaign_id, status, from, to, limit })}`)),
225
- );
226
-
227
- server.tool(
228
- 'km_outreach_email_stats',
229
- 'The day by day shape of email sending across the whole workspace: sent, delivered, opened, clicked, replied, bounced and complained, one ' +
230
- 'row per day plus the totals for the window. Reach for it when the user asks how sending has been going lately, whether volume is up or ' +
231
- 'down, or whether bounces have spiked, which is the early warning that a sending domain is getting into trouble. ' +
232
- 'For one specific message or one person, use km_outreach_email_log instead. Read only.',
233
- {
234
- days: z.number().int().min(1).max(90).optional().describe('How many days back. Default 30.'),
235
- },
236
- async ({ days }) => out(await call('GET', `/outreach-emails/daily${qs({ days })}`)),
237
- );
238
- }
1
+ /**
2
+ * Tool family: outreach - the approval queue, the campaigns, the replies and the
3
+ * email log.
4
+ *
5
+ * The safety line runs straight through this file and never moves: KM Hub writes
6
+ * every outgoing message as a DRAFT, and a person approves it in KM Hub or the
7
+ * Booth before anything leaves the building. So there is no approve tool here, no
8
+ * send tool, no schedule tool and no delete tool, and every description says so
9
+ * out loud, because the model will be asked to send and needs to know the answer
10
+ * before it starts looking for a way.
11
+ *
12
+ * What IS here is the work a person actually wants help with: reading the
13
+ * backlog, rewriting a message that is not right yet, seeing how a campaign is
14
+ * doing, stopping one that should stop, triaging replies, and checking whether
15
+ * anything landed.
16
+ *
17
+ * The family contract this file follows is documented in ./README.md.
18
+ */
19
+ import { z } from 'zod';
20
+
21
+ export const FAMILY = 'outreach';
22
+
23
+ export const TOOLS = [
24
+ 'km_list_outreach_drafts',
25
+ 'km_read_outreach_draft',
26
+ 'km_edit_outreach_draft',
27
+ 'km_list_outreach_campaigns',
28
+ 'km_outreach_campaign_stats',
29
+ 'km_pause_outreach_campaign',
30
+ 'km_resume_outreach_campaign',
31
+ 'km_list_outreach_replies',
32
+ 'km_outreach_email_log',
33
+ 'km_outreach_email_stats',
34
+ ];
35
+
36
+ // The cold outreach machine. tools.mjs already maps this family into the
37
+ // 'outreach' profile; declaring it here keeps the file true on its own.
38
+ export const PROFILES = ['outreach'];
39
+
40
+ const DRAFT_STATUS = ['draft', 'approved', 'scheduled', 'sent', 'cancelled', 'all'];
41
+ const CHANNEL = ['email', 'linkedin', 'ig', 'sms_whatsapp'];
42
+ const CAMPAIGN_STATUS = ['draft', 'running', 'paused', 'completed', 'archived'];
43
+ const REPLY_CLASS = [
44
+ 'interested', 'not_interested', 'objection', 'ooo',
45
+ 'wrong_person', 'unsubscribe', 'question', 'auto_reply', 'neutral',
46
+ ];
47
+
48
+ /**
49
+ * @param {{ tool: Function }} server guarded registrar (see ./README.md)
50
+ * @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
51
+ * @param {{ out: Function, qs: Function }} helpers shared response helpers from ../tools.mjs
52
+ */
53
+ export function register(server, call, { out, qs }) {
54
+ server.tool(
55
+ 'km_list_outreach_drafts',
56
+ 'Show the outreach messages waiting for approval: the emails and DMs that have been written for the user but have not gone anywhere yet. ' +
57
+ 'Reach for it when the question is specifically about THIS queue: how many messages are pending, what is in the queue, or when they want to work through it one message at a time. ' +
58
+ 'Do not answer the broad "what is waiting on me" from here. This is one channel of one kind of work, and km_briefing already carries the top of this queue ranked against the replies, tasks, invoices, enquiries and gigs that this tool cannot see, so a session that answers the whole day from this list reports a quarter of it as if it were all of it. ' +
59
+ 'Each one comes back with the opening lines, who it is for, which campaign it belongs to and whether anyone has already edited it; ' +
60
+ 'read one in full with km_read_outreach_draft. A backlog of a few hundred is normal, so this pages: the answer carries the exact ' +
61
+ 'total and tells you whether there is more, so never report a count from the rows you can see. ' +
62
+ 'This only reads. It cannot approve or send anything - a person does that in KM Hub.',
63
+ {
64
+ status: z.enum(DRAFT_STATUS).optional().describe('Default draft, which is the queue actually waiting on a human. Use all for the whole history.'),
65
+ channel: z.enum(CHANNEL).optional().describe('Narrow to one channel.'),
66
+ campaign_id: z.string().optional().describe('UUID of a campaign (see km_list_outreach_campaigns).'),
67
+ contact_id: z.string().optional().describe('UUID of a contact (see km_find_contact).'),
68
+ deal_id: z.string().optional().describe('UUID of a deal (see km_list_deals).'),
69
+ limit: z.number().int().min(1).max(100).optional().describe('Default 20.'),
70
+ offset: z.number().int().min(0).optional().describe('Skip this many before reading, for paging through a long backlog.'),
71
+ },
72
+ async ({ status, channel, campaign_id, contact_id, deal_id, limit, offset }) =>
73
+ out(await call('GET', `/outreach-drafts${qs({ status, channel, campaign_id, contact_id, deal_id, limit, offset })}`)),
74
+ );
75
+
76
+ server.tool(
77
+ 'km_read_outreach_draft',
78
+ 'Read one waiting outreach message in full: the whole body, the subject, who it is addressed to, which campaign and which step of the ' +
79
+ 'sequence it belongs to, and where it stands. Reach for it before you quote a message back to the user, judge it, or rewrite it, so you ' +
80
+ 'are working from the real wording rather than the preview. It also reports whether the message can still be rewritten from here, ' +
81
+ 'so you know before you try. Read only: it never approves and never sends.',
82
+ {
83
+ id: z.string().describe('UUID of the draft (see km_list_outreach_drafts).'),
84
+ },
85
+ async ({ id }) => out(await call('GET', `/outreach-drafts/${encodeURIComponent(String(id))}`)),
86
+ );
87
+
88
+ server.tool(
89
+ 'km_edit_outreach_draft',
90
+ 'Rewrite a message that is still waiting for approval. This is how "make it shorter", "lead with the fire" or "drop the exclamation marks" ' +
91
+ 'actually lands: the wording in the Approval Queue is replaced with yours, so the user opens KM Hub and reads the better version. ' +
92
+ 'Send the FULL replacement text, never a description of the change, never a diff, never only the paragraph you touched. ' +
93
+ 'Read the message first with km_read_outreach_draft so you are rewriting what is really there. ' +
94
+ 'It refuses, and says why, once a person has approved, scheduled, sent or cancelled the message: wording someone signed off on is the ' +
95
+ 'wording that goes out. ' +
96
+ 'A message carrying PHOTOS rewrites normally: the rich version is rebuilt around your new words and the photos stay exactly where they ' +
97
+ 'are, so there is no need to finish the wording before adding images. ' +
98
+ 'The one thing it will not rewrite is a draft built from a saved TEMPLATE, where the markup is hand built around this copy and the plain ' +
99
+ 'text is only half of what the recipient sees; the subject can still be changed there. ' +
100
+ 'Editing does NOT approve and does NOT send. The message stays in the queue and a person still approves it. ' +
101
+ 'The wording you replaced is kept in the AI activity log so nothing is lost, but there is no one-click undo for an edit: ' +
102
+ 'putting the old words back means editing again, so do not promise the user a revert button.',
103
+ {
104
+ id: z.string().describe('UUID of the draft (see km_list_outreach_drafts).'),
105
+ body: z.string().optional().describe('The complete new message text, replacing what is there now.'),
106
+ subject: z.string().optional().describe('The complete new subject line (email). Leave it out to keep the current one.'),
107
+ },
108
+ async ({ id, body, subject }) => {
109
+ const payload = {};
110
+ if (body !== undefined) payload.body = body;
111
+ if (subject !== undefined) payload.subject = subject;
112
+ return out(await call('POST', `/outreach-drafts/${encodeURIComponent(String(id))}/edit`, payload));
113
+ },
114
+ );
115
+
116
+ server.tool(
117
+ 'km_list_outreach_campaigns',
118
+ 'List the cold outreach campaigns in this workspace: the name of each one, whether it is running, paused, finished, archived or still ' +
119
+ 'being built, how many prospects are on it and when it last sent. Reach for it when the user asks what campaigns are live, which one is ' +
120
+ 'paused, or as the step before pausing or resuming one so you are certain you have the right campaign and can name it back to them. ' +
121
+ 'Read only.',
122
+ {
123
+ status: z.enum(CAMPAIGN_STATUS).optional().describe('Narrow to one status.'),
124
+ limit: z.number().int().min(1).max(100).optional().describe('Default 25.'),
125
+ },
126
+ async ({ status, limit }) => out(await call('GET', `/outreach-campaigns${qs({ status, limit })}`)),
127
+ );
128
+
129
+ server.tool(
130
+ 'km_outreach_campaign_stats',
131
+ 'How one campaign is actually doing. Returns the campaign settings plus a headcount of its prospects by state (still pending, queued, ' +
132
+ 'sent to, replied, stopped, completed, suppressed), how many people opened or clicked, how many bounced, and how many of its messages ' +
133
+ 'are still sitting in the approval queue. Reach for it when the user asks how a campaign is going, whether it is worth continuing, or ' +
134
+ 'why nothing seems to be going out - a big awaiting_approval number usually IS the answer to that last one. ' +
135
+ 'The opened and clicked numbers count PEOPLE, not events: 40 opened means 40 different prospects opened at least once. Read only.',
136
+ {
137
+ campaign_id: z.string().describe('UUID of the campaign (see km_list_outreach_campaigns).'),
138
+ },
139
+ async ({ campaign_id }) => out(await call('GET', `/outreach-campaigns/${encodeURIComponent(String(campaign_id))}/stats`)),
140
+ );
141
+
142
+ server.tool(
143
+ 'km_pause_outreach_campaign',
144
+ 'Pause a running campaign. Nothing more goes out on it until someone resumes, and every prospect keeps their place in the sequence, so ' +
145
+ 'this is fully reversible and nothing is lost. ' +
146
+ 'Pausing a live campaign stops mail reaching real people, so CONFIRM FIRST: name the campaign back to the user and get a yes before ' +
147
+ 'you call this. Use km_list_outreach_campaigns to get the id and the exact name. ' +
148
+ 'It will not pause anything that is not currently running, and it will say so plainly rather than guess. It does not delete, archive ' +
149
+ 'or unenroll anybody.',
150
+ {
151
+ campaign_id: z.string().describe('UUID of the campaign (see km_list_outreach_campaigns).'),
152
+ confirm_token: z
153
+ .string()
154
+ .optional()
155
+ .describe(
156
+ 'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
157
+ + 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
158
+ + 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
159
+ + 'arguments. A yes for one action never authorises a different one.',
160
+ ),
161
+ },
162
+ async ({ campaign_id, confirm_token }) =>
163
+ out(await call('POST', `/outreach-campaigns/${encodeURIComponent(String(campaign_id))}/pause`, { confirm_token })),
164
+ );
165
+
166
+ server.tool(
167
+ 'km_resume_outreach_campaign',
168
+ 'Un-pause a campaign that someone paused. It picks up exactly where it stopped. ' +
169
+ 'Resuming starts messages flowing toward real people again, so CONFIRM FIRST: name the campaign back to the user and get a yes. ' +
170
+ 'This only un-pauses a PAUSED campaign. It will not start a campaign for the first time and will not reopen a completed or archived ' +
171
+ 'one; those are decisions a person makes in KM Hub. Every message on the campaign still waits for a human approval before it sends.',
172
+ {
173
+ campaign_id: z.string().describe('UUID of the campaign (see km_list_outreach_campaigns).'),
174
+ confirm_token: z
175
+ .string()
176
+ .optional()
177
+ .describe(
178
+ 'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
179
+ + 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
180
+ + 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
181
+ + 'arguments. A yes for one action never authorises a different one.',
182
+ ),
183
+ },
184
+ async ({ campaign_id, confirm_token }) =>
185
+ out(await call('POST', `/outreach-campaigns/${encodeURIComponent(String(campaign_id))}/resume`, { confirm_token })),
186
+ );
187
+
188
+ server.tool(
189
+ 'km_list_outreach_replies',
190
+ 'The replies that have come back from outreach, newest first, each with what it appears to be: interested, a question, an objection, not ' +
191
+ 'interested, an out of office, the wrong person, or an unsubscribe. Reach for it when the user asks who replied, wants to work through ' +
192
+ 'the responses, or asks whether anyone bit. Filter by classification to pull out the warm ones, or handled=false to see only what ' +
193
+ 'nobody has dealt with yet. Long quoted email threads come back trimmed to a readable preview. ' +
194
+ 'Read only: it cannot answer anybody. To reply, write the answer with km_create_outreach_draft and a person approves it in KM Hub.',
195
+ {
196
+ classification: z.enum(REPLY_CLASS).optional().describe('Narrow to one kind of reply. interested is the one worth surfacing first.'),
197
+ handled: z.boolean().optional().describe('false shows only replies nobody has dealt with yet.'),
198
+ contact_id: z.string().optional().describe('UUID of a contact (see km_find_contact).'),
199
+ deal_id: z.string().optional().describe('UUID of a deal (see km_list_deals).'),
200
+ from: z.string().optional().describe('Earliest received date, YYYY-MM-DD.'),
201
+ to: z.string().optional().describe('Latest received date, YYYY-MM-DD.'),
202
+ limit: z.number().int().min(1).max(100).optional().describe('Default 25.'),
203
+ },
204
+ async ({ classification, handled, contact_id, deal_id, from, to, limit }) =>
205
+ out(await call('GET', `/outreach-replies${qs({ classification, handled, contact_id, deal_id, from, to, limit })}`)),
206
+ );
207
+
208
+ server.tool(
209
+ 'km_outreach_email_log',
210
+ 'What actually went out by email and what happened to it: who it went to, the subject, when it sent, whether it was delivered, when it ' +
211
+ 'was first opened and how many times, whether any links were clicked, and whether it bounced. Reach for it when the user asks "did that ' +
212
+ 'reach her", "has anyone opened it", "what have we sent this person" (pass contact_id) or wants the recent sending history. ' +
213
+ 'Defaults to messages that have already sent; pass status=all to include the ones still waiting or cancelled. ' +
214
+ 'It returns the envelope and the engagement, not the message text - use km_read_outreach_draft for the wording. Read only.',
215
+ {
216
+ contact_id: z.string().optional().describe('UUID of a contact (see km_find_contact), for one person history.'),
217
+ campaign_id: z.string().optional().describe('UUID of a campaign (see km_list_outreach_campaigns).'),
218
+ status: z.enum(DRAFT_STATUS).optional().describe('Default sent, which is the log of what really went out.'),
219
+ from: z.string().optional().describe('Earliest date, YYYY-MM-DD.'),
220
+ to: z.string().optional().describe('Latest date, YYYY-MM-DD.'),
221
+ limit: z.number().int().min(1).max(100).optional().describe('Default 25.'),
222
+ },
223
+ async ({ contact_id, campaign_id, status, from, to, limit }) =>
224
+ out(await call('GET', `/outreach-emails${qs({ contact_id, campaign_id, status, from, to, limit })}`)),
225
+ );
226
+
227
+ server.tool(
228
+ 'km_outreach_email_stats',
229
+ 'The day by day shape of email sending across the whole workspace: sent, delivered, opened, clicked, replied, bounced and complained, one ' +
230
+ 'row per day plus the totals for the window. Reach for it when the user asks how sending has been going lately, whether volume is up or ' +
231
+ 'down, or whether bounces have spiked, which is the early warning that a sending domain is getting into trouble. ' +
232
+ 'For one specific message or one person, use km_outreach_email_log instead. Read only.',
233
+ {
234
+ days: z.number().int().min(1).max(90).optional().describe('How many days back. Default 30.'),
235
+ },
236
+ async ({ days }) => out(await call('GET', `/outreach-emails/daily${qs({ days })}`)),
237
+ );
238
+ }