@agentplugins/adapter-copilot 0.1.0

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/index.mjs ADDED
@@ -0,0 +1,593 @@
1
+ // src/index.ts
2
+ import {
3
+ Severity
4
+ } from "@agentplugins/core";
5
+ var PLATFORM_NAME = "copilot";
6
+ var DISPLAY_NAME = "GitHub Copilot CLI";
7
+ var MANIFEST_PATH = "plugin.json";
8
+ var MANIFEST_FORMAT = "json";
9
+ var SUPPORTED_HOOKS = [
10
+ "sessionStart",
11
+ "sessionEnd",
12
+ "userPromptSubmit",
13
+ "preToolUse",
14
+ "postToolUse",
15
+ "postToolUseFailure",
16
+ "permissionRequest",
17
+ "subagentStart",
18
+ "subagentStop",
19
+ "preCompact",
20
+ "notification"
21
+ ];
22
+ var SUPPORTED_HANDLERS = ["command", "http", "prompt"];
23
+ var HOOK_MAP = {
24
+ sessionStart: "sessionStart",
25
+ sessionEnd: "sessionEnd",
26
+ userPromptSubmit: "userPromptSubmitted",
27
+ preToolUse: "preToolUse",
28
+ postToolUse: "postToolUse",
29
+ postToolUseFailure: "postToolUseFailure",
30
+ permissionRequest: "permissionRequest",
31
+ subagentStart: "subagentStart",
32
+ subagentStop: "agentStop",
33
+ // Copilot has no subagentStop; agentStop is closest
34
+ preCompact: "preCompact",
35
+ notification: "notification",
36
+ // Explicitly unsupported hooks
37
+ userPromptExpansion: void 0,
38
+ permissionDenied: void 0,
39
+ postCompact: void 0,
40
+ stop: void 0,
41
+ stopFailure: void 0,
42
+ fileChanged: void 0,
43
+ cwdChanged: void 0,
44
+ setup: void 0
45
+ };
46
+ var FIRE_AND_FORGET_HOOKS = ["notification"];
47
+ var FAIL_CLOSED_HOOKS = ["preToolUse"];
48
+ var MAX_HOOK_TIMEOUT_SECONDS = 30;
49
+ var MAX_ADDITIONAL_CONTEXT_BYTES = 10 * 1024;
50
+ var PROMPT_ALLOWED_HOOK = "sessionStart";
51
+ function byteSize(value) {
52
+ try {
53
+ return new globalThis.TextEncoder().encode(JSON.stringify(value)).length;
54
+ } catch {
55
+ return 0;
56
+ }
57
+ }
58
+ function issue(severity, message, field) {
59
+ return { severity, message, field };
60
+ }
61
+ function checkUnsupportedHook(hook, path) {
62
+ const mapped = HOOK_MAP[hook];
63
+ if (mapped === void 0) {
64
+ return issue(
65
+ Severity.ERROR,
66
+ `Hook "${hook}" is not supported by the Copilot CLI platform. Supported hooks: ${SUPPORTED_HOOKS.map((h) => `"${h}"`).join(", ")}.`,
67
+ path
68
+ );
69
+ }
70
+ return void 0;
71
+ }
72
+ function validateHandler(handler, hookName, path) {
73
+ const issues = [];
74
+ const handlerPath = `${path}.handler`;
75
+ if (!SUPPORTED_HANDLERS.includes(handler.type)) {
76
+ issues.push(
77
+ issue(
78
+ Severity.ERROR,
79
+ `Handler type "${handler.type}" is not supported by Copilot CLI. Supported types: ${SUPPORTED_HANDLERS.map((t) => `"${t}"`).join(", ")}.`,
80
+ handlerPath
81
+ )
82
+ );
83
+ return issues;
84
+ }
85
+ switch (handler.type) {
86
+ case "command": {
87
+ const cmd = handler.config;
88
+ if (!cmd.shell) {
89
+ issues.push(
90
+ issue(
91
+ Severity.ERROR,
92
+ `Command handler must specify "shell" (e.g. "bash" or "powershell").`,
93
+ `${handlerPath}.config.shell`
94
+ )
95
+ );
96
+ } else if (!["bash", "powershell"].includes(cmd.shell)) {
97
+ issues.push(
98
+ issue(
99
+ Severity.WARNING,
100
+ `Shell "${cmd.shell}" may not be supported by all Copilot CLI environments. Recommended: "bash" or "powershell".`,
101
+ `${handlerPath}.config.shell`
102
+ )
103
+ );
104
+ }
105
+ if (!cmd.script && !cmd.command) {
106
+ issues.push(
107
+ issue(
108
+ Severity.ERROR,
109
+ `Command handler must specify "script" or "command".`,
110
+ `${handlerPath}.config`
111
+ )
112
+ );
113
+ }
114
+ break;
115
+ }
116
+ case "http": {
117
+ const httpCfg = handler.config;
118
+ if (!httpCfg.url) {
119
+ issues.push(
120
+ issue(
121
+ Severity.ERROR,
122
+ `HTTP handler must specify "url".`,
123
+ `${handlerPath}.config.url`
124
+ )
125
+ );
126
+ }
127
+ if (httpCfg.method && httpCfg.method.toUpperCase() !== "POST") {
128
+ issues.push(
129
+ issue(
130
+ Severity.ERROR,
131
+ `Copilot CLI only supports POST for HTTP handlers. Found method: "${httpCfg.method}".`,
132
+ `${handlerPath}.config.method`
133
+ )
134
+ );
135
+ }
136
+ break;
137
+ }
138
+ case "prompt": {
139
+ if (hookName !== PROMPT_ALLOWED_HOOK) {
140
+ issues.push(
141
+ issue(
142
+ Severity.ERROR,
143
+ `Prompt handler type is only allowed for the "${PROMPT_ALLOWED_HOOK}" hook. Hook "${hookName}" cannot use a prompt handler.`,
144
+ handlerPath
145
+ )
146
+ );
147
+ }
148
+ const promptCfg = handler.config;
149
+ if (!promptCfg.template) {
150
+ issues.push(
151
+ issue(
152
+ Severity.ERROR,
153
+ `Prompt handler must specify a "template".`,
154
+ `${handlerPath}.config.template`
155
+ )
156
+ );
157
+ }
158
+ break;
159
+ }
160
+ }
161
+ return issues;
162
+ }
163
+ function validateHookConstraints(hook, hookName, path) {
164
+ const issues = [];
165
+ const copilotHook = hook;
166
+ if (copilotHook.additionalContext) {
167
+ const size = byteSize(copilotHook.additionalContext);
168
+ if (size > MAX_ADDITIONAL_CONTEXT_BYTES) {
169
+ issues.push(
170
+ issue(
171
+ Severity.ERROR,
172
+ `additionalContext exceeds ${MAX_ADDITIONAL_CONTEXT_BYTES} bytes (${size} bytes found). Reduce the size of data passed to hooks.`,
173
+ `${path}.additionalContext`
174
+ )
175
+ );
176
+ }
177
+ }
178
+ if (copilotHook.timeout !== void 0 && copilotHook.timeout > MAX_HOOK_TIMEOUT_SECONDS) {
179
+ issues.push(
180
+ issue(
181
+ Severity.ERROR,
182
+ `Hook timeout (${copilotHook.timeout}s) exceeds the Copilot CLI maximum of ${MAX_HOOK_TIMEOUT_SECONDS}s.`,
183
+ `${path}.timeout`
184
+ )
185
+ );
186
+ }
187
+ if (hookName === "preToolUse") {
188
+ const cfg = hook.handler?.config;
189
+ if (!cfg?.matcher?.field || !cfg?.matcher?.value) {
190
+ issues.push(
191
+ issue(
192
+ Severity.WARNING,
193
+ `preToolUse hooks should specify a matcher (e.g. toolName: "Bash") to avoid intercepting every tool call. Without a matcher, the hook runs for ALL tools and errors will deny them (fail-closed behavior).`,
194
+ `${path}.handler.config.matcher`
195
+ )
196
+ );
197
+ }
198
+ }
199
+ return issues;
200
+ }
201
+ function compileSkill(skill) {
202
+ const copilotSkill = skill;
203
+ const descriptor = {
204
+ name: skill.name,
205
+ description: skill.description,
206
+ parameters: copilotSkill.parameters?.map((p) => ({
207
+ name: p.name,
208
+ type: p.type,
209
+ description: p.description,
210
+ required: p.required
211
+ })),
212
+ examples: copilotSkill.examples
213
+ };
214
+ const skillDir = `skills/${skill.name}`;
215
+ let md = `# ${skill.name}
216
+
217
+ `;
218
+ md += `${skill.description}
219
+
220
+ `;
221
+ if (descriptor.parameters && descriptor.parameters.length > 0) {
222
+ md += `## Parameters
223
+
224
+ `;
225
+ md += `| Name | Type | Required | Description |
226
+ `;
227
+ md += `|------|------|----------|-------------|
228
+ `;
229
+ for (const p of descriptor.parameters) {
230
+ md += `| ${p.name} | ${p.type} | ${p.required ? "Yes" : "No"} | ${p.description ?? ""} |
231
+ `;
232
+ }
233
+ md += `
234
+ `;
235
+ }
236
+ if (descriptor.examples && descriptor.examples.length > 0) {
237
+ md += `## Examples
238
+
239
+ `;
240
+ for (const ex of descriptor.examples) {
241
+ md += `\`\`\`
242
+ ${ex}
243
+ \`\`\`
244
+
245
+ `;
246
+ }
247
+ }
248
+ md += `## Metadata
249
+
250
+ `;
251
+ md += `\`\`\`json
252
+ ${JSON.stringify(descriptor, null, 2)}
253
+ \`\`\`
254
+ `;
255
+ return { descriptor, skillDir, skillMdContent: md };
256
+ }
257
+ function compileHookEntry(hookName, hook) {
258
+ const handler = hook.handler;
259
+ const copilotEvent = HOOK_MAP[hookName];
260
+ const entry = {
261
+ event: copilotEvent,
262
+ type: handler.type,
263
+ handler: ""
264
+ };
265
+ switch (handler.type) {
266
+ case "command": {
267
+ const cfg = handler.config;
268
+ entry.handler = cfg.script ?? cfg.command ?? "";
269
+ break;
270
+ }
271
+ case "http": {
272
+ const cfg = handler.config;
273
+ entry.handler = cfg.url;
274
+ break;
275
+ }
276
+ case "prompt": {
277
+ const cfg = handler.config;
278
+ entry.handler = cfg.template;
279
+ break;
280
+ }
281
+ }
282
+ const matcher = handler.config?.matcher;
283
+ if (matcher?.field && matcher?.value) {
284
+ entry.matcher = {
285
+ field: matcher.field,
286
+ value: matcher.value
287
+ };
288
+ }
289
+ if (FIRE_AND_FORGET_HOOKS.includes(copilotEvent)) {
290
+ entry.awaitResponse = false;
291
+ }
292
+ const copilotHook = hook;
293
+ const timeout = copilotHook.timeout ?? MAX_HOOK_TIMEOUT_SECONDS;
294
+ entry.timeout = Math.min(timeout, MAX_HOOK_TIMEOUT_SECONDS);
295
+ if (FAIL_CLOSED_HOOKS.includes(copilotEvent)) {
296
+ entry.failClosed = true;
297
+ }
298
+ return entry;
299
+ }
300
+ var CopilotAdapter = class {
301
+ /** Platform identifier. */
302
+ name = PLATFORM_NAME;
303
+ /** Human-readable display name. */
304
+ displayName = DISPLAY_NAME;
305
+ /** Universal hooks supported by this adapter. */
306
+ supportedHooks = SUPPORTED_HOOKS;
307
+ /** Handler types understood by Copilot CLI. */
308
+ supportedHandlers = SUPPORTED_HANDLERS;
309
+ /** Path to the generated manifest file. */
310
+ manifestPath = MANIFEST_PATH;
311
+ /** Manifest format (JSON). */
312
+ manifestFormat = MANIFEST_FORMAT;
313
+ /**
314
+ * Validate a {@link PluginManifest} for Copilot CLI compatibility.
315
+ *
316
+ * Checks performed:
317
+ * 1. Only supported hooks are referenced.
318
+ * 2. Handler types are within the supported set.
319
+ * 3. Command handlers specify shell (bash/powershell) and script/command.
320
+ * 4. HTTP handlers use POST (the only method Copilot sends).
321
+ * 5. Prompt handlers are only used on `sessionStart`.
322
+ * 6. additionalContext does not exceed 10 KB.
323
+ * 7. Hook timeouts do not exceed 30 seconds.
324
+ * 8. preToolUse hooks are encouraged to specify a matcher.
325
+ * 9. All referenced skills have required fields (name, description).
326
+ *
327
+ * @param plugin – the plugin manifest to validate
328
+ * @returns array of validation issues (empty if fully valid)
329
+ */
330
+ validate(plugin) {
331
+ const issues = [];
332
+ if (plugin.hooks) {
333
+ for (const [hookName, hookDef] of Object.entries(plugin.hooks)) {
334
+ const hookPath = `hooks.${hookName}`;
335
+ const uHook = hookName;
336
+ const unsupported = checkUnsupportedHook(uHook, hookPath);
337
+ if (unsupported) {
338
+ issues.push(unsupported);
339
+ continue;
340
+ }
341
+ if (hookDef.handler) {
342
+ issues.push(
343
+ ...validateHandler(
344
+ hookDef.handler,
345
+ uHook,
346
+ hookPath
347
+ )
348
+ );
349
+ } else {
350
+ issues.push(
351
+ issue(
352
+ Severity.ERROR,
353
+ `Hook "${hookName}" is missing a handler definition.`,
354
+ hookPath
355
+ )
356
+ );
357
+ }
358
+ issues.push(...validateHookConstraints(hookDef, uHook, hookPath));
359
+ }
360
+ }
361
+ if (plugin.skills) {
362
+ for (let i = 0; i < plugin.skills.length; i++) {
363
+ const skill = plugin.skills[i];
364
+ const skillPath = `skills[${i}]`;
365
+ if (!skill.name || skill.name.trim() === "") {
366
+ issues.push(
367
+ issue(
368
+ Severity.ERROR,
369
+ `Skill at index ${i} is missing a "name".`,
370
+ `${skillPath}.name`
371
+ )
372
+ );
373
+ }
374
+ if (!skill.description || skill.description.trim() === "") {
375
+ issues.push(
376
+ issue(
377
+ Severity.ERROR,
378
+ `Skill "${skill.name ?? `#${i}`}" is missing a "description".`,
379
+ `${skillPath}.description`
380
+ )
381
+ );
382
+ }
383
+ const copilotSkill = skill;
384
+ if (copilotSkill.parameters) {
385
+ const validTypes = ["string", "number", "boolean", "object", "array"];
386
+ for (let j = 0; j < copilotSkill.parameters.length; j++) {
387
+ const p = copilotSkill.parameters[j];
388
+ if (!validTypes.includes(p.type)) {
389
+ issues.push(
390
+ issue(
391
+ Severity.WARNING,
392
+ `Parameter "${p.name}" has type "${p.type}" which may not be recognised by Copilot CLI. Valid types: ${validTypes.join(", ")}.`,
393
+ `${skillPath}.parameters[${j}].type`
394
+ )
395
+ );
396
+ }
397
+ }
398
+ }
399
+ }
400
+ }
401
+ if (plugin.hooks?.preToolUse) {
402
+ const preToolHandler = plugin.hooks.preToolUse.handler;
403
+ const matcher = preToolHandler?.config?.matcher;
404
+ if (!matcher) {
405
+ issues.push(
406
+ issue(
407
+ Severity.WARNING,
408
+ `Plugin defines a "preToolUse" hook without a matcher. This will intercept ALL tool calls and any error will deny them (fail-closed). Consider adding a matcher like { field: "toolName", value: "Bash" }.`,
409
+ `hooks.preToolUse.handler.config.matcher`
410
+ )
411
+ );
412
+ }
413
+ }
414
+ return issues;
415
+ }
416
+ /**
417
+ * Compile a {@link PluginManifest} into the Copilot CLI file layout.
418
+ *
419
+ * Produces:
420
+ * - `plugin.json` – top-level manifest with metadata and file references
421
+ * - `hooks.json` – hook bindings mapped to Copilot event names
422
+ * - `skills/<name>/SKILL.md` – one file per skill
423
+ * - `.mcp.json` – MCP server configuration (if MCP servers are defined)
424
+ *
425
+ * @param plugin – the validated plugin manifest
426
+ * @returns {@link AdapterOutput} containing all generated files
427
+ */
428
+ compile(plugin) {
429
+ const files = [];
430
+ const warnings = [];
431
+ const hookEntries = [];
432
+ if (plugin.hooks) {
433
+ for (const [hookName, hookDef] of Object.entries(plugin.hooks)) {
434
+ const uHook = hookName;
435
+ if (!SUPPORTED_HOOKS.includes(uHook)) {
436
+ warnings.push(
437
+ `Skipping unsupported hook "${hookName}" during compilation.`
438
+ );
439
+ continue;
440
+ }
441
+ const handler = hookDef.handler;
442
+ if (handler.type === "prompt") {
443
+ const entry = compileHookEntry(uHook, hookDef);
444
+ hookEntries.push(entry);
445
+ } else {
446
+ const entry = compileHookEntry(uHook, hookDef);
447
+ if (handler.type === "command") {
448
+ const cfg = handler.config;
449
+ const scriptName = `hook-${hookName}.${cfg.shell === "powershell" ? "ps1" : "sh"}`;
450
+ const scriptContent = generateWrapperScript(cfg);
451
+ files.push({
452
+ path: scriptName,
453
+ content: scriptContent
454
+ });
455
+ entry.handler = `./${scriptName}`;
456
+ }
457
+ hookEntries.push(entry);
458
+ }
459
+ }
460
+ }
461
+ if (hookEntries.length > 0) {
462
+ files.push({
463
+ path: "hooks.json",
464
+ content: JSON.stringify(hookEntries, null, 2)
465
+ });
466
+ }
467
+ const skillDirs = [];
468
+ if (plugin.skills && plugin.skills.length > 0) {
469
+ for (const skill of plugin.skills) {
470
+ const { skillDir, skillMdContent } = compileSkill(skill);
471
+ skillDirs.push(skillDir);
472
+ files.push({
473
+ path: `${skillDir}/SKILL.md`,
474
+ content: skillMdContent
475
+ });
476
+ }
477
+ }
478
+ const mcpConfig = this.buildMcpConfig(plugin);
479
+ if (mcpConfig) {
480
+ files.push({
481
+ path: ".mcp.json",
482
+ content: JSON.stringify(mcpConfig, null, 2)
483
+ });
484
+ }
485
+ const copilotPlugin = plugin;
486
+ const copilotManifest = {
487
+ schemaVersion: "1.0",
488
+ id: copilotPlugin.id ?? copilotPlugin.name,
489
+ name: copilotPlugin.name,
490
+ description: copilotPlugin.description,
491
+ version: copilotPlugin.version,
492
+ strict: copilotPlugin.strict ?? true,
493
+ maxAdditionalContextBytes: MAX_ADDITIONAL_CONTEXT_BYTES,
494
+ hookTimeoutSeconds: MAX_HOOK_TIMEOUT_SECONDS,
495
+ author: typeof copilotPlugin.author === "string" ? copilotPlugin.author : copilotPlugin.author?.name,
496
+ homepage: copilotPlugin.homepage,
497
+ license: copilotPlugin.license,
498
+ tags: copilotPlugin.tags ?? copilotPlugin.keywords ?? void 0,
499
+ ...hookEntries.length > 0 && { hooks: "hooks.json" },
500
+ ...skillDirs.length > 0 && { skills: skillDirs },
501
+ ...mcpConfig && { mcp: ".mcp.json" }
502
+ };
503
+ files.unshift({
504
+ path: MANIFEST_PATH,
505
+ content: JSON.stringify(copilotManifest, null, 2)
506
+ });
507
+ return {
508
+ files,
509
+ manifest: copilotManifest,
510
+ warnings: warnings.length > 0 ? warnings : [],
511
+ issues: []
512
+ };
513
+ }
514
+ /**
515
+ * Build an MCP server configuration object from the plugin manifest.
516
+ *
517
+ * If the plugin defines MCP servers (in `plugin.mcpServers`), they are
518
+ * converted into Copilot's `.mcp.json` format.
519
+ *
520
+ * @param plugin – the plugin manifest
521
+ * @returns MCP config object, or `null` if no MCP servers are defined
522
+ */
523
+ buildMcpConfig(plugin) {
524
+ const servers = plugin.mcpServers;
525
+ if (!servers || servers.length === 0) {
526
+ return null;
527
+ }
528
+ return {
529
+ schemaVersion: "1.0",
530
+ servers: servers.map((s) => ({
531
+ id: s.id,
532
+ name: s.name,
533
+ transport: s.transport,
534
+ ...s.command && { command: s.command },
535
+ ...s.args && { args: s.args },
536
+ ...s.url && { url: s.url },
537
+ ...s.env && { env: s.env }
538
+ }))
539
+ };
540
+ }
541
+ };
542
+ function generateWrapperScript(cfg) {
543
+ const scriptBody = cfg.script ?? cfg.command ?? "";
544
+ if (cfg.shell === "powershell") {
545
+ return `
546
+ # Auto-generated Copilot CLI hook wrapper
547
+ param(
548
+ [Parameter(ValueFromPipeline = $true)]
549
+ [string]$Payload
550
+ )
551
+
552
+ # Read payload from stdin if not provided as argument
553
+ if (-not $Payload) {
554
+ $Payload = $input | Out-String
555
+ }
556
+
557
+ # Parse JSON payload
558
+ $HookData = $Payload | ConvertFrom-Json -ErrorAction SilentlyContinue
559
+
560
+ # TODO: export relevant fields as environment variables if needed
561
+ # $env:COPILOT_TOOL_NAME = $HookData.toolName
562
+
563
+ # Execute user script
564
+ ${scriptBody}
565
+ `.trim();
566
+ }
567
+ return `#!/usr/bin/env bash
568
+ # Auto-generated Copilot CLI hook wrapper
569
+
570
+ set -euo pipefail
571
+
572
+ # Read JSON payload from stdin
573
+ PAYLOAD="$(cat)"
574
+
575
+ # Parse payload with jq if available; otherwise pass through raw
576
+ if command -v jq &> /dev/null; then
577
+ TOOL_NAME="$(echo "$PAYLOAD" | jq -r '.toolName // empty')"
578
+ export COPILOT_TOOL_NAME="\${TOOL_NAME:-}"
579
+ fi
580
+
581
+ # Execute user script
582
+ ${scriptBody}
583
+ `.trim();
584
+ }
585
+ var copilotAdapter = new CopilotAdapter();
586
+ function createCopilotAdapter() {
587
+ return new CopilotAdapter();
588
+ }
589
+ export {
590
+ CopilotAdapter,
591
+ copilotAdapter,
592
+ createCopilotAdapter
593
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@agentplugins/adapter-copilot",
3
+ "version": "0.1.0",
4
+ "description": "GitHub Copilot CLI platform adapter for AgentPlugins — compiles AgentPlugins plugins into Copilot-compatible manifests, hooks, skills, and MCP configuration",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.mjs",
10
+ "require": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "build": "tsup src/index.ts --format cjs,esm --dts",
23
+ "typecheck": "tsc --noEmit",
24
+ "lint": "eslint src/**/*.ts",
25
+ "test": "vitest run"
26
+ },
27
+ "dependencies": {
28
+ "@agentplugins/core": "workspace:*"
29
+ },
30
+ "devDependencies": {
31
+ "tsup": "^8.0.0",
32
+ "typescript": "^5.3.0",
33
+ "vitest": "^1.0.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/espetro/agentplugins.git",
42
+ "directory": "packages/adapter-copilot"
43
+ },
44
+ "keywords": [
45
+ "agentplugins",
46
+ "github-copilot",
47
+ "copilot-cli",
48
+ "plugin-adapter",
49
+ "ai-agent",
50
+ "mcp"
51
+ ]
52
+ }