llm-cli-gateway 1.1.0 → 1.5.4

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 (57) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +226 -9
  3. package/dist/approval-manager.d.ts +1 -1
  4. package/dist/async-job-manager.d.ts +75 -4
  5. package/dist/async-job-manager.js +303 -19
  6. package/dist/auth.d.ts +15 -0
  7. package/dist/auth.js +46 -0
  8. package/dist/cli-updater.d.ts +55 -0
  9. package/dist/cli-updater.js +248 -0
  10. package/dist/codex-json-parser.d.ts +34 -0
  11. package/dist/codex-json-parser.js +105 -0
  12. package/dist/doctor.d.ts +110 -0
  13. package/dist/doctor.js +280 -0
  14. package/dist/endpoint-exposure.d.ts +22 -0
  15. package/dist/endpoint-exposure.js +231 -0
  16. package/dist/executor.d.ts +2 -0
  17. package/dist/executor.js +2 -2
  18. package/dist/flight-recorder.d.ts +3 -1
  19. package/dist/flight-recorder.js +31 -2
  20. package/dist/gateway-server.d.ts +2 -0
  21. package/dist/gateway-server.js +1 -0
  22. package/dist/gemini-json-parser.d.ts +21 -0
  23. package/dist/gemini-json-parser.js +47 -0
  24. package/dist/health.d.ts +7 -0
  25. package/dist/health.js +22 -0
  26. package/dist/http-transport.d.ts +22 -0
  27. package/dist/http-transport.js +164 -0
  28. package/dist/index.d.ts +210 -2
  29. package/dist/index.js +2880 -1037
  30. package/dist/job-store.d.ts +84 -0
  31. package/dist/job-store.js +251 -0
  32. package/dist/logger.d.ts +9 -0
  33. package/dist/logger.js +14 -0
  34. package/dist/model-registry.d.ts +14 -0
  35. package/dist/model-registry.js +478 -134
  36. package/dist/provider-login-guidance.d.ts +21 -0
  37. package/dist/provider-login-guidance.js +98 -0
  38. package/dist/provider-status.d.ts +41 -0
  39. package/dist/provider-status.js +203 -0
  40. package/dist/request-helpers.d.ts +525 -4
  41. package/dist/request-helpers.js +653 -0
  42. package/dist/resources.js +88 -0
  43. package/dist/session-manager-pg.js +2 -0
  44. package/dist/session-manager.d.ts +1 -1
  45. package/dist/session-manager.js +3 -1
  46. package/dist/validation-normalizer.d.ts +23 -0
  47. package/dist/validation-normalizer.js +79 -0
  48. package/dist/validation-orchestrator.d.ts +47 -0
  49. package/dist/validation-orchestrator.js +145 -0
  50. package/dist/validation-prompts.d.ts +15 -0
  51. package/dist/validation-prompts.js +52 -0
  52. package/dist/validation-report.d.ts +57 -0
  53. package/dist/validation-report.js +129 -0
  54. package/dist/validation-tools.d.ts +7 -0
  55. package/dist/validation-tools.js +198 -0
  56. package/package.json +16 -6
  57. package/setup/status.schema.json +271 -0
@@ -0,0 +1,129 @@
1
+ export function buildValidationReport(input) {
2
+ const perModelOutputs = input.results.map(result => ({
3
+ provider: result.provider,
4
+ model: result.model,
5
+ status: result.status,
6
+ verdict: result.verdict,
7
+ rationale: result.rationale,
8
+ risks: result.risks,
9
+ jobId: result.rawJobReference?.jobId ?? null,
10
+ correlationId: result.rawJobReference?.correlationId ?? null,
11
+ warning: result.warning ?? null,
12
+ error: result.error,
13
+ }));
14
+ const jobIds = perModelOutputs.flatMap(output => (output.jobId ? [output.jobId] : []));
15
+ const disagreements = summarizeDisagreement(input.results);
16
+ const limitations = summarizeLimitations(input.results, input.synthesis);
17
+ const confidence = confidenceFor(input.results, disagreements.hasMaterialDisagreement);
18
+ const finalRecommendation = recommendationFor(input.results, disagreements.hasMaterialDisagreement);
19
+ const structuredContent = {
20
+ validationId: input.validationId,
21
+ status: input.status,
22
+ startedAt: input.startedAt,
23
+ intent: input.intent,
24
+ originalRequest: input.originalRequest,
25
+ modelList: input.modelList,
26
+ perModelOutputs,
27
+ disagreements,
28
+ finalRecommendation,
29
+ confidence,
30
+ limitations,
31
+ jobIds,
32
+ synthesis: input.synthesis,
33
+ };
34
+ return {
35
+ schemaVersion: "validation-report.v1",
36
+ humanReadable: renderHumanReport(structuredContent),
37
+ structuredContent,
38
+ };
39
+ }
40
+ function summarizeDisagreement(results) {
41
+ const completed = results.filter(result => result.status === "completed");
42
+ const terminalProblems = results.filter(result => ["failed", "canceled", "orphaned", "skipped"].includes(result.status));
43
+ const pending = results.filter(result => result.status === "running" || result.verdict === "pending");
44
+ const verdicts = new Set(completed
45
+ .map(result => normalizeVerdict(result.verdict))
46
+ .filter((verdict) => Boolean(verdict)));
47
+ const signals = [];
48
+ if (verdicts.size > 1)
49
+ signals.push(`Completed providers returned ${verdicts.size} different verdicts.`);
50
+ for (const result of terminalProblems)
51
+ signals.push(`${result.provider} is ${result.status}.`);
52
+ for (const result of pending)
53
+ signals.push(`${result.provider} is still pending.`);
54
+ const hasMaterialDisagreement = verdicts.size > 1 || terminalProblems.length > 0 || pending.length > 0;
55
+ return {
56
+ hasMaterialDisagreement,
57
+ summary: hasMaterialDisagreement
58
+ ? "Do not treat this validation as consensus; inspect the per-model outputs and unresolved states."
59
+ : completed.length > 0
60
+ ? "Completed providers do not show material verdict disagreement in the normalized report."
61
+ : "No completed provider outputs are available yet.",
62
+ signals,
63
+ };
64
+ }
65
+ function summarizeLimitations(results, synthesis) {
66
+ const limitations = [];
67
+ if (results.some(result => result.status === "running")) {
68
+ limitations.push("Some provider jobs are still running; poll job_status and job_result before treating the report as final.");
69
+ }
70
+ if (results.some(result => result.status !== "completed")) {
71
+ limitations.push("Only completed provider outputs are suitable as judge synthesis evidence.");
72
+ }
73
+ if (synthesis.status === "waiting_for_provider_results") {
74
+ limitations.push("Judge synthesis has not run because provider results still need to be collected.");
75
+ }
76
+ else if (synthesis.status === "skipped") {
77
+ limitations.push(`Judge synthesis skipped: ${synthesis.note}`);
78
+ }
79
+ else if (synthesis.status === "not_requested") {
80
+ limitations.push("No explicit judge synthesis was requested; use per-model outputs for the decision.");
81
+ }
82
+ limitations.push("Large raw outputs are intentionally kept behind job_result references to fit normal MCP client responses.");
83
+ return limitations;
84
+ }
85
+ function confidenceFor(results, hasMaterialDisagreement) {
86
+ const completedCount = results.filter(result => result.status === "completed").length;
87
+ if (completedCount === 0)
88
+ return "none";
89
+ if (hasMaterialDisagreement)
90
+ return "low";
91
+ if (completedCount === 1)
92
+ return "medium";
93
+ return "high";
94
+ }
95
+ function recommendationFor(results, hasMaterialDisagreement) {
96
+ const completedCount = results.filter(result => result.status === "completed").length;
97
+ if (completedCount === 0) {
98
+ return "Wait for at least one provider job to complete, then collect job_result before deciding.";
99
+ }
100
+ if (hasMaterialDisagreement) {
101
+ return "Review the per-model outputs and resolve disagreements manually before acting.";
102
+ }
103
+ return "Completed provider outputs show no normalized verdict disagreement; review rationales and risks before acting.";
104
+ }
105
+ function renderHumanReport(content) {
106
+ const lines = [
107
+ `Validation report ${content.validationId}`,
108
+ `Status: ${content.status}`,
109
+ `Models: ${content.modelList.join(", ") || "none"}`,
110
+ "",
111
+ "Per-model outputs:",
112
+ ...content.perModelOutputs.map(output => {
113
+ const job = output.jobId ? ` job=${output.jobId}` : "";
114
+ const verdict = output.verdict ? ` verdict=${output.verdict}` : "";
115
+ return `- ${output.provider}: ${output.status}${verdict}${job}`;
116
+ }),
117
+ "",
118
+ `Disagreement: ${content.disagreements.summary}`,
119
+ `Recommendation: ${content.finalRecommendation}`,
120
+ `Confidence: ${content.confidence}`,
121
+ "",
122
+ "Limitations:",
123
+ ...content.limitations.map(limitation => `- ${limitation}`),
124
+ ];
125
+ return lines.join("\n");
126
+ }
127
+ function normalizeVerdict(verdict) {
128
+ return verdict?.trim().toLowerCase() || null;
129
+ }
@@ -0,0 +1,7 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { AsyncJobManager } from "./async-job-manager.js";
3
+ import { type ValidationOrchestratorDeps } from "./validation-orchestrator.js";
4
+ export interface ValidationToolDeps extends ValidationOrchestratorDeps {
5
+ asyncJobManager: AsyncJobManager;
6
+ }
7
+ export declare function registerValidationTools(server: McpServer, deps: ValidationToolDeps): void;
@@ -0,0 +1,198 @@
1
+ import { z } from "zod";
2
+ import { getCliInfo } from "./model-registry.js";
3
+ import { collectValidationJobResult, startJudgeSynthesis, startValidationRun, } from "./validation-orchestrator.js";
4
+ const providerSchema = z.enum(["claude", "codex", "gemini", "grok", "mistral"]);
5
+ const providerListSchema = z.array(providerSchema).min(1).default(["claude", "codex"]);
6
+ const normalizedProviderResultSchema = z.object({
7
+ provider: providerSchema,
8
+ model: z.string().nullable(),
9
+ status: z.enum(["running", "completed", "failed", "canceled", "orphaned", "skipped"]),
10
+ verdict: z.string().nullable(),
11
+ rationale: z.string().nullable(),
12
+ risks: z.array(z.string()).default([]),
13
+ rawJobReference: z
14
+ .object({
15
+ jobId: z.string(),
16
+ correlationId: z.string(),
17
+ statusTool: z.literal("job_status"),
18
+ resultTool: z.literal("job_result"),
19
+ })
20
+ .nullable(),
21
+ error: z.string().nullable(),
22
+ warning: z.string().optional(),
23
+ });
24
+ function textResponse(body) {
25
+ const text = responseText(body);
26
+ return {
27
+ content: [{ type: "text", text }],
28
+ structuredContent: body,
29
+ };
30
+ }
31
+ function responseText(body) {
32
+ const report = findHumanReadableReport(body);
33
+ if (report)
34
+ return report;
35
+ return JSON.stringify(body, null, 2);
36
+ }
37
+ function findHumanReadableReport(value) {
38
+ if (typeof value !== "object" || value === null)
39
+ return null;
40
+ if ("humanReadable" in value &&
41
+ typeof value.humanReadable === "string") {
42
+ return value.humanReadable;
43
+ }
44
+ if ("report" in value) {
45
+ return findHumanReadableReport(value.report);
46
+ }
47
+ return null;
48
+ }
49
+ export function registerValidationTools(server, deps) {
50
+ server.tool("validate_with_models", {
51
+ question: z.string().min(1).describe("Question or content to validate."),
52
+ models: providerListSchema.describe("Providers to ask. Defaults to Claude and Codex."),
53
+ focus: z
54
+ .string()
55
+ .default("correctness, missing assumptions, and practical next steps")
56
+ .describe("What reviewers should pay attention to."),
57
+ judgeModel: providerSchema
58
+ .optional()
59
+ .describe("Optional provider to run an explicit judge synthesis job."),
60
+ }, async ({ question, models, focus, judgeModel }) => textResponse({
61
+ success: true,
62
+ tool: "validate_with_models",
63
+ readMostly: true,
64
+ report: startValidationRun(deps, {
65
+ intent: "validate",
66
+ question,
67
+ providers: models,
68
+ focus,
69
+ judgeProvider: judgeModel,
70
+ }),
71
+ }));
72
+ server.tool("second_opinion", {
73
+ answer: z.string().min(1).describe("Answer to review."),
74
+ question: z.string().optional().describe("Original question, if available."),
75
+ model: providerSchema.default("codex").describe("Provider to ask for the second opinion."),
76
+ }, async ({ answer, question, model }) => textResponse({
77
+ success: true,
78
+ tool: "second_opinion",
79
+ readMostly: true,
80
+ report: startValidationRun(deps, {
81
+ intent: "second_opinion",
82
+ question,
83
+ content: answer,
84
+ providers: [model],
85
+ }),
86
+ }));
87
+ server.tool("compare_answers", {
88
+ question: z.string().min(1).describe("Question the answers respond to."),
89
+ answers: z.array(z.string().min(1)).min(2).describe("Two or more answers to compare."),
90
+ }, async ({ question, answers }) => textResponse({
91
+ success: true,
92
+ tool: "compare_answers",
93
+ readMostly: true,
94
+ comparison: {
95
+ question,
96
+ answerCount: answers.length,
97
+ checks: ["agreement", "contradictions", "missing evidence", "actionable recommendation"],
98
+ status: "local_summary_only",
99
+ note: "Use validate_with_models when independent provider review is needed.",
100
+ },
101
+ }));
102
+ server.tool("red_team_review", {
103
+ content: z.string().min(1).describe("Plan, answer, or document to challenge."),
104
+ riskLevel: z
105
+ .enum(["normal", "high"])
106
+ .default("normal")
107
+ .describe("How aggressively to review."),
108
+ models: providerListSchema.describe("Providers to ask for adversarial review."),
109
+ }, async ({ content, riskLevel, models }) => textResponse({
110
+ success: true,
111
+ tool: "red_team_review",
112
+ readMostly: true,
113
+ report: startValidationRun(deps, {
114
+ intent: "red_team",
115
+ content,
116
+ providers: models,
117
+ riskLevel,
118
+ }),
119
+ }));
120
+ server.tool("consensus_check", {
121
+ claim: z.string().min(1).describe("Claim to check across providers."),
122
+ models: providerListSchema.describe("Providers to ask for agreement or disagreement."),
123
+ }, async ({ claim, models }) => textResponse({
124
+ success: true,
125
+ tool: "consensus_check",
126
+ readMostly: true,
127
+ report: startValidationRun(deps, {
128
+ intent: "consensus",
129
+ content: claim,
130
+ providers: models,
131
+ }),
132
+ }));
133
+ server.tool("ask_model", {
134
+ question: z.string().min(1).describe("Question for one provider."),
135
+ model: providerSchema.default("claude").describe("Provider to ask."),
136
+ }, async ({ question, model }) => textResponse({
137
+ success: true,
138
+ tool: "ask_model",
139
+ readMostly: true,
140
+ report: startValidationRun(deps, {
141
+ intent: "ask_model",
142
+ question,
143
+ providers: [model],
144
+ }),
145
+ }));
146
+ server.tool("synthesize_validation", {
147
+ question: z.string().min(1).describe("Original request that was validated."),
148
+ providerResults: z
149
+ .array(normalizedProviderResultSchema)
150
+ .min(1)
151
+ .describe("Terminal normalized provider results from job_result."),
152
+ judgeModel: providerSchema.default("codex").describe("Provider to run the judge synthesis."),
153
+ }, async ({ question, providerResults, judgeModel }) => textResponse({
154
+ success: true,
155
+ tool: "synthesize_validation",
156
+ readMostly: true,
157
+ synthesis: startJudgeSynthesis(deps, {
158
+ question,
159
+ providerResults,
160
+ judgeProvider: judgeModel,
161
+ }),
162
+ }));
163
+ server.tool("list_available_models", {}, async () => textResponse({ success: true, models: getCliInfo() }));
164
+ server.tool("job_status", {
165
+ jobId: z.string().min(1).describe("Validation job ID."),
166
+ }, async ({ jobId }) => {
167
+ const job = deps.asyncJobManager.getJobSnapshot(jobId);
168
+ if (!job) {
169
+ return textResponse({ success: false, error: "Job not found", jobId });
170
+ }
171
+ return textResponse({ success: true, job });
172
+ });
173
+ server.tool("job_result", {
174
+ jobId: z.string().min(1).describe("Validation job ID."),
175
+ provider: providerSchema
176
+ .optional()
177
+ .describe("Provider that produced the job, used for normalized validation output."),
178
+ maxChars: z
179
+ .number()
180
+ .int()
181
+ .min(1000)
182
+ .max(2000000)
183
+ .default(200000)
184
+ .describe("Maximum result size."),
185
+ }, async ({ jobId, provider, maxChars }) => {
186
+ const result = deps.asyncJobManager.getJobResult(jobId, maxChars);
187
+ if (!result) {
188
+ return textResponse({ success: false, error: "Job not found", jobId });
189
+ }
190
+ return textResponse({
191
+ success: true,
192
+ result,
193
+ normalized: provider !== undefined
194
+ ? collectValidationJobResult(deps, provider, jobId, null, maxChars)
195
+ : null,
196
+ });
197
+ });
198
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "llm-cli-gateway",
3
- "version": "1.1.0",
3
+ "version": "1.5.4",
4
4
  "mcpName": "io.github.verivus-oss/llm-cli-gateway",
5
- "description": "MCP server providing unified access to Claude Code, Codex, and Gemini CLIs with session management, retry logic, and async job orchestration.",
5
+ "description": "MCP server providing unified access to Claude Code, Codex, Gemini, Grok, and Mistral Vibe CLIs with session management, retry logic, async job orchestration, durable job results, and cross-LLM validation.",
6
6
  "license": "MIT",
7
7
  "author": {
8
8
  "name": "VerivusAI Labs",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "https://github.com/verivus-oss/llm-cli-gateway.git"
13
+ "url": "git+https://github.com/verivus-oss/llm-cli-gateway.git"
14
14
  },
15
15
  "homepage": "https://github.com/verivus-oss/llm-cli-gateway#readme",
16
16
  "bugs": {
@@ -33,10 +33,14 @@
33
33
  ".": {
34
34
  "types": "./dist/index.d.ts",
35
35
  "default": "./dist/index.js"
36
+ },
37
+ "./gateway-server": {
38
+ "types": "./dist/gateway-server.d.ts",
39
+ "default": "./dist/gateway-server.js"
36
40
  }
37
41
  },
38
42
  "bin": {
39
- "llm-cli-gateway": "./dist/index.js"
43
+ "llm-cli-gateway": "dist/index.js"
40
44
  },
41
45
  "engines": {
42
46
  "node": ">=20.0.0"
@@ -45,6 +49,7 @@
45
49
  "dist/**/*.js",
46
50
  "dist/**/*.d.ts",
47
51
  "!dist/__tests__/**",
52
+ "setup/status.schema.json",
48
53
  "README.md",
49
54
  "CHANGELOG.md",
50
55
  "LICENSE"
@@ -54,6 +59,8 @@
54
59
  "build:all": "tsc",
55
60
  "prepublishOnly": "npm run build && npm test",
56
61
  "start": "node dist/index.js",
62
+ "start:http": "node dist/index.js --transport=http",
63
+ "doctor": "node dist/index.js doctor --json",
57
64
  "migrate": "node dist/migrate.js",
58
65
  "test": "vitest run",
59
66
  "test:coverage": "vitest run --coverage",
@@ -68,11 +75,14 @@
68
75
  "lint:fix": "eslint src/**/*.ts --fix",
69
76
  "format": "prettier --write 'src/**/*.ts'",
70
77
  "format:check": "prettier --check 'src/**/*.ts'",
71
- "check": "npm run build && npm run lint && npm test"
78
+ "check": "npm run build && npm run lint && npm test",
79
+ "release:build": "bash installer/build-release.sh",
80
+ "release:checksums": "cd installer/dist && sha256sum --check SHA256SUMS",
81
+ "release:docker": "docker compose -f docker-compose.personal.yml build"
72
82
  },
73
83
  "dependencies": {
74
84
  "@modelcontextprotocol/sdk": "^1.0.0",
75
- "better-sqlite3": "^11.0.0",
85
+ "better-sqlite3": "^12.9.0",
76
86
  "ioredis": "^5.4.1",
77
87
  "pg": "^8.12.0",
78
88
  "toml": "^3.0.0",
@@ -0,0 +1,271 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://verivus.ai/schemas/llm-cli-gateway/status.schema.json",
4
+ "title": "llm-cli-gateway doctor status",
5
+ "type": "object",
6
+ "required": [
7
+ "schema_version",
8
+ "ok",
9
+ "generated_at",
10
+ "system",
11
+ "gateway",
12
+ "transport",
13
+ "auth",
14
+ "providers",
15
+ "endpoint_exposure",
16
+ "client_config",
17
+ "next_actions"
18
+ ],
19
+ "properties": {
20
+ "schema_version": { "const": "1.0" },
21
+ "ok": { "type": "boolean" },
22
+ "generated_at": { "type": "string", "format": "date-time" },
23
+ "system": {
24
+ "type": "object",
25
+ "required": ["os", "arch", "release", "node_version"],
26
+ "properties": {
27
+ "os": { "type": "string" },
28
+ "arch": { "type": "string" },
29
+ "release": { "type": "string" },
30
+ "node_version": { "type": "string" }
31
+ },
32
+ "additionalProperties": false
33
+ },
34
+ "gateway": {
35
+ "type": "object",
36
+ "required": ["name", "version"],
37
+ "properties": {
38
+ "name": { "const": "llm-cli-gateway" },
39
+ "version": { "type": "string" }
40
+ },
41
+ "additionalProperties": false
42
+ },
43
+ "transport": {
44
+ "type": "object",
45
+ "required": ["default", "http"],
46
+ "properties": {
47
+ "default": { "enum": ["stdio", "http"] },
48
+ "http": {
49
+ "type": "object",
50
+ "required": ["enabled", "host", "port", "path", "public_url_configured", "public_url"],
51
+ "properties": {
52
+ "enabled": { "type": "boolean" },
53
+ "host": { "type": "string" },
54
+ "port": { "type": "integer", "minimum": 1, "maximum": 65535 },
55
+ "path": { "type": "string" },
56
+ "public_url_configured": { "type": "boolean" },
57
+ "public_url": { "type": ["string", "null"] }
58
+ },
59
+ "additionalProperties": false
60
+ }
61
+ },
62
+ "additionalProperties": false
63
+ },
64
+ "auth": {
65
+ "type": "object",
66
+ "required": ["required", "token_configured", "source"],
67
+ "properties": {
68
+ "required": { "type": "boolean" },
69
+ "token_configured": { "type": "boolean" },
70
+ "source": { "enum": ["env", "disabled"] }
71
+ },
72
+ "additionalProperties": false
73
+ },
74
+ "providers": {
75
+ "type": "object",
76
+ "required": ["claude", "codex", "gemini", "grok", "mistral"],
77
+ "additionalProperties": {
78
+ "type": "object",
79
+ "required": [
80
+ "cli_available",
81
+ "version",
82
+ "login_status",
83
+ "version_command",
84
+ "login_check",
85
+ "install_guidance",
86
+ "login_guidance"
87
+ ],
88
+ "properties": {
89
+ "cli_available": { "type": "boolean" },
90
+ "version": { "type": ["string", "null"] },
91
+ "login_status": {
92
+ "enum": ["authenticated", "not_authenticated", "unknown", "not_checked"]
93
+ },
94
+ "version_command": {
95
+ "type": "array",
96
+ "items": { "type": "string" }
97
+ },
98
+ "login_check": {
99
+ "type": "object",
100
+ "required": ["method", "command", "credential_store", "detail"],
101
+ "properties": {
102
+ "method": { "enum": ["cli", "credential_store", "not_checked"] },
103
+ "command": {
104
+ "type": ["array", "null"],
105
+ "items": { "type": "string" }
106
+ },
107
+ "credential_store": { "enum": ["present", "not_found", "not_checked"] },
108
+ "detail": { "type": "string" }
109
+ },
110
+ "additionalProperties": false
111
+ },
112
+ "install_guidance": {
113
+ "type": "object",
114
+ "required": ["summary", "commands"],
115
+ "properties": {
116
+ "summary": { "type": "string" },
117
+ "commands": {
118
+ "type": "array",
119
+ "items": { "type": "string" }
120
+ },
121
+ "documentation_url": { "type": "string" }
122
+ },
123
+ "additionalProperties": false
124
+ },
125
+ "login_guidance": {
126
+ "type": "object",
127
+ "required": ["summary", "commands", "credential_handling"],
128
+ "properties": {
129
+ "summary": { "type": "string" },
130
+ "commands": {
131
+ "type": "array",
132
+ "items": { "type": "string" }
133
+ },
134
+ "credential_handling": { "type": "string" }
135
+ },
136
+ "additionalProperties": false
137
+ }
138
+ },
139
+ "additionalProperties": false
140
+ }
141
+ },
142
+ "endpoint_exposure": {
143
+ "type": "object",
144
+ "required": [
145
+ "mode",
146
+ "local_url",
147
+ "public_url_configured",
148
+ "public_url",
149
+ "https_required_for_web",
150
+ "https_configured",
151
+ "web_clients_supported",
152
+ "tunnel_provider",
153
+ "reachable_from_web",
154
+ "verification",
155
+ "next_actions"
156
+ ],
157
+ "properties": {
158
+ "mode": {
159
+ "enum": ["local_only", "lan", "tunnel", "byo_reverse_proxy", "misconfigured"]
160
+ },
161
+ "local_url": { "type": "string" },
162
+ "public_url_configured": { "type": "boolean" },
163
+ "public_url": { "type": ["string", "null"] },
164
+ "https_required_for_web": { "type": "boolean" },
165
+ "https_configured": { "type": "boolean" },
166
+ "web_clients_supported": { "type": "boolean" },
167
+ "tunnel_provider": { "type": ["string", "null"] },
168
+ "reachable_from_web": { "enum": ["not_checked", "reachable", "unreachable"] },
169
+ "verification": {
170
+ "type": "object",
171
+ "required": ["method", "checked_url", "status_code", "error"],
172
+ "properties": {
173
+ "method": { "enum": ["not_checked", "http_head"] },
174
+ "checked_url": { "type": ["string", "null"] },
175
+ "status_code": { "type": ["integer", "null"] },
176
+ "error": { "type": ["string", "null"] }
177
+ },
178
+ "additionalProperties": false
179
+ },
180
+ "next_actions": {
181
+ "type": "array",
182
+ "items": { "type": "string" }
183
+ }
184
+ },
185
+ "additionalProperties": false
186
+ },
187
+ "client_config": {
188
+ "type": "object",
189
+ "required": [
190
+ "claude_desktop_config_present",
191
+ "codex_config_present",
192
+ "gemini_settings_present",
193
+ "gemini_config",
194
+ "vibe_session_logging"
195
+ ],
196
+ "properties": {
197
+ "claude_desktop_config_present": { "type": "boolean" },
198
+ "codex_config_present": { "type": "boolean" },
199
+ "gemini_settings_present": { "type": "boolean" },
200
+ "gemini_config": {
201
+ "type": "object",
202
+ "required": [
203
+ "project_gemini_md_present",
204
+ "project_gemini_md_path",
205
+ "user_gemini_md_present",
206
+ "user_gemini_md_path",
207
+ "settings_json_present",
208
+ "settings_json_path",
209
+ "mcp_servers_registered",
210
+ "mcp_reconciliation",
211
+ "next_actions"
212
+ ],
213
+ "properties": {
214
+ "project_gemini_md_present": { "type": "boolean" },
215
+ "project_gemini_md_path": { "type": "string" },
216
+ "user_gemini_md_present": { "type": "boolean" },
217
+ "user_gemini_md_path": { "type": "string" },
218
+ "settings_json_present": { "type": "boolean" },
219
+ "settings_json_path": { "type": "string" },
220
+ "mcp_servers_registered": {
221
+ "type": "array",
222
+ "items": { "type": "string" }
223
+ },
224
+ "mcp_reconciliation": {
225
+ "type": "object",
226
+ "required": ["whitelisted", "missing_from_settings"],
227
+ "properties": {
228
+ "whitelisted": {
229
+ "type": "array",
230
+ "items": { "type": "string" }
231
+ },
232
+ "missing_from_settings": {
233
+ "type": "array",
234
+ "items": { "type": "string" }
235
+ }
236
+ },
237
+ "additionalProperties": false
238
+ },
239
+ "next_actions": {
240
+ "type": "array",
241
+ "items": { "type": "string" }
242
+ }
243
+ },
244
+ "additionalProperties": false
245
+ },
246
+ "vibe_session_logging": {
247
+ "type": "object",
248
+ "required": [
249
+ "config_path",
250
+ "config_present",
251
+ "session_logging_enabled",
252
+ "note"
253
+ ],
254
+ "properties": {
255
+ "config_path": { "type": "string" },
256
+ "config_present": { "type": "boolean" },
257
+ "session_logging_enabled": { "type": "boolean" },
258
+ "note": { "type": "string" }
259
+ },
260
+ "additionalProperties": false
261
+ }
262
+ },
263
+ "additionalProperties": false
264
+ },
265
+ "next_actions": {
266
+ "type": "array",
267
+ "items": { "type": "string" }
268
+ }
269
+ },
270
+ "additionalProperties": false
271
+ }