@j0hanz/filesystem-mcp 1.6.1 → 1.6.2
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/dist/prompts.js +1 -1
- package/dist/resources/generated-instructions.js +19 -22
- package/dist/resources/tool-catalog.js +2 -2
- package/dist/resources/tool-info.js +1 -1
- package/dist/resources/workflows.js +2 -3
- package/dist/resources.js +3 -3
- package/dist/server/bootstrap.js +1 -6
- package/dist/server/capabilities.js +4 -1
- package/dist/server/roots-manager.d.ts +2 -0
- package/dist/server/roots-manager.js +24 -6
- package/dist/tools/shared.d.ts +10 -0
- package/dist/tools/shared.js +20 -8
- package/dist/tools/task-support.js +19 -0
- package/package.json +1 -1
package/dist/prompts.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { withDefaultIcons } from './tools/shared.js';
|
|
3
3
|
const HELP_PROMPT_NAME = 'get-help';
|
|
4
4
|
const HELP_PROMPT_TITLE = 'Get Help';
|
|
5
|
-
const HELP_PROMPT_DESCRIPTION = '
|
|
5
|
+
const HELP_PROMPT_DESCRIPTION = 'Retrieve the full filesystem-mcp XML usage guide.';
|
|
6
6
|
function filterInstructionsByTopic(instructions, topic) {
|
|
7
7
|
const normalized = topic.trim().toLowerCase();
|
|
8
8
|
if (!normalized)
|
|
@@ -1,46 +1,44 @@
|
|
|
1
1
|
import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
|
|
2
2
|
import { buildCoreContextPack, getSharedConstraints, getToolContracts, } from './tool-info.js';
|
|
3
3
|
import { buildWorkflowGuide } from './workflows.js';
|
|
4
|
-
const INSTRUCTIONS_HEADER =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## TOOLS
|
|
4
|
+
const INSTRUCTIONS_HEADER = `<role>
|
|
5
|
+
Expert filesystem agent. Operate ONLY within allowed roots. Always discover before acting — never guess paths.
|
|
6
|
+
</role>
|
|
9
7
|
|
|
8
|
+
<tools_overview>
|
|
10
9
|
| Category | Tools |
|
|
11
10
|
|----------|-------|
|
|
12
11
|
| Navigate | \`roots\`, \`ls\`, \`tree\`, \`find\` |
|
|
13
12
|
| Inspect | \`stat\`, \`stat_many\`, \`grep\`, \`calculate_hash\` |
|
|
14
13
|
| Read | \`read\`, \`read_many\`, \`diff_files\` |
|
|
15
14
|
| Write | \`mkdir\`, \`write\`, \`edit\`, \`mv\`, \`rm\`, \`apply_patch\`, \`search_and_replace\` |
|
|
15
|
+
</tools_overview>
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- \`filesystem-mcp://
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## TASK PROTOCOL
|
|
17
|
+
<resources>
|
|
18
|
+
- \`filesystem-mcp://result/{id}\`: Large output cache. Call \`resources/read\` immediately if \`resourceUri\` is returned.
|
|
19
|
+
- \`filesystem-mcp://metrics\`: Live per-tool stats.
|
|
20
|
+
</resources>
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
<task_protocol>
|
|
23
|
+
Async execution: provide \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, call \`tasks/result\`.
|
|
25
24
|
Task-capable: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
|
|
26
|
-
|
|
25
|
+
</task_protocol>
|
|
27
26
|
`;
|
|
28
|
-
const INSTRUCTIONS_FOOTER =
|
|
29
|
-
## CONSTRAINTS
|
|
30
|
-
|
|
27
|
+
const INSTRUCTIONS_FOOTER = `<constraints>
|
|
31
28
|
${getSharedConstraints()
|
|
32
29
|
.map((c) => `- ${c}`)
|
|
33
30
|
.join('\n')}
|
|
31
|
+
</constraints>
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
<error_handling>
|
|
37
34
|
- \`E_ACCESS_DENIED\` → Call \`roots\`; use allowed path.
|
|
38
35
|
- \`E_NOT_FOUND\` → Call \`ls\`/\`find\`; verify spelling.
|
|
39
36
|
- \`E_TOO_LARGE\` → Use range/head or \`read_many\`.
|
|
40
37
|
- \`E_TIMEOUT\` → Reduce scope or result limits.
|
|
38
|
+
</error_handling>
|
|
41
39
|
`;
|
|
42
40
|
function formatToolSection(tool) {
|
|
43
|
-
const parts = [
|
|
41
|
+
const parts = [`### ${tool.name}\n${tool.description}`];
|
|
44
42
|
if (tool.nuances && tool.nuances.length > 0) {
|
|
45
43
|
parts.push(...tool.nuances.map((n) => `» ${n}`));
|
|
46
44
|
}
|
|
@@ -57,13 +55,12 @@ export function buildServerInstructions() {
|
|
|
57
55
|
'',
|
|
58
56
|
buildToolCatalogDetailsOnly(),
|
|
59
57
|
'',
|
|
60
|
-
'
|
|
61
|
-
'',
|
|
58
|
+
'<tool_reference>',
|
|
62
59
|
toolSections,
|
|
60
|
+
'</tool_reference>',
|
|
63
61
|
'',
|
|
64
62
|
buildWorkflowGuide(),
|
|
65
63
|
'',
|
|
66
|
-
'---',
|
|
67
64
|
INSTRUCTIONS_FOOTER,
|
|
68
65
|
].join('\n');
|
|
69
66
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { buildCoreContextPack } from './tool-info.js';
|
|
2
|
-
const CATALOG_GUIDE =
|
|
3
|
-
|
|
2
|
+
const CATALOG_GUIDE = `<tool_selection_guide>
|
|
4
3
|
## Cross-Tool Data Flow
|
|
5
4
|
|
|
6
5
|
\`\`\`
|
|
@@ -25,6 +24,7 @@ diff_files (patch text) -> apply_patch.patch
|
|
|
25
24
|
- Always generate a patch with \`diff_files\` first.
|
|
26
25
|
- Always use \`dryRun: true\` with \`apply_patch\` to verify changes.
|
|
27
26
|
- \`apply_patch\` works on unified diff format.
|
|
27
|
+
</tool_selection_guide>
|
|
28
28
|
`;
|
|
29
29
|
export function buildToolCatalog() {
|
|
30
30
|
return `${buildCoreContextPack()}\n\n${CATALOG_GUIDE}`;
|
|
@@ -33,7 +33,7 @@ export function buildCoreContextPack() {
|
|
|
33
33
|
const annotations = e.annotations ? ` ${e.annotations.join(' ')}` : '';
|
|
34
34
|
return `| \`${e.name}\` | ${e.description}${annotations} |`;
|
|
35
35
|
});
|
|
36
|
-
return
|
|
36
|
+
return `<core_context>\n| Tool | Purpose |\n|------|---------|\n${rows.join('\n')}\n</core_context>`;
|
|
37
37
|
}
|
|
38
38
|
export function getSharedConstraints() {
|
|
39
39
|
return [
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export function buildWorkflowGuide() {
|
|
2
|
-
return
|
|
3
|
-
|
|
2
|
+
return `<workflows>
|
|
4
3
|
### A: EXPLORE
|
|
5
4
|
Use when: navigating an unfamiliar directory or reading file content.
|
|
6
5
|
1. \`roots\` (List allowed paths).
|
|
@@ -30,5 +29,5 @@ Use when: applying structured diffs produced by \`diff_files\`.
|
|
|
30
29
|
2. \`apply_patch\` (dryRun: true).
|
|
31
30
|
3. \`apply_patch\` (dryRun: false).
|
|
32
31
|
> **Tip:** Pass \`diff_files\` output directly into \`apply_patch\`.
|
|
33
|
-
|
|
32
|
+
</workflows>`;
|
|
34
33
|
}
|
package/dist/resources.js
CHANGED
|
@@ -21,7 +21,7 @@ const TOOL_INFO_RESOURCE_NAME = 'filesystem-mcp-tool-info';
|
|
|
21
21
|
const TOOL_INFO_RESOURCE_DESCRIPTION = 'Per-tool contract details, nuances, and gotchas. Read internal://tool-info/{name} with a tool name such as "read", "ls", or "grep".';
|
|
22
22
|
const INSTRUCTIONS_RESOURCE_NAME = 'filesystem-mcp-instructions';
|
|
23
23
|
const INSTRUCTIONS_RESOURCE_URI = 'internal://instructions';
|
|
24
|
-
const INSTRUCTIONS_RESOURCE_DESCRIPTION = '
|
|
24
|
+
const INSTRUCTIONS_RESOURCE_DESCRIPTION = 'Comprehensive rules and guidelines for filesystem-mcp usage.';
|
|
25
25
|
const RESULT_RESOURCE_NAME = 'filesystem-mcp-result';
|
|
26
26
|
const RESULT_RESOURCE_DESCRIPTION = 'Ephemeral cached tool output exposed as an MCP resource. Not guaranteed to be listed via resources/list.';
|
|
27
27
|
const METRICS_RESOURCE_NAME = 'filesystem-mcp-metrics';
|
|
@@ -29,10 +29,10 @@ const METRICS_RESOURCE_URI = 'filesystem-mcp://metrics';
|
|
|
29
29
|
const METRICS_RESOURCE_DESCRIPTION = 'Live per-tool call/error/avgDurationMs metrics snapshot.';
|
|
30
30
|
const CATALOG_RESOURCE_NAME = 'filesystem-mcp-catalog';
|
|
31
31
|
const CATALOG_RESOURCE_URI = 'internal://tool-catalog';
|
|
32
|
-
const CATALOG_RESOURCE_DESCRIPTION = '
|
|
32
|
+
const CATALOG_RESOURCE_DESCRIPTION = 'Tool selection guide and data flow map.';
|
|
33
33
|
const WORKFLOW_RESOURCE_NAME = 'filesystem-mcp-workflows';
|
|
34
34
|
const WORKFLOW_RESOURCE_URI = 'internal://workflows';
|
|
35
|
-
const WORKFLOW_RESOURCE_DESCRIPTION = '
|
|
35
|
+
const WORKFLOW_RESOURCE_DESCRIPTION = 'Standard operating procedures for exploration, search, edit, and patch.';
|
|
36
36
|
export function registerInstructionResource(server, instructions, iconInfo) {
|
|
37
37
|
server.registerResource(INSTRUCTIONS_RESOURCE_NAME, INSTRUCTIONS_RESOURCE_URI, withDefaultIcons({
|
|
38
38
|
title: 'Server Instructions',
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -59,9 +59,7 @@ export async function createServer(options = {}) {
|
|
|
59
59
|
}),
|
|
60
60
|
};
|
|
61
61
|
if (taskToolSupport) {
|
|
62
|
-
//
|
|
63
|
-
// lifetime. In HTTP mode this may grow unboundedly for long-lived servers.
|
|
64
|
-
// Use a custom TaskStore with eviction for production HTTP deployments.
|
|
62
|
+
// Enabling task tool support requires configuring a task store and message queue on the server config. We use in-memory implementations which are suitable for short-lived stdio sessions. Long-running HTTP servers should replace these with TTL-evicting implementations to avoid unbounded memory growth.
|
|
65
63
|
serverConfig.taskStore = new InMemoryTaskStore();
|
|
66
64
|
serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
|
|
67
65
|
}
|
|
@@ -171,9 +169,6 @@ async function createHttpSession(options, sessions) {
|
|
|
171
169
|
sessions.set(sessionId, { server: mcpServer, transport });
|
|
172
170
|
rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
|
|
173
171
|
},
|
|
174
|
-
onsessionclosed: (sessionId) => {
|
|
175
|
-
sessions.delete(sessionId);
|
|
176
|
-
},
|
|
177
172
|
});
|
|
178
173
|
transport.onclose = () => {
|
|
179
174
|
const { sessionId } = transport;
|
|
@@ -5,13 +5,16 @@ function detectTaskToolSupport() {
|
|
|
5
5
|
return cachedTaskToolSupport;
|
|
6
6
|
}
|
|
7
7
|
try {
|
|
8
|
+
// Instantiate a minimal, unconnected probe server to duck-type check for
|
|
9
|
+
// task tool support. The probe has no transport or active connections, so
|
|
10
|
+
// close() only releases in-memory state; fire-and-forget is safe here.
|
|
8
11
|
const probe = new McpServer({
|
|
9
12
|
name: 'filesystem-mcp-capability-probe',
|
|
10
13
|
version: '0.0.0',
|
|
11
14
|
}, { capabilities: { tools: {} } });
|
|
12
15
|
cachedTaskToolSupport =
|
|
13
16
|
typeof probe.experimental.tasks.registerToolTask === 'function';
|
|
14
|
-
|
|
17
|
+
probe.close().catch(() => { });
|
|
15
18
|
}
|
|
16
19
|
catch {
|
|
17
20
|
cachedTaskToolSupport = false;
|
|
@@ -5,6 +5,8 @@ export declare class RootsManager {
|
|
|
5
5
|
private rootsUpdateTimeout;
|
|
6
6
|
private rootDirectories;
|
|
7
7
|
private clientInitialized;
|
|
8
|
+
private updatingRoots;
|
|
9
|
+
private pendingRootsUpdate;
|
|
8
10
|
private readonly options;
|
|
9
11
|
readonly loggingState: LoggingState;
|
|
10
12
|
constructor(options: ServerOptions, loggingState: LoggingState);
|
|
@@ -84,6 +84,10 @@ export class RootsManager {
|
|
|
84
84
|
rootsUpdateTimeout;
|
|
85
85
|
rootDirectories = [];
|
|
86
86
|
clientInitialized = false;
|
|
87
|
+
// Set to true when an update is in progress, to prevent concurrent executions. If a change arrives while true, we queue a single retry after completion to ensure the last-known state is applied. This
|
|
88
|
+
updatingRoots = false;
|
|
89
|
+
// If an update is in progress and a change arrives, we set this flag to ensure we run another update after completion to apply the latest state
|
|
90
|
+
pendingRootsUpdate = false;
|
|
87
91
|
options;
|
|
88
92
|
loggingState;
|
|
89
93
|
constructor(options, loggingState) {
|
|
@@ -151,23 +155,37 @@ export class RootsManager {
|
|
|
151
155
|
logToMcp(server, 'warning', 'No allowed directories specified. Please provide directories as command-line arguments or enable --allow-cwd to use the current working directory.', this.loggingState.minimumLevel);
|
|
152
156
|
}
|
|
153
157
|
async updateRootsFromClient(server) {
|
|
158
|
+
// Guard against concurrent executions: if one is already running, queue a
|
|
159
|
+
// single retry so the last-known state is always applied after completion.
|
|
160
|
+
if (this.updatingRoots) {
|
|
161
|
+
this.pendingRootsUpdate = true;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.updatingRoots = true;
|
|
154
165
|
try {
|
|
155
166
|
const clientCapabilities = server.server.getClientCapabilities();
|
|
156
167
|
if (!clientCapabilities?.roots) {
|
|
157
168
|
this.rootDirectories = [];
|
|
158
|
-
return;
|
|
159
169
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
else {
|
|
171
|
+
const rootsResult = await server.server.listRoots(undefined, {
|
|
172
|
+
timeout: ROOTS_TIMEOUT_MS,
|
|
173
|
+
});
|
|
174
|
+
const roots = extractRoots(rootsResult);
|
|
175
|
+
this.rootDirectories = await resolveRootDirectories(roots);
|
|
176
|
+
}
|
|
165
177
|
}
|
|
166
178
|
catch (error) {
|
|
167
179
|
logToMcp(server, 'debug', `[DEBUG] MCP Roots protocol unavailable or failed: ${formatUnknownErrorMessage(error)}`, this.loggingState.minimumLevel);
|
|
168
180
|
}
|
|
169
181
|
finally {
|
|
170
182
|
await this.recomputeAllowedDirectories();
|
|
183
|
+
this.updatingRoots = false;
|
|
184
|
+
// If a change arrived while we were running, apply it now.
|
|
185
|
+
if (this.pendingRootsUpdate) {
|
|
186
|
+
this.pendingRootsUpdate = false;
|
|
187
|
+
void this.updateRootsFromClient(server);
|
|
188
|
+
}
|
|
171
189
|
}
|
|
172
190
|
}
|
|
173
191
|
}
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export type ToolResponse<T> = ReturnType<typeof buildToolResponse<T>> & {
|
|
|
47
47
|
interface ToolErrorResponse extends Record<string, unknown> {
|
|
48
48
|
content: ContentBlock[];
|
|
49
49
|
isError: true;
|
|
50
|
+
errorCode?: string;
|
|
50
51
|
}
|
|
51
52
|
export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
|
|
52
53
|
export declare function withValidatedArgs<Args, Result>(schema: z.ZodType<Args>, handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>): (args: unknown, extra: ToolExtra) => Promise<ToolResult<Result>>;
|
|
@@ -116,4 +117,13 @@ export declare function wrapToolHandler<Args, Result>(handler: (args: Args, extr
|
|
|
116
117
|
progressMessage?: (args: Args) => string;
|
|
117
118
|
completionMessage?: (args: Args, result: ToolResult<Result>) => string | undefined;
|
|
118
119
|
}): (args: Args, extra?: ToolExtra) => Promise<ToolResult<Result>>;
|
|
120
|
+
/**
|
|
121
|
+
* Returns `pathValue` if non-empty; otherwise resolves to the single allowed
|
|
122
|
+
* directory from module-level state managed by `RootsManager`. Throws when the
|
|
123
|
+
* path is ambiguous (multiple roots) or when no roots are configured.
|
|
124
|
+
*
|
|
125
|
+
* NOTE: Depends on `getAllowedDirectories()` which reads module-level state
|
|
126
|
+
* updated by `RootsManager`. Ensure the server is initialized before calling.
|
|
127
|
+
* See `src/server/roots-manager.ts` for the update lifecycle.
|
|
128
|
+
*/
|
|
119
129
|
export declare function resolvePathOrRoot(pathValue: string | undefined): string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -198,6 +198,7 @@ export function buildToolErrorResponse(error, defaultCode, path) {
|
|
|
198
198
|
return {
|
|
199
199
|
content: [{ type: 'text', text }],
|
|
200
200
|
isError: true,
|
|
201
|
+
errorCode: detailed.code,
|
|
201
202
|
};
|
|
202
203
|
}
|
|
203
204
|
function buildNotInitializedResult() {
|
|
@@ -259,10 +260,11 @@ export function createProgressReporter(extra) {
|
|
|
259
260
|
// out-of-order progress is undefined in the MCP spec.
|
|
260
261
|
if (current <= lastProgress)
|
|
261
262
|
return;
|
|
262
|
-
//
|
|
263
|
-
//
|
|
263
|
+
// Terminal notifications always bypass the rate limit so clients reliably
|
|
264
|
+
// receive the final state even when updates arrive in quick succession.
|
|
265
|
+
const isTerminal = total !== undefined && current >= total;
|
|
264
266
|
const now = Date.now();
|
|
265
|
-
if (now - lastSentMs < PROGRESS_RATE_LIMIT_MS)
|
|
267
|
+
if (!isTerminal && now - lastSentMs < PROGRESS_RATE_LIMIT_MS)
|
|
266
268
|
return;
|
|
267
269
|
lastProgress = current;
|
|
268
270
|
lastSentMs = now;
|
|
@@ -283,11 +285,12 @@ async function withProgress(message, extra, run, getCompletionMessage) {
|
|
|
283
285
|
return run();
|
|
284
286
|
}
|
|
285
287
|
const total = 1;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
288
|
+
// Emit the start notification only when a progressToken is present; for
|
|
289
|
+
// task-only mode the task status is already 'working' — a zero-progress
|
|
290
|
+
// notification would add unnecessary overhead without client value.
|
|
291
|
+
if (canSendProgress(extra)) {
|
|
292
|
+
await reportProgress(extra, { current: 0, total, message });
|
|
293
|
+
}
|
|
291
294
|
try {
|
|
292
295
|
const result = await run();
|
|
293
296
|
const endMessage = getCompletionMessage?.(result) ?? message;
|
|
@@ -326,6 +329,15 @@ export function wrapToolHandler(handler, options) {
|
|
|
326
329
|
return maybeStripStructuredContentFromResult(result);
|
|
327
330
|
};
|
|
328
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Returns `pathValue` if non-empty; otherwise resolves to the single allowed
|
|
334
|
+
* directory from module-level state managed by `RootsManager`. Throws when the
|
|
335
|
+
* path is ambiguous (multiple roots) or when no roots are configured.
|
|
336
|
+
*
|
|
337
|
+
* NOTE: Depends on `getAllowedDirectories()` which reads module-level state
|
|
338
|
+
* updated by `RootsManager`. Ensure the server is initialized before calling.
|
|
339
|
+
* See `src/server/roots-manager.ts` for the update lifecycle.
|
|
340
|
+
*/
|
|
329
341
|
export function resolvePathOrRoot(pathValue) {
|
|
330
342
|
if (pathValue && pathValue.trim().length > 0)
|
|
331
343
|
return pathValue;
|
|
@@ -134,6 +134,10 @@ function normalizeCallToolResult(value) {
|
|
|
134
134
|
function getToolResultErrorCode(result) {
|
|
135
135
|
if (!isRecord(result) || result['isError'] !== true)
|
|
136
136
|
return undefined;
|
|
137
|
+
// First check for a dedicated errorCode property to avoid regex parsing of the content for structured error results produced by newer code.
|
|
138
|
+
if (typeof result['errorCode'] === 'string')
|
|
139
|
+
return result['errorCode'];
|
|
140
|
+
// Fallback to regex parsing of the human-readable error message for older error results that lack a structured errorCode property.
|
|
137
141
|
const { content } = result;
|
|
138
142
|
if (!Array.isArray(content) || content.length === 0)
|
|
139
143
|
return undefined;
|
|
@@ -293,6 +297,21 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName
|
|
|
293
297
|
}
|
|
294
298
|
catch (innerError) {
|
|
295
299
|
console.error(`Failed to store task failure result for task ${taskId}:`, innerError);
|
|
300
|
+
// If storing the failure result also fails, there's not much we can do. The task will remain in 'working' status until it expires, which is not ideal but at least prevents clients from receiving incorrect results or hanging indefinitely waiting for a result that will never arrive. We log the error to aid debugging, and we attempt to notify the client of the failure if possible, but we don't want to throw further errors that could crash the server or cause cascading failures.
|
|
301
|
+
const syntheticTask = {
|
|
302
|
+
taskId,
|
|
303
|
+
status: 'failed',
|
|
304
|
+
ttl: null,
|
|
305
|
+
createdAt: new Date().toISOString(),
|
|
306
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
307
|
+
};
|
|
308
|
+
const { sendNotification } = extra;
|
|
309
|
+
if (typeof sendNotification === 'function') {
|
|
310
|
+
void sendNotification({
|
|
311
|
+
method: TASK_STATUS_NOTIFICATION_METHOD,
|
|
312
|
+
params: buildTaskStatusNotificationParams(syntheticTask),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
296
315
|
}
|
|
297
316
|
}
|
|
298
317
|
}
|