@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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.
Files changed (163) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +13 -1
  3. package/dist/helpers/integration/api.js +29 -16
  4. package/dist/helpers/integration/api.js.map +2 -2
  5. package/dist/helpers/integration/auth.js +11 -6
  6. package/dist/helpers/integration/auth.js.map +3 -3
  7. package/dist/modules/auth/commands/roles.js +9 -12
  8. package/dist/modules/auth/commands/roles.js.map +2 -2
  9. package/dist/modules/catalog/ai-agents-context.js +147 -0
  10. package/dist/modules/catalog/ai-agents-context.js.map +7 -0
  11. package/dist/modules/catalog/ai-agents.js +383 -0
  12. package/dist/modules/catalog/ai-agents.js.map +7 -0
  13. package/dist/modules/catalog/ai-tools/_shared.js +318 -0
  14. package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
  15. package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
  16. package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
  17. package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
  18. package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
  19. package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
  20. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
  21. package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
  22. package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
  23. package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
  24. package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
  25. package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
  26. package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
  27. package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
  28. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
  29. package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
  30. package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
  31. package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
  32. package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
  33. package/dist/modules/catalog/ai-tools/types.js +10 -0
  34. package/dist/modules/catalog/ai-tools/types.js.map +7 -0
  35. package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
  36. package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
  37. package/dist/modules/catalog/ai-tools.js +28 -0
  38. package/dist/modules/catalog/ai-tools.js.map +7 -0
  39. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
  40. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
  41. package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
  42. package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
  43. package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
  44. package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
  45. package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
  46. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  47. package/dist/modules/catalog/events.js +7 -4
  48. package/dist/modules/catalog/events.js.map +2 -2
  49. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
  50. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
  51. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
  52. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
  53. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
  54. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
  55. package/dist/modules/catalog/widgets/injection-table.js +13 -1
  56. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  57. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
  58. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
  59. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
  60. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
  61. package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
  62. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  63. package/dist/modules/customers/ai-agents-context.js +96 -0
  64. package/dist/modules/customers/ai-agents-context.js.map +7 -0
  65. package/dist/modules/customers/ai-agents.js +244 -0
  66. package/dist/modules/customers/ai-agents.js.map +7 -0
  67. package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
  68. package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
  69. package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
  70. package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
  71. package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
  72. package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
  73. package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
  74. package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
  75. package/dist/modules/customers/ai-tools/people-pack.js +261 -0
  76. package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
  77. package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
  78. package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
  79. package/dist/modules/customers/ai-tools/types.js +10 -0
  80. package/dist/modules/customers/ai-tools/types.js.map +7 -0
  81. package/dist/modules/customers/ai-tools.js +20 -0
  82. package/dist/modules/customers/ai-tools.js.map +7 -0
  83. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
  84. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
  85. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
  86. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
  87. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
  88. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
  89. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
  90. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
  91. package/dist/modules/customers/widgets/injection-table.js +26 -0
  92. package/dist/modules/customers/widgets/injection-table.js.map +7 -0
  93. package/dist/modules/inbox_ops/ai-tools.js +4 -0
  94. package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
  95. package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
  96. package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
  97. package/dist/modules/notifications/setup.js +13 -0
  98. package/dist/modules/notifications/setup.js.map +7 -0
  99. package/jest.config.cjs +1 -0
  100. package/jest.setup.ts +18 -0
  101. package/package.json +5 -3
  102. package/src/helpers/integration/api.ts +38 -16
  103. package/src/helpers/integration/auth.ts +13 -6
  104. package/src/modules/auth/commands/roles.ts +10 -12
  105. package/src/modules/catalog/AGENTS.md +11 -0
  106. package/src/modules/catalog/ai-agents-context.ts +239 -0
  107. package/src/modules/catalog/ai-agents.ts +525 -0
  108. package/src/modules/catalog/ai-tools/_shared.ts +487 -0
  109. package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
  110. package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
  111. package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
  112. package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
  113. package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
  114. package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
  115. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
  116. package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
  117. package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
  118. package/src/modules/catalog/ai-tools/types.ts +81 -0
  119. package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
  120. package/src/modules/catalog/ai-tools.ts +78 -0
  121. package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
  122. package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
  123. package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
  124. package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
  125. package/src/modules/catalog/events.ts +7 -4
  126. package/src/modules/catalog/i18n/de.json +17 -0
  127. package/src/modules/catalog/i18n/en.json +17 -0
  128. package/src/modules/catalog/i18n/es.json +17 -0
  129. package/src/modules/catalog/i18n/pl.json +17 -0
  130. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
  131. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
  132. package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
  133. package/src/modules/catalog/widgets/injection-table.ts +12 -0
  134. package/src/modules/customer_accounts/i18n/de.json +5 -0
  135. package/src/modules/customer_accounts/i18n/en.json +5 -0
  136. package/src/modules/customer_accounts/i18n/es.json +5 -0
  137. package/src/modules/customer_accounts/i18n/pl.json +5 -0
  138. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
  139. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
  140. package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
  141. package/src/modules/customers/AGENTS.md +13 -0
  142. package/src/modules/customers/ai-agents-context.ts +150 -0
  143. package/src/modules/customers/ai-agents.ts +355 -0
  144. package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
  145. package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
  146. package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
  147. package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
  148. package/src/modules/customers/ai-tools/people-pack.ts +369 -0
  149. package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
  150. package/src/modules/customers/ai-tools/types.ts +76 -0
  151. package/src/modules/customers/ai-tools.ts +34 -0
  152. package/src/modules/customers/i18n/de.json +25 -0
  153. package/src/modules/customers/i18n/en.json +25 -0
  154. package/src/modules/customers/i18n/es.json +25 -0
  155. package/src/modules/customers/i18n/pl.json +25 -0
  156. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
  157. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
  158. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
  159. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
  160. package/src/modules/customers/widgets/injection-table.ts +41 -0
  161. package/src/modules/inbox_ops/ai-tools.ts +4 -0
  162. package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
  163. package/src/modules/notifications/setup.ts +11 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * `customers.list_addresses` + `customers.list_tags` (Phase 1 WS-C, Step 3.9).
3
+ */
4
+ import type { EntityManager } from '@mikro-orm/postgresql'
5
+ import { z } from 'zod'
6
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
7
+ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
8
+ import { CustomerAddress, CustomerTag } from '../data/entities'
9
+ import { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'
10
+
11
+ function resolveEm(ctx: CustomersToolContext): EntityManager {
12
+ return ctx.container.resolve<EntityManager>('em')
13
+ }
14
+
15
+ function buildScope(ctx: CustomersToolContext, tenantId: string) {
16
+ return { tenantId, organizationId: ctx.organizationId }
17
+ }
18
+
19
+ const listAddressesInput = z.object({
20
+ entityType: z.enum(['person', 'company']).describe('Parent entity kind.'),
21
+ entityId: z.string().uuid().describe('Parent person/company entity id.'),
22
+ limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 100).'),
23
+ offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
24
+ })
25
+
26
+ const listAddressesTool: CustomersAiToolDefinition = {
27
+ name: 'customers.list_addresses',
28
+ displayName: 'List addresses',
29
+ description:
30
+ 'List addresses attached to a person or company (tenant + organization scoped). `entityType` is informational; the actual filter is by `entityId`.',
31
+ inputSchema: listAddressesInput,
32
+ // Addresses share the same route-level guard as activities in the existing
33
+ // route handler (`customers.activities.view`).
34
+ requiredFeatures: ['customers.activities.view'],
35
+ tags: ['read', 'customers'],
36
+ handler: async (rawInput, ctx) => {
37
+ const { tenantId } = assertTenantScope(ctx)
38
+ const input = listAddressesInput.parse(rawInput)
39
+ const em = resolveEm(ctx)
40
+ const limit = input.limit ?? 100
41
+ const offset = input.offset ?? 0
42
+ const where: Record<string, unknown> = { tenantId, entity: input.entityId }
43
+ if (ctx.organizationId) where.organizationId = ctx.organizationId
44
+ const [rows, total] = await Promise.all([
45
+ findWithDecryption<CustomerAddress>(
46
+ em,
47
+ CustomerAddress,
48
+ where as any,
49
+ { limit, offset, orderBy: { isPrimary: 'desc', createdAt: 'desc' } as any } as any,
50
+ buildScope(ctx, tenantId),
51
+ ),
52
+ em.count(CustomerAddress, where as any),
53
+ ])
54
+ const filtered = rows.filter((row) => row.tenantId === tenantId)
55
+ return {
56
+ entityType: input.entityType,
57
+ entityId: input.entityId,
58
+ items: filtered.map((row) => ({
59
+ id: row.id,
60
+ name: row.name ?? null,
61
+ purpose: row.purpose ?? null,
62
+ companyName: row.companyName ?? null,
63
+ addressLine1: row.addressLine1,
64
+ addressLine2: row.addressLine2 ?? null,
65
+ buildingNumber: row.buildingNumber ?? null,
66
+ flatNumber: row.flatNumber ?? null,
67
+ city: row.city ?? null,
68
+ region: row.region ?? null,
69
+ postalCode: row.postalCode ?? null,
70
+ country: row.country ?? null,
71
+ latitude: row.latitude ?? null,
72
+ longitude: row.longitude ?? null,
73
+ isPrimary: !!row.isPrimary,
74
+ organizationId: row.organizationId ?? null,
75
+ tenantId: row.tenantId ?? null,
76
+ createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
77
+ })),
78
+ total,
79
+ limit,
80
+ offset,
81
+ }
82
+ },
83
+ }
84
+
85
+ const listTagsInput = z
86
+ .object({
87
+ q: z.string().trim().optional().describe('Fuzzy search against tag label / slug. Omit or leave empty to list all.'),
88
+ limit: z.number().int().min(1).max(100).optional().describe('Max rows (default 100).'),
89
+ offset: z.number().int().min(0).optional().describe('Rows to skip (default 0).'),
90
+ })
91
+ .passthrough()
92
+
93
+ const listTagsTool: CustomersAiToolDefinition = {
94
+ name: 'customers.list_tags',
95
+ displayName: 'List tags',
96
+ description:
97
+ 'List available customer tags (slug, label, color, description) scoped to tenant + organization.',
98
+ inputSchema: listTagsInput,
99
+ // Tag administration routes require `customers.activities.*` in the
100
+ // existing codebase; keep the same least-privilege view feature here.
101
+ requiredFeatures: ['customers.activities.view'],
102
+ tags: ['read', 'customers'],
103
+ handler: async (rawInput, ctx) => {
104
+ const { tenantId } = assertTenantScope(ctx)
105
+ const input = listTagsInput.parse(rawInput)
106
+ const em = resolveEm(ctx)
107
+ const limit = input.limit ?? 100
108
+ const offset = input.offset ?? 0
109
+ const where: Record<string, unknown> = { tenantId }
110
+ if (ctx.organizationId) where.organizationId = ctx.organizationId
111
+ if (input.q) {
112
+ const pattern = `%${escapeLikePattern(input.q)}%`
113
+ where.$or = [{ label: { $ilike: pattern } }, { slug: { $ilike: pattern } }]
114
+ }
115
+ const [rows, total] = await Promise.all([
116
+ findWithDecryption<CustomerTag>(
117
+ em,
118
+ CustomerTag,
119
+ where as any,
120
+ { limit, offset, orderBy: { label: 'asc' } as any } as any,
121
+ buildScope(ctx, tenantId),
122
+ ),
123
+ em.count(CustomerTag, where as any),
124
+ ])
125
+ const filtered = rows.filter((row) => row.tenantId === tenantId)
126
+ return {
127
+ items: filtered.map((row) => ({
128
+ id: row.id,
129
+ slug: row.slug,
130
+ label: row.label,
131
+ color: row.color ?? null,
132
+ description: row.description ?? null,
133
+ organizationId: row.organizationId ?? null,
134
+ tenantId: row.tenantId ?? null,
135
+ })),
136
+ total,
137
+ limit,
138
+ offset,
139
+ }
140
+ },
141
+ }
142
+
143
+ export const addressesTagsAiTools: CustomersAiToolDefinition[] = [listAddressesTool, listTagsTool]
144
+
145
+ export default addressesTagsAiTools
@@ -0,0 +1,362 @@
1
+ /**
2
+ * `customers.list_companies` + `customers.get_company` (Phase 1 WS-C, Step 3.9).
3
+ *
4
+ * Phase 3a of `.ai/specs/2026-04-27-ai-tools-api-backed-dry-refactor.md`:
5
+ * `customers.list_companies` is now an API-backed wrapper over
6
+ * `GET /api/customers/companies`. Tool name, schema, requiredFeatures, and
7
+ * output shape are unchanged.
8
+ *
9
+ * Phase 3c of the same spec migrates `customers.get_company` to a single
10
+ * in-process call to `GET /api/customers/companies/<id>?include=...` over the
11
+ * documented aggregate detail route. Tool name, schema, requiredFeatures, and
12
+ * output shape are unchanged.
13
+ */
14
+ import { z } from 'zod'
15
+ import { defineApiBackedAiTool } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool'
16
+ import {
17
+ createAiApiOperationRunner,
18
+ type AiApiOperationRequest,
19
+ type AiToolExecutionContext,
20
+ } from '@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner'
21
+ import { assertTenantScope, type CustomersAiToolDefinition, type CustomersToolContext } from './types'
22
+
23
+ const listCompaniesInput = z
24
+ .object({
25
+ q: z.string().trim().optional().describe('Search text matched against display name / email / domain. Omit or leave empty to list all.'),
26
+ limit: z.number().int().min(1).max(100).optional().describe('Maximum rows to return (default 50, max 100).'),
27
+ offset: z.number().int().min(0).optional().describe('Number of rows to skip (default 0).'),
28
+ tags: z.array(z.string().uuid()).optional().describe('Restrict to companies carrying at least one of these tag ids.'),
29
+ })
30
+ .passthrough()
31
+
32
+ type ListCompaniesInput = z.infer<typeof listCompaniesInput>
33
+
34
+ type ListCompaniesApiItem = {
35
+ id?: string
36
+ display_name?: string | null
37
+ displayName?: string | null
38
+ primary_email?: string | null
39
+ primaryEmail?: string | null
40
+ primary_phone?: string | null
41
+ primaryPhone?: string | null
42
+ status?: string | null
43
+ lifecycle_stage?: string | null
44
+ lifecycleStage?: string | null
45
+ source?: string | null
46
+ owner_user_id?: string | null
47
+ ownerUserId?: string | null
48
+ organization_id?: string | null
49
+ organizationId?: string | null
50
+ tenant_id?: string | null
51
+ tenantId?: string | null
52
+ domain?: string | null
53
+ website_url?: string | null
54
+ websiteUrl?: string | null
55
+ industry?: string | null
56
+ size_bucket?: string | null
57
+ sizeBucket?: string | null
58
+ created_at?: string | null
59
+ createdAt?: string | null
60
+ }
61
+
62
+ type ListCompaniesApiResponse = {
63
+ items?: ListCompaniesApiItem[]
64
+ total?: number
65
+ }
66
+
67
+ type ListCompaniesOutput = {
68
+ items: Array<Record<string, unknown>>
69
+ total: number
70
+ limit: number
71
+ offset: number
72
+ }
73
+
74
+ const listCompaniesTool = defineApiBackedAiTool<
75
+ ListCompaniesInput,
76
+ ListCompaniesApiResponse,
77
+ ListCompaniesOutput
78
+ >({
79
+ name: 'customers.list_companies',
80
+ displayName: 'List companies',
81
+ description:
82
+ 'Search / list companies for the caller tenant + organization. Returns { items, total, limit, offset }.',
83
+ inputSchema: listCompaniesInput,
84
+ requiredFeatures: ['customers.companies.view'],
85
+ toOperation: (input, ctx) => {
86
+ assertTenantScope(ctx as unknown as CustomersToolContext)
87
+ const limit = input.limit ?? 50
88
+ const offset = input.offset ?? 0
89
+ const page = Math.floor(offset / limit) + 1
90
+
91
+ const query: Record<string, string | number | boolean | null | undefined> = {
92
+ page,
93
+ pageSize: limit,
94
+ }
95
+ if (input.q?.trim()) query.search = input.q.trim()
96
+ if (input.tags && input.tags.length > 0) query.tagIds = input.tags.join(',')
97
+
98
+ const operation: AiApiOperationRequest = {
99
+ method: 'GET',
100
+ path: '/customers/companies',
101
+ query,
102
+ }
103
+ return operation
104
+ },
105
+ mapResponse: (response, input) => {
106
+ const limit = input.limit ?? 50
107
+ const offset = input.offset ?? 0
108
+ const data = (response.data ?? {}) as ListCompaniesApiResponse
109
+ const rawItems: ListCompaniesApiItem[] = Array.isArray(data.items) ? data.items : []
110
+ return {
111
+ items: rawItems.map((row) => {
112
+ const createdAtRaw = row.created_at ?? row.createdAt ?? null
113
+ const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null
114
+ return {
115
+ id: row.id,
116
+ displayName: row.display_name ?? row.displayName ?? null,
117
+ primaryEmail: row.primary_email ?? row.primaryEmail ?? null,
118
+ primaryPhone: row.primary_phone ?? row.primaryPhone ?? null,
119
+ status: row.status ?? null,
120
+ lifecycleStage: row.lifecycle_stage ?? row.lifecycleStage ?? null,
121
+ source: row.source ?? null,
122
+ ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,
123
+ organizationId: row.organization_id ?? row.organizationId ?? null,
124
+ tenantId: row.tenant_id ?? row.tenantId ?? null,
125
+ domain: row.domain ?? null,
126
+ websiteUrl: row.website_url ?? row.websiteUrl ?? null,
127
+ industry: row.industry ?? null,
128
+ sizeBucket: row.size_bucket ?? row.sizeBucket ?? null,
129
+ createdAt,
130
+ }
131
+ }),
132
+ total: typeof data.total === 'number' ? data.total : 0,
133
+ limit,
134
+ offset,
135
+ }
136
+ },
137
+ }) as unknown as CustomersAiToolDefinition
138
+
139
+ const getCompanyInput = z.object({
140
+ companyId: z.string().uuid().describe('Company entity id (UUID).'),
141
+ includeRelated: z
142
+ .boolean()
143
+ .optional()
144
+ .describe('When true, include notes, activities, deals, people, addresses, tasks, and tags (each capped at 100).'),
145
+ })
146
+
147
+ type GetCompanyInput = z.infer<typeof getCompanyInput>
148
+
149
+ function toIsoCompany(value: unknown): string | null {
150
+ if (!value) return null
151
+ const dt = value instanceof Date ? value : new Date(String(value))
152
+ if (Number.isNaN(dt.getTime())) return null
153
+ return dt.toISOString()
154
+ }
155
+
156
+ const getCompanyTool: CustomersAiToolDefinition = {
157
+ name: 'customers.get_company',
158
+ displayName: 'Get company',
159
+ description:
160
+ '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.',
161
+ inputSchema: getCompanyInput,
162
+ requiredFeatures: ['customers.companies.view'],
163
+ tags: ['read', 'customers'],
164
+ handler: async (rawInput, ctx) => {
165
+ const { tenantId: _tenantId } = assertTenantScope(ctx)
166
+ void _tenantId
167
+ const input: GetCompanyInput = getCompanyInput.parse(rawInput)
168
+ const includeRelated = !!input.includeRelated
169
+
170
+ const operation: AiApiOperationRequest = {
171
+ method: 'GET',
172
+ path: `/customers/companies/${input.companyId}`,
173
+ }
174
+ if (includeRelated) {
175
+ operation.query = {
176
+ include: 'addresses,comments,activities,interactions,deals,todos,people',
177
+ }
178
+ }
179
+
180
+ const runner = createAiApiOperationRunner(ctx as unknown as AiToolExecutionContext)
181
+ const response = await runner.run<Record<string, unknown>>(operation)
182
+ if (!response.success) {
183
+ if (response.statusCode === 404 || response.statusCode === 403) {
184
+ return { found: false as const, companyId: input.companyId }
185
+ }
186
+ throw new Error(response.error ?? `Failed to fetch company ${input.companyId}`)
187
+ }
188
+ const data = (response.data ?? {}) as Record<string, unknown>
189
+ const companyRow = (data.company ?? null) as Record<string, unknown> | null
190
+ if (!companyRow) {
191
+ return { found: false as const, companyId: input.companyId }
192
+ }
193
+ const profileRow = (data.profile ?? null) as Record<string, unknown> | null
194
+ const customFields = (data.customFields ?? {}) as Record<string, unknown>
195
+
196
+ let related: Record<string, unknown> | null = null
197
+ if (includeRelated) {
198
+ const addresses = Array.isArray(data.addresses) ? (data.addresses as Array<Record<string, unknown>>) : []
199
+ const activities = Array.isArray(data.activities) ? (data.activities as Array<Record<string, unknown>>) : []
200
+ const notes = Array.isArray(data.comments) ? (data.comments as Array<Record<string, unknown>>) : []
201
+ const todos = Array.isArray(data.todos) ? (data.todos as Array<Record<string, unknown>>) : []
202
+ const interactions = Array.isArray(data.interactions) ? (data.interactions as Array<Record<string, unknown>>) : []
203
+ const tagsRows = Array.isArray(data.tags) ? (data.tags as Array<Record<string, unknown>>) : []
204
+ const dealsRows = Array.isArray(data.deals) ? (data.deals as Array<Record<string, unknown>>) : []
205
+ const peopleRows = Array.isArray(data.people) ? (data.people as Array<Record<string, unknown>>) : []
206
+ related = {
207
+ addresses: addresses.map((address) => ({
208
+ id: address.id,
209
+ name: address.name ?? null,
210
+ purpose: address.purpose ?? null,
211
+ addressLine1: address.addressLine1 ?? null,
212
+ addressLine2: address.addressLine2 ?? null,
213
+ city: address.city ?? null,
214
+ region: address.region ?? null,
215
+ postalCode: address.postalCode ?? null,
216
+ country: address.country ?? null,
217
+ isPrimary: !!address.isPrimary,
218
+ })),
219
+ activities: activities.map((activity) => ({
220
+ id: activity.id,
221
+ activityType: activity.activityType,
222
+ subject: activity.subject ?? null,
223
+ body: activity.body ?? null,
224
+ occurredAt: toIsoCompany(activity.occurredAt),
225
+ createdAt: toIsoCompany(activity.createdAt),
226
+ })),
227
+ notes: notes.map((comment) => ({
228
+ id: comment.id,
229
+ body: comment.body,
230
+ authorUserId: comment.authorUserId ?? null,
231
+ createdAt: toIsoCompany(comment.createdAt),
232
+ })),
233
+ tasks: todos.map((task) => ({
234
+ id: task.id,
235
+ todoId: task.todoId ?? task.id,
236
+ todoSource: task.todoSource ?? null,
237
+ createdAt: toIsoCompany(task.createdAt),
238
+ })),
239
+ interactions: interactions.map((interaction) => ({
240
+ id: interaction.id,
241
+ interactionType: interaction.interactionType,
242
+ title: interaction.title ?? null,
243
+ status: interaction.status,
244
+ scheduledAt: toIsoCompany(interaction.scheduledAt),
245
+ occurredAt: toIsoCompany(interaction.occurredAt),
246
+ })),
247
+ tags: tagsRows
248
+ .map((tag) => {
249
+ if (!tag || typeof tag !== 'object') return null
250
+ const id = typeof tag.id === 'string' ? tag.id : null
251
+ const label = typeof tag.label === 'string' ? tag.label : null
252
+ if (!id || !label) return null
253
+ const slug = typeof tag.slug === 'string' ? tag.slug : label
254
+ const color = typeof tag.color === 'string' ? tag.color : null
255
+ return { id, slug, label, color }
256
+ })
257
+ .filter(
258
+ (entry): entry is { id: string; slug: string; label: string; color: string | null } =>
259
+ entry !== null,
260
+ ),
261
+ deals: dealsRows
262
+ .map((deal) => {
263
+ if (!deal || typeof deal !== 'object') return null
264
+ const id = typeof deal.id === 'string' ? deal.id : null
265
+ if (!id) return null
266
+ return {
267
+ id,
268
+ title: typeof deal.title === 'string' ? deal.title : '',
269
+ status: typeof deal.status === 'string' ? deal.status : null,
270
+ pipelineStageId:
271
+ typeof deal.pipelineStageId === 'string' ? deal.pipelineStageId : null,
272
+ valueAmount:
273
+ typeof deal.valueAmount === 'string'
274
+ ? deal.valueAmount
275
+ : deal.valueAmount === null || deal.valueAmount === undefined
276
+ ? null
277
+ : String(deal.valueAmount),
278
+ valueCurrency:
279
+ typeof deal.valueCurrency === 'string' ? deal.valueCurrency : null,
280
+ }
281
+ })
282
+ .filter(
283
+ (
284
+ value,
285
+ ): value is {
286
+ id: string
287
+ title: string
288
+ status: string | null
289
+ pipelineStageId: string | null
290
+ valueAmount: string | null
291
+ valueCurrency: string | null
292
+ } => value !== null,
293
+ ),
294
+ people: peopleRows
295
+ .map((person) => {
296
+ if (!person || typeof person !== 'object') return null
297
+ const id = typeof person.id === 'string' ? person.id : null
298
+ const displayName = typeof person.displayName === 'string' ? person.displayName : null
299
+ if (!id || !displayName) return null
300
+ return {
301
+ id,
302
+ displayName,
303
+ primaryEmail:
304
+ typeof person.primaryEmail === 'string' ? person.primaryEmail : null,
305
+ primaryPhone:
306
+ typeof person.primaryPhone === 'string' ? person.primaryPhone : null,
307
+ jobTitle: typeof person.jobTitle === 'string' ? person.jobTitle : null,
308
+ department: typeof person.department === 'string' ? person.department : null,
309
+ }
310
+ })
311
+ .filter(
312
+ (
313
+ value,
314
+ ): value is {
315
+ id: string
316
+ displayName: string
317
+ primaryEmail: string | null
318
+ primaryPhone: string | null
319
+ jobTitle: string | null
320
+ department: string | null
321
+ } => value !== null,
322
+ ),
323
+ }
324
+ }
325
+ return {
326
+ found: true as const,
327
+ company: {
328
+ id: companyRow.id,
329
+ displayName: companyRow.displayName ?? null,
330
+ description: companyRow.description ?? null,
331
+ primaryEmail: companyRow.primaryEmail ?? null,
332
+ primaryPhone: companyRow.primaryPhone ?? null,
333
+ status: companyRow.status ?? null,
334
+ lifecycleStage: companyRow.lifecycleStage ?? null,
335
+ source: companyRow.source ?? null,
336
+ ownerUserId: companyRow.ownerUserId ?? null,
337
+ organizationId: companyRow.organizationId ?? null,
338
+ tenantId: companyRow.tenantId ?? null,
339
+ createdAt: toIsoCompany(companyRow.createdAt),
340
+ updatedAt: toIsoCompany(companyRow.updatedAt),
341
+ },
342
+ profile: profileRow
343
+ ? {
344
+ id: profileRow.id,
345
+ legalName: profileRow.legalName ?? null,
346
+ brandName: profileRow.brandName ?? null,
347
+ domain: profileRow.domain ?? null,
348
+ websiteUrl: profileRow.websiteUrl ?? null,
349
+ industry: profileRow.industry ?? null,
350
+ sizeBucket: profileRow.sizeBucket ?? null,
351
+ annualRevenue: profileRow.annualRevenue ?? null,
352
+ }
353
+ : null,
354
+ customFields,
355
+ related,
356
+ }
357
+ },
358
+ }
359
+
360
+ export const companiesAiTools: CustomersAiToolDefinition[] = [listCompaniesTool, getCompanyTool]
361
+
362
+ export default companiesAiTools