@zivis/mcp 0.1.19 → 0.2.5
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/dist/lib/harness-bootstrap.d.ts +9 -0
- package/dist/lib/harness-bootstrap.js +78 -0
- package/dist/matcher/index.js +1 -1
- package/dist/matcher/inference-candidates.js +1 -1
- package/dist/pattern-packs/zivis-public-0.2.0/manifest.json +1 -1
- package/dist/prompts/getting-started.js +26 -2
- package/dist/server.js +24 -143
- package/dist/tools/application.d.ts +53 -0
- package/dist/tools/application.js +232 -0
- package/dist/tools/artifacts.d.ts +21 -57
- package/dist/tools/artifacts.js +116 -150
- package/dist/tools/devx-run.d.ts +15 -37
- package/dist/tools/devx-run.js +66 -77
- package/dist/tools/diagram.d.ts +55 -0
- package/dist/tools/diagram.js +209 -0
- package/dist/tools/document.d.ts +48 -0
- package/dist/tools/document.js +137 -0
- package/dist/tools/finding.d.ts +133 -0
- package/dist/tools/finding.js +211 -0
- package/dist/tools/get-started.d.ts +1 -1
- package/dist/tools/get-started.js +93 -33
- package/dist/tools/security-memory.d.ts +21 -55
- package/dist/tools/security-memory.js +93 -138
- package/dist/tools/signal.d.ts +50 -0
- package/dist/tools/signal.js +139 -0
- package/dist/tools/threat-library.d.ts +27 -0
- package/dist/tools/threat-library.js +266 -0
- package/package.json +1 -1
- package/dist/tools/create-diagram.d.ts +0 -39
- package/dist/tools/create-diagram.js +0 -74
- package/dist/tools/create-document.d.ts +0 -23
- package/dist/tools/create-document.js +0 -47
- package/dist/tools/create-finding.d.ts +0 -85
- package/dist/tools/create-finding.js +0 -136
- package/dist/tools/delete-finding.d.ts +0 -19
- package/dist/tools/delete-finding.js +0 -36
- package/dist/tools/discover-local-infra.d.ts +0 -14
- package/dist/tools/discover-local-infra.js +0 -686
- package/dist/tools/explain-signal-for-diff.d.ts +0 -34
- package/dist/tools/explain-signal-for-diff.js +0 -75
- package/dist/tools/get-application-overview.d.ts +0 -22
- package/dist/tools/get-application-overview.js +0 -137
- package/dist/tools/get-application.d.ts +0 -22
- package/dist/tools/get-application.js +0 -77
- package/dist/tools/get-diagram.d.ts +0 -22
- package/dist/tools/get-diagram.js +0 -49
- package/dist/tools/get-document.d.ts +0 -17
- package/dist/tools/get-document.js +0 -37
- package/dist/tools/get-signal.d.ts +0 -24
- package/dist/tools/get-signal.js +0 -75
- package/dist/tools/import-openapi-endpoints.d.ts +0 -28
- package/dist/tools/import-openapi-endpoints.js +0 -87
- package/dist/tools/inspect.d.ts +0 -21
- package/dist/tools/inspect.js +0 -222
- package/dist/tools/list-applications.d.ts +0 -29
- package/dist/tools/list-applications.js +0 -68
- package/dist/tools/list-diagrams.d.ts +0 -37
- package/dist/tools/list-diagrams.js +0 -74
- package/dist/tools/list-documents.d.ts +0 -19
- package/dist/tools/list-documents.js +0 -44
- package/dist/tools/list-endpoints.d.ts +0 -42
- package/dist/tools/list-endpoints.js +0 -92
- package/dist/tools/list-signals.d.ts +0 -37
- package/dist/tools/list-signals.js +0 -83
- package/dist/tools/manage-application.d.ts +0 -64
- package/dist/tools/manage-application.js +0 -129
- package/dist/tools/manage-diagram.d.ts +0 -41
- package/dist/tools/manage-diagram.js +0 -91
- package/dist/tools/manage-endpoint-lifecycle.d.ts +0 -80
- package/dist/tools/manage-endpoint-lifecycle.js +0 -180
- package/dist/tools/security-review.d.ts +0 -40
- package/dist/tools/security-review.js +0 -199
- package/dist/tools/threat-get-capsule.d.ts +0 -15
- package/dist/tools/threat-get-capsule.js +0 -53
- package/dist/tools/threat-get-inference-prompt.d.ts +0 -15
- package/dist/tools/threat-get-inference-prompt.js +0 -73
- package/dist/tools/threat-list-relevant-capsules.d.ts +0 -17
- package/dist/tools/threat-list-relevant-capsules.js +0 -158
- package/dist/tools/threat-run-matcher.d.ts +0 -17
- package/dist/tools/threat-run-matcher.js +0 -145
- package/dist/tools/update-document.d.ts +0 -23
- package/dist/tools/update-document.js +0 -57
- package/dist/tools/update-endpoint.d.ts +0 -60
- package/dist/tools/update-endpoint.js +0 -138
- package/dist/tools/update-finding.d.ts +0 -61
- package/dist/tools/update-finding.js +0 -80
- package/dist/tools/update-mermaid-source.d.ts +0 -21
- package/dist/tools/update-mermaid-source.js +0 -60
|
@@ -1,686 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { readFile, readdir, stat } from "fs/promises";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { exec } from "child_process";
|
|
5
|
-
import { promisify } from "util";
|
|
6
|
-
const execAsync = promisify(exec);
|
|
7
|
-
export const DISCOVER_LOCAL_INFRA_NAME = "zivis_discover_local_infra";
|
|
8
|
-
export const DISCOVER_LOCAL_INFRA_DESCRIPTION = `Discover AI infrastructure in a local project directory by reading Docker, compose, and dependency files.
|
|
9
|
-
|
|
10
|
-
Analyzes the project to understand what services are defined, which are running, and what AI/LLM frameworks are in use. Returns a suggested target shape (URL, chat endpoint) for the coding agent to use as grounding context before running \`zivis test ai\`.
|
|
11
|
-
|
|
12
|
-
What it reads:
|
|
13
|
-
- docker-compose.yml / docker-compose.yaml / compose.yml / compose.yaml
|
|
14
|
-
- Dockerfile(s) in the project root and subdirectories
|
|
15
|
-
- .env files (structure only — values are redacted for safety)
|
|
16
|
-
- package.json, requirements.txt, pyproject.toml, go.mod (to detect AI frameworks)
|
|
17
|
-
|
|
18
|
-
What it checks:
|
|
19
|
-
- Docker daemon status
|
|
20
|
-
- Currently running containers (docker compose ps)
|
|
21
|
-
- Port mappings and exposed services
|
|
22
|
-
|
|
23
|
-
What it returns:
|
|
24
|
-
- List of services with ports and images
|
|
25
|
-
- Running vs stopped status per service
|
|
26
|
-
- Detected AI/LLM patterns (OpenAI, Anthropic, LangChain, etc.)
|
|
27
|
-
- Suggested target_url/target_type/chat_endpoint — informational context, not a tool call to make
|
|
28
|
-
- Whether the zivis-local-runner Docker image is available
|
|
29
|
-
|
|
30
|
-
For actually testing the discovered AI system, run \`zivis test ai\` in the terminal — this tool only maps what's running locally.
|
|
31
|
-
|
|
32
|
-
This tool makes NO network requests and requires NO authentication. It only reads local files and queries the Docker daemon.`;
|
|
33
|
-
export const DISCOVER_LOCAL_INFRA_SCHEMA = {
|
|
34
|
-
project_dir: z
|
|
35
|
-
.string()
|
|
36
|
-
.optional()
|
|
37
|
-
.describe("Path to the project directory to analyze. Defaults to the current working directory."),
|
|
38
|
-
};
|
|
39
|
-
const NPM_AI_PACKAGES = [
|
|
40
|
-
{ package: "openai", name: "OpenAI SDK" },
|
|
41
|
-
{ package: "@anthropic-ai/sdk", name: "Anthropic SDK" },
|
|
42
|
-
{ package: "ai", name: "Vercel AI SDK" },
|
|
43
|
-
{ package: "@ai-sdk/openai", name: "Vercel AI SDK (OpenAI)" },
|
|
44
|
-
{ package: "@ai-sdk/anthropic", name: "Vercel AI SDK (Anthropic)" },
|
|
45
|
-
{ package: "@langchain/core", name: "LangChain" },
|
|
46
|
-
{ package: "langchain", name: "LangChain" },
|
|
47
|
-
{ package: "llamaindex", name: "LlamaIndex" },
|
|
48
|
-
{ package: "@azure/openai", name: "Azure OpenAI" },
|
|
49
|
-
{ package: "ollama", name: "Ollama" },
|
|
50
|
-
{ package: "cohere-ai", name: "Cohere" },
|
|
51
|
-
{ package: "@google/generative-ai", name: "Google Gemini" },
|
|
52
|
-
{ package: "@mistralai/mistralai", name: "Mistral" },
|
|
53
|
-
{ package: "replicate", name: "Replicate" },
|
|
54
|
-
];
|
|
55
|
-
const PIP_AI_PACKAGES = [
|
|
56
|
-
{ package: "openai", name: "OpenAI SDK" },
|
|
57
|
-
{ package: "anthropic", name: "Anthropic SDK" },
|
|
58
|
-
{ package: "langchain", name: "LangChain" },
|
|
59
|
-
{ package: "llama-index", name: "LlamaIndex" },
|
|
60
|
-
{ package: "transformers", name: "HuggingFace Transformers" },
|
|
61
|
-
{ package: "vllm", name: "vLLM" },
|
|
62
|
-
{ package: "litellm", name: "LiteLLM" },
|
|
63
|
-
{ package: "guidance", name: "Microsoft Guidance" },
|
|
64
|
-
{ package: "google-generativeai", name: "Google Gemini" },
|
|
65
|
-
{ package: "cohere", name: "Cohere" },
|
|
66
|
-
{ package: "ollama", name: "Ollama" },
|
|
67
|
-
{ package: "gradio", name: "Gradio" },
|
|
68
|
-
{ package: "streamlit", name: "Streamlit" },
|
|
69
|
-
];
|
|
70
|
-
const GO_AI_PACKAGES = [
|
|
71
|
-
{ package: "sashabaranov/go-openai", name: "Go OpenAI" },
|
|
72
|
-
{ package: "liushuangls/go-anthropic", name: "Go Anthropic" },
|
|
73
|
-
];
|
|
74
|
-
const AI_ENV_KEYS = [
|
|
75
|
-
"OPENAI_API_KEY",
|
|
76
|
-
"ANTHROPIC_API_KEY",
|
|
77
|
-
"AZURE_OPENAI_API_KEY",
|
|
78
|
-
"AZURE_OPENAI_ENDPOINT",
|
|
79
|
-
"HUGGINGFACE_TOKEN",
|
|
80
|
-
"HF_TOKEN",
|
|
81
|
-
"COHERE_API_KEY",
|
|
82
|
-
"GOOGLE_API_KEY",
|
|
83
|
-
"GOOGLE_GENERATIVE_AI_API_KEY",
|
|
84
|
-
"MISTRAL_API_KEY",
|
|
85
|
-
"REPLICATE_API_TOKEN",
|
|
86
|
-
"OLLAMA_HOST",
|
|
87
|
-
"OLLAMA_BASE_URL",
|
|
88
|
-
"LLM_MODEL",
|
|
89
|
-
"MODEL_NAME",
|
|
90
|
-
"EMBEDDING_MODEL",
|
|
91
|
-
"TOGETHER_API_KEY",
|
|
92
|
-
"GROQ_API_KEY",
|
|
93
|
-
"FIREWORKS_API_KEY",
|
|
94
|
-
"PERPLEXITY_API_KEY",
|
|
95
|
-
];
|
|
96
|
-
const INFRA_IMAGES = [
|
|
97
|
-
"postgres",
|
|
98
|
-
"redis",
|
|
99
|
-
"mongo",
|
|
100
|
-
"mysql",
|
|
101
|
-
"mariadb",
|
|
102
|
-
"nginx",
|
|
103
|
-
"traefik",
|
|
104
|
-
"caddy",
|
|
105
|
-
"azurite",
|
|
106
|
-
"pgadmin",
|
|
107
|
-
"adminer",
|
|
108
|
-
"mailhog",
|
|
109
|
-
"rabbitmq",
|
|
110
|
-
"kafka",
|
|
111
|
-
"zookeeper",
|
|
112
|
-
"elasticsearch",
|
|
113
|
-
"minio",
|
|
114
|
-
"vault",
|
|
115
|
-
"consul",
|
|
116
|
-
"prometheus",
|
|
117
|
-
"grafana",
|
|
118
|
-
"jaeger",
|
|
119
|
-
"memcached",
|
|
120
|
-
];
|
|
121
|
-
const MAX_FILE_SIZE = 100 * 1024;
|
|
122
|
-
export function createDiscoverLocalInfraHandler() {
|
|
123
|
-
return async (params) => {
|
|
124
|
-
const projectDir = params.project_dir || process.cwd();
|
|
125
|
-
const result = {
|
|
126
|
-
docker: { running: false },
|
|
127
|
-
compose: { found: false, services: [] },
|
|
128
|
-
dockerfiles: [],
|
|
129
|
-
aiFrameworks: { detected: [], envHints: [] },
|
|
130
|
-
runnerImage: { available: false },
|
|
131
|
-
suggestion: null,
|
|
132
|
-
nextStep: null,
|
|
133
|
-
};
|
|
134
|
-
try {
|
|
135
|
-
const { stdout } = await execAsync("docker version --format '{{.Server.Version}}'", {
|
|
136
|
-
timeout: 5000,
|
|
137
|
-
});
|
|
138
|
-
result.docker = { running: true, version: stdout.trim().replace(/'/g, "") };
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
result.docker = { running: false };
|
|
142
|
-
}
|
|
143
|
-
const composeFileNames = [
|
|
144
|
-
"docker-compose.yml",
|
|
145
|
-
"docker-compose.yaml",
|
|
146
|
-
"compose.yml",
|
|
147
|
-
"compose.yaml",
|
|
148
|
-
];
|
|
149
|
-
const composeDirs = ["", "infra", "docker", "deploy", "deployment", ".docker"];
|
|
150
|
-
for (const dir of composeDirs) {
|
|
151
|
-
if (result.compose.found)
|
|
152
|
-
break;
|
|
153
|
-
for (const file of composeFileNames) {
|
|
154
|
-
const filePath = dir ? join(projectDir, dir, file) : join(projectDir, file);
|
|
155
|
-
const content = await safeReadFile(filePath);
|
|
156
|
-
if (content) {
|
|
157
|
-
result.compose.found = true;
|
|
158
|
-
result.compose.file = dir ? `${dir}/${file}` : file;
|
|
159
|
-
result.compose.services = parseComposeServices(content);
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (result.docker.running && result.compose.found) {
|
|
165
|
-
try {
|
|
166
|
-
const composeDir = result.compose.file?.includes("/")
|
|
167
|
-
? join(projectDir, result.compose.file.split("/").slice(0, -1).join("/"))
|
|
168
|
-
: projectDir;
|
|
169
|
-
const { stdout } = await execAsync("docker compose ps --format json", {
|
|
170
|
-
timeout: 10000,
|
|
171
|
-
cwd: composeDir,
|
|
172
|
-
});
|
|
173
|
-
const runningServices = parseDockerComposePsOutput(stdout);
|
|
174
|
-
for (const svc of result.compose.services) {
|
|
175
|
-
const running = runningServices.find((r) => r.name === svc.name || r.service === svc.name);
|
|
176
|
-
svc.status = running ? "running" : "stopped";
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const detected = [];
|
|
183
|
-
const pkgJson = await safeReadFile(join(projectDir, "package.json"));
|
|
184
|
-
if (pkgJson) {
|
|
185
|
-
try {
|
|
186
|
-
const pkg = JSON.parse(pkgJson);
|
|
187
|
-
const allDeps = {
|
|
188
|
-
...pkg.dependencies,
|
|
189
|
-
...pkg.devDependencies,
|
|
190
|
-
};
|
|
191
|
-
for (const ai of NPM_AI_PACKAGES) {
|
|
192
|
-
if (allDeps[ai.package]) {
|
|
193
|
-
detected.push({
|
|
194
|
-
name: ai.name,
|
|
195
|
-
source: "package.json",
|
|
196
|
-
package: ai.package,
|
|
197
|
-
version: allDeps[ai.package],
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
const subdirs = await safeReaddir(projectDir);
|
|
206
|
-
const searchDirs = [""];
|
|
207
|
-
for (const dir of subdirs) {
|
|
208
|
-
if (dir.startsWith(".") || dir === "node_modules" || dir === "vendor")
|
|
209
|
-
continue;
|
|
210
|
-
searchDirs.push(dir);
|
|
211
|
-
const subSubDirs = await safeReaddir(join(projectDir, dir));
|
|
212
|
-
for (const sub of subSubDirs) {
|
|
213
|
-
if (sub.startsWith(".") || sub === "node_modules" || sub === "vendor")
|
|
214
|
-
continue;
|
|
215
|
-
searchDirs.push(`${dir}/${sub}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
for (const dir of searchDirs) {
|
|
219
|
-
const base = dir ? join(projectDir, dir) : projectDir;
|
|
220
|
-
const prefix = dir ? `${dir}/` : "";
|
|
221
|
-
const subPkgJson = await safeReadFile(join(base, "package.json"));
|
|
222
|
-
if (subPkgJson) {
|
|
223
|
-
try {
|
|
224
|
-
const pkg = JSON.parse(subPkgJson);
|
|
225
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
226
|
-
for (const ai of NPM_AI_PACKAGES) {
|
|
227
|
-
if (allDeps[ai.package] && !detected.some((d) => d.package === ai.package)) {
|
|
228
|
-
detected.push({
|
|
229
|
-
name: ai.name,
|
|
230
|
-
source: `${prefix}package.json`,
|
|
231
|
-
package: ai.package,
|
|
232
|
-
version: allDeps[ai.package],
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const reqTxt = await safeReadFile(join(base, "requirements.txt"));
|
|
241
|
-
if (reqTxt) {
|
|
242
|
-
const lines = reqTxt.toLowerCase().split("\n");
|
|
243
|
-
for (const ai of PIP_AI_PACKAGES) {
|
|
244
|
-
if (lines.some((line) => line.startsWith(ai.package) ||
|
|
245
|
-
line.includes(`${ai.package}==`) ||
|
|
246
|
-
line.includes(`${ai.package}>=`))) {
|
|
247
|
-
if (!detected.some((d) => d.package === ai.package && d.source.includes(prefix))) {
|
|
248
|
-
detected.push({
|
|
249
|
-
name: ai.name,
|
|
250
|
-
source: `${prefix}requirements.txt`,
|
|
251
|
-
package: ai.package,
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const pyproject = await safeReadFile(join(base, "pyproject.toml"));
|
|
258
|
-
if (pyproject) {
|
|
259
|
-
for (const ai of PIP_AI_PACKAGES) {
|
|
260
|
-
if (pyproject.includes(`"${ai.package}"`) ||
|
|
261
|
-
pyproject.includes(`'${ai.package}'`) ||
|
|
262
|
-
pyproject.includes(ai.package)) {
|
|
263
|
-
if (!detected.some((d) => d.package === ai.package)) {
|
|
264
|
-
detected.push({
|
|
265
|
-
name: ai.name,
|
|
266
|
-
source: `${prefix}pyproject.toml`,
|
|
267
|
-
package: ai.package,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
const goMod = await safeReadFile(join(base, "go.mod"));
|
|
274
|
-
if (goMod) {
|
|
275
|
-
for (const ai of GO_AI_PACKAGES) {
|
|
276
|
-
if (goMod.includes(ai.package)) {
|
|
277
|
-
if (!detected.some((d) => d.package === ai.package)) {
|
|
278
|
-
detected.push({
|
|
279
|
-
name: ai.name,
|
|
280
|
-
source: `${prefix}go.mod`,
|
|
281
|
-
package: ai.package,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
result.aiFrameworks.detected = detected;
|
|
289
|
-
const envFileNames = [".env", ".env.local", ".env.development"];
|
|
290
|
-
const envHints = [];
|
|
291
|
-
for (const dir of ["", "infra", "docker"]) {
|
|
292
|
-
for (const envFile of envFileNames) {
|
|
293
|
-
const filePath = dir ? join(projectDir, dir, envFile) : join(projectDir, envFile);
|
|
294
|
-
const content = await safeReadFile(filePath);
|
|
295
|
-
if (content) {
|
|
296
|
-
const keys = content
|
|
297
|
-
.split("\n")
|
|
298
|
-
.filter((line) => line.includes("=") && !line.startsWith("#"))
|
|
299
|
-
.map((line) => line.split("=")[0].trim())
|
|
300
|
-
.filter(Boolean);
|
|
301
|
-
for (const key of keys) {
|
|
302
|
-
if (AI_ENV_KEYS.some((aiKey) => key.toUpperCase().includes(aiKey) || aiKey.includes(key.toUpperCase()))) {
|
|
303
|
-
envHints.push(key);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
result.aiFrameworks.envHints = [...new Set(envHints)];
|
|
310
|
-
const dockerfiles = [];
|
|
311
|
-
for (const dir of searchDirs) {
|
|
312
|
-
const base = dir ? join(projectDir, dir) : projectDir;
|
|
313
|
-
const prefix = dir ? `${dir}/` : "";
|
|
314
|
-
const dirFiles = await safeReaddir(base);
|
|
315
|
-
for (const file of dirFiles) {
|
|
316
|
-
if (file === "Dockerfile" || file.startsWith("Dockerfile.")) {
|
|
317
|
-
const info = await parseDockerfile(base, file);
|
|
318
|
-
if (info) {
|
|
319
|
-
info.path = `${prefix}${file}`;
|
|
320
|
-
dockerfiles.push(info);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
result.dockerfiles = dockerfiles;
|
|
326
|
-
if (result.docker.running) {
|
|
327
|
-
try {
|
|
328
|
-
const { stdout } = await execAsync("docker images ghcr.io/zivisai/zivis-local-runner --format '{{.Tag}}'", { timeout: 5000 });
|
|
329
|
-
const tag = stdout.trim().replace(/'/g, "");
|
|
330
|
-
if (tag) {
|
|
331
|
-
result.runnerImage = { available: true, tag };
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
result.runnerImage = {
|
|
335
|
-
available: false,
|
|
336
|
-
buildCommand: "cd zivis-rt && docker build -f Dockerfile.local-runner -t ghcr.io/zivisai/zivis-local-runner:latest .",
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
catch {
|
|
341
|
-
result.runnerImage = {
|
|
342
|
-
available: false,
|
|
343
|
-
buildCommand: "cd zivis-rt && docker build -f Dockerfile.local-runner -t ghcr.io/zivisai/zivis-local-runner:latest .",
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
result.suggestion = generateSuggestion(result.compose.services, result.aiFrameworks.detected, result.aiFrameworks.envHints, result.dockerfiles);
|
|
348
|
-
if (result.suggestion) {
|
|
349
|
-
result.nextStep =
|
|
350
|
-
`Detected ${result.suggestion.target_type} target at ${result.suggestion.target_url}` +
|
|
351
|
-
(result.suggestion.chat_endpoint ? ` (chat endpoint: ${result.suggestion.chat_endpoint})` : "") +
|
|
352
|
-
`. Run \`zivis test ai\` to test it.`;
|
|
353
|
-
}
|
|
354
|
-
return {
|
|
355
|
-
content: [
|
|
356
|
-
{
|
|
357
|
-
type: "text",
|
|
358
|
-
text: JSON.stringify(result, null, 2),
|
|
359
|
-
},
|
|
360
|
-
],
|
|
361
|
-
};
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
async function safeReadFile(filePath) {
|
|
365
|
-
try {
|
|
366
|
-
const stats = await stat(filePath);
|
|
367
|
-
if (stats.size > MAX_FILE_SIZE)
|
|
368
|
-
return null;
|
|
369
|
-
const content = await readFile(filePath, "utf-8");
|
|
370
|
-
return content;
|
|
371
|
-
}
|
|
372
|
-
catch {
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
async function safeReaddir(dirPath) {
|
|
377
|
-
try {
|
|
378
|
-
const entries = await readdir(dirPath);
|
|
379
|
-
return entries;
|
|
380
|
-
}
|
|
381
|
-
catch {
|
|
382
|
-
return [];
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
function parseComposeServices(content) {
|
|
386
|
-
const lines = content.split("\n");
|
|
387
|
-
const services = [];
|
|
388
|
-
let inServices = false;
|
|
389
|
-
let currentService = null;
|
|
390
|
-
let currentSection = "";
|
|
391
|
-
let serviceIndent = -1;
|
|
392
|
-
let sectionIndent = -1;
|
|
393
|
-
for (const line of lines) {
|
|
394
|
-
const raw = line;
|
|
395
|
-
const trimmed = raw.trimEnd().trimStart();
|
|
396
|
-
if (trimmed === "" || trimmed.startsWith("#"))
|
|
397
|
-
continue;
|
|
398
|
-
const indent = raw.length - raw.trimStart().length;
|
|
399
|
-
if (indent === 0 && /^services:\s*$/.test(trimmed)) {
|
|
400
|
-
inServices = true;
|
|
401
|
-
serviceIndent = -1;
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
if (inServices && indent === 0) {
|
|
405
|
-
inServices = false;
|
|
406
|
-
if (currentService)
|
|
407
|
-
services.push(currentService);
|
|
408
|
-
currentService = null;
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
if (!inServices)
|
|
412
|
-
continue;
|
|
413
|
-
if (serviceIndent === -1 && /^[\w][\w.-]*:\s*$/.test(trimmed)) {
|
|
414
|
-
serviceIndent = indent;
|
|
415
|
-
}
|
|
416
|
-
if (indent === serviceIndent && /^[\w][\w.-]*:\s*$/.test(trimmed)) {
|
|
417
|
-
if (currentService)
|
|
418
|
-
services.push(currentService);
|
|
419
|
-
const name = trimmed.replace(/:\s*$/, "");
|
|
420
|
-
currentService = {
|
|
421
|
-
name,
|
|
422
|
-
ports: [],
|
|
423
|
-
status: "unknown",
|
|
424
|
-
environmentKeys: [],
|
|
425
|
-
dependsOn: [],
|
|
426
|
-
};
|
|
427
|
-
currentSection = "";
|
|
428
|
-
sectionIndent = -1;
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
if (!currentService)
|
|
432
|
-
continue;
|
|
433
|
-
if (indent > serviceIndent) {
|
|
434
|
-
const propMatch = trimmed.match(/^(image|ports|environment|depends_on|build):\s*(.*)/);
|
|
435
|
-
if (propMatch) {
|
|
436
|
-
const key = propMatch[1];
|
|
437
|
-
const value = propMatch[2].trim();
|
|
438
|
-
if (key === "image" && value) {
|
|
439
|
-
currentService.image = value.replace(/["']/g, "");
|
|
440
|
-
currentSection = "";
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
if (key === "ports" || key === "environment" || key === "depends_on") {
|
|
444
|
-
currentSection = key;
|
|
445
|
-
sectionIndent = indent;
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
currentSection = "";
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
if (currentSection && sectionIndent >= 0 && indent > sectionIndent) {
|
|
452
|
-
if (trimmed.startsWith("-")) {
|
|
453
|
-
const value = trimmed.replace(/^-\s*/, "").replace(/["']/g, "").trim();
|
|
454
|
-
if (currentSection === "ports") {
|
|
455
|
-
const portMatch = value.match(/(\d+):(\d+)/);
|
|
456
|
-
if (portMatch) {
|
|
457
|
-
currentService.ports.push({
|
|
458
|
-
host: parseInt(portMatch[1], 10),
|
|
459
|
-
container: parseInt(portMatch[2], 10),
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
else if (currentSection === "depends_on") {
|
|
464
|
-
currentService.dependsOn.push(value);
|
|
465
|
-
}
|
|
466
|
-
else if (currentSection === "environment") {
|
|
467
|
-
const envKey = value.split("=")[0].trim();
|
|
468
|
-
if (envKey)
|
|
469
|
-
currentService.environmentKeys.push(envKey);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
else if (currentSection === "environment" && trimmed.includes(":")) {
|
|
473
|
-
const envKey = trimmed.split(":")[0].trim();
|
|
474
|
-
if (envKey)
|
|
475
|
-
currentService.environmentKeys.push(envKey);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
else if (indent <= sectionIndent) {
|
|
479
|
-
currentSection = "";
|
|
480
|
-
sectionIndent = -1;
|
|
481
|
-
const rePropMatch = trimmed.match(/^(image|ports|environment|depends_on|build):\s*(.*)/);
|
|
482
|
-
if (rePropMatch) {
|
|
483
|
-
const key = rePropMatch[1];
|
|
484
|
-
const value = rePropMatch[2].trim();
|
|
485
|
-
if (key === "image" && value) {
|
|
486
|
-
currentService.image = value.replace(/["']/g, "");
|
|
487
|
-
}
|
|
488
|
-
else if (key === "ports" || key === "environment" || key === "depends_on") {
|
|
489
|
-
currentSection = key;
|
|
490
|
-
sectionIndent = indent;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
if (currentService)
|
|
497
|
-
services.push(currentService);
|
|
498
|
-
return services;
|
|
499
|
-
}
|
|
500
|
-
function parseDockerComposePsOutput(stdout) {
|
|
501
|
-
const containers = [];
|
|
502
|
-
for (const line of stdout.split("\n")) {
|
|
503
|
-
const trimmed = line.trim();
|
|
504
|
-
if (!trimmed)
|
|
505
|
-
continue;
|
|
506
|
-
try {
|
|
507
|
-
const obj = JSON.parse(trimmed);
|
|
508
|
-
containers.push({
|
|
509
|
-
name: obj.Name || obj.name || "",
|
|
510
|
-
service: obj.Service || obj.service || "",
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
catch {
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return containers;
|
|
517
|
-
}
|
|
518
|
-
async function parseDockerfile(dir, filename) {
|
|
519
|
-
const content = await safeReadFile(join(dir, filename));
|
|
520
|
-
if (!content)
|
|
521
|
-
return null;
|
|
522
|
-
const info = {
|
|
523
|
-
path: filename,
|
|
524
|
-
baseImage: "",
|
|
525
|
-
exposedPorts: [],
|
|
526
|
-
aiHints: [],
|
|
527
|
-
};
|
|
528
|
-
for (const line of content.split("\n")) {
|
|
529
|
-
const trimmed = line.trim();
|
|
530
|
-
if (/^FROM\s+/i.test(trimmed)) {
|
|
531
|
-
const fromMatch = trimmed.match(/^FROM\s+(\S+)/i);
|
|
532
|
-
if (fromMatch && !info.baseImage) {
|
|
533
|
-
info.baseImage = fromMatch[1];
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
if (/^EXPOSE\s+/i.test(trimmed)) {
|
|
537
|
-
const ports = trimmed
|
|
538
|
-
.replace(/^EXPOSE\s+/i, "")
|
|
539
|
-
.split(/\s+/)
|
|
540
|
-
.map((p) => parseInt(p, 10))
|
|
541
|
-
.filter((p) => !isNaN(p));
|
|
542
|
-
info.exposedPorts.push(...ports);
|
|
543
|
-
}
|
|
544
|
-
if (trimmed.includes("pip install") || trimmed.includes("pip3 install")) {
|
|
545
|
-
for (const ai of PIP_AI_PACKAGES) {
|
|
546
|
-
if (trimmed.includes(ai.package)) {
|
|
547
|
-
info.aiHints.push(`Installs ${ai.name} (${ai.package})`);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
if (trimmed.includes("npm install") || trimmed.includes("pnpm add") || trimmed.includes("yarn add")) {
|
|
552
|
-
for (const ai of NPM_AI_PACKAGES) {
|
|
553
|
-
if (trimmed.includes(ai.package)) {
|
|
554
|
-
info.aiHints.push(`Installs ${ai.name} (${ai.package})`);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
if (/^FROM\s+/i.test(trimmed)) {
|
|
559
|
-
const image = trimmed.replace(/^FROM\s+/i, "").split(/\s+/)[0].toLowerCase();
|
|
560
|
-
if (image.includes("ollama"))
|
|
561
|
-
info.aiHints.push("Uses Ollama base image");
|
|
562
|
-
if (image.includes("vllm"))
|
|
563
|
-
info.aiHints.push("Uses vLLM base image");
|
|
564
|
-
if (image.includes("huggingface") || image.includes("transformers"))
|
|
565
|
-
info.aiHints.push("Uses HuggingFace base image");
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
return info;
|
|
569
|
-
}
|
|
570
|
-
function generateSuggestion(services, frameworks, envHints, dockerfiles) {
|
|
571
|
-
const reasons = [];
|
|
572
|
-
const appServices = services.filter((svc) => {
|
|
573
|
-
const image = (svc.image || "").toLowerCase();
|
|
574
|
-
return !INFRA_IMAGES.some((infra) => image.includes(infra));
|
|
575
|
-
});
|
|
576
|
-
const hasAiFrameworks = frameworks.length > 0;
|
|
577
|
-
const hasAiEnvKeys = envHints.length > 0;
|
|
578
|
-
const targetType = hasAiFrameworks || hasAiEnvKeys ? "ai_chat" : "api";
|
|
579
|
-
if (hasAiFrameworks) {
|
|
580
|
-
reasons.push(`Detected AI frameworks: ${frameworks.map((f) => f.name).join(", ")}`);
|
|
581
|
-
}
|
|
582
|
-
if (hasAiEnvKeys) {
|
|
583
|
-
reasons.push(`Found AI-related env vars: ${envHints.join(", ")}`);
|
|
584
|
-
}
|
|
585
|
-
let targetPort = null;
|
|
586
|
-
const runningApps = appServices.filter((svc) => svc.status === "running" && svc.ports.length > 0);
|
|
587
|
-
if (runningApps.length > 0) {
|
|
588
|
-
const preferred = runningApps.find((svc) => svc.ports.some((p) => p.host >= 3000 && p.host <= 9000));
|
|
589
|
-
const target = preferred || runningApps[0];
|
|
590
|
-
targetPort = target.ports[0].host;
|
|
591
|
-
reasons.push(`Service "${target.name}" is running on port ${targetPort}`);
|
|
592
|
-
}
|
|
593
|
-
else if (appServices.length > 0) {
|
|
594
|
-
const withPorts = appServices.find((svc) => svc.ports.length > 0);
|
|
595
|
-
if (withPorts) {
|
|
596
|
-
targetPort = withPorts.ports[0].host;
|
|
597
|
-
reasons.push(`Service "${withPorts.name}" has port ${targetPort} mapped`);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
if (!targetPort) {
|
|
601
|
-
for (const df of dockerfiles) {
|
|
602
|
-
if (df.exposedPorts.length > 0) {
|
|
603
|
-
targetPort = df.exposedPorts[0];
|
|
604
|
-
reasons.push(`Dockerfile "${df.path}" exposes port ${targetPort}`);
|
|
605
|
-
break;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
if (!targetPort) {
|
|
610
|
-
targetPort = 3000;
|
|
611
|
-
reasons.push("Using default port 3000 (no port mapping found)");
|
|
612
|
-
}
|
|
613
|
-
const chatEndpoint = inferChatEndpoint(frameworks);
|
|
614
|
-
if (chatEndpoint) {
|
|
615
|
-
reasons.push(`Inferred endpoint ${chatEndpoint} from framework detection`);
|
|
616
|
-
}
|
|
617
|
-
const { requestTemplate, responsePath } = inferRequestFormat(frameworks);
|
|
618
|
-
let confidence = "low";
|
|
619
|
-
if (hasAiFrameworks && targetPort && runningApps.length > 0) {
|
|
620
|
-
confidence = "high";
|
|
621
|
-
}
|
|
622
|
-
else if (hasAiFrameworks || hasAiEnvKeys) {
|
|
623
|
-
confidence = "medium";
|
|
624
|
-
}
|
|
625
|
-
return {
|
|
626
|
-
target_url: `http://localhost:${targetPort}`,
|
|
627
|
-
target_type: targetType,
|
|
628
|
-
chat_endpoint: chatEndpoint,
|
|
629
|
-
request_template: requestTemplate,
|
|
630
|
-
response_path: responsePath,
|
|
631
|
-
confidence,
|
|
632
|
-
reasoning: reasons.join(". ") + ".",
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
function inferChatEndpoint(frameworks) {
|
|
636
|
-
const names = new Set(frameworks.map((f) => f.name));
|
|
637
|
-
if (names.has("Vercel AI SDK") || names.has("Vercel AI SDK (OpenAI)") || names.has("Vercel AI SDK (Anthropic)"))
|
|
638
|
-
return "/api/chat";
|
|
639
|
-
if (names.has("Ollama"))
|
|
640
|
-
return "/api/chat";
|
|
641
|
-
if (names.has("OpenAI SDK") || names.has("Azure OpenAI"))
|
|
642
|
-
return "/v1/chat/completions";
|
|
643
|
-
if (names.has("LiteLLM"))
|
|
644
|
-
return "/chat/completions";
|
|
645
|
-
if (names.has("vLLM"))
|
|
646
|
-
return "/v1/chat/completions";
|
|
647
|
-
if (names.has("Gradio"))
|
|
648
|
-
return "/api/predict";
|
|
649
|
-
if (frameworks.length > 0)
|
|
650
|
-
return "/api/chat";
|
|
651
|
-
return undefined;
|
|
652
|
-
}
|
|
653
|
-
function inferRequestFormat(frameworks) {
|
|
654
|
-
const names = new Set(frameworks.map((f) => f.name));
|
|
655
|
-
if (names.has("OpenAI SDK") ||
|
|
656
|
-
names.has("Azure OpenAI") ||
|
|
657
|
-
names.has("vLLM") ||
|
|
658
|
-
names.has("LiteLLM")) {
|
|
659
|
-
return {
|
|
660
|
-
requestTemplate: JSON.stringify({
|
|
661
|
-
model: "gpt-4",
|
|
662
|
-
messages: [{ role: "user", content: "{{message}}" }],
|
|
663
|
-
}),
|
|
664
|
-
responsePath: "choices[0].message.content",
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
if (names.has("Vercel AI SDK") || names.has("Vercel AI SDK (OpenAI)") || names.has("Vercel AI SDK (Anthropic)")) {
|
|
668
|
-
return {
|
|
669
|
-
requestTemplate: JSON.stringify({
|
|
670
|
-
messages: [{ role: "user", content: "{{message}}" }],
|
|
671
|
-
}),
|
|
672
|
-
responsePath: undefined,
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
if (names.has("Ollama")) {
|
|
676
|
-
return {
|
|
677
|
-
requestTemplate: JSON.stringify({
|
|
678
|
-
model: "llama3",
|
|
679
|
-
messages: [{ role: "user", content: "{{message}}" }],
|
|
680
|
-
stream: false,
|
|
681
|
-
}),
|
|
682
|
-
responsePath: "message.content",
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
return {};
|
|
686
|
-
}
|