@todoforai/tfa-memory 0.4.2 → 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 +73 -12
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -38,9 +38,13 @@ function writeSecure(data) {
38
38
  }
39
39
  function readStoredAuth(apiUrl) {
40
40
  const c = read();
41
- const apiToken = c[apiUrl] || c.apiToken || c.apiKey || "";
42
41
  const host = c.backendHost || "";
43
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 || "";
44
48
  return {
45
49
  apiToken: typeof apiToken === "string" ? apiToken : "",
46
50
  apiUrl: storedApiUrl,
@@ -64,6 +68,8 @@ async function refreshDeviceToken(opts = {}) {
64
68
  const apiUrl = normalizeApiUrl(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL);
65
69
  if (!stored.deviceId || !stored.deviceSecret)
66
70
  return "";
71
+ if (stored.apiUrl && normalizeApiUrl(stored.apiUrl) !== apiUrl)
72
+ return "";
67
73
  try {
68
74
  const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
69
75
  method: "POST",
@@ -90,7 +96,7 @@ async function fetchWithRenew(url, init, opts) {
90
96
  return fetch(url, { ...init, headers });
91
97
  };
92
98
  const res = await send(opts.token);
93
- if (renewOn.includes(res.status) && opts.token.startsWith("dst_")) {
99
+ if (renewOn.includes(res.status)) {
94
100
  const fresh = await refreshDeviceToken({ apiUrl: opts.apiUrl });
95
101
  if (fresh && fresh !== opts.token)
96
102
  return { res: await send(fresh), token: fresh };
@@ -146,9 +152,13 @@ function writeCredentials(fields) {
146
152
  }
147
153
  function readStoredAuth2(apiUrl) {
148
154
  const c = read2();
149
- const apiToken = c[apiUrl] || c.apiToken || c.apiKey || "";
150
155
  const host = c.backendHost || "";
151
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 || "";
152
162
  return {
153
163
  apiToken: typeof apiToken === "string" ? apiToken : "",
154
164
  apiUrl: storedApiUrl,
@@ -172,6 +182,8 @@ async function refreshDeviceToken2(opts = {}) {
172
182
  const apiUrl = normalizeApiUrl2(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL2);
173
183
  if (!stored.deviceId || !stored.deviceSecret)
174
184
  return "";
185
+ if (stored.apiUrl && normalizeApiUrl2(stored.apiUrl) !== apiUrl)
186
+ return "";
175
187
  try {
176
188
  const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
177
189
  method: "POST",
@@ -317,9 +329,13 @@ function writeSecure3(data) {
317
329
  }
318
330
  function readStoredAuth3(apiUrl) {
319
331
  const c = read3();
320
- const apiToken = c[apiUrl] || c.apiToken || c.apiKey || "";
321
332
  const host = c.backendHost || "";
322
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 || "";
323
339
  return {
324
340
  apiToken: typeof apiToken === "string" ? apiToken : "",
325
341
  apiUrl: storedApiUrl,
@@ -343,6 +359,8 @@ async function refreshDeviceToken3(opts = {}) {
343
359
  const apiUrl = normalizeApiUrl3(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL3);
344
360
  if (!stored.deviceId || !stored.deviceSecret)
345
361
  return "";
362
+ if (stored.apiUrl && normalizeApiUrl3(stored.apiUrl) !== apiUrl)
363
+ return "";
346
364
  try {
347
365
  const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
348
366
  method: "POST",
@@ -369,7 +387,7 @@ async function fetchWithRenew2(url, init, opts) {
369
387
  return fetch(url, { ...init, headers });
370
388
  };
371
389
  const res = await send(opts.token);
372
- if (renewOn.includes(res.status) && opts.token.startsWith("dst_")) {
390
+ if (renewOn.includes(res.status)) {
373
391
  const fresh = await refreshDeviceToken3({ apiUrl: opts.apiUrl });
374
392
  if (fresh && fresh !== opts.token)
375
393
  return { res: await send(fresh), token: fresh };
@@ -479,8 +497,8 @@ function checkForUpdates(pkg) {
479
497
  // package.json
480
498
  var package_default = {
481
499
  name: "@todoforai/tfa-memory",
482
- version: "0.4.2",
483
- 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.",
484
502
  type: "module",
485
503
  bin: {
486
504
  "tfa-memory": "dist/cli.js"
@@ -521,6 +539,10 @@ every TODO under the read-only \`todo\` source (\`todo/<todoId>:<messageId>\`,
521
539
  live from the backend index, nothing separate to sync). \`--source todo\` searches
522
540
  past TODO messages only; \`get todo/<todoId>:<messageId>\` prints the whole message.
523
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
+
524
546
  Usage:
525
547
  tfa-memory <command> [options] [args...]
526
548
 
@@ -528,8 +550,10 @@ Commands:
528
550
  add [source/ref] <text|-> Save a row. Without an address → manual/<timestamp>.
529
551
  "-" reads stdin.
530
552
  search <query> Full-text search over saved rows + past TODO messages
553
+ live Re-render L2 working memory (the always-on prompt block)
531
554
  ls List saved rows, newest first (todo messages excluded)
532
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
533
557
  rm <source/ref> Delete one row
534
558
  rm --source <name> Delete every row in a source
535
559
  sources List sources with counts and last write
@@ -539,7 +563,11 @@ Commands:
539
563
  Options:
540
564
  -s, --source <a,b> Restrict search/ls to sources (comma-separated; "todo" = TODO messages)
541
565
  --todo <id> Restrict a todo-message search to one TODO
542
- --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
543
571
  --meta <json> Attach JSON metadata to the row (e.g. '{"todoId":"..."}')
544
572
  -n, --limit <n> Max rows (default 5, max 50)
545
573
  -j, --json Machine-readable output
@@ -554,8 +582,10 @@ Examples:
554
582
  tfa-memory add deploy-notes/rollback "run scripts/rollback.sh, then verify /health"
555
583
  cat docs/auth.md | tfa-memory add acme/docs/auth.md -
556
584
  tfa-memory search "rollback procedure"
557
- tfa-memory search "how do we deploy" --semantic
585
+ tfa-memory search "how do we deploy" --mode semantic
558
586
  tfa-memory search "android build" --source todo
587
+ tfa-memory live
588
+ tfa-memory live -B 800
559
589
  tfa-memory get todo/<todoId>:<messageId>
560
590
  tfa-memory ls --source deploy-notes
561
591
  tfa-memory get deploy-notes/rollback
@@ -591,7 +621,10 @@ function resolveMemoryUrl(flag, apiUrl) {
591
621
  async function api(ctx, method, path2, body) {
592
622
  const init = {
593
623
  method,
594
- 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
+ },
595
628
  body: body !== undefined ? JSON.stringify(body) : undefined
596
629
  };
597
630
  const { res, token } = await fetchWithRenew(`${ctx.memoryUrl}/v1${path2}`, init, { apiUrl: ctx.apiUrl, token: ctx.token, renewOn: [401, 403] });
@@ -631,8 +664,12 @@ async function main() {
631
664
  options: {
632
665
  source: { type: "string", short: "s" },
633
666
  todo: { type: "string" },
667
+ budget: { type: "string", short: "B" },
634
668
  meta: { type: "string" },
635
- semantic: { type: "boolean", default: false },
669
+ mode: { type: "string" },
670
+ exclude: { type: "string", multiple: true },
671
+ before: { type: "string" },
672
+ after: { type: "string" },
636
673
  limit: { type: "string", short: "n" },
637
674
  json: { type: "boolean", short: "j", default: false },
638
675
  "api-token": { type: "string" },
@@ -711,12 +748,28 @@ async function main() {
711
748
  }
712
749
  case "search": {
713
750
  const query = rest[0] ?? die(`usage: tfa-memory search "query"`, 2);
714
- 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 })}`);
715
753
  for (const w of res.warnings ?? [])
716
754
  console.error(`Warning: ${w}`);
755
+ if (res.lexicalCount !== undefined)
756
+ console.error(`${res.lexicalCount} row(s) contain a query word`);
717
757
  printRows(res.rows, json);
718
758
  break;
719
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
+ }
720
773
  case "ls": {
721
774
  const res = await api(ctx, "GET", `/memory${qs({ sources, limit })}`);
722
775
  printRows(res.rows, json);
@@ -732,6 +785,14 @@ async function main() {
732
785
  `);
733
786
  break;
734
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
+ }
735
796
  case "rm":
736
797
  case "delete":
737
798
  case "del": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@todoforai/tfa-memory",
3
- "version": "0.4.2",
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"