@todoforai/tfa-memory 0.1.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.
- package/dist/cli.js +715 -0
- package/package.json +29 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// src/cli.ts
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
8
|
+
|
|
9
|
+
// node_modules/@todoforai/tfa-subagent/dist/auth.js
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
11
|
+
import { join, dirname } from "node:path";
|
|
12
|
+
import { homedir, platform } from "node:os";
|
|
13
|
+
var DEFAULT_API_URL = "https://api.todofor.ai";
|
|
14
|
+
function normalizeApiUrl(url) {
|
|
15
|
+
if (url.startsWith("localhost"))
|
|
16
|
+
return `http://${url}`;
|
|
17
|
+
if (!url.startsWith("http://") && !url.startsWith("https://"))
|
|
18
|
+
return `https://${url}`;
|
|
19
|
+
return url.replace(/\/+$/, "");
|
|
20
|
+
}
|
|
21
|
+
function credentialsPath() {
|
|
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
|
+
}
|
|
24
|
+
function credentialsPaths() {
|
|
25
|
+
return [credentialsPath()];
|
|
26
|
+
}
|
|
27
|
+
function writeSecure(p, data) {
|
|
28
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
29
|
+
writeFileSync(p, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
30
|
+
try {
|
|
31
|
+
chmodSync(p, 384);
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
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: "" };
|
|
50
|
+
}
|
|
51
|
+
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
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function refreshDeviceToken(opts = {}) {
|
|
62
|
+
const envUrl = process.env.TODOFORAI_API_URL || process.env.API_URL;
|
|
63
|
+
const initialApiUrl = normalizeApiUrl(opts.apiUrl || envUrl || DEFAULT_API_URL);
|
|
64
|
+
const stored = readStoredAuth(initialApiUrl);
|
|
65
|
+
const apiUrl = normalizeApiUrl(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL);
|
|
66
|
+
if (!stored.deviceId || !stored.deviceSecret)
|
|
67
|
+
return "";
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "content-type": "application/json" },
|
|
72
|
+
body: JSON.stringify({ deviceId: stored.deviceId, secret: stored.deviceSecret })
|
|
73
|
+
});
|
|
74
|
+
if (!res.ok)
|
|
75
|
+
return "";
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
if (!data.token)
|
|
78
|
+
return "";
|
|
79
|
+
persistApiToken(data.token);
|
|
80
|
+
return data.token;
|
|
81
|
+
} catch {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function fetchWithRenew(url, init, opts) {
|
|
86
|
+
const header = opts.header ?? "x-api-key";
|
|
87
|
+
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_")) {
|
|
91
|
+
const fresh = await refreshDeviceToken({ apiUrl: opts.apiUrl });
|
|
92
|
+
if (fresh && fresh !== opts.token)
|
|
93
|
+
return { res: await send(fresh), token: fresh };
|
|
94
|
+
}
|
|
95
|
+
return { res, token: opts.token };
|
|
96
|
+
}
|
|
97
|
+
function resolveToken(opts = {}) {
|
|
98
|
+
const envUrl = process.env.TODOFORAI_API_URL || process.env.API_URL;
|
|
99
|
+
const initialApiUrl = normalizeApiUrl(opts.apiUrl || envUrl || DEFAULT_API_URL);
|
|
100
|
+
const stored = readStoredAuth(initialApiUrl);
|
|
101
|
+
const apiUrl = normalizeApiUrl(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL);
|
|
102
|
+
const apiToken = opts.apiToken || process.env.TODOFORAI_API_TOKEN || stored.apiToken || "";
|
|
103
|
+
if (!apiToken)
|
|
104
|
+
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
|
+
return { apiUrl, apiToken };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// node_modules/@todoforai/tfa-subagent/dist/login.js
|
|
109
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, chmodSync as chmodSync2 } from "node:fs";
|
|
110
|
+
import { join as join2, dirname as dirname2 } from "node:path";
|
|
111
|
+
import { homedir as homedir2, platform as platform2 } from "node:os";
|
|
112
|
+
import { spawn } from "node:child_process";
|
|
113
|
+
import { platform as platform22, hostname } from "node:os";
|
|
114
|
+
var DEFAULT_API_URL2 = "https://api.todofor.ai";
|
|
115
|
+
function normalizeApiUrl2(url) {
|
|
116
|
+
if (url.startsWith("localhost"))
|
|
117
|
+
return `http://${url}`;
|
|
118
|
+
if (!url.startsWith("http://") && !url.startsWith("https://"))
|
|
119
|
+
return `https://${url}`;
|
|
120
|
+
return url.replace(/\/+$/, "");
|
|
121
|
+
}
|
|
122
|
+
function credentialsPath2() {
|
|
123
|
+
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
|
+
}
|
|
125
|
+
function credentialsPaths2() {
|
|
126
|
+
return [credentialsPath2()];
|
|
127
|
+
}
|
|
128
|
+
function writeSecure2(p, data) {
|
|
129
|
+
mkdirSync2(dirname2(p), { recursive: true });
|
|
130
|
+
writeFileSync2(p, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
131
|
+
try {
|
|
132
|
+
chmodSync2(p, 384);
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
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 });
|
|
142
|
+
}
|
|
143
|
+
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: "" };
|
|
159
|
+
}
|
|
160
|
+
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
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function refreshDeviceToken2(opts = {}) {
|
|
171
|
+
const envUrl = process.env.TODOFORAI_API_URL || process.env.API_URL;
|
|
172
|
+
const initialApiUrl = normalizeApiUrl2(opts.apiUrl || envUrl || DEFAULT_API_URL2);
|
|
173
|
+
const stored = readStoredAuth2(initialApiUrl);
|
|
174
|
+
const apiUrl = normalizeApiUrl2(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL2);
|
|
175
|
+
if (!stored.deviceId || !stored.deviceSecret)
|
|
176
|
+
return "";
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "content-type": "application/json" },
|
|
181
|
+
body: JSON.stringify({ deviceId: stored.deviceId, secret: stored.deviceSecret })
|
|
182
|
+
});
|
|
183
|
+
if (!res.ok)
|
|
184
|
+
return "";
|
|
185
|
+
const data = await res.json();
|
|
186
|
+
if (!data.token)
|
|
187
|
+
return "";
|
|
188
|
+
persistApiToken2(data.token);
|
|
189
|
+
return data.token;
|
|
190
|
+
} catch {
|
|
191
|
+
return "";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function defaultDeviceName() {
|
|
195
|
+
const n = hostname().replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^[-_]+/, "").slice(0, 64);
|
|
196
|
+
return n || undefined;
|
|
197
|
+
}
|
|
198
|
+
var POLL_INTERVAL_MS = 2000;
|
|
199
|
+
function openBrowser(url) {
|
|
200
|
+
const cmd = platform22() === "darwin" ? "open" : platform22() === "win32" ? "cmd" : "xdg-open";
|
|
201
|
+
const args = platform22() === "win32" ? ["/c", "start", "", url] : [url];
|
|
202
|
+
try {
|
|
203
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
204
|
+
} catch {}
|
|
205
|
+
}
|
|
206
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
207
|
+
async function login(opts) {
|
|
208
|
+
const apiUrl = normalizeApiUrl2(opts.apiUrl || process.env.TODOFORAI_API_URL || process.env.API_URL || DEFAULT_API_URL2);
|
|
209
|
+
let init;
|
|
210
|
+
try {
|
|
211
|
+
const res = await fetch(`${apiUrl}/api/v1/cli/login/init`, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: { "content-type": "application/json" },
|
|
214
|
+
body: JSON.stringify({ clientName: "cli", deviceName: opts.deviceName || defaultDeviceName() })
|
|
215
|
+
});
|
|
216
|
+
if (!res.ok) {
|
|
217
|
+
console.error(`${opts.binName}: login init failed (HTTP ${res.status})`);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
init = await res.json();
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.error(`${opts.binName}: login init failed: ${e?.message ?? e}`);
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
console.log(`
|
|
226
|
+
To authenticate, open this URL and approve:
|
|
227
|
+
|
|
228
|
+
${init.url}
|
|
229
|
+
`);
|
|
230
|
+
openBrowser(init.url);
|
|
231
|
+
console.log("Waiting for approval…");
|
|
232
|
+
const deadline = Date.now() + init.expiresIn * 1000;
|
|
233
|
+
while (Date.now() < deadline) {
|
|
234
|
+
await sleep(POLL_INTERVAL_MS);
|
|
235
|
+
let poll;
|
|
236
|
+
try {
|
|
237
|
+
const res = await fetch(`${apiUrl}/api/v1/cli/login/poll?code=${encodeURIComponent(init.code)}`, {
|
|
238
|
+
headers: { "content-type": "application/json" }
|
|
239
|
+
});
|
|
240
|
+
if (!res.ok)
|
|
241
|
+
continue;
|
|
242
|
+
poll = await res.json();
|
|
243
|
+
} catch {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (poll.status === "pending")
|
|
247
|
+
continue;
|
|
248
|
+
if (poll.status === "expired") {
|
|
249
|
+
console.error(`${opts.binName}: login code expired — run login again`);
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
if (!poll.device) {
|
|
253
|
+
console.error(`${opts.binName}: login completed without device credentials`);
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
writeCredentials({
|
|
257
|
+
deviceId: poll.device.id,
|
|
258
|
+
deviceSecret: poll.device.secret,
|
|
259
|
+
deviceName: poll.device.name,
|
|
260
|
+
apiUrl,
|
|
261
|
+
backendHost: new URL(apiUrl).hostname,
|
|
262
|
+
...poll.apiToken ? { apiToken: poll.apiToken } : {},
|
|
263
|
+
...poll.user ? { userId: poll.user.id, userEmail: poll.user.email, ...poll.user.name ? { userName: poll.user.name } : {} } : {}
|
|
264
|
+
});
|
|
265
|
+
if (!poll.apiToken) {
|
|
266
|
+
const minted = await refreshDeviceToken2({ apiUrl });
|
|
267
|
+
if (!minted) {
|
|
268
|
+
console.error(`${opts.binName}: approved, but failed to mint a session token — try again`);
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
console.log(`
|
|
273
|
+
${opts.binName}: authenticated as ${poll.user?.email ?? poll.user?.id ?? "(unknown)"} (device ${poll.device.name})`);
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
console.error(`${opts.binName}: timed out waiting for approval`);
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
async function handleLogin(argv, binName) {
|
|
280
|
+
if (argv[0] !== "login")
|
|
281
|
+
return false;
|
|
282
|
+
const rest = argv.slice(1);
|
|
283
|
+
const get = (flag) => {
|
|
284
|
+
const i = rest.indexOf(flag);
|
|
285
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
286
|
+
};
|
|
287
|
+
process.exit(await login({ binName, deviceName: get("--device-name"), apiUrl: get("--api-url") }));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// node_modules/@todoforai/tfa-subagent/dist/whoami.js
|
|
291
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, chmodSync as chmodSync3 } from "node:fs";
|
|
292
|
+
import { join as join3, dirname as dirname3 } from "node:path";
|
|
293
|
+
import { homedir as homedir3, platform as platform3 } from "node:os";
|
|
294
|
+
var DEFAULT_API_URL3 = "https://api.todofor.ai";
|
|
295
|
+
function normalizeApiUrl3(url) {
|
|
296
|
+
if (url.startsWith("localhost"))
|
|
297
|
+
return `http://${url}`;
|
|
298
|
+
if (!url.startsWith("http://") && !url.startsWith("https://"))
|
|
299
|
+
return `https://${url}`;
|
|
300
|
+
return url.replace(/\/+$/, "");
|
|
301
|
+
}
|
|
302
|
+
function credentialsPath3() {
|
|
303
|
+
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
|
+
}
|
|
305
|
+
function credentialsPaths3() {
|
|
306
|
+
return [credentialsPath3()];
|
|
307
|
+
}
|
|
308
|
+
function writeSecure3(p, data) {
|
|
309
|
+
mkdirSync3(dirname3(p), { recursive: true });
|
|
310
|
+
writeFileSync3(p, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
311
|
+
try {
|
|
312
|
+
chmodSync3(p, 384);
|
|
313
|
+
} catch {}
|
|
314
|
+
}
|
|
315
|
+
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
|
+
}
|
|
332
|
+
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
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function refreshDeviceToken3(opts = {}) {
|
|
343
|
+
const envUrl = process.env.TODOFORAI_API_URL || process.env.API_URL;
|
|
344
|
+
const initialApiUrl = normalizeApiUrl3(opts.apiUrl || envUrl || DEFAULT_API_URL3);
|
|
345
|
+
const stored = readStoredAuth3(initialApiUrl);
|
|
346
|
+
const apiUrl = normalizeApiUrl3(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL3);
|
|
347
|
+
if (!stored.deviceId || !stored.deviceSecret)
|
|
348
|
+
return "";
|
|
349
|
+
try {
|
|
350
|
+
const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
|
|
351
|
+
method: "POST",
|
|
352
|
+
headers: { "content-type": "application/json" },
|
|
353
|
+
body: JSON.stringify({ deviceId: stored.deviceId, secret: stored.deviceSecret })
|
|
354
|
+
});
|
|
355
|
+
if (!res.ok)
|
|
356
|
+
return "";
|
|
357
|
+
const data = await res.json();
|
|
358
|
+
if (!data.token)
|
|
359
|
+
return "";
|
|
360
|
+
persistApiToken3(data.token);
|
|
361
|
+
return data.token;
|
|
362
|
+
} catch {
|
|
363
|
+
return "";
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function fetchWithRenew2(url, init, opts) {
|
|
367
|
+
const header = opts.header ?? "x-api-key";
|
|
368
|
+
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_")) {
|
|
372
|
+
const fresh = await refreshDeviceToken3({ apiUrl: opts.apiUrl });
|
|
373
|
+
if (fresh && fresh !== opts.token)
|
|
374
|
+
return { res: await send(fresh), token: fresh };
|
|
375
|
+
}
|
|
376
|
+
return { res, token: opts.token };
|
|
377
|
+
}
|
|
378
|
+
function resolveToken2(opts = {}) {
|
|
379
|
+
const envUrl = process.env.TODOFORAI_API_URL || process.env.API_URL;
|
|
380
|
+
const initialApiUrl = normalizeApiUrl3(opts.apiUrl || envUrl || DEFAULT_API_URL3);
|
|
381
|
+
const stored = readStoredAuth3(initialApiUrl);
|
|
382
|
+
const apiUrl = normalizeApiUrl3(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL3);
|
|
383
|
+
const apiToken = opts.apiToken || process.env.TODOFORAI_API_TOKEN || stored.apiToken || "";
|
|
384
|
+
if (!apiToken)
|
|
385
|
+
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
|
+
return { apiUrl, apiToken };
|
|
387
|
+
}
|
|
388
|
+
async function whoami(opts) {
|
|
389
|
+
let apiUrl;
|
|
390
|
+
let apiToken;
|
|
391
|
+
try {
|
|
392
|
+
({ apiUrl, apiToken } = resolveToken2({ apiUrl: opts.apiUrl, apiToken: opts.apiToken }));
|
|
393
|
+
} catch {
|
|
394
|
+
console.log(`${opts.binName}: not authenticated`);
|
|
395
|
+
return 1;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const { res } = await fetchWithRenew2(`${apiUrl}/dst/v1/users/profile`, {}, { apiUrl, token: apiToken });
|
|
399
|
+
if (!res.ok) {
|
|
400
|
+
console.log(`${opts.binName}: not authenticated`);
|
|
401
|
+
return 1;
|
|
402
|
+
}
|
|
403
|
+
const body = await res.json();
|
|
404
|
+
console.log(`${opts.binName}: authenticated as ${body.user?.id ?? "(unknown)"}`);
|
|
405
|
+
return 0;
|
|
406
|
+
} catch {
|
|
407
|
+
console.log(`${opts.binName}: not authenticated`);
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// node_modules/@todoforai/tfa-subagent/dist/update-notifier.js
|
|
413
|
+
import fs from "node:fs";
|
|
414
|
+
import path from "node:path";
|
|
415
|
+
import os from "node:os";
|
|
416
|
+
function isLinkedInstall() {
|
|
417
|
+
try {
|
|
418
|
+
return !fs.realpathSync(process.argv[1] || "").includes(`${path.sep}node_modules${path.sep}`);
|
|
419
|
+
} catch {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
var TTL_MS = 24 * 60 * 60 * 1000;
|
|
424
|
+
var CACHE_DIR = path.join(os.homedir(), ".config", "todoforai");
|
|
425
|
+
function cmpVer(a, b) {
|
|
426
|
+
const pa = a.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
|
|
427
|
+
const pb = b.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
|
|
428
|
+
for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
|
|
429
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
430
|
+
if (d)
|
|
431
|
+
return d;
|
|
432
|
+
}
|
|
433
|
+
return 0;
|
|
434
|
+
}
|
|
435
|
+
function checkForUpdates(pkg) {
|
|
436
|
+
if (!process.stderr.isTTY || process.env.CI || process.env.NO_UPDATE_NOTIFIER || isLinkedInstall())
|
|
437
|
+
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 {}
|
|
443
|
+
if (cache.latest && cmpVer(cache.latest, pkg.version) > 0) {
|
|
444
|
+
process.stderr.write(`
|
|
445
|
+
\x1B[33m Update available: \x1B[2m${pkg.version}\x1B[22m → \x1B[1m${cache.latest}\x1B[0m
|
|
446
|
+
` + `\x1B[33m Run:\x1B[0m npm i -g ${pkg.name}
|
|
447
|
+
|
|
448
|
+
`);
|
|
449
|
+
}
|
|
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
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// package.json
|
|
463
|
+
var package_default = {
|
|
464
|
+
name: "@todoforai/tfa-memory",
|
|
465
|
+
version: "0.1.0",
|
|
466
|
+
description: "Zero-config TODOforAI memory CLI — add/search/ls/get/rm text memory rows addressed as source/ref, with full-text and semantic search. Reuses bridge credentials.",
|
|
467
|
+
type: "module",
|
|
468
|
+
bin: {
|
|
469
|
+
"tfa-memory": "dist/cli.js"
|
|
470
|
+
},
|
|
471
|
+
scripts: {
|
|
472
|
+
build: "bun build src/cli.ts --outdir dist --target node --format esm",
|
|
473
|
+
dev: "bun run src/cli.ts",
|
|
474
|
+
prepublishOnly: "bun run build"
|
|
475
|
+
},
|
|
476
|
+
devDependencies: {
|
|
477
|
+
"@todoforai/tfa-subagent": "^0.1.9",
|
|
478
|
+
"@types/node": "^20.11.0"
|
|
479
|
+
},
|
|
480
|
+
keywords: [
|
|
481
|
+
"cli",
|
|
482
|
+
"memory",
|
|
483
|
+
"search",
|
|
484
|
+
"todoforai"
|
|
485
|
+
],
|
|
486
|
+
author: "TODO for AI",
|
|
487
|
+
license: "MIT",
|
|
488
|
+
files: [
|
|
489
|
+
"dist"
|
|
490
|
+
]
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// src/cli.ts
|
|
494
|
+
var isEntrypoint = __require.main == __require.module === true || !!process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
|
|
495
|
+
function printHelp() {
|
|
496
|
+
console.log(`tfa-memory — TODOforAI memory CLI (addressable, searchable text rows).
|
|
497
|
+
|
|
498
|
+
Every row's address is \`source/ref\` — sources are directories, refs are
|
|
499
|
+
filenames. A source exists once the first row names it. Re-adding the same
|
|
500
|
+
address updates in place; identical content is skipped (no re-embed).
|
|
501
|
+
|
|
502
|
+
Usage:
|
|
503
|
+
tfa-memory <command> [options] [args...]
|
|
504
|
+
|
|
505
|
+
Commands:
|
|
506
|
+
add [source/ref] <text|-> Save a row. Without an address → manual/<timestamp>.
|
|
507
|
+
"-" reads stdin.
|
|
508
|
+
search <query> Full-text search (newest first)
|
|
509
|
+
ls List rows, newest first
|
|
510
|
+
get <source/ref> Print full content of one row
|
|
511
|
+
rm <source/ref> Delete one row
|
|
512
|
+
rm --source <name> Delete every row in a source
|
|
513
|
+
sources List sources with counts and last write
|
|
514
|
+
login Device login — mint & persist credentials
|
|
515
|
+
whoami Print resolved auth context
|
|
516
|
+
|
|
517
|
+
Options:
|
|
518
|
+
-s, --source <a,b> Restrict search/ls to sources (comma-separated)
|
|
519
|
+
--semantic Embedding KNN search (meaning, not keywords)
|
|
520
|
+
-n, --limit <n> Max rows (default 10, max 50)
|
|
521
|
+
-j, --json Machine-readable output
|
|
522
|
+
--api-token <token> Override API token
|
|
523
|
+
--api-url <url> Override backend URL
|
|
524
|
+
-h, --help Show this help
|
|
525
|
+
-v, --version Show version
|
|
526
|
+
|
|
527
|
+
Examples:
|
|
528
|
+
tfa-memory add "postgres prod runs on hetzner-3, failover is manual"
|
|
529
|
+
tfa-memory add deploy-notes/rollback "run scripts/rollback.sh, then verify /health"
|
|
530
|
+
cat docs/auth.md | tfa-memory add acme/docs/auth.md -
|
|
531
|
+
tfa-memory search "rollback procedure"
|
|
532
|
+
tfa-memory search "how do we deploy" --semantic
|
|
533
|
+
tfa-memory ls --source deploy-notes
|
|
534
|
+
tfa-memory get deploy-notes/rollback
|
|
535
|
+
tfa-memory rm deploy-notes/rollback
|
|
536
|
+
tfa-memory rm --source acme
|
|
537
|
+
tfa-memory sources
|
|
538
|
+
`);
|
|
539
|
+
}
|
|
540
|
+
function die(msg, code = 1) {
|
|
541
|
+
console.error(`Error: ${msg}`);
|
|
542
|
+
process.exit(code);
|
|
543
|
+
}
|
|
544
|
+
function splitAddr(addr) {
|
|
545
|
+
const i = addr.indexOf("/");
|
|
546
|
+
if (i < 1 || i === addr.length - 1)
|
|
547
|
+
die(`invalid address "${addr}" — expected source/ref`);
|
|
548
|
+
return [addr.slice(0, i), addr.slice(i + 1)];
|
|
549
|
+
}
|
|
550
|
+
async function api(ctx, method, path2, body) {
|
|
551
|
+
const init = {
|
|
552
|
+
method,
|
|
553
|
+
headers: body !== undefined ? { "content-type": "application/json" } : {},
|
|
554
|
+
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
555
|
+
};
|
|
556
|
+
const { res, token } = await fetchWithRenew(`${ctx.apiUrl}/dst/v1${path2}`, init, { apiUrl: ctx.apiUrl, token: ctx.token });
|
|
557
|
+
ctx.token = token;
|
|
558
|
+
if (!res.ok) {
|
|
559
|
+
const text = await res.text();
|
|
560
|
+
let msg = text;
|
|
561
|
+
try {
|
|
562
|
+
msg = JSON.parse(text)?.message ?? text;
|
|
563
|
+
} catch {}
|
|
564
|
+
die(`HTTP ${res.status}: ${msg}`);
|
|
565
|
+
}
|
|
566
|
+
return res.status === 204 ? null : await res.json();
|
|
567
|
+
}
|
|
568
|
+
function printRows(rows, json, snippet = 120) {
|
|
569
|
+
if (json)
|
|
570
|
+
return console.log(JSON.stringify(rows, null, 2));
|
|
571
|
+
if (!rows.length)
|
|
572
|
+
return console.log("(no results)");
|
|
573
|
+
for (const r of rows) {
|
|
574
|
+
const oneline = r.content.replace(/\s+/g, " ").slice(0, snippet);
|
|
575
|
+
console.log(`${r.address} ${new Date(r.createdAt).toISOString().slice(0, 16)} ${oneline}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
var qs = (params) => {
|
|
579
|
+
const p = new URLSearchParams;
|
|
580
|
+
for (const [k, v] of Object.entries(params))
|
|
581
|
+
if (v !== undefined)
|
|
582
|
+
p.set(k, String(v));
|
|
583
|
+
const s = p.toString();
|
|
584
|
+
return s ? `?${s}` : "";
|
|
585
|
+
};
|
|
586
|
+
async function main() {
|
|
587
|
+
await handleLogin(process.argv.slice(2), "tfa-memory");
|
|
588
|
+
const { values, positionals } = parseArgs({
|
|
589
|
+
args: process.argv.slice(2),
|
|
590
|
+
options: {
|
|
591
|
+
source: { type: "string", short: "s" },
|
|
592
|
+
semantic: { type: "boolean", default: false },
|
|
593
|
+
limit: { type: "string", short: "n" },
|
|
594
|
+
json: { type: "boolean", short: "j", default: false },
|
|
595
|
+
"api-token": { type: "string" },
|
|
596
|
+
"api-url": { type: "string" },
|
|
597
|
+
help: { type: "boolean", short: "h", default: false },
|
|
598
|
+
version: { type: "boolean", short: "v", default: false }
|
|
599
|
+
},
|
|
600
|
+
allowPositionals: true,
|
|
601
|
+
strict: false
|
|
602
|
+
});
|
|
603
|
+
if (values.help) {
|
|
604
|
+
printHelp();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (values.version) {
|
|
608
|
+
console.log(`tfa-memory ${VERSION}`);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const [cmd, ...rest] = positionals;
|
|
612
|
+
if (!cmd) {
|
|
613
|
+
printHelp();
|
|
614
|
+
process.exit(2);
|
|
615
|
+
}
|
|
616
|
+
if (cmd === "whoami")
|
|
617
|
+
process.exit(await whoami({
|
|
618
|
+
binName: "tfa-memory",
|
|
619
|
+
apiUrl: values["api-url"],
|
|
620
|
+
apiToken: values["api-token"]
|
|
621
|
+
}));
|
|
622
|
+
let ctx;
|
|
623
|
+
try {
|
|
624
|
+
const { apiUrl, apiToken } = resolveToken({
|
|
625
|
+
apiUrl: values["api-url"],
|
|
626
|
+
apiToken: values["api-token"]
|
|
627
|
+
});
|
|
628
|
+
ctx = { apiUrl, token: apiToken };
|
|
629
|
+
} catch (e) {
|
|
630
|
+
die(e?.message ?? String(e));
|
|
631
|
+
}
|
|
632
|
+
const json = values.json;
|
|
633
|
+
const limit = values.limit !== undefined ? Number(values.limit) : undefined;
|
|
634
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1))
|
|
635
|
+
die(`--limit must be a positive integer`, 2);
|
|
636
|
+
const sources = values.source;
|
|
637
|
+
switch (cmd) {
|
|
638
|
+
case "add": {
|
|
639
|
+
let source = "manual", ref = new Date().toISOString().slice(0, 19), text;
|
|
640
|
+
if (rest.length >= 2 && rest[0].includes("/")) {
|
|
641
|
+
[source, ref] = splitAddr(rest[0]);
|
|
642
|
+
text = rest[1];
|
|
643
|
+
} else if (rest.length === 1) {
|
|
644
|
+
text = rest[0];
|
|
645
|
+
} else
|
|
646
|
+
die(`usage: tfa-memory add [source/ref] "text" | -`, 2);
|
|
647
|
+
if (text === "-")
|
|
648
|
+
text = readFileSync4(0, "utf8");
|
|
649
|
+
text = text.trim();
|
|
650
|
+
if (!text)
|
|
651
|
+
die("empty content");
|
|
652
|
+
const res = await api(ctx, "POST", "/memory", { source, ref, content: text });
|
|
653
|
+
console.log(json ? JSON.stringify(res) : `${res.status} ${res.address}`);
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
case "search": {
|
|
657
|
+
const query = rest[0] ?? die(`usage: tfa-memory search "query"`, 2);
|
|
658
|
+
const res = await api(ctx, "GET", `/memory/search${qs({ query, semantic: values.semantic ? true : undefined, sources, limit })}`);
|
|
659
|
+
printRows(res.rows, json, 200);
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
case "ls": {
|
|
663
|
+
const res = await api(ctx, "GET", `/memory${qs({ sources, limit })}`);
|
|
664
|
+
printRows(res.rows, json);
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
case "get": {
|
|
668
|
+
const [source, ref] = splitAddr(rest[0] ?? die("address required (source/ref)", 2));
|
|
669
|
+
const res = await api(ctx, "GET", `/memory/get${qs({ source, ref })}`);
|
|
670
|
+
if (json)
|
|
671
|
+
console.log(JSON.stringify(res, null, 2));
|
|
672
|
+
else
|
|
673
|
+
process.stdout.write(res.content + `
|
|
674
|
+
`);
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
case "rm":
|
|
678
|
+
case "delete":
|
|
679
|
+
case "del": {
|
|
680
|
+
let body;
|
|
681
|
+
if (rest[0]) {
|
|
682
|
+
const [source, ref] = splitAddr(rest[0]);
|
|
683
|
+
body = { source, ref };
|
|
684
|
+
} else if (sources && !sources.includes(","))
|
|
685
|
+
body = { source: sources };
|
|
686
|
+
else
|
|
687
|
+
die("usage: tfa-memory rm <source/ref> | tfa-memory rm --source <name>", 2);
|
|
688
|
+
const res = await api(ctx, "POST", "/memory/remove", body);
|
|
689
|
+
console.log(json ? JSON.stringify(res) : `removed ${res.removed}`);
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
case "sources": {
|
|
693
|
+
const res = await api(ctx, "GET", "/memory/sources");
|
|
694
|
+
if (json) {
|
|
695
|
+
console.log(JSON.stringify(res.sources, null, 2));
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
if (!res.sources.length) {
|
|
699
|
+
console.log("(no sources)");
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
const w = Math.max(...res.sources.map((s) => s.source.length));
|
|
703
|
+
for (const s of res.sources.sort((a, b) => b.lastWrite - a.lastWrite))
|
|
704
|
+
console.log(`${s.source.padEnd(w)} ${String(s.count).padStart(4)} ${new Date(s.lastWrite).toISOString().slice(0, 16)}`);
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
default:
|
|
708
|
+
die(`unknown command: ${cmd} (try 'tfa-memory --help')`, 2);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
var VERSION = package_default.version;
|
|
712
|
+
if (isEntrypoint) {
|
|
713
|
+
checkForUpdates({ name: package_default.name, version: package_default.version });
|
|
714
|
+
main();
|
|
715
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@todoforai/tfa-memory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-config TODOforAI memory CLI \u2014 add/search/ls/get/rm text memory rows addressed as source/ref, with full-text and semantic search. Reuses bridge credentials.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tfa-memory": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "bun build src/cli.ts --outdir dist --target node --format esm",
|
|
11
|
+
"dev": "bun run src/cli.ts",
|
|
12
|
+
"prepublishOnly": "bun run build"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@todoforai/tfa-subagent": "^0.1.9",
|
|
16
|
+
"@types/node": "^20.11.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"cli",
|
|
20
|
+
"memory",
|
|
21
|
+
"search",
|
|
22
|
+
"todoforai"
|
|
23
|
+
],
|
|
24
|
+
"author": "TODO for AI",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
]
|
|
29
|
+
}
|