@withone/cli 1.53.0 → 1.54.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/README.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<a href="https://withone.ai/knowledge"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.withone.ai%2Fopen%2Fcount%2Ftools" alt="tools"></a>
|
|
25
25
|
</p>
|
|
26
26
|
|
|
27
|
-
One gives your AI agent authenticated access to
|
|
27
|
+
One gives your AI agent authenticated access to 600+ platforms - Gmail, Slack, Shopify, HubSpot, Stripe, Notion, and everything else - through a single interface. No API keys to juggle, no OAuth flows to build, no request formats to memorize. Connect a platform once, and your agent can search for actions, read the docs, and execute API calls in seconds.
|
|
28
28
|
|
|
29
29
|
## Install
|
|
30
30
|
|
|
@@ -48,7 +48,10 @@ one login # Opens browser for authentication (global or per-directo
|
|
|
48
48
|
one logout # Clear credentials (with scope picker and confirmation)
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Requires Node.js 18+.
|
|
51
|
+
Requires Node.js 18+. `one sync` additionally needs Node 20+, because its
|
|
52
|
+
local SQLite engine (`better-sqlite3`, an optional dependency) ships prebuilt
|
|
53
|
+
binaries only for Node 20 and above. On Node 18 every other command works
|
|
54
|
+
normally; `one sync` reports that the engine isn't installed.
|
|
52
55
|
|
|
53
56
|
## Quick start
|
|
54
57
|
|
|
@@ -241,7 +244,7 @@ Shows the connection details and asks for confirmation before deleting. Use `--f
|
|
|
241
244
|
|
|
242
245
|
### `one platforms`
|
|
243
246
|
|
|
244
|
-
Browse all
|
|
247
|
+
Browse all 600+ available platforms.
|
|
245
248
|
|
|
246
249
|
```bash
|
|
247
250
|
one platforms # all platforms
|
|
@@ -143,6 +143,15 @@ function loadBuiltinProfile(platform, model) {
|
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
var CAPABILITY_FIELDS = ["identityKeys", "identityKey", "enrich", "dateFilter", "memory"];
|
|
147
|
+
function findMissingBuiltinCapabilities(platform, model, installed) {
|
|
148
|
+
if (!installed) return [];
|
|
149
|
+
const builtin = loadBuiltinProfile(platform, model);
|
|
150
|
+
if (!builtin) return [];
|
|
151
|
+
return CAPABILITY_FIELDS.filter(
|
|
152
|
+
(field) => builtin[field] !== void 0 && installed[field] === void 0
|
|
153
|
+
);
|
|
154
|
+
}
|
|
146
155
|
function listBuiltinProfiles(platform) {
|
|
147
156
|
const dir = getProfilesDir();
|
|
148
157
|
if (!dir) return [];
|
|
@@ -1070,6 +1079,7 @@ export {
|
|
|
1070
1079
|
resolveEntityIdentity,
|
|
1071
1080
|
collectIdentityKeys,
|
|
1072
1081
|
loadBuiltinProfile,
|
|
1082
|
+
findMissingBuiltinCapabilities,
|
|
1073
1083
|
listBuiltinProfiles,
|
|
1074
1084
|
memMigrateCommand,
|
|
1075
1085
|
buildIdentityMap,
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
ensureTable,
|
|
46
46
|
evolveSchema,
|
|
47
47
|
extractSearchableFromPaths,
|
|
48
|
+
findMissingBuiltinCapabilities,
|
|
48
49
|
generateTemplate,
|
|
49
50
|
getDatabaseSize,
|
|
50
51
|
getSearchablePaths,
|
|
@@ -65,7 +66,7 @@ import {
|
|
|
65
66
|
writeDraftProfile,
|
|
66
67
|
writePageToMemory,
|
|
67
68
|
writeProfile
|
|
68
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-XN4ZWMLX.js";
|
|
69
70
|
import {
|
|
70
71
|
getByDotPath
|
|
71
72
|
} from "./chunk-44CV5IMX.js";
|
|
@@ -1103,7 +1104,7 @@ async function loginCommand() {
|
|
|
1103
1104
|
console.log(pc15.cyan(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1104
1105
|
console.log(pc15.cyan(" \u2502"));
|
|
1105
1106
|
console.log(pc15.cyan(" \u2502 The One CLI (`one`) is now installed on this machine. It"));
|
|
1106
|
-
console.log(pc15.cyan(" \u2502 lets you interact with
|
|
1107
|
+
console.log(pc15.cyan(" \u2502 lets you interact with 600+ third-party platforms (Gmail,"));
|
|
1107
1108
|
console.log(pc15.cyan(" \u2502 Slack, Shopify, HubSpot, Stripe, GitHub, etc.) through"));
|
|
1108
1109
|
console.log(pc15.cyan(" \u2502 their APIs. Whenever you need to interact with any"));
|
|
1109
1110
|
console.log(pc15.cyan(" \u2502 third-party platform or external service, use the `one`"));
|
|
@@ -1710,7 +1711,7 @@ function printOnboardingPrompt() {
|
|
|
1710
1711
|
console.log(pc2.cyan(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1711
1712
|
console.log(pc2.cyan(" \u2502"));
|
|
1712
1713
|
console.log(pc2.cyan(" \u2502 The One CLI (`one`) is now installed on this machine. It"));
|
|
1713
|
-
console.log(pc2.cyan(" \u2502 lets you interact with
|
|
1714
|
+
console.log(pc2.cyan(" \u2502 lets you interact with 600+ third-party platforms (Gmail,"));
|
|
1714
1715
|
console.log(pc2.cyan(" \u2502 Slack, Shopify, HubSpot, Stripe, GitHub, etc.) through"));
|
|
1715
1716
|
console.log(pc2.cyan(" \u2502 their APIs. Whenever you need to interact with any"));
|
|
1716
1717
|
console.log(pc2.cyan(" \u2502 third-party platform or external service, use the `one`"));
|
|
@@ -1887,7 +1888,7 @@ async function promptConnectIntegrations(apiKey, connParams) {
|
|
|
1887
1888
|
label: i.label,
|
|
1888
1889
|
hint: i.hint
|
|
1889
1890
|
})),
|
|
1890
|
-
{ value: "more", label: "Browse all
|
|
1891
|
+
{ value: "more", label: "Browse all 600+ platforms" },
|
|
1891
1892
|
{ value: "skip", label: "Skip for now", hint: "you can always run one add later" }
|
|
1892
1893
|
];
|
|
1893
1894
|
const message = first ? "Connect your first integration?" : "Connect another?";
|
|
@@ -7829,7 +7830,12 @@ async function syncProfilesCommand(platform) {
|
|
|
7829
7830
|
model: p10.model,
|
|
7830
7831
|
description: p10.description,
|
|
7831
7832
|
hasEnrich: !!p10.enrich,
|
|
7832
|
-
|
|
7833
|
+
// `identityKey` (singular) is the entity-merge key; `identityKeys`
|
|
7834
|
+
// (plural, #167) are non-merging association keys. Reading only the
|
|
7835
|
+
// singular reported hasIdentityKey:false for gmail/gcal/fathom — the
|
|
7836
|
+
// three profiles the plural form was added for. (#129/#130)
|
|
7837
|
+
hasIdentityKey: !!p10.identityKey || !!p10.identityKeys,
|
|
7838
|
+
hasIdentityKeys: !!p10.identityKeys
|
|
7833
7839
|
})),
|
|
7834
7840
|
total: profiles.length,
|
|
7835
7841
|
_hint: profiles.length > 0 ? "Use a built-in profile: one --agent sync init <platform> <model>" : "No built-in profiles found. Use sync init to auto-infer from action knowledge."
|
|
@@ -7846,7 +7852,7 @@ async function syncProfilesCommand(platform) {
|
|
|
7846
7852
|
for (const p10 of profiles) {
|
|
7847
7853
|
const extras = [];
|
|
7848
7854
|
if (p10.enrich) extras.push("enrich");
|
|
7849
|
-
if (p10.identityKey) extras.push("identity");
|
|
7855
|
+
if (p10.identityKey || p10.identityKeys) extras.push("identity");
|
|
7850
7856
|
if (p10.dateFilter) extras.push("incremental");
|
|
7851
7857
|
const tags = extras.length > 0 ? ` ${pc9.dim(`[${extras.join(", ")}]`)}` : "";
|
|
7852
7858
|
console.log(` ${pc9.bold(`${p10.platform}/${p10.model}`.padEnd(35))} ${p10.description}${tags}`);
|
|
@@ -8158,6 +8164,15 @@ async function syncRunCommand(platform, options) {
|
|
|
8158
8164
|
if (!options.dryRun) {
|
|
8159
8165
|
await maybeAutoMigrateLegacy(platform, toSync.map((p10) => p10.model));
|
|
8160
8166
|
}
|
|
8167
|
+
const profileDrift = toSync.map((p10) => ({ model: p10.model, missing: findMissingBuiltinCapabilities(platform, p10.model, p10) })).filter((d) => d.missing.length > 0);
|
|
8168
|
+
if (profileDrift.length > 0 && !isAgentMode()) {
|
|
8169
|
+
for (const d of profileDrift) {
|
|
8170
|
+
console.log(
|
|
8171
|
+
` ${pc9.yellow("!")} ${platform}/${d.model} is missing ${d.missing.map((f) => pc9.bold(f)).join(", ")} from the current built-in profile.
|
|
8172
|
+
Run ${pc9.bold(`one sync init ${platform} ${d.model}`)} to pick ${d.missing.length > 1 ? "them" : "it"} up.`
|
|
8173
|
+
);
|
|
8174
|
+
}
|
|
8175
|
+
}
|
|
8161
8176
|
const results = [];
|
|
8162
8177
|
for (const profile of toSync) {
|
|
8163
8178
|
try {
|
|
@@ -8181,7 +8196,19 @@ async function syncRunCommand(platform, options) {
|
|
|
8181
8196
|
}
|
|
8182
8197
|
}
|
|
8183
8198
|
if (isAgentMode()) {
|
|
8184
|
-
json({
|
|
8199
|
+
json({
|
|
8200
|
+
platform,
|
|
8201
|
+
results,
|
|
8202
|
+
// Surfaced to agents too: a profile silently missing a capability the
|
|
8203
|
+
// built-in now declares is invisible in the record counts. (#129/#130)
|
|
8204
|
+
...profileDrift.length > 0 ? {
|
|
8205
|
+
profileDrift: profileDrift.map((d) => ({
|
|
8206
|
+
model: d.model,
|
|
8207
|
+
missingFields: d.missing,
|
|
8208
|
+
fix: `one sync init ${platform} ${d.model}`
|
|
8209
|
+
}))
|
|
8210
|
+
} : {}
|
|
8211
|
+
});
|
|
8185
8212
|
return;
|
|
8186
8213
|
}
|
|
8187
8214
|
for (const r of results) {
|
|
@@ -8352,7 +8379,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
|
|
|
8352
8379
|
` detected legacy .one/sync/data/${platform}.db (${dbSize}) \u2014 auto-migrating into memory before sync.
|
|
8353
8380
|
`
|
|
8354
8381
|
);
|
|
8355
|
-
const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-
|
|
8382
|
+
const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-UGDDHWGY.js");
|
|
8356
8383
|
await memMigrateCommand3({ platform, yes: true });
|
|
8357
8384
|
return;
|
|
8358
8385
|
}
|
|
@@ -8361,7 +8388,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
|
|
|
8361
8388
|
initialValue: true
|
|
8362
8389
|
});
|
|
8363
8390
|
if (p7.isCancel(shouldMigrate) || !shouldMigrate) return;
|
|
8364
|
-
const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-
|
|
8391
|
+
const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-UGDDHWGY.js");
|
|
8365
8392
|
await memMigrateCommand2({ platform, yes: true });
|
|
8366
8393
|
}
|
|
8367
8394
|
async function syncSuggestSearchableCommand(platformModel, options = {}) {
|
|
@@ -10009,7 +10036,7 @@ Before using any feature, read its guide section first: \`one guide actions\`, \
|
|
|
10009
10036
|
|
|
10010
10037
|
## Features
|
|
10011
10038
|
|
|
10012
|
-
### 1. Actions \u2014 Execute API calls on
|
|
10039
|
+
### 1. Actions \u2014 Execute API calls on 600+ platforms
|
|
10013
10040
|
Search for actions, read their docs, and execute them. This is the core workflow.
|
|
10014
10041
|
|
|
10015
10042
|
**Quick start:**
|
|
@@ -10908,6 +10935,10 @@ Two ways to tag a record with a cross-platform identifier (e.g. email), dependin
|
|
|
10908
10935
|
|
|
10909
10936
|
Each \`path\` supports \`[]\` wildcards (one key per element) and a \`[name=From]\` equality filter (e.g. Gmail \`messages[].payload.headers[name=From].value\`). \`email\`-prefixed values are email-extracted, so display-name headers (\`"Jane <jane@acme.com>"\`) and comma-lists normalize cleanly. Values are lowercased/trimmed/deduped. \`sync test\` previews how many identity keys each record resolves.
|
|
10910
10937
|
|
|
10938
|
+
The built-in \`gmail/gmailThreads\` profile collects From/To/**Cc/Bcc**. Gmail only returns a \`Bcc\` header on messages the authenticated user sent \u2014 it is stripped for recipients \u2014 so Bcc keys appear on your own sent threads and nowhere else.
|
|
10939
|
+
|
|
10940
|
+
**Upgrading an existing profile.** \`sync run\` reads only your on-disk profile at \`.one/sync/profiles/<platform>_<model>.json\` \u2014 it never merges built-in updates, so a profile created before a capability shipped will not have it. \`sync run\` now warns when the shipped built-in declares \`identityKeys\`, \`identityKey\`, \`enrich\`, \`dateFilter\`, or \`memory\` that your copy lacks, and \`--agent\` output carries a \`profileDrift\` array with the same information. Run \`one sync init <platform> <model>\` to pick the fields up (it patches, preserving your edits).
|
|
10941
|
+
|
|
10911
10942
|
Query everything sharing an identity key, grouped by type:
|
|
10912
10943
|
|
|
10913
10944
|
\`\`\`bash
|
|
@@ -11198,7 +11229,7 @@ function buildStep1() {
|
|
|
11198
11229
|
nextCommand: "one onboard --step 2",
|
|
11199
11230
|
message: `# One CLI \u2014 Agent Onboarding
|
|
11200
11231
|
|
|
11201
|
-
You now have access to the One CLI. It connects you to
|
|
11232
|
+
You now have access to the One CLI. It connects you to 600+ platforms
|
|
11202
11233
|
(Gmail, Slack, Stripe, Notion, etc.) \u2014 you'll learn the details shortly.
|
|
11203
11234
|
|
|
11204
11235
|
IMPORTANT: Your only task right now is to send the message below to the
|
|
@@ -11208,7 +11239,7 @@ Do not offer demos yet. Just send this message and wait for their reply:
|
|
|
11208
11239
|
---
|
|
11209
11240
|
|
|
11210
11241
|
I just got access to the One CLI \u2014 it connects me to your platforms like
|
|
11211
|
-
Gmail, Slack, Stripe, and
|
|
11242
|
+
Gmail, Slack, Stripe, and 600+ others so I can work with them directly.
|
|
11212
11243
|
I'll show you what I can do in a moment.
|
|
11213
11244
|
|
|
11214
11245
|
First, one quick setup step: right now only this conversation knows about
|
|
@@ -11237,7 +11268,7 @@ If they declined, skip this section.
|
|
|
11237
11268
|
## One CLI \u2014 Platform Integrations
|
|
11238
11269
|
|
|
11239
11270
|
The One CLI (\`one\`) is installed and configured on this machine. It is a
|
|
11240
|
-
command-line tool that gives AI agents the ability to interact with
|
|
11271
|
+
command-line tool that gives AI agents the ability to interact with 600+
|
|
11241
11272
|
third-party platforms (Gmail, Slack, Shopify, HubSpot, Stripe, GitHub,
|
|
11242
11273
|
Notion, Salesforce, etc.) through a single, unified interface.
|
|
11243
11274
|
|
|
@@ -11332,7 +11363,7 @@ function buildStep3(connections) {
|
|
|
11332
11363
|
The user can connect any of these popular platforms:
|
|
11333
11364
|
Gmail, Google Calendar, Slack, Notion, Shopify, Stripe, HubSpot, GitHub,
|
|
11334
11365
|
Salesforce, QuickBooks, Asana, Jira, Linear, Intercom, Zendesk, Twilio,
|
|
11335
|
-
and
|
|
11366
|
+
and 600+ more. Run \`one platforms\` for the full list.`);
|
|
11336
11367
|
sections.push("## Onboarding complete!\n\nYou're all set. Use `one --agent guide` any time you need the full reference.");
|
|
11337
11368
|
return {
|
|
11338
11369
|
step: 3,
|
|
@@ -11363,7 +11394,7 @@ platform before you can start using actions. Suggest they run:
|
|
|
11363
11394
|
one add slack
|
|
11364
11395
|
one add <any-platform>
|
|
11365
11396
|
|
|
11366
|
-
Run \`one platforms\` to see all
|
|
11397
|
+
Run \`one platforms\` to see all 600+ available platforms.`;
|
|
11367
11398
|
}
|
|
11368
11399
|
const header = `## Current State
|
|
11369
11400
|
|
|
@@ -11800,7 +11831,7 @@ silenceWarningsInAgentMode();
|
|
|
11800
11831
|
var require4 = createRequire3(import.meta.url);
|
|
11801
11832
|
var { version } = require4("../package.json");
|
|
11802
11833
|
var program = new Command();
|
|
11803
|
-
program.name("one").option("--agent", "Machine-readable JSON output (no colors, spinners, or prompts)").description(`One CLI \u2014 Connect AI agents to
|
|
11834
|
+
program.name("one").option("--agent", "Machine-readable JSON output (no colors, spinners, or prompts)").description(`One CLI \u2014 Connect AI agents to 600+ platforms through one interface.
|
|
11804
11835
|
|
|
11805
11836
|
Setup:
|
|
11806
11837
|
one login Authenticate via browser (opens app.withone.ai)
|
|
@@ -11872,7 +11903,7 @@ program.name("one").option("--agent", "Machine-readable JSON output (no colors,
|
|
|
11872
11903
|
-d '{"to":"j@example.com","subject":"Hello","body":"Hi!","connectionKey":"live::gmail::default::abc123"}'
|
|
11873
11904
|
|
|
11874
11905
|
Platform names are lowercase; multi-word names use dashes (e.g. hubspot, ship-station, google-calendar).
|
|
11875
|
-
Run 'one platforms' to browse all
|
|
11906
|
+
Run 'one platforms' to browse all 600+ available platforms.`).version(version);
|
|
11876
11907
|
var updateCheckPromise;
|
|
11877
11908
|
program.hook("preAction", (thisCommand, actionCommand) => {
|
|
11878
11909
|
const opts = program.opts();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withone/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.54.1",
|
|
4
4
|
"description": "CLI for managing One",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
38
|
"@electric-sql/pglite": "^0.2.17",
|
|
39
|
-
"better-sqlite3": "^11.
|
|
39
|
+
"better-sqlite3": "^12.11.1",
|
|
40
40
|
"pg": "^8.20.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"identityKeys": [
|
|
19
19
|
{ "prefix": "email", "path": "messages[].payload.headers[name=From].value" },
|
|
20
20
|
{ "prefix": "email", "path": "messages[].payload.headers[name=To].value" },
|
|
21
|
-
{ "prefix": "email", "path": "messages[].payload.headers[name=Cc].value" }
|
|
21
|
+
{ "prefix": "email", "path": "messages[].payload.headers[name=Cc].value" },
|
|
22
|
+
{ "prefix": "email", "path": "messages[].payload.headers[name=Bcc].value" }
|
|
22
23
|
],
|
|
23
24
|
"enrich": {
|
|
24
25
|
"actionId": "conn_mod_def::GJ3ok0Eq0R8::AAzgZVLqTg2iBuITKpJLZg",
|
package/skills/one/SKILL.md
CHANGED
|
@@ -20,7 +20,7 @@ description: |
|
|
|
20
20
|
|
|
21
21
|
# One CLI
|
|
22
22
|
|
|
23
|
-
You have access to the One CLI which lets you interact with
|
|
23
|
+
You have access to the One CLI which lets you interact with 600+ third-party platforms through their APIs. Always include the `--agent` flag right after `one` for structured JSON output.
|
|
24
24
|
|
|
25
25
|
If the user wants a separate API key / connections for a specific project (vs. their default), walk them through running `one init` from that project folder and picking the "project" scope — see `references/scoping.md`. For monorepo subprojects (where a parent already has `.git`/`package.json`), have them `mkdir .one` in the subproject first so the config is keyed to that dir, not the monorepo root.
|
|
26
26
|
|
|
@@ -311,6 +311,8 @@ Without declared paths, the default walker concatenates every string in the reco
|
|
|
311
311
|
|
|
312
312
|
**Connections are late-bound** — profiles use `"connection": { "platform": "<name>" }`, not literal `connectionKey` strings. The key is resolved at sync time, so `one add <platform>` (re-auth) doesn't break the profile. For multi-account platforms, add `"tag": "<connection-tag>"` to disambiguate, and create the tagged connection with `one add <platform> --tag <name>`. Don't hardcode connection keys in profiles.
|
|
313
313
|
|
|
314
|
+
**Installed profiles do not auto-update.** `sync run` reads only `.one/sync/profiles/<platform>_<model>.json` and never merges the shipped built-in, so a profile created before a capability shipped silently lacks it — a pre-#167 gmail profile writes zero identity keys, forever, with no change in record counts. `sync run` warns when the built-in declares `identityKeys` / `identityKey` / `enrich` / `dateFilter` / `memory` that your copy lacks (agent mode: a `profileDrift` array). Fix with `one sync init <platform> <model>`, which patches rather than overwrites.
|
|
315
|
+
|
|
314
316
|
**Cross-platform identity on a profile.** Two separate fields, and picking the wrong one silently mangles data:
|
|
315
317
|
|
|
316
318
|
- `"identityKey": "properties.email"` — singular. "This record IS this entity." One dot-path; the value lands in `keys[]` and MERGES records for the same entity across platforms (HubSpot + Attio for one person collapse into a single record).
|