dsh-desktop-kit 2.2.1 → 2.7.3

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 (130) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +114 -27
  3. package/client.js +3270 -32
  4. package/docs/AGENT-HARNESS.md +144 -0
  5. package/docs/ARCHITECTURE.md +38 -7
  6. package/docs/COMPATIBILITY.md +4 -2
  7. package/docs/DEVELOPMENT.md +56 -0
  8. package/docs/INSTALL.md +20 -11
  9. package/docs/LAPTOP-INSTALL.md +96 -0
  10. package/docs/ROADMAP-3.0.md +353 -0
  11. package/docs/ROADMAP.md +1 -1
  12. package/docs/SUBAGENTS.md +25 -0
  13. package/docs/TEST-0.2.2.md +5 -5
  14. package/docs/TEST-2.3.0.md +49 -0
  15. package/docs/TEST-2.7.2.md +46 -0
  16. package/docs/TEST-2.7.3.md +20 -0
  17. package/docs/TOKENS.md +2 -2
  18. package/docs/TROUBLESHOOTING.md +27 -0
  19. package/docs/UI-QA.md +28 -0
  20. package/docs/images/peak-toast-peak.png +0 -0
  21. package/docs/images/peak-toast-soon.png +0 -0
  22. package/docs/preview/peak-toast.html +155 -0
  23. package/index.js +18 -1
  24. package/package.json +26 -6
  25. package/plugins/dsh-agent-extensions/README.md +107 -0
  26. package/plugins/dsh-agent-extensions/client.js +157 -0
  27. package/plugins/dsh-agent-extensions/index.js +174 -0
  28. package/plugins/dsh-agent-extensions/lib/agents.js +76 -0
  29. package/plugins/dsh-agent-extensions/lib/api.js +53 -0
  30. package/plugins/dsh-agent-extensions/lib/checkpoints.js +103 -0
  31. package/plugins/dsh-agent-extensions/lib/commands.js +255 -0
  32. package/plugins/dsh-agent-extensions/lib/hooks.js +182 -0
  33. package/plugins/dsh-agent-extensions/lib/policy.js +84 -0
  34. package/plugins/dsh-agent-extensions/lib/project.js +144 -0
  35. package/plugins/dsh-agent-extensions/lib/scaffold.js +151 -0
  36. package/plugins/dsh-agent-extensions/package.json +20 -0
  37. package/plugins/dsh-agent-extensions/screenshots/agents-1440.png +0 -0
  38. package/plugins/dsh-agent-extensions/screenshots/agents-390.png +0 -0
  39. package/plugins/dsh-browser-qa/README.md +221 -0
  40. package/plugins/dsh-browser-qa/cordis.patch.yml +4 -0
  41. package/plugins/dsh-browser-qa/index.js +165 -0
  42. package/plugins/dsh-browser-qa/lib/cdp.js +224 -0
  43. package/plugins/dsh-browser-qa/lib/chrome.js +466 -0
  44. package/plugins/dsh-browser-qa/lib/pages.js +552 -0
  45. package/plugins/dsh-browser-qa/lib/qa.js +827 -0
  46. package/plugins/dsh-browser-qa/lib/ui-gate.js +256 -0
  47. package/plugins/dsh-browser-qa/package.json +31 -0
  48. package/plugins/dsh-checkpoints/README.md +22 -0
  49. package/plugins/dsh-checkpoints/cordis.patch.yml +3 -0
  50. package/plugins/dsh-checkpoints/index.js +80 -0
  51. package/plugins/dsh-checkpoints/lib/checkpoints.js +111 -0
  52. package/plugins/dsh-checkpoints/package.json +25 -0
  53. package/plugins/dsh-github-review/README.md +131 -0
  54. package/plugins/dsh-github-review/cordis.patch.yml +3 -0
  55. package/plugins/dsh-github-review/index.js +536 -0
  56. package/plugins/dsh-github-review/lib/gh.js +494 -0
  57. package/plugins/dsh-github-review/lib/normalize.js +662 -0
  58. package/plugins/dsh-github-review/package.json +28 -0
  59. package/plugins/dsh-guardrails/README.md +264 -0
  60. package/plugins/dsh-guardrails/cordis.patch.yml +5 -0
  61. package/plugins/dsh-guardrails/index.js +389 -0
  62. package/plugins/dsh-guardrails/lib/hooks.js +262 -0
  63. package/plugins/dsh-guardrails/lib/modes.js +77 -0
  64. package/plugins/dsh-guardrails/lib/policy.js +592 -0
  65. package/plugins/dsh-guardrails/lib/scan.js +451 -0
  66. package/plugins/dsh-guardrails/lib/secrets.js +274 -0
  67. package/plugins/dsh-guardrails/package.json +26 -0
  68. package/plugins/dsh-mcp-manager/README.md +78 -0
  69. package/plugins/dsh-mcp-manager/client.js +373 -0
  70. package/plugins/dsh-mcp-manager/index.js +159 -0
  71. package/plugins/dsh-mcp-manager/lib/api.js +250 -0
  72. package/plugins/dsh-mcp-manager/lib/client.js +219 -0
  73. package/plugins/dsh-mcp-manager/lib/config.js +217 -0
  74. package/plugins/dsh-mcp-manager/lib/manager.js +146 -0
  75. package/plugins/dsh-mcp-manager/package.json +18 -0
  76. package/plugins/dsh-mcp-manager/screenshots/modal-390.png +0 -0
  77. package/plugins/dsh-mcp-manager/screenshots/settings-1440.png +0 -0
  78. package/plugins/dsh-mcp-manager/screenshots/settings-390.png +0 -0
  79. package/plugins/dsh-memory-bridge/README.md +161 -0
  80. package/plugins/dsh-memory-bridge/client.js +2264 -0
  81. package/plugins/dsh-memory-bridge/cordis.patch.yml +5 -0
  82. package/plugins/dsh-memory-bridge/docs/images/conflicts.png +0 -0
  83. package/plugins/dsh-memory-bridge/docs/images/hero.png +0 -0
  84. package/plugins/dsh-memory-bridge/docs/images/settings-dark.png +0 -0
  85. package/plugins/dsh-memory-bridge/docs/images/settings-light.png +0 -0
  86. package/plugins/dsh-memory-bridge/docs/images/settings-mobile.png +0 -0
  87. package/plugins/dsh-memory-bridge/docs/images/setup.png +0 -0
  88. package/plugins/dsh-memory-bridge/icon.svg +13 -0
  89. package/plugins/dsh-memory-bridge/index.js +378 -0
  90. package/plugins/dsh-memory-bridge/lib/bridge.js +636 -0
  91. package/plugins/dsh-memory-bridge/lib/github.js +473 -0
  92. package/plugins/dsh-memory-bridge/lib/merge.js +205 -0
  93. package/plugins/dsh-memory-bridge/lib/portable.js +314 -0
  94. package/plugins/dsh-memory-bridge/lib/state.js +565 -0
  95. package/plugins/dsh-memory-bridge/lib/sync.js +984 -0
  96. package/plugins/dsh-memory-bridge/locale/en.json +6 -0
  97. package/plugins/dsh-memory-bridge/locale/ru.json +6 -0
  98. package/plugins/dsh-memory-bridge/package.json +67 -0
  99. package/plugins/dsh-peak-guard/README.md +16 -3
  100. package/plugins/dsh-peak-guard/client.js +223 -8
  101. package/plugins/dsh-peak-guard/package.json +1 -1
  102. package/plugins/dsh-plan-mode/README.md +24 -0
  103. package/plugins/dsh-plan-mode/cordis.patch.yml +3 -0
  104. package/plugins/dsh-plan-mode/index.js +103 -0
  105. package/plugins/dsh-plan-mode/lib/plan.js +89 -0
  106. package/plugins/dsh-plan-mode/package.json +18 -0
  107. package/plugins/dsh-profile-transfer/client.js +277 -17
  108. package/plugins/dsh-profile-transfer/index.js +10 -0
  109. package/plugins/dsh-profile-transfer/package.json +3 -1
  110. package/plugins/dsh-profile-transfer/transfer.js +101 -2
  111. package/plugins/dsh-project-memory/index.js +92 -2
  112. package/plugins/dsh-project-memory/package.json +1 -1
  113. package/plugins/dsh-repo-map/README.md +26 -0
  114. package/plugins/dsh-repo-map/cordis.patch.yml +3 -0
  115. package/plugins/dsh-repo-map/index.js +99 -0
  116. package/plugins/dsh-repo-map/lib/repo-map.js +170 -0
  117. package/plugins/dsh-repo-map/package.json +25 -0
  118. package/plugins/dsh-skill-autopilot/index.js +135 -11
  119. package/skills/delivery-gate/SKILL.md +31 -0
  120. package/skills/git-and-ci/SKILL.md +5 -1
  121. package/skills/manifest.json +31 -6
  122. package/skills/requirements-and-planning/SKILL.md +3 -0
  123. package/skills/research-first/SKILL.md +35 -0
  124. package/skills/research-first/references/query-patterns.md +20 -0
  125. package/skills/subagent-playbook/SKILL.md +36 -0
  126. package/skills/subagent-playbook/references/prompts.md +39 -0
  127. package/skills/ui-craft/SKILL.md +43 -0
  128. package/skills/visual-qa/SKILL.md +29 -0
  129. package/skills/visual-qa/references/commands.md +32 -0
  130. package/skills-baseline.json +219 -0
@@ -0,0 +1,373 @@
1
+ /**
2
+ * dsh-mcp-manager — browser half.
3
+ *
4
+ * Adds a premium Settings section for MCP connections: live status, tools,
5
+ * trusted/enabled switches, one-click presets and a manual server form.
6
+ * Secrets are posted once and stored by the host in ~/.dsh/mcp.secrets.json.
7
+ */
8
+ window.__ModuleLoader__.load({
9
+ id: "dsh-mcp-manager",
10
+ factory: (require) => {
11
+ var module = { exports: {} };
12
+ var exports = module.exports;
13
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
14
+ const React = require("react");
15
+ const h = React.createElement;
16
+ const useEffect = React.useEffect;
17
+ const useState = React.useState;
18
+
19
+ const RPC_NS = "dsh-mcp-manager";
20
+
21
+ const CSS = `
22
+ .mcp-root{--mcp-line:var(--dsw-alias-border-l1,rgba(20,40,36,.13));--mcp-soft:var(--dsw-alias-bg-layer-1,#f4f7f5);--mcp-panel:var(--dsw-alias-bg-layer-1,#fff);--mcp-text:var(--dsw-alias-label-primary,#17231f);--mcp-muted:var(--dsw-alias-label-secondary,#61706b);--mcp-accent:var(--dsw-alias-brand-primary,#0f766e);--mcp-accent-soft:color-mix(in srgb,var(--mcp-accent) 9%,transparent);--mcp-warn:#a16207;display:flex;flex-direction:column;gap:20px;max-width:1040px;min-width:0;color:var(--mcp-text)}
23
+ .mcp-head{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;flex-wrap:wrap}
24
+ .mcp-eyebrow{font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--mcp-accent);margin:0 0 6px}
25
+ .mcp-title{font-size:clamp(24px,2.6vw,32px);line-height:1.12;letter-spacing:-.025em;margin:0;font-weight:680}
26
+ .mcp-sub{margin:8px 0 0;max-width:64ch;color:var(--mcp-muted);font-size:14px;line-height:1.5}
27
+ .mcp-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
28
+ .mcp-btn{appearance:none;border:1px solid var(--mcp-line);background:var(--mcp-panel);color:var(--mcp-text);border-radius:10px;padding:9px 13px;font:inherit;font-size:13px;font-weight:600;line-height:1;cursor:pointer;display:inline-flex;align-items:center;gap:7px;min-height:38px;transition:border-color .16s ease,background .16s ease,transform .16s ease}
29
+ .mcp-btn:hover{border-color:rgba(15,118,110,.4);background:var(--mcp-accent-soft)}
30
+ .mcp-btn:active{transform:translateY(1px)}
31
+ .mcp-btn:focus-visible{outline:2px solid var(--mcp-accent);outline-offset:2px}
32
+ .mcp-btn.primary{background:var(--mcp-accent);border-color:var(--mcp-accent);color:#fff}
33
+ .mcp-btn.primary:hover{filter:brightness(1.06);background:var(--mcp-accent)}
34
+ .mcp-btn.danger{color:#9f1239;border-color:rgba(159,18,57,.22)}
35
+ .mcp-btn.ghost{background:transparent}
36
+ .mcp-btn[disabled]{opacity:.5;cursor:not-allowed}
37
+ .mcp-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
38
+ .mcp-stat{background:linear-gradient(150deg,var(--mcp-panel),var(--mcp-soft));border:1px solid var(--mcp-line);border-radius:14px;padding:14px 16px;min-width:0}
39
+ .mcp-stat b{display:block;font-size:24px;letter-spacing:-.03em;line-height:1.1;font-variant-numeric:tabular-nums}
40
+ .mcp-stat span{display:block;margin-top:5px;color:var(--mcp-muted);font-size:11px;font-weight:650;letter-spacing:.08em;text-transform:uppercase}
41
+ .mcp-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
42
+ .mcp-card{border:1px solid var(--mcp-line);background:var(--mcp-panel);border-radius:16px;padding:16px;min-width:0;display:flex;flex-direction:column;gap:12px;box-shadow:0 1px 0 rgba(20,40,36,.03)}
43
+ .mcp-card-top{display:flex;align-items:flex-start;gap:10px;justify-content:space-between;min-width:0}
44
+ .mcp-card-id{display:flex;align-items:center;gap:9px;min-width:0}
45
+ .mcp-dot{width:8px;height:8px;border-radius:50%;background:#94a3b8;flex:0 0 auto;box-shadow:0 0 0 3px rgba(148,163,184,.16)}
46
+ .mcp-dot.on{background:#0f9f6e;box-shadow:0 0 0 3px rgba(15,159,110,.14)}
47
+ .mcp-dot.err{background:#dc2626;box-shadow:0 0 0 3px rgba(220,38,38,.12)}
48
+ .mcp-name{font-size:15px;font-weight:680;letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
49
+ .mcp-badge{font-size:10px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--mcp-muted);border:1px solid var(--mcp-line);border-radius:999px;padding:3px 7px;white-space:nowrap}
50
+ .mcp-meta{display:flex;gap:10px;flex-wrap:wrap;color:var(--mcp-muted);font-size:12px;line-height:1.45}
51
+ .mcp-meta code{font:inherit;color:var(--mcp-text);background:var(--mcp-soft);border-radius:5px;padding:1px 5px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
52
+ .mcp-error{color:#9f1239;background:rgba(159,18,57,.06);border:1px solid rgba(159,18,57,.16);border-radius:9px;padding:8px 10px;font-size:12px;line-height:1.4;word-break:break-word}
53
+ .mcp-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap;border-top:1px solid var(--mcp-line);padding-top:10px;margin-top:auto}
54
+ .mcp-switch{display:inline-flex;align-items:center;gap:7px;font-size:12px;color:var(--mcp-muted);cursor:pointer;min-height:32px}
55
+ .mcp-switch input{position:absolute;opacity:0;width:1px;height:1px}
56
+ .mcp-switch i{width:30px;height:18px;border-radius:999px;background:#cbd5e1;position:relative;transition:background .16s ease;flex:0 0 auto}
57
+ .mcp-switch i:after{content:"";position:absolute;width:14px;height:14px;left:2px;top:2px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:transform .16s ease}
58
+ .mcp-switch input:checked+i{background:var(--mcp-accent)}
59
+ .mcp-switch input:checked+i:after{transform:translateX(12px)}
60
+ .mcp-switch input:focus-visible+i{outline:2px solid var(--mcp-accent);outline-offset:2px}
61
+ .mcp-actions{display:flex;gap:6px;margin-left:auto;flex-wrap:wrap}
62
+ .mcp-actions .mcp-btn{padding:7px 9px;min-height:32px;font-size:12px}
63
+ .mcp-tools{border-top:1px dashed var(--mcp-line);padding-top:10px;display:flex;flex-direction:column;gap:6px}
64
+ .mcp-tool{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:1.45;min-width:0}
65
+ .mcp-tool code{color:var(--mcp-accent);font:inherit;font-weight:650;white-space:nowrap}
66
+ .mcp-tool span{color:var(--mcp-muted);overflow:hidden;text-overflow:ellipsis}
67
+ .mcp-empty{border:1px dashed var(--mcp-line);border-radius:18px;padding:26px;background:linear-gradient(160deg,var(--mcp-soft),transparent 75%);display:flex;flex-direction:column;gap:12px;align-items:flex-start}
68
+ .mcp-empty h3{margin:0;font-size:18px;letter-spacing:-.015em}
69
+ .mcp-empty p{margin:0;color:var(--mcp-muted);font-size:13px;line-height:1.5;max-width:60ch}
70
+ .mcp-presets{display:flex;gap:8px;flex-wrap:wrap}
71
+ .mcp-preset{border:1px solid var(--mcp-line);background:var(--mcp-panel);border-radius:12px;padding:10px 12px;cursor:pointer;text-align:left;min-width:150px;font:inherit;color:inherit}
72
+ .mcp-preset:hover{border-color:rgba(15,118,110,.4);background:var(--mcp-accent-soft)}
73
+ .mcp-preset b{display:block;font-size:13px}
74
+ .mcp-preset small{display:block;color:var(--mcp-muted);font-size:11px;line-height:1.35;margin-top:3px}
75
+ .mcp-note{color:var(--mcp-muted);font-size:11px;line-height:1.45;margin:0}
76
+ .mcp-loading{color:var(--mcp-muted);font-size:13px;padding:18px 0}
77
+ .mcp-overlay{position:fixed;inset:0;background:rgba(10,20,18,.42);backdrop-filter:blur(4px);z-index:80;display:flex;align-items:center;justify-content:center;padding:20px}
78
+ .mcp-modal{width:min(640px,100%);max-height:min(86vh,820px);overflow:auto;background:var(--mcp-panel);border:1px solid var(--mcp-line);border-radius:20px;box-shadow:0 24px 80px rgba(10,20,18,.24);padding:22px;display:flex;flex-direction:column;gap:16px}
79
+ .mcp-modal-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}
80
+ .mcp-modal h2{margin:0;font-size:20px;letter-spacing:-.02em}
81
+ .mcp-modal p{margin:4px 0 0;color:var(--mcp-muted);font-size:13px;line-height:1.45}
82
+ .mcp-form{display:grid;grid-template-columns:1fr 1fr;gap:12px}
83
+ .mcp-field{display:flex;flex-direction:column;gap:5px;min-width:0}
84
+ .mcp-field.wide{grid-column:1/-1}
85
+ .mcp-field label{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--mcp-muted)}
86
+ .mcp-field input,.mcp-field select,.mcp-field textarea{width:100%;box-sizing:border-box;border:1px solid var(--mcp-line);border-radius:9px;background:var(--mcp-panel);color:var(--mcp-text);font:inherit;font-size:13px;padding:9px 10px;min-height:38px}
87
+ .mcp-field textarea{min-height:74px;resize:vertical;line-height:1.45}
88
+ .mcp-field input:focus,.mcp-field select:focus,.mcp-field textarea:focus{outline:2px solid var(--mcp-accent);outline-offset:1px;border-color:transparent}
89
+ .mcp-check{display:flex;gap:8px;align-items:flex-start;font-size:12px;color:var(--mcp-muted);line-height:1.4}
90
+ .mcp-check input{margin-top:2px}
91
+ .mcp-form-actions{display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;border-top:1px solid var(--mcp-line);padding-top:14px}
92
+ .mcp-toast{font-size:12px;border-radius:9px;padding:8px 10px;background:var(--mcp-accent-soft);color:var(--mcp-accent);border:1px solid rgba(15,118,110,.18)}
93
+ @media (max-width:860px){.mcp-grid{grid-template-columns:1fr}.mcp-stats{grid-template-columns:1fr 1fr}.mcp-form{grid-template-columns:1fr}.mcp-field.wide{grid-column:auto}}
94
+ @media (max-width:520px){.mcp-stats{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.mcp-stat{padding:10px 12px}.mcp-stat b{font-size:20px}.mcp-stat span{font-size:9px;letter-spacing:.04em}.mcp-head{align-items:flex-start}.mcp-actions{margin-left:0;width:100%}.mcp-root{gap:16px}.mcp-modal{padding:18px;border-radius:16px}.mcp-overlay{padding:10px}}
95
+ @media (prefers-reduced-motion:reduce){.mcp-btn,.mcp-switch i,.mcp-switch i:after{transition:none}}
96
+ `;
97
+
98
+ const PRESETS = [
99
+ { id: "github", title: "GitHub", subtitle: "Issues, PR и код", transport: "stdio", command: "npx", args: "-y\n@modelcontextprotocol/server-github", tokenEnv: "GITHUB_TOKEN", tokenLabel: "GitHub token", trusted: true },
100
+ { id: "postgres", title: "PostgreSQL", subtitle: "Схема и read-only запросы", transport: "stdio", command: "npx", args: "-y\n@modelcontextprotocol/server-postgres\npostgresql://readonly@localhost:5432/postgres", tokenEnv: "PGPASSWORD", tokenLabel: "Пароль read-only роли", trusted: true },
101
+ { id: "filesystem", title: "Filesystem", subtitle: "Папка проекта, без токена", transport: "stdio", command: "npx", args: "-y\n@modelcontextprotocol/server-filesystem\n.", trusted: true },
102
+ ];
103
+
104
+ function call(ctx, operation, payload, timeout = 60000) {
105
+ let signal;
106
+ try { signal = AbortSignal.timeout(timeout); } catch { signal = undefined; }
107
+ return ctx.connection.rpc.call("/api", `${RPC_NS}/${operation}`, payload ?? {}, signal).then((result) => {
108
+ if (!result || result.ok !== true) throw new Error(result?.error?.message || "Ошибка RPC");
109
+ return result.value;
110
+ });
111
+ }
112
+
113
+ function Toggle({ checked, onChange, label, disabled }) {
114
+ return h("label", { className: "mcp-switch" },
115
+ h("input", { type: "checkbox", checked, disabled, onChange: (event) => onChange(event.target.checked) }),
116
+ h("i", { "aria-hidden": "true" }),
117
+ label,
118
+ );
119
+ }
120
+
121
+ function ServerCard({ server, busy, onToggle, onTest, onTools, onRemove, tools }) {
122
+ const [open, setOpen] = useState(false);
123
+ const connected = server.connected;
124
+ const status = connected ? "on" : (server.error ? "err" : "off");
125
+ return h("article", { className: "mcp-card" },
126
+ h("div", { className: "mcp-card-top" },
127
+ h("div", { className: "mcp-card-id" },
128
+ h("span", { className: `mcp-dot ${status}`, "aria-hidden": "true" }),
129
+ h("span", { className: "mcp-name", title: server.id }, server.id),
130
+ h("span", { className: "mcp-badge" }, server.scope === "project" ? "проект" : "глобально"),
131
+ ),
132
+ h("span", { className: "mcp-badge" }, server.transport === "http" ? "HTTP" : "stdio"),
133
+ ),
134
+ h("div", { className: "mcp-meta" },
135
+ h("span", null, connected ? `подключён · ${server.tools} tools` : server.error ? "ошибка" : "не запущен"),
136
+ server.url ? h("code", { title: server.url }, server.url) : h("code", { title: server.command }, server.command || "—"),
137
+ ),
138
+ server.error ? h("div", { className: "mcp-error" }, server.error) : null,
139
+ h("div", { className: "mcp-controls" },
140
+ h(Toggle, { checked: server.enabled, label: "Включён", disabled: busy, onChange: (value) => onToggle(server.id, { enabled: value }) }),
141
+ h(Toggle, { checked: server.trusted, label: "Доверен", disabled: busy, onChange: (value) => onToggle(server.id, { trusted: value }) }),
142
+ h("div", { className: "mcp-actions" },
143
+ h("button", { className: "mcp-btn ghost", disabled: busy, onClick: () => onTest(server.id) }, "Проверить"),
144
+ h("button", { className: "mcp-btn ghost", disabled: busy, onClick: () => { setOpen(!open); if (!open) onTools(server.id); } }, open ? "Скрыть" : "Tools"),
145
+ h("button", { className: "mcp-btn danger ghost", disabled: busy, onClick: () => onRemove(server.id) }, "Удалить"),
146
+ ),
147
+ ),
148
+ open ? h("div", { className: "mcp-tools" },
149
+ tools === undefined ? h("div", { className: "mcp-meta" }, "Загрузка…")
150
+ : tools.length === 0 ? h("div", { className: "mcp-meta" }, "Нет tools.")
151
+ : tools.map((tool) => h("div", { className: "mcp-tool", key: tool.name },
152
+ h("code", null, tool.name),
153
+ h("span", { title: tool.description }, tool.description || "без описания"),
154
+ )),
155
+ ) : null,
156
+ );
157
+ }
158
+
159
+ function ServerForm({ initial, busy, onCancel, onSave }) {
160
+ const [form, setForm] = useState(initial);
161
+ const set = (patch) => setForm((current) => ({ ...current, ...patch }));
162
+ const submit = (event) => {
163
+ event.preventDefault();
164
+ onSave({
165
+ ...form,
166
+ args: String(form.args ?? "").split(/\r?\n/u).map((line) => line.trim()).filter(Boolean),
167
+ });
168
+ };
169
+ return h("div", { className: "mcp-overlay", onMouseDown: (event) => { if (event.target === event.currentTarget) onCancel(); } },
170
+ h("form", { className: "mcp-modal", role: "dialog", "aria-modal": "true", "aria-label": "Добавить MCP-сервер", onSubmit: submit },
171
+ h("div", { className: "mcp-modal-head" },
172
+ h("div", null,
173
+ h("h2", null, initial.edit ? "Настроить сервер" : "Новый MCP-сервер"),
174
+ h("p", null, "Один раз добавьте подключение — дальше агент сам увидит его инструменты."),
175
+ ),
176
+ h("button", { type: "button", className: "mcp-btn ghost", onClick: onCancel, "aria-label": "Закрыть" }, "Закрыть"),
177
+ ),
178
+ h("div", { className: "mcp-form" },
179
+ h("div", { className: "mcp-field" },
180
+ h("label", { htmlFor: "mcp-id" }, "ID"),
181
+ h("input", { id: "mcp-id", value: form.id, disabled: initial.edit, placeholder: "github", onChange: (event) => set({ id: event.target.value.toLowerCase().replace(/[^a-z0-9_-]/gu, "-") }) }),
182
+ ),
183
+ h("div", { className: "mcp-field" },
184
+ h("label", { htmlFor: "mcp-transport" }, "Транспорт"),
185
+ h("select", { id: "mcp-transport", value: form.transport, onChange: (event) => set({ transport: event.target.value }) },
186
+ h("option", { value: "stdio" }, "stdio — локальная команда"),
187
+ h("option", { value: "http" }, "HTTP — удалённый сервер"),
188
+ ),
189
+ ),
190
+ form.transport === "stdio" ? h(React.Fragment, null,
191
+ h("div", { className: "mcp-field wide" },
192
+ h("label", { htmlFor: "mcp-command" }, "Команда"),
193
+ h("input", { id: "mcp-command", value: form.command, placeholder: "npx", onChange: (event) => set({ command: event.target.value }) }),
194
+ ),
195
+ h("div", { className: "mcp-field wide" },
196
+ h("label", { htmlFor: "mcp-args" }, "Аргументы — по одному в строке"),
197
+ h("textarea", { id: "mcp-args", value: form.args, placeholder: "-y\n@modelcontextprotocol/server-github", onChange: (event) => set({ args: event.target.value }) }),
198
+ ),
199
+ h("div", { className: "mcp-field" },
200
+ h("label", { htmlFor: "mcp-token-env" }, "Переменная токена"),
201
+ h("input", { id: "mcp-token-env", value: form.tokenEnv, placeholder: "GITHUB_TOKEN", onChange: (event) => set({ tokenEnv: event.target.value }) }),
202
+ ),
203
+ ) : h(React.Fragment, null,
204
+ h("div", { className: "mcp-field wide" },
205
+ h("label", { htmlFor: "mcp-url" }, "URL"),
206
+ h("input", { id: "mcp-url", value: form.url, placeholder: "https://example.com/mcp", onChange: (event) => set({ url: event.target.value }) }),
207
+ ),
208
+ h("div", { className: "mcp-field" },
209
+ h("label", { htmlFor: "mcp-token-header" }, "Заголовок токена"),
210
+ h("input", { id: "mcp-token-header", value: form.tokenHeader, placeholder: "Authorization", onChange: (event) => set({ tokenHeader: event.target.value }) }),
211
+ ),
212
+ ),
213
+ h("div", { className: "mcp-field wide" },
214
+ h("label", { htmlFor: "mcp-token" }, form.tokenLabel || "Токен / пароль"),
215
+ h("input", { id: "mcp-token", type: "password", value: form.token, placeholder: form.tokenPlaceholder || "оставьте пустым, если не нужен", autoComplete: "off", onChange: (event) => set({ token: event.target.value }) }),
216
+ ),
217
+ h("label", { className: "mcp-check wide" },
218
+ h("input", { type: "checkbox", checked: form.trusted, onChange: (event) => set({ trusted: event.target.checked }) }),
219
+ h("span", null, "Я доверяю этому серверу. Он сможет выполнять команды и читать данные, к которым у него есть доступ."),
220
+ ),
221
+ ),
222
+ h("p", { className: "mcp-note" }, "Секрет сохраняется в ~/.dsh/mcp.secrets.json с правами 0600 и не попадает в mcp.json или лог."),
223
+ h("div", { className: "mcp-form-actions" },
224
+ h("button", { type: "button", className: "mcp-btn", onClick: onCancel }, "Отмена"),
225
+ h("button", { type: "submit", className: "mcp-btn primary", disabled: busy }, busy ? "Сохраняем…" : "Сохранить и подключить"),
226
+ ),
227
+ ),
228
+ );
229
+ }
230
+
231
+ function McpSection() {
232
+ const [data, setData] = useState(null);
233
+ const [error, setError] = useState(null);
234
+ const [busy, setBusy] = useState(false);
235
+ const [form, setForm] = useState(null);
236
+ const [tools, setTools] = useState({});
237
+ const [toast, setToast] = useState("");
238
+
239
+ const load = async () => {
240
+ try {
241
+ const value = await call(window.__DSH_MCP_CTX__, "status");
242
+ setData(value);
243
+ setError(null);
244
+ } catch (cause) {
245
+ setError(cause instanceof Error ? cause.message : String(cause));
246
+ }
247
+ };
248
+
249
+ useEffect(() => { void load(); }, []);
250
+
251
+ const run = async (operation, payload, message) => {
252
+ setBusy(true);
253
+ setError(null);
254
+ try {
255
+ await call(window.__DSH_MCP_CTX__, operation, payload);
256
+ await load();
257
+ if (message) { setToast(message); setTimeout(() => setToast(""), 3000); }
258
+ } catch (cause) {
259
+ setError(cause instanceof Error ? cause.message : String(cause));
260
+ } finally {
261
+ setBusy(false);
262
+ }
263
+ };
264
+
265
+ const presets = PRESETS;
266
+ const openPreset = (preset) => setForm({
267
+ edit: false,
268
+ id: preset.id,
269
+ transport: preset.transport,
270
+ command: preset.command || "",
271
+ args: preset.args || "",
272
+ url: "",
273
+ tokenEnv: preset.tokenEnv || "",
274
+ tokenHeader: "Authorization",
275
+ tokenLabel: preset.tokenLabel || "Токен",
276
+ tokenPlaceholder: preset.tokenPlaceholder || "",
277
+ token: "",
278
+ trusted: preset.trusted === true,
279
+ });
280
+ const openManual = () => setForm({ edit: false, id: "", transport: "stdio", command: "npx", args: "-y\n", url: "", tokenEnv: "MCP_TOKEN", tokenHeader: "Authorization", tokenLabel: "Токен", tokenPlaceholder: "", token: "", trusted: false });
281
+
282
+ const servers = data?.servers ?? [];
283
+ const connected = servers.filter((server) => server.connected).length;
284
+ const totalTools = servers.reduce((sum, server) => sum + (Number(server.tools) || 0), 0);
285
+
286
+ return h("section", { className: "mcp-root", "aria-label": "MCP-подключения" },
287
+ h("div", { className: "mcp-head" },
288
+ h("div", null,
289
+ h("p", { className: "mcp-eyebrow" }, "MCP · Model Context Protocol"),
290
+ h("h2", { className: "mcp-title" }, "Подключения"),
291
+ h("p", { className: "mcp-sub" }, "Внешние системы для агента: GitHub, PostgreSQL, файлы и любые MCP-серверы. Добавьте один раз — инструменты появятся в новом чате."),
292
+ ),
293
+ h("div", { className: "mcp-toolbar" },
294
+ h("button", { className: "mcp-btn primary", onClick: openManual, disabled: busy }, "Добавить сервер"),
295
+ ),
296
+ ),
297
+ error ? h("div", { className: "mcp-error", role: "alert" }, error) : null,
298
+ toast ? h("div", { className: "mcp-toast", role: "status" }, toast) : null,
299
+ h("div", { className: "mcp-stats" },
300
+ h("div", { className: "mcp-stat" }, h("b", null, servers.length), h("span", null, "серверов")),
301
+ h("div", { className: "mcp-stat" }, h("b", null, connected), h("span", null, "подключено")),
302
+ h("div", { className: "mcp-stat" }, h("b", null, totalTools), h("span", null, "инструментов")),
303
+ ),
304
+ data === null ? h("div", { className: "mcp-loading" }, "Загружаем подключения…") : null,
305
+ data !== null && servers.length === 0 ? h("div", { className: "mcp-empty" },
306
+ h("h3", null, "Пока пусто — но это быстро"),
307
+ h("p", null, "Начните с готового пресета. Понадобится только токен, если он нужен серверу; для Filesystem — ничего."),
308
+ h("div", { className: "mcp-presets" }, presets.map((preset) => h("button", { key: preset.id, className: "mcp-preset", onClick: () => openPreset(preset) },
309
+ h("b", null, preset.title), h("small", null, preset.subtitle),
310
+ ))),
311
+ h("button", { className: "mcp-btn ghost", onClick: openManual }, "Настроить вручную"),
312
+ ) : null,
313
+ servers.length > 0 ? h("div", { className: "mcp-grid" }, servers.map((server) => h(ServerCard, {
314
+ key: server.id,
315
+ server,
316
+ busy,
317
+ tools: tools[server.id],
318
+ onToggle: (id, flags) => run("toggle", { id, ...flags }, "Настройки сервера обновлены"),
319
+ onTest: async (id) => {
320
+ setBusy(true);
321
+ try {
322
+ const result = await call(window.__DSH_MCP_CTX__, "test", { id });
323
+ setToast(result.connected ? `Подключён · ${result.tools} tools` : `Ошибка: ${result.error}`);
324
+ setTimeout(() => setToast(""), 4000);
325
+ await load();
326
+ } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
327
+ finally { setBusy(false); }
328
+ },
329
+ onTools: async (id) => {
330
+ try {
331
+ const result = await call(window.__DSH_MCP_CTX__, "tools", { id });
332
+ setTools((current) => ({ ...current, [id]: result.tools }));
333
+ if (result.error) setError(result.error);
334
+ } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
335
+ },
336
+ onRemove: (id) => {
337
+ if (window.confirm(`Удалить MCP-сервер «${id}»?`)) void run("remove", { id }, "Сервер удалён");
338
+ },
339
+ }))) : null,
340
+ servers.length > 0 ? h("p", { className: "mcp-note" }, `Секреты: ${data?.secretsFile || "~/.dsh/mcp.secrets.json"} · новые tools появятся в новом чате после подключения.`) : null,
341
+ form ? h(ServerForm, {
342
+ initial: form,
343
+ busy,
344
+ onCancel: () => setForm(null),
345
+ onSave: async (payload) => {
346
+ await run("save", payload, "Сервер сохранён и подключён");
347
+ setForm(null);
348
+ },
349
+ }) : null,
350
+ );
351
+ }
352
+
353
+ function apply(ctx) {
354
+ window.__DSH_MCP_CTX__ = ctx;
355
+ ctx.effect(() => {
356
+ const style = document.createElement("style");
357
+ style.id = "dsh-mcp-manager";
358
+ style.textContent = CSS;
359
+ document.head.appendChild(style);
360
+ return () => style.remove();
361
+ }, "mcp-manager: styles");
362
+ ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
363
+ name: "settings.section",
364
+ id: "mcp-manager",
365
+ order: 35,
366
+ label: () => "MCP", }, McpSection)), "mcp-manager: settings");
367
+ }
368
+
369
+ exports.apply = apply;
370
+ exports.inject = ["slots", "connection"];
371
+ return module.exports;
372
+ }
373
+ });
@@ -0,0 +1,159 @@
1
+ /**
2
+ * dsh-mcp-manager — MCP servers for DSH, without an SDK dependency.
3
+ *
4
+ * Global config: `~/.dsh/mcp.json`
5
+ * Project config: `<project>/.dsh/mcp.json` (used by `/mcp`, not auto-started)
6
+ *
7
+ * A server runs only when `trusted: true`. Tools appear as
8
+ * `mcp__<server>__<tool>`; project servers can be inspected and called through
9
+ * `/mcp` even when their tools were not part of the current agent toolset.
10
+ */
11
+ import path from "node:path";
12
+ import fs from "node:fs";
13
+ import { loadConfig, writeTemplate, globalConfigPath } from "./lib/config.js";
14
+ import { McpManager } from "./lib/manager.js";
15
+ import { registerApi } from "./lib/api.js";
16
+
17
+ export const inject = ["tools", "connection"];
18
+
19
+ function serverList(config) {
20
+ return config.servers.map((server) => {
21
+ let transport;
22
+ if (server.url) {
23
+ try { transport = `HTTP ${new URL(server.url).origin}`; } catch { transport = "HTTP (invalid url)"; }
24
+ } else {
25
+ transport = `stdio ${server.command} (${server.args.length} args)`;
26
+ }
27
+ return `- ${server.id} [${server.scope}] ${server.enabled ? "enabled" : "disabled"} ${server.trusted ? "trusted" : "UNTRUSTED"} · ${transport}`;
28
+ });
29
+ }
30
+
31
+ function formatStatus(manager) {
32
+ const rows = manager.status();
33
+ if (rows.length === 0) return "MCP: нет подключённых серверов.";
34
+ return rows
35
+ .map((row) => `- ${row.id}: ${row.connected ? `connected, tools ${row.tools}` : `not connected${row.error ? ` — ${row.error}` : ""}`}${row.serverInfo ? ` · ${row.serverInfo.name ?? ""} ${row.serverInfo.version ?? ""}`.trimEnd() : ""}`)
36
+ .join("\n");
37
+ }
38
+
39
+ function commandCwd(ctx) {
40
+ try {
41
+ return ctx.get?.("agents")?.currentInitiator?.()?.session?.header?.cwd || process.cwd();
42
+ } catch {
43
+ return process.cwd();
44
+ }
45
+ }
46
+
47
+ function registerCommand(ctx, manager) {
48
+ ctx.commands.register({
49
+ name: "mcp",
50
+ description: "MCP-серверы: список, инструменты, вызов, resources, reload",
51
+ input: { hint: "list | trust | untrust | tools [server] | call <server> <tool> [json] | resources <server> | read <server> <uri> | reload | init" },
52
+ recordInput: false,
53
+ async handler({ rawInput }) {
54
+ const input = String(rawInput ?? "").trim();
55
+ const [action = "list", ...rest] = input.split(/\s+/u);
56
+ try {
57
+ if (action === "init") {
58
+ const result = writeTemplate();
59
+ return { kind: "success", text: result.created ? `Создан ${result.file}` : `Уже существует: ${result.file}` };
60
+ }
61
+ const config = loadConfig({ cwd: commandCwd(ctx) });
62
+ if (action === "trust" || action === "untrust") {
63
+ if (!config.projectConfig) return { kind: "error", text: "Нет .dsh/mcp.json в проекте." };
64
+ const marker = path.join(path.dirname(config.projectConfig), "mcp.trusted");
65
+ if (action === "trust") {
66
+ fs.writeFileSync(marker, `${new Date().toISOString()}\n`, "utf8");
67
+ return { kind: "success", text: `Доверие к проектным MCP-серверам включено: ${marker}` };
68
+ }
69
+ fs.rmSync(marker, { force: true });
70
+ return { kind: "success", text: "Доверие к проектным MCP-серверам снято." };
71
+ }
72
+ if (action === "list") {
73
+ const lines = [
74
+ `Global: ${globalConfigPath()}`,
75
+ `Project: ${config.projectConfig ?? "нет"}`,
76
+ "",
77
+ ...(serverList(config).length > 0 ? serverList(config) : ["Нет серверов в mcp.json."]),
78
+ "",
79
+ formatStatus(manager),
80
+ ];
81
+ return { kind: "success", text: lines.join("\n") };
82
+ }
83
+ if (action === "reload") {
84
+ await manager.close();
85
+ const results = await manager.connectAll(config.servers.filter((server) => server.enabled && server.trusted));
86
+ manager.registerTools();
87
+ return { kind: "success", text: `MCP reload: ${results.filter((r) => r.status === "fulfilled" && r.value.client).length}/${results.length} подключено.\n${formatStatus(manager)}` };
88
+ }
89
+ const server = config.servers.find((item) => item.id === rest[0]);
90
+ if (["tools", "call", "resources", "read"].includes(action) && !rest[0]) {
91
+ return { kind: "error", text: `/mcp ${action}: укажите сервер` };
92
+ }
93
+ if (["tools", "call", "resources", "read"].includes(action) && !server) {
94
+ return { kind: "error", text: `MCP-сервер не найден: ${rest[0]}` };
95
+ }
96
+ if (!server.trusted) return { kind: "error", text: `MCP ${server.id}: trusted=false; включите доверие в mcp.json` };
97
+ const record = await manager.connect(server);
98
+ if (!record.client) return { kind: "error", text: `MCP ${server.id}: ${record.error}` };
99
+ if (action === "tools") {
100
+ return {
101
+ kind: "success",
102
+ text: record.tools.length
103
+ ? record.tools.map((tool) => `- ${tool.name}${tool.description ? ` — ${tool.description}` : ""}`).join("\n")
104
+ : "У сервера нет tools.",
105
+ };
106
+ }
107
+ if (action === "call") {
108
+ const toolName = rest[1];
109
+ if (!toolName) return { kind: "error", text: "/mcp call: укажите tool" };
110
+ const args = rest[2] ? JSON.parse(rest.slice(2).join(" ")) : {};
111
+ const text = await manager.call(server.id, toolName, args);
112
+ return { kind: "success", text: text || "(пустой результат)" };
113
+ }
114
+ if (action === "resources") {
115
+ const resources = await manager.resources(server.id);
116
+ return { kind: "success", text: resources.length ? resources.map((item) => `- ${item.uri}${item.name ? ` — ${item.name}` : ""}`).join("\n") : "Нет resources." };
117
+ }
118
+ if (action === "read") {
119
+ const uri = rest.slice(1).join(" ");
120
+ if (!uri) return { kind: "error", text: "/mcp read: укажите uri" };
121
+ const result = await manager.readResource(server.id, uri);
122
+ const contents = Array.isArray(result?.contents) ? result.contents : [];
123
+ const text = contents.map((item) => item.text ?? `[${item.mimeType ?? "resource"}] ${item.uri ?? ""}`).join("\n");
124
+ return { kind: "success", text: text || "(пустой resource)" };
125
+ }
126
+ return { kind: "error", text: `Неизвестное действие /mcp ${action}` };
127
+ } catch (error) {
128
+ return { kind: "error", text: `/mcp ${action}: ${error instanceof Error ? error.message : String(error)}` };
129
+ }
130
+ },
131
+ });
132
+ }
133
+
134
+ export function apply(ctx) {
135
+ const manager = new McpManager(ctx);
136
+ ctx.effect(() => () => manager.close(), "dsh-mcp-manager: connections");
137
+ try {
138
+ registerApi(ctx, manager);
139
+ } catch (error) {
140
+ ctx.logger?.warn?.(`[mcp] settings API skipped: ${error instanceof Error ? error.message : String(error)}`);
141
+ }
142
+ try {
143
+ ctx.inject(["commands"], (child) => registerCommand(child, manager));
144
+ } catch (error) {
145
+ ctx.logger?.warn?.(`[mcp] command skipped: ${error instanceof Error ? error.message : String(error)}`);
146
+ }
147
+ void (async () => {
148
+ try {
149
+ const config = loadConfig({ cwd: process.cwd() });
150
+ const auto = config.servers.filter((server) => server.scope === "global" && server.enabled && server.trusted);
151
+ if (auto.length === 0) return;
152
+ await manager.connectAll(auto);
153
+ manager.registerTools();
154
+ ctx.logger?.info?.(`[mcp] auto-connected ${auto.length} global server(s)`);
155
+ } catch (error) {
156
+ ctx.logger?.warn?.(`[mcp] auto-start skipped: ${error instanceof Error ? error.message : String(error)}`);
157
+ }
158
+ })();
159
+ }