@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.
Files changed (55) hide show
  1. package/README.md +6 -5
  2. package/bin/kmhub.mjs +20 -7
  3. package/coach-book-output-guard.mjs +760 -0
  4. package/index.mjs +2 -0
  5. package/package.json +8 -3
  6. package/prompts/briefing.md +29 -0
  7. package/prompts/luxury.md +70 -0
  8. package/prompts/play.md +49 -0
  9. package/prompts/run.md +36 -0
  10. package/prompts/setup.md +33 -0
  11. package/prompts/vs-booked.md +46 -0
  12. package/prompts/what-can-you-do.md +40 -0
  13. package/prompts.mjs +110 -0
  14. package/read-only-tools.json +142 -0
  15. package/remote.mjs +815 -99
  16. package/tools/balloon-costing.mjs +80 -0
  17. package/tools/booking-equipment.mjs +110 -0
  18. package/tools/bridges.mjs +54 -0
  19. package/tools/calendar.mjs +9 -0
  20. package/tools/capabilities.mjs +155 -0
  21. package/tools/catalog.mjs +288 -0
  22. package/tools/clubs.mjs +176 -0
  23. package/tools/coach.mjs +771 -0
  24. package/tools/compare.mjs +76 -0
  25. package/tools/core.mjs +21 -0
  26. package/tools/crm.mjs +12 -3
  27. package/tools/dubsado.mjs +137 -0
  28. package/tools/exports.mjs +128 -0
  29. package/tools/fact-review.mjs +125 -0
  30. package/tools/flows.mjs +261 -0
  31. package/tools/forms.mjs +158 -0
  32. package/tools/gols.mjs +134 -0
  33. package/tools/hr.mjs +162 -0
  34. package/tools/knowledge.mjs +4 -3
  35. package/tools/marketing.mjs +396 -0
  36. package/tools/meta.mjs +2 -2
  37. package/tools/military.mjs +244 -0
  38. package/tools/outreach.mjs +27 -4
  39. package/tools/pending.mjs +122 -0
  40. package/tools/photos.mjs +140 -0
  41. package/tools/plays.mjs +1 -1
  42. package/tools/profile.mjs +118 -0
  43. package/tools/radar.mjs +173 -0
  44. package/tools/recurring-invoices.mjs +149 -0
  45. package/tools/reengage.mjs +434 -0
  46. package/tools/schedules.mjs +55 -0
  47. package/tools/setup.mjs +168 -0
  48. package/tools/sops-bridges.mjs +86 -0
  49. package/tools/sops.mjs +314 -0
  50. package/tools/sourcing.mjs +50 -2
  51. package/tools/strategy.mjs +146 -0
  52. package/tools/studio.mjs +132 -0
  53. package/tools/venueradar.mjs +151 -0
  54. package/tools/voice.mjs +134 -0
  55. package/tools.mjs +70 -12
package/tools.mjs CHANGED
@@ -26,11 +26,11 @@
26
26
  * wants one side of the workspace. `full` is the default and holds everything, so
27
27
  * an existing client sees no change.
28
28
  */
29
- import { readdirSync } from 'node:fs';
29
+ import { readdirSync, readFileSync } from 'node:fs';
30
30
  import { z } from 'zod';
31
31
 
32
32
  export const SERVER_NAME = 'kmhub';
33
- export const SERVER_VERSION = '2.0.0';
33
+ export const SERVER_VERSION = '2.9.1';
34
34
 
35
35
  export const DEFAULT_BASE =
36
36
  process.env.KMHUB_API_BASE ||
@@ -89,7 +89,15 @@ export function out(r) {
89
89
  lines.push(url ? `To pick things back up, open ${url} and restart the subscription.` : BILLING_FALLBACK);
90
90
  return { content: [{ type: 'text', text: lines.join('\n\n') }], isError: true };
91
91
  }
92
- return { content: [{ type: 'text', text: JSON.stringify(r.data, null, 2) }], isError: !r.ok };
92
+ // The way back to the window (Freshdesk #271, Jackie): "I always have this problem
93
+ // of not getting back to the correct window using Claude and KM Hub once it is
94
+ // closed." A route that answers out of one page of the app says so in
95
+ // `open_in_km_hub`, and that line is appended in plain text rather than left buried
96
+ // in the JSON, so the model reliably shows her a link she can click twice.
97
+ const link = typeof body.open_in_km_hub === 'string' ? body.open_in_km_hub.trim() : '';
98
+ const json = JSON.stringify(r.data, null, 2);
99
+ const payload = link ? `${json}\n\nOpen in KM Hub: ${link}` : json;
100
+ return { content: [{ type: 'text', text: payload }], isError: !r.ok };
93
101
  }
94
102
 
95
103
  /** A plain prose answer. Defaults to a success result: not every "no" is an error. */
@@ -138,7 +146,8 @@ export function qs(params) {
138
146
  // A play's task calls tools from every family at once, so a run
139
147
  // started on a narrow profile fails halfway through, which is a
140
148
  // worse outcome than not offering the play.
141
- // money 38 The money side, whole. The nine read-only money tools, plus crm
149
+ // money The money side, whole. The read-only money tools, plus hr because
150
+ // payroll is money owed to the people who did the work, plus crm
142
151
  // so you can open the client you are about to chase, plus knowledge
143
152
  // because the published ladder is the only legal source of a number.
144
153
  // outreach 51 The cold outreach machine: outreach and sourcing, plus crm to turn
@@ -147,19 +156,28 @@ export function qs(params) {
147
156
  // saves the least of the three, and that is honest rather than
148
157
  // disappointing: the outreach job genuinely reaches most of the
149
158
  // workspace. `core` is the profile that buys real context back.
150
- // full 63 Everything, including the plays. The default.
159
+ // content The being-found side: SEO and the newsletter, plus crm and
160
+ // knowledge, because content that does not know who buys or what
161
+ // the business sounds like is generic content. This profile only
162
+ // became real when the marketing family shipped (workstream B1);
163
+ // before that the name existed in the installer and silently fell
164
+ // back to `full`, which was a small lie told to anyone who used it.
165
+ // full Everything, including the plays. The default.
166
+ // coach Fully Booked Coach operations, client delivery, acquisition,
167
+ // content requests, and approval previews. It exposes no send
168
+ // or publish tool.
151
169
  //
152
- // There is deliberately no `content` profile. There are no content tools. An older
153
- // client that still asks for one gets `full` rather than an error, exactly as any
154
- // other unknown name does.
170
+ // An unknown profile name still gets `full` rather than an error, exactly as before.
155
171
 
156
172
  export const DEFAULT_PROFILE = 'full';
157
173
 
158
174
  export const PROFILES = {
159
175
  core: ['core', 'meta', 'briefing'],
160
- money: ['core', 'meta', 'briefing', 'crm', 'knowledge', 'money'],
161
- outreach: ['core', 'meta', 'briefing', 'crm', 'calendar', 'knowledge', 'outreach', 'sourcing'],
176
+ money: ['core', 'meta', 'briefing', 'crm', 'knowledge', 'money', 'hr'],
177
+ outreach: ['core', 'meta', 'briefing', 'crm', 'calendar', 'knowledge', 'outreach', 'sourcing', 'marketing'],
178
+ content: ['core', 'meta', 'briefing', 'crm', 'knowledge', 'marketing'],
162
179
  full: '*',
180
+ coach: ['coach'],
163
181
  };
164
182
 
165
183
  /**
@@ -168,6 +186,10 @@ export const PROFILES = {
168
186
  * has to be named here on its own, because it is placed too and placed nowhere but
169
187
  * `full`: without this line its own ['*'] would put it back into all three.
170
188
  */
189
+ // Coach is a public pilot surface with an explicit safety review. New families
190
+ // must not silently join it through a wildcard declaration.
191
+ const CLOSED_PROFILES = new Set(['coach']);
192
+
171
193
  const PLACED = new Set([...Object.values(PROFILES).filter(Array.isArray).flat(), 'plays']);
172
194
 
173
195
  export const PROFILE_NAMES = Object.keys(PROFILES);
@@ -256,6 +278,7 @@ function inProfile(family, profile) {
256
278
  const listed = PROFILES[profile];
257
279
  if (listed === '*') return true;
258
280
  if (Array.isArray(listed) && listed.includes(family.FAMILY)) return true;
281
+ if (CLOSED_PROFILES.has(profile)) return false;
259
282
  if (PLACED.has(family.FAMILY)) return false;
260
283
  return family.PROFILES.includes('*') || family.PROFILES.includes(profile);
261
284
  }
@@ -286,6 +309,41 @@ export const TOOL_NAMES = toolNamesFor(DEFAULT_PROFILE);
286
309
  * make the SDK throw mid-request and cost the caller every tool, not just the
287
310
  * clashing one. `raw` is there for the rare family that needs the real server.
288
311
  */
312
+ /**
313
+ * The tools announced as read-only, from read-only-tools.json (generated from the tool
314
+ * source by scripts/generate-read-only-tools.mjs - see there for the rule).
315
+ *
316
+ * 🚨 CODEX STOPS EVERY UNANNOTATED TOOL FOR AN APPROVAL. With no hint on any KM Hub tool,
317
+ * a Codex session asked permission before `km_me`, and `codex exec` failed every call with
318
+ * "MCP tool call requires approval". readOnlyHint: true is what lets a read run. Writes stay
319
+ * unannotated on purpose, so the person still approves each one.
320
+ *
321
+ * A missing or broken file costs the hints, never the tools.
322
+ */
323
+ const READ_ONLY_TOOLS = (() => {
324
+ try {
325
+ const doc = JSON.parse(readFileSync(new URL('./read-only-tools.json', import.meta.url), 'utf8'));
326
+ return new Set(Array.isArray(doc.tools) ? doc.tools : []);
327
+ } catch (e) {
328
+ warn(`read-only-tools.json could not be read (${String(e?.message || e)}). Every tool will be announced without a read-only hint.`);
329
+ return new Set();
330
+ }
331
+ })();
332
+
333
+ export function isReadOnlyTool(name) {
334
+ return READ_ONLY_TOOLS.has(name);
335
+ }
336
+
337
+ function markReadOnly(name, registeredTool) {
338
+ if (!registeredTool || !READ_ONLY_TOOLS.has(name) || typeof registeredTool.update !== 'function') return registeredTool;
339
+ try {
340
+ registeredTool.update({ annotations: { ...(registeredTool.annotations || {}), readOnlyHint: true } });
341
+ } catch (e) {
342
+ warn(`could not mark '${name}' read-only (${String(e?.message || e)}). It still works; hosts will ask before running it.`);
343
+ }
344
+ return registeredTool;
345
+ }
346
+
289
347
  function guardedRegistrar(server, taken, family, registered) {
290
348
  const claim = (name) => {
291
349
  if (typeof name !== 'string' || !name.trim()) {
@@ -302,8 +360,8 @@ function guardedRegistrar(server, taken, family, registered) {
302
360
  return true;
303
361
  };
304
362
  return {
305
- tool: (name, ...rest) => (claim(name) ? server.tool(name, ...rest) : undefined),
306
- registerTool: (name, ...rest) => (claim(name) ? server.registerTool(name, ...rest) : undefined),
363
+ tool: (name, ...rest) => (claim(name) ? markReadOnly(name, server.tool(name, ...rest)) : undefined),
364
+ registerTool: (name, ...rest) => (claim(name) ? markReadOnly(name, server.registerTool(name, ...rest)) : undefined),
307
365
  prompt: (...args) => server.prompt(...args),
308
366
  resource: (...args) => server.resource(...args),
309
367
  raw: server,