@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.
- package/README.md +170 -170
- package/bin/kmhub.mjs +896 -896
- package/coach-book-output-guard.mjs +760 -760
- package/index.mjs +57 -57
- package/package.json +56 -56
- package/prompts/briefing.md +29 -29
- package/prompts/luxury.md +70 -70
- package/prompts/play.md +49 -49
- package/prompts/run.md +37 -36
- package/prompts/setup.md +33 -33
- package/prompts/vs-booked.md +46 -46
- package/prompts/what-can-you-do.md +40 -40
- package/prompts.mjs +110 -110
- package/read-only-tools.json +143 -142
- package/remote.mjs +929 -929
- package/tools/balloon-costing.mjs +80 -80
- package/tools/booking-equipment.mjs +110 -110
- package/tools/bridges.mjs +54 -54
- package/tools/briefing.mjs +91 -91
- package/tools/calendar.mjs +170 -170
- package/tools/capabilities.mjs +155 -155
- package/tools/catalog.mjs +288 -288
- package/tools/clubs.mjs +176 -176
- package/tools/coach.mjs +771 -771
- package/tools/compare.mjs +76 -76
- package/tools/core.mjs +244 -244
- package/tools/crm.mjs +209 -209
- package/tools/dubsado.mjs +137 -137
- package/tools/exports.mjs +128 -128
- package/tools/fact-review.mjs +125 -125
- package/tools/flows.mjs +261 -261
- package/tools/forms.mjs +158 -158
- package/tools/gols.mjs +144 -134
- package/tools/hr.mjs +162 -162
- package/tools/knowledge.mjs +129 -125
- package/tools/marketing.mjs +396 -396
- package/tools/meta.mjs +245 -245
- package/tools/military.mjs +244 -244
- package/tools/money.mjs +235 -197
- package/tools/outreach.mjs +238 -238
- package/tools/pending.mjs +122 -122
- package/tools/photos.mjs +140 -140
- package/tools/plays.mjs +244 -244
- package/tools/profile.mjs +118 -118
- package/tools/radar.mjs +173 -173
- package/tools/recurring-invoices.mjs +149 -149
- package/tools/reengage.mjs +434 -434
- package/tools/schedules.mjs +55 -55
- package/tools/setup.mjs +168 -168
- package/tools/sops-bridges.mjs +86 -86
- package/tools/sops.mjs +314 -314
- package/tools/sourcing.mjs +268 -268
- package/tools/strategy.mjs +146 -146
- package/tools/studio.mjs +132 -132
- package/tools/venueradar.mjs +151 -151
- package/tools/voice.mjs +137 -134
- package/tools.mjs +407 -407
package/tools/military.mjs
CHANGED
|
@@ -1,244 +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
|
-
}
|
|
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
|
+
}
|