@todoforai/tfa-memory 0.4.2 → 0.4.4

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 +88 -12
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -11,6 +11,9 @@ import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
11
11
  import { join, dirname } from "node:path";
12
12
  import { homedir, platform } from "node:os";
13
13
  var DEFAULT_API_URL = "https://api.todofor.ai";
14
+ function runActorToken() {
15
+ return process.env.TODOFORAI_USER_ID ? process.env.TODOFORAI_API_TOKEN || "" : "";
16
+ }
14
17
  function normalizeApiUrl(url) {
15
18
  if (url.startsWith("localhost"))
16
19
  return `http://${url}`;
@@ -38,9 +41,13 @@ function writeSecure(data) {
38
41
  }
39
42
  function readStoredAuth(apiUrl) {
40
43
  const c = read();
41
- const apiToken = c[apiUrl] || c.apiToken || c.apiKey || "";
42
44
  const host = c.backendHost || "";
43
45
  const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
46
+ const dstIsForThisUrl = !storedApiUrl || normalizeApiUrl(storedApiUrl) === normalizeApiUrl(apiUrl);
47
+ const isDst = typeof c.apiToken === "string" && c.apiToken.startsWith("dst_");
48
+ const dst = dstIsForThisUrl && isDst ? c.apiToken : "";
49
+ const fallbackApiToken = isDst && !dstIsForThisUrl ? "" : c.apiToken;
50
+ const apiToken = dst || c[apiUrl] || fallbackApiToken || c.apiKey || "";
44
51
  return {
45
52
  apiToken: typeof apiToken === "string" ? apiToken : "",
46
53
  apiUrl: storedApiUrl,
@@ -64,6 +71,10 @@ async function refreshDeviceToken(opts = {}) {
64
71
  const apiUrl = normalizeApiUrl(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL);
65
72
  if (!stored.deviceId || !stored.deviceSecret)
66
73
  return "";
74
+ if (runActorToken())
75
+ return "";
76
+ if (stored.apiUrl && normalizeApiUrl(stored.apiUrl) !== apiUrl)
77
+ return "";
67
78
  try {
68
79
  const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
69
80
  method: "POST",
@@ -90,7 +101,7 @@ async function fetchWithRenew(url, init, opts) {
90
101
  return fetch(url, { ...init, headers });
91
102
  };
92
103
  const res = await send(opts.token);
93
- if (renewOn.includes(res.status) && opts.token.startsWith("dst_")) {
104
+ if (renewOn.includes(res.status)) {
94
105
  const fresh = await refreshDeviceToken({ apiUrl: opts.apiUrl });
95
106
  if (fresh && fresh !== opts.token)
96
107
  return { res: await send(fresh), token: fresh };
@@ -116,6 +127,9 @@ import { homedir as homedir2, platform as platform2 } from "node:os";
116
127
  import { spawn } from "node:child_process";
117
128
  import { platform as platform22, hostname } from "node:os";
118
129
  var DEFAULT_API_URL2 = "https://api.todofor.ai";
130
+ function runActorToken2() {
131
+ return process.env.TODOFORAI_USER_ID ? process.env.TODOFORAI_API_TOKEN || "" : "";
132
+ }
119
133
  function normalizeApiUrl2(url) {
120
134
  if (url.startsWith("localhost"))
121
135
  return `http://${url}`;
@@ -146,9 +160,13 @@ function writeCredentials(fields) {
146
160
  }
147
161
  function readStoredAuth2(apiUrl) {
148
162
  const c = read2();
149
- const apiToken = c[apiUrl] || c.apiToken || c.apiKey || "";
150
163
  const host = c.backendHost || "";
151
164
  const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
165
+ const dstIsForThisUrl = !storedApiUrl || normalizeApiUrl2(storedApiUrl) === normalizeApiUrl2(apiUrl);
166
+ const isDst = typeof c.apiToken === "string" && c.apiToken.startsWith("dst_");
167
+ const dst = dstIsForThisUrl && isDst ? c.apiToken : "";
168
+ const fallbackApiToken = isDst && !dstIsForThisUrl ? "" : c.apiToken;
169
+ const apiToken = dst || c[apiUrl] || fallbackApiToken || c.apiKey || "";
152
170
  return {
153
171
  apiToken: typeof apiToken === "string" ? apiToken : "",
154
172
  apiUrl: storedApiUrl,
@@ -172,6 +190,10 @@ async function refreshDeviceToken2(opts = {}) {
172
190
  const apiUrl = normalizeApiUrl2(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL2);
173
191
  if (!stored.deviceId || !stored.deviceSecret)
174
192
  return "";
193
+ if (runActorToken2())
194
+ return "";
195
+ if (stored.apiUrl && normalizeApiUrl2(stored.apiUrl) !== apiUrl)
196
+ return "";
175
197
  try {
176
198
  const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
177
199
  method: "POST",
@@ -290,6 +312,9 @@ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSy
290
312
  import { join as join3, dirname as dirname3 } from "node:path";
291
313
  import { homedir as homedir3, platform as platform3 } from "node:os";
292
314
  var DEFAULT_API_URL3 = "https://api.todofor.ai";
315
+ function runActorToken3() {
316
+ return process.env.TODOFORAI_USER_ID ? process.env.TODOFORAI_API_TOKEN || "" : "";
317
+ }
293
318
  function normalizeApiUrl3(url) {
294
319
  if (url.startsWith("localhost"))
295
320
  return `http://${url}`;
@@ -317,9 +342,13 @@ function writeSecure3(data) {
317
342
  }
318
343
  function readStoredAuth3(apiUrl) {
319
344
  const c = read3();
320
- const apiToken = c[apiUrl] || c.apiToken || c.apiKey || "";
321
345
  const host = c.backendHost || "";
322
346
  const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
347
+ const dstIsForThisUrl = !storedApiUrl || normalizeApiUrl3(storedApiUrl) === normalizeApiUrl3(apiUrl);
348
+ const isDst = typeof c.apiToken === "string" && c.apiToken.startsWith("dst_");
349
+ const dst = dstIsForThisUrl && isDst ? c.apiToken : "";
350
+ const fallbackApiToken = isDst && !dstIsForThisUrl ? "" : c.apiToken;
351
+ const apiToken = dst || c[apiUrl] || fallbackApiToken || c.apiKey || "";
323
352
  return {
324
353
  apiToken: typeof apiToken === "string" ? apiToken : "",
325
354
  apiUrl: storedApiUrl,
@@ -343,6 +372,10 @@ async function refreshDeviceToken3(opts = {}) {
343
372
  const apiUrl = normalizeApiUrl3(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL3);
344
373
  if (!stored.deviceId || !stored.deviceSecret)
345
374
  return "";
375
+ if (runActorToken3())
376
+ return "";
377
+ if (stored.apiUrl && normalizeApiUrl3(stored.apiUrl) !== apiUrl)
378
+ return "";
346
379
  try {
347
380
  const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
348
381
  method: "POST",
@@ -369,7 +402,7 @@ async function fetchWithRenew2(url, init, opts) {
369
402
  return fetch(url, { ...init, headers });
370
403
  };
371
404
  const res = await send(opts.token);
372
- if (renewOn.includes(res.status) && opts.token.startsWith("dst_")) {
405
+ if (renewOn.includes(res.status)) {
373
406
  const fresh = await refreshDeviceToken3({ apiUrl: opts.apiUrl });
374
407
  if (fresh && fresh !== opts.token)
375
408
  return { res: await send(fresh), token: fresh };
@@ -479,8 +512,8 @@ function checkForUpdates(pkg) {
479
512
  // package.json
480
513
  var package_default = {
481
514
  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.",
515
+ version: "0.4.4",
516
+ 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
517
  type: "module",
485
518
  bin: {
486
519
  "tfa-memory": "dist/cli.js"
@@ -521,6 +554,10 @@ every TODO under the read-only \`todo\` source (\`todo/<todoId>:<messageId>\`,
521
554
  live from the backend index, nothing separate to sync). \`--source todo\` searches
522
555
  past TODO messages only; \`get todo/<todoId>:<messageId>\` prints the whole message.
523
556
 
557
+ Owner-private TODOs are searchable in personal contexts. In agent shells,
558
+ TODOFORAI_TODO_ID supplies the destination; shared conversations cannot read
559
+ private TODOs or account-private saved notes. --todo selects a source, not an audience.
560
+
524
561
  Usage:
525
562
  tfa-memory <command> [options] [args...]
526
563
 
@@ -528,8 +565,10 @@ Commands:
528
565
  add [source/ref] <text|-> Save a row. Without an address → manual/<timestamp>.
529
566
  "-" reads stdin.
530
567
  search <query> Full-text search over saved rows + past TODO messages
568
+ live Re-render L2 working memory (the always-on prompt block)
531
569
  ls List saved rows, newest first (todo messages excluded)
532
570
  get <source/ref> Print full content of one row
571
+ around <source/ref> Neighbours of one row: todo messages in chat order, saved rows by time within the source
533
572
  rm <source/ref> Delete one row
534
573
  rm --source <name> Delete every row in a source
535
574
  sources List sources with counts and last write
@@ -539,7 +578,11 @@ Commands:
539
578
  Options:
540
579
  -s, --source <a,b> Restrict search/ls to sources (comma-separated; "todo" = TODO messages)
541
580
  --todo <id> Restrict a todo-message search to one TODO
542
- --semantic Embedding KNN search (meaning, not keywords)
581
+ -B, --budget <n> Token budget for \`live\` (default 1500, 100–20000)
582
+ --mode lexical|semantic|fused lexical: all words, newest first (default) · semantic: meaning · fused: best recall
583
+ --exclude <address> Leave out an already-read row (repeatable); fused also prints the lexical match count
584
+ --before <n> around: rows before the pivot (default 2, max 200)
585
+ --after <n> around: rows after the pivot (default 2, max 200); large = read the rest
543
586
  --meta <json> Attach JSON metadata to the row (e.g. '{"todoId":"..."}')
544
587
  -n, --limit <n> Max rows (default 5, max 50)
545
588
  -j, --json Machine-readable output
@@ -554,8 +597,10 @@ Examples:
554
597
  tfa-memory add deploy-notes/rollback "run scripts/rollback.sh, then verify /health"
555
598
  cat docs/auth.md | tfa-memory add acme/docs/auth.md -
556
599
  tfa-memory search "rollback procedure"
557
- tfa-memory search "how do we deploy" --semantic
600
+ tfa-memory search "how do we deploy" --mode semantic
558
601
  tfa-memory search "android build" --source todo
602
+ tfa-memory live
603
+ tfa-memory live -B 800
559
604
  tfa-memory get todo/<todoId>:<messageId>
560
605
  tfa-memory ls --source deploy-notes
561
606
  tfa-memory get deploy-notes/rollback
@@ -591,7 +636,10 @@ function resolveMemoryUrl(flag, apiUrl) {
591
636
  async function api(ctx, method, path2, body) {
592
637
  const init = {
593
638
  method,
594
- headers: body !== undefined ? { "content-type": "application/json" } : {},
639
+ headers: {
640
+ ...body !== undefined && { "content-type": "application/json" },
641
+ ...process.env.TODOFORAI_TODO_ID && { "x-tfa-todo-id": process.env.TODOFORAI_TODO_ID }
642
+ },
595
643
  body: body !== undefined ? JSON.stringify(body) : undefined
596
644
  };
597
645
  const { res, token } = await fetchWithRenew(`${ctx.memoryUrl}/v1${path2}`, init, { apiUrl: ctx.apiUrl, token: ctx.token, renewOn: [401, 403] });
@@ -631,8 +679,12 @@ async function main() {
631
679
  options: {
632
680
  source: { type: "string", short: "s" },
633
681
  todo: { type: "string" },
682
+ budget: { type: "string", short: "B" },
634
683
  meta: { type: "string" },
635
- semantic: { type: "boolean", default: false },
684
+ mode: { type: "string" },
685
+ exclude: { type: "string", multiple: true },
686
+ before: { type: "string" },
687
+ after: { type: "string" },
636
688
  limit: { type: "string", short: "n" },
637
689
  json: { type: "boolean", short: "j", default: false },
638
690
  "api-token": { type: "string" },
@@ -711,12 +763,28 @@ async function main() {
711
763
  }
712
764
  case "search": {
713
765
  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 })}`);
766
+ const exclude = values.exclude?.join(",");
767
+ const res = await api(ctx, "GET", `/memory/search${qs({ query, mode: values.mode, sources, limit, todoId: values.todo, exclude })}`);
715
768
  for (const w of res.warnings ?? [])
716
769
  console.error(`Warning: ${w}`);
770
+ if (res.lexicalCount !== undefined)
771
+ console.error(`${res.lexicalCount} row(s) contain a query word`);
717
772
  printRows(res.rows, json);
718
773
  break;
719
774
  }
775
+ case "live": {
776
+ const budget = values.budget !== undefined ? Number(values.budget) : 1500;
777
+ if (!Number.isInteger(budget) || budget < 100 || budget > 20000)
778
+ die(`--budget must be an integer from 100 to 20000`, 2);
779
+ const res = await api(ctx, "GET", `/live/render${qs({ budget })}`);
780
+ const block = res.block ?? "";
781
+ if (json)
782
+ console.log(JSON.stringify({ block }));
783
+ else
784
+ process.stdout.write((block || "(empty live memory)") + `
785
+ `);
786
+ break;
787
+ }
720
788
  case "ls": {
721
789
  const res = await api(ctx, "GET", `/memory${qs({ sources, limit })}`);
722
790
  printRows(res.rows, json);
@@ -732,6 +800,14 @@ async function main() {
732
800
  `);
733
801
  break;
734
802
  }
803
+ case "around": {
804
+ const [source, ref] = splitAddr(rest[0] ?? die("address required (source/ref)", 2));
805
+ const res = await api(ctx, "GET", `/memory/around${qs({ source, ref, before: values.before, after: values.after })}`);
806
+ if (res.total !== undefined)
807
+ console.error(`message ${res.index + 1} of ${res.total}`);
808
+ printRows(res.rows, json, 2000);
809
+ break;
810
+ }
735
811
  case "rm":
736
812
  case "delete":
737
813
  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.4",
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"