@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.
- package/dist/cli.js +88 -12
- 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)
|
|
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)
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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.
|
|
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"
|