@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.
- package/README.md +6 -5
- package/bin/kmhub.mjs +20 -7
- package/coach-book-output-guard.mjs +760 -0
- package/index.mjs +2 -0
- package/package.json +8 -3
- package/prompts/briefing.md +29 -0
- package/prompts/luxury.md +70 -0
- package/prompts/play.md +49 -0
- package/prompts/run.md +36 -0
- package/prompts/setup.md +33 -0
- package/prompts/vs-booked.md +46 -0
- package/prompts/what-can-you-do.md +40 -0
- package/prompts.mjs +110 -0
- package/read-only-tools.json +142 -0
- package/remote.mjs +815 -99
- package/tools/balloon-costing.mjs +80 -0
- package/tools/booking-equipment.mjs +110 -0
- package/tools/bridges.mjs +54 -0
- package/tools/calendar.mjs +9 -0
- package/tools/capabilities.mjs +155 -0
- package/tools/catalog.mjs +288 -0
- package/tools/clubs.mjs +176 -0
- package/tools/coach.mjs +771 -0
- package/tools/compare.mjs +76 -0
- package/tools/core.mjs +21 -0
- package/tools/crm.mjs +12 -3
- package/tools/dubsado.mjs +137 -0
- package/tools/exports.mjs +128 -0
- package/tools/fact-review.mjs +125 -0
- package/tools/flows.mjs +261 -0
- package/tools/forms.mjs +158 -0
- package/tools/gols.mjs +134 -0
- package/tools/hr.mjs +162 -0
- package/tools/knowledge.mjs +4 -3
- package/tools/marketing.mjs +396 -0
- package/tools/meta.mjs +2 -2
- package/tools/military.mjs +244 -0
- package/tools/outreach.mjs +27 -4
- package/tools/pending.mjs +122 -0
- package/tools/photos.mjs +140 -0
- package/tools/plays.mjs +1 -1
- package/tools/profile.mjs +118 -0
- package/tools/radar.mjs +173 -0
- package/tools/recurring-invoices.mjs +149 -0
- package/tools/reengage.mjs +434 -0
- package/tools/schedules.mjs +55 -0
- package/tools/setup.mjs +168 -0
- package/tools/sops-bridges.mjs +86 -0
- package/tools/sops.mjs +314 -0
- package/tools/sourcing.mjs +50 -2
- package/tools/strategy.mjs +146 -0
- package/tools/studio.mjs +132 -0
- package/tools/venueradar.mjs +151 -0
- package/tools/voice.mjs +134 -0
- package/tools.mjs +70 -12
package/tools/sourcing.mjs
CHANGED
|
@@ -95,6 +95,18 @@ export function register(server, call, { out, text, qs }) {
|
|
|
95
95
|
keywords: z.array(z.string()).optional().describe('Extra words that should show up on the person or the business.'),
|
|
96
96
|
company_sizes: z.array(z.string()).optional().describe('Company head-count bands, only if the person asked for a size. For example ["1,10", "11,50"].'),
|
|
97
97
|
seniorities: z.array(z.string()).optional().describe('Seniority levels, only if the person asked. For example ["owner", "founder", "director"].'),
|
|
98
|
+
avoid_keywords: z
|
|
99
|
+
.array(z.string())
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Words that disqualify a lead, matched whole-word against the company name, job title and snippet. For example ["ticketing", "parking", "dean"]. This narrows a search, it cannot be the only filter.'),
|
|
102
|
+
keep_keywords: z
|
|
103
|
+
.array(z.string())
|
|
104
|
+
.optional()
|
|
105
|
+
.describe('Words that rescue a lead from an avoid word, but only for the avoid words listed in rescuable_avoid_keywords. For example ["events", "homecoming", "graduation"].'),
|
|
106
|
+
rescuable_avoid_keywords: z
|
|
107
|
+
.array(z.string())
|
|
108
|
+
.optional()
|
|
109
|
+
.describe('Which avoid_keywords a keep_keyword is allowed to override. Use for avoid words that name a DEPARTMENT rather than a job, since those also catch that department\'s event desk - who is usually the buyer. For example ["director of athletics", "chancellor"]. Leave out to make every avoid word absolute.'),
|
|
98
110
|
outreach_context: z
|
|
99
111
|
.string()
|
|
100
112
|
.optional()
|
|
@@ -112,6 +124,15 @@ export function register(server, call, { out, text, qs }) {
|
|
|
112
124
|
.boolean()
|
|
113
125
|
.optional()
|
|
114
126
|
.describe('Leave this out on the first call: you get the price back and nothing is charged. Set it to true ONLY after the person has been told the price and has agreed to it.'),
|
|
127
|
+
confirm_token: z
|
|
128
|
+
.string()
|
|
129
|
+
.optional()
|
|
130
|
+
.describe(
|
|
131
|
+
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
132
|
+
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
133
|
+
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
134
|
+
+ 'arguments. A yes for one action never authorises a different one.',
|
|
135
|
+
),
|
|
115
136
|
},
|
|
116
137
|
async (args) => {
|
|
117
138
|
const r = await call('POST', '/sourcing/missions', args);
|
|
@@ -157,11 +178,20 @@ export function register(server, call, { out, text, qs }) {
|
|
|
157
178
|
'Stop a lead scout that is queued or already running. Reach for it the moment someone says stop, cancel, that is enough, or realises the search was set up wrong. Stopping is the money-saving move and is always safe: the workspace is only charged for the lookups already made, never for the work the scout did not do. Anything it already found stays in the pipeline, and any message it already wrote stays as a draft in the Approval Queue. It cannot stop a scout that has already finished, and it will say so plainly rather than pretending. It does not delete anything.',
|
|
158
179
|
{
|
|
159
180
|
mission_id: z.string().describe('The id of the scout to stop, from km_list_lead_scouts or from when it was started.'),
|
|
181
|
+
confirm_token: z
|
|
182
|
+
.string()
|
|
183
|
+
.optional()
|
|
184
|
+
.describe(
|
|
185
|
+
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
186
|
+
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
187
|
+
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
188
|
+
+ 'arguments. A yes for one action never authorises a different one.',
|
|
189
|
+
),
|
|
160
190
|
},
|
|
161
|
-
async ({ mission_id }) => {
|
|
191
|
+
async ({ mission_id, confirm_token }) => {
|
|
162
192
|
const id = String(mission_id || '').trim();
|
|
163
193
|
if (!id) return text('I need the id of the scout to stop it. km_list_lead_scouts will show you which ones are still running.');
|
|
164
|
-
const r = await call('POST', `/sourcing/missions/${encodeURIComponent(id)}/cancel`, {});
|
|
194
|
+
const r = await call('POST', `/sourcing/missions/${encodeURIComponent(id)}/cancel`, { confirm_token });
|
|
165
195
|
if (routeMissing(r)) return text(NOT_SUPPORTED);
|
|
166
196
|
return out(r);
|
|
167
197
|
},
|
|
@@ -186,6 +216,15 @@ export function register(server, call, { out, text, qs }) {
|
|
|
186
216
|
.boolean()
|
|
187
217
|
.optional()
|
|
188
218
|
.describe('Leave this out on the first call: you get the price back and nothing is charged. Set it to true ONLY after the person has agreed to the cost.'),
|
|
219
|
+
confirm_token: z
|
|
220
|
+
.string()
|
|
221
|
+
.optional()
|
|
222
|
+
.describe(
|
|
223
|
+
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
224
|
+
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
225
|
+
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
226
|
+
+ 'arguments. A yes for one action never authorises a different one.',
|
|
227
|
+
),
|
|
189
228
|
},
|
|
190
229
|
async (args) => {
|
|
191
230
|
const r = await call('POST', '/sourcing/company-contacts', args);
|
|
@@ -210,6 +249,15 @@ export function register(server, call, { out, text, qs }) {
|
|
|
210
249
|
.boolean()
|
|
211
250
|
.optional()
|
|
212
251
|
.describe('Leave this out to see the price first without being charged. Set it to true to actually run the check.'),
|
|
252
|
+
confirm_token: z
|
|
253
|
+
.string()
|
|
254
|
+
.optional()
|
|
255
|
+
.describe(
|
|
256
|
+
'Leave this out on the first call. This action needs an explicit yes from the person you are working with: '
|
|
257
|
+
+ 'KM Hub answers 409 with a plain description of exactly what would happen, plus a token. Show them that '
|
|
258
|
+
+ 'description in those words, wait for a real answer, and only then call again with the token and the SAME '
|
|
259
|
+
+ 'arguments. A yes for one action never authorises a different one.',
|
|
260
|
+
),
|
|
213
261
|
},
|
|
214
262
|
async (args) => {
|
|
215
263
|
const r = await call('POST', '/sourcing/verify-email', args);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: strategy - the officers, what they proposed, and what was done.
|
|
3
|
+
*
|
|
4
|
+
* Workstream B6. Strategy was 2 of 8: the pillar that makes "a COO and beyond"
|
|
5
|
+
* true rather than aspirational had almost no terminal reach. A connector that
|
|
6
|
+
* reads the pipeline but cannot see the decisions waiting on the owner, or what was
|
|
7
|
+
* already done in their name, is a reporting tool rather than an operator.
|
|
8
|
+
*
|
|
9
|
+
* km_list_decisions -> GET /strategy/decisions
|
|
10
|
+
* km_list_ai_officers -> GET /strategy/officers
|
|
11
|
+
* km_list_recommendations -> GET /strategy/advice
|
|
12
|
+
* km_list_ai_activity -> GET /strategy/activity
|
|
13
|
+
* km_list_weekly_interviews-> GET /strategy/interviews
|
|
14
|
+
*
|
|
15
|
+
* 🚨 READ ONLY, and approval especially so. Approving a COO proposal executes real
|
|
16
|
+
* work against real records: it is the most consequential button in the product and
|
|
17
|
+
* it deserves its own decision when B8 settles write scope, not to inherit a general
|
|
18
|
+
* one. Never imply you can approve something.
|
|
19
|
+
*
|
|
20
|
+
* The family contract this file follows is documented in ./README.md.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
|
|
24
|
+
export const FAMILY = 'strategy';
|
|
25
|
+
|
|
26
|
+
export const TOOLS = [
|
|
27
|
+
'km_list_decisions',
|
|
28
|
+
'km_list_ai_officers',
|
|
29
|
+
'km_list_recommendations',
|
|
30
|
+
'km_list_ai_activity',
|
|
31
|
+
'km_list_weekly_interviews',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export const PROFILES = ['*'];
|
|
35
|
+
|
|
36
|
+
function routeMissing(r) {
|
|
37
|
+
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const NOT_SUPPORTED =
|
|
41
|
+
'Your KM Hub does not expose the AI Team surface to the terminal yet. That is not a fault: the workspace is fine ' +
|
|
42
|
+
'and every other tool works as normal. The officers, decisions and activity log are all there in the web app at ' +
|
|
43
|
+
'https://hub.kivimedia.co, and this connector will start reading them once your KM Hub is on a build that publishes them.';
|
|
44
|
+
|
|
45
|
+
export function register(server, call, { out, text, qs }) {
|
|
46
|
+
server.tool(
|
|
47
|
+
'km_list_decisions',
|
|
48
|
+
'The proposals the COO has put up for a human decision, what each one would actually DO, and how long it has been ' +
|
|
49
|
+
'waiting. ' +
|
|
50
|
+
'Call this whenever the user asks what needs them, what is outstanding, or what the AI has suggested, and any time ' +
|
|
51
|
+
'you are summarising the state of the business. A decision sitting unmade is different from a task not done: ' +
|
|
52
|
+
'nothing moves until somebody says yes, and the workspace will not chase them for it. ' +
|
|
53
|
+
'🚨 Lead with `waiting_on_you` and `oldest_waiting_days`. A proposal that has been pending for weeks is usually one ' +
|
|
54
|
+
'the owner never saw rather than one they decided against, and saying the number out loud is the whole value here. ' +
|
|
55
|
+
'Read `risk` and `what_it_would_do` before describing anything: they are not all equivalent, and a high-risk ' +
|
|
56
|
+
'proposal deserves to be presented as one. ' +
|
|
57
|
+
'Nothing here can approve or reject. That happens in KM Hub, deliberately, because approval executes real work. ' +
|
|
58
|
+
'Read only.',
|
|
59
|
+
{
|
|
60
|
+
status: z
|
|
61
|
+
.enum(['pending', 'approved', 'rejected', 'executed', 'failed', 'expired'])
|
|
62
|
+
.optional()
|
|
63
|
+
.describe('Narrow to one state. "pending" is what is waiting on a person, which is usually the real question.'),
|
|
64
|
+
limit: z.number().int().min(1).max(100).optional().describe('How many, newest first. Default 30.'),
|
|
65
|
+
},
|
|
66
|
+
async ({ status, limit }) => {
|
|
67
|
+
const r = await call('GET', `/strategy/decisions${qs({ status, limit })}`);
|
|
68
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
69
|
+
return out(r);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
server.tool(
|
|
74
|
+
'km_list_ai_officers',
|
|
75
|
+
'Which AI officers this workspace has customised: their key, the name the owner gave them and the personality they ' +
|
|
76
|
+
'were given. Use it when the user refers to an officer by name and you do not recognise it, or asks who is on ' +
|
|
77
|
+
'their AI team. An empty list is normal and means they are running the defaults, not that something is missing. ' +
|
|
78
|
+
'Useful mainly so you address an officer the way the workspace does rather than inventing your own label. Read only.',
|
|
79
|
+
{},
|
|
80
|
+
async () => {
|
|
81
|
+
const r = await call('GET', '/strategy/officers');
|
|
82
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
83
|
+
return out(r);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
server.tool(
|
|
88
|
+
'km_list_recommendations',
|
|
89
|
+
'Standing recommendations and insights the workspace has generated for itself, each with its category, impact and ' +
|
|
90
|
+
'effort where they were recorded. ' +
|
|
91
|
+
'Use it when the user asks what they should be doing, or before you offer advice of your own: the workspace may ' +
|
|
92
|
+
'already have said the same thing, and repeating it as though it were new is worse than agreeing with it. ' +
|
|
93
|
+
'🚨 These are suggestions, not instructions, and some will be stale. Weigh them against what the owner has actually ' +
|
|
94
|
+
'said they want, and check the date before presenting one as current. Read only.',
|
|
95
|
+
{
|
|
96
|
+
limit: z.number().int().min(1).max(100).optional().describe('How many of each, newest or highest priority first. Default 25.'),
|
|
97
|
+
},
|
|
98
|
+
async ({ limit }) => {
|
|
99
|
+
const r = await call('GET', `/strategy/advice${qs({ limit })}`);
|
|
100
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
101
|
+
return out(r);
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
server.tool(
|
|
106
|
+
'km_list_ai_activity',
|
|
107
|
+
'What has actually been done in this workspace and by which surface: the app, a worker, a cron, the API or this ' +
|
|
108
|
+
'terminal. Each entry names the action, the record it touched, and whether it was later UNDONE. ' +
|
|
109
|
+
'Use it when the user asks what changed, who did something, or whether the AI has been acting on its own. It is ' +
|
|
110
|
+
'also the right call before you claim nothing has happened: a quiet inbox does not mean a quiet workspace. ' +
|
|
111
|
+
'🚨 `undone_count` is the honest number and it must not be skipped to look competent. A pattern of undos says ' +
|
|
112
|
+
'something real about how far a particular automation should be trusted, and the owner is better served knowing it. ' +
|
|
113
|
+
'Undo itself lives in KM Hub. Read only.',
|
|
114
|
+
{
|
|
115
|
+
source: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('Narrow to one surface, for example app, worker, cron, api or desk. Leave it out for everything.'),
|
|
119
|
+
limit: z.number().int().min(1).max(200).optional().describe('How many entries, newest first. Default 40.'),
|
|
120
|
+
},
|
|
121
|
+
async ({ source, limit }) => {
|
|
122
|
+
const r = await call('GET', `/strategy/activity${qs({ source, limit })}`);
|
|
123
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
124
|
+
return out(r);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
server.tool(
|
|
129
|
+
'km_list_weekly_interviews',
|
|
130
|
+
'The weekly interview sessions: which week, who, what topics were covered, the digest of what the session concluded ' +
|
|
131
|
+
'and what it wants to ask about next. ' +
|
|
132
|
+
'Use it when the user mentions their weekly check-in or sit-down, or when you want to know what has recently been ' +
|
|
133
|
+
'learned about how this business is running. The digests are one of the richest sources of context in the ' +
|
|
134
|
+
'workspace and almost nothing else reads them. ' +
|
|
135
|
+
'Full transcripts are deliberately not returned: they are long, and the digest is the part that carries the ' +
|
|
136
|
+
'conclusion. Read only.',
|
|
137
|
+
{
|
|
138
|
+
limit: z.number().int().min(1).max(60).optional().describe('How many sessions, most recent week first. Default 15.'),
|
|
139
|
+
},
|
|
140
|
+
async ({ limit }) => {
|
|
141
|
+
const r = await call('GET', `/strategy/interviews${qs({ limit })}`);
|
|
142
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
143
|
+
return out(r);
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
}
|
package/tools/studio.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: studio - what the creation surfaces have actually produced.
|
|
3
|
+
*
|
|
4
|
+
* The last seven pages. I argued these could not be closed by reading, on the
|
|
5
|
+
* grounds that a studio is a place you make things rather than a place with state.
|
|
6
|
+
* That was wrong. A studio has OUTPUT, and the output is exactly what an owner
|
|
7
|
+
* wants to know about: what has been made, what is waiting on them, what is
|
|
8
|
+
* scheduled to go out.
|
|
9
|
+
*
|
|
10
|
+
* km_list_design_assets -> GET /studio/assets
|
|
11
|
+
* km_list_galleries -> GET /studio/galleries
|
|
12
|
+
* km_list_linkedin_content -> GET /studio/linkedin
|
|
13
|
+
* km_list_balloon_work -> GET /studio/balloon
|
|
14
|
+
* km_review_queue -> GET /studio/review
|
|
15
|
+
*
|
|
16
|
+
* 🚨 READ ONLY. Generating an image or a post spends money with a provider, and
|
|
17
|
+
* approving one publishes in the client's name.
|
|
18
|
+
*
|
|
19
|
+
* The family contract this file follows is documented in ./README.md.
|
|
20
|
+
*/
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
|
|
23
|
+
export const FAMILY = 'studio';
|
|
24
|
+
|
|
25
|
+
export const TOOLS = [
|
|
26
|
+
'km_list_design_assets',
|
|
27
|
+
'km_list_galleries',
|
|
28
|
+
'km_list_linkedin_content',
|
|
29
|
+
'km_list_balloon_work',
|
|
30
|
+
'km_review_queue',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const PROFILES = ['content', 'outreach'];
|
|
34
|
+
|
|
35
|
+
function routeMissing(r) {
|
|
36
|
+
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const NOT_SUPPORTED =
|
|
40
|
+
'Your KM Hub does not expose the studios to the terminal yet. That is not a fault: the workspace is fine and every ' +
|
|
41
|
+
'other tool works as normal. They are there in the web app at https://hub.kivimedia.co.';
|
|
42
|
+
|
|
43
|
+
export function register(server, call, { out, text, qs }) {
|
|
44
|
+
server.tool(
|
|
45
|
+
'km_review_queue',
|
|
46
|
+
'EVERYTHING across the whole product that is waiting on a human: outreach drafts, SEO posts, LinkedIn posts, COO ' +
|
|
47
|
+
'proposals and proposed business facts, each with a count and how old the oldest one is. ' +
|
|
48
|
+
'Call this in any catch-up, any status question, and before telling somebody they are on top of things. ' +
|
|
49
|
+
'🚨 No single page in KM Hub shows all of this, because every surface owns its own queue. Approval queues are ' +
|
|
50
|
+
'where work goes to die quietly: nothing sends until a person releases it, nothing chases them, and the owner ' +
|
|
51
|
+
'usually finds out when a client asks why they never heard back. ' +
|
|
52
|
+
'🚨 LEAD WITH THE OLDEST, NOT THE LARGEST. Forty drafts written this morning are fine. One draft that has been ' +
|
|
53
|
+
'waiting six weeks is a client who was ignored. Read only.',
|
|
54
|
+
{},
|
|
55
|
+
async () => {
|
|
56
|
+
const r = await call('GET', '/studio/review');
|
|
57
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
58
|
+
return out(r);
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
server.tool(
|
|
63
|
+
'km_list_design_assets',
|
|
64
|
+
'Images and design assets the studios have generated, with the template and provider behind each and a link to the ' +
|
|
65
|
+
'file. ' +
|
|
66
|
+
'Use it when the user asks what has been made, wants something to reuse, or before generating anything new: ' +
|
|
67
|
+
'regenerating something that already exists spends money twice for the same picture. ' +
|
|
68
|
+
'The prompt and inputs are not returned, because they are long and what a person wants is what exists and where. ' +
|
|
69
|
+
'Read only.',
|
|
70
|
+
{
|
|
71
|
+
limit: z.number().int().min(1).max(200).optional().describe('How many assets, newest first. Default 40.'),
|
|
72
|
+
},
|
|
73
|
+
async ({ limit }) => {
|
|
74
|
+
const r = await call('GET', `/studio/assets${qs({ limit })}`);
|
|
75
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
76
|
+
return out(r);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
server.tool(
|
|
81
|
+
'km_list_galleries',
|
|
82
|
+
'The galleries this business publishes, whether each is active, and how many items are in them. ' +
|
|
83
|
+
'🚨 An ACTIVE gallery with no active items is a public page showing nothing, which is worse than not having one ' +
|
|
84
|
+
'at all, and nobody notices because the page still loads. That combination is worth raising unprompted. ' +
|
|
85
|
+
'Use it before advising anything about what buyers see before they enquire. Building a gallery happens in KM Hub. ' +
|
|
86
|
+
'🚨 These are the PUBLIC gallery pages on the website, and they have nothing to do with the photos that ride inside ' +
|
|
87
|
+
'an outreach email. If the question is about pictures in an email, this is the wrong tool and an empty answer here ' +
|
|
88
|
+
'means nothing: use km_list_work_photos and km_attach_draft_photos. ' +
|
|
89
|
+
'Read only.',
|
|
90
|
+
{},
|
|
91
|
+
async () => {
|
|
92
|
+
const r = await call('GET', '/studio/galleries');
|
|
93
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
94
|
+
return out(r);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
server.tool(
|
|
99
|
+
'km_list_linkedin_content',
|
|
100
|
+
'LinkedIn posts the workspace has produced: the hook archetype each uses, its status, when it is scheduled, and a ' +
|
|
101
|
+
'preview of the body. ' +
|
|
102
|
+
'Use it when the user asks about their LinkedIn presence, what is going out, or what is waiting on them. ' +
|
|
103
|
+
'🚨 Read `by_hook`. A workspace using one hook archetype for everything becomes predictable, and predictable is ' +
|
|
104
|
+
'invisible on LinkedIn. That is a more useful observation than the post count. `awaiting_approval` is the other ' +
|
|
105
|
+
'number worth naming: those are written and going nowhere. Publishing happens in KM Hub. Read only.',
|
|
106
|
+
{
|
|
107
|
+
limit: z.number().int().min(1).max(200).optional().describe('How many posts, newest first. Default 40.'),
|
|
108
|
+
},
|
|
109
|
+
async ({ limit }) => {
|
|
110
|
+
const r = await call('GET', `/studio/linkedin${qs({ limit })}`);
|
|
111
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
112
|
+
return out(r);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
server.tool(
|
|
117
|
+
'km_list_balloon_work',
|
|
118
|
+
'Balloon designs and the render runs behind them: the event, the guest count, the status, and whether each design ' +
|
|
119
|
+
'ever reached a proposal. ' +
|
|
120
|
+
'🚨 `designs_on_a_proposal` against the design count is the number that matters. A design that never reached a ' +
|
|
121
|
+
'proposal is work that was done and never sold, and that gap is invisible on any single screen. ' +
|
|
122
|
+
'Rendering costs money with an image provider and happens in KM Hub. Read only.',
|
|
123
|
+
{
|
|
124
|
+
limit: z.number().int().min(1).max(100).optional().describe('How many designs and runs, newest first. Default 30.'),
|
|
125
|
+
},
|
|
126
|
+
async ({ limit }) => {
|
|
127
|
+
const r = await call('GET', `/studio/balloon${qs({ limit })}`);
|
|
128
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
129
|
+
return out(r);
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: venueradar - the Venue Radar directory, read from the terminal.
|
|
3
|
+
*
|
|
4
|
+
* ONE directory for the entertainment-buying venue market, in lanes: bars and restaurants,
|
|
5
|
+
* taprooms and tasting rooms (breweries/wineries), and banquet halls / event venues. The engine
|
|
6
|
+
* builds it from Google Places county tiles, Open Brewery DB and the TTB winery CSV, then crawls
|
|
7
|
+
* each venue's OWN pages for two evidence lanes: proof it already books recurring entertainment
|
|
8
|
+
* (trivia, live music, game night - each signal quoted), and a published preferred-vendor list
|
|
9
|
+
* (the categories it names are stored; a category ABSENT from the venue's own list is the
|
|
10
|
+
* placement pitch). So the thing a person wants from the terminal is "show me what is already in
|
|
11
|
+
* there", filtered by lane, and that needs its own family rather than another mission list entry.
|
|
12
|
+
*
|
|
13
|
+
* km_venue_radar_venues -> GET /venue-radar/venues
|
|
14
|
+
* km_venue_radar_venue -> GET /venue-radar/venues/:ref
|
|
15
|
+
*
|
|
16
|
+
* 🚨 READ ONLY, and there are three separate reasons, all of which the descriptions say out loud:
|
|
17
|
+
*
|
|
18
|
+
* 1. NOTHING HERE SUBMITS, APPROVES OR SENDS. Writing to a venue or dialing it is a human act
|
|
19
|
+
* in KM Hub, behind the Approval Queue - venue_radar drafts are a never-auto source, because
|
|
20
|
+
* a cold first impression to a venue is exactly the message a human must read first.
|
|
21
|
+
* 2. NOTHING HERE STARTS A SWEEP. Queueing one is admin-only under RLS in the web app, and the
|
|
22
|
+
* API runs on the service role, which bypasses RLS: a start tool would quietly hand any
|
|
23
|
+
* read/write key a power the product refuses to a non-admin member. Whether a sweep is
|
|
24
|
+
* running is readable through km_list_radar_scans with kind 'venue_radar_ingest'.
|
|
25
|
+
* 3. Reading costs NOTHING and sends NOTHING. The directory is shared reference data with
|
|
26
|
+
* global read, so there is no first click to make and no bill for looking.
|
|
27
|
+
*
|
|
28
|
+
* The family contract this file follows is documented in ./README.md.
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
|
|
32
|
+
export const FAMILY = 'venueradar';
|
|
33
|
+
|
|
34
|
+
export const TOOLS = [
|
|
35
|
+
'km_venue_radar_venues',
|
|
36
|
+
'km_venue_radar_venue',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Finding and reaching buyers is outreach work, so this family joins that profile. It is not in
|
|
40
|
+
// `core`: a session that only wants orientation should not carry two more schemas.
|
|
41
|
+
export const PROFILES = ['outreach'];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Has this KM Hub simply never heard of the route, or did the route run and find nothing?
|
|
45
|
+
*
|
|
46
|
+
* 🚨 Same discrimination as the military and clubs families, for the same reason: asking for a
|
|
47
|
+
* venue that is not in the directory is a real, ordinary 404. Collapsing the two would answer a
|
|
48
|
+
* plain typo with "your KM Hub does not have the venue directory", which is a false statement
|
|
49
|
+
* about the product rather than a wrong answer about a venue. So only the router's own
|
|
50
|
+
* self-describing 404, which carries the whole route list, counts as a missing route.
|
|
51
|
+
*/
|
|
52
|
+
function routeMissing(r) {
|
|
53
|
+
if (r.status === 405 || r.status === 501) return true;
|
|
54
|
+
if (r.status !== 404) return false;
|
|
55
|
+
const body = r && r.data && typeof r.data === 'object' ? r.data : null;
|
|
56
|
+
if (!body) return true;
|
|
57
|
+
if (Array.isArray(body.routes)) return true;
|
|
58
|
+
return typeof body.message === 'string' && body.message.startsWith('Unknown route');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const NOT_SUPPORTED =
|
|
62
|
+
'This KM Hub does not serve the venue directory to the terminal yet. Nothing is broken: the workspace is fine ' +
|
|
63
|
+
'and every other tool works as normal. The directory is there in the web app at https://hub.kivimedia.co under ' +
|
|
64
|
+
'Outbound, named Venue Radar.';
|
|
65
|
+
|
|
66
|
+
const NO_SEND =
|
|
67
|
+
'What it will NOT do: it cannot write to a venue, dial a phone, mark a lane status or send anything, and no ' +
|
|
68
|
+
'other tool here can either. Working a venue - the ladder to a weekly residency, or the funnel onto its vendor ' +
|
|
69
|
+
'list - happens in KM Hub at https://hub.kivimedia.co, on purpose. Never tell somebody a message went out.';
|
|
70
|
+
|
|
71
|
+
const NO_INVENTION =
|
|
72
|
+
'Never construct an address or a claim. books_entertainment is counted from signals quoted off the venue\'s own ' +
|
|
73
|
+
'pages, a venue that publishes no buyer keeps no buyer, and an email marked guess must verify before it may ' +
|
|
74
|
+
'reach a draft - say so whenever one is shown.';
|
|
75
|
+
|
|
76
|
+
export function register(server, call, { out, text, qs }) {
|
|
77
|
+
server.tool(
|
|
78
|
+
'km_venue_radar_venues',
|
|
79
|
+
'The venues KM Hub has already gathered and evidenced: bars, restaurants, breweries/taprooms, wineries, ' +
|
|
80
|
+
'banquet halls and event venues, each with its two evidence lanes - proof it books recurring entertainment ' +
|
|
81
|
+
'(trivia, live music, game night, each signal quoted from its own pages) and whether it publishes a ' +
|
|
82
|
+
'preferred-vendor list, with the categories that list names. ' +
|
|
83
|
+
'Reach for it whenever somebody asks about bars, restaurants, taprooms, breweries, wineries, banquet halls ' +
|
|
84
|
+
'or event venues as BUYERS, and reach for it FIRST rather than describing the feature: reading it costs ' +
|
|
85
|
+
'nothing, spends nothing and sends nothing. ' +
|
|
86
|
+
'🚨 The lane filter is the point. lane "entertainment" answers "who already pays for a weekly night I could ' +
|
|
87
|
+
'fill"; lane "vendor_list" answers "which halls keep a vendor list my category is missing from" - the ' +
|
|
88
|
+
'categories their own list names come back per venue, so the absent-category pitch is readable straight off ' +
|
|
89
|
+
'the row. Per venue you also get this workspace\'s own working state, the fit score and the already_client ' +
|
|
90
|
+
'flag (computed against this workspace\'s own client list - the "has not booked us yet" filter). ' +
|
|
91
|
+
'A venue with no lane data yet is NORMAL, not missing: the enrich sweep fills evidence venue by venue. ' +
|
|
92
|
+
NO_INVENTION +
|
|
93
|
+
' ' +
|
|
94
|
+
'Whether a sweep is running is a different question, answered by km_list_radar_scans with kind ' +
|
|
95
|
+
"'venue_radar_ingest'. Starting one is done in KM Hub, never from here. " +
|
|
96
|
+
NO_SEND,
|
|
97
|
+
{
|
|
98
|
+
kind: z
|
|
99
|
+
.enum(['bar', 'restaurant', 'brewery', 'winery', 'banquet_hall', 'event_venue'])
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('One venue kind. Leave it out to see all six.'),
|
|
102
|
+
lane: z
|
|
103
|
+
.enum(['entertainment', 'vendor_list'])
|
|
104
|
+
.optional()
|
|
105
|
+
.describe('Evidence lane: "entertainment" = proven recurring entertainment buyer; "vendor_list" = publishes a preferred-vendor list.'),
|
|
106
|
+
county: z.string().optional().describe('County name, matched loosely, for example "Monmouth".'),
|
|
107
|
+
state: z.string().optional().describe('Two-letter US state, for example "NJ".'),
|
|
108
|
+
city: z.string().optional().describe('City name, matched loosely.'),
|
|
109
|
+
q: z.string().optional().describe('Free text against name, place, kind and subtype, in any order.'),
|
|
110
|
+
limit: z.number().int().min(1).max(200).optional().describe('How many venues to return. Default 40. Check `truncated`.'),
|
|
111
|
+
},
|
|
112
|
+
async ({ kind, lane, county, state, city, q, limit }) => {
|
|
113
|
+
const r = await call('GET', `/venue-radar/venues${qs({ kind, lane, county, state, city, q, limit })}`);
|
|
114
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
115
|
+
return out(r);
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
server.tool(
|
|
120
|
+
'km_venue_radar_venue',
|
|
121
|
+
'One venue in full: every entertainment signal with the page it was quoted from, the vendor list with the ' +
|
|
122
|
+
'categories it names, the buyer (name, title, role, published email or labelled guess, with its evidence), ' +
|
|
123
|
+
'and whatever working state this workspace has already recorded on both lanes. ' +
|
|
124
|
+
'Use it after km_venue_radar_venues when somebody picks a venue, or whenever they name one directly. It ' +
|
|
125
|
+
'takes the venue_key from a previous answer (for example "obdb:some-brewery-id") or a Google place id. ' +
|
|
126
|
+
'🚨 An evidence entry marked stale means the quarterly recheck could no longer find that signal on its page ' +
|
|
127
|
+
'- the trail is kept, never deleted, and a stale trivia night is not a live pitch. buyer.email was read ' +
|
|
128
|
+
'verbatim off the page in its source_url; buyer.email_guess must verify before any draft, and any draft ' +
|
|
129
|
+
'against it is refused until it does. ' +
|
|
130
|
+
NO_INVENTION +
|
|
131
|
+
' ' +
|
|
132
|
+
'Read only. Costs nothing, spends nothing. ' +
|
|
133
|
+
NO_SEND,
|
|
134
|
+
{
|
|
135
|
+
venue: z
|
|
136
|
+
.string()
|
|
137
|
+
.describe('The venue_key from a previous answer, for example "obdb:some-brewery-id", or the venue\'s Google place id.'),
|
|
138
|
+
},
|
|
139
|
+
async ({ venue }) => {
|
|
140
|
+
const ref = String(venue || '').trim();
|
|
141
|
+
if (!ref) {
|
|
142
|
+
return text(
|
|
143
|
+
'I need to know which venue. km_venue_radar_venues lists them, and each row carries the venue_key this tool takes.',
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const r = await call('GET', `/venue-radar/venues/${encodeURIComponent(ref)}`);
|
|
147
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
148
|
+
return out(r);
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
}
|
package/tools/voice.mjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool family: voice - telling KM Hub how to write, from the terminal.
|
|
3
|
+
*
|
|
4
|
+
* km_update_brand_voice -> PATCH /knowledge/voice (brand_values, sign_emails_as)
|
|
5
|
+
* km_set_writing_rules -> PUT /knowledge/writing-rules (the house rules)
|
|
6
|
+
* km_set_service_rule -> PUT /knowledge/service-rules (one package's writing rule)
|
|
7
|
+
*
|
|
8
|
+
* WHY THIS EXISTS. km_get_brand_voice and km_get_offers_and_pricing could READ how
|
|
9
|
+
* the business sounds and what it sells, and the owner had no way to change either
|
|
10
|
+
* from a terminal conversation. Ziv, 22-Sep-26: "allow Mark to chat through terminal
|
|
11
|
+
* mode with rules about the services, the voice, anything you write". These are the
|
|
12
|
+
* write halves, in their own family so the read family (knowledge.mjs) is untouched.
|
|
13
|
+
*
|
|
14
|
+
* 🚨 Every tool here is a confirmed write. KM Hub answers 409 with a server-written
|
|
15
|
+
* sentence naming the before and after. Put that sentence to the person, wait for a
|
|
16
|
+
* real yes, then call again with the confirm_token and the SAME arguments.
|
|
17
|
+
*
|
|
18
|
+
* The family contract this file follows is documented in ./README.md.
|
|
19
|
+
*/
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
|
|
22
|
+
export const FAMILY = 'voice';
|
|
23
|
+
|
|
24
|
+
export const TOOLS = ['km_update_brand_voice', 'km_set_writing_rules', 'km_set_service_rule'];
|
|
25
|
+
|
|
26
|
+
// Same profiles as the knowledge family that reads these values: anything that
|
|
27
|
+
// writes words for a real person should be able to correct how it writes them.
|
|
28
|
+
export const PROFILES = ['outreach', 'content', 'money'];
|
|
29
|
+
|
|
30
|
+
function routeMissing(r) {
|
|
31
|
+
return r.status === 404 || r.status === 405 || r.status === 501;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const NOT_SUPPORTED =
|
|
35
|
+
'Your KM Hub does not accept voice and writing-rule changes from the terminal yet. Nothing is wrong with the ' +
|
|
36
|
+
'workspace. The brand voice box is in the web app at https://hub.kivimedia.co under Settings, General, and this ' +
|
|
37
|
+
'connector will edit it once your KM Hub is on a build that publishes these routes.';
|
|
38
|
+
|
|
39
|
+
const CONFIRM_TOKEN = z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe(
|
|
43
|
+
'Leave this out on the first call. KM Hub answers 409 with the sentence to put to the person and a token. Only '
|
|
44
|
+
+ 'after they actually say yes, call again with the token and the SAME arguments. A yes for one change never '
|
|
45
|
+
+ 'authorises a different one.',
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/** Forward only what the caller set: undefined means "leave alone", null means "clear". */
|
|
49
|
+
function defined(args) {
|
|
50
|
+
const body = {};
|
|
51
|
+
for (const [k, v] of Object.entries(args)) if (v !== undefined) body[k] = v;
|
|
52
|
+
return body;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function register(server, call, { out, text }) {
|
|
56
|
+
server.tool(
|
|
57
|
+
'km_update_brand_voice',
|
|
58
|
+
'Change how this business sounds in everything the AI writes: the brand voice box (the owner\'s own words about ' +
|
|
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. ' +
|
|
62
|
+
'Call km_get_brand_voice first so you change from what is actually there. When the person asks to ADD to their ' +
|
|
63
|
+
'voice, send the existing text with the addition, never just the new sentence, or the rest is lost. Write ' +
|
|
64
|
+
'what they said in their words; do not polish it into marketing copy. ' +
|
|
65
|
+
'🚨 A confirmed write. KM Hub answers 409 with the exact before and after; say that to the person, wait for a ' +
|
|
66
|
+
'real yes in this turn, then call again with the confirm_token and the same arguments.',
|
|
67
|
+
{
|
|
68
|
+
brand_values: z
|
|
69
|
+
.string()
|
|
70
|
+
.nullable()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe('The full brand voice text, up to 4000 characters. It REPLACES what is there. Send null to clear it.'),
|
|
73
|
+
sign_emails_as: z
|
|
74
|
+
.string()
|
|
75
|
+
.nullable()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe('The name every email is signed with, for example "Mark" or "Mark and the Twisty Art team". Send null to clear it.'),
|
|
78
|
+
confirm_token: CONFIRM_TOKEN,
|
|
79
|
+
},
|
|
80
|
+
async (args) => {
|
|
81
|
+
const r = await call('PATCH', '/knowledge/voice', defined(args));
|
|
82
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
83
|
+
return out(r);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
server.tool(
|
|
88
|
+
'km_set_writing_rules',
|
|
89
|
+
'Set the house writing rules: the standing instructions every message the AI writes for this business must obey, ' +
|
|
90
|
+
'for example "never quote a price in the first email", "always offer a phone call", "never call our performers ' +
|
|
91
|
+
'clowns". They are mandatory, not style, and they apply to every AI writer including automatic ones. ' +
|
|
92
|
+
'Use append to add one rule to the end (the usual case, and safe); use rules only when the person wants the whole ' +
|
|
93
|
+
'list rewritten, and then send every rule they want kept, because rules replaces the list. km_get_brand_voice ' +
|
|
94
|
+
'shows the current list under voice.house_rules. ' +
|
|
95
|
+
'🚨 A confirmed write: KM Hub answers 409 with the before and after, the person says yes, you call again with ' +
|
|
96
|
+
'the confirm_token and the same arguments.',
|
|
97
|
+
{
|
|
98
|
+
append: z.string().optional().describe('One or more new rules to add to the end of the list.'),
|
|
99
|
+
rules: z
|
|
100
|
+
.string()
|
|
101
|
+
.nullable()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('The complete list of rules, one per line, up to 3000 characters. Replaces the list. Send null to clear every rule.'),
|
|
104
|
+
confirm_token: CONFIRM_TOKEN,
|
|
105
|
+
},
|
|
106
|
+
async (args) => {
|
|
107
|
+
const r = await call('PUT', '/knowledge/writing-rules', defined(args));
|
|
108
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
109
|
+
return out(r);
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
server.tool(
|
|
114
|
+
'km_set_service_rule',
|
|
115
|
+
'Give one service its own writing rule: an instruction the AI must follow whenever a message mentions that ' +
|
|
116
|
+
'service, for example "Balloon Twisting: always say each child gets one design, never a number of balloons" or ' +
|
|
117
|
+
'"Face Painting: mention we only use FDA compliant paints". One rule per service; setting it again replaces it, ' +
|
|
118
|
+
'null removes it. Name the service by package_id or by its exact name from km_get_offers_and_pricing, which ' +
|
|
119
|
+
'also lists every current rule under service_rules. ' +
|
|
120
|
+
'🚨 A confirmed write: KM Hub answers 409 with the before and after, the person says yes, you call again with ' +
|
|
121
|
+
'the confirm_token and the same arguments.',
|
|
122
|
+
{
|
|
123
|
+
package_id: z.string().optional().describe('The service package id from km_get_offers_and_pricing (proposal_packages[].id).'),
|
|
124
|
+
package_name: z.string().optional().describe('Or the exact service name, when you do not have the id.'),
|
|
125
|
+
rule: z.string().nullable().describe('The rule, up to 1000 characters. Send null to remove this service\'s rule.'),
|
|
126
|
+
confirm_token: CONFIRM_TOKEN,
|
|
127
|
+
},
|
|
128
|
+
async (args) => {
|
|
129
|
+
const r = await call('PUT', '/knowledge/service-rules', defined(args));
|
|
130
|
+
if (routeMissing(r)) return text(NOT_SUPPORTED, true);
|
|
131
|
+
return out(r);
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
}
|