@withone/cli 1.50.0 → 1.51.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.
@@ -10,7 +10,7 @@ import {
10
10
  import {
11
11
  getBackend,
12
12
  upsertRecord
13
- } from "./chunk-7RV7T5PX.js";
13
+ } from "./chunk-Z4KAQGIG.js";
14
14
 
15
15
  // src/commands/mem/migrate.ts
16
16
  import fs4 from "fs";
@@ -387,6 +387,273 @@ function deleteDatabase(platform) {
387
387
  if (fs3.existsSync(dbPath + "-shm")) fs3.unlinkSync(dbPath + "-shm");
388
388
  }
389
389
 
390
+ // src/lib/memory/sync/mem-writer.ts
391
+ function resolveWildcardPath(root, path5) {
392
+ const segments = path5.split("[]");
393
+ if (segments.length === 1) {
394
+ return [getByDotPath(root, path5)];
395
+ }
396
+ const recurse = (value, idx) => {
397
+ if (value === null || value === void 0) return [];
398
+ const segment = segments[idx];
399
+ const head = segment.startsWith(".") ? segment.slice(1) : segment;
400
+ if (idx === segments.length - 1) {
401
+ if (!Array.isArray(value)) return [];
402
+ if (head === "") return value;
403
+ return value.map((el) => getByDotPath(el, head));
404
+ }
405
+ if (!Array.isArray(value)) return [];
406
+ const results = [];
407
+ for (const el of value) {
408
+ const next = head === "" ? el : getByDotPath(el, head);
409
+ results.push(...recurse(next, idx + 1));
410
+ }
411
+ return results;
412
+ };
413
+ const firstHead = segments[0];
414
+ const firstValue = firstHead === "" ? root : getByDotPath(root, firstHead);
415
+ return recurse(firstValue, 1);
416
+ }
417
+ function extractSearchableFromPaths(record, paths) {
418
+ const parts = [];
419
+ const perPath = [];
420
+ for (const path5 of paths) {
421
+ const values = resolveWildcardPath(record, path5);
422
+ const collected = [];
423
+ const absorb = (v) => {
424
+ if (v === null || v === void 0) return;
425
+ if (typeof v === "string") {
426
+ const t = v.trim();
427
+ if (t) collected.push(t);
428
+ } else if (typeof v === "number" || typeof v === "boolean") {
429
+ collected.push(String(v));
430
+ } else if (Array.isArray(v)) {
431
+ for (const inner of v) absorb(inner);
432
+ }
433
+ };
434
+ for (const v of values) absorb(v);
435
+ if (collected.length > 0) {
436
+ parts.push(...collected);
437
+ perPath.push({ path: path5, found: true, sample: collected.join(" ").slice(0, 80) });
438
+ } else {
439
+ perPath.push({ path: path5, found: false, sample: "" });
440
+ }
441
+ }
442
+ const text = parts.join(" ").replace(/\s+/g, " ").trim();
443
+ return { text, paths: perPath };
444
+ }
445
+ function getSearchablePaths(profile) {
446
+ const paths = profile.memory?.searchable;
447
+ if (!paths || !Array.isArray(paths) || paths.length === 0) return void 0;
448
+ return paths;
449
+ }
450
+ async function writePageToMemory(profile, records, opts = {}) {
451
+ const report = {
452
+ attempted: 0,
453
+ inserted: 0,
454
+ updated: 0,
455
+ skipped: 0,
456
+ preserved: 0,
457
+ sourceKeysSeen: [],
458
+ inserts: [],
459
+ updates: []
460
+ };
461
+ const type = `${profile.platform}/${profile.model}`;
462
+ const embedFlag = opts.embedOverride ?? deriveEmbedFlag(profile);
463
+ const searchablePaths = getSearchablePaths(profile);
464
+ const declaresIdentityKeys = (profile.identityKeys?.length ?? 0) > 0;
465
+ const enrichTimestampField = profile.enrich?.timestampField ?? "_enriched_at";
466
+ for (const record of records) {
467
+ report.attempted++;
468
+ const externalId = getByDotPath(record, profile.idField);
469
+ if (externalId === void 0 || externalId === null || externalId === "") {
470
+ report.skipped++;
471
+ continue;
472
+ }
473
+ if (typeof externalId === "object") {
474
+ report.skipped++;
475
+ continue;
476
+ }
477
+ const sourceKey = `${type}:${String(externalId)}`;
478
+ const alreadyEnriched = opts.alreadyEnrichedIds?.has(String(externalId)) ?? false;
479
+ if (alreadyEnriched) report.preserved++;
480
+ const keys = [sourceKey, ...collectEntityKeys(record, profile)];
481
+ const preEnrichShape = !!profile.enrich && record[enrichTimestampField] == null;
482
+ const identityKeys = !declaresIdentityKeys || preEnrichShape ? void 0 : collectAssociationKeys(record, profile);
483
+ const data = { ...record };
484
+ for (const k of Object.keys(data)) {
485
+ if (k.startsWith("_")) delete data[k];
486
+ }
487
+ const searchable_text = searchablePaths ? extractSearchableFromPaths(record, searchablePaths).text : void 0;
488
+ try {
489
+ const res = await upsertRecord(
490
+ {
491
+ type,
492
+ data,
493
+ keys,
494
+ identity_keys: identityKeys,
495
+ searchable_text,
496
+ sources: {
497
+ [sourceKey]: {
498
+ last_synced_at: (/* @__PURE__ */ new Date()).toISOString(),
499
+ metadata: { source: profile.platform }
500
+ }
501
+ },
502
+ tags: ["synced", profile.platform],
503
+ embed: embedFlag
504
+ },
505
+ // Sync must REPLACE: if a field vanished at the source (phone
506
+ // number removed, Gmail thread archived) it must also vanish
507
+ // from memory. The default merge behaviour is correct for
508
+ // interactive `mem add` / `mem update`, wrong for sync.
509
+ //
510
+ // ...unless this row is already enriched, in which case we are the
511
+ // thin half of the record and must not speak for the whole of it.
512
+ // See the "NEVER DEGRADE" comment above.
513
+ alreadyEnriched ? { embed: embedFlag, replace: false, preserveDerived: true } : { embed: embedFlag, replace: true }
514
+ );
515
+ report.sourceKeysSeen.push(sourceKey);
516
+ if (res.action === "inserted") {
517
+ report.inserted++;
518
+ if (opts.capturePerAction) report.inserts.push(record);
519
+ } else {
520
+ report.updated++;
521
+ if (opts.capturePerAction) report.updates.push(record);
522
+ }
523
+ } catch {
524
+ report.skipped++;
525
+ }
526
+ }
527
+ return report;
528
+ }
529
+ function deriveEmbedFlag(profile) {
530
+ if (profile.memory && typeof profile.memory.embed === "boolean") return profile.memory.embed;
531
+ return false;
532
+ }
533
+ function deriveIdentityPrefix(dotPath) {
534
+ const lower = dotPath.toLowerCase();
535
+ if (lower.includes("email")) return "email";
536
+ if (lower.includes("phone")) return "phone";
537
+ if (lower.includes("domain")) return "domain";
538
+ return "id";
539
+ }
540
+ var EMAIL_RE = /[A-Za-z0-9._%+'-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
541
+ function identityValuesFor(prefix, raw, opts = {}) {
542
+ if (raw === null || raw === void 0 || typeof raw === "object") return [];
543
+ const s = String(raw);
544
+ if (prefix === "email") {
545
+ const found = (s.match(EMAIL_RE) ?? []).map((e) => e.toLowerCase());
546
+ if (found.length > 0 || !opts.fallbackToScalar) return found;
547
+ }
548
+ const v = s.toLowerCase().trim();
549
+ return v ? [v] : [];
550
+ }
551
+ function tokenizeIdentityPath(path5) {
552
+ const tokens = [];
553
+ const re = /([^.[\]]+)|\[([^\]]*)\]/g;
554
+ let m;
555
+ while ((m = re.exec(path5)) !== null) {
556
+ if (m[1] !== void 0) {
557
+ tokens.push({ type: "field", name: m[1] });
558
+ } else {
559
+ const inner = m[2];
560
+ if (inner === "") tokens.push({ type: "wild" });
561
+ else if (/^\d+$/.test(inner)) tokens.push({ type: "index", i: Number(inner) });
562
+ else {
563
+ const eq = inner.indexOf("=");
564
+ if (eq > 0) {
565
+ const field = inner.slice(0, eq).trim();
566
+ const value = inner.slice(eq + 1).trim().replace(/^['"]|['"]$/g, "");
567
+ tokens.push({ type: "filter", field, value });
568
+ }
569
+ }
570
+ }
571
+ }
572
+ return tokens;
573
+ }
574
+ function resolveIdentityPath(root, path5) {
575
+ let frontier = [root];
576
+ for (const tok of tokenizeIdentityPath(path5)) {
577
+ const next = [];
578
+ for (const cur of frontier) {
579
+ if (cur === null || cur === void 0) continue;
580
+ if (tok.type === "field") {
581
+ if (Array.isArray(cur)) {
582
+ if (/^\d+$/.test(tok.name)) next.push(cur[Number(tok.name)]);
583
+ } else if (typeof cur === "object") {
584
+ next.push(cur[tok.name]);
585
+ }
586
+ } else if (tok.type === "index") {
587
+ if (Array.isArray(cur)) next.push(cur[tok.i]);
588
+ } else if (tok.type === "wild") {
589
+ if (Array.isArray(cur)) next.push(...cur);
590
+ } else {
591
+ if (Array.isArray(cur)) {
592
+ for (const el of cur) {
593
+ if (el && typeof el === "object" && String(el[tok.field] ?? "").toLowerCase() === tok.value.toLowerCase()) {
594
+ next.push(el);
595
+ }
596
+ }
597
+ }
598
+ }
599
+ }
600
+ frontier = next;
601
+ }
602
+ return frontier;
603
+ }
604
+ function keysForEntry(record, prefix, path5) {
605
+ if (!prefix || !path5) return [];
606
+ const out = [];
607
+ for (const raw of resolveIdentityPath(record, path5)) {
608
+ for (const value of identityValuesFor(prefix, raw)) out.push(`${prefix}:${value}`);
609
+ }
610
+ return out;
611
+ }
612
+ function normalizePrefix(prefix) {
613
+ const p2 = prefix.trim().toLowerCase();
614
+ return /^[a-z0-9_]+$/.test(p2) ? p2 : null;
615
+ }
616
+ function dedupe(keys) {
617
+ const seen = /* @__PURE__ */ new Set();
618
+ const out = [];
619
+ for (const k of keys) {
620
+ if (seen.has(k)) continue;
621
+ seen.add(k);
622
+ out.push(k);
623
+ }
624
+ return out;
625
+ }
626
+ function resolveEntityIdentity(record, identityKey) {
627
+ if (!identityKey) return null;
628
+ const prefix = deriveIdentityPrefix(identityKey);
629
+ const values = dedupe(
630
+ resolveIdentityPath(record, identityKey).flatMap((raw) => identityValuesFor(prefix, raw, { fallbackToScalar: true }))
631
+ );
632
+ const value = values.length === 1 ? values[0] : null;
633
+ return { prefix, values, value, key: value ? `${prefix}:${value}` : null };
634
+ }
635
+ function normalizeEntityIdentityValue(identityKey, raw) {
636
+ const values = dedupe(identityValuesFor(deriveIdentityPrefix(identityKey), raw, { fallbackToScalar: true }));
637
+ return values.length === 1 ? values[0] : null;
638
+ }
639
+ function collectEntityKeys(record, profile) {
640
+ const identity = resolveEntityIdentity(record, profile.identityKey);
641
+ return identity?.key ? [identity.key] : [];
642
+ }
643
+ function collectAssociationKeys(record, profile) {
644
+ const out = [];
645
+ for (const entry of profile.identityKeys ?? []) {
646
+ if (!entry || !entry.path || !entry.prefix) continue;
647
+ const prefix = normalizePrefix(entry.prefix);
648
+ if (!prefix) continue;
649
+ out.push(...keysForEntry(record, prefix, entry.path));
650
+ }
651
+ return dedupe(out);
652
+ }
653
+ function collectIdentityKeys(record, profile) {
654
+ return dedupe([...collectEntityKeys(record, profile), ...collectAssociationKeys(record, profile)]);
655
+ }
656
+
390
657
  // src/commands/mem/migrate.ts
391
658
  async function memMigrateCommand(flags) {
392
659
  requireMemoryInit();
@@ -472,10 +739,10 @@ async function memMigrateCommand(flags) {
472
739
  const keys = [sourceKey];
473
740
  let identityValueNorm = null;
474
741
  if (identityKey) {
475
- const idValue = getByDotPath(hydrated, identityKey);
476
- if (idValue !== void 0 && idValue !== null && idValue !== "" && typeof idValue !== "object") {
477
- identityValueNorm = String(idValue).toLowerCase().trim();
478
- keys.push(`${deriveIdentityPrefix(identityKey)}:${identityValueNorm}`);
742
+ const identity = resolveEntityIdentity(hydrated, identityKey);
743
+ if (identity?.key) {
744
+ identityValueNorm = identity.value;
745
+ keys.push(identity.key);
479
746
  }
480
747
  }
481
748
  let mergeTarget = false;
@@ -658,7 +925,7 @@ async function buildIdentityMap(backend, type, identityKey) {
658
925
  for (const row of res.rows) {
659
926
  const ident = row.ident;
660
927
  if (ident === null || ident === void 0 || typeof ident !== "string") continue;
661
- const norm = ident.toLowerCase().trim();
928
+ const norm = normalizeEntityIdentityValue(identityKey, ident);
662
929
  if (!norm) continue;
663
930
  if (!map.has(norm)) {
664
931
  map.set(norm, {
@@ -724,13 +991,6 @@ function resolveBuiltinProfilePath(platform, model) {
724
991
  const candidate = path4.join(dir, platform, `${model}.json`);
725
992
  return fs4.existsSync(candidate) ? candidate : null;
726
993
  }
727
- function deriveIdentityPrefix(path5) {
728
- const lower = path5.toLowerCase();
729
- if (lower.includes("email")) return "email";
730
- if (lower.includes("phone")) return "phone";
731
- if (lower.includes("domain")) return "domain";
732
- return "id";
733
- }
734
994
 
735
995
  export {
736
996
  readProfile,
@@ -753,6 +1013,11 @@ export {
753
1013
  dropTable,
754
1014
  countRecords,
755
1015
  deleteDatabase,
1016
+ extractSearchableFromPaths,
1017
+ getSearchablePaths,
1018
+ writePageToMemory,
1019
+ resolveEntityIdentity,
1020
+ collectIdentityKeys,
756
1021
  loadBuiltinProfile,
757
1022
  listBuiltinProfiles,
758
1023
  memMigrateCommand,