@todoforai/tfa-memory 0.4.1 → 0.4.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.
Files changed (2) hide show
  1. package/dist/cli.js +203 -123
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
6
6
  import { parseArgs } from "node:util";
7
7
  import { readFileSync as readFileSync4 } from "node:fs";
8
8
 
9
- // node_modules/@todoforai/tfa-subagent/dist/auth.js
9
+ // ../tfa-subagent/dist/auth.js
10
10
  import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
11
11
  import { join, dirname } from "node:path";
12
12
  import { homedir, platform } from "node:os";
@@ -21,10 +21,15 @@ function normalizeApiUrl(url) {
21
21
  function credentialsPath() {
22
22
  return platform() === "win32" ? join(homedir(), "AppData", "Roaming", "todoforai", "credentials.json") : platform() === "darwin" ? join(homedir(), "Library", "Application Support", "todoforai", "credentials.json") : join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "todoforai", "credentials.json");
23
23
  }
24
- function credentialsPaths() {
25
- return [credentialsPath()];
24
+ function read() {
25
+ try {
26
+ return JSON.parse(readFileSync(credentialsPath(), "utf-8"));
27
+ } catch {
28
+ return {};
29
+ }
26
30
  }
27
- function writeSecure(p, data) {
31
+ function writeSecure(data) {
32
+ const p = credentialsPath();
28
33
  mkdirSync(dirname(p), { recursive: true });
29
34
  writeFileSync(p, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
30
35
  try {
@@ -32,31 +37,29 @@ function writeSecure(p, data) {
32
37
  } catch {}
33
38
  }
34
39
  function readStoredAuth(apiUrl) {
35
- for (const p of credentialsPaths()) {
36
- try {
37
- const c = JSON.parse(readFileSync(p, "utf-8"));
38
- const apiToken = c[apiUrl] || c.apiToken || c.apiKey || "";
39
- const host = c.backendHost || "";
40
- const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
41
- return {
42
- apiToken,
43
- apiUrl: storedApiUrl,
44
- deviceId: typeof c.deviceId === "string" ? c.deviceId : undefined,
45
- deviceSecret: typeof c.deviceSecret === "string" ? c.deviceSecret : undefined
46
- };
47
- } catch {}
48
- }
49
- return { apiToken: "" };
40
+ const c = read();
41
+ const host = c.backendHost || "";
42
+ const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
43
+ const dstIsForThisUrl = !storedApiUrl || normalizeApiUrl(storedApiUrl) === normalizeApiUrl(apiUrl);
44
+ const isDst = typeof c.apiToken === "string" && c.apiToken.startsWith("dst_");
45
+ const dst = dstIsForThisUrl && isDst ? c.apiToken : "";
46
+ const fallbackApiToken = isDst && !dstIsForThisUrl ? "" : c.apiToken;
47
+ const apiToken = dst || c[apiUrl] || fallbackApiToken || c.apiKey || "";
48
+ return {
49
+ apiToken: typeof apiToken === "string" ? apiToken : "",
50
+ apiUrl: storedApiUrl,
51
+ deviceId: typeof c.deviceId === "string" ? c.deviceId : undefined,
52
+ deviceSecret: typeof c.deviceSecret === "string" ? c.deviceSecret : undefined
53
+ };
50
54
  }
51
55
  function persistApiToken(token) {
52
- for (const p of credentialsPaths()) {
53
- try {
54
- const c = JSON.parse(readFileSync(p, "utf-8"));
55
- c.apiToken = token;
56
- writeSecure(p, c);
57
- return;
58
- } catch {}
59
- }
56
+ const c = read();
57
+ if (!Object.keys(c).length)
58
+ return;
59
+ c.apiToken = token;
60
+ try {
61
+ writeSecure(c);
62
+ } catch {}
60
63
  }
61
64
  async function refreshDeviceToken(opts = {}) {
62
65
  const envUrl = process.env.TODOFORAI_API_URL || process.env.API_URL;
@@ -65,6 +68,8 @@ async function refreshDeviceToken(opts = {}) {
65
68
  const apiUrl = normalizeApiUrl(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL);
66
69
  if (!stored.deviceId || !stored.deviceSecret)
67
70
  return "";
71
+ if (stored.apiUrl && normalizeApiUrl(stored.apiUrl) !== apiUrl)
72
+ return "";
68
73
  try {
69
74
  const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
70
75
  method: "POST",
@@ -85,9 +90,13 @@ async function refreshDeviceToken(opts = {}) {
85
90
  async function fetchWithRenew(url, init, opts) {
86
91
  const header = opts.header ?? "x-api-key";
87
92
  const renewOn = opts.renewOn ?? [401];
88
- const send = (token) => fetch(url, { ...init, headers: { ...init.headers, [header]: token } });
89
- let res = await send(opts.token);
90
- if (renewOn.includes(res.status) && opts.token.startsWith("dst_")) {
93
+ const send = (token) => {
94
+ const headers = new Headers(init.headers);
95
+ headers.set(header, token);
96
+ return fetch(url, { ...init, headers });
97
+ };
98
+ const res = await send(opts.token);
99
+ if (renewOn.includes(res.status)) {
91
100
  const fresh = await refreshDeviceToken({ apiUrl: opts.apiUrl });
92
101
  if (fresh && fresh !== opts.token)
93
102
  return { res: await send(fresh), token: fresh };
@@ -99,13 +108,14 @@ function resolveToken(opts = {}) {
99
108
  const initialApiUrl = normalizeApiUrl(opts.apiUrl || envUrl || DEFAULT_API_URL);
100
109
  const stored = readStoredAuth(initialApiUrl);
101
110
  const apiUrl = normalizeApiUrl(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL);
102
- const apiToken = opts.apiToken || process.env.TODOFORAI_API_TOKEN || stored.apiToken || "";
111
+ const finalStored = apiUrl === initialApiUrl ? stored : readStoredAuth(apiUrl);
112
+ const apiToken = opts.apiToken || process.env.TODOFORAI_API_TOKEN || finalStored.apiToken || "";
103
113
  if (!apiToken)
104
114
  throw new Error("TODOFORAI_API_TOKEN not set and no apiToken found in ~/.config/todoforai/credentials.json (run `<cli> login` or `todoforai-bridge login`, or pass --api-token)");
105
115
  return { apiUrl, apiToken };
106
116
  }
107
117
 
108
- // node_modules/@todoforai/tfa-subagent/dist/login.js
118
+ // ../tfa-subagent/dist/login.js
109
119
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync as chmodSync2 } from "node:fs";
110
120
  import { join as join2, dirname as dirname2 } from "node:path";
111
121
  import { homedir as homedir2, platform as platform2 } from "node:os";
@@ -122,10 +132,15 @@ function normalizeApiUrl2(url) {
122
132
  function credentialsPath2() {
123
133
  return platform2() === "win32" ? join2(homedir2(), "AppData", "Roaming", "todoforai", "credentials.json") : platform2() === "darwin" ? join2(homedir2(), "Library", "Application Support", "todoforai", "credentials.json") : join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "todoforai", "credentials.json");
124
134
  }
125
- function credentialsPaths2() {
126
- return [credentialsPath2()];
135
+ function read2() {
136
+ try {
137
+ return JSON.parse(readFileSync2(credentialsPath2(), "utf-8"));
138
+ } catch {
139
+ return {};
140
+ }
127
141
  }
128
- function writeSecure2(p, data) {
142
+ function writeSecure2(data) {
143
+ const p = credentialsPath2();
129
144
  mkdirSync2(dirname2(p), { recursive: true });
130
145
  writeFileSync2(p, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
131
146
  try {
@@ -133,39 +148,32 @@ function writeSecure2(p, data) {
133
148
  } catch {}
134
149
  }
135
150
  function writeCredentials(fields) {
136
- const p = credentialsPath2();
137
- let existing = {};
138
- try {
139
- existing = JSON.parse(readFileSync2(p, "utf-8"));
140
- } catch {}
141
- writeSecure2(p, { ...existing, ...fields });
151
+ writeSecure2({ ...read2(), ...fields });
142
152
  }
143
153
  function readStoredAuth2(apiUrl) {
144
- for (const p of credentialsPaths2()) {
145
- try {
146
- const c = JSON.parse(readFileSync2(p, "utf-8"));
147
- const apiToken = c[apiUrl] || c.apiToken || c.apiKey || "";
148
- const host = c.backendHost || "";
149
- const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
150
- return {
151
- apiToken,
152
- apiUrl: storedApiUrl,
153
- deviceId: typeof c.deviceId === "string" ? c.deviceId : undefined,
154
- deviceSecret: typeof c.deviceSecret === "string" ? c.deviceSecret : undefined
155
- };
156
- } catch {}
157
- }
158
- return { apiToken: "" };
154
+ const c = read2();
155
+ const host = c.backendHost || "";
156
+ const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
157
+ const dstIsForThisUrl = !storedApiUrl || normalizeApiUrl2(storedApiUrl) === normalizeApiUrl2(apiUrl);
158
+ const isDst = typeof c.apiToken === "string" && c.apiToken.startsWith("dst_");
159
+ const dst = dstIsForThisUrl && isDst ? c.apiToken : "";
160
+ const fallbackApiToken = isDst && !dstIsForThisUrl ? "" : c.apiToken;
161
+ const apiToken = dst || c[apiUrl] || fallbackApiToken || c.apiKey || "";
162
+ return {
163
+ apiToken: typeof apiToken === "string" ? apiToken : "",
164
+ apiUrl: storedApiUrl,
165
+ deviceId: typeof c.deviceId === "string" ? c.deviceId : undefined,
166
+ deviceSecret: typeof c.deviceSecret === "string" ? c.deviceSecret : undefined
167
+ };
159
168
  }
160
169
  function persistApiToken2(token) {
161
- for (const p of credentialsPaths2()) {
162
- try {
163
- const c = JSON.parse(readFileSync2(p, "utf-8"));
164
- c.apiToken = token;
165
- writeSecure2(p, c);
166
- return;
167
- } catch {}
168
- }
170
+ const c = read2();
171
+ if (!Object.keys(c).length)
172
+ return;
173
+ c.apiToken = token;
174
+ try {
175
+ writeSecure2(c);
176
+ } catch {}
169
177
  }
170
178
  async function refreshDeviceToken2(opts = {}) {
171
179
  const envUrl = process.env.TODOFORAI_API_URL || process.env.API_URL;
@@ -174,6 +182,8 @@ async function refreshDeviceToken2(opts = {}) {
174
182
  const apiUrl = normalizeApiUrl2(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL2);
175
183
  if (!stored.deviceId || !stored.deviceSecret)
176
184
  return "";
185
+ if (stored.apiUrl && normalizeApiUrl2(stored.apiUrl) !== apiUrl)
186
+ return "";
177
187
  try {
178
188
  const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
179
189
  method: "POST",
@@ -287,7 +297,7 @@ async function handleLogin(argv, binName) {
287
297
  process.exit(await login({ binName, deviceName: get("--device-name"), apiUrl: get("--api-url") }));
288
298
  }
289
299
 
290
- // node_modules/@todoforai/tfa-subagent/dist/whoami.js
300
+ // ../tfa-subagent/dist/whoami.js
291
301
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, chmodSync as chmodSync3 } from "node:fs";
292
302
  import { join as join3, dirname as dirname3 } from "node:path";
293
303
  import { homedir as homedir3, platform as platform3 } from "node:os";
@@ -302,10 +312,15 @@ function normalizeApiUrl3(url) {
302
312
  function credentialsPath3() {
303
313
  return platform3() === "win32" ? join3(homedir3(), "AppData", "Roaming", "todoforai", "credentials.json") : platform3() === "darwin" ? join3(homedir3(), "Library", "Application Support", "todoforai", "credentials.json") : join3(process.env.XDG_CONFIG_HOME || join3(homedir3(), ".config"), "todoforai", "credentials.json");
304
314
  }
305
- function credentialsPaths3() {
306
- return [credentialsPath3()];
315
+ function read3() {
316
+ try {
317
+ return JSON.parse(readFileSync3(credentialsPath3(), "utf-8"));
318
+ } catch {
319
+ return {};
320
+ }
307
321
  }
308
- function writeSecure3(p, data) {
322
+ function writeSecure3(data) {
323
+ const p = credentialsPath3();
309
324
  mkdirSync3(dirname3(p), { recursive: true });
310
325
  writeFileSync3(p, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
311
326
  try {
@@ -313,31 +328,29 @@ function writeSecure3(p, data) {
313
328
  } catch {}
314
329
  }
315
330
  function readStoredAuth3(apiUrl) {
316
- for (const p of credentialsPaths3()) {
317
- try {
318
- const c = JSON.parse(readFileSync3(p, "utf-8"));
319
- const apiToken = c[apiUrl] || c.apiToken || c.apiKey || "";
320
- const host = c.backendHost || "";
321
- const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
322
- return {
323
- apiToken,
324
- apiUrl: storedApiUrl,
325
- deviceId: typeof c.deviceId === "string" ? c.deviceId : undefined,
326
- deviceSecret: typeof c.deviceSecret === "string" ? c.deviceSecret : undefined
327
- };
328
- } catch {}
329
- }
330
- return { apiToken: "" };
331
+ const c = read3();
332
+ const host = c.backendHost || "";
333
+ const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
334
+ const dstIsForThisUrl = !storedApiUrl || normalizeApiUrl3(storedApiUrl) === normalizeApiUrl3(apiUrl);
335
+ const isDst = typeof c.apiToken === "string" && c.apiToken.startsWith("dst_");
336
+ const dst = dstIsForThisUrl && isDst ? c.apiToken : "";
337
+ const fallbackApiToken = isDst && !dstIsForThisUrl ? "" : c.apiToken;
338
+ const apiToken = dst || c[apiUrl] || fallbackApiToken || c.apiKey || "";
339
+ return {
340
+ apiToken: typeof apiToken === "string" ? apiToken : "",
341
+ apiUrl: storedApiUrl,
342
+ deviceId: typeof c.deviceId === "string" ? c.deviceId : undefined,
343
+ deviceSecret: typeof c.deviceSecret === "string" ? c.deviceSecret : undefined
344
+ };
331
345
  }
332
346
  function persistApiToken3(token) {
333
- for (const p of credentialsPaths3()) {
334
- try {
335
- const c = JSON.parse(readFileSync3(p, "utf-8"));
336
- c.apiToken = token;
337
- writeSecure3(p, c);
338
- return;
339
- } catch {}
340
- }
347
+ const c = read3();
348
+ if (!Object.keys(c).length)
349
+ return;
350
+ c.apiToken = token;
351
+ try {
352
+ writeSecure3(c);
353
+ } catch {}
341
354
  }
342
355
  async function refreshDeviceToken3(opts = {}) {
343
356
  const envUrl = process.env.TODOFORAI_API_URL || process.env.API_URL;
@@ -346,6 +359,8 @@ async function refreshDeviceToken3(opts = {}) {
346
359
  const apiUrl = normalizeApiUrl3(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL3);
347
360
  if (!stored.deviceId || !stored.deviceSecret)
348
361
  return "";
362
+ if (stored.apiUrl && normalizeApiUrl3(stored.apiUrl) !== apiUrl)
363
+ return "";
349
364
  try {
350
365
  const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
351
366
  method: "POST",
@@ -366,9 +381,13 @@ async function refreshDeviceToken3(opts = {}) {
366
381
  async function fetchWithRenew2(url, init, opts) {
367
382
  const header = opts.header ?? "x-api-key";
368
383
  const renewOn = opts.renewOn ?? [401];
369
- const send = (token) => fetch(url, { ...init, headers: { ...init.headers, [header]: token } });
370
- let res = await send(opts.token);
371
- if (renewOn.includes(res.status) && opts.token.startsWith("dst_")) {
384
+ const send = (token) => {
385
+ const headers = new Headers(init.headers);
386
+ headers.set(header, token);
387
+ return fetch(url, { ...init, headers });
388
+ };
389
+ const res = await send(opts.token);
390
+ if (renewOn.includes(res.status)) {
372
391
  const fresh = await refreshDeviceToken3({ apiUrl: opts.apiUrl });
373
392
  if (fresh && fresh !== opts.token)
374
393
  return { res: await send(fresh), token: fresh };
@@ -380,7 +399,8 @@ function resolveToken2(opts = {}) {
380
399
  const initialApiUrl = normalizeApiUrl3(opts.apiUrl || envUrl || DEFAULT_API_URL3);
381
400
  const stored = readStoredAuth3(initialApiUrl);
382
401
  const apiUrl = normalizeApiUrl3(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL3);
383
- const apiToken = opts.apiToken || process.env.TODOFORAI_API_TOKEN || stored.apiToken || "";
402
+ const finalStored = apiUrl === initialApiUrl ? stored : readStoredAuth3(apiUrl);
403
+ const apiToken = opts.apiToken || process.env.TODOFORAI_API_TOKEN || finalStored.apiToken || "";
384
404
  if (!apiToken)
385
405
  throw new Error("TODOFORAI_API_TOKEN not set and no apiToken found in ~/.config/todoforai/credentials.json (run `<cli> login` or `todoforai-bridge login`, or pass --api-token)");
386
406
  return { apiUrl, apiToken };
@@ -409,19 +429,46 @@ async function whoami(opts) {
409
429
  }
410
430
  }
411
431
 
412
- // node_modules/@todoforai/tfa-subagent/dist/update-notifier.js
432
+ // ../tfa-subagent/dist/update-notifier.js
413
433
  import fs from "node:fs";
414
434
  import path from "node:path";
415
435
  import os from "node:os";
416
- function isLinkedInstall() {
436
+ function installer() {
437
+ let real;
417
438
  try {
418
- return !fs.realpathSync(process.argv[1] || "").includes(`${path.sep}node_modules${path.sep}`);
439
+ real = fs.realpathSync(process.argv[1] || "");
419
440
  } catch {
420
- return false;
441
+ return null;
421
442
  }
443
+ if (!real.includes(`${path.sep}node_modules${path.sep}`))
444
+ return null;
445
+ return real.includes(path.join(".bun", "install", "global")) ? "bun" : "npm";
422
446
  }
423
447
  var TTL_MS = 24 * 60 * 60 * 1000;
424
448
  var CACHE_DIR = path.join(os.homedir(), ".config", "todoforai");
449
+ var cacheFileFor = (name) => path.join(CACHE_DIR, `notifier-${encodeURIComponent(name)}.json`);
450
+ function readCache(name) {
451
+ try {
452
+ return JSON.parse(fs.readFileSync(cacheFileFor(name), "utf8"));
453
+ } catch {
454
+ return {};
455
+ }
456
+ }
457
+ function writeCache(name, cache) {
458
+ try {
459
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
460
+ fs.writeFileSync(cacheFileFor(name), JSON.stringify(cache));
461
+ } catch {}
462
+ }
463
+ function refreshCache(name, cache) {
464
+ if (Date.now() - (cache.ts ?? 0) <= TTL_MS)
465
+ return;
466
+ writeCache(name, { ...cache, ts: Date.now() });
467
+ fetch(`https://registry.npmjs.org/${name}/latest`, { signal: AbortSignal.timeout(3000) }).then((r) => r.ok ? r.json() : null).then((j) => {
468
+ if (j?.version)
469
+ writeCache(name, { ...cache, ts: Date.now(), latest: j.version });
470
+ }).catch(() => {});
471
+ }
425
472
  function cmpVer(a, b) {
426
473
  const pa = a.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
427
474
  const pb = b.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
@@ -433,13 +480,11 @@ function cmpVer(a, b) {
433
480
  return 0;
434
481
  }
435
482
  function checkForUpdates(pkg) {
436
- if (!process.stderr.isTTY || process.env.CI || process.env.NO_UPDATE_NOTIFIER || isLinkedInstall())
483
+ if (!process.stderr.isTTY || process.env.CI || process.env.NO_UPDATE_NOTIFIER || !installer())
437
484
  return;
438
- const cacheFile = path.join(CACHE_DIR, `notifier-${encodeURIComponent(pkg.name)}.json`);
439
- let cache = {};
440
- try {
441
- cache = JSON.parse(fs.readFileSync(cacheFile, "utf8"));
442
- } catch {}
485
+ if (process.argv.includes("--json"))
486
+ return;
487
+ const cache = readCache(pkg.name);
443
488
  if (cache.latest && cmpVer(cache.latest, pkg.version) > 0) {
444
489
  process.stderr.write(`
445
490
  \x1B[33m Update available: \x1B[2m${pkg.version}\x1B[22m → \x1B[1m${cache.latest}\x1B[0m
@@ -447,23 +492,13 @@ function checkForUpdates(pkg) {
447
492
 
448
493
  `);
449
494
  }
450
- if (Date.now() - (cache.ts ?? 0) > TTL_MS) {
451
- try {
452
- fs.mkdirSync(CACHE_DIR, { recursive: true });
453
- fs.writeFileSync(cacheFile, JSON.stringify({ ...cache, ts: Date.now() }));
454
- } catch {}
455
- fetch(`https://registry.npmjs.org/${pkg.name}/latest`, { signal: AbortSignal.timeout(3000) }).then((r) => r.ok ? r.json() : null).then((j) => {
456
- if (!j?.version)
457
- return;
458
- fs.writeFileSync(cacheFile, JSON.stringify({ ts: Date.now(), latest: j.version }));
459
- }).catch(() => {});
460
- }
495
+ refreshCache(pkg.name, cache);
461
496
  }
462
497
  // package.json
463
498
  var package_default = {
464
499
  name: "@todoforai/tfa-memory",
465
- version: "0.4.1",
466
- description: "Zero-config TODOforAI memory CLI — add/search/ls/get/rm text memory rows addressed as source/ref, plus federated search over every message of every past TODO (read-only `todo` source), with full-text and semantic search. Reuses bridge credentials.",
500
+ version: "0.4.3",
501
+ description: "Zero-config TODOforAI memory CLI — add/search/ls/get/rm/live text memory rows addressed as source/ref, plus federated search over every message of every past TODO (read-only `todo` source), with full-text and semantic search, plus L2 working-memory re-render. Reuses bridge credentials.",
467
502
  type: "module",
468
503
  bin: {
469
504
  "tfa-memory": "dist/cli.js"
@@ -504,6 +539,10 @@ every TODO under the read-only \`todo\` source (\`todo/<todoId>:<messageId>\`,
504
539
  live from the backend index, nothing separate to sync). \`--source todo\` searches
505
540
  past TODO messages only; \`get todo/<todoId>:<messageId>\` prints the whole message.
506
541
 
542
+ Owner-private TODOs are searchable in personal contexts. In agent shells,
543
+ TODOFORAI_TODO_ID supplies the destination; shared conversations cannot read
544
+ private TODOs or account-private saved notes. --todo selects a source, not an audience.
545
+
507
546
  Usage:
508
547
  tfa-memory <command> [options] [args...]
509
548
 
@@ -511,8 +550,10 @@ Commands:
511
550
  add [source/ref] <text|-> Save a row. Without an address → manual/<timestamp>.
512
551
  "-" reads stdin.
513
552
  search <query> Full-text search over saved rows + past TODO messages
553
+ live Re-render L2 working memory (the always-on prompt block)
514
554
  ls List saved rows, newest first (todo messages excluded)
515
555
  get <source/ref> Print full content of one row
556
+ around <source/ref> Neighbours of one row: todo messages in chat order, saved rows by time within the source
516
557
  rm <source/ref> Delete one row
517
558
  rm --source <name> Delete every row in a source
518
559
  sources List sources with counts and last write
@@ -522,7 +563,11 @@ Commands:
522
563
  Options:
523
564
  -s, --source <a,b> Restrict search/ls to sources (comma-separated; "todo" = TODO messages)
524
565
  --todo <id> Restrict a todo-message search to one TODO
525
- --semantic Embedding KNN search (meaning, not keywords)
566
+ -B, --budget <n> Token budget for \`live\` (default 1500, 100–20000)
567
+ --mode lexical|semantic|fused lexical: all words, newest first (default) · semantic: meaning · fused: best recall
568
+ --exclude <address> Leave out an already-read row (repeatable); fused also prints the lexical match count
569
+ --before <n> around: rows before the pivot (default 2, max 200)
570
+ --after <n> around: rows after the pivot (default 2, max 200); large = read the rest
526
571
  --meta <json> Attach JSON metadata to the row (e.g. '{"todoId":"..."}')
527
572
  -n, --limit <n> Max rows (default 5, max 50)
528
573
  -j, --json Machine-readable output
@@ -537,8 +582,10 @@ Examples:
537
582
  tfa-memory add deploy-notes/rollback "run scripts/rollback.sh, then verify /health"
538
583
  cat docs/auth.md | tfa-memory add acme/docs/auth.md -
539
584
  tfa-memory search "rollback procedure"
540
- tfa-memory search "how do we deploy" --semantic
585
+ tfa-memory search "how do we deploy" --mode semantic
541
586
  tfa-memory search "android build" --source todo
587
+ tfa-memory live
588
+ tfa-memory live -B 800
542
589
  tfa-memory get todo/<todoId>:<messageId>
543
590
  tfa-memory ls --source deploy-notes
544
591
  tfa-memory get deploy-notes/rollback
@@ -574,7 +621,10 @@ function resolveMemoryUrl(flag, apiUrl) {
574
621
  async function api(ctx, method, path2, body) {
575
622
  const init = {
576
623
  method,
577
- headers: body !== undefined ? { "content-type": "application/json" } : {},
624
+ headers: {
625
+ ...body !== undefined && { "content-type": "application/json" },
626
+ ...process.env.TODOFORAI_TODO_ID && { "x-tfa-todo-id": process.env.TODOFORAI_TODO_ID }
627
+ },
578
628
  body: body !== undefined ? JSON.stringify(body) : undefined
579
629
  };
580
630
  const { res, token } = await fetchWithRenew(`${ctx.memoryUrl}/v1${path2}`, init, { apiUrl: ctx.apiUrl, token: ctx.token, renewOn: [401, 403] });
@@ -614,8 +664,12 @@ async function main() {
614
664
  options: {
615
665
  source: { type: "string", short: "s" },
616
666
  todo: { type: "string" },
667
+ budget: { type: "string", short: "B" },
617
668
  meta: { type: "string" },
618
- semantic: { type: "boolean", default: false },
669
+ mode: { type: "string" },
670
+ exclude: { type: "string", multiple: true },
671
+ before: { type: "string" },
672
+ after: { type: "string" },
619
673
  limit: { type: "string", short: "n" },
620
674
  json: { type: "boolean", short: "j", default: false },
621
675
  "api-token": { type: "string" },
@@ -687,17 +741,35 @@ async function main() {
687
741
  die(`--meta must be a JSON object`, 2);
688
742
  }
689
743
  const res = await api(ctx, "POST", "/memory", { source, ref, content: text, meta });
744
+ for (const w of res.warnings ?? [])
745
+ console.error(`Warning: ${w}`);
690
746
  console.log(json ? JSON.stringify(res) : `${res.status} ${res.address}`);
691
747
  break;
692
748
  }
693
749
  case "search": {
694
750
  const query = rest[0] ?? die(`usage: tfa-memory search "query"`, 2);
695
- const res = await api(ctx, "GET", `/memory/search${qs({ query, semantic: values.semantic ? true : undefined, sources, limit, todoId: values.todo })}`);
751
+ const exclude = values.exclude?.join(",");
752
+ const res = await api(ctx, "GET", `/memory/search${qs({ query, mode: values.mode, sources, limit, todoId: values.todo, exclude })}`);
696
753
  for (const w of res.warnings ?? [])
697
754
  console.error(`Warning: ${w}`);
755
+ if (res.lexicalCount !== undefined)
756
+ console.error(`${res.lexicalCount} row(s) contain a query word`);
698
757
  printRows(res.rows, json);
699
758
  break;
700
759
  }
760
+ case "live": {
761
+ const budget = values.budget !== undefined ? Number(values.budget) : 1500;
762
+ if (!Number.isInteger(budget) || budget < 100 || budget > 20000)
763
+ die(`--budget must be an integer from 100 to 20000`, 2);
764
+ const res = await api(ctx, "GET", `/live/render${qs({ budget })}`);
765
+ const block = res.block ?? "";
766
+ if (json)
767
+ console.log(JSON.stringify({ block }));
768
+ else
769
+ process.stdout.write((block || "(empty live memory)") + `
770
+ `);
771
+ break;
772
+ }
701
773
  case "ls": {
702
774
  const res = await api(ctx, "GET", `/memory${qs({ sources, limit })}`);
703
775
  printRows(res.rows, json);
@@ -713,6 +785,14 @@ async function main() {
713
785
  `);
714
786
  break;
715
787
  }
788
+ case "around": {
789
+ const [source, ref] = splitAddr(rest[0] ?? die("address required (source/ref)", 2));
790
+ const res = await api(ctx, "GET", `/memory/around${qs({ source, ref, before: values.before, after: values.after })}`);
791
+ if (res.total !== undefined)
792
+ console.error(`message ${res.index + 1} of ${res.total}`);
793
+ printRows(res.rows, json, 2000);
794
+ break;
795
+ }
716
796
  case "rm":
717
797
  case "delete":
718
798
  case "del": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@todoforai/tfa-memory",
3
- "version": "0.4.1",
4
- "description": "Zero-config TODOforAI memory CLI — add/search/ls/get/rm text memory rows addressed as source/ref, plus federated search over every message of every past TODO (read-only `todo` source), with full-text and semantic search. Reuses bridge credentials.",
3
+ "version": "0.4.3",
4
+ "description": "Zero-config TODOforAI memory CLI — add/search/ls/get/rm/live text memory rows addressed as source/ref, plus federated search over every message of every past TODO (read-only `todo` source), with full-text and semantic search, plus L2 working-memory re-render. Reuses bridge credentials.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "tfa-memory": "dist/cli.js"