@todoforai/tfa-surface 0.1.0 → 0.1.2
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 +153 -40
- package/package.json +12 -4
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
2
4
|
|
|
3
5
|
// src/cli.ts
|
|
4
6
|
import { parseArgs } from "node:util";
|
|
@@ -18,10 +20,15 @@ function normalizeApiUrl(url) {
|
|
|
18
20
|
function credentialsPath() {
|
|
19
21
|
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");
|
|
20
22
|
}
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
+
function read() {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(credentialsPath(), "utf-8"));
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
23
29
|
}
|
|
24
|
-
function writeSecure(
|
|
30
|
+
function writeSecure(data) {
|
|
31
|
+
const p = credentialsPath();
|
|
25
32
|
mkdirSync(dirname(p), { recursive: true });
|
|
26
33
|
writeFileSync(p, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
27
34
|
try {
|
|
@@ -29,31 +36,29 @@ function writeSecure(p, data) {
|
|
|
29
36
|
} catch {}
|
|
30
37
|
}
|
|
31
38
|
function readStoredAuth(apiUrl) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
return { apiToken: "" };
|
|
39
|
+
const c = read();
|
|
40
|
+
const host = c.backendHost || "";
|
|
41
|
+
const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
|
|
42
|
+
const dstIsForThisUrl = !storedApiUrl || normalizeApiUrl(storedApiUrl) === normalizeApiUrl(apiUrl);
|
|
43
|
+
const isDst = typeof c.apiToken === "string" && c.apiToken.startsWith("dst_");
|
|
44
|
+
const dst = dstIsForThisUrl && isDst ? c.apiToken : "";
|
|
45
|
+
const fallbackApiToken = isDst && !dstIsForThisUrl ? "" : c.apiToken;
|
|
46
|
+
const apiToken = dst || c[apiUrl] || fallbackApiToken || c.apiKey || "";
|
|
47
|
+
return {
|
|
48
|
+
apiToken: typeof apiToken === "string" ? apiToken : "",
|
|
49
|
+
apiUrl: storedApiUrl,
|
|
50
|
+
deviceId: typeof c.deviceId === "string" ? c.deviceId : undefined,
|
|
51
|
+
deviceSecret: typeof c.deviceSecret === "string" ? c.deviceSecret : undefined
|
|
52
|
+
};
|
|
47
53
|
}
|
|
48
54
|
function persistApiToken(token) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
55
|
+
const c = read();
|
|
56
|
+
if (!Object.keys(c).length)
|
|
57
|
+
return;
|
|
58
|
+
c.apiToken = token;
|
|
59
|
+
try {
|
|
60
|
+
writeSecure(c);
|
|
61
|
+
} catch {}
|
|
57
62
|
}
|
|
58
63
|
async function refreshDeviceToken(opts = {}) {
|
|
59
64
|
const envUrl = process.env.TODOFORAI_API_URL || process.env.API_URL;
|
|
@@ -62,6 +67,8 @@ async function refreshDeviceToken(opts = {}) {
|
|
|
62
67
|
const apiUrl = normalizeApiUrl(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL);
|
|
63
68
|
if (!stored.deviceId || !stored.deviceSecret)
|
|
64
69
|
return "";
|
|
70
|
+
if (stored.apiUrl && normalizeApiUrl(stored.apiUrl) !== apiUrl)
|
|
71
|
+
return "";
|
|
65
72
|
try {
|
|
66
73
|
const res = await fetch(`${apiUrl}/api/v1/cli/device/token`, {
|
|
67
74
|
method: "POST",
|
|
@@ -82,9 +89,13 @@ async function refreshDeviceToken(opts = {}) {
|
|
|
82
89
|
async function fetchWithRenew(url, init, opts) {
|
|
83
90
|
const header = opts.header ?? "x-api-key";
|
|
84
91
|
const renewOn = opts.renewOn ?? [401];
|
|
85
|
-
const send = (token) =>
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
const send = (token) => {
|
|
93
|
+
const headers = new Headers(init.headers);
|
|
94
|
+
headers.set(header, token);
|
|
95
|
+
return fetch(url, { ...init, headers });
|
|
96
|
+
};
|
|
97
|
+
const res = await send(opts.token);
|
|
98
|
+
if (renewOn.includes(res.status)) {
|
|
88
99
|
const fresh = await refreshDeviceToken({ apiUrl: opts.apiUrl });
|
|
89
100
|
if (fresh && fresh !== opts.token)
|
|
90
101
|
return { res: await send(fresh), token: fresh };
|
|
@@ -96,7 +107,8 @@ function resolveToken(opts = {}) {
|
|
|
96
107
|
const initialApiUrl = normalizeApiUrl(opts.apiUrl || envUrl || DEFAULT_API_URL);
|
|
97
108
|
const stored = readStoredAuth(initialApiUrl);
|
|
98
109
|
const apiUrl = normalizeApiUrl(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL);
|
|
99
|
-
const
|
|
110
|
+
const finalStored = apiUrl === initialApiUrl ? stored : readStoredAuth(apiUrl);
|
|
111
|
+
const apiToken = opts.apiToken || process.env.TODOFORAI_API_TOKEN || finalStored.apiToken || "";
|
|
100
112
|
if (!apiToken)
|
|
101
113
|
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)");
|
|
102
114
|
return { apiUrl, apiToken };
|
|
@@ -104,8 +116,8 @@ function resolveToken(opts = {}) {
|
|
|
104
116
|
// package.json
|
|
105
117
|
var package_default = {
|
|
106
118
|
name: "@todoforai/tfa-surface",
|
|
107
|
-
version: "0.1.
|
|
108
|
-
description: "TODOforAI surface CLI — see and drive what is shown in the user's live UI (describe, todo/panel views, surface render, controls). Replaces tfa-frontend. Reuses bridge credentials.",
|
|
119
|
+
version: "0.1.2",
|
|
120
|
+
description: "TODOforAI surface CLI — see and drive what is shown in the user's live UI (describe, todo/panel views, renderer cards, surface render, controls). Replaces tfa-frontend. Reuses bridge credentials.",
|
|
109
121
|
type: "module",
|
|
110
122
|
bin: {
|
|
111
123
|
"tfa-surface": "dist/cli.js"
|
|
@@ -118,10 +130,18 @@ var package_default = {
|
|
|
118
130
|
"@todoforai/tfa-subagent": "^0.1.9",
|
|
119
131
|
"@types/node": "^20.11.0"
|
|
120
132
|
},
|
|
121
|
-
keywords: [
|
|
133
|
+
keywords: [
|
|
134
|
+
"cli",
|
|
135
|
+
"frontend",
|
|
136
|
+
"surfaces",
|
|
137
|
+
"ui-control",
|
|
138
|
+
"todoforai"
|
|
139
|
+
],
|
|
122
140
|
author: "TODO for AI",
|
|
123
141
|
license: "MIT",
|
|
124
|
-
files: [
|
|
142
|
+
files: [
|
|
143
|
+
"dist"
|
|
144
|
+
]
|
|
125
145
|
};
|
|
126
146
|
|
|
127
147
|
// src/cli.ts
|
|
@@ -138,7 +158,8 @@ Usage:
|
|
|
138
158
|
Discovery:
|
|
139
159
|
describe What's open in the target frontend:
|
|
140
160
|
todos, focus, panel surfaces, sidebar,
|
|
141
|
-
embedded-browser CDP URL
|
|
161
|
+
embedded-browser CDP URL, show/card
|
|
162
|
+
instances in loaded chats. YAML; --json
|
|
142
163
|
for the raw structure.
|
|
143
164
|
|
|
144
165
|
Todos (views):
|
|
@@ -149,9 +170,19 @@ Todos (views):
|
|
|
149
170
|
|
|
150
171
|
Panels (surfaces inside a todo; --todo defaults to $TODOFORAI_TODO_ID):
|
|
151
172
|
panel open <surface> [--todo <id>] [--url <url>]
|
|
152
|
-
surface: changes|preview|browser|terminal
|
|
173
|
+
surface: changes|agents|preview|browser|terminal
|
|
153
174
|
panel close [--todo <id>]
|
|
154
175
|
|
|
176
|
+
Renderer cards (data plane — works headless, no frontend needed;
|
|
177
|
+
--project defaults to $TODOFORAI_PROJECT_ID):
|
|
178
|
+
card add <name> [file.html] [--title <t>] [--description <d>] [--project <id>]
|
|
179
|
+
Create/update a project card (stdin when
|
|
180
|
+
no file). Re-using a name updates every
|
|
181
|
+
mounted instance in place.
|
|
182
|
+
card list [--project <id>] List the project's cards.
|
|
183
|
+
card get <name> [--project <id>] Print a card's HTML (--json: metadata).
|
|
184
|
+
card rm <name> [--project <id>] Delete a card.
|
|
185
|
+
|
|
155
186
|
Content (inherited from tfa-frontend, broadcast to all tabs):
|
|
156
187
|
show <surface> <target> [markdown] Render markdown (stdin when omitted).
|
|
157
188
|
clear <surface> <target> Clear a surface.
|
|
@@ -170,6 +201,7 @@ Options:
|
|
|
170
201
|
|
|
171
202
|
Examples:
|
|
172
203
|
tfa-surface describe
|
|
204
|
+
tfa-surface card add kpi-tile dashboards/kpi.html --title "KPI tile"
|
|
173
205
|
tfa-surface todo open 3f2a91... --popout
|
|
174
206
|
tfa-surface panel open browser --url https://example.com
|
|
175
207
|
agent-browser connect "$(tfa-surface describe --json | jq -r '.todos[0].browser.cdp')"
|
|
@@ -209,6 +241,19 @@ async function call(ctx, path, body, method = "POST") {
|
|
|
209
241
|
die(`HTTP ${res.status}: ${JSON.stringify(data)}`);
|
|
210
242
|
return data;
|
|
211
243
|
}
|
|
244
|
+
async function rest(ctx, method, path, body) {
|
|
245
|
+
const { res, token } = await fetchWithRenew(`${ctx.apiUrl}/dst/v1${path}`, { method, body }, {
|
|
246
|
+
apiUrl: ctx.apiUrl,
|
|
247
|
+
token: ctx.token,
|
|
248
|
+
renewOn: [401, 403]
|
|
249
|
+
});
|
|
250
|
+
ctx.token = token;
|
|
251
|
+
if (res.status >= 400) {
|
|
252
|
+
const data = await res.json().catch(() => ({}));
|
|
253
|
+
die(`HTTP ${res.status}: ${data?.error ?? JSON.stringify(data)}`);
|
|
254
|
+
}
|
|
255
|
+
return res;
|
|
256
|
+
}
|
|
212
257
|
async function uiCall(ctx, method, params = {}) {
|
|
213
258
|
if (!ctx.frontendId) {
|
|
214
259
|
die("no target frontend — set TODOFORAI_FRONTEND_ID or pass --frontend <tabId> (run inside an agent shell, or copy the id from `describe` on another window)", 2);
|
|
@@ -246,7 +291,19 @@ function printDescribe(d) {
|
|
|
246
291
|
if (t.panel && t.panel !== "browser")
|
|
247
292
|
console.log(` ${t.panel}`);
|
|
248
293
|
if (t.browser)
|
|
249
|
-
console.log(` browser: ${t.browser.url} cdp: ${t.browser.cdp
|
|
294
|
+
console.log(` browser: ${t.browser.open ? t.browser.url : "(idle)"} cdp: ${t.browser.cdp}`);
|
|
295
|
+
if (t.shows?.length) {
|
|
296
|
+
console.log(" shows:");
|
|
297
|
+
for (const s of t.shows) {
|
|
298
|
+
const bits = [
|
|
299
|
+
s.card ? `card=${s.card}` : "",
|
|
300
|
+
s.mime || "",
|
|
301
|
+
s.url ? `url ${s.url}` : "",
|
|
302
|
+
s.title || ""
|
|
303
|
+
].filter(Boolean);
|
|
304
|
+
console.log(` ${s.ref}${bits.length ? ` ${bits.join(" ")}` : ""}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
250
307
|
}
|
|
251
308
|
}
|
|
252
309
|
async function main() {
|
|
@@ -255,6 +312,9 @@ async function main() {
|
|
|
255
312
|
frontend: { type: "string" },
|
|
256
313
|
todo: { type: "string" },
|
|
257
314
|
url: { type: "string" },
|
|
315
|
+
project: { type: "string" },
|
|
316
|
+
title: { type: "string" },
|
|
317
|
+
description: { type: "string" },
|
|
258
318
|
popout: { type: "boolean", default: false },
|
|
259
319
|
"api-url": { type: "string" },
|
|
260
320
|
"api-token": { type: "string" },
|
|
@@ -283,6 +343,7 @@ async function main() {
|
|
|
283
343
|
frontendId: values.frontend || process.env.TODOFORAI_FRONTEND_ID || undefined
|
|
284
344
|
};
|
|
285
345
|
const todoArg = () => values.todo || process.env.TODOFORAI_TODO_ID || die("no todo — pass --todo <todoId> or set TODOFORAI_TODO_ID");
|
|
346
|
+
const projectArg = () => values.project || process.env.TODOFORAI_PROJECT_ID || die("no project — pass --project <projectId> or set TODOFORAI_PROJECT_ID");
|
|
286
347
|
const [cmd, ...args] = positionals;
|
|
287
348
|
switch (cmd) {
|
|
288
349
|
case "describe": {
|
|
@@ -312,9 +373,10 @@ async function main() {
|
|
|
312
373
|
const [verb, surface] = args;
|
|
313
374
|
if (verb === "open") {
|
|
314
375
|
if (!surface)
|
|
315
|
-
die("usage: tfa-surface panel open changes|preview|browser|terminal [--todo <id>] [--url <url>]");
|
|
316
|
-
await uiCall(ctx, "panel.open", { todoId: todoArg(), surface, ...values.url ? { url: values.url } : {} });
|
|
317
|
-
|
|
376
|
+
die("usage: tfa-surface panel open changes|agents|preview|browser|terminal [--todo <id>] [--url <url>]");
|
|
377
|
+
const result = await uiCall(ctx, "panel.open", { todoId: todoArg(), surface, ...values.url ? { url: values.url } : {} });
|
|
378
|
+
const pending = result?.browserReady === false;
|
|
379
|
+
console.log(ctx.json ? JSON.stringify(result ?? { ok: true }) : pending ? "ok (browser opening — its CDP url appears in `device_list` shortly)" : "ok");
|
|
318
380
|
} else if (verb === "close") {
|
|
319
381
|
await uiCall(ctx, "panel.close", { todoId: todoArg() });
|
|
320
382
|
console.log(ctx.json ? '{"ok":true}' : "ok");
|
|
@@ -323,6 +385,57 @@ async function main() {
|
|
|
323
385
|
}
|
|
324
386
|
break;
|
|
325
387
|
}
|
|
388
|
+
case "card": {
|
|
389
|
+
const [verb, name] = args;
|
|
390
|
+
const cardPath = (n) => `/resources/cards/${projectArg()}/${encodeURIComponent(n)}`;
|
|
391
|
+
if (verb === "add") {
|
|
392
|
+
if (!name)
|
|
393
|
+
die("usage: tfa-surface card add <name> [file.html] [--title <t>] [--description <d>]");
|
|
394
|
+
const file = args[2];
|
|
395
|
+
const html = file ? await (await import("node:fs/promises")).readFile(file, "utf-8") : process.stdin.isTTY ? die("no file given and stdin is a TTY") : await readStdin();
|
|
396
|
+
if (!html.trim())
|
|
397
|
+
die("empty card body");
|
|
398
|
+
const form = new FormData;
|
|
399
|
+
form.append("file", new Blob([html], { type: "text/html" }), `${name}.html`);
|
|
400
|
+
if (values.title)
|
|
401
|
+
form.append("title", values.title);
|
|
402
|
+
if (values.description)
|
|
403
|
+
form.append("description", values.description);
|
|
404
|
+
const card = await (await rest(ctx, "PUT", cardPath(name), form)).json();
|
|
405
|
+
console.log(ctx.json ? JSON.stringify(card) : `ok — card '${name}' saved (instances update live)`);
|
|
406
|
+
} else if (verb === "list") {
|
|
407
|
+
const { items } = await (await rest(ctx, "GET", `/resources/cards/${projectArg()}`)).json();
|
|
408
|
+
if (ctx.json) {
|
|
409
|
+
console.log(JSON.stringify(items));
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
if (!items.length) {
|
|
413
|
+
console.log("no cards in this project");
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
for (const c of items) {
|
|
417
|
+
console.log(`${c.name}${c.title ? ` # ${c.title}` : ""}`);
|
|
418
|
+
if (c.description)
|
|
419
|
+
console.log(` ${c.description}`);
|
|
420
|
+
}
|
|
421
|
+
} else if (verb === "get") {
|
|
422
|
+
if (!name)
|
|
423
|
+
die("usage: tfa-surface card get <name>");
|
|
424
|
+
if (ctx.json) {
|
|
425
|
+
console.log(JSON.stringify(await (await rest(ctx, "GET", cardPath(name))).json()));
|
|
426
|
+
} else {
|
|
427
|
+
process.stdout.write(await (await rest(ctx, "GET", `${cardPath(name)}?body=1`)).text());
|
|
428
|
+
}
|
|
429
|
+
} else if (verb === "rm") {
|
|
430
|
+
if (!name)
|
|
431
|
+
die("usage: tfa-surface card rm <name>");
|
|
432
|
+
await rest(ctx, "DELETE", cardPath(name));
|
|
433
|
+
console.log(ctx.json ? '{"ok":true}' : "ok");
|
|
434
|
+
} else {
|
|
435
|
+
die("usage: tfa-surface card add|list|get|rm ...");
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
326
439
|
case "show": {
|
|
327
440
|
const [surface, target] = args;
|
|
328
441
|
if (!surface || !target)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@todoforai/tfa-surface",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "TODOforAI surface CLI — see and drive what is shown in the user's live UI (describe, todo/panel views, surface render, controls). Replaces tfa-frontend. Reuses bridge credentials.",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "TODOforAI surface CLI — see and drive what is shown in the user's live UI (describe, todo/panel views, renderer cards, surface render, controls). Replaces tfa-frontend. Reuses bridge credentials.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tfa-surface": "dist/cli.js"
|
|
@@ -14,8 +14,16 @@
|
|
|
14
14
|
"@todoforai/tfa-subagent": "^0.1.9",
|
|
15
15
|
"@types/node": "^20.11.0"
|
|
16
16
|
},
|
|
17
|
-
"keywords": [
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cli",
|
|
19
|
+
"frontend",
|
|
20
|
+
"surfaces",
|
|
21
|
+
"ui-control",
|
|
22
|
+
"todoforai"
|
|
23
|
+
],
|
|
18
24
|
"author": "TODO for AI",
|
|
19
25
|
"license": "MIT",
|
|
20
|
-
"files": [
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
]
|
|
21
29
|
}
|