@kivimedia/kmhub 2.0.0 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/bin/kmhub.mjs +20 -7
- package/coach-book-output-guard.mjs +760 -0
- package/index.mjs +2 -0
- package/package.json +8 -3
- package/prompts/briefing.md +29 -0
- package/prompts/luxury.md +70 -0
- package/prompts/play.md +49 -0
- package/prompts/run.md +36 -0
- package/prompts/setup.md +33 -0
- package/prompts/vs-booked.md +46 -0
- package/prompts/what-can-you-do.md +40 -0
- package/prompts.mjs +110 -0
- package/read-only-tools.json +142 -0
- package/remote.mjs +815 -99
- package/tools/balloon-costing.mjs +80 -0
- package/tools/booking-equipment.mjs +110 -0
- package/tools/bridges.mjs +54 -0
- package/tools/calendar.mjs +9 -0
- package/tools/capabilities.mjs +155 -0
- package/tools/catalog.mjs +288 -0
- package/tools/clubs.mjs +176 -0
- package/tools/coach.mjs +771 -0
- package/tools/compare.mjs +76 -0
- package/tools/core.mjs +21 -0
- package/tools/crm.mjs +12 -3
- package/tools/dubsado.mjs +137 -0
- package/tools/exports.mjs +128 -0
- package/tools/fact-review.mjs +125 -0
- package/tools/flows.mjs +261 -0
- package/tools/forms.mjs +158 -0
- package/tools/gols.mjs +134 -0
- package/tools/hr.mjs +162 -0
- package/tools/knowledge.mjs +4 -3
- package/tools/marketing.mjs +396 -0
- package/tools/meta.mjs +2 -2
- package/tools/military.mjs +244 -0
- package/tools/outreach.mjs +27 -4
- package/tools/pending.mjs +122 -0
- package/tools/photos.mjs +140 -0
- package/tools/plays.mjs +1 -1
- package/tools/profile.mjs +118 -0
- package/tools/radar.mjs +173 -0
- package/tools/recurring-invoices.mjs +149 -0
- package/tools/reengage.mjs +434 -0
- package/tools/schedules.mjs +55 -0
- package/tools/setup.mjs +168 -0
- package/tools/sops-bridges.mjs +86 -0
- package/tools/sops.mjs +314 -0
- package/tools/sourcing.mjs +50 -2
- package/tools/strategy.mjs +146 -0
- package/tools/studio.mjs +132 -0
- package/tools/venueradar.mjs +151 -0
- package/tools/voice.mjs +134 -0
- package/tools.mjs +70 -12
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: military - the USO MWR Military Scout, read from the terminal.
|
|
3
|
+
*
|
|
4
|
+
* Every other radar in KM Hub is a scan that goes LOOKING for work, which is why radar.mjs
|
|
5
|
+
* reaches all nine of them through one pair of mission tools. This one is not that shape. The
|
|
6
|
+
* military market is ENUMERABLE rather than searchable: a finite published list of Army garrisons,
|
|
7
|
+
* USO centers, Navy MWR, Marine MCCS, Air Force FSS and Coast Guard desks, each on a predictable
|
|
8
|
+
* URL. So the thing a person wants from the terminal is not "what did the last scan find", it is
|
|
9
|
+
* "show me what is already in there", and that needs its own family rather than a sixth entry in
|
|
10
|
+
* a mission list.
|
|
11
|
+
*
|
|
12
|
+
* km_military_directory -> GET /military/directory
|
|
13
|
+
* km_military_desks -> GET /military/desks
|
|
14
|
+
* km_military_installation -> GET /military/installations/:ref
|
|
15
|
+
*
|
|
16
|
+
* 🚨 READ ONLY, and there are three separate reasons, all of which the descriptions say out loud:
|
|
17
|
+
*
|
|
18
|
+
* 1. NOTHING HERE SUBMITS, APPROVES OR SENDS. The engine worker registers the
|
|
19
|
+
* military_radar_submit job with no browser driver, so it resolves the read-only one and
|
|
20
|
+
* structurally cannot type or press anything. Approving a packet is deliberately a human act
|
|
21
|
+
* in the web app. A terminal tool that appeared to submit would be a lie told to a government
|
|
22
|
+
* office in a client's name, which is the worst outcome this feature has available.
|
|
23
|
+
* 2. NOTHING HERE STARTS A SWEEP. Queueing one is admin-only under RLS in the web app, and the
|
|
24
|
+
* API runs on the service role, which bypasses RLS: a start tool would quietly hand any
|
|
25
|
+
* read/write key a power the product refuses to a non-admin member. The sweep button lives in
|
|
26
|
+
* KM Hub where the person pressing it is checked. Whether a sweep is running is readable, and
|
|
27
|
+
* it is readable through the tool that already answers that for every radar:
|
|
28
|
+
* km_list_radar_scans with kind 'military_radar_ingest'.
|
|
29
|
+
* 3. Reading costs NOTHING and sends NOTHING. The directory is shared reference data with global
|
|
30
|
+
* read, already populated before anybody runs anything, so there is no first click to make
|
|
31
|
+
* and no bill for looking.
|
|
32
|
+
*
|
|
33
|
+
* The family contract this file follows is documented in ./README.md.
|
|
34
|
+
*/
|
|
35
|
+
import { z } from 'zod';
|
|
36
|
+
|
|
37
|
+
export const FAMILY = 'military';
|
|
38
|
+
|
|
39
|
+
export const TOOLS = [
|
|
40
|
+
'km_military_directory',
|
|
41
|
+
'km_military_desks',
|
|
42
|
+
'km_military_installation',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Finding and reaching buyers is outreach work, so this family joins that profile. It is not in
|
|
46
|
+
// `core`: a session that only wants orientation should not carry three more schemas.
|
|
47
|
+
export const PROFILES = ['outreach'];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Has this KM Hub simply never heard of the route, or did the route run and find nothing?
|
|
51
|
+
*
|
|
52
|
+
* 🚨 The neighbouring families treat every 404 as "not deployed here", which is safe for them
|
|
53
|
+
* because none of their routes can answer 404 themselves. This family has one that can: asking for
|
|
54
|
+
* an installation that is not in the directory is a real, ordinary 404. Collapsing the two would
|
|
55
|
+
* answer a plain typo with "your KM Hub does not have the military directory", which is a false
|
|
56
|
+
* statement about the product rather than a wrong answer about a base. So only the router's own
|
|
57
|
+
* self-describing 404, which carries the whole route list, counts as a missing route.
|
|
58
|
+
*/
|
|
59
|
+
function routeMissing(r) {
|
|
60
|
+
if (r.status === 405 || r.status === 501) return true;
|
|
61
|
+
if (r.status !== 404) return false;
|
|
62
|
+
const body = r && r.data && typeof r.data === 'object' ? r.data : null;
|
|
63
|
+
if (!body) return true;
|
|
64
|
+
if (Array.isArray(body.routes)) return true;
|
|
65
|
+
return typeof body.message === 'string' && body.message.startsWith('Unknown route');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const NOT_SUPPORTED =
|
|
69
|
+
'This KM Hub does not serve the military directory to the terminal yet. Nothing is broken: the workspace is fine ' +
|
|
70
|
+
'and every other tool works as normal. The directory is there in the web app at https://hub.kivimedia.co under ' +
|
|
71
|
+
'Outbound, named USO MWR Military Scout.';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The six buyer lanes, in one clause each.
|
|
75
|
+
*
|
|
76
|
+
* 🚨 This paragraph is here because a client on a live call asked "what is an NAF?" and nothing
|
|
77
|
+
* answered him. These labels are military shorthand, and a model that repeats them without
|
|
78
|
+
* expanding them is no more useful than the raw row. Every acronym any answer can print is
|
|
79
|
+
* expanded at least once here.
|
|
80
|
+
*/
|
|
81
|
+
const LANES =
|
|
82
|
+
'The six lanes and who is actually on the other end. `special_events`: the office that puts on ' +
|
|
83
|
+
'the base own concerts, festivals and family days, which is the entertainment budget itself. ' +
|
|
84
|
+
'`marketing`: the desk that advertises those events, which rarely books an act but walks your ' +
|
|
85
|
+
'name to the office that does. `sponsorship`: commercial sponsorship, where a company pays and ' +
|
|
86
|
+
'the base receives the act, so the money never comes out of the events budget. `naf_prime`: NAF ' +
|
|
87
|
+
'is non-appropriated funds, the money a base earns from its own clubs and shops rather than from ' +
|
|
88
|
+
'Congress, and a prime is a private company that already holds the contract to spend it and hires ' +
|
|
89
|
+
'acts underneath it. `uso`: a USO center is the lounge on or beside a base that troops walk into, ' +
|
|
90
|
+
'and each center programs its own floor. `afe`: Armed Forces Entertainment, the Department of ' +
|
|
91
|
+
'Defense office that sends entertainment to troops posted overseas, which publishes no route for ' +
|
|
92
|
+
'an act to contact it at all, so nobody reaches it directly.';
|
|
93
|
+
|
|
94
|
+
const NO_SEND =
|
|
95
|
+
'What it will NOT do: it cannot write to a desk, fill a form, approve a packet or send anything, ' +
|
|
96
|
+
'and no other tool here can either. Approving the words and submitting them is a person job in KM ' +
|
|
97
|
+
'Hub at https://hub.kivimedia.co, on purpose, because the other end is a government office. Never ' +
|
|
98
|
+
'tell somebody a message went out.';
|
|
99
|
+
|
|
100
|
+
const NO_INVENTION =
|
|
101
|
+
'Never construct an address. These offices publish shared desk mailboxes whose middle token ' +
|
|
102
|
+
'differs per garrison, so an address that is not in the answer does not exist to us, and a phone ' +
|
|
103
|
+
'or web form with no mailbox beside it is the NORMAL published state here rather than missing data.';
|
|
104
|
+
|
|
105
|
+
export function register(server, call, { out, text, qs }) {
|
|
106
|
+
server.tool(
|
|
107
|
+
'km_military_directory',
|
|
108
|
+
'The military entertainment buyers KM Hub has already enumerated: every Army garrison, USO center, Navy MWR, ' +
|
|
109
|
+
'Marine MCCS, Air Force FSS and Coast Guard desk that publishes a way for an act to reach it, plus the global ' +
|
|
110
|
+
'gateways. ' +
|
|
111
|
+
'Reach for it whenever somebody asks about military work, bases, garrisons, the USO, MWR, overseas troops or ' +
|
|
112
|
+
'who books entertainment on a base, and reach for it FIRST rather than describing the feature: this directory ' +
|
|
113
|
+
'is shared reference data that is already full before anybody runs anything, so there is nothing to start and ' +
|
|
114
|
+
'nothing to wait for. Reading it costs nothing, spends nothing and sends nothing. ' +
|
|
115
|
+
'🚨 Lead with the `directory` block, which is counted from the rows and not asserted: how many installations, ' +
|
|
116
|
+
'how many distinct desks, how many are overseas, and when it was last read. Say those numbers before any ' +
|
|
117
|
+
'individual row, because the single thing people get wrong about this surface is assuming it is empty. ' +
|
|
118
|
+
'Call it with no arguments to see the whole thing, then narrow. Every filter is optional and `filters_applied` ' +
|
|
119
|
+
'says back which ones are doing the narrowing, so an empty result is explained rather than shrugged at. ' +
|
|
120
|
+
LANES +
|
|
121
|
+
' ' +
|
|
122
|
+
NO_INVENTION +
|
|
123
|
+
' ' +
|
|
124
|
+
'Whether a sweep is running is a different question, answered by km_list_radar_scans with kind ' +
|
|
125
|
+
"'military_radar_ingest'. Starting one is done in KM Hub, never from here. " +
|
|
126
|
+
NO_SEND,
|
|
127
|
+
{
|
|
128
|
+
q: z
|
|
129
|
+
.string()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe(
|
|
132
|
+
'Free text, in the words the person used. Matches the name, the place, the region, the branch as the ' +
|
|
133
|
+
'badge words it, and the theater heading, in any order: "okinawa uso" and "uso okinawa" both work.',
|
|
134
|
+
),
|
|
135
|
+
branch: z
|
|
136
|
+
.enum(['army', 'navy', 'usmc', 'usaf', 'uscg', 'uso', 'afe', 'naf_prime'])
|
|
137
|
+
.optional()
|
|
138
|
+
.describe('One service or gateway. Leave it out to see all of them.'),
|
|
139
|
+
lane: z
|
|
140
|
+
.enum(['special_events', 'marketing', 'sponsorship', 'afe', 'naf_prime', 'uso'])
|
|
141
|
+
.optional()
|
|
142
|
+
.describe('Only installations that publish a desk on this buyer lane. See the lane guide in this description.'),
|
|
143
|
+
route: z
|
|
144
|
+
.enum(['email', 'phone', 'form'])
|
|
145
|
+
.optional()
|
|
146
|
+
.describe('Only installations reachable this way. Use it when somebody says they want places they can email.'),
|
|
147
|
+
region: z
|
|
148
|
+
.string()
|
|
149
|
+
.optional()
|
|
150
|
+
.describe('A region or theater as published or as grouped, for example "Europe", "Indo Pacific", "Japan", "Germany".'),
|
|
151
|
+
state: z.string().optional().describe('A US state or province. Most rows publish no state, so prefer region or q.'),
|
|
152
|
+
overseas: z
|
|
153
|
+
.boolean()
|
|
154
|
+
.optional()
|
|
155
|
+
.describe('True for installations outside the continental United States only, including Guam and Puerto Rico.'),
|
|
156
|
+
limit: z.number().int().min(1).max(200).optional().describe('How many installations to return. Default 40. Check `truncated`.'),
|
|
157
|
+
},
|
|
158
|
+
async ({ q, branch, lane, route, region, state, overseas, limit }) => {
|
|
159
|
+
const r = await call('GET', `/military/directory${qs({ q, branch, lane, route, region, state, overseas, limit })}`);
|
|
160
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
161
|
+
return out(r);
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
server.tool(
|
|
166
|
+
'km_military_desks',
|
|
167
|
+
'The desks themselves rather than the places: one row per mailbox, phone line or web form, however many ' +
|
|
168
|
+
'installations it serves. ' +
|
|
169
|
+
'This is the list to reach for when somebody asks who they would actually contact, or wants the special events ' +
|
|
170
|
+
'offices in Europe, or the mailboxes they could write to. ' +
|
|
171
|
+
'🚨 `serves_installations` is the number that matters and is the whole reason this tool exists separately from ' +
|
|
172
|
+
'km_military_directory: one regional USO organisation answers a SINGLE mailbox for every center under it, so a ' +
|
|
173
|
+
'list counted by location would have somebody send the same desk the same message eight times, which is the ' +
|
|
174
|
+
'fastest way to be ignored across an entire branch. One desk, one message. ' +
|
|
175
|
+
'Most of these buyers publish no named person, so `contact_name` is usually empty and that is the office ' +
|
|
176
|
+
'working normally, not a gap to fill in. `source_url` is the page each route was read off, and `evidence_quote` ' +
|
|
177
|
+
'is available on the installation view if somebody wants to see the wording it came from. ' +
|
|
178
|
+
NO_INVENTION +
|
|
179
|
+
' ' +
|
|
180
|
+
'Read only. Costs nothing, spends nothing. ' +
|
|
181
|
+
NO_SEND,
|
|
182
|
+
{
|
|
183
|
+
lane: z
|
|
184
|
+
.enum(['special_events', 'marketing', 'sponsorship', 'afe', 'naf_prime', 'uso'])
|
|
185
|
+
.optional()
|
|
186
|
+
.describe('The buyer lane. Different lanes spend different budgets and each needs its own message.'),
|
|
187
|
+
route: z
|
|
188
|
+
.enum(['email', 'phone', 'form'])
|
|
189
|
+
.optional()
|
|
190
|
+
.describe('Only desks reachable this way. "email" is the one to use when somebody wants a list they can write to.'),
|
|
191
|
+
branch: z
|
|
192
|
+
.enum(['army', 'navy', 'usmc', 'usaf', 'uscg', 'uso', 'afe', 'naf_prime'])
|
|
193
|
+
.optional()
|
|
194
|
+
.describe('One service or gateway.'),
|
|
195
|
+
region: z.string().optional().describe('A region or theater, for example "Europe", "Indo Pacific", "Germany".'),
|
|
196
|
+
q: z.string().optional().describe('Free text against the installation the desk belongs to, in the words the person used.'),
|
|
197
|
+
overseas: z.boolean().optional().describe('True for desks outside the continental United States only.'),
|
|
198
|
+
limit: z.number().int().min(1).max(200).optional().describe('How many desks to return. Default 40, busiest desks first. Check `truncated`.'),
|
|
199
|
+
},
|
|
200
|
+
async ({ lane, route, branch, region, q, overseas, limit }) => {
|
|
201
|
+
const r = await call('GET', `/military/desks${qs({ lane, route, branch, region, q, overseas, limit })}`);
|
|
202
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
203
|
+
return out(r);
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
server.tool(
|
|
208
|
+
'km_military_installation',
|
|
209
|
+
'One installation in full: every desk it publishes with its lane and its route, the page each one was read off, ' +
|
|
210
|
+
'the events on its own calendar, and whatever outreach this workspace has already recorded against it. ' +
|
|
211
|
+
'Use it after km_military_directory when somebody picks a base, or whenever they name one directly. It takes ' +
|
|
212
|
+
'either the id from a previous answer or the slug, for example "benning". ' +
|
|
213
|
+
'The events are the why-now: naming a garrison own event is the difference between a note that gets read and ' +
|
|
214
|
+
'one that gets filed. ' +
|
|
215
|
+
'🚨 `window_is_published` decides whether the pitch window may be spoken as a fact. True means the date came off ' +
|
|
216
|
+
'something the installation actually published. False means it was worked out from the usual season and this ' +
|
|
217
|
+
'year date is not out yet, so it is a guess and must never be repeated as though it were confirmed. ' +
|
|
218
|
+
'`your_outreach` is this workspace own audit trail and nothing more. A packet reading `approved` means a named ' +
|
|
219
|
+
'person signed off those exact words and NOTHING has been sent to the desk. `possibly_submitted` means a submit ' +
|
|
220
|
+
'button was pressed and the page could not be read back afterwards, so that garrison may or may not hold the ' +
|
|
221
|
+
'packet: only somebody who telephones the office can settle it, and it is never retried on its own. ' +
|
|
222
|
+
NO_INVENTION +
|
|
223
|
+
' ' +
|
|
224
|
+
'Read only. Costs nothing, spends nothing. ' +
|
|
225
|
+
NO_SEND,
|
|
226
|
+
{
|
|
227
|
+
installation: z
|
|
228
|
+
.string()
|
|
229
|
+
.describe('The installation id from a previous answer, or its slug, for example "benning" or "uso-okinawa".'),
|
|
230
|
+
},
|
|
231
|
+
async ({ installation }) => {
|
|
232
|
+
const ref = String(installation || '').trim();
|
|
233
|
+
if (!ref) {
|
|
234
|
+
return text(
|
|
235
|
+
'I need to know which installation. km_military_directory lists them, and each row carries the id and the ' +
|
|
236
|
+
'slug this tool takes.',
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const r = await call('GET', `/military/installations/${encodeURIComponent(ref)}`);
|
|
240
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
241
|
+
return out(r);
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
}
|
package/tools/outreach.mjs
CHANGED
|
@@ -92,8 +92,11 @@ export function register(server, call, { out, qs }) {
|
|
|
92
92
|
'Send the FULL replacement text, never a description of the change, never a diff, never only the paragraph you touched. ' +
|
|
93
93
|
'Read the message first with km_read_outreach_draft so you are rewriting what is really there. ' +
|
|
94
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
|
-
'
|
|
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. ' +
|
|
97
100
|
'Editing does NOT approve and does NOT send. The message stays in the queue and a person still approves it. ' +
|
|
98
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: ' +
|
|
99
102
|
'putting the old words back means editing again, so do not promise the user a revert button.',
|
|
@@ -146,8 +149,18 @@ export function register(server, call, { out, qs }) {
|
|
|
146
149
|
'or unenroll anybody.',
|
|
147
150
|
{
|
|
148
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
|
+
),
|
|
149
161
|
},
|
|
150
|
-
async ({ campaign_id }) =>
|
|
162
|
+
async ({ campaign_id, confirm_token }) =>
|
|
163
|
+
out(await call('POST', `/outreach-campaigns/${encodeURIComponent(String(campaign_id))}/pause`, { confirm_token })),
|
|
151
164
|
);
|
|
152
165
|
|
|
153
166
|
server.tool(
|
|
@@ -158,8 +171,18 @@ export function register(server, call, { out, qs }) {
|
|
|
158
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.',
|
|
159
172
|
{
|
|
160
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
|
+
),
|
|
161
183
|
},
|
|
162
|
-
async ({ campaign_id }) =>
|
|
184
|
+
async ({ campaign_id, confirm_token }) =>
|
|
185
|
+
out(await call('POST', `/outreach-campaigns/${encodeURIComponent(String(campaign_id))}/resume`, { confirm_token })),
|
|
163
186
|
);
|
|
164
187
|
|
|
165
188
|
server.tool(
|
|
@@ -0,0 +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
|
+
}
|
package/tools/photos.mjs
ADDED
|
@@ -0,0 +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
|
+
}
|
package/tools/plays.mjs
CHANGED
|
@@ -128,7 +128,7 @@ export function register(server, call, { out, text }) {
|
|
|
128
128
|
'km_play_catalog',
|
|
129
129
|
[
|
|
130
130
|
'The plays: the proven ways a business like this one actually gets booked, kept current by KM Hub and written by people who book work for a living rather than assembled from general marketing advice.',
|
|
131
|
-
'LOOK HERE FIRST whenever the ask is open ended. "What should I do today", "how do I get more corporate work", "nobody is replying to me", "help me with the fair market", "next month looks empty", "how do I get repeat bookings out of this client": there is very often a play for exactly that, and it will be sharper than anything improvised from what you already know about marketing.',
|
|
131
|
+
'LOOK HERE FIRST whenever the ask is open ended. "What should I do today", "how do I get more corporate work", "nobody is replying to me", "help me with the fair market", "next month looks empty", "how do I get repeat bookings out of this client": there is very often a play for exactly that, and it will be sharper than anything improvised from what you already know about marketing. This is also where brand and pricing work lives, so check here before answering that the workspace has no luxury, brand, positioning or pricing capability: "am I priced too low", "does my website undersell me", "should I run a discount", "should I add this new service", "what do I say on the call about price", "do you have the luxury tools". The luxury family is in this catalog, not in the tool list.',
|
|
132
132
|
'Free, fast, sends nothing and changes nothing. What comes back is a short list, and each entry is a slug to run it by, its name, and the plain reason a working performer would care about it.',
|
|
133
133
|
'What it deliberately does NOT contain is the method. The steps are not in here and cannot be worked out from the name, so never describe how a play works or promise what it will produce from the catalog alone. The method arrives one step at a time from km_play_run, and only while the subscription is live.',
|
|
134
134
|
'Reading the catalog is not doing the work, and reciting the whole list at somebody is not an answer. Name the one or two that actually fit what they said, in their words, and offer to run one.',
|