@fleox-agent-crm/agentcrm 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +143 -0
- package/README.md +223 -2
- package/changelog.json +76 -0
- package/dist/admin/admin-files.json +26 -0
- package/dist/admin/assets/chartsImpl-B45AAqnr.js +91 -0
- package/dist/admin/assets/firebase-C1x_l9P9.js +2 -0
- package/dist/admin/assets/gitJson-CAUSxl8d.js +3 -0
- package/dist/admin/assets/index-ChFauO8y.js +1181 -0
- package/dist/admin/assets/index-TcjySLVg.css +1 -0
- package/dist/admin/assets/index.esm-BFc7vwnl.js +400 -0
- package/dist/admin/assets/index.esm-CtEQZRb1.js +1651 -0
- package/dist/admin/assets/index.esm-Dum0J07z.js +16 -0
- package/dist/admin/assets/index.esm2017-EOzKbpUS.js +390 -0
- package/dist/admin/assets/lucide-react.min--rznIkmQ.js +6 -0
- package/dist/admin/assets/sdk-DgzLACmL.js +1717 -0
- package/dist/admin/config.js +3 -0
- package/dist/admin/favicon.svg +1 -0
- package/dist/admin/index.html +139 -0
- package/dist/admin/runtime/agentcrm-runtime.js +104 -0
- package/dist/admin/runtime/lucide-react.js +791 -0
- package/dist/admin/runtime/react-dom-client.js +17 -0
- package/dist/admin/runtime/react-dom.js +20 -0
- package/dist/admin/runtime/react-i18next.js +19 -0
- package/dist/admin/runtime/react-jsx-runtime.js +17 -0
- package/dist/admin/runtime/react.js +22 -0
- package/dist/agent/agentcrm.mjs +4057 -0
- package/dist/agent/chunks/_commonjsHelpers-ClciMIcB.mjs +14 -0
- package/dist/agent/chunks/activities-Cj_SIOay.mjs +227 -0
- package/dist/agent/chunks/api-Cz0Ubj-B.mjs +98 -0
- package/dist/agent/chunks/appearance-BloHbiLA.mjs +174 -0
- package/dist/agent/chunks/automations-DwWruxmu.mjs +698 -0
- package/dist/agent/chunks/backup-W3CjgkAJ.mjs +116 -0
- package/dist/agent/chunks/batch-CVETiJj5.mjs +142 -0
- package/dist/agent/chunks/compact-BGHI92tm.mjs +69 -0
- package/dist/agent/chunks/context-CxFfqS4D.mjs +65 -0
- package/dist/agent/chunks/core-Dy7vY9H6.mjs +255 -0
- package/dist/agent/chunks/dedupe-BepmsaHv.mjs +123 -0
- package/dist/agent/chunks/encoding-C9MzdP95.mjs +36 -0
- package/dist/agent/chunks/exportCsv-C6e6XEmB.mjs +130 -0
- package/dist/agent/chunks/extCheck-GgHG1dYB.mjs +200 -0
- package/dist/agent/chunks/extPackage-DuZ6GZhp.mjs +115 -0
- package/dist/agent/chunks/extScaffold-Aqyjf1nr.mjs +320 -0
- package/dist/agent/chunks/extensionActions-D2HtpQz4.mjs +48 -0
- package/dist/agent/chunks/extensions-Bp72uZ9a.mjs +164 -0
- package/dist/agent/chunks/featureRegistry-DSsZbhrh.mjs +149 -0
- package/dist/agent/chunks/fields-B0bitgmr.mjs +476 -0
- package/dist/agent/chunks/files-CjsPJSR6.mjs +158 -0
- package/dist/agent/chunks/files-D1daDgY0.mjs +367 -0
- package/dist/agent/chunks/firebase-DxGVQV9E.mjs +454 -0
- package/dist/agent/chunks/firebaseRules-iori-s7U.mjs +221 -0
- package/dist/agent/chunks/forms-BfuWaQjA.mjs +847 -0
- package/dist/agent/chunks/gitJson-fjr8mcHe.mjs +528 -0
- package/dist/agent/chunks/hosting-Bme7LxPc.mjs +144 -0
- package/dist/agent/chunks/http-Bk9ICb7I.mjs +127 -0
- package/dist/agent/chunks/ids-CvnmrMJX.mjs +59 -0
- package/dist/agent/chunks/ids-u-noYcy5.mjs +14 -0
- package/dist/agent/chunks/importCsv-LVmMTaWU.mjs +334 -0
- package/dist/agent/chunks/index-B-4W0-P4.mjs +535 -0
- package/dist/agent/chunks/index-BM8qeGOb.mjs +2489 -0
- package/dist/agent/chunks/index-Bnmav-Oh.mjs +311 -0
- package/dist/agent/chunks/index-CRkw3_iF.mjs +147 -0
- package/dist/agent/chunks/index-CWrrF1wl.mjs +111 -0
- package/dist/agent/chunks/index-DUSdTBkz.mjs +2988 -0
- package/dist/agent/chunks/index-RCmo-uld.mjs +26 -0
- package/dist/agent/chunks/index-w03ReRs4.mjs +9780 -0
- package/dist/agent/chunks/install-CbpD_7gd.mjs +188 -0
- package/dist/agent/chunks/jsx-runtime-Ca2uwGDr.mjs +76 -0
- package/dist/agent/chunks/loader-MPD9iAvh.mjs +184 -0
- package/dist/agent/chunks/lucide-react.min-Bt3gtepY.mjs +64 -0
- package/dist/agent/chunks/memory-WNR3VHUJ.mjs +56 -0
- package/dist/agent/chunks/migrate-CsClnnN8.mjs +182 -0
- package/dist/agent/chunks/modules-clWdE-K2.mjs +202 -0
- package/dist/agent/chunks/nodeImport-ylfRDvqt.mjs +38 -0
- package/dist/agent/chunks/package-B7mI6HGi.mjs +194 -0
- package/dist/agent/chunks/papaparse-Df7i1dHr.mjs +1309 -0
- package/dist/agent/chunks/pipelines-BAKpPk1w.mjs +234 -0
- package/dist/agent/chunks/plan-BmTat6-T.mjs +254 -0
- package/dist/agent/chunks/pluginRegistry-DyHtlkcr.mjs +83 -0
- package/dist/agent/chunks/query-C5nJB_3C.mjs +192 -0
- package/dist/agent/chunks/records-DcHFelq5.mjs +2195 -0
- package/dist/agent/chunks/registry-COfJrSmg.mjs +182 -0
- package/dist/agent/chunks/registry-DijAKZ24.mjs +431 -0
- package/dist/agent/chunks/relations-B9TynBmA.mjs +191 -0
- package/dist/agent/chunks/reports-BACHGccj.mjs +189 -0
- package/dist/agent/chunks/runtime-C0Xf0Axi.mjs +14873 -0
- package/dist/agent/chunks/runtimeConfig-CCOFg1RM.mjs +250 -0
- package/dist/agent/chunks/s3-Dz50-NuA.mjs +903 -0
- package/dist/agent/chunks/schema-C4lSslXv.mjs +373 -0
- package/dist/agent/chunks/schema-DnLj8dUO.mjs +213 -0
- package/dist/agent/chunks/sdk-whvfg786.mjs +43644 -0
- package/dist/agent/chunks/seed-6MRmQQBR.mjs +541 -0
- package/dist/agent/chunks/server-DC3t8r30.mjs +242 -0
- package/dist/agent/chunks/settings-Di8nzNEL.mjs +111 -0
- package/dist/agent/chunks/standardObjects-CheqfDqa.mjs +243 -0
- package/dist/agent/chunks/stats-BfuLWhVV.mjs +51 -0
- package/dist/agent/chunks/store-5gqRSKXg.mjs +122 -0
- package/dist/agent/chunks/themes-DPZFv3yc.mjs +43 -0
- package/dist/agent/chunks/tools-iSH-yMpl.mjs +884 -0
- package/dist/agent/chunks/types-BOy9PUl7.mjs +44 -0
- package/dist/agent/chunks/users-DI1BvS0f.mjs +107 -0
- package/dist/agent/chunks/utils-DmI8FtXP.mjs +83 -0
- package/dist/agent/chunks/views-DH1n9Lrt.mjs +109 -0
- package/dist/version.json +7 -0
- package/package.json +97 -4
- package/plugin/.claude-plugin/plugin.json +17 -0
- package/plugin/skills/agentcrm-admin/SKILL.md +191 -0
- package/plugin/skills/agentcrm-admin/references/appearance.md +26 -0
- package/plugin/skills/agentcrm-admin/references/import.md +16 -0
- package/plugin/skills/agentcrm-admin/references/records.md +30 -0
- package/plugin/skills/agentcrm-admin/references/schema.md +59 -0
- package/plugin/skills/agentcrm-extensions/SKILL.md +93 -0
- package/plugin/skills/agentcrm-extensions/references/module-recipes.md +213 -0
- package/plugin/skills/agentcrm-extensions/references/runtime-api.md +41 -0
- package/plugin/skills/agentcrm-extensions/references/testing.md +78 -0
- package/plugin/skills/agentcrm-extensions/references/theme-recipes.md +65 -0
- package/plugin/skills/agentcrm-install/SKILL.md +116 -0
- package/plugin/skills/agentcrm-install/references/firebase.md +18 -0
- package/plugin/skills/agentcrm-install/references/github.md +20 -0
- package/plugin/skills/agentcrm-install/references/mcp.md +17 -0
- package/plugin/skills/agentcrm-install/references/plans.md +45 -0
- package/plugin/skills/agentcrm-install/references/s3.md +24 -0
- package/plugin/skills/agentcrm-manage/SKILL.md +108 -0
- package/plugin/skills/agentcrm-manage/references/cron.md +11 -0
- package/plugin/skills/agentcrm-manage/references/doctor.md +18 -0
- package/plugin/skills/agentcrm-sales/SKILL.md +92 -0
- package/plugin/skills/agentcrm-sales/references/enrichment.md +13 -0
- package/plugin/skills/agentcrm-sales/references/playbooks.md +29 -0
- package/plugin/skills/agentcrm-sales/references/weekly-report.md +27 -0
- package/types/agentcrm-runtime.d.ts +894 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
import { createRequire as __acrmRequire } from "node:module";
|
|
2
|
+
import { fileURLToPath as __acrmPath } from "node:url";
|
|
3
|
+
import { dirname as __acrmDirname } from "node:path";
|
|
4
|
+
const require2 = __acrmRequire(import.meta.url);
|
|
5
|
+
const __filename = __acrmPath(import.meta.url);
|
|
6
|
+
const __dirname = __acrmDirname(__filename);
|
|
7
|
+
import { recordLabel, normalizeEmail, formatFieldValue } from "./fields-B0bitgmr.mjs";
|
|
8
|
+
import { r as resolveField } from "./query-C5nJB_3C.mjs";
|
|
9
|
+
import { F as FILTER_OPS, A as ACTIVITY_TYPES, a as FIELD_TYPES } from "../agentcrm.mjs";
|
|
10
|
+
import { OpsError } from "./context-CxFfqS4D.mjs";
|
|
11
|
+
import { listActivities, addNote, addActivity, createTask, completeTask, listTasks } from "./activities-Cj_SIOay.mjs";
|
|
12
|
+
import { mergeRecords } from "./dedupe-BepmsaHv.mjs";
|
|
13
|
+
import { importCsv } from "./importCsv-LVmMTaWU.mjs";
|
|
14
|
+
import { moveStage, forecast, listPipelines } from "./pipelines-BAKpPk1w.mjs";
|
|
15
|
+
import { getRecord, searchRecords, listRecords, getRecordDetails, createRecord, updateRecord, deleteRecord, queryContext } from "./records-DcHFelq5.mjs";
|
|
16
|
+
import { runReport, REPORTS } from "./reports-BACHGccj.mjs";
|
|
17
|
+
import { describeSchema, createField } from "./schema-C4lSslXv.mjs";
|
|
18
|
+
import { objectOf, recordOf, userOf, activitiesOf, fileOf } from "./store-5gqRSKXg.mjs";
|
|
19
|
+
import { ACTION_TYPES, TRIGGER_TYPES, listAutomationRuns, AUTOMATION_TEMPLATES, listAutomations, createAutomationFromTemplate, createAutomation, setAutomationEnabled, testAutomation, runScheduled } from "./automations-DwWruxmu.mjs";
|
|
20
|
+
import { listForms, ingestInbox } from "./forms-BfuWaQjA.mjs";
|
|
21
|
+
import { B as BUTTON_STYLES, C as CONTRASTS, d as COLOR_MODES, S as SIDEBAR_STYLES, e as DENSITIES, R as RADII, N as NEUTRALS, F as FONTS } from "./schema-DnLj8dUO.mjs";
|
|
22
|
+
import { listPresets, getAppearance, setAppearance, applyPreset } from "./appearance-BloHbiLA.mjs";
|
|
23
|
+
import { listExtensions, setExtensionEnabled } from "./extensions-Bp72uZ9a.mjs";
|
|
24
|
+
import { listModules, setModuleEnabled } from "./modules-clWdE-K2.mjs";
|
|
25
|
+
import { inlineFileIds, formatBytes, fileKind } from "./files-CjsPJSR6.mjs";
|
|
26
|
+
import { listFiles, fileUsage } from "./files-D1daDgY0.mjs";
|
|
27
|
+
import { l as labelOf } from "./relations-B9TynBmA.mjs";
|
|
28
|
+
import "node:util";
|
|
29
|
+
import "node:fs";
|
|
30
|
+
import "node:path";
|
|
31
|
+
import "node:url";
|
|
32
|
+
import "node:crypto";
|
|
33
|
+
import "node:os";
|
|
34
|
+
import "node:child_process";
|
|
35
|
+
import "node:http";
|
|
36
|
+
import "./pluginRegistry-DyHtlkcr.mjs";
|
|
37
|
+
import "./ids-CvnmrMJX.mjs";
|
|
38
|
+
import "./papaparse-Df7i1dHr.mjs";
|
|
39
|
+
import "./_commonjsHelpers-ClciMIcB.mjs";
|
|
40
|
+
import "stream";
|
|
41
|
+
import "./utils-DmI8FtXP.mjs";
|
|
42
|
+
import "./standardObjects-CheqfDqa.mjs";
|
|
43
|
+
import "./settings-Di8nzNEL.mjs";
|
|
44
|
+
import "./runtimeConfig-CCOFg1RM.mjs";
|
|
45
|
+
import "./extensionActions-D2HtpQz4.mjs";
|
|
46
|
+
import "./themes-DPZFv3yc.mjs";
|
|
47
|
+
import "./api-Cz0Ubj-B.mjs";
|
|
48
|
+
import "./package-B7mI6HGi.mjs";
|
|
49
|
+
import "./index-w03ReRs4.mjs";
|
|
50
|
+
import "events";
|
|
51
|
+
import "buffer";
|
|
52
|
+
import "util";
|
|
53
|
+
import "./index-RCmo-uld.mjs";
|
|
54
|
+
import "./http-Bk9ICb7I.mjs";
|
|
55
|
+
import "./index-BM8qeGOb.mjs";
|
|
56
|
+
function summarizeAutomation(ctx, a) {
|
|
57
|
+
const last = listAutomationRuns(ctx, { automationId: a.id, limit: 1 })[0];
|
|
58
|
+
return {
|
|
59
|
+
id: a.id,
|
|
60
|
+
name: a.name,
|
|
61
|
+
enabled: a.enabled,
|
|
62
|
+
object: a.object,
|
|
63
|
+
trigger: a.trigger,
|
|
64
|
+
conditions: a.conditions,
|
|
65
|
+
actions: a.actions,
|
|
66
|
+
...a.lastRunAt ? { lastRunAt: new Date(a.lastRunAt).toISOString() } : {},
|
|
67
|
+
...last ? { lastRun: { status: last.status, at: new Date(last.startedAt).toISOString(), ...last.message ? { message: last.message } : {} } } : {}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function summarizeForm(f) {
|
|
71
|
+
return {
|
|
72
|
+
id: f.id,
|
|
73
|
+
name: f.name,
|
|
74
|
+
object: f.object,
|
|
75
|
+
fields: f.fields,
|
|
76
|
+
inbox: f.inbox.kind,
|
|
77
|
+
...f.publishedUrl ? { url: f.publishedUrl } : {},
|
|
78
|
+
...f.publishedAt ? { publishedAt: new Date(f.publishedAt).toISOString() } : {},
|
|
79
|
+
...f.lastIngestAt ? { lastIngestAt: new Date(f.lastIngestAt).toISOString() } : {}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const triggerSchema = {
|
|
83
|
+
type: "object",
|
|
84
|
+
description: "record.created | record.updated (optional field) | field.changed (field, optional to = new value: option id or label) | stage.changed (optional to = stage id or kind won/lost/open) | schedule (everyHours) | inactivity (days without activity).",
|
|
85
|
+
properties: {
|
|
86
|
+
type: { type: "string", enum: TRIGGER_TYPES },
|
|
87
|
+
field: { type: "string" },
|
|
88
|
+
to: { description: "Value the field / stage must change to." },
|
|
89
|
+
days: { type: "number" },
|
|
90
|
+
everyHours: { type: "number" }
|
|
91
|
+
},
|
|
92
|
+
required: ["type"]
|
|
93
|
+
};
|
|
94
|
+
const actionSchema = {
|
|
95
|
+
type: "object",
|
|
96
|
+
description: 'One of: {type:"createTask", title, dueInDays?, assignTo?: "owner"|userId} · {type:"updateField", field, value} · {type:"addNote", body} · {type:"assignOwner", userId: userId|"roundRobin"} · {type:"webhook", url}. Texts accept {{label}}, {{<fieldId>}}, {{owner}} placeholders.',
|
|
97
|
+
properties: {
|
|
98
|
+
type: { type: "string", enum: ACTION_TYPES },
|
|
99
|
+
title: { type: "string" },
|
|
100
|
+
dueInDays: { type: "number" },
|
|
101
|
+
assignTo: { type: "string" },
|
|
102
|
+
field: { type: "string" },
|
|
103
|
+
value: {},
|
|
104
|
+
body: { type: "string" },
|
|
105
|
+
userId: { type: "string" },
|
|
106
|
+
url: { type: "string" }
|
|
107
|
+
},
|
|
108
|
+
required: ["type"]
|
|
109
|
+
};
|
|
110
|
+
const conditionSchema = {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: { field: { type: "string" }, op: { type: "string", enum: FILTER_OPS }, value: {} },
|
|
113
|
+
required: ["field", "op"]
|
|
114
|
+
};
|
|
115
|
+
const AUTOMATION_FORM_TOOLS = [
|
|
116
|
+
{
|
|
117
|
+
name: "list_automations",
|
|
118
|
+
description: "List the automations (trigger, conditions, actions, enabled, last run) and, with includeRuns, the latest runs log. Also lists the ready-made templates.",
|
|
119
|
+
inputSchema: { type: "object", properties: { includeRuns: { type: "boolean" }, limit: { type: "number", default: 20, maximum: 100 } } },
|
|
120
|
+
mutating: false,
|
|
121
|
+
handler: (ctx, input) => ({
|
|
122
|
+
automations: listAutomations(ctx).map((a) => summarizeAutomation(ctx, a)),
|
|
123
|
+
templates: AUTOMATION_TEMPLATES.map((t) => ({ id: t.id, name: t.name, description: t.description })),
|
|
124
|
+
...input.includeRuns ? {
|
|
125
|
+
runs: listAutomationRuns(ctx, { limit: Math.min(100, typeof input.limit === "number" ? input.limit : 20) }).map((r) => ({
|
|
126
|
+
automationId: r.automationId,
|
|
127
|
+
recordId: r.recordId,
|
|
128
|
+
status: r.status,
|
|
129
|
+
at: new Date(r.startedAt).toISOString(),
|
|
130
|
+
...r.message ? { message: r.message } : {},
|
|
131
|
+
actions: r.actions
|
|
132
|
+
}))
|
|
133
|
+
} : {}
|
|
134
|
+
})
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "create_automation",
|
|
138
|
+
description: "Create an automation (admin only), either from a template (templateId, see list_automations) or from a full definition (name, object, trigger, conditions, actions). Use describe_schema for field ids. Set enabled false to create it paused.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
templateId: { type: "string", enum: AUTOMATION_TEMPLATES.map((t) => t.id) },
|
|
143
|
+
name: { type: "string" },
|
|
144
|
+
object: { type: "string" },
|
|
145
|
+
trigger: triggerSchema,
|
|
146
|
+
conditions: { type: "array", items: conditionSchema },
|
|
147
|
+
actions: { type: "array", items: actionSchema },
|
|
148
|
+
enabled: { type: "boolean", default: true }
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
mutating: true,
|
|
152
|
+
handler: async (ctx, input) => {
|
|
153
|
+
const enabled = input.enabled === void 0 ? void 0 : input.enabled !== false;
|
|
154
|
+
if (typeof input.templateId === "string") {
|
|
155
|
+
const overrides = { ...typeof input.name === "string" ? { name: input.name } : {}, ...enabled !== void 0 ? { enabled } : {} };
|
|
156
|
+
return summarizeAutomation(ctx, await createAutomationFromTemplate(ctx, input.templateId, overrides));
|
|
157
|
+
}
|
|
158
|
+
if (typeof input.object !== "string" || !input.trigger || !Array.isArray(input.actions)) throw new OpsError("validation", "Give a templateId, or name + object + trigger + actions.");
|
|
159
|
+
const a = await createAutomation(ctx, {
|
|
160
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
161
|
+
object: input.object,
|
|
162
|
+
trigger: input.trigger,
|
|
163
|
+
conditions: input.conditions ?? [],
|
|
164
|
+
actions: input.actions,
|
|
165
|
+
...enabled !== void 0 ? { enabled } : {}
|
|
166
|
+
});
|
|
167
|
+
return summarizeAutomation(ctx, a);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "run_automations",
|
|
172
|
+
description: "Run the scheduled automations (schedule / inactivity triggers) now, or test one automation on one record (automationId + recordId). DRY RUN unless confirm is true — show the planned actions to the user before confirming. Can also enable / disable an automation (enabled).",
|
|
173
|
+
inputSchema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
automationId: { type: "string" },
|
|
177
|
+
recordId: { type: "string", description: "Test the automation on this record (ignores the trigger, checks conditions)." },
|
|
178
|
+
enabled: { type: "boolean", description: "Only enable / disable automationId, don't run." },
|
|
179
|
+
confirm: { type: "boolean", default: false }
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
mutating: true,
|
|
183
|
+
handler: async (ctx, input) => {
|
|
184
|
+
const automationId = typeof input.automationId === "string" ? input.automationId : void 0;
|
|
185
|
+
if (typeof input.enabled === "boolean") {
|
|
186
|
+
if (!automationId) throw new OpsError("validation", "enabled needs an automationId.");
|
|
187
|
+
return summarizeAutomation(ctx, await setAutomationEnabled(ctx, automationId, input.enabled));
|
|
188
|
+
}
|
|
189
|
+
const dryRun = input.confirm !== true;
|
|
190
|
+
if (typeof input.recordId === "string") {
|
|
191
|
+
if (!automationId) throw new OpsError("validation", "recordId needs an automationId.");
|
|
192
|
+
const a = listAutomations(ctx).find((x) => x.id === automationId);
|
|
193
|
+
if (!a) throw new OpsError("notFound", `Automation "${automationId}" not found.`);
|
|
194
|
+
const res = await testAutomation(ctx, a, input.recordId, { dryRun });
|
|
195
|
+
return { dryRun, matched: res.matched, failedConditions: res.failedConditions, status: res.run.status, actions: res.run.actions, message: res.run.message };
|
|
196
|
+
}
|
|
197
|
+
const report = await runScheduled(ctx, { dryRun, automationId, force: !!automationId });
|
|
198
|
+
return {
|
|
199
|
+
dryRun,
|
|
200
|
+
ran: report.ran,
|
|
201
|
+
skipped: report.skipped,
|
|
202
|
+
errors: report.errors,
|
|
203
|
+
runs: report.runs.slice(0, 50).map((r) => ({ automationId: r.automationId, recordId: r.recordId, status: r.status, actions: r.actions, ...r.message ? { message: r.message } : {} })),
|
|
204
|
+
runsTruncated: report.runs.length > 50
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "list_forms",
|
|
210
|
+
description: "List the lead capture forms: object, fields, inbox kind (s3 / firebase / webhook), published URL, last ingestion.",
|
|
211
|
+
inputSchema: { type: "object", properties: {} },
|
|
212
|
+
mutating: false,
|
|
213
|
+
handler: (ctx) => ({ forms: listForms(ctx).map(summarizeForm) })
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "ingest_inbox",
|
|
217
|
+
description: "Import pending lead form submissions from the inbox(es): creates or updates records (dedupe on email), creates companies from email domains, logs a 'Form submission' note, then removes processed submissions. DRY RUN unless confirm is true.",
|
|
218
|
+
inputSchema: { type: "object", properties: { formId: { type: "string" }, confirm: { type: "boolean", default: false } } },
|
|
219
|
+
mutating: true,
|
|
220
|
+
handler: async (ctx, input) => {
|
|
221
|
+
const r = await ingestInbox(ctx, typeof input.formId === "string" ? input.formId : void 0, { dryRun: input.confirm !== true });
|
|
222
|
+
return { dryRun: input.confirm !== true, ...r, results: r.results?.slice(0, 50) };
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "draft_email",
|
|
227
|
+
description: "Prepare an email draft to a person (or a company's people / a deal's contacts): returns recipients, a subject and body (yours if given, else a skeleton) plus context (record, recent activities, last email). Never sends — show the draft to the user; they send it from the record's Emails tab.",
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: "object",
|
|
230
|
+
properties: {
|
|
231
|
+
recordId: { type: "string" },
|
|
232
|
+
subject: { type: "string" },
|
|
233
|
+
body: { type: "string", description: "Markdown / plain text body you wrote." },
|
|
234
|
+
purpose: { type: "string", description: "What the email is for (follow-up, intro, proposal…)." }
|
|
235
|
+
},
|
|
236
|
+
required: ["recordId"]
|
|
237
|
+
},
|
|
238
|
+
mutating: false,
|
|
239
|
+
handler: (ctx, input) => {
|
|
240
|
+
const record = getRecord(ctx, String(input.recordId));
|
|
241
|
+
const object = objectOf(ctx.store, record.object);
|
|
242
|
+
const recipients = [];
|
|
243
|
+
const addPerson = (id) => {
|
|
244
|
+
const p = typeof id === "string" ? recordOf(ctx.store, id) : void 0;
|
|
245
|
+
const email = p && !p.deletedAt ? normalizeEmail(p.fields.email) : "";
|
|
246
|
+
if (p && email && !recipients.some((r) => r.email === email)) recipients.push({ id: p.id, name: recordLabel(objectOf(ctx.store, "people"), p), email });
|
|
247
|
+
};
|
|
248
|
+
if (record.object === "people") addPerson(record.id);
|
|
249
|
+
for (const v of Object.values(record.fields)) {
|
|
250
|
+
for (const id of Array.isArray(v) ? v : [v]) if (typeof id === "string" && recordOf(ctx.store, id)?.object === "people") addPerson(id);
|
|
251
|
+
}
|
|
252
|
+
if (record.object === "companies") {
|
|
253
|
+
for (const p of ctx.store.list("records")) if (p.object === "people" && !p.deletedAt && p.fields.company === record.id) addPerson(p.id);
|
|
254
|
+
}
|
|
255
|
+
const activities = listActivities(ctx, { target: record.id, includeRelated: true, limit: 8 }).items;
|
|
256
|
+
const lastEmail = activities.find((a) => a.type === "email");
|
|
257
|
+
const me = ctx.user ? userOf(ctx.store, ctx.user.id) ?? ctx.user : null;
|
|
258
|
+
const first = recipients[0]?.name.split(" ")[0];
|
|
259
|
+
const subject = typeof input.subject === "string" && input.subject.trim() ? input.subject.trim() : typeof input.purpose === "string" ? input.purpose.trim() : `Following up — ${recordLabel(object, record)}`;
|
|
260
|
+
const body = typeof input.body === "string" && input.body.trim() ? input.body : `Hi ${first ?? "there"},
|
|
261
|
+
|
|
262
|
+
[Write the message${typeof input.purpose === "string" ? ` (${input.purpose})` : ""} here.]
|
|
263
|
+
|
|
264
|
+
Best,
|
|
265
|
+
${me?.name ?? ""}`;
|
|
266
|
+
return {
|
|
267
|
+
draft: { to: recipients.slice(0, 10), subject, body },
|
|
268
|
+
sent: false,
|
|
269
|
+
note: "Draft only — nothing was sent. The user can send it from the record's Emails tab (Gmail / Outlook integration).",
|
|
270
|
+
context: {
|
|
271
|
+
record: { id: record.id, object: record.object, label: recordLabel(object, record) },
|
|
272
|
+
recentActivities: activities.map((a) => ({ type: a.type, title: a.title, at: new Date(a.createdAt).toISOString() })),
|
|
273
|
+
...lastEmail ? { lastEmail: { title: lastEmail.title, body: lastEmail.body?.slice(0, 800), at: new Date(lastEmail.createdAt).toISOString() } } : {}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
];
|
|
279
|
+
function compactAppearance(a) {
|
|
280
|
+
const { logo, favicon, customCss, ...rest } = a;
|
|
281
|
+
return { ...rest, hasLogo: !!logo, hasFavicon: !!favicon, customCssBytes: customCss ? customCss.length : 0 };
|
|
282
|
+
}
|
|
283
|
+
const appearanceProps = {
|
|
284
|
+
accent: { type: "string", description: "Accent color, hex #rrggbb (a full 50–950 scale is generated from it)." },
|
|
285
|
+
neutral: { type: "string", enum: NEUTRALS, description: "Gray scale used for surfaces, borders and text." },
|
|
286
|
+
radius: { type: "string", enum: RADII },
|
|
287
|
+
density: { type: "string", enum: DENSITIES, description: "compact = shorter table rows and paddings." },
|
|
288
|
+
font: { type: "string", enum: FONTS.map((f) => f.id), description: '"system" needs no network; the others load from Google Fonts.' },
|
|
289
|
+
sidebar: { type: "string", enum: SIDEBAR_STYLES },
|
|
290
|
+
defaultMode: { type: "string", enum: COLOR_MODES, description: "Default light/dark mode for users who never toggled it." },
|
|
291
|
+
contrast: { type: "string", enum: CONTRASTS },
|
|
292
|
+
buttons: { type: "string", enum: BUTTON_STYLES, description: "Primary buttons: neutral (near-black) or accent colored." },
|
|
293
|
+
customCss: { type: ["string", "null"], description: "Extra CSS for the whole workspace (≤ 50 KB). null removes it." },
|
|
294
|
+
themeId: { type: ["string", "null"], description: "Installed theme extension to use (see list_extensions); null clears it." }
|
|
295
|
+
};
|
|
296
|
+
const CUSTOMIZE_TOOLS = [
|
|
297
|
+
{
|
|
298
|
+
name: "list_modules",
|
|
299
|
+
description: "List the workspace modules (built-in features such as dashboard, deals, reports, tasks, automations, forms, integrations, assistant, plus installed extension modules) with enabled / required state and dependencies.",
|
|
300
|
+
inputSchema: { type: "object", properties: {} },
|
|
301
|
+
mutating: false,
|
|
302
|
+
handler: (ctx) => listModules(ctx).map((m) => ({
|
|
303
|
+
id: m.id,
|
|
304
|
+
name: m.name,
|
|
305
|
+
category: m.category,
|
|
306
|
+
source: m.source,
|
|
307
|
+
enabled: m.enabled,
|
|
308
|
+
...m.required ? { required: true } : {},
|
|
309
|
+
...m.dependsOn.length ? { dependsOn: m.dependsOn } : {},
|
|
310
|
+
...m.dependents.length ? { dependents: m.dependents } : {},
|
|
311
|
+
...m.objects.length ? { objects: m.objects } : {},
|
|
312
|
+
...m.version ? { version: m.version } : {}
|
|
313
|
+
}))
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: "set_module_enabled",
|
|
317
|
+
description: "Enable or disable a module for the whole workspace (admin only). Disabling hides its pages, navigation and widgets (and objects, e.g. deals) without deleting data. Fails when other modules depend on it unless cascade is true.",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: { id: { type: "string", description: "Module id (see list_modules)." }, enabled: { type: "boolean" }, cascade: { type: "boolean", default: false, description: "Also disable dependents / enable dependencies." } },
|
|
321
|
+
required: ["id", "enabled"]
|
|
322
|
+
},
|
|
323
|
+
mutating: true,
|
|
324
|
+
handler: (ctx, input) => setModuleEnabled(ctx, String(input.id), input.enabled === true, { cascade: input.cascade === true })
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "get_appearance",
|
|
328
|
+
description: "Get the workspace appearance (accent color, neutral scale, radius, density, font, sidebar style, default mode, selected theme) and the available presets / themes.",
|
|
329
|
+
inputSchema: { type: "object", properties: {} },
|
|
330
|
+
mutating: false,
|
|
331
|
+
handler: (ctx) => ({ appearance: compactAppearance(getAppearance(ctx)), presets: listPresets(ctx).map((p) => ({ id: p.id, name: p.name, source: p.source, description: p.description })) })
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "set_appearance",
|
|
335
|
+
description: "Change the workspace appearance for every user (admin only). Pass only the keys to change; null resets a key to its default. Applied live in every open admin.",
|
|
336
|
+
inputSchema: { type: "object", properties: appearanceProps, additionalProperties: false },
|
|
337
|
+
mutating: true,
|
|
338
|
+
handler: async (ctx, input) => compactAppearance(await setAppearance(ctx, input))
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "apply_appearance_preset",
|
|
342
|
+
description: "Apply an appearance preset (default, ocean, forest, sunset, graphite, rose, high-contrast) or an installed theme extension id (admin only). Logo, favicon and custom CSS are kept.",
|
|
343
|
+
inputSchema: { type: "object", properties: { id: { type: "string", description: "Preset or theme id (see get_appearance)." } }, required: ["id"] },
|
|
344
|
+
mutating: true,
|
|
345
|
+
handler: async (ctx, input) => compactAppearance(await applyPreset(ctx, String(input.id)))
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: "list_extensions",
|
|
349
|
+
description: "List the installed extensions (external modules and themes) with version, author, enabled state and API compatibility.",
|
|
350
|
+
inputSchema: { type: "object", properties: { kind: { type: "string", enum: ["module", "theme"] } } },
|
|
351
|
+
mutating: false,
|
|
352
|
+
handler: (ctx, input) => listExtensions(ctx, input.kind === "module" || input.kind === "theme" ? { kind: input.kind } : {})
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: "set_extension_enabled",
|
|
356
|
+
description: "Enable or disable an installed extension without uninstalling it (admin only). Installing new code is not possible through tools — use Settings → Modules or the CLI.",
|
|
357
|
+
inputSchema: { type: "object", properties: { id: { type: "string" }, enabled: { type: "boolean" }, cascade: { type: "boolean", default: false } }, required: ["id", "enabled"] },
|
|
358
|
+
mutating: true,
|
|
359
|
+
handler: (ctx, input) => {
|
|
360
|
+
if (typeof input.id !== "string" || !input.id) throw new OpsError("validation", 'Missing "id".');
|
|
361
|
+
return setExtensionEnabled(ctx, input.id, input.enabled === true, { cascade: input.cascade === true });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
];
|
|
365
|
+
const KINDS = ["image", "pdf", "document", "spreadsheet", "presentation", "archive", "text", "audio", "video", "other"];
|
|
366
|
+
function summarizeFile(ctx, f) {
|
|
367
|
+
return {
|
|
368
|
+
id: f.id,
|
|
369
|
+
name: f.name,
|
|
370
|
+
kind: fileKind(f.mime, f.name),
|
|
371
|
+
mime: f.mime,
|
|
372
|
+
size: f.size,
|
|
373
|
+
sizeLabel: formatBytes(f.size),
|
|
374
|
+
...f.width && f.height ? { dimensions: `${f.width}×${f.height}` } : {},
|
|
375
|
+
createdAt: new Date(f.createdAt).toISOString(),
|
|
376
|
+
...f.createdBy ? { uploadedBy: userOf(ctx.store, f.createdBy)?.name ?? f.createdBy } : {},
|
|
377
|
+
attachedTo: f.targets.map((t) => t.kind === "record" ? { kind: "record", id: t.id, label: labelOf(ctx, t.id) } : { kind: "activity", id: t.id, title: ctx.store.get("activities", t.id)?.title }),
|
|
378
|
+
...f.via ? { via: f.via.kind } : {},
|
|
379
|
+
...f.deletedAt ? { deleted: true } : {}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const FILE_TOOLS = [
|
|
383
|
+
{
|
|
384
|
+
name: "list_files",
|
|
385
|
+
description: "Files (images, PDFs, documents) of a record — attached to it, its image fields, its activities' attachments; includeRelated adds linked records' files — or of one activity. Metadata only (name, type, size, attached to); without recordId / activityId: storage usage + the most recent files.",
|
|
386
|
+
inputSchema: {
|
|
387
|
+
type: "object",
|
|
388
|
+
properties: {
|
|
389
|
+
recordId: { type: "string", description: "Record id." },
|
|
390
|
+
activityId: { type: "string", description: "Activity id (note, task…)." },
|
|
391
|
+
includeRelated: { type: "boolean", description: "Also files of related records (e.g. a company's people and deals)." },
|
|
392
|
+
kind: { type: "string", enum: KINDS },
|
|
393
|
+
search: { type: "string", description: "Part of the file name." },
|
|
394
|
+
limit: { type: "number", default: 30, maximum: 200 }
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
mutating: false,
|
|
398
|
+
handler: (ctx, input) => {
|
|
399
|
+
const target = typeof input.recordId === "string" && input.recordId ? input.recordId : typeof input.activityId === "string" && input.activityId ? input.activityId : null;
|
|
400
|
+
const limit = Math.min(200, typeof input.limit === "number" ? input.limit : 30);
|
|
401
|
+
const items = listFiles(ctx, target, {
|
|
402
|
+
includeRelated: input.includeRelated === true,
|
|
403
|
+
kind: KINDS.includes(input.kind) ? input.kind : void 0,
|
|
404
|
+
search: typeof input.search === "string" ? input.search : void 0
|
|
405
|
+
});
|
|
406
|
+
return {
|
|
407
|
+
...target ? {} : { usage: fileUsage(ctx) },
|
|
408
|
+
total: items.length,
|
|
409
|
+
files: items.slice(0, limit).map((f) => summarizeFile(ctx, f))
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "describe_attachments",
|
|
415
|
+
description: "What is attached to an activity, or to every activity of a record: file metadata (name, type, size, dimensions) and the images referenced inline in the Markdown body. Use it before summarising a note / task that mentions attachments. Never returns file contents.",
|
|
416
|
+
inputSchema: {
|
|
417
|
+
type: "object",
|
|
418
|
+
properties: {
|
|
419
|
+
activityId: { type: "string" },
|
|
420
|
+
recordId: { type: "string", description: "All activities of this record that have attachments." }
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
mutating: false,
|
|
424
|
+
handler: (ctx, input) => {
|
|
425
|
+
const activityId = typeof input.activityId === "string" ? input.activityId : "";
|
|
426
|
+
const recordId = typeof input.recordId === "string" ? input.recordId : "";
|
|
427
|
+
if (!activityId && !recordId) throw new OpsError("validation", "Give activityId or recordId.");
|
|
428
|
+
if (recordId && !recordOf(ctx.store, recordId)) throw new OpsError("notFound", `Record "${recordId}" not found.`);
|
|
429
|
+
const acts = activitiesOf(ctx.store).filter((a) => activityId ? a.id === activityId : a.targets.some((t) => t.id === recordId));
|
|
430
|
+
if (activityId && !acts.length) throw new OpsError("notFound", `Activity "${activityId}" not found.`);
|
|
431
|
+
const out = acts.map((a) => {
|
|
432
|
+
const ids = [.../* @__PURE__ */ new Set([...a.attachments ?? [], ...inlineFileIds(a.body)])];
|
|
433
|
+
const files = ids.map((id) => fileOf(ctx.store, id)).filter((f) => !!f && !f.deletedAt);
|
|
434
|
+
return { activity: { id: a.id, type: a.type, title: a.title }, inline: inlineFileIds(a.body), files: files.map((f) => summarizeFile(ctx, f)) };
|
|
435
|
+
}).filter((x) => x.files.length);
|
|
436
|
+
return { activities: out };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
];
|
|
440
|
+
const objectProp = { type: "string", description: 'Object id, e.g. "companies", "people", "deals" (see describe_schema).' };
|
|
441
|
+
const fieldsProp = {
|
|
442
|
+
type: "object",
|
|
443
|
+
description: "Field values keyed by field id (see describe_schema). Select fields take the option id or label; relation fields take record ids; dates YYYY-MM-DD; datetimes ISO 8601; multi values as arrays.",
|
|
444
|
+
additionalProperties: true
|
|
445
|
+
};
|
|
446
|
+
const filterSchema = {
|
|
447
|
+
type: "object",
|
|
448
|
+
properties: {
|
|
449
|
+
field: { type: "string", description: "Field id, or id / ownerId / createdAt / updatedAt." },
|
|
450
|
+
op: { type: "string", enum: FILTER_OPS },
|
|
451
|
+
value: { description: "Operand. in/notIn: array. withinDays: number (negative = past). Dates: YYYY-MM-DD, 'today', '+30d'." }
|
|
452
|
+
},
|
|
453
|
+
required: ["field", "op"]
|
|
454
|
+
};
|
|
455
|
+
const targetSchema = { type: "string", description: "Record id the activity is attached to." };
|
|
456
|
+
function summarizeRecord(ctx, record, opts = {}) {
|
|
457
|
+
const object = objectOf(ctx.store, record.object);
|
|
458
|
+
const qctx = object ? queryContext(ctx, object) : void 0;
|
|
459
|
+
const values = {};
|
|
460
|
+
for (const [k, v] of Object.entries(record.fields)) {
|
|
461
|
+
const field = object ? resolveField(object, k) : void 0;
|
|
462
|
+
if (!field || !qctx) {
|
|
463
|
+
values[k] = v;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (field.type === "relation") {
|
|
467
|
+
const ids = Array.isArray(v) ? v : [v];
|
|
468
|
+
values[k] = ids.map((id) => ({ id, label: qctx.recordLabel("", String(id)) ?? String(id) }));
|
|
469
|
+
if (field.relation?.cardinality === "one") values[k] = values[k][0];
|
|
470
|
+
} else if (field.type === "image" || field.type === "files") {
|
|
471
|
+
const ids = (Array.isArray(v) ? v : [v]).map(String);
|
|
472
|
+
const files = ids.map((id) => ({ id, name: ctx.store.get("files", id)?.name ?? id }));
|
|
473
|
+
values[k] = field.type === "image" ? files[0] : files;
|
|
474
|
+
} else if (field.type === "select" || field.type === "multiSelect" || field.type === "user") {
|
|
475
|
+
values[k] = { value: v, label: formatFieldValue(field, v, qctx) };
|
|
476
|
+
} else {
|
|
477
|
+
values[k] = v;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
id: record.id,
|
|
482
|
+
object: record.object,
|
|
483
|
+
label: recordLabel(object, record),
|
|
484
|
+
fields: values,
|
|
485
|
+
...record.ownerId ? { owner: { id: record.ownerId, name: userOf(ctx.store, record.ownerId)?.name } } : {},
|
|
486
|
+
...opts.full ? { createdAt: new Date(record.createdAt).toISOString(), updatedAt: new Date(record.updatedAt).toISOString() } : {},
|
|
487
|
+
...record.deletedAt ? { deleted: true } : {}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function summarizeActivity(a) {
|
|
491
|
+
return {
|
|
492
|
+
id: a.id,
|
|
493
|
+
type: a.type,
|
|
494
|
+
title: a.title,
|
|
495
|
+
...a.body ? { body: a.body.length > 600 ? `${a.body.slice(0, 600)}…` : a.body } : {},
|
|
496
|
+
targets: a.targets,
|
|
497
|
+
...a.dueAt ? { dueAt: new Date(a.dueAt).toISOString() } : {},
|
|
498
|
+
...a.doneAt ? { doneAt: new Date(a.doneAt).toISOString() } : {},
|
|
499
|
+
...a.assigneeId ? { assigneeId: a.assigneeId } : {},
|
|
500
|
+
...a.attachments?.length ? { attachments: a.attachments.length } : {},
|
|
501
|
+
createdAt: new Date(a.createdAt).toISOString()
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function str(input, key, required = true) {
|
|
505
|
+
const v = input[key];
|
|
506
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
507
|
+
if (required) throw new OpsError("validation", `Missing "${key}".`);
|
|
508
|
+
return "";
|
|
509
|
+
}
|
|
510
|
+
function num(input, key, fallback) {
|
|
511
|
+
const v = input[key];
|
|
512
|
+
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
513
|
+
}
|
|
514
|
+
const AI_TOOLS = [
|
|
515
|
+
{
|
|
516
|
+
name: "describe_schema",
|
|
517
|
+
description: "Describe the CRM data model: objects, their fields (ids, types, select options, relations) and pipelines with stages. Call this first to learn field ids.",
|
|
518
|
+
inputSchema: { type: "object", properties: { object: { ...objectProp, description: "Only this object (optional)." } } },
|
|
519
|
+
mutating: false,
|
|
520
|
+
handler: (ctx, input) => describeSchema(ctx, typeof input.object === "string" ? input.object : void 0)
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
name: "search_records",
|
|
524
|
+
description: "Full-text search across records (names, emails, domains, phones) and activity titles. Returns the best matches with ids.",
|
|
525
|
+
inputSchema: {
|
|
526
|
+
type: "object",
|
|
527
|
+
properties: {
|
|
528
|
+
query: { type: "string", description: "Search text." },
|
|
529
|
+
objects: { type: "array", items: { type: "string" }, description: "Restrict to these object ids." },
|
|
530
|
+
limit: { type: "number", default: 10, maximum: 50 }
|
|
531
|
+
},
|
|
532
|
+
required: ["query"]
|
|
533
|
+
},
|
|
534
|
+
mutating: false,
|
|
535
|
+
handler: (ctx, input) => searchRecords(ctx, str(input, "query"), { objects: input.objects, limit: Math.min(50, num(input, "limit", 10)) })
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: "list_records",
|
|
539
|
+
description: "List records of an object with optional filters, sorts and pagination (max 100 per call).",
|
|
540
|
+
inputSchema: {
|
|
541
|
+
type: "object",
|
|
542
|
+
properties: {
|
|
543
|
+
object: objectProp,
|
|
544
|
+
filters: { type: "array", items: filterSchema },
|
|
545
|
+
sorts: { type: "array", items: { type: "object", properties: { field: { type: "string" }, dir: { type: "string", enum: ["asc", "desc"] } }, required: ["field", "dir"] } },
|
|
546
|
+
search: { type: "string" },
|
|
547
|
+
viewId: { type: "string", description: "Apply a saved view's filters and sorts." },
|
|
548
|
+
offset: { type: "number", default: 0 },
|
|
549
|
+
limit: { type: "number", default: 25, maximum: 100 }
|
|
550
|
+
},
|
|
551
|
+
required: ["object"]
|
|
552
|
+
},
|
|
553
|
+
mutating: false,
|
|
554
|
+
handler: (ctx, input) => {
|
|
555
|
+
const page = listRecords(ctx, str(input, "object"), {
|
|
556
|
+
filters: input.filters,
|
|
557
|
+
sorts: input.sorts,
|
|
558
|
+
search: typeof input.search === "string" ? input.search : void 0,
|
|
559
|
+
viewId: typeof input.viewId === "string" ? input.viewId : void 0,
|
|
560
|
+
offset: num(input, "offset", 0),
|
|
561
|
+
limit: Math.min(100, num(input, "limit", 25))
|
|
562
|
+
});
|
|
563
|
+
return { total: page.total, offset: page.offset, hasMore: page.hasMore, records: page.items.map((r) => summarizeRecord(ctx, r)) };
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "get_record",
|
|
568
|
+
description: "Get one record with every field, its relations (both directions) and its latest activities.",
|
|
569
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
570
|
+
mutating: false,
|
|
571
|
+
handler: (ctx, input) => {
|
|
572
|
+
const details = getRecordDetails(ctx, str(input, "id"));
|
|
573
|
+
const activities = listActivities(ctx, { target: details.record.id, limit: 10 }).items.map(summarizeActivity);
|
|
574
|
+
return { ...summarizeRecord(ctx, details.record, { full: true }), relations: details.relations, related: details.inverse.filter((i) => i.records.length).map((i) => ({ object: i.object, via: i.field, records: i.records.slice(0, 25), count: i.records.length })), activities };
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: "create_record",
|
|
579
|
+
description: "Create a record of an object. Returns the created record.",
|
|
580
|
+
inputSchema: { type: "object", properties: { object: objectProp, fields: fieldsProp, ownerId: { type: "string", description: "User id owning the record (default: you)." } }, required: ["object", "fields"] },
|
|
581
|
+
mutating: true,
|
|
582
|
+
handler: async (ctx, input) => summarizeRecord(ctx, await createRecord(ctx, str(input, "object"), input.fields ?? {}, { ...typeof input.ownerId === "string" ? { ownerId: input.ownerId } : {} }))
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: "update_record",
|
|
586
|
+
description: "Update some fields of a record (only the given fields change; null clears a field).",
|
|
587
|
+
inputSchema: { type: "object", properties: { id: { type: "string" }, fields: fieldsProp, ownerId: { type: ["string", "null"] } }, required: ["id", "fields"] },
|
|
588
|
+
mutating: true,
|
|
589
|
+
handler: async (ctx, input) => summarizeRecord(ctx, await updateRecord(ctx, str(input, "id"), input.fields ?? {}, input.ownerId !== void 0 ? { ownerId: input.ownerId } : {}))
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
name: "delete_record",
|
|
593
|
+
description: "Delete a record (soft delete: restorable until the data is compacted).",
|
|
594
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
595
|
+
mutating: true,
|
|
596
|
+
handler: (ctx, input) => deleteRecord(ctx, str(input, "id"))
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: "merge_records",
|
|
600
|
+
description: "Merge duplicate records into a primary one: empty fields are filled from the duplicates, relations and activities move to the primary, duplicates are removed.",
|
|
601
|
+
inputSchema: { type: "object", properties: { primaryId: { type: "string" }, duplicateIds: { type: "array", items: { type: "string" } } }, required: ["primaryId", "duplicateIds"] },
|
|
602
|
+
mutating: true,
|
|
603
|
+
handler: async (ctx, input) => {
|
|
604
|
+
const res = await mergeRecords(ctx, str(input, "primaryId"), input.duplicateIds ?? []);
|
|
605
|
+
return { record: summarizeRecord(ctx, res.record), merged: res.merged, movedActivities: res.movedActivities };
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: "add_note",
|
|
610
|
+
description: "Add a note (markdown) to a record's timeline.",
|
|
611
|
+
inputSchema: { type: "object", properties: { recordId: targetSchema, body: { type: "string" }, title: { type: "string" } }, required: ["recordId", "body"] },
|
|
612
|
+
mutating: true,
|
|
613
|
+
handler: async (ctx, input) => summarizeActivity(await addNote(ctx, str(input, "recordId"), str(input, "body"), typeof input.title === "string" ? input.title : void 0))
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: "log_activity",
|
|
617
|
+
description: "Log a call, email or meeting on one or more records.",
|
|
618
|
+
inputSchema: {
|
|
619
|
+
type: "object",
|
|
620
|
+
properties: {
|
|
621
|
+
type: { type: "string", enum: ACTIVITY_TYPES.filter((t) => t !== "task") },
|
|
622
|
+
title: { type: "string" },
|
|
623
|
+
body: { type: "string" },
|
|
624
|
+
recordIds: { type: "array", items: { type: "string" } },
|
|
625
|
+
meta: { type: "object", additionalProperties: true, description: "e.g. { durationMin } for calls, { startAt, endAt } (epoch ms) for meetings." }
|
|
626
|
+
},
|
|
627
|
+
required: ["type", "title", "recordIds"]
|
|
628
|
+
},
|
|
629
|
+
mutating: true,
|
|
630
|
+
handler: async (ctx, input) => {
|
|
631
|
+
const targets = (input.recordIds ?? []).map((id) => ({ object: "", id }));
|
|
632
|
+
return summarizeActivity(await addActivity(ctx, { type: str(input, "type"), title: str(input, "title"), body: typeof input.body === "string" ? input.body : void 0, targets, meta: input.meta }));
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: "create_task",
|
|
637
|
+
description: "Create a task, optionally attached to records, with a due date and an assignee.",
|
|
638
|
+
inputSchema: {
|
|
639
|
+
type: "object",
|
|
640
|
+
properties: {
|
|
641
|
+
title: { type: "string" },
|
|
642
|
+
body: { type: "string" },
|
|
643
|
+
dueAt: { type: "string", description: "ISO date or datetime." },
|
|
644
|
+
assigneeId: { type: "string" },
|
|
645
|
+
recordIds: { type: "array", items: { type: "string" } }
|
|
646
|
+
},
|
|
647
|
+
required: ["title"]
|
|
648
|
+
},
|
|
649
|
+
mutating: true,
|
|
650
|
+
handler: async (ctx, input) => summarizeActivity(
|
|
651
|
+
await createTask(ctx, {
|
|
652
|
+
title: str(input, "title"),
|
|
653
|
+
body: typeof input.body === "string" ? input.body : void 0,
|
|
654
|
+
dueAt: typeof input.dueAt === "string" ? input.dueAt : void 0,
|
|
655
|
+
assigneeId: typeof input.assigneeId === "string" ? input.assigneeId : void 0,
|
|
656
|
+
targets: (input.recordIds ?? []).map((id) => ({ object: "", id }))
|
|
657
|
+
})
|
|
658
|
+
)
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
name: "complete_task",
|
|
662
|
+
description: "Mark a task as done (or reopen it with done: false).",
|
|
663
|
+
inputSchema: { type: "object", properties: { id: { type: "string" }, done: { type: "boolean", default: true } }, required: ["id"] },
|
|
664
|
+
mutating: true,
|
|
665
|
+
handler: async (ctx, input) => summarizeActivity(await completeTask(ctx, str(input, "id"), input.done !== false))
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
name: "list_tasks",
|
|
669
|
+
description: "List tasks: open by default, optionally for one assignee / record, overdue only, or due within N days. Sorted by due date.",
|
|
670
|
+
inputSchema: {
|
|
671
|
+
type: "object",
|
|
672
|
+
properties: {
|
|
673
|
+
assigneeId: { type: "string" },
|
|
674
|
+
recordId: { type: "string" },
|
|
675
|
+
status: { type: "string", enum: ["open", "done", "all"], default: "open" },
|
|
676
|
+
overdue: { type: "boolean" },
|
|
677
|
+
dueWithinDays: { type: "number" },
|
|
678
|
+
limit: { type: "number", default: 25, maximum: 100 }
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
mutating: false,
|
|
682
|
+
handler: (ctx, input) => {
|
|
683
|
+
const page = listTasks(ctx, {
|
|
684
|
+
assigneeId: typeof input.assigneeId === "string" ? input.assigneeId : void 0,
|
|
685
|
+
target: typeof input.recordId === "string" ? input.recordId : void 0,
|
|
686
|
+
status: input.status ?? "open",
|
|
687
|
+
overdue: input.overdue === true,
|
|
688
|
+
dueWithinDays: typeof input.dueWithinDays === "number" ? input.dueWithinDays : void 0,
|
|
689
|
+
limit: Math.min(100, num(input, "limit", 25))
|
|
690
|
+
});
|
|
691
|
+
return { total: page.total, tasks: page.items.map(summarizeActivity) };
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: "list_activities",
|
|
696
|
+
description: "A record's timeline (notes, calls, emails, meetings, tasks), most recent first. includeRelated adds activities of linked records (e.g. a company's people and deals).",
|
|
697
|
+
inputSchema: {
|
|
698
|
+
type: "object",
|
|
699
|
+
properties: {
|
|
700
|
+
recordId: { type: "string" },
|
|
701
|
+
types: { type: "array", items: { type: "string", enum: ACTIVITY_TYPES } },
|
|
702
|
+
includeRelated: { type: "boolean" },
|
|
703
|
+
limit: { type: "number", default: 20, maximum: 100 }
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
mutating: false,
|
|
707
|
+
handler: (ctx, input) => {
|
|
708
|
+
const page = listActivities(ctx, {
|
|
709
|
+
target: typeof input.recordId === "string" ? input.recordId : void 0,
|
|
710
|
+
types: input.types,
|
|
711
|
+
includeRelated: input.includeRelated === true,
|
|
712
|
+
limit: Math.min(100, num(input, "limit", 20))
|
|
713
|
+
});
|
|
714
|
+
return { total: page.total, activities: page.items.map(summarizeActivity) };
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
name: "move_deal_stage",
|
|
719
|
+
description: "Move a deal (or any pipeline record) to a stage, by stage id or label. Updates probability; lostReason for lost stages.",
|
|
720
|
+
inputSchema: { type: "object", properties: { id: { type: "string" }, stage: { type: "string" }, lostReason: { type: "string" } }, required: ["id", "stage"] },
|
|
721
|
+
mutating: true,
|
|
722
|
+
handler: async (ctx, input) => summarizeRecord(ctx, await moveStage(ctx, str(input, "id"), str(input, "stage"), { lostReason: typeof input.lostReason === "string" ? input.lostReason : void 0 }))
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
name: "pipeline_summary",
|
|
726
|
+
description: "Pipeline overview: per stage deal count, amount and weighted amount, plus open / weighted / won totals and open deals by close month.",
|
|
727
|
+
inputSchema: { type: "object", properties: { pipelineId: { type: "string" }, ownerId: { type: "string" } } },
|
|
728
|
+
mutating: false,
|
|
729
|
+
handler: (ctx, input) => ({
|
|
730
|
+
pipelines: listPipelines(ctx).map((p) => ({ id: p.id, name: p.name })),
|
|
731
|
+
...forecast(ctx, { pipelineId: typeof input.pipelineId === "string" ? input.pipelineId : void 0, ownerId: typeof input.ownerId === "string" ? input.ownerId : void 0 })
|
|
732
|
+
})
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: "run_report",
|
|
736
|
+
description: `Run a named report. Reports: ${Object.entries(REPORTS).map(([k, v]) => `${k} (${v.description})`).join("; ")}`,
|
|
737
|
+
inputSchema: {
|
|
738
|
+
type: "object",
|
|
739
|
+
properties: { name: { type: "string", enum: Object.keys(REPORTS) }, params: { type: "object", additionalProperties: true, description: "Report parameters, e.g. { months: 6 } or { days: 30 }." } },
|
|
740
|
+
required: ["name"]
|
|
741
|
+
},
|
|
742
|
+
mutating: false,
|
|
743
|
+
handler: (ctx, input) => runReport(ctx, str(input, "name"), input.params ?? {})
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
name: "create_field",
|
|
747
|
+
description: "Add a custom field to an object (admin only).",
|
|
748
|
+
inputSchema: {
|
|
749
|
+
type: "object",
|
|
750
|
+
properties: {
|
|
751
|
+
object: objectProp,
|
|
752
|
+
label: { type: "string" },
|
|
753
|
+
type: { type: "string", enum: FIELD_TYPES },
|
|
754
|
+
options: { type: "array", items: { type: "string" }, description: "Option labels for select / multiSelect." },
|
|
755
|
+
relationObject: { type: "string", description: "Target object for relation fields." },
|
|
756
|
+
cardinality: { type: "string", enum: ["one", "many"] },
|
|
757
|
+
description: { type: "string" }
|
|
758
|
+
},
|
|
759
|
+
required: ["object", "label", "type"]
|
|
760
|
+
},
|
|
761
|
+
mutating: true,
|
|
762
|
+
handler: (ctx, input) => createField(ctx, str(input, "object"), {
|
|
763
|
+
label: str(input, "label"),
|
|
764
|
+
type: str(input, "type"),
|
|
765
|
+
options: input.options,
|
|
766
|
+
description: typeof input.description === "string" ? input.description : void 0,
|
|
767
|
+
...input.relationObject ? { relation: { object: String(input.relationObject), cardinality: input.cardinality === "many" ? "many" : "one" } } : {}
|
|
768
|
+
})
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
name: "import_csv",
|
|
772
|
+
description: "Import CSV text into an object. Columns are mapped to fields automatically (or via mapping {header: fieldId}); duplicates (people by email, companies by domain) are updated. Runs as a DRY RUN unless confirm is true — show the report to the user before confirming.",
|
|
773
|
+
inputSchema: {
|
|
774
|
+
type: "object",
|
|
775
|
+
properties: {
|
|
776
|
+
object: objectProp,
|
|
777
|
+
csv: { type: "string" },
|
|
778
|
+
mapping: { type: "object", additionalProperties: { type: "string" } },
|
|
779
|
+
onDuplicate: { type: "string", enum: ["update", "skip", "create"], default: "update" },
|
|
780
|
+
confirm: { type: "boolean", default: false }
|
|
781
|
+
},
|
|
782
|
+
required: ["object", "csv"]
|
|
783
|
+
},
|
|
784
|
+
mutating: true,
|
|
785
|
+
handler: async (ctx, input) => {
|
|
786
|
+
const report = await importCsv(ctx, {
|
|
787
|
+
object: str(input, "object"),
|
|
788
|
+
csv: String(input.csv ?? ""),
|
|
789
|
+
mapping: input.mapping,
|
|
790
|
+
onDuplicate: input.onDuplicate ?? "update",
|
|
791
|
+
dryRun: input.confirm !== true
|
|
792
|
+
});
|
|
793
|
+
return { ...report, rows: report.rows.slice(0, 50), rowsTruncated: report.rows.length > 50 };
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
// Automations, lead forms, email drafts (src/ai/automationTools.ts).
|
|
797
|
+
...AUTOMATION_FORM_TOOLS,
|
|
798
|
+
// Modules, appearance, extensions (src/ai/customizeTools.ts).
|
|
799
|
+
...CUSTOMIZE_TOOLS,
|
|
800
|
+
// Files metadata (src/ai/fileTools.ts).
|
|
801
|
+
...FILE_TOOLS
|
|
802
|
+
];
|
|
803
|
+
const extraTools = /* @__PURE__ */ new Map();
|
|
804
|
+
const toolListeners = /* @__PURE__ */ new Set();
|
|
805
|
+
const TOOL_NAME_RE = /^[a-z][a-z0-9_]{1,63}$/;
|
|
806
|
+
function registerAiTool(tool) {
|
|
807
|
+
if (!tool || typeof tool.name !== "string" || !TOOL_NAME_RE.test(tool.name)) throw new Error(`Invalid AI tool name "${String(tool?.name)}" (snake_case, ≤ 64 chars).`);
|
|
808
|
+
if (typeof tool.handler !== "function") throw new Error(`AI tool "${tool.name}" needs a handler.`);
|
|
809
|
+
if (!tool.inputSchema || tool.inputSchema.type !== "object") throw new Error(`AI tool "${tool.name}" needs an object inputSchema.`);
|
|
810
|
+
if (AI_TOOLS.some((t) => t.name === tool.name) || extraTools.has(tool.name)) throw new Error(`AI tool "${tool.name}" already exists.`);
|
|
811
|
+
extraTools.set(tool.name, { ...tool, mutating: tool.mutating !== false });
|
|
812
|
+
for (const l of [...toolListeners]) l();
|
|
813
|
+
return () => {
|
|
814
|
+
if (extraTools.delete(tool.name)) for (const l of [...toolListeners]) l();
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function listAiTools() {
|
|
818
|
+
return extraTools.size ? [...AI_TOOLS, ...extraTools.values()] : AI_TOOLS;
|
|
819
|
+
}
|
|
820
|
+
function subscribeAiTools(listener) {
|
|
821
|
+
toolListeners.add(listener);
|
|
822
|
+
return () => toolListeners.delete(listener);
|
|
823
|
+
}
|
|
824
|
+
function findTool(name) {
|
|
825
|
+
return AI_TOOLS.find((t) => t.name === name) ?? extraTools.get(name);
|
|
826
|
+
}
|
|
827
|
+
function jsonTypeOf(value) {
|
|
828
|
+
if (value === null) return "null";
|
|
829
|
+
if (Array.isArray(value)) return "array";
|
|
830
|
+
return typeof value;
|
|
831
|
+
}
|
|
832
|
+
function checkToolInput(schema, value, path) {
|
|
833
|
+
const types = schema.type === void 0 ? [] : Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
834
|
+
let v = value;
|
|
835
|
+
if (types.length && v !== null && v !== void 0) {
|
|
836
|
+
const actual = jsonTypeOf(v);
|
|
837
|
+
const accepts = (t) => t === actual || t === "integer" && actual === "number" && Number.isInteger(v);
|
|
838
|
+
if (!types.some(accepts)) {
|
|
839
|
+
if ((types.includes("number") || types.includes("integer")) && typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) v = Number(v);
|
|
840
|
+
else if (types.includes("boolean") && (v === "true" || v === "false")) v = v === "true";
|
|
841
|
+
else throw new OpsError("validation", `${path} must be ${types.map((t) => t === "array" || t === "object" || t === "integer" ? `an ${t}` : `a ${t}`).join(" or ")}, got ${actual === "array" || actual === "object" ? `an ${actual}` : `${actual} ${JSON.stringify(v)}`.slice(0, 80)}.`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (v === null || v === void 0) return v;
|
|
845
|
+
if (schema.enum && !schema.enum.includes(v)) {
|
|
846
|
+
throw new OpsError("validation", `${path} must be one of ${schema.enum.map((e) => JSON.stringify(e)).join(", ")}, got ${JSON.stringify(v).slice(0, 80)}.`);
|
|
847
|
+
}
|
|
848
|
+
if (Array.isArray(v) && schema.items) {
|
|
849
|
+
const items = schema.items;
|
|
850
|
+
return v.map((item, i) => checkToolInput(items, item, `${path}[${i}]`));
|
|
851
|
+
}
|
|
852
|
+
if (jsonTypeOf(v) === "object" && (schema.properties || schema.required)) {
|
|
853
|
+
const obj = { ...v };
|
|
854
|
+
for (const key of schema.required ?? []) {
|
|
855
|
+
if (obj[key] === void 0 || obj[key] === null) throw new OpsError("validation", `${path}: missing required "${key}".`);
|
|
856
|
+
}
|
|
857
|
+
for (const [key, sub] of Object.entries(schema.properties ?? {})) {
|
|
858
|
+
if (obj[key] !== void 0) obj[key] = checkToolInput(sub, obj[key], `${path}.${key}`);
|
|
859
|
+
}
|
|
860
|
+
return obj;
|
|
861
|
+
}
|
|
862
|
+
return v;
|
|
863
|
+
}
|
|
864
|
+
async function callTool(ctx, name, input) {
|
|
865
|
+
const tool = findTool(name);
|
|
866
|
+
if (!tool) throw new OpsError("notFound", `Unknown tool "${name}". Tools: ${listAiTools().map((t) => t.name).join(", ")}.`);
|
|
867
|
+
if (input !== void 0 && input !== null && jsonTypeOf(input) !== "object") throw new OpsError("validation", `${name}: the input must be a JSON object, got ${jsonTypeOf(input) === "array" ? "an array" : `a ${jsonTypeOf(input)}`}.`);
|
|
868
|
+
const raw = input ?? {};
|
|
869
|
+
for (const key of tool.inputSchema.required ?? []) {
|
|
870
|
+
if (raw[key] === void 0 || raw[key] === null) throw new OpsError("validation", `${name}: missing required "${key}".`);
|
|
871
|
+
}
|
|
872
|
+
const args = checkToolInput({ ...tool.inputSchema, required: [] }, raw, name);
|
|
873
|
+
return tool.handler(ctx, args);
|
|
874
|
+
}
|
|
875
|
+
export {
|
|
876
|
+
AI_TOOLS,
|
|
877
|
+
callTool,
|
|
878
|
+
checkToolInput,
|
|
879
|
+
findTool,
|
|
880
|
+
listAiTools,
|
|
881
|
+
registerAiTool,
|
|
882
|
+
subscribeAiTools,
|
|
883
|
+
summarizeRecord
|
|
884
|
+
};
|