shadow-claw 1.28.0 → 1.29.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 (194) hide show
  1. package/.well-known/mcp/server-card.json +1 -1
  2. package/.well-known/mcp.json +1 -1
  3. package/README.md +96 -278
  4. package/bin/cli.mjs +4 -0
  5. package/bin/commands/mcp.mjs +409 -125
  6. package/bin/commands/mcp.test.mjs +151 -18
  7. package/bin/utils/control-client.mjs +338 -103
  8. package/bin/utils/control-client.test.mjs +14 -0
  9. package/dist/electron/main.cjs +179 -179
  10. package/dist/lib/components/index.js +21 -21
  11. package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
  12. package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
  13. package/dist/lib/components/settings/shadow-claw-git.js +1 -1
  14. package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
  15. package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
  16. package/dist/lib/components/settings/shadow-claw-notifications.js +1 -1
  17. package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
  18. package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
  19. package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
  20. package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
  21. package/dist/lib/components/shadow-claw-channel-config.js +1 -1
  22. package/dist/lib/components/shadow-claw-channels.js +1 -1
  23. package/dist/lib/components/shadow-claw-chat.js +1 -1
  24. package/dist/lib/components/shadow-claw-control-plane.js +1 -1
  25. package/dist/lib/components/shadow-claw-conversations.js +1 -1
  26. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  27. package/dist/lib/components/shadow-claw-files.js +1 -1
  28. package/dist/lib/components/shadow-claw-git.js +1 -1
  29. package/dist/lib/components/shadow-claw-llm.js +1 -1
  30. package/dist/lib/components/shadow-claw-networking.js +1 -1
  31. package/dist/lib/components/shadow-claw-notifications.js +1 -1
  32. package/dist/lib/components/shadow-claw-pages.js +1 -1
  33. package/dist/lib/components/shadow-claw-peerjs.js +1 -1
  34. package/dist/lib/components/shadow-claw-settings.js +1 -1
  35. package/dist/lib/components/shadow-claw-storage.js +1 -1
  36. package/dist/lib/components/shadow-claw-task-server.js +1 -1
  37. package/dist/lib/components/shadow-claw-tasks.js +1 -1
  38. package/dist/lib/components/shadow-claw-terminal.js +1 -1
  39. package/dist/lib/components/shadow-claw-tools.js +1 -1
  40. package/dist/lib/components/shadow-claw-webvm.js +1 -1
  41. package/dist/lib/components/shadow-claw.js +1 -1
  42. package/dist/lib/{configurePeerJs-D8Ph0-A9.js → configurePeerJs-DG4hliKp.js} +2 -2
  43. package/dist/lib/{configurePeerJs-D8Ph0-A9.js.map → configurePeerJs-DG4hliKp.js.map} +1 -1
  44. package/dist/lib/index.js +21 -21
  45. package/dist/lib/{initControlPlane-BIrWmpzc.js → initControlPlane-DnpAzphg.js} +8 -8
  46. package/dist/lib/{initControlPlane-BIrWmpzc.js.map → initControlPlane-DnpAzphg.js.map} +1 -1
  47. package/dist/lib/{markdown-DEUoM1c6.js → markdown-DgdSt2GD.js} +2 -2
  48. package/dist/lib/{markdown-DEUoM1c6.js.map → markdown-DgdSt2GD.js.map} +1 -1
  49. package/dist/lib/{orchestrator-CFCW-l1O.js → orchestrator-CjEau_RG.js} +5 -5
  50. package/dist/lib/{orchestrator-CFCW-l1O.js.map → orchestrator-CjEau_RG.js.map} +1 -1
  51. package/dist/lib/{orchestrator-BtVj9bBB.js → orchestrator-ClXbJ4Mb.js} +2 -2
  52. package/dist/lib/{orchestrator-BtVj9bBB.js.map → orchestrator-ClXbJ4Mb.js.map} +1 -1
  53. package/dist/lib/{peerjs-BoBkx9DJ.js → peerjs-BdfpltIe.js} +3 -3
  54. package/dist/lib/{peerjs-BoBkx9DJ.js.map → peerjs-BdfpltIe.js.map} +1 -1
  55. package/dist/lib/{push-client-D6hbzDNV.js → push-client-DjwKknNg.js} +9 -3
  56. package/dist/lib/push-client-DjwKknNg.js.map +1 -0
  57. package/dist/lib/{shadow-claw-Cuf2O5bZ.js → shadow-claw-DcPGKenn.js} +4 -4
  58. package/dist/lib/{shadow-claw-Cuf2O5bZ.js.map → shadow-claw-DcPGKenn.js.map} +1 -1
  59. package/dist/lib/{shadow-claw-channel-config-BDPKt6Bd.js → shadow-claw-channel-config-DEL-aZ7N.js} +3 -3
  60. package/dist/lib/{shadow-claw-channel-config-BDPKt6Bd.js.map → shadow-claw-channel-config-DEL-aZ7N.js.map} +1 -1
  61. package/dist/lib/{shadow-claw-channels-BsP5JABM.js → shadow-claw-channels-BgaJiPdE.js} +2 -2
  62. package/dist/lib/{shadow-claw-channels-BsP5JABM.js.map → shadow-claw-channels-BgaJiPdE.js.map} +1 -1
  63. package/dist/lib/{shadow-claw-chat-Cam2lX4k.js → shadow-claw-chat-mXPQZbwT.js} +4 -4
  64. package/dist/lib/{shadow-claw-chat-Cam2lX4k.js.map → shadow-claw-chat-mXPQZbwT.js.map} +1 -1
  65. package/dist/lib/{shadow-claw-control-plane-DB2qcCFs.js → shadow-claw-control-plane-DO6foOLV.js} +6 -6
  66. package/dist/lib/{shadow-claw-control-plane-DB2qcCFs.js.map → shadow-claw-control-plane-DO6foOLV.js.map} +1 -1
  67. package/dist/lib/{shadow-claw-conversations-C9Vf4XGx.js → shadow-claw-conversations-BLwHTwdW.js} +2 -2
  68. package/dist/lib/{shadow-claw-conversations-C9Vf4XGx.js.map → shadow-claw-conversations-BLwHTwdW.js.map} +1 -1
  69. package/dist/lib/{shadow-claw-file-viewer-B4HYiI0v.js → shadow-claw-file-viewer-BSbZvSl-.js} +3 -3
  70. package/dist/lib/{shadow-claw-file-viewer-B4HYiI0v.js.map → shadow-claw-file-viewer-BSbZvSl-.js.map} +1 -1
  71. package/dist/lib/{shadow-claw-files-DVROUESO.js → shadow-claw-files-BLVr9Pua.js} +2 -2
  72. package/dist/lib/{shadow-claw-files-DVROUESO.js.map → shadow-claw-files-BLVr9Pua.js.map} +1 -1
  73. package/dist/lib/{shadow-claw-git-BqrO7cHJ.js → shadow-claw-git-CHC1H86J.js} +3 -3
  74. package/dist/lib/{shadow-claw-git-BqrO7cHJ.js.map → shadow-claw-git-CHC1H86J.js.map} +1 -1
  75. package/dist/lib/{shadow-claw-llm-BexUQdYo.js → shadow-claw-llm-CKXfxd3D.js} +2 -2
  76. package/dist/lib/{shadow-claw-llm-BexUQdYo.js.map → shadow-claw-llm-CKXfxd3D.js.map} +1 -1
  77. package/dist/lib/{shadow-claw-networking-CfXRGpUD.js → shadow-claw-networking-dpYvfchh.js} +2 -2
  78. package/dist/lib/{shadow-claw-networking-CfXRGpUD.js.map → shadow-claw-networking-dpYvfchh.js.map} +1 -1
  79. package/dist/lib/{shadow-claw-notifications-C9KPMgx_.js → shadow-claw-notifications-_YeAVPga.js} +2 -2
  80. package/dist/lib/{shadow-claw-notifications-C9KPMgx_.js.map → shadow-claw-notifications-_YeAVPga.js.map} +1 -1
  81. package/dist/lib/{shadow-claw-pages-CGzGXmrI.js → shadow-claw-pages-DVzZriZ7.js} +3 -3
  82. package/dist/lib/{shadow-claw-pages-CGzGXmrI.js.map → shadow-claw-pages-DVzZriZ7.js.map} +1 -1
  83. package/dist/lib/{shadow-claw-peerjs-B0ZB7yhB.js → shadow-claw-peerjs-BvixjeBz.js} +3 -3
  84. package/dist/lib/{shadow-claw-peerjs-B0ZB7yhB.js.map → shadow-claw-peerjs-BvixjeBz.js.map} +1 -1
  85. package/dist/lib/{shadow-claw-settings-DBfMZdDS.js → shadow-claw-settings-DV9-gD4Q.js} +2 -2
  86. package/dist/lib/{shadow-claw-settings-DBfMZdDS.js.map → shadow-claw-settings-DV9-gD4Q.js.map} +1 -1
  87. package/dist/lib/{shadow-claw-storage-CM-w6mgG.js → shadow-claw-storage-Bv__kXG6.js} +2 -2
  88. package/dist/lib/{shadow-claw-storage-CM-w6mgG.js.map → shadow-claw-storage-Bv__kXG6.js.map} +1 -1
  89. package/dist/lib/{shadow-claw-task-server-DV4b1g2-.js → shadow-claw-task-server-DNOe8PWK.js} +2 -2
  90. package/dist/lib/{shadow-claw-task-server-DV4b1g2-.js.map → shadow-claw-task-server-DNOe8PWK.js.map} +1 -1
  91. package/dist/lib/{shadow-claw-tasks-K-foOHSE.js → shadow-claw-tasks-qmcDT4PU.js} +3 -3
  92. package/dist/lib/{shadow-claw-tasks-K-foOHSE.js.map → shadow-claw-tasks-qmcDT4PU.js.map} +1 -1
  93. package/dist/lib/{shadow-claw-terminal-CpAknKyA.js → shadow-claw-terminal-0QUWP8of.js} +2 -2
  94. package/dist/lib/{shadow-claw-terminal-CpAknKyA.js.map → shadow-claw-terminal-0QUWP8of.js.map} +1 -1
  95. package/dist/lib/{shadow-claw-tools-D98XZ0Ep.js → shadow-claw-tools-Bs_bmI6-.js} +3 -3
  96. package/dist/lib/{shadow-claw-tools-D98XZ0Ep.js.map → shadow-claw-tools-Bs_bmI6-.js.map} +1 -1
  97. package/dist/lib/{shadow-claw-webvm-DLOb8dMI.js → shadow-claw-webvm-DYaoL7QY.js} +2 -2
  98. package/dist/lib/{shadow-claw-webvm-DLOb8dMI.js.map → shadow-claw-webvm-DYaoL7QY.js.map} +1 -1
  99. package/dist/lib/{syncWebMcpRegistration-NspKefOI.js → syncWebMcpRegistration-BhbqI6R2.js} +2 -2
  100. package/dist/lib/{syncWebMcpRegistration-NspKefOI.js.map → syncWebMcpRegistration-BhbqI6R2.js.map} +1 -1
  101. package/dist/public/.well-known/mcp/server-card.json +1 -1
  102. package/dist/public/.well-known/mcp.json +1 -1
  103. package/dist/public/AGENTS.md +22 -3
  104. package/dist/public/README.md +96 -278
  105. package/dist/public/agent.worker.js +50 -50
  106. package/dist/public/{base-task-model-CYkpwnvU-DDfHUUIS.js → base-task-model-CYkpwnvU-DrXr4G1R.js} +1 -1
  107. package/dist/public/{configurePeerJs-BhBokQlC.js → configurePeerJs-B470pdeU.js} +1 -1
  108. package/dist/public/docs/README.md +31 -31
  109. package/dist/public/docs/architecture/context-management.md +7 -0
  110. package/dist/public/docs/architecture/storage.md +10 -1
  111. package/dist/public/docs/decisions/bundled-typescript-architecture.md +0 -1
  112. package/dist/public/docs/decisions/native-web-components-and-signals.md +0 -1
  113. package/dist/public/docs/example/article/index.html +1 -1
  114. package/dist/public/docs/guides/publishing-to-github-pages.md +334 -0
  115. package/dist/public/docs/guides/server-development-configuration.md +15 -0
  116. package/dist/public/docs/publishing/index.html +1 -1
  117. package/dist/public/docs/skill-creator/index.html +1 -1
  118. package/dist/public/docs/subsystems/cli.md +10 -2
  119. package/dist/public/docs/subsystems/control-plane.md +9 -8
  120. package/dist/public/docs/subsystems/crypto.md +9 -0
  121. package/dist/public/docs/subsystems/custom-element-security.md +2 -0
  122. package/dist/public/docs/subsystems/mcp-server.md +31 -16
  123. package/dist/public/docs/subsystems/notifications.md +8 -5
  124. package/dist/public/docs/subsystems/providers.md +9 -0
  125. package/dist/public/docs/subsystems/share-target.md +107 -0
  126. package/dist/public/docs/subsystems/webmcp.md +43 -0
  127. package/dist/public/e2e/README.md +4 -2
  128. package/dist/public/{e2e-bridge-DcBoeFvd.js → e2e-bridge-CSW0S5Yz.js} +1 -1
  129. package/dist/public/{file-viewer-CKtVC9Ix.js → file-viewer-Cs-riNvy.js} +1 -1
  130. package/dist/public/{firebase-OSDX3viP.js → firebase-cwAl_ABm.js} +1 -1
  131. package/dist/public/{git-CkhCJonj.js → git-GD09WATp.js} +25 -25
  132. package/dist/public/index.html +1 -1
  133. package/dist/public/index.js +1 -1
  134. package/dist/public/{initControlPlane-Bi8ouFEb.js → initControlPlane-BqCY0NQy.js} +1 -1
  135. package/dist/public/initControlPlane-C-IXaKYX.js +1 -0
  136. package/dist/public/{language-detector-BRKrlra0.js → language-detector-CIeEuQFk.js} +1 -1
  137. package/dist/public/llms.txt +4 -2
  138. package/dist/public/main/index.html +1 -1
  139. package/dist/public/main/memory/index.html +1 -1
  140. package/dist/public/{markdown-C-hCVJtc.js → markdown-Cw7vCtTg.js} +1 -1
  141. package/dist/public/{openai-Bpd71Oja.js → openai-BJIb2c5j.js} +3 -3
  142. package/dist/public/{orchestrator-8qNGfw8r.js → orchestrator-BWkHHO9Y.js} +1 -1
  143. package/dist/public/{orchestrator-BT3Ks5ip.js → orchestrator-DpkkeD6T.js} +1 -1
  144. package/dist/public/{peerjs-DfhwEiiq.js → peerjs-tf2EmWjB.js} +1 -1
  145. package/dist/public/{prompt-api-polyfill-Dl1kbJhR.js → prompt-api-polyfill-CXd75leW.js} +1 -1
  146. package/dist/public/{push-client-BVLrE9pK.js → push-client-CHjSbrWs.js} +1 -1
  147. package/dist/public/{rewriter-j6D9LDER.js → rewriter-BEHn9gll.js} +1 -1
  148. package/dist/public/service-worker.js +1 -1
  149. package/dist/public/{shadow-claw-DCjxNDCX.js → shadow-claw-BBPV_9X8.js} +2 -2
  150. package/dist/public/{shadow-claw-channels-CITDrqMl.js → shadow-claw-channels-BJvrKQjg.js} +1 -1
  151. package/dist/public/{shadow-claw-chat-BQrSgSKF.js → shadow-claw-chat-BG2EaRaw.js} +1 -1
  152. package/dist/public/{shadow-claw-control-plane-Cp001OY0.js → shadow-claw-control-plane-BBa7WbTg.js} +2 -2
  153. package/dist/public/{shadow-claw-conversations-EhX4RvIj.js → shadow-claw-conversations-CFaWFmkm.js} +1 -1
  154. package/dist/public/{shadow-claw-file-viewer-DiQXIjZD.js → shadow-claw-file-viewer-Z-GIKaE9.js} +1 -1
  155. package/dist/public/{shadow-claw-files-lkTnSJsn.js → shadow-claw-files-BBx4iMsk.js} +1 -1
  156. package/dist/public/{shadow-claw-git-BlcA_z8x.js → shadow-claw-git-vTOKatwd.js} +2 -2
  157. package/dist/public/{shadow-claw-llm-CW16X9Rg.js → shadow-claw-llm-Ce1Ax6-x.js} +1 -1
  158. package/dist/public/{shadow-claw-networking-ySsMBY_i.js → shadow-claw-networking-q4XIzdB5.js} +1 -1
  159. package/dist/public/{shadow-claw-notifications-CyV2zCQ8.js → shadow-claw-notifications-D2cz0gmm.js} +1 -1
  160. package/dist/public/{shadow-claw-pages-Cx7cF9ah.js → shadow-claw-pages-3335lmSC.js} +1 -1
  161. package/dist/public/{shadow-claw-settings-zm4PHHGi.js → shadow-claw-settings-4ptYuJwO.js} +2 -2
  162. package/dist/public/{shadow-claw-storage-dy9JAEGd.js → shadow-claw-storage-bIrPKFhF.js} +1 -1
  163. package/dist/public/{shadow-claw-task-server-Bleww48s.js → shadow-claw-task-server-5yqVBxy1.js} +1 -1
  164. package/dist/public/{shadow-claw-tasks-CTI5oaLN.js → shadow-claw-tasks-DcqYHPLZ.js} +1 -1
  165. package/dist/public/{shadow-claw-terminal-mZ3i4HaQ.js → shadow-claw-terminal-fARaDONr.js} +1 -1
  166. package/dist/public/{shadow-claw-tools-B_nuk8Zl.js → shadow-claw-tools-paEAhcJr.js} +1 -1
  167. package/dist/public/{shadow-claw-webvm-Vl_jH5YL.js → shadow-claw-webvm-CbzH_vyX.js} +1 -1
  168. package/dist/public/{summarizer-VJNr-7xG.js → summarizer-BT3AuFed.js} +1 -1
  169. package/dist/public/{syncWebMcpRegistration-n2vAMJ4w.js → syncWebMcpRegistration-CWSYaHex.js} +1 -1
  170. package/dist/public/{transformers-BhQMWQG5.js → transformers-1VTxqKjT.js} +1 -1
  171. package/dist/public/{translator-DEQXb7ZL.js → translator-B50RYlFt.js} +1 -1
  172. package/dist/public/{webllm-CHN5H-Sy.js → webllm-FmyJWWpt.js} +1 -1
  173. package/dist/public/{writer-CD9oN2pq.js → writer-Bhb4YYRE.js} +1 -1
  174. package/dist/server.js +205 -203
  175. package/package.json +1 -1
  176. package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +2 -0
  177. package/src/core/orchestrator/utils/configurePeerJs.test.ts +2 -0
  178. package/src/server/client-registry.test.ts +9 -0
  179. package/src/server/client-registry.ts +14 -0
  180. package/src/server/mcp/mcp-integration.test.ts +68 -16
  181. package/src/server/mcp/tools/built-in-tools.ts +125 -34
  182. package/src/server/mcp/tools/client-tool-relay.ts +81 -44
  183. package/src/server/mcp/tools/mcp-tools.test.ts +103 -13
  184. package/src/server/mcp/types.ts +3 -0
  185. package/src/server/openapi/paths/push.ts +48 -0
  186. package/src/server/server.ts +30 -0
  187. package/src/subsystems/notifications/push-client.test.ts +17 -0
  188. package/src/subsystems/notifications/push-client.ts +22 -2
  189. package/src/subsystems/notifications/push-routes.test.ts +31 -0
  190. package/src/subsystems/notifications/push-routes.ts +10 -0
  191. package/src/subsystems/notifications/push-store.test.ts +49 -0
  192. package/src/subsystems/notifications/push-store.ts +45 -0
  193. package/src/worker/worker.test.ts +2 -0
  194. package/dist/lib/push-client-D6hbzDNV.js.map +0 -1
@@ -12,74 +12,157 @@ import fs from "node:fs";
12
12
  import { tmpdir } from "node:os";
13
13
  import { CliWebRtcControlClient } from "./webrtc-control-client.mjs";
14
14
 
15
- export function resolveControlToken(customToken, cacheDir) {
16
- if (customToken && typeof customToken === "string") {
17
- return customToken.trim();
15
+ export function resolveControlTokens(customToken, cacheDir, port) {
16
+ if (customToken && typeof customToken === "string" && customToken.trim()) {
17
+ return [customToken.trim()];
18
18
  }
19
19
 
20
20
  if (process.env.SHADOWCLAW_CONTROL_TOKEN) {
21
- return process.env.SHADOWCLAW_CONTROL_TOKEN.trim();
21
+ return [process.env.SHADOWCLAW_CONTROL_TOKEN.trim()];
22
22
  }
23
23
 
24
24
  const explicitCacheDir =
25
25
  cacheDir || (process.env.SHADOWCLAW_CACHE_DIR || "").trim();
26
26
  const tmpCacheDir = path.join(tmpdir(), "shadow-claw");
27
+ const home = typeof process.env.HOME === "string" ? process.env.HOME : "";
27
28
 
28
- // 1. Try reading from control-token.json
29
- try {
30
- const candidates = [
31
- ...(explicitCacheDir
32
- ? [path.join(explicitCacheDir, "control-token.json")]
33
- : []),
34
- path.join(process.cwd(), ".cache", "control-token.json"),
35
- path.join(process.cwd(), "control-token.json"),
36
- path.join(tmpCacheDir, "control-token.json"),
37
- ];
38
- for (const filePath of candidates) {
39
- if (fs.existsSync(filePath)) {
29
+ const candidatesWithTime = [];
30
+
31
+ const checkTokenFile = (filePath) => {
32
+ try {
33
+ if (filePath && fs.existsSync(filePath)) {
34
+ const stat = fs.statSync(filePath);
40
35
  const raw = fs.readFileSync(filePath, "utf8");
41
36
  const parsed = JSON.parse(raw);
42
37
  if (parsed && typeof parsed.token === "string" && parsed.token.trim()) {
43
- return parsed.token.trim();
38
+ const timestamp = Number(parsed.createdAt) || stat.mtimeMs || 0;
39
+ candidatesWithTime.push({
40
+ token: parsed.token.trim(),
41
+ timestamp,
42
+ filePath,
43
+ });
44
44
  }
45
45
  }
46
- }
47
- } catch (_) {}
46
+ } catch (_) {}
47
+ };
48
48
 
49
- // 2. Try reading from SQLite clients.db if available locally
50
- try {
51
- let sqlite;
52
- if (typeof process.getBuiltinModule === "function") {
53
- sqlite = process.getBuiltinModule("node:sqlite");
49
+ const checkDbFile = (dbPath) => {
50
+ try {
51
+ let sqlite;
52
+ if (typeof process.getBuiltinModule === "function") {
53
+ sqlite = process.getBuiltinModule("node:sqlite");
54
+ }
55
+ if (sqlite && sqlite.DatabaseSync && fs.existsSync(dbPath)) {
56
+ const stat = fs.statSync(dbPath);
57
+ const db = new sqlite.DatabaseSync(dbPath);
58
+ const row = db
59
+ .prepare("SELECT value FROM metadata WHERE key = 'control_token'")
60
+ .get();
61
+ db.close();
62
+ if (row && row.value) {
63
+ candidatesWithTime.push({
64
+ token: `${row.value}`.trim(),
65
+ timestamp: stat.mtimeMs || 0,
66
+ filePath: dbPath,
67
+ });
68
+ }
69
+ }
70
+ } catch (_) {}
71
+ };
72
+
73
+ if (explicitCacheDir) {
74
+ checkTokenFile(path.join(explicitCacheDir, "control-token.json"));
75
+ checkDbFile(path.join(explicitCacheDir, "database", "clients.db"));
76
+ checkDbFile(path.join(explicitCacheDir, "clients.db"));
77
+ } else {
78
+ // 1. Check system tmp directory (including port-specific token)
79
+ if (port) {
80
+ checkTokenFile(path.join(tmpCacheDir, `control-token-${port}.json`));
54
81
  }
55
- if (sqlite && sqlite.DatabaseSync) {
56
- const candidates = [
57
- ...(explicitCacheDir
58
- ? [
59
- path.join(explicitCacheDir, "database", "clients.db"),
60
- path.join(explicitCacheDir, "clients.db"),
61
- ]
62
- : []),
63
- path.join(process.cwd(), "database", "clients.db"),
64
- path.join(process.cwd(), ".cache", "database", "clients.db"),
65
- path.join(tmpCacheDir, "database", "clients.db"),
66
- ];
67
- for (const dbPath of candidates) {
68
- if (fs.existsSync(dbPath)) {
69
- const db = new sqlite.DatabaseSync(dbPath);
70
- const row = db
71
- .prepare("SELECT value FROM metadata WHERE key = 'control_token'")
72
- .get();
73
- db.close();
74
- if (row && row.value) {
75
- return `${row.value}`;
76
- }
82
+ checkTokenFile(path.join(tmpCacheDir, "control-token.json"));
83
+ checkDbFile(path.join(tmpCacheDir, "database", "clients.db"));
84
+
85
+ // 2. Check current working directory
86
+ checkTokenFile(path.join(process.cwd(), ".cache", "control-token.json"));
87
+ checkTokenFile(path.join(process.cwd(), "control-token.json"));
88
+ checkDbFile(path.join(process.cwd(), ".cache", "database", "clients.db"));
89
+ checkDbFile(path.join(process.cwd(), "database", "clients.db"));
90
+
91
+ // 3. Check immediate subdirectories (e.g. test/.cache, demo/.cache)
92
+ try {
93
+ const entries = fs.readdirSync(process.cwd(), { withFileTypes: true });
94
+ for (const entry of entries) {
95
+ if (
96
+ entry.isDirectory() &&
97
+ !entry.name.startsWith("node_modules") &&
98
+ !entry.name.startsWith(".")
99
+ ) {
100
+ const subDir = path.join(process.cwd(), entry.name);
101
+ checkTokenFile(path.join(subDir, ".cache", "control-token.json"));
102
+ checkTokenFile(path.join(subDir, "control-token.json"));
103
+ checkDbFile(path.join(subDir, ".cache", "database", "clients.db"));
77
104
  }
78
105
  }
106
+ } catch (_) {}
107
+
108
+ // 4. Walk up parent directories (up to 4 levels) and check siblings
109
+ try {
110
+ let cur = process.cwd();
111
+ for (let i = 0; i < 4; i++) {
112
+ const parent = path.dirname(cur);
113
+ if (!parent || parent === cur) break;
114
+ checkTokenFile(path.join(parent, ".cache", "control-token.json"));
115
+ checkTokenFile(path.join(parent, "control-token.json"));
116
+ checkDbFile(path.join(parent, ".cache", "database", "clients.db"));
117
+ try {
118
+ const subEntries = fs.readdirSync(parent, { withFileTypes: true });
119
+ for (const sub of subEntries) {
120
+ if (
121
+ sub.isDirectory() &&
122
+ !sub.name.startsWith("node_modules") &&
123
+ !sub.name.startsWith(".")
124
+ ) {
125
+ const subDir = path.join(parent, sub.name);
126
+ checkTokenFile(path.join(subDir, ".cache", "control-token.json"));
127
+ checkTokenFile(path.join(subDir, "control-token.json"));
128
+ checkDbFile(
129
+ path.join(subDir, ".cache", "database", "clients.db"),
130
+ );
131
+ }
132
+ }
133
+ } catch (_) {}
134
+ cur = parent;
135
+ }
136
+ } catch (_) {}
137
+
138
+ // 5. User home directory
139
+ if (home) {
140
+ checkTokenFile(
141
+ path.join(home, ".config", "shadow-claw", "control-token.json"),
142
+ );
143
+ checkTokenFile(
144
+ path.join(home, ".cache", "shadow-claw", "control-token.json"),
145
+ );
79
146
  }
80
- } catch (_) {}
147
+ }
81
148
 
82
- return "";
149
+ // Sort descending by timestamp (newest first) and deduplicate
150
+ candidatesWithTime.sort((a, b) => b.timestamp - a.timestamp);
151
+ const seen = new Set();
152
+ const tokens = [];
153
+ for (const item of candidatesWithTime) {
154
+ if (item.token && !seen.has(item.token)) {
155
+ seen.add(item.token);
156
+ tokens.push(item.token);
157
+ }
158
+ }
159
+
160
+ return tokens;
161
+ }
162
+
163
+ export function resolveControlToken(customToken, cacheDir, port) {
164
+ const tokens = resolveControlTokens(customToken, cacheDir, port);
165
+ return tokens[0] || "";
83
166
  }
84
167
 
85
168
  export class CliControlClient {
@@ -87,8 +170,12 @@ export class CliControlClient {
87
170
  this.host = options.host || process.env.SHADOWCLAW_HOST || "127.0.0.1";
88
171
  this.port =
89
172
  options.port || parseInt(process.env.SHADOWCLAW_PORT || "8888", 10);
90
- this.token =
91
- options.token || resolveControlToken(options.token, options.cacheDir);
173
+ this.candidateTokens = resolveControlTokens(
174
+ options.token,
175
+ options.cacheDir,
176
+ this.port,
177
+ );
178
+ this.token = options.token || this.candidateTokens[0] || "";
92
179
  const isHttps = Boolean(
93
180
  options.https ||
94
181
  ["1", "true", "yes"].includes(
@@ -116,68 +203,106 @@ export class CliControlClient {
116
203
  : null;
117
204
  }
118
205
 
119
- _request({ method = "GET", path: reqPath, body = null, timeout = 30000 }) {
120
- return new Promise((resolve, reject) => {
121
- const client = this.protocol === "https" ? https : http;
122
- const headers = {
123
- "Content-Type": "application/json",
124
- };
206
+ async _request({
207
+ method = "GET",
208
+ path: reqPath,
209
+ body = null,
210
+ timeout = 30000,
211
+ }) {
212
+ const makeReq = (tokenToUse) => {
213
+ return new Promise((resolve, reject) => {
214
+ const client = this.protocol === "https" ? https : http;
215
+ const headers = {
216
+ "Content-Type": "application/json",
217
+ };
125
218
 
126
- if (this.token) {
127
- headers["x-control-token"] = this.token;
128
- }
219
+ if (tokenToUse) {
220
+ headers["x-control-token"] = tokenToUse;
221
+ }
129
222
 
130
- const reqOptions = {
131
- hostname: this.host,
132
- port: this.port,
133
- path: reqPath,
134
- method,
135
- headers,
136
- timeout,
137
- };
138
-
139
- if (this.protocol === "https") {
140
- reqOptions.rejectUnauthorized = this.rejectUnauthorized;
141
- }
223
+ const reqOptions = {
224
+ hostname: this.host,
225
+ port: this.port,
226
+ path: reqPath,
227
+ method,
228
+ headers,
229
+ timeout,
230
+ };
142
231
 
143
- const req = client.request(reqOptions, (res) => {
144
- let data = "";
145
- res.on("data", (chunk) => (data += chunk));
146
- res.on("end", () => {
147
- let parsed = data;
148
- try {
149
- parsed = JSON.parse(data);
150
- } catch (_) {}
151
-
152
- if (res.statusCode >= 200 && res.statusCode < 300) {
153
- resolve(parsed);
154
- } else {
155
- const errMsg =
156
- parsed?.error ||
157
- `HTTP request failed with status ${res.statusCode}: ${data}`;
158
- reject(new Error(errMsg));
159
- }
232
+ if (this.protocol === "https") {
233
+ reqOptions.rejectUnauthorized = this.rejectUnauthorized;
234
+ }
235
+
236
+ const req = client.request(reqOptions, (res) => {
237
+ let data = "";
238
+ res.on("data", (chunk) => (data += chunk));
239
+ res.on("end", () => {
240
+ let parsed = data;
241
+ try {
242
+ parsed = JSON.parse(data);
243
+ } catch (_) {}
244
+
245
+ if (res.statusCode >= 200 && res.statusCode < 300) {
246
+ resolve({ ok: true, statusCode: res.statusCode, data: parsed });
247
+ } else {
248
+ const errMsg =
249
+ parsed?.error ||
250
+ `HTTP request failed with status ${res.statusCode}: ${data}`;
251
+ resolve({
252
+ ok: false,
253
+ statusCode: res.statusCode,
254
+ error: errMsg,
255
+ data: parsed,
256
+ });
257
+ }
258
+ });
160
259
  });
161
- });
162
260
 
163
- req.on("error", (err) => {
164
- reject(
165
- new Error(
166
- `Failed to connect to ShadowClaw server at ${this.host}:${this.port}: ${err.message}`,
167
- ),
168
- );
169
- });
261
+ req.on("error", (err) => {
262
+ reject(
263
+ new Error(
264
+ `Failed to connect to ShadowClaw server at ${this.host}:${this.port}: ${err.message}`,
265
+ ),
266
+ );
267
+ });
268
+
269
+ req.on("timeout", () => {
270
+ req.destroy();
271
+ reject(new Error(`Request timed out after ${timeout}ms`));
272
+ });
170
273
 
171
- req.on("timeout", () => {
172
- req.destroy();
173
- reject(new Error(`Request timed out after ${timeout}ms`));
274
+ if (body) {
275
+ req.write(typeof body === "string" ? body : JSON.stringify(body));
276
+ }
277
+ req.end();
174
278
  });
279
+ };
175
280
 
176
- if (body) {
177
- req.write(typeof body === "string" ? body : JSON.stringify(body));
281
+ // First attempt with this.token
282
+ let res = await makeReq(this.token);
283
+ if (res.ok) {
284
+ return res.data;
285
+ }
286
+
287
+ // If 401 Unauthorized and candidate tokens exist, automatically try remaining candidates
288
+ if (
289
+ res.statusCode === 401 &&
290
+ Array.isArray(this.candidateTokens) &&
291
+ this.candidateTokens.length > 1
292
+ ) {
293
+ for (const fallbackToken of this.candidateTokens) {
294
+ if (fallbackToken === this.token) continue;
295
+ const retryRes = await makeReq(fallbackToken);
296
+ if (retryRes.ok) {
297
+ this.token = fallbackToken;
298
+ return retryRes.data;
299
+ }
178
300
  }
179
- req.end();
180
- });
301
+ }
302
+
303
+ throw new Error(
304
+ res.error || `HTTP request failed with status ${res.statusCode}`,
305
+ );
181
306
  }
182
307
 
183
308
  async listClients() {
@@ -288,4 +413,114 @@ export class CliControlClient {
288
413
  async sendNotification(options) {
289
414
  return this.broadcastNotification(options);
290
415
  }
416
+
417
+ async listPushSubscriptions() {
418
+ try {
419
+ const data = await this._request({
420
+ path: "/push/subscriptions",
421
+ method: "GET",
422
+ });
423
+ return Array.isArray(data) ? data : [];
424
+ } catch (_) {
425
+ return [];
426
+ }
427
+ }
428
+
429
+ async listPushClients() {
430
+ // 1. Try /push/clients or /push/subscriptions from HTTP server
431
+ try {
432
+ const clientsData = await this._request({
433
+ path: "/push/clients",
434
+ method: "GET",
435
+ });
436
+ if (clientsData && Array.isArray(clientsData.clients)) {
437
+ return clientsData.clients;
438
+ }
439
+ } catch (_) {}
440
+
441
+ try {
442
+ const subs = await this.listPushSubscriptions();
443
+ if (Array.isArray(subs) && subs.length > 0) {
444
+ const map = new Map();
445
+ for (const sub of subs) {
446
+ const cid =
447
+ (sub.client_id || "").trim() || (sub.id ? String(sub.id) : "");
448
+ if (!cid) continue;
449
+ const existing = map.get(cid);
450
+ if (!existing) {
451
+ map.set(cid, {
452
+ clientId: cid,
453
+ deviceLabel: sub.device_label || undefined,
454
+ });
455
+ } else if (!existing.deviceLabel && sub.device_label) {
456
+ existing.deviceLabel = sub.device_label;
457
+ }
458
+ }
459
+ return Array.from(map.values());
460
+ }
461
+ } catch (_) {}
462
+
463
+ // 2. Fallback: query local sqlite database if available
464
+ try {
465
+ let sqlite;
466
+ if (typeof process.getBuiltinModule === "function") {
467
+ sqlite = process.getBuiltinModule("node:sqlite");
468
+ }
469
+ if (sqlite && sqlite.DatabaseSync) {
470
+ const dbPaths = [
471
+ path.join(
472
+ process.cwd(),
473
+ ".cache",
474
+ "database",
475
+ "push-subscriptions.db",
476
+ ),
477
+ path.join(process.cwd(), "database", "push-subscriptions.db"),
478
+ path.join(
479
+ tmpdir(),
480
+ "shadow-claw",
481
+ "database",
482
+ "push-subscriptions.db",
483
+ ),
484
+ path.join(tmpdir(), "shadow-claw", "push-subscriptions.db"),
485
+ ];
486
+ if (this.cacheDir) {
487
+ dbPaths.unshift(
488
+ path.join(this.cacheDir, "database", "push-subscriptions.db"),
489
+ path.join(this.cacheDir, "push-subscriptions.db"),
490
+ );
491
+ }
492
+ for (const p of dbPaths) {
493
+ if (fs.existsSync(p)) {
494
+ const db = new sqlite.DatabaseSync(p);
495
+ const rows = db
496
+ .prepare(
497
+ "SELECT client_id, device_label, id FROM subscriptions ORDER BY created_at DESC, id DESC",
498
+ )
499
+ .all();
500
+ db.close();
501
+ if (rows && rows.length > 0) {
502
+ const map = new Map();
503
+ for (const r of rows) {
504
+ const cid =
505
+ (r.client_id ? `${r.client_id}`.trim() : "") ||
506
+ (r.id ? String(r.id) : "");
507
+ if (!cid) continue;
508
+ if (!map.has(cid)) {
509
+ map.set(cid, {
510
+ clientId: cid,
511
+ deviceLabel: r.device_label
512
+ ? `${r.device_label}`.trim()
513
+ : undefined,
514
+ });
515
+ }
516
+ }
517
+ return Array.from(map.values());
518
+ }
519
+ }
520
+ }
521
+ }
522
+ } catch (_) {}
523
+
524
+ return [];
525
+ }
291
526
  }
@@ -136,4 +136,18 @@ describe("CliControlClient", () => {
136
136
  clientId: "client-target-42",
137
137
  });
138
138
  });
139
+
140
+ it("automatically falls back to secondary candidate tokens on 401 Unauthorized", async () => {
141
+ const client = new CliControlClient({
142
+ host: "127.0.0.1",
143
+ port,
144
+ token: "initial-wrong-token",
145
+ });
146
+ // Supply candidate tokens where first is wrong and second is correct
147
+ client.candidateTokens = ["initial-wrong-token", token];
148
+
149
+ const clients = await client.listClients();
150
+ expect(Array.isArray(clients)).toBe(true);
151
+ expect(client.token).toBe(token);
152
+ });
139
153
  });