@ran-sh/dsh-crew 1.8.0 → 1.9.1

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 (151) hide show
  1. package/.claude-plugin/marketplace.json +16 -16
  2. package/.claude-plugin/plugin.json +14 -14
  3. package/LICENSE +21 -21
  4. package/README.de.md +359 -359
  5. package/README.es.md +359 -359
  6. package/README.fr.md +359 -359
  7. package/README.hi.md +359 -359
  8. package/README.id.md +359 -359
  9. package/README.ja.md +359 -359
  10. package/README.ko.md +359 -359
  11. package/README.md +126 -126
  12. package/README.pt.md +359 -359
  13. package/README.ru.md +359 -359
  14. package/README.th.md +359 -359
  15. package/README.tr.md +359 -359
  16. package/README.vi.md +359 -359
  17. package/README.zh-TW.md +359 -359
  18. package/README.zh.md +116 -116
  19. package/agents/ds-flash.md +24 -24
  20. package/agents/ds-pro.md +24 -24
  21. package/agents/ds-reviewer.md +23 -23
  22. package/agents/ds-worker.md +23 -23
  23. package/bin/dsh-crew.mjs +16 -16
  24. package/codex/agents/ds-flash.toml +34 -34
  25. package/codex/agents/ds-pro.toml +34 -34
  26. package/codex/agents/ds-reviewer.toml +32 -32
  27. package/codex/agents/ds-worker.toml +32 -32
  28. package/codex/prompts/dsh-config.md +19 -19
  29. package/codex/prompts/dsh-status.md +5 -5
  30. package/commands/config.md +23 -23
  31. package/commands/off.md +5 -5
  32. package/commands/on.md +5 -5
  33. package/commands/status.md +9 -9
  34. package/cordis.patch.yml +4 -4
  35. package/docs/gpt-relay-extension.md +103 -103
  36. package/docs/installation.md +138 -138
  37. package/docs/job-contracts.md +103 -103
  38. package/docs/readiness-matrix.md +85 -85
  39. package/docs/ui-surfaces.md +107 -107
  40. package/official-web-bridge/cordis.patch.yml +4 -4
  41. package/official-web-bridge/entry.mjs +1 -1
  42. package/official-web-bridge/overlay-entry.mjs +59 -59
  43. package/official-web-bridge/package.json +25 -25
  44. package/package.json +1 -1
  45. package/scripts/build-client.mjs +49 -49
  46. package/scripts/live-crew-smoke.mjs +39 -39
  47. package/scripts/live-policy-matrix.mjs +177 -177
  48. package/scripts/policy-probe.mjs +101 -101
  49. package/scripts/remove-legacy-official-bridge.ps1 +89 -89
  50. package/scripts/setup.mjs +393 -393
  51. package/scripts/smoke-real.mjs +110 -110
  52. package/scripts/smoke.mjs +78 -78
  53. package/scripts/verify-crew-ui-polish.mjs +145 -145
  54. package/scripts/verify-history-ui.mjs +97 -97
  55. package/scripts/verify-installer-fix.mjs +26 -26
  56. package/scripts/verify-npm-install.mjs +311 -311
  57. package/scripts/verify-official-bridge-e2e.mjs +192 -192
  58. package/src/adaptive-routing.mjs +260 -260
  59. package/src/client/activation-summary.tsx +64 -64
  60. package/src/client/collapsible-sections.mjs +55 -55
  61. package/src/client/history-panel.tsx +108 -108
  62. package/src/client/host-readiness.mjs +71 -71
  63. package/src/client/index.tsx +1711 -1711
  64. package/src/client/model-callability-view.mjs +17 -17
  65. package/src/client/panel-chrome.tsx +40 -40
  66. package/src/client/quick-entry.tsx +10 -10
  67. package/src/client/quick-panel.tsx +275 -275
  68. package/src/client/readiness-envelope.mjs +34 -34
  69. package/src/client/surface-detection.mjs +43 -43
  70. package/src/config-readiness.mjs +226 -226
  71. package/src/credential-reference.mjs +38 -38
  72. package/src/delivery.mjs +205 -205
  73. package/src/dsh-cli-runtime.mjs +1021 -1021
  74. package/src/dsh-cohort.mjs +20 -20
  75. package/src/extension-contract.mjs +104 -104
  76. package/src/failure-classification.mjs +201 -201
  77. package/src/history/admission-gate.mjs +67 -67
  78. package/src/history/archive-store.mjs +272 -272
  79. package/src/history/cleanup-plan.mjs +89 -89
  80. package/src/history/http.mjs +32 -32
  81. package/src/history/operation.mjs +86 -86
  82. package/src/history/runner-detach.mjs +34 -34
  83. package/src/history/runner.mjs +52 -52
  84. package/src/history/runtime.mjs +36 -36
  85. package/src/history/service.mjs +177 -177
  86. package/src/history/state.mjs +27 -27
  87. package/src/hub/entry.mjs +104 -104
  88. package/src/hub/index.mjs +2694 -2694
  89. package/src/hub-client.mjs +154 -154
  90. package/src/hub-compatibility.mjs +42 -42
  91. package/src/i18n.mjs +19 -19
  92. package/src/information-flow.mjs +67 -67
  93. package/src/install/cli.mjs +28 -28
  94. package/src/install/install-legacy.mjs +711 -711
  95. package/src/install/install.mjs +483 -483
  96. package/src/install/npx-lifecycle.mjs +3456 -3456
  97. package/src/install/official-frontend-assets.mjs +78 -78
  98. package/src/install/official-web.mjs +95 -95
  99. package/src/install/payload-content.mjs +88 -88
  100. package/src/install/windows-startup.mjs +236 -236
  101. package/src/install/windows-supervisor-adapter.mjs +443 -443
  102. package/src/install/windows-supervisor-lifecycle.mjs +782 -782
  103. package/src/install/zcode.mjs +397 -397
  104. package/src/job-contracts.mjs +255 -255
  105. package/src/local-request-guard.mjs +60 -60
  106. package/src/mcp-runtime.mjs +340 -332
  107. package/src/model-callability-contract.mjs +79 -79
  108. package/src/model-catalog.mjs +180 -180
  109. package/src/model-routing.mjs +586 -586
  110. package/src/model-schedule.mjs +207 -207
  111. package/src/official-web-bridge.mjs +447 -447
  112. package/src/policy.mjs +235 -235
  113. package/src/provider-delete-adapters.mjs +1934 -1934
  114. package/src/provider-health.mjs +130 -130
  115. package/src/provider-inventory.mjs +182 -182
  116. package/src/provider-layer-migration-adapters.mjs +759 -759
  117. package/src/provider-layer-migration.mjs +198 -198
  118. package/src/provider-lifecycle-state.mjs +103 -103
  119. package/src/provider-lifecycle.mjs +252 -252
  120. package/src/provider-profile-store.mjs +390 -390
  121. package/src/provider-settings-store.mjs +633 -633
  122. package/src/provider-store-lock.mjs +67 -67
  123. package/src/readiness-matrix.mjs +181 -181
  124. package/src/removable-waiter.mjs +29 -29
  125. package/src/role-profiles.mjs +107 -107
  126. package/src/runtime-controls.mjs +84 -84
  127. package/src/runtime-identity-contract.mjs +34 -34
  128. package/src/runtime-identity.mjs +235 -235
  129. package/src/runtime-readiness-snapshot.mjs +285 -285
  130. package/src/server.mjs +581 -581
  131. package/src/session-origins.mjs +60 -60
  132. package/src/standalone-sdk.mjs +23 -23
  133. package/src/status-shard.mjs +63 -63
  134. package/src/structured-error-code.mjs +38 -38
  135. package/src/supervisor/restart-request.mjs +256 -256
  136. package/src/workflow-runtime.mjs +739 -739
  137. package/src/workflow.mjs +155 -155
  138. package/src/workspace-audit.mjs +231 -231
  139. package/src/workspace-context.mjs +146 -146
  140. package/src/workspace-isolation.mjs +455 -401
  141. package/src/workspace-readiness.mjs +32 -32
  142. package/statusline/statusline.sh +14 -14
  143. package/statusline/worker-segment.sh +35 -35
  144. package/windows/start-dsh-crew.cmd +57 -57
  145. package/windows/start-dsh-crew.ps1 +1302 -1302
  146. package/windows/supervisor-control.ps1 +467 -467
  147. package/worker.cordis.yml +67 -67
  148. package/zcode/agents/ds-reviewer.md +31 -31
  149. package/zcode/agents/ds-worker.md +31 -31
  150. package/zcode/commands/dsh-config.md +17 -17
  151. package/zcode/commands/dsh-status.md +5 -5
@@ -1,399 +1,399 @@
1
- // ZCode host integration. This module is deliberately file-only: it never
2
- // launches ZCode, mutates credentials, or overwrites an unowned MCP server.
3
- // The generated files make ZCode dispatch through the same dsh-crew MCP server
4
- // used by Codex and Claude while keeping ZCode's native config precedence.
5
-
6
- import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
7
- import { createHash } from 'node:crypto';
8
- import { dirname, join, resolve } from 'node:path';
9
- import { fileURLToPath } from 'node:url';
10
- import { homedir } from 'node:os';
11
-
1
+ // ZCode host integration. This module is deliberately file-only: it never
2
+ // launches ZCode, mutates credentials, or overwrites an unowned MCP server.
3
+ // The generated files make ZCode dispatch through the same dsh-crew MCP server
4
+ // used by Codex and Claude while keeping ZCode's native config precedence.
5
+
6
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
7
+ import { createHash } from 'node:crypto';
8
+ import { dirname, join, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { homedir } from 'node:os';
11
+
12
12
  import { crewSkillFiles, installCrewSkill, readCrewSkill, removeCrewSkill } from './crew-skill.mjs';
13
13
 
14
- const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
15
- const HOST = 'zcode';
16
- const SERVER = 'dsh-crew';
17
- const POLICY_START = '<!-- DSH CREW MANAGED ZCODE POLICY:START -->';
18
- const POLICY_END = '<!-- DSH CREW MANAGED ZCODE POLICY:END -->';
19
- const OWNERSHIP_FILE = ({ home = homedir() } = {}) => join(home, '.config', 'dsh-crew', 'integrations', 'zcode.json');
20
-
21
- // Keep the allowlist explicit. ZCode rejects wildcard tool permissions, and a
22
- // future server tool is not silently exposed until a template intentionally
23
- // opts into it.
24
- export const ZCODE_MCP_TOOLS = Object.freeze([
25
- 'dsh_run_worker',
26
- 'dsh_spawn_worker',
27
- 'dsh_worker_status',
28
- 'dsh_worker_result',
29
- 'dsh_worker_cancel',
30
- 'dsh_worker_config',
31
- ]);
32
-
33
- function readText(file) {
34
- try { return readFileSync(file, 'utf8'); } catch { return null; }
35
- }
36
-
37
- function readJson(file, fallback = null) {
38
- try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return fallback; }
39
- }
40
-
41
- function contentHash(text) {
42
- return createHash('sha256').update(text, 'utf8').digest('hex');
43
- }
44
-
45
- function writeJson(file, value) {
46
- mkdirSync(dirname(file), { recursive: true });
47
- writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
48
- }
49
-
50
- function backup(file) {
51
- if (!existsSync(file)) return null;
52
- const path = `${file}.dsh-crew-backup-${Date.now()}`;
53
- copyFileSync(file, path);
54
- return path;
55
- }
56
-
57
- function normalizePath(value) {
58
- if (typeof value !== 'string' || !value.trim()) return null;
59
- try {
60
- const path = resolve(value);
61
- return process.platform === 'win32' ? path.toLowerCase() : path;
62
- } catch { return null; }
63
- }
64
-
65
- function serverTarget(server) {
66
- if (!server || typeof server !== 'object') return null;
67
- if (server.command !== 'node' || !Array.isArray(server.args) || server.args.length !== 1 || typeof server.args[0] !== 'string') return null;
68
- const target = normalizePath(server.args[0]);
69
- if (!target) return null;
70
- try { return statSync(target).isFile() ? target : null; } catch { return null; }
71
- }
72
-
73
- function nativeServers(config) {
74
- const servers = config?.mcp?.servers;
75
- return servers && typeof servers === 'object' && !Array.isArray(servers) ? servers : {};
76
- }
77
-
78
- function sharedServers(config) {
79
- const servers = config?.mcpServers;
80
- return servers && typeof servers === 'object' && !Array.isArray(servers) ? servers : {};
81
- }
82
-
83
- function configSource({ home = homedir() } = {}) {
84
- const nativeFile = join(home, '.zcode', 'cli', 'config.json');
85
- const sharedFile = join(home, '.agents', 'mcp.json');
86
- const native = readJson(nativeFile, {});
87
- const shared = readJson(sharedFile, {});
88
- const nativeMap = nativeServers(native);
89
- const sharedMap = sharedServers(shared);
90
- // ZCode shadows .agents/mcp.json whenever its native config has at least
91
- // one server. If native is empty, the shared file is the least-surprising
92
- // compatibility path for users who already manage MCP centrally.
93
- if (Object.keys(nativeMap).length > 0 || Object.keys(sharedMap).length === 0) {
94
- return { file: nativeFile, kind: 'native', config: native, servers: nativeMap };
95
- }
96
- return { file: sharedFile, kind: 'shared', config: shared, servers: sharedMap };
97
- }
98
-
99
- export function resolveZCodeMcpTarget({ home = homedir() } = {}) {
100
- const source = configSource({ home });
101
- return serverTarget(source.servers[SERVER]);
102
- }
103
-
104
- function expectedTarget({ root = ROOT } = {}) {
105
- return resolve(join(root, 'src', 'server.mjs'));
106
- }
107
-
108
- function removePolicy({ home }) {
109
- const file = join(home, '.zcode', 'AGENTS.md');
110
- const current = readText(file);
111
- if (typeof current !== 'string') return false;
112
- const start = POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
113
- const end = POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
114
- const managed = new RegExp(`(?:\\r?\\n){0,2}${start}[\\s\\S]*?${end}(?:\\r?\\n)?`, 'm');
115
- if (!managed.test(current)) return false;
116
- const next = current.replace(managed, '').trimEnd();
117
- backup(file);
118
- if (next.trim()) writeFileSync(file, `${next}\n`);
119
- else rmSync(file, { force: true });
120
- return true;
121
- }
122
-
123
- function ownership({ home = homedir() } = {}) {
124
- const data = readJson(OWNERSHIP_FILE({ home }), null);
125
- return data && typeof data === 'object' && data.host === HOST ? data : null;
126
- }
127
-
128
- function ownedSourceRecords(owned) {
129
- if (!owned) return [];
130
- const records = [];
131
- const seen = new Set();
132
- const current = owned.config_file ? {
133
- file: owned.config_file,
134
- kind: owned.config_kind === 'shared' ? 'shared' : 'native',
135
- } : null;
136
- for (const record of [current, ...(Array.isArray(owned.config_files) ? owned.config_files : [])]) {
137
- if (!record || typeof record.file !== 'string' || !record.file.trim()) continue;
138
- const key = normalizePath(record.file);
139
- if (!key || seen.has(key)) continue;
140
- seen.add(key);
141
- records.push({ file: record.file, kind: record.kind === 'shared' ? 'shared' : 'native' });
142
- }
143
- return records;
144
- }
145
-
146
- function removeOwnedMcp({ file, kind, target }) {
147
- const config = readJson(file, null);
148
- if (!config || typeof config !== 'object') return false;
149
- const isShared = kind === 'shared';
150
- const servers = isShared ? sharedServers(config) : nativeServers(config);
151
- const current = servers[SERVER];
152
- if (normalizePath(target) !== serverTarget(current)) return false;
153
- const next = { ...config };
154
- if (isShared) {
155
- next.mcpServers = { ...servers };
156
- delete next.mcpServers[SERVER];
157
- if (Object.keys(next.mcpServers).length === 0) delete next.mcpServers;
158
- } else {
159
- next.mcp = { ...(next.mcp ?? {}), servers: { ...servers } };
160
- delete next.mcp.servers[SERVER];
161
- if (Object.keys(next.mcp.servers).length === 0) delete next.mcp.servers;
162
- if (Object.keys(next.mcp).length === 0) delete next.mcp;
163
- }
164
- const changed = JSON.stringify(next) !== JSON.stringify(config);
165
- if (changed) {
166
- backup(file);
167
- writeJson(file, next);
168
- }
169
- return changed;
170
- }
171
-
172
- function cleanupPriorMcpSources({ owned, exceptFile }) {
173
- if (!owned) return [];
174
- const actions = [];
175
- const except = normalizePath(exceptFile);
176
- for (const source of ownedSourceRecords(owned)) {
177
- if (except && normalizePath(source.file) === except) continue;
178
- if (removeOwnedMcp({ file: source.file, kind: source.kind, target: owned.target })) {
179
- actions.push(`mcp: removed ${SERVER} from prior ${source.file}`);
180
- }
181
- }
182
- return actions;
183
- }
184
-
185
- function writeOwnership({ home, configFile, configKind, target, files = [], configFiles = [] }) {
186
- writeJson(OWNERSHIP_FILE({ home }), {
187
- schema_version: 1,
188
- host: HOST,
189
- server: SERVER,
190
- config_file: configFile,
191
- config_kind: configKind,
192
- target,
193
- files,
194
- config_files: configFiles,
195
- managed_at: new Date().toISOString(),
196
- });
197
- }
198
-
199
- function templateFiles({ home, root }) {
200
- return [
201
- ['agents', 'ds-worker.md'],
202
- ['agents', 'ds-reviewer.md'],
203
- ['commands', 'dsh-config.md'],
204
- ['commands', 'dsh-status.md'],
205
- ].map(([dir, file]) => ({ source: join(root, 'zcode', dir, file), dest: join(home, '.zcode', dir, file) }));
206
- }
207
-
208
- function installTemplates({ home, root, priorFiles = [] }) {
209
- const actions = [];
210
- const records = [];
211
- for (const { source, dest } of templateFiles({ home, root })) {
212
- if (!existsSync(source)) return { ok: false, code: 'ZCODE_TEMPLATE_MISSING', source };
213
- mkdirSync(dirname(dest), { recursive: true });
214
- const before = readText(dest);
215
- const rendered = readFileSync(source, 'utf8');
216
- const previous = priorFiles.find((entry) => entry?.path === dest);
217
- let backupFile = previous?.backup ?? null;
218
- if (before !== rendered) {
219
- if (before !== null && !backupFile) backupFile = backup(dest);
220
- writeFileSync(dest, rendered);
221
- }
222
- actions.push(dest);
223
- records.push({
224
- path: dest,
225
- preexisting: previous?.preexisting === true || (previous === undefined && before !== null),
226
- ...(backupFile ? { backup: backupFile } : {}),
227
- managed_sha256: contentHash(rendered),
228
- });
229
- }
230
- return { ok: true, actions, records };
231
- }
232
-
233
- function updateMcp({ home, root }) {
234
- const source = configSource({ home });
235
- const target = expectedTarget({ root });
236
- const normalizedTarget = normalizePath(target);
237
- const current = source.servers[SERVER];
238
- const currentTarget = serverTarget(current);
239
- const currentDeclaredTarget = Array.isArray(current?.args) && typeof current.args[0] === 'string'
240
- ? normalizePath(current.args[0])
241
- : null;
242
- const owned = ownership({ home });
243
- if (current && currentTarget !== normalizedTarget) {
244
- // Only an exact previously-owned entry may be repaired. Any foreign
245
- // command, even if it is also named dsh-crew, is a hard collision.
246
- if (!(owned
247
- && normalizePath(owned.config_file) === normalizePath(source.file)
248
- && normalizePath(owned.target) === currentDeclaredTarget)) {
249
- return { ok: false, code: 'ZCODE_MCP_COLLISION', config_file: source.file };
250
- }
251
- }
252
-
253
- const next = { ...source.config };
254
- if (source.kind === 'native') next.mcp = { ...(next.mcp ?? {}), servers: { ...source.servers, [SERVER]: { command: 'node', args: [target] } } };
255
- else next.mcpServers = { ...source.servers, [SERVER]: { command: 'node', args: [target] } };
256
- const changed = JSON.stringify(next) !== JSON.stringify(source.config);
257
- if (changed) { backup(source.file); writeJson(source.file, next); }
258
- return { ok: true, changed, config_file: source.file, config_kind: source.kind, target };
259
- }
260
-
261
- export function installZCode({ home = homedir(), root = ROOT, env = process.env } = {}) {
262
- const actions = [];
263
- const templates = templateFiles({ home, root });
264
- const missing = templates.find(({ source }) => !existsSync(source));
265
- if (missing) return { ok: false, code: 'ZCODE_TEMPLATE_MISSING', source: missing.source };
266
- const previousOwnership = ownership({ home });
267
- const priorFiles = previousOwnership?.files ?? [];
268
- // Check for collisions before writing any other integration surface so a
269
- // failed install is transaction-like and leaves user files untouched.
270
- const mcp = updateMcp({ home, root });
271
- if (!mcp.ok) return mcp;
272
- // Crew guidance is a skill now, loaded only when the operator asks for it.
273
- // The removal cleans up the policy block an earlier release wrote into the
274
- // host instruction file; ignoring its result keeps a first install quiet.
275
- const droppedPolicy = removePolicy({ home });
276
- if (droppedPolicy) actions.push('policy: removed managed ZCode block');
277
- const skill = installCrewSkill({ home, root, env });
278
- if (!skill.ok) return { ok: false, code: skill.code, source: skill.source };
279
- actions.push(...skill.written.map((file) => `skill: ${file}`));
280
- const installed = installTemplates({ home, root, priorFiles });
281
- if (!installed.ok) return installed;
282
- cleanupPriorMcpSources({ owned: previousOwnership, exceptFile: mcp.config_file });
283
- const currentSourceRecord = { file: mcp.config_file, kind: mcp.config_kind };
284
- const priorSourceRecords = ownedSourceRecords(previousOwnership)
285
- .filter((record) => normalizePath(record.file) !== normalizePath(currentSourceRecord.file));
286
- writeOwnership({
287
- home,
288
- configFile: mcp.config_file,
289
- configKind: mcp.config_kind,
290
- target: mcp.target,
291
- files: installed.records,
292
- configFiles: [currentSourceRecord, ...priorSourceRecords],
293
- });
294
- return { ok: true, ...mcp, skill_files: skill.written, files: installed.actions };
295
- }
296
-
297
- function zcodeComponents({ home = homedir(), root = ROOT, env = process.env } = {}) {
298
- const expected = normalizePath(expectedTarget({ root }));
299
- const source = configSource({ home });
300
- const configured = serverTarget(source.servers[SERVER]);
301
- const owned = ownership({ home });
302
- // The guidance is a skill now; a policy block in the host instruction file is
303
- // no longer written and no longer part of readiness.
304
- const expectedSkill = readCrewSkill({ root });
305
- // A block written by an earlier release still counts as an installed
306
- // footprint, so uninstall knows there is something of ours to clean up.
307
- const legacyPolicyText = readText(join(home, '.zcode', 'AGENTS.md'));
308
- const templates = templateFiles({ home, root });
309
- const templateReady = (entry) => {
310
- if (!entry) return false;
311
- const { source: sourceFile, dest } = entry;
312
- const sourceText = readText(sourceFile);
313
- const installedText = readText(dest);
314
- return sourceText !== null && installedText === sourceText;
315
- };
316
- const templateBySuffix = (suffix) => templates.find(({ dest }) => dest.endsWith(suffix));
317
- const ownershipFilesReady = !!owned && templates.every(({ source: sourceFile, dest }) => {
318
- const expectedText = readText(sourceFile);
319
- const installedText = readText(dest);
320
- const record = Array.isArray(owned.files)
321
- ? owned.files.find((entry) => normalizePath(entry?.path) === normalizePath(dest))
322
- : null;
323
- return expectedText !== null
324
- && installedText === expectedText
325
- && record?.managed_sha256 === contentHash(expectedText)
326
- && contentHash(installedText) === record.managed_sha256;
327
- });
328
- const components = {
329
- mcp: configured === expected,
330
- skill: expectedSkill !== null && readText(crewSkillFiles({ home, env })[0]) === expectedSkill,
331
- worker_agent: templateReady(templateBySuffix(join('agents', 'ds-worker.md'))),
332
- reviewer_agent: templateReady(templateBySuffix(join('agents', 'ds-reviewer.md'))),
333
- config_prompt: templateReady(templateBySuffix(join('commands', 'dsh-config.md'))),
334
- status_prompt: templateReady(templateBySuffix(join('commands', 'dsh-status.md'))),
335
- ownership: !!owned
336
- && normalizePath(owned.config_file) === normalizePath(source.file)
337
- && normalizePath(owned.target) === expected
338
- && ownershipFilesReady,
339
- };
340
- const policyFootprint = typeof legacyPolicyText === 'string'
341
- && legacyPolicyText.includes(POLICY_START)
342
- && legacyPolicyText.includes(POLICY_END);
343
- const installed = Object.hasOwn(source.servers, SERVER)
344
- || !!owned
345
- || policyFootprint
346
- || templates.some(({ dest }) => existsSync(dest));
347
- return { components, source, expected, configured, owned, installed };
348
- }
349
-
350
- export function zcodeStatus({ home = homedir(), root = ROOT, env = process.env } = {}) {
351
- const { components, source, expected, configured, installed } = zcodeComponents({ home, root, env });
352
- const missing = Object.entries(components).filter(([, value]) => !value).map(([key]) => key);
353
- return {
354
- installed,
355
- ready: missing.length === 0,
356
- components,
357
- missing,
358
- config_file: source.file,
359
- config_kind: source.kind,
360
- target: configured ?? expected,
361
- };
362
- }
363
-
364
- export function uninstallZCode({ home = homedir(), env = process.env } = {}) {
365
- const actions = [];
366
- const owned = ownership({ home });
367
- if (owned) {
368
- for (const source of ownedSourceRecords(owned)) {
369
- if (removeOwnedMcp({ file: source.file, kind: source.kind, target: owned.target })) {
370
- actions.push(`mcp: removed ${SERVER} from ${source.file}`);
371
- }
372
- }
373
- rmSync(OWNERSHIP_FILE({ home }), { force: true });
374
- }
375
- if (removePolicy({ home })) actions.push('policy: removed managed ZCode block');
376
- const skillAction = removeCrewSkill({ home, env });
377
- if (skillAction.changed) actions.push(...skillAction.removed.map((dir) => `removed: ${dir}`));
378
- const managedFiles = Array.isArray(owned?.files) && owned.files.length
379
- ? owned.files
380
- : ['agents/ds-worker.md', 'agents/ds-reviewer.md', 'commands/dsh-config.md', 'commands/dsh-status.md']
381
- .map((rel) => ({ path: join(home, '.zcode', rel) }));
382
- for (const entry of managedFiles) {
383
- const file = entry?.path;
384
- if (!file || !existsSync(file)) continue;
385
- const current = readText(file);
386
- const unchanged = !entry.managed_sha256 || (current !== null && contentHash(current) === entry.managed_sha256);
387
- if (!unchanged) continue;
388
- if (entry.preexisting && entry.backup && existsSync(entry.backup)) {
389
- copyFileSync(entry.backup, file);
390
- actions.push(`restored: ${file}`);
391
- } else {
392
- rmSync(file, { force: true });
393
- actions.push(`removed: ${file}`);
394
- }
395
- }
396
- return { ok: true, actions };
397
- }
398
-
399
- export function zcodeOwnershipFile({ home = homedir() } = {}) { return OWNERSHIP_FILE({ home }); }
14
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
15
+ const HOST = 'zcode';
16
+ const SERVER = 'dsh-crew';
17
+ const POLICY_START = '<!-- DSH CREW MANAGED ZCODE POLICY:START -->';
18
+ const POLICY_END = '<!-- DSH CREW MANAGED ZCODE POLICY:END -->';
19
+ const OWNERSHIP_FILE = ({ home = homedir() } = {}) => join(home, '.config', 'dsh-crew', 'integrations', 'zcode.json');
20
+
21
+ // Keep the allowlist explicit. ZCode rejects wildcard tool permissions, and a
22
+ // future server tool is not silently exposed until a template intentionally
23
+ // opts into it.
24
+ export const ZCODE_MCP_TOOLS = Object.freeze([
25
+ 'dsh_run_worker',
26
+ 'dsh_spawn_worker',
27
+ 'dsh_worker_status',
28
+ 'dsh_worker_result',
29
+ 'dsh_worker_cancel',
30
+ 'dsh_worker_config',
31
+ ]);
32
+
33
+ function readText(file) {
34
+ try { return readFileSync(file, 'utf8'); } catch { return null; }
35
+ }
36
+
37
+ function readJson(file, fallback = null) {
38
+ try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return fallback; }
39
+ }
40
+
41
+ function contentHash(text) {
42
+ return createHash('sha256').update(text, 'utf8').digest('hex');
43
+ }
44
+
45
+ function writeJson(file, value) {
46
+ mkdirSync(dirname(file), { recursive: true });
47
+ writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
48
+ }
49
+
50
+ function backup(file) {
51
+ if (!existsSync(file)) return null;
52
+ const path = `${file}.dsh-crew-backup-${Date.now()}`;
53
+ copyFileSync(file, path);
54
+ return path;
55
+ }
56
+
57
+ function normalizePath(value) {
58
+ if (typeof value !== 'string' || !value.trim()) return null;
59
+ try {
60
+ const path = resolve(value);
61
+ return process.platform === 'win32' ? path.toLowerCase() : path;
62
+ } catch { return null; }
63
+ }
64
+
65
+ function serverTarget(server) {
66
+ if (!server || typeof server !== 'object') return null;
67
+ if (server.command !== 'node' || !Array.isArray(server.args) || server.args.length !== 1 || typeof server.args[0] !== 'string') return null;
68
+ const target = normalizePath(server.args[0]);
69
+ if (!target) return null;
70
+ try { return statSync(target).isFile() ? target : null; } catch { return null; }
71
+ }
72
+
73
+ function nativeServers(config) {
74
+ const servers = config?.mcp?.servers;
75
+ return servers && typeof servers === 'object' && !Array.isArray(servers) ? servers : {};
76
+ }
77
+
78
+ function sharedServers(config) {
79
+ const servers = config?.mcpServers;
80
+ return servers && typeof servers === 'object' && !Array.isArray(servers) ? servers : {};
81
+ }
82
+
83
+ function configSource({ home = homedir() } = {}) {
84
+ const nativeFile = join(home, '.zcode', 'cli', 'config.json');
85
+ const sharedFile = join(home, '.agents', 'mcp.json');
86
+ const native = readJson(nativeFile, {});
87
+ const shared = readJson(sharedFile, {});
88
+ const nativeMap = nativeServers(native);
89
+ const sharedMap = sharedServers(shared);
90
+ // ZCode shadows .agents/mcp.json whenever its native config has at least
91
+ // one server. If native is empty, the shared file is the least-surprising
92
+ // compatibility path for users who already manage MCP centrally.
93
+ if (Object.keys(nativeMap).length > 0 || Object.keys(sharedMap).length === 0) {
94
+ return { file: nativeFile, kind: 'native', config: native, servers: nativeMap };
95
+ }
96
+ return { file: sharedFile, kind: 'shared', config: shared, servers: sharedMap };
97
+ }
98
+
99
+ export function resolveZCodeMcpTarget({ home = homedir() } = {}) {
100
+ const source = configSource({ home });
101
+ return serverTarget(source.servers[SERVER]);
102
+ }
103
+
104
+ function expectedTarget({ root = ROOT } = {}) {
105
+ return resolve(join(root, 'src', 'server.mjs'));
106
+ }
107
+
108
+ function removePolicy({ home }) {
109
+ const file = join(home, '.zcode', 'AGENTS.md');
110
+ const current = readText(file);
111
+ if (typeof current !== 'string') return false;
112
+ const start = POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
113
+ const end = POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
114
+ const managed = new RegExp(`(?:\\r?\\n){0,2}${start}[\\s\\S]*?${end}(?:\\r?\\n)?`, 'm');
115
+ if (!managed.test(current)) return false;
116
+ const next = current.replace(managed, '').trimEnd();
117
+ backup(file);
118
+ if (next.trim()) writeFileSync(file, `${next}\n`);
119
+ else rmSync(file, { force: true });
120
+ return true;
121
+ }
122
+
123
+ function ownership({ home = homedir() } = {}) {
124
+ const data = readJson(OWNERSHIP_FILE({ home }), null);
125
+ return data && typeof data === 'object' && data.host === HOST ? data : null;
126
+ }
127
+
128
+ function ownedSourceRecords(owned) {
129
+ if (!owned) return [];
130
+ const records = [];
131
+ const seen = new Set();
132
+ const current = owned.config_file ? {
133
+ file: owned.config_file,
134
+ kind: owned.config_kind === 'shared' ? 'shared' : 'native',
135
+ } : null;
136
+ for (const record of [current, ...(Array.isArray(owned.config_files) ? owned.config_files : [])]) {
137
+ if (!record || typeof record.file !== 'string' || !record.file.trim()) continue;
138
+ const key = normalizePath(record.file);
139
+ if (!key || seen.has(key)) continue;
140
+ seen.add(key);
141
+ records.push({ file: record.file, kind: record.kind === 'shared' ? 'shared' : 'native' });
142
+ }
143
+ return records;
144
+ }
145
+
146
+ function removeOwnedMcp({ file, kind, target }) {
147
+ const config = readJson(file, null);
148
+ if (!config || typeof config !== 'object') return false;
149
+ const isShared = kind === 'shared';
150
+ const servers = isShared ? sharedServers(config) : nativeServers(config);
151
+ const current = servers[SERVER];
152
+ if (normalizePath(target) !== serverTarget(current)) return false;
153
+ const next = { ...config };
154
+ if (isShared) {
155
+ next.mcpServers = { ...servers };
156
+ delete next.mcpServers[SERVER];
157
+ if (Object.keys(next.mcpServers).length === 0) delete next.mcpServers;
158
+ } else {
159
+ next.mcp = { ...(next.mcp ?? {}), servers: { ...servers } };
160
+ delete next.mcp.servers[SERVER];
161
+ if (Object.keys(next.mcp.servers).length === 0) delete next.mcp.servers;
162
+ if (Object.keys(next.mcp).length === 0) delete next.mcp;
163
+ }
164
+ const changed = JSON.stringify(next) !== JSON.stringify(config);
165
+ if (changed) {
166
+ backup(file);
167
+ writeJson(file, next);
168
+ }
169
+ return changed;
170
+ }
171
+
172
+ function cleanupPriorMcpSources({ owned, exceptFile }) {
173
+ if (!owned) return [];
174
+ const actions = [];
175
+ const except = normalizePath(exceptFile);
176
+ for (const source of ownedSourceRecords(owned)) {
177
+ if (except && normalizePath(source.file) === except) continue;
178
+ if (removeOwnedMcp({ file: source.file, kind: source.kind, target: owned.target })) {
179
+ actions.push(`mcp: removed ${SERVER} from prior ${source.file}`);
180
+ }
181
+ }
182
+ return actions;
183
+ }
184
+
185
+ function writeOwnership({ home, configFile, configKind, target, files = [], configFiles = [] }) {
186
+ writeJson(OWNERSHIP_FILE({ home }), {
187
+ schema_version: 1,
188
+ host: HOST,
189
+ server: SERVER,
190
+ config_file: configFile,
191
+ config_kind: configKind,
192
+ target,
193
+ files,
194
+ config_files: configFiles,
195
+ managed_at: new Date().toISOString(),
196
+ });
197
+ }
198
+
199
+ function templateFiles({ home, root }) {
200
+ return [
201
+ ['agents', 'ds-worker.md'],
202
+ ['agents', 'ds-reviewer.md'],
203
+ ['commands', 'dsh-config.md'],
204
+ ['commands', 'dsh-status.md'],
205
+ ].map(([dir, file]) => ({ source: join(root, 'zcode', dir, file), dest: join(home, '.zcode', dir, file) }));
206
+ }
207
+
208
+ function installTemplates({ home, root, priorFiles = [] }) {
209
+ const actions = [];
210
+ const records = [];
211
+ for (const { source, dest } of templateFiles({ home, root })) {
212
+ if (!existsSync(source)) return { ok: false, code: 'ZCODE_TEMPLATE_MISSING', source };
213
+ mkdirSync(dirname(dest), { recursive: true });
214
+ const before = readText(dest);
215
+ const rendered = readFileSync(source, 'utf8');
216
+ const previous = priorFiles.find((entry) => entry?.path === dest);
217
+ let backupFile = previous?.backup ?? null;
218
+ if (before !== rendered) {
219
+ if (before !== null && !backupFile) backupFile = backup(dest);
220
+ writeFileSync(dest, rendered);
221
+ }
222
+ actions.push(dest);
223
+ records.push({
224
+ path: dest,
225
+ preexisting: previous?.preexisting === true || (previous === undefined && before !== null),
226
+ ...(backupFile ? { backup: backupFile } : {}),
227
+ managed_sha256: contentHash(rendered),
228
+ });
229
+ }
230
+ return { ok: true, actions, records };
231
+ }
232
+
233
+ function updateMcp({ home, root }) {
234
+ const source = configSource({ home });
235
+ const target = expectedTarget({ root });
236
+ const normalizedTarget = normalizePath(target);
237
+ const current = source.servers[SERVER];
238
+ const currentTarget = serverTarget(current);
239
+ const currentDeclaredTarget = Array.isArray(current?.args) && typeof current.args[0] === 'string'
240
+ ? normalizePath(current.args[0])
241
+ : null;
242
+ const owned = ownership({ home });
243
+ if (current && currentTarget !== normalizedTarget) {
244
+ // Only an exact previously-owned entry may be repaired. Any foreign
245
+ // command, even if it is also named dsh-crew, is a hard collision.
246
+ if (!(owned
247
+ && normalizePath(owned.config_file) === normalizePath(source.file)
248
+ && normalizePath(owned.target) === currentDeclaredTarget)) {
249
+ return { ok: false, code: 'ZCODE_MCP_COLLISION', config_file: source.file };
250
+ }
251
+ }
252
+
253
+ const next = { ...source.config };
254
+ if (source.kind === 'native') next.mcp = { ...(next.mcp ?? {}), servers: { ...source.servers, [SERVER]: { command: 'node', args: [target] } } };
255
+ else next.mcpServers = { ...source.servers, [SERVER]: { command: 'node', args: [target] } };
256
+ const changed = JSON.stringify(next) !== JSON.stringify(source.config);
257
+ if (changed) { backup(source.file); writeJson(source.file, next); }
258
+ return { ok: true, changed, config_file: source.file, config_kind: source.kind, target };
259
+ }
260
+
261
+ export function installZCode({ home = homedir(), root = ROOT, env = process.env } = {}) {
262
+ const actions = [];
263
+ const templates = templateFiles({ home, root });
264
+ const missing = templates.find(({ source }) => !existsSync(source));
265
+ if (missing) return { ok: false, code: 'ZCODE_TEMPLATE_MISSING', source: missing.source };
266
+ const previousOwnership = ownership({ home });
267
+ const priorFiles = previousOwnership?.files ?? [];
268
+ // Check for collisions before writing any other integration surface so a
269
+ // failed install is transaction-like and leaves user files untouched.
270
+ const mcp = updateMcp({ home, root });
271
+ if (!mcp.ok) return mcp;
272
+ // Crew guidance is a skill now, loaded only when the operator asks for it.
273
+ // The removal cleans up the policy block an earlier release wrote into the
274
+ // host instruction file; ignoring its result keeps a first install quiet.
275
+ const droppedPolicy = removePolicy({ home });
276
+ if (droppedPolicy) actions.push('policy: removed managed ZCode block');
277
+ const skill = installCrewSkill({ home, root, env });
278
+ if (!skill.ok) return { ok: false, code: skill.code, source: skill.source };
279
+ actions.push(...skill.written.map((file) => `skill: ${file}`));
280
+ const installed = installTemplates({ home, root, priorFiles });
281
+ if (!installed.ok) return installed;
282
+ cleanupPriorMcpSources({ owned: previousOwnership, exceptFile: mcp.config_file });
283
+ const currentSourceRecord = { file: mcp.config_file, kind: mcp.config_kind };
284
+ const priorSourceRecords = ownedSourceRecords(previousOwnership)
285
+ .filter((record) => normalizePath(record.file) !== normalizePath(currentSourceRecord.file));
286
+ writeOwnership({
287
+ home,
288
+ configFile: mcp.config_file,
289
+ configKind: mcp.config_kind,
290
+ target: mcp.target,
291
+ files: installed.records,
292
+ configFiles: [currentSourceRecord, ...priorSourceRecords],
293
+ });
294
+ return { ok: true, ...mcp, skill_files: skill.written, files: installed.actions };
295
+ }
296
+
297
+ function zcodeComponents({ home = homedir(), root = ROOT, env = process.env } = {}) {
298
+ const expected = normalizePath(expectedTarget({ root }));
299
+ const source = configSource({ home });
300
+ const configured = serverTarget(source.servers[SERVER]);
301
+ const owned = ownership({ home });
302
+ // The guidance is a skill now; a policy block in the host instruction file is
303
+ // no longer written and no longer part of readiness.
304
+ const expectedSkill = readCrewSkill({ root });
305
+ // A block written by an earlier release still counts as an installed
306
+ // footprint, so uninstall knows there is something of ours to clean up.
307
+ const legacyPolicyText = readText(join(home, '.zcode', 'AGENTS.md'));
308
+ const templates = templateFiles({ home, root });
309
+ const templateReady = (entry) => {
310
+ if (!entry) return false;
311
+ const { source: sourceFile, dest } = entry;
312
+ const sourceText = readText(sourceFile);
313
+ const installedText = readText(dest);
314
+ return sourceText !== null && installedText === sourceText;
315
+ };
316
+ const templateBySuffix = (suffix) => templates.find(({ dest }) => dest.endsWith(suffix));
317
+ const ownershipFilesReady = !!owned && templates.every(({ source: sourceFile, dest }) => {
318
+ const expectedText = readText(sourceFile);
319
+ const installedText = readText(dest);
320
+ const record = Array.isArray(owned.files)
321
+ ? owned.files.find((entry) => normalizePath(entry?.path) === normalizePath(dest))
322
+ : null;
323
+ return expectedText !== null
324
+ && installedText === expectedText
325
+ && record?.managed_sha256 === contentHash(expectedText)
326
+ && contentHash(installedText) === record.managed_sha256;
327
+ });
328
+ const components = {
329
+ mcp: configured === expected,
330
+ skill: expectedSkill !== null && readText(crewSkillFiles({ home, env })[0]) === expectedSkill,
331
+ worker_agent: templateReady(templateBySuffix(join('agents', 'ds-worker.md'))),
332
+ reviewer_agent: templateReady(templateBySuffix(join('agents', 'ds-reviewer.md'))),
333
+ config_prompt: templateReady(templateBySuffix(join('commands', 'dsh-config.md'))),
334
+ status_prompt: templateReady(templateBySuffix(join('commands', 'dsh-status.md'))),
335
+ ownership: !!owned
336
+ && normalizePath(owned.config_file) === normalizePath(source.file)
337
+ && normalizePath(owned.target) === expected
338
+ && ownershipFilesReady,
339
+ };
340
+ const policyFootprint = typeof legacyPolicyText === 'string'
341
+ && legacyPolicyText.includes(POLICY_START)
342
+ && legacyPolicyText.includes(POLICY_END);
343
+ const installed = Object.hasOwn(source.servers, SERVER)
344
+ || !!owned
345
+ || policyFootprint
346
+ || templates.some(({ dest }) => existsSync(dest));
347
+ return { components, source, expected, configured, owned, installed };
348
+ }
349
+
350
+ export function zcodeStatus({ home = homedir(), root = ROOT, env = process.env } = {}) {
351
+ const { components, source, expected, configured, installed } = zcodeComponents({ home, root, env });
352
+ const missing = Object.entries(components).filter(([, value]) => !value).map(([key]) => key);
353
+ return {
354
+ installed,
355
+ ready: missing.length === 0,
356
+ components,
357
+ missing,
358
+ config_file: source.file,
359
+ config_kind: source.kind,
360
+ target: configured ?? expected,
361
+ };
362
+ }
363
+
364
+ export function uninstallZCode({ home = homedir(), env = process.env } = {}) {
365
+ const actions = [];
366
+ const owned = ownership({ home });
367
+ if (owned) {
368
+ for (const source of ownedSourceRecords(owned)) {
369
+ if (removeOwnedMcp({ file: source.file, kind: source.kind, target: owned.target })) {
370
+ actions.push(`mcp: removed ${SERVER} from ${source.file}`);
371
+ }
372
+ }
373
+ rmSync(OWNERSHIP_FILE({ home }), { force: true });
374
+ }
375
+ if (removePolicy({ home })) actions.push('policy: removed managed ZCode block');
376
+ const skillAction = removeCrewSkill({ home, env });
377
+ if (skillAction.changed) actions.push(...skillAction.removed.map((dir) => `removed: ${dir}`));
378
+ const managedFiles = Array.isArray(owned?.files) && owned.files.length
379
+ ? owned.files
380
+ : ['agents/ds-worker.md', 'agents/ds-reviewer.md', 'commands/dsh-config.md', 'commands/dsh-status.md']
381
+ .map((rel) => ({ path: join(home, '.zcode', rel) }));
382
+ for (const entry of managedFiles) {
383
+ const file = entry?.path;
384
+ if (!file || !existsSync(file)) continue;
385
+ const current = readText(file);
386
+ const unchanged = !entry.managed_sha256 || (current !== null && contentHash(current) === entry.managed_sha256);
387
+ if (!unchanged) continue;
388
+ if (entry.preexisting && entry.backup && existsSync(entry.backup)) {
389
+ copyFileSync(entry.backup, file);
390
+ actions.push(`restored: ${file}`);
391
+ } else {
392
+ rmSync(file, { force: true });
393
+ actions.push(`removed: ${file}`);
394
+ }
395
+ }
396
+ return { ok: true, actions };
397
+ }
398
+
399
+ export function zcodeOwnershipFile({ home = homedir() } = {}) { return OWNERSHIP_FILE({ home }); }