wave-code 0.9.7 → 0.10.1

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,740 @@
1
+ import {
2
+ Agent as WaveAgent,
3
+ AgentOptions,
4
+ PermissionDecision,
5
+ ToolPermissionContext,
6
+ AgentToolBlockUpdateParams,
7
+ Task,
8
+ listSessions as listWaveSessions,
9
+ deleteSession as deleteWaveSession,
10
+ } from "wave-agent-sdk";
11
+ import * as fs from "node:fs/promises";
12
+ import * as path from "node:path";
13
+ import { logger } from "../utils/logger.js";
14
+ import {
15
+ type Agent as AcpAgent,
16
+ type AgentSideConnection,
17
+ type InitializeResponse,
18
+ type NewSessionRequest,
19
+ type NewSessionResponse,
20
+ type LoadSessionRequest,
21
+ type LoadSessionResponse,
22
+ type ListSessionsRequest,
23
+ type ListSessionsResponse,
24
+ type PromptRequest,
25
+ type PromptResponse,
26
+ type CancelNotification,
27
+ type AuthenticateResponse,
28
+ type SessionId as AcpSessionId,
29
+ type ToolCallStatus,
30
+ type StopReason,
31
+ type PermissionOption,
32
+ type SessionInfo,
33
+ type ToolCallContent,
34
+ type ToolCallLocation,
35
+ type ToolKind,
36
+ type SessionConfigOption,
37
+ type SetSessionModeRequest,
38
+ type SetSessionConfigOptionRequest,
39
+ type SetSessionConfigOptionResponse,
40
+ AGENT_METHODS,
41
+ } from "@agentclientprotocol/sdk";
42
+
43
+ export class WaveAcpAgent implements AcpAgent {
44
+ private agents: Map<string, WaveAgent> = new Map();
45
+ private connection: AgentSideConnection;
46
+
47
+ constructor(connection: AgentSideConnection) {
48
+ this.connection = connection;
49
+ }
50
+
51
+ private getSessionModeState(agent: WaveAgent) {
52
+ return {
53
+ currentModeId: agent.getPermissionMode(),
54
+ availableModes: [
55
+ {
56
+ id: "default",
57
+ name: "Default",
58
+ description: "Ask for permission for restricted tools",
59
+ },
60
+ {
61
+ id: "acceptEdits",
62
+ name: "Accept Edits",
63
+ description: "Automatically accept file edits",
64
+ },
65
+ {
66
+ id: "plan",
67
+ name: "Plan",
68
+ description: "Plan mode for complex tasks",
69
+ },
70
+ {
71
+ id: "bypassPermissions",
72
+ name: "Bypass Permissions",
73
+ description: "Automatically accept all tool calls",
74
+ },
75
+ ],
76
+ };
77
+ }
78
+
79
+ private getSessionConfigOptions(agent: WaveAgent): SessionConfigOption[] {
80
+ return [
81
+ {
82
+ id: "permission_mode",
83
+ name: "Permission Mode",
84
+ type: "select",
85
+ category: "mode",
86
+ currentValue: agent.getPermissionMode(),
87
+ options: [
88
+ { value: "default", name: "Default" },
89
+ { value: "acceptEdits", name: "Accept Edits" },
90
+ { value: "plan", name: "Plan" },
91
+ { value: "bypassPermissions", name: "Bypass Permissions" },
92
+ ],
93
+ },
94
+ ];
95
+ }
96
+
97
+ private async cleanupAllAgents() {
98
+ logger.info("Cleaning up all active agents due to connection closure");
99
+ const destroyPromises = Array.from(this.agents.values()).map((agent) =>
100
+ agent.destroy(),
101
+ );
102
+ await Promise.all(destroyPromises);
103
+ this.agents.clear();
104
+ }
105
+
106
+ async initialize(): Promise<InitializeResponse> {
107
+ logger.info("Initializing WaveAcpAgent");
108
+ // Setup cleanup on connection closure
109
+ this.connection.closed.then(() => this.cleanupAllAgents());
110
+ return {
111
+ protocolVersion: 1,
112
+ agentInfo: {
113
+ name: "wave-agent",
114
+ version: "0.1.0",
115
+ },
116
+ agentCapabilities: {
117
+ loadSession: true,
118
+ sessionCapabilities: {
119
+ list: {},
120
+ close: {},
121
+ },
122
+ },
123
+ };
124
+ }
125
+
126
+ async authenticate(): Promise<AuthenticateResponse | void> {
127
+ // No authentication required for now
128
+ }
129
+
130
+ private async createAgent(
131
+ sessionId: string | undefined,
132
+ cwd: string,
133
+ ): Promise<WaveAgent> {
134
+ const callbacks: AgentOptions["callbacks"] = {};
135
+ const agentRef: { instance?: WaveAgent } = {};
136
+
137
+ const agent = await WaveAgent.create({
138
+ workdir: cwd,
139
+ restoreSessionId: sessionId,
140
+ canUseTool: (context) => {
141
+ if (!agentRef.instance) {
142
+ throw new Error("Agent instance not yet initialized");
143
+ }
144
+ return this.handlePermissionRequest(
145
+ agentRef.instance.sessionId,
146
+ context,
147
+ );
148
+ },
149
+ callbacks: {
150
+ onAssistantContentUpdated: (chunk: string) =>
151
+ callbacks.onAssistantContentUpdated?.(chunk, ""),
152
+ onAssistantReasoningUpdated: (chunk: string) =>
153
+ callbacks.onAssistantReasoningUpdated?.(chunk, ""),
154
+ onToolBlockUpdated: (params: unknown) => {
155
+ const cb = callbacks.onToolBlockUpdated as
156
+ | ((params: unknown) => void)
157
+ | undefined;
158
+ cb?.(params);
159
+ },
160
+ onTasksChange: (tasks) => callbacks.onTasksChange?.(tasks as Task[]),
161
+ onPermissionModeChange: (mode) =>
162
+ callbacks.onPermissionModeChange?.(mode),
163
+ },
164
+ });
165
+
166
+ agentRef.instance = agent;
167
+ const actualSessionId = agent.sessionId;
168
+ this.agents.set(actualSessionId, agent);
169
+
170
+ // Update the callbacks object with the correct sessionId
171
+ Object.assign(callbacks, this.createCallbacks(actualSessionId));
172
+
173
+ return agent;
174
+ }
175
+
176
+ async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
177
+ const { cwd } = params;
178
+ logger.info(`Creating new session in ${cwd}`);
179
+ const agent = await this.createAgent(undefined, cwd);
180
+ logger.info(`New session created with ID: ${agent.sessionId}`);
181
+
182
+ // Send initial available commands after agent creation
183
+ setImmediate(() => {
184
+ this.connection.sessionUpdate({
185
+ sessionId: agent.sessionId as AcpSessionId,
186
+ update: {
187
+ sessionUpdate: "available_commands_update",
188
+ availableCommands: agent.getSlashCommands().map((cmd) => ({
189
+ name: cmd.name,
190
+ description: cmd.description,
191
+ input: {
192
+ hint: "Enter arguments...",
193
+ },
194
+ })),
195
+ },
196
+ });
197
+ });
198
+
199
+ return {
200
+ sessionId: agent.sessionId as AcpSessionId,
201
+ modes: this.getSessionModeState(agent),
202
+ configOptions: this.getSessionConfigOptions(agent),
203
+ };
204
+ }
205
+
206
+ async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
207
+ const { sessionId, cwd } = params;
208
+ logger.info(`Loading session: ${sessionId} in ${cwd}`);
209
+ const agent = await this.createAgent(sessionId, cwd);
210
+
211
+ // Send initial available commands after agent creation
212
+ setImmediate(() => {
213
+ this.connection.sessionUpdate({
214
+ sessionId: agent.sessionId as AcpSessionId,
215
+ update: {
216
+ sessionUpdate: "available_commands_update",
217
+ availableCommands: agent.getSlashCommands().map((cmd) => ({
218
+ name: cmd.name,
219
+ description: cmd.description,
220
+ input: {
221
+ hint: "Enter arguments...",
222
+ },
223
+ })),
224
+ },
225
+ });
226
+ });
227
+
228
+ return {
229
+ modes: this.getSessionModeState(agent),
230
+ configOptions: this.getSessionConfigOptions(agent),
231
+ };
232
+ }
233
+
234
+ async listSessions(
235
+ params: ListSessionsRequest,
236
+ ): Promise<ListSessionsResponse> {
237
+ const { cwd } = params;
238
+ logger.info(`listSessions called with params: ${JSON.stringify(params)}`);
239
+ if (!cwd) {
240
+ logger.warn("listSessions called without cwd, returning empty list");
241
+ return { sessions: [] };
242
+ }
243
+
244
+ logger.info(`Listing sessions for ${cwd}`);
245
+ const waveSessions = await listWaveSessions(cwd);
246
+ logger.info(`Found ${waveSessions.length} sessions for ${cwd}`);
247
+ const sessions: SessionInfo[] = waveSessions.map((meta) => ({
248
+ sessionId: meta.id as AcpSessionId,
249
+ cwd: meta.workdir,
250
+ updatedAt: meta.lastActiveAt.toISOString(),
251
+ }));
252
+ return { sessions };
253
+ }
254
+
255
+ async unstable_closeSession(
256
+ params: Record<string, unknown>,
257
+ ): Promise<Record<string, unknown>> {
258
+ const sessionId = params.sessionId as string;
259
+ logger.info(`Stopping session ${sessionId}`);
260
+ const agent = this.agents.get(sessionId);
261
+ if (agent) {
262
+ const workdir = agent.workingDirectory;
263
+ await agent.destroy();
264
+ this.agents.delete(sessionId);
265
+ // Delete the session file so it doesn't show up in listSessions
266
+ await deleteWaveSession(sessionId, workdir);
267
+ }
268
+ return {};
269
+ }
270
+
271
+ async extMethod(
272
+ method: string,
273
+ params: Record<string, unknown>,
274
+ ): Promise<Record<string, unknown>> {
275
+ if (method === AGENT_METHODS.session_close) {
276
+ return this.unstable_closeSession(params);
277
+ }
278
+ throw new Error(`Method ${method} not implemented`);
279
+ }
280
+
281
+ async setSessionMode(params: SetSessionModeRequest): Promise<void> {
282
+ const { sessionId, modeId } = params;
283
+ const agent = this.agents.get(sessionId);
284
+ if (!agent) throw new Error(`Session ${sessionId} not found`);
285
+ agent.setPermissionMode(
286
+ modeId as "default" | "acceptEdits" | "plan" | "bypassPermissions",
287
+ );
288
+ }
289
+
290
+ async setSessionConfigOption(
291
+ params: SetSessionConfigOptionRequest,
292
+ ): Promise<SetSessionConfigOptionResponse> {
293
+ const { sessionId, configId, value } = params;
294
+ const agent = this.agents.get(sessionId);
295
+ if (!agent) throw new Error(`Session ${sessionId} not found`);
296
+
297
+ if (configId === "permission_mode") {
298
+ agent.setPermissionMode(
299
+ value as "default" | "acceptEdits" | "plan" | "bypassPermissions",
300
+ );
301
+ }
302
+
303
+ return {
304
+ configOptions: this.getSessionConfigOptions(agent),
305
+ };
306
+ }
307
+
308
+ async prompt(params: PromptRequest): Promise<PromptResponse> {
309
+ const { sessionId, prompt } = params;
310
+ logger.info(`Received prompt for session ${sessionId}`);
311
+ const agent = this.agents.get(sessionId);
312
+ if (!agent) {
313
+ logger.error(`Session ${sessionId} not found`);
314
+ throw new Error(`Session ${sessionId} not found`);
315
+ }
316
+
317
+ // Map ACP prompt to Wave Agent sendMessage
318
+ const textContent = prompt
319
+ .filter((block) => block.type === "text")
320
+ .map((block) => (block as { text: string }).text)
321
+ .join("\n");
322
+
323
+ const images = prompt
324
+ .filter((block) => block.type === "image")
325
+ .map((block) => {
326
+ const img = block as { data: string; mimeType: string };
327
+ return {
328
+ path: `data:${img.mimeType};base64,${img.data}`,
329
+ mimeType: img.mimeType,
330
+ };
331
+ });
332
+
333
+ try {
334
+ logger.info(
335
+ `Sending message to agent: ${textContent.substring(0, 50)}...`,
336
+ );
337
+ await agent.sendMessage(
338
+ textContent,
339
+ images.length > 0 ? images : undefined,
340
+ );
341
+ logger.info(`Message sent successfully for session ${sessionId}`);
342
+ return {
343
+ stopReason: "end_turn" as StopReason,
344
+ };
345
+ } catch (error) {
346
+ if (error instanceof Error && error.message.includes("abort")) {
347
+ logger.info(`Message aborted for session ${sessionId}`);
348
+ return {
349
+ stopReason: "cancelled" as StopReason,
350
+ };
351
+ }
352
+ logger.error(`Error sending message for session ${sessionId}:`, error);
353
+ throw error;
354
+ }
355
+ }
356
+
357
+ async cancel(params: CancelNotification): Promise<void> {
358
+ const { sessionId } = params;
359
+ logger.info(`Cancelling message for session ${sessionId}`);
360
+ const agent = this.agents.get(sessionId);
361
+ if (agent) {
362
+ agent.abortMessage();
363
+ }
364
+ }
365
+
366
+ private async handlePermissionRequest(
367
+ sessionId: string,
368
+ context: ToolPermissionContext,
369
+ ): Promise<PermissionDecision> {
370
+ logger.info(
371
+ `Handling permission request for ${context.toolName} in session ${sessionId}`,
372
+ );
373
+
374
+ const agent = this.agents.get(sessionId);
375
+ const workdir = agent?.workingDirectory || process.cwd();
376
+
377
+ const toolCallId =
378
+ context.toolCallId ||
379
+ "perm-" + Math.random().toString(36).substring(2, 9);
380
+
381
+ const options: PermissionOption[] = [
382
+ {
383
+ optionId: "allow_once",
384
+ name: "Allow Once",
385
+ kind: "allow_once",
386
+ },
387
+ {
388
+ optionId: "allow_always",
389
+ name: "Allow Always",
390
+ kind: "allow_always",
391
+ },
392
+ {
393
+ optionId: "reject_once",
394
+ name: "Reject Once",
395
+ kind: "reject_once",
396
+ },
397
+ {
398
+ optionId: "reject_always",
399
+ name: "Reject Always",
400
+ kind: "reject_always",
401
+ },
402
+ ];
403
+
404
+ const content = context.toolName
405
+ ? await this.getToolContentAsync(
406
+ context.toolName,
407
+ context.toolInput,
408
+ workdir,
409
+ )
410
+ : undefined;
411
+ const locations = context.toolName
412
+ ? this.getToolLocations(context.toolName, context.toolInput)
413
+ : undefined;
414
+ const kind = context.toolName
415
+ ? this.getToolKind(context.toolName)
416
+ : undefined;
417
+
418
+ try {
419
+ const response = await this.connection.requestPermission({
420
+ sessionId: sessionId as AcpSessionId,
421
+ toolCall: {
422
+ toolCallId,
423
+ title: `Permission for ${context.toolName}`,
424
+ status: "pending",
425
+ rawInput: context.toolInput,
426
+ content,
427
+ locations,
428
+ kind,
429
+ },
430
+ options,
431
+ });
432
+
433
+ if (response.outcome.outcome === "cancelled") {
434
+ return { behavior: "deny", message: "Cancelled by user" };
435
+ }
436
+
437
+ const selectedOptionId = response.outcome.optionId;
438
+ logger.info(`User selected permission option: ${selectedOptionId}`);
439
+
440
+ switch (selectedOptionId) {
441
+ case "allow_once":
442
+ return { behavior: "allow" };
443
+ case "allow_always":
444
+ return {
445
+ behavior: "allow",
446
+ newPermissionRule: `${context.toolName}(*)`,
447
+ };
448
+ case "reject_once":
449
+ return { behavior: "deny", message: "Rejected by user" };
450
+ case "reject_always":
451
+ return {
452
+ behavior: "deny",
453
+ message: "Rejected by user",
454
+ newPermissionRule: `!${context.toolName}(*)`,
455
+ };
456
+ default:
457
+ return { behavior: "deny", message: "Unknown option selected" };
458
+ }
459
+ } catch (error) {
460
+ logger.error("Error requesting permission via ACP:", error);
461
+ return {
462
+ behavior: "deny",
463
+ message: `Error requesting permission: ${error instanceof Error ? error.message : String(error)}`,
464
+ };
465
+ }
466
+ }
467
+
468
+ private async getToolContentAsync(
469
+ name: string,
470
+ parameters: Record<string, unknown> | undefined,
471
+ workdir: string,
472
+ ): Promise<ToolCallContent[] | undefined> {
473
+ if (!parameters) return undefined;
474
+ if (name === "Write") {
475
+ let oldText: string | null = null;
476
+ try {
477
+ const filePath = parameters.file_path as string;
478
+ const fullPath = path.isAbsolute(filePath)
479
+ ? filePath
480
+ : path.join(workdir, filePath);
481
+ oldText = await fs.readFile(fullPath, "utf-8");
482
+ } catch {
483
+ // File might not exist, which is fine for Write
484
+ }
485
+ return [
486
+ {
487
+ type: "diff",
488
+ path: parameters.file_path as string,
489
+ oldText,
490
+ newText: parameters.content as string,
491
+ },
492
+ ];
493
+ }
494
+ if (name === "Edit") {
495
+ let oldText: string | null = null;
496
+ let newText: string | null = null;
497
+ try {
498
+ const filePath = parameters.file_path as string;
499
+ const fullPath = path.isAbsolute(filePath)
500
+ ? filePath
501
+ : path.join(workdir, filePath);
502
+ oldText = await fs.readFile(fullPath, "utf-8");
503
+ if (oldText) {
504
+ if (parameters.replace_all) {
505
+ newText = oldText
506
+ .split(parameters.old_string as string)
507
+ .join(parameters.new_string as string);
508
+ } else {
509
+ newText = oldText.replace(
510
+ parameters.old_string as string,
511
+ parameters.new_string as string,
512
+ );
513
+ }
514
+ }
515
+ } catch {
516
+ logger.error("Failed to read file for Edit diff");
517
+ }
518
+
519
+ if (oldText && newText) {
520
+ return [
521
+ {
522
+ type: "diff",
523
+ path: parameters.file_path as string,
524
+ oldText,
525
+ newText,
526
+ },
527
+ ];
528
+ }
529
+
530
+ // Fallback to snippets if file reading fails
531
+ return [
532
+ {
533
+ type: "diff",
534
+ path: parameters.file_path as string,
535
+ oldText: parameters.old_string as string,
536
+ newText: parameters.new_string as string,
537
+ },
538
+ ];
539
+ }
540
+ return this.getToolContent(name, parameters);
541
+ }
542
+
543
+ private getToolContent(
544
+ name: string,
545
+ parameters: Record<string, unknown> | undefined,
546
+ ): ToolCallContent[] | undefined {
547
+ if (!parameters) return undefined;
548
+ if (name === "Write") {
549
+ return [
550
+ {
551
+ type: "diff",
552
+ path: parameters.file_path as string,
553
+ oldText: null,
554
+ newText: parameters.content as string,
555
+ },
556
+ ];
557
+ }
558
+ if (name === "Edit") {
559
+ return [
560
+ {
561
+ type: "diff",
562
+ path: parameters.file_path as string,
563
+ oldText: parameters.old_string as string,
564
+ newText: parameters.new_string as string,
565
+ },
566
+ ];
567
+ }
568
+ return undefined;
569
+ }
570
+
571
+ private getToolLocations(
572
+ name: string,
573
+ parameters: Record<string, unknown> | undefined,
574
+ ): ToolCallLocation[] | undefined {
575
+ if (!parameters) return undefined;
576
+ if (name === "Write" || name === "Edit" || name === "Read") {
577
+ return [
578
+ {
579
+ path: parameters.file_path as string,
580
+ line: parameters.offset as number,
581
+ },
582
+ ];
583
+ }
584
+ return undefined;
585
+ }
586
+
587
+ private getToolKind(name: string): ToolKind {
588
+ switch (name) {
589
+ case "Read":
590
+ case "Glob":
591
+ case "Grep":
592
+ case "LSP":
593
+ return "read";
594
+ case "Write":
595
+ case "Edit":
596
+ return "edit";
597
+ case "Bash":
598
+ return "execute";
599
+ case "Agent":
600
+ return "other";
601
+ default:
602
+ return "other";
603
+ }
604
+ }
605
+
606
+ private createCallbacks(sessionId: string): AgentOptions["callbacks"] {
607
+ const getAgent = () => this.agents.get(sessionId);
608
+ return {
609
+ onAssistantContentUpdated: (chunk: string) => {
610
+ this.connection.sessionUpdate({
611
+ sessionId: sessionId as AcpSessionId,
612
+ update: {
613
+ sessionUpdate: "agent_message_chunk",
614
+ content: {
615
+ type: "text",
616
+ text: chunk,
617
+ },
618
+ },
619
+ });
620
+ },
621
+ onAssistantReasoningUpdated: (chunk: string) => {
622
+ this.connection.sessionUpdate({
623
+ sessionId: sessionId as AcpSessionId,
624
+ update: {
625
+ sessionUpdate: "agent_thought_chunk",
626
+ content: {
627
+ type: "text",
628
+ text: chunk,
629
+ },
630
+ },
631
+ });
632
+ },
633
+ onToolBlockUpdated: (params: AgentToolBlockUpdateParams) => {
634
+ const { id, name, stage, success, error, result, parameters } = params;
635
+
636
+ let parsedParameters: Record<string, unknown> | undefined = undefined;
637
+ if (parameters) {
638
+ try {
639
+ parsedParameters = JSON.parse(parameters);
640
+ } catch {
641
+ // Ignore parse errors during streaming
642
+ }
643
+ }
644
+
645
+ const content =
646
+ name && parsedParameters
647
+ ? this.getToolContent(name, parsedParameters)
648
+ : undefined;
649
+ const locations =
650
+ name && parsedParameters
651
+ ? this.getToolLocations(name, parsedParameters)
652
+ : undefined;
653
+ const kind = name ? this.getToolKind(name) : undefined;
654
+
655
+ if (stage === "start") {
656
+ this.connection.sessionUpdate({
657
+ sessionId: sessionId as AcpSessionId,
658
+ update: {
659
+ sessionUpdate: "tool_call",
660
+ toolCallId: id,
661
+ title: name || "Tool Call",
662
+ status: "pending",
663
+ content,
664
+ locations,
665
+ kind,
666
+ rawInput: parsedParameters,
667
+ },
668
+ });
669
+ return;
670
+ }
671
+
672
+ if (stage === "streaming") {
673
+ // We don't support streaming tool arguments in ACP yet
674
+ return;
675
+ }
676
+
677
+ const status: ToolCallStatus =
678
+ stage === "end"
679
+ ? success
680
+ ? "completed"
681
+ : "failed"
682
+ : stage === "running"
683
+ ? "in_progress"
684
+ : "pending";
685
+
686
+ this.connection.sessionUpdate({
687
+ sessionId: sessionId as AcpSessionId,
688
+ update: {
689
+ sessionUpdate: "tool_call_update",
690
+ toolCallId: id,
691
+ status,
692
+ title: name || "Tool Call",
693
+ rawOutput: result || error,
694
+ content,
695
+ locations,
696
+ kind,
697
+ rawInput: parsedParameters,
698
+ },
699
+ });
700
+ },
701
+ onTasksChange: (tasks) => {
702
+ this.connection.sessionUpdate({
703
+ sessionId: sessionId as AcpSessionId,
704
+ update: {
705
+ sessionUpdate: "plan",
706
+ entries: tasks.map((task) => ({
707
+ content: task.subject,
708
+ status:
709
+ task.status === "completed"
710
+ ? "completed"
711
+ : task.status === "in_progress"
712
+ ? "in_progress"
713
+ : "pending",
714
+ priority: "medium",
715
+ })),
716
+ },
717
+ });
718
+ },
719
+ onPermissionModeChange: (mode) => {
720
+ this.connection.sessionUpdate({
721
+ sessionId: sessionId as AcpSessionId,
722
+ update: {
723
+ sessionUpdate: "current_mode_update",
724
+ currentModeId: mode,
725
+ },
726
+ });
727
+ const agent = getAgent();
728
+ if (agent) {
729
+ this.connection.sessionUpdate({
730
+ sessionId: sessionId as AcpSessionId,
731
+ update: {
732
+ sessionUpdate: "config_option_update",
733
+ configOptions: this.getSessionConfigOptions(agent),
734
+ },
735
+ });
736
+ }
737
+ },
738
+ };
739
+ }
740
+ }