@ran-sh/dsh-crew 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -112
- package/README.zh.md +93 -111
- package/codex/AGENTS.md +92 -0
- package/docs/installation.md +63 -0
- package/docs/ui-surfaces.md +1 -2
- package/lib/client.js +41 -4
- package/official-web-bridge/lib/client.js +41 -4
- package/package.json +162 -133
- package/scripts/setup.mjs +65 -19
- package/scripts/verify-npm-install.mjs +16 -3
- package/src/client/host-readiness.mjs +2 -1
- package/src/client/index.tsx +29 -15
- package/src/hub/index.mjs +8 -6
- package/src/install/install-legacy.mjs +65 -11
- package/src/install/install.mjs +3 -1
- package/src/install/npx-lifecycle.mjs +55 -19
- package/src/install/windows-startup.mjs +98 -0
- package/src/install/zcode.mjs +316 -0
- package/src/runtime-identity.mjs +1 -1
- package/windows/start-dsh-crew.cmd +55 -0
- package/windows/start-dsh-crew.vbs +9 -0
- package/zcode/AGENTS.md +21 -0
- package/zcode/agents/ds-reviewer.md +19 -0
- package/zcode/agents/ds-worker.md +20 -0
- package/zcode/commands/dsh-config.md +6 -0
- package/zcode/commands/dsh-status.md +4 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
|
|
12
|
+
export const WINDOWS_STARTUP_FILENAME = 'DSH Crew.vbs';
|
|
13
|
+
export const WINDOWS_LAUNCHER_FILENAME = 'start-dsh-crew.cmd';
|
|
14
|
+
|
|
15
|
+
function defaultStartupDir({ home, env }) {
|
|
16
|
+
if (home === homedir() && env.APPDATA) {
|
|
17
|
+
return join(env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
18
|
+
}
|
|
19
|
+
return join(home, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function paths({ home, startupDir, env }) {
|
|
23
|
+
const launcherFile = join(home, '.config', 'dsh-crew', 'launchers', WINDOWS_LAUNCHER_FILENAME);
|
|
24
|
+
const resolvedStartupDir = startupDir ?? defaultStartupDir({ home, env });
|
|
25
|
+
return {
|
|
26
|
+
launcherFile,
|
|
27
|
+
startupFile: join(resolvedStartupDir, WINDOWS_STARTUP_FILENAME),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderVbs(template, launcherFile) {
|
|
32
|
+
const escaped = launcherFile.replace(/"/g, '""');
|
|
33
|
+
return template.replace('__LAUNCHER__', escaped);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function windowsStartupStatus({
|
|
37
|
+
home = homedir(),
|
|
38
|
+
startupDir,
|
|
39
|
+
platform = process.platform,
|
|
40
|
+
env = process.env,
|
|
41
|
+
} = {}) {
|
|
42
|
+
if (platform !== 'win32') return { supported: false, installed: false, ready: false };
|
|
43
|
+
const resolved = paths({ home, startupDir, env });
|
|
44
|
+
const installed = existsSync(resolved.startupFile) || existsSync(resolved.launcherFile);
|
|
45
|
+
let ready = existsSync(resolved.startupFile) && existsSync(resolved.launcherFile);
|
|
46
|
+
if (ready) {
|
|
47
|
+
try {
|
|
48
|
+
const text = readFileSync(resolved.startupFile, 'utf16le').replace(/^\uFEFF/, '');
|
|
49
|
+
ready = text.includes(resolved.launcherFile);
|
|
50
|
+
} catch { ready = false; }
|
|
51
|
+
}
|
|
52
|
+
return { supported: true, installed, ready, ...resolved };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function installWindowsStartup({
|
|
56
|
+
home = homedir(),
|
|
57
|
+
root,
|
|
58
|
+
startupDir,
|
|
59
|
+
platform = process.platform,
|
|
60
|
+
env = process.env,
|
|
61
|
+
} = {}) {
|
|
62
|
+
if (platform !== 'win32') return { ok: true, supported: false, changed: false };
|
|
63
|
+
if (!root) return { ok: false, supported: true, code: 'STARTUP_SOURCE_REQUIRED' };
|
|
64
|
+
const sourceLauncher = join(root, 'windows', WINDOWS_LAUNCHER_FILENAME);
|
|
65
|
+
const sourceVbs = join(root, 'windows', 'start-dsh-crew.vbs');
|
|
66
|
+
if (!existsSync(sourceLauncher) || !existsSync(sourceVbs)) {
|
|
67
|
+
return { ok: false, supported: true, code: 'STARTUP_ASSET_MISSING' };
|
|
68
|
+
}
|
|
69
|
+
const resolved = paths({ home, startupDir, env });
|
|
70
|
+
mkdirSync(dirname(resolved.launcherFile), { recursive: true });
|
|
71
|
+
mkdirSync(dirname(resolved.startupFile), { recursive: true });
|
|
72
|
+
const beforeLauncher = existsSync(resolved.launcherFile) ? readFileSync(resolved.launcherFile) : null;
|
|
73
|
+
const beforeStartup = existsSync(resolved.startupFile) ? readFileSync(resolved.startupFile) : null;
|
|
74
|
+
copyFileSync(sourceLauncher, resolved.launcherFile);
|
|
75
|
+
const rendered = renderVbs(readFileSync(sourceVbs, 'utf8'), resolved.launcherFile);
|
|
76
|
+
writeFileSync(resolved.startupFile, `\uFEFF${rendered}`, 'utf16le');
|
|
77
|
+
const changed = !beforeLauncher?.equals(readFileSync(resolved.launcherFile))
|
|
78
|
+
|| !beforeStartup?.equals(readFileSync(resolved.startupFile));
|
|
79
|
+
return { ok: true, supported: true, changed, ...resolved };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function uninstallWindowsStartup({
|
|
83
|
+
home = homedir(),
|
|
84
|
+
startupDir,
|
|
85
|
+
platform = process.platform,
|
|
86
|
+
env = process.env,
|
|
87
|
+
} = {}) {
|
|
88
|
+
if (platform !== 'win32') return { ok: true, supported: false, removed: false };
|
|
89
|
+
const resolved = paths({ home, startupDir, env });
|
|
90
|
+
let removed = false;
|
|
91
|
+
for (const file of [resolved.startupFile, resolved.launcherFile]) {
|
|
92
|
+
if (existsSync(file)) {
|
|
93
|
+
rmSync(file, { force: true });
|
|
94
|
+
removed = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, supported: true, removed, ...resolved };
|
|
98
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
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, 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
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
13
|
+
const HOST = 'zcode';
|
|
14
|
+
const SERVER = 'dsh-crew';
|
|
15
|
+
const POLICY_START = '<!-- DSH CREW MANAGED ZCODE POLICY:START -->';
|
|
16
|
+
const POLICY_END = '<!-- DSH CREW MANAGED ZCODE POLICY:END -->';
|
|
17
|
+
const OWNERSHIP_FILE = ({ home = homedir() } = {}) => join(home, '.config', 'dsh-crew', 'integrations', 'zcode.json');
|
|
18
|
+
|
|
19
|
+
// Keep the allowlist explicit. ZCode rejects wildcard tool permissions, and a
|
|
20
|
+
// future server tool is not silently exposed until a template intentionally
|
|
21
|
+
// opts into it.
|
|
22
|
+
export const ZCODE_MCP_TOOLS = Object.freeze([
|
|
23
|
+
'dsh_run_worker',
|
|
24
|
+
'dsh_spawn_worker',
|
|
25
|
+
'dsh_worker_status',
|
|
26
|
+
'dsh_worker_result',
|
|
27
|
+
'dsh_worker_cancel',
|
|
28
|
+
'dsh_worker_config',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function readText(file) {
|
|
32
|
+
try { return readFileSync(file, 'utf8'); } catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readJson(file, fallback = null) {
|
|
36
|
+
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return fallback; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function contentHash(text) {
|
|
40
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeJson(file, value) {
|
|
44
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
45
|
+
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function backup(file) {
|
|
49
|
+
if (!existsSync(file)) return null;
|
|
50
|
+
const path = `${file}.dsh-crew-backup-${Date.now()}`;
|
|
51
|
+
copyFileSync(file, path);
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizePath(value) {
|
|
56
|
+
if (typeof value !== 'string' || !value.trim()) return null;
|
|
57
|
+
try {
|
|
58
|
+
const path = resolve(value);
|
|
59
|
+
return process.platform === 'win32' ? path.toLowerCase() : path;
|
|
60
|
+
} catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function serverTarget(server) {
|
|
64
|
+
if (!server || typeof server !== 'object') return null;
|
|
65
|
+
if (server.command !== 'node' || !Array.isArray(server.args) || typeof server.args[0] !== 'string') return null;
|
|
66
|
+
return normalizePath(server.args[0]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function nativeServers(config) {
|
|
70
|
+
const servers = config?.mcp?.servers;
|
|
71
|
+
return servers && typeof servers === 'object' && !Array.isArray(servers) ? servers : {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sharedServers(config) {
|
|
75
|
+
const servers = config?.mcpServers;
|
|
76
|
+
return servers && typeof servers === 'object' && !Array.isArray(servers) ? servers : {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function configSource({ home = homedir() } = {}) {
|
|
80
|
+
const nativeFile = join(home, '.zcode', 'cli', 'config.json');
|
|
81
|
+
const sharedFile = join(home, '.agents', 'mcp.json');
|
|
82
|
+
const native = readJson(nativeFile, {});
|
|
83
|
+
const shared = readJson(sharedFile, {});
|
|
84
|
+
const nativeMap = nativeServers(native);
|
|
85
|
+
const sharedMap = sharedServers(shared);
|
|
86
|
+
// ZCode shadows .agents/mcp.json whenever its native config has at least
|
|
87
|
+
// one server. If native is empty, the shared file is the least-surprising
|
|
88
|
+
// compatibility path for users who already manage MCP centrally.
|
|
89
|
+
if (Object.keys(nativeMap).length > 0 || Object.keys(sharedMap).length === 0) {
|
|
90
|
+
return { file: nativeFile, kind: 'native', config: native, servers: nativeMap };
|
|
91
|
+
}
|
|
92
|
+
return { file: sharedFile, kind: 'shared', config: shared, servers: sharedMap };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveZCodeMcpTarget({ home = homedir() } = {}) {
|
|
96
|
+
const source = configSource({ home });
|
|
97
|
+
return serverTarget(source.servers[SERVER]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function expectedTarget({ root = ROOT } = {}) {
|
|
101
|
+
return resolve(join(root, 'src', 'server.mjs'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function managedPolicyBlock(root) {
|
|
105
|
+
const template = readText(join(root, 'zcode', 'AGENTS.md'))?.trim();
|
|
106
|
+
return template ? `${POLICY_START}\n${template}\n${POLICY_END}` : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function installPolicy({ home, root }) {
|
|
110
|
+
const file = join(home, '.zcode', 'AGENTS.md');
|
|
111
|
+
const block = managedPolicyBlock(root);
|
|
112
|
+
if (!block) return { ok: false, code: 'ZCODE_POLICY_TEMPLATE_MISSING' };
|
|
113
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
114
|
+
const current = readText(file) ?? '';
|
|
115
|
+
const escapedStart = POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
116
|
+
const escapedEnd = POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
117
|
+
const managed = new RegExp(`${escapedStart}[\\s\\S]*?${escapedEnd}`, 'm');
|
|
118
|
+
const next = managed.test(current)
|
|
119
|
+
? current.replace(managed, block)
|
|
120
|
+
: `${current.trimEnd()}${current.trim() ? '\n\n' : ''}${block}\n`;
|
|
121
|
+
if (next !== current) { backup(file); writeFileSync(file, next); }
|
|
122
|
+
return { ok: true, file };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function removePolicy({ home }) {
|
|
126
|
+
const file = join(home, '.zcode', 'AGENTS.md');
|
|
127
|
+
const current = readText(file);
|
|
128
|
+
if (typeof current !== 'string') return false;
|
|
129
|
+
const start = POLICY_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
130
|
+
const end = POLICY_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
131
|
+
const managed = new RegExp(`(?:\\r?\\n){0,2}${start}[\\s\\S]*?${end}(?:\\r?\\n)?`, 'm');
|
|
132
|
+
if (!managed.test(current)) return false;
|
|
133
|
+
const next = current.replace(managed, '').trimEnd();
|
|
134
|
+
backup(file);
|
|
135
|
+
if (next.trim()) writeFileSync(file, `${next}\n`);
|
|
136
|
+
else rmSync(file, { force: true });
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function ownership({ home = homedir() } = {}) {
|
|
141
|
+
const data = readJson(OWNERSHIP_FILE({ home }), null);
|
|
142
|
+
return data && typeof data === 'object' && data.host === HOST ? data : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function writeOwnership({ home, configFile, configKind, target, files = [] }) {
|
|
146
|
+
writeJson(OWNERSHIP_FILE({ home }), {
|
|
147
|
+
schema_version: 1,
|
|
148
|
+
host: HOST,
|
|
149
|
+
server: SERVER,
|
|
150
|
+
config_file: configFile,
|
|
151
|
+
config_kind: configKind,
|
|
152
|
+
target,
|
|
153
|
+
files,
|
|
154
|
+
managed_at: new Date().toISOString(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function templateFiles({ home, root }) {
|
|
159
|
+
return [
|
|
160
|
+
['agents', 'ds-worker.md'],
|
|
161
|
+
['agents', 'ds-reviewer.md'],
|
|
162
|
+
['commands', 'dsh-config.md'],
|
|
163
|
+
['commands', 'dsh-status.md'],
|
|
164
|
+
].map(([dir, file]) => ({ source: join(root, 'zcode', dir, file), dest: join(home, '.zcode', dir, file) }));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function installTemplates({ home, root, priorFiles = [] }) {
|
|
168
|
+
const actions = [];
|
|
169
|
+
const records = [];
|
|
170
|
+
for (const { source, dest } of templateFiles({ home, root })) {
|
|
171
|
+
if (!existsSync(source)) return { ok: false, code: 'ZCODE_TEMPLATE_MISSING', source };
|
|
172
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
173
|
+
const before = readText(dest);
|
|
174
|
+
const rendered = readFileSync(source, 'utf8');
|
|
175
|
+
const previous = priorFiles.find((entry) => entry?.path === dest);
|
|
176
|
+
let backupFile = previous?.backup ?? null;
|
|
177
|
+
if (before !== rendered) {
|
|
178
|
+
if (before !== null && !backupFile) backupFile = backup(dest);
|
|
179
|
+
writeFileSync(dest, rendered);
|
|
180
|
+
}
|
|
181
|
+
actions.push(dest);
|
|
182
|
+
records.push({
|
|
183
|
+
path: dest,
|
|
184
|
+
preexisting: previous?.preexisting === true || (previous === undefined && before !== null),
|
|
185
|
+
...(backupFile ? { backup: backupFile } : {}),
|
|
186
|
+
managed_sha256: contentHash(rendered),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return { ok: true, actions, records };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function updateMcp({ home, root }) {
|
|
193
|
+
const source = configSource({ home });
|
|
194
|
+
const target = expectedTarget({ root });
|
|
195
|
+
const current = source.servers[SERVER];
|
|
196
|
+
const currentTarget = serverTarget(current);
|
|
197
|
+
const owned = ownership({ home });
|
|
198
|
+
if (current && currentTarget !== target) {
|
|
199
|
+
// Only an exact previously-owned entry may be repaired. Any foreign
|
|
200
|
+
// command, even if it is also named dsh-crew, is a hard collision.
|
|
201
|
+
if (!(owned && owned.config_file === source.file && normalizePath(owned.target) === currentTarget)) {
|
|
202
|
+
return { ok: false, code: 'ZCODE_MCP_COLLISION', config_file: source.file };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const next = { ...source.config };
|
|
207
|
+
if (source.kind === 'native') next.mcp = { ...(next.mcp ?? {}), servers: { ...source.servers, [SERVER]: { command: 'node', args: [target] } } };
|
|
208
|
+
else next.mcpServers = { ...source.servers, [SERVER]: { command: 'node', args: [target] } };
|
|
209
|
+
const changed = JSON.stringify(next) !== JSON.stringify(source.config);
|
|
210
|
+
if (changed) { backup(source.file); writeJson(source.file, next); }
|
|
211
|
+
return { ok: true, changed, config_file: source.file, config_kind: source.kind, target };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function installZCode({ home = homedir(), root = ROOT } = {}) {
|
|
215
|
+
const templates = templateFiles({ home, root });
|
|
216
|
+
const missing = templates.find(({ source }) => !existsSync(source));
|
|
217
|
+
if (missing) return { ok: false, code: 'ZCODE_TEMPLATE_MISSING', source: missing.source };
|
|
218
|
+
const priorFiles = ownership({ home })?.files ?? [];
|
|
219
|
+
// Check for collisions before writing any other integration surface so a
|
|
220
|
+
// failed install is transaction-like and leaves user files untouched.
|
|
221
|
+
const mcp = updateMcp({ home, root });
|
|
222
|
+
if (!mcp.ok) return mcp;
|
|
223
|
+
const policy = installPolicy({ home, root });
|
|
224
|
+
if (!policy.ok) return policy;
|
|
225
|
+
const installed = installTemplates({ home, root, priorFiles });
|
|
226
|
+
if (!installed.ok) return installed;
|
|
227
|
+
writeOwnership({
|
|
228
|
+
home,
|
|
229
|
+
configFile: mcp.config_file,
|
|
230
|
+
configKind: mcp.config_kind,
|
|
231
|
+
target: mcp.target,
|
|
232
|
+
files: installed.records,
|
|
233
|
+
});
|
|
234
|
+
return { ok: true, ...mcp, policy_file: policy.file, files: installed.actions };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function zcodeComponents({ home = homedir(), root = ROOT } = {}) {
|
|
238
|
+
const expected = normalizePath(expectedTarget({ root }));
|
|
239
|
+
const source = configSource({ home });
|
|
240
|
+
const configured = serverTarget(source.servers[SERVER]);
|
|
241
|
+
const owned = ownership({ home });
|
|
242
|
+
const components = {
|
|
243
|
+
mcp: configured === expected,
|
|
244
|
+
policy: typeof readText(join(home, '.zcode', 'AGENTS.md')) === 'string' && readText(join(home, '.zcode', 'AGENTS.md')).includes(POLICY_START),
|
|
245
|
+
worker_agent: existsSync(join(home, '.zcode', 'agents', 'ds-worker.md')),
|
|
246
|
+
reviewer_agent: existsSync(join(home, '.zcode', 'agents', 'ds-reviewer.md')),
|
|
247
|
+
config_prompt: existsSync(join(home, '.zcode', 'commands', 'dsh-config.md')),
|
|
248
|
+
status_prompt: existsSync(join(home, '.zcode', 'commands', 'dsh-status.md')),
|
|
249
|
+
ownership: !!owned && owned.config_file === source.file && normalizePath(owned.target) === expected,
|
|
250
|
+
};
|
|
251
|
+
return { components, source, expected, configured, owned };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function zcodeStatus({ home = homedir(), root = ROOT } = {}) {
|
|
255
|
+
const { components, source, expected, configured } = zcodeComponents({ home, root });
|
|
256
|
+
const missing = Object.entries(components).filter(([, value]) => !value).map(([key]) => key);
|
|
257
|
+
const installed = Object.values(components).some(Boolean);
|
|
258
|
+
return {
|
|
259
|
+
installed,
|
|
260
|
+
ready: missing.length === 0,
|
|
261
|
+
components,
|
|
262
|
+
missing,
|
|
263
|
+
config_file: source.file,
|
|
264
|
+
config_kind: source.kind,
|
|
265
|
+
target: configured ?? expected,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function uninstallZCode({ home = homedir() } = {}) {
|
|
270
|
+
const actions = [];
|
|
271
|
+
const owned = ownership({ home });
|
|
272
|
+
if (owned) {
|
|
273
|
+
const file = owned.config_file;
|
|
274
|
+
const config = readJson(file, null);
|
|
275
|
+
if (config && typeof config === 'object') {
|
|
276
|
+
const kind = owned.config_kind === 'shared' ? 'shared' : 'native';
|
|
277
|
+
const servers = kind === 'shared' ? sharedServers(config) : nativeServers(config);
|
|
278
|
+
const current = servers[SERVER];
|
|
279
|
+
if (normalizePath(owned.target) === serverTarget(current)) {
|
|
280
|
+
const next = { ...config };
|
|
281
|
+
if (kind === 'shared') {
|
|
282
|
+
next.mcpServers = { ...servers }; delete next.mcpServers[SERVER];
|
|
283
|
+
if (Object.keys(next.mcpServers).length === 0) delete next.mcpServers;
|
|
284
|
+
} else {
|
|
285
|
+
next.mcp = { ...(next.mcp ?? {}), servers: { ...servers } }; delete next.mcp.servers[SERVER];
|
|
286
|
+
if (Object.keys(next.mcp.servers).length === 0) delete next.mcp.servers;
|
|
287
|
+
if (Object.keys(next.mcp).length === 0) delete next.mcp;
|
|
288
|
+
}
|
|
289
|
+
backup(file); writeJson(file, next); actions.push(`mcp: removed ${SERVER} from ${file}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
rmSync(OWNERSHIP_FILE({ home }), { force: true });
|
|
293
|
+
}
|
|
294
|
+
if (removePolicy({ home })) actions.push('policy: removed managed ZCode block');
|
|
295
|
+
const managedFiles = Array.isArray(owned?.files) && owned.files.length
|
|
296
|
+
? owned.files
|
|
297
|
+
: ['agents/ds-worker.md', 'agents/ds-reviewer.md', 'commands/dsh-config.md', 'commands/dsh-status.md']
|
|
298
|
+
.map((rel) => ({ path: join(home, '.zcode', rel) }));
|
|
299
|
+
for (const entry of managedFiles) {
|
|
300
|
+
const file = entry?.path;
|
|
301
|
+
if (!file || !existsSync(file)) continue;
|
|
302
|
+
const current = readText(file);
|
|
303
|
+
const unchanged = !entry.managed_sha256 || (current !== null && contentHash(current) === entry.managed_sha256);
|
|
304
|
+
if (!unchanged) continue;
|
|
305
|
+
if (entry.preexisting && entry.backup && existsSync(entry.backup)) {
|
|
306
|
+
copyFileSync(entry.backup, file);
|
|
307
|
+
actions.push(`restored: ${file}`);
|
|
308
|
+
} else {
|
|
309
|
+
rmSync(file, { force: true });
|
|
310
|
+
actions.push(`removed: ${file}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { ok: true, actions };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function zcodeOwnershipFile({ home = homedir() } = {}) { return OWNERSHIP_FILE({ home }); }
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Keep this module pure and dependency-free so Hub, MCP and tests all use the
|
|
9
9
|
// exact same compatibility rules.
|
|
10
10
|
|
|
11
|
-
export const RUNTIME_VERSION = '0.5.
|
|
11
|
+
export const RUNTIME_VERSION = '0.5.2';
|
|
12
12
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
13
13
|
|
|
14
14
|
export const HUB_CAPABILITIES = Object.freeze([
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal EnableExtensions
|
|
3
|
+
title DSH Crew Launcher
|
|
4
|
+
|
|
5
|
+
set "CREW_HOME=%USERPROFILE%\.config\dsh-crew\harness"
|
|
6
|
+
set "OFFICIAL_HOME=%USERPROFILE%\.dsh"
|
|
7
|
+
set "DSH_CLI=%CREW_HOME%\runtime\node_modules\.bin\dsh.cmd"
|
|
8
|
+
set "CREW_URL=http://127.0.0.1:3210"
|
|
9
|
+
set "UI_URL=http://127.0.0.1:3080"
|
|
10
|
+
set "LAUNCH_LOG=%TEMP%\dsh-crew-launcher.log"
|
|
11
|
+
|
|
12
|
+
if not exist "%DSH_CLI%" goto :not_installed
|
|
13
|
+
if not exist "%CREW_HOME%\profiles\dsh-crew\package.json" goto :not_installed
|
|
14
|
+
if not exist "%OFFICIAL_HOME%\profiles\web\package.json" goto :official_missing
|
|
15
|
+
|
|
16
|
+
call :ensure_service 3210 dsh-crew "%CREW_HOME%" "%CREW_URL%"
|
|
17
|
+
if errorlevel 1 goto :failed
|
|
18
|
+
call :ensure_service 3080 web "%OFFICIAL_HOME%" "%UI_URL%"
|
|
19
|
+
if errorlevel 1 goto :failed
|
|
20
|
+
exit /b 0
|
|
21
|
+
|
|
22
|
+
:ensure_service
|
|
23
|
+
set "LAUNCH_PORT=%~1"
|
|
24
|
+
set "LAUNCH_PROFILE=%~2"
|
|
25
|
+
set "LAUNCH_HOME=%~3"
|
|
26
|
+
set "LAUNCH_URL=%~4"
|
|
27
|
+
|
|
28
|
+
call :health_check "%LAUNCH_URL%"
|
|
29
|
+
if not errorlevel 1 exit /b 0
|
|
30
|
+
|
|
31
|
+
powershell.exe -NoLogo -NoProfile -NonInteractive -Command "$listener=Get-NetTCPConnection -State Listen -LocalPort $env:LAUNCH_PORT -ErrorAction SilentlyContinue; if ($listener) { exit 0 } else { exit 1 }" >nul 2>&1
|
|
32
|
+
if not errorlevel 1 exit /b 1
|
|
33
|
+
|
|
34
|
+
powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -Command "try { $env:DSH_HOME=$env:LAUNCH_HOME; Start-Process -FilePath $env:DSH_CLI -ArgumentList @('--profile',$env:LAUNCH_PROFILE,'--host','127.0.0.1','--port',$env:LAUNCH_PORT,'--no-open') -WindowStyle Hidden -ErrorAction Stop } catch { ('['+(Get-Date -Format s)+'] Failed to start '+$env:LAUNCH_PROFILE+': '+$_.Exception.Message) | Add-Content -LiteralPath $env:LAUNCH_LOG; exit 1 }" >nul 2>&1
|
|
35
|
+
if errorlevel 1 exit /b 1
|
|
36
|
+
|
|
37
|
+
powershell.exe -NoLogo -NoProfile -NonInteractive -Command "$lastError=$null; $deadline=(Get-Date).AddSeconds(90); do { try { $r=Invoke-RestMethod -Uri ($env:LAUNCH_URL+'/_dsh/dsh-crew/extension') -TimeoutSec 2; if ($r.ok -eq $true -and $r.extension.runtime.runtime_version) { exit 0 } } catch { $lastError=$_.Exception.Message }; Start-Sleep -Milliseconds 500 } while ((Get-Date) -lt $deadline); if (-not $lastError) { $lastError='No healthy response before the startup deadline.' }; ('['+(Get-Date -Format s)+'] '+$env:LAUNCH_PROFILE+' health check failed: '+$lastError) | Add-Content -LiteralPath $env:LAUNCH_LOG; exit 1" >nul 2>&1
|
|
38
|
+
exit /b %ERRORLEVEL%
|
|
39
|
+
|
|
40
|
+
:health_check
|
|
41
|
+
set "HEALTH_URL=%~1"
|
|
42
|
+
powershell.exe -NoLogo -NoProfile -NonInteractive -Command "try { $r=Invoke-RestMethod -Uri ($env:HEALTH_URL+'/_dsh/dsh-crew/extension') -TimeoutSec 2; if ($r.ok -eq $true -and $r.extension.runtime.runtime_version) { exit 0 } } catch {}; exit 1" >nul 2>&1
|
|
43
|
+
exit /b %ERRORLEVEL%
|
|
44
|
+
|
|
45
|
+
:not_installed
|
|
46
|
+
echo [%date% %time%] DSH Crew is not installed completely.>>"%LAUNCH_LOG%"
|
|
47
|
+
exit /b 1
|
|
48
|
+
|
|
49
|
+
:official_missing
|
|
50
|
+
echo [%date% %time%] Official DeepSeek Harness web profile was not found.>>"%LAUNCH_LOG%"
|
|
51
|
+
exit /b 1
|
|
52
|
+
|
|
53
|
+
:failed
|
|
54
|
+
echo [%date% %time%] DSH Crew startup failed.>>"%LAUNCH_LOG%"
|
|
55
|
+
exit /b 1
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Option Explicit
|
|
2
|
+
|
|
3
|
+
Dim shell, launcher, command
|
|
4
|
+
Set shell = CreateObject("WScript.Shell")
|
|
5
|
+
launcher = "__LAUNCHER__"
|
|
6
|
+
command = shell.ExpandEnvironmentStrings("%COMSPEC%") & " /d /c " & _
|
|
7
|
+
Chr(34) & Chr(34) & launcher & Chr(34) & Chr(34)
|
|
8
|
+
|
|
9
|
+
shell.Run command, 0, False
|
package/zcode/AGENTS.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Global capability-aware delegation policy for ZCode
|
|
2
|
+
|
|
3
|
+
ZCode is a host adapter for DSH Crew. Use the `dsh-crew` MCP server for Crew
|
|
4
|
+
work only after its live capability and readiness surfaces have been checked.
|
|
5
|
+
|
|
6
|
+
- Discover the current Crew configuration, capabilities, activation state and
|
|
7
|
+
readiness before delegating substantial work. Installed, configured, enabled
|
|
8
|
+
and callable are different states; do not infer one from another.
|
|
9
|
+
- Match a bounded work unit to an available Crew role/model and preserve the
|
|
10
|
+
repository/worktree and Result Contract boundaries. Keep planning, ambiguous
|
|
11
|
+
requirements, integration, external side effects and final communication in
|
|
12
|
+
the host agent.
|
|
13
|
+
- If Crew is selected and any required capability is unavailable, non-callable,
|
|
14
|
+
or returns an unknown/runtime/configuration/credential/routing/timeout error,
|
|
15
|
+
pause. Report the evidence and wait for the operator to choose repair Crew or
|
|
16
|
+
continue locally; never silently fall back or retry blindly.
|
|
17
|
+
- Validate returned evidence, changed scope, tests and completion state before
|
|
18
|
+
accepting delegated work. Do not expose credentials or raw provider payloads.
|
|
19
|
+
|
|
20
|
+
This file is installed as a managed block in `~/.zcode/AGENTS.md`; user-authored
|
|
21
|
+
instructions outside the block are preserved.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ds-reviewer
|
|
3
|
+
description: Independent DSH Crew reviewer dispatcher. Inspect completed changes read-only and return a structured verdict.
|
|
4
|
+
mcpServers:
|
|
5
|
+
- dsh-crew
|
|
6
|
+
tools:
|
|
7
|
+
- mcp__dsh-crew__dsh_run_worker
|
|
8
|
+
- mcp__dsh-crew__dsh_worker_status
|
|
9
|
+
- mcp__dsh-crew__dsh_worker_result
|
|
10
|
+
- mcp__dsh-crew__dsh_worker_cancel
|
|
11
|
+
- mcp__dsh-crew__dsh_worker_config
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
You are a thin, read-only dispatcher. Never edit files or implement fixes.
|
|
15
|
+
|
|
16
|
+
Pass the review request verbatim to `dsh_run_worker` with role `reviewer` and
|
|
17
|
+
the current workspace as `cwd`; omit effort unless explicitly requested. Wait
|
|
18
|
+
for the result and return its Review Findings, Evidence, Risks and Verdict.
|
|
19
|
+
Treat failing tests or incomplete evidence as not approved.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ds-worker
|
|
3
|
+
description: DSH Crew worker dispatcher for implementation, fixes, tests, search and analysis. Never implement locally; return the auditable Crew result.
|
|
4
|
+
mcpServers:
|
|
5
|
+
- dsh-crew
|
|
6
|
+
tools:
|
|
7
|
+
- mcp__dsh-crew__dsh_run_worker
|
|
8
|
+
- mcp__dsh-crew__dsh_spawn_worker
|
|
9
|
+
- mcp__dsh-crew__dsh_worker_status
|
|
10
|
+
- mcp__dsh-crew__dsh_worker_result
|
|
11
|
+
- mcp__dsh-crew__dsh_worker_cancel
|
|
12
|
+
- mcp__dsh-crew__dsh_worker_config
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
You are a thin dispatcher. You never do the task yourself.
|
|
16
|
+
|
|
17
|
+
Pass the task verbatim to `dsh_run_worker` with role `worker` and the current
|
|
18
|
+
workspace as `cwd`; omit effort unless the task explicitly requests it. Wait
|
|
19
|
+
for the result. If it is `done`, return the result and its evidence footer. If
|
|
20
|
+
it is not done, report the error and stop reason clearly and stop.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Parse arguments after this command as key=value pairs and call the
|
|
2
|
+
`dsh_worker_config` tool. Supported keys include enabled, tier, effort, mode,
|
|
3
|
+
timeout, policy, escalate, collab, main, flash, pro, review and reset. With no
|
|
4
|
+
arguments read the current configuration. Show one compact table including
|
|
5
|
+
hub_reachable, effective flash/pro state, effective policy and routing guidance.
|
|
6
|
+
Name changed fields. Reply in the user's language and do nothing else.
|