@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/profile.mjs
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool family: profile - who this business IS, and correcting it when it is wrong.
|
|
3
|
-
*
|
|
4
|
-
* km_get_profile -> GET /profile
|
|
5
|
-
* km_update_profile -> PATCH /profile
|
|
6
|
-
*
|
|
7
|
-
* WHY THIS EXISTS. A connector could already READ enough to notice that a profile
|
|
8
|
-
* was wrong and had no way to fix it. The nearest write was km_propose_business_fact,
|
|
9
|
-
* which lands a PENDING proposal: it does not change the talent category, the
|
|
10
|
-
* timezone or the sending identity, and nothing downstream reads it until a human
|
|
11
|
-
* approves it in the web app. A model that answers "I have recorded that you are a
|
|
12
|
-
* mentalist" on the back of a proposal has told the user something untrue.
|
|
13
|
-
*
|
|
14
|
-
* 🚨 The talent category (verticals) is the field that matters. It decides the
|
|
15
|
-
* flavour every AI writer and every lead scout works in. A magician's workspace
|
|
16
|
-
* that is really a mentalist's produces subtly wrong copy in every direction, and
|
|
17
|
-
* the owner usually cannot tell WHY it sounds off.
|
|
18
|
-
*
|
|
19
|
-
* 🚨 km_update_profile is a confirmed write. KM Hub answers 409 with the exact
|
|
20
|
-
* before -> after sentence it wants approved, written server-side from the live
|
|
21
|
-
* values. Show that sentence and wait for a real answer.
|
|
22
|
-
*
|
|
23
|
-
* The family contract this file follows is documented in ./README.md.
|
|
24
|
-
*/
|
|
25
|
-
import { z } from 'zod';
|
|
26
|
-
|
|
27
|
-
export const FAMILY = 'profile';
|
|
28
|
-
|
|
29
|
-
export const TOOLS = ['km_get_profile', 'km_update_profile'];
|
|
30
|
-
|
|
31
|
-
export const PROFILES = ['*'];
|
|
32
|
-
|
|
33
|
-
function routeMissing(r) {
|
|
34
|
-
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const NOT_SUPPORTED =
|
|
38
|
-
'Your KM Hub does not expose the business profile to the terminal yet. That is not a fault: the workspace is fine ' +
|
|
39
|
-
'and every other tool works as normal. The profile is in the web app at https://hub.kivimedia.co under Settings, ' +
|
|
40
|
-
'and this connector will read and edit it once your KM Hub is on a build that publishes it.';
|
|
41
|
-
|
|
42
|
-
export function register(server, call, { out, text }) {
|
|
43
|
-
server.tool(
|
|
44
|
-
'km_get_profile',
|
|
45
|
-
'Who this business actually is, as the software understands it: its name, its talent category, the timezone and ' +
|
|
46
|
-
'currency it works in, the language the AI writes in, and the public email, phone, website and address clients ' +
|
|
47
|
-
'see on quotes and invoices. This is the live row the web app Settings page reads, not a cached copy. ' +
|
|
48
|
-
'Call it before writing anything in the business\'s own voice, before quoting a price, and before saying anything ' +
|
|
49
|
-
'about what they do. ' +
|
|
50
|
-
'🚨 The talent category is the one to look at hardest. It decides the flavour every AI writer and every lead scout ' +
|
|
51
|
-
'works in, so a workspace still set to Magician when the person is a Mentalist produces subtly wrong copy ' +
|
|
52
|
-
'everywhere, and the owner usually cannot tell why it sounds off. If what you see does not match what the person ' +
|
|
53
|
-
'has told you, say so plainly and offer to correct it with km_update_profile. Do not quietly work around it, and ' +
|
|
54
|
-
'do not file it as a business-knowledge fact instead: a proposed fact does not change the profile. Read only.',
|
|
55
|
-
{},
|
|
56
|
-
async () => {
|
|
57
|
-
const r = await call('GET', '/profile');
|
|
58
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
59
|
-
return out(r);
|
|
60
|
-
},
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
server.tool(
|
|
64
|
-
'km_update_profile',
|
|
65
|
-
'Correct the live business profile: the talent category, the business name, the timezone, the currency, the ' +
|
|
66
|
-
'language the AI writes in, or the public email, phone, website and address. Send only the fields that are wrong. ' +
|
|
67
|
-
'This is a real change to the live workspace and it takes effect immediately - the web app Settings page will show ' +
|
|
68
|
-
'the same values a second later. ' +
|
|
69
|
-
'Call km_get_profile first so you are changing from a value you have actually seen, and so you use vertical keys ' +
|
|
70
|
-
'from the vocabulary it returns rather than inventing one. ' +
|
|
71
|
-
'🚨 This needs an explicit yes from the person, in this turn. KM Hub answers 409 with a plain sentence naming ' +
|
|
72
|
-
'exactly what would change, from what, to what, written by the server from the live values. Put that sentence to ' +
|
|
73
|
-
'them in those words, wait for a real answer, and only then call again with the confirm_token and the SAME ' +
|
|
74
|
-
'arguments. Do not treat something said earlier in the conversation as agreement. ' +
|
|
75
|
-
'🚨 Changing the currency does NOT convert any money already recorded: every stored amount keeps its number and is ' +
|
|
76
|
-
'simply re-labelled. Changing the talent category does not rewrite anything already written, only work that comes ' +
|
|
77
|
-
'after it. ' +
|
|
78
|
-
'The account type (solo or agency), the logo, the default tax rate and the portal colour are NOT editable from ' +
|
|
79
|
-
'here and no argument unlocks them - they are money, an upload, or something you cannot see the result of from a ' +
|
|
80
|
-
'terminal. Those stay in KM Hub.',
|
|
81
|
-
{
|
|
82
|
-
name: z.string().optional().describe('The business name, as it should appear to clients.'),
|
|
83
|
-
verticals: z
|
|
84
|
-
.array(z.string())
|
|
85
|
-
.optional()
|
|
86
|
-
.describe(
|
|
87
|
-
'What this business does, as a list of KM Hub vertical keys, most important first. The valid keys come back '
|
|
88
|
-
+ 'from km_get_profile under editable.verticals_vocabulary - use those exact strings and never invent one. '
|
|
89
|
-
+ 'It cannot be an empty list.',
|
|
90
|
-
),
|
|
91
|
-
timezone: z.string().optional().describe('An IANA zone name, for example Asia/Jerusalem or America/New_York.'),
|
|
92
|
-
currency: z.string().optional().describe('A three-letter code KM Hub supports: USD, CAD, GBP, EUR, AUD or ILS.'),
|
|
93
|
-
language: z.string().optional().describe('The language the AI writes to customers in by default: en, he or es.'),
|
|
94
|
-
public_email: z.string().nullable().optional().describe('The address clients see. Send null to clear it.'),
|
|
95
|
-
public_phone: z.string().nullable().optional().describe('The number clients see. Send null to clear it.'),
|
|
96
|
-
website_url: z.string().nullable().optional().describe('Must start with http:// or https://. Send null to clear it.'),
|
|
97
|
-
address: z.string().nullable().optional().describe('The business address on one line. Send null to clear it.'),
|
|
98
|
-
confirm_token: z
|
|
99
|
-
.string()
|
|
100
|
-
.optional()
|
|
101
|
-
.describe(
|
|
102
|
-
'Leave this out on the first call. KM Hub will answer 409 with the sentence to put to the person and a token. '
|
|
103
|
-
+ 'Only after they actually say yes, call again with the token and the SAME arguments. A yes for one change '
|
|
104
|
-
+ 'never authorises a different one.',
|
|
105
|
-
),
|
|
106
|
-
},
|
|
107
|
-
async (args) => {
|
|
108
|
-
// Only forward what the caller actually set: an undefined key would be sent
|
|
109
|
-
// as an absent field anyway, but a null one MEANS "clear this", so the two
|
|
110
|
-
// must not be flattened together.
|
|
111
|
-
const body = {};
|
|
112
|
-
for (const [k, v] of Object.entries(args)) if (v !== undefined) body[k] = v;
|
|
113
|
-
const r = await call('PATCH', '/profile', body);
|
|
114
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
115
|
-
return out(r);
|
|
116
|
-
},
|
|
117
|
-
);
|
|
118
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: profile - who this business IS, and correcting it when it is wrong.
|
|
3
|
+
*
|
|
4
|
+
* km_get_profile -> GET /profile
|
|
5
|
+
* km_update_profile -> PATCH /profile
|
|
6
|
+
*
|
|
7
|
+
* WHY THIS EXISTS. A connector could already READ enough to notice that a profile
|
|
8
|
+
* was wrong and had no way to fix it. The nearest write was km_propose_business_fact,
|
|
9
|
+
* which lands a PENDING proposal: it does not change the talent category, the
|
|
10
|
+
* timezone or the sending identity, and nothing downstream reads it until a human
|
|
11
|
+
* approves it in the web app. A model that answers "I have recorded that you are a
|
|
12
|
+
* mentalist" on the back of a proposal has told the user something untrue.
|
|
13
|
+
*
|
|
14
|
+
* 🚨 The talent category (verticals) is the field that matters. It decides the
|
|
15
|
+
* flavour every AI writer and every lead scout works in. A magician's workspace
|
|
16
|
+
* that is really a mentalist's produces subtly wrong copy in every direction, and
|
|
17
|
+
* the owner usually cannot tell WHY it sounds off.
|
|
18
|
+
*
|
|
19
|
+
* 🚨 km_update_profile is a confirmed write. KM Hub answers 409 with the exact
|
|
20
|
+
* before -> after sentence it wants approved, written server-side from the live
|
|
21
|
+
* values. Show that sentence and wait for a real answer.
|
|
22
|
+
*
|
|
23
|
+
* The family contract this file follows is documented in ./README.md.
|
|
24
|
+
*/
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
|
|
27
|
+
export const FAMILY = 'profile';
|
|
28
|
+
|
|
29
|
+
export const TOOLS = ['km_get_profile', 'km_update_profile'];
|
|
30
|
+
|
|
31
|
+
export const PROFILES = ['*'];
|
|
32
|
+
|
|
33
|
+
function routeMissing(r) {
|
|
34
|
+
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const NOT_SUPPORTED =
|
|
38
|
+
'Your KM Hub does not expose the business profile to the terminal yet. That is not a fault: the workspace is fine ' +
|
|
39
|
+
'and every other tool works as normal. The profile is in the web app at https://hub.kivimedia.co under Settings, ' +
|
|
40
|
+
'and this connector will read and edit it once your KM Hub is on a build that publishes it.';
|
|
41
|
+
|
|
42
|
+
export function register(server, call, { out, text }) {
|
|
43
|
+
server.tool(
|
|
44
|
+
'km_get_profile',
|
|
45
|
+
'Who this business actually is, as the software understands it: its name, its talent category, the timezone and ' +
|
|
46
|
+
'currency it works in, the language the AI writes in, and the public email, phone, website and address clients ' +
|
|
47
|
+
'see on quotes and invoices. This is the live row the web app Settings page reads, not a cached copy. ' +
|
|
48
|
+
'Call it before writing anything in the business\'s own voice, before quoting a price, and before saying anything ' +
|
|
49
|
+
'about what they do. ' +
|
|
50
|
+
'🚨 The talent category is the one to look at hardest. It decides the flavour every AI writer and every lead scout ' +
|
|
51
|
+
'works in, so a workspace still set to Magician when the person is a Mentalist produces subtly wrong copy ' +
|
|
52
|
+
'everywhere, and the owner usually cannot tell why it sounds off. If what you see does not match what the person ' +
|
|
53
|
+
'has told you, say so plainly and offer to correct it with km_update_profile. Do not quietly work around it, and ' +
|
|
54
|
+
'do not file it as a business-knowledge fact instead: a proposed fact does not change the profile. Read only.',
|
|
55
|
+
{},
|
|
56
|
+
async () => {
|
|
57
|
+
const r = await call('GET', '/profile');
|
|
58
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
59
|
+
return out(r);
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
server.tool(
|
|
64
|
+
'km_update_profile',
|
|
65
|
+
'Correct the live business profile: the talent category, the business name, the timezone, the currency, the ' +
|
|
66
|
+
'language the AI writes in, or the public email, phone, website and address. Send only the fields that are wrong. ' +
|
|
67
|
+
'This is a real change to the live workspace and it takes effect immediately - the web app Settings page will show ' +
|
|
68
|
+
'the same values a second later. ' +
|
|
69
|
+
'Call km_get_profile first so you are changing from a value you have actually seen, and so you use vertical keys ' +
|
|
70
|
+
'from the vocabulary it returns rather than inventing one. ' +
|
|
71
|
+
'🚨 This needs an explicit yes from the person, in this turn. KM Hub answers 409 with a plain sentence naming ' +
|
|
72
|
+
'exactly what would change, from what, to what, written by the server from the live values. Put that sentence to ' +
|
|
73
|
+
'them in those words, wait for a real answer, and only then call again with the confirm_token and the SAME ' +
|
|
74
|
+
'arguments. Do not treat something said earlier in the conversation as agreement. ' +
|
|
75
|
+
'🚨 Changing the currency does NOT convert any money already recorded: every stored amount keeps its number and is ' +
|
|
76
|
+
'simply re-labelled. Changing the talent category does not rewrite anything already written, only work that comes ' +
|
|
77
|
+
'after it. ' +
|
|
78
|
+
'The account type (solo or agency), the logo, the default tax rate and the portal colour are NOT editable from ' +
|
|
79
|
+
'here and no argument unlocks them - they are money, an upload, or something you cannot see the result of from a ' +
|
|
80
|
+
'terminal. Those stay in KM Hub.',
|
|
81
|
+
{
|
|
82
|
+
name: z.string().optional().describe('The business name, as it should appear to clients.'),
|
|
83
|
+
verticals: z
|
|
84
|
+
.array(z.string())
|
|
85
|
+
.optional()
|
|
86
|
+
.describe(
|
|
87
|
+
'What this business does, as a list of KM Hub vertical keys, most important first. The valid keys come back '
|
|
88
|
+
+ 'from km_get_profile under editable.verticals_vocabulary - use those exact strings and never invent one. '
|
|
89
|
+
+ 'It cannot be an empty list.',
|
|
90
|
+
),
|
|
91
|
+
timezone: z.string().optional().describe('An IANA zone name, for example Asia/Jerusalem or America/New_York.'),
|
|
92
|
+
currency: z.string().optional().describe('A three-letter code KM Hub supports: USD, CAD, GBP, EUR, AUD or ILS.'),
|
|
93
|
+
language: z.string().optional().describe('The language the AI writes to customers in by default: en, he or es.'),
|
|
94
|
+
public_email: z.string().nullable().optional().describe('The address clients see. Send null to clear it.'),
|
|
95
|
+
public_phone: z.string().nullable().optional().describe('The number clients see. Send null to clear it.'),
|
|
96
|
+
website_url: z.string().nullable().optional().describe('Must start with http:// or https://. Send null to clear it.'),
|
|
97
|
+
address: z.string().nullable().optional().describe('The business address on one line. Send null to clear it.'),
|
|
98
|
+
confirm_token: z
|
|
99
|
+
.string()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe(
|
|
102
|
+
'Leave this out on the first call. KM Hub will answer 409 with the sentence to put to the person and a token. '
|
|
103
|
+
+ 'Only after they actually say yes, call again with the token and the SAME arguments. A yes for one change '
|
|
104
|
+
+ 'never authorises a different one.',
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
async (args) => {
|
|
108
|
+
// Only forward what the caller actually set: an undefined key would be sent
|
|
109
|
+
// as an absent field anyway, but a null one MEANS "clear this", so the two
|
|
110
|
+
// must not be flattened together.
|
|
111
|
+
const body = {};
|
|
112
|
+
for (const [k, v] of Object.entries(args)) if (v !== undefined) body[k] = v;
|
|
113
|
+
const r = await call('PATCH', '/profile', body);
|
|
114
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
115
|
+
return out(r);
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
}
|
package/tools/radar.mjs
CHANGED
|
@@ -1,173 +1,173 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tool family: radar - the scanners that go looking for work, plus the tail.
|
|
3
|
-
*
|
|
4
|
-
* The last of the parity sweep. KM Hub runs a family of radars: new business
|
|
5
|
-
* openings, milestones, schools, libraries, property, races, conferences, events
|
|
6
|
-
* and the gig marketplaces. They looked like nine separate gaps and they are not:
|
|
7
|
-
* every one is a scout mission with a different kind, so two tools reach all of
|
|
8
|
-
* them, and a radar added next year is readable the day it ships.
|
|
9
|
-
*
|
|
10
|
-
* km_list_radar_scans -> GET /radar/missions
|
|
11
|
-
* km_property_scan -> GET /radar/communities
|
|
12
|
-
* km_list_marketplace_leads -> GET /radar/finds
|
|
13
|
-
* km_list_reviews -> GET /ops/reviews
|
|
14
|
-
* km_list_trips -> GET /ops/trips
|
|
15
|
-
* km_list_catalogs -> GET /ops/catalogs
|
|
16
|
-
*
|
|
17
|
-
* 🚨 READ ONLY. Starting a scan spends the client's money on lead-sourcing
|
|
18
|
-
* providers. That already exists as km_start_lead_scout, which was built with cost
|
|
19
|
-
* disclosure around it, and this family deliberately adds no second quieter way
|
|
20
|
-
* to spend.
|
|
21
|
-
*
|
|
22
|
-
* The family contract this file follows is documented in ./README.md.
|
|
23
|
-
*/
|
|
24
|
-
import { z } from 'zod';
|
|
25
|
-
|
|
26
|
-
export const FAMILY = 'radar';
|
|
27
|
-
|
|
28
|
-
export const TOOLS = [
|
|
29
|
-
'km_list_radar_scans',
|
|
30
|
-
'km_property_scan',
|
|
31
|
-
'km_list_marketplace_leads',
|
|
32
|
-
'km_list_reviews',
|
|
33
|
-
'km_list_trips',
|
|
34
|
-
'km_list_catalogs',
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
export const PROFILES = ['outreach', 'money'];
|
|
38
|
-
|
|
39
|
-
function routeMissing(r) {
|
|
40
|
-
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const NOT_SUPPORTED =
|
|
44
|
-
'Your KM Hub does not expose this surface to the terminal yet. That is not a fault: the workspace is fine and ' +
|
|
45
|
-
'every other tool works as normal. It is there in the web app at https://hub.kivimedia.co.';
|
|
46
|
-
|
|
47
|
-
export function register(server, call, { out, text, qs }) {
|
|
48
|
-
server.tool(
|
|
49
|
-
'km_list_radar_scans',
|
|
50
|
-
'Every automated scan KM Hub has run looking for work, whatever kind: new business openings, milestones, schools, ' +
|
|
51
|
-
'libraries, property, races, conferences, events and marketplace sweeps. Each returns its kind, phase, progress, ' +
|
|
52
|
-
'a summary of what it found and any error. ' +
|
|
53
|
-
'Use it when the user asks whether the lead finding is working, why no new leads have appeared, or what the ' +
|
|
54
|
-
'radars are doing. ' +
|
|
55
|
-
'🚨 `failed` is the number to lead with. A radar that quietly stopped is a lead SOURCE that stopped, and nothing ' +
|
|
56
|
-
'else in the workspace will ever mention it: the pipeline just gets thinner for reasons nobody connects to a ' +
|
|
57
|
-
'broken scan weeks earlier. ' +
|
|
58
|
-
'Full result blobs are summarised rather than returned whole. Starting a scan costs real money and is done with ' +
|
|
59
|
-
'km_start_lead_scout or in KM Hub, never here. Read only.',
|
|
60
|
-
{
|
|
61
|
-
kind: z.string().optional().describe('Narrow to one radar type, for example the job_type a previous call returned.'),
|
|
62
|
-
status: z.string().optional().describe('Narrow to one state, for example running, failed or completed.'),
|
|
63
|
-
limit: z.number().int().min(1).max(150).optional().describe('How many scans, newest first. Default 30.'),
|
|
64
|
-
},
|
|
65
|
-
async ({ kind, status, limit }) => {
|
|
66
|
-
const r = await call('GET', `/radar/missions${qs({ kind, status, limit })}`);
|
|
67
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
68
|
-
return out(r);
|
|
69
|
-
},
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
server.tool(
|
|
73
|
-
'km_property_scan',
|
|
74
|
-
'The residential communities and owner associations Property Radar has on record for one market: apartment ' +
|
|
75
|
-
'communities, homeowner and property owner associations (HOA and POA), condo associations and 55+ active adult ' +
|
|
76
|
-
'communities. Each returns its size, tier, management company, any resident events already evidenced, and how ' +
|
|
77
|
-
'many reachable STAFF contacts it has. ' +
|
|
78
|
-
'Two ways in, because the two buyers are filed differently: give a `city` for apartment communities, or a ' +
|
|
79
|
-
'`county` for associations, which is how the state registries publish them. ' +
|
|
80
|
-
'🚨 `staff_contacts` is the number that decides whether a row is actionable, not the contact field. An ' +
|
|
81
|
-
'association is run by paid staff (a lifestyle director, an activities director, a community manager) and by ' +
|
|
82
|
-
'unpaid volunteers on its board. Only the staff are a lawful outreach target: a board member\'s address is a ' +
|
|
83
|
-
'personal one, it changes at every annual meeting, and several states forbid the association from releasing it ' +
|
|
84
|
-
'even to a fellow owner. A row with zero staff contacts needs enrichment before anyone writes to it. ' +
|
|
85
|
-
'🚨 Timing is the other thing to say out loud. Around nine in ten associations run a calendar fiscal year: the ' +
|
|
86
|
-
'events budget is drafted from late July, bids close through October and the board votes in November. A pitch ' +
|
|
87
|
-
'in August is inside the number being written; the same pitch in February is arguing with one already fixed for ' +
|
|
88
|
-
'twelve months. ' +
|
|
89
|
-
'Starting a sweep costs real money and is done with km_start_lead_scout or in KM Hub, never here. Read only.',
|
|
90
|
-
{
|
|
91
|
-
state: z.string().describe('Two-letter US state code, like TX or VA.'),
|
|
92
|
-
city: z.string().optional().describe('For apartment communities. Case insensitive.'),
|
|
93
|
-
county: z.string().optional().describe('For homeowner and property owner associations, which are filed by county. Name only, without the word County.'),
|
|
94
|
-
assoc_kind: z.string().optional().describe('Narrow to one kind: hoa, poa, condo_assoc, master_assoc, sub_assoc, or any_association for all of them.'),
|
|
95
|
-
limit: z.number().int().min(1).max(200).optional().describe('How many communities. Default 40.'),
|
|
96
|
-
},
|
|
97
|
-
async ({ state, city, county, assoc_kind, limit }) => {
|
|
98
|
-
const r = await call('GET', `/radar/communities${qs({ state, city, county, assoc_kind, limit })}`);
|
|
99
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
100
|
-
return out(r);
|
|
101
|
-
},
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
server.tool(
|
|
105
|
-
'km_list_marketplace_leads',
|
|
106
|
-
'Leads swept from the gig marketplaces this business sells on, each with its portal, the event, a fit score, a ' +
|
|
107
|
-
'margin verdict and whether anybody turned it into a deal or skipped it. ' +
|
|
108
|
-
'🚨 `unactioned` is the important number: leads that are neither a deal nor skipped. On gig portals speed decides ' +
|
|
109
|
-
'who gets the work, so an unactioned lead is almost always a LOST one rather than a waiting one, and saying that ' +
|
|
110
|
-
'plainly is more useful than listing them neutrally. ' +
|
|
111
|
-
'`skip_reason` is worth reading in bulk: the same reason recurring usually means the sweep is aimed slightly wrong. ' +
|
|
112
|
-
'Replying happens in KM Hub. Read only.',
|
|
113
|
-
{
|
|
114
|
-
limit: z.number().int().min(1).max(200).optional().describe('How many leads, newest first. Default 40.'),
|
|
115
|
-
},
|
|
116
|
-
async ({ limit }) => {
|
|
117
|
-
const r = await call('GET', `/radar/finds${qs({ limit })}`);
|
|
118
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
119
|
-
return out(r);
|
|
120
|
-
},
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
server.tool(
|
|
124
|
-
'km_list_reviews',
|
|
125
|
-
'Reviews this business has received and requested, with platform, rating, the text, and whether each was replied to. ' +
|
|
126
|
-
'Use it when the user asks about their reputation, what clients are saying, or before any play that leans on ' +
|
|
127
|
-
'social proof. ' +
|
|
128
|
-
'🚨 Lead with `unreplied`. An unanswered review, good or bad, is read by every future buyer who finds it, and a ' +
|
|
129
|
-
'reply is the cheapest reputation work available. `requested_but_not_received` is the other useful number: those ' +
|
|
130
|
-
'are asks that went nowhere. Requesting and replying happen in KM Hub. Read only.',
|
|
131
|
-
{
|
|
132
|
-
limit: z.number().int().min(1).max(200).optional().describe('How many reviews, newest first. Default 40.'),
|
|
133
|
-
},
|
|
134
|
-
async ({ limit }) => {
|
|
135
|
-
const r = await call('GET', `/ops/reviews${qs({ limit })}`);
|
|
136
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
137
|
-
return out(r);
|
|
138
|
-
},
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
server.tool(
|
|
142
|
-
'km_list_trips',
|
|
143
|
-
'Mileage logged in a window, with distance and what is reimbursable. Defaults to the last 90 days. ' +
|
|
144
|
-
'Use it when the user asks about travel, vehicle costs or expenses tied to getting to jobs. ' +
|
|
145
|
-
'Reimbursement is in CENTS. Unclaimed mileage is money the business is entitled to and routinely does not take, ' +
|
|
146
|
-
'so a large reimbursable total that never appears in the accounts is worth mentioning. Read only.',
|
|
147
|
-
{
|
|
148
|
-
from: z.string().optional().describe('Start date, YYYY-MM-DD. Default 90 days ago.'),
|
|
149
|
-
to: z.string().optional().describe('End date, YYYY-MM-DD. Default tomorrow.'),
|
|
150
|
-
limit: z.number().int().min(1).max(500).optional().describe('How many trips. Default 100. Check `truncated` before trusting a total.'),
|
|
151
|
-
},
|
|
152
|
-
async ({ from, to, limit }) => {
|
|
153
|
-
const r = await call('GET', `/ops/trips${qs({ from, to, limit })}`);
|
|
154
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
155
|
-
return out(r);
|
|
156
|
-
},
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
server.tool(
|
|
160
|
-
'km_list_catalogs',
|
|
161
|
-
'The booking catalogues that decide what a buyer is offered on the public booking pages, which is default, and ' +
|
|
162
|
-
'which customer types each is aimed at. ' +
|
|
163
|
-
'Use it before describing what this business sells publicly, because the catalogue and the internal price list ' +
|
|
164
|
-
'are not the same thing and a buyer only ever sees the catalogue. Prices come from km_get_offers_and_pricing, ' +
|
|
165
|
-
'never from here. Editing happens in KM Hub. Read only.',
|
|
166
|
-
{},
|
|
167
|
-
async () => {
|
|
168
|
-
const r = await call('GET', '/ops/catalogs');
|
|
169
|
-
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
170
|
-
return out(r);
|
|
171
|
-
},
|
|
172
|
-
);
|
|
173
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: radar - the scanners that go looking for work, plus the tail.
|
|
3
|
+
*
|
|
4
|
+
* The last of the parity sweep. KM Hub runs a family of radars: new business
|
|
5
|
+
* openings, milestones, schools, libraries, property, races, conferences, events
|
|
6
|
+
* and the gig marketplaces. They looked like nine separate gaps and they are not:
|
|
7
|
+
* every one is a scout mission with a different kind, so two tools reach all of
|
|
8
|
+
* them, and a radar added next year is readable the day it ships.
|
|
9
|
+
*
|
|
10
|
+
* km_list_radar_scans -> GET /radar/missions
|
|
11
|
+
* km_property_scan -> GET /radar/communities
|
|
12
|
+
* km_list_marketplace_leads -> GET /radar/finds
|
|
13
|
+
* km_list_reviews -> GET /ops/reviews
|
|
14
|
+
* km_list_trips -> GET /ops/trips
|
|
15
|
+
* km_list_catalogs -> GET /ops/catalogs
|
|
16
|
+
*
|
|
17
|
+
* 🚨 READ ONLY. Starting a scan spends the client's money on lead-sourcing
|
|
18
|
+
* providers. That already exists as km_start_lead_scout, which was built with cost
|
|
19
|
+
* disclosure around it, and this family deliberately adds no second quieter way
|
|
20
|
+
* to spend.
|
|
21
|
+
*
|
|
22
|
+
* The family contract this file follows is documented in ./README.md.
|
|
23
|
+
*/
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
|
|
26
|
+
export const FAMILY = 'radar';
|
|
27
|
+
|
|
28
|
+
export const TOOLS = [
|
|
29
|
+
'km_list_radar_scans',
|
|
30
|
+
'km_property_scan',
|
|
31
|
+
'km_list_marketplace_leads',
|
|
32
|
+
'km_list_reviews',
|
|
33
|
+
'km_list_trips',
|
|
34
|
+
'km_list_catalogs',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const PROFILES = ['outreach', 'money'];
|
|
38
|
+
|
|
39
|
+
function routeMissing(r) {
|
|
40
|
+
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const NOT_SUPPORTED =
|
|
44
|
+
'Your KM Hub does not expose this surface to the terminal yet. That is not a fault: the workspace is fine and ' +
|
|
45
|
+
'every other tool works as normal. It is there in the web app at https://hub.kivimedia.co.';
|
|
46
|
+
|
|
47
|
+
export function register(server, call, { out, text, qs }) {
|
|
48
|
+
server.tool(
|
|
49
|
+
'km_list_radar_scans',
|
|
50
|
+
'Every automated scan KM Hub has run looking for work, whatever kind: new business openings, milestones, schools, ' +
|
|
51
|
+
'libraries, property, races, conferences, events and marketplace sweeps. Each returns its kind, phase, progress, ' +
|
|
52
|
+
'a summary of what it found and any error. ' +
|
|
53
|
+
'Use it when the user asks whether the lead finding is working, why no new leads have appeared, or what the ' +
|
|
54
|
+
'radars are doing. ' +
|
|
55
|
+
'🚨 `failed` is the number to lead with. A radar that quietly stopped is a lead SOURCE that stopped, and nothing ' +
|
|
56
|
+
'else in the workspace will ever mention it: the pipeline just gets thinner for reasons nobody connects to a ' +
|
|
57
|
+
'broken scan weeks earlier. ' +
|
|
58
|
+
'Full result blobs are summarised rather than returned whole. Starting a scan costs real money and is done with ' +
|
|
59
|
+
'km_start_lead_scout or in KM Hub, never here. Read only.',
|
|
60
|
+
{
|
|
61
|
+
kind: z.string().optional().describe('Narrow to one radar type, for example the job_type a previous call returned.'),
|
|
62
|
+
status: z.string().optional().describe('Narrow to one state, for example running, failed or completed.'),
|
|
63
|
+
limit: z.number().int().min(1).max(150).optional().describe('How many scans, newest first. Default 30.'),
|
|
64
|
+
},
|
|
65
|
+
async ({ kind, status, limit }) => {
|
|
66
|
+
const r = await call('GET', `/radar/missions${qs({ kind, status, limit })}`);
|
|
67
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
68
|
+
return out(r);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
server.tool(
|
|
73
|
+
'km_property_scan',
|
|
74
|
+
'The residential communities and owner associations Property Radar has on record for one market: apartment ' +
|
|
75
|
+
'communities, homeowner and property owner associations (HOA and POA), condo associations and 55+ active adult ' +
|
|
76
|
+
'communities. Each returns its size, tier, management company, any resident events already evidenced, and how ' +
|
|
77
|
+
'many reachable STAFF contacts it has. ' +
|
|
78
|
+
'Two ways in, because the two buyers are filed differently: give a `city` for apartment communities, or a ' +
|
|
79
|
+
'`county` for associations, which is how the state registries publish them. ' +
|
|
80
|
+
'🚨 `staff_contacts` is the number that decides whether a row is actionable, not the contact field. An ' +
|
|
81
|
+
'association is run by paid staff (a lifestyle director, an activities director, a community manager) and by ' +
|
|
82
|
+
'unpaid volunteers on its board. Only the staff are a lawful outreach target: a board member\'s address is a ' +
|
|
83
|
+
'personal one, it changes at every annual meeting, and several states forbid the association from releasing it ' +
|
|
84
|
+
'even to a fellow owner. A row with zero staff contacts needs enrichment before anyone writes to it. ' +
|
|
85
|
+
'🚨 Timing is the other thing to say out loud. Around nine in ten associations run a calendar fiscal year: the ' +
|
|
86
|
+
'events budget is drafted from late July, bids close through October and the board votes in November. A pitch ' +
|
|
87
|
+
'in August is inside the number being written; the same pitch in February is arguing with one already fixed for ' +
|
|
88
|
+
'twelve months. ' +
|
|
89
|
+
'Starting a sweep costs real money and is done with km_start_lead_scout or in KM Hub, never here. Read only.',
|
|
90
|
+
{
|
|
91
|
+
state: z.string().describe('Two-letter US state code, like TX or VA.'),
|
|
92
|
+
city: z.string().optional().describe('For apartment communities. Case insensitive.'),
|
|
93
|
+
county: z.string().optional().describe('For homeowner and property owner associations, which are filed by county. Name only, without the word County.'),
|
|
94
|
+
assoc_kind: z.string().optional().describe('Narrow to one kind: hoa, poa, condo_assoc, master_assoc, sub_assoc, or any_association for all of them.'),
|
|
95
|
+
limit: z.number().int().min(1).max(200).optional().describe('How many communities. Default 40.'),
|
|
96
|
+
},
|
|
97
|
+
async ({ state, city, county, assoc_kind, limit }) => {
|
|
98
|
+
const r = await call('GET', `/radar/communities${qs({ state, city, county, assoc_kind, limit })}`);
|
|
99
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
100
|
+
return out(r);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
server.tool(
|
|
105
|
+
'km_list_marketplace_leads',
|
|
106
|
+
'Leads swept from the gig marketplaces this business sells on, each with its portal, the event, a fit score, a ' +
|
|
107
|
+
'margin verdict and whether anybody turned it into a deal or skipped it. ' +
|
|
108
|
+
'🚨 `unactioned` is the important number: leads that are neither a deal nor skipped. On gig portals speed decides ' +
|
|
109
|
+
'who gets the work, so an unactioned lead is almost always a LOST one rather than a waiting one, and saying that ' +
|
|
110
|
+
'plainly is more useful than listing them neutrally. ' +
|
|
111
|
+
'`skip_reason` is worth reading in bulk: the same reason recurring usually means the sweep is aimed slightly wrong. ' +
|
|
112
|
+
'Replying happens in KM Hub. Read only.',
|
|
113
|
+
{
|
|
114
|
+
limit: z.number().int().min(1).max(200).optional().describe('How many leads, newest first. Default 40.'),
|
|
115
|
+
},
|
|
116
|
+
async ({ limit }) => {
|
|
117
|
+
const r = await call('GET', `/radar/finds${qs({ limit })}`);
|
|
118
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
119
|
+
return out(r);
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
server.tool(
|
|
124
|
+
'km_list_reviews',
|
|
125
|
+
'Reviews this business has received and requested, with platform, rating, the text, and whether each was replied to. ' +
|
|
126
|
+
'Use it when the user asks about their reputation, what clients are saying, or before any play that leans on ' +
|
|
127
|
+
'social proof. ' +
|
|
128
|
+
'🚨 Lead with `unreplied`. An unanswered review, good or bad, is read by every future buyer who finds it, and a ' +
|
|
129
|
+
'reply is the cheapest reputation work available. `requested_but_not_received` is the other useful number: those ' +
|
|
130
|
+
'are asks that went nowhere. Requesting and replying happen in KM Hub. Read only.',
|
|
131
|
+
{
|
|
132
|
+
limit: z.number().int().min(1).max(200).optional().describe('How many reviews, newest first. Default 40.'),
|
|
133
|
+
},
|
|
134
|
+
async ({ limit }) => {
|
|
135
|
+
const r = await call('GET', `/ops/reviews${qs({ limit })}`);
|
|
136
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
137
|
+
return out(r);
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
server.tool(
|
|
142
|
+
'km_list_trips',
|
|
143
|
+
'Mileage logged in a window, with distance and what is reimbursable. Defaults to the last 90 days. ' +
|
|
144
|
+
'Use it when the user asks about travel, vehicle costs or expenses tied to getting to jobs. ' +
|
|
145
|
+
'Reimbursement is in CENTS. Unclaimed mileage is money the business is entitled to and routinely does not take, ' +
|
|
146
|
+
'so a large reimbursable total that never appears in the accounts is worth mentioning. Read only.',
|
|
147
|
+
{
|
|
148
|
+
from: z.string().optional().describe('Start date, YYYY-MM-DD. Default 90 days ago.'),
|
|
149
|
+
to: z.string().optional().describe('End date, YYYY-MM-DD. Default tomorrow.'),
|
|
150
|
+
limit: z.number().int().min(1).max(500).optional().describe('How many trips. Default 100. Check `truncated` before trusting a total.'),
|
|
151
|
+
},
|
|
152
|
+
async ({ from, to, limit }) => {
|
|
153
|
+
const r = await call('GET', `/ops/trips${qs({ from, to, limit })}`);
|
|
154
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
155
|
+
return out(r);
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
server.tool(
|
|
160
|
+
'km_list_catalogs',
|
|
161
|
+
'The booking catalogues that decide what a buyer is offered on the public booking pages, which is default, and ' +
|
|
162
|
+
'which customer types each is aimed at. ' +
|
|
163
|
+
'Use it before describing what this business sells publicly, because the catalogue and the internal price list ' +
|
|
164
|
+
'are not the same thing and a buyer only ever sees the catalogue. Prices come from km_get_offers_and_pricing, ' +
|
|
165
|
+
'never from here. Editing happens in KM Hub. Read only.',
|
|
166
|
+
{},
|
|
167
|
+
async () => {
|
|
168
|
+
const r = await call('GET', '/ops/catalogs');
|
|
169
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
170
|
+
return out(r);
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
}
|