@agent-native/core 0.123.0 → 0.123.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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +16 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/templates-meta.ts +12 -0
- package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
- package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
- package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
- package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
- package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
- package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
- package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
- package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
- package/corpus/templates/crm/.env.example +5 -0
- package/corpus/templates/crm/AGENTS.md +82 -0
- package/corpus/templates/crm/CHANGELOG.md +25 -0
- package/corpus/templates/crm/DEVELOPING.md +66 -0
- package/corpus/templates/crm/README.md +122 -0
- package/corpus/templates/crm/_gitignore +12 -0
- package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
- package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
- package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
- package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
- package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
- package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
- package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
- package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
- package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
- package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
- package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
- package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
- package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
- package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
- package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
- package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
- package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
- package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
- package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
- package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
- package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
- package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
- package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
- package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
- package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
- package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
- package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
- package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
- package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
- package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
- package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
- package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
- package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
- package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
- package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
- package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
- package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
- package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
- package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
- package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
- package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
- package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
- package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
- package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
- package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
- package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
- package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
- package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
- package/corpus/templates/crm/actions/navigate.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
- package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
- package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
- package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
- package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
- package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
- package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
- package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
- package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
- package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
- package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
- package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
- package/corpus/templates/crm/actions/run.ts +7 -0
- package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
- package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
- package/corpus/templates/crm/actions/sync-crm.ts +146 -0
- package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
- package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
- package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
- package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
- package/corpus/templates/crm/actions/view-screen.ts +324 -0
- package/corpus/templates/crm/agent-native.app-skill.json +69 -0
- package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
- package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
- package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
- package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
- package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
- package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
- package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
- package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
- package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
- package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
- package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
- package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
- package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
- package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
- package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
- package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
- package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
- package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
- package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
- package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
- package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
- package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
- package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
- package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
- package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
- package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
- package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
- package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
- package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
- package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
- package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
- package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
- package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
- package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
- package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
- package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
- package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
- package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
- package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
- package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
- package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
- package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
- package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
- package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/crm/app/design-system.ts +3 -0
- package/corpus/templates/crm/app/entry.client.tsx +19 -0
- package/corpus/templates/crm/app/entry.server.tsx +10 -0
- package/corpus/templates/crm/app/global.css +167 -0
- package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
- package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
- package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
- package/corpus/templates/crm/app/i18n/index.ts +34 -0
- package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
- package/corpus/templates/crm/app/lib/navigation.ts +17 -0
- package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
- package/corpus/templates/crm/app/lib/types.ts +172 -0
- package/corpus/templates/crm/app/lib/utils.ts +1 -0
- package/corpus/templates/crm/app/root.tsx +93 -0
- package/corpus/templates/crm/app/routes/_index.tsx +26 -0
- package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
- package/corpus/templates/crm/app/routes/agent.tsx +5 -0
- package/corpus/templates/crm/app/routes/ask.tsx +53 -0
- package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
- package/corpus/templates/crm/app/routes/lists.tsx +228 -0
- package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
- package/corpus/templates/crm/app/routes/people.tsx +41 -0
- package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
- package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
- package/corpus/templates/crm/app/routes/records.tsx +64 -0
- package/corpus/templates/crm/app/routes/settings.tsx +118 -0
- package/corpus/templates/crm/app/routes/setup.tsx +308 -0
- package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
- package/corpus/templates/crm/app/routes/views.tsx +683 -0
- package/corpus/templates/crm/app/routes.ts +19 -0
- package/corpus/templates/crm/app/vite-env.d.ts +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-any-list-or-view-can-be-shown-as-a-board-grouped-by-stage-so.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-press-cmd-k-to-jump-to-any-record-list-view-or-setting-and-u.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
- package/corpus/templates/crm/components.json +20 -0
- package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
- package/corpus/templates/crm/docs/features/README.md +55 -0
- package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
- package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
- package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
- package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
- package/corpus/templates/crm/learnings.defaults.md +5 -0
- package/corpus/templates/crm/netlify.toml +11 -0
- package/corpus/templates/crm/package.json +61 -0
- package/corpus/templates/crm/public/favicon.svg +1 -0
- package/corpus/templates/crm/public/icon-180.svg +1 -0
- package/corpus/templates/crm/public/manifest.json +11 -0
- package/corpus/templates/crm/react-router.config.ts +7 -0
- package/corpus/templates/crm/server/crm/adapter.ts +32 -0
- package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
- package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
- package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
- package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
- package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
- package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
- package/corpus/templates/crm/server/crm/read-through.ts +207 -0
- package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
- package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
- package/corpus/templates/crm/server/db/index.ts +191 -0
- package/corpus/templates/crm/server/db/schema.ts +647 -0
- package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
- package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
- package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
- package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
- package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
- package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
- package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
- package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
- package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
- package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
- package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
- package/corpus/templates/crm/server/middleware/auth.ts +4 -0
- package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
- package/corpus/templates/crm/server/plugins/auth.ts +14 -0
- package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
- package/corpus/templates/crm/server/plugins/db.ts +738 -0
- package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
- package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
- package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
- package/corpus/templates/crm/shared/crm-contract.ts +417 -0
- package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
- package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
- package/corpus/templates/crm/ssr-entry.ts +11 -0
- package/corpus/templates/crm/tsconfig.json +24 -0
- package/corpus/templates/crm/vite.config.ts +15 -0
- package/corpus/templates/crm/vitest.config.ts +5 -0
- package/corpus/templates/design/app/i18n-data.ts +12 -12
- package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +12 -0
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/templates-meta.ts +12 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core/action";
|
|
2
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
|
|
8
|
+
export default defineAction({
|
|
9
|
+
description:
|
|
10
|
+
"Confirm, dismiss, or return one access-scoped CRM signal to the unreviewed queue.",
|
|
11
|
+
schema: z.object({
|
|
12
|
+
signalId: z.string().trim().min(1).max(128),
|
|
13
|
+
reviewStatus: z.enum(["unreviewed", "confirmed", "dismissed"]),
|
|
14
|
+
}),
|
|
15
|
+
run: async (args) => {
|
|
16
|
+
await assertAccess("crm-signal", args.signalId, "editor");
|
|
17
|
+
const now = new Date().toISOString();
|
|
18
|
+
await getDb()
|
|
19
|
+
.update(schema.crmSignals)
|
|
20
|
+
.set({ reviewStatus: args.reviewStatus, updatedAt: now })
|
|
21
|
+
.where(eq(schema.crmSignals.id, args.signalId));
|
|
22
|
+
const [saved] = await getDb()
|
|
23
|
+
.select()
|
|
24
|
+
.from(schema.crmSignals)
|
|
25
|
+
.where(eq(schema.crmSignals.id, args.signalId))
|
|
26
|
+
.limit(1);
|
|
27
|
+
if (!saved)
|
|
28
|
+
throw new Error("CRM signal could not be verified after review.");
|
|
29
|
+
return saved;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { defineAction, type ActionRunContext } from "@agent-native/core/action";
|
|
2
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq, inArray, isNull } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
CrmAttributeValueError,
|
|
9
|
+
normalizeCrmAttributeValue,
|
|
10
|
+
writeCrmRecordField,
|
|
11
|
+
type CrmWritableAttribute,
|
|
12
|
+
} from "../server/lib/record-fields.js";
|
|
13
|
+
import { ATTRIBUTE_TYPE_SPECS } from "../shared/crm-attributes.js";
|
|
14
|
+
import { requireCrmScope, toJson } from "./_crm-action-utils.js";
|
|
15
|
+
import {
|
|
16
|
+
legacyValueTypeFor,
|
|
17
|
+
loadAttributeOptions,
|
|
18
|
+
requireEditableAttribute,
|
|
19
|
+
type CrmAttributeRow,
|
|
20
|
+
} from "./_crm-attribute-utils.js";
|
|
21
|
+
|
|
22
|
+
const MAX_RECORDS = 100;
|
|
23
|
+
const MAX_CONTEXT_ATTRIBUTES = 12;
|
|
24
|
+
const MAX_CONTEXT_CHARS = 200;
|
|
25
|
+
const MAX_PROVENANCE_CHARS = 4000;
|
|
26
|
+
|
|
27
|
+
/** Fill modes this action executes. `formula` is computed, not reasoned. */
|
|
28
|
+
const AGENT_FILL_MODES = [
|
|
29
|
+
"agent-summarize",
|
|
30
|
+
"agent-classify",
|
|
31
|
+
"agent-research",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
const valueInput = z.object({
|
|
35
|
+
recordId: z.string().trim().min(1).max(128),
|
|
36
|
+
value: z.unknown().describe("The value to write, typed for this attribute."),
|
|
37
|
+
source: z
|
|
38
|
+
.string()
|
|
39
|
+
.trim()
|
|
40
|
+
.max(200)
|
|
41
|
+
.optional()
|
|
42
|
+
.describe(
|
|
43
|
+
"Where the value came from, e.g. the page title or 'call notes'.",
|
|
44
|
+
),
|
|
45
|
+
sourceUrl: z.string().trim().max(2000).optional(),
|
|
46
|
+
reasoning: z
|
|
47
|
+
.string()
|
|
48
|
+
.trim()
|
|
49
|
+
.max(1000)
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Why this value, in one or two sentences. Shown in the cell."),
|
|
52
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export default defineAction({
|
|
56
|
+
description:
|
|
57
|
+
"Fill an agent-filled CRM attribute for a bounded set of records. MANUAL TRIGGER ONLY — nothing schedules or auto-runs this, by design. Call it with no `values` to get the brief: the attribute, its managed options, and each record's current value and context. Reason over that brief yourself, then call it again with `values` to write. Every written value carries its source, reasoning, and confidence as provenance and is stamped actorType=agent, so the grid can show the user WHY a cell says what it says. Writes MERGE: a value a human edited, or a value bought by a paid enrichment run, is kept and reported as kept-existing; an unchanged value is not rewritten at all. Classifying against a status or select attribute must produce one of its managed options — an unknown option is rejected for the whole call and nothing is written.",
|
|
58
|
+
schema: z.object({
|
|
59
|
+
attributeId: z.string().trim().min(1).max(128),
|
|
60
|
+
recordIds: z
|
|
61
|
+
.array(z.string().trim().min(1).max(128))
|
|
62
|
+
.min(1)
|
|
63
|
+
.max(MAX_RECORDS),
|
|
64
|
+
values: z
|
|
65
|
+
.array(valueInput)
|
|
66
|
+
.max(MAX_RECORDS)
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Omit to receive the brief; supply to write the results."),
|
|
69
|
+
}),
|
|
70
|
+
audit: {
|
|
71
|
+
summary: (args, result) => {
|
|
72
|
+
const outcome = result as { mode: string; written?: number };
|
|
73
|
+
return outcome.mode === "apply"
|
|
74
|
+
? `Agent-filled CRM attribute ${args.attributeId} on ${outcome.written ?? 0} of ${args.recordIds.length} records`
|
|
75
|
+
: `Prepared agent fill for CRM attribute ${args.attributeId} across ${args.recordIds.length} records`;
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
run: async (args, ctx?: ActionRunContext) => {
|
|
79
|
+
const ownership = requireCrmScope(ctx);
|
|
80
|
+
const attributeRow = await requireEditableAttribute(args.attributeId);
|
|
81
|
+
const attribute = requireAgentFillable(attributeRow);
|
|
82
|
+
const spec = ATTRIBUTE_TYPE_SPECS[attributeRow.attributeType];
|
|
83
|
+
|
|
84
|
+
const options = (
|
|
85
|
+
await loadAttributeOptions({
|
|
86
|
+
attributeIds: [attributeRow.id],
|
|
87
|
+
includeArchived: false,
|
|
88
|
+
})
|
|
89
|
+
).get(attributeRow.id);
|
|
90
|
+
const allowedOptions = (options ?? []).map((option) => ({
|
|
91
|
+
value: option.value,
|
|
92
|
+
title: option.title,
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
const records = await loadRecords(args.recordIds);
|
|
96
|
+
const current = await loadCurrentValues(args.recordIds, attribute.apiSlug);
|
|
97
|
+
|
|
98
|
+
if (!args.values) {
|
|
99
|
+
const context = await loadRecordContext(args.recordIds);
|
|
100
|
+
return {
|
|
101
|
+
mode: "prepare" as const,
|
|
102
|
+
attribute: {
|
|
103
|
+
id: attributeRow.id,
|
|
104
|
+
apiSlug: attribute.apiSlug,
|
|
105
|
+
label: attributeRow.label,
|
|
106
|
+
description: attributeRow.description,
|
|
107
|
+
attributeType: attributeRow.attributeType,
|
|
108
|
+
multi: attributeRow.multi,
|
|
109
|
+
fillMode: attributeRow.fillMode,
|
|
110
|
+
fillConfigJson: attributeRow.fillConfigJson,
|
|
111
|
+
allowedOptions,
|
|
112
|
+
},
|
|
113
|
+
records: args.recordIds.map((recordId) => {
|
|
114
|
+
const record = records.get(recordId)!;
|
|
115
|
+
const existing = current.get(recordId);
|
|
116
|
+
return {
|
|
117
|
+
recordId,
|
|
118
|
+
displayName: record.displayName,
|
|
119
|
+
domain: record.domain,
|
|
120
|
+
objectType: record.objectType,
|
|
121
|
+
currentValue: existing ? readStoredValue(existing) : null,
|
|
122
|
+
// A protected cell is reported up front so the caller does not
|
|
123
|
+
// spend reasoning on a value that will be kept anyway.
|
|
124
|
+
protectedBy: existing ? protectedBy(existing) : null,
|
|
125
|
+
context: context.get(recordId) ?? {},
|
|
126
|
+
};
|
|
127
|
+
}),
|
|
128
|
+
next: "Reason over these records, then call run-crm-attribute-fill again with the same attributeId and a values array.",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const supplied = new Map(
|
|
133
|
+
args.values.map((entry) => [entry.recordId, entry]),
|
|
134
|
+
);
|
|
135
|
+
const unknownTarget = args.values.find(
|
|
136
|
+
(entry) => !args.recordIds.includes(entry.recordId),
|
|
137
|
+
);
|
|
138
|
+
if (unknownTarget) {
|
|
139
|
+
throw new CrmAttributeValueError(
|
|
140
|
+
"crm-attribute-fill-unknown-record",
|
|
141
|
+
`Record ${unknownTarget.recordId} is not in this fill's recordIds. Only records the fill was prepared for may be written.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Validate EVERY value before writing ANY. A batch that writes three cells
|
|
146
|
+
// and then rejects the fourth leaves the user with a partly-filled column
|
|
147
|
+
// and no way to tell which half the agent stands behind.
|
|
148
|
+
const knownOptionValues = new Set(
|
|
149
|
+
allowedOptions.map((option) => option.value),
|
|
150
|
+
);
|
|
151
|
+
for (const entry of args.values) {
|
|
152
|
+
normalizeCrmAttributeValue(attribute, entry.value as never);
|
|
153
|
+
if (!spec.usesOptions) continue;
|
|
154
|
+
for (const value of optionValuesOf(entry.value)) {
|
|
155
|
+
if (!knownOptionValues.has(value)) {
|
|
156
|
+
throw new CrmAttributeValueError(
|
|
157
|
+
"crm-unknown-option",
|
|
158
|
+
`"${value}" is not an option of ${attribute.apiSlug}. Known options: ${
|
|
159
|
+
allowedOptions.map((option) => option.value).join(", ") ||
|
|
160
|
+
"(none defined)"
|
|
161
|
+
}. Add the option first — a fill never creates one.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const now = new Date().toISOString();
|
|
168
|
+
const results: Array<{
|
|
169
|
+
recordId: string;
|
|
170
|
+
outcome: "written" | "unchanged" | "kept-existing" | "no-value";
|
|
171
|
+
keptBecause?: string;
|
|
172
|
+
}> = [];
|
|
173
|
+
|
|
174
|
+
for (const recordId of args.recordIds) {
|
|
175
|
+
const entry = supplied.get(recordId);
|
|
176
|
+
if (!entry) {
|
|
177
|
+
results.push({ recordId, outcome: "no-value" });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const existing = current.get(recordId);
|
|
181
|
+
const kept = existing ? protectedBy(existing) : null;
|
|
182
|
+
if (kept) {
|
|
183
|
+
results.push({ recordId, outcome: "kept-existing", keptBecause: kept });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const write = await writeCrmRecordField({
|
|
188
|
+
target: { recordId },
|
|
189
|
+
attribute,
|
|
190
|
+
value: entry.value as never,
|
|
191
|
+
actor: { type: "agent", id: ctx?.userEmail ?? ownership.ownerEmail },
|
|
192
|
+
ownership,
|
|
193
|
+
provenanceJson: toJson(
|
|
194
|
+
[
|
|
195
|
+
{
|
|
196
|
+
fieldName: attribute.apiSlug,
|
|
197
|
+
// The grid renders this as the cell's "source" line.
|
|
198
|
+
provider: entry.source ?? `fill:${attributeRow.fillMode}`,
|
|
199
|
+
...(entry.sourceUrl ? { sourceUrl: entry.sourceUrl } : {}),
|
|
200
|
+
...(entry.reasoning ? { reasoning: entry.reasoning } : {}),
|
|
201
|
+
...(entry.confidence === undefined
|
|
202
|
+
? {}
|
|
203
|
+
: { confidence: entry.confidence }),
|
|
204
|
+
observedAt: now,
|
|
205
|
+
fillMode: attributeRow.fillMode,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
MAX_PROVENANCE_CHARS,
|
|
209
|
+
),
|
|
210
|
+
now,
|
|
211
|
+
});
|
|
212
|
+
results.push({
|
|
213
|
+
recordId,
|
|
214
|
+
outcome: write.changed ? "written" : "unchanged",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
mode: "apply" as const,
|
|
220
|
+
attributeId: attributeRow.id,
|
|
221
|
+
apiSlug: attribute.apiSlug,
|
|
222
|
+
written: results.filter((row) => row.outcome === "written").length,
|
|
223
|
+
unchanged: results.filter((row) => row.outcome === "unchanged").length,
|
|
224
|
+
keptExisting: results.filter((row) => row.outcome === "kept-existing")
|
|
225
|
+
.length,
|
|
226
|
+
noValue: results.filter((row) => row.outcome === "no-value").length,
|
|
227
|
+
results,
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The attribute as something this action may write, or a typed 422 explaining
|
|
234
|
+
* why not. A misconfigured attribute fails here rather than filling a column the
|
|
235
|
+
* next provider sync will overwrite.
|
|
236
|
+
*/
|
|
237
|
+
function requireAgentFillable(row: CrmAttributeRow): CrmWritableAttribute {
|
|
238
|
+
if (!row.fillMode) {
|
|
239
|
+
throw new CrmAttributeValueError(
|
|
240
|
+
"crm-attribute-not-fillable",
|
|
241
|
+
`Attribute ${row.id} has no fill mode. Set one of ${AGENT_FILL_MODES.join(", ")} on the attribute first.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (!(AGENT_FILL_MODES as readonly string[]).includes(row.fillMode)) {
|
|
245
|
+
throw new CrmAttributeValueError(
|
|
246
|
+
"crm-attribute-fill-mode-unsupported",
|
|
247
|
+
`Attribute ${row.id} has fill mode "${row.fillMode}", which is computed rather than reasoned. This action runs ${AGENT_FILL_MODES.join(", ")} only.`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (row.archived) {
|
|
251
|
+
throw new CrmAttributeValueError(
|
|
252
|
+
"crm-attribute-archived",
|
|
253
|
+
`Attribute ${row.id} is archived. Restore it before filling it.`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (row.authority === "provider") {
|
|
257
|
+
throw new CrmAttributeValueError(
|
|
258
|
+
"crm-attribute-provider-authority",
|
|
259
|
+
`Attribute ${row.id} is provider-authoritative, so the next sync would overwrite anything written here. Change its authority to derived-local or local-authoritative first.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
if (
|
|
263
|
+
row.storagePolicy !== "mirrored" &&
|
|
264
|
+
row.storagePolicy !== "derived-local" &&
|
|
265
|
+
row.storagePolicy !== "local-authoritative"
|
|
266
|
+
) {
|
|
267
|
+
throw new CrmAttributeValueError(
|
|
268
|
+
"crm-attribute-not-stored-locally",
|
|
269
|
+
`Attribute ${row.id} has storage policy "${row.storagePolicy}" and holds no local value to fill.`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
if (
|
|
273
|
+
row.fillMode === "agent-classify" &&
|
|
274
|
+
!ATTRIBUTE_TYPE_SPECS[row.attributeType].usesOptions
|
|
275
|
+
) {
|
|
276
|
+
throw new CrmAttributeValueError(
|
|
277
|
+
"crm-attribute-classify-without-options",
|
|
278
|
+
`Attribute ${row.id} is a ${row.attributeType}, which has no managed options to classify into. Use agent-summarize or agent-research instead.`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
id: row.id,
|
|
283
|
+
apiSlug: row.apiSlug ?? row.fieldName,
|
|
284
|
+
attributeType: row.attributeType,
|
|
285
|
+
multi: row.multi,
|
|
286
|
+
historyTracked: row.historyTracked,
|
|
287
|
+
valueType: legacyValueTypeFor(row.attributeType, row.multi),
|
|
288
|
+
storagePolicy: row.storagePolicy,
|
|
289
|
+
fieldPolicyId: row.id,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export type CurrentFieldRow = {
|
|
294
|
+
recordId: string;
|
|
295
|
+
stringValue: string | null;
|
|
296
|
+
numberValue: number | null;
|
|
297
|
+
booleanValue: boolean | null;
|
|
298
|
+
jsonValue: string | null;
|
|
299
|
+
actorType: string;
|
|
300
|
+
provenanceJson: string;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Why an existing value outranks a fill, or null when the fill may proceed.
|
|
305
|
+
*
|
|
306
|
+
* A human edit and a value someone paid a provider for are the two things a
|
|
307
|
+
* later automated fill must never quietly replace. The paid enrichment ingest
|
|
308
|
+
* shares this rule — it honours `human-edit` and overwrites its own earlier
|
|
309
|
+
* `paid-enrichment` values, which is why the decision lives in one function.
|
|
310
|
+
*/
|
|
311
|
+
export function protectedBy(row: CurrentFieldRow): string | null {
|
|
312
|
+
if (row.actorType === "user") return "human-edit";
|
|
313
|
+
if (isPaidProvenance(row.provenanceJson)) return "paid-enrichment";
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isPaidProvenance(provenanceJson: string): boolean {
|
|
318
|
+
let parsed: unknown;
|
|
319
|
+
try {
|
|
320
|
+
parsed = JSON.parse(provenanceJson);
|
|
321
|
+
} catch {
|
|
322
|
+
// An unreadable provenance blob is not proof the value was free. Treat it
|
|
323
|
+
// as protected: keeping a value we cannot explain beats overwriting one.
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
const entries = Array.isArray(parsed) ? parsed : [parsed];
|
|
327
|
+
return entries.some(
|
|
328
|
+
(entry) =>
|
|
329
|
+
Boolean(entry) &&
|
|
330
|
+
typeof entry === "object" &&
|
|
331
|
+
(entry as Record<string, unknown>).paid === true,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function readStoredValue(row: CurrentFieldRow): unknown {
|
|
336
|
+
if (row.jsonValue !== null) {
|
|
337
|
+
try {
|
|
338
|
+
return JSON.parse(row.jsonValue) as unknown;
|
|
339
|
+
} catch {
|
|
340
|
+
return row.jsonValue;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (row.stringValue !== null) return row.stringValue;
|
|
344
|
+
if (row.numberValue !== null) return row.numberValue;
|
|
345
|
+
if (row.booleanValue !== null) return row.booleanValue;
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function optionValuesOf(value: unknown): string[] {
|
|
350
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
351
|
+
return entries.filter((entry): entry is string => typeof entry === "string");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function loadRecords(recordIds: string[]) {
|
|
355
|
+
const rows = await getDb()
|
|
356
|
+
.select({
|
|
357
|
+
id: schema.crmRecords.id,
|
|
358
|
+
displayName: schema.crmRecords.displayName,
|
|
359
|
+
domain: schema.crmRecords.domain,
|
|
360
|
+
objectType: schema.crmRecords.objectType,
|
|
361
|
+
})
|
|
362
|
+
.from(schema.crmRecords)
|
|
363
|
+
.where(
|
|
364
|
+
and(
|
|
365
|
+
inArray(schema.crmRecords.id, recordIds),
|
|
366
|
+
accessFilter(
|
|
367
|
+
schema.crmRecords,
|
|
368
|
+
schema.crmRecordShares,
|
|
369
|
+
undefined,
|
|
370
|
+
"editor",
|
|
371
|
+
),
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
375
|
+
const missing = recordIds.filter((recordId) => !byId.has(recordId));
|
|
376
|
+
if (missing.length > 0) {
|
|
377
|
+
throw new CrmAttributeValueError(
|
|
378
|
+
"crm-record-not-writable",
|
|
379
|
+
`These CRM records were not found or are not editable by you: ${missing.join(", ")}.`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return byId;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function loadCurrentValues(recordIds: string[], apiSlug: string) {
|
|
386
|
+
const rows = await getDb()
|
|
387
|
+
.select({
|
|
388
|
+
recordId: schema.crmRecordFields.recordId,
|
|
389
|
+
stringValue: schema.crmRecordFields.stringValue,
|
|
390
|
+
numberValue: schema.crmRecordFields.numberValue,
|
|
391
|
+
booleanValue: schema.crmRecordFields.booleanValue,
|
|
392
|
+
jsonValue: schema.crmRecordFields.jsonValue,
|
|
393
|
+
actorType: schema.crmRecordFields.actorType,
|
|
394
|
+
provenanceJson: schema.crmRecordFields.provenanceJson,
|
|
395
|
+
})
|
|
396
|
+
.from(schema.crmRecordFields)
|
|
397
|
+
.where(
|
|
398
|
+
and(
|
|
399
|
+
inArray(schema.crmRecordFields.recordId, recordIds),
|
|
400
|
+
eq(schema.crmRecordFields.fieldName, apiSlug),
|
|
401
|
+
isNull(schema.crmRecordFields.entryId),
|
|
402
|
+
isNull(schema.crmRecordFields.activeUntil),
|
|
403
|
+
accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
return new Map(rows.map((row) => [row.recordId, row]));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** A bounded slice of each record's other current values, for the brief. */
|
|
410
|
+
async function loadRecordContext(recordIds: string[]) {
|
|
411
|
+
const rows = await getDb()
|
|
412
|
+
.select({
|
|
413
|
+
recordId: schema.crmRecordFields.recordId,
|
|
414
|
+
fieldName: schema.crmRecordFields.fieldName,
|
|
415
|
+
stringValue: schema.crmRecordFields.stringValue,
|
|
416
|
+
numberValue: schema.crmRecordFields.numberValue,
|
|
417
|
+
booleanValue: schema.crmRecordFields.booleanValue,
|
|
418
|
+
jsonValue: schema.crmRecordFields.jsonValue,
|
|
419
|
+
})
|
|
420
|
+
.from(schema.crmRecordFields)
|
|
421
|
+
.where(
|
|
422
|
+
and(
|
|
423
|
+
inArray(schema.crmRecordFields.recordId, recordIds),
|
|
424
|
+
isNull(schema.crmRecordFields.entryId),
|
|
425
|
+
isNull(schema.crmRecordFields.activeUntil),
|
|
426
|
+
accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
|
|
427
|
+
),
|
|
428
|
+
);
|
|
429
|
+
const byRecord = new Map<string, Record<string, string>>();
|
|
430
|
+
for (const row of rows) {
|
|
431
|
+
const bucket = byRecord.get(row.recordId) ?? {};
|
|
432
|
+
if (Object.keys(bucket).length >= MAX_CONTEXT_ATTRIBUTES) continue;
|
|
433
|
+
const value =
|
|
434
|
+
row.jsonValue ??
|
|
435
|
+
row.stringValue ??
|
|
436
|
+
(row.numberValue !== null ? String(row.numberValue) : null) ??
|
|
437
|
+
(row.booleanValue !== null ? String(row.booleanValue) : null);
|
|
438
|
+
if (value === null) continue;
|
|
439
|
+
bucket[row.fieldName] = value.slice(0, MAX_CONTEXT_CHARS);
|
|
440
|
+
byRecord.set(row.recordId, bucket);
|
|
441
|
+
}
|
|
442
|
+
return byRecord;
|
|
443
|
+
}
|