@khoavu882/doflow 2.0.0-beta.3 → 2.0.0-beta.5
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/bin/doflow.js +2 -2
- package/core/.antigravity-plugin/plugin.json +0 -1
- package/core/.claude-plugin/plugin.json +1 -1
- package/core/.codex-plugin/plugin.json +1 -1
- package/core/.plugin/plugin.json +1 -1
- package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
- package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
- package/core/harnesses/claude/hooks/session-start.sh +5 -3
- package/core/harnesses/claude/hooks/stop-check.sh +5 -3
- package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
- package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
- package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
- package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
- package/core/harnesses/codex/hooks/session-start.sh +5 -3
- package/core/harnesses/codex/hooks/stop-check.sh +6 -5
- package/core/harnesses/codex/hooks/subagent-audit.sh +17 -6
- package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
- package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-bash-guard.sh +7 -18
- package/core/harnesses/gemini/hooks/pre-implement-gate.sh +7 -19
- package/core/harnesses/gemini/hooks/session-start.sh +5 -3
- package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-bash-guard.sh +20 -5
- package/core/harnesses/kiro/hooks/pre-implement-gate.sh +20 -5
- package/core/harnesses/kiro/hooks/session-start.sh +11 -5
- package/core/harnesses/kiro/hooks/stop-check.sh +21 -30
- package/core/harnesses/pi/extensions/doflow-guard.js +159 -0
- package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -11
- package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
- package/core/harnesses/shared/hooks/bridge/bridge.sh +184 -0
- package/core/harnesses/shared/hooks/bridge/envelope.jq +60 -0
- package/core/harnesses/shared/hooks/bridge/respond.sh +62 -0
- package/core/harnesses/shared/hooks/policies/capture-failure.sh +4 -2
- package/core/harnesses/shared/hooks/policies/contract.sh +75 -0
- package/core/harnesses/shared/hooks/policies/deny-json.sh +39 -4
- package/core/harnesses/shared/hooks/policies/lib.sh +4 -2
- package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +46 -34
- package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +75 -25
- package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +67 -73
- package/core/harnesses/shared/hooks/policies/session-context.sh +37 -7
- package/core/harnesses/shared/hooks/policies/stop-check.sh +42 -15
- package/core/harnesses/shared/hooks/policies/subagent-audit.sh +35 -5
- package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +40 -7
- package/core/harnesses/shared/hooks/stream-hook-runner.js +69 -181
- package/core/registry/assets.json +19 -0
- package/core/registry/contracts.json +2 -2
- package/core/registry/harnesses.json +196 -10
- package/core/registry/lifecycle.json +2626 -7
- package/core/registry/storage.json +110 -83
- package/core/registry/verification.json +1 -1
- package/core/shared/scripts/doflow/bash/render-puml.sh +2 -2
- package/core/shared/scripts/doflow/bin/doflow-run +10 -6
- package/core/shared/skills/do-design/SKILL.md +6 -6
- package/core/shared/skills/do-execute-plan/SKILL.md +11 -10
- package/core/shared/skills/do-execute-plan/references/readiness_gate.md +2 -0
- package/core/shared/skills/do-execute-plan/references/scaffold.md +2 -2
- package/core/shared/skills/do-implement/SKILL.md +3 -2
- package/core/shared/skills/do-plan/SKILL.md +7 -8
- package/package.json +4 -2
- package/src/adapters/antigravity/{index.ts → index.js} +141 -105
- package/src/adapters/claude/{index.ts → index.js} +63 -49
- package/src/adapters/claude/{mcp.ts → mcp.js} +16 -16
- package/src/adapters/codex/{agents.ts → agents.js} +24 -24
- package/src/adapters/codex/{config.ts → config.js} +49 -49
- package/src/adapters/codex/{hooks.ts → hooks.js} +62 -27
- package/src/adapters/codex/{index.ts → index.js} +148 -105
- package/src/adapters/codex/{mcp.ts → mcp.js} +38 -38
- package/src/adapters/codex/{native-projection.ts → native-projection.js} +5 -5
- package/src/adapters/copilot/{index.ts → index.js} +42 -42
- package/src/adapters/{copy-tree.ts → copy-tree.js} +74 -74
- package/src/adapters/gemini/{hooks.ts → hooks.js} +78 -37
- package/src/adapters/gemini/{index.ts → index.js} +96 -57
- package/src/adapters/{hook-commands.ts → hook-commands.js} +8 -8
- package/src/adapters/{index.ts → index.js} +13 -13
- package/src/adapters/kiro/{index.ts → index.js} +49 -35
- package/src/adapters/{mcp-entries.ts → mcp-entries.js} +55 -55
- package/src/adapters/opencode/{index.ts → index.js} +52 -52
- package/src/adapters/pi/hooks.js +119 -0
- package/src/adapters/pi/{index.ts → index.js} +64 -39
- package/src/adapters/pi/{json-members.ts → json-members.js} +23 -23
- package/src/adapters/pi/{mcp.ts → mcp.js} +70 -70
- package/src/adapters/{resolve-source.ts → resolve-source.js} +1 -1
- package/src/cli/commands/{install.ts → install.js} +25 -25
- package/src/cli/commands/{list-backups.ts → list-backups.js} +4 -4
- package/src/cli/commands/{reconcile.ts → reconcile.js} +19 -19
- package/src/cli/commands/{remove.ts → remove.js} +10 -10
- package/src/cli/commands/{rollback.ts → rollback.js} +17 -17
- package/src/cli/commands/{self-update.ts → self-update.js} +3 -3
- package/src/cli/commands/{status.ts → status.js} +24 -24
- package/src/cli/commands/{tools.ts → tools.js} +16 -16
- package/src/cli/commands/{update.ts → update.js} +20 -20
- package/src/cli/{index.ts → index.js} +43 -31
- package/src/cli/{runtime-commands.ts → runtime-commands.js} +97 -58
- package/src/cli/{shared.ts → shared.js} +40 -40
- package/src/helper/{bash-helper.ts → bash-helper.js} +7 -7
- package/src/helper/{git.ts → git.js} +2 -2
- package/src/helper/{guardrails.ts → guardrails.js} +10 -10
- package/src/helper/{harness-paths.ts → harness-paths.js} +24 -24
- package/src/helper/{marker-merge.ts → marker-merge.js} +4 -4
- package/src/helper/{prompt.ts → prompt.js} +5 -5
- package/src/helper/{repo-root.ts → repo-root.js} +2 -2
- package/src/helper/{settings-merge.ts → settings-merge.js} +27 -27
- package/src/helper/{settings-scope.ts → settings-scope.js} +3 -3
- package/src/helper/{toml.ts → toml.js} +48 -48
- package/src/install/{backup-set.ts → backup-set.js} +16 -16
- package/src/install/{backup.ts → backup.js} +65 -65
- package/src/install/{context.ts → context.js} +15 -15
- package/src/install/{manifest.ts → manifest.js} +27 -27
- package/src/install/{mcp.ts → mcp.js} +30 -30
- package/src/install/{paths.ts → paths.js} +15 -15
- package/src/install/{targets.ts → targets.js} +10 -10
- package/src/install/{tool-lifecycle.ts → tool-lifecycle.js} +20 -20
- package/src/lifecycle/{bash-availability.ts → bash-availability.js} +1 -1
- package/src/lifecycle/coverage.js +57 -0
- package/src/lifecycle/hook-step.js +61 -0
- package/src/lifecycle/{index.ts → index.js} +160 -146
- package/src/lifecycle/{mcp-index.ts → mcp-index.js} +1 -1
- package/src/lifecycle/{policies.ts → policies.js} +11 -11
- package/src/lifecycle/{view.ts → view.js} +28 -28
- package/src/registry/{index.ts → index.js} +195 -66
- package/src/release/build-package.js +91 -0
- package/src/release/changelog-section.js +70 -0
- package/src/release/{sync-plugin-versions.ts → sync-plugin-versions.js} +4 -4
- package/src/runtime/advice/{cli.ts → cli.js} +31 -31
- package/src/runtime/advice/{client.ts → client.js} +44 -44
- package/src/runtime/advice/{config.ts → config.js} +16 -16
- package/src/runtime/advice/{doctor.ts → doctor.js} +32 -32
- package/src/runtime/advice/{endpoint-guard.ts → endpoint-guard.js} +20 -20
- package/src/runtime/advice/{kinds.ts → kinds.js} +16 -16
- package/src/runtime/advice/providers/{ollama-systemone.ts → ollama-systemone.js} +21 -21
- package/src/runtime/advice/{record.ts → record.js} +19 -19
- package/src/runtime/advice/{registry.ts → registry.js} +16 -16
- package/src/runtime/advice/{strength.ts → strength.js} +7 -7
- package/src/runtime/{brief-tools.ts → brief-tools.js} +50 -50
- package/src/runtime/{c4-project.ts → c4-project.js} +27 -27
- package/src/runtime/candidate.js +505 -0
- package/src/runtime/{capability-router.ts → capability-router.js} +39 -39
- package/src/runtime/{checkouts.ts → checkouts.js} +21 -21
- package/src/runtime/{claims.ts → claims.js} +44 -35
- package/src/runtime/{cli-result.ts → cli-result.js} +15 -8
- package/src/runtime/{cli.ts → cli.js} +59 -292
- package/src/runtime/{command-detect.ts → command-detect.js} +47 -47
- package/src/runtime/context/budget.js +203 -0
- package/src/runtime/context/compose.js +1367 -0
- package/src/runtime/context/delivery-store.js +102 -0
- package/src/runtime/context/mandatory.js +594 -0
- package/src/runtime/context/pointer.js +142 -0
- package/src/runtime/context-pack.js +1277 -0
- package/src/runtime/{decision-register.ts → decision-register.js} +67 -67
- package/src/runtime/evidence-fields.js +39 -0
- package/src/runtime/{evidence-ledger.ts → evidence-ledger.js} +32 -25
- package/src/runtime/failure/{capture.ts → capture.js} +29 -29
- package/src/runtime/failure/{classifier.ts → classifier.js} +4 -4
- package/src/runtime/failure/{cli.ts → cli.js} +30 -30
- package/src/runtime/failure/{home.ts → home.js} +6 -6
- package/src/runtime/failure/{store.ts → store.js} +32 -32
- package/src/runtime/{feature-resolve.ts → feature-resolve.js} +6 -6
- package/src/runtime/{freshness.ts → freshness.js} +59 -22
- package/src/runtime/{health.ts → health.js} +72 -72
- package/src/runtime/{history-compactor.ts → history-compactor.js} +25 -25
- package/src/runtime/{implementation-gate.ts → implementation-gate.js} +30 -30
- package/src/runtime/inventory/{coverage.ts → coverage.js} +10 -10
- package/src/runtime/inventory/{identity.ts → identity.js} +2 -2
- package/src/runtime/inventory/{index.ts → index.js} +36 -36
- package/src/runtime/inventory/{precedence.ts → precedence.js} +2 -2
- package/src/runtime/inventory/{read-scopes.ts → read-scopes.js} +9 -9
- package/src/runtime/inventory/{siblings.ts → siblings.js} +15 -15
- package/src/runtime/knowledge/{chunker.ts → chunker.js} +12 -12
- package/src/runtime/knowledge/{hybrid.ts → hybrid.js} +17 -17
- package/src/runtime/knowledge/{index-store.ts → index-store.js} +23 -23
- package/src/runtime/knowledge/{retrieval.ts → retrieval.js} +27 -27
- package/src/runtime/{leak-scan.ts → leak-scan.js} +12 -12
- package/src/runtime/lifecycle/{cli.ts → cli.js} +32 -32
- package/src/runtime/lifecycle/{event-store.ts → event-store.js} +48 -48
- package/src/runtime/lifecycle/{fold.ts → fold.js} +41 -41
- package/src/runtime/lifecycle/{followup.ts → followup.js} +54 -54
- package/src/runtime/lifecycle/{goal.ts → goal.js} +50 -50
- package/src/runtime/lifecycle/{intent-writer.ts → intent-writer.js} +5 -5
- package/src/runtime/lifecycle/{overview.ts → overview.js} +47 -47
- package/src/runtime/lifecycle/{release.ts → release.js} +29 -29
- package/src/runtime/lifecycle/{report-store.ts → report-store.js} +25 -25
- package/src/runtime/lifecycle/{retention.ts → retention.js} +37 -37
- package/src/runtime/lifecycle/{root.ts → root.js} +9 -9
- package/src/runtime/lifecycle/{status.ts → status.js} +64 -64
- package/src/runtime/lifecycle/{store-upkeep.ts → store-upkeep.js} +46 -46
- package/src/runtime/{locator-resolve.ts → locator-resolve.js} +7 -7
- package/src/runtime/{mask.ts → mask.js} +22 -22
- package/src/runtime/{mcp-consultations.ts → mcp-consultations.js} +54 -54
- package/src/runtime/{mcp-reach.ts → mcp-reach.js} +24 -24
- package/src/runtime/{model-router.ts → model-router.js} +20 -20
- package/src/runtime/{outcome.ts → outcome.js} +27 -27
- package/src/runtime/{reach.ts → reach.js} +21 -21
- package/src/runtime/{readiness-record.ts → readiness-record.js} +12 -12
- package/src/runtime/{readiness.ts → readiness.js} +113 -43
- package/src/runtime/{recovery.ts → recovery.js} +15 -15
- package/src/runtime/{research-request.ts → research-request.js} +34 -34
- package/src/runtime/{retrieval-plan.ts → retrieval-plan.js} +453 -104
- package/src/runtime/retrieval-record.js +223 -0
- package/src/runtime/scaffold/{artifacts.ts → artifacts.js} +22 -19
- package/src/runtime/scaffold/{fingerprint.ts → fingerprint.js} +3 -3
- package/src/runtime/scaffold/{generate.ts → generate.js} +109 -109
- package/src/runtime/scaffold/{languages.ts → languages.js} +34 -34
- package/src/runtime/source-version.js +289 -0
- package/src/runtime/storage/{cli.ts → cli.js} +9 -9
- package/src/runtime/storage/{engine.ts → engine.js} +37 -37
- package/src/runtime/storage/{registry.ts → registry.js} +19 -19
- package/src/runtime/storage/{roots.ts → roots.js} +8 -8
- package/src/runtime/storage/{rules.ts → rules.js} +26 -26
- package/src/runtime/storage/{upkeep.ts → upkeep.js} +35 -35
- package/src/runtime/storage/{work-status.ts → work-status.js} +39 -39
- package/src/runtime/{task-attempts.ts → task-attempts.js} +62 -62
- package/src/runtime/{task-classifier.ts → task-classifier.js} +34 -34
- package/src/runtime/{task-scope.ts → task-scope.js} +40 -15
- package/src/runtime/{task-state.ts → task-state.js} +86 -21
- package/src/runtime/trace/{indicators.ts → indicators.js} +22 -22
- package/src/runtime/trace/{ledger.ts → ledger.js} +42 -42
- package/src/runtime/trace/{render.ts → render.js} +7 -7
- package/src/runtime/trace/{views.ts → views.js} +40 -40
- package/src/runtime/verification/{contract-runner.ts → contract-runner.js} +23 -23
- package/src/runtime/verification/{engine.ts → engine.js} +143 -143
- package/src/runtime/verification/{registry.ts → registry.js} +6 -6
- package/src/runtime/verification/{scope-bound.ts → scope-bound.js} +50 -50
- package/src/runtime/verification/{test-selection.ts → test-selection.js} +28 -28
- package/src/runtime/{workflow-engine.ts → workflow-engine.js} +50 -50
- package/src/runtime/{workflow-orchestrator.ts → workflow-orchestrator.js} +91 -91
- package/src/runtime/{worktree.ts → worktree.js} +31 -31
- package/src/state/{index.ts → index.js} +26 -26
- package/src/state/{lockfile.ts → lockfile.js} +20 -20
- package/src/state/{run-lock.ts → run-lock.js} +44 -44
- package/core/.antigravity-plugin/hooks.json +0 -39
- package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
- package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
- package/core/harnesses/shared/hooks/hooks.json +0 -47
- package/src/runtime/context-pack.ts +0 -492
|
@@ -13,9 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// MCP servers are merged per server into Pi's mcp.json (.pi/mcp.json project; ~/.pi/agent/mcp.json
|
|
15
15
|
// or $PI_CODING_AGENT_DIR/mcp.json user) by ./mcp.ts; Pi reads it through its built-in MCP from
|
|
16
|
-
// 0.99.0.
|
|
17
|
-
// executing with full system permissions, which is a different trust model from the shell scripts
|
|
18
|
-
// DoFlow ships.
|
|
16
|
+
// 0.99.0. The user- and project-scope lifecycle extension is ledger-owned and resolves its shared bridge at install time.
|
|
19
17
|
//
|
|
20
18
|
// Evidence: https://pi.dev/docs/latest/settings, https://pi.dev/docs/latest/skills,
|
|
21
19
|
// https://pi.dev/docs/latest/quickstart, https://pi.dev/docs/latest/extensions,
|
|
@@ -23,13 +21,15 @@
|
|
|
23
21
|
const fs = require('node:fs');
|
|
24
22
|
const path = require('node:path');
|
|
25
23
|
|
|
26
|
-
const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.
|
|
27
|
-
const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, sourceDirFor } = require('../copy-tree.
|
|
28
|
-
const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.
|
|
29
|
-
const { mcpRows, discoverPiMcp, ownedPiMcpIds, planPiMcp, applyPiMcp, verifyPiMcp } = require('./mcp.
|
|
24
|
+
const { MARKER_START, MARKER_END } = require('../../helper/marker-merge.js');
|
|
25
|
+
const { planTree, applyTree, removeTree, verifyTree, copyTreeAssets, copyTreeDestDir, sharedTreeDestDir, ledgerFileResources, ledgerSiblingFingerprints, siblingReplacedNotices, fingerprint, sourceDirFor } = require('../copy-tree.js');
|
|
26
|
+
const { declaredHarnessPaths, resolveHarnessPaths } = require('../../helper/harness-paths.js');
|
|
27
|
+
const { mcpRows, discoverPiMcp, ownedPiMcpIds, planPiMcp, applyPiMcp, verifyPiMcp } = require('./mcp.js');
|
|
28
|
+
const { planPiHooks, verifyPiHooks, removePiHooks } = require('./hooks.js');
|
|
30
29
|
|
|
31
30
|
const HARNESS = 'pi';
|
|
32
|
-
const
|
|
31
|
+
const AGENT_DIR_MCP_NOTICE = 'PI_CODING_AGENT_DIR is set: Pi reads its whole agent dir from it, but DoFlow moves only mcp.json there; skills and AGENTS.md stay in ~/.pi/agent.';
|
|
32
|
+
const AGENT_DIR_NOTICE = 'PI_CODING_AGENT_DIR is set: Pi reads its whole agent dir from it, and DoFlow moves mcp.json and the guard extension there; skills and AGENTS.md stay in ~/.pi/agent.';
|
|
33
33
|
|
|
34
34
|
// Native path facts live in core/registry/harnesses.json under this harness's "paths" section and
|
|
35
35
|
// resolve through the shared harness-paths resolver; nothing below hardcodes them.
|
|
@@ -40,11 +40,11 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
40
40
|
// Parity note: Pi's historical nativePaths() derived even its user-scope root from scopeRoot
|
|
41
41
|
// (the CLI passes $HOME there for -g installs), never from a separate homeDir argument — so the
|
|
42
42
|
// resolver is fed no homeDir and "root" keeps resolving exactly as before.
|
|
43
|
-
function nativePaths({ scope, scopeRoot }
|
|
43
|
+
function nativePaths({ scope, scopeRoot } = {}) {
|
|
44
44
|
const paths = resolveHarnessPaths(declaredPaths, { scope, scopeRoot });
|
|
45
45
|
// Pi's user-scope mcp.json follows $PI_CODING_AGENT_DIR when set. It is not declared in the
|
|
46
|
-
// registry because the path schema has no environment base (src/helper/harness-paths.js);
|
|
47
|
-
//
|
|
46
|
+
// registry because the path schema has no environment base (src/helper/harness-paths.js); the
|
|
47
|
+
// guard extension follows it too, while skills and AGENTS.md keep their declared locations.
|
|
48
48
|
if ((scope === 'user' || scope === 'global') && agentDirOverride()) paths.mcp = path.join(path.resolve(agentDirOverride()), 'mcp.json');
|
|
49
49
|
return paths;
|
|
50
50
|
}
|
|
@@ -54,7 +54,7 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
54
54
|
return typeof agentDir === 'string' && agentDir.trim() !== '' ? agentDir.trim() : null;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function discover({ scope, scopeRoot, mcp = [], ledger, context = {}, fsImpl = fs }
|
|
57
|
+
function discover({ scope, scopeRoot, mcp = [], ledger, context = {}, fsImpl = fs } ) {
|
|
58
58
|
const paths = nativePaths({ scope, scopeRoot });
|
|
59
59
|
const instruction = fsImpl.existsSync(paths.instruction) ? fsImpl.readFileSync(paths.instruction, 'utf8') : null;
|
|
60
60
|
const rows = mcpRows(ledger);
|
|
@@ -62,18 +62,18 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
62
62
|
// Pi reads mcp.json only for a selection or a row; the names in it are read here regardless.
|
|
63
63
|
const file = snapshot[paths.mcp] ?? discoverPiMcp({ selected: [true], rows: [], file: paths.mcp, fsImpl })[paths.mcp];
|
|
64
64
|
const members = file?.exists && file.doc?.ok ? file.doc.servers?.members ?? [] : null;
|
|
65
|
-
const mcpConfigured = members && members.map((member
|
|
65
|
+
const mcpConfigured = members && members.map((member ) => member.key).sort();
|
|
66
66
|
return { paths, instruction, mcp: snapshot, mcpOwned: ownedPiMcpIds({ rows, snapshot }), mcpConfigured };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function render({ content = '' }
|
|
69
|
+
function render({ content = '' } = {}) {
|
|
70
70
|
return `${MARKER_START}\n${String(content).trimEnd()}\n${MARKER_END}\n`;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/** Replace only the span between DoFlow's markers. An AGENTS.md with no DoFlow section is refused
|
|
74
74
|
* rather than appended to — Pi shares AGENTS.md with every other agent that reads it, so where
|
|
75
75
|
* DoFlow's content belongs is its owner's decision. Add an empty marker pair to opt in. */
|
|
76
|
-
function managedInstruction(existing
|
|
76
|
+
function managedInstruction(existing , rendered ) {
|
|
77
77
|
if (existing === null) return { ok: true, operation: 'create', content: rendered };
|
|
78
78
|
const start = existing.indexOf(MARKER_START);
|
|
79
79
|
const end = existing.indexOf(MARKER_END);
|
|
@@ -83,7 +83,7 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
83
83
|
return { ok: true, operation: content === existing ? 'none' : 'merge', content };
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function strippedInstruction(existing
|
|
86
|
+
function strippedInstruction(existing ) {
|
|
87
87
|
if (typeof existing !== 'string') return null;
|
|
88
88
|
const start = existing.indexOf(MARKER_START);
|
|
89
89
|
const end = existing.indexOf(MARKER_END);
|
|
@@ -95,7 +95,7 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
95
95
|
|
|
96
96
|
// ---- copy-tree assets (skills) ----
|
|
97
97
|
|
|
98
|
-
function planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl = fs }
|
|
98
|
+
function planCopyTreeAssets({ assets, scope, scopeRoot, context, ledger, removing, fsImpl = fs } ) {
|
|
99
99
|
const paths = nativePaths({ scope, scopeRoot });
|
|
100
100
|
const changes = [];
|
|
101
101
|
const conflicts = [];
|
|
@@ -112,10 +112,10 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
112
112
|
// is never deleted on removal, forced or not.
|
|
113
113
|
force: !removing && context?.force === true, });
|
|
114
114
|
treeResults.push(result);
|
|
115
|
-
conflicts.push(...result.conflicts.map((reason
|
|
115
|
+
conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
|
|
116
116
|
// A hand-edited file at a path DoFlow no longer writes is the user's: it stays, only its
|
|
117
117
|
// ownership row is released.
|
|
118
|
-
notices.push(...result.kept.map((item
|
|
118
|
+
notices.push(...result.kept.map((item ) => `kept hand-edited ${path.relative(paths.root, item.target)}; DoFlow no longer manages it`));
|
|
119
119
|
for (const change of result.changes) {
|
|
120
120
|
changes.push({
|
|
121
121
|
assetId: asset.id, target: change.target, source: change.source, operation: change.operation,
|
|
@@ -130,19 +130,19 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
130
130
|
return { changes, conflicts, notices: [...siblingReplacedNotices(treeResults), ...notices] };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
function applyCopyTreeAssets(changes
|
|
134
|
-
const treeChanges = changes.filter((change
|
|
135
|
-
.map((change
|
|
133
|
+
function applyCopyTreeAssets(changes , { fsImpl = fs } = {}) {
|
|
134
|
+
const treeChanges = changes.filter((change ) => change.projection?.renderer === 'copy-tree' && change.operation !== 'remove')
|
|
135
|
+
.map((change ) => ({ relPath: change.identity, target: change.target, source: change.source, operation: change.operation, fingerprint: change.fingerprint }));
|
|
136
136
|
return applyTree({ changes: treeChanges, fsImpl }).applied;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
function removeCopyTreeAssets(changes
|
|
140
|
-
const treeChanges = changes.filter((change
|
|
141
|
-
.map((change
|
|
139
|
+
function removeCopyTreeAssets(changes , { fsImpl = fs } = {}) {
|
|
140
|
+
const treeChanges = changes.filter((change ) => change.kind === 'copy-tree-file' && change.operation === 'remove' && !change.retained)
|
|
141
|
+
.map((change ) => ({ relPath: change.identity, target: change.target, operation: 'remove', fingerprint: change.fingerprint, pruneGuard: change.pruneGuard }));
|
|
142
142
|
return removeTree({ changes: treeChanges, fsImpl }).removed;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl = fs }
|
|
145
|
+
function verifyCopyTreeAssets({ assets, scope, scopeRoot, context, fsImpl = fs } ) {
|
|
146
146
|
const paths = nativePaths({ scope, scopeRoot });
|
|
147
147
|
const statuses = [];
|
|
148
148
|
const resources = [];
|
|
@@ -151,7 +151,7 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
151
151
|
const destDir = sharedTreeDestDir(paths.root, asset.nativeDir) ?? copyTreeDestDir(paths.configDir, asset);
|
|
152
152
|
const sourceDir = sourceDirFor(asset, context, fsImpl, 'Pi');
|
|
153
153
|
const result = verifyTree({ sourceDir, destDir, fsImpl, layout: asset.layout, plannedWrites: context?.plannedWrites });
|
|
154
|
-
conflicts.push(...result.conflicts.map((reason
|
|
154
|
+
conflicts.push(...result.conflicts.map((reason ) => `${asset.id}: ${reason}`));
|
|
155
155
|
for (const resource of result.resources) {
|
|
156
156
|
resources.push({
|
|
157
157
|
assetId: asset.id, target: resource.target, ownershipIdentity: `doflow:pi:copy-tree:${asset.id}:${resource.relPath}`,
|
|
@@ -167,13 +167,13 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
167
167
|
|
|
168
168
|
// ---- shared adapter contract ----
|
|
169
169
|
|
|
170
|
-
function plan({ scope, scopeRoot, assets = [], mcp = [], discovery, context = {}, ledger, fsImpl = fs }
|
|
170
|
+
function plan({ scope, scopeRoot, assets = [], mcp = [], discovery, context = {}, ledger, fsImpl = fs } ) {
|
|
171
171
|
const found = discovery || discover({ scope, scopeRoot, mcp, ledger, context, fsImpl });
|
|
172
172
|
const changes = [];
|
|
173
173
|
const conflicts = [];
|
|
174
174
|
const removing = context.operation === 'remove';
|
|
175
175
|
|
|
176
|
-
const guidance = assets.find((asset
|
|
176
|
+
const guidance = assets.find((asset ) => asset.capability === 'instructions');
|
|
177
177
|
if (guidance) {
|
|
178
178
|
const rendered = render({ content: fsImpl.readFileSync(path.resolve(context.repoRoot, guidance.source), 'utf8') });
|
|
179
179
|
const outcome = removing
|
|
@@ -191,23 +191,40 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
191
191
|
changes.push(...copyTree.changes);
|
|
192
192
|
conflicts.push(...copyTree.conflicts);
|
|
193
193
|
|
|
194
|
+
const piHooks = planPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
|
|
195
|
+
changes.push(...piHooks.changes);
|
|
196
|
+
conflicts.push(...piHooks.conflicts);
|
|
197
|
+
|
|
194
198
|
const piMcp = planPiMcp({ selected: mcp, rows: mcpRows(ledger), file: found.paths.mcp, scope, removing, snapshot: found.mcp, assets });
|
|
195
199
|
changes.push(...piMcp.changes);
|
|
196
200
|
conflicts.push(...piMcp.conflicts);
|
|
197
201
|
|
|
198
|
-
const notices = [...copyTree.notices, ...piMcp.notices];
|
|
199
|
-
if (!removing && scope !== 'project' && agentDirOverride())
|
|
202
|
+
const notices = [...copyTree.notices, ...piMcp.notices, ...piHooks.notices];
|
|
203
|
+
if (!removing && scope !== 'project' && agentDirOverride()) {
|
|
204
|
+
notices.push(assets.some((asset ) => asset.id === 'pi.guard-extension') ? AGENT_DIR_NOTICE : AGENT_DIR_MCP_NOTICE);
|
|
205
|
+
}
|
|
200
206
|
return { changes, conflicts, notices, paths: found.paths };
|
|
201
207
|
}
|
|
202
208
|
|
|
203
|
-
|
|
209
|
+
/** The hook front door (IC-037): the guard extension. */
|
|
210
|
+
function planHookDoors({ scope, scopeRoot, assets = [], context = {}, ledger, fsImpl = fs } ) {
|
|
211
|
+
const door = planPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
|
|
212
|
+
return { changes: door.changes, conflicts: door.conflicts, notices: door.notices };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function verifyHookDoors({ scope, scopeRoot, assets = [], ledger, context = {}, fsImpl = fs } ) {
|
|
216
|
+
const door = verifyPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
|
|
217
|
+
return { ok: door.conflicts.length === 0 && !door.statuses.some((s ) => s.status === 'invalid' || s.status === 'conflict'), ...door };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function writeChange(change , fsImpl ) {
|
|
204
221
|
fsImpl.mkdirSync(path.dirname(change.target), { recursive: true });
|
|
205
222
|
fsImpl.writeFileSync(change.target, change.content, 'utf8');
|
|
206
223
|
}
|
|
207
224
|
|
|
208
|
-
function isPiMcp(change
|
|
225
|
+
function isPiMcp(change ) { return change.projection?.renderer === 'pi-mcp'; }
|
|
209
226
|
|
|
210
|
-
function apply({ changes = [], fsImpl = fs }
|
|
227
|
+
function apply({ changes = [], fsImpl = fs } ) {
|
|
211
228
|
// MCP first: its base-hash check refuses a stale plan before any other Pi file is written.
|
|
212
229
|
let applied = applyPiMcp(changes.filter(isPiMcp), { fsImpl }).applied;
|
|
213
230
|
for (const change of changes) {
|
|
@@ -222,24 +239,26 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
222
239
|
return { applied };
|
|
223
240
|
}
|
|
224
241
|
|
|
225
|
-
function remove({ changes = [], fsImpl = fs }
|
|
242
|
+
function remove({ changes = [], fsImpl = fs } ) {
|
|
226
243
|
let removed = applyPiMcp(changes.filter(isPiMcp), { fsImpl }).applied;
|
|
227
244
|
for (const change of changes) {
|
|
228
245
|
if (change.operation !== 'remove' || change.projection?.renderer === 'copy-tree' || isPiMcp(change)) continue;
|
|
246
|
+
if (change.projection?.renderer === 'pi-guard-extension') continue;
|
|
229
247
|
if (change.content === null) continue;
|
|
230
248
|
writeChange(change, fsImpl);
|
|
231
249
|
removed += 1;
|
|
232
250
|
}
|
|
251
|
+
removed += removePiHooks(changes, { fsImpl });
|
|
233
252
|
removed += removeCopyTreeAssets(changes, { fsImpl });
|
|
234
253
|
return { removed };
|
|
235
254
|
}
|
|
236
255
|
|
|
237
|
-
function verify({ scope, scopeRoot, assets = [], mcp = [], discovery, ledger, context = {}, fsImpl = fs }
|
|
256
|
+
function verify({ scope, scopeRoot, assets = [], mcp = [], discovery, ledger, context = {}, fsImpl = fs } ) {
|
|
238
257
|
const found = discover({ scope, scopeRoot, mcp, ledger, context, fsImpl });
|
|
239
258
|
const statuses = [];
|
|
240
259
|
const resources = [];
|
|
241
260
|
const conflicts = [];
|
|
242
|
-
const instructionsAssetId = assets.find((asset
|
|
261
|
+
const instructionsAssetId = assets.find((asset ) => asset.capability === 'instructions')?.id ?? assets[0]?.id;
|
|
243
262
|
|
|
244
263
|
const hasSection = typeof found.instruction === 'string'
|
|
245
264
|
&& found.instruction.includes(MARKER_START) && found.instruction.includes(MARKER_END);
|
|
@@ -263,10 +282,15 @@ function createPiAdapter({ declaredPaths = declaredHarnessPaths()[HARNESS], env
|
|
|
263
282
|
resources.push(...piMcp.resources);
|
|
264
283
|
conflicts.push(...piMcp.conflicts);
|
|
265
284
|
|
|
285
|
+
const piHooks = verifyPiHooks({ scope, scopeRoot, assets, ledger, context, adopt: context?.adopt === true, env, fsImpl });
|
|
286
|
+
statuses.push(...piHooks.statuses);
|
|
287
|
+
resources.push(...piHooks.resources);
|
|
288
|
+
conflicts.push(...piHooks.conflicts);
|
|
289
|
+
|
|
266
290
|
return { ok: conflicts.length === 0 && !statuses.some((s) => s.status === 'invalid' || s.status === 'conflict'), resources, statuses, conflicts };
|
|
267
291
|
}
|
|
268
292
|
|
|
269
|
-
return { nativePaths, discover, render, plan, apply, remove, verify };
|
|
293
|
+
return { nativePaths, discover, render, plan, apply, remove, verify, planHookDoors, verifyHookDoors };
|
|
270
294
|
}
|
|
271
295
|
|
|
272
296
|
const singleton = createPiAdapter();
|
|
@@ -274,6 +298,7 @@ const singleton = createPiAdapter();
|
|
|
274
298
|
module.exports = {
|
|
275
299
|
HARNESS, nativePaths: singleton.nativePaths, discover: singleton.discover, render: singleton.render,
|
|
276
300
|
plan: singleton.plan, apply: singleton.apply, remove: singleton.remove, verify: singleton.verify,
|
|
277
|
-
|
|
301
|
+
planHookDoors: singleton.planHookDoors, verifyHookDoors: singleton.verifyHookDoors,
|
|
302
|
+
managedInstruction: (singleton ).managedInstruction, strippedInstruction: (singleton ).strippedInstruction,
|
|
278
303
|
createPiAdapter,
|
|
279
304
|
};
|
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
const SKELETON = '{\n "mcpServers": {}\n}\n';
|
|
11
11
|
const WHITESPACE = ' \t\n\r';
|
|
12
12
|
|
|
13
|
-
function isPlainObject(value
|
|
13
|
+
function isPlainObject(value ) {
|
|
14
14
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function skipWhitespace(text
|
|
17
|
+
function skipWhitespace(text , i ) {
|
|
18
18
|
while (i < text.length && WHITESPACE.includes(text[i])) i += 1;
|
|
19
19
|
return i;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** Index just past the string token that opens at `i`. */
|
|
23
|
-
function stringEnd(text
|
|
23
|
+
function stringEnd(text , i ) {
|
|
24
24
|
for (i += 1; text[i] !== '"'; i += 1) if (text[i] === '\\') i += 1;
|
|
25
25
|
return i + 1;
|
|
26
26
|
}
|
|
@@ -28,7 +28,7 @@ function stringEnd(text: string, i: number): number {
|
|
|
28
28
|
// The text has already passed JSON.parse, so the scanner only has to find spans, never validate.
|
|
29
29
|
// Only the top-level object and mcpServers need member spans; every other value is skipped by
|
|
30
30
|
// counting brackets, without recursion, so a deeply nested file cannot exhaust the stack.
|
|
31
|
-
function valueEnd(text
|
|
31
|
+
function valueEnd(text , i ) {
|
|
32
32
|
const ch = text[i];
|
|
33
33
|
if (ch === '"') return stringEnd(text, i);
|
|
34
34
|
if (ch !== '{' && ch !== '[') {
|
|
@@ -47,8 +47,8 @@ function valueEnd(text: string, i: number): number {
|
|
|
47
47
|
|
|
48
48
|
/** Member spans of the object opening at `open`; `nested` names the one member whose object value
|
|
49
49
|
* is scanned for its own members too. */
|
|
50
|
-
function scanObject(text
|
|
51
|
-
const members
|
|
50
|
+
function scanObject(text , open , nested = null) {
|
|
51
|
+
const members = [];
|
|
52
52
|
let i = skipWhitespace(text, open + 1);
|
|
53
53
|
while (text[i] !== '}') {
|
|
54
54
|
const keyStart = i;
|
|
@@ -66,7 +66,7 @@ function scanObject(text: string, open: number, nested: string | null = null): {
|
|
|
66
66
|
|
|
67
67
|
/** JSON.parse's message quotes the text around the error, which may be a token or a password, so
|
|
68
68
|
* only the position is reported. */
|
|
69
|
-
function invalidJsonReason(text
|
|
69
|
+
function invalidJsonReason(text , error ) {
|
|
70
70
|
const match = /at position (\d+)/.exec(error.message);
|
|
71
71
|
if (!match) return 'invalid JSON';
|
|
72
72
|
const before = text.slice(0, Number(match[1]));
|
|
@@ -75,14 +75,14 @@ function invalidJsonReason(text: string, error: any): string {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/** The whitespace that starts the line `at` sits on, or null when something else precedes it there. */
|
|
78
|
-
function lineIndent(text
|
|
78
|
+
function lineIndent(text , at ) {
|
|
79
79
|
const prefix = text.slice(text.lastIndexOf('\n', at - 1) + 1, at);
|
|
80
80
|
return /^[ \t]*$/.test(prefix) ? prefix : null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/** The line ending most of the lines in `[start, end)` use, or `fallback` when the span has none:
|
|
84
84
|
* one stray CRLF in an LF file does not turn every inserted line into CRLF. */
|
|
85
|
-
function lineEnding(text
|
|
85
|
+
function lineEnding(text , start , end , fallback ) {
|
|
86
86
|
const span = text.slice(start, end);
|
|
87
87
|
const lines = span.split('\n').length - 1;
|
|
88
88
|
if (!lines) return fallback;
|
|
@@ -91,7 +91,7 @@ function lineEnding(text: string, start: number, end: number, fallback: string):
|
|
|
91
91
|
|
|
92
92
|
/** Attach the formatting a container's new members follow: whether it spans lines, its line ending,
|
|
93
93
|
* the indent of its members and the indent of its closing brace. An empty container takes its parent's. */
|
|
94
|
-
function attachLayout(text
|
|
94
|
+
function attachLayout(text , node , parent , unit , eol ) {
|
|
95
95
|
const parentIndent = parent ? parent.indent : '';
|
|
96
96
|
const empty = node.members.length === 0;
|
|
97
97
|
node.multiline = text.slice(node.open, node.close).includes('\n') || (empty && Boolean(parent?.multiline));
|
|
@@ -100,8 +100,8 @@ function attachLayout(text: string, node: any, parent: any, unit: string, eol: s
|
|
|
100
100
|
node.closeIndent = parentIndent;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function duplicateKey(node
|
|
104
|
-
const seen = new Set
|
|
103
|
+
function duplicateKey(node ) {
|
|
104
|
+
const seen = new Set ();
|
|
105
105
|
for (const member of node.members) {
|
|
106
106
|
if (seen.has(member.key)) return member.key;
|
|
107
107
|
seen.add(member.key);
|
|
@@ -109,9 +109,9 @@ function duplicateKey(node: any): string | null {
|
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function readDocument(text
|
|
112
|
+
function readDocument(text ) {
|
|
113
113
|
let parsed;
|
|
114
|
-
try { parsed = JSON.parse(text); } catch (error
|
|
114
|
+
try { parsed = JSON.parse(text); } catch (error ) { return { ok: false, reason: invalidJsonReason(text, error) }; }
|
|
115
115
|
if (!isPlainObject(parsed)) return { ok: false, reason: 'top level is not an object' };
|
|
116
116
|
if (Object.prototype.hasOwnProperty.call(parsed, 'mcpServers') && !isPlainObject(parsed.mcpServers)) {
|
|
117
117
|
return { ok: false, reason: 'mcpServers is not an object' };
|
|
@@ -120,7 +120,7 @@ function readDocument(text: string): any {
|
|
|
120
120
|
try { root = scanObject(text, skipWhitespace(text, 0), 'mcpServers'); } catch { return { ok: false, reason: 'the file could not be scanned' }; }
|
|
121
121
|
const duplicateTop = duplicateKey(root);
|
|
122
122
|
if (duplicateTop !== null) return { ok: false, reason: `duplicate key '${duplicateTop}'` };
|
|
123
|
-
const servers = root.members.find((member
|
|
123
|
+
const servers = root.members.find((member ) => member.key === 'mcpServers')?.node ?? null;
|
|
124
124
|
const duplicateServer = servers ? duplicateKey(servers) : null;
|
|
125
125
|
if (duplicateServer !== null) return { ok: false, reason: `duplicate key '${duplicateServer}'` };
|
|
126
126
|
|
|
@@ -132,21 +132,21 @@ function readDocument(text: string): any {
|
|
|
132
132
|
return { ok: true, root, servers, eol, unit };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
function renderValue(value
|
|
135
|
+
function renderValue(value , container , layout ) {
|
|
136
136
|
if (!container.multiline) return JSON.stringify(value);
|
|
137
137
|
return JSON.stringify(value, null, layout.unit).split('\n').join(container.eol + container.indent);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
function renderMember(key
|
|
140
|
+
function renderMember(key , value , container , layout ) {
|
|
141
141
|
return `${JSON.stringify(key)}: ${renderValue(value, container, layout)}`;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
function splice(text
|
|
144
|
+
function splice(text , start , end , inserted ) {
|
|
145
145
|
return text.slice(0, start) + inserted + text.slice(end);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/** `layout` is the readDocument result the container or member came from. */
|
|
149
|
-
function insertMember(text
|
|
149
|
+
function insertMember(text , container , key , value , layout ) {
|
|
150
150
|
const member = renderMember(key, value, container, layout);
|
|
151
151
|
const last = container.members[container.members.length - 1];
|
|
152
152
|
if (last) {
|
|
@@ -159,12 +159,12 @@ function insertMember(text: string, container: any, key: string, value: any, lay
|
|
|
159
159
|
return splice(text, container.open + 1, container.close, inserted);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
function replaceValue(text
|
|
162
|
+
function replaceValue(text , member , value , layout ) {
|
|
163
163
|
const container = layout.servers?.members.includes(member) ? layout.servers : layout.root;
|
|
164
164
|
return splice(text, member.valueStart, member.valueEnd, renderValue(value, container, layout));
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
function removeMember(text
|
|
167
|
+
function removeMember(text , container , member ) {
|
|
168
168
|
const index = container.members.indexOf(member);
|
|
169
169
|
if (index === -1) throw new Error(`member '${member.key}' is not in this container`);
|
|
170
170
|
if (container.members.length === 1) return splice(text, container.open + 1, container.close, '');
|
|
@@ -173,12 +173,12 @@ function removeMember(text: string, container: any, member: any): string {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
/** Set the text between an empty container's braces, which a removal of its only member drops. */
|
|
176
|
-
function replaceInner(text
|
|
176
|
+
function replaceInner(text , container , inner ) {
|
|
177
177
|
if (container.members.length) throw new Error('only an empty container\'s inner text can be replaced');
|
|
178
178
|
return splice(text, container.open + 1, container.close, inner);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
function renderNewDocument(entries
|
|
181
|
+
function renderNewDocument(entries ) {
|
|
182
182
|
return `${JSON.stringify({ mcpServers: Object.fromEntries(entries) }, null, 2)}\n`;
|
|
183
183
|
}
|
|
184
184
|
|