@adrata/adrata-mcp 1.0.1 → 1.0.3
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 +1 -1
- package/access/auth.js +193 -4
- package/access/oauth.js +105 -15
- package/access/resource-metadata.js +9 -1
- package/access/tiers.js +35 -0
- package/api-bridge.js +84 -5
- package/governance/governed-args.js +261 -0
- package/package.json +5 -4
- package/product-profile.js +43 -0
- package/resources.js +22 -10
- package/security.js +34 -6
- package/server.js +282 -58
- package/server.json +0 -6
- package/skills/board-review/SKILL.md +10 -2
- package/skills/incident-to-card/SKILL.md +23 -15
- package/skills/ship-the-card/SKILL.md +27 -4
- package/tool-annotations.js +63 -7
- package/tools/billing.js +5 -1
- package/tools/roadmap-tools.js +192 -0
- package/tools/work-board-tools.js +455 -16
- package/tools/work-hub/audit.js +162 -0
- package/toolsets/crm.js +72 -10
- package/toolsets/prospecting.js +118 -12
- package/toolsets/spaces.js +628 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure operating-health projection for Starfield boards.
|
|
3
|
+
*
|
|
4
|
+
* A board is trustworthy when its workflow claims are backed by the minimum
|
|
5
|
+
* facts needed to act: ownership, a work type, a definition of done, and a
|
|
6
|
+
* current stage. Delivery evidence is deliberately added separately because a
|
|
7
|
+
* column is not a deployment receipt.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TERMINAL_STAGES = new Set(['production', 'deep backlog']);
|
|
11
|
+
// Up Next is the deliberately unowned pull buffer. Triage/Aligning are capture
|
|
12
|
+
// and grooming, where thin cards are allowed. Only these stages mean somebody
|
|
13
|
+
// has actually taken a pass and therefore require both an end-to-end owner and
|
|
14
|
+
// a current handler.
|
|
15
|
+
const ACTIVE_STAGES = new Set(['in progress', 'staging qa1', 'staging qa2']);
|
|
16
|
+
// A card crossing the cut line into Up Next must be executable. Production is
|
|
17
|
+
// included because historical evidence does not stop mattering after release.
|
|
18
|
+
const EXECUTABLE_STAGES = new Set(['up next', ...ACTIVE_STAGES, 'production']);
|
|
19
|
+
|
|
20
|
+
function normalized(value) {
|
|
21
|
+
return String(value ?? '').trim().toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hoursSince(iso, nowMs) {
|
|
25
|
+
const entered = Date.parse(iso);
|
|
26
|
+
if (!Number.isFinite(entered)) return null;
|
|
27
|
+
return Math.max(0, (nowMs - entered) / 3_600_000);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function deliveryContradictions(evidence) {
|
|
31
|
+
const contradictions = [];
|
|
32
|
+
const workflowStage = normalized(evidence?.workflow?.columnName);
|
|
33
|
+
if (workflowStage === 'production' && evidence?.production?.state !== 'live') {
|
|
34
|
+
contradictions.push({
|
|
35
|
+
code: 'production_column_without_live_evidence',
|
|
36
|
+
message:
|
|
37
|
+
'The card is in Production, but exact-SHA production evidence is not live. Treat the column as a workflow claim, not a deployment receipt.',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (evidence?.production?.state === 'live' && workflowStage !== 'production') {
|
|
41
|
+
contradictions.push({
|
|
42
|
+
code: 'live_evidence_outside_production_column',
|
|
43
|
+
message:
|
|
44
|
+
'Exact-SHA production evidence is live, but the workflow card has not reached Production.',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return contradictions;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {Array<object>} boards Full board payloads from GET /work-boards/{id}.
|
|
52
|
+
* @param {{now?: Date|string|number}} options
|
|
53
|
+
*/
|
|
54
|
+
export function auditWorkHubBoards(boards, { now = new Date() } = {}) {
|
|
55
|
+
const nowMs = new Date(now).getTime();
|
|
56
|
+
const findings = [];
|
|
57
|
+
const perBoard = [];
|
|
58
|
+
let totalCards = 0;
|
|
59
|
+
let openCards = 0;
|
|
60
|
+
let activeCards = 0;
|
|
61
|
+
|
|
62
|
+
for (const board of boards) {
|
|
63
|
+
const columns = new Map((board.columns ?? []).map((column) => [column.id, column]));
|
|
64
|
+
const boardFindings = [];
|
|
65
|
+
const items = board.items ?? [];
|
|
66
|
+
totalCards += items.length;
|
|
67
|
+
|
|
68
|
+
if (board.truncated === true) {
|
|
69
|
+
boardFindings.push({
|
|
70
|
+
code: 'board_truncated',
|
|
71
|
+
itemId: null,
|
|
72
|
+
title: null,
|
|
73
|
+
detail: `The board returned ${items.length} of ${board.itemCount ?? 'an unknown number of'} cards.`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
const column = columns.get(item.columnId);
|
|
79
|
+
const stage = column?.name ?? 'Unknown';
|
|
80
|
+
const stageKey = normalized(stage);
|
|
81
|
+
const isTerminal = TERMINAL_STAGES.has(stageKey);
|
|
82
|
+
const isActive = ACTIVE_STAGES.has(stageKey);
|
|
83
|
+
const mustBeExecutable = EXECUTABLE_STAGES.has(stageKey);
|
|
84
|
+
if (!isTerminal) openCards += 1;
|
|
85
|
+
if (isActive) activeCards += 1;
|
|
86
|
+
|
|
87
|
+
const add = (code, detail) =>
|
|
88
|
+
boardFindings.push({ code, itemId: item.id, title: item.title, stage, detail });
|
|
89
|
+
|
|
90
|
+
if (mustBeExecutable && !String(item.body ?? '').trim()) {
|
|
91
|
+
add('missing_body', 'The card crossed the cut line without working context.');
|
|
92
|
+
}
|
|
93
|
+
if (mustBeExecutable) {
|
|
94
|
+
if (item.criteria?.total === 0) {
|
|
95
|
+
add('missing_acceptance_criteria', 'Nobody has stated a checkable definition of done.');
|
|
96
|
+
} else if (item.criteria === undefined) {
|
|
97
|
+
add('criteria_not_measured', 'This read did not include acceptance-criteria status.');
|
|
98
|
+
}
|
|
99
|
+
if (!item.kind) add('missing_kind', 'The card is not classified as a story, bug, or chore.');
|
|
100
|
+
}
|
|
101
|
+
if (isActive && !item.assigneeUserId) {
|
|
102
|
+
add('unowned_active_card', 'The card is active but has no end-to-end owner.');
|
|
103
|
+
}
|
|
104
|
+
if (isActive && !item.handler) {
|
|
105
|
+
add('unhandled_active_pass', 'The active build or QA pass has no handler.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const staleAfter = column?.staleness?.staleAfterHours;
|
|
109
|
+
const ageHours = hoursSince(item.enteredColumnAt, nowMs);
|
|
110
|
+
if (!isTerminal && Number.isFinite(staleAfter) && ageHours !== null && ageHours > staleAfter) {
|
|
111
|
+
add(
|
|
112
|
+
'stale_open_card',
|
|
113
|
+
`The card has spent ${Math.floor(ageHours)}h in ${stage}; this column is stale after ${staleAfter}h.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
findings.push(...boardFindings.map((finding) => ({ boardId: board.id, boardName: board.name, ...finding })));
|
|
119
|
+
perBoard.push({
|
|
120
|
+
boardId: board.id,
|
|
121
|
+
boardName: board.name,
|
|
122
|
+
cards: items.length,
|
|
123
|
+
activeCards: items.filter((item) => {
|
|
124
|
+
const stage = columns.get(item.columnId)?.name;
|
|
125
|
+
return ACTIVE_STAGES.has(normalized(stage));
|
|
126
|
+
}).length,
|
|
127
|
+
findings: boardFindings.length,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const counts = {};
|
|
132
|
+
for (const finding of findings) counts[finding.code] = (counts[finding.code] ?? 0) + 1;
|
|
133
|
+
|
|
134
|
+
const priorityOrder = [
|
|
135
|
+
'board_truncated',
|
|
136
|
+
'unowned_active_card',
|
|
137
|
+
'unhandled_active_pass',
|
|
138
|
+
'missing_acceptance_criteria',
|
|
139
|
+
'stale_open_card',
|
|
140
|
+
'missing_body',
|
|
141
|
+
'missing_kind',
|
|
142
|
+
'criteria_not_measured',
|
|
143
|
+
];
|
|
144
|
+
const rank = new Map(priorityOrder.map((code, index) => [code, index]));
|
|
145
|
+
findings.sort((a, b) => (rank.get(a.code) ?? 99) - (rank.get(b.code) ?? 99));
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
trustworthy: findings.length === 0,
|
|
149
|
+
generatedAt: new Date(nowMs).toISOString(),
|
|
150
|
+
boards: boards.length,
|
|
151
|
+
totalCards,
|
|
152
|
+
openCards,
|
|
153
|
+
activeCards,
|
|
154
|
+
counts,
|
|
155
|
+
perBoard,
|
|
156
|
+
findings,
|
|
157
|
+
interpretation:
|
|
158
|
+
findings.length === 0
|
|
159
|
+
? 'Every returned card has the minimum facts required to operate from this hub. Delivery truth must still be read from exact-SHA evidence.'
|
|
160
|
+
: 'The hub is not yet trustworthy as an operating ledger. Work the findings in order; do not infer completion from column position.',
|
|
161
|
+
};
|
|
162
|
+
}
|
package/toolsets/crm.js
CHANGED
|
@@ -9,6 +9,23 @@ import { z } from 'zod';
|
|
|
9
9
|
import {
|
|
10
10
|
md, mdError, table, formatEntityTimeline, formatDate,
|
|
11
11
|
} from '../output-formatter.js';
|
|
12
|
+
import {
|
|
13
|
+
governedWriteArgs, governedWriteNote, isLiveWrite, runGovernedMarkdownWrite,
|
|
14
|
+
enrichCostLine, COMPANY_ENRICH_COST_SENTENCE, PERSON_ENRICH_COST_SENTENCE,
|
|
15
|
+
} from '../governance/governed-args.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The `manage_*` tools are composites: one tool, one `action` argument, reads
|
|
19
|
+
* and writes side by side. Only the write actions are governed — a `get`,
|
|
20
|
+
* `list`, `search` or `timeline` must keep working exactly as before, with no
|
|
21
|
+
* new required arguments and no preview step.
|
|
22
|
+
*
|
|
23
|
+
* This is the list of actions that mutate. Everything not in it is a read.
|
|
24
|
+
*/
|
|
25
|
+
const WRITE_ACTIONS = new Set([
|
|
26
|
+
'create', 'update', 'delete', 'enrich', 'complete',
|
|
27
|
+
'bulk_delete', 'add_member', 'update_member', 'remove_member',
|
|
28
|
+
]);
|
|
12
29
|
|
|
13
30
|
export function register(server, api, AUTH) {
|
|
14
31
|
|
|
@@ -17,7 +34,10 @@ export function register(server, api, AUTH) {
|
|
|
17
34
|
// -----------------------------------------------------------------------
|
|
18
35
|
server.tool(
|
|
19
36
|
'manage_company',
|
|
20
|
-
'Full CRUD for companies: create, read, update, delete, enrich, and view activity timeline. One tool for all company operations.'
|
|
37
|
+
'Full CRUD for companies: create, read, update, delete, enrich, and view activity timeline. One tool for all company operations.'
|
|
38
|
+
+ ' Reads (get/search/timeline) run directly. create/update/delete/enrich are governed writes.'
|
|
39
|
+
+ COMPANY_ENRICH_COST_SENTENCE
|
|
40
|
+
+ governedWriteNote('write:companies'),
|
|
21
41
|
{
|
|
22
42
|
action: z.enum(['create', 'get', 'update', 'delete', 'enrich', 'timeline', 'search']).describe('Operation'),
|
|
23
43
|
id: z.string().optional().describe('Company ID (for get/update/delete/enrich/timeline)'),
|
|
@@ -28,6 +48,7 @@ export function register(server, api, AUTH) {
|
|
|
28
48
|
status: z.string().optional().describe('ACTIVE, PROSPECT, CUSTOMER, CHURNED'),
|
|
29
49
|
customFields: z.record(z.unknown()).optional().describe('Custom fields (JSONB merged)'),
|
|
30
50
|
query: z.string().optional().describe('Search query (for search action)'),
|
|
51
|
+
...governedWriteArgs(z),
|
|
31
52
|
},
|
|
32
53
|
async (args) => {
|
|
33
54
|
try {
|
|
@@ -40,9 +61,20 @@ export function register(server, api, AUTH) {
|
|
|
40
61
|
if (args.website) body.website = args.website;
|
|
41
62
|
if (args.status) body.status = args.status;
|
|
42
63
|
if (args.customFields) body.customFields = args.customFields;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
return runGovernedMarkdownWrite(
|
|
65
|
+
api, args,
|
|
66
|
+
{
|
|
67
|
+
method: 'POST',
|
|
68
|
+
path: '/api/v1/companies',
|
|
69
|
+
body,
|
|
70
|
+
heading: 'Create Company',
|
|
71
|
+
preview: { entity: 'company', operation: 'create', fields: Object.keys(body) },
|
|
72
|
+
},
|
|
73
|
+
(result) => {
|
|
74
|
+
const c = result?.data || result || {};
|
|
75
|
+
return `## Company Created\n\n- **Name:** ${c.name || args.name}\n- **ID:** ${c.id || '\u2014'}\n- **Status:** ${c.status || 'PROSPECT'}\n`;
|
|
76
|
+
},
|
|
77
|
+
);
|
|
46
78
|
}
|
|
47
79
|
case 'get': {
|
|
48
80
|
const c = await api('GET', `/api/v1/companies/${args.id}`);
|
|
@@ -64,16 +96,46 @@ export function register(server, api, AUTH) {
|
|
|
64
96
|
if (args.website) body.website = args.website;
|
|
65
97
|
if (args.status) body.status = args.status;
|
|
66
98
|
if (args.customFields) body.customFields = args.customFields;
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
return runGovernedMarkdownWrite(
|
|
100
|
+
api, args,
|
|
101
|
+
{
|
|
102
|
+
method: 'PATCH',
|
|
103
|
+
path: `/api/v1/companies/${args.id}`,
|
|
104
|
+
body,
|
|
105
|
+
heading: 'Update Company',
|
|
106
|
+
preview: { entity: 'company', entityId: args.id, operation: 'update', fields: Object.keys(body) },
|
|
107
|
+
},
|
|
108
|
+
() => `## Company Updated\n\nID: ${args.id}\nFields updated: ${Object.keys(body).join(', ')}\n`,
|
|
109
|
+
);
|
|
69
110
|
}
|
|
70
111
|
case 'delete': {
|
|
71
|
-
|
|
72
|
-
|
|
112
|
+
return runGovernedMarkdownWrite(
|
|
113
|
+
api, args,
|
|
114
|
+
{
|
|
115
|
+
method: 'DELETE',
|
|
116
|
+
path: `/api/v1/companies/${args.id}`,
|
|
117
|
+
heading: 'Delete Company',
|
|
118
|
+
preview: { entity: 'company', entityId: args.id, operation: 'soft-delete' },
|
|
119
|
+
},
|
|
120
|
+
() => `## Company Deleted\n\nID: ${args.id}\n`,
|
|
121
|
+
);
|
|
73
122
|
}
|
|
74
123
|
case 'enrich': {
|
|
75
|
-
|
|
76
|
-
|
|
124
|
+
return runGovernedMarkdownWrite(
|
|
125
|
+
api, args,
|
|
126
|
+
{
|
|
127
|
+
method: 'POST',
|
|
128
|
+
path: `/api/v1/companies/${args.id}/enrich`,
|
|
129
|
+
heading: 'Enrich Company',
|
|
130
|
+
preview: {
|
|
131
|
+
entity: 'company',
|
|
132
|
+
entityId: args.id,
|
|
133
|
+
operation: 'enrich',
|
|
134
|
+
cost: enrichCostLine('company'),
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
() => `## Enrichment Triggered\n\nCompany ID: ${args.id}\nCost: ${enrichCostLine('company')}\nData will be available shortly.\n`,
|
|
138
|
+
);
|
|
77
139
|
}
|
|
78
140
|
case 'timeline': {
|
|
79
141
|
const [company, actions] = await Promise.all([
|
package/toolsets/prospecting.js
CHANGED
|
@@ -12,6 +12,89 @@ import {
|
|
|
12
12
|
formatQualification, formatCompanyResearch, formatPersonResearch,
|
|
13
13
|
formatSpeedrunList, formatNextContacts, table, signalEmoji, formatDate, daysSince,
|
|
14
14
|
} from '../output-formatter.js';
|
|
15
|
+
import {
|
|
16
|
+
governedWriteArgs, governedWriteNote, isLiveWrite, missingLiveWriteFields,
|
|
17
|
+
previewMarkdown, enrichCostLine,
|
|
18
|
+
COMPANY_ENRICH_COST_SENTENCE, PERSON_ENRICH_COST_SENTENCE,
|
|
19
|
+
} from '../governance/governed-args.js';
|
|
20
|
+
import { governedWrite } from '../api-bridge.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run the enrichment leg of a research tool through the governed-write
|
|
24
|
+
* contract and render the outcome as a Markdown section.
|
|
25
|
+
*
|
|
26
|
+
* `research_company` / `research_person` are read tools with one write inside
|
|
27
|
+
* them: a POST to `/enrich`, which spends real Coresignal credits. They used to
|
|
28
|
+
* fire that POST on every call with no preview, no audit reason and no
|
|
29
|
+
* idempotency key — while the IDENTICAL endpoint reached through
|
|
30
|
+
* `enrich_company` / `enrich_person` in server.js previewed and refused.
|
|
31
|
+
*
|
|
32
|
+
* So the reads still run and still produce the report. The enrich is withheld
|
|
33
|
+
* by default and reported as withheld, with the credit cost named, rather than
|
|
34
|
+
* being silently skipped — a research report that quietly lost its enrichment
|
|
35
|
+
* would be worse than one that spent the credits.
|
|
36
|
+
*
|
|
37
|
+
* @returns `{ enrichment, section }` — `enrichment` is the live result (or an
|
|
38
|
+
* empty object when nothing was executed) so the report renders
|
|
39
|
+
* identically either way.
|
|
40
|
+
*/
|
|
41
|
+
async function governedEnrich(api, args, { method, path, kind, subjectId }) {
|
|
42
|
+
if (isLiveWrite(args)) {
|
|
43
|
+
const missing = missingLiveWriteFields(args);
|
|
44
|
+
if (missing.length > 0) {
|
|
45
|
+
return {
|
|
46
|
+
enrichment: {},
|
|
47
|
+
section:
|
|
48
|
+
`\n---\n\n## Enrichment refused — nothing was spent\n\n` +
|
|
49
|
+
`A live enrichment write is missing: ${missing.join(', ')}.\n\n` +
|
|
50
|
+
`- **Would have called:** ${method} ${path}\n` +
|
|
51
|
+
`- **Cost if executed:** ${enrichCostLine(kind)}\n\n` +
|
|
52
|
+
`Re-call with \`dryRun:false\` plus ${missing.join(', ')}.\n`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const outcome = await governedWrite(api, args, {
|
|
59
|
+
method,
|
|
60
|
+
path,
|
|
61
|
+
preview: {
|
|
62
|
+
entity: kind,
|
|
63
|
+
entityId: subjectId,
|
|
64
|
+
operation: 'enrich',
|
|
65
|
+
cost: enrichCostLine(kind),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (outcome.dryRun) {
|
|
70
|
+
return {
|
|
71
|
+
enrichment: {},
|
|
72
|
+
section:
|
|
73
|
+
`\n---\n\n${previewMarkdown('Enrichment', outcome.preview)}` +
|
|
74
|
+
`\nThe research above was assembled from reads only. No vendor credits were spent.\n`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
enrichment: outcome.result || {},
|
|
80
|
+
section:
|
|
81
|
+
`\n---\n\n## Enrichment executed\n\n` +
|
|
82
|
+
`- **Call:** ${method} ${path}\n` +
|
|
83
|
+
`- **Cost:** ${enrichCostLine(kind)}\n` +
|
|
84
|
+
`- **Reason:** ${args.reason}\n` +
|
|
85
|
+
`- **Idempotency key:** ${args.idempotencyKey}\n`,
|
|
86
|
+
};
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return {
|
|
89
|
+
enrichment: {},
|
|
90
|
+
section:
|
|
91
|
+
`\n---\n\n## Enrichment not performed\n\n` +
|
|
92
|
+
`- **Attempted:** ${method} ${path}\n` +
|
|
93
|
+
`- **Error:** ${err.message}\n` +
|
|
94
|
+
`\nThe research above was assembled from reads only.\n`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
15
98
|
|
|
16
99
|
export function register(server, api, AUTH) {
|
|
17
100
|
|
|
@@ -63,10 +146,14 @@ export function register(server, api, AUTH) {
|
|
|
63
146
|
// -----------------------------------------------------------------------
|
|
64
147
|
server.tool(
|
|
65
148
|
'research_company',
|
|
66
|
-
'Deep research report on a company: firmographics, tech stack, funding, news, competitors, intent signals. Orchestrates 5+ API calls into a structured report.'
|
|
149
|
+
'Deep research report on a company: firmographics, tech stack, funding, news, competitors, intent signals. Orchestrates 5+ API calls into a structured report.'
|
|
150
|
+
+ ' The reads always run. The enrichment leg is a governed write:'
|
|
151
|
+
+ COMPANY_ENRICH_COST_SENTENCE
|
|
152
|
+
+ governedWriteNote('write:companies'),
|
|
67
153
|
{
|
|
68
154
|
companyId: z.string().optional().describe('Company ID'),
|
|
69
155
|
name: z.string().optional().describe('Company name to search'),
|
|
156
|
+
...governedWriteArgs(z),
|
|
70
157
|
},
|
|
71
158
|
async (args) => {
|
|
72
159
|
try {
|
|
@@ -86,14 +173,22 @@ export function register(server, api, AUTH) {
|
|
|
86
173
|
return mdError('Provide companyId or name');
|
|
87
174
|
}
|
|
88
175
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
176
|
+
// Reads run unconditionally. The enrich POST is a governed write and
|
|
177
|
+
// previews by default, so a research call costs zero vendor credits
|
|
178
|
+
// unless the caller explicitly authorised the spend.
|
|
179
|
+
const [signals, competitors] = await Promise.all([
|
|
92
180
|
api('GET', '/api/v1/intent-signals', { params: { companyId } }).catch(() => ({ data: [] })),
|
|
93
181
|
api('GET', '/api/v1/competitors', { params: { companyId } }).catch(() => ({ data: [] })),
|
|
94
182
|
]);
|
|
95
183
|
|
|
96
|
-
|
|
184
|
+
const { enrichment, section } = await governedEnrich(api, args, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
path: `/api/v1/companies/${companyId}/enrich`,
|
|
187
|
+
kind: 'company',
|
|
188
|
+
subjectId: companyId,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return md(formatCompanyResearch(company, enrichment, signals, competitors) + section);
|
|
97
192
|
} catch (err) {
|
|
98
193
|
return mdError('Research failed', err.message);
|
|
99
194
|
}
|
|
@@ -105,11 +200,15 @@ export function register(server, api, AUTH) {
|
|
|
105
200
|
// -----------------------------------------------------------------------
|
|
106
201
|
server.tool(
|
|
107
202
|
'research_person',
|
|
108
|
-
'Deep research on a person: professional profile, communication preferences, intro paths, recent activity. Orchestrates 3+ API calls.'
|
|
203
|
+
'Deep research on a person: professional profile, communication preferences, intro paths, recent activity. Orchestrates 3+ API calls.'
|
|
204
|
+
+ ' The reads always run. The enrichment leg is a governed write:'
|
|
205
|
+
+ PERSON_ENRICH_COST_SENTENCE
|
|
206
|
+
+ governedWriteNote('write:people'),
|
|
109
207
|
{
|
|
110
208
|
personId: z.string().optional().describe('Person ID'),
|
|
111
209
|
name: z.string().optional().describe('Person name to search'),
|
|
112
210
|
email: z.string().optional().describe('Email to search by'),
|
|
211
|
+
...governedWriteArgs(z),
|
|
113
212
|
},
|
|
114
213
|
async (args) => {
|
|
115
214
|
try {
|
|
@@ -132,13 +231,20 @@ export function register(server, api, AUTH) {
|
|
|
132
231
|
return mdError('Provide personId, name, or email');
|
|
133
232
|
}
|
|
134
233
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
234
|
+
// Reads run unconditionally; the enrich POST is governed and previews
|
|
235
|
+
// by default. A Coresignal collect is 20 credits per person, so an
|
|
236
|
+
// ungoverned research call was a per-invocation vendor charge.
|
|
237
|
+
const actions = await api('GET', '/api/v1/actions', { params: { personId, limit: 10, page: 1 } })
|
|
238
|
+
.catch(() => ({ data: [] }));
|
|
239
|
+
|
|
240
|
+
const { enrichment, section } = await governedEnrich(api, args, {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
path: `/api/v1/people/${personId}/enrich`,
|
|
243
|
+
kind: 'person',
|
|
244
|
+
subjectId: personId,
|
|
245
|
+
});
|
|
140
246
|
|
|
141
|
-
return md(formatPersonResearch(person, enrichment, actions));
|
|
247
|
+
return md(formatPersonResearch(person, enrichment, actions) + section);
|
|
142
248
|
} catch (err) {
|
|
143
249
|
return mdError('Research failed', err.message);
|
|
144
250
|
}
|