shadow-claw 1.40.0 → 1.40.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 (86) hide show
  1. package/.well-known/mcp/server-card.json +2 -2
  2. package/.well-known/mcp/servers.json +1 -1
  3. package/.well-known/mcp.json +2 -2
  4. package/dist/cli/cli.js +2 -2
  5. package/dist/lib/components/index.js +2 -2
  6. package/dist/lib/components/shadow-claw-file-viewer/shadow-claw-file-viewer.d.ts +1 -1
  7. package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
  8. package/dist/lib/components/shadow-claw.js +1 -1
  9. package/dist/lib/core/orchestrator/orchestrator-state.d.ts +2 -1
  10. package/dist/lib/core/orchestrator/orchestrator.d.ts +2 -1
  11. package/dist/lib/core/orchestrator/utils/operations/task.d.ts +1 -0
  12. package/dist/lib/index.js +2 -2
  13. package/dist/lib/{orchestrator-Bq9ft3uG.js → orchestrator-CMN43L0F.js} +9 -13
  14. package/dist/lib/orchestrator-CMN43L0F.js.map +1 -0
  15. package/dist/lib/orchestrator-D9f5fDCb.js.map +1 -1
  16. package/dist/lib/{shadow-claw-MumteAJy.js → shadow-claw-4x3LqlII.js} +2 -2
  17. package/dist/lib/{shadow-claw-MumteAJy.js.map → shadow-claw-4x3LqlII.js.map} +1 -1
  18. package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js → shadow-claw-file-viewer-CuDhkVcL.js} +21 -21
  19. package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js.map → shadow-claw-file-viewer-CuDhkVcL.js.map} +1 -1
  20. package/dist/lib/syncWebMcpRegistration-Bbph0XZl.js.map +1 -1
  21. package/dist/public/.well-known/mcp/server-card.json +2 -2
  22. package/dist/public/.well-known/mcp/servers.json +1 -1
  23. package/dist/public/.well-known/mcp.json +2 -2
  24. package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
  25. package/dist/public/docs/example/article/index.html +2 -2
  26. package/dist/public/docs/publishing/index.html +2 -2
  27. package/dist/public/docs/skill-creator/index.html +2 -2
  28. package/dist/public/index.html +2 -2
  29. package/dist/public/index.js +1 -1
  30. package/dist/public/main/index.html +2 -2
  31. package/dist/public/main/memory/index.html +2 -2
  32. package/dist/public/{orchestrator-BLkS_jpm.js → orchestrator-Dc_Q73La.js} +1 -1
  33. package/dist/public/service-worker.js +1 -1
  34. package/dist/public/{shadow-claw-DmrxnX3Q.js → shadow-claw-Dabb2AEp.js} +2 -2
  35. package/dist/public/{shadow-claw-file-viewer-a_uRA30F.js → shadow-claw-file-viewer-CDs1QGXk.js} +1 -1
  36. package/dist/server.js +1 -1
  37. package/package.json +1 -1
  38. package/src/cli/cli.ts +2 -2
  39. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +31 -31
  40. package/src/core/effect.test.ts +19 -0
  41. package/src/core/orchestrator/orchestrator-state.ts +2 -1
  42. package/src/core/orchestrator/orchestrator.test.ts +686 -5
  43. package/src/core/orchestrator/orchestrator.ts +3 -10
  44. package/src/core/orchestrator/utils/compactContext.test.ts +423 -121
  45. package/src/core/orchestrator/utils/configurePeerJs.test.ts +27 -0
  46. package/src/core/orchestrator/utils/deliverResponse.test.ts +127 -0
  47. package/src/core/orchestrator/utils/dispatchSubagentInvoke.test.ts +23 -0
  48. package/src/core/orchestrator/utils/enqueue.test.ts +520 -86
  49. package/src/core/orchestrator/utils/handleWorkerMessage.test.ts +664 -144
  50. package/src/core/orchestrator/utils/initTasks.node.test.ts +147 -0
  51. package/src/core/orchestrator/utils/initTasks.test.ts +926 -191
  52. package/src/core/orchestrator/utils/initTasks.ts +5 -4
  53. package/src/core/orchestrator/utils/invokeAgent.test.ts +563 -110
  54. package/src/core/orchestrator/utils/invokeAgent.ts +8 -10
  55. package/src/core/orchestrator/utils/loadChannelConfigurations.test.ts +50 -0
  56. package/src/core/orchestrator/utils/operations/channel.test.ts +377 -170
  57. package/src/core/orchestrator/utils/operations/provider.test.ts +663 -85
  58. package/src/core/orchestrator/utils/operations/room.test.ts +74 -11
  59. package/src/core/orchestrator/utils/operations/task.test.ts +452 -56
  60. package/src/core/orchestrator/utils/operations/task.ts +1 -1
  61. package/src/core/orchestrator/utils/operations/vm.test.ts +66 -7
  62. package/src/core/orchestrator/utils/parseDirectToolCommand.test.ts +6 -0
  63. package/src/core/orchestrator/utils/settings.test.ts +248 -81
  64. package/src/core/orchestrator/utils/setupPushTaskListener.test.ts +58 -0
  65. package/src/core/orchestrator/utils/syncWebMcpRegistration.test.ts +341 -65
  66. package/src/core/orchestrator/utils/syncWebMcpRegistration.ts +4 -2
  67. package/src/server/routes/mcp.ts +2 -2
  68. package/src/stores/file-viewer.test.ts +11 -0
  69. package/src/stores/theme.test.ts +41 -2
  70. package/src/stores/toast.test.ts +25 -0
  71. package/src/worker/tools/bash/native-bash-executor.test.ts +31 -1
  72. package/src/worker/tools/git/utils/extractConflictPaths.test.ts +24 -0
  73. package/src/worker/tools/git/utils/parseConflictRegions.test.ts +46 -0
  74. package/src/worker/tools/git/utils/resolveCorsProxy.test.ts +26 -5
  75. package/src/worker/tools/git/utils/truncateSnippet.test.ts +16 -0
  76. package/src/worker/tools/node-transformers-executor.test.ts +105 -6
  77. package/src/worker/tools/remote-mcp/remote-mcp.test.ts +172 -47
  78. package/src/worker/tools/remote-mcp/utils/formatListToolsOutput.test.ts +19 -0
  79. package/src/worker/tools/remote-mcp/utils/isMcpReauthError.test.ts +45 -0
  80. package/src/worker/tools/remote-mcp/utils/resolveMcpReauth.test.ts +48 -0
  81. package/src/worker/tools/rooms/list-room-members.test.ts +24 -10
  82. package/src/worker/tools/tasks/list-tasks.test.ts +39 -8
  83. package/src/worker/tools/tasks/update-task.test.ts +78 -14
  84. package/src/worker/tools/ui/javascript.test.ts +70 -17
  85. package/src/worker/tools/ui/list-tool-profiles.test.ts +31 -9
  86. package/dist/lib/orchestrator-Bq9ft3uG.js.map +0 -1
@@ -11,8 +11,8 @@ describe("themeStore", () => {
11
11
  localStorage.clear();
12
12
  document.documentElement.className = "";
13
13
 
14
- // Mock dispatchEvent
15
- (window as any).dispatchEvent = jest.fn();
14
+ // Spy on dispatchEvent so event listeners still fire
15
+ jest.spyOn(window, "dispatchEvent");
16
16
  });
17
17
 
18
18
  it("applies and persists explicit theme", async () => {
@@ -135,4 +135,43 @@ describe("themeStore", () => {
135
135
  expect(themeStore.resolved).toBe("light");
136
136
  expect(localStorage.getItem("shadow-claw-theme")).toBe("system");
137
137
  });
138
+
139
+ it("handles storage event for tab synchronization", async () => {
140
+ const { getNamespacedStorageKey } =
141
+ await import("../utils/namespacedStorage.js");
142
+ const { themeStore } = await import("./theme.js");
143
+ const targetKey = getNamespacedStorageKey("shadow-claw-theme");
144
+
145
+ // Other key should be ignored
146
+ const otherEv = new Event("storage") as any;
147
+ otherEv.key = "other-key";
148
+ otherEv.newValue = "dark";
149
+ window.dispatchEvent(otherEv);
150
+ expect(themeStore.theme).not.toBe("dark");
151
+
152
+ // Target key with null newValue should be ignored
153
+ const nullEv = new Event("storage") as any;
154
+ nullEv.key = targetKey;
155
+ nullEv.newValue = null;
156
+ window.dispatchEvent(nullEv);
157
+
158
+ // Target key with valid newValue updates theme
159
+ const validEv = new Event("storage") as any;
160
+ validEv.key = targetKey;
161
+ validEv.newValue = "dark";
162
+ window.dispatchEvent(validEv);
163
+
164
+ expect(themeStore.theme).toBe("dark");
165
+ expect(themeStore.resolved).toBe("dark");
166
+ });
167
+
168
+ it("falls back gracefully when matchMedia is not a function", async () => {
169
+ (window as any).matchMedia = undefined;
170
+ jest.resetModules();
171
+ localStorage.clear();
172
+ const { themeStore } = await import("./theme.js");
173
+
174
+ themeStore.setTheme("system");
175
+ expect(themeStore.resolved).toBe("light");
176
+ });
138
177
  });
@@ -101,6 +101,31 @@ describe("ToastStore", () => {
101
101
 
102
102
  // No-op for non-existent or action-less toast
103
103
  await store.runAction(999);
104
+
105
+ const noActionId = store.show("without action");
106
+ await store.runAction(noActionId);
107
+ });
108
+
109
+ test("pause and resume handle missing or invalid timers gracefully", () => {
110
+ const store = new ToastStore();
111
+
112
+ // Calling pause on nonexistent toast or already paused toast
113
+ expect(() => store.pause(999)).not.toThrow();
114
+
115
+ const id = store.show("toast", { duration: 1000 });
116
+ store.pause(id);
117
+ // Pause again when timeoutId is undefined
118
+ expect(() => store.pause(id)).not.toThrow();
119
+
120
+ // Calling resume on nonexistent toast, or already running toast, or expired toast
121
+ expect(() => store.resume(999)).not.toThrow();
122
+
123
+ const runningId = store.show("running", { duration: 1000 });
124
+ // Resume when timer.timeoutId is still active
125
+ expect(() => store.resume(runningId)).not.toThrow();
126
+
127
+ // Clear timer on nonexistent toast
128
+ expect(() => store.clearTimer(999)).not.toThrow();
104
129
  });
105
130
 
106
131
  test("resolvers handle invalid inputs gracefully", () => {
@@ -1,7 +1,11 @@
1
- import { describe, it, expect } from "@jest/globals";
1
+ import { describe, it, expect, afterEach } from "@jest/globals";
2
2
  import { nativeBashExecutor } from "./native-bash-executor.js";
3
3
 
4
4
  describe("nativeBashExecutor", () => {
5
+ afterEach(() => {
6
+ delete (globalThis as Record<string, unknown>).__getStorageRootPath;
7
+ });
8
+
5
9
  it("executes a shell command and captures stdout", async () => {
6
10
  const output = await nativeBashExecutor({
7
11
  command: "echo 'hello from native bash'",
@@ -26,4 +30,30 @@ describe("nativeBashExecutor", () => {
26
30
  });
27
31
  expect(output).toContain("test stderr output");
28
32
  });
33
+
34
+ it("captures both stdout and stderr formatted with newline", async () => {
35
+ const output = await nativeBashExecutor({
36
+ command: 'node -e \'process.stdout.write("out"); console.error("err");\'',
37
+ timeoutSec: 5,
38
+ });
39
+ expect(output).toContain("out\nerr");
40
+ });
41
+
42
+ it("falls back to error message when command fails with empty output", async () => {
43
+ const output = await nativeBashExecutor({
44
+ command: "node -e 'process.exit(42)'",
45
+ timeoutSec: 5,
46
+ });
47
+ expect(output).toContain("Command failed");
48
+ });
49
+
50
+ it("uses __getStorageRootPath when provided on globalThis", async () => {
51
+ (globalThis as Record<string, unknown>).__getStorageRootPath = () =>
52
+ process.cwd();
53
+ const output = await nativeBashExecutor({
54
+ command: "pwd",
55
+ timeoutSec: 5,
56
+ });
57
+ expect(output.trim()).toBe(process.cwd());
58
+ });
29
59
  });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import { extractConflictPaths } from "./extractConflictPaths.js";
3
+
4
+ describe("extractConflictPaths", () => {
5
+ it("extracts multiple file paths from conflict message", () => {
6
+ const message =
7
+ "Automatic merge failed; conflicts in the following files: fileA.ts, fileB.ts, fileC.ts";
8
+ expect(extractConflictPaths(message)).toEqual([
9
+ "fileA.ts",
10
+ "fileB.ts",
11
+ "fileC.ts",
12
+ ]);
13
+ });
14
+
15
+ it("extracts single file path from singular conflict message", () => {
16
+ const message = "conflict in the following file: readme.md";
17
+ expect(extractConflictPaths(message)).toEqual(["readme.md"]);
18
+ });
19
+
20
+ it("returns empty array when message does not match conflict format", () => {
21
+ const message = "Merge successful with fast-forward";
22
+ expect(extractConflictPaths(message)).toEqual([]);
23
+ });
24
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import { parseConflictRegions } from "./parseConflictRegions.js";
3
+
4
+ describe("parseConflictRegions", () => {
5
+ it("parses single standard conflict region", () => {
6
+ const content = [
7
+ "before",
8
+ "<<<<<<< HEAD",
9
+ "our content line 1",
10
+ "our content line 2",
11
+ "=======",
12
+ "their content",
13
+ ">>>>>>> branch-feat",
14
+ "after",
15
+ ].join("\n");
16
+
17
+ const regions = parseConflictRegions(content);
18
+ expect(regions).toHaveLength(1);
19
+ expect(regions[0]).toEqual({
20
+ ours: "our content line 1\nour content line 2",
21
+ oursLabel: "HEAD",
22
+ startLine: 2,
23
+ theirs: "their content",
24
+ theirsLabel: "branch-feat",
25
+ });
26
+ });
27
+
28
+ it("handles unclosed conflict regions that reach end-of-file without closing marker", () => {
29
+ const content = [
30
+ "<<<<<<< HEAD",
31
+ "our changes",
32
+ "=======",
33
+ "their unclosed changes",
34
+ ].join("\n");
35
+
36
+ const regions = parseConflictRegions(content);
37
+ expect(regions).toHaveLength(1);
38
+ expect(regions[0].theirsLabel).toBe("");
39
+ expect(regions[0].theirs).toBe("their unclosed changes");
40
+ });
41
+
42
+ it("returns empty array when no conflict markers are present", () => {
43
+ const content = "regular file content\nwithout conflicts";
44
+ expect(parseConflictRegions(content)).toEqual([]);
45
+ });
46
+ });
@@ -1,27 +1,29 @@
1
1
  import { describe, it, expect, beforeEach } from "@jest/globals";
2
2
  import { resolveCorsProxy } from "./resolveCorsProxy.js";
3
3
  import { setHeadlessMode } from "../../../../config/headless.js";
4
+ import type { ShadowClawDatabase } from "../../../../db/types.js";
5
+ import type { GitToolDeps } from "../../../../subsystems/git/types.js";
4
6
 
5
7
  describe("resolveCorsProxy", () => {
6
- let mockDb: any;
7
- let mockDeps: any;
8
+ const mockDb = {} as ShadowClawDatabase;
9
+ let mockDeps: GitToolDeps;
8
10
  let configStore: Record<string, string>;
9
11
 
10
12
  beforeEach(() => {
11
13
  configStore = {};
12
- mockDb = {};
13
14
  mockDeps = {
14
15
  configKeys: {
15
16
  GIT_CORS_PROXY: "git_cors_proxy",
16
17
  GIT_PROXY_URL: "git_proxy_url",
17
18
  },
18
- getConfig: async (_db: any, key: string) => configStore[key],
19
+ getConfig: async (_db: ShadowClawDatabase, key: string) =>
20
+ configStore[key],
19
21
  getProxyUrl: (pref: string, custom?: string) => {
20
22
  if (pref === "custom") return custom || "https://custom.proxy";
21
23
  if (pref === "public") return "https://public.proxy";
22
24
  return "http://127.0.0.1:8888/git-proxy";
23
25
  },
24
- };
26
+ } as unknown as GitToolDeps;
25
27
  });
26
28
 
27
29
  it("returns local proxy URL in browser mode by default", async () => {
@@ -44,10 +46,29 @@ describe("resolveCorsProxy", () => {
44
46
  expect(result).toBe("https://my-proxy.internal");
45
47
  });
46
48
 
49
+ it("falls back when custom proxy is selected but no custom URL is provided", async () => {
50
+ setHeadlessMode(false);
51
+ configStore["git_cors_proxy"] = "custom";
52
+ configStore["git_proxy_url"] = "";
53
+ const result = await resolveCorsProxy(mockDb, mockDeps);
54
+ expect(result).toBe("http://127.0.0.1:8888/git-proxy");
55
+ });
56
+
47
57
  it("returns public proxy URL when configured in browser mode", async () => {
48
58
  setHeadlessMode(false);
49
59
  configStore["git_cors_proxy"] = "public";
50
60
  const result = await resolveCorsProxy(mockDb, mockDeps);
51
61
  expect(result).toBe("https://public.proxy");
52
62
  });
63
+
64
+ it("returns undefined when pref is direct or none", async () => {
65
+ setHeadlessMode(false);
66
+ configStore["git_cors_proxy"] = "direct";
67
+ const directResult = await resolveCorsProxy(mockDb, mockDeps);
68
+ expect(directResult).toBeUndefined();
69
+
70
+ configStore["git_cors_proxy"] = "none";
71
+ const noneResult = await resolveCorsProxy(mockDb, mockDeps);
72
+ expect(noneResult).toBeUndefined();
73
+ });
53
74
  });
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+ import { truncateSnippet } from "./truncateSnippet.js";
3
+
4
+ describe("truncateSnippet", () => {
5
+ it("returns unchanged text when line count is less than or equal to maxLines", () => {
6
+ const text = "line 1\nline 2\nline 3";
7
+ expect(truncateSnippet(text, 5)).toBe(text);
8
+ expect(truncateSnippet(text, 3)).toBe(text);
9
+ });
10
+
11
+ it("truncates text and appends remaining line count when line count exceeds maxLines", () => {
12
+ const text = "line 1\nline 2\nline 3\nline 4\nline 5";
13
+ const result = truncateSnippet(text, 2);
14
+ expect(result).toBe("line 1\nline 2\n [... 3 more lines]");
15
+ });
16
+ });
@@ -1,21 +1,41 @@
1
1
  /** @jest-environment node */
2
2
  import { describe, it, expect, jest, beforeEach } from "@jest/globals";
3
- import {
4
- executeNodeTransformersCompletion,
5
- setTransformersServiceForTests,
6
- } from "./node-transformers-executor.js";
3
+ import type { TransformersRuntimeService } from "../../server/services/transformers-runtime.js";
4
+
5
+ const mockCreateServiceInstance: TransformersRuntimeService = {
6
+ runChatCompletion: jest.fn(async () => ({
7
+ text: "Created service default output",
8
+ promptTokens: 5,
9
+ completionTokens: 5,
10
+ })),
11
+ } as unknown as TransformersRuntimeService;
12
+
13
+ const mockCreateTransformersRuntimeService = jest.fn(
14
+ () => mockCreateServiceInstance,
15
+ );
16
+
17
+ jest.unstable_mockModule(
18
+ "../../server/services/transformers-runtime.js",
19
+ () => ({
20
+ createTransformersRuntimeService: mockCreateTransformersRuntimeService,
21
+ }),
22
+ );
23
+
24
+ const { executeNodeTransformersCompletion, setTransformersServiceForTests } =
25
+ await import("./node-transformers-executor.js");
7
26
 
8
27
  describe("executeNodeTransformersCompletion", () => {
9
- let mockService: any;
28
+ let mockService: TransformersRuntimeService;
10
29
 
11
30
  beforeEach(() => {
31
+ jest.clearAllMocks();
12
32
  mockService = {
13
33
  runChatCompletion: jest.fn(async () => ({
14
34
  text: "Hello from local ONNX!",
15
35
  promptTokens: 12,
16
36
  completionTokens: 8,
17
37
  })),
18
- };
38
+ } as unknown as TransformersRuntimeService;
19
39
  setTransformersServiceForTests(mockService);
20
40
  });
21
41
 
@@ -55,4 +75,83 @@ describe("executeNodeTransformersCompletion", () => {
55
75
  }),
56
76
  );
57
77
  });
78
+
79
+ it("handles tool calls returned by the model output", async () => {
80
+ mockService.runChatCompletion = jest.fn(async () => ({
81
+ text: 'call:read_file{"path":"test.txt"}',
82
+ promptTokens: 20,
83
+ completionTokens: 15,
84
+ })) as typeof mockService.runChatCompletion;
85
+
86
+ const tools = [
87
+ {
88
+ name: "read_file",
89
+ description: "Read a file",
90
+ parameters: {
91
+ type: "object",
92
+ properties: { path: { type: "string" } },
93
+ },
94
+ },
95
+ ];
96
+
97
+ const response = await executeNodeTransformersCompletion({
98
+ modelId: "onnx-community/gemma-4-E2B-it-ONNX",
99
+ messages: [{ role: "user", content: "Read test.txt" }],
100
+ tools,
101
+ });
102
+
103
+ expect(mockService.runChatCompletion).toHaveBeenCalledWith(
104
+ expect.objectContaining({
105
+ tools: expect.arrayContaining([
106
+ expect.objectContaining({
107
+ type: "function",
108
+ function: expect.objectContaining({ name: "read_file" }),
109
+ }),
110
+ ]),
111
+ }),
112
+ );
113
+
114
+ expect(response.choices[0].finish_reason).toBe("tool_calls");
115
+ expect(response.choices[0].message.content).toBeNull();
116
+ expect(response.choices[0].message.tool_calls).toHaveLength(1);
117
+ expect(response.choices[0].message.tool_calls[0].function.name).toBe(
118
+ "read_file",
119
+ );
120
+ expect(response.choices[0].message.tool_calls[0].function.arguments).toBe(
121
+ JSON.stringify({ path: "test.txt" }),
122
+ );
123
+ });
124
+
125
+ it("creates default service when defaultService is null", async () => {
126
+ setTransformersServiceForTests(null);
127
+
128
+ const response = await executeNodeTransformersCompletion({
129
+ modelId: "onnx-community/Qwen3-0.6B-ONNX",
130
+ messages: [{ role: "user", content: "Test default" }],
131
+ });
132
+
133
+ expect(mockCreateTransformersRuntimeService).toHaveBeenCalled();
134
+ expect(response.choices[0].message.content).toBe(
135
+ "Created service default output",
136
+ );
137
+ });
138
+
139
+ it("uses custom injected service when provided in options", async () => {
140
+ const customService: TransformersRuntimeService = {
141
+ runChatCompletion: jest.fn(async () => ({
142
+ text: "Custom service response",
143
+ promptTokens: 3,
144
+ completionTokens: 4,
145
+ })),
146
+ } as unknown as TransformersRuntimeService;
147
+
148
+ const response = await executeNodeTransformersCompletion({
149
+ modelId: "onnx-community/Qwen3-0.6B-ONNX",
150
+ messages: [{ role: "user", content: "Custom" }],
151
+ service: customService,
152
+ });
153
+
154
+ expect(customService.runChatCompletion).toHaveBeenCalled();
155
+ expect(response.choices[0].message.content).toBe("Custom service response");
156
+ });
58
157
  });