@besales/mcp 1.0.0-beta.5 → 1.0.0-beta.9
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/README.md +41 -6
- package/dist/auth/oauth-client.d.ts +1 -1
- package/dist/auth/oauth-client.js +2 -0
- package/dist/auth/oauth-client.js.map +1 -1
- package/dist/http/api-client.d.ts +10 -0
- package/dist/http/api-client.js +32 -0
- package/dist/http/api-client.js.map +1 -1
- package/dist/resources/concepts/sandbox.md +8 -1
- package/dist/resources/concepts/triggers.md +1 -0
- package/dist/resources/docs/platforms.md +58 -1
- package/dist/resources/docs/trigger-actions.md +24 -0
- package/dist/resources/workflows/iterate-with-sandbox.md +6 -2
- package/dist/resources/workflows/platform-setup-from-scratch.md +27 -0
- package/dist/schemas/mcp-tools.json +904 -27
- package/dist/tools/definitions/agent-design.d.ts +2 -0
- package/dist/tools/definitions/agent-design.js +15 -4
- package/dist/tools/definitions/agent-design.js.map +1 -1
- package/dist/tools/definitions/agent-settings.d.ts +1 -0
- package/dist/tools/definitions/agent-settings.js +2 -0
- package/dist/tools/definitions/agent-settings.js.map +1 -1
- package/dist/tools/definitions/broadcast-analytics.js +1 -1
- package/dist/tools/definitions/broadcast-analytics.js.map +1 -1
- package/dist/tools/definitions/broadcast-campaigns.d.ts +29 -0
- package/dist/tools/definitions/broadcast-campaigns.js +107 -0
- package/dist/tools/definitions/broadcast-campaigns.js.map +1 -0
- package/dist/tools/definitions/dialogue-export.js +2 -0
- package/dist/tools/definitions/dialogue-export.js.map +1 -1
- package/dist/tools/definitions/follow-up.d.ts +1 -1
- package/dist/tools/definitions/platform-setup-wave.d.ts +1 -1
- package/dist/tools/definitions/platform-setup.d.ts +12 -1
- package/dist/tools/definitions/task-state.d.ts +1 -1
- package/dist/tools/definitions/telegram-outreach-calendar.d.ts +17 -0
- package/dist/tools/definitions/telegram-outreach-calendar.js +68 -0
- package/dist/tools/definitions/telegram-outreach-calendar.js.map +1 -0
- package/dist/tools/definitions/telegram-personal-connect.d.ts +12 -0
- package/dist/tools/definitions/telegram-personal-connect.js +61 -0
- package/dist/tools/definitions/telegram-personal-connect.js.map +1 -0
- package/dist/tools/definitions/telegram-personal-management.d.ts +1 -1
- package/dist/tools/definitions/telegram-personal-profile.d.ts +2 -0
- package/dist/tools/definitions/telegram-personal-profile.js +14 -0
- package/dist/tools/definitions/telegram-personal-profile.js.map +1 -1
- package/dist/tools/definitions/telegram-personal-proxies.d.ts +42 -0
- package/dist/tools/definitions/telegram-personal-proxies.js +204 -0
- package/dist/tools/definitions/telegram-personal-proxies.js.map +1 -0
- package/dist/tools/definitions/telegram-proxy-stock.d.ts +34 -0
- package/dist/tools/definitions/telegram-proxy-stock.js +129 -0
- package/dist/tools/definitions/telegram-proxy-stock.js.map +1 -0
- package/dist/tools/definitions/telegram-session-import.d.ts +5 -0
- package/dist/tools/definitions/telegram-session-import.js +33 -0
- package/dist/tools/definitions/telegram-session-import.js.map +1 -0
- package/dist/tools/definitions/trigger-action-item.schema.d.ts +10 -0
- package/dist/tools/definitions/trigger-action-item.schema.js +28 -11
- package/dist/tools/definitions/trigger-action-item.schema.js.map +1 -1
- package/dist/tools/definitions/trigger-upsert.tool.d.ts +1 -0
- package/dist/tools/definitions/trigger-upsert.tool.js +13 -0
- package/dist/tools/definitions/trigger-upsert.tool.js.map +1 -1
- package/dist/tools/definitions.d.ts +119 -9
- package/dist/tools/definitions.js +13 -0
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/operation-recovery.d.ts +1 -1
- package/dist/tools/operation-recovery.js +1 -1
- package/dist/tools/operation-recovery.js.map +1 -1
- package/dist/tools/registry-mapping.cases.js +210 -8
- package/dist/tools/registry-mapping.cases.js.map +1 -1
- package/dist/tools/registry.js +20 -7
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/result.d.ts +2 -1
- package/dist/tools/result.js +100 -27
- package/dist/tools/result.js.map +1 -1
- package/dist/types/api-contract.gen.d.ts +2220 -216
- package/docs/async-operation-lifecycle.md +13 -2
- package/docs/failure-semantics.md +15 -2
- package/docs/help-navigation.md +2 -0
- package/docs/secret-read-boundary.md +7 -0
- package/docs/telegram-personal-management.md +95 -24
- package/docs/telegram-personal-proxies.md +135 -0
- package/docs/telegram-session-import.md +53 -0
- package/package.json +8 -7
- package/scripts/body-idempotency-contract.mjs +95 -0
- package/scripts/generate-followup-session-contract.mjs +14 -7
- package/scripts/generated-output.mjs +9 -0
- package/scripts/sync-agent-model-schemas.mjs +11 -11
- package/scripts/sync-api-contracts.mjs +99 -40
- package/scripts/sync-crm-stage-pool-schema.mjs +6 -7
- package/scripts/sync-route-map.mjs +189 -35
- package/scripts/sync-sandbox-request-key.mjs +40 -0
- package/scripts/sync-telegram-profile-photo.mjs +57 -0
- package/scripts/sync-telegram-session-import.mjs +39 -0
- package/scripts/sync-tool-inventory.mjs +15 -7
- package/scripts/sync-trigger-argument-schema.mjs +31 -14
- package/scripts/sync-trigger-event-schema.mjs +11 -6
- package/scripts/sync-trigger-step-reference-schema.mjs +21 -0
- package/scripts/sync-wave-id-schemas.mjs +7 -6
- package/scripts/telegram-personal-profile-mutants.mjs +59 -18
- package/scripts/telegram-personal-proxy-mutants.mjs +171 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Узкое расширение отгруженной формы: прежние поля и ограничения остаются входом.
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import {
|
|
5
|
+
telegramPersonalProfileTools,
|
|
6
|
+
profilePhotoHelp,
|
|
7
|
+
} from '../src/tools/definitions/telegram-personal-profile.js';
|
|
8
|
+
import { mappingCases } from '../src/tools/registry-mapping.cases.js';
|
|
9
|
+
import { writeGeneratedFile } from './generated-output.mjs';
|
|
10
|
+
|
|
11
|
+
const name = 'besales_platform_telegram_personal_profile_bulk';
|
|
12
|
+
const definition = telegramPersonalProfileTools.find((tool) => tool.name === name);
|
|
13
|
+
const registryFile = new URL('../src/schemas/mcp-tools.json', import.meta.url);
|
|
14
|
+
const registry = JSON.parse(readFileSync(registryFile, 'utf8'));
|
|
15
|
+
const entry = registry.tools.find((tool) => tool.name === name);
|
|
16
|
+
if (!entry || !definition) throw new Error('TELEGRAM_PROFILE_TOOL_MISSING');
|
|
17
|
+
const derived = z.toJSONSchema(definition.schema, { io: 'input' });
|
|
18
|
+
entry.inputSchema.properties.stored_file_id = derived.properties.stored_file_id;
|
|
19
|
+
const changes = entry.inputSchema.allOf.find((rule) => rule.anyOf);
|
|
20
|
+
if (!changes) throw new Error('TELEGRAM_PROFILE_CHANGES_RULE_MISSING');
|
|
21
|
+
if (!changes.anyOf.some((rule) => rule.required?.includes('stored_file_id')))
|
|
22
|
+
changes.anyOf.push({ required: ['stored_file_id'] });
|
|
23
|
+
const old = 'Фото через этот инструмент не меняется — только в приложении.';
|
|
24
|
+
if (entry.description.includes(old))
|
|
25
|
+
entry.description = entry.description.replace(old, profilePhotoHelp);
|
|
26
|
+
else {
|
|
27
|
+
const start = entry.description.indexOf('Фото: передай stored_file_id');
|
|
28
|
+
const end = entry.description.indexOf(' Требует manage:channels.', start);
|
|
29
|
+
if (start < 0 || end <= start) throw new Error('TELEGRAM_PROFILE_DESCRIPTION_DRIFT');
|
|
30
|
+
entry.description =
|
|
31
|
+
entry.description.slice(0, start) + profilePhotoHelp + entry.description.slice(end);
|
|
32
|
+
}
|
|
33
|
+
writeGeneratedFile(registryFile, JSON.stringify(registry, null, 2) + '\n');
|
|
34
|
+
|
|
35
|
+
const routesFile = new URL('../src/schemas/mcp-v2-route-map.json', import.meta.url);
|
|
36
|
+
const routes = JSON.parse(readFileSync(routesFile, 'utf8'));
|
|
37
|
+
const variants = ['photo-selected', 'channel-face'];
|
|
38
|
+
const samples = mappingCases.filter((row) => row.name === name && row.input.stored_file_id);
|
|
39
|
+
if (samples.length !== variants.length) throw new Error('TELEGRAM_PROFILE_PHOTO_SAMPLES_MISSING');
|
|
40
|
+
for (const [index, sample] of samples.entries()) {
|
|
41
|
+
const request = definition.createRequest(definition.schema.parse(sample.input));
|
|
42
|
+
const row = {
|
|
43
|
+
tool: name + '@' + variants[index],
|
|
44
|
+
canonicalArgs: sample.input,
|
|
45
|
+
expected: {
|
|
46
|
+
method: request.method,
|
|
47
|
+
path: request.path,
|
|
48
|
+
pathTemplate: '/channels/:channelId/telegram-personal/profile/bulk',
|
|
49
|
+
body: request.body,
|
|
50
|
+
},
|
|
51
|
+
backendTarget: 'besales-api',
|
|
52
|
+
};
|
|
53
|
+
const previous = routes.findIndex((candidate) => candidate.tool === row.tool);
|
|
54
|
+
if (previous < 0) routes.push(row);
|
|
55
|
+
else routes[previous] = row;
|
|
56
|
+
}
|
|
57
|
+
writeGeneratedFile(routesFile, JSON.stringify(routes, null, 2) + '\n');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Реестр и опись выводятся из исполняемого определения, не из второй рукописной схемы.
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import {
|
|
5
|
+
telegramSessionImportDescription,
|
|
6
|
+
telegramSessionImportTools,
|
|
7
|
+
} from '../src/tools/definitions/telegram-session-import.js';
|
|
8
|
+
import { writeGeneratedFile } from './generated-output.mjs';
|
|
9
|
+
|
|
10
|
+
const registryFile = new URL('../src/schemas/mcp-tools.json', import.meta.url);
|
|
11
|
+
const registry = JSON.parse(readFileSync(registryFile, 'utf8'));
|
|
12
|
+
for (const definition of telegramSessionImportTools) {
|
|
13
|
+
const { $schema: _dialect, ...inputSchema } = z.toJSONSchema(definition.schema);
|
|
14
|
+
const entry = {
|
|
15
|
+
name: definition.name,
|
|
16
|
+
description: telegramSessionImportDescription,
|
|
17
|
+
inputSchema,
|
|
18
|
+
};
|
|
19
|
+
const index = registry.tools.findIndex((tool) => tool.name === entry.name);
|
|
20
|
+
if (index < 0) registry.tools.push(entry);
|
|
21
|
+
else registry.tools[index] = entry;
|
|
22
|
+
}
|
|
23
|
+
writeGeneratedFile(registryFile, JSON.stringify(registry, null, 2) + '\n');
|
|
24
|
+
|
|
25
|
+
const inventoryFile = new URL(
|
|
26
|
+
'../src/tools/__fixtures__/secret-read-inventory-additions.json',
|
|
27
|
+
import.meta.url,
|
|
28
|
+
);
|
|
29
|
+
const inventory = JSON.parse(readFileSync(inventoryFile, 'utf8'));
|
|
30
|
+
const reader = 'src/tools/definitions/telegram-session-import.ts';
|
|
31
|
+
const group = {
|
|
32
|
+
reader,
|
|
33
|
+
domain: 'channels-setup',
|
|
34
|
+
tools: telegramSessionImportTools.map((tool) => tool.name),
|
|
35
|
+
};
|
|
36
|
+
const index = inventory.groups.findIndex((entry) => entry.reader === reader);
|
|
37
|
+
if (index < 0) inventory.groups.push(group);
|
|
38
|
+
else inventory.groups[index] = group;
|
|
39
|
+
writeGeneratedFile(inventoryFile, JSON.stringify(inventory, null, 2) + '\n');
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeGeneratedFile } from './generated-output.mjs';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import ts from 'typescript';
|
|
3
4
|
|
|
4
5
|
const source = readFileSync(new URL('../src/tools/inventory.ts', import.meta.url), 'utf8');
|
|
5
6
|
const compiled = ts.transpileModule(source, {
|
|
6
|
-
compilerOptions: {
|
|
7
|
+
compilerOptions: {
|
|
8
|
+
module: ts.ModuleKind.ESNext,
|
|
9
|
+
target: ts.ScriptTarget.ES2022,
|
|
10
|
+
},
|
|
7
11
|
}).outputText;
|
|
8
12
|
const { renderToolInventoryMarkdown, toolInventoryCounts } = await import(
|
|
9
13
|
'data:text/javascript;base64,' + Buffer.from(compiled).toString('base64')
|
|
@@ -13,15 +17,19 @@ const schema = JSON.parse(readFileSync(schemaUrl, 'utf8'));
|
|
|
13
17
|
const names = schema.tools.filter((tool) => tool.status !== 'removed').map((tool) => tool.name);
|
|
14
18
|
schema.summary.total_tools = names.length;
|
|
15
19
|
schema.summary.tools_by_category = toolInventoryCounts(names);
|
|
16
|
-
|
|
20
|
+
writeGeneratedFile(schemaUrl, JSON.stringify(schema, null, 2) + '\n');
|
|
17
21
|
const readmeUrl = new URL('../README.md', import.meta.url);
|
|
18
22
|
const readme = readFileSync(readmeUrl, 'utf8');
|
|
19
23
|
const begin = '<!-- BEGIN GENERATED TOOL INVENTORY -->';
|
|
20
24
|
const end = '<!-- END GENERATED TOOL INVENTORY -->';
|
|
21
|
-
const first = readme.indexOf(begin),
|
|
25
|
+
const first = readme.indexOf(begin),
|
|
26
|
+
last = readme.indexOf(end);
|
|
22
27
|
if (first < 0 || last < first) throw new Error('README inventory markers missing');
|
|
23
|
-
|
|
28
|
+
writeGeneratedFile(
|
|
24
29
|
readmeUrl,
|
|
25
|
-
readme.slice(0, first + begin.length) +
|
|
26
|
-
|
|
30
|
+
readme.slice(0, first + begin.length) +
|
|
31
|
+
'\n\n' +
|
|
32
|
+
renderToolInventoryMarkdown(names) +
|
|
33
|
+
'\n\n' +
|
|
34
|
+
readme.slice(last),
|
|
27
35
|
);
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeGeneratedFile } from './generated-output.mjs';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { resolve } from 'node:path';
|
|
3
4
|
import { pathToFileURL } from 'node:url';
|
|
4
5
|
import openapiTS from 'openapi-typescript';
|
|
5
6
|
import { z } from 'zod';
|
|
6
|
-
import { linkTrackingTools } from '../
|
|
7
|
-
import { triggerActionArgumentSchema } from '../
|
|
8
|
-
import { toolDefinitions } from '../
|
|
9
|
-
import { triggerActionItemSchema } from '../
|
|
10
|
-
import { canonicalCallOperatorTaskSchema } from '../
|
|
7
|
+
import { linkTrackingTools } from '../src/tools/definitions/link-tracking.js';
|
|
8
|
+
import { triggerActionArgumentSchema } from '../src/tools/definitions/trigger-argument.schema.js';
|
|
9
|
+
import { toolDefinitions } from '../src/tools/definitions.js';
|
|
10
|
+
import { triggerActionItemSchema } from '../src/tools/definitions/platform-setup.js';
|
|
11
|
+
import { canonicalCallOperatorTaskSchema } from '../src/tools/definitions/call-operator-task.schema.js';
|
|
12
|
+
import { stagePoolFields } from '../src/tools/definitions/crm-stage-pool.schema.js';
|
|
11
13
|
import { syncCallOperatorTaskSchema } from './sync-trigger-task-schema.mjs';
|
|
12
14
|
|
|
13
15
|
// Границы опубликованных полей — из контракта выбранного API checkout, не из ручного JSON.
|
|
@@ -54,7 +56,10 @@ for (const [name, field] of [
|
|
|
54
56
|
delete published.$schema;
|
|
55
57
|
tool.inputSchema.properties[field] = published;
|
|
56
58
|
}
|
|
57
|
-
const argument = z.toJSONSchema(triggerActionArgumentSchema, {
|
|
59
|
+
const argument = z.toJSONSchema(triggerActionArgumentSchema, {
|
|
60
|
+
target: 'draft-7',
|
|
61
|
+
io: 'input',
|
|
62
|
+
});
|
|
58
63
|
delete argument.$schema;
|
|
59
64
|
for (const name of ['besales_trigger_upsert']) {
|
|
60
65
|
const tool = registry.tools.find((entry) => entry.name === name);
|
|
@@ -65,14 +70,25 @@ for (const name of ['besales_trigger_upsert']) {
|
|
|
65
70
|
throw new Error(`TRIGGER_ACTION_SCHEMA_MISSING:${name}`);
|
|
66
71
|
const publishedAction = tool.inputSchema.properties.actions.items;
|
|
67
72
|
publishedAction.properties.callOperatorConfig = action.properties.callOperatorConfig;
|
|
68
|
-
|
|
73
|
+
const poolProperties = Object.fromEntries(
|
|
74
|
+
[...Object.keys(stagePoolFields), 'extra']
|
|
75
|
+
.filter((field) => publishedAction.properties.config.properties[field] !== undefined)
|
|
76
|
+
.map((field) => [field, publishedAction.properties.config.properties[field]]),
|
|
77
|
+
);
|
|
78
|
+
publishedAction.properties.config = {
|
|
79
|
+
...action.properties.config,
|
|
80
|
+
properties: { ...action.properties.config.properties, ...poolProperties },
|
|
81
|
+
};
|
|
69
82
|
}
|
|
70
83
|
// Поля каталога публикуются из исполняемой схемы: null очистки и refine входят в носитель.
|
|
71
84
|
for (const name of ['besales_tracked_link_create', 'besales_tracked_link_update']) {
|
|
72
85
|
const definition = linkTrackingTools.find((entry) => entry.name === name);
|
|
73
86
|
const tool = registry.tools.find((entry) => entry.name === name);
|
|
74
87
|
if (!definition || !tool) throw new Error(`LINK_CATALOG_SCHEMA_MISSING:${name}`);
|
|
75
|
-
const derived = z.toJSONSchema(definition.schema, {
|
|
88
|
+
const derived = z.toJSONSchema(definition.schema, {
|
|
89
|
+
target: 'draft-7',
|
|
90
|
+
io: 'input',
|
|
91
|
+
});
|
|
76
92
|
delete derived.$schema;
|
|
77
93
|
if (!derived.properties?.personal_url_variable)
|
|
78
94
|
throw new Error(`PERSONAL_URL_SCHEMA_MISSING:${name}`);
|
|
@@ -86,12 +102,13 @@ for (const name of ['besales_tracked_link_create', 'besales_tracked_link_update'
|
|
|
86
102
|
throw new Error(`PERSONAL_URL_NULLABLE_SCHEMA_UNEXPECTED:${name}`);
|
|
87
103
|
}
|
|
88
104
|
// Отгруженная форма сохраняет ограничения строки и отдельное разрешение очистки через null.
|
|
89
|
-
personalUrlVariable = {
|
|
105
|
+
personalUrlVariable = {
|
|
106
|
+
...anyOf[0],
|
|
107
|
+
...metadata,
|
|
108
|
+
type: ['string', 'null'],
|
|
109
|
+
};
|
|
90
110
|
}
|
|
91
111
|
tool.inputSchema.properties.personal_url_variable = personalUrlVariable;
|
|
92
112
|
}
|
|
93
113
|
const serialized = `${JSON.stringify(registry, null, 2)}\n`;
|
|
94
|
-
|
|
95
|
-
// Уже собранный пакет и публикуемый исходник обязаны рекламировать одинаковые ограничения.
|
|
96
|
-
mkdirSync(new URL('../dist/schemas/', import.meta.url), { recursive: true });
|
|
97
|
-
writeFileSync(new URL('../dist/schemas/mcp-tools.json', import.meta.url), serialized);
|
|
114
|
+
writeGeneratedFile(target, serialized);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { writeGeneratedFile } from './generated-output.mjs';
|
|
1
2
|
// Точечный вариант sync-api-contracts: только схема и описание событийного триггера.
|
|
2
3
|
// Сохраняет ручные ограничения и все прочие инструменты; типы и маршруты уже актуальны.
|
|
3
|
-
import { readFileSync
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
import { toolDefinitions } from '../src/tools/definitions.ts';
|
|
6
7
|
import { eventTriggerDescription } from '../src/tools/definitions/trigger-upsert.tool.ts';
|
|
@@ -9,12 +10,16 @@ import { syncSchemaDescriptions } from './sync-schema-descriptions.mjs';
|
|
|
9
10
|
const registryPath = new URL('../src/schemas/mcp-tools.json', import.meta.url);
|
|
10
11
|
const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
11
12
|
const name = 'besales_trigger_upsert';
|
|
12
|
-
const definition = toolDefinitions.find(tool => tool.name === name);
|
|
13
|
-
const tool = registry.tools.find(tool => tool.name === name);
|
|
13
|
+
const definition = toolDefinitions.find((tool) => tool.name === name);
|
|
14
|
+
const tool = registry.tools.find((tool) => tool.name === name);
|
|
14
15
|
if (!definition || !tool) throw new Error(`REGISTRY_TOOL_MISSING:${name}`);
|
|
15
|
-
const derived = z.toJSONSchema(definition.schema, {
|
|
16
|
+
const derived = z.toJSONSchema(definition.schema, {
|
|
17
|
+
io: 'input',
|
|
18
|
+
unrepresentable: 'any',
|
|
19
|
+
});
|
|
16
20
|
if (!derived.properties?.trigger) throw new Error('EVENT_TRIGGER_SCHEMA_MISSING');
|
|
17
21
|
tool.inputSchema.properties.trigger = derived.properties.trigger;
|
|
18
22
|
syncSchemaDescriptions(tool.inputSchema, derived);
|
|
19
|
-
if (!tool.description.includes(eventTriggerDescription))
|
|
20
|
-
|
|
23
|
+
if (!tool.description.includes(eventTriggerDescription))
|
|
24
|
+
tool.description += ' ' + eventTriggerDescription;
|
|
25
|
+
writeGeneratedFile(registryPath, JSON.stringify(registry, null, 2) + '\n');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { writeGeneratedFile } from './generated-output.mjs';
|
|
2
|
+
// Пересобирает только вход ссылок, сохраняя ручные ограничения остальных полей реестра.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { triggerSetupActionItemSchema } from '../src/tools/definitions/trigger-action-item.schema.ts';
|
|
6
|
+
import { editOneStepDescription } from '../src/tools/definitions/trigger-upsert.tool.ts';
|
|
7
|
+
|
|
8
|
+
const target = new URL('../src/schemas/mcp-tools.json', import.meta.url);
|
|
9
|
+
const registry = JSON.parse(readFileSync(target, 'utf8'));
|
|
10
|
+
const tool = registry.tools.find((item) => item.name === 'besales_trigger_upsert');
|
|
11
|
+
const derived = z.toJSONSchema(triggerSetupActionItemSchema, {
|
|
12
|
+
io: 'input',
|
|
13
|
+
unrepresentable: 'any',
|
|
14
|
+
});
|
|
15
|
+
if (!tool?.inputSchema.properties.actions.items.properties || !derived.properties?.stepId)
|
|
16
|
+
throw new Error('TRIGGER_STEP_REFERENCE_SCHEMA_MISSING');
|
|
17
|
+
for (const field of ['stepId', 'onError', 'stopOnSuccess'])
|
|
18
|
+
tool.inputSchema.properties.actions.items.properties[field] = derived.properties[field];
|
|
19
|
+
if (!tool.description.includes(editOneStepDescription))
|
|
20
|
+
tool.description += ' ' + editOneStepDescription;
|
|
21
|
+
writeGeneratedFile(target, JSON.stringify(registry, null, 2) + '\n');
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeGeneratedFile } from './generated-output.mjs';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
|
|
4
|
-
import { platformSetupWaveTools } from '../
|
|
5
|
+
import { platformSetupWaveTools } from '../src/tools/definitions/platform-setup-wave.js';
|
|
5
6
|
|
|
6
7
|
// Ограничения берутся из исполняемых схем; подписи объясняют номера без имён таблиц.
|
|
7
8
|
const numericPaths = {
|
|
@@ -46,10 +47,10 @@ for (const [name, paths] of Object.entries(numericPaths)) {
|
|
|
46
47
|
throw new Error(`WAVE_ID_SCHEMA_MISSING:${name}:${path.join('.')}`);
|
|
47
48
|
}
|
|
48
49
|
for (const key of Object.keys(published)) delete published[key];
|
|
49
|
-
Object.assign(published, executable, {
|
|
50
|
+
Object.assign(published, executable, {
|
|
51
|
+
description: descriptions[path.at(-1)],
|
|
52
|
+
});
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
const serialized = `${JSON.stringify(registry, null, 2)}\n`;
|
|
53
|
-
|
|
54
|
-
mkdirSync(new URL('../dist/schemas/', import.meta.url), { recursive: true });
|
|
55
|
-
writeFileSync(new URL('../dist/schemas/mcp-tools.json', import.meta.url), serialized);
|
|
56
|
+
writeGeneratedFile(target, serialized);
|
|
@@ -9,11 +9,19 @@ const published = 'src/schemas/mcp-tools.json';
|
|
|
9
9
|
const temporary = mkdtempSync(join(tmpdir(), 'telegram-profile-mutants-'));
|
|
10
10
|
function run(pattern, name) {
|
|
11
11
|
const output = join(temporary, name + '.json');
|
|
12
|
-
const result = spawnSync(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const result = spawnSync(
|
|
13
|
+
process.execPath,
|
|
14
|
+
[
|
|
15
|
+
'node_modules/vitest/vitest.mjs',
|
|
16
|
+
'run',
|
|
17
|
+
suite,
|
|
18
|
+
'--maxWorkers=1',
|
|
19
|
+
'--reporter=json',
|
|
20
|
+
'--outputFile=' + output,
|
|
21
|
+
...(pattern ? ['--testNamePattern', pattern] : []),
|
|
22
|
+
],
|
|
23
|
+
{ encoding: 'utf8', timeout: 180000 },
|
|
24
|
+
);
|
|
17
25
|
const report = JSON.parse(readFileSync(output, 'utf8'));
|
|
18
26
|
return { result, report };
|
|
19
27
|
}
|
|
@@ -29,6 +37,28 @@ try {
|
|
|
29
37
|
}
|
|
30
38
|
console.log('BASELINE PASS ' + baseline.report.numPassedTests);
|
|
31
39
|
for (const entry of [
|
|
40
|
+
{
|
|
41
|
+
name: 'photo-mapping-removed',
|
|
42
|
+
file: tool,
|
|
43
|
+
pattern: 'одно фото из stored_file_id уходит выбранным номерам',
|
|
44
|
+
mutate: (text) => replaceOnce(text, ' avatar: input.stored_file_id,\n', ''),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'photo-not-counted-as-change-published',
|
|
48
|
+
file: published,
|
|
49
|
+
pattern: 'одно фото из stored_file_id уходит выбранным номерам',
|
|
50
|
+
mutate: (text) => {
|
|
51
|
+
const registry = JSON.parse(text);
|
|
52
|
+
const schema = registry.tools.find(
|
|
53
|
+
(item) => item.name === 'besales_platform_telegram_personal_profile_bulk',
|
|
54
|
+
).inputSchema;
|
|
55
|
+
const rule = schema.allOf.find((item) => item.anyOf);
|
|
56
|
+
const before = rule.anyOf.length;
|
|
57
|
+
rule.anyOf = rule.anyOf.filter((item) => !item.required?.includes('stored_file_id'));
|
|
58
|
+
if (rule.anyOf.length !== before - 1) throw new Error('MUTATION_ANCHOR_NOT_UNIQUE');
|
|
59
|
+
return JSON.stringify(registry, null, 2) + '\n';
|
|
60
|
+
},
|
|
61
|
+
},
|
|
32
62
|
{
|
|
33
63
|
// Главный мутант пачки: оба режима ников разрешены одновременно в Zod-схеме инструмента.
|
|
34
64
|
name: 'username-modes-both-allowed-runtime',
|
|
@@ -53,7 +83,11 @@ try {
|
|
|
53
83
|
).inputSchema;
|
|
54
84
|
const before = schema.allOf.length;
|
|
55
85
|
schema.allOf = schema.allOf.filter(
|
|
56
|
-
(rule) =>
|
|
86
|
+
(rule) =>
|
|
87
|
+
!(
|
|
88
|
+
rule.not?.required?.includes('usernames') &&
|
|
89
|
+
rule.not.required.includes('username_base')
|
|
90
|
+
),
|
|
57
91
|
);
|
|
58
92
|
if (schema.allOf.length !== before - 1) throw new Error('MUTATION_ANCHOR_NOT_UNIQUE');
|
|
59
93
|
return JSON.stringify(registry, null, 2) + '\n';
|
|
@@ -64,8 +98,7 @@ try {
|
|
|
64
98
|
name: 'idempotency-key-default-header',
|
|
65
99
|
file: tool,
|
|
66
100
|
pattern: 'идёт заголовком x-idempotency-key',
|
|
67
|
-
mutate: (text) =>
|
|
68
|
-
replaceOnce(text, " idempotencyKeyHeader: 'X-Idempotency-Key',\n", ''),
|
|
101
|
+
mutate: (text) => replaceOnce(text, " idempotencyKeyHeader: 'X-Idempotency-Key',\n", ''),
|
|
69
102
|
},
|
|
70
103
|
]) {
|
|
71
104
|
const original = readFileSync(entry.file, 'utf8');
|
|
@@ -75,18 +108,26 @@ try {
|
|
|
75
108
|
// Убитым считается только провал утверждения внутри названного теста: AssertionError
|
|
76
109
|
// или отказ vitest expect(...).rejects («promise resolved … instead of rejecting»).
|
|
77
110
|
// Ошибка загрузки набора не даёт assertionResults и остаётся INFRA_FAILURE.
|
|
78
|
-
const failures = report.testResults
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
111
|
+
const failures = report.testResults
|
|
112
|
+
.flatMap((item) => item.assertionResults)
|
|
113
|
+
.filter(
|
|
114
|
+
(test) =>
|
|
115
|
+
test.status === 'failed' &&
|
|
116
|
+
test.fullName.includes(entry.pattern) &&
|
|
117
|
+
test.failureMessages.some(
|
|
118
|
+
(message) =>
|
|
119
|
+
message.includes('AssertionError') ||
|
|
120
|
+
/^Error: promise resolved .* instead of rejecting/su.test(message),
|
|
121
|
+
),
|
|
122
|
+
);
|
|
86
123
|
if (result.status === 0 || result.signal || failures.length === 0) {
|
|
87
|
-
throw new Error(
|
|
124
|
+
throw new Error(
|
|
125
|
+
'SURVIVED_OR_INFRA_FAILURE:' + entry.name + '\n' + result.stdout + result.stderr,
|
|
126
|
+
);
|
|
88
127
|
}
|
|
89
|
-
console.log(
|
|
128
|
+
console.log(
|
|
129
|
+
'KILLED ' + entry.name + ' ' + JSON.stringify(failures.map((test) => test.fullName)),
|
|
130
|
+
);
|
|
90
131
|
} finally {
|
|
91
132
|
writeFileSync(entry.file, original);
|
|
92
133
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const suite = 'src/tools/definitions/telegram-personal-proxies.spec.ts';
|
|
7
|
+
const tool = 'src/tools/definitions/telegram-personal-proxies.ts';
|
|
8
|
+
const temporary = mkdtempSync(join(tmpdir(), 'telegram-proxy-mutants-'));
|
|
9
|
+
function run(pattern, name, suitePath = suite) {
|
|
10
|
+
const output = join(temporary, name + '.json');
|
|
11
|
+
const result = spawnSync(process.execPath, [
|
|
12
|
+
'node_modules/vitest/vitest.mjs', 'run', suitePath,
|
|
13
|
+
'--maxWorkers=1', '--reporter=json', '--outputFile=' + output,
|
|
14
|
+
...(pattern ? ['--testNamePattern', pattern] : []),
|
|
15
|
+
], { encoding: 'utf8', timeout: 180000 });
|
|
16
|
+
const report = JSON.parse(readFileSync(output, 'utf8'));
|
|
17
|
+
return { result, report };
|
|
18
|
+
}
|
|
19
|
+
function replaceOnce(text, anchor, replacement) {
|
|
20
|
+
if (text.split(anchor).length !== 2) throw new Error('MUTATION_ANCHOR_NOT_UNIQUE');
|
|
21
|
+
return text.replace(anchor, replacement);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const baseline = run(undefined, 'baseline');
|
|
25
|
+
if (baseline.result.status !== 0 || baseline.report.numPassedTests === 0) {
|
|
26
|
+
throw new Error('BASELINE_FAILED\n' + baseline.result.stdout + baseline.result.stderr);
|
|
27
|
+
}
|
|
28
|
+
console.log('BASELINE PASS ' + baseline.report.numPassedTests);
|
|
29
|
+
for (const [suffix, description] of [
|
|
30
|
+
['proxy_delete', 'Явное согласие человека на удаление прокси.'],
|
|
31
|
+
['account_proxy_change', 'Явное согласие человека: номер переподключится.'],
|
|
32
|
+
]) {
|
|
33
|
+
for (const target of ['runtime', 'published']) {
|
|
34
|
+
const file = target === 'runtime' ? tool : 'src/schemas/mcp-tools.json';
|
|
35
|
+
const name = suffix + '-without-confirmation-' + target;
|
|
36
|
+
const pattern = suffix + ': без явного true отказ до HTTP';
|
|
37
|
+
const original = readFileSync(file, 'utf8');
|
|
38
|
+
try {
|
|
39
|
+
let mutated;
|
|
40
|
+
if (target === 'runtime') {
|
|
41
|
+
const anchor = `explicit_confirmation: z.literal(true).describe('${description}')`;
|
|
42
|
+
mutated = replaceOnce(original, anchor,
|
|
43
|
+
`explicit_confirmation: z.literal(true).optional().describe('${description}')`);
|
|
44
|
+
} else {
|
|
45
|
+
const registry = JSON.parse(original);
|
|
46
|
+
const schema = registry.tools.find((entry) =>
|
|
47
|
+
entry.name === 'besales_platform_telegram_personal_' + suffix,
|
|
48
|
+
).inputSchema;
|
|
49
|
+
if (!schema.required.includes('explicit_confirmation')) throw new Error('MUTATION_ANCHOR_NOT_UNIQUE');
|
|
50
|
+
schema.required = schema.required.filter((field) => field !== 'explicit_confirmation');
|
|
51
|
+
mutated = JSON.stringify(registry, null, 2) + '\n';
|
|
52
|
+
}
|
|
53
|
+
writeFileSync(file, mutated);
|
|
54
|
+
const { result, report } = run(pattern, name);
|
|
55
|
+
// Убит только утверждением именованного теста; ошибка загрузки — INFRA_FAILURE.
|
|
56
|
+
const failures = report.testResults.flatMap((entry) => entry.assertionResults).filter((test) =>
|
|
57
|
+
test.status === 'failed' && test.fullName.includes(pattern) &&
|
|
58
|
+
test.failureMessages.some((message) =>
|
|
59
|
+
message.includes('AssertionError') ||
|
|
60
|
+
/^Error: promise resolved .* instead of rejecting/su.test(message),
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
if (result.status === 0 || result.signal || failures.length === 0) {
|
|
64
|
+
throw new Error('SURVIVED_OR_INFRA_FAILURE:' + name + '\n' + result.stdout + result.stderr);
|
|
65
|
+
}
|
|
66
|
+
console.log('KILLED ' + name + ' ' + JSON.stringify(failures.map((test) => test.fullName)));
|
|
67
|
+
} finally {
|
|
68
|
+
writeFileSync(file, original);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const stockSuite = 'src/tools/definitions/telegram-proxy-stock.spec.ts';
|
|
73
|
+
const stockFile = 'src/tools/definitions/telegram-proxy-stock.ts';
|
|
74
|
+
const connectFile = 'src/tools/definitions/telegram-personal-connect.ts';
|
|
75
|
+
const apiClientFile = 'src/http/api-client.ts';
|
|
76
|
+
const resultFile = 'src/tools/result.ts';
|
|
77
|
+
const publishedFile = 'src/schemas/mcp-tools.json';
|
|
78
|
+
const stockBaseline = run(undefined, 'stock-baseline', stockSuite);
|
|
79
|
+
if (stockBaseline.result.status !== 0 || stockBaseline.report.numPassedTests === 0) {
|
|
80
|
+
throw new Error('BASELINE_FAILED\n' + stockBaseline.result.stdout + stockBaseline.result.stderr);
|
|
81
|
+
}
|
|
82
|
+
console.log('STOCK BASELINE PASS ' + stockBaseline.report.numPassedTests);
|
|
83
|
+
function publishedMutation(suffix, change) {
|
|
84
|
+
return (original) => {
|
|
85
|
+
const registry = JSON.parse(original);
|
|
86
|
+
const schema = registry.tools.find((entry) => entry.name === 'besales_platform_telegram_personal_' + suffix).inputSchema;
|
|
87
|
+
change(schema);
|
|
88
|
+
return JSON.stringify(registry, null, 2) + '\n';
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function inTool(original, suffix, anchor, replacement) {
|
|
92
|
+
const start = original.indexOf("name: 'besales_platform_telegram_personal_" + suffix + "'");
|
|
93
|
+
if (start < 0) throw new Error('MUTATION_ANCHOR_NOT_UNIQUE');
|
|
94
|
+
const next = original.indexOf(' defineTool({', start);
|
|
95
|
+
const end = next < 0 ? original.length : next;
|
|
96
|
+
return original.slice(0, start) + replaceOnce(original.slice(start, end), anchor, replacement) + original.slice(end);
|
|
97
|
+
}
|
|
98
|
+
const mutations = [
|
|
99
|
+
['stock-loose-schema', stockFile, 'proxy_stock_import_link: строгая схема отвергает секреты до HTTP',
|
|
100
|
+
(original) => inTool(original, 'proxy_stock_import_link', 'schema: z.object(workspace).strict()', 'schema: z.object(workspace).passthrough()')],
|
|
101
|
+
['stock-delete-without-confirmation-runtime', stockFile, 'proxy_stock_delete: без явного true отказ до HTTP',
|
|
102
|
+
(original) => replaceOnce(original, "explicit_confirmation: z.literal(true).describe('Явное согласие человека на удаление прокси из запаса.')", "explicit_confirmation: z.literal(true).optional().describe('Явное согласие человека на удаление прокси из запаса.')")],
|
|
103
|
+
['stock-delete-without-confirmation-published', publishedFile, 'proxy_stock_delete: без явного true отказ до HTTP',
|
|
104
|
+
publishedMutation('proxy_stock_delete', (schema) => { schema.required = schema.required.filter((field) => field !== 'explicit_confirmation'); })],
|
|
105
|
+
...['proxy_take_from_stock', 'proxy_return_to_stock'].map((suffix) => [
|
|
106
|
+
suffix + '-changed-key', tool, suffix + ': повтор сохраняет настоящий заголовок и тело',
|
|
107
|
+
(original) => inTool(original, suffix, 'idempotencyKey: input.action_id', "idempotencyKey: '00000000-0000-4000-8000-000000000099'"),
|
|
108
|
+
]),
|
|
109
|
+
...['proxy_take_from_stock', 'proxy_return_to_stock'].map((suffix) => [
|
|
110
|
+
suffix + '-wrong-key-header', tool, suffix + ': повтор сохраняет настоящий заголовок и тело',
|
|
111
|
+
(original) => inTool(original, suffix, "idempotencyKeyHeader: 'X-Idempotency-Key'", "idempotencyKeyHeader: 'Idempotency-Key'"),
|
|
112
|
+
]),
|
|
113
|
+
['stock-details-at-top-level', apiClientFile, 'PROXY_STOCK_NOT_ENOUGH: доступность из details видна в тексте',
|
|
114
|
+
(original) => replaceOnce(original, "...proxyStockErrorDetails(asRecord(source?.['details']), code)", '...proxyStockErrorDetails(source, code)')],
|
|
115
|
+
['stock-available-dropped', apiClientFile, 'PROXY_STOCK_NOT_ENOUGH: доступность из details видна в тексте',
|
|
116
|
+
(original) => replaceOnce(original, '? { available } : {}', '? {} : {}')],
|
|
117
|
+
['stock-items-dropped', apiClientFile, 'PROXY_STOCK_REFUSED: причины из details видны по каждому id',
|
|
118
|
+
(original) => replaceOnce(original, 'return { items };', 'return {};')],
|
|
119
|
+
['stock-unknown-reason-exposed', apiClientFile, 'PROXY_STOCK_REFUSED: причины из details видны по каждому id',
|
|
120
|
+
(original) => replaceOnce(original, "['NOT_FOUND', 'NOT_READY', 'BUSY', 'ADDRESS_IN_USE'].includes(reason)", 'true')],
|
|
121
|
+
['stock-visible-reasons-dropped', resultFile, 'PROXY_STOCK_REFUSED: причины из details видны по каждому id',
|
|
122
|
+
(original) => replaceOnce(original, 'for (const item of details.items ?? []) parts.push(`${item.proxyId}: ${item.reason}.`);', '')],
|
|
123
|
+
['stock-requested-dropped', apiClientFile, 'отказы подключения: available, requested и files читаются из details закрытым набором',
|
|
124
|
+
(original) => replaceOnce(original, '? { requested } : {}', '? {} : {}')],
|
|
125
|
+
['stock-files-dropped', apiClientFile, 'отказы подключения: available, requested и files читаются из details закрытым набором',
|
|
126
|
+
(original) => replaceOnce(original, 'return { files };', 'return {};')],
|
|
127
|
+
['stock-route-removed', 'src/schemas/mcp-v2-route-map.json', 'содержит ровно десять новых маршрутов',
|
|
128
|
+
(original) => JSON.stringify(JSON.parse(original).filter((entry) => entry.tool !== 'besales_platform_telegram_personal_connect_link'), null, 2) + '\n'],
|
|
129
|
+
['stock-link-truncatable', stockFile, 'proxy_stock_import_link: ссылка neverTruncate',
|
|
130
|
+
(original) => replaceOnce(original, 'const resultPolicy = { neverTruncate: true, maxResultBytes: 16 * 1024 }', 'const resultPolicy = { neverTruncate: false, maxResultBytes: 16 * 1024 }')],
|
|
131
|
+
['connect-link-truncatable', connectFile, 'connect_link: ссылка neverTruncate',
|
|
132
|
+
(original) => replaceOnce(original, 'resultPolicy: { neverTruncate: true, maxResultBytes: 16 * 1024 }', 'resultPolicy: { neverTruncate: false, maxResultBytes: 16 * 1024 }')],
|
|
133
|
+
['plan-two-forms-runtime', connectFile, 'план: строгая форма',
|
|
134
|
+
(original) => replaceOnce(original, "z.object({ country_code: z.string().regex(/^[A-Z]{2}$/u) }).strict()", "z.object({ country_code: z.string().regex(/^[A-Z]{2}$/u) }).passthrough()")],
|
|
135
|
+
['plan-two-forms-published', publishedFile, 'план: строгая форма',
|
|
136
|
+
publishedMutation('connect_link', (schema) => {
|
|
137
|
+
const plan = schema.properties.proxy_plan;
|
|
138
|
+
plan.anyOf = plan.oneOf;
|
|
139
|
+
delete plan.oneOf;
|
|
140
|
+
for (const form of plan.anyOf) delete form.additionalProperties;
|
|
141
|
+
})],
|
|
142
|
+
['plan-without-channel-runtime', connectFile, 'план: строгая форма',
|
|
143
|
+
(original) => replaceOnce(original, 'channel_id: uuidSchema,', 'channel_id: uuidSchema.optional(),')],
|
|
144
|
+
['plan-without-channel-published', publishedFile, 'план: строгая форма',
|
|
145
|
+
publishedMutation('connect_link', (schema) => { schema.required = []; })],
|
|
146
|
+
['take-without-selection-runtime', tool, 'take: ровно одна форма',
|
|
147
|
+
(original) => replaceOnce(original, ': input.count === undefined)', ': false)')],
|
|
148
|
+
['take-without-selection-published', publishedFile, 'take: ровно одна форма',
|
|
149
|
+
publishedMutation('proxy_take_from_stock', (schema) => { delete schema.oneOf; })],
|
|
150
|
+
];
|
|
151
|
+
for (const [name, file, pattern, mutate] of mutations) {
|
|
152
|
+
const original = readFileSync(file, 'utf8');
|
|
153
|
+
try {
|
|
154
|
+
writeFileSync(file, mutate(original));
|
|
155
|
+
const { result, report } = run(pattern, name, stockSuite);
|
|
156
|
+
const failures = report.testResults.flatMap((entry) => entry.assertionResults).filter((test) =>
|
|
157
|
+
test.status === 'failed' && test.fullName.includes(pattern) &&
|
|
158
|
+
test.failureMessages.some((message) => message.includes('AssertionError') || /^Error: promise resolved .* instead of rejecting/su.test(message)),
|
|
159
|
+
);
|
|
160
|
+
if (result.status === 0 || result.signal || failures.length === 0) {
|
|
161
|
+
throw new Error('SURVIVED_OR_INFRA_FAILURE:' + name + '\n' + result.stdout + result.stderr);
|
|
162
|
+
}
|
|
163
|
+
console.log('KILLED ' + name + ' ' + JSON.stringify(failures.map((test) => test.fullName)));
|
|
164
|
+
} finally {
|
|
165
|
+
writeFileSync(file, original);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
} finally {
|
|
170
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
171
|
+
}
|