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.
- package/.well-known/mcp/server-card.json +2 -2
- package/.well-known/mcp/servers.json +1 -1
- package/.well-known/mcp.json +2 -2
- package/dist/cli/cli.js +2 -2
- package/dist/lib/components/index.js +2 -2
- package/dist/lib/components/shadow-claw-file-viewer/shadow-claw-file-viewer.d.ts +1 -1
- package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
- package/dist/lib/components/shadow-claw.js +1 -1
- package/dist/lib/core/orchestrator/orchestrator-state.d.ts +2 -1
- package/dist/lib/core/orchestrator/orchestrator.d.ts +2 -1
- package/dist/lib/core/orchestrator/utils/operations/task.d.ts +1 -0
- package/dist/lib/index.js +2 -2
- package/dist/lib/{orchestrator-Bq9ft3uG.js → orchestrator-CMN43L0F.js} +9 -13
- package/dist/lib/orchestrator-CMN43L0F.js.map +1 -0
- package/dist/lib/orchestrator-D9f5fDCb.js.map +1 -1
- package/dist/lib/{shadow-claw-MumteAJy.js → shadow-claw-4x3LqlII.js} +2 -2
- package/dist/lib/{shadow-claw-MumteAJy.js.map → shadow-claw-4x3LqlII.js.map} +1 -1
- package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js → shadow-claw-file-viewer-CuDhkVcL.js} +21 -21
- package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js.map → shadow-claw-file-viewer-CuDhkVcL.js.map} +1 -1
- package/dist/lib/syncWebMcpRegistration-Bbph0XZl.js.map +1 -1
- package/dist/public/.well-known/mcp/server-card.json +2 -2
- package/dist/public/.well-known/mcp/servers.json +1 -1
- package/dist/public/.well-known/mcp.json +2 -2
- package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
- package/dist/public/docs/example/article/index.html +2 -2
- package/dist/public/docs/publishing/index.html +2 -2
- package/dist/public/docs/skill-creator/index.html +2 -2
- package/dist/public/index.html +2 -2
- package/dist/public/index.js +1 -1
- package/dist/public/main/index.html +2 -2
- package/dist/public/main/memory/index.html +2 -2
- package/dist/public/{orchestrator-BLkS_jpm.js → orchestrator-Dc_Q73La.js} +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-DmrxnX3Q.js → shadow-claw-Dabb2AEp.js} +2 -2
- package/dist/public/{shadow-claw-file-viewer-a_uRA30F.js → shadow-claw-file-viewer-CDs1QGXk.js} +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/src/cli/cli.ts +2 -2
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +31 -31
- package/src/core/effect.test.ts +19 -0
- package/src/core/orchestrator/orchestrator-state.ts +2 -1
- package/src/core/orchestrator/orchestrator.test.ts +686 -5
- package/src/core/orchestrator/orchestrator.ts +3 -10
- package/src/core/orchestrator/utils/compactContext.test.ts +423 -121
- package/src/core/orchestrator/utils/configurePeerJs.test.ts +27 -0
- package/src/core/orchestrator/utils/deliverResponse.test.ts +127 -0
- package/src/core/orchestrator/utils/dispatchSubagentInvoke.test.ts +23 -0
- package/src/core/orchestrator/utils/enqueue.test.ts +520 -86
- package/src/core/orchestrator/utils/handleWorkerMessage.test.ts +664 -144
- package/src/core/orchestrator/utils/initTasks.node.test.ts +147 -0
- package/src/core/orchestrator/utils/initTasks.test.ts +926 -191
- package/src/core/orchestrator/utils/initTasks.ts +5 -4
- package/src/core/orchestrator/utils/invokeAgent.test.ts +563 -110
- package/src/core/orchestrator/utils/invokeAgent.ts +8 -10
- package/src/core/orchestrator/utils/loadChannelConfigurations.test.ts +50 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +377 -170
- package/src/core/orchestrator/utils/operations/provider.test.ts +663 -85
- package/src/core/orchestrator/utils/operations/room.test.ts +74 -11
- package/src/core/orchestrator/utils/operations/task.test.ts +452 -56
- package/src/core/orchestrator/utils/operations/task.ts +1 -1
- package/src/core/orchestrator/utils/operations/vm.test.ts +66 -7
- package/src/core/orchestrator/utils/parseDirectToolCommand.test.ts +6 -0
- package/src/core/orchestrator/utils/settings.test.ts +248 -81
- package/src/core/orchestrator/utils/setupPushTaskListener.test.ts +58 -0
- package/src/core/orchestrator/utils/syncWebMcpRegistration.test.ts +341 -65
- package/src/core/orchestrator/utils/syncWebMcpRegistration.ts +4 -2
- package/src/server/routes/mcp.ts +2 -2
- package/src/stores/file-viewer.test.ts +11 -0
- package/src/stores/theme.test.ts +41 -2
- package/src/stores/toast.test.ts +25 -0
- package/src/worker/tools/bash/native-bash-executor.test.ts +31 -1
- package/src/worker/tools/git/utils/extractConflictPaths.test.ts +24 -0
- package/src/worker/tools/git/utils/parseConflictRegions.test.ts +46 -0
- package/src/worker/tools/git/utils/resolveCorsProxy.test.ts +26 -5
- package/src/worker/tools/git/utils/truncateSnippet.test.ts +16 -0
- package/src/worker/tools/node-transformers-executor.test.ts +105 -6
- package/src/worker/tools/remote-mcp/remote-mcp.test.ts +172 -47
- package/src/worker/tools/remote-mcp/utils/formatListToolsOutput.test.ts +19 -0
- package/src/worker/tools/remote-mcp/utils/isMcpReauthError.test.ts +45 -0
- package/src/worker/tools/remote-mcp/utils/resolveMcpReauth.test.ts +48 -0
- package/src/worker/tools/rooms/list-room-members.test.ts +24 -10
- package/src/worker/tools/tasks/list-tasks.test.ts +39 -8
- package/src/worker/tools/tasks/update-task.test.ts +78 -14
- package/src/worker/tools/ui/javascript.test.ts +70 -17
- package/src/worker/tools/ui/list-tool-profiles.test.ts +31 -9
- package/dist/lib/orchestrator-Bq9ft3uG.js.map +0 -1
|
@@ -61,6 +61,18 @@ describe("setupPushTaskListener", () => {
|
|
|
61
61
|
{} as ShadowClawDatabase,
|
|
62
62
|
),
|
|
63
63
|
).not.toThrow();
|
|
64
|
+
|
|
65
|
+
Object.defineProperty(global, "navigator", {
|
|
66
|
+
value: {},
|
|
67
|
+
configurable: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(() =>
|
|
71
|
+
setupPushTaskListener(
|
|
72
|
+
mockOrchestrator as Orchestrator,
|
|
73
|
+
{} as ShadowClawDatabase,
|
|
74
|
+
),
|
|
75
|
+
).not.toThrow();
|
|
64
76
|
});
|
|
65
77
|
|
|
66
78
|
it("should register a message listener if serviceWorker exists", () => {
|
|
@@ -203,6 +215,52 @@ describe("setupPushTaskListener", () => {
|
|
|
203
215
|
);
|
|
204
216
|
});
|
|
205
217
|
|
|
218
|
+
it("should ignore events without data or without groupId", () => {
|
|
219
|
+
listener({ data: null });
|
|
220
|
+
listener({ data: { type: "scheduled-task-trigger", taskId: "t1" } });
|
|
221
|
+
expect(mockOrchestrator.schedulerTriggeredGroups?.size).toBe(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should create default taskId and prompt for tool task when omitted", async () => {
|
|
225
|
+
mockOrchestratorStore.tasks = [];
|
|
226
|
+
|
|
227
|
+
listener({
|
|
228
|
+
data: {
|
|
229
|
+
type: "scheduled-task-trigger",
|
|
230
|
+
groupId: "g2",
|
|
231
|
+
taskType: "tools",
|
|
232
|
+
tools: ["t1"],
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await new Promise(process.nextTick);
|
|
237
|
+
|
|
238
|
+
expect(mockOrchestratorStore.runTask).toHaveBeenCalledWith(
|
|
239
|
+
expect.objectContaining({
|
|
240
|
+
groupId: "g2",
|
|
241
|
+
type: "tools",
|
|
242
|
+
tools: ["t1"],
|
|
243
|
+
prompt: "",
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should do nothing if neither task, tools, nor prompt are present", async () => {
|
|
249
|
+
mockOrchestratorStore.tasks = [];
|
|
250
|
+
|
|
251
|
+
listener({
|
|
252
|
+
data: {
|
|
253
|
+
type: "scheduled-task-trigger",
|
|
254
|
+
groupId: "g-empty",
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await new Promise(process.nextTick);
|
|
259
|
+
|
|
260
|
+
expect(mockOrchestratorStore.runTask).not.toHaveBeenCalled();
|
|
261
|
+
expect(mockSubmitMessage).not.toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
|
|
206
264
|
it("should handle error in runTaskHandler", async () => {
|
|
207
265
|
mockOrchestratorStore.tasks = [];
|
|
208
266
|
mockSubmitMessage.mockImplementation(() => {
|
|
@@ -1,34 +1,72 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { CONFIG_KEYS } from "../../../config/config.js";
|
|
4
|
+
import type { ShadowClawDatabase } from "../../../db/db.js";
|
|
5
|
+
import type { DeclarativeToolDefinition } from "../../../subsystems/tools/declarative.js";
|
|
6
|
+
import type { SkillRecord } from "../../../subsystems/skills/types.js";
|
|
7
|
+
import type { ToolDefinition } from "../../../subsystems/tools/tools.js";
|
|
8
|
+
import type { WebMcpMode } from "../../../subsystems/mcp/webmcp.js";
|
|
9
|
+
import { activate_skill } from "../../../subsystems/skills/tool.js";
|
|
10
|
+
import type { Orchestrator } from "../orchestrator.js";
|
|
11
|
+
|
|
12
|
+
const mockRegisterWebMcpTools = jest
|
|
13
|
+
.fn<
|
|
14
|
+
(
|
|
15
|
+
worker: Worker | null,
|
|
16
|
+
onMessage: (msg: unknown) => Promise<void>,
|
|
17
|
+
activeGroupId: string,
|
|
18
|
+
tools: ToolDefinition[],
|
|
19
|
+
) => Promise<void>
|
|
20
|
+
>()
|
|
21
|
+
.mockResolvedValue(undefined);
|
|
22
|
+
|
|
23
|
+
const mockUnregisterWebMcpTools = jest.fn<() => void>();
|
|
24
|
+
const mockApplyWebMcpMode = jest.fn<(mode: WebMcpMode) => void>();
|
|
5
25
|
|
|
6
26
|
jest.unstable_mockModule("../../../subsystems/mcp/webmcp.js", () => ({
|
|
7
27
|
registerWebMcpTools: mockRegisterWebMcpTools,
|
|
8
28
|
unregisterWebMcpTools: mockUnregisterWebMcpTools,
|
|
9
|
-
setWebMcpMode:
|
|
29
|
+
setWebMcpMode: mockApplyWebMcpMode,
|
|
10
30
|
}));
|
|
11
31
|
|
|
12
|
-
const mockHandleWorkerMessage = jest
|
|
32
|
+
const mockHandleWorkerMessage = jest
|
|
33
|
+
.fn<
|
|
34
|
+
(
|
|
35
|
+
orchestrator: Orchestrator,
|
|
36
|
+
db: ShadowClawDatabase,
|
|
37
|
+
msg: unknown,
|
|
38
|
+
) => Promise<void>
|
|
39
|
+
>()
|
|
40
|
+
.mockResolvedValue(undefined);
|
|
41
|
+
|
|
13
42
|
jest.unstable_mockModule("./handleWorkerMessage.js", () => ({
|
|
14
43
|
handleWorkerMessage: mockHandleWorkerMessage,
|
|
15
44
|
}));
|
|
16
45
|
|
|
17
46
|
jest.unstable_mockModule("../../effect.js", () => ({
|
|
18
|
-
effect: (cb:
|
|
47
|
+
effect: (cb: () => void) => {
|
|
19
48
|
cb();
|
|
20
49
|
return jest.fn();
|
|
21
50
|
},
|
|
22
51
|
}));
|
|
23
52
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
53
|
+
const mockSetConfig = jest
|
|
54
|
+
.fn<(db: ShadowClawDatabase, key: string, value: string) => Promise<void>>()
|
|
55
|
+
.mockResolvedValue(undefined);
|
|
56
|
+
|
|
57
|
+
jest.unstable_mockModule("../../../db/setConfig.js", () => ({
|
|
58
|
+
setConfig: mockSetConfig,
|
|
29
59
|
}));
|
|
30
60
|
|
|
31
|
-
const mockDiscoverSkills = jest
|
|
61
|
+
const mockDiscoverSkills = jest
|
|
62
|
+
.fn<
|
|
63
|
+
(
|
|
64
|
+
db: ShadowClawDatabase,
|
|
65
|
+
groupId: string,
|
|
66
|
+
) => Promise<{ skills: SkillRecord[] }>
|
|
67
|
+
>()
|
|
68
|
+
.mockResolvedValue({ skills: [] });
|
|
69
|
+
|
|
32
70
|
jest.unstable_mockModule(
|
|
33
71
|
"../../../subsystems/skills/discoverSkills.js",
|
|
34
72
|
() => ({
|
|
@@ -36,79 +74,147 @@ jest.unstable_mockModule(
|
|
|
36
74
|
}),
|
|
37
75
|
);
|
|
38
76
|
|
|
39
|
-
const mockLoadDeclarativeTools = jest
|
|
77
|
+
const mockLoadDeclarativeTools = jest
|
|
78
|
+
.fn<
|
|
79
|
+
(
|
|
80
|
+
db: ShadowClawDatabase,
|
|
81
|
+
groupId: string,
|
|
82
|
+
) => Promise<{ tools: DeclarativeToolDefinition[] }>
|
|
83
|
+
>()
|
|
84
|
+
.mockResolvedValue({ tools: [] });
|
|
85
|
+
|
|
40
86
|
jest.unstable_mockModule("../../../subsystems/tools/declarative.js", () => ({
|
|
41
87
|
loadDeclarativeTools: mockLoadDeclarativeTools,
|
|
42
88
|
}));
|
|
43
89
|
|
|
90
|
+
let activeGroupId = "group1";
|
|
91
|
+
const mockGroups: { groupId: string; toolTags?: string[] }[] = [
|
|
92
|
+
{ groupId: "group1", toolTags: ["tool1", "enabled_decl_tool"] },
|
|
93
|
+
{ groupId: "group2" },
|
|
94
|
+
];
|
|
95
|
+
const mockAllTools: ToolDefinition[] = [
|
|
96
|
+
{
|
|
97
|
+
name: "tool1",
|
|
98
|
+
description: "Tool 1",
|
|
99
|
+
input_schema: { type: "object", properties: {} },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "tool2",
|
|
103
|
+
description: "Tool 2",
|
|
104
|
+
input_schema: { type: "object", properties: {} },
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
let mockGlobalTools: ToolDefinition[] = [
|
|
108
|
+
{
|
|
109
|
+
name: "tool2",
|
|
110
|
+
description: "Tool 2",
|
|
111
|
+
input_schema: { type: "object", properties: {} },
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
let declarativeToolsFail = false;
|
|
115
|
+
|
|
116
|
+
jest.unstable_mockModule("../../../stores/orchestrator.js", () => ({
|
|
117
|
+
orchestratorStore: {
|
|
118
|
+
get activeGroupId() {
|
|
119
|
+
return activeGroupId;
|
|
120
|
+
},
|
|
121
|
+
get groups() {
|
|
122
|
+
return mockGroups;
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
|
|
44
127
|
jest.unstable_mockModule("../../../stores/tools.js", () => ({
|
|
45
128
|
toolsStore: {
|
|
46
|
-
allTools
|
|
47
|
-
|
|
129
|
+
get allTools() {
|
|
130
|
+
return mockAllTools;
|
|
131
|
+
},
|
|
132
|
+
get enabledTools() {
|
|
133
|
+
return mockGlobalTools;
|
|
134
|
+
},
|
|
48
135
|
declarativeTools: [],
|
|
49
136
|
declarativeToolNamesEnabled: null,
|
|
50
|
-
refreshDeclarativeTools: jest.fn(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
137
|
+
refreshDeclarativeTools: jest.fn(
|
|
138
|
+
async (db: ShadowClawDatabase, gId: string) => {
|
|
139
|
+
if (declarativeToolsFail) {
|
|
140
|
+
throw new Error("OPFS declarative tools load failed");
|
|
141
|
+
}
|
|
142
|
+
const res = await mockLoadDeclarativeTools(db, gId);
|
|
143
|
+
return res?.tools || [];
|
|
144
|
+
},
|
|
145
|
+
),
|
|
146
|
+
isDeclarativeToolEnabled: jest.fn(
|
|
147
|
+
(name: string) => name === "enabled_decl_tool",
|
|
148
|
+
),
|
|
55
149
|
},
|
|
56
150
|
}));
|
|
57
151
|
|
|
58
|
-
const { syncWebMcpRegistration } =
|
|
152
|
+
const { syncWebMcpRegistration, setWebMcpMode, setWebMcpToolsEnabled } =
|
|
153
|
+
await import("./syncWebMcpRegistration.js");
|
|
154
|
+
|
|
155
|
+
interface MockOrchestrator {
|
|
156
|
+
webMcpEffectCleanup: (() => void) | null;
|
|
157
|
+
webMcpToolsEnabled: boolean;
|
|
158
|
+
webMcpRegistrationLock: Promise<void>;
|
|
159
|
+
agentWorker: Worker | null;
|
|
160
|
+
}
|
|
59
161
|
|
|
60
162
|
describe("syncWebMcpRegistration", () => {
|
|
61
|
-
let mockOrchestrator:
|
|
62
|
-
let mockDb:
|
|
163
|
+
let mockOrchestrator: MockOrchestrator;
|
|
164
|
+
let mockDb: ShadowClawDatabase;
|
|
63
165
|
|
|
64
166
|
beforeEach(() => {
|
|
65
167
|
jest.clearAllMocks();
|
|
66
|
-
|
|
168
|
+
activeGroupId = "group1";
|
|
169
|
+
declarativeToolsFail = false;
|
|
170
|
+
mockGlobalTools = [
|
|
171
|
+
{
|
|
172
|
+
name: "tool2",
|
|
173
|
+
description: "Tool 2",
|
|
174
|
+
input_schema: { type: "object", properties: {} },
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
mockDb = {} as unknown as ShadowClawDatabase;
|
|
67
178
|
mockOrchestrator = {
|
|
68
179
|
webMcpEffectCleanup: jest.fn(),
|
|
69
180
|
webMcpToolsEnabled: true,
|
|
70
181
|
webMcpRegistrationLock: Promise.resolve(),
|
|
71
|
-
agentWorker: {},
|
|
72
|
-
handleWorkerMessage: jest.fn(),
|
|
182
|
+
agentWorker: {} as unknown as Worker,
|
|
73
183
|
};
|
|
74
184
|
mockDiscoverSkills.mockResolvedValue({ skills: [] });
|
|
75
185
|
mockLoadDeclarativeTools.mockResolvedValue({
|
|
76
186
|
tools: [
|
|
77
187
|
{
|
|
78
|
-
name: "
|
|
79
|
-
description: "
|
|
188
|
+
name: "enabled_decl_tool",
|
|
189
|
+
description: "Enabled Declarative Tool",
|
|
190
|
+
input_schema: { type: "object", properties: {} },
|
|
191
|
+
execution: { type: "bash", command: "echo test" },
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "disabled_decl_tool",
|
|
195
|
+
description: "Disabled Declarative Tool",
|
|
196
|
+
input_schema: { type: "object", properties: {} },
|
|
197
|
+
execution: { type: "bash", command: "echo disabled" },
|
|
80
198
|
},
|
|
81
|
-
{ name: "unpinned_decl_tool", description: "Unpinned tool" },
|
|
82
199
|
],
|
|
83
200
|
});
|
|
84
201
|
});
|
|
85
202
|
|
|
86
203
|
it("should clean up existing effect and unregister if not enabled", () => {
|
|
87
204
|
mockOrchestrator.webMcpToolsEnabled = false;
|
|
205
|
+
const cleanupMock = mockOrchestrator.webMcpEffectCleanup!;
|
|
88
206
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
syncWebMcpRegistration(mockOrchestrator, mockDb);
|
|
207
|
+
syncWebMcpRegistration(mockOrchestrator as unknown as Orchestrator, mockDb);
|
|
92
208
|
|
|
93
209
|
expect(cleanupMock).toHaveBeenCalled();
|
|
94
210
|
expect(mockOrchestrator.webMcpEffectCleanup).toBeNull();
|
|
95
211
|
expect(mockUnregisterWebMcpTools).toHaveBeenCalled();
|
|
96
212
|
});
|
|
97
213
|
|
|
98
|
-
it("should setup effect and register tools
|
|
99
|
-
|
|
100
|
-
const orchestratorStoreModule =
|
|
101
|
-
await import("../../../stores/orchestrator.js");
|
|
102
|
-
orchestratorStoreModule.orchestratorStore.groups[0].toolTags = [
|
|
103
|
-
"tool1",
|
|
104
|
-
"generate_random_number",
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
syncWebMcpRegistration(mockOrchestrator, mockDb);
|
|
214
|
+
it("should setup effect and register tools filtered by group toolTags", async () => {
|
|
215
|
+
syncWebMcpRegistration(mockOrchestrator as unknown as Orchestrator, mockDb);
|
|
108
216
|
|
|
109
217
|
expect(mockOrchestrator.webMcpEffectCleanup).toBeDefined();
|
|
110
|
-
|
|
111
|
-
// The effect should run and chain onto webMcpRegistrationLock
|
|
112
218
|
await mockOrchestrator.webMcpRegistrationLock;
|
|
113
219
|
|
|
114
220
|
expect(mockUnregisterWebMcpTools).toHaveBeenCalled();
|
|
@@ -117,47 +223,100 @@ describe("syncWebMcpRegistration", () => {
|
|
|
117
223
|
expect.any(Function),
|
|
118
224
|
"group1",
|
|
119
225
|
[
|
|
120
|
-
{ name: "tool1" },
|
|
121
226
|
{
|
|
122
|
-
name: "
|
|
123
|
-
description: "
|
|
227
|
+
name: "tool1",
|
|
228
|
+
description: "Tool 1",
|
|
229
|
+
input_schema: { type: "object", properties: {} },
|
|
124
230
|
},
|
|
231
|
+
expect.objectContaining({
|
|
232
|
+
name: "enabled_decl_tool",
|
|
233
|
+
description: "Enabled Declarative Tool",
|
|
234
|
+
}),
|
|
125
235
|
],
|
|
126
236
|
);
|
|
127
237
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
await cb("test message");
|
|
238
|
+
const onMessageCb = mockRegisterWebMcpTools.mock.calls[0][1];
|
|
239
|
+
await onMessageCb({ type: "worker-message" });
|
|
131
240
|
expect(mockHandleWorkerMessage).toHaveBeenCalledWith(
|
|
132
|
-
mockOrchestrator,
|
|
241
|
+
mockOrchestrator as unknown as Orchestrator,
|
|
133
242
|
mockDb,
|
|
134
|
-
"
|
|
243
|
+
{ type: "worker-message" },
|
|
135
244
|
);
|
|
136
245
|
});
|
|
137
246
|
|
|
138
|
-
it("should
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
247
|
+
it("should register globalTools and enabledDeclarativeTools when group has no toolTags", async () => {
|
|
248
|
+
activeGroupId = "group2";
|
|
249
|
+
|
|
250
|
+
syncWebMcpRegistration(mockOrchestrator as unknown as Orchestrator, mockDb);
|
|
251
|
+
await mockOrchestrator.webMcpRegistrationLock;
|
|
252
|
+
|
|
253
|
+
expect(mockRegisterWebMcpTools).toHaveBeenCalledWith(
|
|
254
|
+
mockOrchestrator.agentWorker,
|
|
255
|
+
expect.any(Function),
|
|
256
|
+
"group2",
|
|
257
|
+
[
|
|
258
|
+
{
|
|
259
|
+
name: "tool2",
|
|
260
|
+
description: "Tool 2",
|
|
261
|
+
input_schema: { type: "object", properties: {} },
|
|
262
|
+
},
|
|
263
|
+
expect.objectContaining({
|
|
264
|
+
name: "enabled_decl_tool",
|
|
265
|
+
description: "Enabled Declarative Tool",
|
|
266
|
+
}),
|
|
267
|
+
],
|
|
268
|
+
);
|
|
269
|
+
});
|
|
143
270
|
|
|
144
|
-
|
|
271
|
+
it("should register globalTools and enabledDeclarativeTools when activeGroupId is not found in groups", async () => {
|
|
272
|
+
activeGroupId = "unknown-group";
|
|
145
273
|
|
|
274
|
+
syncWebMcpRegistration(mockOrchestrator as unknown as Orchestrator, mockDb);
|
|
146
275
|
await mockOrchestrator.webMcpRegistrationLock;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
276
|
+
|
|
277
|
+
expect(mockRegisterWebMcpTools).toHaveBeenCalledWith(
|
|
278
|
+
mockOrchestrator.agentWorker,
|
|
279
|
+
expect.any(Function),
|
|
280
|
+
"unknown-group",
|
|
281
|
+
[
|
|
282
|
+
{
|
|
283
|
+
name: "tool2",
|
|
284
|
+
description: "Tool 2",
|
|
285
|
+
input_schema: { type: "object", properties: {} },
|
|
286
|
+
},
|
|
287
|
+
expect.objectContaining({
|
|
288
|
+
name: "enabled_decl_tool",
|
|
289
|
+
description: "Enabled Declarative Tool",
|
|
290
|
+
}),
|
|
291
|
+
],
|
|
150
292
|
);
|
|
293
|
+
});
|
|
151
294
|
|
|
152
|
-
|
|
295
|
+
it("should ignore OPFS load errors in refreshDeclarativeTools and discoverSkills", async () => {
|
|
296
|
+
declarativeToolsFail = true;
|
|
297
|
+
mockDiscoverSkills.mockRejectedValueOnce(new Error("OPFS skills failure"));
|
|
298
|
+
|
|
299
|
+
syncWebMcpRegistration(mockOrchestrator as unknown as Orchestrator, mockDb);
|
|
300
|
+
await mockOrchestrator.webMcpRegistrationLock;
|
|
301
|
+
|
|
302
|
+
expect(mockRegisterWebMcpTools).toHaveBeenCalled();
|
|
153
303
|
});
|
|
154
304
|
|
|
155
|
-
it("should append activate_skill
|
|
156
|
-
mockDiscoverSkills.
|
|
157
|
-
skills: [
|
|
305
|
+
it("should append activate_skill when skills are discovered and tool is not present", async () => {
|
|
306
|
+
mockDiscoverSkills.mockResolvedValueOnce({
|
|
307
|
+
skills: [
|
|
308
|
+
{
|
|
309
|
+
name: "test-skill",
|
|
310
|
+
description: "Test skill description",
|
|
311
|
+
path: "skills/test/SKILL.md",
|
|
312
|
+
basePath: "skills/test",
|
|
313
|
+
userInvocable: true,
|
|
314
|
+
disableModelInvocation: false,
|
|
315
|
+
},
|
|
316
|
+
],
|
|
158
317
|
});
|
|
159
318
|
|
|
160
|
-
syncWebMcpRegistration(mockOrchestrator, mockDb);
|
|
319
|
+
syncWebMcpRegistration(mockOrchestrator as unknown as Orchestrator, mockDb);
|
|
161
320
|
await mockOrchestrator.webMcpRegistrationLock;
|
|
162
321
|
|
|
163
322
|
expect(mockRegisterWebMcpTools).toHaveBeenCalledWith(
|
|
@@ -169,4 +328,121 @@ describe("syncWebMcpRegistration", () => {
|
|
|
169
328
|
]),
|
|
170
329
|
);
|
|
171
330
|
});
|
|
331
|
+
|
|
332
|
+
it("should not duplicate activate_skill if already present in tool list", async () => {
|
|
333
|
+
mockDiscoverSkills.mockResolvedValueOnce({
|
|
334
|
+
skills: [
|
|
335
|
+
{
|
|
336
|
+
name: "test-skill",
|
|
337
|
+
description: "Test skill description",
|
|
338
|
+
path: "skills/test/SKILL.md",
|
|
339
|
+
basePath: "skills/test",
|
|
340
|
+
userInvocable: true,
|
|
341
|
+
disableModelInvocation: false,
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
});
|
|
345
|
+
activeGroupId = "group2";
|
|
346
|
+
mockGlobalTools = [
|
|
347
|
+
{
|
|
348
|
+
name: "tool2",
|
|
349
|
+
description: "Tool 2",
|
|
350
|
+
input_schema: { type: "object", properties: {} },
|
|
351
|
+
},
|
|
352
|
+
activate_skill,
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
syncWebMcpRegistration(mockOrchestrator as unknown as Orchestrator, mockDb);
|
|
356
|
+
await mockOrchestrator.webMcpRegistrationLock;
|
|
357
|
+
|
|
358
|
+
const registeredTools = mockRegisterWebMcpTools.mock.calls[0][3];
|
|
359
|
+
const activateSkillCount = registeredTools.filter(
|
|
360
|
+
(t) => t.name === "activate_skill",
|
|
361
|
+
).length;
|
|
362
|
+
expect(activateSkillCount).toBe(1);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("should catch and log errors in registration lock promise", async () => {
|
|
366
|
+
mockRegisterWebMcpTools.mockRejectedValueOnce(
|
|
367
|
+
new Error("Registration error"),
|
|
368
|
+
);
|
|
369
|
+
const consoleError = jest
|
|
370
|
+
.spyOn(console, "error")
|
|
371
|
+
.mockImplementation(() => {});
|
|
372
|
+
|
|
373
|
+
syncWebMcpRegistration(mockOrchestrator as unknown as Orchestrator, mockDb);
|
|
374
|
+
await mockOrchestrator.webMcpRegistrationLock;
|
|
375
|
+
|
|
376
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
377
|
+
"WebMCP registration failed:",
|
|
378
|
+
expect.any(Error),
|
|
379
|
+
);
|
|
380
|
+
consoleError.mockRestore();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("should proceed without calling cleanup if webMcpEffectCleanup is null", async () => {
|
|
384
|
+
mockOrchestrator.webMcpEffectCleanup = null;
|
|
385
|
+
|
|
386
|
+
syncWebMcpRegistration(mockOrchestrator as unknown as Orchestrator, mockDb);
|
|
387
|
+
await mockOrchestrator.webMcpRegistrationLock;
|
|
388
|
+
|
|
389
|
+
expect(mockRegisterWebMcpTools).toHaveBeenCalled();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe("setWebMcpMode", () => {
|
|
393
|
+
it("should unregister tools, apply mode, save config, and trigger sync", async () => {
|
|
394
|
+
const syncDeps = {
|
|
395
|
+
orchestrator: mockOrchestrator as unknown as Orchestrator,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
await setWebMcpMode(mockOrchestrator, mockDb, "native", syncDeps);
|
|
399
|
+
await mockOrchestrator.webMcpRegistrationLock;
|
|
400
|
+
|
|
401
|
+
expect(mockUnregisterWebMcpTools).toHaveBeenCalled();
|
|
402
|
+
expect(mockApplyWebMcpMode).toHaveBeenCalledWith("native");
|
|
403
|
+
expect(mockSetConfig).toHaveBeenCalledWith(
|
|
404
|
+
mockDb,
|
|
405
|
+
CONFIG_KEYS.WEBMCP_MODE,
|
|
406
|
+
"native",
|
|
407
|
+
);
|
|
408
|
+
expect(mockRegisterWebMcpTools).toHaveBeenCalled();
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe("setWebMcpToolsEnabled", () => {
|
|
413
|
+
it("should update enabled state to true, persist config, and sync", async () => {
|
|
414
|
+
mockOrchestrator.webMcpToolsEnabled = false;
|
|
415
|
+
const syncDeps = {
|
|
416
|
+
orchestrator: mockOrchestrator as unknown as Orchestrator,
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
await setWebMcpToolsEnabled(mockOrchestrator, mockDb, true, syncDeps);
|
|
420
|
+
await mockOrchestrator.webMcpRegistrationLock;
|
|
421
|
+
|
|
422
|
+
expect(mockOrchestrator.webMcpToolsEnabled).toBe(true);
|
|
423
|
+
expect(mockSetConfig).toHaveBeenCalledWith(
|
|
424
|
+
mockDb,
|
|
425
|
+
CONFIG_KEYS.WEBMCP_TOOLS_ENABLED,
|
|
426
|
+
"true",
|
|
427
|
+
);
|
|
428
|
+
expect(mockRegisterWebMcpTools).toHaveBeenCalled();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("should update enabled state to false, persist config, and sync", async () => {
|
|
432
|
+
mockOrchestrator.webMcpToolsEnabled = true;
|
|
433
|
+
const syncDeps = {
|
|
434
|
+
orchestrator: mockOrchestrator as unknown as Orchestrator,
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
await setWebMcpToolsEnabled(mockOrchestrator, mockDb, false, syncDeps);
|
|
438
|
+
|
|
439
|
+
expect(mockOrchestrator.webMcpToolsEnabled).toBe(false);
|
|
440
|
+
expect(mockSetConfig).toHaveBeenCalledWith(
|
|
441
|
+
mockDb,
|
|
442
|
+
CONFIG_KEYS.WEBMCP_TOOLS_ENABLED,
|
|
443
|
+
"false",
|
|
444
|
+
);
|
|
445
|
+
expect(mockUnregisterWebMcpTools).toHaveBeenCalled();
|
|
446
|
+
});
|
|
447
|
+
});
|
|
172
448
|
});
|
|
@@ -14,6 +14,8 @@ import { CONFIG_KEYS } from "../../../config/config.js";
|
|
|
14
14
|
import { setConfig } from "../../../db/setConfig.js";
|
|
15
15
|
|
|
16
16
|
import type { ShadowClawDatabase } from "../../../db/db.js";
|
|
17
|
+
import type { DeclarativeToolDefinition } from "../../../subsystems/tools/declarative.js";
|
|
18
|
+
import type { SkillRecord } from "../../../subsystems/skills/types.js";
|
|
17
19
|
import type { WebMcpMode } from "../../../subsystems/mcp/webmcp.js";
|
|
18
20
|
import type { OrchestratorState } from "../orchestrator-state.js";
|
|
19
21
|
import type { Orchestrator } from "../orchestrator.js";
|
|
@@ -60,7 +62,7 @@ export function syncWebMcpRegistration(
|
|
|
60
62
|
// Small delay to allow the browser's ModelContext to process the unregistrations.
|
|
61
63
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
62
64
|
|
|
63
|
-
let declarativeTools:
|
|
65
|
+
let declarativeTools: DeclarativeToolDefinition[] = [];
|
|
64
66
|
try {
|
|
65
67
|
declarativeTools = await toolsStore.refreshDeclarativeTools(
|
|
66
68
|
db,
|
|
@@ -80,7 +82,7 @@ export function syncWebMcpRegistration(
|
|
|
80
82
|
? candidateTools.filter((t) => group.toolTags!.includes(t.name))
|
|
81
83
|
: [...globalTools, ...enabledDeclarativeTools];
|
|
82
84
|
|
|
83
|
-
let skillDiscovery: { skills:
|
|
85
|
+
let skillDiscovery: { skills: SkillRecord[] } = { skills: [] };
|
|
84
86
|
try {
|
|
85
87
|
skillDiscovery = await discoverSkills(db, activeGroupId);
|
|
86
88
|
} catch {
|
package/src/server/routes/mcp.ts
CHANGED
|
@@ -256,7 +256,7 @@ export function registerMcpRoutes(
|
|
|
256
256
|
name: "shadow-claw",
|
|
257
257
|
version: getPackageVersion(),
|
|
258
258
|
description:
|
|
259
|
-
"
|
|
259
|
+
"Multi-runtime AI assistant with local and remote LLM orchestration, Web Workers, OPFS storage, and agentic tool execution.",
|
|
260
260
|
title: "ShadowClaw MCP Server",
|
|
261
261
|
websiteUrl: `${origin}/shadow-claw/`,
|
|
262
262
|
repository: {
|
|
@@ -332,7 +332,7 @@ export function registerMcpRoutes(
|
|
|
332
332
|
servers: [
|
|
333
333
|
{
|
|
334
334
|
name: "shadow-claw",
|
|
335
|
-
description: "ShadowClaw
|
|
335
|
+
description: "ShadowClaw Multi-Runtime AI Assistant MCP Server",
|
|
336
336
|
endpoint: `${origin}/mcp`,
|
|
337
337
|
capabilities: ["tools", "extensions"],
|
|
338
338
|
card: `${origin}/.well-known/mcp.json`,
|
|
@@ -155,4 +155,15 @@ describe("FileViewerStore", () => {
|
|
|
155
155
|
mimeType: "text/plain",
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
it("handles empty path and extension-less filenames", async () => {
|
|
160
|
+
(readGroupFile as any).mockResolvedValue("empty-content");
|
|
161
|
+
const s = new FileViewerStore();
|
|
162
|
+
|
|
163
|
+
expect(s.getPreviewBinaryMimeType("")).toBe("");
|
|
164
|
+
expect(s.getPreviewBinaryMimeType("noextension")).toBe("");
|
|
165
|
+
|
|
166
|
+
await s.openFile({} as any, "");
|
|
167
|
+
expect(s.file?.name).toBe("");
|
|
168
|
+
});
|
|
158
169
|
});
|