agent-browser 0.1.2 → 0.2.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.
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Enhanced snapshot with element refs for deterministic element selection.
3
+ *
4
+ * This module generates accessibility snapshots with embedded refs that can be
5
+ * used to click/fill/interact with elements without re-querying the DOM.
6
+ *
7
+ * Example output:
8
+ * - heading "Example Domain" [ref=e1] [level=1]
9
+ * - paragraph: Some text content
10
+ * - button "Submit" [ref=e2]
11
+ * - textbox "Email" [ref=e3]
12
+ *
13
+ * Usage:
14
+ * agent-browser snapshot # Full snapshot
15
+ * agent-browser snapshot -i # Interactive elements only
16
+ * agent-browser snapshot --depth 3 # Limit depth
17
+ * agent-browser click @e2 # Click element by ref
18
+ */
19
+ // Counter for generating refs
20
+ let refCounter = 0;
21
+ /**
22
+ * Reset ref counter (call at start of each snapshot)
23
+ */
24
+ export function resetRefs() {
25
+ refCounter = 0;
26
+ }
27
+ /**
28
+ * Generate next ref ID
29
+ */
30
+ function nextRef() {
31
+ return `e${++refCounter}`;
32
+ }
33
+ /**
34
+ * Roles that are interactive and should get refs
35
+ */
36
+ const INTERACTIVE_ROLES = new Set([
37
+ 'button',
38
+ 'link',
39
+ 'textbox',
40
+ 'checkbox',
41
+ 'radio',
42
+ 'combobox',
43
+ 'listbox',
44
+ 'menuitem',
45
+ 'menuitemcheckbox',
46
+ 'menuitemradio',
47
+ 'option',
48
+ 'searchbox',
49
+ 'slider',
50
+ 'spinbutton',
51
+ 'switch',
52
+ 'tab',
53
+ 'treeitem',
54
+ ]);
55
+ /**
56
+ * Roles that provide structure/context (get refs for text extraction)
57
+ */
58
+ const CONTENT_ROLES = new Set([
59
+ 'heading',
60
+ 'cell',
61
+ 'gridcell',
62
+ 'columnheader',
63
+ 'rowheader',
64
+ 'listitem',
65
+ 'article',
66
+ 'region',
67
+ 'main',
68
+ 'navigation',
69
+ ]);
70
+ /**
71
+ * Roles that are purely structural (can be filtered in compact mode)
72
+ */
73
+ const STRUCTURAL_ROLES = new Set([
74
+ 'generic',
75
+ 'group',
76
+ 'list',
77
+ 'table',
78
+ 'row',
79
+ 'rowgroup',
80
+ 'grid',
81
+ 'treegrid',
82
+ 'menu',
83
+ 'menubar',
84
+ 'toolbar',
85
+ 'tablist',
86
+ 'tree',
87
+ 'directory',
88
+ 'document',
89
+ 'application',
90
+ 'presentation',
91
+ 'none',
92
+ ]);
93
+ /**
94
+ * Build a selector string for storing in ref map
95
+ */
96
+ function buildSelector(role, name) {
97
+ if (name) {
98
+ const escapedName = name.replace(/"/g, '\\"');
99
+ return `getByRole('${role}', { name: "${escapedName}" })`;
100
+ }
101
+ return `getByRole('${role}')`;
102
+ }
103
+ /**
104
+ * Get enhanced snapshot with refs and optional filtering
105
+ */
106
+ export async function getEnhancedSnapshot(page, options = {}) {
107
+ resetRefs();
108
+ const refs = {};
109
+ // Get ARIA snapshot from Playwright
110
+ const locator = options.selector ? page.locator(options.selector) : page.locator(':root');
111
+ const ariaTree = await locator.ariaSnapshot();
112
+ if (!ariaTree) {
113
+ return {
114
+ tree: '(empty)',
115
+ refs: {},
116
+ };
117
+ }
118
+ // Parse and enhance the ARIA tree
119
+ const enhancedTree = processAriaTree(ariaTree, refs, options);
120
+ return { tree: enhancedTree, refs };
121
+ }
122
+ /**
123
+ * Process ARIA snapshot: add refs and apply filters
124
+ */
125
+ function processAriaTree(ariaTree, refs, options) {
126
+ const lines = ariaTree.split('\n');
127
+ const result = [];
128
+ // For interactive-only mode, we collect just interactive elements
129
+ if (options.interactive) {
130
+ for (const line of lines) {
131
+ const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
132
+ if (!match)
133
+ continue;
134
+ const [, , role, name, suffix] = match;
135
+ const roleLower = role.toLowerCase();
136
+ if (INTERACTIVE_ROLES.has(roleLower)) {
137
+ const ref = nextRef();
138
+ refs[ref] = {
139
+ selector: buildSelector(roleLower, name),
140
+ role: roleLower,
141
+ name,
142
+ };
143
+ let enhanced = `- ${role}`;
144
+ if (name)
145
+ enhanced += ` "${name}"`;
146
+ enhanced += ` [ref=${ref}]`;
147
+ if (suffix && suffix.includes('['))
148
+ enhanced += suffix;
149
+ result.push(enhanced);
150
+ }
151
+ }
152
+ return result.join('\n') || '(no interactive elements)';
153
+ }
154
+ // Normal processing with depth/compact filters
155
+ for (const line of lines) {
156
+ const processed = processLine(line, refs, options);
157
+ if (processed !== null) {
158
+ result.push(processed);
159
+ }
160
+ }
161
+ // If compact mode, remove empty structural elements
162
+ if (options.compact) {
163
+ return compactTree(result.join('\n'));
164
+ }
165
+ return result.join('\n');
166
+ }
167
+ /**
168
+ * Get indentation level (number of spaces / 2)
169
+ */
170
+ function getIndentLevel(line) {
171
+ const match = line.match(/^(\s*)/);
172
+ return match ? Math.floor(match[1].length / 2) : 0;
173
+ }
174
+ /**
175
+ * Process a single line: add ref if needed, filter if requested
176
+ */
177
+ function processLine(line, refs, options) {
178
+ const depth = getIndentLevel(line);
179
+ // Check max depth
180
+ if (options.maxDepth !== undefined && depth > options.maxDepth) {
181
+ return null;
182
+ }
183
+ // Match lines like:
184
+ // - button "Submit"
185
+ // - heading "Title" [level=1]
186
+ // - link "Click me":
187
+ const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
188
+ if (!match) {
189
+ // Metadata lines (like /url:) or text content
190
+ if (options.interactive) {
191
+ // In interactive mode, only keep metadata under interactive elements
192
+ return null;
193
+ }
194
+ return line;
195
+ }
196
+ const [, prefix, role, name, suffix] = match;
197
+ const roleLower = role.toLowerCase();
198
+ // Skip metadata lines (like /url:)
199
+ if (role.startsWith('/')) {
200
+ return line;
201
+ }
202
+ const isInteractive = INTERACTIVE_ROLES.has(roleLower);
203
+ const isContent = CONTENT_ROLES.has(roleLower);
204
+ const isStructural = STRUCTURAL_ROLES.has(roleLower);
205
+ // In interactive-only mode, filter non-interactive elements
206
+ if (options.interactive && !isInteractive) {
207
+ return null;
208
+ }
209
+ // In compact mode, skip unnamed structural elements
210
+ if (options.compact && isStructural && !name) {
211
+ return null;
212
+ }
213
+ // Add ref for interactive or named content elements
214
+ const shouldHaveRef = isInteractive || (isContent && name);
215
+ if (shouldHaveRef) {
216
+ const ref = nextRef();
217
+ refs[ref] = {
218
+ selector: buildSelector(roleLower, name),
219
+ role: roleLower,
220
+ name,
221
+ };
222
+ // Build enhanced line with ref
223
+ let enhanced = `${prefix}${role}`;
224
+ if (name)
225
+ enhanced += ` "${name}"`;
226
+ enhanced += ` [ref=${ref}]`;
227
+ if (suffix)
228
+ enhanced += suffix;
229
+ return enhanced;
230
+ }
231
+ return line;
232
+ }
233
+ /**
234
+ * Remove empty structural branches in compact mode
235
+ */
236
+ function compactTree(tree) {
237
+ const lines = tree.split('\n');
238
+ const result = [];
239
+ // Simple pass: keep lines that have content or refs
240
+ for (let i = 0; i < lines.length; i++) {
241
+ const line = lines[i];
242
+ // Always keep lines with refs
243
+ if (line.includes('[ref=')) {
244
+ result.push(line);
245
+ continue;
246
+ }
247
+ // Keep lines with text content (after :)
248
+ if (line.includes(':') && !line.endsWith(':')) {
249
+ result.push(line);
250
+ continue;
251
+ }
252
+ // Check if this structural element has children with refs
253
+ const currentIndent = getIndentLevel(line);
254
+ let hasRelevantChildren = false;
255
+ for (let j = i + 1; j < lines.length; j++) {
256
+ const childIndent = getIndentLevel(lines[j]);
257
+ if (childIndent <= currentIndent)
258
+ break;
259
+ if (lines[j].includes('[ref=')) {
260
+ hasRelevantChildren = true;
261
+ break;
262
+ }
263
+ }
264
+ if (hasRelevantChildren) {
265
+ result.push(line);
266
+ }
267
+ }
268
+ return result.join('\n');
269
+ }
270
+ /**
271
+ * Parse a ref from command argument (e.g., "@e1" -> "e1")
272
+ */
273
+ export function parseRef(arg) {
274
+ if (arg.startsWith('@')) {
275
+ return arg.slice(1);
276
+ }
277
+ if (arg.startsWith('ref=')) {
278
+ return arg.slice(4);
279
+ }
280
+ if (/^e\d+$/.test(arg)) {
281
+ return arg;
282
+ }
283
+ return null;
284
+ }
285
+ /**
286
+ * Get snapshot statistics
287
+ */
288
+ export function getSnapshotStats(tree, refs) {
289
+ const interactive = Object.values(refs).filter(r => INTERACTIVE_ROLES.has(r.role)).length;
290
+ return {
291
+ lines: tree.split('\n').length,
292
+ chars: tree.length,
293
+ tokens: Math.ceil(tree.length / 4),
294
+ refs: Object.keys(refs).length,
295
+ interactive,
296
+ };
297
+ }
298
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../src/snapshot.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-browser",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Headless browser automation CLI for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,7 +8,8 @@
8
8
  "agent-browser": "./bin/agent-browser"
9
9
  },
10
10
  "scripts": {
11
- "build": "tsc",
11
+ "build": "tsc && bun build ./src/cli-light.ts --compile --outfile ./bin/agent-browser",
12
+ "build:node": "tsc",
12
13
  "start": "node dist/index.js",
13
14
  "dev": "tsx src/index.ts",
14
15
  "typecheck": "tsc --noEmit",
@@ -29,12 +30,12 @@
29
30
  "author": "",
30
31
  "license": "Apache-2.0",
31
32
  "dependencies": {
32
- "playwright-core": "^1.40.0",
33
+ "playwright-core": "^1.57.0",
33
34
  "zod": "^3.22.4"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/node": "^20.10.0",
37
- "playwright": "^1.40.0",
38
+ "playwright": "^1.57.0",
38
39
  "prettier": "^3.7.4",
39
40
  "tsx": "^4.6.0",
40
41
  "typescript": "^5.3.0",
package/src/actions.ts CHANGED
@@ -108,6 +108,7 @@ import { successResponse, errorResponse } from './protocol.js';
108
108
  // Snapshot response type
109
109
  interface SnapshotData {
110
110
  snapshot: string;
111
+ refs?: Record<string, { role: string; name?: string }>;
111
112
  }
112
113
 
113
114
  /**
@@ -380,8 +381,10 @@ async function handleNavigate(
380
381
  }
381
382
 
382
383
  async function handleClick(command: ClickCommand, browser: BrowserManager): Promise<Response> {
383
- const page = browser.getPage();
384
- await page.click(command.selector, {
384
+ // Support both refs (@e1) and regular selectors
385
+ const locator = browser.getLocator(command.selector);
386
+
387
+ await locator.click({
385
388
  button: command.button,
386
389
  clickCount: command.clickCount,
387
390
  delay: command.delay,
@@ -391,13 +394,13 @@ async function handleClick(command: ClickCommand, browser: BrowserManager): Prom
391
394
  }
392
395
 
393
396
  async function handleType(command: TypeCommand, browser: BrowserManager): Promise<Response> {
394
- const page = browser.getPage();
397
+ const locator = browser.getLocator(command.selector);
395
398
 
396
399
  if (command.clear) {
397
- await page.fill(command.selector, '');
400
+ await locator.fill('');
398
401
  }
399
402
 
400
- await page.type(command.selector, command.text, {
403
+ await locator.pressSequentially(command.text, {
401
404
  delay: command.delay,
402
405
  });
403
406
 
@@ -446,15 +449,26 @@ async function handleScreenshot(
446
449
  }
447
450
 
448
451
  async function handleSnapshot(
449
- command: Command & { action: 'snapshot' },
452
+ command: Command & { action: 'snapshot'; interactive?: boolean; maxDepth?: number; compact?: boolean; selector?: string },
450
453
  browser: BrowserManager
451
454
  ): Promise<Response<SnapshotData>> {
452
- const page = browser.getPage();
453
- // Use ariaSnapshot which returns a string representation of the accessibility tree
454
- const snapshot = await page.locator(':root').ariaSnapshot();
455
+ // Use enhanced snapshot with refs and optional filtering
456
+ const { tree, refs } = await browser.getSnapshot({
457
+ interactive: command.interactive,
458
+ maxDepth: command.maxDepth,
459
+ compact: command.compact,
460
+ selector: command.selector,
461
+ });
462
+
463
+ // Simplify refs for output (just role and name)
464
+ const simpleRefs: Record<string, { role: string; name?: string }> = {};
465
+ for (const [ref, data] of Object.entries(refs)) {
466
+ simpleRefs[ref] = { role: data.role, name: data.name };
467
+ }
455
468
 
456
469
  return successResponse(command.id, {
457
- snapshot: snapshot ?? 'Empty page',
470
+ snapshot: tree || 'Empty page',
471
+ refs: Object.keys(simpleRefs).length > 0 ? simpleRefs : undefined,
458
472
  });
459
473
  }
460
474
 
@@ -533,17 +547,17 @@ async function handleScroll(command: ScrollCommand, browser: BrowserManager): Pr
533
547
  }
534
548
 
535
549
  async function handleSelect(command: SelectCommand, browser: BrowserManager): Promise<Response> {
536
- const page = browser.getPage();
550
+ const locator = browser.getLocator(command.selector);
537
551
  const values = Array.isArray(command.values) ? command.values : [command.values];
538
552
 
539
- await page.selectOption(command.selector, values);
553
+ await locator.selectOption(values);
540
554
 
541
555
  return successResponse(command.id, { selected: values });
542
556
  }
543
557
 
544
558
  async function handleHover(command: HoverCommand, browser: BrowserManager): Promise<Response> {
545
- const page = browser.getPage();
546
- await page.hover(command.selector);
559
+ const locator = browser.getLocator(command.selector);
560
+ await locator.hover();
547
561
 
548
562
  return successResponse(command.id, { hovered: true });
549
563
  }
@@ -622,27 +636,27 @@ async function handleWindowNew(
622
636
  // New handlers for enhanced Playwright parity
623
637
 
624
638
  async function handleFill(command: FillCommand, browser: BrowserManager): Promise<Response> {
625
- const frame = browser.getFrame();
626
- await frame.fill(command.selector, command.value);
639
+ const locator = browser.getLocator(command.selector);
640
+ await locator.fill(command.value);
627
641
  return successResponse(command.id, { filled: true });
628
642
  }
629
643
 
630
644
  async function handleCheck(command: CheckCommand, browser: BrowserManager): Promise<Response> {
631
- const frame = browser.getFrame();
632
- await frame.check(command.selector);
645
+ const locator = browser.getLocator(command.selector);
646
+ await locator.check();
633
647
  return successResponse(command.id, { checked: true });
634
648
  }
635
649
 
636
650
  async function handleUncheck(command: UncheckCommand, browser: BrowserManager): Promise<Response> {
637
- const frame = browser.getFrame();
638
- await frame.uncheck(command.selector);
651
+ const locator = browser.getLocator(command.selector);
652
+ await locator.uncheck();
639
653
  return successResponse(command.id, { unchecked: true });
640
654
  }
641
655
 
642
656
  async function handleUpload(command: UploadCommand, browser: BrowserManager): Promise<Response> {
643
- const frame = browser.getFrame();
657
+ const locator = browser.getLocator(command.selector);
644
658
  const files = Array.isArray(command.files) ? command.files : [command.files];
645
- await frame.setInputFiles(command.selector, files);
659
+ await locator.setInputFiles(files);
646
660
  return successResponse(command.id, { uploaded: files });
647
661
  }
648
662
 
@@ -650,14 +664,14 @@ async function handleDoubleClick(
650
664
  command: DoubleClickCommand,
651
665
  browser: BrowserManager
652
666
  ): Promise<Response> {
653
- const frame = browser.getFrame();
654
- await frame.dblclick(command.selector);
667
+ const locator = browser.getLocator(command.selector);
668
+ await locator.dblclick();
655
669
  return successResponse(command.id, { clicked: true });
656
670
  }
657
671
 
658
672
  async function handleFocus(command: FocusCommand, browser: BrowserManager): Promise<Response> {
659
- const frame = browser.getFrame();
660
- await frame.focus(command.selector);
673
+ const locator = browser.getLocator(command.selector);
674
+ await locator.focus();
661
675
  return successResponse(command.id, { focused: true });
662
676
  }
663
677
 
@@ -1017,14 +1031,14 @@ async function handleGetAttribute(
1017
1031
  command: GetAttributeCommand,
1018
1032
  browser: BrowserManager
1019
1033
  ): Promise<Response> {
1020
- const page = browser.getPage();
1021
- const value = await page.getAttribute(command.selector, command.attribute);
1034
+ const locator = browser.getLocator(command.selector);
1035
+ const value = await locator.getAttribute(command.attribute);
1022
1036
  return successResponse(command.id, { attribute: command.attribute, value });
1023
1037
  }
1024
1038
 
1025
1039
  async function handleGetText(command: GetTextCommand, browser: BrowserManager): Promise<Response> {
1026
- const page = browser.getPage();
1027
- const text = await page.textContent(command.selector);
1040
+ const locator = browser.getLocator(command.selector);
1041
+ const text = await locator.textContent();
1028
1042
  return successResponse(command.id, { text });
1029
1043
  }
1030
1044
 
@@ -1032,8 +1046,8 @@ async function handleIsVisible(
1032
1046
  command: IsVisibleCommand,
1033
1047
  browser: BrowserManager
1034
1048
  ): Promise<Response> {
1035
- const page = browser.getPage();
1036
- const visible = await page.isVisible(command.selector);
1049
+ const locator = browser.getLocator(command.selector);
1050
+ const visible = await locator.isVisible();
1037
1051
  return successResponse(command.id, { visible });
1038
1052
  }
1039
1053
 
@@ -1041,8 +1055,8 @@ async function handleIsEnabled(
1041
1055
  command: IsEnabledCommand,
1042
1056
  browser: BrowserManager
1043
1057
  ): Promise<Response> {
1044
- const page = browser.getPage();
1045
- const enabled = await page.isEnabled(command.selector);
1058
+ const locator = browser.getLocator(command.selector);
1059
+ const enabled = await locator.isEnabled();
1046
1060
  return successResponse(command.id, { enabled });
1047
1061
  }
1048
1062
 
@@ -1050,8 +1064,8 @@ async function handleIsChecked(
1050
1064
  command: IsCheckedCommand,
1051
1065
  browser: BrowserManager
1052
1066
  ): Promise<Response> {
1053
- const page = browser.getPage();
1054
- const checked = await page.isChecked(command.selector);
1067
+ const locator = browser.getLocator(command.selector);
1068
+ const checked = await locator.isChecked();
1055
1069
  return successResponse(command.id, { checked });
1056
1070
  }
1057
1071
 
package/src/browser.ts CHANGED
@@ -10,8 +10,10 @@ import {
10
10
  type Dialog,
11
11
  type Request,
12
12
  type Route,
13
+ type Locator,
13
14
  } from 'playwright-core';
14
15
  import type { LaunchCommand } from './types.js';
16
+ import { type RefMap, type EnhancedSnapshot, getEnhancedSnapshot, parseRef } from './snapshot.js';
15
17
 
16
18
  interface TrackedRequest {
17
19
  url: string;
@@ -47,6 +49,8 @@ export class BrowserManager {
47
49
  private consoleMessages: ConsoleMessage[] = [];
48
50
  private pageErrors: PageError[] = [];
49
51
  private isRecordingHar: boolean = false;
52
+ private refMap: RefMap = {};
53
+ private lastSnapshot: string = '';
50
54
 
51
55
  /**
52
56
  * Check if browser is launched
@@ -55,6 +59,70 @@ export class BrowserManager {
55
59
  return this.browser !== null;
56
60
  }
57
61
 
62
+ /**
63
+ * Get enhanced snapshot with refs and cache the ref map
64
+ */
65
+ async getSnapshot(options?: {
66
+ interactive?: boolean;
67
+ maxDepth?: number;
68
+ compact?: boolean;
69
+ selector?: string;
70
+ }): Promise<EnhancedSnapshot> {
71
+ const page = this.getPage();
72
+ const snapshot = await getEnhancedSnapshot(page, options);
73
+ this.refMap = snapshot.refs;
74
+ this.lastSnapshot = snapshot.tree;
75
+ return snapshot;
76
+ }
77
+
78
+ /**
79
+ * Get the cached ref map from last snapshot
80
+ */
81
+ getRefMap(): RefMap {
82
+ return this.refMap;
83
+ }
84
+
85
+ /**
86
+ * Get a locator from a ref (e.g., "e1", "@e1", "ref=e1")
87
+ * Returns null if ref doesn't exist or is invalid
88
+ */
89
+ getLocatorFromRef(refArg: string): Locator | null {
90
+ const ref = parseRef(refArg);
91
+ if (!ref) return null;
92
+
93
+ const refData = this.refMap[ref];
94
+ if (!refData) return null;
95
+
96
+ const page = this.getPage();
97
+
98
+ // Parse the selector and create locator
99
+ if (refData.name) {
100
+ return page.getByRole(refData.role as any, { name: refData.name });
101
+ } else {
102
+ return page.getByRole(refData.role as any);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Check if a selector looks like a ref
108
+ */
109
+ isRef(selector: string): boolean {
110
+ return parseRef(selector) !== null;
111
+ }
112
+
113
+ /**
114
+ * Get locator - supports both refs and regular selectors
115
+ */
116
+ getLocator(selectorOrRef: string): Locator {
117
+ // Check if it's a ref first
118
+ const locator = this.getLocatorFromRef(selectorOrRef);
119
+ if (locator) return locator;
120
+
121
+ // Otherwise treat as regular selector
122
+ const page = this.getPage();
123
+ return page.locator(selectorOrRef);
124
+ }
125
+
58
126
  /**
59
127
  * Get the current active page, throws if not launched
60
128
  */
@@ -428,11 +496,12 @@ export class BrowserManager {
428
496
 
429
497
  /**
430
498
  * Launch the browser with the specified options
499
+ * If already launched, this is a no-op (browser stays open)
431
500
  */
432
501
  async launch(options: LaunchCommand): Promise<void> {
433
- // Close existing browser if any
502
+ // If already launched, don't relaunch
434
503
  if (this.browser) {
435
- await this.close();
504
+ return;
436
505
  }
437
506
 
438
507
  // Select browser type
@@ -611,5 +680,7 @@ export class BrowserManager {
611
680
  }
612
681
 
613
682
  this.activePageIndex = 0;
683
+ this.refMap = {};
684
+ this.lastSnapshot = '';
614
685
  }
615
686
  }