@withone/cli 1.54.1 → 1.55.0

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.
@@ -523,7 +523,9 @@ async function writePageToMemory(profile, records, opts = {}) {
523
523
  const searchablePaths = getSearchablePaths(profile);
524
524
  const declaresIdentityKeys = (profile.identityKeys?.length ?? 0) > 0;
525
525
  const enrichTimestampField = profile.enrich?.timestampField ?? "_enriched_at";
526
- for (const record of records) {
526
+ for (const rawRecord of records) {
527
+ const derived = deriveFields(rawRecord, profile.derive);
528
+ const record = Object.keys(derived).length > 0 ? { ...rawRecord, ...derived } : rawRecord;
527
529
  report.attempted++;
528
530
  const externalId = getByDotPath(record, profile.idField);
529
531
  if (externalId === void 0 || externalId === null || externalId === "") {
@@ -608,6 +610,24 @@ function identityValuesFor(prefix, raw, opts = {}) {
608
610
  const v = s.toLowerCase().trim();
609
611
  return v ? [v] : [];
610
612
  }
613
+ function deriveFields(record, derive) {
614
+ const out = {};
615
+ if (!derive) return out;
616
+ for (const [field, spec] of Object.entries(derive)) {
617
+ const { path: path5, extract } = typeof spec === "string" ? { path: spec, extract: void 0 } : spec;
618
+ if (!path5) continue;
619
+ const values = resolveIdentityPath(record, path5).filter((v) => v !== null && v !== void 0 && typeof v !== "object");
620
+ if (values.length === 0) continue;
621
+ if (extract === "email") {
622
+ const emails = values.flatMap((v) => identityValuesFor("email", v));
623
+ if (emails.length === 0) continue;
624
+ out[field] = emails[0];
625
+ continue;
626
+ }
627
+ out[field] = values[0];
628
+ }
629
+ return out;
630
+ }
611
631
  function tokenizeIdentityPath(path5) {
612
632
  const tokens = [];
613
633
  const re = /([^.[\]]+)|\[([^\]]*)\]/g;
package/dist/index.js CHANGED
@@ -66,7 +66,7 @@ import {
66
66
  writeDraftProfile,
67
67
  writePageToMemory,
68
68
  writeProfile
69
- } from "./chunk-XN4ZWMLX.js";
69
+ } from "./chunk-FS4HAKZ6.js";
70
70
  import {
71
71
  getByDotPath
72
72
  } from "./chunk-44CV5IMX.js";
@@ -5888,6 +5888,21 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
5888
5888
  const duration = elapsed < 1e3 ? `${elapsed}ms` : elapsed < 6e4 ? `${(elapsed / 1e3).toFixed(1)}s` : `${Math.floor(elapsed / 6e4)}m ${Math.floor(elapsed % 6e4 / 1e3)}s`;
5889
5889
  return { enriched, skipped, rateLimited, total, duration };
5890
5890
  }
5891
+ async function enrichOneForPreview(api, profile, record, connectionKey) {
5892
+ const config2 = profile.enrich;
5893
+ if (!config2) return null;
5894
+ try {
5895
+ const detailAction = (await resolveActionDetails(api, config2.actionId)).details;
5896
+ const detail = await enrichSingleRow(api, detailAction, config2, record, connectionKey, profile.platform);
5897
+ if (!detail) return null;
5898
+ let enrichedData = detail;
5899
+ if (config2.fields && config2.fields.length > 0) enrichedData = pickFields(enrichedData, config2.fields);
5900
+ if (config2.exclude && config2.exclude.length > 0) stripExcludedFields(enrichedData, config2.exclude);
5901
+ return config2.merge !== false ? deepMerge(record, enrichedData) : { ...enrichedData, [profile.idField]: record[profile.idField] };
5902
+ } catch {
5903
+ return null;
5904
+ }
5905
+ }
5891
5906
  async function enrichSingleRow(api, detailAction, config2, row, connectionKey, platform) {
5892
5907
  const pathVars = interpolateParams(config2.pathVars, row);
5893
5908
  const queryParams = interpolateParams(config2.queryParams, row);
@@ -6879,7 +6894,14 @@ async function testSyncProfile(api, profile) {
6879
6894
  }));
6880
6895
  report.sample = first;
6881
6896
  report.samples = records.slice(0, SEARCHABLE_SAMPLE_SIZE);
6882
- report.identityKeysPreview = buildIdentityKeysPreview(report.samples, profile);
6897
+ if (profile.enrich && (profile.identityKeys?.length ?? 0) > 0 && report.samples.length > 0) {
6898
+ const merged = await enrichOneForPreview(api, profile, report.samples[0], connectionKey);
6899
+ if (merged) {
6900
+ const preview = buildIdentityKeysPreview([merged], profile);
6901
+ if (preview) report.identityKeysPreview = { ...preview, previewedAfterEnrich: true };
6902
+ }
6903
+ }
6904
+ report.identityKeysPreview ??= buildIdentityKeysPreview(report.samples, profile);
6883
6905
  report.ok = checks.every((c) => c.ok);
6884
6906
  return report;
6885
6907
  }
@@ -8379,7 +8401,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
8379
8401
  ` detected legacy .one/sync/data/${platform}.db (${dbSize}) \u2014 auto-migrating into memory before sync.
8380
8402
  `
8381
8403
  );
8382
- const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-UGDDHWGY.js");
8404
+ const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-37A463L6.js");
8383
8405
  await memMigrateCommand3({ platform, yes: true });
8384
8406
  return;
8385
8407
  }
@@ -8388,7 +8410,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
8388
8410
  initialValue: true
8389
8411
  });
8390
8412
  if (p7.isCancel(shouldMigrate) || !shouldMigrate) return;
8391
- const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-UGDDHWGY.js");
8413
+ const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-37A463L6.js");
8392
8414
  await memMigrateCommand2({ platform, yes: true });
8393
8415
  }
8394
8416
  async function syncSuggestSearchableCommand(platformModel, options = {}) {
@@ -10859,6 +10881,30 @@ Enrichment runs after list sync completes (Phase 2), not inline. It's inherently
10859
10881
 
10860
10882
  **Limitation:** Each profile supports one enrich action. If you need multiple enrichments (e.g. both summary and transcript from Fathom), create a second profile/model for the second enrichment.
10861
10883
 
10884
+ ## Derived fields (\`derive\`)
10885
+
10886
+ Add flat, queryable top-level fields computed from paths already in the record \u2014 no shell, no \`jq\`:
10887
+
10888
+ \`\`\`json
10889
+ {
10890
+ "derive": {
10891
+ "from_email": {
10892
+ "path": "messages[0].payload.headers[name=From].value",
10893
+ "extract": "email"
10894
+ },
10895
+ "company": "organization.name"
10896
+ }
10897
+ }
10898
+ \`\`\`
10899
+
10900
+ - Paths use the same resolver as \`identityKeys\`, so \`[]\` wildcards, \`[0]\` indexes and \`[name=From]\` filters all work
10901
+ - \`extract: "email"\` pulls the address out of a display-name header (\`"Jane <jane@acme.com>"\` \u2192 \`jane@acme.com\`), lowercased
10902
+ - A path resolving to nothing **omits** the field rather than writing null, so \`--where\` filters behave
10903
+ - A path resolving to several values takes the first \u2014 it's a flat field by definition, and the path syntax lets you be specific
10904
+ - Applied on both sync phases, so an enriching profile gets the same field whether the record came from the list or the detail pass
10905
+
10906
+ **Prefer \`derive\` over \`transform\` for extracting a field.** \`transform\` spawns \`sh -c\`, so it needs \`jq\` (or whatever you invoke) on PATH and does nothing on Windows. \`derive\` is pure and works everywhere \u2014 which is why the built-in \`gmail/gmailThreads\` profile uses it for \`from_email\`. Reach for \`transform\` when you need real computation, not field extraction.
10907
+
10862
10908
  ## Record Transform
10863
10909
 
10864
10910
  Pipe records through any shell command or flow between fetch and store. The command receives a JSON array on stdin and must return a JSON array on stdout.
@@ -10933,7 +10979,9 @@ Two ways to tag a record with a cross-platform identifier (e.g. email), dependin
10933
10979
  ]}
10934
10980
  \`\`\`
10935
10981
 
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.
10982
+ 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.
10983
+
10984
+ \`sync test\` previews how many identity keys each record resolves. For an **enriching** profile the participant paths live in the detail payload, so \`sync test\` spends one detail call on the first sample and previews against the merged shape \u2014 you see the keys a real sync would write, not zero plus a promise. If that call can't be made (rate limit, permissions), it falls back to the list-shape preview and says the keys resolve after enrichment.
10937
10985
 
10938
10986
  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
10987
 
@@ -3,7 +3,7 @@ import {
3
3
  dotPathToJsonbExpr,
4
4
  memMigrateCommand,
5
5
  reviveStringifiedJson
6
- } from "./chunk-XN4ZWMLX.js";
6
+ } from "./chunk-FS4HAKZ6.js";
7
7
  import "./chunk-44CV5IMX.js";
8
8
  import "./chunk-GNSR3NYN.js";
9
9
  import "./chunk-DDCDPVJH.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/cli",
3
- "version": "1.54.1",
3
+ "version": "1.55.0",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [
@@ -21,6 +21,12 @@
21
21
  { "prefix": "email", "path": "messages[].payload.headers[name=Cc].value" },
22
22
  { "prefix": "email", "path": "messages[].payload.headers[name=Bcc].value" }
23
23
  ],
24
+ "derive": {
25
+ "from_email": {
26
+ "path": "messages[0].payload.headers[name=From].value",
27
+ "extract": "email"
28
+ }
29
+ },
24
30
  "enrich": {
25
31
  "actionId": "conn_mod_def::GJ3ok0Eq0R8::AAzgZVLqTg2iBuITKpJLZg",
26
32
  "pathVars": { "userId": "me", "id": "{id}" },
@@ -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
+ **Extracting a flat field? Use `derive`, not `transform`.** `derive` computes top-level fields from paths already in the record (`"derive": { "from_email": { "path": "messages[0].payload.headers[name=From].value", "extract": "email" } }`), using the same path syntax as `identityKeys`. `transform` spawns `sh -c`, so it needs `jq` on PATH and silently does nothing on Windows — never put one in a profile you intend to share. A path that resolves to nothing omits the field rather than writing null.
315
+
314
316
  **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
317
 
316
318
  **Cross-platform identity on a profile.** Two separate fields, and picking the wrong one silently mangles data: