vgxness 0.1.0

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 (121) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +110 -0
  3. package/dist/agents/agent-activation-service.js +144 -0
  4. package/dist/agents/agent-registry-service.js +46 -0
  5. package/dist/agents/agent-resolver.js +249 -0
  6. package/dist/agents/agent-seed-service.js +146 -0
  7. package/dist/agents/manager-profile-overlay-service.js +34 -0
  8. package/dist/agents/profile-model-routing.js +26 -0
  9. package/dist/agents/renderers/claude-renderer.js +98 -0
  10. package/dist/agents/renderers/index.js +16 -0
  11. package/dist/agents/renderers/json-renderer.js +87 -0
  12. package/dist/agents/renderers/opencode-renderer.js +100 -0
  13. package/dist/agents/renderers/provider-adapter.js +6 -0
  14. package/dist/agents/repositories/agents.js +185 -0
  15. package/dist/agents/repositories/manager-profile-overlays.js +81 -0
  16. package/dist/agents/schema.js +1 -0
  17. package/dist/cli/dashboard-operational-read-models.js +153 -0
  18. package/dist/cli/dashboard-renderer.js +109 -0
  19. package/dist/cli/dashboard-screen-renderers.js +332 -0
  20. package/dist/cli/dashboard-tui-read-model.js +71 -0
  21. package/dist/cli/dashboard-tui-state.js +218 -0
  22. package/dist/cli/dispatcher.js +2880 -0
  23. package/dist/cli/index.js +27 -0
  24. package/dist/cli/interactive-dashboard.js +29 -0
  25. package/dist/cli/mcp-start-path.js +21 -0
  26. package/dist/cli/setup-status-renderer.js +29 -0
  27. package/dist/cli/setup-wizard-read-model.js +56 -0
  28. package/dist/cli/setup-wizard-renderer.js +148 -0
  29. package/dist/cli/setup-wizard-state.js +82 -0
  30. package/dist/cli/tui-render-helpers.js +192 -0
  31. package/dist/export/redaction.js +71 -0
  32. package/dist/harness/tools/agents.js +245 -0
  33. package/dist/harness/tools/memory.js +29 -0
  34. package/dist/mcp/client-install-opencode-contract.js +227 -0
  35. package/dist/mcp/client-install-opencode.js +194 -0
  36. package/dist/mcp/client-setup-preview.js +38 -0
  37. package/dist/mcp/control-plane.js +175 -0
  38. package/dist/mcp/doctor.js +193 -0
  39. package/dist/mcp/index.js +10 -0
  40. package/dist/mcp/opencode-default-agent-config.js +156 -0
  41. package/dist/mcp/opencode-visibility.js +102 -0
  42. package/dist/mcp/schema.js +234 -0
  43. package/dist/mcp/stdio-server.js +56 -0
  44. package/dist/mcp/validation.js +761 -0
  45. package/dist/memory/import/dry-run-planner.js +58 -0
  46. package/dist/memory/import/index.js +3 -0
  47. package/dist/memory/import/observation-writer.js +220 -0
  48. package/dist/memory/import/package.js +178 -0
  49. package/dist/memory/memory-service.js +126 -0
  50. package/dist/memory/repositories/artifacts.js +41 -0
  51. package/dist/memory/repositories/observations.js +133 -0
  52. package/dist/memory/repositories/sessions.js +105 -0
  53. package/dist/memory/repositories/traces.js +58 -0
  54. package/dist/memory/schema.js +1 -0
  55. package/dist/memory/search.js +11 -0
  56. package/dist/memory/sqlite/database.js +97 -0
  57. package/dist/memory/sqlite/migrations/001_initial.sql +128 -0
  58. package/dist/memory/sqlite/migrations/002_observation_revisions.sql +14 -0
  59. package/dist/memory/sqlite/migrations/003_agent_registry.sql +26 -0
  60. package/dist/memory/sqlite/migrations/004_run_runtime.sql +62 -0
  61. package/dist/memory/sqlite/migrations/005_run_approvals.sql +20 -0
  62. package/dist/memory/sqlite/migrations/006_run_operation_attempts.sql +32 -0
  63. package/dist/memory/sqlite/migrations/007_abandoned_operation_attempts.sql +46 -0
  64. package/dist/memory/sqlite/migrations/008_run_execution_plan_events.sql +105 -0
  65. package/dist/memory/sqlite/migrations/009_multiple_operation_attempts.sql +73 -0
  66. package/dist/memory/sqlite/migrations/010_skill_registry.sql +66 -0
  67. package/dist/memory/sqlite/migrations/011_skill_usage_resolution_outcomes.sql +21 -0
  68. package/dist/memory/sqlite/migrations/012_skill_improvement_proposals.sql +37 -0
  69. package/dist/memory/sqlite/migrations/013_skill_evaluation_scenarios.sql +43 -0
  70. package/dist/memory/sqlite/migrations/014_manager_profile_overlays.sql +14 -0
  71. package/dist/memory/storage-paths.js +72 -0
  72. package/dist/orchestrator/natural-language-planner.js +191 -0
  73. package/dist/orchestrator/schema.js +1 -0
  74. package/dist/permissions/index.js +2 -0
  75. package/dist/permissions/policy-evaluator.js +109 -0
  76. package/dist/permissions/schema.js +1 -0
  77. package/dist/providers/opencode/injection-preview.js +134 -0
  78. package/dist/providers/opencode/manager-payload.js +129 -0
  79. package/dist/runs/execution-planning.js +117 -0
  80. package/dist/runs/operation-execution.js +1 -0
  81. package/dist/runs/operation-retry.js +124 -0
  82. package/dist/runs/repositories/runs.js +611 -0
  83. package/dist/runs/run-insights.js +145 -0
  84. package/dist/runs/run-service.js +713 -0
  85. package/dist/runs/run-snapshot-export-service.js +31 -0
  86. package/dist/runs/sandbox-process-execution.js +218 -0
  87. package/dist/runs/sandbox-worktree-planning.js +59 -0
  88. package/dist/runs/schema.js +1 -0
  89. package/dist/sdd/artifact-portability-service.js +118 -0
  90. package/dist/sdd/schema.js +17 -0
  91. package/dist/sdd/sdd-workflow-service.js +217 -0
  92. package/dist/setup/backup-rollback-service.js +76 -0
  93. package/dist/setup/index.js +3 -0
  94. package/dist/setup/providers/antigravity-setup-adapter.js +18 -0
  95. package/dist/setup/providers/claude-setup-adapter.js +30 -0
  96. package/dist/setup/providers/custom-setup-adapter.js +18 -0
  97. package/dist/setup/providers/index.js +6 -0
  98. package/dist/setup/providers/opencode-setup-adapter.js +104 -0
  99. package/dist/setup/providers/provider-setup-adapter.js +15 -0
  100. package/dist/setup/providers/provider-setup-registry.js +11 -0
  101. package/dist/setup/schema.js +1 -0
  102. package/dist/setup/setup-defaults.js +11 -0
  103. package/dist/setup/setup-lifecycle-service.js +175 -0
  104. package/dist/setup/setup-plan.js +105 -0
  105. package/dist/skills/repositories/skill-evaluation-scenarios.js +289 -0
  106. package/dist/skills/repositories/skill-improvement-proposals.js +288 -0
  107. package/dist/skills/repositories/skills.js +430 -0
  108. package/dist/skills/schema.js +1 -0
  109. package/dist/skills/skill-payload.js +94 -0
  110. package/dist/skills/skill-registry-service.js +92 -0
  111. package/dist/skills/skill-resolver.js +191 -0
  112. package/dist/workflows/command-allowlist-adapter.js +70 -0
  113. package/dist/workflows/schema.js +4 -0
  114. package/dist/workflows/workflow-executor.js +345 -0
  115. package/dist/workflows/workflow-registry.js +66 -0
  116. package/docs/architecture.md +698 -0
  117. package/docs/cli.md +741 -0
  118. package/docs/funcionamiento-del-sistema.md +868 -0
  119. package/docs/harness-gap-analysis.md +229 -0
  120. package/docs/prd.md +372 -0
  121. package/package.json +57 -0
@@ -0,0 +1,245 @@
1
+ import { permissionCategories, permissionDecisions } from '../../permissions/policy-evaluator.js';
2
+ export function createAgentRegistryToolHandlers(dependencies) {
3
+ const { service } = dependencies;
4
+ return {
5
+ registerAgent: (input) => {
6
+ const mapped = mapRegisterAgentInput(input, false);
7
+ return mapped.ok ? service.registerAgent(mapped.value) : mapped;
8
+ },
9
+ registerSubagent: (input) => {
10
+ const mapped = mapRegisterAgentInput(input, true);
11
+ return mapped.ok ? service.registerSubagent(mapped.value) : mapped;
12
+ },
13
+ getAgent: (input) => {
14
+ const mappedInput = requireRecord(input, 'input');
15
+ if (!mappedInput.ok)
16
+ return mappedInput;
17
+ const payload = mappedInput.value;
18
+ const id = requireString(payload.id, 'id');
19
+ return id.ok ? service.getAgent(id.value) : id;
20
+ },
21
+ getAgentByName: (input) => {
22
+ const mapped = mapNaturalKey(input);
23
+ return mapped.ok ? service.getAgentByName(mapped.value.project, mapped.value.scope, mapped.value.name) : mapped;
24
+ },
25
+ listAgents: (input) => {
26
+ const mapped = mapListFilters(input ?? {});
27
+ return mapped.ok ? service.listAgents(mapped.value) : mapped;
28
+ },
29
+ listSubagents: (input) => {
30
+ const mappedInput = requireRecord(input, 'input');
31
+ if (!mappedInput.ok)
32
+ return mappedInput;
33
+ const payload = mappedInput.value;
34
+ const parentAgentId = requireString(payload.parentAgentId, 'parentAgentId');
35
+ return parentAgentId.ok ? service.listSubagents(parentAgentId.value) : parentAgentId;
36
+ },
37
+ };
38
+ }
39
+ function mapRegisterAgentInput(input, subagent) {
40
+ const mappedInput = requireRecord(input, 'input');
41
+ if (!mappedInput.ok)
42
+ return mappedInput;
43
+ const payload = mappedInput.value;
44
+ const project = requireString(payload.project, 'project');
45
+ if (!project.ok)
46
+ return project;
47
+ const scope = requireScope(payload.scope, 'scope');
48
+ if (!scope.ok)
49
+ return scope;
50
+ const name = requireString(payload.name, 'name');
51
+ if (!name.ok)
52
+ return name;
53
+ const description = requireString(payload.description, 'description');
54
+ if (!description.ok)
55
+ return description;
56
+ const instructions = requireInstructions(payload.instructions);
57
+ if (!instructions.ok)
58
+ return instructions;
59
+ const capabilities = optionalStringArray(payload.capabilities, 'capabilities');
60
+ if (!capabilities.ok)
61
+ return capabilities;
62
+ const permissions = optionalPermissions(payload.permissions);
63
+ if (!permissions.ok)
64
+ return permissions;
65
+ const memory = optionalMemory(payload.memory);
66
+ if (!memory.ok)
67
+ return memory;
68
+ const workflows = optionalStringArray(payload.workflows, 'workflows');
69
+ if (!workflows.ok)
70
+ return workflows;
71
+ const skills = optionalStringArray(payload.skills, 'skills');
72
+ if (!skills.ok)
73
+ return skills;
74
+ const adapters = optionalAdapters(payload.adapters);
75
+ if (!adapters.ok)
76
+ return adapters;
77
+ const value = {
78
+ project: project.value,
79
+ scope: scope.value,
80
+ mode: subagent ? 'subagent' : 'agent',
81
+ name: name.value,
82
+ description: description.value,
83
+ instructions: instructions.value,
84
+ };
85
+ if (capabilities.value !== undefined)
86
+ value.capabilities = capabilities.value;
87
+ if (permissions.value !== undefined)
88
+ value.permissions = permissions.value;
89
+ if (memory.value !== undefined)
90
+ value.memory = memory.value;
91
+ if (workflows.value !== undefined)
92
+ value.workflows = workflows.value;
93
+ if (skills.value !== undefined)
94
+ value.skills = skills.value;
95
+ if (adapters.value !== undefined)
96
+ value.adapters = adapters.value;
97
+ if (subagent) {
98
+ const parentAgentId = requireString(payload.parentAgentId, 'parentAgentId');
99
+ if (!parentAgentId.ok)
100
+ return parentAgentId;
101
+ value.parentAgentId = parentAgentId.value;
102
+ }
103
+ return ok(value);
104
+ }
105
+ function mapNaturalKey(input) {
106
+ const mappedInput = requireRecord(input, 'input');
107
+ if (!mappedInput.ok)
108
+ return mappedInput;
109
+ const payload = mappedInput.value;
110
+ const project = requireString(payload.project, 'project');
111
+ if (!project.ok)
112
+ return project;
113
+ const scope = requireScope(payload.scope, 'scope');
114
+ if (!scope.ok)
115
+ return scope;
116
+ const name = requireString(payload.name, 'name');
117
+ if (!name.ok)
118
+ return name;
119
+ return ok({ project: project.value, scope: scope.value, name: name.value });
120
+ }
121
+ function mapListFilters(input) {
122
+ const mappedInput = requireRecord(input, 'input');
123
+ if (!mappedInput.ok)
124
+ return mappedInput;
125
+ const payload = mappedInput.value;
126
+ const filters = {};
127
+ if (payload.project !== undefined) {
128
+ const project = requireString(payload.project, 'project');
129
+ if (!project.ok)
130
+ return project;
131
+ filters.project = project.value;
132
+ }
133
+ if (payload.scope !== undefined) {
134
+ const scope = requireScope(payload.scope, 'scope');
135
+ if (!scope.ok)
136
+ return scope;
137
+ filters.scope = scope.value;
138
+ }
139
+ if (payload.mode !== undefined) {
140
+ if (payload.mode !== 'agent' && payload.mode !== 'subagent')
141
+ return validationFailure('mode must be agent or subagent');
142
+ filters.mode = payload.mode;
143
+ }
144
+ if (payload.parentAgentId !== undefined) {
145
+ const parentAgentId = requireString(payload.parentAgentId, 'parentAgentId');
146
+ if (!parentAgentId.ok)
147
+ return parentAgentId;
148
+ filters.parentAgentId = parentAgentId.value;
149
+ }
150
+ return ok(filters);
151
+ }
152
+ function requireString(value, field) {
153
+ return typeof value === 'string' && value.trim() ? ok(value) : validationFailure(`${field} must be a non-empty string`);
154
+ }
155
+ function requireScope(value, field) {
156
+ return value === 'project' || value === 'personal' ? ok(value) : validationFailure(`${field} must be project or personal`);
157
+ }
158
+ function requireRecord(value, field) {
159
+ return isRecord(value) ? ok(value) : validationFailure(`${field} must be an object`);
160
+ }
161
+ function requireInstructions(value) {
162
+ if (!isRecord(value))
163
+ return validationFailure('instructions must be an object');
164
+ if (value.kind !== 'inline' && value.kind !== 'path')
165
+ return validationFailure('instructions.kind must be inline or path');
166
+ const instructionValue = requireString(value.value, 'instructions.value');
167
+ return instructionValue.ok ? ok({ kind: value.kind, value: instructionValue.value }) : instructionValue;
168
+ }
169
+ function optionalStringArray(value, field) {
170
+ if (value === undefined)
171
+ return ok(undefined);
172
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string'))
173
+ return validationFailure(`${field} must be an array of strings`);
174
+ return ok(value);
175
+ }
176
+ function optionalPermissions(value) {
177
+ if (value === undefined)
178
+ return ok(undefined);
179
+ if (!isRecord(value))
180
+ return validationFailure('permissions must be an object');
181
+ const permissions = {};
182
+ for (const [key, decision] of Object.entries(value)) {
183
+ if (!permissionCategories.includes(key))
184
+ return validationFailure(`Unknown permission category: ${key}`);
185
+ if (!permissionDecisions.includes(decision))
186
+ return validationFailure(`Invalid permission decision for ${key}`);
187
+ permissions[key] = decision;
188
+ }
189
+ return ok(permissions);
190
+ }
191
+ function optionalMemory(value) {
192
+ if (value === undefined)
193
+ return ok(undefined);
194
+ if (!isRecord(value) || !Array.isArray(value.scopes))
195
+ return validationFailure('memory.scopes must be an array');
196
+ const scopes = [];
197
+ for (const scope of value.scopes) {
198
+ const mapped = requireScope(scope, 'memory.scopes[]');
199
+ if (!mapped.ok)
200
+ return mapped;
201
+ scopes.push(mapped.value);
202
+ }
203
+ return ok({ scopes });
204
+ }
205
+ function optionalAdapters(value) {
206
+ if (value === undefined)
207
+ return ok(undefined);
208
+ if (!isRecord(value))
209
+ return validationFailure('adapters must be an object');
210
+ const adapters = {};
211
+ for (const [name, adapter] of Object.entries(value)) {
212
+ if (!isRecord(adapter))
213
+ return validationFailure(`adapters.${name} must be an object`);
214
+ const config = {};
215
+ if (adapter.model !== undefined) {
216
+ if (typeof adapter.model !== 'string')
217
+ return validationFailure(`adapters.${name}.model must be a string`);
218
+ config.model = adapter.model;
219
+ }
220
+ if (adapter.config !== undefined) {
221
+ if (!isRecord(adapter.config) || !isJsonValue(adapter.config))
222
+ return validationFailure(`adapters.${name}.config must be JSON-compatible`);
223
+ config.config = adapter.config;
224
+ }
225
+ adapters[name] = config;
226
+ }
227
+ return ok(adapters);
228
+ }
229
+ function isJsonValue(value) {
230
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
231
+ return true;
232
+ if (typeof value === 'number')
233
+ return Number.isFinite(value);
234
+ if (Array.isArray(value))
235
+ return value.every(isJsonValue);
236
+ return isRecord(value) && Object.values(value).every(isJsonValue);
237
+ }
238
+ function isRecord(value) {
239
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
240
+ }
241
+ function ok(value) { return { ok: true, value }; }
242
+ function validationFailure(message) {
243
+ const error = { code: 'validation_failed', message };
244
+ return { ok: false, error };
245
+ }
@@ -0,0 +1,29 @@
1
+ export function createMemoryToolHandlers(dependencies) {
2
+ const { service, config, fallback } = dependencies;
3
+ return {
4
+ saveObservation: (input, context) => localOrFallback(config, fallback?.saveObservation, context, () => service.saveObservation(input, context), input),
5
+ getObservation: (input, context) => localOrFallback(config, fallback?.getObservation, context, () => service.getObservation(input.id, context), input.id),
6
+ updateObservation: (input, context) => localOrFallback(config, fallback?.updateObservation, context, () => service.updateObservation(input, context), input),
7
+ searchObservations: (input, context) => localOrFallback(config, fallback?.searchObservations, context, () => service.searchObservations(input, context), input),
8
+ listObservationRevisions: (input, context) => localOrFallback(config, fallback?.listObservationRevisions, context, () => service.listObservationRevisions(input.id, context), input),
9
+ saveSession: (input, context) => localOrFallback(config, fallback?.saveSession, context, () => service.saveSession(input, context), input),
10
+ appendActivity: (input, context) => localOrFallback(config, fallback?.appendActivity, context, () => service.appendActivity(input, context), input),
11
+ closeSession: (input, context) => localOrFallback(config, fallback?.closeSession, context, () => service.closeSession(input, context), input),
12
+ restoreSession: (input, context) => localOrFallback(config, fallback?.restoreSession, context, () => service.restoreSession(input), input),
13
+ saveArtifact: (input, context) => localOrFallback(config, fallback?.saveArtifact, context, () => service.saveArtifact(input, context), input),
14
+ getArtifact: (input, context) => localOrFallback(config, fallback?.getArtifact, context, () => service.getArtifact(input.project, input.topicKey, context), input),
15
+ };
16
+ }
17
+ function localOrFallback(config, fallback, context, local, input) {
18
+ if (config.localMemoryEnabled)
19
+ return local();
20
+ if (fallback)
21
+ return fallback(input, context);
22
+ return { ok: false, error: disabledFailure() };
23
+ }
24
+ function disabledFailure() {
25
+ return {
26
+ code: 'validation_failed',
27
+ message: 'Local memory tool is disabled and no Engram fallback is configured',
28
+ };
29
+ }
@@ -0,0 +1,227 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { createOpenCodeDefaultAgentConfig, createOpenCodeDefaultAgentInstallPlan } from './opencode-default-agent-config.js';
4
+ const opencodeConfigSchema = 'https://opencode.ai/config.json';
5
+ const supportedConfigTargets = [
6
+ '.opencode/opencode.json',
7
+ 'opencode.json',
8
+ '.opencode/opencode.jsonc',
9
+ 'opencode.jsonc',
10
+ ];
11
+ export function resolveOpenCodeMcpInstallTarget(input) {
12
+ const scope = input.scope ?? 'project';
13
+ if (scope === 'project')
14
+ return { ok: true, scope, path: join(input.cwd, '.opencode', 'opencode.json') };
15
+ const home = input.env?.HOME;
16
+ if (typeof home !== 'string' || home.trim().length === 0) {
17
+ return { ok: false, scope, reason: 'missing_home', message: 'Unable to resolve OpenCode user config because HOME is not set.' };
18
+ }
19
+ return { ok: true, scope, path: join(home, '.config', 'opencode', 'opencode.json') };
20
+ }
21
+ export function planOpenCodeMcpInstall(input) {
22
+ const databasePathSource = input.databasePathSource ?? 'flag';
23
+ const server = createOpenCodeLocalMcpServerConfig(input.databasePath, databasePathSource);
24
+ const scope = input.scope ?? 'project';
25
+ const agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: input.mcpOnly });
26
+ if (scope === 'user') {
27
+ return planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan);
28
+ }
29
+ const existingTargets = supportedConfigTargets
30
+ .map((relativePath) => ({ relativePath, absolutePath: join(input.cwd, relativePath) }))
31
+ .filter((target) => existsSync(target.absolutePath));
32
+ if (existingTargets.length > 1) {
33
+ return refusal('ambiguous_target', `Multiple OpenCode project config targets exist: ${existingTargets.map((target) => target.relativePath).join(', ')}. Remove ambiguity before installing.`, input.databasePath, databasePathSource, scope, undefined, [{ kind: 'manual-check', message: 'Remove ambiguity by keeping exactly one OpenCode project config target before installing.' }], agentPlan);
34
+ }
35
+ if (existingTargets.length === 0) {
36
+ return {
37
+ ...baseContract(input.databasePath, databasePathSource, scope, join(input.cwd, '.opencode', 'opencode.json'), false, 'create', agentPlan),
38
+ status: 'would_install',
39
+ action: 'create',
40
+ targetPath: join(input.cwd, '.opencode', 'opencode.json'),
41
+ backupRequired: false,
42
+ server,
43
+ preservedTopLevelKeys: agentPlan.installsAgents ? ['$schema', 'default_agent', 'agent', 'mcp'] : ['$schema', 'mcp'],
44
+ existingSchema: null,
45
+ };
46
+ }
47
+ const [target] = existingTargets;
48
+ if (target === undefined)
49
+ return refusal('ambiguous_target', 'Unable to resolve OpenCode project config target.', input.databasePath, databasePathSource, scope);
50
+ if (target.relativePath.endsWith('.jsonc')) {
51
+ return refusal('unsupported_jsonc', `OpenCode JSONC config ${target.relativePath} is not supported yet; use JSON or remove comments first.`, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan);
52
+ }
53
+ const parsed = parseConfig(target.absolutePath);
54
+ if (!parsed.ok)
55
+ return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan);
56
+ const config = parsed.value;
57
+ if (config.mcp !== undefined && !isRecord(config.mcp)) {
58
+ return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan);
59
+ }
60
+ if (isRecord(config.mcp) && Object.hasOwn(config.mcp, 'vgxness')) {
61
+ return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, scope, target.absolutePath, [], agentPlan);
62
+ }
63
+ const agentConflict = findConflictingVgxnessAgents(config, agentPlan);
64
+ if (agentConflict.length > 0) {
65
+ return refusal('existing_vgxness_agent', `Existing OpenCode config contains custom VGXNESS agent entries that would be overwritten: ${agentConflict.join(', ')}. Remove, rename, or manually reconcile them before installing.`, input.databasePath, databasePathSource, scope, target.absolutePath, [{ kind: 'manual-check', message: `Manually reconcile conflicting VGXNESS agent entries: ${agentConflict.join(', ')}.` }], agentPlan);
66
+ }
67
+ return {
68
+ ...baseContract(input.databasePath, databasePathSource, scope, target.absolutePath, true, 'merge-preserve-existing', agentPlan),
69
+ status: 'would_install',
70
+ action: 'merge',
71
+ targetPath: target.absolutePath,
72
+ backupRequired: true,
73
+ server,
74
+ preservedTopLevelKeys: Object.keys(config),
75
+ existingSchema: typeof config.$schema === 'string' ? config.$schema : opencodeConfigSchema,
76
+ };
77
+ }
78
+ function planUserOpenCodeMcpInstall(input, databasePathSource, server, agentPlan) {
79
+ const target = resolveOpenCodeMcpInstallTarget({ cwd: input.cwd, scope: 'user', env: input.env });
80
+ if (!target.ok) {
81
+ return refusal('unsupported_config_shape', target.message, input.databasePath, databasePathSource, 'user');
82
+ }
83
+ const jsoncPath = `${target.path}c`;
84
+ if (existsSync(jsoncPath)) {
85
+ return refusal('unsupported_jsonc', 'OpenCode user JSONC config opencode.jsonc is not supported yet; use JSON or remove comments first.', input.databasePath, databasePathSource, 'user', jsoncPath, [], agentPlan);
86
+ }
87
+ if (!existsSync(target.path)) {
88
+ return {
89
+ ...baseContract(input.databasePath, databasePathSource, 'user', target.path, false, 'create', agentPlan),
90
+ status: 'would_install',
91
+ action: 'create',
92
+ targetPath: target.path,
93
+ backupRequired: false,
94
+ server,
95
+ preservedTopLevelKeys: agentPlan.installsAgents ? ['$schema', 'default_agent', 'agent', 'mcp'] : ['$schema', 'mcp'],
96
+ existingSchema: null,
97
+ };
98
+ }
99
+ const parsed = parseConfig(target.path);
100
+ if (!parsed.ok)
101
+ return refusal(parsed.reason, parsed.message, input.databasePath, databasePathSource, 'user', target.path, [], agentPlan);
102
+ const config = parsed.value;
103
+ if (config.mcp !== undefined && !isRecord(config.mcp)) {
104
+ return refusal('invalid_mcp_shape', 'Existing top-level mcp must be a JSON object before vgxness can be merged.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan);
105
+ }
106
+ if (isRecord(config.mcp) && Object.hasOwn(config.mcp, 'vgxness')) {
107
+ return refusal('existing_vgxness_mcp', 'Existing OpenCode config already contains mcp.vgxness; overwrite is refused by default.', input.databasePath, databasePathSource, 'user', target.path, [], agentPlan);
108
+ }
109
+ const agentConflict = findConflictingVgxnessAgents(config, agentPlan);
110
+ if (agentConflict.length > 0) {
111
+ return refusal('existing_vgxness_agent', `Existing OpenCode config contains custom VGXNESS agent entries that would be overwritten: ${agentConflict.join(', ')}. Remove, rename, or manually reconcile them before installing.`, input.databasePath, databasePathSource, 'user', target.path, [{ kind: 'manual-check', message: `Manually reconcile conflicting VGXNESS agent entries: ${agentConflict.join(', ')}.` }], agentPlan);
112
+ }
113
+ return {
114
+ ...baseContract(input.databasePath, databasePathSource, 'user', target.path, true, 'merge-preserve-existing', agentPlan),
115
+ status: 'would_install',
116
+ action: 'merge',
117
+ targetPath: target.path,
118
+ backupRequired: true,
119
+ server,
120
+ preservedTopLevelKeys: Object.keys(config),
121
+ existingSchema: typeof config.$schema === 'string' ? config.$schema : opencodeConfigSchema,
122
+ };
123
+ }
124
+ function createOpenCodeLocalMcpServerConfig(databasePath, source) {
125
+ return { type: 'local', command: createVgxnessMcpServerCommand(databasePath, source), enabled: true };
126
+ }
127
+ function createVgxnessMcpServerCommand(databasePath, source) {
128
+ return source === 'global-default' ? ['vgxness', 'mcp', 'start'] : ['vgxness', 'mcp', 'start', '--db', databasePath];
129
+ }
130
+ function createVgxnessMcpDoctorCommand(databasePath, source) {
131
+ return source === 'global-default' ? ['vgxness', 'mcp', 'doctor'] : ['vgxness', 'mcp', 'doctor', '--db', databasePath];
132
+ }
133
+ function parseConfig(path) {
134
+ try {
135
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
136
+ return isRecord(parsed)
137
+ ? { ok: true, value: parsed }
138
+ : { ok: false, reason: 'unsupported_config_shape', message: 'OpenCode config must be a JSON object.' };
139
+ }
140
+ catch {
141
+ return { ok: false, reason: 'malformed_json', message: 'OpenCode config must be valid JSON; malformed JSON and JSONC comments are refused.' };
142
+ }
143
+ }
144
+ function isRecord(value) {
145
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
146
+ }
147
+ export function findConflictingVgxnessAgents(config, agentPlan) {
148
+ if (!agentPlan.installsAgents || !isRecord(config.agent))
149
+ return [];
150
+ const defaults = createOpenCodeDefaultAgentConfig().agents;
151
+ return agentPlan.agentNames.filter((name) => Object.hasOwn(config.agent, name) && !deepEqual(config.agent[name], defaults[name]));
152
+ }
153
+ function deepEqual(left, right) {
154
+ if (Object.is(left, right))
155
+ return true;
156
+ if (Array.isArray(left) || Array.isArray(right)) {
157
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => deepEqual(value, right[index]));
158
+ }
159
+ if (!isRecord(left) || !isRecord(right))
160
+ return false;
161
+ const leftKeys = Object.keys(left).sort();
162
+ const rightKeys = Object.keys(right).sort();
163
+ return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index] && deepEqual(left[key], right[key]));
164
+ }
165
+ function baseContract(databasePath, source, scope, targetPath, backupRequired, mergePolicy, agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true })) {
166
+ return {
167
+ version: 1,
168
+ kind: 'mcp-client-install-opencode',
169
+ installable: true,
170
+ mutating: false,
171
+ safety: {
172
+ operation: 'plan',
173
+ mutating: false,
174
+ writesProviderConfig: false,
175
+ ...(targetPath !== undefined ? { targetPath } : {}),
176
+ backupRequired,
177
+ mergePolicy,
178
+ },
179
+ scope,
180
+ warnings: warningsForScope(scope),
181
+ verificationHints: verificationHints(databasePath, source),
182
+ manualTest: manualTestForScope(scope, databasePath, source),
183
+ installsAgents: agentPlan.installsAgents,
184
+ agentNames: agentPlan.agentNames,
185
+ ...(agentPlan.defaultAgent !== undefined ? { defaultAgent: agentPlan.defaultAgent } : {}),
186
+ };
187
+ }
188
+ function refusal(reason, message, databasePath, source, scope, targetPath, extraVerificationHints = [], agentPlan = createOpenCodeDefaultAgentInstallPlan({ mcpOnly: true })) {
189
+ const contract = baseContract(databasePath, source, scope, targetPath, false, 'refuse-no-clobber', agentPlan);
190
+ return {
191
+ ...contract,
192
+ verificationHints: [...extraVerificationHints, ...contract.verificationHints],
193
+ status: 'refused',
194
+ reason,
195
+ message,
196
+ ...(targetPath !== undefined ? { targetPath } : {}),
197
+ };
198
+ }
199
+ function warningsForScope(scope) {
200
+ if (scope === 'project')
201
+ return ['Restart OpenCode after installation so it reloads the project MCP config.'];
202
+ return [
203
+ 'Restart OpenCode after installation so it reloads the user MCP config.',
204
+ 'OpenCode project config may override user config for a workspace; check project-level config if vgxness is not visible.',
205
+ ];
206
+ }
207
+ function manualTestForScope(scope, databasePath, source) {
208
+ if (scope === 'project') {
209
+ return {
210
+ restart: 'Restart OpenCode after the config is installed.',
211
+ verify: 'Open the project in OpenCode and verify the vgxness MCP server is available.',
212
+ doctorCommand: createVgxnessMcpDoctorCommand(databasePath, source),
213
+ };
214
+ }
215
+ return {
216
+ restart: 'Restart OpenCode after the user config is installed.',
217
+ verify: 'Open OpenCode and verify the vgxness MCP server is available from the user OpenCode config; project config may override it.',
218
+ doctorCommand: createVgxnessMcpDoctorCommand(databasePath, source),
219
+ };
220
+ }
221
+ function verificationHints(databasePath, source) {
222
+ return [
223
+ { kind: 'restart-client', message: 'Restart OpenCode after the config is installed.' },
224
+ { kind: 'manual-check', message: 'Open the project in OpenCode and verify the vgxness MCP server is available.' },
225
+ { kind: 'command', message: 'Run the MCP doctor command after installation.', command: createVgxnessMcpDoctorCommand(databasePath, source) },
226
+ ];
227
+ }