@enconvo/dxt 0.2.6
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/LICENSE.md +7 -0
- package/README.md +118 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.js +4 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +275 -0
- package/dist/cli/init.d.ts +185 -0
- package/dist/cli/init.js +704 -0
- package/dist/cli/pack.d.ts +7 -0
- package/dist/cli/pack.js +194 -0
- package/dist/cli/unpack.d.ts +7 -0
- package/dist/cli/unpack.js +101 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +11 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/node/files.d.ts +20 -0
- package/dist/node/files.js +115 -0
- package/dist/node/sign.d.ts +32 -0
- package/dist/node/sign.js +333 -0
- package/dist/node/validate.d.ts +2 -0
- package/dist/node/validate.js +124 -0
- package/dist/node.d.ts +6 -0
- package/dist/node.js +8 -0
- package/dist/schemas-loose.d.ts +629 -0
- package/dist/schemas-loose.js +97 -0
- package/dist/schemas.d.ts +637 -0
- package/dist/schemas.js +98 -0
- package/dist/shared/config.d.ts +34 -0
- package/dist/shared/config.js +157 -0
- package/dist/shared/log.d.ts +11 -0
- package/dist/shared/log.js +29 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.js +1 -0
- package/package.json +83 -0
package/dist/schemas.js
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
import * as z from "zod";
|
2
|
+
export const McpServerConfigSchema = z.strictObject({
|
3
|
+
command: z.string(),
|
4
|
+
args: z.array(z.string()).optional(),
|
5
|
+
env: z.record(z.string(), z.string()).optional(),
|
6
|
+
});
|
7
|
+
export const DxtManifestAuthorSchema = z.strictObject({
|
8
|
+
name: z.string(),
|
9
|
+
email: z.string().email().optional(),
|
10
|
+
url: z.string().url().optional(),
|
11
|
+
});
|
12
|
+
export const DxtManifestRepositorySchema = z.strictObject({
|
13
|
+
type: z.string(),
|
14
|
+
url: z.string().url(),
|
15
|
+
});
|
16
|
+
export const DxtManifestPlatformOverrideSchema = McpServerConfigSchema.partial();
|
17
|
+
export const DxtManifestMcpConfigSchema = McpServerConfigSchema.extend({
|
18
|
+
platform_overrides: z
|
19
|
+
.record(z.string(), DxtManifestPlatformOverrideSchema)
|
20
|
+
.optional(),
|
21
|
+
});
|
22
|
+
export const DxtManifestServerSchema = z.strictObject({
|
23
|
+
type: z.enum(["python", "node", "binary"]),
|
24
|
+
entry_point: z.string(),
|
25
|
+
mcp_config: DxtManifestMcpConfigSchema,
|
26
|
+
});
|
27
|
+
export const DxtManifestCompatibilitySchema = z
|
28
|
+
.strictObject({
|
29
|
+
claude_desktop: z.string().optional(),
|
30
|
+
platforms: z.array(z.enum(["darwin", "win32", "linux"])).optional(),
|
31
|
+
runtimes: z
|
32
|
+
.strictObject({
|
33
|
+
python: z.string().optional(),
|
34
|
+
node: z.string().optional(),
|
35
|
+
})
|
36
|
+
.optional(),
|
37
|
+
})
|
38
|
+
.passthrough();
|
39
|
+
export const DxtManifestToolSchema = z.strictObject({
|
40
|
+
name: z.string(),
|
41
|
+
description: z.string().optional(),
|
42
|
+
});
|
43
|
+
export const DxtManifestPromptSchema = z.strictObject({
|
44
|
+
name: z.string(),
|
45
|
+
description: z.string().optional(),
|
46
|
+
arguments: z.array(z.string()).optional(),
|
47
|
+
text: z.string(),
|
48
|
+
});
|
49
|
+
export const DxtUserConfigurationOptionSchema = z.strictObject({
|
50
|
+
type: z.enum(["string", "number", "boolean", "directory", "file"]),
|
51
|
+
title: z.string(),
|
52
|
+
description: z.string(),
|
53
|
+
required: z.boolean().optional(),
|
54
|
+
default: z
|
55
|
+
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
|
56
|
+
.optional(),
|
57
|
+
multiple: z.boolean().optional(),
|
58
|
+
sensitive: z.boolean().optional(),
|
59
|
+
min: z.number().optional(),
|
60
|
+
max: z.number().optional(),
|
61
|
+
credentials: z.array(z.string()).optional(),
|
62
|
+
});
|
63
|
+
export const DxtUserConfigValuesSchema = z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]));
|
64
|
+
export const DxtManifestSchema = z.strictObject({
|
65
|
+
$schema: z.string().optional(),
|
66
|
+
dxt_version: z.string(),
|
67
|
+
name: z.string(),
|
68
|
+
display_name: z.string().optional(),
|
69
|
+
version: z.string(),
|
70
|
+
description: z.string(),
|
71
|
+
long_description: z.string().optional(),
|
72
|
+
author: DxtManifestAuthorSchema,
|
73
|
+
repository: DxtManifestRepositorySchema.optional(),
|
74
|
+
homepage: z.string().url().optional(),
|
75
|
+
documentation: z.string().url().optional(),
|
76
|
+
support: z.string().url().optional(),
|
77
|
+
icon: z.string().optional(),
|
78
|
+
screenshots: z.array(z.string()).optional(),
|
79
|
+
server: DxtManifestServerSchema,
|
80
|
+
tools: z.array(DxtManifestToolSchema).optional(),
|
81
|
+
tools_generated: z.boolean().optional(),
|
82
|
+
prompts: z.array(DxtManifestPromptSchema).optional(),
|
83
|
+
prompts_generated: z.boolean().optional(),
|
84
|
+
keywords: z.array(z.string()).optional(),
|
85
|
+
license: z.string().optional(),
|
86
|
+
compatibility: DxtManifestCompatibilitySchema.optional(),
|
87
|
+
user_config: z
|
88
|
+
.record(z.string(), DxtUserConfigurationOptionSchema)
|
89
|
+
.optional(),
|
90
|
+
});
|
91
|
+
export const DxtSignatureInfoSchema = z.strictObject({
|
92
|
+
status: z.enum(["signed", "unsigned", "self-signed"]),
|
93
|
+
publisher: z.string().optional(),
|
94
|
+
issuer: z.string().optional(),
|
95
|
+
valid_from: z.string().optional(),
|
96
|
+
valid_to: z.string().optional(),
|
97
|
+
fingerprint: z.string().optional(),
|
98
|
+
});
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import type { DxtManifest, DxtUserConfigValues, Logger, McpServerConfig } from "../types.js";
|
2
|
+
/**
|
3
|
+
* This file contains utility functions for handling DXT configuration,
|
4
|
+
* including variable replacement and MCP server configuration generation.
|
5
|
+
*/
|
6
|
+
/**
|
7
|
+
* Recursively replaces variables in any value. Handles strings, arrays, and objects.
|
8
|
+
*
|
9
|
+
* @param value The value to process
|
10
|
+
* @param variables Object containing variable replacements
|
11
|
+
* @returns The processed value with all variables replaced
|
12
|
+
*/
|
13
|
+
export declare function replaceVariables(value: unknown, variables: Record<string, string | string[]>): unknown;
|
14
|
+
interface GetMcpConfigForManifestOptions {
|
15
|
+
manifest: DxtManifest;
|
16
|
+
extensionPath: string;
|
17
|
+
systemDirs: Record<string, string>;
|
18
|
+
userConfig: DxtUserConfigValues;
|
19
|
+
pathSeparator: string;
|
20
|
+
logger?: Logger;
|
21
|
+
}
|
22
|
+
export declare function getMcpConfigForManifest(options: GetMcpConfigForManifestOptions): Promise<McpServerConfig | undefined>;
|
23
|
+
interface HasRequiredConfigMissingOptions {
|
24
|
+
manifest: DxtManifest;
|
25
|
+
userConfig?: DxtUserConfigValues;
|
26
|
+
}
|
27
|
+
/**
|
28
|
+
* Check if an extension has missing required configuration
|
29
|
+
* @param manifest The extension manifest
|
30
|
+
* @param userConfig The user configuration
|
31
|
+
* @returns true if required configuration is missing
|
32
|
+
*/
|
33
|
+
export declare function hasRequiredConfigMissing({ manifest, userConfig, }: HasRequiredConfigMissingOptions): boolean;
|
34
|
+
export {};
|
@@ -0,0 +1,157 @@
|
|
1
|
+
/**
|
2
|
+
* This file contains utility functions for handling DXT configuration,
|
3
|
+
* including variable replacement and MCP server configuration generation.
|
4
|
+
*/
|
5
|
+
/**
|
6
|
+
* Recursively replaces variables in any value. Handles strings, arrays, and objects.
|
7
|
+
*
|
8
|
+
* @param value The value to process
|
9
|
+
* @param variables Object containing variable replacements
|
10
|
+
* @returns The processed value with all variables replaced
|
11
|
+
*/
|
12
|
+
export function replaceVariables(value, variables) {
|
13
|
+
if (typeof value === "string") {
|
14
|
+
let result = value;
|
15
|
+
// Replace all variables in the string
|
16
|
+
for (const [key, replacement] of Object.entries(variables)) {
|
17
|
+
const pattern = new RegExp(`\\$\\{${key}\\}`, "g");
|
18
|
+
// Check if this pattern actually exists in the string
|
19
|
+
if (result.match(pattern)) {
|
20
|
+
if (Array.isArray(replacement)) {
|
21
|
+
console.warn(`Cannot replace ${key} with array value in string context: "${value}"`, { key, replacement });
|
22
|
+
}
|
23
|
+
else {
|
24
|
+
result = result.replace(pattern, replacement);
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
return result;
|
29
|
+
}
|
30
|
+
else if (Array.isArray(value)) {
|
31
|
+
// For arrays, we need to handle special case of array expansion
|
32
|
+
const result = [];
|
33
|
+
for (const item of value) {
|
34
|
+
if (typeof item === "string" &&
|
35
|
+
item.match(/^\$\{user_config\.[^}]+\}$/)) {
|
36
|
+
// This is a user config variable that might expand to multiple values
|
37
|
+
const varName = item.match(/^\$\{([^}]+)\}$/)?.[1];
|
38
|
+
if (varName && variables[varName]) {
|
39
|
+
const replacement = variables[varName];
|
40
|
+
if (Array.isArray(replacement)) {
|
41
|
+
// Expand array inline
|
42
|
+
result.push(...replacement);
|
43
|
+
}
|
44
|
+
else {
|
45
|
+
result.push(replacement);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
else {
|
49
|
+
// Variable not found, keep original
|
50
|
+
result.push(item);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
else {
|
54
|
+
// Recursively process non-variable items
|
55
|
+
result.push(replaceVariables(item, variables));
|
56
|
+
}
|
57
|
+
}
|
58
|
+
return result;
|
59
|
+
}
|
60
|
+
else if (value && typeof value === "object") {
|
61
|
+
const result = {};
|
62
|
+
for (const [key, val] of Object.entries(value)) {
|
63
|
+
result[key] = replaceVariables(val, variables);
|
64
|
+
}
|
65
|
+
return result;
|
66
|
+
}
|
67
|
+
return value;
|
68
|
+
}
|
69
|
+
export async function getMcpConfigForManifest(options) {
|
70
|
+
const { manifest, extensionPath, systemDirs, userConfig, pathSeparator, logger, } = options;
|
71
|
+
const baseConfig = manifest.server?.mcp_config;
|
72
|
+
if (!baseConfig) {
|
73
|
+
return undefined;
|
74
|
+
}
|
75
|
+
let result = {
|
76
|
+
...baseConfig,
|
77
|
+
};
|
78
|
+
if (baseConfig.platform_overrides) {
|
79
|
+
if (process.platform in baseConfig.platform_overrides) {
|
80
|
+
const platformConfig = baseConfig.platform_overrides[process.platform];
|
81
|
+
result.command = platformConfig.command || result.command;
|
82
|
+
result.args = platformConfig.args || result.args;
|
83
|
+
result.env = platformConfig.env || result.env;
|
84
|
+
}
|
85
|
+
}
|
86
|
+
// Check if required configuration is missing
|
87
|
+
if (hasRequiredConfigMissing({ manifest, userConfig })) {
|
88
|
+
logger?.warn(`Extension ${manifest.name} has missing required configuration, skipping MCP config`);
|
89
|
+
return undefined;
|
90
|
+
}
|
91
|
+
const variables = {
|
92
|
+
__dirname: extensionPath,
|
93
|
+
pathSeparator,
|
94
|
+
"/": pathSeparator,
|
95
|
+
...systemDirs,
|
96
|
+
};
|
97
|
+
// Build merged configuration from defaults and user settings
|
98
|
+
const mergedConfig = {};
|
99
|
+
// First, add defaults from manifest
|
100
|
+
if (manifest.user_config) {
|
101
|
+
for (const [key, configOption] of Object.entries(manifest.user_config)) {
|
102
|
+
if (configOption.default !== undefined) {
|
103
|
+
mergedConfig[key] = configOption.default;
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
// Then, override with user settings
|
108
|
+
if (userConfig) {
|
109
|
+
Object.assign(mergedConfig, userConfig);
|
110
|
+
}
|
111
|
+
// Add merged configuration variables for substitution
|
112
|
+
for (const [key, value] of Object.entries(mergedConfig)) {
|
113
|
+
// Convert user config to the format expected by variable substitution
|
114
|
+
const userConfigKey = `user_config.${key}`;
|
115
|
+
if (Array.isArray(value)) {
|
116
|
+
// Keep arrays as arrays for proper expansion
|
117
|
+
variables[userConfigKey] = value.map(String);
|
118
|
+
}
|
119
|
+
else if (typeof value === "boolean") {
|
120
|
+
// Convert booleans to "true"/"false" strings as per spec
|
121
|
+
variables[userConfigKey] = value ? "true" : "false";
|
122
|
+
}
|
123
|
+
else {
|
124
|
+
// Convert other types to strings
|
125
|
+
variables[userConfigKey] = String(value);
|
126
|
+
}
|
127
|
+
}
|
128
|
+
// Replace all variables in the config
|
129
|
+
result = replaceVariables(result, variables);
|
130
|
+
return result;
|
131
|
+
}
|
132
|
+
function isInvalidSingleValue(value) {
|
133
|
+
return value === undefined || value === null || value === "";
|
134
|
+
}
|
135
|
+
/**
|
136
|
+
* Check if an extension has missing required configuration
|
137
|
+
* @param manifest The extension manifest
|
138
|
+
* @param userConfig The user configuration
|
139
|
+
* @returns true if required configuration is missing
|
140
|
+
*/
|
141
|
+
export function hasRequiredConfigMissing({ manifest, userConfig, }) {
|
142
|
+
if (!manifest.user_config) {
|
143
|
+
return false;
|
144
|
+
}
|
145
|
+
const config = userConfig || {};
|
146
|
+
for (const [key, configOption] of Object.entries(manifest.user_config)) {
|
147
|
+
if (configOption.required) {
|
148
|
+
const value = config[key];
|
149
|
+
if (isInvalidSingleValue(value) ||
|
150
|
+
(Array.isArray(value) &&
|
151
|
+
(value.length === 0 || value.some(isInvalidSingleValue)))) {
|
152
|
+
return true;
|
153
|
+
}
|
154
|
+
}
|
155
|
+
}
|
156
|
+
return false;
|
157
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
interface LoggerOptions {
|
2
|
+
silent?: boolean;
|
3
|
+
}
|
4
|
+
export declare function getLogger({ silent }?: LoggerOptions): {
|
5
|
+
log: (...args: unknown[]) => void;
|
6
|
+
error: (...args: unknown[]) => void;
|
7
|
+
warn: (...args: unknown[]) => void;
|
8
|
+
info: (...args: unknown[]) => void;
|
9
|
+
debug: (...args: unknown[]) => void;
|
10
|
+
};
|
11
|
+
export {};
|
@@ -0,0 +1,29 @@
|
|
1
|
+
export function getLogger({ silent = false } = {}) {
|
2
|
+
return {
|
3
|
+
log: (...args) => {
|
4
|
+
if (!silent) {
|
5
|
+
console.log(...args);
|
6
|
+
}
|
7
|
+
},
|
8
|
+
error: (...args) => {
|
9
|
+
if (!silent) {
|
10
|
+
console.error(...args);
|
11
|
+
}
|
12
|
+
},
|
13
|
+
warn: (...args) => {
|
14
|
+
if (!silent) {
|
15
|
+
console.warn(...args);
|
16
|
+
}
|
17
|
+
},
|
18
|
+
info: (...args) => {
|
19
|
+
if (!silent) {
|
20
|
+
console.info(...args);
|
21
|
+
}
|
22
|
+
},
|
23
|
+
debug: (...args) => {
|
24
|
+
if (!silent) {
|
25
|
+
console.debug(...args);
|
26
|
+
}
|
27
|
+
},
|
28
|
+
};
|
29
|
+
}
|
package/dist/types.d.ts
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
import type * as z from "zod";
|
2
|
+
import type { DxtManifestAuthorSchema, DxtManifestCompatibilitySchema, DxtManifestMcpConfigSchema, DxtManifestPlatformOverrideSchema, DxtManifestPromptSchema, DxtManifestRepositorySchema, DxtManifestSchema, DxtManifestServerSchema, DxtManifestToolSchema, DxtSignatureInfoSchema, DxtUserConfigurationOptionSchema, DxtUserConfigValuesSchema, McpServerConfigSchema } from "./schemas.js";
|
3
|
+
export type McpServerConfig = z.infer<typeof McpServerConfigSchema>;
|
4
|
+
export type DxtManifestAuthor = z.infer<typeof DxtManifestAuthorSchema>;
|
5
|
+
export type DxtManifestRepository = z.infer<typeof DxtManifestRepositorySchema>;
|
6
|
+
export type DxtManifestPlatformOverride = z.infer<typeof DxtManifestPlatformOverrideSchema>;
|
7
|
+
export type DxtManifestMcpConfig = z.infer<typeof DxtManifestMcpConfigSchema>;
|
8
|
+
export type DxtManifestServer = z.infer<typeof DxtManifestServerSchema>;
|
9
|
+
export type DxtManifestCompatibility = z.infer<typeof DxtManifestCompatibilitySchema>;
|
10
|
+
export type DxtManifestTool = z.infer<typeof DxtManifestToolSchema>;
|
11
|
+
export type DxtManifestPrompt = z.infer<typeof DxtManifestPromptSchema>;
|
12
|
+
export type DxtUserConfigurationOption = z.infer<typeof DxtUserConfigurationOptionSchema>;
|
13
|
+
export type DxtUserConfigValues = z.infer<typeof DxtUserConfigValuesSchema>;
|
14
|
+
export type DxtManifest = z.infer<typeof DxtManifestSchema>;
|
15
|
+
/**
|
16
|
+
* Information about a DXT package signature
|
17
|
+
*/
|
18
|
+
export type DxtSignatureInfo = z.infer<typeof DxtSignatureInfoSchema>;
|
19
|
+
export interface Logger {
|
20
|
+
log: (...args: unknown[]) => void;
|
21
|
+
error: (...args: unknown[]) => void;
|
22
|
+
warn: (...args: unknown[]) => void;
|
23
|
+
}
|
package/dist/types.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
package/package.json
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
{
|
2
|
+
"name": "@enconvo/dxt",
|
3
|
+
"description": "Tools for building Desktop Extensions",
|
4
|
+
"version": "0.2.6",
|
5
|
+
"type": "module",
|
6
|
+
"main": "dist/index.js",
|
7
|
+
"module": "dist/index.js",
|
8
|
+
"types": "dist/index.d.ts",
|
9
|
+
"exports": {
|
10
|
+
".": {
|
11
|
+
"types": "./dist/index.d.ts",
|
12
|
+
"import": "./dist/index.js",
|
13
|
+
"require": "./dist/index.js"
|
14
|
+
},
|
15
|
+
"./browser": {
|
16
|
+
"types": "./dist/browser.d.ts",
|
17
|
+
"import": "./dist/browser.js",
|
18
|
+
"require": "./dist/browser.js"
|
19
|
+
},
|
20
|
+
"./node": {
|
21
|
+
"types": "./dist/node.d.ts",
|
22
|
+
"import": "./dist/node.js",
|
23
|
+
"require": "./dist/node.js"
|
24
|
+
},
|
25
|
+
"./cli": {
|
26
|
+
"types": "./dist/cli.d.ts",
|
27
|
+
"import": "./dist/cli.js",
|
28
|
+
"require": "./dist/cli.js"
|
29
|
+
}
|
30
|
+
},
|
31
|
+
"bin": {
|
32
|
+
"dxt": "dist/cli/cli.js"
|
33
|
+
},
|
34
|
+
"files": [
|
35
|
+
"dist"
|
36
|
+
],
|
37
|
+
"scripts": {
|
38
|
+
"build": "yarn run build:code",
|
39
|
+
"build:code": "tsc",
|
40
|
+
"build:schema": "node ./scripts/build-dxt-schema.js",
|
41
|
+
"dev": "tsc --watch",
|
42
|
+
"test": "jest",
|
43
|
+
"test:watch": "jest --watch",
|
44
|
+
"prepublishOnly": "npm run build",
|
45
|
+
"fix": "eslint --ext .ts . --fix && prettier -w .",
|
46
|
+
"lint": "tsc -p ./tsconfig.json && eslint --ext .ts .",
|
47
|
+
"dev-version": "./scripts/create-dev-version.js"
|
48
|
+
},
|
49
|
+
"author": "Anthropic <support@anthropic.com>",
|
50
|
+
"license": "MIT",
|
51
|
+
"devDependencies": {
|
52
|
+
"@types/jest": "^29.5.14",
|
53
|
+
"@types/node": "^22.15.3",
|
54
|
+
"@types/node-forge": "1.3.11",
|
55
|
+
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
56
|
+
"@typescript-eslint/parser": "^5.62.0",
|
57
|
+
"eslint": "^8.43.0",
|
58
|
+
"eslint-config-prettier": "^8.10.0",
|
59
|
+
"eslint-import-resolver-typescript": "^3.6.1",
|
60
|
+
"eslint-plugin-import": "^2.29.1",
|
61
|
+
"eslint-plugin-prettier": "^5.1.3",
|
62
|
+
"eslint-plugin-simple-import-sort": "^10.0.0",
|
63
|
+
"jest": "^29.7.0",
|
64
|
+
"prettier": "^3.3.3",
|
65
|
+
"ts-jest": "^29.3.2",
|
66
|
+
"typescript": "^5.6.3"
|
67
|
+
},
|
68
|
+
"dependencies": {
|
69
|
+
"@inquirer/prompts": "^6.0.1",
|
70
|
+
"commander": "^13.1.0",
|
71
|
+
"fflate": "^0.8.2",
|
72
|
+
"galactus": "^1.0.0",
|
73
|
+
"ignore": "^7.0.5",
|
74
|
+
"node-forge": "^1.3.1",
|
75
|
+
"pretty-bytes": "^5.6.0",
|
76
|
+
"zod": "^3.25.67"
|
77
|
+
},
|
78
|
+
"resolutions": {
|
79
|
+
"@babel/helpers": "7.27.1",
|
80
|
+
"@babel/parser": "7.27.3"
|
81
|
+
},
|
82
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
83
|
+
}
|