@kivimedia/kmhub 2.10.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kivimedia/kmhub",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "KM Hub Terminal Mode. Installs the KM Hub MCP connector into your own Claude Code on your own machine, and ships the kmhub CLI that registers, updates and diagnoses it.",
5
5
  "keywords": [
6
6
  "kmhub",
package/prompts/run.md CHANGED
@@ -21,6 +21,7 @@ The request is `$ARGUMENTS`, in the user's own words. Pass it through as they wr
21
21
  - **A named piece of work** (a follow up sweep, a reply to handle, a quote, a rate rise, a silence check, a whale hunt): this is a play. Use `/kmhub-play`, or call `km_play_catalog` then `km_play_run`.
22
22
  - **One specific thing** ("show me the Meister invoice", "who has not signed", "what is on Thursday"): go straight to the read tool for it. `km_get_*` for one record, `km_list_*` for a set.
23
23
  - **Anything the workspace should know** ("we now charge 2,500 for corporate"): `km_propose_business_fact`. It proposes, a human confirms.
24
+ - **Their voice** ("teach KM Hub my voice", "now that you know my voice make sure KM Hub knows it"): call `km_get_brand_voice`, then `km_update_brand_voice` with the full profile (up to 8000 characters). Never upload a voice profile as a Business Knowledge document: a document is cut down to a short summary, and the brand voice box is the one field every KM Hub writer follows.
24
25
 
25
26
  ## The rules that do not bend
26
27
 
@@ -34,7 +34,7 @@ function routeMissing(r) {
34
34
  const NOT_SUPPORTED =
35
35
  'Your KM Hub cannot review business facts from the terminal yet. Nothing is wrong with the workspace and nothing ' +
36
36
  'was changed. The owner can confirm or reject them in the web app at https://hub.kivimedia.co under Settings, ' +
37
- 'Connect, Business Knowledge.';
37
+ 'Account, Business Knowledge.';
38
38
 
39
39
  const FACT_IDS = z
40
40
  .array(z.string())
package/tools/gols.mjs CHANGED
@@ -28,7 +28,7 @@ function routeMissing(r) {
28
28
 
29
29
  const NOT_SUPPORTED =
30
30
  'This KM Hub does not expose GOLS (the grand-opening scout) to the terminal yet. Nothing is broken; it is in the ' +
31
- 'web app at https://hub.kivimedia.co/gols. Nothing was charged.';
31
+ 'web app at https://hub.kivimedia.co/?page=gols. Nothing was charged.';
32
32
 
33
33
  const CONFIRM_TOKEN = z
34
34
  .string()
@@ -42,23 +42,30 @@ const CONFIRM_TOKEN = z
42
42
  export function register(server, call, { out, text, qs }) {
43
43
  server.tool(
44
44
  'km_gols_finds',
45
- 'GOLS (the Grand Opening / Event Scout, also written "gols"): the businesses KM Hub has found that just opened or ' +
46
- 'have announced an opening near the owner, each scored hot / warm / cold for how likely it is to book event ' +
47
- 'services for a grand opening, launch or ribbon cutting. Best first. Reach for it when the owner asks about ' +
48
- 'GOLS, grand openings, new businesses near them, or "who is opening soon". `when` says "Opens <date>" only ' +
49
- 'for a date the announcement stated; "In the news <date>" is just the article date, so never call that an ' +
50
- 'opening date. Free, read only. To find more, use km_gols_scan; to act on one, km_gols_push_find.',
45
+ 'GOLS (the Grand Opening / Event Scout, also written "gols"): businesses near the owner that announced an opening ' +
46
+ 'and have NOT opened yet, each scored hot / warm / cold for how likely it is to book event services for its ' +
47
+ 'grand opening, launch or ribbon cutting. By default only openings at least 2 weeks away or with the date not ' +
48
+ 'announced, soonest dated first; the reply counts what it hid (already opened, under 2 weeks away, not an ' +
49
+ 'opening) and when "all" shows them. Reach for it when the owner asks about GOLS, grand openings, new businesses ' +
50
+ 'near them, or "who is opening soon". `when` says "Opens <date>" only for a date the announcement stated; ' +
51
+ '"Announced, opening date not stated" means the article gave no date, so never invent one. `location` is the ' +
52
+ "business's own town, or null when the article did not say. Free, read only. To find more, use km_gols_scan; " +
53
+ 'to act on one, km_gols_push_find.',
51
54
  {
52
55
  status: z
53
56
  .enum(['new', 'pushed', 'dismissed', 'all'])
54
57
  .optional()
55
58
  .describe('Which finds. Default "new" = not yet acted on. "pushed" = already in the pipeline.'),
59
+ when: z
60
+ .enum(['upcoming', 'all'])
61
+ .optional()
62
+ .describe('Default "upcoming" = not open yet and at least 2 weeks out (or date not announced). "all" = every row, including past ones.'),
56
63
  tier: z.enum(['hot', 'warm', 'cold']).optional().describe('Only this fit tier. Leave out for all.'),
57
64
  city: z.string().optional().describe('Only finds from scans of this city, as the scan was run.'),
58
65
  limit: z.number().int().min(1).max(100).optional().describe('How many. Default 30.'),
59
66
  },
60
- async ({ status, tier, city, limit }) => {
61
- const r = await call('GET', `/gols/finds${qs({ status, tier, city, limit })}`);
67
+ async ({ status, when, tier, city, limit }) => {
68
+ const r = await call('GET', `/gols/finds${qs({ status, when, tier, city, limit })}`);
62
69
  if (routeMissing(r)) return text(NOT_SUPPORTED, true);
63
70
  return out(r);
64
71
  },
@@ -67,23 +74,26 @@ export function register(server, call, { out, text, qs }) {
67
74
  server.tool(
68
75
  'km_gols_scan',
69
76
  [
70
- 'Run a GOLS scan (grand-opening scout): search the news (and Google Places when connected) for businesses that',
71
- 'just opened near a city, or with ahead=true for ones that have ANNOUNCED an opening that has not happened yet,',
72
- 'then score each one for event-services fit and save it. It takes about a minute and returns the finds.',
77
+ 'Run a GOLS scan (grand-opening scout): read the last 90 days of opening announcements in the news for ONE US',
78
+ 'county (county + state: the county by name plus its biggest towns) or near a city (city + state), keep only',
79
+ 'businesses that have NOT opened yet (finds that already opened, or that are outside the county, are dropped',
80
+ 'and counted), score each one for event-services fit and save it. A business already on the list is updated,',
81
+ 'not added twice. For several counties, call it once per county; each county is priced on its own. For a place',
82
+ 'outside the US, such as Windsor, Ontario, scan the city. There is no look-back setting and no "recently',
83
+ 'opened" mode. It takes about a minute and returns the finds.',
73
84
  'THIS COSTS MONEY (AI time, billed to the workspace). Call it FIRST without confirm_spend: nothing runs and you',
74
85
  'get the price. Tell the person the price, and on a yes call again with confirm_spend true; KM Hub then answers',
75
86
  '409 with the exact action and a confirm_token, which you show them and send back with the same arguments.',
76
87
  'Never start one on your own initiative. It contacts nobody.',
77
88
  ].join(' '),
78
89
  {
79
- city: z.string().describe('The city to scan around, as the person said it. For example "Southfield".'),
80
- state: z.string().describe('State or province, for example "MI" or "Ontario".'),
81
- radius_miles: z.number().int().min(1).max(200).optional().describe('How far around the city. Default 25.'),
82
- lookback_days: z.number().int().min(1).max(120).optional().describe('How far back in the news to look. Default 30.'),
83
- ahead: z
84
- .boolean()
90
+ county: z
91
+ .string()
85
92
  .optional()
86
- .describe('true = announced openings that have NOT happened yet (the best time to pitch). Default false = already opened.'),
93
+ .describe('A US county to scan, as the person said it, for example "Oakland" or "Oakland County". Needs state. Leave out to scan around a city.'),
94
+ city: z.string().optional().describe('The city to scan around, for example "Southfield". Leave out when you pass county.'),
95
+ state: z.string().describe('State or province, for example "MI" or "Ontario".'),
96
+ radius_miles: z.number().int().min(1).max(200).optional().describe('How far around the city (city scans only). Default 25.'),
87
97
  vertical: z.string().optional().describe('The trade to score fit for, only if it differs from the workspace, e.g. "balloon decor".'),
88
98
  zip: z.string().optional().describe('ZIP code, only if the person gave one.'),
89
99
  confirm_spend: z
@@ -60,7 +60,7 @@ export function register(server, call, { out, qs }) {
60
60
 
61
61
  server.tool(
62
62
  'km_get_business_facts',
63
- 'What this business has told KM Hub about itself: the facts its owner has CONFIRMED, the standing facts they asked to be remembered permanently, and the knowledge distilled from documents they uploaded such as past proposals, pricing sheets and contracts. Read it before you write anything in the business name, so you work from what is actually true here instead of from a generic idea of the trade. Everything it returns is owner-approved and safe to rely on. Facts that are still waiting for the owner are counted but deliberately not shown, because nobody has confirmed them yet - if that count is above zero it is worth telling the person they have some waiting: km_list_pending_business_facts shows them, and km_confirm_business_facts confirms the ones the owner says are right. Read only.',
63
+ 'What this business has told KM Hub about itself: the facts its owner has CONFIRMED, the standing facts they asked to be remembered permanently, and the knowledge distilled from documents they uploaded such as past proposals, pricing sheets and contracts. Read it before you write anything in the business name, so you work from what is actually true here instead of from a generic idea of the trade. Everything it returns is owner-approved and safe to rely on. A confirmed fact marked internal: true (a cost, a margin, a goal) is for the owner only: use it to advise them, but never quote it to a customer and never put it in a draft. Facts that are still waiting for the owner are counted but deliberately not shown, because nobody has confirmed them yet - if that count is above zero it is worth telling the person they have some waiting: km_list_pending_business_facts shows them, and km_confirm_business_facts confirms the ones the owner says are right. Read only.',
64
64
  {
65
65
  kind: z
66
66
  .enum(['business', 'service', 'price', 'voice', 'vertical_craft'])
@@ -109,7 +109,7 @@ export function register(server, call, { out, qs }) {
109
109
 
110
110
  server.tool(
111
111
  'km_propose_business_fact',
112
- 'Write down something durable this business has learned about itself, for example that they no longer travel more than two hours, or that their busy season starts in March, or that they always need a stage of a certain size. It is saved as a PROPOSAL and not as truth: it waits in KM Hub under Business Knowledge until the owner confirms it, and no draft, quote or answer will use it before then. Use it only for something that will still be true next month. A detail about ONE client or ONE booking is not a business fact and belongs on that record instead. Always say in source_note where the fact came from, in one line, because that is what the owner reads when deciding whether to accept it. After calling this, tell the person plainly that it is waiting for their approval; when they say it is right, km_confirm_business_facts confirms it with the fact_id this returns. This tool itself cannot confirm a fact, cannot edit or replace an existing one, and cannot remove one.',
112
+ 'Write down something durable this business has learned about itself, for example that they no longer travel more than two hours, or that their busy season starts in March, or that they always need a stage of a certain size. It is saved as a PROPOSAL and not as truth: it waits in KM Hub under Business Knowledge until the owner confirms it, and nothing uses it before then. Once the owner confirms it, KM Hub\'s AI writers use it: business, service and craft facts in the drafts, replies and messages they write, a price fact only in replies and quotes and never in a first cold email, and a voice fact as style for every writer. Set audience to internal for anything that must never reach a customer, such as what something costs the business, a margin, or a sales goal: an internal fact never reaches any AI writer or any customer, and stays visible to the owner and to you. Use it only for something that will still be true next month. A detail about ONE client or ONE booking is not a business fact and belongs on that record instead. Always say in source_note where the fact came from, in one line, because that is what the owner reads when deciding whether to accept it. After calling this, tell the person plainly that it is waiting for their approval; when they say it is right, km_confirm_business_facts confirms it with the fact_id this returns. This tool itself cannot confirm a fact, cannot edit or replace an existing one, and cannot remove one.',
113
113
  {
114
114
  text: z.string().describe('The fact itself, in one plain sentence, written the way the owner would say it.'),
115
115
  source_note: z
@@ -119,6 +119,10 @@ export function register(server, call, { out, qs }) {
119
119
  .enum(['business', 'service', 'price', 'voice', 'vertical_craft'])
120
120
  .optional()
121
121
  .describe('What sort of fact it is. business is the default and covers how they operate. Use price only for a real published price, service for something they offer, voice for how they sound.'),
122
+ audience: z
123
+ .enum(['customer', 'internal'])
124
+ .optional()
125
+ .describe('Who the fact is for. customer is the default. internal keeps it away from every AI writer and every customer: use it for costs, margins, goals and anything the owner said never to quote. Text that starts with INTERNAL is treated as internal anyway.'),
122
126
  },
123
127
  async (args) => out(await call('POST', '/knowledge/facts', args)),
124
128
  );
package/tools/voice.mjs CHANGED
@@ -57,8 +57,11 @@ export function register(server, call, { out, text }) {
57
57
  'km_update_brand_voice',
58
58
  'Change how this business sounds in everything the AI writes: the brand voice box (the owner\'s own words about ' +
59
59
  'their values, tone and style) and the name every email is signed with. It is the same box the owner sees in ' +
60
- 'KM Hub under Settings, General, and in the Sales Playbook Values tab, and every AI writer reads it: first ' +
61
- 'replies, rewrites, review replies, broadcasts, templates, follow-ups. ' +
60
+ 'KM Hub under Business Knowledge, Voice, under Settings, General, and in the Sales Playbook Values tab, and ' +
61
+ 'every AI writer reads it: first replies, rewrites, review replies, broadcasts, templates, follow-ups. ' +
62
+ 'When the owner asks you to teach KM Hub their voice, or says "now that you know my voice make sure KM Hub ' +
63
+ 'knows it", this is the tool: send the full profile here (up to 8000 characters). Never upload a voice ' +
64
+ 'profile as a Business Knowledge document. ' +
62
65
  'Call km_get_brand_voice first so you change from what is actually there. When the person asks to ADD to their ' +
63
66
  'voice, send the existing text with the addition, never just the new sentence, or the rest is lost. Write ' +
64
67
  'what they said in their words; do not polish it into marketing copy. ' +
@@ -69,7 +72,7 @@ export function register(server, call, { out, text }) {
69
72
  .string()
70
73
  .nullable()
71
74
  .optional()
72
- .describe('The full brand voice text, up to 4000 characters. It REPLACES what is there. Send null to clear it.'),
75
+ .describe('The full brand voice text, up to 8000 characters. It REPLACES what is there. Send null to clear it.'),
73
76
  sign_emails_as: z
74
77
  .string()
75
78
  .nullable()
package/tools.mjs CHANGED
@@ -30,7 +30,7 @@ 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.10.0';
33
+ export const SERVER_VERSION = '2.11.0';
34
34
 
35
35
  export const DEFAULT_BASE =
36
36
  process.env.KMHUB_API_BASE ||