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 +12 -0
- package/README.md +49 -0
- package/package.json +54 -0
- package/packages/cli/bin/verytis.mjs +517 -0
- package/packages/mcp/retrieval.ts +432 -0
- package/packages/mcp/server.ts +84 -0
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
|
+
})
|