@withone/cli 1.42.0 → 1.43.3

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
@@ -303,24 +303,61 @@ Default TTL is 1 hour. Configure via `ONE_CACHE_TTL` environment variable or `ca
303
303
 
304
304
  Note: `actions execute` is never cached — it always hits the API fresh.
305
305
 
306
- ### `one sync`
306
+ ### `one mem` — unified memory store
307
307
 
308
- Sync platform data into local SQLite for instant queries, full-text search, scheduled refresh, and change-driven automation. The sync engine (`better-sqlite3`) is an optional dependency install it once per machine:
308
+ One ships a local memory store (a real Postgres process bootstrapped on demand via the bundled `embedded-postgres` plugin, with a `postgres` plugin for remote/self-hosted) that backs both user-authored notes and synced platform data. **Zero-config** the first `one mem` call on a fresh machine auto-initializes the cluster at `~/.one/pg/cluster/` and writes a daemon PID file so subsequent CLI invocations reuse it.
309
309
 
310
310
  ```bash
311
- one sync install && one sync doctor
311
+ # User memories works immediately on a new install
312
+ one mem add note '{"content":"Design review is Thursday"}' --tags work --weight 7
313
+ one mem search "design review" # hybrid FTS + semantic (if key set)
314
+ one mem list note --limit 20
315
+
316
+ # Listing synced platform rows — type is positional and namespaced as <platform>/<model>;
317
+ # there is NO --platform flag and NO platform column in the schema.
318
+ one mem list "gmail/threads"
319
+ one mem list "attio/attioPeople" --limit 5
320
+
321
+ # Enable semantic search (optional)
322
+ one init # re-run — prompts for OpenAI key
323
+ # OR
324
+ one mem config set embedding.apiKey sk-... # writes top-level openaiApiKey
325
+ # OR
326
+ export OPENAI_API_KEY=sk-... # no persistence
327
+
328
+ # Status + diagnostics
329
+ one mem status # backend, provider, _upgrade hint
330
+ one mem doctor # 7-check health report
312
331
  ```
313
332
 
333
+ Key surfaces: `add`, `get`, `update`, `archive`, `list`, `search` (`--deep` forces semantic), `context`, `link`/`linked`/`unlink`, `sources`, `find-by-source`, `export`, `import`, `migrate`, `vacuum`, `reindex`. Run `one guide memory` for the full reference.
334
+
335
+ ### `one sync` (and `one mem sync` alias)
336
+
337
+ Sync platform data into the unified memory store for instant queries, hybrid FTS + semantic search, scheduled refresh, and change-driven automation. Memory is always written; pass `--no-memory` to skip (rare).
338
+
314
339
  ```bash
315
- # Discover → init (one command: infer + late-bound connection + auto-test) → run
340
+ # Discover → init (seeds from built-in, auto-tests) → declare searchable → preview → run
316
341
  one sync models stripe
317
- one sync init stripe balanceTransactions # connection: { platform } baked in, test auto-run
342
+ one sync init stripe balanceTransactions
343
+
344
+ # Optional: declare clean fields for embedding, then preview
345
+ one sync init stripe balanceTransactions --config '{
346
+ "memory": {
347
+ "embed": true,
348
+ "searchable": ["description","type","amount","currency"]
349
+ }
350
+ }'
351
+ one sync test stripe/balanceTransactions --show-searchable
352
+
353
+ # Run — every row lands in memory (SQLite also written for enrich-phase compat)
318
354
  one sync run stripe --since 90d
355
+ one mem sync run stripe # identical (alias)
319
356
 
320
- # Query, search, SQL
357
+ # Query + search (reads from memory)
321
358
  one sync query stripe/balanceTransactions --where "status=available" --limit 20
322
- one sync search "refund"
323
- one sync sql stripe "SELECT count(*) FROM balanceTransactions"
359
+ one sync query stripe/customers --where 'address.city=SF' # dotted paths supported
360
+ one sync search "refund" # hybrid, per-type
324
361
 
325
362
  # Schedule unattended syncs + change hooks
326
363
  one sync schedule add stripe --every 1h
@@ -334,20 +371,23 @@ one sync run stripe --full-refresh
334
371
 
335
372
  > **Connections are late-bound.** Profiles use `"connection": { "platform": "<name>", "tag"?: "..." }` instead of literal `connectionKey` strings. The key is resolved at sync time, so `one add <platform>` (re-auth) doesn't break the profile. `tag` only needed for multi-account platforms (e.g. two Gmail accounts).
336
373
 
374
+ > **`memory.searchable` paths.** Drives what gets embedded + FTS-indexed. Supports numeric indexes (`values.name[0].full_name`) and `[]` wildcards (`messages[].snippet`, `messages[].payload.parts[].body.data`). Without declared paths the default walker concatenates every string — correct but noisy for hierarchical APIs. Always declare when `embed: true`.
375
+
337
376
  | Subcommand | What it does |
338
377
  |------------|-------------|
339
- | `install` / `doctor` | Install + verify the SQLite engine |
378
+ | `profiles [platform]` | List built-in pre-validated profiles |
379
+ | `doctor` | Verify sync engine health |
340
380
  | `models <platform>` | Discover available data models |
341
- | `init <platform> <model>` | Create profile (auto-infers all fields, auto-resolves key, auto-runs test) |
342
- | `test <platform>/<model>` | Validate + auto-fix profile from real API response (also runs inside init) |
343
- | `run <platform>` | Sync data (`--full-refresh`, `--since`, `--dry-run`) |
344
- | `query <platform>/<model>` | Query with `--where`, `--after/before`, `--refresh` |
345
- | `search <query>` | FTS5 across all synced data |
346
- | `sql <platform> <sql>` | Raw SELECT queries |
381
+ | `init <platform> <model>` | Create/patch profile (seeds from built-in, auto-tests) |
382
+ | `test <platform>/<model>` | Validate profile. `--show-searchable` previews embedded text |
383
+ | `run <platform>` | Sync data (`--full-refresh`, `--since`, `--dry-run`, `--no-memory`) |
384
+ | `query <platform>/<model>` | Query memory with `--where` (dotted paths), `--after/before` |
385
+ | `search <query>` | Hybrid FTS + semantic across all synced data |
386
+ | `list [platform]` | Show profiles, record counts, freshness |
347
387
  | `schedule add/list/status/remove/repair` | Cron-backed scheduled syncs with drift detection |
348
- | `remove <platform>` | Delete local data (`--dry-run` to preview) |
388
+ | `remove <platform>` | Delete synced data (`--dry-run` to preview) |
349
389
 
350
- Change hooks (`onInsert`, `onUpdate`, `onChange`) fire per-page during sync — pipe to a shell command, a flow, or an event log. Root-array responses (e.g. Hacker News `/v0/topstories.json` → `[9129911, 9129199, ...]`) are supported by setting `resultsPath` to `""`, `"$"`, or `"."`; primitive elements are auto-wrapped as `{ [idField]: value }`. Run `one guide sync` for the full reference.
390
+ Change hooks (`onInsert`, `onUpdate`, `onChange`) fire per-page during sync — pipe to a shell command, a flow, or an event log. Root-array responses (e.g. Hacker News `/v0/topstories.json` → `[9129911, 9129199, ...]`) are supported by setting `resultsPath` to `""`, `"$"`, or `"."`; primitive elements are auto-wrapped as `{ [idField]: value }`. Run `one guide sync` or `one guide memory` for the full reference.
351
391
 
352
392
  ### `one relay`
353
393
 
@@ -0,0 +1,59 @@
1
+ import {
2
+ error,
3
+ isAgentMode,
4
+ json,
5
+ requireMemoryInit
6
+ } from "./chunk-MRGKKO54.js";
7
+ import {
8
+ getBackend
9
+ } from "./chunk-IAFQVFCB.js";
10
+
11
+ // src/commands/mem/sql.ts
12
+ async function memSqlCommand(sql) {
13
+ requireMemoryInit();
14
+ const backend = await getBackend();
15
+ if (!backend.capabilities().rawSql || !backend.raw) {
16
+ error(
17
+ "This memory backend does not support raw SQL (capabilities.rawSql = false). Use `mem list` / `mem search` / `mem find-by-source` for high-level queries."
18
+ );
19
+ }
20
+ try {
21
+ const result = await backend.raw(sql);
22
+ if (isAgentMode()) {
23
+ json({
24
+ columns: result.columns,
25
+ rows: result.rows,
26
+ rowCount: result.rowCount
27
+ });
28
+ return;
29
+ }
30
+ if (result.rows.length === 0) {
31
+ console.log("(0 rows)");
32
+ return;
33
+ }
34
+ console.log(JSON.stringify(result.rows, null, 2));
35
+ console.log(`
36
+ ${result.rowCount} row(s)`);
37
+ } catch (err) {
38
+ error(err instanceof Error ? err.message : String(err));
39
+ }
40
+ }
41
+ async function syncSqlCommand(platformModel, sql) {
42
+ const [platform, model] = platformModel.split("/");
43
+ if (!platform || !model) {
44
+ error(`Usage: one sync sql <platform>/<model> "<SELECT ...>". Example: one sync sql attio/attioPeople "SELECT data->>'id' FROM mem_records WHERE type = 'attio/attioPeople'"`);
45
+ }
46
+ const type = `${platform}/${model}`;
47
+ if (!isAgentMode() && !sql.includes(`'${type}'`) && !sql.includes(`"${type}"`)) {
48
+ process.stderr.write(
49
+ ` note: this query does not filter to type = '${type}'. Results span all types. Add \`WHERE type = '${type}'\` to scope.
50
+ `
51
+ );
52
+ }
53
+ await memSqlCommand(sql);
54
+ }
55
+
56
+ export {
57
+ memSqlCommand,
58
+ syncSqlCommand
59
+ };
@@ -0,0 +1,38 @@
1
+ // src/lib/dot-path.ts
2
+ function getByDotPath(obj, dotPath) {
3
+ const parts = dotPath.split(".").flatMap((part) => {
4
+ const bracketMatch = part.match(/^([^[]+)\[(\d+)\]$/);
5
+ if (bracketMatch) {
6
+ return [bracketMatch[1], bracketMatch[2]];
7
+ }
8
+ return [part];
9
+ });
10
+ let current = obj;
11
+ for (const part of parts) {
12
+ if (current === null || current === void 0) return void 0;
13
+ if (Array.isArray(current) && /^\d+$/.test(part)) {
14
+ current = current[parseInt(part, 10)];
15
+ } else if (typeof current === "object") {
16
+ current = current[part];
17
+ } else {
18
+ return void 0;
19
+ }
20
+ }
21
+ return current;
22
+ }
23
+ function setByDotPath(obj, dotPath, value) {
24
+ const parts = dotPath.split(".");
25
+ let current = obj;
26
+ for (let i = 0; i < parts.length - 1; i++) {
27
+ if (current[parts[i]] === void 0 || current[parts[i]] === null) {
28
+ current[parts[i]] = {};
29
+ }
30
+ current = current[parts[i]];
31
+ }
32
+ current[parts[parts.length - 1]] = value;
33
+ }
34
+
35
+ export {
36
+ getByDotPath,
37
+ setByDotPath
38
+ };
@@ -0,0 +1,477 @@
1
+ // src/lib/config.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+ function configDir() {
6
+ return path.join(os.homedir(), ".one");
7
+ }
8
+ function configFile() {
9
+ return path.join(configDir(), "config.json");
10
+ }
11
+ function projectsDir() {
12
+ return path.join(configDir(), "projects");
13
+ }
14
+ function getProjectRoot(cwd = process.cwd()) {
15
+ let dir = path.resolve(cwd);
16
+ const root = path.parse(dir).root;
17
+ while (dir !== root) {
18
+ if (fs.existsSync(path.join(dir, ".git")) || fs.existsSync(path.join(dir, "package.json"))) {
19
+ return dir;
20
+ }
21
+ dir = path.dirname(dir);
22
+ }
23
+ return path.resolve(cwd);
24
+ }
25
+ function getProjectSlug(projectRoot = getProjectRoot()) {
26
+ return projectRoot.replace(/[\\/]/g, "-");
27
+ }
28
+ function getProjectConfigDir(projectRoot = getProjectRoot()) {
29
+ return path.join(projectsDir(), getProjectSlug(projectRoot));
30
+ }
31
+ function getProjectConfigPath(projectRoot = getProjectRoot()) {
32
+ return path.join(getProjectConfigDir(projectRoot), "config.json");
33
+ }
34
+ function getGlobalConfigPath() {
35
+ return configFile();
36
+ }
37
+ function resolveConfig() {
38
+ const projectRoot = getProjectRoot();
39
+ const projectSlug = getProjectSlug(projectRoot);
40
+ const projectPath = getProjectConfigPath(projectRoot);
41
+ if (fs.existsSync(projectPath)) {
42
+ const config = readConfigFile(projectPath);
43
+ if (config) {
44
+ return { config, scope: "project", path: projectPath, projectRoot, projectSlug };
45
+ }
46
+ }
47
+ const root = path.parse(process.cwd()).root;
48
+ let dir = path.resolve(process.cwd());
49
+ while (dir !== root) {
50
+ dir = path.dirname(dir);
51
+ const slug = getProjectSlug(dir);
52
+ const configPath = path.join(projectsDir(), slug, "config.json");
53
+ if (fs.existsSync(configPath)) {
54
+ const config = readConfigFile(configPath);
55
+ if (config) {
56
+ return { config, scope: "project", path: configPath, projectRoot: dir, projectSlug: slug };
57
+ }
58
+ }
59
+ }
60
+ if (fs.existsSync(configFile())) {
61
+ const config = readConfigFile(configFile());
62
+ if (config) {
63
+ return { config, scope: "global", path: configFile(), projectRoot, projectSlug };
64
+ }
65
+ }
66
+ return { config: null, scope: null, path: configFile(), projectRoot, projectSlug };
67
+ }
68
+ function readConfigFile(filePath) {
69
+ try {
70
+ const content = fs.readFileSync(filePath, "utf-8");
71
+ return JSON.parse(content);
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+ function configExists() {
77
+ return resolveConfig().config !== null;
78
+ }
79
+ function globalConfigExists() {
80
+ return fs.existsSync(configFile());
81
+ }
82
+ function projectConfigExists(projectRoot = getProjectRoot()) {
83
+ return fs.existsSync(getProjectConfigPath(projectRoot));
84
+ }
85
+ function readConfig() {
86
+ return resolveConfig().config;
87
+ }
88
+ function readGlobalConfig() {
89
+ if (!fs.existsSync(configFile())) return null;
90
+ return readConfigFile(configFile());
91
+ }
92
+ function readProjectConfig() {
93
+ const projectPath = getProjectConfigPath();
94
+ if (!fs.existsSync(projectPath)) return null;
95
+ return readConfigFile(projectPath);
96
+ }
97
+ function writeConfig(config, scope) {
98
+ const targetScope = scope ?? resolveConfig().scope ?? "global";
99
+ if (targetScope === "project") {
100
+ const dir = getProjectConfigDir();
101
+ if (!fs.existsSync(dir)) {
102
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
103
+ }
104
+ const filePath = getProjectConfigPath();
105
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2), { mode: 384 });
106
+ return;
107
+ }
108
+ if (!fs.existsSync(configDir())) {
109
+ fs.mkdirSync(configDir(), { mode: 448 });
110
+ }
111
+ fs.writeFileSync(configFile(), JSON.stringify(config, null, 2), { mode: 384 });
112
+ }
113
+ function readOneRc() {
114
+ const rcPath = path.join(process.cwd(), ".onerc");
115
+ if (!fs.existsSync(rcPath)) return {};
116
+ try {
117
+ const content = fs.readFileSync(rcPath, "utf-8");
118
+ const result = {};
119
+ for (const line of content.split("\n")) {
120
+ const trimmed = line.trim();
121
+ if (!trimmed || trimmed.startsWith("#")) continue;
122
+ const eqIndex = trimmed.indexOf("=");
123
+ if (eqIndex === -1) continue;
124
+ const key = trimmed.slice(0, eqIndex).trim();
125
+ const value = trimmed.slice(eqIndex + 1).trim();
126
+ result[key] = value;
127
+ }
128
+ return result;
129
+ } catch {
130
+ return {};
131
+ }
132
+ }
133
+ function getApiKey() {
134
+ if (process.env.ONE_SECRET) return process.env.ONE_SECRET;
135
+ const rc = readOneRc();
136
+ if (rc.ONE_SECRET) return rc.ONE_SECRET;
137
+ return readConfig()?.apiKey ?? null;
138
+ }
139
+ function getOpenAiApiKey() {
140
+ if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY;
141
+ const rc = readOneRc();
142
+ if (rc.OPENAI_API_KEY) return rc.OPENAI_API_KEY;
143
+ return readConfig()?.openaiApiKey ?? null;
144
+ }
145
+ function setOpenAiApiKey(key) {
146
+ const resolved = resolveConfig();
147
+ if (!resolved.config) {
148
+ throw new Error("No One config found. Run `one init` first.");
149
+ }
150
+ if (key === "") {
151
+ delete resolved.config.openaiApiKey;
152
+ } else {
153
+ resolved.config.openaiApiKey = key;
154
+ }
155
+ writeConfig(resolved.config, resolved.scope ?? "global");
156
+ }
157
+ function getAccessControlFromAllSources() {
158
+ const rc = readOneRc();
159
+ const fileAc = getAccessControl();
160
+ const merged = { ...fileAc };
161
+ if (rc.ONE_PERMISSIONS) {
162
+ merged.permissions = rc.ONE_PERMISSIONS;
163
+ }
164
+ if (rc.ONE_CONNECTION_KEYS) {
165
+ merged.connectionKeys = rc.ONE_CONNECTION_KEYS.split(",").map((s) => s.trim()).filter(Boolean);
166
+ }
167
+ if (rc.ONE_ACTION_IDS) {
168
+ merged.actionIds = rc.ONE_ACTION_IDS.split(",").map((s) => s.trim()).filter(Boolean);
169
+ }
170
+ if (rc.ONE_KNOWLEDGE_AGENT) {
171
+ merged.knowledgeAgent = rc.ONE_KNOWLEDGE_AGENT === "true";
172
+ }
173
+ return merged;
174
+ }
175
+ function getAccessControl() {
176
+ return readConfig()?.accessControl ?? {};
177
+ }
178
+ var DEFAULT_API_BASE = "https://api.withone.ai/v1";
179
+ function getApiBase() {
180
+ const config = readConfig();
181
+ if (config?.apiBase) return `${config.apiBase}/v1`;
182
+ return DEFAULT_API_BASE;
183
+ }
184
+ function updateApiBase(url) {
185
+ const config = readConfig();
186
+ if (!config) return;
187
+ if (url) {
188
+ config.apiBase = url;
189
+ } else {
190
+ delete config.apiBase;
191
+ }
192
+ delete config.whoami;
193
+ writeConfig(config);
194
+ }
195
+ function getCacheTtl() {
196
+ if (process.env.ONE_CACHE_TTL) {
197
+ const val = parseInt(process.env.ONE_CACHE_TTL, 10);
198
+ if (!isNaN(val) && val > 0) return val;
199
+ }
200
+ const config = readConfig();
201
+ if (config?.cacheTtl && config.cacheTtl > 0) return config.cacheTtl;
202
+ return 3600;
203
+ }
204
+ function updateAccessControl(settings) {
205
+ const config = readConfig();
206
+ if (!config) return;
207
+ const cleaned = {};
208
+ if (settings.permissions && settings.permissions !== "admin") {
209
+ cleaned.permissions = settings.permissions;
210
+ }
211
+ if (settings.connectionKeys && !(settings.connectionKeys.length === 1 && settings.connectionKeys[0] === "*")) {
212
+ cleaned.connectionKeys = settings.connectionKeys;
213
+ }
214
+ if (settings.actionIds && !(settings.actionIds.length === 1 && settings.actionIds[0] === "*")) {
215
+ cleaned.actionIds = settings.actionIds;
216
+ }
217
+ if (settings.knowledgeAgent) {
218
+ cleaned.knowledgeAgent = true;
219
+ }
220
+ if (Object.keys(cleaned).length === 0) {
221
+ delete config.accessControl;
222
+ } else {
223
+ config.accessControl = cleaned;
224
+ }
225
+ writeConfig(config);
226
+ }
227
+ function getWhoAmI() {
228
+ return readConfig()?.whoami ?? null;
229
+ }
230
+ function updateWhoAmI(whoami) {
231
+ const config = readConfig();
232
+ if (!config) return;
233
+ config.whoami = whoami;
234
+ writeConfig(config);
235
+ }
236
+ async function ensureWhoAmI(api) {
237
+ const cached = getWhoAmI();
238
+ if (cached) return cached;
239
+ try {
240
+ const whoami = await api.whoami();
241
+ updateWhoAmI(whoami);
242
+ return whoami;
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+ function getEnvFromApiKey(apiKey) {
248
+ return apiKey.startsWith("sk_test_") ? "test" : "live";
249
+ }
250
+
251
+ // src/lib/memory/config.ts
252
+ var DEFAULT_MEMORY_CONFIG = {
253
+ backend: "embedded-postgres",
254
+ plugins: [],
255
+ embedding: {
256
+ provider: "none",
257
+ model: "text-embedding-3-small",
258
+ dimensions: 1536
259
+ },
260
+ defaults: {
261
+ trackAccessOnSearch: true,
262
+ embedOnAdd: true,
263
+ embedOnSync: false
264
+ }
265
+ };
266
+ function getMemoryConfig() {
267
+ const config = readConfig();
268
+ return config?.memory ?? null;
269
+ }
270
+ function getMemoryConfigOrDefault() {
271
+ return getMemoryConfig() ?? DEFAULT_MEMORY_CONFIG;
272
+ }
273
+ function memoryConfigExists() {
274
+ return getMemoryConfig() !== null;
275
+ }
276
+ function updateMemoryConfig(patch, opts = {}) {
277
+ const config = readConfig();
278
+ if (!config) {
279
+ throw new Error("No One config found. Run `one init` first.");
280
+ }
281
+ const current = config.memory ?? DEFAULT_MEMORY_CONFIG;
282
+ const next = opts.replace ? patch : { ...current, ...patch };
283
+ config.memory = next;
284
+ writeConfig(config);
285
+ return next;
286
+ }
287
+ function getEmbeddingApiKey() {
288
+ const fromCore = getOpenAiApiKey();
289
+ if (fromCore) return fromCore;
290
+ const mem = getMemoryConfig();
291
+ return mem?.embedding.apiKey ?? null;
292
+ }
293
+ function setOpenAiApiKey2(key) {
294
+ setOpenAiApiKey(key);
295
+ if (key === "") return;
296
+ const mem = getMemoryConfig();
297
+ if (!mem) return;
298
+ if (mem.embedding.provider === "openai") return;
299
+ updateMemoryConfig({
300
+ ...mem,
301
+ embedding: { ...mem.embedding, provider: "openai" }
302
+ });
303
+ }
304
+
305
+ // src/lib/memory/embedding.ts
306
+ var FETCH_TIMEOUT_MS = 3e4;
307
+ function fetchWithTimeout(url, init, timeoutMs) {
308
+ const ctrl = new AbortController();
309
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
310
+ return fetch(url, { ...init, signal: ctrl.signal }).finally(() => clearTimeout(t));
311
+ }
312
+ async function embed(text, opts = {}) {
313
+ const clean = text?.trim();
314
+ if (!clean) return null;
315
+ const cfg = getMemoryConfigOrDefault();
316
+ if (cfg.embedding.provider !== "openai") return null;
317
+ const apiKey = getEmbeddingApiKey();
318
+ if (!apiKey) return null;
319
+ const model = opts.model ?? cfg.embedding.model;
320
+ const dimensions = cfg.embedding.dimensions;
321
+ for (let attempt = 0; attempt < 3; attempt++) {
322
+ try {
323
+ const res = await fetchWithTimeout("https://api.openai.com/v1/embeddings", {
324
+ method: "POST",
325
+ headers: {
326
+ "Content-Type": "application/json",
327
+ Authorization: `Bearer ${apiKey}`
328
+ },
329
+ body: JSON.stringify({
330
+ model,
331
+ input: clean.slice(0, 8e3),
332
+ dimensions
333
+ })
334
+ }, FETCH_TIMEOUT_MS);
335
+ if (!res.ok) {
336
+ if (res.status === 429 || res.status >= 500) {
337
+ await sleep(500 * (attempt + 1));
338
+ continue;
339
+ }
340
+ const body2 = await res.text();
341
+ throw new Error(`OpenAI embeddings ${res.status}: ${body2}`);
342
+ }
343
+ const body = await res.json();
344
+ const vector = body.data[0]?.embedding;
345
+ if (!vector || vector.length !== dimensions) {
346
+ throw new Error(`Unexpected embedding shape (got length ${vector?.length})`);
347
+ }
348
+ return { vector, model: `openai:${model}` };
349
+ } catch (err) {
350
+ if (attempt === 2) {
351
+ process.stderr.write(`[mem] embedding failed: ${err instanceof Error ? err.message : String(err)}
352
+ `);
353
+ return null;
354
+ }
355
+ await sleep(500 * (attempt + 1));
356
+ }
357
+ }
358
+ return null;
359
+ }
360
+ async function embedBatch(texts, opts = {}) {
361
+ if (texts.length === 0) return [];
362
+ const cfg = getMemoryConfigOrDefault();
363
+ if (cfg.embedding.provider !== "openai") return texts.map(() => null);
364
+ const apiKey = getEmbeddingApiKey();
365
+ if (!apiKey) return texts.map(() => null);
366
+ const model = opts.model ?? cfg.embedding.model;
367
+ const dimensions = cfg.embedding.dimensions;
368
+ const active = [];
369
+ texts.forEach((t, i) => {
370
+ const clean = t?.trim();
371
+ if (clean) active.push({ index: i, input: clean.slice(0, 8e3) });
372
+ });
373
+ if (active.length === 0) return texts.map(() => null);
374
+ const result = texts.map(() => null);
375
+ for (let attempt = 0; attempt < 3; attempt++) {
376
+ try {
377
+ const res = await fetchWithTimeout("https://api.openai.com/v1/embeddings", {
378
+ method: "POST",
379
+ headers: {
380
+ "Content-Type": "application/json",
381
+ Authorization: `Bearer ${apiKey}`
382
+ },
383
+ body: JSON.stringify({
384
+ model,
385
+ input: active.map((a) => a.input),
386
+ dimensions
387
+ })
388
+ }, FETCH_TIMEOUT_MS);
389
+ if (!res.ok) {
390
+ if (res.status === 429 || res.status >= 500) {
391
+ await sleep(500 * (attempt + 1));
392
+ continue;
393
+ }
394
+ const body2 = await res.text();
395
+ throw new Error(`OpenAI embeddings ${res.status}: ${body2}`);
396
+ }
397
+ const body = await res.json();
398
+ for (const item of body.data) {
399
+ const slot = active[item.index];
400
+ if (!slot) continue;
401
+ result[slot.index] = { vector: item.embedding, model: `openai:${model}` };
402
+ }
403
+ return result;
404
+ } catch (err) {
405
+ if (attempt === 2) {
406
+ process.stderr.write(`[mem] batch embedding failed: ${err instanceof Error ? err.message : String(err)}
407
+ `);
408
+ return result;
409
+ }
410
+ await sleep(500 * (attempt + 1));
411
+ }
412
+ }
413
+ return result;
414
+ }
415
+ function sleep(ms) {
416
+ return new Promise((resolve) => setTimeout(resolve, ms));
417
+ }
418
+ function defaultSearchableText(data, maxLen = 4e3) {
419
+ const parts = [];
420
+ const walk = (value, depth = 0) => {
421
+ if (value === null || value === void 0) return;
422
+ if (typeof value === "string" && value.trim()) {
423
+ parts.push(value.trim());
424
+ return;
425
+ }
426
+ if (typeof value === "number" || typeof value === "boolean") {
427
+ parts.push(String(value));
428
+ return;
429
+ }
430
+ if (depth > 4) return;
431
+ if (Array.isArray(value)) {
432
+ for (const v of value) walk(v, depth + 1);
433
+ return;
434
+ }
435
+ if (typeof value === "object") {
436
+ for (const v of Object.values(value)) walk(v, depth + 1);
437
+ }
438
+ };
439
+ walk(data);
440
+ const joined = parts.join(" ").replace(/\s+/g, " ").trim();
441
+ return joined.length > maxLen ? joined.slice(0, maxLen) : joined;
442
+ }
443
+
444
+ export {
445
+ getProjectRoot,
446
+ getProjectConfigPath,
447
+ getGlobalConfigPath,
448
+ resolveConfig,
449
+ configExists,
450
+ globalConfigExists,
451
+ projectConfigExists,
452
+ readConfig,
453
+ readGlobalConfig,
454
+ readProjectConfig,
455
+ writeConfig,
456
+ getApiKey,
457
+ getOpenAiApiKey,
458
+ getAccessControlFromAllSources,
459
+ getAccessControl,
460
+ getApiBase,
461
+ updateApiBase,
462
+ getCacheTtl,
463
+ updateAccessControl,
464
+ getWhoAmI,
465
+ updateWhoAmI,
466
+ ensureWhoAmI,
467
+ getEnvFromApiKey,
468
+ DEFAULT_MEMORY_CONFIG,
469
+ getMemoryConfig,
470
+ getMemoryConfigOrDefault,
471
+ memoryConfigExists,
472
+ updateMemoryConfig,
473
+ setOpenAiApiKey2 as setOpenAiApiKey,
474
+ embed,
475
+ embedBatch,
476
+ defaultSearchableText
477
+ };