@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.
Files changed (253) hide show
  1. package/dist/api-client.d.ts +42 -0
  2. package/dist/api-client.js +145 -0
  3. package/dist/api-client.js.map +1 -0
  4. package/dist/auth/index.d.ts +29 -0
  5. package/dist/auth/index.js +73 -0
  6. package/dist/auth/index.js.map +1 -0
  7. package/dist/auth/jwt.d.ts +24 -0
  8. package/dist/auth/jwt.js +25 -0
  9. package/dist/auth/jwt.js.map +1 -0
  10. package/dist/auth/keychain.d.ts +34 -0
  11. package/dist/auth/keychain.js +272 -0
  12. package/dist/auth/keychain.js.map +1 -0
  13. package/dist/auth/oauth.d.ts +38 -0
  14. package/dist/auth/oauth.js +241 -0
  15. package/dist/auth/oauth.js.map +1 -0
  16. package/dist/auth/sessions.d.ts +28 -0
  17. package/dist/auth/sessions.js +69 -0
  18. package/dist/auth/sessions.js.map +1 -0
  19. package/dist/auth/token-refresh.d.ts +24 -0
  20. package/dist/auth/token-refresh.js +91 -0
  21. package/dist/auth/token-refresh.js.map +1 -0
  22. package/dist/cli.d.ts +23 -0
  23. package/dist/cli.js +962 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/index.d.ts +8 -0
  26. package/dist/index.js +15 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/project-binding.d.ts +25 -0
  29. package/dist/project-binding.js +67 -0
  30. package/dist/project-binding.js.map +1 -0
  31. package/dist/resolve-application-id.d.ts +12 -0
  32. package/dist/resolve-application-id.js +21 -0
  33. package/dist/resolve-application-id.js.map +1 -0
  34. package/dist/sanitize.d.ts +23 -0
  35. package/dist/sanitize.js +125 -0
  36. package/dist/sanitize.js.map +1 -0
  37. package/dist/server.d.ts +14 -0
  38. package/dist/server.js +447 -0
  39. package/dist/server.js.map +1 -0
  40. package/dist/tools/check-project.d.ts +32 -0
  41. package/dist/tools/check-project.js +180 -0
  42. package/dist/tools/check-project.js.map +1 -0
  43. package/dist/tools/check-repo-trust.d.ts +33 -0
  44. package/dist/tools/check-repo-trust.js +212 -0
  45. package/dist/tools/check-repo-trust.js.map +1 -0
  46. package/dist/tools/create-diagram.d.ts +114 -0
  47. package/dist/tools/create-diagram.js +176 -0
  48. package/dist/tools/create-diagram.js.map +1 -0
  49. package/dist/tools/create-document.d.ts +28 -0
  50. package/dist/tools/create-document.js +53 -0
  51. package/dist/tools/create-document.js.map +1 -0
  52. package/dist/tools/discover-local-infra.d.ts +24 -0
  53. package/dist/tools/discover-local-infra.js +763 -0
  54. package/dist/tools/discover-local-infra.js.map +1 -0
  55. package/dist/tools/enterprise/capture-evidence.d.ts +54 -0
  56. package/dist/tools/enterprise/capture-evidence.js +72 -0
  57. package/dist/tools/enterprise/capture-evidence.js.map +1 -0
  58. package/dist/tools/enterprise/create-report-ticket.d.ts +34 -0
  59. package/dist/tools/enterprise/create-report-ticket.js +44 -0
  60. package/dist/tools/enterprise/create-report-ticket.js.map +1 -0
  61. package/dist/tools/enterprise/end-pentest-session.d.ts +33 -0
  62. package/dist/tools/enterprise/end-pentest-session.js +42 -0
  63. package/dist/tools/enterprise/end-pentest-session.js.map +1 -0
  64. package/dist/tools/enterprise/generate-report.d.ts +55 -0
  65. package/dist/tools/enterprise/generate-report.js +111 -0
  66. package/dist/tools/enterprise/generate-report.js.map +1 -0
  67. package/dist/tools/enterprise/get-org-zat.d.ts +27 -0
  68. package/dist/tools/enterprise/get-org-zat.js +62 -0
  69. package/dist/tools/enterprise/get-org-zat.js.map +1 -0
  70. package/dist/tools/enterprise/get-report-status.d.ts +27 -0
  71. package/dist/tools/enterprise/get-report-status.js +38 -0
  72. package/dist/tools/enterprise/get-report-status.js.map +1 -0
  73. package/dist/tools/enterprise/get-threat-model.d.ts +29 -0
  74. package/dist/tools/enterprise/get-threat-model.js +83 -0
  75. package/dist/tools/enterprise/get-threat-model.js.map +1 -0
  76. package/dist/tools/enterprise/list-agents.d.ts +42 -0
  77. package/dist/tools/enterprise/list-agents.js +95 -0
  78. package/dist/tools/enterprise/list-agents.js.map +1 -0
  79. package/dist/tools/enterprise/list-report-issues.d.ts +36 -0
  80. package/dist/tools/enterprise/list-report-issues.js +50 -0
  81. package/dist/tools/enterprise/list-report-issues.js.map +1 -0
  82. package/dist/tools/enterprise/list-threat-models.d.ts +36 -0
  83. package/dist/tools/enterprise/list-threat-models.js +82 -0
  84. package/dist/tools/enterprise/list-threat-models.js.map +1 -0
  85. package/dist/tools/enterprise/manage-actor.d.ts +58 -0
  86. package/dist/tools/enterprise/manage-actor.js +99 -0
  87. package/dist/tools/enterprise/manage-actor.js.map +1 -0
  88. package/dist/tools/enterprise/manage-attack-scenario.d.ts +70 -0
  89. package/dist/tools/enterprise/manage-attack-scenario.js +89 -0
  90. package/dist/tools/enterprise/manage-attack-scenario.js.map +1 -0
  91. package/dist/tools/enterprise/manage-component.d.ts +58 -0
  92. package/dist/tools/enterprise/manage-component.js +90 -0
  93. package/dist/tools/enterprise/manage-component.js.map +1 -0
  94. package/dist/tools/enterprise/manage-data-asset.d.ts +53 -0
  95. package/dist/tools/enterprise/manage-data-asset.js +90 -0
  96. package/dist/tools/enterprise/manage-data-asset.js.map +1 -0
  97. package/dist/tools/enterprise/manage-data-flow.d.ts +57 -0
  98. package/dist/tools/enterprise/manage-data-flow.js +98 -0
  99. package/dist/tools/enterprise/manage-data-flow.js.map +1 -0
  100. package/dist/tools/enterprise/manage-finding.d.ts +69 -0
  101. package/dist/tools/enterprise/manage-finding.js +106 -0
  102. package/dist/tools/enterprise/manage-finding.js.map +1 -0
  103. package/dist/tools/enterprise/manage-security-control.d.ts +59 -0
  104. package/dist/tools/enterprise/manage-security-control.js +96 -0
  105. package/dist/tools/enterprise/manage-security-control.js.map +1 -0
  106. package/dist/tools/enterprise/manage-test-case.d.ts +88 -0
  107. package/dist/tools/enterprise/manage-test-case.js +119 -0
  108. package/dist/tools/enterprise/manage-test-case.js.map +1 -0
  109. package/dist/tools/enterprise/promote-finding-to-issue.d.ts +36 -0
  110. package/dist/tools/enterprise/promote-finding-to-issue.js +65 -0
  111. package/dist/tools/enterprise/promote-finding-to-issue.js.map +1 -0
  112. package/dist/tools/enterprise/publish-report-version.d.ts +34 -0
  113. package/dist/tools/enterprise/publish-report-version.js +75 -0
  114. package/dist/tools/enterprise/publish-report-version.js.map +1 -0
  115. package/dist/tools/enterprise/publish-test-to-library.d.ts +31 -0
  116. package/dist/tools/enterprise/publish-test-to-library.js +40 -0
  117. package/dist/tools/enterprise/publish-test-to-library.js.map +1 -0
  118. package/dist/tools/enterprise/record-test-result.d.ts +67 -0
  119. package/dist/tools/enterprise/record-test-result.js +75 -0
  120. package/dist/tools/enterprise/record-test-result.js.map +1 -0
  121. package/dist/tools/enterprise/start-pentest-session.d.ts +35 -0
  122. package/dist/tools/enterprise/start-pentest-session.js +50 -0
  123. package/dist/tools/enterprise/start-pentest-session.js.map +1 -0
  124. package/dist/tools/enterprise/sync-report-section.d.ts +41 -0
  125. package/dist/tools/enterprise/sync-report-section.js +79 -0
  126. package/dist/tools/enterprise/sync-report-section.js.map +1 -0
  127. package/dist/tools/enterprise/update-report-issue.d.ts +55 -0
  128. package/dist/tools/enterprise/update-report-issue.js +69 -0
  129. package/dist/tools/enterprise/update-report-issue.js.map +1 -0
  130. package/dist/tools/generate-diagram.d.ts +36 -0
  131. package/dist/tools/generate-diagram.js +153 -0
  132. package/dist/tools/generate-diagram.js.map +1 -0
  133. package/dist/tools/get-agent-test-results.d.ts +41 -0
  134. package/dist/tools/get-agent-test-results.js +86 -0
  135. package/dist/tools/get-agent-test-results.js.map +1 -0
  136. package/dist/tools/get-application-overview.d.ts +27 -0
  137. package/dist/tools/get-application-overview.js +143 -0
  138. package/dist/tools/get-application-overview.js.map +1 -0
  139. package/dist/tools/get-application.d.ts +27 -0
  140. package/dist/tools/get-application.js +83 -0
  141. package/dist/tools/get-application.js.map +1 -0
  142. package/dist/tools/get-artifact-content.d.ts +29 -0
  143. package/dist/tools/get-artifact-content.js +86 -0
  144. package/dist/tools/get-artifact-content.js.map +1 -0
  145. package/dist/tools/get-diagram.d.ts +27 -0
  146. package/dist/tools/get-diagram.js +109 -0
  147. package/dist/tools/get-diagram.js.map +1 -0
  148. package/dist/tools/get-document.d.ts +22 -0
  149. package/dist/tools/get-document.js +43 -0
  150. package/dist/tools/get-document.js.map +1 -0
  151. package/dist/tools/get-finding-details.d.ts +28 -0
  152. package/dist/tools/get-finding-details.js +43 -0
  153. package/dist/tools/get-finding-details.js.map +1 -0
  154. package/dist/tools/get-findings.d.ts +57 -0
  155. package/dist/tools/get-findings.js +103 -0
  156. package/dist/tools/get-findings.js.map +1 -0
  157. package/dist/tools/get-oss-zat.d.ts +27 -0
  158. package/dist/tools/get-oss-zat.js +105 -0
  159. package/dist/tools/get-oss-zat.js.map +1 -0
  160. package/dist/tools/get-recon-scan.d.ts +27 -0
  161. package/dist/tools/get-recon-scan.js +46 -0
  162. package/dist/tools/get-recon-scan.js.map +1 -0
  163. package/dist/tools/get-scan-output.d.ts +27 -0
  164. package/dist/tools/get-scan-output.js +64 -0
  165. package/dist/tools/get-scan-output.js.map +1 -0
  166. package/dist/tools/get-scenario-details.d.ts +27 -0
  167. package/dist/tools/get-scenario-details.js +46 -0
  168. package/dist/tools/get-scenario-details.js.map +1 -0
  169. package/dist/tools/get-test-case-results.d.ts +37 -0
  170. package/dist/tools/get-test-case-results.js +66 -0
  171. package/dist/tools/get-test-case-results.js.map +1 -0
  172. package/dist/tools/get-test-details.d.ts +29 -0
  173. package/dist/tools/get-test-details.js +49 -0
  174. package/dist/tools/get-test-details.js.map +1 -0
  175. package/dist/tools/get-trust-keys.d.ts +22 -0
  176. package/dist/tools/get-trust-keys.js +67 -0
  177. package/dist/tools/get-trust-keys.js.map +1 -0
  178. package/dist/tools/import-openapi-endpoints.d.ts +33 -0
  179. package/dist/tools/import-openapi-endpoints.js +93 -0
  180. package/dist/tools/import-openapi-endpoints.js.map +1 -0
  181. package/dist/tools/inspect-zat.d.ts +26 -0
  182. package/dist/tools/inspect-zat.js +247 -0
  183. package/dist/tools/inspect-zat.js.map +1 -0
  184. package/dist/tools/list-agent-test-cases.d.ts +41 -0
  185. package/dist/tools/list-agent-test-cases.js +73 -0
  186. package/dist/tools/list-agent-test-cases.js.map +1 -0
  187. package/dist/tools/list-applications.d.ts +34 -0
  188. package/dist/tools/list-applications.js +74 -0
  189. package/dist/tools/list-applications.js.map +1 -0
  190. package/dist/tools/list-diagrams.d.ts +42 -0
  191. package/dist/tools/list-diagrams.js +81 -0
  192. package/dist/tools/list-diagrams.js.map +1 -0
  193. package/dist/tools/list-documents.d.ts +24 -0
  194. package/dist/tools/list-documents.js +50 -0
  195. package/dist/tools/list-documents.js.map +1 -0
  196. package/dist/tools/list-endpoints.d.ts +47 -0
  197. package/dist/tools/list-endpoints.js +98 -0
  198. package/dist/tools/list-endpoints.js.map +1 -0
  199. package/dist/tools/list-recon-scans.d.ts +36 -0
  200. package/dist/tools/list-recon-scans.js +74 -0
  201. package/dist/tools/list-recon-scans.js.map +1 -0
  202. package/dist/tools/list-scan-artifacts.d.ts +41 -0
  203. package/dist/tools/list-scan-artifacts.js +63 -0
  204. package/dist/tools/list-scan-artifacts.js.map +1 -0
  205. package/dist/tools/list-scans.d.ts +44 -0
  206. package/dist/tools/list-scans.js +91 -0
  207. package/dist/tools/list-scans.js.map +1 -0
  208. package/dist/tools/list-test-library.d.ts +44 -0
  209. package/dist/tools/list-test-library.js +83 -0
  210. package/dist/tools/list-test-library.js.map +1 -0
  211. package/dist/tools/list-test-scenarios.d.ts +31 -0
  212. package/dist/tools/list-test-scenarios.js +66 -0
  213. package/dist/tools/list-test-scenarios.js.map +1 -0
  214. package/dist/tools/local-scan.d.ts +40 -0
  215. package/dist/tools/local-scan.js +261 -0
  216. package/dist/tools/local-scan.js.map +1 -0
  217. package/dist/tools/manage-diagram.d.ts +109 -0
  218. package/dist/tools/manage-diagram.js +316 -0
  219. package/dist/tools/manage-diagram.js.map +1 -0
  220. package/dist/tools/open-in-ide.d.ts +39 -0
  221. package/dist/tools/open-in-ide.js +61 -0
  222. package/dist/tools/open-in-ide.js.map +1 -0
  223. package/dist/tools/run-test-scenario.d.ts +37 -0
  224. package/dist/tools/run-test-scenario.js +70 -0
  225. package/dist/tools/run-test-scenario.js.map +1 -0
  226. package/dist/tools/scan-pr.d.ts +27 -0
  227. package/dist/tools/scan-pr.js +51 -0
  228. package/dist/tools/scan-pr.js.map +1 -0
  229. package/dist/tools/security-review.d.ts +65 -0
  230. package/dist/tools/security-review.js +378 -0
  231. package/dist/tools/security-review.js.map +1 -0
  232. package/dist/tools/setup-red-team-target.d.ts +79 -0
  233. package/dist/tools/setup-red-team-target.js +430 -0
  234. package/dist/tools/setup-red-team-target.js.map +1 -0
  235. package/dist/tools/triage-finding.d.ts +47 -0
  236. package/dist/tools/triage-finding.js +116 -0
  237. package/dist/tools/triage-finding.js.map +1 -0
  238. package/dist/tools/update-document.d.ts +28 -0
  239. package/dist/tools/update-document.js +63 -0
  240. package/dist/tools/update-document.js.map +1 -0
  241. package/dist/tools/update-endpoint.d.ts +65 -0
  242. package/dist/tools/update-endpoint.js +145 -0
  243. package/dist/tools/update-endpoint.js.map +1 -0
  244. package/dist/tools/update-mermaid-source.d.ts +34 -0
  245. package/dist/tools/update-mermaid-source.js +76 -0
  246. package/dist/tools/update-mermaid-source.js.map +1 -0
  247. package/dist/tools/verify-trust-mark.d.ts +29 -0
  248. package/dist/tools/verify-trust-mark.js +108 -0
  249. package/dist/tools/verify-trust-mark.js.map +1 -0
  250. package/dist/types.d.ts +220 -0
  251. package/dist/types.js +33 -0
  252. package/dist/types.js.map +1 -0
  253. 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