@zivis/mcp 0.1.0-alpha.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/dist/api-client.d.ts +42 -0
- package/dist/api-client.js +145 -0
- package/dist/api-client.js.map +1 -0
- package/dist/auth/index.d.ts +29 -0
- package/dist/auth/index.js +73 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/jwt.d.ts +24 -0
- package/dist/auth/jwt.js +25 -0
- package/dist/auth/jwt.js.map +1 -0
- package/dist/auth/keychain.d.ts +34 -0
- package/dist/auth/keychain.js +272 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/oauth.d.ts +38 -0
- package/dist/auth/oauth.js +241 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/sessions.d.ts +28 -0
- package/dist/auth/sessions.js +69 -0
- package/dist/auth/sessions.js.map +1 -0
- package/dist/auth/token-refresh.d.ts +24 -0
- package/dist/auth/token-refresh.js +91 -0
- package/dist/auth/token-refresh.js.map +1 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.js +962 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/project-binding.d.ts +25 -0
- package/dist/project-binding.js +67 -0
- package/dist/project-binding.js.map +1 -0
- package/dist/resolve-application-id.d.ts +12 -0
- package/dist/resolve-application-id.js +21 -0
- package/dist/resolve-application-id.js.map +1 -0
- package/dist/sanitize.d.ts +23 -0
- package/dist/sanitize.js +125 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +447 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/check-project.d.ts +32 -0
- package/dist/tools/check-project.js +180 -0
- package/dist/tools/check-project.js.map +1 -0
- package/dist/tools/check-repo-trust.d.ts +33 -0
- package/dist/tools/check-repo-trust.js +212 -0
- package/dist/tools/check-repo-trust.js.map +1 -0
- package/dist/tools/create-diagram.d.ts +114 -0
- package/dist/tools/create-diagram.js +176 -0
- package/dist/tools/create-diagram.js.map +1 -0
- package/dist/tools/create-document.d.ts +28 -0
- package/dist/tools/create-document.js +53 -0
- package/dist/tools/create-document.js.map +1 -0
- package/dist/tools/discover-local-infra.d.ts +24 -0
- package/dist/tools/discover-local-infra.js +763 -0
- package/dist/tools/discover-local-infra.js.map +1 -0
- package/dist/tools/enterprise/capture-evidence.d.ts +54 -0
- package/dist/tools/enterprise/capture-evidence.js +72 -0
- package/dist/tools/enterprise/capture-evidence.js.map +1 -0
- package/dist/tools/enterprise/create-report-ticket.d.ts +34 -0
- package/dist/tools/enterprise/create-report-ticket.js +44 -0
- package/dist/tools/enterprise/create-report-ticket.js.map +1 -0
- package/dist/tools/enterprise/end-pentest-session.d.ts +33 -0
- package/dist/tools/enterprise/end-pentest-session.js +42 -0
- package/dist/tools/enterprise/end-pentest-session.js.map +1 -0
- package/dist/tools/enterprise/generate-report.d.ts +55 -0
- package/dist/tools/enterprise/generate-report.js +111 -0
- package/dist/tools/enterprise/generate-report.js.map +1 -0
- package/dist/tools/enterprise/get-org-zat.d.ts +27 -0
- package/dist/tools/enterprise/get-org-zat.js +62 -0
- package/dist/tools/enterprise/get-org-zat.js.map +1 -0
- package/dist/tools/enterprise/get-report-status.d.ts +27 -0
- package/dist/tools/enterprise/get-report-status.js +38 -0
- package/dist/tools/enterprise/get-report-status.js.map +1 -0
- package/dist/tools/enterprise/get-threat-model.d.ts +29 -0
- package/dist/tools/enterprise/get-threat-model.js +83 -0
- package/dist/tools/enterprise/get-threat-model.js.map +1 -0
- package/dist/tools/enterprise/list-agents.d.ts +42 -0
- package/dist/tools/enterprise/list-agents.js +95 -0
- package/dist/tools/enterprise/list-agents.js.map +1 -0
- package/dist/tools/enterprise/list-report-issues.d.ts +36 -0
- package/dist/tools/enterprise/list-report-issues.js +50 -0
- package/dist/tools/enterprise/list-report-issues.js.map +1 -0
- package/dist/tools/enterprise/list-threat-models.d.ts +36 -0
- package/dist/tools/enterprise/list-threat-models.js +82 -0
- package/dist/tools/enterprise/list-threat-models.js.map +1 -0
- package/dist/tools/enterprise/manage-actor.d.ts +58 -0
- package/dist/tools/enterprise/manage-actor.js +99 -0
- package/dist/tools/enterprise/manage-actor.js.map +1 -0
- package/dist/tools/enterprise/manage-attack-scenario.d.ts +70 -0
- package/dist/tools/enterprise/manage-attack-scenario.js +89 -0
- package/dist/tools/enterprise/manage-attack-scenario.js.map +1 -0
- package/dist/tools/enterprise/manage-component.d.ts +58 -0
- package/dist/tools/enterprise/manage-component.js +90 -0
- package/dist/tools/enterprise/manage-component.js.map +1 -0
- package/dist/tools/enterprise/manage-data-asset.d.ts +53 -0
- package/dist/tools/enterprise/manage-data-asset.js +90 -0
- package/dist/tools/enterprise/manage-data-asset.js.map +1 -0
- package/dist/tools/enterprise/manage-data-flow.d.ts +57 -0
- package/dist/tools/enterprise/manage-data-flow.js +98 -0
- package/dist/tools/enterprise/manage-data-flow.js.map +1 -0
- package/dist/tools/enterprise/manage-finding.d.ts +69 -0
- package/dist/tools/enterprise/manage-finding.js +106 -0
- package/dist/tools/enterprise/manage-finding.js.map +1 -0
- package/dist/tools/enterprise/manage-security-control.d.ts +59 -0
- package/dist/tools/enterprise/manage-security-control.js +96 -0
- package/dist/tools/enterprise/manage-security-control.js.map +1 -0
- package/dist/tools/enterprise/manage-test-case.d.ts +88 -0
- package/dist/tools/enterprise/manage-test-case.js +119 -0
- package/dist/tools/enterprise/manage-test-case.js.map +1 -0
- package/dist/tools/enterprise/promote-finding-to-issue.d.ts +36 -0
- package/dist/tools/enterprise/promote-finding-to-issue.js +65 -0
- package/dist/tools/enterprise/promote-finding-to-issue.js.map +1 -0
- package/dist/tools/enterprise/publish-report-version.d.ts +34 -0
- package/dist/tools/enterprise/publish-report-version.js +75 -0
- package/dist/tools/enterprise/publish-report-version.js.map +1 -0
- package/dist/tools/enterprise/publish-test-to-library.d.ts +31 -0
- package/dist/tools/enterprise/publish-test-to-library.js +40 -0
- package/dist/tools/enterprise/publish-test-to-library.js.map +1 -0
- package/dist/tools/enterprise/record-test-result.d.ts +67 -0
- package/dist/tools/enterprise/record-test-result.js +75 -0
- package/dist/tools/enterprise/record-test-result.js.map +1 -0
- package/dist/tools/enterprise/start-pentest-session.d.ts +35 -0
- package/dist/tools/enterprise/start-pentest-session.js +50 -0
- package/dist/tools/enterprise/start-pentest-session.js.map +1 -0
- package/dist/tools/enterprise/sync-report-section.d.ts +41 -0
- package/dist/tools/enterprise/sync-report-section.js +79 -0
- package/dist/tools/enterprise/sync-report-section.js.map +1 -0
- package/dist/tools/enterprise/update-report-issue.d.ts +55 -0
- package/dist/tools/enterprise/update-report-issue.js +69 -0
- package/dist/tools/enterprise/update-report-issue.js.map +1 -0
- package/dist/tools/generate-diagram.d.ts +36 -0
- package/dist/tools/generate-diagram.js +153 -0
- package/dist/tools/generate-diagram.js.map +1 -0
- package/dist/tools/get-agent-test-results.d.ts +41 -0
- package/dist/tools/get-agent-test-results.js +86 -0
- package/dist/tools/get-agent-test-results.js.map +1 -0
- package/dist/tools/get-application-overview.d.ts +27 -0
- package/dist/tools/get-application-overview.js +143 -0
- package/dist/tools/get-application-overview.js.map +1 -0
- package/dist/tools/get-application.d.ts +27 -0
- package/dist/tools/get-application.js +83 -0
- package/dist/tools/get-application.js.map +1 -0
- package/dist/tools/get-artifact-content.d.ts +29 -0
- package/dist/tools/get-artifact-content.js +86 -0
- package/dist/tools/get-artifact-content.js.map +1 -0
- package/dist/tools/get-diagram.d.ts +27 -0
- package/dist/tools/get-diagram.js +109 -0
- package/dist/tools/get-diagram.js.map +1 -0
- package/dist/tools/get-document.d.ts +22 -0
- package/dist/tools/get-document.js +43 -0
- package/dist/tools/get-document.js.map +1 -0
- package/dist/tools/get-finding-details.d.ts +28 -0
- package/dist/tools/get-finding-details.js +43 -0
- package/dist/tools/get-finding-details.js.map +1 -0
- package/dist/tools/get-findings.d.ts +57 -0
- package/dist/tools/get-findings.js +103 -0
- package/dist/tools/get-findings.js.map +1 -0
- package/dist/tools/get-oss-zat.d.ts +27 -0
- package/dist/tools/get-oss-zat.js +105 -0
- package/dist/tools/get-oss-zat.js.map +1 -0
- package/dist/tools/get-recon-scan.d.ts +27 -0
- package/dist/tools/get-recon-scan.js +46 -0
- package/dist/tools/get-recon-scan.js.map +1 -0
- package/dist/tools/get-scan-output.d.ts +27 -0
- package/dist/tools/get-scan-output.js +64 -0
- package/dist/tools/get-scan-output.js.map +1 -0
- package/dist/tools/get-scenario-details.d.ts +27 -0
- package/dist/tools/get-scenario-details.js +46 -0
- package/dist/tools/get-scenario-details.js.map +1 -0
- package/dist/tools/get-test-case-results.d.ts +37 -0
- package/dist/tools/get-test-case-results.js +66 -0
- package/dist/tools/get-test-case-results.js.map +1 -0
- package/dist/tools/get-test-details.d.ts +29 -0
- package/dist/tools/get-test-details.js +49 -0
- package/dist/tools/get-test-details.js.map +1 -0
- package/dist/tools/get-trust-keys.d.ts +22 -0
- package/dist/tools/get-trust-keys.js +67 -0
- package/dist/tools/get-trust-keys.js.map +1 -0
- package/dist/tools/import-openapi-endpoints.d.ts +33 -0
- package/dist/tools/import-openapi-endpoints.js +93 -0
- package/dist/tools/import-openapi-endpoints.js.map +1 -0
- package/dist/tools/inspect-zat.d.ts +26 -0
- package/dist/tools/inspect-zat.js +247 -0
- package/dist/tools/inspect-zat.js.map +1 -0
- package/dist/tools/list-agent-test-cases.d.ts +41 -0
- package/dist/tools/list-agent-test-cases.js +73 -0
- package/dist/tools/list-agent-test-cases.js.map +1 -0
- package/dist/tools/list-applications.d.ts +34 -0
- package/dist/tools/list-applications.js +74 -0
- package/dist/tools/list-applications.js.map +1 -0
- package/dist/tools/list-diagrams.d.ts +42 -0
- package/dist/tools/list-diagrams.js +81 -0
- package/dist/tools/list-diagrams.js.map +1 -0
- package/dist/tools/list-documents.d.ts +24 -0
- package/dist/tools/list-documents.js +50 -0
- package/dist/tools/list-documents.js.map +1 -0
- package/dist/tools/list-endpoints.d.ts +47 -0
- package/dist/tools/list-endpoints.js +98 -0
- package/dist/tools/list-endpoints.js.map +1 -0
- package/dist/tools/list-recon-scans.d.ts +36 -0
- package/dist/tools/list-recon-scans.js +74 -0
- package/dist/tools/list-recon-scans.js.map +1 -0
- package/dist/tools/list-scan-artifacts.d.ts +41 -0
- package/dist/tools/list-scan-artifacts.js +63 -0
- package/dist/tools/list-scan-artifacts.js.map +1 -0
- package/dist/tools/list-scans.d.ts +44 -0
- package/dist/tools/list-scans.js +91 -0
- package/dist/tools/list-scans.js.map +1 -0
- package/dist/tools/list-test-library.d.ts +44 -0
- package/dist/tools/list-test-library.js +83 -0
- package/dist/tools/list-test-library.js.map +1 -0
- package/dist/tools/list-test-scenarios.d.ts +31 -0
- package/dist/tools/list-test-scenarios.js +66 -0
- package/dist/tools/list-test-scenarios.js.map +1 -0
- package/dist/tools/local-scan.d.ts +40 -0
- package/dist/tools/local-scan.js +261 -0
- package/dist/tools/local-scan.js.map +1 -0
- package/dist/tools/manage-diagram.d.ts +109 -0
- package/dist/tools/manage-diagram.js +316 -0
- package/dist/tools/manage-diagram.js.map +1 -0
- package/dist/tools/open-in-ide.d.ts +39 -0
- package/dist/tools/open-in-ide.js +61 -0
- package/dist/tools/open-in-ide.js.map +1 -0
- package/dist/tools/run-test-scenario.d.ts +37 -0
- package/dist/tools/run-test-scenario.js +70 -0
- package/dist/tools/run-test-scenario.js.map +1 -0
- package/dist/tools/scan-pr.d.ts +27 -0
- package/dist/tools/scan-pr.js +51 -0
- package/dist/tools/scan-pr.js.map +1 -0
- package/dist/tools/security-review.d.ts +65 -0
- package/dist/tools/security-review.js +378 -0
- package/dist/tools/security-review.js.map +1 -0
- package/dist/tools/setup-red-team-target.d.ts +79 -0
- package/dist/tools/setup-red-team-target.js +430 -0
- package/dist/tools/setup-red-team-target.js.map +1 -0
- package/dist/tools/triage-finding.d.ts +47 -0
- package/dist/tools/triage-finding.js +116 -0
- package/dist/tools/triage-finding.js.map +1 -0
- package/dist/tools/update-document.d.ts +28 -0
- package/dist/tools/update-document.js +63 -0
- package/dist/tools/update-document.js.map +1 -0
- package/dist/tools/update-endpoint.d.ts +65 -0
- package/dist/tools/update-endpoint.js +145 -0
- package/dist/tools/update-endpoint.js.map +1 -0
- package/dist/tools/update-mermaid-source.d.ts +34 -0
- package/dist/tools/update-mermaid-source.js +76 -0
- package/dist/tools/update-mermaid-source.js.map +1 -0
- package/dist/tools/verify-trust-mark.d.ts +29 -0
- package/dist/tools/verify-trust-mark.js +108 -0
- package/dist/tools/verify-trust-mark.js.map +1 -0
- package/dist/types.d.ts +220 -0
- package/dist/types.js +33 -0
- package/dist/types.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zivis_discover_local_infra — Discover AI infrastructure in a local project
|
|
3
|
+
*
|
|
4
|
+
* Reads Docker, compose, and dependency files to understand the local AI
|
|
5
|
+
* infrastructure. Returns structured recommendations for red team testing
|
|
6
|
+
* via zivis_local_scan.
|
|
7
|
+
*
|
|
8
|
+
* This is a local-only tool — no API calls or authentication required.
|
|
9
|
+
* Env values are always redacted for safety.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import { exec } from "child_process";
|
|
15
|
+
import { promisify } from "util";
|
|
16
|
+
const execAsync = promisify(exec);
|
|
17
|
+
// ── Constants ──
|
|
18
|
+
export const DISCOVER_LOCAL_INFRA_NAME = "zivis_discover_local_infra";
|
|
19
|
+
export const DISCOVER_LOCAL_INFRA_DESCRIPTION = `Discover AI infrastructure in a local project directory by reading Docker, compose, and dependency files.
|
|
20
|
+
|
|
21
|
+
Analyzes the project to understand what services are defined, which are running, and what AI/LLM frameworks are in use. Returns structured recommendations for red team testing via zivis_local_scan.
|
|
22
|
+
|
|
23
|
+
What it reads:
|
|
24
|
+
- docker-compose.yml / docker-compose.yaml / compose.yml / compose.yaml
|
|
25
|
+
- Dockerfile(s) in the project root and subdirectories
|
|
26
|
+
- .env files (structure only — values are redacted for safety)
|
|
27
|
+
- package.json, requirements.txt, pyproject.toml, go.mod (to detect AI frameworks)
|
|
28
|
+
|
|
29
|
+
What it checks:
|
|
30
|
+
- Docker daemon status
|
|
31
|
+
- Currently running containers (docker compose ps)
|
|
32
|
+
- Port mappings and exposed services
|
|
33
|
+
|
|
34
|
+
What it returns:
|
|
35
|
+
- List of services with ports and images
|
|
36
|
+
- Running vs stopped status per service
|
|
37
|
+
- Detected AI/LLM patterns (OpenAI, Anthropic, LangChain, etc.)
|
|
38
|
+
- Suggested target_url, chat_endpoint, target_type for zivis_local_scan
|
|
39
|
+
- Whether the zivis-local-runner Docker image is available
|
|
40
|
+
|
|
41
|
+
This tool makes NO network requests and requires NO authentication. It only reads local files and queries the Docker daemon.`;
|
|
42
|
+
export const DISCOVER_LOCAL_INFRA_SCHEMA = {
|
|
43
|
+
project_dir: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Path to the project directory to analyze. Defaults to the current working directory."),
|
|
47
|
+
};
|
|
48
|
+
// ── AI Framework Detection Maps ──
|
|
49
|
+
const NPM_AI_PACKAGES = [
|
|
50
|
+
{ package: "openai", name: "OpenAI SDK" },
|
|
51
|
+
{ package: "@anthropic-ai/sdk", name: "Anthropic SDK" },
|
|
52
|
+
{ package: "ai", name: "Vercel AI SDK" },
|
|
53
|
+
{ package: "@ai-sdk/openai", name: "Vercel AI SDK (OpenAI)" },
|
|
54
|
+
{ package: "@ai-sdk/anthropic", name: "Vercel AI SDK (Anthropic)" },
|
|
55
|
+
{ package: "@langchain/core", name: "LangChain" },
|
|
56
|
+
{ package: "langchain", name: "LangChain" },
|
|
57
|
+
{ package: "llamaindex", name: "LlamaIndex" },
|
|
58
|
+
{ package: "@azure/openai", name: "Azure OpenAI" },
|
|
59
|
+
{ package: "ollama", name: "Ollama" },
|
|
60
|
+
{ package: "cohere-ai", name: "Cohere" },
|
|
61
|
+
{ package: "@google/generative-ai", name: "Google Gemini" },
|
|
62
|
+
{ package: "@mistralai/mistralai", name: "Mistral" },
|
|
63
|
+
{ package: "replicate", name: "Replicate" },
|
|
64
|
+
];
|
|
65
|
+
const PIP_AI_PACKAGES = [
|
|
66
|
+
{ package: "openai", name: "OpenAI SDK" },
|
|
67
|
+
{ package: "anthropic", name: "Anthropic SDK" },
|
|
68
|
+
{ package: "langchain", name: "LangChain" },
|
|
69
|
+
{ package: "llama-index", name: "LlamaIndex" },
|
|
70
|
+
{ package: "transformers", name: "HuggingFace Transformers" },
|
|
71
|
+
{ package: "vllm", name: "vLLM" },
|
|
72
|
+
{ package: "litellm", name: "LiteLLM" },
|
|
73
|
+
{ package: "guidance", name: "Microsoft Guidance" },
|
|
74
|
+
{ package: "google-generativeai", name: "Google Gemini" },
|
|
75
|
+
{ package: "cohere", name: "Cohere" },
|
|
76
|
+
{ package: "ollama", name: "Ollama" },
|
|
77
|
+
{ package: "gradio", name: "Gradio" },
|
|
78
|
+
{ package: "streamlit", name: "Streamlit" },
|
|
79
|
+
];
|
|
80
|
+
const GO_AI_PACKAGES = [
|
|
81
|
+
{ package: "sashabaranov/go-openai", name: "Go OpenAI" },
|
|
82
|
+
{ package: "liushuangls/go-anthropic", name: "Go Anthropic" },
|
|
83
|
+
];
|
|
84
|
+
const AI_ENV_KEYS = [
|
|
85
|
+
"OPENAI_API_KEY",
|
|
86
|
+
"ANTHROPIC_API_KEY",
|
|
87
|
+
"AZURE_OPENAI_API_KEY",
|
|
88
|
+
"AZURE_OPENAI_ENDPOINT",
|
|
89
|
+
"HUGGINGFACE_TOKEN",
|
|
90
|
+
"HF_TOKEN",
|
|
91
|
+
"COHERE_API_KEY",
|
|
92
|
+
"GOOGLE_API_KEY",
|
|
93
|
+
"GOOGLE_GENERATIVE_AI_API_KEY",
|
|
94
|
+
"MISTRAL_API_KEY",
|
|
95
|
+
"REPLICATE_API_TOKEN",
|
|
96
|
+
"OLLAMA_HOST",
|
|
97
|
+
"OLLAMA_BASE_URL",
|
|
98
|
+
"LLM_MODEL",
|
|
99
|
+
"MODEL_NAME",
|
|
100
|
+
"EMBEDDING_MODEL",
|
|
101
|
+
"TOGETHER_API_KEY",
|
|
102
|
+
"GROQ_API_KEY",
|
|
103
|
+
"FIREWORKS_API_KEY",
|
|
104
|
+
"PERPLEXITY_API_KEY",
|
|
105
|
+
];
|
|
106
|
+
const INFRA_IMAGES = [
|
|
107
|
+
"postgres",
|
|
108
|
+
"redis",
|
|
109
|
+
"mongo",
|
|
110
|
+
"mysql",
|
|
111
|
+
"mariadb",
|
|
112
|
+
"nginx",
|
|
113
|
+
"traefik",
|
|
114
|
+
"caddy",
|
|
115
|
+
"azurite",
|
|
116
|
+
"pgadmin",
|
|
117
|
+
"adminer",
|
|
118
|
+
"mailhog",
|
|
119
|
+
"rabbitmq",
|
|
120
|
+
"kafka",
|
|
121
|
+
"zookeeper",
|
|
122
|
+
"elasticsearch",
|
|
123
|
+
"minio",
|
|
124
|
+
"vault",
|
|
125
|
+
"consul",
|
|
126
|
+
"prometheus",
|
|
127
|
+
"grafana",
|
|
128
|
+
"jaeger",
|
|
129
|
+
"memcached",
|
|
130
|
+
];
|
|
131
|
+
const MAX_FILE_SIZE = 100 * 1024; // 100KB cap
|
|
132
|
+
// ── Handler ──
|
|
133
|
+
export function createDiscoverLocalInfraHandler() {
|
|
134
|
+
return async (params) => {
|
|
135
|
+
const projectDir = params.project_dir || process.cwd();
|
|
136
|
+
const result = {
|
|
137
|
+
docker: { running: false },
|
|
138
|
+
compose: { found: false, services: [] },
|
|
139
|
+
dockerfiles: [],
|
|
140
|
+
aiFrameworks: { detected: [], envHints: [] },
|
|
141
|
+
runnerImage: { available: false },
|
|
142
|
+
suggestion: null,
|
|
143
|
+
localScanCommand: null,
|
|
144
|
+
};
|
|
145
|
+
// Step 1: Check Docker daemon
|
|
146
|
+
try {
|
|
147
|
+
const { stdout } = await execAsync("docker version --format '{{.Server.Version}}'", {
|
|
148
|
+
timeout: 5000,
|
|
149
|
+
});
|
|
150
|
+
result.docker = { running: true, version: stdout.trim().replace(/'/g, "") };
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
result.docker = { running: false };
|
|
154
|
+
}
|
|
155
|
+
// Step 2: Find and parse compose files (check root and common subdirs)
|
|
156
|
+
const composeFileNames = [
|
|
157
|
+
"docker-compose.yml",
|
|
158
|
+
"docker-compose.yaml",
|
|
159
|
+
"compose.yml",
|
|
160
|
+
"compose.yaml",
|
|
161
|
+
];
|
|
162
|
+
const composeDirs = ["", "infra", "docker", "deploy", "deployment", ".docker"];
|
|
163
|
+
for (const dir of composeDirs) {
|
|
164
|
+
if (result.compose.found)
|
|
165
|
+
break;
|
|
166
|
+
for (const file of composeFileNames) {
|
|
167
|
+
const filePath = dir ? join(projectDir, dir, file) : join(projectDir, file);
|
|
168
|
+
const content = await safeReadFile(filePath);
|
|
169
|
+
if (content) {
|
|
170
|
+
result.compose.found = true;
|
|
171
|
+
result.compose.file = dir ? `${dir}/${file}` : file;
|
|
172
|
+
result.compose.services = parseComposeServices(content);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Step 3: Check running containers
|
|
178
|
+
if (result.docker.running && result.compose.found) {
|
|
179
|
+
try {
|
|
180
|
+
// Use the directory containing the compose file
|
|
181
|
+
const composeDir = result.compose.file?.includes("/")
|
|
182
|
+
? join(projectDir, result.compose.file.split("/").slice(0, -1).join("/"))
|
|
183
|
+
: projectDir;
|
|
184
|
+
const { stdout } = await execAsync("docker compose ps --format json", {
|
|
185
|
+
timeout: 10000,
|
|
186
|
+
cwd: composeDir,
|
|
187
|
+
});
|
|
188
|
+
const runningServices = parseDockerComposePsOutput(stdout);
|
|
189
|
+
for (const svc of result.compose.services) {
|
|
190
|
+
const running = runningServices.find((r) => r.name === svc.name || r.service === svc.name);
|
|
191
|
+
svc.status = running ? "running" : "stopped";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// docker compose ps failed, leave statuses as unknown
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Step 4: Read dependency files and detect AI frameworks
|
|
199
|
+
const detected = [];
|
|
200
|
+
// package.json
|
|
201
|
+
const pkgJson = await safeReadFile(join(projectDir, "package.json"));
|
|
202
|
+
if (pkgJson) {
|
|
203
|
+
try {
|
|
204
|
+
const pkg = JSON.parse(pkgJson);
|
|
205
|
+
const allDeps = {
|
|
206
|
+
...pkg.dependencies,
|
|
207
|
+
...pkg.devDependencies,
|
|
208
|
+
};
|
|
209
|
+
for (const ai of NPM_AI_PACKAGES) {
|
|
210
|
+
if (allDeps[ai.package]) {
|
|
211
|
+
detected.push({
|
|
212
|
+
name: ai.name,
|
|
213
|
+
source: "package.json",
|
|
214
|
+
package: ai.package,
|
|
215
|
+
version: allDeps[ai.package],
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Invalid JSON, skip
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Search subdirectories (up to 2 levels deep) for dependency files
|
|
225
|
+
const subdirs = await safeReaddir(projectDir);
|
|
226
|
+
const searchDirs = [""];
|
|
227
|
+
for (const dir of subdirs) {
|
|
228
|
+
if (dir.startsWith(".") || dir === "node_modules" || dir === "vendor")
|
|
229
|
+
continue;
|
|
230
|
+
searchDirs.push(dir);
|
|
231
|
+
// Also check one more level (e.g., demo-app/api/, apps/api/)
|
|
232
|
+
const subSubDirs = await safeReaddir(join(projectDir, dir));
|
|
233
|
+
for (const sub of subSubDirs) {
|
|
234
|
+
if (sub.startsWith(".") || sub === "node_modules" || sub === "vendor")
|
|
235
|
+
continue;
|
|
236
|
+
searchDirs.push(`${dir}/${sub}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const dir of searchDirs) {
|
|
240
|
+
const base = dir ? join(projectDir, dir) : projectDir;
|
|
241
|
+
const prefix = dir ? `${dir}/` : "";
|
|
242
|
+
// package.json
|
|
243
|
+
const subPkgJson = await safeReadFile(join(base, "package.json"));
|
|
244
|
+
if (subPkgJson) {
|
|
245
|
+
try {
|
|
246
|
+
const pkg = JSON.parse(subPkgJson);
|
|
247
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
248
|
+
for (const ai of NPM_AI_PACKAGES) {
|
|
249
|
+
if (allDeps[ai.package] && !detected.some((d) => d.package === ai.package)) {
|
|
250
|
+
detected.push({
|
|
251
|
+
name: ai.name,
|
|
252
|
+
source: `${prefix}package.json`,
|
|
253
|
+
package: ai.package,
|
|
254
|
+
version: allDeps[ai.package],
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// Skip
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// requirements.txt
|
|
264
|
+
const reqTxt = await safeReadFile(join(base, "requirements.txt"));
|
|
265
|
+
if (reqTxt) {
|
|
266
|
+
const lines = reqTxt.toLowerCase().split("\n");
|
|
267
|
+
for (const ai of PIP_AI_PACKAGES) {
|
|
268
|
+
if (lines.some((line) => line.startsWith(ai.package) ||
|
|
269
|
+
line.includes(`${ai.package}==`) ||
|
|
270
|
+
line.includes(`${ai.package}>=`))) {
|
|
271
|
+
if (!detected.some((d) => d.package === ai.package && d.source.includes(prefix))) {
|
|
272
|
+
detected.push({
|
|
273
|
+
name: ai.name,
|
|
274
|
+
source: `${prefix}requirements.txt`,
|
|
275
|
+
package: ai.package,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// pyproject.toml
|
|
282
|
+
const pyproject = await safeReadFile(join(base, "pyproject.toml"));
|
|
283
|
+
if (pyproject) {
|
|
284
|
+
for (const ai of PIP_AI_PACKAGES) {
|
|
285
|
+
if (pyproject.includes(`"${ai.package}"`) ||
|
|
286
|
+
pyproject.includes(`'${ai.package}'`) ||
|
|
287
|
+
pyproject.includes(ai.package)) {
|
|
288
|
+
if (!detected.some((d) => d.package === ai.package)) {
|
|
289
|
+
detected.push({
|
|
290
|
+
name: ai.name,
|
|
291
|
+
source: `${prefix}pyproject.toml`,
|
|
292
|
+
package: ai.package,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// go.mod
|
|
299
|
+
const goMod = await safeReadFile(join(base, "go.mod"));
|
|
300
|
+
if (goMod) {
|
|
301
|
+
for (const ai of GO_AI_PACKAGES) {
|
|
302
|
+
if (goMod.includes(ai.package)) {
|
|
303
|
+
if (!detected.some((d) => d.package === ai.package)) {
|
|
304
|
+
detected.push({
|
|
305
|
+
name: ai.name,
|
|
306
|
+
source: `${prefix}go.mod`,
|
|
307
|
+
package: ai.package,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
result.aiFrameworks.detected = detected;
|
|
315
|
+
// Step 5: Read .env files, redact values, flag AI keys
|
|
316
|
+
const envFileNames = [".env", ".env.local", ".env.development"];
|
|
317
|
+
const envHints = [];
|
|
318
|
+
// Check root and subdirs for .env files
|
|
319
|
+
for (const dir of ["", "infra", "docker"]) {
|
|
320
|
+
for (const envFile of envFileNames) {
|
|
321
|
+
const filePath = dir ? join(projectDir, dir, envFile) : join(projectDir, envFile);
|
|
322
|
+
const content = await safeReadFile(filePath);
|
|
323
|
+
if (content) {
|
|
324
|
+
const keys = content
|
|
325
|
+
.split("\n")
|
|
326
|
+
.filter((line) => line.includes("=") && !line.startsWith("#"))
|
|
327
|
+
.map((line) => line.split("=")[0].trim())
|
|
328
|
+
.filter(Boolean);
|
|
329
|
+
for (const key of keys) {
|
|
330
|
+
if (AI_ENV_KEYS.some((aiKey) => key.toUpperCase().includes(aiKey) || aiKey.includes(key.toUpperCase()))) {
|
|
331
|
+
envHints.push(key);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
result.aiFrameworks.envHints = [...new Set(envHints)];
|
|
338
|
+
// Step 6: Scan for Dockerfiles (reuse searchDirs from dependency scan)
|
|
339
|
+
const dockerfiles = [];
|
|
340
|
+
for (const dir of searchDirs) {
|
|
341
|
+
const base = dir ? join(projectDir, dir) : projectDir;
|
|
342
|
+
const prefix = dir ? `${dir}/` : "";
|
|
343
|
+
const dirFiles = await safeReaddir(base);
|
|
344
|
+
for (const file of dirFiles) {
|
|
345
|
+
if (file === "Dockerfile" || file.startsWith("Dockerfile.")) {
|
|
346
|
+
const info = await parseDockerfile(base, file);
|
|
347
|
+
if (info) {
|
|
348
|
+
info.path = `${prefix}${file}`;
|
|
349
|
+
dockerfiles.push(info);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
result.dockerfiles = dockerfiles;
|
|
355
|
+
// Step 7: Check if local runner image exists
|
|
356
|
+
if (result.docker.running) {
|
|
357
|
+
try {
|
|
358
|
+
const { stdout } = await execAsync("docker images ghcr.io/zivisai/zivis-local-runner --format '{{.Tag}}'", { timeout: 5000 });
|
|
359
|
+
const tag = stdout.trim().replace(/'/g, "");
|
|
360
|
+
if (tag) {
|
|
361
|
+
result.runnerImage = { available: true, tag };
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
result.runnerImage = {
|
|
365
|
+
available: false,
|
|
366
|
+
buildCommand: "cd zivis-rt && docker build -f Dockerfile.local-runner -t ghcr.io/zivisai/zivis-local-runner:latest .",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
result.runnerImage = {
|
|
372
|
+
available: false,
|
|
373
|
+
buildCommand: "cd zivis-rt && docker build -f Dockerfile.local-runner -t ghcr.io/zivisai/zivis-local-runner:latest .",
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Step 8: Generate suggestion
|
|
378
|
+
result.suggestion = generateSuggestion(result.compose.services, result.aiFrameworks.detected, result.aiFrameworks.envHints, result.dockerfiles);
|
|
379
|
+
if (result.suggestion) {
|
|
380
|
+
const parts = [`target_url="${result.suggestion.target_url}"`];
|
|
381
|
+
parts.push(`target_type="${result.suggestion.target_type}"`);
|
|
382
|
+
if (result.suggestion.chat_endpoint) {
|
|
383
|
+
parts.push(`chat_endpoint="${result.suggestion.chat_endpoint}"`);
|
|
384
|
+
}
|
|
385
|
+
if (result.suggestion.request_template) {
|
|
386
|
+
parts.push(`request_template='${result.suggestion.request_template}'`);
|
|
387
|
+
}
|
|
388
|
+
if (result.suggestion.response_path) {
|
|
389
|
+
parts.push(`response_path="${result.suggestion.response_path}"`);
|
|
390
|
+
}
|
|
391
|
+
parts.push(`scan_depth="standard"`);
|
|
392
|
+
result.localScanCommand = `zivis_local_scan(${parts.join(", ")})`;
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
content: [
|
|
396
|
+
{
|
|
397
|
+
type: "text",
|
|
398
|
+
text: JSON.stringify(result, null, 2),
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
// ── Helpers ──
|
|
405
|
+
async function safeReadFile(filePath) {
|
|
406
|
+
try {
|
|
407
|
+
const stats = await stat(filePath);
|
|
408
|
+
if (stats.size > MAX_FILE_SIZE)
|
|
409
|
+
return null;
|
|
410
|
+
const content = await readFile(filePath, "utf-8");
|
|
411
|
+
return content;
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async function safeReaddir(dirPath) {
|
|
418
|
+
try {
|
|
419
|
+
const entries = await readdir(dirPath);
|
|
420
|
+
return entries;
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Parse a docker-compose file using line-by-line analysis.
|
|
428
|
+
* Extracts service names, images, ports, environment keys, and depends_on.
|
|
429
|
+
*/
|
|
430
|
+
function parseComposeServices(content) {
|
|
431
|
+
const lines = content.split("\n");
|
|
432
|
+
const services = [];
|
|
433
|
+
let inServices = false;
|
|
434
|
+
let currentService = null;
|
|
435
|
+
let currentSection = ""; // "ports", "environment", "depends_on", ""
|
|
436
|
+
let serviceIndent = -1;
|
|
437
|
+
let sectionIndent = -1;
|
|
438
|
+
for (const line of lines) {
|
|
439
|
+
const raw = line;
|
|
440
|
+
const trimmed = raw.trimEnd().trimStart();
|
|
441
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
442
|
+
continue;
|
|
443
|
+
const indent = raw.length - raw.trimStart().length;
|
|
444
|
+
// Top-level "services:" key (indent 0)
|
|
445
|
+
if (indent === 0 && /^services:\s*$/.test(trimmed)) {
|
|
446
|
+
inServices = true;
|
|
447
|
+
serviceIndent = -1;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
// Another top-level key ends the services block
|
|
451
|
+
if (inServices && indent === 0) {
|
|
452
|
+
inServices = false;
|
|
453
|
+
if (currentService)
|
|
454
|
+
services.push(currentService);
|
|
455
|
+
currentService = null;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (!inServices)
|
|
459
|
+
continue;
|
|
460
|
+
// Detect service names: lines at the first indent level under services
|
|
461
|
+
// that end with ":" and contain only word/dash chars
|
|
462
|
+
if (serviceIndent === -1 && /^[\w][\w.-]*:\s*$/.test(trimmed)) {
|
|
463
|
+
serviceIndent = indent;
|
|
464
|
+
}
|
|
465
|
+
if (indent === serviceIndent && /^[\w][\w.-]*:\s*$/.test(trimmed)) {
|
|
466
|
+
if (currentService)
|
|
467
|
+
services.push(currentService);
|
|
468
|
+
const name = trimmed.replace(/:\s*$/, "");
|
|
469
|
+
currentService = {
|
|
470
|
+
name,
|
|
471
|
+
ports: [],
|
|
472
|
+
status: "unknown",
|
|
473
|
+
environmentKeys: [],
|
|
474
|
+
dependsOn: [],
|
|
475
|
+
};
|
|
476
|
+
currentSection = "";
|
|
477
|
+
sectionIndent = -1;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (!currentService)
|
|
481
|
+
continue;
|
|
482
|
+
// Service properties (indent > serviceIndent)
|
|
483
|
+
if (indent > serviceIndent) {
|
|
484
|
+
// Check for property keys
|
|
485
|
+
const propMatch = trimmed.match(/^(image|ports|environment|depends_on|build):\s*(.*)/);
|
|
486
|
+
if (propMatch) {
|
|
487
|
+
const key = propMatch[1];
|
|
488
|
+
const value = propMatch[2].trim();
|
|
489
|
+
if (key === "image" && value) {
|
|
490
|
+
currentService.image = value.replace(/["']/g, "");
|
|
491
|
+
currentSection = "";
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (key === "ports" || key === "environment" || key === "depends_on") {
|
|
495
|
+
currentSection = key;
|
|
496
|
+
sectionIndent = indent;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
currentSection = "";
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
// If we're at a deeper indent than section, parse items
|
|
503
|
+
if (currentSection && sectionIndent >= 0 && indent > sectionIndent) {
|
|
504
|
+
if (trimmed.startsWith("-")) {
|
|
505
|
+
const value = trimmed.replace(/^-\s*/, "").replace(/["']/g, "").trim();
|
|
506
|
+
if (currentSection === "ports") {
|
|
507
|
+
const portMatch = value.match(/(\d+):(\d+)/);
|
|
508
|
+
if (portMatch) {
|
|
509
|
+
currentService.ports.push({
|
|
510
|
+
host: parseInt(portMatch[1], 10),
|
|
511
|
+
container: parseInt(portMatch[2], 10),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else if (currentSection === "depends_on") {
|
|
516
|
+
currentService.dependsOn.push(value);
|
|
517
|
+
}
|
|
518
|
+
else if (currentSection === "environment") {
|
|
519
|
+
const envKey = value.split("=")[0].trim();
|
|
520
|
+
if (envKey)
|
|
521
|
+
currentService.environmentKeys.push(envKey);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else if (currentSection === "environment" && trimmed.includes(":")) {
|
|
525
|
+
// Environment as mapping (KEY: value)
|
|
526
|
+
const envKey = trimmed.split(":")[0].trim();
|
|
527
|
+
if (envKey)
|
|
528
|
+
currentService.environmentKeys.push(envKey);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
else if (indent <= sectionIndent) {
|
|
532
|
+
// Back to service property level, reset section
|
|
533
|
+
currentSection = "";
|
|
534
|
+
sectionIndent = -1;
|
|
535
|
+
// Re-check for property match
|
|
536
|
+
const rePropMatch = trimmed.match(/^(image|ports|environment|depends_on|build):\s*(.*)/);
|
|
537
|
+
if (rePropMatch) {
|
|
538
|
+
const key = rePropMatch[1];
|
|
539
|
+
const value = rePropMatch[2].trim();
|
|
540
|
+
if (key === "image" && value) {
|
|
541
|
+
currentService.image = value.replace(/["']/g, "");
|
|
542
|
+
}
|
|
543
|
+
else if (key === "ports" || key === "environment" || key === "depends_on") {
|
|
544
|
+
currentSection = key;
|
|
545
|
+
sectionIndent = indent;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (currentService)
|
|
552
|
+
services.push(currentService);
|
|
553
|
+
return services;
|
|
554
|
+
}
|
|
555
|
+
function parseDockerComposePsOutput(stdout) {
|
|
556
|
+
const containers = [];
|
|
557
|
+
// docker compose ps --format json outputs one JSON object per line
|
|
558
|
+
for (const line of stdout.split("\n")) {
|
|
559
|
+
const trimmed = line.trim();
|
|
560
|
+
if (!trimmed)
|
|
561
|
+
continue;
|
|
562
|
+
try {
|
|
563
|
+
const obj = JSON.parse(trimmed);
|
|
564
|
+
containers.push({
|
|
565
|
+
name: obj.Name || obj.name || "",
|
|
566
|
+
service: obj.Service || obj.service || "",
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// Not JSON, skip
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return containers;
|
|
574
|
+
}
|
|
575
|
+
async function parseDockerfile(dir, filename) {
|
|
576
|
+
const content = await safeReadFile(join(dir, filename));
|
|
577
|
+
if (!content)
|
|
578
|
+
return null;
|
|
579
|
+
const info = {
|
|
580
|
+
path: filename,
|
|
581
|
+
baseImage: "",
|
|
582
|
+
exposedPorts: [],
|
|
583
|
+
aiHints: [],
|
|
584
|
+
};
|
|
585
|
+
for (const line of content.split("\n")) {
|
|
586
|
+
const trimmed = line.trim();
|
|
587
|
+
// FROM instruction
|
|
588
|
+
if (/^FROM\s+/i.test(trimmed)) {
|
|
589
|
+
const fromMatch = trimmed.match(/^FROM\s+(\S+)/i);
|
|
590
|
+
if (fromMatch && !info.baseImage) {
|
|
591
|
+
info.baseImage = fromMatch[1];
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// EXPOSE instruction
|
|
595
|
+
if (/^EXPOSE\s+/i.test(trimmed)) {
|
|
596
|
+
const ports = trimmed
|
|
597
|
+
.replace(/^EXPOSE\s+/i, "")
|
|
598
|
+
.split(/\s+/)
|
|
599
|
+
.map((p) => parseInt(p, 10))
|
|
600
|
+
.filter((p) => !isNaN(p));
|
|
601
|
+
info.exposedPorts.push(...ports);
|
|
602
|
+
}
|
|
603
|
+
// AI hints from pip install / npm install
|
|
604
|
+
if (trimmed.includes("pip install") || trimmed.includes("pip3 install")) {
|
|
605
|
+
for (const ai of PIP_AI_PACKAGES) {
|
|
606
|
+
if (trimmed.includes(ai.package)) {
|
|
607
|
+
info.aiHints.push(`Installs ${ai.name} (${ai.package})`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (trimmed.includes("npm install") || trimmed.includes("pnpm add") || trimmed.includes("yarn add")) {
|
|
612
|
+
for (const ai of NPM_AI_PACKAGES) {
|
|
613
|
+
if (trimmed.includes(ai.package)) {
|
|
614
|
+
info.aiHints.push(`Installs ${ai.name} (${ai.package})`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Known AI base images
|
|
619
|
+
if (/^FROM\s+/i.test(trimmed)) {
|
|
620
|
+
const image = trimmed.replace(/^FROM\s+/i, "").split(/\s+/)[0].toLowerCase();
|
|
621
|
+
if (image.includes("ollama"))
|
|
622
|
+
info.aiHints.push("Uses Ollama base image");
|
|
623
|
+
if (image.includes("vllm"))
|
|
624
|
+
info.aiHints.push("Uses vLLM base image");
|
|
625
|
+
if (image.includes("huggingface") || image.includes("transformers"))
|
|
626
|
+
info.aiHints.push("Uses HuggingFace base image");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return info;
|
|
630
|
+
}
|
|
631
|
+
function generateSuggestion(services, frameworks, envHints, dockerfiles) {
|
|
632
|
+
const reasons = [];
|
|
633
|
+
// Filter out infrastructure services
|
|
634
|
+
const appServices = services.filter((svc) => {
|
|
635
|
+
const image = (svc.image || "").toLowerCase();
|
|
636
|
+
return !INFRA_IMAGES.some((infra) => image.includes(infra));
|
|
637
|
+
});
|
|
638
|
+
// Determine target type
|
|
639
|
+
const hasAiFrameworks = frameworks.length > 0;
|
|
640
|
+
const hasAiEnvKeys = envHints.length > 0;
|
|
641
|
+
const targetType = hasAiFrameworks || hasAiEnvKeys ? "ai_chat" : "api";
|
|
642
|
+
if (hasAiFrameworks) {
|
|
643
|
+
reasons.push(`Detected AI frameworks: ${frameworks.map((f) => f.name).join(", ")}`);
|
|
644
|
+
}
|
|
645
|
+
if (hasAiEnvKeys) {
|
|
646
|
+
reasons.push(`Found AI-related env vars: ${envHints.join(", ")}`);
|
|
647
|
+
}
|
|
648
|
+
// Find the best service to target
|
|
649
|
+
let targetPort = null;
|
|
650
|
+
// Prefer running app services with ports
|
|
651
|
+
const runningApps = appServices.filter((svc) => svc.status === "running" && svc.ports.length > 0);
|
|
652
|
+
if (runningApps.length > 0) {
|
|
653
|
+
// Pick the first one with a port in the typical app range
|
|
654
|
+
const preferred = runningApps.find((svc) => svc.ports.some((p) => p.host >= 3000 && p.host <= 9000));
|
|
655
|
+
const target = preferred || runningApps[0];
|
|
656
|
+
targetPort = target.ports[0].host;
|
|
657
|
+
reasons.push(`Service "${target.name}" is running on port ${targetPort}`);
|
|
658
|
+
}
|
|
659
|
+
else if (appServices.length > 0) {
|
|
660
|
+
// Use any app service with ports
|
|
661
|
+
const withPorts = appServices.find((svc) => svc.ports.length > 0);
|
|
662
|
+
if (withPorts) {
|
|
663
|
+
targetPort = withPorts.ports[0].host;
|
|
664
|
+
reasons.push(`Service "${withPorts.name}" has port ${targetPort} mapped`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// Fallback: check Dockerfiles for EXPOSE
|
|
668
|
+
if (!targetPort) {
|
|
669
|
+
for (const df of dockerfiles) {
|
|
670
|
+
if (df.exposedPorts.length > 0) {
|
|
671
|
+
targetPort = df.exposedPorts[0];
|
|
672
|
+
reasons.push(`Dockerfile "${df.path}" exposes port ${targetPort}`);
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// If still no port, try common defaults
|
|
678
|
+
if (!targetPort) {
|
|
679
|
+
targetPort = 3000;
|
|
680
|
+
reasons.push("Using default port 3000 (no port mapping found)");
|
|
681
|
+
}
|
|
682
|
+
// Infer chat endpoint from frameworks
|
|
683
|
+
const chatEndpoint = inferChatEndpoint(frameworks);
|
|
684
|
+
if (chatEndpoint) {
|
|
685
|
+
reasons.push(`Inferred endpoint ${chatEndpoint} from framework detection`);
|
|
686
|
+
}
|
|
687
|
+
// Infer request template and response path
|
|
688
|
+
const { requestTemplate, responsePath } = inferRequestFormat(frameworks);
|
|
689
|
+
// Calculate confidence
|
|
690
|
+
let confidence = "low";
|
|
691
|
+
if (hasAiFrameworks && targetPort && runningApps.length > 0) {
|
|
692
|
+
confidence = "high";
|
|
693
|
+
}
|
|
694
|
+
else if (hasAiFrameworks || hasAiEnvKeys) {
|
|
695
|
+
confidence = "medium";
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
target_url: `http://localhost:${targetPort}`,
|
|
699
|
+
target_type: targetType,
|
|
700
|
+
chat_endpoint: chatEndpoint,
|
|
701
|
+
request_template: requestTemplate,
|
|
702
|
+
response_path: responsePath,
|
|
703
|
+
confidence,
|
|
704
|
+
reasoning: reasons.join(". ") + ".",
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function inferChatEndpoint(frameworks) {
|
|
708
|
+
const names = new Set(frameworks.map((f) => f.name));
|
|
709
|
+
if (names.has("Vercel AI SDK") || names.has("Vercel AI SDK (OpenAI)") || names.has("Vercel AI SDK (Anthropic)"))
|
|
710
|
+
return "/api/chat";
|
|
711
|
+
if (names.has("Ollama"))
|
|
712
|
+
return "/api/chat";
|
|
713
|
+
if (names.has("OpenAI SDK") || names.has("Azure OpenAI"))
|
|
714
|
+
return "/v1/chat/completions";
|
|
715
|
+
if (names.has("LiteLLM"))
|
|
716
|
+
return "/chat/completions";
|
|
717
|
+
if (names.has("vLLM"))
|
|
718
|
+
return "/v1/chat/completions";
|
|
719
|
+
if (names.has("Gradio"))
|
|
720
|
+
return "/api/predict";
|
|
721
|
+
// Default for any detected AI framework
|
|
722
|
+
if (frameworks.length > 0)
|
|
723
|
+
return "/api/chat";
|
|
724
|
+
return undefined;
|
|
725
|
+
}
|
|
726
|
+
function inferRequestFormat(frameworks) {
|
|
727
|
+
const names = new Set(frameworks.map((f) => f.name));
|
|
728
|
+
// OpenAI-compatible APIs
|
|
729
|
+
if (names.has("OpenAI SDK") ||
|
|
730
|
+
names.has("Azure OpenAI") ||
|
|
731
|
+
names.has("vLLM") ||
|
|
732
|
+
names.has("LiteLLM")) {
|
|
733
|
+
return {
|
|
734
|
+
requestTemplate: JSON.stringify({
|
|
735
|
+
model: "gpt-4",
|
|
736
|
+
messages: [{ role: "user", content: "{{message}}" }],
|
|
737
|
+
}),
|
|
738
|
+
responsePath: "choices[0].message.content",
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
// Vercel AI SDK
|
|
742
|
+
if (names.has("Vercel AI SDK") || names.has("Vercel AI SDK (OpenAI)") || names.has("Vercel AI SDK (Anthropic)")) {
|
|
743
|
+
return {
|
|
744
|
+
requestTemplate: JSON.stringify({
|
|
745
|
+
messages: [{ role: "user", content: "{{message}}" }],
|
|
746
|
+
}),
|
|
747
|
+
responsePath: undefined, // Vercel AI SDK streams by default
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
// Ollama
|
|
751
|
+
if (names.has("Ollama")) {
|
|
752
|
+
return {
|
|
753
|
+
requestTemplate: JSON.stringify({
|
|
754
|
+
model: "llama3",
|
|
755
|
+
messages: [{ role: "user", content: "{{message}}" }],
|
|
756
|
+
stream: false,
|
|
757
|
+
}),
|
|
758
|
+
responsePath: "message.content",
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
return {};
|
|
762
|
+
}
|
|
763
|
+
//# sourceMappingURL=discover-local-infra.js.map
|