btcp-browser-agent 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.
Files changed (117) hide show
  1. package/CLAUDE.md +230 -0
  2. package/LICENSE +21 -0
  3. package/README.md +309 -0
  4. package/SKILL.md +143 -0
  5. package/SNAPSHOT_IMPROVEMENTS.md +302 -0
  6. package/USAGE.md +146 -0
  7. package/dist/index.d.ts +34 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +35 -0
  10. package/dist/index.js.map +1 -0
  11. package/docs/browser-cli-design.md +500 -0
  12. package/examples/chrome-extension/CHANGELOG.md +210 -0
  13. package/examples/chrome-extension/DEBUG.md +231 -0
  14. package/examples/chrome-extension/ERROR_FIXED.md +147 -0
  15. package/examples/chrome-extension/QUICK_TEST.md +189 -0
  16. package/examples/chrome-extension/README.md +149 -0
  17. package/examples/chrome-extension/SESSION_ONLY_MODE.md +305 -0
  18. package/examples/chrome-extension/TEST_WITH_YOUR_TABS.md +97 -0
  19. package/examples/chrome-extension/build.js +43 -0
  20. package/examples/chrome-extension/manifest.json +37 -0
  21. package/examples/chrome-extension/package-lock.json +1063 -0
  22. package/examples/chrome-extension/package.json +21 -0
  23. package/examples/chrome-extension/popup.html +195 -0
  24. package/examples/chrome-extension/src/background.ts +12 -0
  25. package/examples/chrome-extension/src/content.ts +7 -0
  26. package/examples/chrome-extension/src/popup.ts +303 -0
  27. package/examples/chrome-extension/src/scenario-google-github.ts +389 -0
  28. package/examples/chrome-extension/test-page.html +127 -0
  29. package/examples/chrome-extension/tests/README.md +206 -0
  30. package/examples/chrome-extension/tests/scenario-google-to-github-star.ts +380 -0
  31. package/examples/chrome-extension/tsconfig.json +14 -0
  32. package/examples/snapshots/README.md +207 -0
  33. package/examples/snapshots/amazon-com-detail.html +9528 -0
  34. package/examples/snapshots/amazon-com-detail.snapshot.txt +997 -0
  35. package/examples/snapshots/convert-snapshots.ts +97 -0
  36. package/examples/snapshots/edition-cnn-com.html +13292 -0
  37. package/examples/snapshots/edition-cnn-com.snapshot.txt +562 -0
  38. package/examples/snapshots/github-com-microsoft-vscode.html +2916 -0
  39. package/examples/snapshots/github-com-microsoft-vscode.snapshot.txt +455 -0
  40. package/examples/snapshots/google-search.html +20012 -0
  41. package/examples/snapshots/google-search.snapshot.txt +195 -0
  42. package/examples/snapshots/metadata.json +86 -0
  43. package/examples/snapshots/npr-org-templates.html +2031 -0
  44. package/examples/snapshots/npr-org-templates.snapshot.txt +224 -0
  45. package/examples/snapshots/stackoverflow-com.html +5216 -0
  46. package/examples/snapshots/stackoverflow-com.snapshot.txt +2404 -0
  47. package/examples/snapshots/test-all-mode.html +46 -0
  48. package/examples/snapshots/test-all-mode.snapshot.txt +5 -0
  49. package/examples/snapshots/validate.test.ts +296 -0
  50. package/package.json +65 -0
  51. package/packages/cli/package.json +42 -0
  52. package/packages/cli/src/__tests__/cli.test.ts +434 -0
  53. package/packages/cli/src/__tests__/errors.test.ts +226 -0
  54. package/packages/cli/src/__tests__/executor.test.ts +275 -0
  55. package/packages/cli/src/__tests__/formatter.test.ts +260 -0
  56. package/packages/cli/src/__tests__/parser.test.ts +288 -0
  57. package/packages/cli/src/__tests__/suggestions.test.ts +255 -0
  58. package/packages/cli/src/commands/back.ts +22 -0
  59. package/packages/cli/src/commands/check.ts +33 -0
  60. package/packages/cli/src/commands/clear.ts +33 -0
  61. package/packages/cli/src/commands/click.ts +32 -0
  62. package/packages/cli/src/commands/closetab.ts +31 -0
  63. package/packages/cli/src/commands/eval.ts +41 -0
  64. package/packages/cli/src/commands/fill.ts +30 -0
  65. package/packages/cli/src/commands/focus.ts +33 -0
  66. package/packages/cli/src/commands/forward.ts +22 -0
  67. package/packages/cli/src/commands/goto.ts +34 -0
  68. package/packages/cli/src/commands/help.ts +162 -0
  69. package/packages/cli/src/commands/hover.ts +34 -0
  70. package/packages/cli/src/commands/index.ts +129 -0
  71. package/packages/cli/src/commands/newtab.ts +35 -0
  72. package/packages/cli/src/commands/press.ts +40 -0
  73. package/packages/cli/src/commands/reload.ts +25 -0
  74. package/packages/cli/src/commands/screenshot.ts +27 -0
  75. package/packages/cli/src/commands/scroll.ts +64 -0
  76. package/packages/cli/src/commands/select.ts +35 -0
  77. package/packages/cli/src/commands/snapshot.ts +21 -0
  78. package/packages/cli/src/commands/tab.ts +32 -0
  79. package/packages/cli/src/commands/tabs.ts +26 -0
  80. package/packages/cli/src/commands/text.ts +27 -0
  81. package/packages/cli/src/commands/title.ts +17 -0
  82. package/packages/cli/src/commands/type.ts +38 -0
  83. package/packages/cli/src/commands/uncheck.ts +33 -0
  84. package/packages/cli/src/commands/url.ts +17 -0
  85. package/packages/cli/src/commands/wait.ts +54 -0
  86. package/packages/cli/src/errors.ts +164 -0
  87. package/packages/cli/src/executor.ts +68 -0
  88. package/packages/cli/src/formatter.ts +215 -0
  89. package/packages/cli/src/index.ts +257 -0
  90. package/packages/cli/src/parser.ts +195 -0
  91. package/packages/cli/src/suggestions.ts +207 -0
  92. package/packages/cli/src/terminal/Terminal.ts +365 -0
  93. package/packages/cli/src/terminal/index.ts +5 -0
  94. package/packages/cli/src/types.ts +155 -0
  95. package/packages/cli/tsconfig.json +20 -0
  96. package/packages/core/package.json +35 -0
  97. package/packages/core/src/actions.ts +1210 -0
  98. package/packages/core/src/errors.ts +296 -0
  99. package/packages/core/src/index.test.ts +638 -0
  100. package/packages/core/src/index.ts +220 -0
  101. package/packages/core/src/ref-map.ts +107 -0
  102. package/packages/core/src/snapshot.ts +873 -0
  103. package/packages/core/src/types.ts +536 -0
  104. package/packages/core/tsconfig.json +23 -0
  105. package/packages/extension/README.md +129 -0
  106. package/packages/extension/package.json +43 -0
  107. package/packages/extension/src/background.ts +888 -0
  108. package/packages/extension/src/content.ts +172 -0
  109. package/packages/extension/src/index.ts +579 -0
  110. package/packages/extension/src/session-manager.ts +385 -0
  111. package/packages/extension/src/session-types.ts +144 -0
  112. package/packages/extension/src/types.ts +162 -0
  113. package/packages/extension/tsconfig.json +28 -0
  114. package/src/index.ts +64 -0
  115. package/tsconfig.build.json +12 -0
  116. package/tsconfig.json +26 -0
  117. package/vitest.config.ts +13 -0
@@ -0,0 +1,46 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Test All Mode - Images, Headings, and Text</title>
7
+ </head>
8
+ <body>
9
+ <header>
10
+ <h1>Welcome to Our Website</h1>
11
+ <img src="/images/logo.png" alt="Company Logo" />
12
+ </header>
13
+
14
+ <main>
15
+ <section>
16
+ <h2>About Us</h2>
17
+ <p>We are a leading provider of innovative solutions for modern businesses. Our team has over 20 years of experience in delivering exceptional results.</p>
18
+ <img src="/images/team.jpg" alt="Our dedicated team working together" title="Team collaboration" />
19
+ </section>
20
+
21
+ <section>
22
+ <h2>Our Services</h2>
23
+ <h3>Consulting</h3>
24
+ <p>Expert consulting services to help your business grow and thrive in today's competitive market.</p>
25
+
26
+ <h3>Development</h3>
27
+ <p>Custom software development tailored to your specific needs and requirements.</p>
28
+ <img src="/images/code.png" alt="Code editor showing programming" />
29
+ </section>
30
+
31
+ <section>
32
+ <h2>Contact Us</h2>
33
+ <form>
34
+ <label for="email">Email:</label>
35
+ <input type="email" id="email" name="email" />
36
+ <button type="submit">Submit</button>
37
+ </form>
38
+ </section>
39
+ </main>
40
+
41
+ <footer>
42
+ <p>Copyright 2025 - All rights reserved</p>
43
+ <img src="footer-logo.png" alt="Footer brand logo" />
44
+ </footer>
45
+ </body>
46
+ </html>
@@ -0,0 +1,5 @@
1
+ PAGE: http://localhost/ | Test All Mode - Images, Headings, and Text | viewport=1024x768
2
+ SNAPSHOT: elements=25 refs=2
3
+
4
+ TEXTBOX "Email:" @ref:0 [type=email] /main/section[3]/form/input#email
5
+ BUTTON "Submit" @ref:1 /main/section[3]/form/button
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Validation tests for real-world HTML snapshots
3
+ *
4
+ * Tests snapshot structure and smart label selection against real-world websites
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest';
8
+ import { readFileSync, readdirSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { JSDOM, VirtualConsole } from 'jsdom';
11
+ import { createSnapshot } from '../../packages/core/src/snapshot.js';
12
+ import { createSimpleRefMap } from '../../packages/core/src/ref-map.js';
13
+
14
+ const SNAPSHOTS_DIR = join(__dirname);
15
+
16
+ // Helper to load HTML and generate snapshot
17
+ function generateSnapshotFromHTML(htmlPath: string) {
18
+ const html = readFileSync(htmlPath, 'utf-8');
19
+
20
+ const virtualConsole = new VirtualConsole();
21
+ virtualConsole.on('error', () => {});
22
+ virtualConsole.on('warn', () => {});
23
+
24
+ const dom = new JSDOM(html, {
25
+ url: 'http://localhost',
26
+ contentType: 'text/html',
27
+ pretendToBeVisual: true,
28
+ virtualConsole,
29
+ });
30
+
31
+ const { document } = dom.window;
32
+ const refMap = createSimpleRefMap();
33
+
34
+ return createSnapshot(document, refMap, {
35
+ interactive: true,
36
+ compact: true,
37
+ maxDepth: 10,
38
+ includeHidden: false,
39
+ });
40
+ }
41
+
42
+ // Find all HTML test files
43
+ const htmlFiles = readdirSync(SNAPSHOTS_DIR)
44
+ .filter(f => f.endsWith('.html'))
45
+ .map(f => ({
46
+ name: f.replace('.html', ''),
47
+ path: join(SNAPSHOTS_DIR, f),
48
+ }));
49
+
50
+ describe('Real-World Snapshot Validation', () => {
51
+ describe('Snapshot Structure', () => {
52
+ htmlFiles.forEach(({ name, path }) => {
53
+ describe(name, () => {
54
+ let snapshot: ReturnType<typeof createSnapshot>;
55
+
56
+ beforeEach(() => {
57
+ try {
58
+ snapshot = generateSnapshotFromHTML(path);
59
+ } catch (error) {
60
+ // Skip files that fail to load (like amazon-com.html with invalid selectors)
61
+ snapshot = { tree: '', refs: {} };
62
+ }
63
+ });
64
+
65
+ it('should generate snapshot without errors', () => {
66
+ expect(snapshot).toBeDefined();
67
+ expect(typeof snapshot).toBe('string');
68
+ expect(snapshot.length).toBeGreaterThan(0);
69
+ });
70
+
71
+ it('should include PAGE header with URL, title, and viewport', () => {
72
+ const lines = snapshot.split('\n');
73
+ const pageHeader = lines[0];
74
+
75
+ if (pageHeader && pageHeader.startsWith('PAGE:')) {
76
+ expect(pageHeader).toMatch(/^PAGE: .+ \| .+ \| viewport=\d+x\d+$/);
77
+ }
78
+ });
79
+
80
+ it('should include SNAPSHOT header with statistics', () => {
81
+ const lines = snapshot.split('\n');
82
+ const snapshotHeader = lines.find(l => l.startsWith('SNAPSHOT:'));
83
+
84
+ if (snapshotHeader) {
85
+ expect(snapshotHeader).toMatch(/SNAPSHOT: elements=\d+/);
86
+ expect(snapshotHeader).toMatch(/depth=\d+\/\d+/);
87
+ expect(snapshotHeader).toMatch(/mode=/);
88
+ }
89
+ });
90
+
91
+ it('should format heading levels correctly', () => {
92
+ const lines = snapshot.split('\n');
93
+ const headings = lines.filter(l => l.includes('HEADING LEVEL='));
94
+
95
+ headings.forEach(heading => {
96
+ expect(heading).toMatch(/HEADING LEVEL=[1-6]/);
97
+ });
98
+ });
99
+
100
+ it('should show form boundaries where present', () => {
101
+ const lines = snapshot.split('\n');
102
+ const forms = lines.filter(l => l.trim().startsWith('FORM'));
103
+
104
+ forms.forEach(form => {
105
+ // Forms should have id or action attributes
106
+ const hasAttributes = form.includes('id=') || form.includes('action=');
107
+ if (hasAttributes) {
108
+ expect(form).toMatch(/FORM/);
109
+ }
110
+ });
111
+ });
112
+
113
+ it('should show input types and validation attributes', () => {
114
+ const lines = snapshot.split('\n');
115
+ const inputs = lines.filter(l => l.includes('TEXTBOX') || l.includes('CHECKBOX'));
116
+
117
+ inputs.forEach(input => {
118
+ // Check if input has attribute notation
119
+ if (input.includes('[')) {
120
+ expect(input).toMatch(/\[.*\]/);
121
+ // Common attributes: type, required, invalid
122
+ }
123
+ });
124
+ });
125
+
126
+ it('should show children indicators for large elements', () => {
127
+ const lines = snapshot.split('\n');
128
+ const withChildren = lines.filter(l => l.includes('children'));
129
+
130
+ withChildren.forEach(line => {
131
+ // Should show one of: "N children:", "N shown", "hidden by"
132
+ const hasIndicator =
133
+ line.includes('children:') ||
134
+ line.includes('shown') ||
135
+ line.includes('hidden by') ||
136
+ line.includes('filtered');
137
+ expect(hasIndicator).toBe(true);
138
+ });
139
+ });
140
+ });
141
+ });
142
+ });
143
+
144
+ describe('Smart Label Selection', () => {
145
+ htmlFiles.forEach(({ name, path }) => {
146
+ describe(name, () => {
147
+ let snapshot: ReturnType<typeof createSnapshot>;
148
+
149
+ beforeEach(() => {
150
+ try {
151
+ snapshot = generateSnapshotFromHTML(path);
152
+ } catch (error) {
153
+ snapshot = { tree: '', refs: {} };
154
+ }
155
+ });
156
+
157
+ it('should have meaningful labels for buttons', () => {
158
+ const lines = snapshot.split('\n');
159
+ const buttons = lines.filter(l => l.includes('BUTTON'));
160
+
161
+ buttons.forEach(button => {
162
+ // Extract label (text between BUTTON and @ref or end)
163
+ const match = button.match(/BUTTON "([^"]+)"/);
164
+ if (match) {
165
+ const label = match[1];
166
+ expect(label.length).toBeGreaterThan(0);
167
+ // Should not be just whitespace
168
+ expect(label.trim()).toBe(label);
169
+ }
170
+ });
171
+ });
172
+
173
+ it('should show link destinations', () => {
174
+ const lines = snapshot.split('\n');
175
+ const links = lines.filter(l => l.includes('LINK'));
176
+
177
+ const linksWithHref = links.filter(l => l.includes('href='));
178
+
179
+ // At least some links should have href
180
+ if (links.length > 0) {
181
+ expect(linksWithHref.length).toBeGreaterThan(0);
182
+ }
183
+ });
184
+
185
+ it('should have meaningful labels for links', () => {
186
+ const lines = snapshot.split('\n');
187
+ const links = lines.filter(l => l.includes('LINK'));
188
+
189
+ links.forEach(link => {
190
+ // Extract label
191
+ const match = link.match(/LINK "([^"]+)"/);
192
+ if (match) {
193
+ const label = match[1];
194
+ expect(label.length).toBeGreaterThan(0);
195
+ expect(label.trim()).toBe(label);
196
+ }
197
+ });
198
+ });
199
+
200
+ it('should not use placeholders as input labels', () => {
201
+ const lines = snapshot.split('\n');
202
+ const inputs = lines.filter(l =>
203
+ l.includes('TEXTBOX') ||
204
+ l.includes('TEXTAREA') ||
205
+ l.includes('COMBOBOX')
206
+ );
207
+
208
+ // This is a negative test - we can't easily check what WASN'T used
209
+ // But we can verify inputs have labels (from label elements, aria-label, or title)
210
+ inputs.forEach(input => {
211
+ // Should have a label in quotes
212
+ const hasLabel = input.match(/"([^"]+)"/);
213
+ if (hasLabel) {
214
+ const label = hasLabel[1];
215
+ // Label should not be common placeholder text
216
+ const isPlaceholder =
217
+ label.toLowerCase().includes('enter ') ||
218
+ label.toLowerCase().includes('type ') ||
219
+ label.toLowerCase().startsWith('e.g.');
220
+
221
+ // Most inputs should not have placeholder-style labels
222
+ // (This is a soft check since some sites do use these as actual labels)
223
+ }
224
+ });
225
+ });
226
+
227
+ it('should generate refs for interactive elements', () => {
228
+ // Refs are now internal, just count @ref: in output
229
+ const lines = snapshot.split('\n');
230
+ const interactiveLines = lines.filter(l =>
231
+ l.includes('@ref:') ||
232
+ l.includes('BUTTON') ||
233
+ l.includes('LINK') ||
234
+ l.includes('TEXTBOX')
235
+ );
236
+
237
+ // Should have refs for interactive elements
238
+ if (interactiveLines.length > 0) {
239
+ expect(refCount).toBeGreaterThan(0);
240
+ }
241
+ });
242
+
243
+ it.skip('should have bounding boxes in refs', () => {
244
+ // Refs are now internal - this test is skipped
245
+ // The highlight feature still works using internal refs
246
+ expect(true).toBe(true);
247
+
248
+ // Should have bounding box info
249
+ if (ref.bbox) {
250
+ expect(ref.bbox.x).toBeDefined();
251
+ expect(ref.bbox.y).toBeDefined();
252
+ expect(ref.bbox.width).toBeDefined();
253
+ expect(ref.bbox.height).toBeDefined();
254
+ }
255
+
256
+ // Should have viewport detection
257
+ expect(typeof ref.inViewport).toBe('boolean');
258
+ });
259
+ });
260
+ });
261
+ });
262
+ });
263
+
264
+ describe('Performance', () => {
265
+ htmlFiles.forEach(({ name, path }) => {
266
+ it(`${name}: should generate snapshot in reasonable time`, () => {
267
+ const start = performance.now();
268
+
269
+ try {
270
+ generateSnapshotFromHTML(path);
271
+ } catch (error) {
272
+ // Skip invalid files
273
+ }
274
+
275
+ const duration = performance.now() - start;
276
+
277
+ // Should complete in under 5 seconds (generous limit for large pages)
278
+ expect(duration).toBeLessThan(5000);
279
+ });
280
+
281
+ it(`${name}: should generate manageable output size`, () => {
282
+ let snapshot;
283
+ try {
284
+ snapshot = generateSnapshotFromHTML(path);
285
+ } catch (error) {
286
+ snapshot = { tree: '', refs: {} };
287
+ }
288
+
289
+ const sizeKB = snapshot.length / 1024;
290
+
291
+ // Should be under 50KB (adaptive depth should prevent excessive output)
292
+ expect(sizeKB).toBeLessThan(50);
293
+ });
294
+ });
295
+ });
296
+ });
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "btcp-browser-agent",
3
+ "version": "0.1.0",
4
+ "description": "Give AI agents the power to control browsers. A foundation for building agentic systems with smart DOM snapshots and stable element references.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./core": {
14
+ "types": "./packages/core/dist/index.d.ts",
15
+ "import": "./packages/core/dist/index.js"
16
+ },
17
+ "./extension": {
18
+ "types": "./packages/extension/dist/index.d.ts",
19
+ "import": "./packages/extension/dist/index.js"
20
+ },
21
+ "./extension/content": {
22
+ "types": "./packages/extension/dist/content.d.ts",
23
+ "import": "./packages/extension/dist/content.js"
24
+ },
25
+ "./extension/background": {
26
+ "types": "./packages/extension/dist/background.d.ts",
27
+ "import": "./packages/extension/dist/background.js"
28
+ },
29
+ "./cli": {
30
+ "types": "./packages/cli/dist/index.d.ts",
31
+ "import": "./packages/cli/dist/index.js"
32
+ },
33
+ "./cli/terminal": {
34
+ "types": "./packages/cli/dist/terminal/index.d.ts",
35
+ "import": "./packages/cli/dist/terminal/index.js"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "build": "npm run build:packages && tsc -p tsconfig.build.json",
40
+ "build:packages": "tsc -p packages/core/tsconfig.json && tsc -p packages/extension/tsconfig.json && tsc -p packages/cli/tsconfig.json",
41
+ "clean": "rm -rf dist packages/*/dist",
42
+ "prepare": "npm run build",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "typecheck": "tsc --noEmit"
46
+ },
47
+ "workspaces": [
48
+ "packages/core",
49
+ "packages/extension",
50
+ "packages/cli"
51
+ ],
52
+ "license": "Apache-2.0",
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/browser-tool-calling-protocol/btcp-browser-agent.git"
56
+ },
57
+ "dependencies": {},
58
+ "devDependencies": {
59
+ "@types/chrome": "^0.0.268",
60
+ "@types/node": "^20.10.0",
61
+ "jsdom": "^24.0.0",
62
+ "typescript": "^5.3.0",
63
+ "vitest": "^2.0.0"
64
+ }
65
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@btcp/cli",
3
+ "version": "0.1.0",
4
+ "description": "Browser-based CLI for browser automation - runs in Chrome extension",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./terminal": {
14
+ "types": "./dist/terminal/index.d.ts",
15
+ "import": "./dist/terminal/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "clean": "rm -rf dist",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src"
27
+ ],
28
+ "keywords": [
29
+ "browser",
30
+ "automation",
31
+ "cli",
32
+ "chrome-extension"
33
+ ],
34
+ "license": "Apache-2.0",
35
+ "dependencies": {
36
+ "@btcp/extension": "file:../extension"
37
+ },
38
+ "devDependencies": {
39
+ "@types/chrome": "^0.0.268",
40
+ "typescript": "^5.3.0"
41
+ }
42
+ }