agent-gear 1.1.5 → 1.1.7

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,158 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { readFileSync, rmSync, mkdtempSync, existsSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ // Force the real (unmocked) ACP SDK through the vite pipeline (same rationale
8
+ // as acp-wire-mcp.test.ts): keep actual SDK serialization behavior on the wire.
9
+ vi.mock('@agentclientprotocol/sdk', async (importOriginal) => {
10
+ return await importOriginal<typeof import('@agentclientprotocol/sdk')>();
11
+ });
12
+
13
+ import { AcpSessionBridge } from '../../src/acp/bridge.js';
14
+ import { AcpSpawner } from '../../src/acp/spawner.js';
15
+
16
+ const FAKE_AGENT = join(dirname(fileURLToPath(import.meta.url)), 'fixtures', 'fake-acp-agent.mjs');
17
+
18
+ interface CapturedEntry {
19
+ method: string;
20
+ params?: Record<string, unknown>;
21
+ response?: Record<string, unknown> | null;
22
+ }
23
+
24
+ // Wire-level test: drives a REAL ACP client connection against a fake agent
25
+ // process that issues `elicitation/create` (the same wire behavior as an
26
+ // eliciting agent such as opencode), and asserts both the Socket.IO payload
27
+ // emitted by the bridge and the exact JSON-RPC response that goes back over
28
+ // stdio — no mocked ClientSideConnection.
29
+ describe('AcpSessionBridge - Elicitation wire format (ACP spec)', () => {
30
+ let workdir: string;
31
+ let captureFile: string;
32
+ let bridge: AcpSessionBridge | null = null;
33
+
34
+ afterEach(async () => {
35
+ delete process.env.CAPTURE_FILE;
36
+ delete process.env.FAKE_AGENT_ELICITATION;
37
+ if (bridge) {
38
+ await bridge.stop().catch(() => {});
39
+ bridge = null;
40
+ }
41
+ if (workdir) rmSync(workdir, { recursive: true, force: true });
42
+ vi.restoreAllMocks();
43
+ });
44
+
45
+ function setup(mode: 'form' | 'url') {
46
+ workdir = mkdtempSync(join(tmpdir(), 'acp-wire-elic-'));
47
+ captureFile = join(workdir, 'capture.jsonl');
48
+ process.env.CAPTURE_FILE = captureFile;
49
+ process.env.FAKE_AGENT_ELICITATION = mode;
50
+ const spawner = new AcpSpawner({
51
+ sessionId: 'sess-wire',
52
+ cwd: workdir,
53
+ command: process.execPath,
54
+ args: [FAKE_AGENT],
55
+ });
56
+ const socket = { emit: vi.fn(), on: vi.fn(), off: vi.fn() };
57
+ bridge = new AcpSessionBridge(socket as never, {
58
+ sessionId: 'sess-wire',
59
+ cwd: workdir,
60
+ }, spawner);
61
+ return socket;
62
+ }
63
+
64
+ function readCapture(): CapturedEntry[] {
65
+ if (!existsSync(captureFile)) return [];
66
+ return readFileSync(captureFile, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
67
+ }
68
+
69
+ function emittedCall(socket: { emit: ReturnType<typeof vi.fn> }, event: string) {
70
+ return socket.emit.mock.calls.find((c) => c[0] === event);
71
+ }
72
+
73
+ it('should advertise elicitation capabilities in initialize on the wire', async () => {
74
+ vi.spyOn(console, 'log').mockImplementation(() => {});
75
+ setup('form');
76
+
77
+ await bridge!.start();
78
+
79
+ const init = readCapture().find((e) => e.method === 'initialize');
80
+ expect(init).toBeDefined();
81
+ const capabilities = init!.params!.clientCapabilities as Record<string, unknown>;
82
+ expect(capabilities.elicitation).toEqual({ form: {}, url: {} });
83
+ }, 15000);
84
+
85
+ it('should forward form elicitation and respond with spec-compliant payload', async () => {
86
+ vi.spyOn(console, 'log').mockImplementation(() => {});
87
+ const socket = setup('form');
88
+
89
+ await bridge!.start();
90
+ bridge!.sendInput({ type: 'text', content: 'please refactor this' });
91
+
92
+ // The agent's elicitation/create must surface as an acp:elicitation socket event
93
+ await vi.waitFor(() => {
94
+ expect(emittedCall(socket, 'acp:elicitation')).toBeDefined();
95
+ }, { timeout: 10000 });
96
+
97
+ const [, payload] = emittedCall(socket, 'acp:elicitation')!;
98
+ expect(payload.sessionId).toBe('sess-wire');
99
+ expect(payload.mode).toBe('form');
100
+ expect(payload.message).toBe('How should I approach this refactoring?');
101
+ expect(payload.requestedSchema.required).toEqual(['strategy']);
102
+ expect(payload.requestId).toMatch(/^elic_/);
103
+
104
+ // Simulate the server returning the user's answer
105
+ bridge!.handleElicitationResponse(payload.requestId, 'accept', { strategy: 'balanced' });
106
+
107
+ // The agent must receive a spec-compliant CreateElicitationResponse on the wire
108
+ await vi.waitFor(() => {
109
+ const elicitation = readCapture().find((e) => e.method === 'elicitation/create');
110
+ expect(elicitation?.response).toBeDefined();
111
+ }, { timeout: 10000 });
112
+
113
+ const elicitation = readCapture().find((e) => e.method === 'elicitation/create');
114
+ expect(elicitation!.response).toEqual({ action: 'accept', content: { strategy: 'balanced' } });
115
+
116
+ // Prompt completes after the elicitation resolves
117
+ await vi.waitFor(() => {
118
+ expect(emittedCall(socket, 'acp:prompt:complete')).toBeDefined();
119
+ }, { timeout: 10000 });
120
+ }, 20000);
121
+
122
+ it('should forward url elicitation and relay the completion notification', async () => {
123
+ vi.spyOn(console, 'log').mockImplementation(() => {});
124
+ const socket = setup('url');
125
+
126
+ await bridge!.start();
127
+ bridge!.sendInput({ type: 'text', content: 'connect my account' });
128
+
129
+ await vi.waitFor(() => {
130
+ expect(emittedCall(socket, 'acp:elicitation')).toBeDefined();
131
+ }, { timeout: 10000 });
132
+
133
+ const [, payload] = emittedCall(socket, 'acp:elicitation')!;
134
+ expect(payload.mode).toBe('url');
135
+ expect(payload.elicitationId).toBe('fake-oauth-001');
136
+ expect(payload.url).toBe('https://agent.example.com/connect?elicitationId=fake-oauth-001');
137
+ expect(payload.message).toBe('Please authorize access to your repositories.');
138
+
139
+ // User consented to open the URL (accept without content)
140
+ bridge!.handleElicitationResponse(payload.requestId, 'accept');
141
+
142
+ await vi.waitFor(() => {
143
+ const elicitation = readCapture().find((e) => e.method === 'elicitation/create');
144
+ expect(elicitation?.response).toBeDefined();
145
+ }, { timeout: 10000 });
146
+
147
+ const elicitation = readCapture().find((e) => e.method === 'elicitation/create');
148
+ expect(elicitation!.response).toEqual({ action: 'accept' });
149
+
150
+ // The agent's elicitation/complete notification must be forwarded
151
+ await vi.waitFor(() => {
152
+ expect(emittedCall(socket, 'acp:elicitation:complete')).toBeDefined();
153
+ }, { timeout: 10000 });
154
+
155
+ const [, completePayload] = emittedCall(socket, 'acp:elicitation:complete')!;
156
+ expect(completePayload).toEqual({ sessionId: 'sess-wire', elicitationId: 'fake-oauth-001' });
157
+ }, 20000);
158
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { readFileSync, rmSync, mkdtempSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ // Force the real (unmocked) ACP SDK through the vite pipeline: returning the
8
+ // original module keeps actual SDK serialization behavior while avoiding the
9
+ // node-native externalization that breaks .js -> .ts source resolution.
10
+ vi.mock('@agentclientprotocol/sdk', async (importOriginal) => {
11
+ return await importOriginal<typeof import('@agentclientprotocol/sdk')>();
12
+ });
13
+
14
+ import { AcpSessionBridge } from '../../src/acp/bridge.js';
15
+ import { AcpSpawner } from '../../src/acp/spawner.js';
16
+
17
+ const FAKE_AGENT = join(dirname(fileURLToPath(import.meta.url)), 'fixtures', 'fake-acp-agent.mjs');
18
+
19
+ // Wire-level test: drives a REAL ACP client connection (SDK serialization,
20
+ // ndJsonStream) against a fake agent process, and asserts the exact JSON-RPC
21
+ // payload that goes over stdin — no mocked ClientSideConnection.
22
+ describe('AcpSessionBridge - MCP wire format (ACP spec)', () => {
23
+ let workdir: string;
24
+ let captureFile: string;
25
+ let bridge: AcpSessionBridge | null = null;
26
+
27
+ afterEach(async () => {
28
+ delete process.env.CAPTURE_FILE;
29
+ if (bridge) {
30
+ await bridge.stop().catch(() => {});
31
+ bridge = null;
32
+ }
33
+ if (workdir) rmSync(workdir, { recursive: true, force: true });
34
+ vi.restoreAllMocks();
35
+ });
36
+
37
+ function setup() {
38
+ workdir = mkdtempSync(join(tmpdir(), 'acp-wire-'));
39
+ captureFile = join(workdir, 'capture.jsonl');
40
+ process.env.CAPTURE_FILE = captureFile;
41
+ const spawner = new AcpSpawner({
42
+ sessionId: 'sess-wire',
43
+ cwd: workdir,
44
+ command: process.execPath,
45
+ args: [FAKE_AGENT],
46
+ });
47
+ const socket = { emit: vi.fn(), on: vi.fn(), off: vi.fn() };
48
+ bridge = new AcpSessionBridge(socket as never, {
49
+ sessionId: 'sess-wire',
50
+ cwd: workdir,
51
+ mcpEndpoint: {
52
+ url: 'http://gateway/mcp/sess-wire',
53
+ token: 'wire-token',
54
+ serverNames: ['fluentui-mcp', 'fs-mcp'],
55
+ },
56
+ }, spawner);
57
+ return socket;
58
+ }
59
+
60
+ function readCapture(): Array<{ method: string; params: Record<string, unknown> }> {
61
+ return readFileSync(captureFile, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
62
+ }
63
+
64
+ it('should send session/new with spec-compliant mcpServers on the wire', async () => {
65
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
66
+ setup();
67
+
68
+ await bridge!.start();
69
+
70
+ const captured = readCapture();
71
+ expect(captured).toHaveLength(1);
72
+ expect(captured[0].method).toBe('session/new');
73
+ // ACP spec (session/new): mcpServers entries for HTTP transport are
74
+ // { type: "http", name, url, headers: [{ name, value }] }
75
+ expect(captured[0].params.mcpServers).toEqual([
76
+ {
77
+ type: 'http',
78
+ name: 'fluentui-mcp',
79
+ url: 'http://gateway/mcp/sess-wire/fluentui-mcp',
80
+ headers: [{ name: 'Authorization', value: 'Bearer wire-token' }],
81
+ },
82
+ {
83
+ type: 'http',
84
+ name: 'fs-mcp',
85
+ url: 'http://gateway/mcp/sess-wire/fs-mcp',
86
+ headers: [{ name: 'Authorization', value: 'Bearer wire-token' }],
87
+ },
88
+ ]);
89
+ expect(captured[0].params.cwd).toBe(workdir);
90
+
91
+ // The MCP handoff must be visible in logs, without leaking the token
92
+ const logLines = logSpy.mock.calls.map((c) => String(c[0]));
93
+ const mcpLog = logLines.find((l) => l.includes('passing MCP servers via session/new'));
94
+ expect(mcpLog).toBeDefined();
95
+ expect(mcpLog).toContain('fluentui-mcp(type=http, url=http://gateway/mcp/sess-wire/fluentui-mcp, headers=Authorization)');
96
+ expect(mcpLog).not.toContain('wire-token');
97
+ }, 15000);
98
+
99
+ it('should send session/resume with spec-compliant mcpServers on the wire', async () => {
100
+ vi.spyOn(console, 'log').mockImplementation(() => {});
101
+ setup();
102
+
103
+ await bridge!.resume('ext-session-1');
104
+
105
+ const captured = readCapture();
106
+ expect(captured).toHaveLength(1);
107
+ expect(captured[0].method).toBe('session/resume');
108
+ expect(captured[0].params.sessionId).toBe('ext-session-1');
109
+ expect(captured[0].params.mcpServers).toEqual([
110
+ expect.objectContaining({ type: 'http', name: 'fluentui-mcp' }),
111
+ expect.objectContaining({ type: 'http', name: 'fs-mcp' }),
112
+ ]);
113
+ }, 15000);
114
+ });
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { EventEmitter } from 'events';
3
+ import { PassThrough } from 'node:stream';
4
+ import type { ChildProcess } from 'child_process';
5
+ import { createStdioDispatcher } from '../../src/commands/mcp-proxy.js';
6
+
7
+ function createFakeChild(): ChildProcess & EventEmitter {
8
+ const proc = new EventEmitter() as ChildProcess & EventEmitter;
9
+ Object.defineProperty(proc, 'stdin', { value: new PassThrough(), writable: true });
10
+ Object.defineProperty(proc, 'stdout', { value: new PassThrough(), writable: true });
11
+ Object.defineProperty(proc, 'stderr', { value: new PassThrough(), writable: true });
12
+ return proc;
13
+ }
14
+
15
+ function stdinWrites(child: ChildProcess): string[] {
16
+ const chunks: string[] = [];
17
+ (child.stdin as PassThrough).on('data', (d: Buffer) => chunks.push(d.toString()));
18
+ return chunks;
19
+ }
20
+
21
+ describe('createStdioDispatcher', () => {
22
+ let child: ChildProcess & EventEmitter;
23
+
24
+ beforeEach(() => {
25
+ child = createFakeChild();
26
+ vi.spyOn(console, 'log').mockImplementation(() => {});
27
+ vi.spyOn(console, 'error').mockImplementation(() => {});
28
+ });
29
+
30
+ afterEach(() => {
31
+ vi.restoreAllMocks();
32
+ });
33
+
34
+ it('should forward notifications fire-and-forget without waiting for a response', async () => {
35
+ const writes = stdinWrites(child);
36
+ const dispatcher = createStdioDispatcher(child);
37
+
38
+ const start = Date.now();
39
+ const result = await dispatcher.forward({ jsonrpc: '2.0', method: 'notifications/initialized' });
40
+
41
+ // must resolve immediately — the MCP server never responds to notifications
42
+ expect(Date.now() - start).toBeLessThan(1000);
43
+ expect(result).toBeNull();
44
+ expect(writes.join('')).toContain('"method":"notifications/initialized"');
45
+ });
46
+
47
+ it('should resolve a request when the child responds with the matching id', async () => {
48
+ const dispatcher = createStdioDispatcher(child);
49
+
50
+ const promise = dispatcher.forward({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
51
+ (child.stdout as PassThrough).write(JSON.stringify({ jsonrpc: '2.0', id: 1, result: { ok: true } }) + '\n');
52
+
53
+ await expect(promise).resolves.toEqual({ jsonrpc: '2.0', id: 1, result: { ok: true } });
54
+ });
55
+
56
+ it('should reassemble a JSON response split across multiple chunks', async () => {
57
+ const dispatcher = createStdioDispatcher(child);
58
+
59
+ const promise = dispatcher.forward({ jsonrpc: '2.0', id: 2, method: 'tools/list' });
60
+ const full = JSON.stringify({ jsonrpc: '2.0', id: 2, result: { tools: [{ name: 'probe_tool' }] } }) + '\n';
61
+ // TCP/stdio gives no message boundary guarantees: split the response
62
+ (child.stdout as PassThrough).write(full.slice(0, 10));
63
+ (child.stdout as PassThrough).write(full.slice(10, 40));
64
+ (child.stdout as PassThrough).write(full.slice(40));
65
+
66
+ await expect(promise).resolves.toEqual({ jsonrpc: '2.0', id: 2, result: { tools: [{ name: 'probe_tool' }] } });
67
+ });
68
+
69
+ it('should match concurrent responses to their requests by id, even out of order', async () => {
70
+ const dispatcher = createStdioDispatcher(child);
71
+
72
+ const p1 = dispatcher.forward({ jsonrpc: '2.0', id: 1, method: 'tools/list' });
73
+ const p2 = dispatcher.forward({ jsonrpc: '2.0', id: 2, method: 'tools/call' });
74
+
75
+ // second request answers first, and both arrive in one chunk
76
+ (child.stdout as PassThrough).write(
77
+ JSON.stringify({ jsonrpc: '2.0', id: 2, result: { content: [] } }) + '\n' +
78
+ JSON.stringify({ jsonrpc: '2.0', id: 1, result: { tools: [] } }) + '\n',
79
+ );
80
+
81
+ await expect(p1).resolves.toEqual({ jsonrpc: '2.0', id: 1, result: { tools: [] } });
82
+ await expect(p2).resolves.toEqual({ jsonrpc: '2.0', id: 2, result: { content: [] } });
83
+ });
84
+
85
+ it('should reject when the request times out', async () => {
86
+ vi.useFakeTimers();
87
+ try {
88
+ const dispatcher = createStdioDispatcher(child);
89
+
90
+ const promise = dispatcher.forward({ jsonrpc: '2.0', id: 9, method: 'tools/list' });
91
+ const assertion = expect(promise).rejects.toThrow('Request timeout waiting for tools/list');
92
+ await vi.advanceTimersByTimeAsync(30_000);
93
+ await assertion;
94
+ } finally {
95
+ vi.useRealTimers();
96
+ }
97
+ });
98
+
99
+ it('should reject all pending requests when the child exits', async () => {
100
+ const dispatcher = createStdioDispatcher(child);
101
+
102
+ const promise = dispatcher.forward({ jsonrpc: '2.0', id: 3, method: 'tools/list' });
103
+ child.emit('exit', 1);
104
+
105
+ await expect(promise).rejects.toThrow('MCP command exited with code 1');
106
+ });
107
+
108
+ it('should ignore non-JSON lines instead of failing the pending request', async () => {
109
+ const dispatcher = createStdioDispatcher(child);
110
+
111
+ const promise = dispatcher.forward({ jsonrpc: '2.0', id: 4, method: 'initialize' });
112
+ (child.stdout as PassThrough).write('[some log line that is not json]\n');
113
+ (child.stdout as PassThrough).write(JSON.stringify({ jsonrpc: '2.0', id: 4, result: {} }) + '\n');
114
+
115
+ await expect(promise).resolves.toEqual({ jsonrpc: '2.0', id: 4, result: {} });
116
+ });
117
+ });
@@ -0,0 +1,122 @@
1
+ // Fake ACP agent used by acp-wire-mcp.test.ts and acp-wire-elicitation.test.ts.
2
+ // Speaks ndjson JSON-RPC over stdio: answers initialize/session/new/session/resume
3
+ // and records every session/new & session/resume params payload to CAPTURE_FILE
4
+ // (one JSON per line) so tests can assert the exact wire format.
5
+ //
6
+ // When FAKE_AGENT_ELICITATION=form|url is set, a session/prompt makes the agent
7
+ // behave like an eliciting agent (e.g. opencode asking a question):
8
+ // 1. capture the initialize params (clientCapabilities advertisement)
9
+ // 2. send an outgoing `elicitation/create` request and await the client response
10
+ // 3. capture the elicitation response payload exactly as received on the wire
11
+ // 4. reply to session/prompt with stopReason "end_turn"
12
+ // 5. (url mode only) send an `elicitation/complete` notification afterwards
13
+ import { appendFileSync } from 'node:fs';
14
+ import { createInterface } from 'node:readline';
15
+
16
+ const captureFile = process.env.CAPTURE_FILE;
17
+ const elicitationMode = process.env.FAKE_AGENT_ELICITATION; // 'form' | 'url' | undefined
18
+
19
+ const rl = createInterface({ input: process.stdin });
20
+
21
+ let nextOutgoingId = 1000;
22
+ // outgoing request id -> continuation invoked with the client's response result
23
+ const pendingOutgoing = new Map();
24
+
25
+ function reply(id, result) {
26
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
27
+ }
28
+
29
+ function request(method, params, onResult) {
30
+ const id = nextOutgoingId++;
31
+ pendingOutgoing.set(id, onResult);
32
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
33
+ }
34
+
35
+ function notify(method, params) {
36
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n');
37
+ }
38
+
39
+ function capture(entry) {
40
+ if (captureFile) {
41
+ appendFileSync(captureFile, JSON.stringify(entry) + '\n');
42
+ }
43
+ }
44
+
45
+ function handlePrompt(msg) {
46
+ if (!elicitationMode) {
47
+ reply(msg.id, { stopReason: 'end_turn' });
48
+ return;
49
+ }
50
+
51
+ const sessionId = msg.params?.sessionId ?? 'fake-acp-session-1';
52
+ const params =
53
+ elicitationMode === 'form'
54
+ ? {
55
+ sessionId,
56
+ mode: 'form',
57
+ message: 'How should I approach this refactoring?',
58
+ requestedSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ strategy: { type: 'string', enum: ['conservative', 'balanced', 'aggressive'] },
62
+ },
63
+ required: ['strategy'],
64
+ },
65
+ }
66
+ : {
67
+ sessionId,
68
+ mode: 'url',
69
+ elicitationId: 'fake-oauth-001',
70
+ url: 'https://agent.example.com/connect?elicitationId=fake-oauth-001',
71
+ message: 'Please authorize access to your repositories.',
72
+ };
73
+
74
+ request('elicitation/create', params, (result) => {
75
+ capture({ method: 'elicitation/create', response: result ?? null });
76
+ reply(msg.id, { stopReason: 'end_turn' });
77
+ if (elicitationMode === 'url') {
78
+ notify('elicitation/complete', { elicitationId: 'fake-oauth-001' });
79
+ }
80
+ });
81
+ }
82
+
83
+ rl.on('line', (line) => {
84
+ let msg;
85
+ try { msg = JSON.parse(line); } catch { return; }
86
+
87
+ // Response to an outgoing request (has id, no method)
88
+ if (msg.id !== undefined && !msg.method) {
89
+ const continuation = pendingOutgoing.get(msg.id);
90
+ if (continuation) {
91
+ pendingOutgoing.delete(msg.id);
92
+ continuation(msg.result);
93
+ }
94
+ return;
95
+ }
96
+
97
+ if (msg.id === undefined || !msg.method) return; // ignore notifications
98
+
99
+ switch (msg.method) {
100
+ case 'initialize':
101
+ if (elicitationMode) {
102
+ capture({ method: 'initialize', params: msg.params });
103
+ }
104
+ reply(msg.id, {
105
+ protocolVersion: 1,
106
+ agentCapabilities: { mcpCapabilities: { http: true, sse: true } },
107
+ agentInfo: { name: 'fake-acp-agent', version: '0.0.1' },
108
+ authMethods: [],
109
+ });
110
+ break;
111
+ case 'session/new':
112
+ case 'session/resume':
113
+ capture({ method: msg.method, params: msg.params });
114
+ reply(msg.id, { sessionId: 'fake-acp-session-1' });
115
+ break;
116
+ case 'session/prompt':
117
+ handlePrompt(msg);
118
+ break;
119
+ default:
120
+ reply(msg.id, {});
121
+ }
122
+ });