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.
- package/.well-known/mcp/server-card.json +1 -1
- package/.well-known/mcp.json +1 -1
- package/README.md +96 -278
- package/bin/cli.mjs +4 -0
- package/bin/commands/mcp.mjs +409 -125
- package/bin/commands/mcp.test.mjs +151 -18
- package/bin/utils/control-client.mjs +338 -103
- package/bin/utils/control-client.test.mjs +14 -0
- package/dist/electron/main.cjs +179 -179
- package/dist/lib/components/index.js +21 -21
- package/dist/lib/components/settings/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/settings/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/settings/shadow-claw-git.js +1 -1
- package/dist/lib/components/settings/shadow-claw-llm.js +1 -1
- package/dist/lib/components/settings/shadow-claw-networking.js +1 -1
- package/dist/lib/components/settings/shadow-claw-notifications.js +1 -1
- package/dist/lib/components/settings/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/settings/shadow-claw-storage.js +1 -1
- package/dist/lib/components/settings/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/settings/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw-channel-config.js +1 -1
- package/dist/lib/components/shadow-claw-channels.js +1 -1
- package/dist/lib/components/shadow-claw-chat.js +1 -1
- package/dist/lib/components/shadow-claw-control-plane.js +1 -1
- package/dist/lib/components/shadow-claw-conversations.js +1 -1
- package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
- package/dist/lib/components/shadow-claw-files.js +1 -1
- package/dist/lib/components/shadow-claw-git.js +1 -1
- package/dist/lib/components/shadow-claw-llm.js +1 -1
- package/dist/lib/components/shadow-claw-networking.js +1 -1
- package/dist/lib/components/shadow-claw-notifications.js +1 -1
- package/dist/lib/components/shadow-claw-pages.js +1 -1
- package/dist/lib/components/shadow-claw-peerjs.js +1 -1
- package/dist/lib/components/shadow-claw-settings.js +1 -1
- package/dist/lib/components/shadow-claw-storage.js +1 -1
- package/dist/lib/components/shadow-claw-task-server.js +1 -1
- package/dist/lib/components/shadow-claw-tasks.js +1 -1
- package/dist/lib/components/shadow-claw-terminal.js +1 -1
- package/dist/lib/components/shadow-claw-tools.js +1 -1
- package/dist/lib/components/shadow-claw-webvm.js +1 -1
- package/dist/lib/components/shadow-claw.js +1 -1
- package/dist/lib/{configurePeerJs-D8Ph0-A9.js → configurePeerJs-DG4hliKp.js} +2 -2
- package/dist/lib/{configurePeerJs-D8Ph0-A9.js.map → configurePeerJs-DG4hliKp.js.map} +1 -1
- package/dist/lib/index.js +21 -21
- package/dist/lib/{initControlPlane-BIrWmpzc.js → initControlPlane-DnpAzphg.js} +8 -8
- package/dist/lib/{initControlPlane-BIrWmpzc.js.map → initControlPlane-DnpAzphg.js.map} +1 -1
- package/dist/lib/{markdown-DEUoM1c6.js → markdown-DgdSt2GD.js} +2 -2
- package/dist/lib/{markdown-DEUoM1c6.js.map → markdown-DgdSt2GD.js.map} +1 -1
- package/dist/lib/{orchestrator-CFCW-l1O.js → orchestrator-CjEau_RG.js} +5 -5
- package/dist/lib/{orchestrator-CFCW-l1O.js.map → orchestrator-CjEau_RG.js.map} +1 -1
- package/dist/lib/{orchestrator-BtVj9bBB.js → orchestrator-ClXbJ4Mb.js} +2 -2
- package/dist/lib/{orchestrator-BtVj9bBB.js.map → orchestrator-ClXbJ4Mb.js.map} +1 -1
- package/dist/lib/{peerjs-BoBkx9DJ.js → peerjs-BdfpltIe.js} +3 -3
- package/dist/lib/{peerjs-BoBkx9DJ.js.map → peerjs-BdfpltIe.js.map} +1 -1
- package/dist/lib/{push-client-D6hbzDNV.js → push-client-DjwKknNg.js} +9 -3
- package/dist/lib/push-client-DjwKknNg.js.map +1 -0
- package/dist/lib/{shadow-claw-Cuf2O5bZ.js → shadow-claw-DcPGKenn.js} +4 -4
- package/dist/lib/{shadow-claw-Cuf2O5bZ.js.map → shadow-claw-DcPGKenn.js.map} +1 -1
- package/dist/lib/{shadow-claw-channel-config-BDPKt6Bd.js → shadow-claw-channel-config-DEL-aZ7N.js} +3 -3
- package/dist/lib/{shadow-claw-channel-config-BDPKt6Bd.js.map → shadow-claw-channel-config-DEL-aZ7N.js.map} +1 -1
- package/dist/lib/{shadow-claw-channels-BsP5JABM.js → shadow-claw-channels-BgaJiPdE.js} +2 -2
- package/dist/lib/{shadow-claw-channels-BsP5JABM.js.map → shadow-claw-channels-BgaJiPdE.js.map} +1 -1
- package/dist/lib/{shadow-claw-chat-Cam2lX4k.js → shadow-claw-chat-mXPQZbwT.js} +4 -4
- package/dist/lib/{shadow-claw-chat-Cam2lX4k.js.map → shadow-claw-chat-mXPQZbwT.js.map} +1 -1
- package/dist/lib/{shadow-claw-control-plane-DB2qcCFs.js → shadow-claw-control-plane-DO6foOLV.js} +6 -6
- package/dist/lib/{shadow-claw-control-plane-DB2qcCFs.js.map → shadow-claw-control-plane-DO6foOLV.js.map} +1 -1
- package/dist/lib/{shadow-claw-conversations-C9Vf4XGx.js → shadow-claw-conversations-BLwHTwdW.js} +2 -2
- package/dist/lib/{shadow-claw-conversations-C9Vf4XGx.js.map → shadow-claw-conversations-BLwHTwdW.js.map} +1 -1
- package/dist/lib/{shadow-claw-file-viewer-B4HYiI0v.js → shadow-claw-file-viewer-BSbZvSl-.js} +3 -3
- package/dist/lib/{shadow-claw-file-viewer-B4HYiI0v.js.map → shadow-claw-file-viewer-BSbZvSl-.js.map} +1 -1
- package/dist/lib/{shadow-claw-files-DVROUESO.js → shadow-claw-files-BLVr9Pua.js} +2 -2
- package/dist/lib/{shadow-claw-files-DVROUESO.js.map → shadow-claw-files-BLVr9Pua.js.map} +1 -1
- package/dist/lib/{shadow-claw-git-BqrO7cHJ.js → shadow-claw-git-CHC1H86J.js} +3 -3
- package/dist/lib/{shadow-claw-git-BqrO7cHJ.js.map → shadow-claw-git-CHC1H86J.js.map} +1 -1
- package/dist/lib/{shadow-claw-llm-BexUQdYo.js → shadow-claw-llm-CKXfxd3D.js} +2 -2
- package/dist/lib/{shadow-claw-llm-BexUQdYo.js.map → shadow-claw-llm-CKXfxd3D.js.map} +1 -1
- package/dist/lib/{shadow-claw-networking-CfXRGpUD.js → shadow-claw-networking-dpYvfchh.js} +2 -2
- package/dist/lib/{shadow-claw-networking-CfXRGpUD.js.map → shadow-claw-networking-dpYvfchh.js.map} +1 -1
- package/dist/lib/{shadow-claw-notifications-C9KPMgx_.js → shadow-claw-notifications-_YeAVPga.js} +2 -2
- package/dist/lib/{shadow-claw-notifications-C9KPMgx_.js.map → shadow-claw-notifications-_YeAVPga.js.map} +1 -1
- package/dist/lib/{shadow-claw-pages-CGzGXmrI.js → shadow-claw-pages-DVzZriZ7.js} +3 -3
- package/dist/lib/{shadow-claw-pages-CGzGXmrI.js.map → shadow-claw-pages-DVzZriZ7.js.map} +1 -1
- package/dist/lib/{shadow-claw-peerjs-B0ZB7yhB.js → shadow-claw-peerjs-BvixjeBz.js} +3 -3
- package/dist/lib/{shadow-claw-peerjs-B0ZB7yhB.js.map → shadow-claw-peerjs-BvixjeBz.js.map} +1 -1
- package/dist/lib/{shadow-claw-settings-DBfMZdDS.js → shadow-claw-settings-DV9-gD4Q.js} +2 -2
- package/dist/lib/{shadow-claw-settings-DBfMZdDS.js.map → shadow-claw-settings-DV9-gD4Q.js.map} +1 -1
- package/dist/lib/{shadow-claw-storage-CM-w6mgG.js → shadow-claw-storage-Bv__kXG6.js} +2 -2
- package/dist/lib/{shadow-claw-storage-CM-w6mgG.js.map → shadow-claw-storage-Bv__kXG6.js.map} +1 -1
- package/dist/lib/{shadow-claw-task-server-DV4b1g2-.js → shadow-claw-task-server-DNOe8PWK.js} +2 -2
- package/dist/lib/{shadow-claw-task-server-DV4b1g2-.js.map → shadow-claw-task-server-DNOe8PWK.js.map} +1 -1
- package/dist/lib/{shadow-claw-tasks-K-foOHSE.js → shadow-claw-tasks-qmcDT4PU.js} +3 -3
- package/dist/lib/{shadow-claw-tasks-K-foOHSE.js.map → shadow-claw-tasks-qmcDT4PU.js.map} +1 -1
- package/dist/lib/{shadow-claw-terminal-CpAknKyA.js → shadow-claw-terminal-0QUWP8of.js} +2 -2
- package/dist/lib/{shadow-claw-terminal-CpAknKyA.js.map → shadow-claw-terminal-0QUWP8of.js.map} +1 -1
- package/dist/lib/{shadow-claw-tools-D98XZ0Ep.js → shadow-claw-tools-Bs_bmI6-.js} +3 -3
- package/dist/lib/{shadow-claw-tools-D98XZ0Ep.js.map → shadow-claw-tools-Bs_bmI6-.js.map} +1 -1
- package/dist/lib/{shadow-claw-webvm-DLOb8dMI.js → shadow-claw-webvm-DYaoL7QY.js} +2 -2
- package/dist/lib/{shadow-claw-webvm-DLOb8dMI.js.map → shadow-claw-webvm-DYaoL7QY.js.map} +1 -1
- package/dist/lib/{syncWebMcpRegistration-NspKefOI.js → syncWebMcpRegistration-BhbqI6R2.js} +2 -2
- package/dist/lib/{syncWebMcpRegistration-NspKefOI.js.map → syncWebMcpRegistration-BhbqI6R2.js.map} +1 -1
- package/dist/public/.well-known/mcp/server-card.json +1 -1
- package/dist/public/.well-known/mcp.json +1 -1
- package/dist/public/AGENTS.md +22 -3
- package/dist/public/README.md +96 -278
- package/dist/public/agent.worker.js +50 -50
- package/dist/public/{base-task-model-CYkpwnvU-DDfHUUIS.js → base-task-model-CYkpwnvU-DrXr4G1R.js} +1 -1
- package/dist/public/{configurePeerJs-BhBokQlC.js → configurePeerJs-B470pdeU.js} +1 -1
- package/dist/public/docs/README.md +31 -31
- package/dist/public/docs/architecture/context-management.md +7 -0
- package/dist/public/docs/architecture/storage.md +10 -1
- package/dist/public/docs/decisions/bundled-typescript-architecture.md +0 -1
- package/dist/public/docs/decisions/native-web-components-and-signals.md +0 -1
- package/dist/public/docs/example/article/index.html +1 -1
- package/dist/public/docs/guides/publishing-to-github-pages.md +334 -0
- package/dist/public/docs/guides/server-development-configuration.md +15 -0
- package/dist/public/docs/publishing/index.html +1 -1
- package/dist/public/docs/skill-creator/index.html +1 -1
- package/dist/public/docs/subsystems/cli.md +10 -2
- package/dist/public/docs/subsystems/control-plane.md +9 -8
- package/dist/public/docs/subsystems/crypto.md +9 -0
- package/dist/public/docs/subsystems/custom-element-security.md +2 -0
- package/dist/public/docs/subsystems/mcp-server.md +31 -16
- package/dist/public/docs/subsystems/notifications.md +8 -5
- package/dist/public/docs/subsystems/providers.md +9 -0
- package/dist/public/docs/subsystems/share-target.md +107 -0
- package/dist/public/docs/subsystems/webmcp.md +43 -0
- package/dist/public/e2e/README.md +4 -2
- package/dist/public/{e2e-bridge-DcBoeFvd.js → e2e-bridge-CSW0S5Yz.js} +1 -1
- package/dist/public/{file-viewer-CKtVC9Ix.js → file-viewer-Cs-riNvy.js} +1 -1
- package/dist/public/{firebase-OSDX3viP.js → firebase-cwAl_ABm.js} +1 -1
- package/dist/public/{git-CkhCJonj.js → git-GD09WATp.js} +25 -25
- package/dist/public/index.html +1 -1
- package/dist/public/index.js +1 -1
- package/dist/public/{initControlPlane-Bi8ouFEb.js → initControlPlane-BqCY0NQy.js} +1 -1
- package/dist/public/initControlPlane-C-IXaKYX.js +1 -0
- package/dist/public/{language-detector-BRKrlra0.js → language-detector-CIeEuQFk.js} +1 -1
- package/dist/public/llms.txt +4 -2
- package/dist/public/main/index.html +1 -1
- package/dist/public/main/memory/index.html +1 -1
- package/dist/public/{markdown-C-hCVJtc.js → markdown-Cw7vCtTg.js} +1 -1
- package/dist/public/{openai-Bpd71Oja.js → openai-BJIb2c5j.js} +3 -3
- package/dist/public/{orchestrator-8qNGfw8r.js → orchestrator-BWkHHO9Y.js} +1 -1
- package/dist/public/{orchestrator-BT3Ks5ip.js → orchestrator-DpkkeD6T.js} +1 -1
- package/dist/public/{peerjs-DfhwEiiq.js → peerjs-tf2EmWjB.js} +1 -1
- package/dist/public/{prompt-api-polyfill-Dl1kbJhR.js → prompt-api-polyfill-CXd75leW.js} +1 -1
- package/dist/public/{push-client-BVLrE9pK.js → push-client-CHjSbrWs.js} +1 -1
- package/dist/public/{rewriter-j6D9LDER.js → rewriter-BEHn9gll.js} +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-DCjxNDCX.js → shadow-claw-BBPV_9X8.js} +2 -2
- package/dist/public/{shadow-claw-channels-CITDrqMl.js → shadow-claw-channels-BJvrKQjg.js} +1 -1
- package/dist/public/{shadow-claw-chat-BQrSgSKF.js → shadow-claw-chat-BG2EaRaw.js} +1 -1
- package/dist/public/{shadow-claw-control-plane-Cp001OY0.js → shadow-claw-control-plane-BBa7WbTg.js} +2 -2
- package/dist/public/{shadow-claw-conversations-EhX4RvIj.js → shadow-claw-conversations-CFaWFmkm.js} +1 -1
- package/dist/public/{shadow-claw-file-viewer-DiQXIjZD.js → shadow-claw-file-viewer-Z-GIKaE9.js} +1 -1
- package/dist/public/{shadow-claw-files-lkTnSJsn.js → shadow-claw-files-BBx4iMsk.js} +1 -1
- package/dist/public/{shadow-claw-git-BlcA_z8x.js → shadow-claw-git-vTOKatwd.js} +2 -2
- package/dist/public/{shadow-claw-llm-CW16X9Rg.js → shadow-claw-llm-Ce1Ax6-x.js} +1 -1
- package/dist/public/{shadow-claw-networking-ySsMBY_i.js → shadow-claw-networking-q4XIzdB5.js} +1 -1
- package/dist/public/{shadow-claw-notifications-CyV2zCQ8.js → shadow-claw-notifications-D2cz0gmm.js} +1 -1
- package/dist/public/{shadow-claw-pages-Cx7cF9ah.js → shadow-claw-pages-3335lmSC.js} +1 -1
- package/dist/public/{shadow-claw-settings-zm4PHHGi.js → shadow-claw-settings-4ptYuJwO.js} +2 -2
- package/dist/public/{shadow-claw-storage-dy9JAEGd.js → shadow-claw-storage-bIrPKFhF.js} +1 -1
- package/dist/public/{shadow-claw-task-server-Bleww48s.js → shadow-claw-task-server-5yqVBxy1.js} +1 -1
- package/dist/public/{shadow-claw-tasks-CTI5oaLN.js → shadow-claw-tasks-DcqYHPLZ.js} +1 -1
- package/dist/public/{shadow-claw-terminal-mZ3i4HaQ.js → shadow-claw-terminal-fARaDONr.js} +1 -1
- package/dist/public/{shadow-claw-tools-B_nuk8Zl.js → shadow-claw-tools-paEAhcJr.js} +1 -1
- package/dist/public/{shadow-claw-webvm-Vl_jH5YL.js → shadow-claw-webvm-CbzH_vyX.js} +1 -1
- package/dist/public/{summarizer-VJNr-7xG.js → summarizer-BT3AuFed.js} +1 -1
- package/dist/public/{syncWebMcpRegistration-n2vAMJ4w.js → syncWebMcpRegistration-CWSYaHex.js} +1 -1
- package/dist/public/{transformers-BhQMWQG5.js → transformers-1VTxqKjT.js} +1 -1
- package/dist/public/{translator-DEQXb7ZL.js → translator-B50RYlFt.js} +1 -1
- package/dist/public/{webllm-CHN5H-Sy.js → webllm-FmyJWWpt.js} +1 -1
- package/dist/public/{writer-CD9oN2pq.js → writer-Bhb4YYRE.js} +1 -1
- package/dist/server.js +205 -203
- package/package.json +1 -1
- package/src/components/settings/shadow-claw-llm/shadow-claw-llm.test.ts +2 -0
- package/src/core/orchestrator/utils/configurePeerJs.test.ts +2 -0
- package/src/server/client-registry.test.ts +9 -0
- package/src/server/client-registry.ts +14 -0
- package/src/server/mcp/mcp-integration.test.ts +68 -16
- package/src/server/mcp/tools/built-in-tools.ts +125 -34
- package/src/server/mcp/tools/client-tool-relay.ts +81 -44
- package/src/server/mcp/tools/mcp-tools.test.ts +103 -13
- package/src/server/mcp/types.ts +3 -0
- package/src/server/openapi/paths/push.ts +48 -0
- package/src/server/server.ts +30 -0
- package/src/subsystems/notifications/push-client.test.ts +17 -0
- package/src/subsystems/notifications/push-client.ts +22 -2
- package/src/subsystems/notifications/push-routes.test.ts +31 -0
- package/src/subsystems/notifications/push-routes.ts +10 -0
- package/src/subsystems/notifications/push-store.test.ts +49 -0
- package/src/subsystems/notifications/push-store.ts +45 -0
- package/src/worker/worker.test.ts +2 -0
- 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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
46
|
+
} catch (_) {}
|
|
47
|
+
};
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
for (const
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
}
|
|
147
|
+
}
|
|
81
148
|
|
|
82
|
-
|
|
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.
|
|
91
|
-
options.token
|
|
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({
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
219
|
+
if (tokenToUse) {
|
|
220
|
+
headers["x-control-token"] = tokenToUse;
|
|
221
|
+
}
|
|
129
222
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
274
|
+
if (body) {
|
|
275
|
+
req.write(typeof body === "string" ? body : JSON.stringify(body));
|
|
276
|
+
}
|
|
277
|
+
req.end();
|
|
174
278
|
});
|
|
279
|
+
};
|
|
175
280
|
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
});
|