@withone/cli 1.48.0 → 1.49.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/README.md CHANGED
@@ -329,9 +329,18 @@ One ships a local memory store (a real Postgres process bootstrapped on demand v
329
329
  ```bash
330
330
  # User memories — works immediately on a new install
331
331
  one mem add note '{"content":"Design review is Thursday"}' --tags work --weight 7
332
+ one mem update <id> '{"status":"done"}' # merges into data; refreshes searchable_text
332
333
  one mem search "design review" # hybrid FTS + semantic (if key set)
333
334
  one mem list note --limit 20
334
335
 
336
+ # Identity keys are a first-class column (unique across ACTIVE records; archiving
337
+ # frees them). Manage them after creation with `mem key` — NOT via `mem update`.
338
+ one mem key <id> --add email:x@y.com # also --remove / --set <csv>
339
+
340
+ # Backfill searchable_text for rows that landed NULL or with UUID-noise-heavy text
341
+ # (drops ids/timestamps, leads with name/title/email). No embedding provider needed.
342
+ one mem reindex --searchable --type attio/attioPeople
343
+
335
344
  # Listing synced platform rows — type is positional and namespaced as <platform>/<model>;
336
345
  # there is NO --platform flag and NO platform column in the schema.
337
346
  one mem list "gmail/threads"
@@ -3,10 +3,10 @@ import {
3
3
  isAgentMode,
4
4
  json,
5
5
  requireMemoryInit
6
- } from "./chunk-M326Y5X6.js";
6
+ } from "./chunk-CLJFSFFM.js";
7
7
  import {
8
8
  getBackend
9
- } from "./chunk-BHAEEALR.js";
9
+ } from "./chunk-7RV7T5PX.js";
10
10
 
11
11
  // src/commands/mem/sql.ts
12
12
  async function memSqlCommand(sql) {
@@ -5,13 +5,13 @@ import {
5
5
  getMemoryConfig,
6
6
  getMemoryConfigOrDefault,
7
7
  updateMemoryConfig
8
- } from "./chunk-BV2NYIA7.js";
8
+ } from "./chunk-C7DWZ7B5.js";
9
9
  import {
10
10
  getOpenAiApiKey
11
11
  } from "./chunk-SO323PZP.js";
12
12
 
13
13
  // src/lib/memory/schema.ts
14
- var SCHEMA_VERSION = "2.1.0";
14
+ var SCHEMA_VERSION = "2.2.0";
15
15
  var EXTENSIONS_SQL = ``;
16
16
  var VECTOR_EXTENSION_SQL = `
17
17
  CREATE EXTENSION IF NOT EXISTS vector;
@@ -105,19 +105,31 @@ BEGIN
105
105
  END $$;
106
106
  `;
107
107
  var FUNCTIONS_SQL = `
108
- -- Enforce global uniqueness of the "keys" array across records.
108
+ -- Enforce uniqueness of the "keys" array across ACTIVE records only.
109
+ --
110
+ -- Scoping to active is deliberate: an archived record must not squat on a
111
+ -- key forever. If it did, re-adding the same identity (e.g. a person who
112
+ -- was archived and re-surfaced) would hard-fail with 23505 and the key
113
+ -- could never be reclaimed. So:
114
+ -- - Only ACTIVE incoming rows are checked (an archived NEW row can hold
115
+ -- any keys, including ones a live record also holds \u2014 provenance).
116
+ -- - Conflicts are only raised against other ACTIVE records.
117
+ -- Array-overlap uniqueness can't be expressed as a partial UNIQUE index
118
+ -- (btree can't index "arrays overlap"), so the invariant lives here in a
119
+ -- trigger. find-by-source, key lookups, and mem_upsert_by_keys all
120
+ -- prefer active rows to match this scoping (see below).
109
121
  CREATE OR REPLACE FUNCTION mem_enforce_key_uniqueness()
110
122
  RETURNS TRIGGER LANGUAGE plpgsql AS $$
111
123
  DECLARE
112
124
  conflicting_id UUID;
113
125
  BEGIN
114
- IF NEW.keys IS NULL THEN RETURN NEW; END IF;
126
+ IF NEW.keys IS NULL OR NEW.status <> 'active' THEN RETURN NEW; END IF;
115
127
  SELECT id INTO conflicting_id
116
128
  FROM mem_records
117
- WHERE keys && NEW.keys AND id != NEW.id
129
+ WHERE keys && NEW.keys AND id != NEW.id AND status = 'active'
118
130
  LIMIT 1;
119
131
  IF conflicting_id IS NOT NULL THEN
120
- RAISE EXCEPTION 'Key conflict: one or more keys in % already exist on record %',
132
+ RAISE EXCEPTION 'Key conflict: one or more keys in % already exist on active record %',
121
133
  NEW.keys, conflicting_id USING ERRCODE = 'unique_violation';
122
134
  END IF;
123
135
  RETURN NEW;
@@ -224,14 +236,15 @@ DECLARE
224
236
  BEGIN
225
237
  -- Deterministic pick when more than one row's keys[] overlap p_keys
226
238
  -- (e.g. the incoming row identity-merges with an old Attio-id row AND
227
- -- a newer email-keyed row from a separate sync). Newest-updated wins;
228
- -- ties broken by id. Without ORDER BY, PG returns whichever row the
229
- -- planner happens to surface first, leaving the loser persisted with
230
- -- overlapping keys and breaking the keys-are-unique invariant.
239
+ -- a newer email-keyed row from a separate sync). Active rows win over
240
+ -- archived (key uniqueness is scoped to active, so an archived dupe may
241
+ -- coexist \u2014 never merge into it when a live owner exists); then newest-
242
+ -- updated; ties broken by id. Without ORDER BY, PG returns whichever
243
+ -- row the planner surfaces first, breaking the keys-are-unique invariant.
231
244
  SELECT r.id INTO existing_id
232
245
  FROM mem_records r
233
246
  WHERE r.keys && p_keys
234
- ORDER BY r.updated_at DESC NULLS LAST, r.id ASC
247
+ ORDER BY (r.status = 'active') DESC, r.updated_at DESC NULLS LAST, r.id ASC
235
248
  LIMIT 1;
236
249
 
237
250
  IF existing_id IS NOT NULL THEN
@@ -308,12 +321,12 @@ DECLARE
308
321
  BEGIN
309
322
  v_embedding := CASE WHEN p_embedding IS NOT NULL THEN p_embedding::vector ELSE NULL END;
310
323
 
311
- -- See no-vector variant for ORDER BY rationale (deterministic pick
312
- -- when multiple rows' keys overlap p_keys).
324
+ -- See no-vector variant for ORDER BY rationale (active-preferred,
325
+ -- deterministic pick when multiple rows' keys overlap p_keys).
313
326
  SELECT r.id INTO existing_id
314
327
  FROM mem_records r
315
328
  WHERE r.keys && p_keys
316
- ORDER BY r.updated_at DESC NULLS LAST, r.id ASC
329
+ ORDER BY (r.status = 'active') DESC, r.updated_at DESC NULLS LAST, r.id ASC
317
330
  LIMIT 1;
318
331
 
319
332
  IF existing_id IS NOT NULL THEN
@@ -478,6 +491,13 @@ function hotColumnIndexName(type, jsonPath) {
478
491
 
479
492
  // src/lib/memory/plugins/postgres-core/backend.ts
480
493
  var SCHEMA_LOCK_ID = 7193095520723763200n.toString();
494
+ function isUniqueViolation(err) {
495
+ if (!err || typeof err !== "object") return false;
496
+ const e = err;
497
+ if (e.code === "23505") return true;
498
+ const msg = typeof e.message === "string" ? e.message : "";
499
+ return /23505|unique_violation|Key conflict/i.test(msg);
500
+ }
481
501
  function toRecord(row) {
482
502
  return {
483
503
  id: row.id,
@@ -512,6 +532,10 @@ var CoreBackend = class {
512
532
  }
513
533
  async ensureSchema() {
514
534
  const caps = this.caps;
535
+ try {
536
+ if (await this.getSchemaVersion() === SCHEMA_VERSION) return;
537
+ } catch {
538
+ }
515
539
  await this.client.transaction(async (tx) => {
516
540
  try {
517
541
  await tx.query(`SELECT pg_advisory_xact_lock(${SCHEMA_LOCK_ID}::bigint)`);
@@ -623,6 +647,9 @@ var CoreBackend = class {
623
647
  const searchable = patch.searchable_text ?? existing.searchable_text ?? null;
624
648
  const hash = patch.content_hash ?? existing.content_hash ?? null;
625
649
  const weight = patch.weight ?? existing.weight;
650
+ const textChanged = searchable !== (existing.searchable_text ?? null);
651
+ const clearEmbedding = textChanged && this.caps.vectorSearch;
652
+ const embeddingReset = clearEmbedding ? `, embedding = NULL, embedded_at = NULL, embedding_model = NULL` : "";
626
653
  const res = await this.client.query(
627
654
  `UPDATE mem_records
628
655
  SET data = $2::jsonb,
@@ -631,13 +658,46 @@ var CoreBackend = class {
631
658
  sources = $5::jsonb,
632
659
  searchable_text = $6,
633
660
  content_hash = $7,
634
- weight = $8
661
+ weight = $8${embeddingReset}
635
662
  WHERE id = $1
636
663
  RETURNING *`,
637
664
  [id, JSON.stringify(data), tags, keys, JSON.stringify(sources), searchable, hash, weight]
638
665
  );
639
666
  return res.rows[0] ? toRecord(res.rows[0]) : null;
640
667
  }
668
+ async updateKeys(id, keys) {
669
+ try {
670
+ return await this.client.transaction(async (tx) => {
671
+ const exists = await tx.query(
672
+ `SELECT id FROM mem_records WHERE id = $1`,
673
+ [id]
674
+ );
675
+ if (!exists.rows[0]) return { status: "not_found" };
676
+ const conflict = await tx.query(
677
+ `SELECT r.id, r.type,
678
+ (SELECT k FROM unnest($2::text[]) AS k WHERE k = ANY(r.keys) LIMIT 1) AS key
679
+ FROM mem_records r
680
+ WHERE r.keys && $2::text[] AND r.id != $1 AND r.status = 'active'
681
+ LIMIT 1`,
682
+ [id, keys]
683
+ );
684
+ const c = conflict.rows[0];
685
+ if (c) {
686
+ return { status: "conflict", key: c.key, recordId: c.id, recordType: c.type };
687
+ }
688
+ const res = await tx.query(
689
+ `UPDATE mem_records SET keys = $2::text[] WHERE id = $1 RETURNING *`,
690
+ [id, keys.length > 0 ? keys : null]
691
+ );
692
+ return { status: "ok", record: toRecord(res.rows[0]) };
693
+ });
694
+ } catch (err) {
695
+ if (isUniqueViolation(err)) {
696
+ return { status: "conflict", key: keys[0] ?? "", recordId: "unknown", recordType: "unknown" };
697
+ }
698
+ throw err;
699
+ }
700
+ }
641
701
  async remove(id) {
642
702
  const res = await this.client.query(`DELETE FROM mem_records WHERE id = $1`, [id]);
643
703
  return (res.rowCount ?? 0) > 0;
@@ -651,12 +711,30 @@ var CoreBackend = class {
651
711
  return (res.rowCount ?? 0) > 0;
652
712
  }
653
713
  async unarchive(id) {
654
- const res = await this.client.query(
655
- `UPDATE mem_records SET status = 'active', archived_reason = NULL
656
- WHERE id = $1 AND status = 'archived'`,
657
- [id]
658
- );
659
- return (res.rowCount ?? 0) > 0;
714
+ try {
715
+ const res = await this.client.query(
716
+ `UPDATE mem_records SET status = 'active', archived_reason = NULL
717
+ WHERE id = $1 AND status = 'archived'`,
718
+ [id]
719
+ );
720
+ return (res.rowCount ?? 0) > 0;
721
+ } catch (err) {
722
+ if (!isUniqueViolation(err)) throw err;
723
+ const owner = await this.client.query(
724
+ `SELECT r.id, r.type,
725
+ (SELECT k FROM unnest(a.keys) AS k
726
+ WHERE k = ANY(r.keys) LIMIT 1) AS key
727
+ FROM mem_records a
728
+ JOIN mem_records r
729
+ ON r.keys && a.keys AND r.id != a.id AND r.status = 'active'
730
+ WHERE a.id = $1
731
+ LIMIT 1`,
732
+ [id]
733
+ );
734
+ const o = owner.rows[0];
735
+ const detail = o ? `key "${o.key}" was reclaimed by active record ${o.id} (type ${o.type}) \u2014 re-key it or archive that record first` : `one of its keys was reclaimed by another active record`;
736
+ throw new Error(`Cannot unarchive ${id}: ${detail}`);
737
+ }
660
738
  }
661
739
  async list(type, opts = {}) {
662
740
  const limit = opts.limit ?? 100;
@@ -733,6 +811,48 @@ var CoreBackend = class {
733
811
  [id, literal, model]
734
812
  );
735
813
  }
814
+ async listForSearchableBackfill(opts = {}) {
815
+ const limit = opts.limit ?? 1e3;
816
+ const offset = opts.offset ?? 0;
817
+ const params = [];
818
+ const clauses = [`status = 'active'`];
819
+ if (opts.type) {
820
+ params.push(opts.type);
821
+ clauses.push(`type = $${params.length}`);
822
+ }
823
+ if (opts.onlyNull) {
824
+ clauses.push(`(searchable_text IS NULL OR searchable_text = '')`);
825
+ }
826
+ params.push(limit);
827
+ params.push(offset);
828
+ const res = await this.client.query(
829
+ `SELECT id, type, data, searchable_text
830
+ FROM mem_records
831
+ WHERE ${clauses.join(" AND ")}
832
+ ORDER BY id ASC
833
+ LIMIT $${params.length - 1} OFFSET $${params.length}`,
834
+ params
835
+ );
836
+ return res.rows;
837
+ }
838
+ async updateSearchableText(id, text) {
839
+ if (this.caps.vectorSearch) {
840
+ await this.client.query(
841
+ `UPDATE mem_records
842
+ SET searchable_text = $2,
843
+ embedding = NULL,
844
+ embedded_at = NULL,
845
+ embedding_model = NULL
846
+ WHERE id = $1`,
847
+ [id, text]
848
+ );
849
+ return;
850
+ }
851
+ await this.client.query(
852
+ `UPDATE mem_records SET searchable_text = $2 WHERE id = $1`,
853
+ [id, text]
854
+ );
855
+ }
736
856
  async listKeysByType(type) {
737
857
  const res = await this.client.query(
738
858
  `SELECT id, keys FROM mem_records WHERE type = $1 AND status = 'active'`,
@@ -871,7 +991,10 @@ var CoreBackend = class {
871
991
  }
872
992
  async findBySource(sourceKey) {
873
993
  const res = await this.client.query(
874
- `SELECT * FROM mem_records WHERE $1 = ANY(keys) LIMIT 1`,
994
+ `SELECT * FROM mem_records
995
+ WHERE $1 = ANY(keys)
996
+ ORDER BY (status = 'active') DESC, updated_at DESC
997
+ LIMIT 1`,
875
998
  [sourceKey]
876
999
  );
877
1000
  return res.rows[0] ? toRecord(res.rows[0]) : null;
@@ -1105,6 +1228,9 @@ var LazyPostgresBackend = class {
1105
1228
  async update(...a) {
1106
1229
  return (await this.ensure()).update(...a);
1107
1230
  }
1231
+ async updateKeys(...a) {
1232
+ return (await this.ensure()).updateKeys(...a);
1233
+ }
1108
1234
  async remove(...a) {
1109
1235
  return (await this.ensure()).remove(...a);
1110
1236
  }
@@ -1126,9 +1252,15 @@ var LazyPostgresBackend = class {
1126
1252
  async listKeysByType(...a) {
1127
1253
  return (await this.ensure()).listKeysByType(...a);
1128
1254
  }
1255
+ async listForSearchableBackfill(...a) {
1256
+ return (await this.ensure()).listForSearchableBackfill(...a);
1257
+ }
1129
1258
  async updateEmbedding(...a) {
1130
1259
  return (await this.ensure()).updateEmbedding(...a);
1131
1260
  }
1261
+ async updateSearchableText(...a) {
1262
+ return (await this.ensure()).updateSearchableText(...a);
1263
+ }
1132
1264
  async raw(sql, params) {
1133
1265
  const b = await this.ensure();
1134
1266
  if (!b.raw) throw new Error("Backend does not support raw SQL");
@@ -1451,6 +1583,9 @@ var LazyEmbeddedPostgresBackend = class {
1451
1583
  async update(...a) {
1452
1584
  return (await this.ensure()).update(...a);
1453
1585
  }
1586
+ async updateKeys(...a) {
1587
+ return (await this.ensure()).updateKeys(...a);
1588
+ }
1454
1589
  async remove(...a) {
1455
1590
  return (await this.ensure()).remove(...a);
1456
1591
  }
@@ -1472,9 +1607,15 @@ var LazyEmbeddedPostgresBackend = class {
1472
1607
  async listKeysByType(...a) {
1473
1608
  return (await this.ensure()).listKeysByType(...a);
1474
1609
  }
1610
+ async listForSearchableBackfill(...a) {
1611
+ return (await this.ensure()).listForSearchableBackfill(...a);
1612
+ }
1475
1613
  async updateEmbedding(...a) {
1476
1614
  return (await this.ensure()).updateEmbedding(...a);
1477
1615
  }
1616
+ async updateSearchableText(...a) {
1617
+ return (await this.ensure()).updateSearchableText(...a);
1618
+ }
1478
1619
  async raw(sql, params) {
1479
1620
  const b = await this.ensure();
1480
1621
  if (!b.raw) throw new Error("Backend does not support raw SQL");
@@ -1671,6 +1812,21 @@ async function addRecord(input, opts = {}) {
1671
1812
  };
1672
1813
  return backend.insert(prepared);
1673
1814
  }
1815
+ async function updateRecord(id, patch) {
1816
+ const backend = await getBackend();
1817
+ const existing = await backend.getById(id);
1818
+ if (!existing) return null;
1819
+ const mergedData = patch.data ? { ...existing.data, ...patch.data } : existing.data;
1820
+ const dataChanged = patch.data !== void 0;
1821
+ const searchable_text = patch.searchable_text ?? (dataChanged ? defaultSearchableText(mergedData) : existing.searchable_text ?? void 0);
1822
+ const content_hash = patch.content_hash ?? (dataChanged ? contentHash(mergedData) : existing.content_hash ?? void 0);
1823
+ return backend.update(id, {
1824
+ ...patch,
1825
+ data: mergedData,
1826
+ searchable_text,
1827
+ content_hash
1828
+ });
1829
+ }
1674
1830
  async function upsertRecord(input, opts = {}) {
1675
1831
  const backend = await getBackend();
1676
1832
  const { searchable_text, content_hash, embedding, embedding_model } = await prepareRecord(input, opts, "sync");
@@ -1706,5 +1862,6 @@ export {
1706
1862
  resetBackendSingleton,
1707
1863
  closeBackendIfCached,
1708
1864
  addRecord,
1865
+ updateRecord,
1709
1866
  upsertRecord
1710
1867
  };
@@ -172,29 +172,78 @@ async function embedBatch(texts, opts = {}) {
172
172
  function sleep(ms) {
173
173
  return new Promise((resolve) => setTimeout(resolve, ms));
174
174
  }
175
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
176
+ var TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/;
177
+ var NOISE_KEY_RE = /(^|_)(id|ids|uuid|guid|url|href|link|avatar|photo|icon|hash|token|slug)$/i;
178
+ var NOISE_KEY_EXACT = /* @__PURE__ */ new Set([
179
+ "attribute_type",
180
+ "actor_type",
181
+ "created_by_actor",
182
+ "active_from",
183
+ "active_until",
184
+ "created_at",
185
+ "updated_at",
186
+ "last_synced_at"
187
+ ]);
188
+ var CONTAINER_KEYS = /* @__PURE__ */ new Set([
189
+ "values",
190
+ "value",
191
+ "data",
192
+ "attributes",
193
+ "properties",
194
+ "items",
195
+ "records",
196
+ "result",
197
+ "results",
198
+ "fields"
199
+ ]);
200
+ var PRIORITY_KEY_RE = /(^|_)(name|full_name|first_name|last_name|display_name|title|job_title|role|position|email|email_address|company|organization|org|description|bio|summary|headline|label|subject|content|body|text_content)$/i;
175
201
  function defaultSearchableText(data, maxLen = 4e3) {
176
- const parts = [];
177
- const walk = (value, depth = 0) => {
202
+ const priority = [];
203
+ const normal = [];
204
+ const push = (key, text) => {
205
+ if (key && PRIORITY_KEY_RE.test(key)) priority.push(text);
206
+ else normal.push(text);
207
+ };
208
+ const walk = (value, key, depth) => {
178
209
  if (value === null || value === void 0) return;
179
- if (typeof value === "string" && value.trim()) {
180
- parts.push(value.trim());
210
+ if (typeof value === "string") {
211
+ const t = value.trim();
212
+ if (!t) return;
213
+ if (UUID_RE.test(t) || TIMESTAMP_RE.test(t)) return;
214
+ if (key && NOISE_KEY_RE.test(key)) return;
215
+ push(key, t);
181
216
  return;
182
217
  }
183
218
  if (typeof value === "number" || typeof value === "boolean") {
184
- parts.push(String(value));
219
+ if (key && NOISE_KEY_RE.test(key)) return;
220
+ push(key, String(value));
185
221
  return;
186
222
  }
187
- if (depth > 4) return;
223
+ if (depth > 6) return;
188
224
  if (Array.isArray(value)) {
189
- for (const v of value) walk(v, depth + 1);
225
+ for (const v of value) walk(v, key, depth + 1);
190
226
  return;
191
227
  }
192
228
  if (typeof value === "object") {
193
- for (const v of Object.values(value)) walk(v, depth + 1);
229
+ for (const [k, v] of Object.entries(value)) {
230
+ const lower = k.toLowerCase();
231
+ if (NOISE_KEY_EXACT.has(lower) || NOISE_KEY_RE.test(k)) continue;
232
+ const nextKey = CONTAINER_KEYS.has(lower) ? key : k;
233
+ walk(v, nextKey, depth + 1);
234
+ }
194
235
  }
195
236
  };
196
- walk(data);
197
- const joined = parts.join(" ").replace(/\s+/g, " ").trim();
237
+ walk(data, void 0, 0);
238
+ const seen = /* @__PURE__ */ new Set();
239
+ const ordered = [];
240
+ for (const t of [...priority, ...normal]) {
241
+ const norm = t.toLowerCase();
242
+ if (seen.has(norm)) continue;
243
+ seen.add(norm);
244
+ ordered.push(t);
245
+ }
246
+ const joined = ordered.join(" ").replace(/\s+/g, " ").trim();
198
247
  return joined.length > maxLen ? joined.slice(0, maxLen) : joined;
199
248
  }
200
249
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getMemoryConfigOrDefault
3
- } from "./chunk-BV2NYIA7.js";
3
+ } from "./chunk-C7DWZ7B5.js";
4
4
  import {
5
5
  getOpenAiApiKey,
6
6
  readConfig
@@ -6,11 +6,11 @@ import {
6
6
  note,
7
7
  okJson,
8
8
  requireMemoryInit
9
- } from "./chunk-M326Y5X6.js";
9
+ } from "./chunk-CLJFSFFM.js";
10
10
  import {
11
11
  getBackend,
12
12
  upsertRecord
13
- } from "./chunk-BHAEEALR.js";
13
+ } from "./chunk-7RV7T5PX.js";
14
14
 
15
15
  // src/commands/mem/migrate.ts
16
16
  import fs4 from "fs";
@@ -2,7 +2,7 @@ import {
2
2
  defaultSearchableText,
3
3
  embed,
4
4
  embedBatch
5
- } from "./chunk-BV2NYIA7.js";
5
+ } from "./chunk-C7DWZ7B5.js";
6
6
  import "./chunk-SO323PZP.js";
7
7
  export {
8
8
  defaultSearchableText,
package/dist/index.js CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  } from "./chunk-2TWFL3CS.js";
35
35
  import {
36
36
  memSqlCommand
37
- } from "./chunk-H2PP5XEQ.js";
37
+ } from "./chunk-4WWK3GO5.js";
38
38
  import {
39
39
  countRecords,
40
40
  deleteDatabase,
@@ -59,7 +59,7 @@ import {
59
59
  upsertRecords,
60
60
  writeDraftProfile,
61
61
  writeProfile
62
- } from "./chunk-QE6Z676D.js";
62
+ } from "./chunk-GXOIR3GF.js";
63
63
  import {
64
64
  getByDotPath
65
65
  } from "./chunk-44CV5IMX.js";
@@ -83,7 +83,7 @@ import {
83
83
  semanticSearchUpgradeLine,
84
84
  setAgentMode,
85
85
  silenceWarningsInAgentMode
86
- } from "./chunk-M326Y5X6.js";
86
+ } from "./chunk-CLJFSFFM.js";
87
87
  import {
88
88
  SCHEMA_VERSION,
89
89
  addRecord,
@@ -92,8 +92,9 @@ import {
92
92
  getBackendPlugin,
93
93
  listBackendPlugins,
94
94
  loadBackendFromConfig,
95
+ updateRecord,
95
96
  upsertRecord
96
- } from "./chunk-BHAEEALR.js";
97
+ } from "./chunk-7RV7T5PX.js";
97
98
  import {
98
99
  DEFAULT_MEMORY_CONFIG,
99
100
  defaultSearchableText,
@@ -103,7 +104,7 @@ import {
103
104
  memoryConfigExists,
104
105
  setOpenAiApiKey,
105
106
  updateMemoryConfig
106
- } from "./chunk-BV2NYIA7.js";
107
+ } from "./chunk-C7DWZ7B5.js";
107
108
  import {
108
109
  appendAnalyticsQueue,
109
110
  appendUsageLog,
@@ -6062,7 +6063,7 @@ async function syncModel(api, profile, options) {
6062
6063
  updateModelState(platform, model, { status: "failed", pagesProcessed, lastCursor }),
6063
6064
  (async () => {
6064
6065
  try {
6065
- const { getBackend: getBackend2 } = await import("./runtime-V4PXJJUV.js");
6066
+ const { getBackend: getBackend2 } = await import("./runtime-2PRVL2NO.js");
6066
6067
  const backend = await getBackend2();
6067
6068
  await Promise.race([
6068
6069
  backend.close(),
@@ -6417,7 +6418,7 @@ async function syncModel(api, profile, options) {
6417
6418
  db.exec(`DROP TABLE IF EXISTS _seen_ids`);
6418
6419
  }
6419
6420
  if (options.toMemory !== false) {
6420
- const backend = await (await import("./runtime-V4PXJJUV.js")).getBackend();
6421
+ const backend = await (await import("./runtime-2PRVL2NO.js")).getBackend();
6421
6422
  const type = `${platform}/${model}`;
6422
6423
  const existing = await backend.listKeysByType(type);
6423
6424
  const sourcePrefix = `${type}:`;
@@ -6497,7 +6498,7 @@ async function syncModel(api, profile, options) {
6497
6498
  let statusCounts;
6498
6499
  if (options.toMemory !== false) {
6499
6500
  try {
6500
- const backend = await (await import("./runtime-V4PXJJUV.js")).getBackend();
6501
+ const backend = await (await import("./runtime-2PRVL2NO.js")).getBackend();
6501
6502
  const typeName = `${platform}/${model}`;
6502
6503
  const [active, archived] = await Promise.all([
6503
6504
  backend.count(typeName, { status: "active" }),
@@ -8130,7 +8131,7 @@ ${result.total} results`);
8130
8131
  }
8131
8132
  }
8132
8133
  async function syncSqlCommand(platformModel, sql) {
8133
- const { syncSqlCommand: runSyncSql } = await import("./sql-CBZQHP4R.js");
8134
+ const { syncSqlCommand: runSyncSql } = await import("./sql-HG7R2JML.js");
8134
8135
  await runSyncSql(platformModel, sql);
8135
8136
  }
8136
8137
  async function syncDeleteCommand(platformModel, options) {
@@ -8208,7 +8209,7 @@ async function syncDeleteCommand(platformModel, options) {
8208
8209
  async function maybeAutoMigrateLegacy(platform, models) {
8209
8210
  const dbSize = getDatabaseSize(platform);
8210
8211
  if (!dbSize || dbSize === "0 B") return;
8211
- const { getBackend: getBackend2 } = await import("./runtime-V4PXJJUV.js");
8212
+ const { getBackend: getBackend2 } = await import("./runtime-2PRVL2NO.js");
8212
8213
  const backend = await getBackend2();
8213
8214
  let memoryHasData = false;
8214
8215
  for (const model of models) {
@@ -8224,7 +8225,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
8224
8225
  ` detected legacy .one/sync/data/${platform}.db (${dbSize}) \u2014 auto-migrating into memory before sync.
8225
8226
  `
8226
8227
  );
8227
- const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-CO4LQVY6.js");
8228
+ const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-TRUZVAT5.js");
8228
8229
  await memMigrateCommand3({ platform, yes: true });
8229
8230
  return;
8230
8231
  }
@@ -8233,7 +8234,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
8233
8234
  initialValue: true
8234
8235
  });
8235
8236
  if (p7.isCancel(shouldMigrate) || !shouldMigrate) return;
8236
- const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-CO4LQVY6.js");
8237
+ const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-TRUZVAT5.js");
8237
8238
  await memMigrateCommand2({ platform, yes: true });
8238
8239
  }
8239
8240
  async function syncSuggestSearchableCommand(platformModel, options = {}) {
@@ -8294,7 +8295,7 @@ async function syncSuggestSearchableCommand(platformModel, options = {}) {
8294
8295
  async function syncListCommand(platform) {
8295
8296
  const profiles = listProfiles(platform);
8296
8297
  const state = await readSyncState();
8297
- const { getBackend: getBackend2 } = await import("./runtime-V4PXJJUV.js");
8298
+ const { getBackend: getBackend2 } = await import("./runtime-2PRVL2NO.js");
8298
8299
  const backend = await getBackend2();
8299
8300
  const syncs = await Promise.all(profiles.map(async (p10) => {
8300
8301
  const modelState = state[p10.platform]?.[p10.model];
@@ -8569,7 +8570,7 @@ function registerSyncSubcommands(sync) {
8569
8570
  await syncSqlCommand(platformModel, sql);
8570
8571
  });
8571
8572
  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) => {
8572
- const { syncSchemaCommand } = await import("./schema-UC4LBV5V.js");
8573
+ const { syncSchemaCommand } = await import("./schema-LCRWSFD5.js");
8573
8574
  await syncSchemaCommand(platformModel);
8574
8575
  });
8575
8576
  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) => {
@@ -9015,11 +9016,41 @@ async function memGetCommand(id, flags) {
9015
9016
  async function memUpdateCommand(id, patchRaw) {
9016
9017
  requireMemoryInit();
9017
9018
  const patch = parseJsonArg(patchRaw, "patch");
9018
- const backend = await getBackend();
9019
- const updated = await backend.update(id, { type: "", data: patch });
9019
+ if (patch && typeof patch === "object" && !Array.isArray(patch) && "keys" in patch) {
9020
+ error("`keys` is not a data field. Use `one mem key <id> --set <csv>` (or --add/--remove) to change a record's keys.");
9021
+ }
9022
+ const updated = await updateRecord(id, { data: patch });
9020
9023
  if (!updated) error(`Record ${id} not found`);
9021
9024
  printRecord(updated);
9022
9025
  }
9026
+ async function memKeyCommand(id, flags) {
9027
+ requireMemoryInit();
9028
+ const backend = await getBackend();
9029
+ const setList = parseCsv(flags.set);
9030
+ const addList = parseCsv(flags.add) ?? [];
9031
+ const removeList = parseCsv(flags.remove) ?? [];
9032
+ if (!setList && addList.length === 0 && removeList.length === 0) {
9033
+ error("Pass at least one of --add <csv>, --remove <csv>, or --set <csv>.");
9034
+ }
9035
+ const existing = await backend.getById(id);
9036
+ if (!existing) error(`Record ${id} not found`);
9037
+ let next;
9038
+ if (setList) {
9039
+ next = setList;
9040
+ } else {
9041
+ const removeSet = new Set(removeList);
9042
+ next = [...existing.keys ?? [], ...addList].filter((k) => !removeSet.has(k));
9043
+ }
9044
+ next = [...new Set(next)];
9045
+ const outcome = await backend.updateKeys(id, next);
9046
+ if (outcome.status === "not_found") error(`Record ${id} not found`);
9047
+ if (outcome.status === "conflict") {
9048
+ error(
9049
+ `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.`
9050
+ );
9051
+ }
9052
+ printRecord(outcome.record);
9053
+ }
9023
9054
  async function memArchiveCommand(id, flags) {
9024
9055
  requireMemoryInit();
9025
9056
  const backend = await getBackend();
@@ -9216,7 +9247,7 @@ async function memDoctorCommand() {
9216
9247
  }
9217
9248
  if (cfg.embedding.provider === "openai") {
9218
9249
  try {
9219
- const { embed: embed2 } = await import("./embedding-K2CFCLXI.js");
9250
+ const { embed: embed2 } = await import("./embedding-2YB6CGBA.js");
9220
9251
  const result = await embed2("connectivity check");
9221
9252
  checks.push({
9222
9253
  name: "OpenAI embedding provider reachable",
@@ -9375,6 +9406,9 @@ async function memVacuumCommand() {
9375
9406
  }
9376
9407
  async function memReindexCommand(flags) {
9377
9408
  requireMemoryInit();
9409
+ if (flags.searchable) {
9410
+ return memReindexSearchableCommand(flags);
9411
+ }
9378
9412
  const backend = await getBackend();
9379
9413
  const cfg = getMemoryConfigOrDefault();
9380
9414
  if (cfg.embedding.provider !== "openai") {
@@ -9437,6 +9471,58 @@ async function memReindexCommand(flags) {
9437
9471
  force: !!flags.force
9438
9472
  });
9439
9473
  }
9474
+ async function memReindexSearchableCommand(flags) {
9475
+ const backend = await getBackend();
9476
+ const onlyNull = !flags.all;
9477
+ const totalCap = flags.limit ? parsePositiveInt(flags.limit, 1e6, "limit") : 1e6;
9478
+ const PAGE = 500;
9479
+ let considered = 0;
9480
+ let updated = 0;
9481
+ let unchanged = 0;
9482
+ let emptied = 0;
9483
+ let offset = 0;
9484
+ while (considered < totalCap) {
9485
+ const pageSize = Math.min(PAGE, totalCap - considered);
9486
+ const rows = await backend.listForSearchableBackfill({
9487
+ type: flags.type,
9488
+ onlyNull,
9489
+ limit: pageSize,
9490
+ offset
9491
+ });
9492
+ if (rows.length === 0) break;
9493
+ considered += rows.length;
9494
+ let updatedThisPage = 0;
9495
+ for (const r of rows) {
9496
+ const next = defaultSearchableText(r.data);
9497
+ if (!next) {
9498
+ emptied++;
9499
+ continue;
9500
+ }
9501
+ if (next === (r.searchable_text ?? "")) {
9502
+ unchanged++;
9503
+ continue;
9504
+ }
9505
+ await backend.updateSearchableText(r.id, next);
9506
+ updated++;
9507
+ updatedThisPage++;
9508
+ }
9509
+ offset += onlyNull ? rows.length - updatedThisPage : rows.length;
9510
+ if (!isAgentMode()) {
9511
+ process.stderr.write(` searchable: updated ${updated} / considered ${considered}\r`);
9512
+ }
9513
+ }
9514
+ if (!isAgentMode()) process.stderr.write("\n");
9515
+ okJson({
9516
+ status: "ok",
9517
+ mode: "searchable",
9518
+ type: flags.type ?? null,
9519
+ scope: onlyNull ? "null_only" : "all",
9520
+ considered,
9521
+ updated,
9522
+ unchanged,
9523
+ emptied
9524
+ });
9525
+ }
9440
9526
 
9441
9527
  // src/commands/mem.ts
9442
9528
  function memStatusCommand() {
@@ -9475,11 +9561,12 @@ function registerMemoryCommands(program2) {
9475
9561
  );
9476
9562
  mem.command("doctor").description("Diagnose schema, indexes, plugin resolution, connectivity, embeddings").action(memDoctorCommand);
9477
9563
  mem.command("vacuum").description("Run backend maintenance (VACUUM ANALYZE on tables)").action(memVacuumCommand);
9478
- mem.command("reindex").description("Backfill embeddings: re-embed records missing an embedding or under a different model").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)").action(memReindexCommand);
9564
+ 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);
9479
9565
  mem.command("sql <query>").description("Run a read-only SELECT / WITH / EXPLAIN against the memory store").action(memSqlCommand);
9480
9566
  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);
9481
9567
  mem.command("get <id>").description("Get a record by id").option("--links", "Include outgoing and incoming links", false).action(memGetCommand);
9482
- mem.command("update <id> <patch>").description("Update a record (patch is JSON merged into data)").action(memUpdateCommand);
9568
+ mem.command("update <id> <patch>").description("Update a record (patch is JSON merged into data; regenerates searchable_text)").action(memUpdateCommand);
9569
+ 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));
9483
9570
  mem.command("archive <id>").description("Archive a record").option("--reason <text>", "Why it was archived (user_archived | deleted_upstream | superseded | \u2026)").action(memArchiveCommand);
9484
9571
  mem.command("weight <id> <n>").description("Set record relevance weight (1-10)").action(memWeightCommand);
9485
9572
  mem.command("flush <id>").description("Reset access count for a record").action(memFlushCommand);
@@ -10122,10 +10209,19 @@ one --agent mem add note '{"content":"..."}' --tags work,urgent --weight 8 --key
10122
10209
  # Get (optionally with links)
10123
10210
  one --agent mem get <id> --links
10124
10211
 
10125
- # Update (shallow merge into data)
10212
+ # Update (shallow merge into data; regenerates searchable_text from the merged data)
10126
10213
  one --agent mem update <id> '{"status":"done"}'
10127
10214
 
10128
- # Archive / unarchive
10215
+ # Manage identity keys AFTER creation (keys are a first-class column, NOT data).
10216
+ # Unique across ACTIVE records \u2014 a clear error names the record that owns a taken key.
10217
+ one --agent mem key <id> --add email:new@x.com
10218
+ one --agent mem key <id> --remove email:old@x.com
10219
+ one --agent mem key <id> --set 'email:a@x.com,phone:+1555' # replace all keys
10220
+ # NOTE: passing keys to 'mem update' is rejected \u2014 keys are not a data field.
10221
+
10222
+ # Archive / unarchive. Archiving FREES a record's keys: an archived record no
10223
+ # longer blocks a new active record from reusing the same key (uniqueness is
10224
+ # scoped to active records).
10129
10225
  one --agent mem archive <id> --reason superseded
10130
10226
 
10131
10227
  # List by type \u2014 type is positional. Synced rows are namespaced as <platform>/<model>.
@@ -10191,8 +10287,12 @@ one --agent mem status # backend, provider, _upgrade hint
10191
10287
  one --agent mem doctor # 7-check health report
10192
10288
  one --agent mem vacuum # backend maintenance (VACUUM ANALYZE)
10193
10289
  one --agent mem reindex # re-embed records under current model
10290
+ one --agent mem reindex --searchable # backfill searchable_text where NULL
10291
+ one --agent mem reindex --searchable --type attio/attioPeople --all # also rewrite stale text
10194
10292
  \`\`\`
10195
10293
 
10294
+ \`mem reindex --searchable\` regenerates \`searchable_text\` from each record's own \`data\` (no embedding provider needed). It fixes rows that landed with NULL searchable_text and, thanks to the cleaned-up text builder, drops UUID/timestamp noise and leads with name/title/email fields so FTS ranks on what humans search. Regenerated rows are re-queued for embedding on the next \`mem reindex\`.
10295
+
10196
10296
  ## Admin
10197
10297
 
10198
10298
  \`\`\`bash
@@ -3,11 +3,11 @@ import {
3
3
  dotPathToJsonbExpr,
4
4
  memMigrateCommand,
5
5
  reviveStringifiedJson
6
- } from "./chunk-QE6Z676D.js";
6
+ } from "./chunk-GXOIR3GF.js";
7
7
  import "./chunk-44CV5IMX.js";
8
- import "./chunk-M326Y5X6.js";
9
- import "./chunk-BHAEEALR.js";
10
- import "./chunk-BV2NYIA7.js";
8
+ import "./chunk-CLJFSFFM.js";
9
+ import "./chunk-7RV7T5PX.js";
10
+ import "./chunk-C7DWZ7B5.js";
11
11
  import "./chunk-SO323PZP.js";
12
12
  export {
13
13
  buildIdentityMap,
@@ -3,14 +3,16 @@ import {
3
3
  closeBackendIfCached,
4
4
  getBackend,
5
5
  resetBackendSingleton,
6
+ updateRecord,
6
7
  upsertRecord
7
- } from "./chunk-BHAEEALR.js";
8
- import "./chunk-BV2NYIA7.js";
8
+ } from "./chunk-7RV7T5PX.js";
9
+ import "./chunk-C7DWZ7B5.js";
9
10
  import "./chunk-SO323PZP.js";
10
11
  export {
11
12
  addRecord,
12
13
  closeBackendIfCached,
13
14
  getBackend,
14
15
  resetBackendSingleton,
16
+ updateRecord,
15
17
  upsertRecord
16
18
  };
@@ -3,11 +3,11 @@ import {
3
3
  note,
4
4
  okJson,
5
5
  requireMemoryInit
6
- } from "./chunk-M326Y5X6.js";
6
+ } from "./chunk-CLJFSFFM.js";
7
7
  import {
8
8
  getBackend
9
- } from "./chunk-BHAEEALR.js";
10
- import "./chunk-BV2NYIA7.js";
9
+ } from "./chunk-7RV7T5PX.js";
10
+ import "./chunk-C7DWZ7B5.js";
11
11
  import "./chunk-SO323PZP.js";
12
12
 
13
13
  // src/lib/memory/sync/schema.ts
@@ -0,0 +1,12 @@
1
+ import {
2
+ memSqlCommand,
3
+ syncSqlCommand
4
+ } from "./chunk-4WWK3GO5.js";
5
+ import "./chunk-CLJFSFFM.js";
6
+ import "./chunk-7RV7T5PX.js";
7
+ import "./chunk-C7DWZ7B5.js";
8
+ import "./chunk-SO323PZP.js";
9
+ export {
10
+ memSqlCommand,
11
+ syncSqlCommand
12
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/cli",
3
- "version": "1.48.0",
3
+ "version": "1.49.0",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [
@@ -170,10 +170,19 @@ Underneath, the store has no `platform` column — `type` is the only platform-s
170
170
  ```bash
171
171
  # User memories
172
172
  one --agent mem add note '{"content":"..."}' --tags work --weight 7
173
+ one --agent mem update <id> '{"status":"done"}' # merges into data; refreshes searchable_text
173
174
  one --agent mem search "deadline" # hybrid if key set, else FTS
174
175
  one --agent mem list note --limit 20
175
176
  one --agent mem link <from-id> <to-id> relates_to --bi
176
177
 
178
+ # Identity keys (first-class column, NOT data). Unique across ACTIVE records;
179
+ # archiving a record frees its keys. `mem update '{"keys":[...]}'` is rejected.
180
+ one --agent mem key <id> --add email:x@y.com # add/--remove/--set
181
+ one --agent mem find-by-source email:x@y.com # prefers the active owner
182
+
183
+ # Backfill searchable_text (no embedding provider needed) — fixes NULL/noisy text
184
+ one --agent mem reindex --searchable --type attio/attioPeople
185
+
177
186
  # Status + diagnostics
178
187
  one --agent mem status # backend, provider, _upgrade hint
179
188
  one --agent mem doctor # full health report
@@ -1,12 +0,0 @@
1
- import {
2
- memSqlCommand,
3
- syncSqlCommand
4
- } from "./chunk-H2PP5XEQ.js";
5
- import "./chunk-M326Y5X6.js";
6
- import "./chunk-BHAEEALR.js";
7
- import "./chunk-BV2NYIA7.js";
8
- import "./chunk-SO323PZP.js";
9
- export {
10
- memSqlCommand,
11
- syncSqlCommand
12
- };