@zachjxyz/moxie 0.2.4 → 0.3.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,549 @@
1
+ #!/usr/bin/env node
2
+ // moxie/lib/gateway-agent.mjs — AI Gateway agentic client
3
+ // Zero npm dependencies. Node 14+ compatible (uses https.request, not fetch).
4
+ // Receives prompt as argv[2], config from env vars.
5
+
6
+ import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync } from 'fs';
7
+ import { resolve, dirname, basename, join, relative } from 'path';
8
+ import { execSync } from 'child_process';
9
+ import https from 'https';
10
+ import http from 'http';
11
+
12
+ // ---- Config ----
13
+
14
+ const API_KEY = process.env.GATEWAY_API_KEY;
15
+ const ENDPOINT = process.env.GATEWAY_ENDPOINT || 'https://ai-gateway.vercel.sh';
16
+ const MODEL = process.env.GATEWAY_MODEL;
17
+ const MAX_TURNS = parseInt(process.env.GATEWAY_MAX_TURNS || '50', 10);
18
+ const IDLE_TIMEOUT_MS = parseInt(process.env.GATEWAY_IDLE_TIMEOUT || '120000', 10);
19
+ const PHASE = process.env.GATEWAY_PHASE || 'unknown';
20
+ const AGENT_NAME = process.env.GATEWAY_AGENT || 'unknown';
21
+ const CWD = process.cwd();
22
+
23
+ if (!API_KEY) { process.stderr.write('ERROR: GATEWAY_API_KEY not set\n'); process.exit(1); }
24
+ if (!MODEL) { process.stderr.write('ERROR: GATEWAY_MODEL not set\n'); process.exit(1); }
25
+
26
+ const prompt = process.argv[2];
27
+ if (!prompt) { process.stderr.write('ERROR: No prompt provided\n'); process.exit(1); }
28
+
29
+ // ---- Tool implementations ----
30
+
31
+ function toolReadFile(args) {
32
+ const filepath = resolve(CWD, args.path);
33
+ if (!existsSync(filepath)) return `Error: File not found: ${args.path}`;
34
+ const content = readFileSync(filepath, 'utf8');
35
+ const lines = content.split('\n');
36
+ const offset = (args.offset || 1) - 1;
37
+ const limit = args.limit || lines.length;
38
+ const slice = lines.slice(offset, offset + limit);
39
+ return slice.map((line, i) => `${offset + i + 1}\t${line}`).join('\n');
40
+ }
41
+
42
+ function toolWriteFile(args) {
43
+ const filepath = resolve(CWD, args.path);
44
+ mkdirSync(dirname(filepath), { recursive: true });
45
+ writeFileSync(filepath, args.content, 'utf8');
46
+ return `Written ${args.content.length} bytes to ${args.path}`;
47
+ }
48
+
49
+ function toolEditFile(args) {
50
+ const filepath = resolve(CWD, args.path);
51
+ if (!existsSync(filepath)) return `Error: File not found: ${args.path}`;
52
+ const content = readFileSync(filepath, 'utf8');
53
+ const count = content.split(args.old_string).length - 1;
54
+ if (count === 0) return `Error: old_string not found in ${args.path}`;
55
+ if (count > 1 && !args.replace_all) return `Error: old_string found ${count} times in ${args.path}. Set replace_all to true, or provide more context to make it unique.`;
56
+ const updated = args.replace_all
57
+ ? content.split(args.old_string).join(args.new_string)
58
+ : content.replace(args.old_string, args.new_string);
59
+ writeFileSync(filepath, updated, 'utf8');
60
+ return `Edited ${args.path} (${count} replacement${count > 1 ? 's' : ''})`;
61
+ }
62
+
63
+ function toolRunCommand(args) {
64
+ const timeout = args.timeout || 60000;
65
+ try {
66
+ const output = execSync(args.command, {
67
+ cwd: CWD,
68
+ timeout,
69
+ maxBuffer: 1024 * 1024,
70
+ encoding: 'utf8',
71
+ shell: true,
72
+ stdio: ['pipe', 'pipe', 'pipe'],
73
+ });
74
+ const result = (output || '').slice(0, 102400);
75
+ return result || '(no output)';
76
+ } catch (err) {
77
+ const stderr = err.stderr ? err.stderr.slice(0, 51200) : '';
78
+ const stdout = err.stdout ? err.stdout.slice(0, 51200) : '';
79
+ return `Exit code: ${err.status || 1}\nstdout:\n${stdout}\nstderr:\n${stderr}`;
80
+ }
81
+ }
82
+
83
+ function toolListDirectory(args) {
84
+ const dirpath = resolve(CWD, args.path || '.');
85
+ if (!existsSync(dirpath)) return `Error: Directory not found: ${args.path}`;
86
+ const entries = readdirSync(dirpath, { withFileTypes: true });
87
+ return entries.map(e => {
88
+ const suffix = e.isDirectory() ? '/' : e.isSymbolicLink() ? '@' : '';
89
+ return `${e.name}${suffix}`;
90
+ }).join('\n');
91
+ }
92
+
93
+ function toolGlob(args) {
94
+ const base = resolve(CWD, args.path || '.');
95
+ const regex = globToRegex(args.pattern);
96
+ const matches = [];
97
+ walkDir(base, base, regex, matches, 0);
98
+ return matches.slice(0, 500).join('\n') || '(no matches)';
99
+ }
100
+
101
+ function walkDir(root, dir, regex, matches, depth) {
102
+ if (depth > 20) return;
103
+ let entries;
104
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
105
+ for (const e of entries) {
106
+ if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === '__pycache__') continue;
107
+ const full = join(dir, e.name);
108
+ const rel = relative(root, full);
109
+ if (e.isDirectory()) {
110
+ walkDir(root, full, regex, matches, depth + 1);
111
+ }
112
+ if (regex.test(rel)) matches.push(rel);
113
+ }
114
+ }
115
+
116
+ function globToRegex(pattern) {
117
+ let re = '';
118
+ let i = 0;
119
+ while (i < pattern.length) {
120
+ const c = pattern[i];
121
+ if (c === '*' && pattern[i + 1] === '*') {
122
+ re += '.*';
123
+ i += pattern[i + 2] === '/' ? 3 : 2;
124
+ } else if (c === '*') {
125
+ re += '[^/]*';
126
+ i++;
127
+ } else if (c === '?') {
128
+ re += '[^/]';
129
+ i++;
130
+ } else if (c === '.') {
131
+ re += '\\.';
132
+ i++;
133
+ } else {
134
+ re += c;
135
+ i++;
136
+ }
137
+ }
138
+ return new RegExp('^' + re + '$');
139
+ }
140
+
141
+ function toolGrep(args) {
142
+ const searchPath = resolve(CWD, args.path || '.');
143
+ const includeFlag = args.include ? `--include="${args.include}"` : '';
144
+ try {
145
+ const cmd = `grep -rn ${includeFlag} -- ${JSON.stringify(args.pattern)} ${JSON.stringify(searchPath)} 2>/dev/null | head -200`;
146
+ const output = execSync(cmd, { encoding: 'utf8', maxBuffer: 512 * 1024, timeout: 30000 });
147
+ return (output || '').slice(0, 51200) || '(no matches)';
148
+ } catch (err) {
149
+ if (err.status === 1) return '(no matches)';
150
+ return `Error: ${err.message}`;
151
+ }
152
+ }
153
+
154
+ function executeTool(name, args) {
155
+ try {
156
+ switch (name) {
157
+ case 'read_file': return toolReadFile(args);
158
+ case 'write_file': return toolWriteFile(args);
159
+ case 'edit_file': return toolEditFile(args);
160
+ case 'run_command': return toolRunCommand(args);
161
+ case 'list_directory': return toolListDirectory(args);
162
+ case 'glob': return toolGlob(args);
163
+ case 'grep': return toolGrep(args);
164
+ default: return `Error: Unknown tool: ${name}`;
165
+ }
166
+ } catch (err) {
167
+ return `Error executing ${name}: ${err.message}`;
168
+ }
169
+ }
170
+
171
+ // ---- Tool definitions (OpenAI format) ----
172
+
173
+ const TOOL_DEFINITIONS = [
174
+ {
175
+ type: 'function',
176
+ function: {
177
+ name: 'read_file',
178
+ description: 'Read the contents of a file. Returns lines with line numbers.',
179
+ parameters: {
180
+ type: 'object',
181
+ properties: {
182
+ path: { type: 'string', description: 'File path relative to project root' },
183
+ offset: { type: 'integer', description: 'Start reading from this line number (1-based)' },
184
+ limit: { type: 'integer', description: 'Maximum number of lines to read' },
185
+ },
186
+ required: ['path'],
187
+ },
188
+ },
189
+ },
190
+ {
191
+ type: 'function',
192
+ function: {
193
+ name: 'write_file',
194
+ description: 'Write content to a file. Creates parent directories if needed.',
195
+ parameters: {
196
+ type: 'object',
197
+ properties: {
198
+ path: { type: 'string', description: 'File path relative to project root' },
199
+ content: { type: 'string', description: 'Content to write' },
200
+ },
201
+ required: ['path', 'content'],
202
+ },
203
+ },
204
+ },
205
+ {
206
+ type: 'function',
207
+ function: {
208
+ name: 'edit_file',
209
+ description: 'Replace a string in a file. Fails if old_string is not found or is ambiguous.',
210
+ parameters: {
211
+ type: 'object',
212
+ properties: {
213
+ path: { type: 'string', description: 'File path relative to project root' },
214
+ old_string: { type: 'string', description: 'Text to find and replace' },
215
+ new_string: { type: 'string', description: 'Replacement text' },
216
+ replace_all: { type: 'boolean', description: 'Replace all occurrences (default false)' },
217
+ },
218
+ required: ['path', 'old_string', 'new_string'],
219
+ },
220
+ },
221
+ },
222
+ {
223
+ type: 'function',
224
+ function: {
225
+ name: 'run_command',
226
+ description: 'Execute a shell command and return its output.',
227
+ parameters: {
228
+ type: 'object',
229
+ properties: {
230
+ command: { type: 'string', description: 'Shell command to execute' },
231
+ timeout: { type: 'integer', description: 'Timeout in milliseconds (default 60000)' },
232
+ },
233
+ required: ['command'],
234
+ },
235
+ },
236
+ },
237
+ {
238
+ type: 'function',
239
+ function: {
240
+ name: 'list_directory',
241
+ description: 'List files and directories. Directories have / suffix, symlinks have @ suffix.',
242
+ parameters: {
243
+ type: 'object',
244
+ properties: {
245
+ path: { type: 'string', description: 'Directory path relative to project root' },
246
+ },
247
+ required: ['path'],
248
+ },
249
+ },
250
+ },
251
+ {
252
+ type: 'function',
253
+ function: {
254
+ name: 'glob',
255
+ description: 'Find files matching a glob pattern (supports *, **, ?).',
256
+ parameters: {
257
+ type: 'object',
258
+ properties: {
259
+ pattern: { type: 'string', description: 'Glob pattern (e.g., "**/*.ts", "src/*.js")' },
260
+ path: { type: 'string', description: 'Base directory (default: project root)' },
261
+ },
262
+ required: ['pattern'],
263
+ },
264
+ },
265
+ },
266
+ {
267
+ type: 'function',
268
+ function: {
269
+ name: 'grep',
270
+ description: 'Search file contents using regex. Returns matching lines with file paths and line numbers.',
271
+ parameters: {
272
+ type: 'object',
273
+ properties: {
274
+ pattern: { type: 'string', description: 'Regex pattern to search for' },
275
+ path: { type: 'string', description: 'File or directory to search (default: project root)' },
276
+ include: { type: 'string', description: 'File pattern filter (e.g., "*.ts")' },
277
+ },
278
+ required: ['pattern'],
279
+ },
280
+ },
281
+ },
282
+ ];
283
+
284
+ // ---- System prompt ----
285
+
286
+ const SYSTEM_PROMPT = `You are a coding agent working in the directory: ${CWD}
287
+
288
+ You have access to tools for reading, writing, and editing files, running shell commands, listing directories, and searching code. Use these tools to accomplish the task given to you.
289
+
290
+ Rules:
291
+ - Always use tools to read files before modifying them.
292
+ - Use tools to verify your work (e.g., read a file after editing to confirm the change).
293
+ - When editing files, provide enough surrounding context in old_string to make the match unique.
294
+ - Be thorough and complete the task fully.`;
295
+
296
+ // ---- SSE streaming API client ----
297
+
298
+ function chatCompletion(messages) {
299
+ return new Promise((resolve, reject) => {
300
+ const url = new URL(`${ENDPOINT}/v1/chat/completions`);
301
+ const isHttps = url.protocol === 'https:';
302
+ const mod = isHttps ? https : http;
303
+
304
+ const body = JSON.stringify({
305
+ model: MODEL,
306
+ messages,
307
+ tools: TOOL_DEFINITIONS,
308
+ tool_choice: 'auto',
309
+ stream: true,
310
+ stream_options: { include_usage: true },
311
+ providerOptions: {
312
+ gateway: {
313
+ tags: [`project:moxie`, `phase:${PHASE}`, `agent:${AGENT_NAME}`],
314
+ user: 'moxie',
315
+ },
316
+ },
317
+ });
318
+
319
+ const options = {
320
+ hostname: url.hostname,
321
+ port: url.port || (isHttps ? 443 : 80),
322
+ path: url.pathname + url.search,
323
+ method: 'POST',
324
+ headers: {
325
+ 'Content-Type': 'application/json',
326
+ 'Authorization': `Bearer ${API_KEY}`,
327
+ 'Content-Length': Buffer.byteLength(body),
328
+ },
329
+ };
330
+
331
+ const req = mod.request(options, (res) => {
332
+ if (res.statusCode !== 200) {
333
+ let errBody = '';
334
+ res.on('data', (chunk) => { errBody += chunk; });
335
+ res.on('end', () => {
336
+ process.stderr.write(`API error ${res.statusCode}: ${errBody.slice(0, 2000)}\n`);
337
+ reject(new Error(`API error ${res.statusCode}`));
338
+ });
339
+ return;
340
+ }
341
+
342
+ let buffer = '';
343
+ let content = '';
344
+ const toolCalls = new Map(); // index -> { id, name, arguments }
345
+ let usage = null;
346
+ let idleTimer = null;
347
+
348
+ const resetIdle = () => {
349
+ if (idleTimer) clearTimeout(idleTimer);
350
+ idleTimer = setTimeout(() => {
351
+ process.stderr.write(`Idle timeout (${IDLE_TIMEOUT_MS}ms) — no SSE events received\n`);
352
+ req.destroy();
353
+ reject(new Error('Idle timeout'));
354
+ }, IDLE_TIMEOUT_MS);
355
+ };
356
+
357
+ resetIdle();
358
+
359
+ res.on('data', (chunk) => {
360
+ resetIdle();
361
+ buffer += chunk.toString();
362
+
363
+ const lines = buffer.split('\n');
364
+ buffer = lines.pop(); // keep incomplete line in buffer
365
+
366
+ for (const line of lines) {
367
+ const trimmed = line.trim();
368
+ if (!trimmed || trimmed.startsWith(':')) continue;
369
+ if (!trimmed.startsWith('data: ')) continue;
370
+
371
+ const data = trimmed.slice(6);
372
+ if (data === '[DONE]') continue;
373
+
374
+ let parsed;
375
+ try { parsed = JSON.parse(data); } catch { continue; }
376
+
377
+ // Usage comes in a separate chunk (stream_options.include_usage)
378
+ if (parsed.usage) {
379
+ usage = parsed.usage;
380
+ }
381
+
382
+ const choice = parsed.choices && parsed.choices[0];
383
+ if (!choice) continue;
384
+
385
+ const delta = choice.delta;
386
+ if (!delta) continue;
387
+
388
+ // Stream text content
389
+ if (delta.content) {
390
+ content += delta.content;
391
+ process.stdout.write(delta.content);
392
+ }
393
+
394
+ // Accumulate tool calls
395
+ if (delta.tool_calls) {
396
+ for (const tc of delta.tool_calls) {
397
+ const idx = tc.index;
398
+ if (!toolCalls.has(idx)) {
399
+ toolCalls.set(idx, { id: tc.id || '', name: '', arguments: '' });
400
+ }
401
+ const entry = toolCalls.get(idx);
402
+ if (tc.id) entry.id = tc.id;
403
+ if (tc.function) {
404
+ if (tc.function.name) entry.name = tc.function.name;
405
+ if (tc.function.arguments) entry.arguments += tc.function.arguments;
406
+ }
407
+ }
408
+ }
409
+ }
410
+ });
411
+
412
+ res.on('end', () => {
413
+ if (idleTimer) clearTimeout(idleTimer);
414
+
415
+ // Convert Map to sorted array
416
+ const sortedTools = [];
417
+ const indices = Array.from(toolCalls.keys()).sort((a, b) => a - b);
418
+ for (const idx of indices) {
419
+ const tc = toolCalls.get(idx);
420
+ sortedTools.push({
421
+ id: tc.id,
422
+ type: 'function',
423
+ function: { name: tc.name, arguments: tc.arguments },
424
+ });
425
+ }
426
+
427
+ resolve({ content, toolCalls: sortedTools, usage });
428
+ });
429
+
430
+ res.on('error', (err) => {
431
+ if (idleTimer) clearTimeout(idleTimer);
432
+ reject(err);
433
+ });
434
+ });
435
+
436
+ req.on('error', reject);
437
+ req.write(body);
438
+ req.end();
439
+ });
440
+ }
441
+
442
+ // ---- Agentic loop ----
443
+
444
+ async function agentLoop() {
445
+ const messages = [
446
+ { role: 'system', content: SYSTEM_PROMPT },
447
+ { role: 'user', content: prompt },
448
+ ];
449
+
450
+ const totalUsage = { input_tokens: 0, output_tokens: 0 };
451
+
452
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
453
+ let response;
454
+ try {
455
+ response = await chatCompletion(messages);
456
+ } catch (err) {
457
+ process.stderr.write(`\nAPI call failed on turn ${turn + 1}: ${err.message}\n`);
458
+ // Retry once
459
+ if (turn === 0 || err.message.includes('Idle timeout')) {
460
+ process.stderr.write('Retrying...\n');
461
+ await new Promise(r => setTimeout(r, 2000));
462
+ try {
463
+ response = await chatCompletion(messages);
464
+ } catch (retryErr) {
465
+ process.stderr.write(`Retry failed: ${retryErr.message}\n`);
466
+ break;
467
+ }
468
+ } else {
469
+ break;
470
+ }
471
+ }
472
+
473
+ // Accumulate usage
474
+ if (response.usage) {
475
+ totalUsage.input_tokens += response.usage.prompt_tokens || 0;
476
+ totalUsage.output_tokens += response.usage.completion_tokens || 0;
477
+ }
478
+
479
+ // No tool calls = final response
480
+ if (!response.toolCalls || response.toolCalls.length === 0) {
481
+ break;
482
+ }
483
+
484
+ // Add assistant message with tool calls
485
+ messages.push({
486
+ role: 'assistant',
487
+ content: response.content || null,
488
+ tool_calls: response.toolCalls,
489
+ });
490
+
491
+ // Execute each tool
492
+ for (const tc of response.toolCalls) {
493
+ let args;
494
+ try {
495
+ args = JSON.parse(tc.function.arguments);
496
+ } catch {
497
+ args = {};
498
+ process.stderr.write(`Failed to parse tool args for ${tc.function.name}\n`);
499
+ }
500
+
501
+ // Log tool call summary
502
+ const summary = Object.entries(args)
503
+ .filter(([, v]) => typeof v === 'string' && v.length < 80)
504
+ .map(([k, v]) => `${k}=${v}`)
505
+ .join(', ');
506
+ process.stdout.write(`\n[tool: ${tc.function.name}] ${summary}\n`);
507
+
508
+ const result = executeTool(tc.function.name, args);
509
+
510
+ messages.push({
511
+ role: 'tool',
512
+ tool_call_id: tc.id,
513
+ content: typeof result === 'string' ? result : JSON.stringify(result),
514
+ });
515
+ }
516
+ }
517
+
518
+ return totalUsage;
519
+ }
520
+
521
+ // ---- Main ----
522
+
523
+ try {
524
+ const usage = await agentLoop();
525
+
526
+ // Emit final result line for moxie's token extraction (Strategy 2)
527
+ process.stdout.write('\n');
528
+ process.stdout.write(JSON.stringify({
529
+ type: 'result',
530
+ subtype: 'success',
531
+ usage: {
532
+ input_tokens: usage.input_tokens,
533
+ output_tokens: usage.output_tokens,
534
+ },
535
+ }) + '\n');
536
+
537
+ process.exit(0);
538
+ } catch (err) {
539
+ process.stderr.write(`\nFatal error: ${err.message}\n`);
540
+ // Still emit a result line so token extraction doesn't fail
541
+ process.stdout.write('\n');
542
+ process.stdout.write(JSON.stringify({
543
+ type: 'result',
544
+ subtype: 'error',
545
+ is_error: true,
546
+ usage: { input_tokens: 0, output_tokens: 0 },
547
+ }) + '\n');
548
+ process.exit(1);
549
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ // moxie/lib/gateway-cost.mjs — Fetch cost data from Vercel AI Gateway Reporting API
3
+ // Zero npm dependencies. Node 14+ compatible.
4
+ // Usage: GATEWAY_API_KEY=xxx node gateway-cost.mjs <endpoint> <start_date> <end_date>
5
+
6
+ import https from 'https';
7
+ import http from 'http';
8
+
9
+ const API_KEY = process.env.GATEWAY_API_KEY;
10
+ const endpoint = process.argv[2] || 'https://ai-gateway.vercel.sh';
11
+ const startDate = process.argv[3];
12
+ const endDate = process.argv[4];
13
+
14
+ if (!API_KEY) { process.stderr.write('ERROR: GATEWAY_API_KEY not set\n'); process.exit(1); }
15
+ if (!startDate || !endDate) { process.stderr.write('ERROR: start_date and end_date required\n'); process.exit(1); }
16
+
17
+ const url = new URL(`${endpoint}/v1/report`);
18
+ url.searchParams.set('start_date', startDate);
19
+ url.searchParams.set('end_date', endDate);
20
+ url.searchParams.set('group_by', 'tag');
21
+ url.searchParams.set('filter_tag', 'project:moxie');
22
+
23
+ const isHttps = url.protocol === 'https:';
24
+ const mod = isHttps ? https : http;
25
+
26
+ const options = {
27
+ hostname: url.hostname,
28
+ port: url.port || (isHttps ? 443 : 80),
29
+ path: url.pathname + url.search,
30
+ method: 'GET',
31
+ headers: {
32
+ 'Authorization': `Bearer ${API_KEY}`,
33
+ 'Accept': 'application/json',
34
+ },
35
+ };
36
+
37
+ const req = mod.request(options, (res) => {
38
+ let body = '';
39
+ res.on('data', (chunk) => { body += chunk; });
40
+ res.on('end', () => {
41
+ if (res.statusCode !== 200) {
42
+ process.stderr.write(`API error ${res.statusCode}: ${body.slice(0, 500)}\n`);
43
+ process.exit(1);
44
+ }
45
+
46
+ try {
47
+ const data = JSON.parse(body);
48
+ const rows = data.results || data.data || data || [];
49
+
50
+ // Normalize into a flat cost array filtered to agent: tags
51
+ const costs = [];
52
+ for (const row of (Array.isArray(rows) ? rows : [])) {
53
+ const tag = row.tag || row.group || '';
54
+ if (!tag.startsWith('agent:')) continue;
55
+ costs.push({
56
+ agent: tag.replace('agent:', ''),
57
+ total_cost: parseFloat(row.total_cost || 0),
58
+ market_cost: parseFloat(row.market_cost || 0),
59
+ input_tokens: parseInt(row.input_tokens || 0, 10),
60
+ output_tokens: parseInt(row.output_tokens || 0, 10),
61
+ request_count: parseInt(row.request_count || 0, 10),
62
+ });
63
+ }
64
+
65
+ process.stdout.write(JSON.stringify({ costs }) + '\n');
66
+ } catch (err) {
67
+ process.stderr.write(`Failed to parse response: ${err.message}\n`);
68
+ process.exit(1);
69
+ }
70
+ });
71
+ });
72
+
73
+ req.on('error', (err) => {
74
+ process.stderr.write(`Network error: ${err.message}\n`);
75
+ process.exit(1);
76
+ });
77
+
78
+ req.end();