@todoforai/tfa-surface 0.1.0 → 0.1.1
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 +144 -37
- 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,25 @@ 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
|
-
deviceSecret: typeof c.deviceSecret === "string" ? c.deviceSecret : undefined
|
|
43
|
-
};
|
|
44
|
-
} catch {}
|
|
45
|
-
}
|
|
46
|
-
return { apiToken: "" };
|
|
39
|
+
const c = read();
|
|
40
|
+
const apiToken = c[apiUrl] || c.apiToken || c.apiKey || "";
|
|
41
|
+
const host = c.backendHost || "";
|
|
42
|
+
const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
|
|
43
|
+
return {
|
|
44
|
+
apiToken: typeof apiToken === "string" ? apiToken : "",
|
|
45
|
+
apiUrl: storedApiUrl,
|
|
46
|
+
deviceId: typeof c.deviceId === "string" ? c.deviceId : undefined,
|
|
47
|
+
deviceSecret: typeof c.deviceSecret === "string" ? c.deviceSecret : undefined
|
|
48
|
+
};
|
|
47
49
|
}
|
|
48
50
|
function persistApiToken(token) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
51
|
+
const c = read();
|
|
52
|
+
if (!Object.keys(c).length)
|
|
53
|
+
return;
|
|
54
|
+
c.apiToken = token;
|
|
55
|
+
try {
|
|
56
|
+
writeSecure(c);
|
|
57
|
+
} catch {}
|
|
57
58
|
}
|
|
58
59
|
async function refreshDeviceToken(opts = {}) {
|
|
59
60
|
const envUrl = process.env.TODOFORAI_API_URL || process.env.API_URL;
|
|
@@ -82,8 +83,12 @@ async function refreshDeviceToken(opts = {}) {
|
|
|
82
83
|
async function fetchWithRenew(url, init, opts) {
|
|
83
84
|
const header = opts.header ?? "x-api-key";
|
|
84
85
|
const renewOn = opts.renewOn ?? [401];
|
|
85
|
-
const send = (token) =>
|
|
86
|
-
|
|
86
|
+
const send = (token) => {
|
|
87
|
+
const headers = new Headers(init.headers);
|
|
88
|
+
headers.set(header, token);
|
|
89
|
+
return fetch(url, { ...init, headers });
|
|
90
|
+
};
|
|
91
|
+
const res = await send(opts.token);
|
|
87
92
|
if (renewOn.includes(res.status) && opts.token.startsWith("dst_")) {
|
|
88
93
|
const fresh = await refreshDeviceToken({ apiUrl: opts.apiUrl });
|
|
89
94
|
if (fresh && fresh !== opts.token)
|
|
@@ -96,7 +101,8 @@ function resolveToken(opts = {}) {
|
|
|
96
101
|
const initialApiUrl = normalizeApiUrl(opts.apiUrl || envUrl || DEFAULT_API_URL);
|
|
97
102
|
const stored = readStoredAuth(initialApiUrl);
|
|
98
103
|
const apiUrl = normalizeApiUrl(opts.apiUrl || envUrl || stored.apiUrl || DEFAULT_API_URL);
|
|
99
|
-
const
|
|
104
|
+
const finalStored = apiUrl === initialApiUrl ? stored : readStoredAuth(apiUrl);
|
|
105
|
+
const apiToken = opts.apiToken || process.env.TODOFORAI_API_TOKEN || finalStored.apiToken || "";
|
|
100
106
|
if (!apiToken)
|
|
101
107
|
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
108
|
return { apiUrl, apiToken };
|
|
@@ -104,8 +110,8 @@ function resolveToken(opts = {}) {
|
|
|
104
110
|
// package.json
|
|
105
111
|
var package_default = {
|
|
106
112
|
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.",
|
|
113
|
+
version: "0.1.1",
|
|
114
|
+
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
115
|
type: "module",
|
|
110
116
|
bin: {
|
|
111
117
|
"tfa-surface": "dist/cli.js"
|
|
@@ -118,10 +124,18 @@ var package_default = {
|
|
|
118
124
|
"@todoforai/tfa-subagent": "^0.1.9",
|
|
119
125
|
"@types/node": "^20.11.0"
|
|
120
126
|
},
|
|
121
|
-
keywords: [
|
|
127
|
+
keywords: [
|
|
128
|
+
"cli",
|
|
129
|
+
"frontend",
|
|
130
|
+
"surfaces",
|
|
131
|
+
"ui-control",
|
|
132
|
+
"todoforai"
|
|
133
|
+
],
|
|
122
134
|
author: "TODO for AI",
|
|
123
135
|
license: "MIT",
|
|
124
|
-
files: [
|
|
136
|
+
files: [
|
|
137
|
+
"dist"
|
|
138
|
+
]
|
|
125
139
|
};
|
|
126
140
|
|
|
127
141
|
// src/cli.ts
|
|
@@ -138,7 +152,8 @@ Usage:
|
|
|
138
152
|
Discovery:
|
|
139
153
|
describe What's open in the target frontend:
|
|
140
154
|
todos, focus, panel surfaces, sidebar,
|
|
141
|
-
embedded-browser CDP URL
|
|
155
|
+
embedded-browser CDP URL, show/card
|
|
156
|
+
instances in loaded chats. YAML; --json
|
|
142
157
|
for the raw structure.
|
|
143
158
|
|
|
144
159
|
Todos (views):
|
|
@@ -152,6 +167,16 @@ Panels (surfaces inside a todo; --todo defaults to $TODOFORAI_TODO_ID):
|
|
|
152
167
|
surface: changes|preview|browser|terminal
|
|
153
168
|
panel close [--todo <id>]
|
|
154
169
|
|
|
170
|
+
Renderer cards (data plane — works headless, no frontend needed;
|
|
171
|
+
--project defaults to $TODOFORAI_PROJECT_ID):
|
|
172
|
+
card add <name> [file.html] [--title <t>] [--description <d>] [--project <id>]
|
|
173
|
+
Create/update a project card (stdin when
|
|
174
|
+
no file). Re-using a name updates every
|
|
175
|
+
mounted instance in place.
|
|
176
|
+
card list [--project <id>] List the project's cards.
|
|
177
|
+
card get <name> [--project <id>] Print a card's HTML (--json: metadata).
|
|
178
|
+
card rm <name> [--project <id>] Delete a card.
|
|
179
|
+
|
|
155
180
|
Content (inherited from tfa-frontend, broadcast to all tabs):
|
|
156
181
|
show <surface> <target> [markdown] Render markdown (stdin when omitted).
|
|
157
182
|
clear <surface> <target> Clear a surface.
|
|
@@ -170,6 +195,7 @@ Options:
|
|
|
170
195
|
|
|
171
196
|
Examples:
|
|
172
197
|
tfa-surface describe
|
|
198
|
+
tfa-surface card add kpi-tile dashboards/kpi.html --title "KPI tile"
|
|
173
199
|
tfa-surface todo open 3f2a91... --popout
|
|
174
200
|
tfa-surface panel open browser --url https://example.com
|
|
175
201
|
agent-browser connect "$(tfa-surface describe --json | jq -r '.todos[0].browser.cdp')"
|
|
@@ -209,6 +235,19 @@ async function call(ctx, path, body, method = "POST") {
|
|
|
209
235
|
die(`HTTP ${res.status}: ${JSON.stringify(data)}`);
|
|
210
236
|
return data;
|
|
211
237
|
}
|
|
238
|
+
async function rest(ctx, method, path, body) {
|
|
239
|
+
const { res, token } = await fetchWithRenew(`${ctx.apiUrl}/dst/v1${path}`, { method, body }, {
|
|
240
|
+
apiUrl: ctx.apiUrl,
|
|
241
|
+
token: ctx.token,
|
|
242
|
+
renewOn: [401, 403]
|
|
243
|
+
});
|
|
244
|
+
ctx.token = token;
|
|
245
|
+
if (res.status >= 400) {
|
|
246
|
+
const data = await res.json().catch(() => ({}));
|
|
247
|
+
die(`HTTP ${res.status}: ${data?.error ?? JSON.stringify(data)}`);
|
|
248
|
+
}
|
|
249
|
+
return res;
|
|
250
|
+
}
|
|
212
251
|
async function uiCall(ctx, method, params = {}) {
|
|
213
252
|
if (!ctx.frontendId) {
|
|
214
253
|
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 +285,19 @@ function printDescribe(d) {
|
|
|
246
285
|
if (t.panel && t.panel !== "browser")
|
|
247
286
|
console.log(` ${t.panel}`);
|
|
248
287
|
if (t.browser)
|
|
249
|
-
console.log(` browser: ${t.browser.url} cdp: ${t.browser.cdp
|
|
288
|
+
console.log(` browser: ${t.browser.open ? t.browser.url : "(idle)"} cdp: ${t.browser.cdp}`);
|
|
289
|
+
if (t.shows?.length) {
|
|
290
|
+
console.log(" shows:");
|
|
291
|
+
for (const s of t.shows) {
|
|
292
|
+
const bits = [
|
|
293
|
+
s.card ? `card=${s.card}` : "",
|
|
294
|
+
s.mime || "",
|
|
295
|
+
s.url ? `url ${s.url}` : "",
|
|
296
|
+
s.title || ""
|
|
297
|
+
].filter(Boolean);
|
|
298
|
+
console.log(` ${s.ref}${bits.length ? ` ${bits.join(" ")}` : ""}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
250
301
|
}
|
|
251
302
|
}
|
|
252
303
|
async function main() {
|
|
@@ -255,6 +306,9 @@ async function main() {
|
|
|
255
306
|
frontend: { type: "string" },
|
|
256
307
|
todo: { type: "string" },
|
|
257
308
|
url: { type: "string" },
|
|
309
|
+
project: { type: "string" },
|
|
310
|
+
title: { type: "string" },
|
|
311
|
+
description: { type: "string" },
|
|
258
312
|
popout: { type: "boolean", default: false },
|
|
259
313
|
"api-url": { type: "string" },
|
|
260
314
|
"api-token": { type: "string" },
|
|
@@ -283,6 +337,7 @@ async function main() {
|
|
|
283
337
|
frontendId: values.frontend || process.env.TODOFORAI_FRONTEND_ID || undefined
|
|
284
338
|
};
|
|
285
339
|
const todoArg = () => values.todo || process.env.TODOFORAI_TODO_ID || die("no todo — pass --todo <todoId> or set TODOFORAI_TODO_ID");
|
|
340
|
+
const projectArg = () => values.project || process.env.TODOFORAI_PROJECT_ID || die("no project — pass --project <projectId> or set TODOFORAI_PROJECT_ID");
|
|
286
341
|
const [cmd, ...args] = positionals;
|
|
287
342
|
switch (cmd) {
|
|
288
343
|
case "describe": {
|
|
@@ -313,8 +368,9 @@ async function main() {
|
|
|
313
368
|
if (verb === "open") {
|
|
314
369
|
if (!surface)
|
|
315
370
|
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
|
-
|
|
371
|
+
const result = await uiCall(ctx, "panel.open", { todoId: todoArg(), surface, ...values.url ? { url: values.url } : {} });
|
|
372
|
+
const pending = result?.browserReady === false;
|
|
373
|
+
console.log(ctx.json ? JSON.stringify(result ?? { ok: true }) : pending ? "ok (browser opening — its CDP url appears in `device_list` shortly)" : "ok");
|
|
318
374
|
} else if (verb === "close") {
|
|
319
375
|
await uiCall(ctx, "panel.close", { todoId: todoArg() });
|
|
320
376
|
console.log(ctx.json ? '{"ok":true}' : "ok");
|
|
@@ -323,6 +379,57 @@ async function main() {
|
|
|
323
379
|
}
|
|
324
380
|
break;
|
|
325
381
|
}
|
|
382
|
+
case "card": {
|
|
383
|
+
const [verb, name] = args;
|
|
384
|
+
const cardPath = (n) => `/resources/cards/${projectArg()}/${encodeURIComponent(n)}`;
|
|
385
|
+
if (verb === "add") {
|
|
386
|
+
if (!name)
|
|
387
|
+
die("usage: tfa-surface card add <name> [file.html] [--title <t>] [--description <d>]");
|
|
388
|
+
const file = args[2];
|
|
389
|
+
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();
|
|
390
|
+
if (!html.trim())
|
|
391
|
+
die("empty card body");
|
|
392
|
+
const form = new FormData;
|
|
393
|
+
form.append("file", new Blob([html], { type: "text/html" }), `${name}.html`);
|
|
394
|
+
if (values.title)
|
|
395
|
+
form.append("title", values.title);
|
|
396
|
+
if (values.description)
|
|
397
|
+
form.append("description", values.description);
|
|
398
|
+
const card = await (await rest(ctx, "PUT", cardPath(name), form)).json();
|
|
399
|
+
console.log(ctx.json ? JSON.stringify(card) : `ok — card '${name}' saved (instances update live)`);
|
|
400
|
+
} else if (verb === "list") {
|
|
401
|
+
const { items } = await (await rest(ctx, "GET", `/resources/cards/${projectArg()}`)).json();
|
|
402
|
+
if (ctx.json) {
|
|
403
|
+
console.log(JSON.stringify(items));
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
if (!items.length) {
|
|
407
|
+
console.log("no cards in this project");
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
for (const c of items) {
|
|
411
|
+
console.log(`${c.name}${c.title ? ` # ${c.title}` : ""}`);
|
|
412
|
+
if (c.description)
|
|
413
|
+
console.log(` ${c.description}`);
|
|
414
|
+
}
|
|
415
|
+
} else if (verb === "get") {
|
|
416
|
+
if (!name)
|
|
417
|
+
die("usage: tfa-surface card get <name>");
|
|
418
|
+
if (ctx.json) {
|
|
419
|
+
console.log(JSON.stringify(await (await rest(ctx, "GET", cardPath(name))).json()));
|
|
420
|
+
} else {
|
|
421
|
+
process.stdout.write(await (await rest(ctx, "GET", `${cardPath(name)}?body=1`)).text());
|
|
422
|
+
}
|
|
423
|
+
} else if (verb === "rm") {
|
|
424
|
+
if (!name)
|
|
425
|
+
die("usage: tfa-surface card rm <name>");
|
|
426
|
+
await rest(ctx, "DELETE", cardPath(name));
|
|
427
|
+
console.log(ctx.json ? '{"ok":true}' : "ok");
|
|
428
|
+
} else {
|
|
429
|
+
die("usage: tfa-surface card add|list|get|rm ...");
|
|
430
|
+
}
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
326
433
|
case "show": {
|
|
327
434
|
const [surface, target] = args;
|
|
328
435
|
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
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "TODOforAI surface CLI \u2014 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
|
}
|