@poolzin/pool-bot 2026.3.17 → 2026.3.18

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.
Files changed (86) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/agents/tools/web-fetch.js +1 -1
  3. package/dist/build-info.json +2 -2
  4. package/dist/commands/skills-openclaw.command.js +123 -0
  5. package/dist/config/paths.js +7 -0
  6. package/dist/infra/net/fetch-guard.js +191 -146
  7. package/dist/media/fetch.js +83 -112
  8. package/dist/media/inbound-path-policy.js +90 -97
  9. package/dist/media/read-response-with-limit.js +49 -26
  10. package/dist/media-understanding/attachments.js +1 -1
  11. package/dist/plugin-sdk/audio.js +7 -0
  12. package/dist/plugin-sdk/bluebubbles.js +7 -0
  13. package/dist/plugin-sdk/browser.js +7 -0
  14. package/dist/plugin-sdk/canvas.js +7 -0
  15. package/dist/plugin-sdk/cron.js +7 -0
  16. package/dist/plugin-sdk/discord-actions.js +6 -0
  17. package/dist/plugin-sdk/discord.js +7 -0
  18. package/dist/plugin-sdk/image.js +7 -0
  19. package/dist/plugin-sdk/imessage.js +6 -0
  20. package/dist/plugin-sdk/keyed-async-queue.js +35 -0
  21. package/dist/plugin-sdk/media.js +8 -0
  22. package/dist/plugin-sdk/memory.js +7 -0
  23. package/dist/plugin-sdk/pdf.js +7 -0
  24. package/dist/plugin-sdk/sessions.js +7 -0
  25. package/dist/plugin-sdk/signal.js +6 -0
  26. package/dist/plugin-sdk/slack-actions.js +7 -0
  27. package/dist/plugin-sdk/slack.js +7 -0
  28. package/dist/plugin-sdk/telegram-actions.js +6 -0
  29. package/dist/plugin-sdk/telegram.js +6 -0
  30. package/dist/plugin-sdk/test-utils.js +110 -0
  31. package/dist/plugin-sdk/tts.js +7 -0
  32. package/dist/plugin-sdk/whatsapp.js +6 -0
  33. package/dist/providers/github-copilot-auth.js +53 -76
  34. package/dist/providers/github-copilot-models.js +63 -35
  35. package/dist/providers/github-copilot-token.js +46 -89
  36. package/dist/security/audit-findings.js +165 -0
  37. package/dist/security/audit.js +141 -572
  38. package/dist/skills/openclaw-skill-loader.js +191 -0
  39. package/dist/slack/monitor/media.js +2 -1
  40. package/docs/improvements/OPENCLAW-IMPLEMENTATION.md +45 -0
  41. package/docs/skills/openclaw-integration.md +295 -0
  42. package/docs/testing/TEST-PLAN-2026-03-13.md +338 -0
  43. package/extensions/acpx/package.json +19 -0
  44. package/extensions/acpx/poolbot.plugin.json +9 -0
  45. package/extensions/acpx/src/index.ts +34 -0
  46. package/extensions/bluebubbles/src/runtime.ts +1 -0
  47. package/extensions/diffs/package.json +15 -0
  48. package/extensions/diffs/poolbot.plugin.json +10 -0
  49. package/extensions/diffs/src/index.ts +106 -0
  50. package/extensions/discord/src/runtime.ts +1 -0
  51. package/extensions/feishu/src/runtime.ts +1 -0
  52. package/extensions/github-copilot/package.json +28 -0
  53. package/extensions/github-copilot/poolbot.plugin.json +29 -0
  54. package/extensions/github-copilot/src/index.ts +126 -0
  55. package/extensions/github-copilot/tsconfig.json +10 -0
  56. package/extensions/googlechat/src/runtime.ts +1 -0
  57. package/extensions/imessage/src/runtime.ts +1 -0
  58. package/extensions/irc/src/runtime.ts +1 -0
  59. package/extensions/line/src/runtime.ts +1 -0
  60. package/extensions/matrix/src/runtime.ts +1 -0
  61. package/extensions/mattermost/src/mattermost/monitor-helpers.ts +10 -1
  62. package/extensions/mattermost/src/runtime.ts +6 -3
  63. package/extensions/msteams/src/runtime.ts +1 -0
  64. package/extensions/nextcloud-talk/src/runtime.ts +1 -0
  65. package/extensions/nostr/src/runtime.ts +5 -2
  66. package/extensions/ollama/package.json +20 -0
  67. package/extensions/ollama/poolbot.plugin.json +14 -0
  68. package/extensions/ollama/src/index.ts +95 -0
  69. package/extensions/sglang/package.json +18 -0
  70. package/extensions/sglang/poolbot.plugin.json +13 -0
  71. package/extensions/sglang/src/index.ts +62 -0
  72. package/extensions/signal/src/runtime.ts +1 -0
  73. package/extensions/slack/src/runtime.ts +1 -0
  74. package/extensions/telegram/src/runtime.ts +1 -0
  75. package/extensions/test-utils/package.json +17 -0
  76. package/extensions/test-utils/poolbot.plugin.json +16 -0
  77. package/extensions/test-utils/src/index.ts +220 -0
  78. package/extensions/tlon/src/runtime.ts +1 -0
  79. package/extensions/twitch/src/runtime.ts +1 -0
  80. package/extensions/vllm/package.json +19 -0
  81. package/extensions/vllm/poolbot.plugin.json +13 -0
  82. package/extensions/vllm/src/index.ts +90 -0
  83. package/extensions/whatsapp/src/runtime.ts +1 -0
  84. package/extensions/zalo/src/runtime.ts +1 -0
  85. package/extensions/zalouser/src/runtime.ts +1 -0
  86. package/package.json +77 -3
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Pool Bot Test Utilities
3
+ *
4
+ * Provides testing utilities and helpers for Pool Bot development
5
+ */
6
+
7
+ import type { PoolBotConfig } from "poolbot/config/config.js";
8
+ import type { PluginRuntime } from "poolbot/plugins/runtime/types.js";
9
+
10
+ /**
11
+ * Create a mock Pool Bot config for testing
12
+ */
13
+ export function createMockConfig(overrides?: Partial<PoolBotConfig>): PoolBotConfig {
14
+ return {
15
+ models: {
16
+ primary: "gpt-4o",
17
+ },
18
+ agents: {
19
+ list: [],
20
+ },
21
+ channels: {},
22
+ gateway: {
23
+ port: 18789,
24
+ host: "127.0.0.1",
25
+ },
26
+ ...overrides,
27
+ } as PoolBotConfig;
28
+ }
29
+
30
+ /**
31
+ * Create a mock Plugin Runtime for testing
32
+ */
33
+ export function createMockRuntime(): PluginRuntime {
34
+ return {
35
+ version: "test",
36
+ config: {
37
+ loadConfig: async () => createMockConfig(),
38
+ writeConfigFile: async () => {},
39
+ },
40
+ system: {
41
+ enqueueSystemEvent: async () => {},
42
+ runCommandWithTimeout: async () => ({ stdout: "", stderr: "", exitCode: 0 }),
43
+ formatNativeDependencyHint: () => "",
44
+ },
45
+ media: {
46
+ loadWebMedia: async () => ({ buffer: Buffer.from([]) }),
47
+ detectMime: async () => "application/octet-stream",
48
+ mediaKindFromMime: () => "unknown" as any,
49
+ isVoiceCompatibleAudio: () => false,
50
+ getImageMetadata: async () => ({ width: 0, height: 0 }),
51
+ resizeToJpeg: async () => Buffer.from([]),
52
+ },
53
+ tts: {
54
+ textToSpeechTelephony: async () => ({ audioPath: "" }),
55
+ },
56
+ tools: {
57
+ createMemoryGetTool: () => ({} as any),
58
+ createMemorySearchTool: () => ({} as any),
59
+ registerMemoryCli: () => {},
60
+ },
61
+ channel: {
62
+ text: {
63
+ chunkByNewline: () => [],
64
+ chunkMarkdownText: () => [],
65
+ chunkMarkdownTextWithMode: () => [],
66
+ chunkText: () => [],
67
+ chunkTextWithMode: () => [],
68
+ resolveChunkMode: () => "markdown" as const,
69
+ resolveTextChunkLimit: () => 4000,
70
+ hasControlCommand: () => false,
71
+ resolveMarkdownTableMode: () => "convert" as const,
72
+ convertMarkdownTables: () => "",
73
+ },
74
+ reply: {
75
+ dispatchReplyWithBufferedBlockDispatcher: async () => {},
76
+ createReplyDispatcherWithTyping: () => ({} as any),
77
+ resolveEffectiveMessagesConfig: () => ({} as any),
78
+ resolveHumanDelayConfig: () => ({} as any),
79
+ dispatchReplyFromConfig: async () => {},
80
+ finalizeInboundContext: async () => ({} as any),
81
+ formatAgentEnvelope: () => "",
82
+ formatInboundEnvelope: () => "",
83
+ resolveEnvelopeFormatOptions: () => ({} as any),
84
+ },
85
+ routing: {
86
+ resolveAgentRoute: async () => null,
87
+ },
88
+ pairing: {
89
+ buildPairingReply: () => "",
90
+ readAllowFromStore: async () => [],
91
+ upsertPairingRequest: async () => {},
92
+ },
93
+ identity: {
94
+ resolveEffectiveMessagesConfig: () => ({} as any),
95
+ resolveHumanDelayConfig: () => ({} as any),
96
+ },
97
+ lifecycle: {
98
+ enqueueSystemEvent: async () => {},
99
+ },
100
+ gateway: {
101
+ call: async () => ({} as any),
102
+ },
103
+ security: {
104
+ checkDmPolicy: async () => "allow" as const,
105
+ },
106
+ mentions: {
107
+ buildMentionRegexes: () => [],
108
+ matchesMentionPatterns: () => false,
109
+ matchesMentionWithExplicit: () => false,
110
+ },
111
+ reactions: {
112
+ shouldAckReaction: () => false,
113
+ removeAckReactionAfterReply: async () => {},
114
+ },
115
+ group: {
116
+ resolveChannelGroupPolicy: () => "mention" as const,
117
+ resolveChannelGroupRequireMention: () => true,
118
+ },
119
+ debounce: {
120
+ createInboundDebouncer: () => ({} as any),
121
+ resolveInboundDebounceMs: () => 0,
122
+ },
123
+ gating: {
124
+ resolveCommandAuthorizedFromAuthorizers: async () => false,
125
+ },
126
+ },
127
+ logging: {
128
+ debug: () => {},
129
+ info: () => {},
130
+ warn: () => {},
131
+ error: () => {},
132
+ },
133
+ state: {
134
+ getStateDir: () => "/tmp/poolbot-test",
135
+ },
136
+ logger: {
137
+ debug: () => {},
138
+ info: () => {},
139
+ warn: () => {},
140
+ error: () => {},
141
+ },
142
+ } as PluginRuntime;
143
+ }
144
+
145
+ /**
146
+ * Test helper: wait for a condition to be true
147
+ */
148
+ export async function waitForCondition(
149
+ condition: () => boolean | Promise<boolean>,
150
+ timeoutMs: number = 5000,
151
+ intervalMs: number = 100,
152
+ ): Promise<void> {
153
+ const start = Date.now();
154
+
155
+ while (Date.now() - start < timeoutMs) {
156
+ const result = await condition();
157
+ if (result) return;
158
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
159
+ }
160
+
161
+ throw new Error(`Condition not met within ${timeoutMs}ms`);
162
+ }
163
+
164
+ /**
165
+ * Test helper: create a temporary file
166
+ */
167
+ export async function createTempFile(content: string, extension: string = ".txt"): Promise<string> {
168
+ const fs = await import("node:fs/promises");
169
+ const path = await import("node:path");
170
+ const os = await import("node:os");
171
+
172
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "poolbot-test-"));
173
+ const filePath = path.join(tmpDir, `test${extension}`);
174
+ await fs.writeFile(filePath, content, "utf-8");
175
+
176
+ return filePath;
177
+ }
178
+
179
+ /**
180
+ * Test helper: cleanup temporary files
181
+ */
182
+ export async function cleanupTempFiles(...paths: string[]): Promise<void> {
183
+ const fs = await import("node:fs/promises");
184
+
185
+ for (const p of paths) {
186
+ try {
187
+ await fs.rm(p, { recursive: true, force: true });
188
+ } catch {
189
+ // Ignore cleanup errors
190
+ }
191
+ }
192
+ }
193
+
194
+ // Export CLI command for test utilities
195
+ export default async function createPlugin(ctx: any): Promise<any> {
196
+ ctx.logger.info("Test Utils initialized");
197
+
198
+ ctx.cli
199
+ .command("test.mock-config")
200
+ .description("Generate a mock config for testing")
201
+ .option("-o, --output <file>", "Output file path")
202
+ .action(async (options?: { output?: string }) => {
203
+ const config = createMockConfig();
204
+ const json = JSON.stringify(config, null, 2);
205
+
206
+ if (options?.output) {
207
+ const fs = await import("node:fs/promises");
208
+ await fs.writeFile(options.output, json, "utf-8");
209
+ console.log(`✅ Mock config written to ${options.output}`);
210
+ } else {
211
+ console.log(json);
212
+ }
213
+ });
214
+
215
+ return {
216
+ name: "test-utils",
217
+ version: "2026.3.17",
218
+ commands: ["test.mock-config"],
219
+ };
220
+ }
@@ -1,5 +1,6 @@
1
1
  import type { PluginRuntime } from "poolbot/plugin-sdk";
2
2
 
3
+ // oxlint-disable-next-line typescript-eslint/no-redundant-type-constituents
3
4
  let runtime: PluginRuntime | null = null;
4
5
 
5
6
  export function setTlonRuntime(next: PluginRuntime) {
@@ -1,5 +1,6 @@
1
1
  import type { PluginRuntime } from "poolbot/plugin-sdk";
2
2
 
3
+ // oxlint-disable-next-line typescript-eslint/no-redundant-type-constituents
3
4
  let runtime: PluginRuntime | null = null;
4
5
 
5
6
  export function setTwitchRuntime(next: PluginRuntime) {
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@poolbot/vllm",
3
+ "version": "2026.3.17",
4
+ "description": "vLLM provider for Pool Bot - high-throughput LLM inference",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "dependencies": {
8
+ "poolbot": "workspace:*"
9
+ },
10
+ "poolbot": {
11
+ "extensions": ["./src/index.ts"],
12
+ "provider": {
13
+ "id": "vllm",
14
+ "name": "vLLM",
15
+ "description": "vLLM for high-throughput LLM inference",
16
+ "models": ["llama", "mistral", "qwen", "yi"]
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "id": "vllm",
3
+ "name": "vLLM Provider",
4
+ "version": "2026.3.17",
5
+ "description": "vLLM for high-throughput LLM inference",
6
+ "main": "dist/index.js",
7
+ "capabilities": ["provider", "high-throughput"],
8
+ "commands": ["vllm.status", "vllm.models", "vllm.generate"],
9
+ "config": {
10
+ "host": "http://127.0.0.1",
11
+ "port": 8000
12
+ }
13
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Pool Bot vLLM Provider Extension
3
+ *
4
+ * Provides vLLM runtime for high-throughput LLM inference
5
+ */
6
+
7
+ export default async function createPlugin(ctx: any): Promise<any> {
8
+ const config = ctx.config as {
9
+ host?: string;
10
+ port?: number;
11
+ };
12
+
13
+ const host = config.host ?? "http://127.0.0.1";
14
+ const port = config.port ?? 8000;
15
+ const baseUrl = `${host}:${port}`;
16
+
17
+ ctx.logger.info(`vLLM Provider initialized (${baseUrl})`);
18
+
19
+ ctx.cli
20
+ .command("vllm.status")
21
+ .description("Check vLLM server status")
22
+ .action(async () => {
23
+ try {
24
+ const res = await fetch(`${baseUrl}/health`);
25
+ if (res.ok) {
26
+ console.log("✅ vLLM Status");
27
+ console.log(` URL: ${baseUrl}`);
28
+ console.log(` Status: Running`);
29
+ } else {
30
+ console.log("❌ vLLM not running");
31
+ }
32
+ } catch (error) {
33
+ console.log("❌ vLLM connection failed");
34
+ console.log(` Error: ${error instanceof Error ? error.message : error}`);
35
+ }
36
+ });
37
+
38
+ ctx.cli
39
+ .command("vllm.models")
40
+ .description("List available vLLM models")
41
+ .action(async () => {
42
+ try {
43
+ const res = await fetch(`${baseUrl}/v1/models`);
44
+ if (!res.ok) throw new Error("vLLM not running");
45
+
46
+ const data = await res.json() as { data: Array<{ id: string }> };
47
+ console.log("📦 vLLM Models\n");
48
+
49
+ for (const model of data.data) {
50
+ console.log(`🤖 ${model.id}`);
51
+ }
52
+ } catch (error) {
53
+ console.error("Failed to list models:", error instanceof Error ? error.message : error);
54
+ }
55
+ });
56
+
57
+ ctx.cli
58
+ .command("vllm.generate")
59
+ .description("Generate text with vLLM")
60
+ .argument("<prompt>", "Input prompt")
61
+ .option("-m, --model <model>", "Model to use")
62
+ .option("-t, --max-tokens <number>", "Max tokens to generate", "256")
63
+ .action(async (prompt: string, options?: { model?: string; maxTokens?: string }) => {
64
+ try {
65
+ const res = await fetch(`${baseUrl}/v1/completions`, {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify({
69
+ model: options?.model ?? "llama",
70
+ prompt,
71
+ max_tokens: parseInt(options?.maxTokens ?? "256"),
72
+ }),
73
+ });
74
+
75
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
76
+
77
+ const data = await res.json() as { choices: Array<{ text: string }> };
78
+ console.log(data.choices[0]?.text ?? "No response");
79
+ } catch (error) {
80
+ console.error("Generation failed:", error instanceof Error ? error.message : error);
81
+ process.exit(1);
82
+ }
83
+ });
84
+
85
+ return {
86
+ name: "vllm",
87
+ version: "2026.3.17",
88
+ commands: ["vllm.status", "vllm.models", "vllm.generate"],
89
+ };
90
+ }
@@ -1,5 +1,6 @@
1
1
  import type { PluginRuntime } from "poolbot/plugin-sdk";
2
2
 
3
+ // oxlint-disable-next-line typescript-eslint/no-redundant-type-constituents
3
4
  let runtime: PluginRuntime | null = null;
4
5
 
5
6
  export function setWhatsAppRuntime(next: PluginRuntime) {
@@ -1,5 +1,6 @@
1
1
  import type { PluginRuntime } from "poolbot/plugin-sdk";
2
2
 
3
+ // oxlint-disable-next-line typescript-eslint/no-redundant-type-constituents
3
4
  let runtime: PluginRuntime | null = null;
4
5
 
5
6
  export function setZaloRuntime(next: PluginRuntime): void {
@@ -1,5 +1,6 @@
1
1
  import type { PluginRuntime } from "poolbot/plugin-sdk";
2
2
 
3
+ // oxlint-disable-next-line typescript-eslint/no-redundant-type-constituents
3
4
  let runtime: PluginRuntime | null = null;
4
5
 
5
6
  export function setZalouserRuntime(next: PluginRuntime): void {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poolzin/pool-bot",
3
- "version": "2026.3.17",
3
+ "version": "2026.3.18",
4
4
  "description": "🎱 Pool Bot - AI assistant with PLCODE integrations",
5
5
  "keywords": [],
6
6
  "license": "MIT",
@@ -24,8 +24,82 @@
24
24
  "main": "dist/index.js",
25
25
  "exports": {
26
26
  ".": "./dist/index.js",
27
- "./plugin-sdk": "./dist/plugin-sdk/index.js",
28
- "./plugin-sdk/*": "./dist/plugin-sdk/*",
27
+ "./plugin-sdk": {
28
+ "types": "./dist/plugin-sdk/index.d.ts",
29
+ "default": "./dist/plugin-sdk/index.js"
30
+ },
31
+ "./plugin-sdk/account-id": {
32
+ "types": "./dist/plugin-sdk/account-id.d.ts",
33
+ "default": "./dist/plugin-sdk/account-id.js"
34
+ },
35
+ "./plugin-sdk/agent-media-payload": {
36
+ "types": "./dist/plugin-sdk/agent-media-payload.d.ts",
37
+ "default": "./dist/plugin-sdk/agent-media-payload.js"
38
+ },
39
+ "./plugin-sdk/allow-from": {
40
+ "types": "./dist/plugin-sdk/allow-from.d.ts",
41
+ "default": "./dist/plugin-sdk/allow-from.js"
42
+ },
43
+ "./plugin-sdk/command-auth": {
44
+ "types": "./dist/plugin-sdk/command-auth.d.ts",
45
+ "default": "./dist/plugin-sdk/command-auth.js"
46
+ },
47
+ "./plugin-sdk/config-paths": {
48
+ "types": "./dist/plugin-sdk/config-paths.d.ts",
49
+ "default": "./dist/plugin-sdk/config-paths.js"
50
+ },
51
+ "./plugin-sdk/file-lock": {
52
+ "types": "./dist/plugin-sdk/file-lock.d.ts",
53
+ "default": "./dist/plugin-sdk/file-lock.js"
54
+ },
55
+ "./plugin-sdk/json-store": {
56
+ "types": "./dist/plugin-sdk/json-store.d.ts",
57
+ "default": "./dist/plugin-sdk/json-store.js"
58
+ },
59
+ "./plugin-sdk/keyed-async-queue": {
60
+ "types": "./dist/plugin-sdk/keyed-async-queue.d.ts",
61
+ "default": "./dist/plugin-sdk/keyed-async-queue.js"
62
+ },
63
+ "./plugin-sdk/onboarding": {
64
+ "types": "./dist/plugin-sdk/onboarding.d.ts",
65
+ "default": "./dist/plugin-sdk/onboarding.js"
66
+ },
67
+ "./plugin-sdk/provider-auth-result": {
68
+ "types": "./dist/plugin-sdk/provider-auth-result.d.ts",
69
+ "default": "./dist/plugin-sdk/provider-auth-result.js"
70
+ },
71
+ "./plugin-sdk/slack-message-actions": {
72
+ "types": "./dist/plugin-sdk/slack-message-actions.d.ts",
73
+ "default": "./dist/plugin-sdk/slack-message-actions.js"
74
+ },
75
+ "./plugin-sdk/status-helpers": {
76
+ "types": "./dist/plugin-sdk/status-helpers.d.ts",
77
+ "default": "./dist/plugin-sdk/status-helpers.js"
78
+ },
79
+ "./plugin-sdk/temp-path": {
80
+ "types": "./dist/plugin-sdk/temp-path.d.ts",
81
+ "default": "./dist/plugin-sdk/temp-path.js"
82
+ },
83
+ "./plugin-sdk/text-chunking": {
84
+ "types": "./dist/plugin-sdk/text-chunking.d.ts",
85
+ "default": "./dist/plugin-sdk/text-chunking.js"
86
+ },
87
+ "./plugin-sdk/tool-send": {
88
+ "types": "./dist/plugin-sdk/tool-send.d.ts",
89
+ "default": "./dist/plugin-sdk/tool-send.js"
90
+ },
91
+ "./plugin-sdk/webhook-path": {
92
+ "types": "./dist/plugin-sdk/webhook-path.d.ts",
93
+ "default": "./dist/plugin-sdk/webhook-path.js"
94
+ },
95
+ "./plugin-sdk/webhook-targets": {
96
+ "types": "./dist/plugin-sdk/webhook-targets.d.ts",
97
+ "default": "./dist/plugin-sdk/webhook-targets.js"
98
+ },
99
+ "./plugin-sdk/windows-spawn": {
100
+ "types": "./dist/plugin-sdk/windows-spawn.d.ts",
101
+ "default": "./dist/plugin-sdk/windows-spawn.js"
102
+ },
29
103
  "./cli-entry": "./dist/entry.js"
30
104
  },
31
105
  "scripts": {