@tidegap/cli 0.18.0 → 0.19.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.
Files changed (57) hide show
  1. package/README.md +13 -2
  2. package/dist/commands/clients.d.ts +20 -0
  3. package/dist/commands/clients.d.ts.map +1 -0
  4. package/dist/commands/clients.js +36 -0
  5. package/dist/commands/clients.js.map +1 -0
  6. package/dist/commands/env-file.d.ts +16 -15
  7. package/dist/commands/env-file.d.ts.map +1 -1
  8. package/dist/commands/env-file.js +49 -23
  9. package/dist/commands/env-file.js.map +1 -1
  10. package/dist/commands/install.d.ts +2 -3
  11. package/dist/commands/install.d.ts.map +1 -1
  12. package/dist/commands/install.js +13 -15
  13. package/dist/commands/install.js.map +1 -1
  14. package/dist/commands/tick.d.ts +1 -1
  15. package/dist/commands/tick.d.ts.map +1 -1
  16. package/dist/commands/tick.js +3 -3
  17. package/dist/commands/tick.js.map +1 -1
  18. package/dist/commands/ui.d.ts +8 -0
  19. package/dist/commands/ui.d.ts.map +1 -1
  20. package/dist/commands/ui.js +41 -5
  21. package/dist/commands/ui.js.map +1 -1
  22. package/dist/env.d.ts +12 -10
  23. package/dist/env.d.ts.map +1 -1
  24. package/dist/env.js +50 -35
  25. package/dist/env.js.map +1 -1
  26. package/dist/index.d.ts +1 -7
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +35 -20
  29. package/dist/index.js.map +1 -1
  30. package/dist/ui/api.d.ts +7 -0
  31. package/dist/ui/api.d.ts.map +1 -1
  32. package/dist/ui/api.js +180 -26
  33. package/dist/ui/api.js.map +1 -1
  34. package/dist/ui/clients.d.ts +65 -0
  35. package/dist/ui/clients.d.ts.map +1 -0
  36. package/dist/ui/clients.js +179 -0
  37. package/dist/ui/clients.js.map +1 -0
  38. package/dist/ui/discovery.d.ts +25 -0
  39. package/dist/ui/discovery.d.ts.map +1 -0
  40. package/dist/ui/discovery.js +46 -0
  41. package/dist/ui/discovery.js.map +1 -0
  42. package/dist/ui/server.d.ts +24 -9
  43. package/dist/ui/server.d.ts.map +1 -1
  44. package/dist/ui/server.js +99 -35
  45. package/dist/ui/server.js.map +1 -1
  46. package/dist/ui/summary.d.ts +47 -0
  47. package/dist/ui/summary.d.ts.map +1 -0
  48. package/dist/ui/summary.js +120 -0
  49. package/dist/ui/summary.js.map +1 -0
  50. package/package.json +4 -5
  51. package/ui/api.js +3 -0
  52. package/ui/app.css +9 -3
  53. package/ui/app.js +2 -0
  54. package/ui/clients.js +104 -0
  55. package/ui/dialog.js +5 -0
  56. package/ui/index.html +2 -1
  57. package/ui/tokens.css +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tidegap/cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "The tidegap command: runs queued Tidegap LLM tasks at the cleanest grid hour before their deadline.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Vitalii Borovyk",
@@ -10,8 +10,7 @@
10
10
  },
11
11
  "main": "./dist/index.js",
12
12
  "bin": {
13
- "tidegap": "./dist/index.js",
14
- "ebb": "./dist/index.js"
13
+ "tidegap": "./dist/index.js"
15
14
  },
16
15
  "files": [
17
16
  "dist",
@@ -21,7 +20,7 @@
21
20
  ],
22
21
  "dependencies": {
23
22
  "commander": "^12.1.0",
24
- "@tidegap/core": "0.18.0"
23
+ "@tidegap/core": "0.19.0"
25
24
  },
26
25
  "peerDependencies": {
27
26
  "@anthropic-ai/sdk": ">=0.32.0",
@@ -38,7 +37,7 @@
38
37
  "devDependencies": {
39
38
  "typescript": "^5.6.3",
40
39
  "vitest": "^4.1.11",
41
- "@types/node": "^22.20.4"
40
+ "@types/node": "^22.20.5"
42
41
  },
43
42
  "repository": {
44
43
  "type": "git",
package/ui/api.js CHANGED
@@ -108,4 +108,7 @@ export const api = {
108
108
  read: (id) => post(tasksPath(id, "read")),
109
109
  unread: (id) => post(tasksPath(id, "unread")),
110
110
  startDispatcher: () => post("/api/dispatcher/start"),
111
+ clients: () => get("/api/clients"),
112
+ addClient: (client) => post("/api/clients", client),
113
+ revokeClient: (id) => post(`/api/clients/${enc(id)}/revoke`),
111
114
  };
package/ui/app.css CHANGED
@@ -62,9 +62,9 @@ kbd {
62
62
  font-size: var(--fs-lg); font-weight: var(--w-bold); letter-spacing: -0.01em;
63
63
  }
64
64
  .brand-mark {
65
- width: 18px; height: 18px; border-radius: 50%;
66
- background: var(--accent);
67
- box-shadow: inset -5px -5px 0 0 var(--band-very-clean-bg);
65
+ flex: none;
66
+ width: 32px; height: 11px;
67
+ color: var(--brand-ink);
68
68
  }
69
69
  .tabs {
70
70
  display: flex; gap: 2px; padding: 3px;
@@ -578,6 +578,12 @@ body.dragging .day-head[data-drop-day] { outline: 1px dashed var(--fg-muted); ou
578
578
  .radio-label { font-weight: var(--w-medium); }
579
579
  .radio-hint { color: var(--fg-muted); font-size: var(--fs-sm); line-height: var(--lh-sm); }
580
580
  .field-row { display: flex; gap: var(--sp-3); }
581
+ .app-list { list-style: none; margin: 0 0 var(--sp-4); padding: 0; }
582
+ .app-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-3); padding: var(--sp-2) 0; border-bottom: 1px solid var(--line); }
583
+ .app-name { flex: 1 1 auto; font-weight: var(--w-medium); overflow-wrap: anywhere; }
584
+ .app-meta { color: var(--fg-muted); font-size: var(--fs-sm); }
585
+ .token-box { margin: 0 0 var(--sp-4); }
586
+ .token-field { flex: 1; min-width: 0; font-family: var(--mono); }
581
587
  .field { display: flex; flex: 1; flex-direction: column; gap: var(--sp-1); }
582
588
  .field-label { color: var(--fg-muted); font-size: var(--fs-xs); font-weight: var(--w-medium); text-transform: uppercase; letter-spacing: 0.04em; }
583
589
  .form-error { margin: var(--sp-3) 0 0; color: var(--danger); font-size: var(--fs-sm); }
package/ui/app.js CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  sendReply,
30
30
  trapFocus,
31
31
  } from "./detail.js";
32
+ import { initClients } from "./clients.js";
32
33
  import { ago, h, isUnread, parse, relDay, swapChildren, taskHref, title } from "./dom.js";
33
34
  import { invalidateJournal, renderJournal, toggleExpand } from "./journal.js";
34
35
  import { initKeys } from "./keys.js";
@@ -567,6 +568,7 @@ function boot() {
567
568
  });
568
569
  document.getElementById("scrim").addEventListener("click", closeDrawer);
569
570
  document.getElementById("help-btn").addEventListener("click", helpDialog);
571
+ void initClients();
570
572
  document.getElementById("dispatcher-pill").addEventListener("click", dispatcherDialog);
571
573
  document.getElementById("view").addEventListener("keydown", (e) => {
572
574
  if (e.key === "Enter" || e.key === " ") {
package/ui/clients.js ADDED
@@ -0,0 +1,104 @@
1
+ // "Connected apps": list the browser extension and Mac app clients, mint a
2
+ // token for a new one (shown once), and revoke one. Page token only: with a
3
+ // client token `/api/clients` answers 403 and the button stays hidden.
4
+ // Client names are user text and render as text nodes only.
5
+
6
+ import { api, ApiError } from "./api.js";
7
+ import { closeDialog, confirmDialog, openDialog } from "./dialog.js";
8
+ import { ago, h, parse } from "./dom.js";
9
+ import { errorToast, toast } from "./toast.js";
10
+
11
+ const KIND_LABEL = { extension: "Browser extension", macos: "Mac app" };
12
+
13
+ /** Show the Apps button when this token may manage clients. */
14
+ export async function initClients() {
15
+ const btn = document.getElementById("apps-btn");
16
+ if (!btn) return;
17
+ try {
18
+ await api.clients();
19
+ } catch (err) {
20
+ if (err instanceof ApiError && err.status === 403) return;
21
+ }
22
+ btn.hidden = false;
23
+ btn.addEventListener("click", () => void openApps());
24
+ }
25
+
26
+ function tokenBox(minted) {
27
+ const field = h("input", { class: "input token-field", readonly: true, value: minted.token, "aria-label": "Client token" });
28
+ const copy = async () => {
29
+ try {
30
+ await navigator.clipboard.writeText(minted.token);
31
+ toast("Token copied");
32
+ } catch {
33
+ field.select();
34
+ }
35
+ };
36
+ return h("section", { class: "token-box", role: "status" },
37
+ h("p", { class: "dialog-body" }, `Token for "${minted.client.name}". Paste it into the extension's options. It is shown only now.`),
38
+ h("div", { class: "field-row" },
39
+ field,
40
+ h("button", { type: "button", class: "btn btn-primary", onclick: () => void copy() }, "Copy")));
41
+ }
42
+
43
+ function clientRow(c) {
44
+ const created = parse(c.created_at);
45
+ return h("li", { class: "app-row" },
46
+ h("span", { class: "app-name" }, c.name),
47
+ h("span", { class: "app-meta" }, `${KIND_LABEL[c.kind] || c.kind} · added ${Number.isFinite(created) ? ago(created) : "?"}`),
48
+ h("button", { type: "button", class: "btn btn-sm", onclick: () => void revoke(c) }, "Revoke"));
49
+ }
50
+
51
+ async function openApps(minted) {
52
+ let clients = [];
53
+ let failed = "";
54
+ try {
55
+ clients = (await api.clients()).clients;
56
+ } catch (err) {
57
+ failed = err.message;
58
+ }
59
+ const name = h("input", { class: "input", id: "app-name", name: "name", maxlength: "64", required: true, placeholder: "Chrome on this Mac", autofocus: !minted });
60
+ const submit = h("button", { type: "submit", class: "btn btn-primary" }, "Connect an app");
61
+ const form = h("form", { class: "dialog-form" },
62
+ h("header", { class: "dialog-head" }, h("h2", { id: "dialog-title", class: "dialog-title" }, "Connected apps"),
63
+ h("p", { class: "dialog-sub" }, "Apps with a token can see and queue tasks on this Mac. Revoke one to lock it out at once.")),
64
+ minted ? tokenBox(minted) : null,
65
+ failed ? h("p", { class: "dialog-body" }, failed)
66
+ : clients.length ? h("ul", { class: "app-list" }, clients.map(clientRow))
67
+ : h("p", { class: "dialog-body" }, "No apps connected."),
68
+ h("label", { class: "field", for: "app-name" }, h("span", { class: "field-label" }, "New app name"), name),
69
+ h("footer", { class: "dialog-foot" },
70
+ h("button", { type: "button", class: "btn btn-ghost", onclick: closeDialog }, "Close"),
71
+ submit));
72
+ // One press, one token: a second submit while the first is in flight is ignored.
73
+ form.addEventListener("submit", async (e) => {
74
+ e.preventDefault();
75
+ if (submit.disabled) return;
76
+ submit.disabled = true;
77
+ try {
78
+ const out = await api.addClient({ name: name.value, kind: "extension" });
79
+ await openApps(out);
80
+ } catch (err) {
81
+ errorToast(err.message);
82
+ } finally {
83
+ submit.disabled = false;
84
+ }
85
+ });
86
+ openDialog([form], { label: "Connected apps", wide: true });
87
+ }
88
+
89
+ async function revoke(c) {
90
+ const ok = await confirmDialog({
91
+ heading: `Revoke ${c.name}?`,
92
+ body: "Its token stops working on the next request. Connect it again to get a new one.",
93
+ confirm: "Revoke",
94
+ danger: true,
95
+ });
96
+ if (!ok) return void openApps();
97
+ try {
98
+ await api.revokeClient(c.id);
99
+ toast(`Revoked ${c.name}`);
100
+ } catch (err) {
101
+ errorToast(err.message);
102
+ }
103
+ await openApps();
104
+ }
package/ui/dialog.js CHANGED
@@ -30,6 +30,11 @@ export function dialogOpen() {
30
30
  return dlg().open;
31
31
  }
32
32
 
33
+ /** Open the shared dialog with `content` (used by other panels). */
34
+ export function openDialog(content, opts) {
35
+ open(content, opts);
36
+ }
37
+
33
38
  function open(content, { label, wide = false, close } = {}) {
34
39
  const d = dlg();
35
40
  if (d.open) d.close();
package/ui/index.html CHANGED
@@ -14,7 +14,7 @@
14
14
  <body>
15
15
  <a class="skip" href="#view">Skip to tasks</a>
16
16
  <header class="top">
17
- <a class="brand" href="#/agenda" aria-label="Tidegap, Agenda"><span class="brand-mark" aria-hidden="true"></span>Tidegap</a>
17
+ <a class="brand" href="#/agenda" aria-label="Tidegap, Agenda"><!-- brand-mark:start --><svg class="brand-mark" viewBox="0 0 32 11" aria-hidden="true" focusable="false"><defs><clipPath id="tg-ui-ink"><rect x="-0.07" y="-0.07" width="32.15" height="5.48"/></clipPath><clipPath id="tg-ui-teal"><rect x="6.68" y="4.91" width="18.65" height="5.98"/></clipPath></defs><path class="ink" d="M-2.65 9.14A4.03 4.03 0 0 0 0.69 7.37L3.34 3.44A4.03 4.03 0 0 1 6.68 1.67A4.03 4.03 0 0 1 10.01 3.44L12.66 7.37A4.03 4.03 0 0 0 16 9.14A4.03 4.03 0 0 0 19.34 7.37L21.99 3.44A4.03 4.03 0 0 1 25.32 1.67A4.03 4.03 0 0 1 28.66 3.44L31.31 7.37A4.03 4.03 0 0 0 34.65 9.14" fill="none" stroke="currentColor" stroke-width="3.34" clip-path="url(#tg-ui-ink)"/><path d="M-2.65 9.14A4.03 4.03 0 0 0 0.69 7.37L3.34 3.44A4.03 4.03 0 0 1 6.68 1.67A4.03 4.03 0 0 1 10.01 3.44L12.66 7.37A4.03 4.03 0 0 0 16 9.14A4.03 4.03 0 0 0 19.34 7.37L21.99 3.44A4.03 4.03 0 0 1 25.32 1.67A4.03 4.03 0 0 1 28.66 3.44L31.31 7.37A4.03 4.03 0 0 0 34.65 9.14" fill="none" stroke="#5FC6C4" stroke-width="3.34" clip-path="url(#tg-ui-teal)"/></svg><!-- brand-mark:end -->Tidegap</a>
18
18
  <nav class="tabs" aria-label="Views">
19
19
  <a href="#/agenda" id="nav-agenda" class="tab"><span class="tab-glyph" aria-hidden="true">&#9776;</span><span class="tab-label">Agenda</span></a>
20
20
  <a href="#/timeline" id="nav-timeline" class="tab"><span class="tab-glyph" aria-hidden="true">&#8767;</span><span class="tab-label">Timeline</span></a>
@@ -22,6 +22,7 @@
22
22
  </nav>
23
23
  <div class="top-end">
24
24
  <button type="button" class="pill dispatcher" id="dispatcher-pill" data-state="unknown"><span class="pill-dot" aria-hidden="true"></span><span id="dispatcher-text">Dispatcher</span></button>
25
+ <button type="button" class="pill" id="apps-btn" hidden>Apps</button>
25
26
  <button type="button" class="icon-btn" id="help-btn" aria-label="Keyboard shortcuts" title="Keyboard shortcuts (?)">?</button>
26
27
  </div>
27
28
  </header>
package/ui/tokens.css CHANGED
@@ -14,6 +14,7 @@
14
14
  --accent: #1d6b4d;
15
15
  --accent-fg: #ffffff;
16
16
  --accent-soft: #e1efe7;
17
+ --brand-ink: #0c1017;
17
18
  --danger: #a8361f;
18
19
  --danger-soft: #f8e4de;
19
20
  --warn: #8a6200;
@@ -78,6 +79,7 @@
78
79
  --accent: #5cc796;
79
80
  --accent-fg: #0b1410;
80
81
  --accent-soft: #163024;
82
+ --brand-ink: #ffffff;
81
83
  --danger: #f08a74;
82
84
  --danger-soft: #3a1d17;
83
85
  --warn: #e3b341;