@ran-sh/dsh-crew 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.de.md +359 -0
- package/README.es.md +359 -0
- package/README.fr.md +359 -0
- package/README.hi.md +359 -0
- package/README.id.md +359 -0
- package/README.ja.md +359 -0
- package/README.ko.md +359 -0
- package/README.md +360 -0
- package/README.pt.md +359 -0
- package/README.ru.md +359 -0
- package/README.th.md +359 -0
- package/README.tr.md +359 -0
- package/README.vi.md +359 -0
- package/README.zh-TW.md +359 -0
- package/README.zh.md +305 -0
- package/agents/ds-flash.md +26 -0
- package/agents/ds-pro.md +32 -0
- package/agents/ds-reviewer.md +23 -0
- package/agents/ds-worker.md +22 -0
- package/codex/agents/ds-flash.toml +30 -0
- package/codex/agents/ds-pro.toml +31 -0
- package/codex/agents/ds-reviewer.toml +28 -0
- package/codex/agents/ds-worker.toml +28 -0
- package/codex/prompts/dsh-config.md +3 -0
- package/codex/prompts/dsh-status.md +1 -0
- package/commands/config.md +11 -0
- package/commands/off.md +5 -0
- package/commands/on.md +5 -0
- package/commands/status.md +5 -0
- package/cordis.patch.yml +4 -0
- package/docs/images/dsh-crew-host.png +0 -0
- package/docs/images/dsh-crew-jobs.png +0 -0
- package/docs/images/dsh-crew-logo.png +0 -0
- package/docs/images/dsh-crew-overview.png +0 -0
- package/lib/client.js +2765 -0
- package/package.json +125 -0
- package/scripts/build-client.mjs +28 -0
- package/scripts/live-crew-smoke.mjs +39 -0
- package/scripts/live-policy-matrix.mjs +177 -0
- package/scripts/policy-probe.mjs +101 -0
- package/scripts/setup.mjs +294 -0
- package/scripts/smoke-real.mjs +110 -0
- package/scripts/smoke.mjs +78 -0
- package/scripts/verify-installer-fix.mjs +26 -0
- package/src/adaptive-routing.mjs +260 -0
- package/src/client/activation-summary.tsx +64 -0
- package/src/client/entry.tsx +236 -0
- package/src/client/index.tsx +1120 -0
- package/src/config-readiness.mjs +59 -0
- package/src/delivery.mjs +205 -0
- package/src/dsh-cli-runtime.mjs +251 -0
- package/src/failure-classification.mjs +172 -0
- package/src/hub/entry.mjs +98 -0
- package/src/hub/index.mjs +757 -0
- package/src/hub-client.mjs +132 -0
- package/src/hub-compatibility.mjs +49 -0
- package/src/i18n.mjs +19 -0
- package/src/install/cli.mjs +28 -0
- package/src/install/install-legacy.mjs +460 -0
- package/src/install/install.mjs +451 -0
- package/src/jobs.mjs +275 -0
- package/src/mcp-runtime.mjs +257 -0
- package/src/model-catalog.mjs +173 -0
- package/src/model-routing.mjs +391 -0
- package/src/multimodal.mjs +0 -0
- package/src/policy-legacy.mjs +830 -0
- package/src/policy.mjs +197 -0
- package/src/readiness-matrix.mjs +169 -0
- package/src/runtime-controls.mjs +90 -0
- package/src/runtime-identity.mjs +108 -0
- package/src/server.mjs +477 -0
- package/src/status-shard.mjs +52 -0
- package/src/structured-error-code.mjs +39 -0
- package/src/vision-route.mjs +138 -0
- package/src/workflow-runtime.mjs +567 -0
- package/src/workflow.mjs +160 -0
- package/src/workspace-audit.mjs +231 -0
- package/src/workspace-isolation.mjs +306 -0
- package/statusline/statusline.sh +14 -0
- package/statusline/worker-segment.sh +35 -0
- package/worker.cordis.yml +77 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// One-click installers: register the Claude Code plugin persistently and
|
|
2
|
+
// render Codex agent roles with real paths. Called from the CLI entry or the
|
|
3
|
+
// DSH settings page. All edits are backed up and idempotent.
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, readdirSync, rmSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve, relative } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { normalizeModelPriority } from '../model-routing.mjs';
|
|
10
|
+
|
|
11
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
12
|
+
const MARKETPLACE_NAME = 'dsh-crew';
|
|
13
|
+
const PLUGIN_KEY = `dsh-crew@${MARKETPLACE_NAME}`;
|
|
14
|
+
// dsh_worker_config is included so the session commands (/dsh-crew:config,
|
|
15
|
+
// /dsh-config) and any orchestrator policy lookup run without an extra
|
|
16
|
+
// authorization prompt.
|
|
17
|
+
export const MCP_TOOLS = ['dsh_run_worker', 'dsh_spawn_worker', 'dsh_worker_status', 'dsh_worker_result', 'dsh_worker_cancel', 'dsh_worker_config'];
|
|
18
|
+
|
|
19
|
+
function backup(file) {
|
|
20
|
+
if (existsSync(file)) {
|
|
21
|
+
const bak = `${file}.dsh-crew-backup-${Date.now()}`;
|
|
22
|
+
copyFileSync(file, bak);
|
|
23
|
+
return bak;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readJson(file, fallback) {
|
|
29
|
+
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return fallback; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// One-time migration from the pre-rename config dir (dsh-workers → dsh-crew).
|
|
33
|
+
try {
|
|
34
|
+
const oldDir = join(homedir(), '.config', 'dsh-workers');
|
|
35
|
+
const newDir = join(homedir(), '.config', 'dsh-crew');
|
|
36
|
+
if (existsSync(oldDir) && !existsSync(newDir)) {
|
|
37
|
+
const { renameSync } = await import('node:fs');
|
|
38
|
+
renameSync(oldDir, newDir);
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
|
|
42
|
+
const GLOBAL_CONFIG_FILE = join(CONFIG_DIR_HOME(), 'config.json');
|
|
43
|
+
function CONFIG_DIR_HOME() { return join(homedir(), '.config', 'dsh-crew'); }
|
|
44
|
+
|
|
45
|
+
export const GLOBAL_CONFIG_DEFAULTS = {
|
|
46
|
+
default_tier: 'flash',
|
|
47
|
+
default_effort: 'max',
|
|
48
|
+
mode: 'auto',
|
|
49
|
+
default_timeout_seconds: 1800,
|
|
50
|
+
hub_url: 'http://127.0.0.1:3080',
|
|
51
|
+
// auto = orchestrator picks the tier; flash-only / pro-only clamp every
|
|
52
|
+
// dispatch to one tier at the tool layer regardless of what was requested.
|
|
53
|
+
tier_policy: 'flash-only',
|
|
54
|
+
// When a blocking flash run fails, retry the same task once on pro.
|
|
55
|
+
escalate_on_failure: false,
|
|
56
|
+
// ---- configurable-crew orchestration (see src/policy.mjs) ----
|
|
57
|
+
// Master switch for the whole worker dispatch path (hard, backend-enforced).
|
|
58
|
+
subagents_enabled: true,
|
|
59
|
+
// flash-only | pro-only | balanced | review-pipeline | custom
|
|
60
|
+
collaboration_mode: 'flash-only',
|
|
61
|
+
// direct-allowed | coordinator-first | dispatcher-only (host routing
|
|
62
|
+
// guidance — the Crew MCP backend cannot restrict the host's own tools).
|
|
63
|
+
main_agent_mode: 'direct-allowed',
|
|
64
|
+
// Per-tier state: disabled | manual | auto. Owned by the collaboration
|
|
65
|
+
// preset unless collaboration_mode is "custom".
|
|
66
|
+
flash_state: 'auto',
|
|
67
|
+
pro_state: 'disabled',
|
|
68
|
+
// Roles per tier: routing guidance only, not a keyword classifier.
|
|
69
|
+
flash_roles: ['implementation', 'simple_fix', 'tests', 'search_inspection'],
|
|
70
|
+
pro_roles: ['architecture', 'complex_debugging', 'refactor', 'code_review', 'implementation'],
|
|
71
|
+
// Automatic Pro review after a successful Flash run (review-pipeline forces
|
|
72
|
+
// this on; balanced/custom only when this flag is set).
|
|
73
|
+
pro_reviews_flash: false,
|
|
74
|
+
// Worker provider routing for HUB workers: which DSH provider backs each
|
|
75
|
+
// worker session. follow-dsh = use the provider selected in DSH Models
|
|
76
|
+
// (Flash/Pro still map to deepseek-v4-flash / deepseek-v4-pro);
|
|
77
|
+
// deepseek-official = always use the built-in provider (legacy behavior,
|
|
78
|
+
// kept the default so upgraded configs never silently switch providers).
|
|
79
|
+
// Standalone mode always uses deepseek-official + DEEPSEEK_API_KEY.
|
|
80
|
+
worker_provider_mode: 'follow-dsh',
|
|
81
|
+
// Ordered Harness provider/model selections. The configured flags preserve
|
|
82
|
+
// the distinction between a fresh recommendation and a user-cleared list.
|
|
83
|
+
flash_model_priority: [],
|
|
84
|
+
flash_model_priority_configured: false,
|
|
85
|
+
flash_model_fallback: 'harness-default',
|
|
86
|
+
pro_model_priority: [],
|
|
87
|
+
pro_model_priority_configured: false,
|
|
88
|
+
pro_model_fallback: 'harness-default',
|
|
89
|
+
// ---- configurable-crew multimodal switches ----
|
|
90
|
+
// False = the Crew describe_image tool and vision route are not registered
|
|
91
|
+
// at DSH boot (takes effect after a DSH restart). Provider/model values are
|
|
92
|
+
// kept untouched so re-enabling restores the previous setup.
|
|
93
|
+
vision_enabled: false,
|
|
94
|
+
imagegen_enabled: false,
|
|
95
|
+
// Multimodal bridge: which subscription CLI lends the text-only DS model
|
|
96
|
+
// eyes (describe_image) and a brush (generate_image).
|
|
97
|
+
vision_provider: 'claude-code', // claude-code | codex | grok | agy | <custom id> | off
|
|
98
|
+
vision_model: 'haiku', // model passed to the vision CLI; free-form
|
|
99
|
+
imagegen_provider: 'codex', // codex | agy | grok | <custom id> | off
|
|
100
|
+
// User-defined providers, each either an OpenAI-compatible HTTP endpoint or a
|
|
101
|
+
// local command. Entry shape:
|
|
102
|
+
// { id, name, type: 'api' | 'cli', models: [],
|
|
103
|
+
// api: base_url, api_key, imagegen_model
|
|
104
|
+
// cli: vision_command, imagegen_command }
|
|
105
|
+
// CLI commands run via bash with shell-quoted placeholders: vision
|
|
106
|
+
// {image} {question} {model} (stdout is the answer), imagegen
|
|
107
|
+
// {prompt} {output} {size} (must write the file to {output}).
|
|
108
|
+
custom_providers: [],
|
|
109
|
+
// User-added model ids per provider, merged into the panel's model list.
|
|
110
|
+
extra_models: {},
|
|
111
|
+
// Hub-mode agent preset per tier: 'default' follows the DSH roster default.
|
|
112
|
+
// Harness Default uses the session-aware filesystem/shell stack. The DSH
|
|
113
|
+
// minimal preset's provider-native tools use the Host process cwd instead,
|
|
114
|
+
// so it is unsafe as the fresh default for jobs targeting another workspace.
|
|
115
|
+
preset_flash: 'default',
|
|
116
|
+
preset_pro: 'default',
|
|
117
|
+
// ---- v0.2 runtime execution (writable via the settings config endpoint) ----
|
|
118
|
+
// Concurrency cap for parallel workflows; isolation: worktree = run coding
|
|
119
|
+
// workers in per-job git worktrees (fail-closed for non-git workspaces),
|
|
120
|
+
// shared = legacy in-place behaviour.
|
|
121
|
+
max_parallel: 3,
|
|
122
|
+
isolation: 'worktree',
|
|
123
|
+
// ---- v0.2 role state overrides (writable; unset = derived by migration) ----
|
|
124
|
+
// Explicit worker/reviewer role states (auto | manual | disabled) and the
|
|
125
|
+
// automatic-review switch. When unset, migrateLegacyConfig derives them from
|
|
126
|
+
// collaboration_mode / tier_policy / pro_reviews_flash.
|
|
127
|
+
worker_state: undefined,
|
|
128
|
+
review_state: undefined,
|
|
129
|
+
auto_review: undefined,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export function mergeStoredGlobalConfig(stored) {
|
|
133
|
+
if (!stored || typeof stored !== 'object' || Array.isArray(stored)) return { ...GLOBAL_CONFIG_DEFAULTS };
|
|
134
|
+
const has = (key) => Object.prototype.hasOwnProperty.call(stored, key);
|
|
135
|
+
const merged = { ...GLOBAL_CONFIG_DEFAULTS, ...stored };
|
|
136
|
+
// Fields introduced by configurable Crew keep the prior release's behavior
|
|
137
|
+
// when an existing file predates them. Only a genuinely fresh config gets
|
|
138
|
+
// the new minimal defaults above.
|
|
139
|
+
if (!has('collaboration_mode')) {
|
|
140
|
+
merged.collaboration_mode = stored.tier_policy === 'flash-only' ? 'flash-only'
|
|
141
|
+
: stored.tier_policy === 'pro-only' ? 'pro-only' : 'balanced';
|
|
142
|
+
}
|
|
143
|
+
if (!has('flash_state') || !has('pro_state')) {
|
|
144
|
+
if (stored.tier_policy === 'flash-only') { merged.flash_state = 'auto'; merged.pro_state = 'disabled'; }
|
|
145
|
+
else if (stored.tier_policy === 'pro-only') { merged.flash_state = 'disabled'; merged.pro_state = 'auto'; }
|
|
146
|
+
else { merged.flash_state = 'auto'; merged.pro_state = 'auto'; }
|
|
147
|
+
}
|
|
148
|
+
if (!has('main_agent_mode')) merged.main_agent_mode = 'coordinator-first';
|
|
149
|
+
if (!has('worker_provider_mode')) merged.worker_provider_mode = 'deepseek-official';
|
|
150
|
+
// Existing configs that predate preset_flash should inherit the current safe
|
|
151
|
+
// Harness Default. The older implicit `minimal` migration is unsafe for
|
|
152
|
+
// workspace-targeted jobs on Windows; an explicit user-set `minimal` value
|
|
153
|
+
// is still preserved by the normal object merge above.
|
|
154
|
+
if (!has('preset_flash')) merged.preset_flash = 'default';
|
|
155
|
+
if (!has('vision_enabled')) merged.vision_enabled = stored.vision_provider !== 'off';
|
|
156
|
+
if (!has('imagegen_enabled')) merged.imagegen_enabled = stored.imagegen_provider !== 'off';
|
|
157
|
+
return merged;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function readGlobalConfig({ configFile = GLOBAL_CONFIG_FILE } = {}) {
|
|
161
|
+
if (!existsSync(configFile)) return { ...GLOBAL_CONFIG_DEFAULTS };
|
|
162
|
+
return mergeStoredGlobalConfig(readJson(configFile, {}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function writeGlobalConfig(patch) {
|
|
166
|
+
mkdirSync(CONFIG_DIR_HOME(), { recursive: true });
|
|
167
|
+
const next = { ...readGlobalConfig() };
|
|
168
|
+
for (const [k, v] of Object.entries(patch ?? {})) {
|
|
169
|
+
if (v !== undefined && k in GLOBAL_CONFIG_DEFAULTS) next[k] = v;
|
|
170
|
+
}
|
|
171
|
+
for (const tier of ['flash', 'pro']) {
|
|
172
|
+
const key = `${tier}_model_priority`;
|
|
173
|
+
if (patch?.[key] !== undefined) {
|
|
174
|
+
next[key] = normalizeModelPriority(patch[key]);
|
|
175
|
+
next[`${tier}_model_priority_configured`] = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Legacy clamp fields stay writable (session commands still read them), and
|
|
179
|
+
// a tier_policy write resyncs the new fields so old UIs can't drift apart
|
|
180
|
+
// from the new policy. Deprecated but harmless.
|
|
181
|
+
if (patch?.tier_policy !== undefined) {
|
|
182
|
+
next.tier_policy = patch.tier_policy;
|
|
183
|
+
if (patch.tier_policy === 'flash-only') { next.collaboration_mode = 'flash-only'; }
|
|
184
|
+
else if (patch.tier_policy === 'pro-only') { next.collaboration_mode = 'pro-only'; }
|
|
185
|
+
else if (next.collaboration_mode === 'flash-only' || next.collaboration_mode === 'pro-only') { next.collaboration_mode = 'balanced'; }
|
|
186
|
+
}
|
|
187
|
+
writeFileSync(GLOBAL_CONFIG_FILE, JSON.stringify(next, null, 2) + '\n');
|
|
188
|
+
return next;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** What is currently installed where — drives the settings-page buttons. */
|
|
192
|
+
export function installStatus({ home = homedir() } = {}) {
|
|
193
|
+
const settings = readJson(join(home, '.claude', 'settings.json'), {});
|
|
194
|
+
const enabled = settings.enabledPlugins;
|
|
195
|
+
const claudeInstalled = !!(enabled && !Array.isArray(enabled) && enabled[PLUGIN_KEY]);
|
|
196
|
+
const hudWired = typeof settings.statusLine?.command === 'string'
|
|
197
|
+
&& settings.statusLine.command.includes('worker-segment.sh');
|
|
198
|
+
const codexInstalled = existsSync(join(home, '.codex', 'agents', 'ds-flash.toml'))
|
|
199
|
+
|| existsSync(join(home, '.codex', 'agents', 'ds-worker.toml'));
|
|
200
|
+
return { claude: { installed: claudeInstalled, hud: hudWired }, codex: { installed: codexInstalled } };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function uninstallCodex({ home = homedir() } = {}) {
|
|
204
|
+
const actions = [];
|
|
205
|
+
// Both the v0.2 roles (ds-worker / ds-reviewer) and the deprecated v0.1
|
|
206
|
+
// aliases (ds-flash / ds-pro) are dsh-crew managed; uninstall removes only
|
|
207
|
+
// these, never a user's own role files.
|
|
208
|
+
for (const f of ['ds-flash.toml', 'ds-pro.toml', 'ds-worker.toml', 'ds-reviewer.toml']) {
|
|
209
|
+
const p = join(home, '.codex', 'agents', f);
|
|
210
|
+
if (existsSync(p)) { backup(p); rmSync(p); actions.push(`removed: ${p} (backup kept)`); }
|
|
211
|
+
}
|
|
212
|
+
for (const f of ['dsh-config.md', 'dsh-status.md']) {
|
|
213
|
+
const p = join(home, '.codex', 'prompts', f);
|
|
214
|
+
if (existsSync(p)) { rmSync(p); actions.push(`removed: ${p}`); }
|
|
215
|
+
}
|
|
216
|
+
// Remove only the dsh-crew entry from [mcp_servers], keeping any other
|
|
217
|
+
// MCP servers the user configured.
|
|
218
|
+
const configFile = join(home, '.codex', 'config.toml');
|
|
219
|
+
if (existsSync(configFile)) {
|
|
220
|
+
const before = readFileSync(configFile, 'utf8');
|
|
221
|
+
const lineRe = /(^|\n)[ \t]*dsh-crew\s*=\s*\{[^\n]*\}/;
|
|
222
|
+
const after = lineRe.test(before) ? before.replace(lineRe, '') : before;
|
|
223
|
+
if (after !== before) {
|
|
224
|
+
backup(configFile);
|
|
225
|
+
writeFileSync(configFile, after.replace(/(^|\n){2,}/g, '\n\n').replace(/^\[mcp_servers\]\n?$/, ''));
|
|
226
|
+
actions.push(`codex config: removed dsh-crew MCP entry`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { ok: true, actions: actions.length ? actions : ['nothing to remove'] };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function installClaudeCode({ home = homedir(), statusline = false } = {}) {
|
|
233
|
+
const actions = [];
|
|
234
|
+
|
|
235
|
+
// The repository carries its own marketplace manifest (.claude-plugin/
|
|
236
|
+
// marketplace.json with source "."), so the marketplace root IS the plugin
|
|
237
|
+
// checkout itself — no parent-directory layout assumption. This keeps the
|
|
238
|
+
// installer working when the repo is cloned anywhere (e.g. Desktop\dsh-crew).
|
|
239
|
+
const mpDir = ROOT;
|
|
240
|
+
actions.push(`marketplace: ${mpDir} (self-marketplace, source .)`);
|
|
241
|
+
|
|
242
|
+
// 2. settings.json: marketplace + enabledPlugins + permissions allowlist.
|
|
243
|
+
const settingsFile = join(home, '.claude', 'settings.json');
|
|
244
|
+
mkdirSync(dirname(settingsFile), { recursive: true });
|
|
245
|
+
const bak = backup(settingsFile);
|
|
246
|
+
if (bak) actions.push(`backup: ${bak}`);
|
|
247
|
+
const settings = readJson(settingsFile, {});
|
|
248
|
+
|
|
249
|
+
// Both fields are records (see json.schemastore.org/claude-code-settings.json).
|
|
250
|
+
// Older versions of this installer wrote arrays, which Claude Code ignores
|
|
251
|
+
// with a warning — migrate those in place.
|
|
252
|
+
const markets = (settings.extraKnownMarketplaces && !Array.isArray(settings.extraKnownMarketplaces)
|
|
253
|
+
&& typeof settings.extraKnownMarketplaces === 'object') ? settings.extraKnownMarketplaces : {};
|
|
254
|
+
if (Array.isArray(settings.extraKnownMarketplaces)) actions.push('migrated legacy extraKnownMarketplaces array');
|
|
255
|
+
markets[MARKETPLACE_NAME] = { source: { source: 'directory', path: mpDir } };
|
|
256
|
+
if (markets['dsh-workers']) {
|
|
257
|
+
delete markets['dsh-workers'];
|
|
258
|
+
actions.push('removed pre-rename dsh-workers marketplace entry');
|
|
259
|
+
}
|
|
260
|
+
settings.extraKnownMarketplaces = markets;
|
|
261
|
+
|
|
262
|
+
const enabled = (settings.enabledPlugins && !Array.isArray(settings.enabledPlugins)
|
|
263
|
+
&& typeof settings.enabledPlugins === 'object') ? settings.enabledPlugins : {};
|
|
264
|
+
if (Array.isArray(settings.enabledPlugins)) {
|
|
265
|
+
for (const key of settings.enabledPlugins) if (typeof key === 'string') enabled[key] = true;
|
|
266
|
+
actions.push('migrated legacy enabledPlugins array');
|
|
267
|
+
}
|
|
268
|
+
enabled[PLUGIN_KEY] = true;
|
|
269
|
+
if (enabled['dsh-workers@dsh-workers']) {
|
|
270
|
+
delete enabled['dsh-workers@dsh-workers'];
|
|
271
|
+
actions.push('removed pre-rename dsh-workers plugin entry');
|
|
272
|
+
}
|
|
273
|
+
settings.enabledPlugins = enabled;
|
|
274
|
+
|
|
275
|
+
settings.permissions = settings.permissions ?? {};
|
|
276
|
+
let allow = Array.isArray(settings.permissions.allow) ? settings.permissions.allow : [];
|
|
277
|
+
const preRename = allow.filter((r) => typeof r === 'string' && r.startsWith('mcp__plugin_dsh-workers_'));
|
|
278
|
+
if (preRename.length) {
|
|
279
|
+
allow = allow.filter((r) => !preRename.includes(r));
|
|
280
|
+
actions.push(`removed ${preRename.length} pre-rename permission rules`);
|
|
281
|
+
}
|
|
282
|
+
for (const tool of MCP_TOOLS) {
|
|
283
|
+
const rule = `mcp__plugin_dsh-crew_dsh-crew__${tool}`;
|
|
284
|
+
if (!allow.includes(rule)) allow.push(rule);
|
|
285
|
+
}
|
|
286
|
+
settings.permissions.allow = allow;
|
|
287
|
+
|
|
288
|
+
if (statusline && !settings.statusLine) {
|
|
289
|
+
settings.statusLine = { type: 'command', command: `bash ${join(ROOT, 'statusline', 'statusline.sh')}` };
|
|
290
|
+
actions.push('statusline: installed');
|
|
291
|
+
} else if (statusline) {
|
|
292
|
+
actions.push('statusline: skipped (one already configured)');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
296
|
+
actions.push(`settings: registered ${PLUGIN_KEY} + ${MCP_TOOLS.length} permission rules`);
|
|
297
|
+
|
|
298
|
+
// Materialize the install through the claude CLI: registers the marketplace
|
|
299
|
+
// in the plugin cache (settings alone leave a stale/absent cache entry) and
|
|
300
|
+
// pulls the plugin so the next session loads it. Best-effort: settings are
|
|
301
|
+
// already correct, so a missing CLI just means one manual `plugin install`.
|
|
302
|
+
if (home !== homedir()) {
|
|
303
|
+
actions.push('cli: skipped (non-default home; test mode)');
|
|
304
|
+
return { ok: true, actions };
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const { execSync } = await import('node:child_process');
|
|
308
|
+
const run = (cmd) => execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 120_000 });
|
|
309
|
+
try { run(`claude plugin marketplace add ${JSON.stringify(mpDir)}`); actions.push('cli: marketplace registered'); }
|
|
310
|
+
catch { actions.push('cli: marketplace add skipped (already registered)'); }
|
|
311
|
+
// `plugin install` on an already-installed plugin is a no-op and leaves a
|
|
312
|
+
// stale snapshot in ~/.claude/plugins/cache — uninstall first so an update
|
|
313
|
+
// always re-copies the current code.
|
|
314
|
+
try { run(`claude plugin uninstall ${PLUGIN_KEY}`); } catch {}
|
|
315
|
+
// Newer Claude Code (>= 2.1.x) dropped the -y flag; older builds accepted
|
|
316
|
+
// it. Try without it first, fall back to the legacy flag.
|
|
317
|
+
try {
|
|
318
|
+
run(`claude plugin install ${PLUGIN_KEY} --scope user`);
|
|
319
|
+
} catch (errNoFlag) {
|
|
320
|
+
if (!String(errNoFlag?.message ?? '').includes('unknown option')) throw errNoFlag;
|
|
321
|
+
run(`claude plugin install ${PLUGIN_KEY} --scope user -y`);
|
|
322
|
+
}
|
|
323
|
+
actions.push(`cli: plugin snapshot refreshed (${PLUGIN_KEY})`);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
actions.push(`cli: plugin install failed — run manually: claude plugin install ${PLUGIN_KEY} (${String(err?.message ?? err).slice(0, 120)})`);
|
|
326
|
+
}
|
|
327
|
+
return { ok: true, actions };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function installCodex({ home = homedir(), scope } = {}) {
|
|
331
|
+
const actions = [];
|
|
332
|
+
const agentsDir = scope === 'project' ? join(process.cwd(), '.codex', 'agents') : join(home, '.codex', 'agents');
|
|
333
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
334
|
+
const srcDir = join(ROOT, 'codex', 'agents');
|
|
335
|
+
// Windows drive paths must render with forward slashes: backslashes in a
|
|
336
|
+
// TOML basic string are escape sequences (\\U\\u... are invalid), which made
|
|
337
|
+
// the role files unparseable for Codex Desktop. node accepts forward slashes
|
|
338
|
+
// on Windows, so the absolute path rendered as D:/... is both valid TOML and
|
|
339
|
+
// runnable.
|
|
340
|
+
const renderedPath = join(ROOT, 'src', 'server.mjs').replace(/\\/g, '/');
|
|
341
|
+
for (const f of readdirSync(srcDir).filter((f) => f.endsWith('.toml'))) {
|
|
342
|
+
const dest = join(agentsDir, f);
|
|
343
|
+
const bak = backup(dest);
|
|
344
|
+
if (bak) actions.push(`backup: ${bak}`);
|
|
345
|
+
const rendered = readFileSync(join(srcDir, f), 'utf8')
|
|
346
|
+
.replace(/args = \[.*server\.mjs"\]/, `args = ["${renderedPath}"]`);
|
|
347
|
+
writeFileSync(dest, rendered);
|
|
348
|
+
actions.push(`role: ${dest}`);
|
|
349
|
+
}
|
|
350
|
+
const promptsDir = scope === 'project' ? join(process.cwd(), '.codex', 'prompts') : join(home, '.codex', 'prompts');
|
|
351
|
+
mkdirSync(promptsDir, { recursive: true });
|
|
352
|
+
const promptsSrc = join(ROOT, 'codex', 'prompts');
|
|
353
|
+
for (const f of readdirSync(promptsSrc).filter((f) => f.endsWith('.md'))) {
|
|
354
|
+
writeFileSync(join(promptsDir, f), readFileSync(join(promptsSrc, f), 'utf8'));
|
|
355
|
+
actions.push(`prompt: ${join(promptsDir, f)}`);
|
|
356
|
+
}
|
|
357
|
+
if (scope !== 'project') {
|
|
358
|
+
const act = writeGlobalCodexMcpServer(home, renderedPath);
|
|
359
|
+
actions.push(...act);
|
|
360
|
+
}
|
|
361
|
+
return { ok: true, actions };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Codex Desktop reads the shared config at ~/.codex/config.toml, and the role
|
|
366
|
+
* files alone only expose dsh MCP tools inside the ds-flash/ds-pro subagents.
|
|
367
|
+
* To make dsh_worker_config (policy) visible to the MAIN Codex session, add a
|
|
368
|
+
* top-level [mcp_servers] entry under dsh-crew — preserving any other servers
|
|
369
|
+
* the user already configured. Idempotent (updates in place). Never requires
|
|
370
|
+
* the codex CLI.
|
|
371
|
+
*/
|
|
372
|
+
export function writeGlobalCodexMcpServer(home, renderedPath) {
|
|
373
|
+
const configFile = join(home, '.codex', 'config.toml');
|
|
374
|
+
const entry = `dsh-crew = { command = "node", args = ["${renderedPath}"] }`;
|
|
375
|
+
const backupFile = backup(configFile);
|
|
376
|
+
const existing = existsSync(configFile) ? readFileSync(configFile, 'utf8') : '';
|
|
377
|
+
const hasSection = /(^|\n)\s*\[mcp_servers\]/.test(existing);
|
|
378
|
+
const sectionIdx = hasSection ? existing.search(/(^|\n)\s*\[mcp_servers\]/) : -1;
|
|
379
|
+
|
|
380
|
+
let next;
|
|
381
|
+
if (hasSection) {
|
|
382
|
+
// Section exists: find its extent (next top-level [section]) and patch the
|
|
383
|
+
// dsh-crew entry within it.
|
|
384
|
+
const afterHeader = sectionIdx + existing.slice(sectionIdx).indexOf(']') + 1;
|
|
385
|
+
const rest = existing.slice(afterHeader);
|
|
386
|
+
const nextHeader = rest.search(/(^|\n)\s*\[[^\]]+\]/);
|
|
387
|
+
const sectionBody = nextHeader === -1 ? rest : rest.slice(0, nextHeader);
|
|
388
|
+
const tail = nextHeader === -1 ? '' : rest.slice(nextHeader);
|
|
389
|
+
const lineRe = /(^|\n)([ \t]*)dsh-crew\s*=\s*\{[^\n]*\}/;
|
|
390
|
+
const patched = lineRe.test(sectionBody)
|
|
391
|
+
? sectionBody.replace(lineRe, `$1$2${entry}`)
|
|
392
|
+
: `${sectionBody.replace(/\s*$/, '')}\n${entry}`;
|
|
393
|
+
next = existing.slice(0, afterHeader) + patched + tail;
|
|
394
|
+
} else {
|
|
395
|
+
// No [mcp_servers] section: append a fresh one.
|
|
396
|
+
const sep = existing && !/[\n]$/.test(existing) ? '\n' : '';
|
|
397
|
+
next = `${existing}${sep}[mcp_servers]\n${entry}\n`;
|
|
398
|
+
}
|
|
399
|
+
writeFileSync(configFile, next);
|
|
400
|
+
const actions = [];
|
|
401
|
+
if (backupFile) actions.push(`config backup: ${backupFile}`);
|
|
402
|
+
actions.push(`codex config: [mcp_servers] dsh-crew → ${renderedPath}`);
|
|
403
|
+
return actions;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function installHudSegment({ home = homedir() } = {}) {
|
|
407
|
+
const settingsFile = join(home, '.claude', 'settings.json');
|
|
408
|
+
const settings = readJson(settingsFile, null);
|
|
409
|
+
if (!settings) return { ok: false, actions: ['settings.json not found'] };
|
|
410
|
+
const cmd = settings.statusLine?.command;
|
|
411
|
+
if (typeof cmd !== 'string' || !cmd.includes('claude-hud')) {
|
|
412
|
+
return { ok: false, actions: ['statusLine is not claude-hud; use statusline/statusline.sh directly instead'] };
|
|
413
|
+
}
|
|
414
|
+
const segment = join(ROOT, 'statusline', 'worker-segment.sh');
|
|
415
|
+
if (cmd.includes(segment)) return { ok: true, actions: ['already wired'] };
|
|
416
|
+
if (cmd.includes('worker-segment.sh')) {
|
|
417
|
+
// Wired to a stale copy (e.g. pre-rename path) — repoint the --extra-cmd at the current segment.
|
|
418
|
+
const next = cmd.replace(/--extra-cmd "bash [^"]*worker-segment\.sh"/, `--extra-cmd "bash ${segment}"`);
|
|
419
|
+
if (next === cmd) return { ok: false, actions: ['worker-segment.sh present but path not replaceable; rewire manually'] };
|
|
420
|
+
const bak = backup(settingsFile);
|
|
421
|
+
settings.statusLine.command = next;
|
|
422
|
+
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
423
|
+
return { ok: true, actions: [...(bak ? [`backup: ${bak}`] : []), `statusLine: repointed worker-segment.sh to ${segment}`] };
|
|
424
|
+
}
|
|
425
|
+
let next = cmd;
|
|
426
|
+
if (!next.includes('CLAUDE_HUD_ALLOW_EXTRA_CMD')) {
|
|
427
|
+
next = next.replace(/exec\s+/, 'exec env CLAUDE_HUD_ALLOW_EXTRA_CMD=1 ');
|
|
428
|
+
if (!next.includes('CLAUDE_HUD_ALLOW_EXTRA_CMD')) return { ok: false, actions: ['could not find exec in statusLine command; wire manually'] };
|
|
429
|
+
}
|
|
430
|
+
const extra = ` --extra-cmd "bash ${segment}"`;
|
|
431
|
+
if (next.endsWith("'")) next = next.slice(0, -1) + extra + "'";
|
|
432
|
+
else next += extra;
|
|
433
|
+
const bak = backup(settingsFile);
|
|
434
|
+
settings.statusLine.command = next;
|
|
435
|
+
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
436
|
+
return { ok: true, actions: [...(bak ? [`backup: ${bak}`] : []), 'statusLine: claude-hud now runs worker-segment.sh via --extra-cmd'] };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function uninstallClaudeCode({ home = homedir() } = {}) {
|
|
440
|
+
const settingsFile = join(home, '.claude', 'settings.json');
|
|
441
|
+
const settings = readJson(settingsFile, null);
|
|
442
|
+
if (!settings) return { ok: true, actions: ['settings.json not found'] };
|
|
443
|
+
backup(settingsFile);
|
|
444
|
+
const mpDir = join(home, '.config', 'dsh-crew', 'marketplace');
|
|
445
|
+
if (Array.isArray(settings.extraKnownMarketplaces)) {
|
|
446
|
+
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces.filter((m) => m?.path !== mpDir);
|
|
447
|
+
} else if (settings.extraKnownMarketplaces) {
|
|
448
|
+
delete settings.extraKnownMarketplaces[MARKETPLACE_NAME];
|
|
449
|
+
}
|
|
450
|
+
if (Array.isArray(settings.enabledPlugins)) {
|
|
451
|
+
settings.enabledPlugins = settings.enabledPlugins.filter((p) => p !== PLUGIN_KEY);
|
|
452
|
+
} else if (settings.enabledPlugins) {
|
|
453
|
+
delete settings.enabledPlugins[PLUGIN_KEY];
|
|
454
|
+
}
|
|
455
|
+
if (settings.permissions?.allow) {
|
|
456
|
+
settings.permissions.allow = settings.permissions.allow.filter((r) => !r.startsWith('mcp__plugin_dsh-crew_'));
|
|
457
|
+
}
|
|
458
|
+
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
459
|
+
return { ok: true, actions: ['unregistered from settings.json (backup kept)'] };
|
|
460
|
+
}
|