@kivimedia/kmhub 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -170
- package/bin/kmhub.mjs +896 -896
- package/coach-book-output-guard.mjs +760 -760
- package/index.mjs +57 -57
- package/package.json +56 -56
- package/prompts/briefing.md +29 -29
- package/prompts/luxury.md +70 -70
- package/prompts/play.md +49 -49
- package/prompts/run.md +36 -36
- package/prompts/setup.md +33 -33
- package/prompts/vs-booked.md +46 -46
- package/prompts/what-can-you-do.md +40 -40
- package/prompts.mjs +110 -109
- package/read-only-tools.json +143 -142
- package/remote.mjs +929 -929
- package/tools/balloon-costing.mjs +80 -80
- package/tools/booking-equipment.mjs +110 -110
- package/tools/bridges.mjs +54 -54
- package/tools/briefing.mjs +91 -91
- package/tools/calendar.mjs +170 -170
- package/tools/capabilities.mjs +155 -155
- package/tools/catalog.mjs +288 -288
- package/tools/clubs.mjs +176 -176
- package/tools/coach.mjs +771 -771
- package/tools/compare.mjs +76 -76
- package/tools/core.mjs +244 -244
- package/tools/crm.mjs +209 -209
- package/tools/dubsado.mjs +137 -137
- package/tools/exports.mjs +128 -128
- package/tools/fact-review.mjs +125 -125
- package/tools/flows.mjs +261 -261
- package/tools/forms.mjs +158 -158
- package/tools/gols.mjs +134 -134
- package/tools/hr.mjs +162 -162
- package/tools/knowledge.mjs +125 -125
- package/tools/marketing.mjs +396 -396
- package/tools/meta.mjs +245 -245
- package/tools/military.mjs +244 -244
- package/tools/money.mjs +235 -197
- package/tools/outreach.mjs +238 -238
- package/tools/pending.mjs +122 -122
- package/tools/photos.mjs +140 -140
- package/tools/plays.mjs +244 -244
- package/tools/profile.mjs +118 -118
- package/tools/radar.mjs +173 -173
- package/tools/recurring-invoices.mjs +149 -149
- package/tools/reengage.mjs +434 -434
- package/tools/schedules.mjs +55 -55
- package/tools/setup.mjs +168 -168
- package/tools/sops-bridges.mjs +86 -86
- package/tools/sops.mjs +314 -314
- package/tools/sourcing.mjs +268 -268
- package/tools/strategy.mjs +146 -146
- package/tools/studio.mjs +132 -132
- package/tools/venueradar.mjs +151 -151
- package/tools/voice.mjs +134 -134
- package/tools.mjs +407 -407
package/tools/plays.mjs
CHANGED
|
@@ -1,244 +1,244 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool family: plays - the operating intelligence, leased one step at a time.
|
|
3
|
-
*
|
|
4
|
-
* Every other family is a door onto the workspace data. This one is the reason a
|
|
5
|
-
* client would rather have KM Hub in their terminal than a generic assistant with
|
|
6
|
-
* a CRM connector: the plays are the proven ways this kind of business actually
|
|
7
|
-
* gets booked, and they are worth more than any single record they touch.
|
|
8
|
-
*
|
|
9
|
-
* Which is why none of them live on the client's machine. The protection model,
|
|
10
|
-
* stated plainly so nobody has to reverse engineer it from the code:
|
|
11
|
-
*
|
|
12
|
-
* - The client's disk holds three things: a URL, an API key, and a rules file
|
|
13
|
-
* of pure orientation. No play, no runbook, no criterion, ever.
|
|
14
|
-
* - km_play_catalog returns METADATA ONLY: slug, label, and the plain reason a
|
|
15
|
-
* human would care. Never the task, never the criterion, never an internal.
|
|
16
|
-
* - km_play_run leases ONE bounded step per call. The text arrives in the
|
|
17
|
-
* conversation, is used, and is never written to a file by us.
|
|
18
|
-
* - Quotas are what stop somebody enumerating the catalog by running everything
|
|
19
|
-
* once: a cap per hour, a cap per day, and a cap on how many DIFFERENT plays
|
|
20
|
-
* in a rolling day. Repeating a play already seen in that window is free of
|
|
21
|
-
* the variety cap, so normal work never hits it and harvesting does.
|
|
22
|
-
* - Each issued step carries an HMAC watermark. Be honest about what that is:
|
|
23
|
-
* it is forensic attribution, NOT DRM. The task is plaintext in the client's
|
|
24
|
-
* own transcript and a determined person can strip the marker. It tells us
|
|
25
|
-
* WHERE a leaked task came from. It does not stop the leak.
|
|
26
|
-
* - The real kill switch is Phase 1 and it already works: cancel the
|
|
27
|
-
* subscription and the entitlement gate answers 402 on every route here, so
|
|
28
|
-
* no catalog, no runs, no rules refresh, from the next call onwards.
|
|
29
|
-
*
|
|
30
|
-
* Nothing in this family sends anything to anybody. A play that produces a
|
|
31
|
-
* message still ends at km_create_outreach_draft and the Approval Queue.
|
|
32
|
-
*
|
|
33
|
-
* The routes behind it are GET /plays, POST /plays/run and POST /plays/verify.
|
|
34
|
-
* They own the truth about the body shape, so the two POST handlers pass their
|
|
35
|
-
* arguments straight through rather than reshaping them here.
|
|
36
|
-
*
|
|
37
|
-
* The family contract this file follows is documented in ./README.md.
|
|
38
|
-
*/
|
|
39
|
-
import { z } from 'zod';
|
|
40
|
-
|
|
41
|
-
export const FAMILY = 'plays';
|
|
42
|
-
|
|
43
|
-
export const TOOLS = ['km_play_catalog', 'km_play_run', 'km_play_verify'];
|
|
44
|
-
|
|
45
|
-
// Every profile. A session that can read the calendar but cannot reach the plays
|
|
46
|
-
// is a crippled session: the plays are what make the rest of the tools add up to
|
|
47
|
-
// a way of working rather than a pile of endpoints.
|
|
48
|
-
export const PROFILES = ['*'];
|
|
49
|
-
|
|
50
|
-
/** The API has not shipped these routes yet (404 / 405 / 501 all mean the same here). */
|
|
51
|
-
function routeMissing(r) {
|
|
52
|
-
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const NOT_SUPPORTED =
|
|
56
|
-
'This KM Hub does not have the plays switched on yet. Nothing is broken and nothing is lost: the workspace is fine and every other tool works as normal. Carry on with the tools you have, and say plainly that the plays are not available on this account rather than inventing a method and calling it one.';
|
|
57
|
-
|
|
58
|
-
/** Round a retry delay into something a person says out loud. Empty when unknown. */
|
|
59
|
-
function inAbout(seconds) {
|
|
60
|
-
const s = Number(seconds);
|
|
61
|
-
if (!Number.isFinite(s) || s <= 0) return '';
|
|
62
|
-
if (s < 90) return `about ${Math.max(1, Math.round(s))} seconds`;
|
|
63
|
-
const minutes = Math.round(s / 60);
|
|
64
|
-
if (minutes < 90) return `about ${minutes} minute${minutes === 1 ? '' : 's'}`;
|
|
65
|
-
const hours = Math.round(s / 3600);
|
|
66
|
-
return `about ${hours} hour${hours === 1 ? '' : 's'}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Turn a 429 into a sentence a working performer understands.
|
|
71
|
-
*
|
|
72
|
-
* The shared out() helper already does this for a 402, and a refusal is the one
|
|
73
|
-
* moment where a raw status code is least use to anybody. Two different things
|
|
74
|
-
* answer 429 on this API and they need different words:
|
|
75
|
-
*
|
|
76
|
-
* - `rate_limited` from the API front door: too many calls per minute. A pause
|
|
77
|
-
* fixes it.
|
|
78
|
-
* - anything else: the play allowance itself. Not a fault, not a charge, and
|
|
79
|
-
* not something a retry loop will get around.
|
|
80
|
-
*
|
|
81
|
-
* Returns null when the response is not a 429, so callers can carry on normally.
|
|
82
|
-
*/
|
|
83
|
-
function quotaRefusal(r) {
|
|
84
|
-
if (!r || r.status !== 429) return null;
|
|
85
|
-
const body = r.data && typeof r.data === 'object' ? r.data : {};
|
|
86
|
-
const code = String(body.error || body.reason || body.code || '').toLowerCase();
|
|
87
|
-
const said = typeof body.message === 'string' && body.message.trim() ? body.message.trim() : '';
|
|
88
|
-
const wait = inAbout(body.retry_after ?? body.retryAfter ?? body.retry_after_seconds);
|
|
89
|
-
const lines = [];
|
|
90
|
-
|
|
91
|
-
if (code.includes('rate_limit')) {
|
|
92
|
-
lines.push('I am calling KM Hub faster than it allows, so it asked me to slow down. Nothing failed and nothing changed.');
|
|
93
|
-
lines.push(wait ? `It clears in ${wait}.` : 'It clears on its own in well under a minute.');
|
|
94
|
-
if (said) lines.push(said);
|
|
95
|
-
lines.push('Wait it out and try the same call once more. Do not split the work into more calls, which makes it worse.');
|
|
96
|
-
return { content: [{ type: 'text', text: lines.join('\n\n') }], isError: true };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const variety = code.includes('distinct') || code.includes('variety') || code.includes('skill_limit');
|
|
100
|
-
if (variety) {
|
|
101
|
-
lines.push(
|
|
102
|
-
'That is a play this workspace has not opened today, and it has already opened as many different ones as a day allows. Nothing is wrong, nothing was charged, and nothing in the workspace has changed.',
|
|
103
|
-
);
|
|
104
|
-
lines.push(
|
|
105
|
-
'A play already run today can still be run again as much as it is needed, so if one of those covers what is being asked, use it instead. Otherwise this one is worth doing tomorrow.',
|
|
106
|
-
);
|
|
107
|
-
} else {
|
|
108
|
-
lines.push(
|
|
109
|
-
'This workspace has run as many plays as its allowance covers for now. Nothing is wrong, nothing was charged, and nothing in the workspace has changed.',
|
|
110
|
-
);
|
|
111
|
-
lines.push('The allowance is there so the plays get used on real work rather than pulled through in bulk.');
|
|
112
|
-
}
|
|
113
|
-
if (said) lines.push(said);
|
|
114
|
-
lines.push(wait ? `It frees up again in ${wait}.` : 'It frees up again on its own.');
|
|
115
|
-
lines.push(
|
|
116
|
-
'Tell the person that plainly and get on with the work by hand: the clients, the calendar, the drafts and everything else are untouched and still work. Do not retry this in a loop.',
|
|
117
|
-
);
|
|
118
|
-
return { content: [{ type: 'text', text: lines.join('\n\n') }], isError: true };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* @param {{ tool: Function }} server guarded registrar (see ./README.md)
|
|
123
|
-
* @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
|
|
124
|
-
* @param {{ out: Function, text: Function, qs: Function }} helpers
|
|
125
|
-
*/
|
|
126
|
-
export function register(server, call, { out, text }) {
|
|
127
|
-
server.tool(
|
|
128
|
-
'km_play_catalog',
|
|
129
|
-
[
|
|
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. 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
|
-
'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
|
-
'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
|
-
'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.',
|
|
135
|
-
].join(' '),
|
|
136
|
-
{},
|
|
137
|
-
async () => {
|
|
138
|
-
const r = await call('GET', '/plays');
|
|
139
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
140
|
-
const refused = quotaRefusal(r);
|
|
141
|
-
if (refused) return refused;
|
|
142
|
-
return out(r);
|
|
143
|
-
},
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
server.tool(
|
|
147
|
-
'km_play_run',
|
|
148
|
-
[
|
|
149
|
-
'Run a play: start one, or come back for its next step.',
|
|
150
|
-
'This is a lease, one step at a time. KM Hub hands back a SINGLE bounded piece of work, aimed at this workspace and this situation, that you can actually finish with the km_* tools already in front of you. Do that step. Tell the person what happened in your own words. Then call this again with the same run_id to get the next one. Keep going until the play says it is finished, then call km_play_verify.',
|
|
151
|
-
'The step text is working material on loan for this run, not a document to hand over. Do not paste it back to the person verbatim, do not save it into a file, a note, a scratch document or a memory, and do not quote it at length in your reply. They asked for the outcome, not the recipe, and the recipe is the part KM Hub is paid for. Each issued step carries a marker identifying the workspace it was leased to.',
|
|
152
|
-
'Run the play that fits what was actually asked. There is an allowance on how many plays get pulled in a day and on how many DIFFERENT ones, so do not open plays speculatively, do not work down the catalog to see what each one says, and do not start a second play while the first is unfinished. A play already opened today can be run again as often as it is useful.',
|
|
153
|
-
'It sends nothing to anybody. A play that produces a message still ends at km_create_outreach_draft, in the Approval Queue, for a human to read and release. If a step asks for something you do not have, say so and ask, rather than filling the gap with a plausible guess.',
|
|
154
|
-
].join(' '),
|
|
155
|
-
{
|
|
156
|
-
slug: z
|
|
157
|
-
.string()
|
|
158
|
-
.describe('Which play to run, exactly as km_play_catalog gave it, for example quiet-week-rescue. Never invent a slug or guess one from a name.'),
|
|
159
|
-
run_id: z
|
|
160
|
-
.string()
|
|
161
|
-
.optional()
|
|
162
|
-
.describe('Leave this out for the first step. To continue a play you already started, pass back the run id from the previous step and you get the next one.'),
|
|
163
|
-
context: z
|
|
164
|
-
.string()
|
|
165
|
-
.optional()
|
|
166
|
-
.describe('What the person actually asked for, in their own words, plus anything about right now that the step should take into account. It lets KM Hub aim the step at the real situation instead of the general case.'),
|
|
167
|
-
step_result: z
|
|
168
|
-
.string()
|
|
169
|
-
.optional()
|
|
170
|
-
.describe('Only when continuing a run: what happened on the step you just finished, in a sentence or two, including anything that did not work. The next step is built on it, so an honest account beats a tidy one.'),
|
|
171
|
-
},
|
|
172
|
-
async (args) => {
|
|
173
|
-
// Slugs are lowercase kebab everywhere in this codebase, and a model that
|
|
174
|
-
// has read the catalog once and is typing from memory will occasionally
|
|
175
|
-
// capitalise one. Fixing that here costs nothing and saves a 404.
|
|
176
|
-
const slug = String(args?.slug || '').trim().toLowerCase();
|
|
177
|
-
if (!slug) {
|
|
178
|
-
return text(
|
|
179
|
-
'I need to know which play to run. km_play_catalog lists the ones this workspace has, with the slug for each.',
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
// The route requires an idempotency_key (8-128 of [A-Za-z0-9._:-]) so a
|
|
183
|
-
// retry cannot double-spend a quota slot. It is minted here, never asked
|
|
184
|
-
// of the model: a caller-chosen key is a footgun, and a model has no
|
|
185
|
-
// basis to invent a stable one. Continuing a run reuses a key derived
|
|
186
|
-
// from the run id so a retried continuation replays instead of counting
|
|
187
|
-
// twice; a fresh run gets a random one.
|
|
188
|
-
const runId = String(args?.run_id || '').trim();
|
|
189
|
-
const idempotency_key = runId
|
|
190
|
-
? `run-${runId.replace(/[^A-Za-z0-9._:-]/g, '')}`.slice(0, 128)
|
|
191
|
-
: crypto.randomUUID().replace(/-/g, '');
|
|
192
|
-
const r = await call('POST', '/plays/run', { ...args, slug, idempotency_key });
|
|
193
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
194
|
-
const refused = quotaRefusal(r);
|
|
195
|
-
if (refused) return refused;
|
|
196
|
-
return out(r);
|
|
197
|
-
},
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
server.tool(
|
|
201
|
-
'km_play_verify',
|
|
202
|
-
[
|
|
203
|
-
'Check whether the play actually worked. Every play has a criterion: the observable thing that should be true now if it did its job. That is a different question from whether you got through the steps, and it is the one that matters.',
|
|
204
|
-
'Call it when a play reaches its end, or whenever someone asks whether the thing you ran last week did anything. KM Hub gives you the criterion, you check it against what you can genuinely see through the km_* tools, and you report what you found.',
|
|
205
|
-
'Being honest here is the whole point of the tool. "I ran it, and I cannot confirm yet that it worked" is a real answer, a useful one, and far better than a confident yes that falls apart a week later. Say which part you could confirm, which part you could not, and what would settle it.',
|
|
206
|
-
'Do not call a play successful because the steps are done. Do not verify a play you did not run. This changes nothing in the workspace, sends nothing to anybody, and cannot make a play have worked.',
|
|
207
|
-
].join(' '),
|
|
208
|
-
{
|
|
209
|
-
run_id: z
|
|
210
|
-
.string()
|
|
211
|
-
.optional()
|
|
212
|
-
.describe('The run being checked, as returned by km_play_run. Give this whenever you have it: it is what ties the criterion to the work that was actually done.'),
|
|
213
|
-
slug: z
|
|
214
|
-
.string()
|
|
215
|
-
.optional()
|
|
216
|
-
.describe('The play being checked, as km_play_catalog gave it. Pass it alongside run_id for context. On its own it is not enough: the check is tied to a specific run, so without the run id from km_play_run there is nothing to judge against.'),
|
|
217
|
-
outcome: z
|
|
218
|
-
.string()
|
|
219
|
-
.optional()
|
|
220
|
-
.describe('What you did and what you can see now, in plain words, so the criterion is judged against reality rather than against intent. Include what did not happen as well as what did.'),
|
|
221
|
-
},
|
|
222
|
-
async (args) => {
|
|
223
|
-
const runId = String(args?.run_id || '').trim();
|
|
224
|
-
const slug = String(args?.slug || '').trim().toLowerCase();
|
|
225
|
-
if (!runId) {
|
|
226
|
-
return text(
|
|
227
|
-
'I need the run id that km_play_run gave back, because the check is tied to that specific run. ' +
|
|
228
|
-
'If it is genuinely lost, run the play again rather than guessing whether the old one worked.',
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
// The route reads `evidence`; this tool calls it `outcome` because that
|
|
232
|
-
// is the friendlier word for a model. Map it, or every call 400s.
|
|
233
|
-
const r = await call('POST', '/plays/verify', {
|
|
234
|
-
run_id: runId || undefined,
|
|
235
|
-
slug: slug || undefined,
|
|
236
|
-
evidence: String(args?.outcome || '').trim() || undefined,
|
|
237
|
-
});
|
|
238
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
239
|
-
const refused = quotaRefusal(r);
|
|
240
|
-
if (refused) return refused;
|
|
241
|
-
return out(r);
|
|
242
|
-
},
|
|
243
|
-
);
|
|
244
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: plays - the operating intelligence, leased one step at a time.
|
|
3
|
+
*
|
|
4
|
+
* Every other family is a door onto the workspace data. This one is the reason a
|
|
5
|
+
* client would rather have KM Hub in their terminal than a generic assistant with
|
|
6
|
+
* a CRM connector: the plays are the proven ways this kind of business actually
|
|
7
|
+
* gets booked, and they are worth more than any single record they touch.
|
|
8
|
+
*
|
|
9
|
+
* Which is why none of them live on the client's machine. The protection model,
|
|
10
|
+
* stated plainly so nobody has to reverse engineer it from the code:
|
|
11
|
+
*
|
|
12
|
+
* - The client's disk holds three things: a URL, an API key, and a rules file
|
|
13
|
+
* of pure orientation. No play, no runbook, no criterion, ever.
|
|
14
|
+
* - km_play_catalog returns METADATA ONLY: slug, label, and the plain reason a
|
|
15
|
+
* human would care. Never the task, never the criterion, never an internal.
|
|
16
|
+
* - km_play_run leases ONE bounded step per call. The text arrives in the
|
|
17
|
+
* conversation, is used, and is never written to a file by us.
|
|
18
|
+
* - Quotas are what stop somebody enumerating the catalog by running everything
|
|
19
|
+
* once: a cap per hour, a cap per day, and a cap on how many DIFFERENT plays
|
|
20
|
+
* in a rolling day. Repeating a play already seen in that window is free of
|
|
21
|
+
* the variety cap, so normal work never hits it and harvesting does.
|
|
22
|
+
* - Each issued step carries an HMAC watermark. Be honest about what that is:
|
|
23
|
+
* it is forensic attribution, NOT DRM. The task is plaintext in the client's
|
|
24
|
+
* own transcript and a determined person can strip the marker. It tells us
|
|
25
|
+
* WHERE a leaked task came from. It does not stop the leak.
|
|
26
|
+
* - The real kill switch is Phase 1 and it already works: cancel the
|
|
27
|
+
* subscription and the entitlement gate answers 402 on every route here, so
|
|
28
|
+
* no catalog, no runs, no rules refresh, from the next call onwards.
|
|
29
|
+
*
|
|
30
|
+
* Nothing in this family sends anything to anybody. A play that produces a
|
|
31
|
+
* message still ends at km_create_outreach_draft and the Approval Queue.
|
|
32
|
+
*
|
|
33
|
+
* The routes behind it are GET /plays, POST /plays/run and POST /plays/verify.
|
|
34
|
+
* They own the truth about the body shape, so the two POST handlers pass their
|
|
35
|
+
* arguments straight through rather than reshaping them here.
|
|
36
|
+
*
|
|
37
|
+
* The family contract this file follows is documented in ./README.md.
|
|
38
|
+
*/
|
|
39
|
+
import { z } from 'zod';
|
|
40
|
+
|
|
41
|
+
export const FAMILY = 'plays';
|
|
42
|
+
|
|
43
|
+
export const TOOLS = ['km_play_catalog', 'km_play_run', 'km_play_verify'];
|
|
44
|
+
|
|
45
|
+
// Every profile. A session that can read the calendar but cannot reach the plays
|
|
46
|
+
// is a crippled session: the plays are what make the rest of the tools add up to
|
|
47
|
+
// a way of working rather than a pile of endpoints.
|
|
48
|
+
export const PROFILES = ['*'];
|
|
49
|
+
|
|
50
|
+
/** The API has not shipped these routes yet (404 / 405 / 501 all mean the same here). */
|
|
51
|
+
function routeMissing(r) {
|
|
52
|
+
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const NOT_SUPPORTED =
|
|
56
|
+
'This KM Hub does not have the plays switched on yet. Nothing is broken and nothing is lost: the workspace is fine and every other tool works as normal. Carry on with the tools you have, and say plainly that the plays are not available on this account rather than inventing a method and calling it one.';
|
|
57
|
+
|
|
58
|
+
/** Round a retry delay into something a person says out loud. Empty when unknown. */
|
|
59
|
+
function inAbout(seconds) {
|
|
60
|
+
const s = Number(seconds);
|
|
61
|
+
if (!Number.isFinite(s) || s <= 0) return '';
|
|
62
|
+
if (s < 90) return `about ${Math.max(1, Math.round(s))} seconds`;
|
|
63
|
+
const minutes = Math.round(s / 60);
|
|
64
|
+
if (minutes < 90) return `about ${minutes} minute${minutes === 1 ? '' : 's'}`;
|
|
65
|
+
const hours = Math.round(s / 3600);
|
|
66
|
+
return `about ${hours} hour${hours === 1 ? '' : 's'}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Turn a 429 into a sentence a working performer understands.
|
|
71
|
+
*
|
|
72
|
+
* The shared out() helper already does this for a 402, and a refusal is the one
|
|
73
|
+
* moment where a raw status code is least use to anybody. Two different things
|
|
74
|
+
* answer 429 on this API and they need different words:
|
|
75
|
+
*
|
|
76
|
+
* - `rate_limited` from the API front door: too many calls per minute. A pause
|
|
77
|
+
* fixes it.
|
|
78
|
+
* - anything else: the play allowance itself. Not a fault, not a charge, and
|
|
79
|
+
* not something a retry loop will get around.
|
|
80
|
+
*
|
|
81
|
+
* Returns null when the response is not a 429, so callers can carry on normally.
|
|
82
|
+
*/
|
|
83
|
+
function quotaRefusal(r) {
|
|
84
|
+
if (!r || r.status !== 429) return null;
|
|
85
|
+
const body = r.data && typeof r.data === 'object' ? r.data : {};
|
|
86
|
+
const code = String(body.error || body.reason || body.code || '').toLowerCase();
|
|
87
|
+
const said = typeof body.message === 'string' && body.message.trim() ? body.message.trim() : '';
|
|
88
|
+
const wait = inAbout(body.retry_after ?? body.retryAfter ?? body.retry_after_seconds);
|
|
89
|
+
const lines = [];
|
|
90
|
+
|
|
91
|
+
if (code.includes('rate_limit')) {
|
|
92
|
+
lines.push('I am calling KM Hub faster than it allows, so it asked me to slow down. Nothing failed and nothing changed.');
|
|
93
|
+
lines.push(wait ? `It clears in ${wait}.` : 'It clears on its own in well under a minute.');
|
|
94
|
+
if (said) lines.push(said);
|
|
95
|
+
lines.push('Wait it out and try the same call once more. Do not split the work into more calls, which makes it worse.');
|
|
96
|
+
return { content: [{ type: 'text', text: lines.join('\n\n') }], isError: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const variety = code.includes('distinct') || code.includes('variety') || code.includes('skill_limit');
|
|
100
|
+
if (variety) {
|
|
101
|
+
lines.push(
|
|
102
|
+
'That is a play this workspace has not opened today, and it has already opened as many different ones as a day allows. Nothing is wrong, nothing was charged, and nothing in the workspace has changed.',
|
|
103
|
+
);
|
|
104
|
+
lines.push(
|
|
105
|
+
'A play already run today can still be run again as much as it is needed, so if one of those covers what is being asked, use it instead. Otherwise this one is worth doing tomorrow.',
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
lines.push(
|
|
109
|
+
'This workspace has run as many plays as its allowance covers for now. Nothing is wrong, nothing was charged, and nothing in the workspace has changed.',
|
|
110
|
+
);
|
|
111
|
+
lines.push('The allowance is there so the plays get used on real work rather than pulled through in bulk.');
|
|
112
|
+
}
|
|
113
|
+
if (said) lines.push(said);
|
|
114
|
+
lines.push(wait ? `It frees up again in ${wait}.` : 'It frees up again on its own.');
|
|
115
|
+
lines.push(
|
|
116
|
+
'Tell the person that plainly and get on with the work by hand: the clients, the calendar, the drafts and everything else are untouched and still work. Do not retry this in a loop.',
|
|
117
|
+
);
|
|
118
|
+
return { content: [{ type: 'text', text: lines.join('\n\n') }], isError: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {{ tool: Function }} server guarded registrar (see ./README.md)
|
|
123
|
+
* @param {(method: string, path: string, body?: any) => Promise<{ok:boolean,status:number,data:any}>} call
|
|
124
|
+
* @param {{ out: Function, text: Function, qs: Function }} helpers
|
|
125
|
+
*/
|
|
126
|
+
export function register(server, call, { out, text }) {
|
|
127
|
+
server.tool(
|
|
128
|
+
'km_play_catalog',
|
|
129
|
+
[
|
|
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. 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
|
+
'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
|
+
'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
|
+
'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.',
|
|
135
|
+
].join(' '),
|
|
136
|
+
{},
|
|
137
|
+
async () => {
|
|
138
|
+
const r = await call('GET', '/plays');
|
|
139
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
140
|
+
const refused = quotaRefusal(r);
|
|
141
|
+
if (refused) return refused;
|
|
142
|
+
return out(r);
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
server.tool(
|
|
147
|
+
'km_play_run',
|
|
148
|
+
[
|
|
149
|
+
'Run a play: start one, or come back for its next step.',
|
|
150
|
+
'This is a lease, one step at a time. KM Hub hands back a SINGLE bounded piece of work, aimed at this workspace and this situation, that you can actually finish with the km_* tools already in front of you. Do that step. Tell the person what happened in your own words. Then call this again with the same run_id to get the next one. Keep going until the play says it is finished, then call km_play_verify.',
|
|
151
|
+
'The step text is working material on loan for this run, not a document to hand over. Do not paste it back to the person verbatim, do not save it into a file, a note, a scratch document or a memory, and do not quote it at length in your reply. They asked for the outcome, not the recipe, and the recipe is the part KM Hub is paid for. Each issued step carries a marker identifying the workspace it was leased to.',
|
|
152
|
+
'Run the play that fits what was actually asked. There is an allowance on how many plays get pulled in a day and on how many DIFFERENT ones, so do not open plays speculatively, do not work down the catalog to see what each one says, and do not start a second play while the first is unfinished. A play already opened today can be run again as often as it is useful.',
|
|
153
|
+
'It sends nothing to anybody. A play that produces a message still ends at km_create_outreach_draft, in the Approval Queue, for a human to read and release. If a step asks for something you do not have, say so and ask, rather than filling the gap with a plausible guess.',
|
|
154
|
+
].join(' '),
|
|
155
|
+
{
|
|
156
|
+
slug: z
|
|
157
|
+
.string()
|
|
158
|
+
.describe('Which play to run, exactly as km_play_catalog gave it, for example quiet-week-rescue. Never invent a slug or guess one from a name.'),
|
|
159
|
+
run_id: z
|
|
160
|
+
.string()
|
|
161
|
+
.optional()
|
|
162
|
+
.describe('Leave this out for the first step. To continue a play you already started, pass back the run id from the previous step and you get the next one.'),
|
|
163
|
+
context: z
|
|
164
|
+
.string()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe('What the person actually asked for, in their own words, plus anything about right now that the step should take into account. It lets KM Hub aim the step at the real situation instead of the general case.'),
|
|
167
|
+
step_result: z
|
|
168
|
+
.string()
|
|
169
|
+
.optional()
|
|
170
|
+
.describe('Only when continuing a run: what happened on the step you just finished, in a sentence or two, including anything that did not work. The next step is built on it, so an honest account beats a tidy one.'),
|
|
171
|
+
},
|
|
172
|
+
async (args) => {
|
|
173
|
+
// Slugs are lowercase kebab everywhere in this codebase, and a model that
|
|
174
|
+
// has read the catalog once and is typing from memory will occasionally
|
|
175
|
+
// capitalise one. Fixing that here costs nothing and saves a 404.
|
|
176
|
+
const slug = String(args?.slug || '').trim().toLowerCase();
|
|
177
|
+
if (!slug) {
|
|
178
|
+
return text(
|
|
179
|
+
'I need to know which play to run. km_play_catalog lists the ones this workspace has, with the slug for each.',
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
// The route requires an idempotency_key (8-128 of [A-Za-z0-9._:-]) so a
|
|
183
|
+
// retry cannot double-spend a quota slot. It is minted here, never asked
|
|
184
|
+
// of the model: a caller-chosen key is a footgun, and a model has no
|
|
185
|
+
// basis to invent a stable one. Continuing a run reuses a key derived
|
|
186
|
+
// from the run id so a retried continuation replays instead of counting
|
|
187
|
+
// twice; a fresh run gets a random one.
|
|
188
|
+
const runId = String(args?.run_id || '').trim();
|
|
189
|
+
const idempotency_key = runId
|
|
190
|
+
? `run-${runId.replace(/[^A-Za-z0-9._:-]/g, '')}`.slice(0, 128)
|
|
191
|
+
: crypto.randomUUID().replace(/-/g, '');
|
|
192
|
+
const r = await call('POST', '/plays/run', { ...args, slug, idempotency_key });
|
|
193
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
194
|
+
const refused = quotaRefusal(r);
|
|
195
|
+
if (refused) return refused;
|
|
196
|
+
return out(r);
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
server.tool(
|
|
201
|
+
'km_play_verify',
|
|
202
|
+
[
|
|
203
|
+
'Check whether the play actually worked. Every play has a criterion: the observable thing that should be true now if it did its job. That is a different question from whether you got through the steps, and it is the one that matters.',
|
|
204
|
+
'Call it when a play reaches its end, or whenever someone asks whether the thing you ran last week did anything. KM Hub gives you the criterion, you check it against what you can genuinely see through the km_* tools, and you report what you found.',
|
|
205
|
+
'Being honest here is the whole point of the tool. "I ran it, and I cannot confirm yet that it worked" is a real answer, a useful one, and far better than a confident yes that falls apart a week later. Say which part you could confirm, which part you could not, and what would settle it.',
|
|
206
|
+
'Do not call a play successful because the steps are done. Do not verify a play you did not run. This changes nothing in the workspace, sends nothing to anybody, and cannot make a play have worked.',
|
|
207
|
+
].join(' '),
|
|
208
|
+
{
|
|
209
|
+
run_id: z
|
|
210
|
+
.string()
|
|
211
|
+
.optional()
|
|
212
|
+
.describe('The run being checked, as returned by km_play_run. Give this whenever you have it: it is what ties the criterion to the work that was actually done.'),
|
|
213
|
+
slug: z
|
|
214
|
+
.string()
|
|
215
|
+
.optional()
|
|
216
|
+
.describe('The play being checked, as km_play_catalog gave it. Pass it alongside run_id for context. On its own it is not enough: the check is tied to a specific run, so without the run id from km_play_run there is nothing to judge against.'),
|
|
217
|
+
outcome: z
|
|
218
|
+
.string()
|
|
219
|
+
.optional()
|
|
220
|
+
.describe('What you did and what you can see now, in plain words, so the criterion is judged against reality rather than against intent. Include what did not happen as well as what did.'),
|
|
221
|
+
},
|
|
222
|
+
async (args) => {
|
|
223
|
+
const runId = String(args?.run_id || '').trim();
|
|
224
|
+
const slug = String(args?.slug || '').trim().toLowerCase();
|
|
225
|
+
if (!runId) {
|
|
226
|
+
return text(
|
|
227
|
+
'I need the run id that km_play_run gave back, because the check is tied to that specific run. ' +
|
|
228
|
+
'If it is genuinely lost, run the play again rather than guessing whether the old one worked.',
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
// The route reads `evidence`; this tool calls it `outcome` because that
|
|
232
|
+
// is the friendlier word for a model. Map it, or every call 400s.
|
|
233
|
+
const r = await call('POST', '/plays/verify', {
|
|
234
|
+
run_id: runId || undefined,
|
|
235
|
+
slug: slug || undefined,
|
|
236
|
+
evidence: String(args?.outcome || '').trim() || undefined,
|
|
237
|
+
});
|
|
238
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
239
|
+
const refused = quotaRefusal(r);
|
|
240
|
+
if (refused) return refused;
|
|
241
|
+
return out(r);
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
}
|