verytis 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2026 Verytis. All rights reserved.
2
+
3
+ This software and associated documentation files (the "Software") are proprietary and confidential to Verytis.
4
+
5
+ You are granted a non-exclusive, non-transferable limited right to install and execute the binary package for its intended use as a client and Model Context Protocol (MCP) server for Verytis.
6
+
7
+ You are NOT permitted to:
8
+ 1. Modify, reverse engineer, decompile, or disassemble the Software.
9
+ 2. Sublicense, rent, lease, or redistribute copies of the Software (or modified versions thereof) to third parties.
10
+ 3. Remove, alter, or obscure any copyright, trademark, or other proprietary notices from the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [https://www.verytis.com](https://www.verytis.com) with your browser to see the result.
18
+
19
+ ## CLI event storage
20
+
21
+ To store events from another local project, generate a CLI key from Verytis Settings, then set:
22
+
23
+ ```bash
24
+ export VERYTIS_API_URL="https://www.verytis.com"
25
+ export VERYTIS_API_KEY="vt_live_xxxxx"
26
+ verytis run "node fichier-inexistant.js"
27
+ ```
28
+
29
+ The CLI also reads `VERYTIS_API_KEY` and `VERYTIS_API_URL` from the current project's `.env.local`.
30
+ The key is sent as `Authorization: Bearer <vt_key>` and is not printed in CLI logs.
31
+
32
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
33
+
34
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
35
+
36
+ ## Learn More
37
+
38
+ To learn more about Next.js, take a look at the following resources:
39
+
40
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
41
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
42
+
43
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
44
+
45
+ ## Deploy on Vercel
46
+
47
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
48
+
49
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "verytis",
3
+ "version": "0.1.1",
4
+ "license": "UNLICENSED",
5
+ "files": [
6
+ "packages"
7
+ ],
8
+ "bin": {
9
+ "verytis": "packages/cli/bin/verytis.mjs"
10
+ },
11
+ "scripts": {
12
+ "dev": "next dev",
13
+ "build": "next build",
14
+ "start": "next start",
15
+ "lint": "eslint",
16
+ "test": "vitest run",
17
+ "mcp:verytis": "tsx packages/mcp/server.ts"
18
+ },
19
+ "dependencies": {
20
+ "@base-ui/react": "^1.4.1",
21
+ "@modelcontextprotocol/sdk": "^1.29.0",
22
+ "@supabase/ssr": "^0.10.3",
23
+ "@supabase/supabase-js": "^2.105.4",
24
+ "@vercel/analytics": "^2.0.1",
25
+ "@vercel/speed-insights": "^2.0.0",
26
+ "class-variance-authority": "^0.7.1",
27
+ "clsx": "^2.1.1",
28
+ "date-fns": "^4.1.0",
29
+ "dotenv": "^16.4.7",
30
+ "lucide-react": "^1.14.0",
31
+ "next": "16.2.6",
32
+ "openai": "^6.37.0",
33
+ "react": "19.2.4",
34
+ "react-dom": "19.2.4",
35
+ "react-markdown": "^10.1.0",
36
+ "rehype-highlight": "^7.0.2",
37
+ "shadcn": "^4.7.0",
38
+ "tailwind-merge": "^3.6.0",
39
+ "tsx": "^4.22.0",
40
+ "tw-animate-css": "^1.4.0",
41
+ "zod": "^4.4.3"
42
+ },
43
+ "devDependencies": {
44
+ "@tailwindcss/postcss": "^4",
45
+ "@types/node": "^20",
46
+ "@types/react": "^19",
47
+ "@types/react-dom": "^19",
48
+ "eslint": "^9",
49
+ "eslint-config-next": "16.2.6",
50
+ "tailwindcss": "^4",
51
+ "typescript": "^5",
52
+ "vitest": "^4.1.6"
53
+ }
54
+ }
@@ -0,0 +1,517 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn, spawnSync } from "node:child_process"
4
+ import { createHash } from "node:crypto"
5
+ import { existsSync, readFileSync } from "node:fs"
6
+ import path from "node:path"
7
+
8
+ const MAX_LOG_CHARS = 50000
9
+
10
+ function usage() {
11
+ console.log('Usage: verytis run "npm run build" [--api-key <vt_key>] [--error-id <id>] [--fix-id <id>] [--before-fingerprint <fingerprint>]')
12
+ console.log(' verytis mcp (start the MCP stdio server)')
13
+ console.log("Auth: set VERYTIS_API_KEY=vt_... in your shell or project .env.local")
14
+ console.log("API URL: set VERYTIS_API_URL=https://www.verytis.com (or custom URL)")
15
+ console.log(" verytis --version")
16
+ }
17
+
18
+ function readCliVersion() {
19
+ try {
20
+ const packageJson = JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8"))
21
+ return packageJson.version || "0.0.0"
22
+ } catch {
23
+ return "0.0.0"
24
+ }
25
+ }
26
+
27
+ function parseArgs(argv) {
28
+ const [commandName, ...rest] = argv
29
+
30
+ if (commandName === "mcp") {
31
+ return { commandName, command: "mcp", options: {} }
32
+ }
33
+
34
+ if (commandName !== "run") {
35
+ return { commandName, command: "", options: {} }
36
+ }
37
+
38
+ const commandParts = []
39
+ const options = {}
40
+
41
+ for (let index = 0; index < rest.length; index += 1) {
42
+ const value = rest[index]
43
+
44
+ if (value === "--api-url") {
45
+ options.apiUrl = rest[index + 1]
46
+ index += 1
47
+ } else if (value === "--api-key") {
48
+ options.apiKey = rest[index + 1]
49
+ index += 1
50
+ } else if (value === "--error-id") {
51
+ options.errorId = rest[index + 1]
52
+ index += 1
53
+ } else if (value === "--fix-id") {
54
+ options.fixId = rest[index + 1]
55
+ index += 1
56
+ } else if (value === "--before-fingerprint") {
57
+ options.beforeFingerprint = rest[index + 1]
58
+ index += 1
59
+ } else {
60
+ commandParts.push(value)
61
+ }
62
+ }
63
+
64
+ return {
65
+ commandName,
66
+ command: commandParts.join(" ").trim(),
67
+ options,
68
+ }
69
+ }
70
+
71
+ function parseEnvFileValue(rawValue) {
72
+ const value = rawValue.trim()
73
+ const withoutComment = value
74
+ .replace(/\s+#.*$/, "")
75
+ .trim()
76
+
77
+ if (
78
+ (withoutComment.startsWith('"') && withoutComment.endsWith('"')) ||
79
+ (withoutComment.startsWith("'") && withoutComment.endsWith("'"))
80
+ ) {
81
+ return withoutComment.slice(1, -1)
82
+ }
83
+
84
+ return withoutComment
85
+ }
86
+
87
+ function readLocalCliEnv() {
88
+ const values = {}
89
+ const envFiles = [".env", ".env.local"]
90
+
91
+ for (const envFile of envFiles) {
92
+ const envPath = path.join(process.cwd(), envFile)
93
+
94
+ if (!existsSync(envPath)) {
95
+ continue
96
+ }
97
+
98
+ try {
99
+ const contents = readFileSync(envPath, "utf8")
100
+
101
+ for (const line of contents.split(/\r?\n/)) {
102
+ const match = line.match(/^\s*(?:export\s+)?(VERYTIS_API_KEY|VERYTIS_API_URL|FIXGRAPH_API_KEY|FIXGRAPH_API_URL)\s*=\s*(.*)\s*$/)
103
+
104
+ if (!match) {
105
+ continue
106
+ }
107
+
108
+ values[match[1]] = parseEnvFileValue(match[2])
109
+ }
110
+ } catch {
111
+ // Ignore unreadable local env files; explicit shell env and flags still work.
112
+ }
113
+ }
114
+
115
+ return values
116
+ }
117
+
118
+ function resolveCliConfig(options) {
119
+ const localEnv = readLocalCliEnv()
120
+
121
+ return {
122
+ apiKey: options.apiKey || process.env.VERYTIS_API_KEY || localEnv.VERYTIS_API_KEY || process.env.FIXGRAPH_API_KEY || localEnv.FIXGRAPH_API_KEY || null,
123
+ apiUrl:
124
+ options.apiUrl ||
125
+ process.env.VERYTIS_API_URL ||
126
+ localEnv.VERYTIS_API_URL ||
127
+ process.env.FIXGRAPH_API_URL ||
128
+ localEnv.FIXGRAPH_API_URL ||
129
+ "https://www.verytis.com",
130
+ }
131
+ }
132
+
133
+ function runGit(args) {
134
+ const result = spawnSync("git", args, {
135
+ encoding: "utf8",
136
+ stdio: ["ignore", "pipe", "ignore"],
137
+ })
138
+
139
+ if (result.status !== 0) {
140
+ return null
141
+ }
142
+
143
+ return result.stdout.trim()
144
+ }
145
+
146
+ function captureGitState() {
147
+ const insideTree = runGit(["rev-parse", "--is-inside-work-tree"])
148
+
149
+ if (insideTree !== "true") {
150
+ return {
151
+ available: false,
152
+ changedFiles: [],
153
+ diffStat: null,
154
+ }
155
+ }
156
+
157
+ const changedFiles = (runGit(["diff", "--name-only"]) ?? "")
158
+ .split("\n")
159
+ .map((filePath) => filePath.trim())
160
+ .filter(Boolean)
161
+ const diffStat = runGit(["diff", "--shortstat"]) || null
162
+
163
+ return {
164
+ available: true,
165
+ changedFiles,
166
+ diffStat,
167
+ }
168
+ }
169
+
170
+ function categorizeChangedFile(filePath) {
171
+ const normalized = filePath.replace(/\\/g, "/").toLowerCase()
172
+ const fileName = normalized.split("/").pop() || normalized
173
+
174
+ if (
175
+ fileName === "tsconfig.json" ||
176
+ fileName.startsWith("next.config") ||
177
+ fileName.startsWith("eslint") ||
178
+ fileName.startsWith(".eslintrc") ||
179
+ fileName.startsWith("tailwind.config") ||
180
+ fileName.startsWith("postcss.config") ||
181
+ fileName.startsWith("vercel.") ||
182
+ fileName === ".env.example" ||
183
+ fileName.endsWith(".env.example")
184
+ ) {
185
+ return "config"
186
+ }
187
+
188
+ if (
189
+ fileName === "package.json" ||
190
+ fileName === "package-lock.json" ||
191
+ fileName === "pnpm-lock.yaml" ||
192
+ fileName === "yarn.lock"
193
+ ) {
194
+ return "dependency"
195
+ }
196
+
197
+ if (
198
+ normalized.includes("supabase/migrations/") ||
199
+ normalized.includes("/prisma/") ||
200
+ normalized.startsWith("prisma/") ||
201
+ normalized.endsWith(".sql")
202
+ ) {
203
+ return "database"
204
+ }
205
+
206
+ if (normalized.includes("/app/api/") || normalized.startsWith("app/api/") || normalized.includes("/pages/api/")) {
207
+ return "api_route"
208
+ }
209
+
210
+ if (
211
+ normalized.includes("/lib/supabase/") ||
212
+ normalized.includes("/lib/stripe/") ||
213
+ normalized.includes("/lib/auth/") ||
214
+ normalized.includes("/lib/openai/") ||
215
+ normalized.startsWith("lib/supabase/") ||
216
+ normalized.startsWith("lib/stripe/") ||
217
+ normalized.startsWith("lib/auth/") ||
218
+ normalized.startsWith("lib/openai/")
219
+ ) {
220
+ return "integration"
221
+ }
222
+
223
+ if (
224
+ normalized.includes("__tests__/") ||
225
+ normalized.includes("/test/") ||
226
+ normalized.includes("/tests/") ||
227
+ normalized.includes(".test.") ||
228
+ normalized.includes(".spec.")
229
+ ) {
230
+ return "test"
231
+ }
232
+
233
+ if (
234
+ normalized.includes("/components/") ||
235
+ normalized.startsWith("components/") ||
236
+ normalized.includes("/app/") ||
237
+ normalized.startsWith("app/") ||
238
+ normalized.endsWith(".jsx") ||
239
+ normalized.endsWith(".tsx")
240
+ ) {
241
+ return "ui"
242
+ }
243
+
244
+ return "unknown"
245
+ }
246
+
247
+ function categorizeChangedFiles(filePaths) {
248
+ return Array.from(new Set(filePaths.map(categorizeChangedFile))).sort()
249
+ }
250
+
251
+ function redactSecrets(input) {
252
+ return input
253
+ .replace(/https?:\/\/[^/\s:@]+:[^/\s@]+@/gi, "https://[REDACTED]@")
254
+ .replace(/\bOPENAI_API_KEY\s*=\s*[^\s]+/gi, "OPENAI_API_KEY=[REDACTED]")
255
+ .replace(/\b(?:NEXT_PUBLIC_)?SUPABASE(?:_[A-Z]+)*_KEY\s*=\s*[^\s]+/gi, "SUPABASE_KEY=[REDACTED]")
256
+ .replace(/\bDATABASE_URL\s*=\s*[^\s]+/gi, "DATABASE_URL=[REDACTED]")
257
+ .replace(/\bJWT_SECRET\s*=\s*[^\s]+/gi, "JWT_SECRET=[REDACTED]")
258
+ .replace(/\b[A-Z0-9_]*(?:API_)?(?:KEY|TOKEN|SECRET|PASSWORD|PWD)\s*=\s*[^\s]+/gi, "[SECRET]=[REDACTED]")
259
+ .replace(/"[^"]*(?:key|token|secret|password|pwd)[^"]*"\s*:\s*"[^"]*"/gi, '"[SECRET]":"[REDACTED]"')
260
+ .replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "PRIVATE_KEY=[REDACTED]")
261
+ .replace(/\bPRIVATE_KEY\s*=\s*[^\n]+/gi, "PRIVATE_KEY=[REDACTED]")
262
+ .replace(/\bsk-[A-Za-z0-9_-]{16,}\b/g, "sk-[REDACTED]")
263
+ .replace(/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{16,}\b/g, "github_[REDACTED]")
264
+ .replace(/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, "github_pat_[REDACTED]")
265
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gi, "Bearer [REDACTED]")
266
+ .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[EMAIL_REDACTED]")
267
+ .replace(/\b[A-Za-z]:\\Users\\[^\\\s:)'"`]+(?:\\[^\\\s:)'"`]+)*/g, "[path]")
268
+ .replace(/\b[A-Za-z]:\\[^\\\s:)'"`]+(?:\\[^\\\s:)'"`]+)*/g, "[path]")
269
+ .replace(/\/Users\/[^\s:)'"`]+/g, "[path]")
270
+ .replace(/\/home\/[^\s:)'"`]+/g, "[path]")
271
+ .replace(/\/private\/var\/[^\s:)'"`]+/g, "[path]")
272
+ }
273
+
274
+ function createErrorFingerprint(errorMessage) {
275
+ const normalized = errorMessage
276
+ .toLowerCase()
277
+ .replace(/\b(line|column|col)\s+\d+\b/g, "$1")
278
+ .replace(/:\d+:\d+/g, "")
279
+ .replace(/:\d+/g, "")
280
+ .replace(/(?:[A-Za-z]:)?(?:\/|\\)[\w./\\-]+/g, "[path]")
281
+ .replace(/\b[\w.-]+\.(ts|tsx|js|jsx|mjs|cjs|json|css|scss)\b/g, "[file]")
282
+ .replace(/\s+/g, " ")
283
+ .trim()
284
+
285
+ if (!normalized) {
286
+ return null
287
+ }
288
+
289
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 16)
290
+ }
291
+
292
+ function extractFirstUsefulErrorBlock(output) {
293
+ const lines = output
294
+ .split(/\r?\n/)
295
+ .map((line) => line.trimEnd())
296
+ .filter((line) => line.trim().length > 0)
297
+ const startIndex = lines.findIndex((line) =>
298
+ /(error|failed|exception|module not found|type error|cannot find|permission denied)/i.test(line)
299
+ )
300
+
301
+ if (startIndex === -1) {
302
+ return lines.slice(0, 12).join("\n").slice(0, 4000)
303
+ }
304
+
305
+ return lines.slice(startIndex, startIndex + 10).join("\n").slice(0, 4000)
306
+ }
307
+
308
+ function readPackageContext() {
309
+ const packageJsonPath = path.join(process.cwd(), "package.json")
310
+
311
+ if (!existsSync(packageJsonPath)) {
312
+ return {}
313
+ }
314
+
315
+ try {
316
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"))
317
+ const dependencies = {
318
+ ...(packageJson.dependencies ?? {}),
319
+ ...(packageJson.devDependencies ?? {}),
320
+ }
321
+ const trackedPackages = [
322
+ "next",
323
+ "react",
324
+ "typescript",
325
+ "@supabase/supabase-js",
326
+ "@supabase/ssr",
327
+ "zod",
328
+ "openai",
329
+ "stripe",
330
+ ]
331
+ const packageVersions = Object.fromEntries(
332
+ trackedPackages
333
+ .filter((packageName) => dependencies[packageName])
334
+ .map((packageName) => [packageName, dependencies[packageName]])
335
+ )
336
+
337
+ return { packageVersions }
338
+ } catch {
339
+ return {}
340
+ }
341
+ }
342
+
343
+ function runCommand(command) {
344
+ return new Promise((resolve) => {
345
+ const startedAt = Date.now()
346
+ const child = spawn(command, {
347
+ shell: true,
348
+ stdio: ["inherit", "pipe", "pipe"],
349
+ env: process.env,
350
+ })
351
+ const stdoutChunks = []
352
+ const stderrChunks = []
353
+
354
+ child.stdout.on("data", (chunk) => {
355
+ process.stdout.write(chunk)
356
+ stdoutChunks.push(Buffer.from(chunk))
357
+ })
358
+
359
+ child.stderr.on("data", (chunk) => {
360
+ process.stderr.write(chunk)
361
+ stderrChunks.push(Buffer.from(chunk))
362
+ })
363
+
364
+ child.on("close", (code) => {
365
+ resolve({
366
+ exitCode: code,
367
+ status: code === 0 ? "passed" : "failed",
368
+ durationMs: Date.now() - startedAt,
369
+ stdout: Buffer.concat(stdoutChunks).toString("utf8").slice(0, MAX_LOG_CHARS),
370
+ stderr: Buffer.concat(stderrChunks).toString("utf8").slice(0, MAX_LOG_CHARS),
371
+ })
372
+ })
373
+ })
374
+ }
375
+
376
+ async function postCliEvent(apiUrl, payload, apiKey) {
377
+ const endpoint = `${apiUrl.replace(/\/$/, "")}/api/cli/events`
378
+ const headers = { "Content-Type": "application/json" }
379
+
380
+ if (!apiKey) {
381
+ throw new Error(
382
+ "Verytis could not store this event because the CLI is not authenticated. Set VERYTIS_API_KEY to a valid vt_ key, add it to project .env.local, or pass --api-key."
383
+ )
384
+ }
385
+
386
+ if (apiKey) {
387
+ headers.Authorization = `Bearer ${apiKey}`
388
+ }
389
+
390
+ const response = await fetch(endpoint, {
391
+ method: "POST",
392
+ headers,
393
+ body: JSON.stringify(payload),
394
+ })
395
+
396
+ const data = await response.json().catch(() => null)
397
+
398
+ if (response.status === 401) {
399
+ throw new Error(
400
+ "Verytis could not store this event because the CLI is not authenticated. Set VERYTIS_API_KEY to a valid vt_ key, add it to project .env.local, or pass --api-key."
401
+ )
402
+ }
403
+
404
+ if (!response.ok) {
405
+ throw new Error(data?.error || `Verytis event failed with HTTP ${response.status}`)
406
+ }
407
+
408
+ return data
409
+ }
410
+
411
+ function printEventSummary(metadata, result) {
412
+ const categories = metadata.changeCategories.length > 0 ? metadata.changeCategories.join(", ") : "none"
413
+
414
+ console.log("")
415
+ console.log("Verytis event")
416
+ console.log(`- command status: ${metadata.status}`)
417
+ console.log(`- stored: ${result.stored ? "yes" : "no"}`)
418
+ console.log(`- changed files: ${metadata.changedFiles.length}`)
419
+ console.log(`- categories: ${categories}`)
420
+ console.log(`- diff stat: ${metadata.diffStat || "none"}`)
421
+ console.log(`- created new error: ${metadata.createdNewError ? "yes" : "no"}`)
422
+ }
423
+
424
+ async function main() {
425
+ const argv = process.argv.slice(2)
426
+
427
+ if (argv[0] === "--version" || argv[0] === "-v") {
428
+ console.log(readCliVersion())
429
+ process.exit(0)
430
+ }
431
+
432
+ if (argv[0] === "--help" || argv[0] === "-h") {
433
+ usage()
434
+ process.exit(0)
435
+ }
436
+
437
+ const { commandName, command, options } = parseArgs(argv)
438
+
439
+ if (commandName === "mcp") {
440
+ const serverPath = new URL("../../mcp/server.ts", import.meta.url).pathname
441
+ const child = spawn("npx", ["tsx", serverPath], {
442
+ stdio: "inherit",
443
+ env: process.env,
444
+ })
445
+ child.on("close", (code) => {
446
+ process.exit(code ?? 0)
447
+ })
448
+ return
449
+ }
450
+
451
+ if (commandName !== "run" || !command) {
452
+ usage()
453
+ process.exit(commandName === "run" ? 1 : 0)
454
+ }
455
+
456
+ const beforeGitState = captureGitState()
457
+ const commandResult = await runCommand(command)
458
+ const eventCommand = redactSecrets(command)
459
+ const afterGitState = captureGitState()
460
+ const combinedOutput = `${commandResult.stderr}\n${commandResult.stdout}`
461
+ const redactedStdout = redactSecrets(commandResult.stdout)
462
+ const redactedStderr = redactSecrets(commandResult.stderr)
463
+ const errorBlock =
464
+ commandResult.status === "failed" ? extractFirstUsefulErrorBlock(redactSecrets(combinedOutput)) : ""
465
+ const beforeErrorFingerprint =
466
+ options.beforeFingerprint || process.env.FIXGRAPH_ERROR_FINGERPRINT || null
467
+ const afterErrorFingerprint =
468
+ commandResult.status === "failed" ? createErrorFingerprint(errorBlock) : null
469
+ const errorFingerprint = beforeErrorFingerprint || afterErrorFingerprint
470
+ const changedFiles = afterGitState.available ? afterGitState.changedFiles : beforeGitState.changedFiles
471
+ const changeCategories = categorizeChangedFiles(changedFiles)
472
+ const metadata = {
473
+ command: eventCommand,
474
+ status: commandResult.status,
475
+ exitCode: commandResult.exitCode,
476
+ durationMs: commandResult.durationMs,
477
+ stdout: redactedStdout,
478
+ stderr: redactedStderr,
479
+ redacted:
480
+ eventCommand !== command ||
481
+ redactedStdout !== commandResult.stdout ||
482
+ redactedStderr !== commandResult.stderr,
483
+ sourceCodeCollected: false,
484
+ packageContext: readPackageContext(),
485
+ changedFiles,
486
+ changeCategories,
487
+ diffStat: afterGitState.diffStat,
488
+ errorFingerprint,
489
+ createdNewError: Boolean(
490
+ beforeErrorFingerprint && afterErrorFingerprint && beforeErrorFingerprint !== afterErrorFingerprint
491
+ ),
492
+ afterErrorFingerprint,
493
+ source: "cli",
494
+ }
495
+ const cliConfig = resolveCliConfig(options)
496
+ const payload = {
497
+ errorId: options.errorId || process.env.FIXGRAPH_ERROR_ID || undefined,
498
+ fixId: options.fixId || process.env.FIXGRAPH_FIX_ID || undefined,
499
+ ...metadata,
500
+ }
501
+
502
+ try {
503
+ const result = await postCliEvent(cliConfig.apiUrl, payload, cliConfig.apiKey)
504
+ printEventSummary(result.metadata ?? metadata, result)
505
+ } catch (error) {
506
+ console.warn("")
507
+ console.warn(`Verytis event was not stored: ${error instanceof Error ? error.message : String(error)}`)
508
+ printEventSummary(metadata, { stored: false })
509
+ }
510
+
511
+ process.exit(commandResult.exitCode ?? 1)
512
+ }
513
+
514
+ main().catch((error) => {
515
+ console.error(error instanceof Error ? error.message : error)
516
+ process.exit(1)
517
+ })
@@ -0,0 +1,432 @@
1
+ const SEARCH_CAUTION = "Similar cases are not guaranteed to have the same root cause."
2
+ const VARIANT_CAUTION = "Worked in similar cases, not guaranteed for this case."
3
+ const PRIVACY_NOTE = "This payload is anonymized and does not include raw user data."
4
+
5
+ export type SimilarityLevel = "high" | "medium" | "low"
6
+
7
+ // --- Sanitized types for FixGraph API responses (no raw events, no private fields) ---
8
+
9
+ type ApiSearchError = {
10
+ id?: string
11
+ slug?: string
12
+ title?: string
13
+ errorMessage?: string
14
+ fingerprint?: string
15
+ stack?: string[]
16
+ probableCause?: string
17
+ relatedCasesCount?: number
18
+ }
19
+
20
+ type ApiSearchFix = {
21
+ summary?: string
22
+ steps?: string[] | null
23
+ validationCount?: number
24
+ buildPassedCount?: number
25
+ testPassedCount?: number
26
+ confidenceScore?: number
27
+ }
28
+
29
+ type ApiSearchResult = {
30
+ error?: ApiSearchError
31
+ bestFix?: ApiSearchFix
32
+ }
33
+
34
+ type ApiPatternVariant = {
35
+ title?: string
36
+ category?: string
37
+ similarity?: string
38
+ sharedSignals?: string[]
39
+ keyDifferences?: string[]
40
+ occurrenceCount?: number
41
+ commonEnvironment?: string | null
42
+ possibleFix?: string | null
43
+ }
44
+
45
+ type ApiPatternFix = {
46
+ summary?: string
47
+ steps?: string[] | null
48
+ similarity?: string
49
+ confirmationCount?: number
50
+ confidenceScore?: number
51
+ }
52
+
53
+ type ApiPattern = {
54
+ fingerprint?: string
55
+ title?: string
56
+ category?: string
57
+ explanation?: string
58
+ occurrenceCount?: number
59
+ commonEnvironments?: string[]
60
+ variants?: ApiPatternVariant[]
61
+ fixes?: ApiPatternFix[]
62
+ }
63
+
64
+ type ApiBundle = {
65
+ error?: ApiSearchError
66
+ fixes?: ApiSearchFix[]
67
+ bestFix?: ApiSearchFix
68
+ }
69
+
70
+ type ApiErrorResponse = {
71
+ pattern?: ApiPattern
72
+ } & ApiBundle
73
+
74
+ // --- HTTP helpers ---
75
+
76
+ const FETCH_TIMEOUT_MS = 12_000
77
+
78
+ function getApiBase(): string {
79
+ return (process.env.VERYTIS_API_URL ?? process.env.FIXGRAPH_API_URL ?? "https://www.verytis.com").replace(/\/$/, "")
80
+ }
81
+
82
+ function apiHeaders(): Record<string, string> {
83
+ const headers: Record<string, string> = { "Content-Type": "application/json" }
84
+ const key = process.env.VERYTIS_API_KEY ?? process.env.FIXGRAPH_API_KEY
85
+ // Never log the key itself
86
+ if (key) headers["Authorization"] = `Bearer ${key}`
87
+ return headers
88
+ }
89
+
90
+ function debugLog(message: string): void {
91
+ process.stderr.write(`[verytis-mcp] ${message}\n`)
92
+ }
93
+
94
+ async function apiGet(urlPath: string): Promise<{ data: unknown; status: number }> {
95
+ const url = `${getApiBase()}${urlPath}`
96
+ debugLog(`GET ${url}`)
97
+ const response = await fetch(url, {
98
+ headers: apiHeaders(),
99
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
100
+ })
101
+ debugLog(`status ${response.status} for ${urlPath}`)
102
+ if (!response.ok) {
103
+ throw new Error(`Verytis API ${response.status}: ${response.statusText}`)
104
+ }
105
+ const data = await response.json()
106
+ return { data, status: response.status }
107
+ }
108
+
109
+ // --- Token similarity ---
110
+
111
+ function tokenize(text: string): Set<string> {
112
+ const stopwords = new Set(["the", "and", "for", "not", "with", "this", "that"])
113
+ return new Set(
114
+ text
115
+ .toLowerCase()
116
+ .replace(/[^a-z0-9\s@.-]/g, " ")
117
+ .split(/\s+/)
118
+ .map((t) => t.trim())
119
+ .filter((t) => t.length >= 3 && !stopwords.has(t))
120
+ )
121
+ }
122
+
123
+ function jaccard(a: Set<string>, b: Set<string>): number {
124
+ if (a.size === 0 || b.size === 0) return 0
125
+ let intersection = 0
126
+ a.forEach((token) => {
127
+ if (b.has(token)) intersection++
128
+ })
129
+ return intersection / (a.size + b.size - intersection)
130
+ }
131
+
132
+ function computeSimilarity(query: string, title = "", errorMessage = ""): SimilarityLevel {
133
+ const queryTokens = tokenize(query)
134
+ const textTokens = tokenize(`${title} ${errorMessage}`)
135
+ const score = jaccard(queryTokens, textTokens)
136
+ if (score >= 0.3) return "high"
137
+ if (score >= 0.1) return "medium"
138
+ return "low"
139
+ }
140
+
141
+ function normalizeSimilarity(level: string | undefined): SimilarityLevel {
142
+ if (level?.toLowerCase() === "high") return "high"
143
+ if (level?.toLowerCase() === "low") return "low"
144
+ return "medium"
145
+ }
146
+
147
+ // --- Signal extraction ---
148
+
149
+ const FRAMEWORK_TAGS = new Set([
150
+ "next", "nextjs", "react", "vue", "nuxt", "angular",
151
+ "svelte", "astro", "remix", "vite",
152
+ ])
153
+ const RUNTIME_TAGS = new Set(["node", "nodejs", "deno", "bun", "browser"])
154
+
155
+ function categorizeStack(stack: string[]): { frameworks: string[]; runtimes: string[] } {
156
+ const frameworks: string[] = []
157
+ const runtimes: string[] = []
158
+ for (const tag of stack) {
159
+ const lower = tag.toLowerCase()
160
+ if (FRAMEWORK_TAGS.has(lower)) frameworks.push(tag)
161
+ else if (RUNTIME_TAGS.has(lower)) runtimes.push(tag)
162
+ }
163
+ return { frameworks, runtimes }
164
+ }
165
+
166
+ function inferCategory(title: string): string | null {
167
+ const lower = title.toLowerCase()
168
+ if (/module|import|require|resolve/.test(lower)) return "Module resolution"
169
+ if (/export|no exported member/.test(lower)) return "Missing export"
170
+ if (/typescript|cannot find name|property.*does not exist/.test(lower)) return "TypeScript"
171
+ if (/permission|denied|access/.test(lower)) return "Permissions"
172
+ if (/connection|econnrefused|network/.test(lower)) return "Connection"
173
+ if (/syntax|unexpected token/.test(lower)) return "Syntax error"
174
+ return null
175
+ }
176
+
177
+ function extractSharedSignals(
178
+ query: string,
179
+ error: ApiSearchError,
180
+ fix: ApiSearchFix | undefined
181
+ ): string[] {
182
+ const signals: string[] = []
183
+ const queryTokens = tokenize(query)
184
+ const errorTokens = tokenize(`${error.title ?? ""} ${error.errorMessage ?? ""}`)
185
+ const matched = [...queryTokens].filter((t) => errorTokens.has(t)).slice(0, 4)
186
+ if (matched.length > 0) signals.push(`Matched terms: ${matched.join(", ")}`)
187
+
188
+ const { frameworks, runtimes } = categorizeStack(error.stack ?? [])
189
+ if (frameworks.length > 0) signals.push(`Framework: ${frameworks.join(", ")}`)
190
+ if (runtimes.length > 0) signals.push(`Runtime: ${runtimes.join(", ")}`)
191
+
192
+ const category = inferCategory(error.title ?? "")
193
+ if (category) signals.push(`Error category: ${category}`)
194
+
195
+ const validations = fix?.validationCount ?? 0
196
+ if (validations > 0) signals.push(`Validated fix available (${validations} validations)`)
197
+
198
+ return signals.slice(0, 6)
199
+ }
200
+
201
+ // --- Safe text helpers ---
202
+
203
+ import { sanitizePublicText } from "@/lib/fixgraph/redact-secrets"
204
+
205
+ function safeText(text: string | null | undefined, maxLength = 500): string {
206
+ if (!text) return ""
207
+ return sanitizePublicText(text, maxLength)
208
+ }
209
+
210
+ function safePossibleFixes(fix: ApiSearchFix | undefined): string[] {
211
+ if (!fix) return []
212
+ const parts: string[] = []
213
+ if (fix.summary) parts.push(safeText(fix.summary, 1000))
214
+ for (const step of (fix.steps ?? []).slice(0, 6)) {
215
+ const clean = safeText(step, 1000)
216
+ if (clean) parts.push(clean)
217
+ }
218
+ return parts.filter(Boolean).slice(0, 8)
219
+ }
220
+
221
+ // --- Result mapping ---
222
+
223
+ export type McpSearchResult = {
224
+ pattern_id: string
225
+ title: string
226
+ category: string | null
227
+ similarity: SimilarityLevel
228
+ shared_signals: string[]
229
+ key_differences: string[]
230
+ occurrences: number
231
+ common_environment: string[]
232
+ possible_fixes: string[]
233
+ caution: string
234
+ }
235
+
236
+ export type McpSearchResponse = {
237
+ query: string
238
+ results: McpSearchResult[]
239
+ }
240
+
241
+ function apiResultsToMcp(
242
+ params: SearchParams,
243
+ raw: unknown
244
+ ): McpSearchResult[] {
245
+ const allResults: ApiSearchResult[] = Array.isArray(raw) ? raw : []
246
+ const limit = Math.min(params.limit ?? 10, 20)
247
+
248
+ // Client-side framework/runtime filter — fall back to all if filter empties the set
249
+ let filtered = allResults.filter((r) => {
250
+ if (!r.error) return false
251
+ const stack = (r.error.stack ?? []).map((s) => s.toLowerCase())
252
+ if (params.framework && !stack.some((s) => s.includes(params.framework!.toLowerCase()))) return false
253
+ if (params.runtime && !stack.some((s) => s.includes(params.runtime!.toLowerCase()))) return false
254
+ return true
255
+ })
256
+ if (filtered.length === 0) filtered = allResults.filter((r) => Boolean(r.error))
257
+
258
+ return filtered.slice(0, limit).map((r) => {
259
+ const error = r.error ?? {}
260
+ const fix = r.bestFix
261
+ return {
262
+ pattern_id: sanitizePublicText(error.fingerprint ?? error.slug ?? error.id ?? "unknown", 256),
263
+ title: safeText(error.title, 1000) || "Unknown error pattern",
264
+ category: safeText(inferCategory(error.title ?? "")),
265
+ similarity: computeSimilarity(params.query, error.title, error.errorMessage),
266
+ shared_signals: extractSharedSignals(params.query, error, fix).map((s) => safeText(s, 500)),
267
+ key_differences: [].map((d) => safeText(d, 500)),
268
+ occurrences: Number(error.relatedCasesCount ?? 0),
269
+ common_environment: (error.stack ?? []).slice(0, 4).map((e) => safeText(e, 200)),
270
+ possible_fixes: safePossibleFixes(fix),
271
+ caution: safeText(SEARCH_CAUTION),
272
+ } satisfies McpSearchResult
273
+ })
274
+ }
275
+
276
+ // --- Public search function (with MCP-endpoint primary + public fallback) ---
277
+
278
+ export type SearchParams = {
279
+ query: string
280
+ framework?: string
281
+ runtime?: string
282
+ command?: string
283
+ limit?: number
284
+ }
285
+
286
+ async function tryMcpSearch(params: SearchParams): Promise<McpSearchResult[]> {
287
+ const encoded = encodeURIComponent(params.query)
288
+ const limit = Math.min(params.limit ?? 10, 20)
289
+ const { data } = await apiGet(`/api/mcp/search?q=${encoded}&limit=${limit}`)
290
+ const results = apiResultsToMcp(params, data)
291
+ debugLog(`mcp/search: ${results.length} results after scoring`)
292
+ return results
293
+ }
294
+
295
+ async function tryPublicSearch(params: SearchParams): Promise<McpSearchResult[]> {
296
+ const encoded = encodeURIComponent(params.query)
297
+ const { data } = await apiGet(`/api/search?q=${encoded}`)
298
+ const results = apiResultsToMcp(params, data)
299
+ debugLog(`api/search: ${results.length} results after scoring`)
300
+ return results
301
+ }
302
+
303
+ export async function searchVerytisErrors(params: SearchParams): Promise<McpSearchResponse> {
304
+ let results: McpSearchResult[] = []
305
+
306
+ // Primary: MCP-dedicated endpoint uses service client (bypasses RLS)
307
+ try {
308
+ results = await tryMcpSearch(params)
309
+ } catch (err) {
310
+ debugLog(`mcp/search failed: ${err instanceof Error ? err.message : String(err)}`)
311
+ }
312
+
313
+ // Fallback: public search endpoint (anon client, only reads public tables)
314
+ if (results.length === 0) {
315
+ debugLog(`mcp/search returned empty — trying public search fallback`)
316
+ try {
317
+ results = await tryPublicSearch(params)
318
+ } catch (err) {
319
+ debugLog(`api/search failed: ${err instanceof Error ? err.message : String(err)}`)
320
+ }
321
+ }
322
+
323
+ debugLog(`search complete: ${results.length} total results for query "${params.query.slice(0, 80)}"`)
324
+ return { query: params.query, results }
325
+ }
326
+
327
+ // --- Public context function ---
328
+
329
+ export type McpSimilarCase = {
330
+ variant_title: string
331
+ similarity: SimilarityLevel
332
+ shared_signals: string[]
333
+ key_differences: string[]
334
+ observed_fixes: string[]
335
+ caution: string
336
+ }
337
+
338
+ export type McpContextResponse = {
339
+ pattern_id: string
340
+ title: string
341
+ summary: string
342
+ similar_cases: McpSimilarCase[]
343
+ common_causes: string[]
344
+ possible_fixes: string[]
345
+ privacy_note: string
346
+ }
347
+
348
+ function patternToContext(pattern: ApiPattern, patternId: string): McpContextResponse {
349
+ const fixes = pattern.fixes ?? []
350
+ const variants = pattern.variants ?? []
351
+
352
+ const possibleFixes: string[] = []
353
+ for (const fix of fixes.slice(0, 5)) {
354
+ if (fix.summary) possibleFixes.push(safeText(fix.summary, 1000))
355
+ for (const step of (fix.steps ?? []).slice(0, 4)) {
356
+ const clean = safeText(step, 1000)
357
+ if (clean) possibleFixes.push(clean)
358
+ }
359
+ }
360
+
361
+ const similarCases: McpSimilarCase[] = variants.slice(0, 8).map((v) => ({
362
+ variant_title: safeText(v.title, 1000) || "Comparable case",
363
+ similarity: normalizeSimilarity(v.similarity),
364
+ shared_signals: (v.sharedSignals ?? []).slice(0, 5).map((s) => safeText(s, 500)),
365
+ key_differences: (v.keyDifferences ?? []).slice(0, 4).map((d) => safeText(d, 500)),
366
+ observed_fixes: v.possibleFix ? [safeText(v.possibleFix, 1000)] : [],
367
+ caution: safeText(VARIANT_CAUTION),
368
+ }))
369
+
370
+ return {
371
+ pattern_id: sanitizePublicText(pattern.fingerprint ?? patternId, 256),
372
+ title: safeText(pattern.title, 1000) || "Unknown error pattern",
373
+ summary: safeText(pattern.explanation, 3000) || "",
374
+ similar_cases: similarCases,
375
+ common_causes: pattern.explanation ? [safeText(pattern.explanation, 3000)] : [],
376
+ possible_fixes: possibleFixes.filter(Boolean).slice(0, 10),
377
+ privacy_note: safeText(PRIVACY_NOTE),
378
+ }
379
+ }
380
+
381
+ function bundleToContext(bundle: ApiBundle, patternId: string): McpContextResponse {
382
+ const error = bundle.error ?? {}
383
+ const allFixes = bundle.fixes ?? []
384
+ const best = bundle.bestFix ?? allFixes[0]
385
+
386
+ const possibleFixes: string[] = []
387
+ if (best?.summary) possibleFixes.push(safeText(best.summary, 1000))
388
+ for (const step of (best?.steps ?? []).slice(0, 6)) {
389
+ const clean = safeText(step, 1000)
390
+ if (clean) possibleFixes.push(clean)
391
+ }
392
+
393
+ return {
394
+ pattern_id: sanitizePublicText(error.fingerprint ?? patternId, 256),
395
+ title: safeText(error.title, 1000) || "Unknown error pattern",
396
+ summary: safeText(error.probableCause, 3000) || "",
397
+ similar_cases: [],
398
+ common_causes: error.probableCause ? [safeText(error.probableCause, 3000)] : [],
399
+ possible_fixes: possibleFixes.filter(Boolean).slice(0, 10),
400
+ privacy_note: safeText(PRIVACY_NOTE),
401
+ }
402
+ }
403
+
404
+ export async function getVerytisContext(patternId: string): Promise<McpContextResponse> {
405
+ debugLog(`GET /api/errors/${patternId}`)
406
+ const { data } = await apiGet(`/api/errors/${encodeURIComponent(patternId)}`)
407
+ debugLog(`context response received for pattern: ${patternId}`)
408
+
409
+ if (!data || typeof data !== "object") {
410
+ throw new Error(`No context found for pattern: ${patternId}`)
411
+ }
412
+
413
+ const response = data as ApiErrorResponse
414
+
415
+ if ("pattern" in response && response.pattern) {
416
+ const ctx = patternToContext(response.pattern, patternId)
417
+ debugLog(
418
+ `context: ${ctx.similar_cases.length} similar cases, ${ctx.possible_fixes.length} possible fixes`
419
+ )
420
+ return ctx
421
+ }
422
+
423
+ if ("error" in response && response.error) {
424
+ const ctx = bundleToContext(response as ApiBundle, patternId)
425
+ debugLog(
426
+ `context (bundle): ${ctx.common_causes.length} causes, ${ctx.possible_fixes.length} possible fixes`
427
+ )
428
+ return ctx
429
+ }
430
+
431
+ throw new Error(`Unexpected API response for pattern: ${patternId}`)
432
+ }
@@ -0,0 +1,84 @@
1
+ import path from "node:path"
2
+ import { config as dotenvConfig } from "dotenv"
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
+ import { z } from "zod"
6
+
7
+ import { searchVerytisErrors, getVerytisContext } from "./retrieval"
8
+
9
+ // Load env vars from project root (.env.local takes precedence over .env)
10
+ const projectRoot = path.resolve(process.cwd())
11
+ dotenvConfig({ path: path.join(projectRoot, ".env.local"), override: false })
12
+ dotenvConfig({ path: path.join(projectRoot, ".env"), override: false })
13
+
14
+ const server = new McpServer({
15
+ name: "verytis",
16
+ version: "0.1.0",
17
+ })
18
+
19
+ server.tool(
20
+ "search_verytis_errors",
21
+ [
22
+ "Search Verytis for similar historical error patterns before attempting a fix.",
23
+ "Returns anonymized, agent-friendly context: similarity signals, observed environments, occurrence counts, and possible fix approaches.",
24
+ "Do not assume similar cases have the same root cause. Use results as investigation guidance only.",
25
+ ].join(" "),
26
+ {
27
+ query: z.string().min(1).max(5000).describe(
28
+ "Error message, stack trace summary, or description of the error"
29
+ ),
30
+ framework: z.string().max(100).optional().describe(
31
+ "Framework to filter by (e.g. 'next', 'react', 'vue', 'nuxt')"
32
+ ),
33
+ runtime: z.string().max(100).optional().describe(
34
+ "Runtime to filter by (e.g. 'node', 'bun', 'deno')"
35
+ ),
36
+ command: z.string().max(500).optional().describe(
37
+ "Command that failed (e.g. 'npm run build', 'npx tsc')"
38
+ ),
39
+ limit: z.number().int().min(1).max(20).optional().describe(
40
+ "Maximum results to return (default 10, max 20)"
41
+ ),
42
+ },
43
+ async (args) => {
44
+ const result = await searchVerytisErrors(args)
45
+ return {
46
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
47
+ }
48
+ }
49
+ )
50
+
51
+ server.tool(
52
+ "get_verytis_context",
53
+ [
54
+ "Retrieve a full anonymized context bundle for a single error pattern.",
55
+ "Use the pattern_id value from search_verytis_errors results.",
56
+ "Returns similar historical cases, common causes, and observed fix approaches.",
57
+ "No raw source code, user data, or private paths are included.",
58
+ ].join(" "),
59
+ {
60
+ pattern_id: z.string().min(1).max(256).describe(
61
+ "Pattern ID (fingerprint or slug) from search_verytis_errors results"
62
+ ),
63
+ },
64
+ async (args) => {
65
+ const result = await getVerytisContext(args.pattern_id)
66
+ return {
67
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
68
+ }
69
+ }
70
+ )
71
+
72
+ async function main() {
73
+ const transport = new StdioServerTransport()
74
+ await server.connect(transport)
75
+ process.stderr.write(
76
+ `[verytis-mcp] connected — API: ${process.env.VERYTIS_API_URL ?? process.env.FIXGRAPH_API_URL ?? "https://www.verytis.com (default)"}\n`
77
+ )
78
+ }
79
+
80
+ main().catch((error: unknown) => {
81
+ const message = error instanceof Error ? error.message : String(error)
82
+ process.stderr.write(`[verytis-mcp] fatal: ${message}\n`)
83
+ process.exit(1)
84
+ })