@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.
Files changed (2) hide show
  1. package/dist/cli.js +144 -37
  2. 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 credentialsPaths() {
22
- return [credentialsPath()];
23
+ function read() {
24
+ try {
25
+ return JSON.parse(readFileSync(credentialsPath(), "utf-8"));
26
+ } catch {
27
+ return {};
28
+ }
23
29
  }
24
- function writeSecure(p, data) {
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
- for (const p of credentialsPaths()) {
33
- try {
34
- const c = JSON.parse(readFileSync(p, "utf-8"));
35
- const apiToken = c[apiUrl] || c.apiToken || c.apiKey || "";
36
- const host = c.backendHost || "";
37
- const storedApiUrl = typeof c.apiUrl === "string" && c.apiUrl ? c.apiUrl : host ? `${host === "localhost" || host === "127.0.0.1" ? "http" : "https"}://${host}` : undefined;
38
- return {
39
- apiToken,
40
- apiUrl: storedApiUrl,
41
- deviceId: typeof c.deviceId === "string" ? c.deviceId : undefined,
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
- for (const p of credentialsPaths()) {
50
- try {
51
- const c = JSON.parse(readFileSync(p, "utf-8"));
52
- c.apiToken = token;
53
- writeSecure(p, c);
54
- return;
55
- } catch {}
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) => fetch(url, { ...init, headers: { ...init.headers, [header]: token } });
86
- let res = await send(opts.token);
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 apiToken = opts.apiToken || process.env.TODOFORAI_API_TOKEN || stored.apiToken || "";
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.0",
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: ["cli", "frontend", "surfaces", "ui-control", "todoforai"],
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: ["dist"]
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. YAML; --json
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 ?? "(connecting)"}`);
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
- console.log(ctx.json ? '{"ok":true}' : "ok");
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.0",
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.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": ["cli", "frontend", "surfaces", "ui-control", "todoforai"],
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": ["dist"]
26
+ "files": [
27
+ "dist"
28
+ ]
21
29
  }