@open-mercato/core 0.6.6-develop.6313.1.618a49a6b4 → 0.6.6-develop.6317.1.b3be10ab84
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/ai-tools/_shared.js +158 -0
- package/dist/modules/customers/ai-tools/_shared.js.map +7 -0
- package/dist/modules/customers/ai-tools/companies-pack.js +15 -121
- package/dist/modules/customers/ai-tools/companies-pack.js.map +2 -2
- package/dist/modules/customers/ai-tools/people-pack.js +9 -107
- package/dist/modules/customers/ai-tools/people-pack.js.map +2 -2
- package/dist/modules/customers/components/DictionarySettings.js +92 -37
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarScreen.js +1 -0
- package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventPeekPopover.js +12 -1
- package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +3 -0
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/types.js.map +1 -1
- package/dist/modules/customers/components/formConfig.js +7 -1
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/lib/dictionaries.js +10 -0
- package/dist/modules/customers/lib/dictionaries.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/ai-tools/_shared.ts +270 -0
- package/src/modules/customers/ai-tools/companies-pack.ts +17 -157
- package/src/modules/customers/ai-tools/people-pack.ts +11 -133
- package/src/modules/customers/components/DictionarySettings.tsx +60 -2
- package/src/modules/customers/components/calendar/CalendarScreen.tsx +1 -0
- package/src/modules/customers/components/calendar/EventPeekPopover.tsx +29 -11
- package/src/modules/customers/components/calendar/TimeGrid.tsx +3 -0
- package/src/modules/customers/components/calendar/types.ts +1 -0
- package/src/modules/customers/components/formConfig.tsx +8 -2
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/lib/dictionaries.ts +10 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
function toIso(value) {
|
|
2
|
+
if (!value) return null;
|
|
3
|
+
const dt = value instanceof Date ? value : new Date(String(value));
|
|
4
|
+
if (Number.isNaN(dt.getTime())) return null;
|
|
5
|
+
return dt.toISOString();
|
|
6
|
+
}
|
|
7
|
+
function resolveEm(ctx) {
|
|
8
|
+
return ctx.container.resolve("em");
|
|
9
|
+
}
|
|
10
|
+
function buildScope(ctx, tenantId) {
|
|
11
|
+
return {
|
|
12
|
+
tenantId,
|
|
13
|
+
organizationId: ctx.organizationId
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function toCustomerListSummary(row) {
|
|
17
|
+
const createdAtRaw = row.created_at ?? row.createdAt ?? null;
|
|
18
|
+
const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null;
|
|
19
|
+
return {
|
|
20
|
+
id: row.id,
|
|
21
|
+
displayName: row.display_name ?? row.displayName ?? null,
|
|
22
|
+
primaryEmail: row.primary_email ?? row.primaryEmail ?? null,
|
|
23
|
+
primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,
|
|
24
|
+
status: row.status ?? null,
|
|
25
|
+
lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,
|
|
26
|
+
source: row.source ?? null,
|
|
27
|
+
ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,
|
|
28
|
+
organizationId: row.organization_id ?? row.organizationId ?? null,
|
|
29
|
+
tenantId: row.tenant_id ?? row.tenantId ?? null,
|
|
30
|
+
createdAt
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function asRows(value) {
|
|
34
|
+
return Array.isArray(value) ? value : [];
|
|
35
|
+
}
|
|
36
|
+
function mapAddresses(rows) {
|
|
37
|
+
return rows.map((address) => ({
|
|
38
|
+
id: address.id,
|
|
39
|
+
name: address.name ?? null,
|
|
40
|
+
purpose: address.purpose ?? null,
|
|
41
|
+
addressLine1: address.addressLine1 ?? null,
|
|
42
|
+
addressLine2: address.addressLine2 ?? null,
|
|
43
|
+
city: address.city ?? null,
|
|
44
|
+
region: address.region ?? null,
|
|
45
|
+
postalCode: address.postalCode ?? null,
|
|
46
|
+
country: address.country ?? null,
|
|
47
|
+
isPrimary: !!address.isPrimary
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
function mapActivities(rows) {
|
|
51
|
+
return rows.map((activity) => ({
|
|
52
|
+
id: activity.id,
|
|
53
|
+
activityType: activity.activityType,
|
|
54
|
+
subject: activity.subject ?? null,
|
|
55
|
+
body: activity.body ?? null,
|
|
56
|
+
occurredAt: toIso(activity.occurredAt),
|
|
57
|
+
createdAt: toIso(activity.createdAt)
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
function mapNotes(rows) {
|
|
61
|
+
return rows.map((comment) => ({
|
|
62
|
+
id: comment.id,
|
|
63
|
+
body: comment.body,
|
|
64
|
+
authorUserId: comment.authorUserId ?? null,
|
|
65
|
+
createdAt: toIso(comment.createdAt)
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
function mapTasks(rows) {
|
|
69
|
+
return rows.map((task) => ({
|
|
70
|
+
id: task.id,
|
|
71
|
+
todoId: task.todoId ?? task.id,
|
|
72
|
+
todoSource: task.todoSource ?? null,
|
|
73
|
+
createdAt: toIso(task.createdAt)
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
function mapInteractions(rows) {
|
|
77
|
+
return rows.map((interaction) => ({
|
|
78
|
+
id: interaction.id,
|
|
79
|
+
interactionType: interaction.interactionType,
|
|
80
|
+
title: interaction.title ?? null,
|
|
81
|
+
status: interaction.status,
|
|
82
|
+
scheduledAt: toIso(interaction.scheduledAt),
|
|
83
|
+
occurredAt: toIso(interaction.occurredAt)
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
function mapTags(rows) {
|
|
87
|
+
return rows.map((tag) => {
|
|
88
|
+
if (!tag || typeof tag !== "object") return null;
|
|
89
|
+
const id = typeof tag.id === "string" ? tag.id : null;
|
|
90
|
+
const label = typeof tag.label === "string" ? tag.label : null;
|
|
91
|
+
if (!id || !label) return null;
|
|
92
|
+
const slug = typeof tag.slug === "string" ? tag.slug : label;
|
|
93
|
+
const color = typeof tag.color === "string" ? tag.color : null;
|
|
94
|
+
return { id, slug, label, color };
|
|
95
|
+
}).filter((entry) => entry !== null);
|
|
96
|
+
}
|
|
97
|
+
function mapDeals(rows) {
|
|
98
|
+
return rows.map((deal) => {
|
|
99
|
+
if (!deal || typeof deal !== "object") return null;
|
|
100
|
+
const id = typeof deal.id === "string" ? deal.id : null;
|
|
101
|
+
if (!id) return null;
|
|
102
|
+
return {
|
|
103
|
+
id,
|
|
104
|
+
title: typeof deal.title === "string" ? deal.title : "",
|
|
105
|
+
status: typeof deal.status === "string" ? deal.status : null,
|
|
106
|
+
pipelineStageId: typeof deal.pipelineStageId === "string" ? deal.pipelineStageId : null,
|
|
107
|
+
valueAmount: typeof deal.valueAmount === "string" ? deal.valueAmount : deal.valueAmount === null || deal.valueAmount === void 0 ? null : String(deal.valueAmount),
|
|
108
|
+
valueCurrency: typeof deal.valueCurrency === "string" ? deal.valueCurrency : null
|
|
109
|
+
};
|
|
110
|
+
}).filter((value) => value !== null);
|
|
111
|
+
}
|
|
112
|
+
function mapPeople(rows) {
|
|
113
|
+
return rows.map((person) => {
|
|
114
|
+
if (!person || typeof person !== "object") return null;
|
|
115
|
+
const id = typeof person.id === "string" ? person.id : null;
|
|
116
|
+
const displayName = typeof person.displayName === "string" ? person.displayName : null;
|
|
117
|
+
if (!id || !displayName) return null;
|
|
118
|
+
return {
|
|
119
|
+
id,
|
|
120
|
+
displayName,
|
|
121
|
+
primaryEmail: typeof person.primaryEmail === "string" ? person.primaryEmail : null,
|
|
122
|
+
primaryPhone: typeof person.primaryPhone === "string" ? person.primaryPhone : null,
|
|
123
|
+
jobTitle: typeof person.jobTitle === "string" ? person.jobTitle : null,
|
|
124
|
+
department: typeof person.department === "string" ? person.department : null
|
|
125
|
+
};
|
|
126
|
+
}).filter((value) => value !== null);
|
|
127
|
+
}
|
|
128
|
+
function buildRelatedRecords(data, options = {}) {
|
|
129
|
+
const related = {
|
|
130
|
+
addresses: mapAddresses(asRows(data.addresses)),
|
|
131
|
+
activities: mapActivities(asRows(data.activities)),
|
|
132
|
+
notes: mapNotes(asRows(data.comments)),
|
|
133
|
+
tasks: mapTasks(asRows(data.todos)),
|
|
134
|
+
interactions: mapInteractions(asRows(data.interactions)),
|
|
135
|
+
tags: mapTags(asRows(data.tags)),
|
|
136
|
+
deals: mapDeals(asRows(data.deals))
|
|
137
|
+
};
|
|
138
|
+
if (options.includePeople) {
|
|
139
|
+
related.people = mapPeople(asRows(data.people));
|
|
140
|
+
}
|
|
141
|
+
return related;
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
buildRelatedRecords,
|
|
145
|
+
buildScope,
|
|
146
|
+
mapActivities,
|
|
147
|
+
mapAddresses,
|
|
148
|
+
mapDeals,
|
|
149
|
+
mapInteractions,
|
|
150
|
+
mapNotes,
|
|
151
|
+
mapPeople,
|
|
152
|
+
mapTags,
|
|
153
|
+
mapTasks,
|
|
154
|
+
resolveEm,
|
|
155
|
+
toCustomerListSummary,
|
|
156
|
+
toIso
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=_shared.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/customers/ai-tools/_shared.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Shared helpers for customers AI tool packs.\n *\n * Mirrors the catalog precedent (`packages/core/src/modules/catalog/ai-tools/_shared.ts`):\n * the company and people packs previously carried byte-for-byte-identical copies\n * of the date-to-ISO helper, the `resolveEm` / `buildScope` accessors, the\n * list-row \u2192 summary mapper, and the related-records builder\n * (addresses / activities / notes / tasks / interactions / tags / deals, plus a\n * companies-only `people` mapper). Centralizing them here gives both packs one\n * source of truth so a change to the related-records output shape no longer has\n * to be applied \u2014 and kept in sync \u2014 in two places.\n *\n * This is a pure internal refactor: tool names, input schemas, `requiredFeatures`,\n * and emitted output shapes stay identical.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AiToolExecutionContext } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport type { CustomersToolContext } from './types'\n\nexport function toIso(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nexport function resolveEm(ctx: CustomersToolContext | AiToolExecutionContext): EntityManager {\n return ctx.container.resolve<EntityManager>('em')\n}\n\nexport function buildScope(ctx: CustomersToolContext | AiToolExecutionContext, tenantId: string) {\n return {\n tenantId,\n organizationId: ctx.organizationId,\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* List-row summary mapper */\n/* -------------------------------------------------------------------------- */\n\nexport type CustomerListApiItemBase = {\n id?: string\n display_name?: string | null\n displayName?: string | null\n primary_email?: string | null\n primaryEmail?: string | null\n primary_phone?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycle_stage?: string | null\n lifecycleStage?: string | null\n source?: string | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\nexport type CustomerListSummary = {\n id: string | undefined\n displayName: string | null\n primaryEmail: string | null\n primaryPhone: string | null\n status: string | null\n lifecycleStage: string | null\n source: string | null\n ownerUserId: string | null\n organizationId: string | null\n tenantId: string | null\n createdAt: string | null\n}\n\nexport function toCustomerListSummary(row: CustomerListApiItemBase): CustomerListSummary {\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n displayName: row.display_name ?? row.displayName ?? null,\n primaryEmail: row.primary_email ?? row.primaryEmail ?? null,\n primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,\n status: row.status ?? null,\n lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,\n source: row.source ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n createdAt,\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Related-records mappers */\n/* -------------------------------------------------------------------------- */\n\nfunction asRows(value: unknown): Array<Record<string, unknown>> {\n return Array.isArray(value) ? (value as Array<Record<string, unknown>>) : []\n}\n\nexport function mapAddresses(rows: Array<Record<string, unknown>>) {\n return rows.map((address) => ({\n id: address.id,\n name: address.name ?? null,\n purpose: address.purpose ?? null,\n addressLine1: address.addressLine1 ?? null,\n addressLine2: address.addressLine2 ?? null,\n city: address.city ?? null,\n region: address.region ?? null,\n postalCode: address.postalCode ?? null,\n country: address.country ?? null,\n isPrimary: !!address.isPrimary,\n }))\n}\n\nexport function mapActivities(rows: Array<Record<string, unknown>>) {\n return rows.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIso(activity.occurredAt),\n createdAt: toIso(activity.createdAt),\n }))\n}\n\nexport function mapNotes(rows: Array<Record<string, unknown>>) {\n return rows.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? null,\n createdAt: toIso(comment.createdAt),\n }))\n}\n\nexport function mapTasks(rows: Array<Record<string, unknown>>) {\n return rows.map((task) => ({\n id: task.id,\n todoId: task.todoId ?? task.id,\n todoSource: task.todoSource ?? null,\n createdAt: toIso(task.createdAt),\n }))\n}\n\nexport function mapInteractions(rows: Array<Record<string, unknown>>) {\n return rows.map((interaction) => ({\n id: interaction.id,\n interactionType: interaction.interactionType,\n title: interaction.title ?? null,\n status: interaction.status,\n scheduledAt: toIso(interaction.scheduledAt),\n occurredAt: toIso(interaction.occurredAt),\n }))\n}\n\nexport type RelatedTag = { id: string; slug: string; label: string; color: string | null }\n\nexport function mapTags(rows: Array<Record<string, unknown>>): RelatedTag[] {\n return rows\n .map((tag) => {\n if (!tag || typeof tag !== 'object') return null\n const id = typeof tag.id === 'string' ? tag.id : null\n const label = typeof tag.label === 'string' ? tag.label : null\n if (!id || !label) return null\n const slug = typeof tag.slug === 'string' ? tag.slug : label\n const color = typeof tag.color === 'string' ? tag.color : null\n return { id, slug, label, color }\n })\n .filter((entry): entry is RelatedTag => entry !== null)\n}\n\nexport type RelatedDeal = {\n id: string\n title: string\n status: string | null\n pipelineStageId: string | null\n valueAmount: string | null\n valueCurrency: string | null\n}\n\nexport function mapDeals(rows: Array<Record<string, unknown>>): RelatedDeal[] {\n return rows\n .map((deal) => {\n if (!deal || typeof deal !== 'object') return null\n const id = typeof deal.id === 'string' ? deal.id : null\n if (!id) return null\n return {\n id,\n title: typeof deal.title === 'string' ? deal.title : '',\n status: typeof deal.status === 'string' ? deal.status : null,\n pipelineStageId: typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,\n valueAmount:\n typeof deal.valueAmount === 'string'\n ? deal.valueAmount\n : deal.valueAmount === null || deal.valueAmount === undefined\n ? null\n : String(deal.valueAmount),\n valueCurrency: typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,\n }\n })\n .filter((value): value is RelatedDeal => value !== null)\n}\n\nexport type RelatedPerson = {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n jobTitle: string | null\n department: string | null\n}\n\nexport function mapPeople(rows: Array<Record<string, unknown>>): RelatedPerson[] {\n return rows\n .map((person) => {\n if (!person || typeof person !== 'object') return null\n const id = typeof person.id === 'string' ? person.id : null\n const displayName = typeof person.displayName === 'string' ? person.displayName : null\n if (!id || !displayName) return null\n return {\n id,\n displayName,\n primaryEmail: typeof person.primaryEmail === 'string' ? person.primaryEmail : null,\n primaryPhone: typeof person.primaryPhone === 'string' ? person.primaryPhone : null,\n jobTitle: typeof person.jobTitle === 'string' ? person.jobTitle : null,\n department: typeof person.department === 'string' ? person.department : null,\n }\n })\n .filter((value): value is RelatedPerson => value !== null)\n}\n\nexport type CustomerRelatedRecords = {\n addresses: ReturnType<typeof mapAddresses>\n activities: ReturnType<typeof mapActivities>\n notes: ReturnType<typeof mapNotes>\n tasks: ReturnType<typeof mapTasks>\n interactions: ReturnType<typeof mapInteractions>\n tags: RelatedTag[]\n deals: RelatedDeal[]\n people?: RelatedPerson[]\n}\n\n/**\n * Builds the related-records block from a customers detail API payload\n * (`addresses`, `activities`, `comments`, `todos`, `interactions`, `tags`,\n * `deals`, and \u2014 for companies \u2014 `people`). The key order matches the\n * pre-refactor literals so the emitted shape is unchanged. Pass\n * `includePeople: true` to add the companies-only `people` collection.\n */\nexport function buildRelatedRecords(\n data: Record<string, unknown>,\n options: { includePeople?: boolean } = {},\n): CustomerRelatedRecords {\n const related: CustomerRelatedRecords = {\n addresses: mapAddresses(asRows(data.addresses)),\n activities: mapActivities(asRows(data.activities)),\n notes: mapNotes(asRows(data.comments)),\n tasks: mapTasks(asRows(data.todos)),\n interactions: mapInteractions(asRows(data.interactions)),\n tags: mapTags(asRows(data.tags)),\n deals: mapDeals(asRows(data.deals)),\n }\n if (options.includePeople) {\n related.people = mapPeople(asRows(data.people))\n }\n return related\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
createAiApiOperationRunner
|
|
5
5
|
} from "@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner";
|
|
6
6
|
import { assertTenantScope } from "./types.js";
|
|
7
|
+
import {
|
|
8
|
+
buildRelatedRecords,
|
|
9
|
+
toCustomerListSummary,
|
|
10
|
+
toIso
|
|
11
|
+
} from "./_shared.js";
|
|
7
12
|
const listCompaniesInput = z.object({
|
|
8
13
|
q: z.string().trim().optional().describe("Search text matched against display name / email / domain. Omit or leave empty to list all."),
|
|
9
14
|
limit: z.number().int().min(1).max(100).optional().describe("Maximum rows to return (default 50, max 100)."),
|
|
@@ -40,27 +45,13 @@ const listCompaniesTool = defineApiBackedAiTool({
|
|
|
40
45
|
const data = response.data ?? {};
|
|
41
46
|
const rawItems = Array.isArray(data.items) ? data.items : [];
|
|
42
47
|
return {
|
|
43
|
-
items: rawItems.map((row) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,
|
|
51
|
-
status: row.status ?? null,
|
|
52
|
-
lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,
|
|
53
|
-
source: row.source ?? null,
|
|
54
|
-
ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,
|
|
55
|
-
organizationId: row.organization_id ?? row.organizationId ?? null,
|
|
56
|
-
tenantId: row.tenant_id ?? row.tenantId ?? null,
|
|
57
|
-
domain: row.domain ?? null,
|
|
58
|
-
websiteUrl: row.website_url ?? row.websiteUrl ?? null,
|
|
59
|
-
industry: row.industry ?? null,
|
|
60
|
-
sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,
|
|
61
|
-
createdAt
|
|
62
|
-
};
|
|
63
|
-
}),
|
|
48
|
+
items: rawItems.map((row) => ({
|
|
49
|
+
...toCustomerListSummary(row),
|
|
50
|
+
domain: row.domain ?? null,
|
|
51
|
+
websiteUrl: row.website_url ?? row.websiteUrl ?? null,
|
|
52
|
+
industry: row.industry ?? null,
|
|
53
|
+
sizeBucket: row.size_bucket ?? row.sizeBucket ?? null
|
|
54
|
+
})),
|
|
64
55
|
total: typeof data.total === "number" ? data.total : 0,
|
|
65
56
|
limit,
|
|
66
57
|
offset
|
|
@@ -71,12 +62,6 @@ const getCompanyInput = z.object({
|
|
|
71
62
|
companyId: z.string().uuid().describe("Company entity id (UUID)."),
|
|
72
63
|
includeRelated: z.boolean().optional().describe("When true, include notes, activities, deals, people, addresses, tasks, and tags (each capped at 100).")
|
|
73
64
|
});
|
|
74
|
-
function toIsoCompany(value) {
|
|
75
|
-
if (!value) return null;
|
|
76
|
-
const dt = value instanceof Date ? value : new Date(String(value));
|
|
77
|
-
if (Number.isNaN(dt.getTime())) return null;
|
|
78
|
-
return dt.toISOString();
|
|
79
|
-
}
|
|
80
65
|
const getCompanyTool = {
|
|
81
66
|
name: "customers.get_company",
|
|
82
67
|
displayName: "Get company",
|
|
@@ -115,98 +100,7 @@ const getCompanyTool = {
|
|
|
115
100
|
const customFields = data.customFields ?? {};
|
|
116
101
|
let related = null;
|
|
117
102
|
if (includeRelated) {
|
|
118
|
-
|
|
119
|
-
const activities = Array.isArray(data.activities) ? data.activities : [];
|
|
120
|
-
const notes = Array.isArray(data.comments) ? data.comments : [];
|
|
121
|
-
const todos = Array.isArray(data.todos) ? data.todos : [];
|
|
122
|
-
const interactions = Array.isArray(data.interactions) ? data.interactions : [];
|
|
123
|
-
const tagsRows = Array.isArray(data.tags) ? data.tags : [];
|
|
124
|
-
const dealsRows = Array.isArray(data.deals) ? data.deals : [];
|
|
125
|
-
const peopleRows = Array.isArray(data.people) ? data.people : [];
|
|
126
|
-
related = {
|
|
127
|
-
addresses: addresses.map((address) => ({
|
|
128
|
-
id: address.id,
|
|
129
|
-
name: address.name ?? null,
|
|
130
|
-
purpose: address.purpose ?? null,
|
|
131
|
-
addressLine1: address.addressLine1 ?? null,
|
|
132
|
-
addressLine2: address.addressLine2 ?? null,
|
|
133
|
-
city: address.city ?? null,
|
|
134
|
-
region: address.region ?? null,
|
|
135
|
-
postalCode: address.postalCode ?? null,
|
|
136
|
-
country: address.country ?? null,
|
|
137
|
-
isPrimary: !!address.isPrimary
|
|
138
|
-
})),
|
|
139
|
-
activities: activities.map((activity) => ({
|
|
140
|
-
id: activity.id,
|
|
141
|
-
activityType: activity.activityType,
|
|
142
|
-
subject: activity.subject ?? null,
|
|
143
|
-
body: activity.body ?? null,
|
|
144
|
-
occurredAt: toIsoCompany(activity.occurredAt),
|
|
145
|
-
createdAt: toIsoCompany(activity.createdAt)
|
|
146
|
-
})),
|
|
147
|
-
notes: notes.map((comment) => ({
|
|
148
|
-
id: comment.id,
|
|
149
|
-
body: comment.body,
|
|
150
|
-
authorUserId: comment.authorUserId ?? null,
|
|
151
|
-
createdAt: toIsoCompany(comment.createdAt)
|
|
152
|
-
})),
|
|
153
|
-
tasks: todos.map((task) => ({
|
|
154
|
-
id: task.id,
|
|
155
|
-
todoId: task.todoId ?? task.id,
|
|
156
|
-
todoSource: task.todoSource ?? null,
|
|
157
|
-
createdAt: toIsoCompany(task.createdAt)
|
|
158
|
-
})),
|
|
159
|
-
interactions: interactions.map((interaction) => ({
|
|
160
|
-
id: interaction.id,
|
|
161
|
-
interactionType: interaction.interactionType,
|
|
162
|
-
title: interaction.title ?? null,
|
|
163
|
-
status: interaction.status,
|
|
164
|
-
scheduledAt: toIsoCompany(interaction.scheduledAt),
|
|
165
|
-
occurredAt: toIsoCompany(interaction.occurredAt)
|
|
166
|
-
})),
|
|
167
|
-
tags: tagsRows.map((tag) => {
|
|
168
|
-
if (!tag || typeof tag !== "object") return null;
|
|
169
|
-
const id = typeof tag.id === "string" ? tag.id : null;
|
|
170
|
-
const label = typeof tag.label === "string" ? tag.label : null;
|
|
171
|
-
if (!id || !label) return null;
|
|
172
|
-
const slug = typeof tag.slug === "string" ? tag.slug : label;
|
|
173
|
-
const color = typeof tag.color === "string" ? tag.color : null;
|
|
174
|
-
return { id, slug, label, color };
|
|
175
|
-
}).filter(
|
|
176
|
-
(entry) => entry !== null
|
|
177
|
-
),
|
|
178
|
-
deals: dealsRows.map((deal) => {
|
|
179
|
-
if (!deal || typeof deal !== "object") return null;
|
|
180
|
-
const id = typeof deal.id === "string" ? deal.id : null;
|
|
181
|
-
if (!id) return null;
|
|
182
|
-
return {
|
|
183
|
-
id,
|
|
184
|
-
title: typeof deal.title === "string" ? deal.title : "",
|
|
185
|
-
status: typeof deal.status === "string" ? deal.status : null,
|
|
186
|
-
pipelineStageId: typeof deal.pipelineStageId === "string" ? deal.pipelineStageId : null,
|
|
187
|
-
valueAmount: typeof deal.valueAmount === "string" ? deal.valueAmount : deal.valueAmount === null || deal.valueAmount === void 0 ? null : String(deal.valueAmount),
|
|
188
|
-
valueCurrency: typeof deal.valueCurrency === "string" ? deal.valueCurrency : null
|
|
189
|
-
};
|
|
190
|
-
}).filter(
|
|
191
|
-
(value) => value !== null
|
|
192
|
-
),
|
|
193
|
-
people: peopleRows.map((person) => {
|
|
194
|
-
if (!person || typeof person !== "object") return null;
|
|
195
|
-
const id = typeof person.id === "string" ? person.id : null;
|
|
196
|
-
const displayName = typeof person.displayName === "string" ? person.displayName : null;
|
|
197
|
-
if (!id || !displayName) return null;
|
|
198
|
-
return {
|
|
199
|
-
id,
|
|
200
|
-
displayName,
|
|
201
|
-
primaryEmail: typeof person.primaryEmail === "string" ? person.primaryEmail : null,
|
|
202
|
-
primaryPhone: typeof person.primaryPhone === "string" ? person.primaryPhone : null,
|
|
203
|
-
jobTitle: typeof person.jobTitle === "string" ? person.jobTitle : null,
|
|
204
|
-
department: typeof person.department === "string" ? person.department : null
|
|
205
|
-
};
|
|
206
|
-
}).filter(
|
|
207
|
-
(value) => value !== null
|
|
208
|
-
)
|
|
209
|
-
};
|
|
103
|
+
related = buildRelatedRecords(data, { includePeople: true });
|
|
210
104
|
}
|
|
211
105
|
return {
|
|
212
106
|
found: true,
|
|
@@ -222,8 +116,8 @@ const getCompanyTool = {
|
|
|
222
116
|
ownerUserId: companyRow.ownerUserId ?? null,
|
|
223
117
|
organizationId: companyRow.organizationId ?? null,
|
|
224
118
|
tenantId: companyRow.tenantId ?? null,
|
|
225
|
-
createdAt:
|
|
226
|
-
updatedAt:
|
|
119
|
+
createdAt: toIso(companyRow.createdAt),
|
|
120
|
+
updatedAt: toIso(companyRow.updatedAt)
|
|
227
121
|
},
|
|
228
122
|
profile: profileRow ? {
|
|
229
123
|
id: profileRow.id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customers/ai-tools/companies-pack.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_companies` is now an API-backed wrapper over\n * `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_company` to a single\n * in-process call to `GET /api/customers/companies/<id>?include=...` over the\n * documented aggregate detail route. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n */\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\n\nconst listCompaniesInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against display name / email / domain. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n tags: z.array(z.string().uuid()).optional().describe('Restrict to companies carrying at least one of these tag ids.'),\n })\n .passthrough()\n\ntype ListCompaniesInput = z.infer<typeof listCompaniesInput>\n\ntype ListCompaniesApiItem = {\n id?: string\n display_name?: string | null\n displayName?: string | null\n primary_email?: string | null\n primaryEmail?: string | null\n primary_phone?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycle_stage?: string | null\n lifecycleStage?: string | null\n source?: string | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n domain?: string | null\n website_url?: string | null\n websiteUrl?: string | null\n industry?: string | null\n size_bucket?: string | null\n sizeBucket?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListCompaniesApiResponse = {\n items?: ListCompaniesApiItem[]\n total?: number\n}\n\ntype ListCompaniesOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listCompaniesTool = defineApiBackedAiTool<\n ListCompaniesInput,\n ListCompaniesApiResponse,\n ListCompaniesOutput\n>({\n name: 'customers.list_companies',\n displayName: 'List companies',\n description:\n 'Search / list companies for the caller tenant + organization. Returns { items, total, limit, offset }.',\n inputSchema: listCompaniesInput,\n requiredFeatures: ['customers.companies.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/companies',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListCompaniesApiResponse\n const rawItems: ListCompaniesApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => {\n const createdAtRaw = row.created_at ?? row.createdAt ?? null\n const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null\n return {\n id: row.id,\n displayName: row.display_name ?? row.displayName ?? null,\n primaryEmail: row.primary_email ?? row.primaryEmail ?? null,\n primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,\n status: row.status ?? null,\n lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,\n source: row.source ?? null,\n ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,\n organizationId: row.organization_id ?? row.organizationId ?? null,\n tenantId: row.tenant_id ?? row.tenantId ?? null,\n domain: row.domain ?? null,\n websiteUrl: row.website_url ?? row.websiteUrl ?? null,\n industry: row.industry ?? null,\n sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,\n createdAt,\n }\n }),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getCompanyInput = z.object({\n companyId: z.string().uuid().describe('Company entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, people, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetCompanyInput = z.infer<typeof getCompanyInput>\n\nfunction toIsoCompany(value: unknown): string | null {\n if (!value) return null\n const dt = value instanceof Date ? value : new Date(String(value))\n if (Number.isNaN(dt.getTime())) return null\n return dt.toISOString()\n}\n\nconst getCompanyTool: CustomersAiToolDefinition = {\n name: 'customers.get_company',\n displayName: 'Get company',\n description:\n 'Fetch a company customer record by id with profile fields and (optionally) notes, activities, deals, people, addresses, tasks, tags, and custom fields. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getCompanyInput,\n requiredFeatures: ['customers.companies.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetCompanyInput = getCompanyInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/companies/${input.companyId}`,\n }\n if (includeRelated) {\n operation.query = {\n include: 'addresses,comments,activities,interactions,deals,todos,people',\n }\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run<Record<string, unknown>>(operation)\n if (!response.success) {\n if (response.statusCode === 404 || response.statusCode === 403) {\n return { found: false as const, companyId: input.companyId }\n }\n throw new Error(response.error ?? `Failed to fetch company ${input.companyId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const companyRow = (data.company ?? null) as Record<string, unknown> | null\n if (!companyRow) {\n return { found: false as const, companyId: input.companyId }\n }\n const profileRow = (data.profile ?? null) as Record<string, unknown> | null\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: Record<string, unknown> | null = null\n if (includeRelated) {\n const addresses = Array.isArray(data.addresses) ? (data.addresses as Array<Record<string, unknown>>) : []\n const activities = Array.isArray(data.activities) ? (data.activities as Array<Record<string, unknown>>) : []\n const notes = Array.isArray(data.comments) ? (data.comments as Array<Record<string, unknown>>) : []\n const todos = Array.isArray(data.todos) ? (data.todos as Array<Record<string, unknown>>) : []\n const interactions = Array.isArray(data.interactions) ? (data.interactions as Array<Record<string, unknown>>) : []\n const tagsRows = Array.isArray(data.tags) ? (data.tags as Array<Record<string, unknown>>) : []\n const dealsRows = Array.isArray(data.deals) ? (data.deals as Array<Record<string, unknown>>) : []\n const peopleRows = Array.isArray(data.people) ? (data.people as Array<Record<string, unknown>>) : []\n related = {\n addresses: addresses.map((address) => ({\n id: address.id,\n name: address.name ?? null,\n purpose: address.purpose ?? null,\n addressLine1: address.addressLine1 ?? null,\n addressLine2: address.addressLine2 ?? null,\n city: address.city ?? null,\n region: address.region ?? null,\n postalCode: address.postalCode ?? null,\n country: address.country ?? null,\n isPrimary: !!address.isPrimary,\n })),\n activities: activities.map((activity) => ({\n id: activity.id,\n activityType: activity.activityType,\n subject: activity.subject ?? null,\n body: activity.body ?? null,\n occurredAt: toIsoCompany(activity.occurredAt),\n createdAt: toIsoCompany(activity.createdAt),\n })),\n notes: notes.map((comment) => ({\n id: comment.id,\n body: comment.body,\n authorUserId: comment.authorUserId ?? null,\n createdAt: toIsoCompany(comment.createdAt),\n })),\n tasks: todos.map((task) => ({\n id: task.id,\n todoId: task.todoId ?? task.id,\n todoSource: task.todoSource ?? null,\n createdAt: toIsoCompany(task.createdAt),\n })),\n interactions: interactions.map((interaction) => ({\n id: interaction.id,\n interactionType: interaction.interactionType,\n title: interaction.title ?? null,\n status: interaction.status,\n scheduledAt: toIsoCompany(interaction.scheduledAt),\n occurredAt: toIsoCompany(interaction.occurredAt),\n })),\n tags: tagsRows\n .map((tag) => {\n if (!tag || typeof tag !== 'object') return null\n const id = typeof tag.id === 'string' ? tag.id : null\n const label = typeof tag.label === 'string' ? tag.label : null\n if (!id || !label) return null\n const slug = typeof tag.slug === 'string' ? tag.slug : label\n const color = typeof tag.color === 'string' ? tag.color : null\n return { id, slug, label, color }\n })\n .filter(\n (entry): entry is { id: string; slug: string; label: string; color: string | null } =>\n entry !== null,\n ),\n deals: dealsRows\n .map((deal) => {\n if (!deal || typeof deal !== 'object') return null\n const id = typeof deal.id === 'string' ? deal.id : null\n if (!id) return null\n return {\n id,\n title: typeof deal.title === 'string' ? deal.title : '',\n status: typeof deal.status === 'string' ? deal.status : null,\n pipelineStageId:\n typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,\n valueAmount:\n typeof deal.valueAmount === 'string'\n ? deal.valueAmount\n : deal.valueAmount === null || deal.valueAmount === undefined\n ? null\n : String(deal.valueAmount),\n valueCurrency:\n typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,\n }\n })\n .filter(\n (\n value,\n ): value is {\n id: string\n title: string\n status: string | null\n pipelineStageId: string | null\n valueAmount: string | null\n valueCurrency: string | null\n } => value !== null,\n ),\n people: peopleRows\n .map((person) => {\n if (!person || typeof person !== 'object') return null\n const id = typeof person.id === 'string' ? person.id : null\n const displayName = typeof person.displayName === 'string' ? person.displayName : null\n if (!id || !displayName) return null\n return {\n id,\n displayName,\n primaryEmail:\n typeof person.primaryEmail === 'string' ? person.primaryEmail : null,\n primaryPhone:\n typeof person.primaryPhone === 'string' ? person.primaryPhone : null,\n jobTitle: typeof person.jobTitle === 'string' ? person.jobTitle : null,\n department: typeof person.department === 'string' ? person.department : null,\n }\n })\n .filter(\n (\n value,\n ): value is {\n id: string\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n jobTitle: string | null\n department: string | null\n } => value !== null,\n ),\n }\n }\n return {\n found: true as const,\n company: {\n id: companyRow.id,\n displayName: companyRow.displayName ?? null,\n description: companyRow.description ?? null,\n primaryEmail: companyRow.primaryEmail ?? null,\n primaryPhone: companyRow.primaryPhone ?? null,\n status: companyRow.status ?? null,\n lifecycleStage: companyRow.lifecycleStage ?? null,\n source: companyRow.source ?? null,\n ownerUserId: companyRow.ownerUserId ?? null,\n organizationId: companyRow.organizationId ?? null,\n tenantId: companyRow.tenantId ?? null,\n createdAt: toIsoCompany(companyRow.createdAt),\n updatedAt: toIsoCompany(companyRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n legalName: profileRow.legalName ?? null,\n brandName: profileRow.brandName ?? null,\n domain: profileRow.domain ?? null,\n websiteUrl: profileRow.websiteUrl ?? null,\n industry: profileRow.industry ?? null,\n sizeBucket: profileRow.sizeBucket ?? null,\n annualRevenue: profileRow.annualRevenue ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const companiesAiTools: CustomersAiToolDefinition[] = [listCompaniesTool, getCompanyTool]\n\nexport default companiesAiTools\n"],
|
|
5
|
-
"mappings": "AAaA,SAAS,SAAS;AAClB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,OAGK;AACP,SAAS,yBAAoF;
|
|
4
|
+
"sourcesContent": ["/**\n * `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).\n *\n * Phase 3a of `.ai/specs/implemented/2026-04-27-ai-tools-api-backed-dry-refactor.md`:\n * `customers.list_companies` is now an API-backed wrapper over\n * `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n *\n * Phase 3c of the same spec migrates `customers.get_company` to a single\n * in-process call to `GET /api/customers/companies/<id>?include=...` over the\n * documented aggregate detail route. Tool name, schema, requiredFeatures, and\n * output shape are unchanged.\n */\nimport { z } from 'zod'\nimport { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'\nimport {\n createAiApiOperationRunner,\n type AiApiOperationRequest,\n type AiToolExecutionContext,\n} from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'\nimport { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'\nimport {\n buildRelatedRecords,\n toCustomerListSummary,\n toIso,\n type CustomerRelatedRecords,\n} from './_shared'\n\nconst listCompaniesInput = z\n .object({\n q: z.string().trim().optional().describe('Search text matched against display name / email / domain. Omit or leave empty to list all.'),\n limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),\n offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),\n tags: z.array(z.string().uuid()).optional().describe('Restrict to companies carrying at least one of these tag ids.'),\n })\n .passthrough()\n\ntype ListCompaniesInput = z.infer<typeof listCompaniesInput>\n\ntype ListCompaniesApiItem = {\n id?: string\n display_name?: string | null\n displayName?: string | null\n primary_email?: string | null\n primaryEmail?: string | null\n primary_phone?: string | null\n primaryPhone?: string | null\n status?: string | null\n lifecycle_stage?: string | null\n lifecycleStage?: string | null\n source?: string | null\n owner_user_id?: string | null\n ownerUserId?: string | null\n organization_id?: string | null\n organizationId?: string | null\n tenant_id?: string | null\n tenantId?: string | null\n domain?: string | null\n website_url?: string | null\n websiteUrl?: string | null\n industry?: string | null\n size_bucket?: string | null\n sizeBucket?: string | null\n created_at?: string | null\n createdAt?: string | null\n}\n\ntype ListCompaniesApiResponse = {\n items?: ListCompaniesApiItem[]\n total?: number\n}\n\ntype ListCompaniesOutput = {\n items: Array<Record<string, unknown>>\n total: number\n limit: number\n offset: number\n}\n\nconst listCompaniesTool = defineApiBackedAiTool<\n ListCompaniesInput,\n ListCompaniesApiResponse,\n ListCompaniesOutput\n>({\n name: 'customers.list_companies',\n displayName: 'List companies',\n description:\n 'Search / list companies for the caller tenant + organization. Returns { items, total, limit, offset }.',\n inputSchema: listCompaniesInput,\n requiredFeatures: ['customers.companies.view'],\n toOperation: (input, ctx) => {\n assertTenantScope(ctx as unknown as CustomersToolContext)\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const page = Math.floor(offset / limit) + 1\n\n const query: Record<string, string | number | boolean | null | undefined> = {\n page,\n pageSize: limit,\n }\n if (input.q?.trim()) query.search = input.q.trim()\n if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: '/customers/companies',\n query,\n }\n return operation\n },\n mapResponse: (response, input) => {\n const limit = input.limit ?? 50\n const offset = input.offset ?? 0\n const data = (response.data ?? {}) as ListCompaniesApiResponse\n const rawItems: ListCompaniesApiItem[] = Array.isArray(data.items) ? data.items : []\n return {\n items: rawItems.map((row) => ({\n ...toCustomerListSummary(row),\n domain: row.domain ?? null,\n websiteUrl: row.website_url ?? row.websiteUrl ?? null,\n industry: row.industry ?? null,\n sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,\n })),\n total: typeof data.total === 'number' ? data.total : 0,\n limit,\n offset,\n }\n },\n}) as unknown as CustomersAiToolDefinition\n\nconst getCompanyInput = z.object({\n companyId: z.string().uuid().describe('Company entity id (UUID).'),\n includeRelated: z\n .boolean()\n .optional()\n .describe('When true, include notes, activities, deals, people, addresses, tasks, and tags (each capped at 100).'),\n})\n\ntype GetCompanyInput = z.infer<typeof getCompanyInput>\n\nconst getCompanyTool: CustomersAiToolDefinition = {\n name: 'customers.get_company',\n displayName: 'Get company',\n description:\n 'Fetch a company customer record by id with profile fields and (optionally) notes, activities, deals, people, addresses, tasks, tags, and custom fields. Returns { found: false } when outside tenant/org scope.',\n inputSchema: getCompanyInput,\n requiredFeatures: ['customers.companies.view'],\n tags: ['read', 'customers'],\n handler: async (rawInput, ctx) => {\n const { tenantId: _tenantId } = assertTenantScope(ctx)\n void _tenantId\n const input: GetCompanyInput = getCompanyInput.parse(rawInput)\n const includeRelated = !!input.includeRelated\n\n const operation: AiApiOperationRequest = {\n method: 'GET',\n path: `/customers/companies/${input.companyId}`,\n }\n if (includeRelated) {\n operation.query = {\n include: 'addresses,comments,activities,interactions,deals,todos,people',\n }\n }\n\n const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)\n const response = await runner.run<Record<string, unknown>>(operation)\n if (!response.success) {\n if (response.statusCode === 404 || response.statusCode === 403) {\n return { found: false as const, companyId: input.companyId }\n }\n throw new Error(response.error ?? `Failed to fetch company ${input.companyId}`)\n }\n const data = (response.data ?? {}) as Record<string, unknown>\n const companyRow = (data.company ?? null) as Record<string, unknown> | null\n if (!companyRow) {\n return { found: false as const, companyId: input.companyId }\n }\n const profileRow = (data.profile ?? null) as Record<string, unknown> | null\n const customFields = (data.customFields ?? {}) as Record<string, unknown>\n\n let related: CustomerRelatedRecords | null = null\n if (includeRelated) {\n related = buildRelatedRecords(data, { includePeople: true })\n }\n return {\n found: true as const,\n company: {\n id: companyRow.id,\n displayName: companyRow.displayName ?? null,\n description: companyRow.description ?? null,\n primaryEmail: companyRow.primaryEmail ?? null,\n primaryPhone: companyRow.primaryPhone ?? null,\n status: companyRow.status ?? null,\n lifecycleStage: companyRow.lifecycleStage ?? null,\n source: companyRow.source ?? null,\n ownerUserId: companyRow.ownerUserId ?? null,\n organizationId: companyRow.organizationId ?? null,\n tenantId: companyRow.tenantId ?? null,\n createdAt: toIso(companyRow.createdAt),\n updatedAt: toIso(companyRow.updatedAt),\n },\n profile: profileRow\n ? {\n id: profileRow.id,\n legalName: profileRow.legalName ?? null,\n brandName: profileRow.brandName ?? null,\n domain: profileRow.domain ?? null,\n websiteUrl: profileRow.websiteUrl ?? null,\n industry: profileRow.industry ?? null,\n sizeBucket: profileRow.sizeBucket ?? null,\n annualRevenue: profileRow.annualRevenue ?? null,\n }\n : null,\n customFields,\n related,\n }\n },\n}\n\nexport const companiesAiTools: CustomersAiToolDefinition[] = [listCompaniesTool, getCompanyTool]\n\nexport default companiesAiTools\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -8,16 +8,14 @@ import {
|
|
|
8
8
|
CustomerPersonProfile
|
|
9
9
|
} from "../data/entities.js";
|
|
10
10
|
import { assertTenantScope } from "./types.js";
|
|
11
|
+
import {
|
|
12
|
+
buildRelatedRecords,
|
|
13
|
+
buildScope,
|
|
14
|
+
resolveEm,
|
|
15
|
+
toCustomerListSummary,
|
|
16
|
+
toIso
|
|
17
|
+
} from "./_shared.js";
|
|
11
18
|
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
12
|
-
function resolveEm(ctx) {
|
|
13
|
-
return ctx.container.resolve("em");
|
|
14
|
-
}
|
|
15
|
-
function buildScope(ctx, tenantId) {
|
|
16
|
-
return {
|
|
17
|
-
tenantId,
|
|
18
|
-
organizationId: ctx.organizationId
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
19
|
const listPeopleInput = z.object({
|
|
22
20
|
q: z.string().trim().optional().describe("Optional search text matched against display name / email / phone. Omit or leave empty to list all."),
|
|
23
21
|
limit: z.number().int().min(1).max(100).optional().describe("Maximum rows to return (default 50, max 100)."),
|
|
@@ -73,23 +71,7 @@ const listPeopleTool = defineApiBackedAiTool({
|
|
|
73
71
|
const data = response.data ?? {};
|
|
74
72
|
const rawItems = Array.isArray(data.items) ? data.items : [];
|
|
75
73
|
return {
|
|
76
|
-
items: rawItems.map((row) =>
|
|
77
|
-
const createdAtRaw = row.created_at ?? row.createdAt ?? null;
|
|
78
|
-
const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null;
|
|
79
|
-
return {
|
|
80
|
-
id: row.id,
|
|
81
|
-
displayName: row.display_name ?? row.displayName ?? null,
|
|
82
|
-
primaryEmail: row.primary_email ?? row.primaryEmail ?? null,
|
|
83
|
-
primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,
|
|
84
|
-
status: row.status ?? null,
|
|
85
|
-
lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,
|
|
86
|
-
source: row.source ?? null,
|
|
87
|
-
ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,
|
|
88
|
-
organizationId: row.organization_id ?? row.organizationId ?? null,
|
|
89
|
-
tenantId: row.tenant_id ?? row.tenantId ?? null,
|
|
90
|
-
createdAt
|
|
91
|
-
};
|
|
92
|
-
}),
|
|
74
|
+
items: rawItems.map((row) => toCustomerListSummary(row)),
|
|
93
75
|
total: typeof data.total === "number" ? data.total : 0,
|
|
94
76
|
limit,
|
|
95
77
|
offset
|
|
@@ -100,12 +82,6 @@ const getPersonInput = z.object({
|
|
|
100
82
|
personId: z.string().uuid().describe("Person entity id (UUID)."),
|
|
101
83
|
includeRelated: z.boolean().optional().describe("When true, include notes, activities, deals, addresses, tasks, and tags (each capped at 100).")
|
|
102
84
|
});
|
|
103
|
-
function toIso(value) {
|
|
104
|
-
if (!value) return null;
|
|
105
|
-
const dt = value instanceof Date ? value : new Date(String(value));
|
|
106
|
-
if (Number.isNaN(dt.getTime())) return null;
|
|
107
|
-
return dt.toISOString();
|
|
108
|
-
}
|
|
109
85
|
const getPersonTool = {
|
|
110
86
|
name: "customers.get_person",
|
|
111
87
|
displayName: "Get person",
|
|
@@ -141,81 +117,7 @@ const getPersonTool = {
|
|
|
141
117
|
const customFields = data.customFields ?? {};
|
|
142
118
|
let related = null;
|
|
143
119
|
if (includeRelated) {
|
|
144
|
-
|
|
145
|
-
const activities = Array.isArray(data.activities) ? data.activities : [];
|
|
146
|
-
const notes = Array.isArray(data.comments) ? data.comments : [];
|
|
147
|
-
const todos = Array.isArray(data.todos) ? data.todos : [];
|
|
148
|
-
const interactions = Array.isArray(data.interactions) ? data.interactions : [];
|
|
149
|
-
const tagsRows = Array.isArray(data.tags) ? data.tags : [];
|
|
150
|
-
const dealsRows = Array.isArray(data.deals) ? data.deals : [];
|
|
151
|
-
related = {
|
|
152
|
-
addresses: addresses.map((address) => ({
|
|
153
|
-
id: address.id,
|
|
154
|
-
name: address.name ?? null,
|
|
155
|
-
purpose: address.purpose ?? null,
|
|
156
|
-
addressLine1: address.addressLine1 ?? null,
|
|
157
|
-
addressLine2: address.addressLine2 ?? null,
|
|
158
|
-
city: address.city ?? null,
|
|
159
|
-
region: address.region ?? null,
|
|
160
|
-
postalCode: address.postalCode ?? null,
|
|
161
|
-
country: address.country ?? null,
|
|
162
|
-
isPrimary: !!address.isPrimary
|
|
163
|
-
})),
|
|
164
|
-
activities: activities.map((activity) => ({
|
|
165
|
-
id: activity.id,
|
|
166
|
-
activityType: activity.activityType,
|
|
167
|
-
subject: activity.subject ?? null,
|
|
168
|
-
body: activity.body ?? null,
|
|
169
|
-
occurredAt: toIso(activity.occurredAt),
|
|
170
|
-
createdAt: toIso(activity.createdAt)
|
|
171
|
-
})),
|
|
172
|
-
notes: notes.map((comment) => ({
|
|
173
|
-
id: comment.id,
|
|
174
|
-
body: comment.body,
|
|
175
|
-
authorUserId: comment.authorUserId ?? null,
|
|
176
|
-
createdAt: toIso(comment.createdAt)
|
|
177
|
-
})),
|
|
178
|
-
tasks: todos.map((task) => ({
|
|
179
|
-
id: task.id,
|
|
180
|
-
todoId: task.todoId ?? task.id,
|
|
181
|
-
todoSource: task.todoSource ?? null,
|
|
182
|
-
createdAt: toIso(task.createdAt)
|
|
183
|
-
})),
|
|
184
|
-
interactions: interactions.map((interaction) => ({
|
|
185
|
-
id: interaction.id,
|
|
186
|
-
interactionType: interaction.interactionType,
|
|
187
|
-
title: interaction.title ?? null,
|
|
188
|
-
status: interaction.status,
|
|
189
|
-
scheduledAt: toIso(interaction.scheduledAt),
|
|
190
|
-
occurredAt: toIso(interaction.occurredAt)
|
|
191
|
-
})),
|
|
192
|
-
tags: tagsRows.map((tag) => {
|
|
193
|
-
if (!tag || typeof tag !== "object") return null;
|
|
194
|
-
const id = typeof tag.id === "string" ? tag.id : null;
|
|
195
|
-
const label = typeof tag.label === "string" ? tag.label : null;
|
|
196
|
-
if (!id || !label) return null;
|
|
197
|
-
const slug = typeof tag.slug === "string" ? tag.slug : label;
|
|
198
|
-
const color = typeof tag.color === "string" ? tag.color : null;
|
|
199
|
-
return { id, slug, label, color };
|
|
200
|
-
}).filter(
|
|
201
|
-
(entry) => entry !== null
|
|
202
|
-
),
|
|
203
|
-
deals: dealsRows.map((deal) => {
|
|
204
|
-
if (!deal || typeof deal !== "object") return null;
|
|
205
|
-
const id = typeof deal.id === "string" ? deal.id : null;
|
|
206
|
-
if (!id) return null;
|
|
207
|
-
return {
|
|
208
|
-
id,
|
|
209
|
-
title: typeof deal.title === "string" ? deal.title : "",
|
|
210
|
-
status: typeof deal.status === "string" ? deal.status : null,
|
|
211
|
-
pipelineStageId: typeof deal.pipelineStageId === "string" ? deal.pipelineStageId : null,
|
|
212
|
-
valueAmount: typeof deal.valueAmount === "string" ? deal.valueAmount : deal.valueAmount === null || deal.valueAmount === void 0 ? null : String(deal.valueAmount),
|
|
213
|
-
valueCurrency: typeof deal.valueCurrency === "string" ? deal.valueCurrency : null
|
|
214
|
-
};
|
|
215
|
-
}).filter(
|
|
216
|
-
(value) => value !== null
|
|
217
|
-
)
|
|
218
|
-
};
|
|
120
|
+
related = buildRelatedRecords(data);
|
|
219
121
|
}
|
|
220
122
|
return {
|
|
221
123
|
found: true,
|