clauderipple 0.10.0 → 0.11.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 (78) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.es.md +45 -0
  3. package/README.ja.md +17 -0
  4. package/README.ko.md +31 -0
  5. package/README.md +39 -0
  6. package/README.pt-BR.md +45 -0
  7. package/README.zh-CN.md +17 -0
  8. package/dist/app/assets/icon.icns +0 -0
  9. package/dist/app/assets/icon.ico +0 -0
  10. package/dist/app/assets/icon.png +0 -0
  11. package/dist/app/assets/trayDirectTemplate.png +0 -0
  12. package/dist/app/assets/trayDirectTemplate@2x.png +0 -0
  13. package/dist/app/assets/trayDownTemplate.png +0 -0
  14. package/dist/app/assets/trayDownTemplate@2x.png +0 -0
  15. package/dist/app/assets/trayTemplate.png +0 -0
  16. package/dist/app/assets/trayTemplate@2x.png +0 -0
  17. package/dist/app/assets/trayWarnTemplate.png +0 -0
  18. package/dist/app/assets/trayWarnTemplate@2x.png +0 -0
  19. package/dist/app/assets/trayWin.png +0 -0
  20. package/dist/app/assets/trayWin@2x.png +0 -0
  21. package/dist/app/assets/trayWinDirect.png +0 -0
  22. package/dist/app/assets/trayWinDirect@2x.png +0 -0
  23. package/dist/app/assets/trayWinDown.png +0 -0
  24. package/dist/app/assets/trayWinDown@2x.png +0 -0
  25. package/dist/app/assets/trayWinWarn.png +0 -0
  26. package/dist/app/assets/trayWinWarn@2x.png +0 -0
  27. package/dist/app/dist/claude-desktop.js +67 -0
  28. package/dist/app/dist/main.js +423 -110
  29. package/dist/app/dist/preload.js +21 -0
  30. package/dist/app/dist/protocol.js +121 -0
  31. package/dist/app/dist/status-model.js +87 -0
  32. package/dist/app/dist/window.js +133 -0
  33. package/dist/app/package.json +1 -1
  34. package/dist/cli/src/awake-setup.js +314 -0
  35. package/dist/cli/src/bundle.js +9 -5
  36. package/dist/cli/src/direct.js +159 -0
  37. package/dist/cli/src/doctor.js +205 -0
  38. package/dist/cli/src/index.js +184 -31
  39. package/dist/cli/src/launchd.js +29 -20
  40. package/dist/cli/src/launcher.js +215 -0
  41. package/dist/cli/src/power.js +182 -0
  42. package/dist/cli/src/registration.js +157 -0
  43. package/dist/cli/src/schtasks.js +2 -0
  44. package/dist/cli/src/settings.js +24 -0
  45. package/dist/cli/src/tray.js +5 -2
  46. package/dist/mod/hooks/i18n.ts +237 -237
  47. package/dist/mod/hooks/register.tsx +4 -5
  48. package/dist/mod/hooks/session.ts +6 -2
  49. package/dist/mod/hooks/ui/agents.tsx +81 -36
  50. package/dist/mod/hooks/ui/view.ts +8 -5
  51. package/dist/mod/tests/ui.test.tsx +46 -13
  52. package/dist/mod/types/index.d.ts +0 -2
  53. package/dist/remote/src/bridge-client.js +45 -4
  54. package/dist/router/src/admin.js +105 -2
  55. package/dist/router/src/awake-win.js +590 -0
  56. package/dist/router/src/awake.js +432 -0
  57. package/dist/router/src/config.js +64 -0
  58. package/dist/router/src/health.js +86 -3
  59. package/dist/router/src/hold.js +107 -0
  60. package/dist/router/src/index.js +69 -6
  61. package/dist/router/src/ingress/server.js +26 -4
  62. package/dist/router/src/power.js +107 -0
  63. package/dist/router/src/providers/google/antigravity.js +73 -10
  64. package/dist/router/src/providers/google/index.js +168 -17
  65. package/dist/router/src/providers/google/thoughtSignatures.js +13 -0
  66. package/dist/router/src/providers/google/transport.js +1 -1
  67. package/dist/router/src/providers/google/usage.js +71 -26
  68. package/dist/router/src/providers/retry.js +4 -1
  69. package/dist/router/src/proxy.js +50 -21
  70. package/dist/router/src/tether-win.js +211 -0
  71. package/dist/router/src/tether.js +712 -0
  72. package/dist/router/src/version.js +1 -1
  73. package/dist/ui/app.js +649 -2
  74. package/dist/ui/i18n.js +494 -77
  75. package/dist/ui/index.html +9 -1
  76. package/dist/ui/style.css +138 -0
  77. package/docs/ARCHITECTURE.md +294 -6
  78. package/package.json +2 -2
@@ -14,6 +14,9 @@
14
14
  // PATCH/DELETE /api/google-accounts/:id rename, pause/resume, clear a cooldown; remove
15
15
  // GET /api/update whether a newer version is published, and how this copy was installed
16
16
  // POST /api/update start `clauderipple update` in the background (it restarts this router)
17
+ // GET /api/power lid-closed awake + hotspot failover state (power.ts)
18
+ // PUT /api/power change awake mode/until, tether on/off (saved), pin (in memory)
19
+ // POST /api/power/setup start `clauderipple awake setup [--ssid]` (macOS password dialog); GET polls it
17
20
  // GET /* static files from packages/ui (the GUI itself)
18
21
  import crypto from "node:crypto";
19
22
  import { phoneRequest } from "./phone.js";
@@ -24,7 +27,7 @@ import path from "node:path";
24
27
  import http from "node:http";
25
28
  import { execFile, spawn } from "node:child_process";
26
29
  import { fileURLToPath } from "node:url";
27
- import { adminPort, headerValueProblem, homeDir, searchProviderFor, validate } from "./config.js";
30
+ import { adminPort, headerValueProblem, homeDir, searchProviderFor, ssidProblem, validate } from "./config.js";
28
31
  export { adminPort } from "./config.js";
29
32
  import { generatedAgents } from "./agents.js";
30
33
  import { isJudgeOnlyModel } from "./model-policy.js";
@@ -60,6 +63,7 @@ import { GoogleOAuthSession } from "./providers/google/login.js";
60
63
  import { GROK_AUTH_HINT, grokAuthPath, grokHome, readGrokToken, sharedGrokAuth } from "./providers/grok/auth.js";
61
64
  import { GROK_DEFAULT_URL, GROK_EFFORT_LEVELS, GROK_FALLBACK_MODELS, grokModelsFromListing } from "./providers/grok/catalog.js";
62
65
  import { GROK_WARNING, grokHeaders } from "./providers/grok/index.js";
66
+ import { parsePowerChange, persistPowerChange } from "./power.js";
63
67
  const MAX_BODY = 1024 * 1024;
64
68
  /** `/api/image` carries reference images inline, so it gets more room than a config save. */
65
69
  const MAX_IMAGE_BODY = 32 * 1024 * 1024;
@@ -190,12 +194,17 @@ async function googleLimitAccounts(deps, name) {
190
194
  const groups = (await google.usage?.(name)) ?? {};
191
195
  const kept = statuses.filter((a) => a.state !== "paused" && a.state !== "needs-login");
192
196
  const at = Date.now();
197
+ // The third-party (Claude and GPT) windows only when this provider has such a model selected:
198
+ // beside Gemini's own they made four figures, two of them a pool nobody here spends (2026-10-20).
199
+ const provider = deps.config().providers[name];
200
+ const models = provider && "models" in provider && Array.isArray(provider.models) ? provider.models : [];
201
+ const thirdParty = models.some((m) => /^(claude-|gpt-oss(-|$))/i.test(typeof m === "string" ? m : m.id));
193
202
  const out = [];
194
203
  kept.forEach((account, index) => {
195
204
  const quotient = groups[account.id];
196
205
  if (!quotient || !quotient.length)
197
206
  return;
198
- const windows = googleWindowsFromGroups(quotient);
207
+ const windows = googleWindowsFromGroups(quotient).filter((w) => thirdParty || w.kind !== "scoped");
199
208
  if (windows.length)
200
209
  out.push({ account: index, windows, at });
201
210
  });
@@ -1665,6 +1674,8 @@ export function startAdmin(deps) {
1665
1674
  const scores = new ModelScoreCatalog({ home: homeDir(), warn: message => deps.log.warn(message), ...(deps.modelScoresFetch ? { fetch: deps.modelScoresFetch } : {}) });
1666
1675
  // Set again once the socket is bound (port 0 in tests); the Origin check below compares against it.
1667
1676
  let boundPort = adminPort(deps.config());
1677
+ /** The dashboard's `awake setup` run in progress (or the last one), for GET /api/power/setup. */
1678
+ let powerSetup = { running: false };
1668
1679
  const server = http.createServer((req, res) => {
1669
1680
  void handle(req, res);
1670
1681
  });
@@ -1747,6 +1758,98 @@ export function startAdmin(deps) {
1747
1758
  sendJson(res, 200, { hosts: deps.remote.reload() });
1748
1759
  return;
1749
1760
  }
1761
+ if (pathname === "/api/power" && (method === "GET" || method === "PUT")) {
1762
+ if (!deps.power) {
1763
+ sendJson(res, 501, { error: "power control not available" });
1764
+ return;
1765
+ }
1766
+ if (method === "GET") {
1767
+ sendJson(res, 200, deps.power.status());
1768
+ return;
1769
+ }
1770
+ let parsed;
1771
+ try {
1772
+ parsed = JSON.parse((await readBody(req, 4096)).toString("utf8"));
1773
+ }
1774
+ catch {
1775
+ sendJson(res, 400, { error: "invalid JSON" });
1776
+ return;
1777
+ }
1778
+ const change = parsePowerChange(parsed);
1779
+ if (typeof change === "string") {
1780
+ sendJson(res, 400, { error: change });
1781
+ return;
1782
+ }
1783
+ if (persistPowerChange(deps.configFile, change)) {
1784
+ deps.log.info(`admin: power saved (${JSON.stringify({ ...(change.awake ? { awake: change.awake } : {}), ...(change.tether?.enabled !== undefined ? { tether: { enabled: change.tether.enabled } } : {}) })})`);
1785
+ }
1786
+ if (change.tether?.pin !== undefined)
1787
+ deps.power.pin(change.tether.pin);
1788
+ await deps.power.poke();
1789
+ sendJson(res, 200, deps.power.status());
1790
+ return;
1791
+ }
1792
+ // The one privileged step, from the dashboard: the CLI's `awake setup`, which shows macOS's own
1793
+ // password dialog. It runs in the background and is polled, like the ChatGPT sign-in: the user
1794
+ // may take minutes to type, and the app's window gives up on a response with no headers after
1795
+ // 4s (packages/app/src/protocol.ts) — a reply held open until the end would read as a dead router.
1796
+ if (pathname === "/api/power/setup" && (method === "GET" || method === "POST")) {
1797
+ if (!deps.power) {
1798
+ sendJson(res, 501, { error: "power control not available" });
1799
+ return;
1800
+ }
1801
+ if (method === "GET") {
1802
+ sendJson(res, 200, powerSetup);
1803
+ return;
1804
+ }
1805
+ let parsed;
1806
+ try {
1807
+ const text = (await readBody(req, 4096)).toString("utf8");
1808
+ parsed = text.trim() ? JSON.parse(text) : {};
1809
+ }
1810
+ catch {
1811
+ sendJson(res, 400, { error: "invalid JSON" });
1812
+ return;
1813
+ }
1814
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1815
+ sendJson(res, 400, { error: "expected an object" });
1816
+ return;
1817
+ }
1818
+ const extra = Object.keys(parsed).find((key) => key !== "ssid");
1819
+ if (extra) {
1820
+ sendJson(res, 400, { error: `unknown field "${extra}"` });
1821
+ return;
1822
+ }
1823
+ const ssid = parsed.ssid;
1824
+ if (ssid !== undefined) {
1825
+ // Checked here too so a bad name is refused before a password dialog, not after it.
1826
+ const problem = ssidProblem(ssid);
1827
+ if (problem) {
1828
+ sendJson(res, 400, { error: problem });
1829
+ return;
1830
+ }
1831
+ }
1832
+ if (powerSetup.running) {
1833
+ sendJson(res, 409, { error: "setup is already running", running: true });
1834
+ return;
1835
+ }
1836
+ powerSetup = { running: true, startedAt: new Date().toISOString() };
1837
+ const args = ["awake", "setup", ...(typeof ssid === "string" ? ["--ssid", ssid] : [])];
1838
+ deps.log.info(`admin: awake setup via GUI -> started${typeof ssid === "string" ? ` (hotspot "${ssid}")` : ""}`);
1839
+ const power = deps.power;
1840
+ // The CLI's own dialog times out at 5 min; this leaves it room to say so itself.
1841
+ void (deps.runCli ?? runCli)(args, 330_000).then((result) => ({ ok: result.ok, output: result.output }), (error) => ({ ok: false, output: errorText(error) })).then(async (result) => {
1842
+ powerSetup = { ...powerSetup, running: false, finishedAt: new Date().toISOString(), ...result };
1843
+ deps.log.info(`admin: awake setup via GUI -> ${result.ok ? "ok" : "failed"}`);
1844
+ // Re-read whether the rule is in place now, so the next GET /api/power says so.
1845
+ try {
1846
+ await power.poke();
1847
+ }
1848
+ catch { /* the next tick does it */ }
1849
+ });
1850
+ sendJson(res, 202, { started: true });
1851
+ return;
1852
+ }
1750
1853
  // Liveness is answering at all; readiness is 200 only when a request can actually go through.
1751
1854
  if (pathname === "/readyz" && method === "GET") {
1752
1855
  const readiness = (await buildStatus(deps)).readiness;