@withone/cli 1.50.0 → 1.52.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.
package/dist/index.js CHANGED
@@ -32,19 +32,22 @@ import {
32
32
  validateActionInput,
33
33
  walkSteps,
34
34
  writeCache
35
- } from "./chunk-D6W756DN.js";
35
+ } from "./chunk-K56Z5BTM.js";
36
36
  import {
37
37
  memSqlCommand
38
- } from "./chunk-4WWK3GO5.js";
38
+ } from "./chunk-UV4YYDF3.js";
39
39
  import {
40
+ collectIdentityKeys,
40
41
  countRecords,
41
42
  deleteDatabase,
42
43
  deleteRecords,
43
44
  dropTable,
44
45
  ensureTable,
45
46
  evolveSchema,
47
+ extractSearchableFromPaths,
46
48
  generateTemplate,
47
49
  getDatabaseSize,
50
+ getSearchablePaths,
48
51
  isSqliteAvailable,
49
52
  listBuiltinProfiles,
50
53
  listProfiles,
@@ -54,13 +57,15 @@ import {
54
57
  openDatabase,
55
58
  readProfile,
56
59
  rebuildFtsIndex,
60
+ resolveEntityIdentity,
57
61
  resolveProfileConnectionKey,
58
62
  sanitizeTableName,
59
63
  tableExists,
60
64
  upsertRecords,
61
65
  writeDraftProfile,
66
+ writePageToMemory,
62
67
  writeProfile
63
- } from "./chunk-GXOIR3GF.js";
68
+ } from "./chunk-ZOJW6IMU.js";
64
69
  import {
65
70
  getByDotPath
66
71
  } from "./chunk-44CV5IMX.js";
@@ -93,9 +98,8 @@ import {
93
98
  getBackendPlugin,
94
99
  listBackendPlugins,
95
100
  loadBackendFromConfig,
96
- updateRecord,
97
- upsertRecord
98
- } from "./chunk-7RV7T5PX.js";
101
+ updateRecord
102
+ } from "./chunk-Z4KAQGIG.js";
99
103
  import {
100
104
  DEFAULT_MEMORY_CONFIG,
101
105
  defaultSearchableText,
@@ -1033,25 +1037,25 @@ async function loginCommand() {
1033
1037
  let targetScope = "global";
1034
1038
  const existingKey = getApiKey();
1035
1039
  if (existingKey) {
1036
- const pc15 = (await import("picocolors")).default;
1040
+ const pc16 = (await import("picocolors")).default;
1037
1041
  const resolved2 = resolveConfig();
1038
1042
  const whoami2 = resolved2.config?.whoami;
1039
1043
  const env2 = getEnvFromApiKey(existingKey);
1040
- const envLabel2 = env2 === "test" ? pc15.yellow("test") : pc15.green("live");
1041
- const currentScope = resolved2.scope === "project" ? pc15.cyan("local config") : pc15.magenta("global config");
1044
+ const envLabel2 = env2 === "test" ? pc16.yellow("test") : pc16.green("live");
1045
+ const currentScope = resolved2.scope === "project" ? pc16.cyan("local config") : pc16.magenta("global config");
1042
1046
  const lines = ["You are already logged in.", ""];
1043
1047
  if (whoami2) {
1044
1048
  const contextParts2 = [];
1045
1049
  if (whoami2.organization) contextParts2.push(whoami2.organization.name);
1046
1050
  if (whoami2.project) contextParts2.push(whoami2.project.name);
1047
1051
  const scopeDisplay2 = contextParts2.length > 0 ? contextParts2.join(" / ") : "Personal";
1048
- lines.push(`${pc15.bold(scopeDisplay2)} ${pc15.dim("\xB7")} ${envLabel2}`);
1049
- lines.push(`${whoami2.user.name} ${pc15.dim(`(${whoami2.user.email})`)}`);
1050
- if (whoami2.organization) lines.push(`${pc15.dim("Org:")} ${whoami2.organization.name}`);
1051
- if (whoami2.project) lines.push(`${pc15.dim("Project:")} ${whoami2.project.name}`);
1052
+ lines.push(`${pc16.bold(scopeDisplay2)} ${pc16.dim("\xB7")} ${envLabel2}`);
1053
+ lines.push(`${whoami2.user.name} ${pc16.dim(`(${whoami2.user.email})`)}`);
1054
+ if (whoami2.organization) lines.push(`${pc16.dim("Org:")} ${whoami2.organization.name}`);
1055
+ if (whoami2.project) lines.push(`${pc16.dim("Project:")} ${whoami2.project.name}`);
1052
1056
  }
1053
1057
  lines.push("");
1054
- lines.push(`${pc15.dim("Stored in")} ${currentScope}`);
1058
+ lines.push(`${pc16.dim("Stored in")} ${currentScope}`);
1055
1059
  p2.note(lines.join("\n"));
1056
1060
  const scopeChoice = await p2.select({
1057
1061
  message: "Where would you like to log in?",
@@ -1074,40 +1078,40 @@ async function loginCommand() {
1074
1078
  if (resolved.config) {
1075
1079
  writeConfig({ ...resolved.config, whoami }, targetScope);
1076
1080
  }
1077
- const pc14 = (await import("picocolors")).default;
1081
+ const pc15 = (await import("picocolors")).default;
1078
1082
  const env = getEnvFromApiKey(apiKey);
1079
1083
  const contextParts = [];
1080
1084
  if (whoami.organization) contextParts.push(whoami.organization.name);
1081
1085
  if (whoami.project) contextParts.push(whoami.project.name);
1082
1086
  const scopeDisplay = contextParts.length > 0 ? contextParts.join(" / ") : "Personal";
1083
- const envLabel = env === "test" ? pc14.yellow("test") : pc14.green("live");
1084
- const configLabel = targetScope === "project" ? pc14.cyan("local config") : pc14.magenta("global config");
1087
+ const envLabel = env === "test" ? pc15.yellow("test") : pc15.green("live");
1088
+ const configLabel = targetScope === "project" ? pc15.cyan("local config") : pc15.magenta("global config");
1085
1089
  const infoLines = [
1086
- `${pc14.bold(scopeDisplay)} ${pc14.dim("\xB7")} ${envLabel}`,
1087
- `${whoami.user.name} ${pc14.dim(`(${whoami.user.email})`)}`
1090
+ `${pc15.bold(scopeDisplay)} ${pc15.dim("\xB7")} ${envLabel}`,
1091
+ `${whoami.user.name} ${pc15.dim(`(${whoami.user.email})`)}`
1088
1092
  ];
1089
- if (whoami.organization) infoLines.push(`${pc14.dim("Org:")} ${whoami.organization.name}`);
1090
- if (whoami.project) infoLines.push(`${pc14.dim("Project:")} ${whoami.project.name}`);
1093
+ if (whoami.organization) infoLines.push(`${pc15.dim("Org:")} ${whoami.organization.name}`);
1094
+ if (whoami.project) infoLines.push(`${pc15.dim("Project:")} ${whoami.project.name}`);
1091
1095
  infoLines.push("");
1092
- infoLines.push(`${pc14.dim("Stored in")} ${configLabel}`);
1096
+ infoLines.push(`${pc15.dim("Stored in")} ${configLabel}`);
1093
1097
  p2.note(infoLines.join("\n"), "Logged in");
1094
1098
  console.log();
1095
- console.log(` ${pc14.dim("Next steps:")}`);
1096
- console.log(` ${pc14.cyan("one add <platform>")} ${pc14.dim("\u2014")} Connect a platform (e.g. gmail, slack, stripe)`);
1099
+ console.log(` ${pc15.dim("Next steps:")}`);
1100
+ console.log(` ${pc15.cyan("one add <platform>")} ${pc15.dim("\u2014")} Connect a platform (e.g. gmail, slack, stripe)`);
1097
1101
  console.log();
1098
- console.log(pc14.bold(" Copy-paste this to your AI agent:"));
1102
+ console.log(pc15.bold(" Copy-paste this to your AI agent:"));
1099
1103
  console.log();
1100
- console.log(pc14.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"));
1101
- console.log(pc14.cyan(" \u2502"));
1102
- console.log(pc14.cyan(" \u2502 The One CLI (`one`) is now installed on this machine. It"));
1103
- console.log(pc14.cyan(" \u2502 lets you interact with 400+ third-party platforms (Gmail,"));
1104
- console.log(pc14.cyan(" \u2502 Slack, Shopify, HubSpot, Stripe, GitHub, etc.) through"));
1105
- console.log(pc14.cyan(" \u2502 their APIs. Whenever you need to interact with any"));
1106
- console.log(pc14.cyan(" \u2502 third-party platform or external service, use the `one`"));
1107
- console.log(pc14.cyan(" \u2502 skill. Please save this to your project instructions or"));
1108
- console.log(pc14.cyan(" \u2502 memory so you remember it in future conversations."));
1109
- console.log(pc14.cyan(" \u2502"));
1110
- console.log(pc14.cyan(" \u2514\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
+ 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"));
1105
+ console.log(pc15.cyan(" \u2502"));
1106
+ console.log(pc15.cyan(" \u2502 The One CLI (`one`) is now installed on this machine. It"));
1107
+ console.log(pc15.cyan(" \u2502 lets you interact with 400+ third-party platforms (Gmail,"));
1108
+ console.log(pc15.cyan(" \u2502 Slack, Shopify, HubSpot, Stripe, GitHub, etc.) through"));
1109
+ console.log(pc15.cyan(" \u2502 their APIs. Whenever you need to interact with any"));
1110
+ console.log(pc15.cyan(" \u2502 third-party platform or external service, use the `one`"));
1111
+ console.log(pc15.cyan(" \u2502 skill. Please save this to your project instructions or"));
1112
+ console.log(pc15.cyan(" \u2502 memory so you remember it in future conversations."));
1113
+ console.log(pc15.cyan(" \u2502"));
1114
+ console.log(pc15.cyan(" \u2514\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"));
1111
1115
  console.log();
1112
1116
  p2.outro("Happy building!");
1113
1117
  }
@@ -5480,152 +5484,6 @@ function runShellHook(command, events) {
5480
5484
  });
5481
5485
  }
5482
5486
 
5483
- // src/lib/memory/sync/mem-writer.ts
5484
- function resolveWildcardPath(root, path12) {
5485
- const segments = path12.split("[]");
5486
- if (segments.length === 1) {
5487
- return [getByDotPath(root, path12)];
5488
- }
5489
- const recurse = (value, idx) => {
5490
- if (value === null || value === void 0) return [];
5491
- const segment = segments[idx];
5492
- const head = segment.startsWith(".") ? segment.slice(1) : segment;
5493
- if (idx === segments.length - 1) {
5494
- if (!Array.isArray(value)) return [];
5495
- if (head === "") return value;
5496
- return value.map((el) => getByDotPath(el, head));
5497
- }
5498
- if (!Array.isArray(value)) return [];
5499
- const results = [];
5500
- for (const el of value) {
5501
- const next = head === "" ? el : getByDotPath(el, head);
5502
- results.push(...recurse(next, idx + 1));
5503
- }
5504
- return results;
5505
- };
5506
- const firstHead = segments[0];
5507
- const firstValue = firstHead === "" ? root : getByDotPath(root, firstHead);
5508
- return recurse(firstValue, 1);
5509
- }
5510
- function extractSearchableFromPaths(record, paths) {
5511
- const parts = [];
5512
- const perPath = [];
5513
- for (const path12 of paths) {
5514
- const values = resolveWildcardPath(record, path12);
5515
- const collected = [];
5516
- const absorb = (v) => {
5517
- if (v === null || v === void 0) return;
5518
- if (typeof v === "string") {
5519
- const t = v.trim();
5520
- if (t) collected.push(t);
5521
- } else if (typeof v === "number" || typeof v === "boolean") {
5522
- collected.push(String(v));
5523
- } else if (Array.isArray(v)) {
5524
- for (const inner of v) absorb(inner);
5525
- }
5526
- };
5527
- for (const v of values) absorb(v);
5528
- if (collected.length > 0) {
5529
- parts.push(...collected);
5530
- perPath.push({ path: path12, found: true, sample: collected.join(" ").slice(0, 80) });
5531
- } else {
5532
- perPath.push({ path: path12, found: false, sample: "" });
5533
- }
5534
- }
5535
- const text5 = parts.join(" ").replace(/\s+/g, " ").trim();
5536
- return { text: text5, paths: perPath };
5537
- }
5538
- function getSearchablePaths(profile) {
5539
- const paths = profile.memory?.searchable;
5540
- if (!paths || !Array.isArray(paths) || paths.length === 0) return void 0;
5541
- return paths;
5542
- }
5543
- async function writePageToMemory(profile, records, opts = {}) {
5544
- const report = {
5545
- attempted: 0,
5546
- inserted: 0,
5547
- updated: 0,
5548
- skipped: 0,
5549
- sourceKeysSeen: [],
5550
- inserts: [],
5551
- updates: []
5552
- };
5553
- const type = `${profile.platform}/${profile.model}`;
5554
- const identityKey = profile.identityKey;
5555
- const embedFlag = opts.embedOverride ?? deriveEmbedFlag(profile);
5556
- const searchablePaths = getSearchablePaths(profile);
5557
- for (const record of records) {
5558
- report.attempted++;
5559
- const externalId = getByDotPath(record, profile.idField);
5560
- if (externalId === void 0 || externalId === null || externalId === "") {
5561
- report.skipped++;
5562
- continue;
5563
- }
5564
- if (typeof externalId === "object") {
5565
- report.skipped++;
5566
- continue;
5567
- }
5568
- const sourceKey = `${type}:${String(externalId)}`;
5569
- const keys = [sourceKey];
5570
- if (identityKey) {
5571
- const raw = getByDotPath(record, identityKey);
5572
- if (raw !== null && raw !== void 0 && raw !== "") {
5573
- keys.push(`${deriveIdentityPrefix(identityKey)}:${String(raw).toLowerCase().trim()}`);
5574
- }
5575
- }
5576
- const data = { ...record };
5577
- for (const k of Object.keys(data)) {
5578
- if (k.startsWith("_")) delete data[k];
5579
- }
5580
- const searchable_text = searchablePaths ? extractSearchableFromPaths(record, searchablePaths).text : void 0;
5581
- try {
5582
- const res = await upsertRecord(
5583
- {
5584
- type,
5585
- data,
5586
- keys,
5587
- searchable_text,
5588
- sources: {
5589
- [sourceKey]: {
5590
- last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
5591
- metadata: { source: profile.platform }
5592
- }
5593
- },
5594
- tags: ["synced", profile.platform],
5595
- embed: embedFlag
5596
- },
5597
- // Sync must REPLACE: if a field vanished at the source (phone
5598
- // number removed, Gmail thread archived) it must also vanish
5599
- // from memory. The default merge behaviour is correct for
5600
- // interactive `mem add` / `mem update`, wrong for sync.
5601
- { embed: embedFlag, replace: true }
5602
- );
5603
- report.sourceKeysSeen.push(sourceKey);
5604
- if (res.action === "inserted") {
5605
- report.inserted++;
5606
- if (opts.capturePerAction) report.inserts.push(record);
5607
- } else {
5608
- report.updated++;
5609
- if (opts.capturePerAction) report.updates.push(record);
5610
- }
5611
- } catch {
5612
- report.skipped++;
5613
- }
5614
- }
5615
- return report;
5616
- }
5617
- function deriveEmbedFlag(profile) {
5618
- if (profile.memory && typeof profile.memory.embed === "boolean") return profile.memory.embed;
5619
- return false;
5620
- }
5621
- function deriveIdentityPrefix(dotPath) {
5622
- const lower = dotPath.toLowerCase();
5623
- if (lower.includes("email")) return "email";
5624
- if (lower.includes("phone")) return "phone";
5625
- if (lower.includes("domain")) return "domain";
5626
- return "id";
5627
- }
5628
-
5629
5487
  // src/lib/memory/sync/transform.ts
5630
5488
  import { spawn as spawn3 } from "child_process";
5631
5489
  var TRANSFORM_TIMEOUT_MS = 6e4;
@@ -5789,6 +5647,32 @@ function pickFields(obj, fields) {
5789
5647
  }
5790
5648
  return result;
5791
5649
  }
5650
+ function findEnrichedIds(db, model, idField, config2, records, tableCreated) {
5651
+ const enriched = /* @__PURE__ */ new Set();
5652
+ if (!tableCreated || records.length === 0) return enriched;
5653
+ const tsField = config2.timestampField ?? "_enriched_at";
5654
+ const safeTable = model.replace(/[^a-zA-Z0-9_]/g, "_");
5655
+ const safeIdField = idField.replace(/"/g, '""');
5656
+ const safeTsField = tsField.replace(/"/g, '""');
5657
+ try {
5658
+ const cols = db.prepare(`PRAGMA table_info("${safeTable}")`).all().map((c) => c.name);
5659
+ if (!cols.includes(tsField) || !cols.includes(idField)) return enriched;
5660
+ const ids = records.map((r) => r[idField]).filter((id) => typeof id === "string" || typeof id === "number");
5661
+ if (ids.length === 0) return enriched;
5662
+ const CHUNK = 500;
5663
+ for (let i = 0; i < ids.length; i += CHUNK) {
5664
+ const chunk = ids.slice(i, i + CHUNK);
5665
+ const placeholders = chunk.map(() => "?").join(",");
5666
+ const rows = db.prepare(
5667
+ `SELECT "${safeIdField}" AS id FROM "${safeTable}" WHERE "${safeTsField}" IS NOT NULL AND "${safeIdField}" IN (${placeholders})`
5668
+ ).all(...chunk);
5669
+ for (const row of rows) enriched.add(String(row.id));
5670
+ }
5671
+ } catch {
5672
+ return /* @__PURE__ */ new Set();
5673
+ }
5674
+ return enriched;
5675
+ }
5792
5676
  async function enrichPhase(api, db, config2, model, idField, connectionKey, platform, ctx = {}) {
5793
5677
  const startTime = Date.now();
5794
5678
  const tsField = config2.timestampField ?? "_enriched_at";
@@ -5888,9 +5772,9 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
5888
5772
  stripExcludedFields(merged, ctx.exclude);
5889
5773
  }
5890
5774
  if (ctx.identityKey) {
5891
- const raw = getByDotPath2(merged, ctx.identityKey);
5892
- if (raw !== null && raw !== void 0) {
5893
- merged._identity = String(raw).toLowerCase().trim();
5775
+ const identity = resolveEntityIdentity(merged, ctx.identityKey);
5776
+ if (identity?.value) {
5777
+ merged._identity = identity.value;
5894
5778
  }
5895
5779
  }
5896
5780
  const setClauses = [];
@@ -6169,7 +6053,7 @@ async function syncModel(api, profile, options) {
6169
6053
  updateModelState(platform, model, { status: "failed", pagesProcessed, lastCursor }),
6170
6054
  (async () => {
6171
6055
  try {
6172
- const { getBackend: getBackend2 } = await import("./runtime-2PRVL2NO.js");
6056
+ const { getBackend: getBackend2 } = await import("./runtime-HDZCBTWH.js");
6173
6057
  const backend = await getBackend2();
6174
6058
  await Promise.race([
6175
6059
  backend.close(),
@@ -6200,6 +6084,7 @@ async function syncModel(api, profile, options) {
6200
6084
  let enrichedTotal = 0;
6201
6085
  let enrichSkipped = 0;
6202
6086
  let enrichRateLimited = 0;
6087
+ let memPreserved = 0;
6203
6088
  try {
6204
6089
  if (needsSqlite) db = await openDatabase(platform);
6205
6090
  let sinceDate = null;
@@ -6369,9 +6254,9 @@ async function syncModel(api, profile, options) {
6369
6254
  }
6370
6255
  if (profile.identityKey) {
6371
6256
  for (const record of records) {
6372
- const raw = getByDotPath(record, profile.identityKey);
6373
- if (raw !== null && raw !== void 0) {
6374
- record._identity = String(raw).toLowerCase().trim();
6257
+ const identity = resolveEntityIdentity(record, profile.identityKey);
6258
+ if (identity?.value) {
6259
+ record._identity = identity.value;
6375
6260
  }
6376
6261
  }
6377
6262
  }
@@ -6404,6 +6289,14 @@ async function syncModel(api, profile, options) {
6404
6289
  let pageReport = null;
6405
6290
  if (options.toMemory !== false) {
6406
6291
  try {
6292
+ const alreadyEnrichedIds = profile.enrich && db ? findEnrichedIds(
6293
+ db,
6294
+ model,
6295
+ profile.idField,
6296
+ profile.enrich,
6297
+ records,
6298
+ tableCreated
6299
+ ) : void 0;
6407
6300
  const report = await writePageToMemory(
6408
6301
  profile,
6409
6302
  records,
@@ -6412,10 +6305,12 @@ async function syncModel(api, profile, options) {
6412
6305
  // --embed / --no-embed on `sync run` threads through as a
6413
6306
  // per-run override so you can backfill embeddings on a
6414
6307
  // first-synced-without-embed platform in one shot.
6415
- embedOverride: options.embed
6308
+ embedOverride: options.embed,
6309
+ alreadyEnrichedIds
6416
6310
  }
6417
6311
  );
6418
6312
  pageReport = report;
6313
+ memPreserved += report.preserved;
6419
6314
  for (const key of report.sourceKeysSeen) seenSourceKeys.add(key);
6420
6315
  if (hasHooks && !db) {
6421
6316
  inserts = report.inserts;
@@ -6524,7 +6419,7 @@ async function syncModel(api, profile, options) {
6524
6419
  db.exec(`DROP TABLE IF EXISTS _seen_ids`);
6525
6420
  }
6526
6421
  if (options.toMemory !== false) {
6527
- const backend = await (await import("./runtime-2PRVL2NO.js")).getBackend();
6422
+ const backend = await (await import("./runtime-HDZCBTWH.js")).getBackend();
6528
6423
  const type = `${platform}/${model}`;
6529
6424
  const existing = await backend.listKeysByType(type);
6530
6425
  const sourcePrefix = `${type}:`;
@@ -6556,6 +6451,12 @@ async function syncModel(api, profile, options) {
6556
6451
  process.stderr.write(`Syncing ${platform}/${model}... page ${pagesProcessed} (${totalRecords} records) done
6557
6452
  `);
6558
6453
  }
6454
+ if (!isAgentMode() && memPreserved > 0) {
6455
+ process.stderr.write(
6456
+ ` Kept ${memPreserved} already-enriched record(s) in memory (the list shape would have replaced the enriched payload).
6457
+ `
6458
+ );
6459
+ }
6559
6460
  let enrichResult = null;
6560
6461
  if (profile.enrich && db && tableCreated && !options.dryRun) {
6561
6462
  enrichResult = await enrichPhase(
@@ -6604,7 +6505,7 @@ async function syncModel(api, profile, options) {
6604
6505
  let statusCounts;
6605
6506
  if (options.toMemory !== false) {
6606
6507
  try {
6607
- const backend = await (await import("./runtime-2PRVL2NO.js")).getBackend();
6508
+ const backend = await (await import("./runtime-HDZCBTWH.js")).getBackend();
6608
6509
  const typeName = `${platform}/${model}`;
6609
6510
  const [active, archived] = await Promise.all([
6610
6511
  backend.count(typeName, { status: "active" }),
@@ -6635,7 +6536,12 @@ async function syncModel(api, profile, options) {
6635
6536
  ...reconcileSkipped ? { reconcileSkipped } : {},
6636
6537
  ...statusCounts ? { statusCounts } : {},
6637
6538
  ...hasHooks ? { hooksInserted, hooksUpdated } : {},
6638
- ...profile.enrich ? { enriched: enrichedTotal, enrichSkipped, enrichRateLimited } : {}
6539
+ // `memPreserved` counts already-enriched rows this run wrote
6540
+ // non-authoritatively (enriched payload kept, list fields refreshed).
6541
+ // Reported to agents too — the human path only gets a stderr line, and
6542
+ // an agent seeing `recordsSynced: 200, enriched: 0` otherwise has no way
6543
+ // to tell "nothing needed enriching" from "enrich is broken".
6544
+ ...profile.enrich ? { enriched: enrichedTotal, enrichSkipped, enrichRateLimited, ...memPreserved > 0 ? { memPreserved } : {} } : {}
6639
6545
  };
6640
6546
  } catch (err) {
6641
6547
  const failedCount = db ? countRecords(db, model) : existingState?.totalRecords ?? 0;
@@ -6675,6 +6581,33 @@ async function syncModel(api, profile, options) {
6675
6581
 
6676
6582
  // src/lib/memory/sync/test.ts
6677
6583
  var SEARCHABLE_SAMPLE_SIZE = 5;
6584
+ function buildIdentityKeysPreview(samples, profile) {
6585
+ if (!profile.identityKey && !(profile.identityKeys && profile.identityKeys.length > 0)) return void 0;
6586
+ const perRecord = [];
6587
+ const sampleKeys = /* @__PURE__ */ new Set();
6588
+ const fanOutValues = /* @__PURE__ */ new Set();
6589
+ let fanOutCount = 0;
6590
+ for (const rec of samples) {
6591
+ const keys = collectIdentityKeys(rec, profile);
6592
+ perRecord.push(keys.length);
6593
+ for (const k of keys) {
6594
+ if (sampleKeys.size < 8) sampleKeys.add(k);
6595
+ }
6596
+ const identity = resolveEntityIdentity(rec, profile.identityKey);
6597
+ if (identity && identity.values.length > 1) {
6598
+ fanOutCount++;
6599
+ for (const v of identity.values) {
6600
+ if (fanOutValues.size < 4) fanOutValues.add(`${identity.prefix}:${v}`);
6601
+ }
6602
+ }
6603
+ }
6604
+ return {
6605
+ perRecord,
6606
+ sampleKeys: [...sampleKeys],
6607
+ ...fanOutCount > 0 ? { entityFanOut: { count: fanOutCount, sampleValues: [...fanOutValues] } } : {},
6608
+ ...profile.enrich && (profile.identityKeys?.length ?? 0) > 0 ? { resolvesAfterEnrich: true } : {}
6609
+ };
6610
+ }
6678
6611
  function detectColumnType2(value) {
6679
6612
  if (value === null || value === void 0) return "TEXT";
6680
6613
  if (typeof value === "string") return "TEXT";
@@ -6881,6 +6814,7 @@ async function testSyncProfile(api, profile) {
6881
6814
  }));
6882
6815
  report.sample = first;
6883
6816
  report.samples = records.slice(0, SEARCHABLE_SAMPLE_SIZE);
6817
+ report.identityKeysPreview = buildIdentityKeysPreview(report.samples, profile);
6884
6818
  report.ok = checks.every((c) => c.ok);
6885
6819
  return report;
6886
6820
  }
@@ -8080,6 +8014,29 @@ async function syncTestCommand(platformModel, options = {}) {
8080
8014
  console.log(pc9.dim(` ... and ${report.detectedColumns.length - 20} more`));
8081
8015
  }
8082
8016
  }
8017
+ if (report.identityKeysPreview) {
8018
+ const { perRecord, sampleKeys, entityFanOut, resolvesAfterEnrich } = report.identityKeysPreview;
8019
+ const total = perRecord.reduce((a, b) => a + b, 0);
8020
+ const min = perRecord.length ? Math.min(...perRecord) : 0;
8021
+ const max = perRecord.length ? Math.max(...perRecord) : 0;
8022
+ const mark = total === 0 ? pc9.yellow("~") : pc9.green("\u2713");
8023
+ console.log(`
8024
+ ${pc9.bold("Merge + identity keys")} ${pc9.dim(`(cross-platform \u2014 #128)`)}`);
8025
+ console.log(` ${mark} ${perRecord.length} sample${perRecord.length === 1 ? "" : "s"}, ${min}\u2013${max} key${max === 1 ? "" : "s"} per record`);
8026
+ if (sampleKeys.length > 0) {
8027
+ console.log(` ${pc9.dim("e.g.")} ${sampleKeys.slice(0, 8).map((k) => pc9.cyan(k)).join(", ")}`);
8028
+ } else if (resolvesAfterEnrich) {
8029
+ console.log(` ${pc9.dim("note:")} 0 on these list-shape samples \u2014 this profile enriches, and its identityKeys paths resolve in the enrich phase.`);
8030
+ } else {
8031
+ console.log(` ${pc9.yellow("note:")} no identity keys resolved on these samples \u2014 check the identityKey/identityKeys paths.`);
8032
+ }
8033
+ if (entityFanOut) {
8034
+ console.log(` ${pc9.yellow("warn:")} identityKey resolved to MULTIPLE values on ${entityFanOut.count} of ${perRecord.length} samples \u2014 those records get NO merge key.`);
8035
+ console.log(` ${pc9.dim("saw:")} ${entityFanOut.sampleValues.map((v) => pc9.cyan(v)).join(", ")}`);
8036
+ console.log(` ${pc9.dim('A singular identityKey means "this record IS this entity", so a fan-out has no safe answer.')}`);
8037
+ console.log(` ${pc9.dim("Point it at a single-valued path, or move it to identityKeys[] for participant associations.")}`);
8038
+ }
8039
+ }
8083
8040
  if (searchablePreview) {
8084
8041
  console.log(`
8085
8042
  ${pc9.bold("Searchable preview")} ${pc9.dim(`(${searchablePreview.mode}, ${searchablePreview.sampledRecords} sample${searchablePreview.sampledRecords === 1 ? "" : "s"})`)}`);
@@ -8237,7 +8194,7 @@ ${result.total} results`);
8237
8194
  }
8238
8195
  }
8239
8196
  async function syncSqlCommand(platformModel, sql) {
8240
- const { syncSqlCommand: runSyncSql } = await import("./sql-HG7R2JML.js");
8197
+ const { syncSqlCommand: runSyncSql } = await import("./sql-AZDCMY7G.js");
8241
8198
  await runSyncSql(platformModel, sql);
8242
8199
  }
8243
8200
  async function syncDeleteCommand(platformModel, options) {
@@ -8315,7 +8272,7 @@ async function syncDeleteCommand(platformModel, options) {
8315
8272
  async function maybeAutoMigrateLegacy(platform, models) {
8316
8273
  const dbSize = getDatabaseSize(platform);
8317
8274
  if (!dbSize || dbSize === "0 B") return;
8318
- const { getBackend: getBackend2 } = await import("./runtime-2PRVL2NO.js");
8275
+ const { getBackend: getBackend2 } = await import("./runtime-HDZCBTWH.js");
8319
8276
  const backend = await getBackend2();
8320
8277
  let memoryHasData = false;
8321
8278
  for (const model of models) {
@@ -8331,7 +8288,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
8331
8288
  ` detected legacy .one/sync/data/${platform}.db (${dbSize}) \u2014 auto-migrating into memory before sync.
8332
8289
  `
8333
8290
  );
8334
- const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-TRUZVAT5.js");
8291
+ const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-LXDIZMXH.js");
8335
8292
  await memMigrateCommand3({ platform, yes: true });
8336
8293
  return;
8337
8294
  }
@@ -8340,7 +8297,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
8340
8297
  initialValue: true
8341
8298
  });
8342
8299
  if (p7.isCancel(shouldMigrate) || !shouldMigrate) return;
8343
- const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-TRUZVAT5.js");
8300
+ const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-LXDIZMXH.js");
8344
8301
  await memMigrateCommand2({ platform, yes: true });
8345
8302
  }
8346
8303
  async function syncSuggestSearchableCommand(platformModel, options = {}) {
@@ -8401,7 +8358,7 @@ async function syncSuggestSearchableCommand(platformModel, options = {}) {
8401
8358
  async function syncListCommand(platform) {
8402
8359
  const profiles = listProfiles(platform);
8403
8360
  const state = await readSyncState();
8404
- const { getBackend: getBackend2 } = await import("./runtime-2PRVL2NO.js");
8361
+ const { getBackend: getBackend2 } = await import("./runtime-HDZCBTWH.js");
8405
8362
  const backend = await getBackend2();
8406
8363
  const syncs = await Promise.all(profiles.map(async (p10) => {
8407
8364
  const modelState = state[p10.platform]?.[p10.model];
@@ -8676,7 +8633,7 @@ function registerSyncSubcommands(sync) {
8676
8633
  await syncSqlCommand(platformModel, sql);
8677
8634
  });
8678
8635
  sync.command("schema <platform/model>").description("Inspect the JSON structure of synced records (field paths, types, examples) \u2014 useful before writing `sync sql` queries").action(async (platformModel) => {
8679
- const { syncSchemaCommand } = await import("./schema-LCRWSFD5.js");
8636
+ const { syncSchemaCommand } = await import("./schema-SYMG6SRC.js");
8680
8637
  await syncSchemaCommand(platformModel);
8681
8638
  });
8682
8639
  sync.command("delete <platform/model>").description('Delete records from local sync data (e.g. one sync delete notion/pages --id "abc-123")').option("--id <value>", "Delete record by ID").option("--where <conditions>", 'Delete records matching conditions (e.g. "status=archived")').option("--where-sql <predicate>", `Delete using a raw SQL WHERE clause (e.g. "json_extract(data, '$.type') = 'promotion'")`).option("--yes", "Skip confirmation prompt").action(async (platformModel, options) => {
@@ -9098,6 +9055,7 @@ function parseValue(raw) {
9098
9055
  }
9099
9056
 
9100
9057
  // src/commands/mem/records.ts
9058
+ import pc10 from "picocolors";
9101
9059
  async function memAddCommand(type, dataRaw, flags) {
9102
9060
  requireMemoryInit();
9103
9061
  const data = parseJsonArg(dataRaw, "data");
@@ -9122,8 +9080,12 @@ async function memGetCommand(id, flags) {
9122
9080
  async function memUpdateCommand(id, patchRaw) {
9123
9081
  requireMemoryInit();
9124
9082
  const patch = parseJsonArg(patchRaw, "patch");
9125
- if (patch && typeof patch === "object" && !Array.isArray(patch) && "keys" in patch) {
9126
- error("`keys` is not a data field. Use `one mem key <id> --set <csv>` (or --add/--remove) to change a record's keys.");
9083
+ const isPatchObject = !!patch && typeof patch === "object" && !Array.isArray(patch);
9084
+ if (isPatchObject && "keys" in patch) {
9085
+ error("`keys` is not a data field. Use `one mem key <id> --set <csv>` (or --add/--remove) to change a record's merge keys.");
9086
+ }
9087
+ if (isPatchObject && ("identity_keys" in patch || "identityKeys" in patch)) {
9088
+ error("`identity_keys` is not a data field. Participant associations are derived on sync from the profile's `identityKeys` \u2014 edit the sync profile and re-run `one sync run`, then query with `one mem find-by-key <key>`.");
9127
9089
  }
9128
9090
  const updated = await updateRecord(id, { data: patch });
9129
9091
  if (!updated) error(`Record ${id} not found`);
@@ -9152,7 +9114,7 @@ async function memKeyCommand(id, flags) {
9152
9114
  if (outcome.status === "not_found") error(`Record ${id} not found`);
9153
9115
  if (outcome.status === "conflict") {
9154
9116
  error(
9155
- `Key "${outcome.key}" is already owned by active record ${outcome.recordId} (type ${outcome.recordType}). Keys must be unique across active records \u2014 archive or re-key that record first.`
9117
+ `Key "${outcome.key}" is already owned by active record ${outcome.recordId} (type ${outcome.recordType}). Merge keys must be unique across active records \u2014 archive or re-key that record first.`
9156
9118
  );
9157
9119
  }
9158
9120
  printRecord(outcome.record);
@@ -9286,9 +9248,104 @@ async function memFindBySourceCommand(sourceKey) {
9286
9248
  if (!record) error(`No record owns source "${sourceKey}"`);
9287
9249
  printRecord(record);
9288
9250
  }
9251
+ function summarizeRecord(r) {
9252
+ const d = r.data ?? {};
9253
+ for (const field of ["title", "subject", "name", "full_name", "display_name", "summary", "headline", "email"]) {
9254
+ const v = d[field];
9255
+ if (typeof v === "string" && v.trim()) return v.trim().slice(0, 80);
9256
+ }
9257
+ return pc10.dim("(untitled)");
9258
+ }
9259
+ function relativeTime(iso) {
9260
+ if (!iso) return "";
9261
+ const then = new Date(iso).getTime();
9262
+ if (Number.isNaN(then)) return "";
9263
+ const s = Math.max(0, Math.floor((Date.now() - then) / 1e3));
9264
+ if (s < 60) return `${s}s ago`;
9265
+ const m = Math.floor(s / 60);
9266
+ if (m < 60) return `${m}m ago`;
9267
+ const h = Math.floor(m / 60);
9268
+ if (h < 24) return `${h}h ago`;
9269
+ const d = Math.floor(h / 24);
9270
+ if (d < 30) return `${d}d ago`;
9271
+ const mo = Math.floor(d / 30);
9272
+ if (mo < 12) return `${mo}mo ago`;
9273
+ return `${Math.floor(mo / 12)}y ago`;
9274
+ }
9275
+ var FIND_BY_KEY_FETCH_CAP = 2e3;
9276
+ function normalizeQueryKey(key) {
9277
+ return key.trim().toLowerCase();
9278
+ }
9279
+ async function memFindByKeyCommand(key, secondKey, flags) {
9280
+ requireMemoryInit();
9281
+ const backend = await getBackend();
9282
+ const rawKeys = secondKey ? [key, secondKey] : [key];
9283
+ const keys = rawKeys.map(normalizeQueryKey);
9284
+ const perType = parsePositiveInt(flags.limit, 10, "--limit");
9285
+ const fetchPage = async (ks) => {
9286
+ const rows = await backend.findByKeys(ks, { type: flags.type, limit: FIND_BY_KEY_FETCH_CAP + 1 });
9287
+ return { records: rows.slice(0, FIND_BY_KEY_FETCH_CAP), truncated: rows.length > FIND_BY_KEY_FETCH_CAP };
9288
+ };
9289
+ let page = await fetchPage(keys);
9290
+ let matchedKeys = keys;
9291
+ if (page.records.length === 0 && keys.some((k, i) => k !== rawKeys[i])) {
9292
+ const verbatim = await fetchPage(rawKeys);
9293
+ if (verbatim.records.length > 0) {
9294
+ page = verbatim;
9295
+ matchedKeys = rawKeys;
9296
+ }
9297
+ }
9298
+ const { records, truncated } = page;
9299
+ const byType = /* @__PURE__ */ new Map();
9300
+ for (const r of records) {
9301
+ const list = byType.get(r.type) ?? [];
9302
+ list.push(r);
9303
+ byType.set(r.type, list);
9304
+ }
9305
+ if (isAgentMode()) {
9306
+ const grouped = {};
9307
+ for (const [type, list] of byType) {
9308
+ grouped[type] = { count: list.length, items: list.slice(0, perType) };
9309
+ }
9310
+ json({
9311
+ keys: matchedKeys,
9312
+ total: records.length,
9313
+ truncated,
9314
+ fetchCap: FIND_BY_KEY_FETCH_CAP,
9315
+ perTypeLimit: perType,
9316
+ byType: grouped
9317
+ });
9318
+ return;
9319
+ }
9320
+ const label = matchedKeys.map((k) => pc10.cyan(k)).join(pc10.dim(" + "));
9321
+ if (records.length === 0) {
9322
+ console.log(`
9323
+ No records linked to ${label}.
9324
+ `);
9325
+ return;
9326
+ }
9327
+ console.log();
9328
+ console.log(` ${label} ${pc10.dim("\u2014")} ${records.length}${truncated ? "+" : ""} record${records.length === 1 ? "" : "s"} across ${byType.size} type${byType.size === 1 ? "" : "s"}`);
9329
+ console.log();
9330
+ const typeWidth = Math.min(30, Math.max(...[...byType.keys()].map((t) => t.length)));
9331
+ for (const [type, list] of byType) {
9332
+ console.log(` ${pc10.bold(type.padEnd(typeWidth))} ${pc10.dim(`${list.length} record${list.length === 1 ? "" : "s"}`)}`);
9333
+ for (const r of list.slice(0, perType)) {
9334
+ console.log(` ${pc10.dim("\xB7")} ${summarizeRecord(r)} ${pc10.dim(relativeTime(r.updated_at))} ${pc10.dim(r.id)}`);
9335
+ }
9336
+ if (list.length > perType) {
9337
+ console.log(` ${pc10.dim(`\u2026 and ${list.length - perType} more (raise --limit)`)}`);
9338
+ }
9339
+ }
9340
+ if (truncated) {
9341
+ console.log();
9342
+ console.log(` ${pc10.yellow("\u26A0")} Stopped at ${FIND_BY_KEY_FETCH_CAP} matches \u2014 there are more, and types sorting after the last one shown are missing entirely. Re-run with --type <type> to see them.`);
9343
+ }
9344
+ console.log();
9345
+ }
9289
9346
 
9290
9347
  // src/commands/mem/doctor.ts
9291
- import pc10 from "picocolors";
9348
+ import pc11 from "picocolors";
9292
9349
  async function memDoctorCommand() {
9293
9350
  const checks = [];
9294
9351
  const cfg = getMemoryConfig();
@@ -9395,19 +9452,19 @@ function emit(checks, capInfo) {
9395
9452
  return;
9396
9453
  }
9397
9454
  for (const c of checks) {
9398
- const mark = c.ok ? pc10.green("\u2713") : pc10.red("\u2717");
9399
- const detail = c.detail ? pc10.dim(` \u2014 ${c.detail}`) : "";
9455
+ const mark = c.ok ? pc11.green("\u2713") : pc11.red("\u2717");
9456
+ const detail = c.detail ? pc11.dim(` \u2014 ${c.detail}`) : "";
9400
9457
  console.log(` ${mark} ${c.name}${detail}`);
9401
9458
  }
9402
9459
  if (!allOk) {
9403
- console.log("\n" + pc10.yellow("Memory is not fully healthy."));
9460
+ console.log("\n" + pc11.yellow("Memory is not fully healthy."));
9404
9461
  process.exitCode = 1;
9405
9462
  } else {
9406
- console.log("\n" + pc10.green("Memory is healthy."));
9463
+ console.log("\n" + pc11.green("Memory is healthy."));
9407
9464
  }
9408
9465
  const line = semanticSearchUpgradeLine({ vectorSearchAvailable: capInfo.vectorSearchAvailable });
9409
9466
  if (line) console.log(`
9410
- ${pc10.dim(line)}`);
9467
+ ${pc11.dim(line)}`);
9411
9468
  }
9412
9469
 
9413
9470
  // src/commands/mem/export.ts
@@ -9483,6 +9540,14 @@ async function memImportCommand(file) {
9483
9540
  data: record.data,
9484
9541
  tags: record.tags,
9485
9542
  keys,
9543
+ // Carry `identity_keys` through (#128). Export writes the whole row, so
9544
+ // the JSONL has them; omitting them here only looked harmless because
9545
+ // re-importing over a store that still holds the record takes the union
9546
+ // branch in mem_upsert_by_keys and preserves what's already there. On the
9547
+ // INSERT path — restore into a wiped store, or a backend swap
9548
+ // (pglite → postgres), which is exactly what export|import is for —
9549
+ // dropping them here loses every participant association permanently.
9550
+ identity_keys: record.identity_keys,
9486
9551
  sources: record.sources,
9487
9552
  searchable_text: record.searchable_text ?? null,
9488
9553
  content_hash: record.content_hash ?? null,
@@ -9669,10 +9734,10 @@ function registerMemoryCommands(program2) {
9669
9734
  mem.command("vacuum").description("Run backend maintenance (VACUUM ANALYZE on tables)").action(memVacuumCommand);
9670
9735
  mem.command("reindex").description("Backfill embeddings (default) or searchable_text (--searchable)").option("--type <type>", "Restrict to one record type, e.g. attio/attioPeople").option("--model <name>", "Override the embedding model").option("--force", "Re-embed even rows that already have a matching embedding", false).option("--batch <n>", "OpenAI calls per batch (default 50)", "50").option("--limit <n>", "Safety cap on total records to scan (default 100000)").option("--searchable", "Regenerate searchable_text (NULL or stale vs. data) instead of embeddings", false).option("--all", "With --searchable: also rewrite rows whose text is stale, not just NULL", false).action(memReindexCommand);
9671
9736
  mem.command("sql <query>").description("Run a read-only SELECT / WITH / EXPLAIN against the memory store").action(memSqlCommand);
9672
- mem.command("add <type> <data>").description("Add a new memory record (data is JSON)").option("--tags <csv>", "Comma-separated tags").option("--keys <csv>", "Comma-separated keys (prefixed, e.g. email:x@y.com)").option("--weight <n>", "Importance 1-10 (default 5)").option("--embed", "Force embedding for this record").option("--no-embed", "Skip embedding for this record").action(memAddCommand);
9737
+ mem.command("add <type> <data>").description("Add a new memory record (data is JSON)").option("--tags <csv>", "Comma-separated tags").option("--keys <csv>", "Comma-separated merge keys (`keys[]`, prefixed, e.g. email:x@y.com \u2014 unique across active records)").option("--weight <n>", "Importance 1-10 (default 5)").option("--embed", "Force embedding for this record").option("--no-embed", "Skip embedding for this record").action(memAddCommand);
9673
9738
  mem.command("get <id>").description("Get a record by id").option("--links", "Include outgoing and incoming links", false).action(memGetCommand);
9674
9739
  mem.command("update <id> <patch>").description("Update a record (patch is JSON merged into data; regenerates searchable_text)").action(memUpdateCommand);
9675
- mem.command("key <id>").description("Manage a record's identity keys (unique across active records)").option("--add <csv>", "Keys to add (comma-separated, e.g. email:x@y.com)").option("--remove <csv>", "Keys to remove (comma-separated)").option("--set <csv>", "Replace all keys with this comma-separated list").action((id, flags) => memKeyCommand(id, flags));
9740
+ mem.command("key <id>").description("Manage a record's merge keys (`keys[]` \u2014 unique across active records; these drive upsert-merge). Not participant associations \u2014 those live in `identity_keys[]`, written by a sync profile's `identityKeys`.").option("--add <csv>", "Merge keys to add (comma-separated, e.g. email:x@y.com)").option("--remove <csv>", "Merge keys to remove (comma-separated)").option("--set <csv>", "Replace all merge keys with this comma-separated list").action((id, flags) => memKeyCommand(id, flags));
9676
9741
  mem.command("archive <id>").description("Archive a record").option("--reason <text>", "Why it was archived (user_archived | deleted_upstream | superseded | \u2026)").action(memArchiveCommand);
9677
9742
  mem.command("weight <id> <n>").description("Set record relevance weight (1-10)").action(memWeightCommand);
9678
9743
  mem.command("flush <id>").description("Reset access count for a record").action(memFlushCommand);
@@ -9684,6 +9749,7 @@ function registerMemoryCommands(program2) {
9684
9749
  mem.command("linked <id>").description("List linked records").option("--relation <name>", "Filter by relation").option("--direction <dir>", "outgoing | incoming | both", "outgoing").action(memLinkedCommand);
9685
9750
  mem.command("sources <id>").description("List source entries on a record").action(memSourcesCommand);
9686
9751
  mem.command("find-by-source <sourceKey>").description('Look up the record owning "<system>/<model>:<external_id>"').action(memFindBySourceCommand);
9752
+ mem.command("find-by-key <key> [secondKey]").description("Records carrying a key (e.g. email:jane@acme.com) in either `keys[]` or `identity_keys[]`, grouped by type. Two keys = intersection. Keys are matched case-insensitively.").option("--type <type>", "Only this record type (e.g. gmail/gmailThreads) \u2014 also the way past the 2000-match fetch cap").option("--limit <n>", "Max records shown per type (default 10)").action((key, secondKey, flags) => memFindByKeyCommand(key, secondKey, flags));
9687
9753
  registerSyncCommands(mem, {
9688
9754
  description: "Sync platform data into memory (alias of `one sync`)"
9689
9755
  });
@@ -9693,7 +9759,7 @@ function registerMemoryCommands(program2) {
9693
9759
  }
9694
9760
 
9695
9761
  // src/commands/cache.ts
9696
- import pc11 from "picocolors";
9762
+ import pc12 from "picocolors";
9697
9763
  async function cacheClearCommand(actionId) {
9698
9764
  if (actionId) {
9699
9765
  const deleted = clearEntry(actionId);
@@ -9702,9 +9768,9 @@ async function cacheClearCommand(actionId) {
9702
9768
  return;
9703
9769
  }
9704
9770
  if (deleted) {
9705
- console.log(`Cleared cache for ${pc11.cyan(actionId)}`);
9771
+ console.log(`Cleared cache for ${pc12.cyan(actionId)}`);
9706
9772
  } else {
9707
- console.log(`No cache entry found for ${pc11.dim(actionId)}`);
9773
+ console.log(`No cache entry found for ${pc12.dim(actionId)}`);
9708
9774
  }
9709
9775
  } else {
9710
9776
  const count = clearAll();
@@ -9741,7 +9807,7 @@ async function cacheListCommand(options) {
9741
9807
  type: e.type,
9742
9808
  key: e.entry.key,
9743
9809
  age: formatAge(getAge(e.entry)),
9744
- status: isFresh(e.entry) ? pc11.green("fresh") : pc11.yellow("expired")
9810
+ status: isFresh(e.entry) ? pc12.green("fresh") : pc12.yellow("expired")
9745
9811
  }));
9746
9812
  printTable(
9747
9813
  [
@@ -9832,13 +9898,13 @@ async function cacheUpdateAllCommand() {
9832
9898
  if (errors.length > 0) {
9833
9899
  console.log();
9834
9900
  for (const e of errors) {
9835
- console.log(` ${pc11.red("\u2717")} ${e.key}: ${pc11.dim(e.error)}`);
9901
+ console.log(` ${pc12.red("\u2717")} ${e.key}: ${pc12.dim(e.error)}`);
9836
9902
  }
9837
9903
  }
9838
9904
  }
9839
9905
 
9840
9906
  // src/commands/guide.ts
9841
- import pc12 from "picocolors";
9907
+ import pc13 from "picocolors";
9842
9908
 
9843
9909
  // src/lib/guide-content.ts
9844
9910
  var GUIDE_OVERVIEW = `# One CLI \u2014 Agent Guide
@@ -9924,7 +9990,7 @@ one --agent flow list # List all workflows
9924
9990
  - Code steps can reference an external \`.mjs\` module under the flow's \`lib/\` folder (stdin JSON in, stdout JSON out) \u2014 keeps JS out of JSON strings and makes flows shareable
9925
9991
  - 12 step types: action, transform, code, condition, loop, parallel, file-read, file-write, while, flow, paginate, bash
9926
9992
  - Data wiring via selectors: \`$.input.param\`, \`$.steps.stepId.response\`, \`$.loop.item\`
9927
- - AI analysis via bash steps: \`claude --print\` with \`parseJson: true\`
9993
+ - AI analysis via bash steps: \`claude --print --output-format json\` with \`parseEnvelope: true\` (unwraps the CLI envelope + fences + preamble into clean parsed JSON; use instead of \`parseJson\` for claude steps)
9928
9994
  - Use \`--allow-bash\` to enable bash steps, \`--mock\` for dry-run with realistic mock responses (uses example data from action schemas)
9929
9995
  - Use \`--skip-validation\` to bypass input validation on action steps
9930
9996
  - Use \`--output-file <path>\` to stream the full result to a file instead of stdout \u2014 for large results that would otherwise be truncated or hit the JSON string-size limit; stdout (and \`--agent\` output) then carries an \`outputFile\` pointer instead of inline \`steps\`
@@ -10329,15 +10395,18 @@ one --agent mem get <id> --links
10329
10395
  # Update (shallow merge into data; regenerates searchable_text from the merged data)
10330
10396
  one --agent mem update <id> '{"status":"done"}'
10331
10397
 
10332
- # Manage identity keys AFTER creation (keys are a first-class column, NOT data).
10333
- # Unique across ACTIVE records \u2014 a clear error names the record that owns a taken key.
10398
+ # Manage MERGE KEYS after creation \u2014 the \`keys[]\` column (a first-class column,
10399
+ # NOT data). Unique across ACTIVE records \u2014 a clear error names the record that
10400
+ # owns a taken key. This is the column that collapses two records into one.
10334
10401
  one --agent mem key <id> --add email:new@x.com
10335
10402
  one --agent mem key <id> --remove email:old@x.com
10336
- one --agent mem key <id> --set 'email:a@x.com,phone:+1555' # replace all keys
10403
+ one --agent mem key <id> --set 'email:a@x.com,phone:+1555' # replace ALL merge keys
10337
10404
  # NOTE: passing keys to 'mem update' is rejected \u2014 keys are not a data field.
10405
+ # NOTE: 'mem key' WRITES keys[]. To READ across records, use 'mem find-by-key'
10406
+ # (see Identity keys below) \u2014 different command, different column coverage.
10338
10407
 
10339
- # Archive / unarchive. Archiving FREES a record's keys: an archived record no
10340
- # longer blocks a new active record from reusing the same key (uniqueness is
10408
+ # Archive / unarchive. Archiving FREES a record's merge keys: an archived record
10409
+ # no longer blocks a new active record from reusing the same key (uniqueness is
10341
10410
  # scoped to active records).
10342
10411
  one --agent mem archive <id> --reason superseded
10343
10412
 
@@ -10379,6 +10448,55 @@ one --agent mem sources <id> # list source entries
10379
10448
  one --agent mem find-by-source attio/attioPeople:abc-123
10380
10449
  \`\`\`
10381
10450
 
10451
+ \`find-by-source\` answers "which ONE record owns this external id" \u2014 a single record, preferring the active owner. For "everything about this person", use \`find-by-key\` below.
10452
+
10453
+ ## Identity keys (cross-platform join)
10454
+
10455
+ Two different columns carry prefixed keys like \`email:jane@acme.com\`, and the difference is the whole point:
10456
+
10457
+ - **\`keys[]\` \u2014 merge keys.** "This record IS this entity." Unique across active records, and \`sync\` merges any incoming record that overlaps an existing one. Written by \`mem add --keys\`, \`mem key\`, and a profile's singular \`identityKey\`.
10458
+ - **\`identity_keys[]\` \u2014 association keys (#128).** "This record INVOLVES these people." Deliberately exempt from both the uniqueness constraint and the overlap-merge, so a Gmail thread with 12 participants stays one thread instead of collapsing into a contact. Written by a profile's plural \`identityKeys\` (see \`one guide sync\`).
10459
+
10460
+ \`mem find-by-key\` queries BOTH columns at once, so one call surfaces the CRM record, the email threads, the calendar events, and the meeting notes for the same person:
10461
+
10462
+ \`\`\`bash
10463
+ one --agent mem find-by-key email:jane@acme.com # every record carrying the key
10464
+ one --agent mem find-by-key email:jane@acme.com --type gmail/gmailThreads # one record type
10465
+ one --agent mem find-by-key email:a@x.com email:b@y.com # intersection \u2014 records with BOTH
10466
+ one --agent mem find-by-key email:jane@acme.com --limit 25 # show more per type (default 10)
10467
+ \`\`\`
10468
+
10469
+ Any prefix works (\`email:\`, \`domain:\`, \`phone:\`, even a raw source key) \u2014 matching is plain array containment over the two columns combined, so a two-key query still matches when one key sits in \`keys[]\` and the other in \`identity_keys[]\`. Only ACTIVE records are returned. Lookup keys are lowercased/trimmed to match how sync writes them, with a verbatim retry for hand-written mixed-case keys, so \`email:Jane@Acme.com\` and \`email:jane@acme.com\` find the same records.
10470
+
10471
+ Agent output is grouped by record type:
10472
+
10473
+ \`\`\`json
10474
+ {
10475
+ "keys": ["email:jane@acme.com"],
10476
+ "total": 13,
10477
+ "truncated": false,
10478
+ "fetchCap": 2000,
10479
+ "perTypeLimit": 10,
10480
+ "byType": {
10481
+ "attio/attioPeople": { "count": 1, "items": [ { "id": "...", "type": "...", "data": {}, "keys": ["attio/attioPeople:J1", "email:jane@acme.com"] } ] },
10482
+ "gmail/gmailThreads": { "count": 12, "items": [] }
10483
+ }
10484
+ }
10485
+ \`\`\`
10486
+
10487
+ \`items\` are whole mem records. \`keys\` and \`identity_keys\` are **omitted, not \`[]\`**, when the record has none \u2014 the contact above matched on \`keys[]\` and carries no \`identity_keys\` field at all. Read them as \`(item.identity_keys ?? [])\`.
10488
+
10489
+ There are TWO independent truncations, and conflating them gives wrong answers:
10490
+
10491
+ - **\`--limit\` (default 10, echoed as \`perTypeLimit\`)** is display only. It slices each \`items\` array; \`total\` and \`count\` stay full match counts. \`count > items.length\` just means "raise \`--limit\` to list the rest".
10492
+ - **\`truncated: true\`** means the query hit the \`fetchCap\` (2000 matches) before it ran out of records. Now \`total\` and every \`count\` are floors, and because rows arrive ordered by type, any type sorting after the cut is absent from \`byType\` altogether. One high-volume type \u2014 your own address on every synced Gmail thread, say \u2014 can consume the whole budget. Re-run with \`--type <type>\` to get an accurate answer for the type you care about; the interactive (non-\`--agent\`) output prints a \`\u26A0\` and a \`2000+\` style count in the same situation.
10493
+
10494
+ \`items\` entries are whole mem records, same fields as \`mem get\`, so no follow-up \`mem get\` is needed.
10495
+
10496
+ \`keys\` echoes the key form that actually matched, which is the normalized one unless the verbatim retry won.
10497
+
10498
+ Do not confuse \`mem key\` with \`mem find-by-key\`: \`mem key\` WRITES \`keys[]\` on a single record (\`--set\` replaces the array outright and can trigger a merge), while \`find-by-key\` is a read-only query across every record.
10499
+
10382
10500
  ## Sync into memory
10383
10501
 
10384
10502
  \`one mem sync\` is a full alias of \`one sync\` \u2014 same handlers, same options. Use either.
@@ -10669,9 +10787,30 @@ The transform can be any command: \`jq\`, \`python3\`, a bash script, or \`one f
10669
10787
 
10670
10788
  Transform, exclude, identityKey, and hooks all fire in **both** phases. In Phase 1 they run on the raw list page; in Phase 2 they run again on the merged (list + enriched) record so that transforms can extract columns from fields that only appear after enrichment. Phase 2 fires \`onUpdate\`/\`onChange\` for every row it writes \u2014 \`onInsert\` is Phase-1-only because the row already exists in SQL by the time enrichment runs.
10671
10789
 
10790
+ **Exception \u2014 plural \`identityKeys\` on an enriching profile.** When a profile declares \`enrich\`, Phase 1 deliberately writes *no opinion* about \`identity_keys[]\` (it leaves whatever is stored untouched) and only Phase 2 sets them. Phase 1 sees the list shape, where participant paths like \`messages[].payload.headers[name=From].value\` don't exist yet \u2014 if it wrote its empty result, every re-sync would erase the participants Phase 2 had resolved. Two consequences worth knowing: a row whose enrichment is skipped or rate-limited keeps \`identity_keys\` \`NULL\` until it enriches, and a participant removed upstream is only cleared once that row re-enriches.
10791
+
10792
+ ### Phase 1 never degrades an already-enriched record
10793
+
10794
+ Phase 2 only visits rows \`WHERE _enriched_at IS NULL\`, so it never revisits a row it has already enriched. That makes Phase 1's write dangerous on every run after the first: it holds only the thin list shape, and an authoritative write would replace the enriched payload with it \u2014 permanently, since Phase 2 won't come back to repair it.
10795
+
10796
+ So for a row the mirror reports as already enriched, Phase 1 writes **non-authoritatively**:
10797
+
10798
+ | | Already-enriched row | Not yet enriched |
10799
+ |---|---|---|
10800
+ | \`data\` | merged \u2014 enrich-only fields survive, list fields still refresh | replaced |
10801
+ | \`searchable_text\` / \`content_hash\` | kept (thin text can't overwrite the enriched text) | recomputed |
10802
+ | \`identity_keys[]\` | kept | written |
10803
+ | \`keys[]\`, \`tags\`, \`sources.last_synced_at\`, embeddings | updated normally | updated normally |
10804
+
10805
+ The write still happens, so \`--embed\`, profile edits, and the store's un-archive self-heal all keep working on enriched rows. The one thing given up: a field that disappears from the *list* shape upstream no longer disappears from an enriched record, because a merge cannot delete. \`sync run\` reports the count as \`memPreserved\`.
10806
+
10807
+ **Known gap:** \`--full-refresh\` does **not** clear \`_enriched_at\`, so it does not re-enrich \u2014 and there is currently no way to re-enrich a record whose upstream detail content changed after its first enrichment. Delete the mirror (\`.one/sync/data/<platform>.db\`) to force a full re-enrich.
10808
+
10672
10809
  ## Cross-Platform Identity
10673
10810
 
10674
- Add \`identityKey\` to a sync profile to extract a stable cross-platform identifier (e.g. email) into a normalized \`_identity\` column:
10811
+ Two ways to tag a record with a cross-platform identifier (e.g. email), depending on whether the record IS an entity or INVOLVES many:
10812
+
10813
+ **\`identityKey\` (singular)** \u2014 "this record IS this entity". The value goes into the record's \`keys[]\`, which the store uses to MERGE records for the same entity across platforms (Attio + HubSpot for the same person collapse into one):
10675
10814
 
10676
10815
  \`\`\`json
10677
10816
  {"platform": "hubspot", "model": "contacts", "identityKey": "properties.email"}
@@ -10679,14 +10818,34 @@ Add \`identityKey\` to a sync profile to extract a stable cross-platform identif
10679
10818
  {"platform": "attio", "model": "attioPeople", "identityKey": "email_addresses[0].email_address"}
10680
10819
  \`\`\`
10681
10820
 
10682
- The value is lowercased and trimmed, stored as a prefixed key on the mem record (e.g. \`email:jane@acme.com\`). Look up across platforms:
10821
+ **\`identityKeys\` (plural, #128)** \u2014 "this record INVOLVES these people". For records with N participants (Gmail thread From/To/Cc, calendar attendees, meeting invitees) where a single key can't capture everyone. These go into a separate \`identity_keys[]\` column that does NOT merge \u2014 so a thread with many participants stays its own record:
10822
+
10823
+ \`\`\`json
10824
+ {"platform": "google-calendar", "model": "events", "identityKeys": [
10825
+ {"prefix": "email", "path": "organizer.email"},
10826
+ {"prefix": "email", "path": "attendees[].email"}
10827
+ ]}
10828
+ \`\`\`
10829
+
10830
+ 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.
10831
+
10832
+ Query everything sharing an identity key, grouped by type:
10683
10833
 
10684
10834
  \`\`\`bash
10835
+ one --agent mem find-by-key email:jane@acme.com # all records carrying this key
10836
+ one --agent mem find-by-key email:jane@acme.com --type gmail/gmailThreads
10837
+ one --agent mem find-by-key email:jane@acme.com email:bob@acme.com # intersection (both keys)
10838
+ one --agent mem find-by-key email:jane@acme.com --limit 25 # show more records per type
10839
+ # Look up the single record owning a source key:
10685
10840
  one --agent mem find-by-source hubspot/contacts:<id>
10686
- # Or via the dotted --where path on the identity key:
10687
- one --agent sync query hubspot/contacts --where 'email=jane@acme.com'
10841
+ # Or filter ONE model on the underlying field via the dotted --where path. Note
10842
+ # this is the raw data path, NOT the identity key \u2014 for the hubspot profile above
10843
+ # that's 'properties.email', the same path its identityKey extracts from.
10844
+ one --agent sync query hubspot/contacts --where 'properties.email=jane@acme.com'
10688
10845
  \`\`\`
10689
10846
 
10847
+ \`find-by-key\` spans BOTH columns \u2014 entity keys in \`keys[]\` and association keys in \`identity_keys[]\`. \`--limit\` (default 10) caps only how many records are listed per type; the per-type \`count\` and overall \`total\` are still full match counts, so \`count\` exceeding the listed items is the signal to raise it. A separate \`truncated\` flag reports the very different case where the 2000-match fetch cap was hit and whole types are missing \u2014 see \`one guide memory\` for the full response shape.
10848
+
10690
10849
  \`sync sql\` was retired in the unified memory cutover \u2014 a raw-SQL surface can't safely span the embedded Postgres, remote Postgres, and third-party backends without leaking specifics. Use \`mem search\` / \`sync search\` / \`sync query\` with dotted \`--where\` paths instead.
10691
10850
 
10692
10851
  ## Exclude Fields
@@ -10774,7 +10933,8 @@ Every \`sync X\` command is also exposed as \`mem sync X\` \u2014 same handlers,
10774
10933
  | limitLocation | no | "query" (default) or "body" for POST endpoints |
10775
10934
  | enrich | no | Detail endpoint config for record enrichment (actionId, pathVars, concurrency) |
10776
10935
  | transform | no | Shell command to transform records (stdin: JSON array, stdout: JSON array) |
10777
- | identityKey | no | Dot-path to cross-platform identifier (e.g. email) \u2192 stored as \`_identity\` column |
10936
+ | identityKey | no | "This record IS this entity" \u2014 dot-path to a cross-platform id (e.g. email) \u2192 \`keys[]\` (drives entity merge) |
10937
+ | identityKeys | no | "This record INVOLVES these people" \u2014 \`[{prefix, path}]\` for N participants (#128) \u2192 non-merging \`identity_keys[]\`; query with \`mem find-by-key\` |
10778
10938
  | exclude | no | Dot-path fields to strip before storing (e.g. \`["messages[].body"]\`) |
10779
10939
  | onInsert/onUpdate/onChange | no | Change hooks (shell command, "log", or flow) |
10780
10940
 
@@ -10855,14 +11015,14 @@ async function guideCommand(topic = "all") {
10855
11015
  json({ topic, title, content, availableTopics });
10856
11016
  return;
10857
11017
  }
10858
- intro(pc12.bgCyan(pc12.black(" One Guide ")));
11018
+ intro(pc13.bgCyan(pc13.black(" One Guide ")));
10859
11019
  console.log();
10860
11020
  console.log(content);
10861
- console.log(pc12.dim("\u2500".repeat(60)));
11021
+ console.log(pc13.dim("\u2500".repeat(60)));
10862
11022
  console.log(
10863
- pc12.dim("Available topics: ") + availableTopics.map((t) => pc12.cyan(t.topic)).join(", ")
11023
+ pc13.dim("Available topics: ") + availableTopics.map((t) => pc13.cyan(t.topic)).join(", ")
10864
11024
  );
10865
- console.log(pc12.dim(`Run ${pc12.cyan("one guide <topic>")} for a specific section.`));
11025
+ console.log(pc13.dim(`Run ${pc13.cyan("one guide <topic>")} for a specific section.`));
10866
11026
  }
10867
11027
 
10868
11028
  // src/lib/platform-meta.ts
@@ -11185,20 +11345,20 @@ function buildWorkflowIdeas(connections) {
11185
11345
  // src/commands/logout.ts
11186
11346
  import fs12 from "fs";
11187
11347
  import * as p9 from "@clack/prompts";
11188
- function formatWhoami(config2, apiKey, pc14) {
11348
+ function formatWhoami(config2, apiKey, pc15) {
11189
11349
  const whoami = config2.whoami;
11190
11350
  const env = getEnvFromApiKey(apiKey);
11191
- const envLabel = env === "test" ? pc14.yellow("test") : pc14.green("live");
11351
+ const envLabel = env === "test" ? pc15.yellow("test") : pc15.green("live");
11192
11352
  const lines = [];
11193
11353
  if (whoami) {
11194
11354
  const contextParts = [];
11195
11355
  if (whoami.organization) contextParts.push(whoami.organization.name);
11196
11356
  if (whoami.project) contextParts.push(whoami.project.name);
11197
11357
  const scopeDisplay = contextParts.length > 0 ? contextParts.join(" / ") : "Personal";
11198
- lines.push(`${pc14.bold(scopeDisplay)} ${pc14.dim("\xB7")} ${envLabel}`);
11199
- lines.push(`${whoami.user.name} ${pc14.dim(`(${whoami.user.email})`)}`);
11358
+ lines.push(`${pc15.bold(scopeDisplay)} ${pc15.dim("\xB7")} ${envLabel}`);
11359
+ lines.push(`${whoami.user.name} ${pc15.dim(`(${whoami.user.email})`)}`);
11200
11360
  } else {
11201
- lines.push(`${pc14.dim("Key:")} ${apiKey.slice(0, 8)}... ${pc14.dim("\xB7")} ${envLabel}`);
11361
+ lines.push(`${pc15.dim("Key:")} ${apiKey.slice(0, 8)}... ${pc15.dim("\xB7")} ${envLabel}`);
11202
11362
  }
11203
11363
  return lines;
11204
11364
  }
@@ -11227,7 +11387,7 @@ async function logoutCommand() {
11227
11387
  json({ status: cleared ? "logged_out" : "not_logged_in", message: cleared ? "Credentials cleared." : "No config found." });
11228
11388
  return;
11229
11389
  }
11230
- const pc14 = (await import("picocolors")).default;
11390
+ const pc15 = (await import("picocolors")).default;
11231
11391
  const globalConfig = readGlobalConfig();
11232
11392
  const projectConfig = readProjectConfig();
11233
11393
  const hasGlobal = globalConfig?.apiKey != null;
@@ -11236,13 +11396,13 @@ async function logoutCommand() {
11236
11396
  if (hasGlobal && hasProject) {
11237
11397
  const infoLines = ["You are logged in with multiple configs.", ""];
11238
11398
  if (projectConfig) {
11239
- infoLines.push(`${pc14.cyan("Local config:")}`);
11240
- infoLines.push(...formatWhoami(projectConfig, projectConfig.apiKey, pc14));
11399
+ infoLines.push(`${pc15.cyan("Local config:")}`);
11400
+ infoLines.push(...formatWhoami(projectConfig, projectConfig.apiKey, pc15));
11241
11401
  infoLines.push("");
11242
11402
  }
11243
11403
  if (globalConfig) {
11244
- infoLines.push(`${pc14.magenta("Global config:")}`);
11245
- infoLines.push(...formatWhoami(globalConfig, globalConfig.apiKey, pc14));
11404
+ infoLines.push(`${pc15.magenta("Global config:")}`);
11405
+ infoLines.push(...formatWhoami(globalConfig, globalConfig.apiKey, pc15));
11246
11406
  }
11247
11407
  p9.note(infoLines.join("\n"));
11248
11408
  const choice = await p9.select({
@@ -11260,14 +11420,14 @@ async function logoutCommand() {
11260
11420
  targetScope = choice;
11261
11421
  } else if (hasProject) {
11262
11422
  const infoLines = ["You are logged in.", ""];
11263
- infoLines.push(`${pc14.dim("Stored in")} ${pc14.cyan("local config")}`);
11264
- infoLines.push(...formatWhoami(projectConfig, projectConfig.apiKey, pc14));
11423
+ infoLines.push(`${pc15.dim("Stored in")} ${pc15.cyan("local config")}`);
11424
+ infoLines.push(...formatWhoami(projectConfig, projectConfig.apiKey, pc15));
11265
11425
  p9.note(infoLines.join("\n"));
11266
11426
  targetScope = "project";
11267
11427
  } else if (hasGlobal) {
11268
11428
  const infoLines = ["You are logged in.", ""];
11269
- infoLines.push(`${pc14.dim("Stored in")} ${pc14.magenta("global config")}`);
11270
- infoLines.push(...formatWhoami(globalConfig, globalConfig.apiKey, pc14));
11429
+ infoLines.push(`${pc15.dim("Stored in")} ${pc15.magenta("global config")}`);
11430
+ infoLines.push(...formatWhoami(globalConfig, globalConfig.apiKey, pc15));
11271
11431
  p9.note(infoLines.join("\n"));
11272
11432
  targetScope = "global";
11273
11433
  } else {
@@ -11304,7 +11464,7 @@ async function logoutCommand() {
11304
11464
  // src/lib/analytics.ts
11305
11465
  import { createRequire as createRequire2 } from "module";
11306
11466
  import { randomUUID, createHash } from "crypto";
11307
- import pc13 from "picocolors";
11467
+ import pc14 from "picocolors";
11308
11468
  var require3 = createRequire2(import.meta.url);
11309
11469
  var DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com";
11310
11470
  var DEFAULT_POSTHOG_KEY = "phc_a9ok4w0uxiZcVoSWOISIlin85lHMXQD3vWPaYnuRlRV";
@@ -11550,7 +11710,7 @@ function maybeShowTelemetryNotice() {
11550
11710
  if (telemetryNoticeShown()) return;
11551
11711
  markTelemetryNoticeShown();
11552
11712
  process.stderr.write(
11553
- pc13.dim(
11713
+ pc14.dim(
11554
11714
  "One CLI collects usage analytics (which commands run, linked to your One account) to improve the product.\nNo arguments, inputs, or secrets are ever collected. Opt out anytime with ONE_NO_TELEMETRY=1.\n"
11555
11715
  )
11556
11716
  );
@@ -11979,21 +12139,21 @@ program.command("whoami").description("Show the user, organization, and project
11979
12139
  });
11980
12140
  return;
11981
12141
  }
11982
- const pc14 = (await import("picocolors")).default;
12142
+ const pc15 = (await import("picocolors")).default;
11983
12143
  const contextParts = [];
11984
12144
  if (whoami.organization) contextParts.push(whoami.organization.name);
11985
12145
  if (whoami.project) contextParts.push(whoami.project.name);
11986
12146
  const scopeDisplay = contextParts.length > 0 ? contextParts.join(" / ") : "Personal";
11987
- const envLabel = env === "test" ? pc14.yellow("test") : pc14.green("live");
11988
- const configLabel = configScope === "project" ? pc14.cyan("project config") : pc14.magenta("global config");
12147
+ const envLabel = env === "test" ? pc15.yellow("test") : pc15.green("live");
12148
+ const configLabel = configScope === "project" ? pc15.cyan("project config") : pc15.magenta("global config");
11989
12149
  console.log();
11990
- console.log(` ${pc14.bold(scopeDisplay)} ${pc14.dim("\xB7")} ${envLabel}`);
11991
- console.log(` ${whoami.user.name} ${pc14.dim(`(${whoami.user.email})`)}`);
11992
- if (whoami.organization) console.log(` ${pc14.dim("Org:")} ${whoami.organization.name} ${pc14.dim(`(${whoami.organization.id})`)}`);
11993
- if (whoami.project) console.log(` ${pc14.dim("Project:")} ${whoami.project.name} ${pc14.dim(`(${whoami.project.id})`)}`);
12150
+ console.log(` ${pc15.bold(scopeDisplay)} ${pc15.dim("\xB7")} ${envLabel}`);
12151
+ console.log(` ${whoami.user.name} ${pc15.dim(`(${whoami.user.email})`)}`);
12152
+ if (whoami.organization) console.log(` ${pc15.dim("Org:")} ${whoami.organization.name} ${pc15.dim(`(${whoami.organization.id})`)}`);
12153
+ if (whoami.project) console.log(` ${pc15.dim("Project:")} ${whoami.project.name} ${pc15.dim(`(${whoami.project.id})`)}`);
11994
12154
  console.log();
11995
- console.log(` ${pc14.dim("Using")} ${configLabel}`);
11996
- console.log(` ${pc14.dim("API:")} ${apiBase}`);
12155
+ console.log(` ${pc15.dim("Using")} ${configLabel}`);
12156
+ console.log(` ${pc15.dim("API:")} ${apiBase}`);
11997
12157
  console.log();
11998
12158
  });
11999
12159
  program.command("add [platform]").description("Shortcut for: connection add").option("--tag <name>", "Tag the new connection (disambiguates multiple connections per platform in sync/flow profiles)").action(async (platform, options) => {