@tacuchi/agent-workflow-cli 22.4.2 → 24.0.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.
- package/README.md +39 -3
- package/dist/adapters/git-cli.js +59 -17
- package/dist/adapters/git-cli.js.map +1 -1
- package/dist/adapters/node-file-system.js +55 -1
- package/dist/adapters/node-file-system.js.map +1 -1
- package/dist/adapters/postgres-readonly-tools.js +277 -0
- package/dist/adapters/postgres-readonly-tools.js.map +1 -0
- package/dist/application/artifacts-service.js +2 -2
- package/dist/application/artifacts-service.js.map +1 -1
- package/dist/application/capability/dispatcher.js +2 -2
- package/dist/application/capability/dispatcher.js.map +1 -1
- package/dist/application/capability/installed-inventory.js +2 -2
- package/dist/application/capability/installed-inventory.js.map +1 -1
- package/dist/application/capability/readiness.js +1 -1
- package/dist/application/capability/readiness.js.map +1 -1
- package/dist/application/check-branch-service.js +2 -2
- package/dist/application/check-branch-service.js.map +1 -1
- package/dist/application/checkpoint/files-touched.js +271 -0
- package/dist/application/checkpoint/files-touched.js.map +1 -0
- package/dist/application/checkpoint/markdown.js +61 -8
- package/dist/application/checkpoint/markdown.js.map +1 -1
- package/dist/application/checkpoint/state-reader.js +8 -2
- package/dist/application/checkpoint/state-reader.js.map +1 -1
- package/dist/application/checkpoint-write-service.js +16 -5
- package/dist/application/checkpoint-write-service.js.map +1 -1
- package/dist/application/claims-ledger.js +330 -0
- package/dist/application/claims-ledger.js.map +1 -0
- package/dist/application/claims-recovery.js +338 -0
- package/dist/application/claims-recovery.js.map +1 -0
- package/dist/application/database-mcp-server.js +168 -0
- package/dist/application/database-mcp-server.js.map +1 -0
- package/dist/application/database-mcp-stdio.js +137 -0
- package/dist/application/database-mcp-stdio.js.map +1 -0
- package/dist/application/database-tool-catalog.js +226 -0
- package/dist/application/database-tool-catalog.js.map +1 -0
- package/dist/application/dev-only-services.js +154 -44
- package/dist/application/dev-only-services.js.map +1 -1
- package/dist/application/dsn-reader-service.js +1 -1
- package/dist/application/elicitation-server.js +382 -0
- package/dist/application/elicitation-server.js.map +1 -0
- package/dist/application/elicitation-stdio.js +48 -0
- package/dist/application/elicitation-stdio.js.map +1 -0
- package/dist/application/flow/advance.js +86 -5
- package/dist/application/flow/advance.js.map +1 -1
- package/dist/application/flow/checkout-observation.js +152 -0
- package/dist/application/flow/checkout-observation.js.map +1 -0
- package/dist/application/flow/flow-service.js +72 -13
- package/dist/application/flow/flow-service.js.map +1 -1
- package/dist/application/flow/internal-actions.js +460 -1
- package/dist/application/flow/internal-actions.js.map +1 -1
- package/dist/application/flow/internal-drive.js +22 -9
- package/dist/application/flow/internal-drive.js.map +1 -1
- package/dist/application/flow/prove.js +205 -0
- package/dist/application/flow/prove.js.map +1 -0
- package/dist/application/flow/run-journey.js +15 -0
- package/dist/application/flow/run-journey.js.map +1 -0
- package/dist/application/flow/run-projection.js +15 -3
- package/dist/application/flow/run-projection.js.map +1 -1
- package/dist/application/flow/run-state-service.js +19 -4
- package/dist/application/flow/run-state-service.js.map +1 -1
- package/dist/application/flow/submit.js +250 -72
- package/dist/application/flow/submit.js.map +1 -1
- package/dist/application/generate-launch-service.js +4 -4
- package/dist/application/generate-launch-service.js.map +1 -1
- package/dist/application/git-flow-service.js +9 -0
- package/dist/application/git-flow-service.js.map +1 -1
- package/dist/application/local-proposal.js +9 -0
- package/dist/application/local-proposal.js.map +1 -1
- package/dist/application/lock-service.js +96 -28
- package/dist/application/lock-service.js.map +1 -1
- package/dist/application/logging/log-events.js +19 -3
- package/dist/application/logging/log-events.js.map +1 -1
- package/dist/application/logging/logger.js +2 -2
- package/dist/application/logging/logger.js.map +1 -1
- package/dist/application/mcp-connections-service.js +115 -41
- package/dist/application/mcp-connections-service.js.map +1 -1
- package/dist/application/mcp-doctor-service.js +98 -54
- package/dist/application/mcp-doctor-service.js.map +1 -1
- package/dist/application/mcp-entry-classification.js +60 -0
- package/dist/application/mcp-entry-classification.js.map +1 -0
- package/dist/application/mcp-host-reader.js +114 -81
- package/dist/application/mcp-host-reader.js.map +1 -1
- package/dist/application/mcp-host-receipt-service.js +399 -0
- package/dist/application/mcp-host-receipt-service.js.map +1 -0
- package/dist/application/mcp-host-receipt-store.js +122 -0
- package/dist/application/mcp-host-receipt-store.js.map +1 -0
- package/dist/application/mcp-host-receipts.js +28 -0
- package/dist/application/mcp-host-receipts.js.map +1 -0
- package/dist/application/mcp-host-writer.js +321 -126
- package/dist/application/mcp-host-writer.js.map +1 -1
- package/dist/application/mcp-launch-probe-service.js +142 -0
- package/dist/application/mcp-launch-probe-service.js.map +1 -0
- package/dist/application/mcp-migration-service.js +351 -0
- package/dist/application/mcp-migration-service.js.map +1 -0
- package/dist/application/mcp-native-host-check-service.js +154 -0
- package/dist/application/mcp-native-host-check-service.js.map +1 -0
- package/dist/application/mcp-receipt-registration-service.js +105 -0
- package/dist/application/mcp-receipt-registration-service.js.map +1 -0
- package/dist/application/mcp-receipt-removal-service.js +62 -0
- package/dist/application/mcp-receipt-removal-service.js.map +1 -0
- package/dist/application/mcp-remove-service.js +84 -21
- package/dist/application/mcp-remove-service.js.map +1 -1
- package/dist/application/mcp-scope-common.js +2 -4
- package/dist/application/mcp-scope-common.js.map +1 -1
- package/dist/application/mcp-setup-service.js +60 -21
- package/dist/application/mcp-setup-service.js.map +1 -1
- package/dist/application/mcp-stdio-probe.js +526 -0
- package/dist/application/mcp-stdio-probe.js.map +1 -0
- package/dist/application/mcp-test-connection-service.js +23 -50
- package/dist/application/mcp-test-connection-service.js.map +1 -1
- package/dist/application/merge-state-service.js +10 -16
- package/dist/application/merge-state-service.js.map +1 -1
- package/dist/application/multiroot/claude.js +34 -15
- package/dist/application/multiroot/claude.js.map +1 -1
- package/dist/application/multiroot/codex.js +9 -5
- package/dist/application/multiroot/codex.js.map +1 -1
- package/dist/application/multiroot-service.js +20 -11
- package/dist/application/multiroot-service.js.map +1 -1
- package/dist/application/parsers/project-block.js +25 -2
- package/dist/application/parsers/project-block.js.map +1 -1
- package/dist/application/paths-service.js +30 -31
- package/dist/application/paths-service.js.map +1 -1
- package/dist/application/plan-exec-batch-service.js +549 -0
- package/dist/application/plan-exec-batch-service.js.map +1 -0
- package/dist/application/plan-exec-decision-service.js +273 -0
- package/dist/application/plan-exec-decision-service.js.map +1 -0
- package/dist/application/project-md-service.js +2 -2
- package/dist/application/project-md-service.js.map +1 -1
- package/dist/application/project-md-upsert-service.js +4 -4
- package/dist/application/project-md-upsert-service.js.map +1 -1
- package/dist/application/project-tab-data.js +12 -7
- package/dist/application/project-tab-data.js.map +1 -1
- package/dist/application/release-data-service.js +2 -2
- package/dist/application/release-data-service.js.map +1 -1
- package/dist/application/resume-service.js +32 -11
- package/dist/application/resume-service.js.map +1 -1
- package/dist/application/retirement/apply.js +84 -0
- package/dist/application/retirement/apply.js.map +1 -1
- package/dist/application/retirement/attribution.js +31 -1
- package/dist/application/retirement/attribution.js.map +1 -1
- package/dist/application/retirement/graph.js +19 -1
- package/dist/application/retirement/graph.js.map +1 -1
- package/dist/application/retirement/journal.js +11 -0
- package/dist/application/retirement/journal.js.map +1 -1
- package/dist/application/retirement/prepare.js +89 -3
- package/dist/application/retirement/prepare.js.map +1 -1
- package/dist/application/retirement/preview.js +16 -0
- package/dist/application/retirement/preview.js.map +1 -1
- package/dist/application/retirement/resolve.js +65 -0
- package/dist/application/retirement/resolve.js.map +1 -1
- package/dist/application/self/host-states.js +2 -0
- package/dist/application/self/host-states.js.map +1 -1
- package/dist/application/self/install-skill.js +21 -17
- package/dist/application/self/install-skill.js.map +1 -1
- package/dist/application/self/mcp-config.js +392 -84
- package/dist/application/self/mcp-config.js.map +1 -1
- package/dist/application/self/mcp-via-state.js +62 -0
- package/dist/application/self/mcp-via-state.js.map +1 -0
- package/dist/application/self/uninstall.js +15 -5
- package/dist/application/self/uninstall.js.map +1 -1
- package/dist/application/session-close-service.js +24 -1
- package/dist/application/session-close-service.js.map +1 -1
- package/dist/application/session-create-service.js +7 -0
- package/dist/application/session-create-service.js.map +1 -1
- package/dist/application/session-resume-service.js +2 -2
- package/dist/application/session-resume-service.js.map +1 -1
- package/dist/application/sessions-service.js +2 -4
- package/dist/application/sessions-service.js.map +1 -1
- package/dist/application/skills-resolver-service.js +4 -4
- package/dist/application/skills-resolver-service.js.map +1 -1
- package/dist/application/source-boundary-policy.js +18 -7
- package/dist/application/source-boundary-policy.js.map +1 -1
- package/dist/application/source-remove-service.js +12 -1
- package/dist/application/source-remove-service.js.map +1 -1
- package/dist/application/sources-service.js +2 -2
- package/dist/application/sources-service.js.map +1 -1
- package/dist/application/status-service.js +4 -0
- package/dist/application/status-service.js.map +1 -1
- package/dist/application/visibility-doctor-service.js +1 -1
- package/dist/application/visibility-doctor-service.js.map +1 -1
- package/dist/application/workline-index-service.js +182 -27
- package/dist/application/workline-index-service.js.map +1 -1
- package/dist/application/workspace-init-service.js +89 -271
- package/dist/application/workspace-init-service.js.map +1 -1
- package/dist/application/workspace-materialization-service.js +300 -0
- package/dist/application/workspace-materialization-service.js.map +1 -0
- package/dist/application/worktree-service.js +32 -3
- package/dist/application/worktree-service.js.map +1 -1
- package/dist/cli/commands/capability.js +4 -7
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/claims.js +72 -0
- package/dist/cli/commands/claims.js.map +1 -0
- package/dist/cli/commands/dev-only.js +73 -25
- package/dist/cli/commands/dev-only.js.map +1 -1
- package/dist/cli/commands/flow.js +145 -42
- package/dist/cli/commands/flow.js.map +1 -1
- package/dist/cli/commands/index.js +4 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/mcp.js +752 -76
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/multiroot.js +40 -7
- package/dist/cli/commands/multiroot.js.map +1 -1
- package/dist/cli/commands/resume.js +9 -3
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/retirement.js +9 -0
- package/dist/cli/commands/retirement.js.map +1 -1
- package/dist/cli/commands/session-create.js +1 -1
- package/dist/cli/commands/session-create.js.map +1 -1
- package/dist/cli/commands/skills.js +1 -1
- package/dist/cli/commands/skills.js.map +1 -1
- package/dist/cli/commands/status.js +72 -24
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/tool.js +141 -0
- package/dist/cli/commands/tool.js.map +1 -0
- package/dist/cli/commands/workspace-init.js +5 -5
- package/dist/cli/commands/workspace-init.js.map +1 -1
- package/dist/cli/help-groups.js +6 -1
- package/dist/cli/help-groups.js.map +1 -1
- package/dist/cli/main.js +272 -67
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/parser.js +8 -1
- package/dist/cli/parser.js.map +1 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli/render.js +15 -2
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/tui/app.js +3 -2
- package/dist/cli/tui/app.js.map +1 -1
- package/dist/cli/tui/components/workspace-init-form.js +7 -7
- package/dist/cli/tui/components/workspace-init-form.js.map +1 -1
- package/dist/cli/tui/data/workflow-content.js +2 -2
- package/dist/cli/tui/data/workflow-content.js.map +1 -1
- package/dist/cli/tui/tabs/config-tab.js +4 -3
- package/dist/cli/tui/tabs/config-tab.js.map +1 -1
- package/dist/cli/tui/tabs/mcp-tab-helpers.js +72 -1
- package/dist/cli/tui/tabs/mcp-tab-helpers.js.map +1 -1
- package/dist/cli/tui/tabs/mcp-tab.js +101 -42
- package/dist/cli/tui/tabs/mcp-tab.js.map +1 -1
- package/dist/cli/tui/tabs/project-tab.js +18 -21
- package/dist/cli/tui/tabs/project-tab.js.map +1 -1
- package/dist/cli/tui/workspace-root.js +13 -0
- package/dist/cli/tui/workspace-root.js.map +1 -0
- package/dist/domain/capability/protocol.js +2 -2
- package/dist/domain/capability/protocol.js.map +1 -1
- package/dist/domain/database-tools.js +445 -0
- package/dist/domain/database-tools.js.map +1 -0
- package/dist/domain/degradation-notice.js +60 -0
- package/dist/domain/degradation-notice.js.map +1 -0
- package/dist/domain/elicitation.js +152 -0
- package/dist/domain/elicitation.js.map +1 -0
- package/dist/domain/flow/answer.js +173 -42
- package/dist/domain/flow/answer.js.map +1 -1
- package/dist/domain/flow/authority.js +184 -127
- package/dist/domain/flow/authority.js.map +1 -1
- package/dist/domain/flow/directive.js +47 -1
- package/dist/domain/flow/directive.js.map +1 -1
- package/dist/domain/flow/rules.js +7 -4
- package/dist/domain/flow/rules.js.map +1 -1
- package/dist/domain/flow/run-state.js +636 -31
- package/dist/domain/flow/run-state.js.map +1 -1
- package/dist/domain/harnesses.js +16 -0
- package/dist/domain/harnesses.js.map +1 -1
- package/dist/domain/mcp-entry.js +164 -28
- package/dist/domain/mcp-entry.js.map +1 -1
- package/dist/domain/postgres-object-search.js +320 -0
- package/dist/domain/postgres-object-search.js.map +1 -0
- package/dist/domain/redaction.js +47 -0
- package/dist/domain/redaction.js.map +1 -0
- package/dist/domain/retirement/proposal.js +1 -0
- package/dist/domain/retirement/proposal.js.map +1 -1
- package/dist/domain/structured-choice-stamp.js +32 -4
- package/dist/domain/structured-choice-stamp.js.map +1 -1
- package/dist/domain/workline-mcp-entry.js +34 -0
- package/dist/domain/workline-mcp-entry.js.map +1 -0
- package/dist/ports/postgres-tools.js +10 -0
- package/dist/ports/postgres-tools.js.map +1 -0
- package/dist/runtime/namespace-resolver.js +104 -18
- package/dist/runtime/namespace-resolver.js.map +1 -1
- package/package.json +5 -1
- package/skills/w/README.md +2 -2
- package/skills/w/SKILL.md +9 -10
- package/skills/w/artifacts/README.md +1 -1
- package/skills/w/commands/README.md +3 -3
- package/skills/w/commands/persist.md +3 -1
- package/skills/w/commands/resume.md +1 -1
- package/skills/w/commands/spec-new.md +2 -2
- package/skills/w/commands/status.md +1 -1
- package/skills/w/commands/workspace-init.md +9 -10
- package/skills/w/context/MANIFEST.json +1 -4
- package/skills/w/harness/HARNESS.md +1 -1
- package/skills/w/loops/quick-loop/LOOP.md +1 -1
- package/skills/w/modules/PROMPT-CONTINUITY.md +1 -1
- package/skills/w/modules/WORKSPACE-SCAFFOLD.md +17 -20
- package/skills/w/schemas/capability-descriptor.schema.json +1 -1
- package/dist/application/mcp-dbhub-launcher.js +0 -216
- package/dist/application/mcp-dbhub-launcher.js.map +0 -1
|
@@ -1,32 +1,84 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { closeSync, existsSync, fchmodSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
4
|
import { parse as parseToml } from "smol-toml";
|
|
5
|
-
import {
|
|
5
|
+
import { mcpEntryShapeForHost, } from "../domain/mcp-entry.js";
|
|
6
6
|
import { crushGlobalMcpFile, opencodeGlobalMcpFile } from "./mcp-host-paths.js";
|
|
7
7
|
import { backupFile, escapeRegex, purgeStaleBackups } from "./multiroot/paths.js";
|
|
8
8
|
import { resolveWarpGlobalMcpPath, resolveWarpProjectMcpPath } from "./multiroot/warp.js";
|
|
9
9
|
// Atomic replace: stage to a tmp sibling and rename over the target. At global
|
|
10
10
|
// scope the targets are live user files (~/.claude.json is rewritten by any
|
|
11
11
|
// running Claude Code session); rename keeps a concurrent reader from seeing a
|
|
12
|
-
// truncated/half-written file.
|
|
13
|
-
//
|
|
14
|
-
|
|
12
|
+
// truncated/half-written file. `withHostConfigLock` serializes Workline
|
|
13
|
+
// writers; an external host can still race, so every caller must read back.
|
|
14
|
+
const MAX_ATOMIC_TEMP_ATTEMPTS = 64;
|
|
15
15
|
function atomicWriteFileSync(path, content) {
|
|
16
|
-
|
|
16
|
+
let descriptor;
|
|
17
|
+
let tmp;
|
|
17
18
|
try {
|
|
18
|
-
|
|
19
|
+
const staged = reserveAtomicTempFile(path);
|
|
20
|
+
descriptor = staged.descriptor;
|
|
21
|
+
tmp = staged.path;
|
|
22
|
+
// The temporary file is exclusively created with 0600. Tighten it again
|
|
23
|
+
// where POSIX permissions exist; Windows ACL filesystems can reject
|
|
24
|
+
// fchmod, and that must not turn an otherwise safe atomic replacement
|
|
25
|
+
// into a platform-specific setup failure.
|
|
26
|
+
try {
|
|
27
|
+
fchmodSync(descriptor, 0o600);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// The exclusive create mode above remains the only portable guarantee.
|
|
31
|
+
}
|
|
32
|
+
writeFileSync(descriptor, content, "utf-8");
|
|
33
|
+
closeSync(descriptor);
|
|
34
|
+
descriptor = undefined;
|
|
19
35
|
renameSync(tmp, path);
|
|
36
|
+
tmp = undefined;
|
|
20
37
|
}
|
|
21
38
|
catch (err) {
|
|
39
|
+
if (descriptor !== undefined)
|
|
40
|
+
closeAtomicTempFile(descriptor);
|
|
41
|
+
if (tmp !== undefined)
|
|
42
|
+
discardAtomicTempFile(tmp);
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function reserveAtomicTempFile(path) {
|
|
47
|
+
for (let attempt = 0; attempt < MAX_ATOMIC_TEMP_ATTEMPTS; attempt += 1) {
|
|
48
|
+
const tmp = `${path}.${process.pid}.${attempt + 1}.tmp`;
|
|
22
49
|
try {
|
|
23
|
-
|
|
50
|
+
return { path: tmp, descriptor: openSync(tmp, "wx", 0o600) };
|
|
24
51
|
}
|
|
25
|
-
catch {
|
|
26
|
-
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (filesystemErrorCode(error) === "EEXIST")
|
|
54
|
+
continue;
|
|
55
|
+
throw error;
|
|
27
56
|
}
|
|
28
|
-
throw err;
|
|
29
57
|
}
|
|
58
|
+
throw new McpWriterError("no se pudo reservar un archivo temporal seguro para la configuración MCP", path);
|
|
59
|
+
}
|
|
60
|
+
function closeAtomicTempFile(descriptor) {
|
|
61
|
+
try {
|
|
62
|
+
closeSync(descriptor);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// The failed write is reported below; cleanup is best-effort.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function discardAtomicTempFile(path) {
|
|
69
|
+
try {
|
|
70
|
+
unlinkSync(path);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Only an exclusively-created temp reaches this branch. It may already
|
|
74
|
+
// have been removed by the filesystem after a failed write.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function filesystemErrorCode(error) {
|
|
78
|
+
if (error === null || typeof error !== "object")
|
|
79
|
+
return undefined;
|
|
80
|
+
const code = error.code;
|
|
81
|
+
return typeof code === "string" ? code : undefined;
|
|
30
82
|
}
|
|
31
83
|
function claudeMcpFile(scope) {
|
|
32
84
|
return scope.kind === "global"
|
|
@@ -36,58 +88,69 @@ function claudeMcpFile(scope) {
|
|
|
36
88
|
function legacyClaudeSettingsFile(scope) {
|
|
37
89
|
return join(scope.scopeDir, ".claude", "settings.json");
|
|
38
90
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
91
|
+
/**
|
|
92
|
+
* The old Claude settings location is still live user configuration. A
|
|
93
|
+
* same-named server there is ours only when it has the exact current generated
|
|
94
|
+
* Claude shape. Invalid containers are also a conflict: writing a second
|
|
95
|
+
* location while one cannot be safely interpreted would make the operation
|
|
96
|
+
* partially mutate the user's configuration.
|
|
97
|
+
*/
|
|
98
|
+
function inspectLegacyClaudeMcpEntry(scope, entry, replaceLegacy) {
|
|
99
|
+
const target = legacyClaudeSettingsFile(scope);
|
|
100
|
+
if (!existsSync(target))
|
|
101
|
+
return { state: "absent", target };
|
|
45
102
|
let data;
|
|
46
103
|
try {
|
|
47
|
-
const text = readFileSync(
|
|
104
|
+
const text = readFileSync(target, "utf-8");
|
|
48
105
|
if (text.trim().length === 0)
|
|
49
|
-
return;
|
|
106
|
+
return { state: "absent", target };
|
|
50
107
|
const parsed = JSON.parse(text);
|
|
51
|
-
if (!
|
|
52
|
-
return;
|
|
108
|
+
if (!isRecord(parsed))
|
|
109
|
+
return { state: "conflict", target };
|
|
53
110
|
data = parsed;
|
|
54
111
|
}
|
|
55
112
|
catch {
|
|
56
|
-
return;
|
|
113
|
+
return { state: "conflict", target };
|
|
57
114
|
}
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
return;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
115
|
+
const current = data.mcpServers;
|
|
116
|
+
if (current === undefined)
|
|
117
|
+
return { state: "absent", target };
|
|
118
|
+
if (!isRecord(current))
|
|
119
|
+
return { state: "conflict", target };
|
|
120
|
+
if (!(entry.name in current))
|
|
121
|
+
return { state: "absent", target };
|
|
122
|
+
const expected = mcpEntryShapeForHost("claude", entry);
|
|
123
|
+
const legacy = replaceLegacy === undefined ? undefined : mcpEntryShapeForHost("claude", replaceLegacy);
|
|
124
|
+
if (!isDeepStrictEqual(current[entry.name], expected) &&
|
|
125
|
+
!isDeepStrictEqual(current[entry.name], legacy)) {
|
|
126
|
+
return { state: "conflict", target };
|
|
127
|
+
}
|
|
128
|
+
return { state: "owned", target, data, servers: current };
|
|
129
|
+
}
|
|
130
|
+
function cleanupLegacyClaudeMcpEntry(scope, entry, dryRun, replaceLegacy) {
|
|
131
|
+
if (dryRun)
|
|
68
132
|
return;
|
|
69
|
-
|
|
133
|
+
const legacy = inspectLegacyClaudeMcpEntry(scope, entry, replaceLegacy);
|
|
134
|
+
if (legacy.state !== "owned")
|
|
70
135
|
return;
|
|
71
|
-
servers[name] = undefined;
|
|
72
|
-
const remaining = Object.fromEntries(Object.entries(servers).filter(([,
|
|
136
|
+
legacy.servers[entry.name] = undefined;
|
|
137
|
+
const remaining = Object.fromEntries(Object.entries(legacy.servers).filter(([, value]) => value !== undefined));
|
|
73
138
|
if (Object.keys(remaining).length === 0) {
|
|
74
|
-
data.mcpServers = undefined;
|
|
139
|
+
legacy.data.mcpServers = undefined;
|
|
75
140
|
}
|
|
76
141
|
else {
|
|
77
|
-
data.mcpServers = remaining;
|
|
142
|
+
legacy.data.mcpServers = remaining;
|
|
78
143
|
}
|
|
79
|
-
purgeStaleBackups(legacy);
|
|
80
|
-
const legacyBackup = backupFile(legacy);
|
|
81
|
-
atomicWriteFileSync(legacy, `${JSON.stringify(data, null, 2)}\n`);
|
|
144
|
+
purgeStaleBackups(legacy.target);
|
|
145
|
+
const legacyBackup = backupFile(legacy.target);
|
|
146
|
+
atomicWriteFileSync(legacy.target, `${JSON.stringify(legacy.data, null, 2)}\n`);
|
|
82
147
|
discardBackup(legacyBackup);
|
|
83
148
|
}
|
|
84
149
|
export class McpWriterError extends Error {
|
|
85
150
|
target;
|
|
86
|
-
|
|
87
|
-
constructor(message, target, cause) {
|
|
151
|
+
constructor(message, target) {
|
|
88
152
|
super(message);
|
|
89
153
|
this.target = target;
|
|
90
|
-
this.cause = cause;
|
|
91
154
|
this.name = "McpWriterError";
|
|
92
155
|
}
|
|
93
156
|
}
|
|
@@ -100,6 +163,16 @@ export class McpWriterError extends Error {
|
|
|
100
163
|
* `assertNeverHost` default turns that into a compile error instead.
|
|
101
164
|
*/
|
|
102
165
|
export function writeMcpEntry(host, entry, scope, opts = {}) {
|
|
166
|
+
if (opts.dryRun)
|
|
167
|
+
return writeMcpEntryUnlocked(host, entry, scope, opts);
|
|
168
|
+
return withHostConfigLock(hostConfigTarget(host, scope), () => writeMcpEntryUnlocked(host, entry, scope, opts));
|
|
169
|
+
}
|
|
170
|
+
export function removeMcpEntry(host, entry, scope, opts = {}) {
|
|
171
|
+
if (opts.dryRun)
|
|
172
|
+
return removeMcpEntryUnlocked(host, entry, scope, opts);
|
|
173
|
+
return withHostConfigLock(hostConfigTarget(host, scope), () => removeMcpEntryUnlocked(host, entry, scope, opts));
|
|
174
|
+
}
|
|
175
|
+
function writeMcpEntryUnlocked(host, entry, scope, opts) {
|
|
103
176
|
switch (host) {
|
|
104
177
|
case "claude":
|
|
105
178
|
return writeClaudeMcpEntry(entry, scope, opts);
|
|
@@ -119,7 +192,7 @@ export function writeMcpEntry(host, entry, scope, opts = {}) {
|
|
|
119
192
|
return assertNeverHost(host);
|
|
120
193
|
}
|
|
121
194
|
}
|
|
122
|
-
|
|
195
|
+
function removeMcpEntryUnlocked(host, entry, scope, opts) {
|
|
123
196
|
switch (host) {
|
|
124
197
|
case "claude":
|
|
125
198
|
return removeClaudeMcpEntry(entry, scope, opts);
|
|
@@ -139,6 +212,62 @@ export function removeMcpEntry(host, entry, scope, opts = {}) {
|
|
|
139
212
|
return assertNeverHost(host);
|
|
140
213
|
}
|
|
141
214
|
}
|
|
215
|
+
function hostConfigTarget(host, scope) {
|
|
216
|
+
switch (host) {
|
|
217
|
+
case "claude":
|
|
218
|
+
return claudeMcpFile(scope);
|
|
219
|
+
case "codex":
|
|
220
|
+
return join(scope.scopeDir, ".codex", "config.toml");
|
|
221
|
+
case "warp":
|
|
222
|
+
return warpMcpFile(scope);
|
|
223
|
+
case "gemini":
|
|
224
|
+
return geminiMcpFile(scope);
|
|
225
|
+
case "kimi":
|
|
226
|
+
return kimiMcpFile(scope);
|
|
227
|
+
case "opencode":
|
|
228
|
+
return opencodeMcpFile(scope);
|
|
229
|
+
case "crush":
|
|
230
|
+
return crushMcpFile(scope);
|
|
231
|
+
default:
|
|
232
|
+
return assertNeverHost(host);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Serializes the whole read-modify-write window, not merely the final rename.
|
|
237
|
+
* A competing writer fails explicitly instead of silently dropping one host
|
|
238
|
+
* entry. A stale lock is intentionally not guessed away: removing it requires
|
|
239
|
+
* an operator decision because it may belong to another live process.
|
|
240
|
+
*/
|
|
241
|
+
function withHostConfigLock(target, work) {
|
|
242
|
+
const lock = `${target}.agent-workflow.lock`;
|
|
243
|
+
mkdirSync(dirname(lock), { recursive: true });
|
|
244
|
+
let descriptor;
|
|
245
|
+
try {
|
|
246
|
+
descriptor = openSync(lock, "wx", 0o600);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
if (filesystemErrorCode(error) === "EEXIST") {
|
|
250
|
+
throw new McpWriterError("otro proceso está actualizando esta configuración MCP; reintentá cuando termine", target);
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
return work();
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
try {
|
|
259
|
+
closeSync(descriptor);
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
try {
|
|
263
|
+
unlinkSync(lock);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// A remaining lock is safer than claiming a concurrent write succeeded.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
142
271
|
/** Compile-time exhaustiveness; at runtime it refuses instead of writing somewhere wrong. */
|
|
143
272
|
function assertNeverHost(host) {
|
|
144
273
|
const id = String(host);
|
|
@@ -169,57 +298,30 @@ function crushMcpFile(scope) {
|
|
|
169
298
|
function kimiMcpFile(scope) {
|
|
170
299
|
return join(scope.scopeDir, ".kimi-code", "mcp.json");
|
|
171
300
|
}
|
|
172
|
-
// Per-host serialization of a dbhub McpEntry into the host's JSON schema.
|
|
173
|
-
// Exhaustive for the same reason the dispatchers are: a new host must state its
|
|
174
|
-
// shape, not inherit Claude's by falling off the end.
|
|
175
|
-
function mcpShapeFor(host, entry) {
|
|
176
|
-
switch (host) {
|
|
177
|
-
case "opencode":
|
|
178
|
-
// OpenCode: { type: "local", command: [cmd, ...args], environment, enabled }
|
|
179
|
-
return {
|
|
180
|
-
type: "local",
|
|
181
|
-
command: [entry.command, ...entry.args],
|
|
182
|
-
environment: { ...entry.env },
|
|
183
|
-
enabled: true,
|
|
184
|
-
};
|
|
185
|
-
case "crush":
|
|
186
|
-
// Crush: { type: "stdio", command, args, env }
|
|
187
|
-
return {
|
|
188
|
-
type: "stdio",
|
|
189
|
-
command: entry.command,
|
|
190
|
-
args: [...entry.args],
|
|
191
|
-
env: { ...entry.env },
|
|
192
|
-
};
|
|
193
|
-
case "claude":
|
|
194
|
-
case "codex":
|
|
195
|
-
case "warp":
|
|
196
|
-
case "gemini":
|
|
197
|
-
// Kimi's stdio entry is `command` + optional `args`/`env`/`cwd` — the same
|
|
198
|
-
// shape, verified against v0.29.2's MCP config schema.
|
|
199
|
-
case "kimi":
|
|
200
|
-
return expectedClaudeShape(entry);
|
|
201
|
-
default:
|
|
202
|
-
return assertNeverHost(host);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
301
|
// Generic writer for hosts whose MCP config is a JSON file with a top-level
|
|
206
302
|
// object keyed by server name (Gemini `mcpServers`, OpenCode/Crush `mcp`).
|
|
207
303
|
// Preserves other top-level keys and other server entries; idempotent; dry-run;
|
|
208
304
|
// transient backup purged on success.
|
|
209
305
|
function writeJsonMcpEntry(host, file, topKey, entry, opts) {
|
|
210
306
|
const data = readJsonFile(file);
|
|
211
|
-
const bag =
|
|
307
|
+
const bag = recordContainer(data, topKey, file) ?? {};
|
|
212
308
|
const existing = bag[entry.name];
|
|
213
|
-
const expected =
|
|
309
|
+
const expected = mcpEntryShapeForHost(host, entry);
|
|
310
|
+
const legacy = opts.replaceLegacy === undefined ? undefined : mcpEntryShapeForHost(host, opts.replaceLegacy);
|
|
214
311
|
if (isDeepStrictEqual(existing, expected)) {
|
|
215
312
|
return resultSkipped(host, file, entry.name);
|
|
216
313
|
}
|
|
314
|
+
// A matching name is not enough to establish ownership. Replacing a server
|
|
315
|
+
// whose shape differs would overwrite somebody else's configuration.
|
|
316
|
+
if (existing !== undefined && !isDeepStrictEqual(existing, legacy)) {
|
|
317
|
+
return resultConflict(host, file, entry.name);
|
|
318
|
+
}
|
|
217
319
|
bag[entry.name] = expected;
|
|
218
320
|
data[topKey] = bag;
|
|
219
321
|
const newJson = `${JSON.stringify(data, null, 2)}\n`;
|
|
220
322
|
if (opts.dryRun) {
|
|
221
323
|
return resultDryRun(host, file, entry.name, [
|
|
222
|
-
`${topKey}.${entry.name}: ${existing ? "
|
|
324
|
+
`${topKey}.${entry.name}: ${existing === undefined ? "add" : "replace known legacy"}`,
|
|
223
325
|
]);
|
|
224
326
|
}
|
|
225
327
|
mkdirSync(dirname(file), { recursive: true });
|
|
@@ -231,11 +333,18 @@ function writeJsonMcpEntry(host, file, topKey, entry, opts) {
|
|
|
231
333
|
}
|
|
232
334
|
function removeJsonMcpEntry(host, file, topKey, entry, opts) {
|
|
233
335
|
const data = readJsonFile(file);
|
|
234
|
-
const bag =
|
|
336
|
+
const bag = recordContainer(data, topKey, file);
|
|
337
|
+
if (bag === undefined)
|
|
338
|
+
return resultSkipped(host, file, entry.name);
|
|
235
339
|
const existing = bag[entry.name];
|
|
340
|
+
const expected = mcpEntryShapeForHost(host, entry);
|
|
341
|
+
const legacy = opts.replaceLegacy === undefined ? undefined : mcpEntryShapeForHost(host, opts.replaceLegacy);
|
|
236
342
|
if (existing === undefined) {
|
|
237
343
|
return resultSkipped(host, file, entry.name);
|
|
238
344
|
}
|
|
345
|
+
if (!isDeepStrictEqual(existing, expected) && !isDeepStrictEqual(existing, legacy)) {
|
|
346
|
+
return resultConflict(host, file, entry.name);
|
|
347
|
+
}
|
|
239
348
|
bag[entry.name] = undefined;
|
|
240
349
|
const remaining = Object.fromEntries(Object.entries(bag).filter(([, v]) => v !== undefined));
|
|
241
350
|
data[topKey] = Object.keys(remaining).length === 0 ? undefined : remaining;
|
|
@@ -250,33 +359,60 @@ function removeJsonMcpEntry(host, file, topKey, entry, opts) {
|
|
|
250
359
|
discardBackup(backup);
|
|
251
360
|
return resultRemoved(host, file, entry.name, null);
|
|
252
361
|
}
|
|
253
|
-
// Claude = the generic JSON writer plus
|
|
254
|
-
//
|
|
255
|
-
//
|
|
362
|
+
// Claude = the generic JSON writer plus an ownership-checked legacy
|
|
363
|
+
// .claude/settings.json sweep. A foreign or malformed legacy entry stops before
|
|
364
|
+
// the primary file changes; an exact own entry is swept on a real write/remove.
|
|
256
365
|
function writeClaudeMcpEntry(entry, scope, opts) {
|
|
366
|
+
const legacy = inspectLegacyClaudeMcpEntry(scope, entry, opts.replaceLegacy);
|
|
367
|
+
if (legacy.state === "conflict")
|
|
368
|
+
return resultConflict("claude", legacy.target, entry.name);
|
|
257
369
|
const res = writeJsonMcpEntry("claude", claudeMcpFile(scope), "mcpServers", entry, opts);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
370
|
+
// The canonical entry may already be present while the old location still
|
|
371
|
+
// needs a cleanup. That is still an observable write: report it as such so
|
|
372
|
+
// a caller's dry-run and first-mutation guard do not mistake it for a noop.
|
|
373
|
+
if (legacy.state === "owned" && res.action === "skipped-idempotent") {
|
|
374
|
+
if (opts.dryRun)
|
|
375
|
+
return resultDryRun("claude", legacy.target, entry.name, [
|
|
376
|
+
`mcpServers.${entry.name}: remove legacy entry`,
|
|
377
|
+
]);
|
|
378
|
+
try {
|
|
379
|
+
cleanupLegacyClaudeMcpEntry(scope, entry, false, opts.replaceLegacy);
|
|
380
|
+
return resultWritten("claude", legacy.target, entry.name, null);
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return partialLegacyCleanup(res, legacy.target);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (legacy.state === "owned" && res.action === "written") {
|
|
387
|
+
try {
|
|
388
|
+
cleanupLegacyClaudeMcpEntry(scope, entry, false, opts.replaceLegacy);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// The primary descriptor is already durable. Do not hide that mutation
|
|
392
|
+
// or roll it back blindly: the old location might have been changed by a
|
|
393
|
+
// host meanwhile. The caller will fail readback, withhold the receipt,
|
|
394
|
+
// and a later explicit setup/migration can retry exact cleanup.
|
|
395
|
+
return partialLegacyCleanup(res, legacy.target);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
262
398
|
return res;
|
|
263
399
|
}
|
|
264
400
|
function writeCodexMcpEntry(entry, scope, opts) {
|
|
265
401
|
const configFile = join(scope.scopeDir, ".codex", "config.toml");
|
|
266
402
|
const oldContent = existsSync(configFile) ? readFileSync(configFile, "utf-8") : "";
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
parsed = oldContent.length > 0 ? parseToml(oldContent) : {};
|
|
270
|
-
}
|
|
271
|
-
catch (err) {
|
|
272
|
-
throw new McpWriterError(`config.toml inválido en ${configFile}`, configFile, err.message);
|
|
273
|
-
}
|
|
274
|
-
const mcpServers = (parsed.mcp_servers ?? {});
|
|
403
|
+
const parsed = parseCodexConfig(oldContent, configFile);
|
|
404
|
+
const mcpServers = recordContainer(parsed, "mcp_servers", configFile) ?? {};
|
|
275
405
|
const existing = mcpServers[entry.name];
|
|
276
|
-
const expected =
|
|
406
|
+
const expected = mcpEntryShapeForHost("codex", entry);
|
|
407
|
+
const legacy = opts.replaceLegacy === undefined
|
|
408
|
+
? undefined
|
|
409
|
+
: mcpEntryShapeForHost("codex", opts.replaceLegacy);
|
|
277
410
|
if (isDeepStrictEqual(existing, expected)) {
|
|
278
411
|
return resultSkipped("codex", configFile, entry.name);
|
|
279
412
|
}
|
|
413
|
+
if (existing !== undefined && !isDeepStrictEqual(existing, legacy)) {
|
|
414
|
+
return resultConflict("codex", configFile, entry.name);
|
|
415
|
+
}
|
|
280
416
|
const cleaned = removeCodexMcpBlocks(oldContent, entry.name);
|
|
281
417
|
const newContent = appendCodexMcpBlocks(cleaned, entry);
|
|
282
418
|
if (newContent === oldContent) {
|
|
@@ -284,8 +420,8 @@ function writeCodexMcpEntry(entry, scope, opts) {
|
|
|
284
420
|
}
|
|
285
421
|
if (opts.dryRun) {
|
|
286
422
|
return resultDryRun("codex", configFile, entry.name, [
|
|
287
|
-
`[mcp_servers.${entry.name}]: ${existing ? "
|
|
288
|
-
`[mcp_servers.${entry.name}.env]:
|
|
423
|
+
`[mcp_servers.${entry.name}]: ${existing === undefined ? "add" : "replace known legacy"}`,
|
|
424
|
+
`[mcp_servers.${entry.name}.env]: add`,
|
|
289
425
|
]);
|
|
290
426
|
}
|
|
291
427
|
mkdirSync(dirname(configFile), { recursive: true });
|
|
@@ -296,23 +432,55 @@ function writeCodexMcpEntry(entry, scope, opts) {
|
|
|
296
432
|
return resultWritten("codex", configFile, entry.name, null);
|
|
297
433
|
}
|
|
298
434
|
function removeClaudeMcpEntry(entry, scope, opts) {
|
|
435
|
+
const legacy = inspectLegacyClaudeMcpEntry(scope, entry, opts.replaceLegacy);
|
|
436
|
+
if (legacy.state === "conflict")
|
|
437
|
+
return resultConflict("claude", legacy.target, entry.name);
|
|
299
438
|
const res = removeJsonMcpEntry("claude", claudeMcpFile(scope), "mcpServers", entry, opts);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (res.action === "
|
|
303
|
-
|
|
439
|
+
// See the matching setup path above: removing an own entry left only in the
|
|
440
|
+
// legacy file is a removal, not an idempotent skip.
|
|
441
|
+
if (legacy.state === "owned" && res.action === "skipped-idempotent") {
|
|
442
|
+
if (opts.dryRun)
|
|
443
|
+
return resultDryRun("claude", legacy.target, entry.name, [
|
|
444
|
+
`mcpServers.${entry.name}: remove legacy entry`,
|
|
445
|
+
]);
|
|
446
|
+
try {
|
|
447
|
+
cleanupLegacyClaudeMcpEntry(scope, entry, false, opts.replaceLegacy);
|
|
448
|
+
return resultRemoved("claude", legacy.target, entry.name, null);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
return partialLegacyCleanup(res, legacy.target);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (legacy.state === "owned" && res.action === "removed") {
|
|
455
|
+
try {
|
|
456
|
+
cleanupLegacyClaudeMcpEntry(scope, entry, false, opts.replaceLegacy);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// As in setup, the primary removal is real but legacy retirement is not
|
|
460
|
+
// complete. Keep the receipt and registry through the caller's partial
|
|
461
|
+
// error path so the next explicit remove can recover safely.
|
|
462
|
+
return partialLegacyCleanup(res, legacy.target);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
304
465
|
return res;
|
|
305
466
|
}
|
|
306
467
|
function removeCodexMcpEntry(entry, scope, opts) {
|
|
307
468
|
const configFile = join(scope.scopeDir, ".codex", "config.toml");
|
|
308
469
|
const oldContent = existsSync(configFile) ? readFileSync(configFile, "utf-8") : "";
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
470
|
+
const parsed = parseCodexConfig(oldContent, configFile);
|
|
471
|
+
const mcpServers = recordContainer(parsed, "mcp_servers", configFile);
|
|
472
|
+
if (mcpServers === undefined)
|
|
473
|
+
return resultSkipped("codex", configFile, entry.name);
|
|
474
|
+
const existing = mcpServers[entry.name];
|
|
475
|
+
if (existing === undefined) {
|
|
476
|
+
return resultSkipped("codex", configFile, entry.name);
|
|
477
|
+
}
|
|
478
|
+
const legacy = opts.replaceLegacy === undefined
|
|
479
|
+
? undefined
|
|
480
|
+
: mcpEntryShapeForHost("codex", opts.replaceLegacy);
|
|
481
|
+
if (!isDeepStrictEqual(existing, mcpEntryShapeForHost("codex", entry)) &&
|
|
482
|
+
!isDeepStrictEqual(existing, legacy)) {
|
|
483
|
+
return resultConflict("codex", configFile, entry.name);
|
|
316
484
|
}
|
|
317
485
|
const newContent = removeCodexMcpBlocks(oldContent, entry.name);
|
|
318
486
|
if (newContent === oldContent) {
|
|
@@ -352,30 +520,42 @@ function readJsonFile(file) {
|
|
|
352
520
|
return {};
|
|
353
521
|
try {
|
|
354
522
|
const parsed = JSON.parse(text);
|
|
355
|
-
if (
|
|
523
|
+
if (isRecord(parsed))
|
|
356
524
|
return parsed;
|
|
357
|
-
}
|
|
358
525
|
throw new Error("contenido no es un objeto JSON");
|
|
359
526
|
}
|
|
360
|
-
catch
|
|
361
|
-
throw new McpWriterError(`JSON inválido en ${file}`, file
|
|
527
|
+
catch {
|
|
528
|
+
throw new McpWriterError(`JSON inválido en ${file}`, file);
|
|
362
529
|
}
|
|
363
530
|
}
|
|
364
|
-
function
|
|
531
|
+
function parseCodexConfig(content, configFile) {
|
|
532
|
+
if (content.trim().length === 0)
|
|
533
|
+
return {};
|
|
534
|
+
try {
|
|
535
|
+
const parsed = parseToml(content);
|
|
536
|
+
if (isRecord(parsed))
|
|
537
|
+
return parsed;
|
|
538
|
+
throw new Error("contenido TOML no es un objeto");
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
throw new McpWriterError(`config.toml inválido en ${configFile}`, configFile);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Missing MCP containers are created only by a successful write. A present
|
|
546
|
+
* scalar, null, or array is malformed user configuration and must never be
|
|
547
|
+
* replaced with an empty object behind their back.
|
|
548
|
+
*/
|
|
549
|
+
function recordContainer(parent, key, file) {
|
|
365
550
|
const current = parent[key];
|
|
366
|
-
if (current
|
|
551
|
+
if (current === undefined)
|
|
552
|
+
return undefined;
|
|
553
|
+
if (isRecord(current))
|
|
367
554
|
return current;
|
|
368
|
-
}
|
|
369
|
-
const fresh = {};
|
|
370
|
-
parent[key] = fresh;
|
|
371
|
-
return fresh;
|
|
555
|
+
throw new McpWriterError(`contenedor '${key}' inválido en ${file}`, file);
|
|
372
556
|
}
|
|
373
|
-
function
|
|
374
|
-
return
|
|
375
|
-
command: entry.command,
|
|
376
|
-
args: [...entry.args],
|
|
377
|
-
env: { ...entry.env },
|
|
378
|
-
};
|
|
557
|
+
function isRecord(value) {
|
|
558
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
379
559
|
}
|
|
380
560
|
// backupFile/purgeStaleBackups/escapeRegex live in multiroot/paths.ts (single
|
|
381
561
|
// backup mechanism for host-config files: keep-latest, purge-then-copy).
|
|
@@ -420,6 +600,8 @@ export function appendCodexMcpBlocks(text, entry) {
|
|
|
420
600
|
buffer.push(`[mcp_servers.${entry.name}]`);
|
|
421
601
|
buffer.push(`command = ${tomlString(entry.command)}`);
|
|
422
602
|
buffer.push(`args = [${entry.args.map(tomlString).join(", ")}]`);
|
|
603
|
+
if (entry.optional)
|
|
604
|
+
buffer.push("required = false");
|
|
423
605
|
buffer.push("");
|
|
424
606
|
buffer.push(`[mcp_servers.${entry.name}.env]`);
|
|
425
607
|
for (const [k, v] of Object.entries(entry.env)) {
|
|
@@ -444,6 +626,19 @@ function resultSkipped(host, target, name) {
|
|
|
444
626
|
function resultDryRun(host, target, name, diff) {
|
|
445
627
|
return { ...action(host, target, name, "dry-run", null), diff };
|
|
446
628
|
}
|
|
629
|
+
function resultConflict(host, target, name) {
|
|
630
|
+
return action(host, target, name, "conflict", null);
|
|
631
|
+
}
|
|
632
|
+
function partialLegacyCleanup(result, target) {
|
|
633
|
+
return {
|
|
634
|
+
...result,
|
|
635
|
+
partial: {
|
|
636
|
+
code: "MCP_LEGACY_CLEANUP_FAILED",
|
|
637
|
+
target,
|
|
638
|
+
message: "No se pudo retirar la ubicación legacy de Claude; no recargues el host y reintentá la operación.",
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
}
|
|
447
642
|
function action(host, target, name, status, backup) {
|
|
448
643
|
return { host, target, name, action: status, backup };
|
|
449
644
|
}
|