@withone/cli 1.48.0 → 1.50.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
@@ -179,7 +179,7 @@ Opens your browser, you authorize, done. The CLI polls until the connection is l
179
179
 
180
180
  ### `one list`
181
181
 
182
- List your active connections with their status and connection keys.
182
+ List your active connections with their status, connection keys, and what your access config lets you run on each.
183
183
 
184
184
  ```bash
185
185
  one list
@@ -193,6 +193,37 @@ one list
193
193
 
194
194
  You need the connection key (rightmost column) when executing actions.
195
195
 
196
+ **Access reporting.** Every connection carries an `access` field describing what the current [access control](#one-config) config permits — so an agent knows its reach up front instead of discovering it as a failure mid-workflow. Three policies:
197
+
198
+ | Policy | Means | Shown when |
199
+ |--------|-------|-----------|
200
+ | `{"policy": "full"}` | Every action on the connection | Default (admin, no action allowlist) |
201
+ | `{"policy": "methods", "methods": ["GET"]}` | Only actions with those HTTP methods | Permission level is `read` or `write` |
202
+ | `{"policy": "actions", "actions": [{"actionId": "...", "title": "...", "method": "..."}]}` | Only these specific actions | An action allowlist is configured |
203
+
204
+ An action allowlist wins over the permission level (and is then method-filtered by it). When the policy is `actions`, those actions are exactly what may run — no `actions search` needed.
205
+
206
+ ```jsonc
207
+ // one --agent list
208
+ {
209
+ "total": 2,
210
+ "showing": 2,
211
+ "connections": [
212
+ {
213
+ "platform": "gmail",
214
+ "state": "operational",
215
+ "key": "live::gmail::default::abc123",
216
+ "access": { "policy": "methods", "methods": ["GET"] }
217
+ }
218
+ ],
219
+ "accessHint": "Permission level \"read\": only GET actions will execute."
220
+ }
221
+ ```
222
+
223
+ `knowledgeOnly: true` appears when knowledge-only mode is on (execution disabled), and `unresolvedActionIds` lists any allowlisted action ids that could not be looked up. In human output, an `Access` column and a summary note appear only when access is actually scoped.
224
+
225
+ This mirrors the `access` field on the One MCP server's `list_one_integrations` tool, so both surfaces report access the same way.
226
+
196
227
  ### `one connection delete <connection-key>`
197
228
 
198
229
  Remove a connection by its key.
@@ -329,9 +360,18 @@ One ships a local memory store (a real Postgres process bootstrapped on demand v
329
360
  ```bash
330
361
  # User memories — works immediately on a new install
331
362
  one mem add note '{"content":"Design review is Thursday"}' --tags work --weight 7
363
+ one mem update <id> '{"status":"done"}' # merges into data; refreshes searchable_text
332
364
  one mem search "design review" # hybrid FTS + semantic (if key set)
333
365
  one mem list note --limit 20
334
366
 
367
+ # Identity keys are a first-class column (unique across ACTIVE records; archiving
368
+ # frees them). Manage them after creation with `mem key` — NOT via `mem update`.
369
+ one mem key <id> --add email:x@y.com # also --remove / --set <csv>
370
+
371
+ # Backfill searchable_text for rows that landed NULL or with UUID-noise-heavy text
372
+ # (drops ids/timestamps, leads with name/title/email). No embedding provider needed.
373
+ one mem reindex --searchable --type attio/attioPeople
374
+
335
375
  # Listing synced platform rows — type is positional and namespaced as <platform>/<model>;
336
376
  # there is NO --platform flag and NO platform column in the schema.
337
377
  one mem list "gmail/threads"
@@ -567,6 +607,8 @@ one config
567
607
 
568
608
  Settings propagate automatically to all installed agent configs.
569
609
 
610
+ Run [`one list`](#one-list) to see the effect: each connection reports an `access` field describing exactly what these settings let you run on it.
611
+
570
612
  #### `one config skills status` / `one config skills sync`
571
613
 
572
614
  `one init` copies the packaged skill files (`SKILL.md`, `references/`) into `~/.agents/skills/one/` and symlinks per-agent paths to that canonical directory. When the CLI self-updates, the skill files in the canonical dir would normally stay frozen at the version that was installed. To prevent stale docs, every CLI command checks a `.one-cli-version` marker in the canonical dir and silently refreshes the skill files if they don't match the running CLI version. No user action required.
@@ -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
@@ -2319,7 +2319,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
2319
2319
  if (flowStack.includes(resolvedKey)) {
2320
2320
  throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
2321
2321
  }
2322
- const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-IBB42ED2.js");
2322
+ const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-YDKAYGZW.js");
2323
2323
  const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
2324
2324
  const subContext = await executeFlow(
2325
2325
  subFlow,
@@ -3238,11 +3238,11 @@ export {
3238
3238
  ApiError,
3239
3239
  OneApi,
3240
3240
  TimeoutError,
3241
+ PERMISSION_METHODS,
3241
3242
  filterByPermissions,
3242
3243
  isMethodAllowed,
3243
3244
  isActionAllowed,
3244
3245
  buildActionKnowledgeWithGuidance,
3245
- validateActionInput,
3246
3246
  knowledgeCachePath,
3247
3247
  searchCachePath,
3248
3248
  readCache,
@@ -3256,6 +3256,7 @@ export {
3256
3256
  clearEntry,
3257
3257
  makeCacheEntry,
3258
3258
  resolveActionDetails,
3259
+ validateActionInput,
3259
3260
  FLOW_SCHEMA,
3260
3261
  getStepTypeDescriptor,
3261
3262
  getNestedStepsKeys,
@@ -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,
@@ -12,7 +12,7 @@ import {
12
12
  stripStepsAlias,
13
13
  summarizeFlowInputs,
14
14
  walkSteps
15
- } from "./chunk-2TWFL3CS.js";
15
+ } from "./chunk-D6W756DN.js";
16
16
  import "./chunk-44CV5IMX.js";
17
17
  import "./chunk-SO323PZP.js";
18
18
  export {
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  FLOW_SCHEMA,
4
4
  FlowRunner,
5
5
  OneApi,
6
+ PERMISSION_METHODS,
6
7
  TimeoutError,
7
8
  buildActionKnowledgeWithGuidance,
8
9
  buildCacheMeta,
@@ -31,10 +32,10 @@ import {
31
32
  validateActionInput,
32
33
  walkSteps,
33
34
  writeCache
34
- } from "./chunk-2TWFL3CS.js";
35
+ } from "./chunk-D6W756DN.js";
35
36
  import {
36
37
  memSqlCommand
37
- } from "./chunk-H2PP5XEQ.js";
38
+ } from "./chunk-4WWK3GO5.js";
38
39
  import {
39
40
  countRecords,
40
41
  deleteDatabase,
@@ -59,7 +60,7 @@ import {
59
60
  upsertRecords,
60
61
  writeDraftProfile,
61
62
  writeProfile
62
- } from "./chunk-QE6Z676D.js";
63
+ } from "./chunk-GXOIR3GF.js";
63
64
  import {
64
65
  getByDotPath
65
66
  } from "./chunk-44CV5IMX.js";
@@ -83,7 +84,7 @@ import {
83
84
  semanticSearchUpgradeLine,
84
85
  setAgentMode,
85
86
  silenceWarningsInAgentMode
86
- } from "./chunk-M326Y5X6.js";
87
+ } from "./chunk-CLJFSFFM.js";
87
88
  import {
88
89
  SCHEMA_VERSION,
89
90
  addRecord,
@@ -92,8 +93,9 @@ import {
92
93
  getBackendPlugin,
93
94
  listBackendPlugins,
94
95
  loadBackendFromConfig,
96
+ updateRecord,
95
97
  upsertRecord
96
- } from "./chunk-BHAEEALR.js";
98
+ } from "./chunk-7RV7T5PX.js";
97
99
  import {
98
100
  DEFAULT_MEMORY_CONFIG,
99
101
  defaultSearchableText,
@@ -103,7 +105,7 @@ import {
103
105
  memoryConfigExists,
104
106
  setOpenAiApiKey,
105
107
  updateMemoryConfig
106
- } from "./chunk-BV2NYIA7.js";
108
+ } from "./chunk-C7DWZ7B5.js";
107
109
  import {
108
110
  appendAnalyticsQueue,
109
111
  appendUsageLog,
@@ -1941,6 +1943,54 @@ function maskApiKey(key) {
1941
1943
  import * as p4 from "@clack/prompts";
1942
1944
  import pc4 from "picocolors";
1943
1945
 
1946
+ // src/lib/access.ts
1947
+ async function resolveAllowedActions(api, actionIds) {
1948
+ if (actionIds.includes("*")) return [];
1949
+ const resolved = await Promise.all(
1950
+ actionIds.map(async (actionId) => {
1951
+ try {
1952
+ const { details } = await resolveActionDetails(api, actionId, { warn: () => {
1953
+ } });
1954
+ if (!details.connectionPlatform) return null;
1955
+ return {
1956
+ actionId,
1957
+ title: details.title,
1958
+ method: details.method,
1959
+ platform: details.connectionPlatform
1960
+ };
1961
+ } catch {
1962
+ return null;
1963
+ }
1964
+ })
1965
+ );
1966
+ return resolved.filter((a) => a !== null);
1967
+ }
1968
+ function computeConnectionAccess(platform, permissions, allowedActionIds, grantedActions) {
1969
+ if (!allowedActionIds.includes("*")) {
1970
+ const actions2 = grantedActions.filter((a) => a.platform === platform).map(({ actionId, title, method }) => ({ actionId, title, method }));
1971
+ return { policy: "actions", actions: actions2 };
1972
+ }
1973
+ const methods = PERMISSION_METHODS[permissions];
1974
+ if (methods !== null) {
1975
+ return { policy: "methods", methods };
1976
+ }
1977
+ return { policy: "full" };
1978
+ }
1979
+ function formatAccess(access, maxActions = 2) {
1980
+ switch (access.policy) {
1981
+ case "full":
1982
+ return "full";
1983
+ case "methods":
1984
+ return access.methods.join(", ");
1985
+ case "actions": {
1986
+ if (access.actions.length === 0) return "none";
1987
+ const shown = access.actions.slice(0, maxActions).map((a) => `${a.title} (${a.method})`);
1988
+ const rest = access.actions.length - shown.length;
1989
+ return rest > 0 ? `${shown.join(", ")} +${rest} more` : shown.join(", ");
1990
+ }
1991
+ }
1992
+ }
1993
+
1944
1994
  // src/lib/platforms.ts
1945
1995
  function findPlatform(platforms, query) {
1946
1996
  const normalizedQuery = query.toLowerCase().trim();
@@ -2154,8 +2204,16 @@ async function connectionListCommand(options) {
2154
2204
  try {
2155
2205
  const allConnections = await api.listConnections();
2156
2206
  const ac = getAccessControlFromAllSources();
2207
+ const permissions = ac.permissions || "admin";
2157
2208
  const allowedKeys = ac.connectionKeys || ["*"];
2209
+ const actionIds = ac.actionIds || ["*"];
2210
+ const knowledgeOnly = ac.knowledgeAgent || false;
2158
2211
  const accessFiltered = allowedKeys.includes("*") ? allConnections : allConnections.filter((conn) => allowedKeys.includes(conn.key));
2212
+ const resolvedActions = await resolveAllowedActions(api, actionIds);
2213
+ const unresolvedActionIds = actionIds.includes("*") ? [] : actionIds.filter((id) => !resolvedActions.some((a) => a.actionId === id));
2214
+ const grantedActions = resolvedActions.filter((a) => isMethodAllowed(a.method, permissions));
2215
+ const accessFor = (platform) => computeConnectionAccess(platform, permissions, actionIds, grantedActions);
2216
+ const hintText = accessHint(permissions, actionIds, knowledgeOnly);
2159
2217
  const searchQuery = options?.search?.toLowerCase();
2160
2218
  const filtered = searchQuery ? accessFiltered.filter((conn) => conn.platform.toLowerCase().includes(searchQuery)) : accessFiltered;
2161
2219
  const limitArg = options?.limit ? parseInt(options.limit, 10) : void 0;
@@ -2171,8 +2229,12 @@ async function connectionListCommand(options) {
2171
2229
  state: conn.state,
2172
2230
  key: conn.key,
2173
2231
  ...conn.name && { name: conn.name },
2174
- ...conn.tags?.length && { tags: conn.tags }
2232
+ ...conn.tags?.length && { tags: conn.tags },
2233
+ access: accessFor(conn.platform)
2175
2234
  })),
2235
+ ...knowledgeOnly && { knowledgeOnly: true },
2236
+ ...unresolvedActionIds.length > 0 && { unresolvedActionIds },
2237
+ ...hintText && { accessHint: hintText },
2176
2238
  ...limited.length < filtered.length && {
2177
2239
  hint: `Showing ${limited.length} of ${filtered.length} connections. Use --search <query> to filter by platform or --limit <n> to see more.`
2178
2240
  }
@@ -2205,20 +2267,34 @@ Add one with: ${pc4.cyan("one connection add gmail")}`,
2205
2267
  platform: conn.platform,
2206
2268
  state: conn.state,
2207
2269
  key: conn.key,
2208
- tags: conn.tags?.length ? conn.tags.join(", ") : ""
2270
+ tags: conn.tags?.length ? conn.tags.join(", ") : "",
2271
+ access: formatAccess(accessFor(conn.platform))
2209
2272
  }));
2210
2273
  const hasTags = rows.some((r) => r.tags);
2274
+ const hasScopedAccess = rows.some((r) => r.access !== "full");
2211
2275
  printTable(
2212
2276
  [
2213
2277
  { key: "status", label: "" },
2214
2278
  { key: "platform", label: "Platform" },
2215
2279
  { key: "state", label: "Status" },
2216
2280
  { key: "key", label: "Connection Key", color: pc4.dim },
2217
- ...hasTags ? [{ key: "tags", label: "Tags", color: pc4.dim }] : []
2281
+ ...hasTags ? [{ key: "tags", label: "Tags", color: pc4.dim }] : [],
2282
+ ...hasScopedAccess ? [{ key: "access", label: "Access", color: pc4.yellow }] : []
2218
2283
  ],
2219
2284
  rows
2220
2285
  );
2221
2286
  console.log();
2287
+ if (hintText) {
2288
+ const lines = [hintText];
2289
+ if (unresolvedActionIds.length > 0) {
2290
+ lines.push(
2291
+ `Could not resolve ${unresolvedActionIds.length} allowlisted action id(s): ${unresolvedActionIds.join(", ")}`
2292
+ );
2293
+ }
2294
+ p4.note(`${wrapText(lines.join("\n"))}
2295
+
2296
+ Change it with: ${pc4.cyan("one config")}`, "Access");
2297
+ }
2222
2298
  if (displayed.length < filtered.length) {
2223
2299
  p4.note(
2224
2300
  `Showing ${displayed.length} of ${filtered.length}. Increase --limit or run without it to see all.`,
@@ -2291,6 +2367,37 @@ async function connectionDeleteCommand(connectionKey, options) {
2291
2367
  error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
2292
2368
  }
2293
2369
  }
2370
+ function accessHint(permissions, actionIds, knowledgeOnly) {
2371
+ const parts = [];
2372
+ if (!actionIds.includes("*")) {
2373
+ parts.push(
2374
+ "Action-scoped: each connection's `access.actions` are the only actions you may run \u2014 use them directly, no `actions search` needed."
2375
+ );
2376
+ } else if (permissions !== "admin") {
2377
+ const methods = PERMISSION_METHODS[permissions]?.join(", ") ?? "";
2378
+ parts.push(`Permission level "${permissions}": only ${methods} actions will execute.`);
2379
+ }
2380
+ if (knowledgeOnly) {
2381
+ parts.push("Knowledge-only mode: `actions execute` is disabled \u2014 read knowledge and write code instead.");
2382
+ }
2383
+ return parts.length > 0 ? parts.join(" ") : null;
2384
+ }
2385
+ function wrapText(text5, width = 72) {
2386
+ return text5.split("\n").map((paragraph) => {
2387
+ const lines = [];
2388
+ let line = "";
2389
+ for (const word of paragraph.split(/\s+/)) {
2390
+ if (line && line.length + 1 + word.length > width) {
2391
+ lines.push(line);
2392
+ line = word;
2393
+ } else {
2394
+ line = line ? `${line} ${word}` : word;
2395
+ }
2396
+ }
2397
+ if (line) lines.push(line);
2398
+ return lines.join("\n");
2399
+ }).join("\n");
2400
+ }
2294
2401
  function getStatusIndicator(state) {
2295
2402
  switch (state) {
2296
2403
  case "operational":
@@ -6062,7 +6169,7 @@ async function syncModel(api, profile, options) {
6062
6169
  updateModelState(platform, model, { status: "failed", pagesProcessed, lastCursor }),
6063
6170
  (async () => {
6064
6171
  try {
6065
- const { getBackend: getBackend2 } = await import("./runtime-V4PXJJUV.js");
6172
+ const { getBackend: getBackend2 } = await import("./runtime-2PRVL2NO.js");
6066
6173
  const backend = await getBackend2();
6067
6174
  await Promise.race([
6068
6175
  backend.close(),
@@ -6417,7 +6524,7 @@ async function syncModel(api, profile, options) {
6417
6524
  db.exec(`DROP TABLE IF EXISTS _seen_ids`);
6418
6525
  }
6419
6526
  if (options.toMemory !== false) {
6420
- const backend = await (await import("./runtime-V4PXJJUV.js")).getBackend();
6527
+ const backend = await (await import("./runtime-2PRVL2NO.js")).getBackend();
6421
6528
  const type = `${platform}/${model}`;
6422
6529
  const existing = await backend.listKeysByType(type);
6423
6530
  const sourcePrefix = `${type}:`;
@@ -6497,7 +6604,7 @@ async function syncModel(api, profile, options) {
6497
6604
  let statusCounts;
6498
6605
  if (options.toMemory !== false) {
6499
6606
  try {
6500
- const backend = await (await import("./runtime-V4PXJJUV.js")).getBackend();
6607
+ const backend = await (await import("./runtime-2PRVL2NO.js")).getBackend();
6501
6608
  const typeName = `${platform}/${model}`;
6502
6609
  const [active, archived] = await Promise.all([
6503
6610
  backend.count(typeName, { status: "active" }),
@@ -8130,7 +8237,7 @@ ${result.total} results`);
8130
8237
  }
8131
8238
  }
8132
8239
  async function syncSqlCommand(platformModel, sql) {
8133
- const { syncSqlCommand: runSyncSql } = await import("./sql-CBZQHP4R.js");
8240
+ const { syncSqlCommand: runSyncSql } = await import("./sql-HG7R2JML.js");
8134
8241
  await runSyncSql(platformModel, sql);
8135
8242
  }
8136
8243
  async function syncDeleteCommand(platformModel, options) {
@@ -8208,7 +8315,7 @@ async function syncDeleteCommand(platformModel, options) {
8208
8315
  async function maybeAutoMigrateLegacy(platform, models) {
8209
8316
  const dbSize = getDatabaseSize(platform);
8210
8317
  if (!dbSize || dbSize === "0 B") return;
8211
- const { getBackend: getBackend2 } = await import("./runtime-V4PXJJUV.js");
8318
+ const { getBackend: getBackend2 } = await import("./runtime-2PRVL2NO.js");
8212
8319
  const backend = await getBackend2();
8213
8320
  let memoryHasData = false;
8214
8321
  for (const model of models) {
@@ -8224,7 +8331,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
8224
8331
  ` detected legacy .one/sync/data/${platform}.db (${dbSize}) \u2014 auto-migrating into memory before sync.
8225
8332
  `
8226
8333
  );
8227
- const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-CO4LQVY6.js");
8334
+ const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-TRUZVAT5.js");
8228
8335
  await memMigrateCommand3({ platform, yes: true });
8229
8336
  return;
8230
8337
  }
@@ -8233,7 +8340,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
8233
8340
  initialValue: true
8234
8341
  });
8235
8342
  if (p7.isCancel(shouldMigrate) || !shouldMigrate) return;
8236
- const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-CO4LQVY6.js");
8343
+ const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-TRUZVAT5.js");
8237
8344
  await memMigrateCommand2({ platform, yes: true });
8238
8345
  }
8239
8346
  async function syncSuggestSearchableCommand(platformModel, options = {}) {
@@ -8294,7 +8401,7 @@ async function syncSuggestSearchableCommand(platformModel, options = {}) {
8294
8401
  async function syncListCommand(platform) {
8295
8402
  const profiles = listProfiles(platform);
8296
8403
  const state = await readSyncState();
8297
- const { getBackend: getBackend2 } = await import("./runtime-V4PXJJUV.js");
8404
+ const { getBackend: getBackend2 } = await import("./runtime-2PRVL2NO.js");
8298
8405
  const backend = await getBackend2();
8299
8406
  const syncs = await Promise.all(profiles.map(async (p10) => {
8300
8407
  const modelState = state[p10.platform]?.[p10.model];
@@ -8569,7 +8676,7 @@ function registerSyncSubcommands(sync) {
8569
8676
  await syncSqlCommand(platformModel, sql);
8570
8677
  });
8571
8678
  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");
8679
+ const { syncSchemaCommand } = await import("./schema-LCRWSFD5.js");
8573
8680
  await syncSchemaCommand(platformModel);
8574
8681
  });
8575
8682
  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 +9122,41 @@ async function memGetCommand(id, flags) {
9015
9122
  async function memUpdateCommand(id, patchRaw) {
9016
9123
  requireMemoryInit();
9017
9124
  const patch = parseJsonArg(patchRaw, "patch");
9018
- const backend = await getBackend();
9019
- const updated = await backend.update(id, { type: "", data: 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.");
9127
+ }
9128
+ const updated = await updateRecord(id, { data: patch });
9020
9129
  if (!updated) error(`Record ${id} not found`);
9021
9130
  printRecord(updated);
9022
9131
  }
9132
+ async function memKeyCommand(id, flags) {
9133
+ requireMemoryInit();
9134
+ const backend = await getBackend();
9135
+ const setList = parseCsv(flags.set);
9136
+ const addList = parseCsv(flags.add) ?? [];
9137
+ const removeList = parseCsv(flags.remove) ?? [];
9138
+ if (!setList && addList.length === 0 && removeList.length === 0) {
9139
+ error("Pass at least one of --add <csv>, --remove <csv>, or --set <csv>.");
9140
+ }
9141
+ const existing = await backend.getById(id);
9142
+ if (!existing) error(`Record ${id} not found`);
9143
+ let next;
9144
+ if (setList) {
9145
+ next = setList;
9146
+ } else {
9147
+ const removeSet = new Set(removeList);
9148
+ next = [...existing.keys ?? [], ...addList].filter((k) => !removeSet.has(k));
9149
+ }
9150
+ next = [...new Set(next)];
9151
+ const outcome = await backend.updateKeys(id, next);
9152
+ if (outcome.status === "not_found") error(`Record ${id} not found`);
9153
+ if (outcome.status === "conflict") {
9154
+ 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.`
9156
+ );
9157
+ }
9158
+ printRecord(outcome.record);
9159
+ }
9023
9160
  async function memArchiveCommand(id, flags) {
9024
9161
  requireMemoryInit();
9025
9162
  const backend = await getBackend();
@@ -9216,7 +9353,7 @@ async function memDoctorCommand() {
9216
9353
  }
9217
9354
  if (cfg.embedding.provider === "openai") {
9218
9355
  try {
9219
- const { embed: embed2 } = await import("./embedding-K2CFCLXI.js");
9356
+ const { embed: embed2 } = await import("./embedding-2YB6CGBA.js");
9220
9357
  const result = await embed2("connectivity check");
9221
9358
  checks.push({
9222
9359
  name: "OpenAI embedding provider reachable",
@@ -9375,6 +9512,9 @@ async function memVacuumCommand() {
9375
9512
  }
9376
9513
  async function memReindexCommand(flags) {
9377
9514
  requireMemoryInit();
9515
+ if (flags.searchable) {
9516
+ return memReindexSearchableCommand(flags);
9517
+ }
9378
9518
  const backend = await getBackend();
9379
9519
  const cfg = getMemoryConfigOrDefault();
9380
9520
  if (cfg.embedding.provider !== "openai") {
@@ -9437,6 +9577,58 @@ async function memReindexCommand(flags) {
9437
9577
  force: !!flags.force
9438
9578
  });
9439
9579
  }
9580
+ async function memReindexSearchableCommand(flags) {
9581
+ const backend = await getBackend();
9582
+ const onlyNull = !flags.all;
9583
+ const totalCap = flags.limit ? parsePositiveInt(flags.limit, 1e6, "limit") : 1e6;
9584
+ const PAGE = 500;
9585
+ let considered = 0;
9586
+ let updated = 0;
9587
+ let unchanged = 0;
9588
+ let emptied = 0;
9589
+ let offset = 0;
9590
+ while (considered < totalCap) {
9591
+ const pageSize = Math.min(PAGE, totalCap - considered);
9592
+ const rows = await backend.listForSearchableBackfill({
9593
+ type: flags.type,
9594
+ onlyNull,
9595
+ limit: pageSize,
9596
+ offset
9597
+ });
9598
+ if (rows.length === 0) break;
9599
+ considered += rows.length;
9600
+ let updatedThisPage = 0;
9601
+ for (const r of rows) {
9602
+ const next = defaultSearchableText(r.data);
9603
+ if (!next) {
9604
+ emptied++;
9605
+ continue;
9606
+ }
9607
+ if (next === (r.searchable_text ?? "")) {
9608
+ unchanged++;
9609
+ continue;
9610
+ }
9611
+ await backend.updateSearchableText(r.id, next);
9612
+ updated++;
9613
+ updatedThisPage++;
9614
+ }
9615
+ offset += onlyNull ? rows.length - updatedThisPage : rows.length;
9616
+ if (!isAgentMode()) {
9617
+ process.stderr.write(` searchable: updated ${updated} / considered ${considered}\r`);
9618
+ }
9619
+ }
9620
+ if (!isAgentMode()) process.stderr.write("\n");
9621
+ okJson({
9622
+ status: "ok",
9623
+ mode: "searchable",
9624
+ type: flags.type ?? null,
9625
+ scope: onlyNull ? "null_only" : "all",
9626
+ considered,
9627
+ updated,
9628
+ unchanged,
9629
+ emptied
9630
+ });
9631
+ }
9440
9632
 
9441
9633
  // src/commands/mem.ts
9442
9634
  function memStatusCommand() {
@@ -9475,11 +9667,12 @@ function registerMemoryCommands(program2) {
9475
9667
  );
9476
9668
  mem.command("doctor").description("Diagnose schema, indexes, plugin resolution, connectivity, embeddings").action(memDoctorCommand);
9477
9669
  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);
9670
+ 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
9671
  mem.command("sql <query>").description("Run a read-only SELECT / WITH / EXPLAIN against the memory store").action(memSqlCommand);
9480
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);
9481
9673
  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);
9674
+ 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));
9483
9676
  mem.command("archive <id>").description("Archive a record").option("--reason <text>", "Why it was archived (user_archived | deleted_upstream | superseded | \u2026)").action(memArchiveCommand);
9484
9677
  mem.command("weight <id> <n>").description("Set record relevance weight (1-10)").action(memWeightCommand);
9485
9678
  mem.command("flush <id>").description("Reset access count for a record").action(memFlushCommand);
@@ -9690,7 +9883,7 @@ Search for actions, read their docs, and execute them. This is the core workflow
9690
9883
 
9691
9884
  **Quick start:**
9692
9885
  \`\`\`bash
9693
- one --agent connection list # See connected platforms
9886
+ one --agent connection list # See connected platforms + your access on each
9694
9887
  one --agent connection delete <connection-key> # Remove a connection
9695
9888
  one --agent actions search <platform> "<query>" -t execute # Find an action
9696
9889
  one --agent actions knowledge <platform> <actionId> # Read docs (REQUIRED)
@@ -9779,6 +9972,7 @@ Request specific sections:
9779
9972
  - Always use the **exact action ID** from search results \u2014 don't guess
9780
9973
  - Always read **knowledge** before executing any action
9781
9974
  - Connection keys come from \`one connection list\` \u2014 don't hardcode them
9975
+ - \`connection list\` also reports an \`access\` field per connection (\`full\` / \`methods\` / \`actions\`) \u2014 read it before planning so you don't propose an action the access config will reject
9782
9976
  - Skills stay in lockstep with the CLI version automatically \u2014 every command checks a \`.one-cli-version\` marker in the canonical skill dir and refreshes the files if the CLI has been upgraded. Check manually with \`one config skills status\`; force a resync with \`one config skills sync\`
9783
9977
  `;
9784
9978
  var GUIDE_ACTIONS = `# One Actions \u2014 Reference
@@ -9793,7 +9987,17 @@ Always follow this sequence. Never skip the knowledge step.
9793
9987
  one --agent connection list
9794
9988
  \`\`\`
9795
9989
 
9796
- Returns platforms, status, connection keys, and tags.
9990
+ Returns platforms, status, connection keys, tags, and an \`access\` field per connection describing what the current access config lets you run there:
9991
+
9992
+ | \`access\` | Meaning |
9993
+ |----------|---------|
9994
+ | \`{"policy": "full"}\` | Every action on the connection |
9995
+ | \`{"policy": "methods", "methods": ["GET"]}\` | Only actions with these HTTP methods will execute |
9996
+ | \`{"policy": "actions", "actions": [{"actionId", "title", "method"}]}\` | Only these exact actions \u2014 use them directly, skip \`actions search\` |
9997
+
9998
+ Also present when relevant: \`knowledgeOnly: true\` (execution disabled \u2014 read knowledge and write code instead), \`unresolvedActionIds\` (allowlisted ids that could not be looked up), and \`accessHint\` (a one-line summary of the restriction).
9999
+
10000
+ Read \`access\` before planning \u2014 it prevents proposing an action the config will reject. Change it with \`one config\`.
9797
10001
 
9798
10002
  ### 1b. Delete a Connection
9799
10003
 
@@ -10122,10 +10326,19 @@ one --agent mem add note '{"content":"..."}' --tags work,urgent --weight 8 --key
10122
10326
  # Get (optionally with links)
10123
10327
  one --agent mem get <id> --links
10124
10328
 
10125
- # Update (shallow merge into data)
10329
+ # Update (shallow merge into data; regenerates searchable_text from the merged data)
10126
10330
  one --agent mem update <id> '{"status":"done"}'
10127
10331
 
10128
- # Archive / unarchive
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.
10334
+ one --agent mem key <id> --add email:new@x.com
10335
+ 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
10337
+ # NOTE: passing keys to 'mem update' is rejected \u2014 keys are not a data field.
10338
+
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
10341
+ # scoped to active records).
10129
10342
  one --agent mem archive <id> --reason superseded
10130
10343
 
10131
10344
  # List by type \u2014 type is positional. Synced rows are namespaced as <platform>/<model>.
@@ -10191,8 +10404,12 @@ one --agent mem status # backend, provider, _upgrade hint
10191
10404
  one --agent mem doctor # 7-check health report
10192
10405
  one --agent mem vacuum # backend maintenance (VACUUM ANALYZE)
10193
10406
  one --agent mem reindex # re-embed records under current model
10407
+ one --agent mem reindex --searchable # backfill searchable_text where NULL
10408
+ one --agent mem reindex --searchable --type attio/attioPeople --all # also rewrite stale text
10194
10409
  \`\`\`
10195
10410
 
10411
+ \`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\`.
10412
+
10196
10413
  ## Admin
10197
10414
 
10198
10415
  \`\`\`bash
@@ -11356,7 +11573,7 @@ program.name("one").option("--agent", "Machine-readable JSON output (no colors,
11356
11573
  one whoami Show current user, organization, and project
11357
11574
 
11358
11575
  Workflow (use these in order):
11359
- 1. one list List your connected platforms and connection keys
11576
+ 1. one list List connected platforms, keys, and your access on each
11360
11577
  2. one actions search <platform> <q> Search for actions using natural language
11361
11578
  3. one actions knowledge <plat> <id> Get full docs for an action (ALWAYS do this before execute)
11362
11579
  4. one actions execute <p> <id> <key> Execute the action
@@ -11614,7 +11831,7 @@ var connection = program.command("connection").description("Manage connections")
11614
11831
  connection.command("add [platform]").alias("a").description("Add a new connection").option("--tag <name>", "Tag the new connection (disambiguates multiple connections per platform in sync/flow profiles)").action(async (platform, options) => {
11615
11832
  await connectionAddCommand(platform, { tag: options.tag });
11616
11833
  });
11617
- connection.command("list").alias("ls").description("List your connections").option("-s, --search <query>", "Filter connections by platform name").option("-l, --limit <n>", "Max connections to return (agent mode default: 20)").action(async (options) => {
11834
+ connection.command("list").alias("ls").description("List your connections and what your access config lets you run on each").option("-s, --search <query>", "Filter connections by platform name").option("-l, --limit <n>", "Max connections to return (agent mode default: 20)").action(async (options) => {
11618
11835
  await connectionListCommand(options);
11619
11836
  });
11620
11837
  connection.command("delete <connection-key>").alias("rm").description("Delete a connection").option("-f, --force", "Skip confirmation prompt").action(async (connectionKey, options) => {
@@ -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.50.0",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [
@@ -45,7 +45,21 @@ Always follow this sequence when the user wants to do something on a connected p
45
45
  one --agent connection list
46
46
  ```
47
47
 
48
- Returns connected platforms with their connection keys (needed for execution) and platform names in kebab-case (needed for searching).
48
+ Returns connected platforms with their connection keys (needed for execution), platform names in kebab-case (needed for searching), and an `access` field per connection telling you what you may run there.
49
+
50
+ **Read `access` before you plan a workflow** — it saves you from discovering a restriction as a 403 halfway through:
51
+
52
+ | `access` | What it means |
53
+ |----------|---------------|
54
+ | `{"policy": "full"}` | Every action on this connection is available |
55
+ | `{"policy": "methods", "methods": ["GET"]}` | Only actions with these HTTP methods will execute — don't propose writes |
56
+ | `{"policy": "actions", "actions": [...]}` | Only these exact actions may run. Each has `actionId`, `title`, `method` — **use them directly and skip `actions search`** |
57
+
58
+ Two more fields appear only when relevant:
59
+ - `"knowledgeOnly": true` — `actions execute` is disabled. Read knowledge and write integration code instead of executing.
60
+ - `"unresolvedActionIds": [...]` — allowlisted ids that couldn't be looked up; treat them as unavailable and tell the user.
61
+
62
+ An empty `actions` array means the allowlist grants nothing on that connection — say so rather than searching for alternatives.
49
63
 
50
64
  ### 1b. Delete a connection
51
65
 
@@ -170,10 +184,19 @@ Underneath, the store has no `platform` column — `type` is the only platform-s
170
184
  ```bash
171
185
  # User memories
172
186
  one --agent mem add note '{"content":"..."}' --tags work --weight 7
187
+ one --agent mem update <id> '{"status":"done"}' # merges into data; refreshes searchable_text
173
188
  one --agent mem search "deadline" # hybrid if key set, else FTS
174
189
  one --agent mem list note --limit 20
175
190
  one --agent mem link <from-id> <to-id> relates_to --bi
176
191
 
192
+ # Identity keys (first-class column, NOT data). Unique across ACTIVE records;
193
+ # archiving a record frees its keys. `mem update '{"keys":[...]}'` is rejected.
194
+ one --agent mem key <id> --add email:x@y.com # add/--remove/--set
195
+ one --agent mem find-by-source email:x@y.com # prefers the active owner
196
+
197
+ # Backfill searchable_text (no embedding provider needed) — fixes NULL/noisy text
198
+ one --agent mem reindex --searchable --type attio/attioPeople
199
+
177
200
  # Status + diagnostics
178
201
  one --agent mem status # backend, provider, _upgrade hint
179
202
  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
- };