@todoforai/tfa-memory 0.4.5 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/dist/cli.js +58 -187
  3. package/package.json +2 -2
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @todoforai/tfa-memory
2
+
3
+ Search past TODOforAI conversations from the shell. Read-only; reuses your bridge credentials.
4
+
5
+ ```sh
6
+ tfa-memory search "how do we deploy" --mode fused # lexical (default) | semantic | fused
7
+ tfa-memory get todo/<todoId>:<messageId> # one whole message
8
+ tfa-memory around todo/<todoId>:<messageId> --before 5 --after 20
9
+ ```
10
+
11
+ Memory itself is files, not this tool: `projects/<id>/memory/` (team) and `projects/<id>/me/memory/` (personal) are pasted into every agent prompt; `context/` beside them is read on demand.
package/dist/cli.js CHANGED
@@ -4,7 +4,6 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/cli.ts
6
6
  import { parseArgs } from "node:util";
7
- import { readFileSync as readFileSync4 } from "node:fs";
8
7
 
9
8
  // ../tfa-subagent/dist/auth.js
10
9
  import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
@@ -512,8 +511,8 @@ function checkForUpdates(pkg) {
512
511
  // package.json
513
512
  var package_default = {
514
513
  name: "@todoforai/tfa-memory",
515
- version: "0.4.5",
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.",
514
+ version: "0.5.0",
515
+ description: "Zero-config TODOforAI CLI to search past TODO conversations (full-text and semantic), print one message or read the conversation around it. Read-only; memory itself lives in project files. Reuses bridge credentials.",
517
516
  type: "module",
518
517
  bin: {
519
518
  "tfa-memory": "dist/cli.js"
@@ -542,71 +541,44 @@ var package_default = {
542
541
 
543
542
  // src/cli.ts
544
543
  var isEntrypoint = __require.main == __require.module === true || !!process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
544
+ var MEMORY_FILES = `Memory is files now: save durable facts as short files in todoforai:projects/<projectId>/me/memory/ (personal) or …/memory/ (team) — they are pasted into every agent prompt; longer material goes in context/. Notes saved earlier with \`tfa-memory add\` are in todoforai:projects/<projectId>/me/context/notes/.`;
545
545
  function printHelp() {
546
- console.log(`tfa-memory — TODOforAI memory CLI (addressable, searchable text rows).
546
+ console.log(`tfa-memory — search past TODO conversations (read-only).
547
547
 
548
- Every row's address is \`source/ref\` — sources are directories, refs are
549
- filenames. A source exists once the first row names it. Re-adding the same
550
- address updates in place; identical content+meta is skipped (no re-embed).
548
+ Every message of every TODO you can see is addressed \`todo/<todoId>:<messageId>\`.
549
+ Private TODOs are searchable only in personal contexts: in agent shells
550
+ TODOFORAI_TODO_ID names the conversation asking, and a shared one cannot read them.
551
551
 
552
- \`search\` covers two corpora at once: rows you saved, plus every message of
553
- every TODO under the read-only \`todo\` source (\`todo/<todoId>:<messageId>\`,
554
- live from the backend index, nothing separate to sync). \`--source todo\` searches
555
- past TODO messages only; \`get todo/<todoId>:<messageId>\` prints the whole message.
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.
552
+ ${MEMORY_FILES}
560
553
 
561
554
  Usage:
562
555
  tfa-memory <command> [options] [args...]
563
556
 
564
557
  Commands:
565
- add [source/ref] <text|-> Save a row. Without an address → manual/<timestamp>.
566
- "-" reads stdin.
567
- search <query> Full-text search over saved rows + past TODO messages
568
- live Re-render L2 working memory (the always-on prompt block)
569
- ls List saved rows, newest first (todo messages excluded)
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
572
- rm <source/ref> Delete one row
573
- rm --source <name> Delete every row in a source
574
- sources List sources with counts and last write
558
+ search <query> Search past TODO messages
559
+ get <address> Print one whole message
560
+ around <address> Messages around one, in chat order
575
561
  login Device login — mint & persist credentials
576
562
  whoami Print resolved auth context
577
563
 
578
564
  Options:
579
- -s, --source <a,b> Restrict search/ls to sources (comma-separated; "todo" = TODO messages)
580
- --todo <id> Restrict a todo-message search to one TODO
581
- -B, --budget <n> Token budget for \`live\` (default 1500, 100–20000)
582
565
  --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
586
- --meta <json> Attach JSON metadata to the row (e.g. '{"todoId":"..."}')
587
- -n, --limit <n> Max rows (default 5, max 50)
566
+ --todo <id> Search one TODO only
567
+ --exclude <address> Leave out an already-read message (repeatable), to page past it
568
+ --before <n> around: messages before the pivot (default 2, max 200)
569
+ --after <n> around: messages after the pivot (default 2, max 200); large = read the rest
570
+ -n, --limit <n> Max results (default 5, max 50)
588
571
  -j, --json Machine-readable output
589
572
  --api-token <token> Override API token
590
- --api-url <url> Override backend URL (used to derive memory URL)
591
- --memory-url <url> Override memory-manager URL (default: derived — api.<d> → memory.<d>; localhost/IP → :8900)
573
+ --api-url <url> Override backend URL
592
574
  -h, --help Show this help
593
575
  -v, --version Show version
594
576
 
595
577
  Examples:
596
- tfa-memory add "postgres prod runs on hetzner-3, failover is manual"
597
- tfa-memory add deploy-notes/rollback "run scripts/rollback.sh, then verify /health"
598
- cat docs/auth.md | tfa-memory add acme/docs/auth.md -
599
578
  tfa-memory search "rollback procedure"
600
- tfa-memory search "how do we deploy" --mode semantic
601
- tfa-memory search "android build" --source todo
602
- tfa-memory live
603
- tfa-memory live -B 800
579
+ tfa-memory search "how do we deploy" --mode fused
604
580
  tfa-memory get todo/<todoId>:<messageId>
605
- tfa-memory ls --source deploy-notes
606
- tfa-memory get deploy-notes/rollback
607
- tfa-memory rm deploy-notes/rollback
608
- tfa-memory rm --source acme
609
- tfa-memory sources
581
+ tfa-memory around todo/<todoId>:<messageId> --before 5 --after 20
610
582
  `);
611
583
  }
612
584
  function die(msg, code = 1) {
@@ -614,35 +586,15 @@ function die(msg, code = 1) {
614
586
  process.exit(code);
615
587
  }
616
588
  function splitAddr(addr) {
617
- const i = addr.indexOf("/");
618
- if (i < 1 || i === addr.length - 1)
619
- die(`invalid address "${addr}" — expected source/ref`);
620
- return [addr.slice(0, i), addr.slice(i + 1)];
621
- }
622
- function deriveMemoryUrl(apiUrl) {
623
- const u = new URL(apiUrl);
624
- if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || /^\d{1,3}(\.\d{1,3}){3}$/.test(u.hostname)) {
625
- u.port = "8900";
626
- } else if (u.hostname.startsWith("api.")) {
627
- u.hostname = "memory." + u.hostname.slice(4);
628
- } else {
629
- u.hostname = "memory." + u.hostname;
630
- }
631
- return u.origin;
632
- }
633
- function resolveMemoryUrl(flag, apiUrl) {
634
- return flag || process.env.MEMORY_URL || deriveMemoryUrl(apiUrl);
635
- }
636
- async function api(ctx, method, path2, body) {
637
- const init = {
638
- method,
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
- },
643
- body: body !== undefined ? JSON.stringify(body) : undefined
644
- };
645
- const { res, token } = await fetchWithRenew(`${ctx.memoryUrl}/v1${path2}`, init, { apiUrl: ctx.apiUrl, token: ctx.token, renewOn: [401, 403] });
589
+ const m = /^todo\/([^:/]+):([^:/]+)$/.exec(addr);
590
+ if (!m)
591
+ die(`invalid address "${addr}" — expected todo/<todoId>:<messageId>`, 2);
592
+ return { todoId: m[1], messageId: m[2] };
593
+ }
594
+ var mount = (token) => token.startsWith("dst_") ? "/dst/v1" : "/api/v1";
595
+ async function api(ctx, path2) {
596
+ const headers = process.env.TODOFORAI_TODO_ID ? { "x-tfa-todo-id": process.env.TODOFORAI_TODO_ID } : undefined;
597
+ const { res, token } = await fetchWithRenew(`${ctx.apiUrl}${mount(ctx.token)}${path2}`, { headers }, { apiUrl: ctx.apiUrl, token: ctx.token });
646
598
  ctx.token = token;
647
599
  if (!res.ok) {
648
600
  const text = await res.text();
@@ -650,28 +602,33 @@ async function api(ctx, method, path2, body) {
650
602
  try {
651
603
  msg = JSON.parse(text)?.message ?? text;
652
604
  } catch {}
653
- die(`HTTP ${res.status}: ${msg}`);
605
+ die(res.status === 403 || res.status === 404 ? `not found or not yours (${res.status})` : `HTTP ${res.status}: ${msg}`);
654
606
  }
655
- return res.status === 204 ? null : await res.json();
607
+ return await res.json();
656
608
  }
609
+ var messageText = (m) => [m.content, ...(m.blocks ?? []).flatMap((b) => [b.content, b.cmd, b.path])].filter((p) => !!p?.trim()).join(`
610
+ `);
611
+ var rowOf = (todoId, m) => ({ address: `todo/${todoId}:${m.id}`, createdAt: m.createdAt, content: messageText(m), meta: { projectId: m.projectId, role: m.role } });
657
612
  function printRows(rows, json, snippet = 200) {
658
613
  if (json)
659
- return console.log(JSON.stringify(rows.map(({ source: _s, ref: _r, ...row }) => row), null, process.stdout.isTTY ? 2 : 0));
614
+ return console.log(JSON.stringify(rows, null, process.stdout.isTTY ? 2 : 0));
660
615
  if (!rows.length)
661
616
  return console.log("(no results)");
662
- for (const r of rows) {
663
- const oneline = r.content.replace(/\s+/g, " ").slice(0, snippet);
664
- console.log(`${r.address} ${new Date(r.createdAt).toISOString().slice(0, 16)} ${oneline}`);
665
- }
617
+ for (const r of rows)
618
+ console.log(`${r.address} ${new Date(r.createdAt).toISOString().slice(0, 16)} ${r.content.replace(/\s+/g, " ").slice(0, snippet)}`);
666
619
  }
667
620
  var qs = (params) => {
668
621
  const p = new URLSearchParams;
669
622
  for (const [k, v] of Object.entries(params))
670
623
  if (v !== undefined)
671
624
  p.set(k, String(v));
672
- const s = p.toString();
673
- return s ? `?${s}` : "";
625
+ return p.size ? `?${p}` : "";
674
626
  };
627
+ async function around(ctx, addr, before, after) {
628
+ const { todoId, messageId } = splitAddr(addr);
629
+ const res = await api(ctx, `/todos/${encodeURIComponent(todoId)}/messages/around${qs({ messageId, before, after })}`);
630
+ return { ...res, rows: res.messages.map((m) => rowOf(todoId, m)) };
631
+ }
675
632
  async function main() {
676
633
  await handleLogin(process.argv.slice(2), "tfa-memory");
677
634
  const { values, positionals } = parseArgs({
@@ -679,8 +636,6 @@ async function main() {
679
636
  options: {
680
637
  source: { type: "string", short: "s" },
681
638
  todo: { type: "string" },
682
- budget: { type: "string", short: "B" },
683
- meta: { type: "string" },
684
639
  mode: { type: "string" },
685
640
  exclude: { type: "string", multiple: true },
686
641
  before: { type: "string" },
@@ -689,7 +644,6 @@ async function main() {
689
644
  json: { type: "boolean", short: "j", default: false },
690
645
  "api-token": { type: "string" },
691
646
  "api-url": { type: "string" },
692
- "memory-url": { type: "string" },
693
647
  help: { type: "boolean", short: "h", default: false },
694
648
  version: { type: "boolean", short: "v", default: false }
695
649
  },
@@ -701,7 +655,7 @@ async function main() {
701
655
  return;
702
656
  }
703
657
  if (values.version) {
704
- console.log(`tfa-memory ${VERSION}`);
658
+ console.log(`tfa-memory ${package_default.version}`);
705
659
  return;
706
660
  }
707
661
  const [cmd, ...rest] = positionals;
@@ -709,19 +663,23 @@ async function main() {
709
663
  printHelp();
710
664
  process.exit(2);
711
665
  }
666
+ if (["add", "ls", "rm", "delete", "del", "live", "sources"].includes(cmd))
667
+ die(`\`${cmd}\` was removed. ${MEMORY_FILES}`, 2);
712
668
  if (cmd === "whoami")
713
669
  process.exit(await whoami({
714
670
  binName: "tfa-memory",
715
671
  apiUrl: values["api-url"],
716
672
  apiToken: values["api-token"]
717
673
  }));
674
+ if (values.source !== undefined && values.source !== "todo")
675
+ die(`only past TODO messages are searchable. ${MEMORY_FILES}`, 2);
718
676
  let ctx;
719
677
  try {
720
678
  const { apiUrl, apiToken } = resolveToken({
721
679
  apiUrl: values["api-url"],
722
680
  apiToken: values["api-token"]
723
681
  });
724
- ctx = { apiUrl, memoryUrl: resolveMemoryUrl(values["memory-url"], apiUrl), token: apiToken };
682
+ ctx = { apiUrl, token: apiToken };
725
683
  } catch (e) {
726
684
  die(e?.message ?? String(e));
727
685
  }
@@ -729,125 +687,38 @@ async function main() {
729
687
  const limit = values.limit !== undefined ? Number(values.limit) : undefined;
730
688
  if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 50))
731
689
  die(`--limit must be an integer from 1 to 50`, 2);
732
- const sources = values.source;
733
690
  switch (cmd) {
734
- case "add": {
735
- let source = "manual", ref = new Date().toISOString().slice(0, 19), text;
736
- if (rest.length >= 2 && rest[0].includes("/")) {
737
- [source, ref] = splitAddr(rest[0]);
738
- text = rest[1];
739
- } else if (rest.length === 1) {
740
- text = rest[0];
741
- } else
742
- die(`usage: tfa-memory add [source/ref] "text" | -`, 2);
743
- if (text === "-")
744
- text = readFileSync4(0, "utf8");
745
- text = text.trim();
746
- if (!text)
747
- die("empty content");
748
- let meta;
749
- if (values.meta !== undefined) {
750
- try {
751
- meta = JSON.parse(values.meta);
752
- } catch {
753
- die(`--meta must be valid JSON`, 2);
754
- }
755
- if (typeof meta !== "object" || meta === null || Array.isArray(meta))
756
- die(`--meta must be a JSON object`, 2);
757
- }
758
- const res = await api(ctx, "POST", "/memory", { source, ref, content: text, meta });
759
- for (const w of res.warnings ?? [])
760
- console.error(`Warning: ${w}`);
761
- console.log(json ? JSON.stringify(res) : `${res.status} ${res.address}`);
762
- break;
763
- }
764
691
  case "search": {
765
692
  const query = rest[0] ?? die(`usage: tfa-memory search "query"`, 2);
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 })}`);
768
- for (const w of res.warnings ?? [])
769
- console.error(`Warning: ${w}`);
770
- if (res.lexicalCount !== undefined)
771
- console.error(`${res.lexicalCount} row(s) contain a query word`);
772
- printRows(res.rows, json);
773
- break;
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
- }
788
- case "ls": {
789
- const res = await api(ctx, "GET", `/memory${qs({ sources, limit })}`);
790
- printRows(res.rows, json);
693
+ const exclude = values.exclude?.map((a) => splitAddr(a).messageId).join(",");
694
+ const res = await api(ctx, `/messages/search${qs({ query, mode: values.mode, limit: limit ?? 5, todoId: values.todo, exclude })}`);
695
+ printRows(res.hits.map((h) => ({ address: `todo/${h.todoId}:${h.messageId}`, createdAt: h.createdAt, content: h.snippet, meta: { projectId: h.projectId, role: h.role } })), json);
791
696
  break;
792
697
  }
793
698
  case "get": {
794
- const [source, ref] = splitAddr(rest[0] ?? die("address required (source/ref)", 2));
795
- const res = await api(ctx, "GET", `/memory/get${qs({ source, ref })}`);
699
+ const addr = rest[0] ?? die("address required (todo/<todoId>:<messageId>)", 2);
700
+ const [row] = (await around(ctx, addr, "0", "0")).rows;
796
701
  if (json)
797
- console.log(JSON.stringify(res, null, 2));
702
+ console.log(JSON.stringify(row, null, 2));
798
703
  else
799
- process.stdout.write(res.content + `
704
+ process.stdout.write(row.content + `
800
705
  `);
801
706
  break;
802
707
  }
803
708
  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}`);
709
+ const res = await around(ctx, rest[0] ?? die("address required (todo/<todoId>:<messageId>)", 2), values.before, values.after);
710
+ console.error(`message ${res.index + 1} of ${res.total}`);
808
711
  printRows(res.rows, json, 2000);
809
712
  break;
810
713
  }
811
- case "rm":
812
- case "delete":
813
- case "del": {
814
- let body;
815
- if (rest[0]) {
816
- const [source, ref] = splitAddr(rest[0]);
817
- body = { source, ref };
818
- } else if (sources && !sources.includes(","))
819
- body = { source: sources };
820
- else
821
- die("usage: tfa-memory rm <source/ref> | tfa-memory rm --source <name>", 2);
822
- const res = await api(ctx, "POST", "/memory/remove", body);
823
- console.log(json ? JSON.stringify(res) : `removed ${res.removed}`);
824
- break;
825
- }
826
- case "sources": {
827
- const res = await api(ctx, "GET", "/memory/sources");
828
- if (json) {
829
- console.log(JSON.stringify(res.sources, null, 2));
830
- break;
831
- }
832
- if (!res.sources.length) {
833
- console.log("(no sources)");
834
- break;
835
- }
836
- const w = Math.max(...res.sources.map((s) => s.source.length));
837
- for (const s of res.sources.sort((a, b) => (b.lastWrite ?? Infinity) - (a.lastWrite ?? Infinity)))
838
- console.log(`${s.source.padEnd(w)} ${String(s.count ?? "live").padStart(4)} ${s.lastWrite ? new Date(s.lastWrite).toISOString().slice(0, 16) : "live"}`);
839
- break;
840
- }
841
714
  default:
842
715
  die(`unknown command: ${cmd} (try 'tfa-memory --help')`, 2);
843
716
  }
844
717
  }
845
- var VERSION = package_default.version;
846
718
  if (isEntrypoint) {
847
719
  checkForUpdates({ name: package_default.name, version: package_default.version });
848
720
  main();
849
721
  }
850
722
  export {
851
- resolveMemoryUrl,
852
- deriveMemoryUrl
723
+ splitAddr
853
724
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@todoforai/tfa-memory",
3
- "version": "0.4.5",
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.",
3
+ "version": "0.5.0",
4
+ "description": "Zero-config TODOforAI CLI to search past TODO conversations (full-text and semantic), print one message or read the conversation around it. Read-only; memory itself lives in project files. Reuses bridge credentials.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "tfa-memory": "dist/cli.js"