@ran-sh/dsh-crew 1.0.2 → 1.1.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.
Files changed (40) hide show
  1. package/README.md +4 -0
  2. package/README.zh.md +4 -0
  3. package/codex/AGENTS.md +101 -92
  4. package/docs/ui-surfaces.md +48 -21
  5. package/lib/client.js +235 -34
  6. package/official-web-bridge/lib/client.js +368 -4532
  7. package/official-web-bridge/package.json +1 -2
  8. package/package.json +130 -67
  9. package/scripts/build-client.mjs +30 -15
  10. package/scripts/remove-legacy-official-bridge.ps1 +89 -0
  11. package/scripts/setup.mjs +152 -140
  12. package/scripts/verify-npm-install.mjs +311 -310
  13. package/scripts/verify-official-bridge-e2e.mjs +12 -1
  14. package/src/client/host-readiness.mjs +62 -59
  15. package/src/client/index.tsx +117 -24
  16. package/src/client/quick-entry.tsx +10 -0
  17. package/src/client/quick-panel.tsx +295 -0
  18. package/src/client/surface-detection.mjs +43 -30
  19. package/src/credential-reference.mjs +38 -0
  20. package/src/dsh-cli-runtime.mjs +500 -40
  21. package/src/dsh-cohort.mjs +20 -0
  22. package/src/hub/index.mjs +1602 -1016
  23. package/src/install/npx-lifecycle.mjs +1983 -468
  24. package/src/install/official-web.mjs +36 -73
  25. package/src/jobs.mjs +20 -24
  26. package/src/model-catalog.mjs +8 -1
  27. package/src/official-web-bridge.mjs +329 -249
  28. package/src/provider-delete-adapters.mjs +165 -17
  29. package/src/provider-inventory.mjs +17 -3
  30. package/src/provider-layer-migration-adapters.mjs +759 -0
  31. package/src/provider-layer-migration.mjs +198 -0
  32. package/src/provider-lifecycle-state.mjs +4 -1
  33. package/src/provider-profile-store.mjs +193 -4
  34. package/src/provider-settings-store.mjs +82 -6
  35. package/src/provider-store-lock.mjs +67 -0
  36. package/src/runtime-identity.mjs +24 -1
  37. package/src/supervisor/restart-request.mjs +202 -0
  38. package/windows/start-dsh-crew.ps1 +783 -370
  39. package/worker.cordis.yml +32 -46
  40. package/zcode/AGENTS.md +35 -26
@@ -0,0 +1,295 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ // DSH Crew QUICK CONTROLS panel for the official 3080 surface.
4
+ //
5
+ // Narrow by design: only the user-facing master switch, flash/pro model
6
+ // priority lists and vision/imagegen toggles — all other operations belong
7
+ // to the native 3210 full control plane. Talks only to the quick endpoints
8
+ // (/_dsh/dsh-crew/quick-config, /quick-status, /runtime/restart-request,
9
+ // /runtime/restart-status). NEVER writes to any other surface.
10
+
11
+ const API = '/_dsh/dsh-crew';
12
+ const CREW_CONTROL_PLANE_URL = 'http://127.0.0.1:3210/';
13
+ const FULL = 'http://127.0.0.1:3210/';
14
+
15
+ type ModelEntry = { provider: string; model: string };
16
+ type QuickConfig = {
17
+ subagents_enabled?: boolean;
18
+ flash_model_priority?: ModelEntry[];
19
+ pro_model_priority?: ModelEntry[];
20
+ vision_enabled?: boolean;
21
+ imagegen_enabled?: boolean;
22
+ vision_provider?: string;
23
+ imagegen_provider?: string;
24
+ };
25
+
26
+ const RESTART_KEYS = new Set([
27
+ 'vision_enabled',
28
+ 'imagegen_enabled',
29
+ 'vision_provider',
30
+ 'imagegen_provider',
31
+ ]);
32
+
33
+ const T = {
34
+ zh: {
35
+ title: 'DSH Crew 快捷控制',
36
+ openFull: '打开完整设置 →',
37
+ running: '运行中',
38
+ unavailable: 'Crew 后端不可用',
39
+ openDiag: '打开诊断',
40
+ crew: 'Crew',
41
+ enabled: '启用子 Agent',
42
+ flash: 'Flash 模型',
43
+ pro: 'Pro 模型',
44
+ addModel: '+ 添加模型',
45
+ multimodal: '多模态',
46
+ vision: '视觉',
47
+ imagegen: '生图',
48
+ provider: 'Provider',
49
+ applyRestart: '应用并重启 Crew',
50
+ savedNeedsRestart: '配置已保存 · 需要重启 Crew 才会生效',
51
+ saved: '已保存',
52
+ working: '处理中…',
53
+ providerPlaceholder: 'provider',
54
+ modelPlaceholder: 'model',
55
+ },
56
+ en: {
57
+ title: 'DSH Crew Quick Controls',
58
+ openFull: 'Open full settings →',
59
+ running: 'Running',
60
+ unavailable: 'Crew backend unavailable',
61
+ openDiag: 'Open diagnostics',
62
+ crew: 'Crew',
63
+ enabled: 'Enable sub-agents',
64
+ flash: 'Flash models',
65
+ pro: 'Pro models',
66
+ addModel: '+ Add model',
67
+ multimodal: 'Multimodal',
68
+ vision: 'Vision',
69
+ imagegen: 'Imagegen',
70
+ provider: 'Provider',
71
+ applyRestart: 'Apply & restart Crew',
72
+ savedNeedsRestart: 'Saved · restart Crew to take effect',
73
+ saved: 'Saved',
74
+ working: 'Working…',
75
+ providerPlaceholder: 'provider',
76
+ modelPlaceholder: 'model',
77
+ },
78
+ };
79
+
80
+ function LocalStyles() {
81
+ return (
82
+ <style>{`
83
+ .crew-quick-card { border: 1px solid rgba(128,128,128,0.24); border-radius: 12px; padding: 14px 15px; font-size: 13px; line-height: 1.55; display: flex; flex-direction: column; gap: 10px; background: linear-gradient(135deg, rgba(74,158,255,0.10), rgba(128,128,128,0.025) 56%); }
84
+ .crew-quick-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
85
+ .crew-quick-title { font-size: 16px; font-weight: 680; }
86
+ .crew-quick-section { font-weight: 650; opacity: 0.85; }
87
+ .crew-quick-chip { border: 1px solid rgba(128,128,128,0.35); border-radius: 999px; padding: 1px 10px; font-size: 12px; }
88
+ .crew-quick-btn { border: 1px solid rgba(128,128,128,0.4); border-radius: 8px; padding: 2px 10px; cursor: pointer; background: transparent; font-size: 12.5px; }
89
+ .crew-quick-btn.primary { border-color: #4a9eff; color: #4a9eff; font-weight: 650; }
90
+ .crew-quick-input { border: 1px solid rgba(128,128,128,0.35); border-radius: 6px; padding: 2px 8px; font-size: 12.5px; width: 130px; background: transparent; color: inherit; }
91
+ .crew-quick-notice { opacity: 0.75; font-size: 12.5px; }
92
+ .crew-quick-model { display: flex; gap: 6px; align-items: center; font-size: 12.5px; }
93
+ `}</style>
94
+ );
95
+ }
96
+
97
+ async function readJson(res: Response) {
98
+ if (!res.ok) return { ok: false, status: res.status };
99
+ try { return await res.json(); } catch { return { ok: false }; }
100
+ }
101
+
102
+ export function QuickPanel({ ctx }: { ctx: any }) {
103
+ const locale = ctx?.locale?.getLocale?.().active === 'zh' ? 'zh' : 'en';
104
+ const t = (T as any)[locale] ?? T.zh;
105
+ const [config, setConfig] = useState<QuickConfig | null>(null);
106
+ const [ready, setReady] = useState<boolean | null>(null);
107
+ const [notice, setNotice] = useState('');
108
+ const [busy, setBusy] = useState(false);
109
+ const [restartPending, setRestartPending] = useState(false);
110
+ const [drafts, setDrafts] = useState<Record<string, ModelEntry>>({});
111
+
112
+ const load = useCallback(async () => {
113
+ try {
114
+ const status = await readJson(await fetch(`${API}/quick-status`, { cache: 'no-store' }));
115
+ if (!status.ok) { setReady(false); return; }
116
+ setReady(true);
117
+ setConfig(status.config ?? {});
118
+ } catch { setReady(false); }
119
+ }, []);
120
+
121
+ useEffect(() => { void load(); }, [load]);
122
+
123
+ const patch = useCallback(async (next: QuickConfig) => {
124
+ setBusy(true); setNotice(t.working);
125
+ try {
126
+ const res = await readJson(await fetch(`${API}/quick-config`, {
127
+ method: 'POST', headers: { 'content-type': 'application/json' },
128
+ body: JSON.stringify(next),
129
+ }));
130
+ if (!res.ok) throw new Error(res.code ?? res.error ?? 'save failed');
131
+ setConfig(res.config ?? {});
132
+ const needsRestart = Object.keys(next).some((k) => RESTART_KEYS.has(k));
133
+ if (needsRestart) { setRestartPending(true); setNotice(t.savedNeedsRestart); }
134
+ else { setNotice(t.saved); setTimeout(() => setNotice(''), 1500); }
135
+ return true;
136
+ } catch (e: any) {
137
+ setNotice(String(e?.message ?? e));
138
+ return false;
139
+ } finally { setBusy(false); }
140
+ }, [t]);
141
+
142
+ const toggle = (key: 'subagents_enabled' | 'vision_enabled' | 'imagegen_enabled', value: boolean) => {
143
+ setConfig((c) => ({ ...(c ?? {}), [key]: value }));
144
+ void patch({ [key]: value } as QuickConfig);
145
+ };
146
+
147
+ const setProvider = (key: 'vision_provider' | 'imagegen_provider', value: string) => {
148
+ setConfig((c) => ({ ...(c ?? {}), [key]: value }));
149
+ void patch({ [key]: value } as QuickConfig);
150
+ };
151
+
152
+ const moveModel = (listKey: 'flash_model_priority' | 'pro_model_priority', index: number, dir: -1 | 1) => {
153
+ const list = [...((config?.[listKey] as ModelEntry[] | undefined) ?? [])];
154
+ const to = index + dir;
155
+ if (to < 0 || to >= list.length) return;
156
+ [list[index], list[to]] = [list[to], list[index]];
157
+ setConfig((c) => ({ ...(c ?? {}), [listKey]: list }));
158
+ void patch({ [listKey]: list } as unknown as QuickConfig);
159
+ };
160
+
161
+ const removeModel = (listKey: 'flash_model_priority' | 'pro_model_priority', index: number) => {
162
+ const list = [...((config?.[listKey] as ModelEntry[] | undefined) ?? [])];
163
+ list.splice(index, 1);
164
+ setConfig((c) => ({ ...(c ?? {}), [listKey]: list }));
165
+ void patch({ [listKey]: list } as unknown as QuickConfig);
166
+ };
167
+
168
+ const addModel = (listKey: 'flash_model_priority' | 'pro_model_priority') => {
169
+ const draft = drafts[listKey] ?? { provider: '', model: '' };
170
+ if (!draft.provider.trim() || !draft.model.trim()) return;
171
+ const list = [...((config?.[listKey] as ModelEntry[] | undefined) ?? []), { provider: draft.provider.trim(), model: draft.model.trim() }];
172
+ setConfig((c) => ({ ...(c ?? {}), [listKey]: list }));
173
+ setDrafts((d) => ({ ...d, [listKey]: { provider: '', model: '' } }));
174
+ void patch({ [listKey]: list } as unknown as QuickConfig);
175
+ };
176
+
177
+ const applyRestart = useCallback(async () => {
178
+ setBusy(true); setNotice(t.working);
179
+ try {
180
+ const created = await readJson(await fetch(`${API}/runtime/restart-request`, {
181
+ method: 'POST', headers: { 'content-type': 'application/json' },
182
+ body: JSON.stringify({ confirm: true, reason: 'quick panel restart' }),
183
+ }));
184
+ if (!created.ok) throw new Error(created.code ?? created.error ?? 'restart failed');
185
+ const requestId = created.request_id;
186
+ const deadline = Date.now() + 90_000;
187
+ for (;;) {
188
+ await new Promise((r) => setTimeout(r, 1000));
189
+ if (Date.now() > deadline) throw new Error('restart timed out');
190
+ const status = await readJson(await fetch(`${API}/runtime/restart-status?id=${encodeURIComponent(requestId)}`, { cache: 'no-store' }));
191
+ if (!status.ok) continue;
192
+ if (status.state === 'VERIFIED') { setRestartPending(false); setNotice(t.saved); return; }
193
+ if (status.state !== 'RESTART_REQUESTED') throw new Error(status.state ?? 'restart failed');
194
+ }
195
+ } catch (e: any) {
196
+ setNotice(String(e?.message ?? e));
197
+ } finally { setBusy(false); }
198
+ }, [t]);
199
+
200
+ const modelList = (listKey: 'flash_model_priority' | 'pro_model_priority', label: string) => {
201
+ const list: ModelEntry[] = (config?.[listKey] as ModelEntry[] | undefined) ?? [];
202
+ const draft = drafts[listKey] ?? { provider: '', model: '' };
203
+ return (
204
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
205
+ <div className="crew-quick-section">{label}</div>
206
+ {list.map((entry, i) => (
207
+ <div key={`${entry.provider}/${entry.model}/${i}`} className="crew-quick-model">
208
+ <span>{i + 1}. {entry.provider} / {entry.model}</span>
209
+ <button className="crew-quick-btn" disabled={busy || i === 0} onClick={() => moveModel(listKey, i, -1)}>↑</button>
210
+ <button className="crew-quick-btn" disabled={busy || i === list.length - 1} onClick={() => moveModel(listKey, i, 1)}>↓</button>
211
+ <button className="crew-quick-btn" disabled={busy} onClick={() => removeModel(listKey, i)}>×</button>
212
+ </div>
213
+ ))}
214
+ <div className="crew-quick-row">
215
+ <input className="crew-quick-input" placeholder={t.providerPlaceholder} disabled={busy}
216
+ value={draft.provider} onChange={(e) => setDrafts((d) => ({ ...d, [listKey]: { ...draft, provider: e.target.value } }))} />
217
+ <input className="crew-quick-input" placeholder={t.modelPlaceholder} disabled={busy}
218
+ value={draft.model} onChange={(e) => setDrafts((d) => ({ ...d, [listKey]: { ...draft, model: e.target.value } }))} />
219
+ <button className="crew-quick-btn" disabled={busy} onClick={() => addModel(listKey)}>{t.addModel}</button>
220
+ </div>
221
+ </div>
222
+ );
223
+ };
224
+
225
+ if (ready === false) {
226
+ return (
227
+ <div className="crew-quick-card">
228
+ <LocalStyles />
229
+ <div className="crew-quick-title">{t.title}</div>
230
+ <div className="crew-quick-notice">{t.unavailable}</div>
231
+ <div className="crew-quick-row">
232
+ <a href={FULL} target="_blank" rel="noreferrer">{t.openDiag}</a>
233
+ </div>
234
+ </div>
235
+ );
236
+ }
237
+ if (config === null) {
238
+ return (<div className="crew-quick-card"><LocalStyles /><div className="crew-quick-notice">{t.working}</div></div>);
239
+ }
240
+
241
+ return (
242
+ <div className="crew-quick-card">
243
+ <LocalStyles />
244
+ <div className="crew-quick-row" style={{ justifyContent: 'space-between' }}>
245
+ <span className="crew-quick-title">{t.title}</span>
246
+ <span className="crew-quick-chip">{t.running} · 3210</span>
247
+ <a href={FULL} target="_blank" rel="noreferrer">{t.openFull}</a>
248
+ </div>
249
+ <div className="crew-quick-row">
250
+ <span className="crew-quick-section">{t.crew}</span>
251
+ <label><input type="checkbox" disabled={busy} checked={config.subagents_enabled !== false}
252
+ onChange={(e) => toggle('subagents_enabled', e.target.checked)} /> {t.enabled}</label>
253
+ </div>
254
+ {modelList('flash_model_priority', t.flash)}
255
+ {modelList('pro_model_priority', t.pro)}
256
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
257
+ <div className="crew-quick-section">{t.multimodal}</div>
258
+ <div className="crew-quick-row">
259
+ <label><input type="checkbox" disabled={busy} checked={config.vision_enabled === true}
260
+ onChange={(e) => toggle('vision_enabled', e.target.checked)} /> {t.vision}</label>
261
+ <span>{t.provider}</span>
262
+ <input className="crew-quick-input" disabled={busy} defaultValue={config.vision_provider ?? ''} key={`vision-${config.vision_provider ?? ''}`}
263
+ onBlur={(e) => setProvider('vision_provider', e.target.value)} />
264
+ </div>
265
+ <div className="crew-quick-row">
266
+ <label><input type="checkbox" disabled={busy} checked={config.imagegen_enabled === true}
267
+ onChange={(e) => toggle('imagegen_enabled', e.target.checked)} /> {t.imagegen}</label>
268
+ <span>{t.provider}</span>
269
+ <input className="crew-quick-input" disabled={busy} defaultValue={config.imagegen_provider ?? ''}
270
+ onBlur={(e) => setProvider('imagegen_provider', e.target.value)} />
271
+ </div>
272
+ </div>
273
+ {restartPending && (
274
+ <div className="crew-quick-row">
275
+ <button className="crew-quick-btn primary" disabled={busy} onClick={() => void applyRestart()}>{t.applyRestart}</button>
276
+ </div>
277
+ )}
278
+ {notice && <div className="crew-quick-notice">{notice}</div>}
279
+ </div>
280
+ );
281
+ }
282
+
283
+ export function applyQuick(ctx: any): void {
284
+ ctx.slots.inject('settings.section', () => {
285
+ return ctx.slots.register(
286
+ {
287
+ name: 'settings.section',
288
+ id: 'dsh-crew-quick',
289
+ order: 65,
290
+ label: () => 'DSH Crew',
291
+ },
292
+ () => <QuickPanel ctx={ctx} />,
293
+ );
294
+ });
295
+ }
@@ -1,30 +1,43 @@
1
- export const CREW_UI_SURFACES = Object.freeze({
2
- OFFICIAL: 'official-bridge',
3
- NATIVE: 'native-crew-harness',
4
- UNKNOWN: 'unknown',
5
- });
6
-
7
- /**
8
- * Classify the current same-origin UI from explicit backend contracts. The
9
- * official bridge signal wins because its proxied runtime response correctly
10
- * describes the 3210 backend, not the browser-facing 3080 surface.
11
- */
12
- export function classifyCrewSurface({ bridgeStatus, runtime } = {}) {
13
- if (bridgeStatus?.ok === true
14
- && (bridgeStatus.surface === CREW_UI_SURFACES.OFFICIAL
15
- || bridgeStatus.mode === 'official-3080-isolated-3210')) {
16
- return CREW_UI_SURFACES.OFFICIAL;
17
- }
18
- if (runtime?.ok === true
19
- && runtime.service === 'dsh-crew-hub'
20
- && (runtime.surface === CREW_UI_SURFACES.NATIVE || runtime.surface === undefined)) {
21
- return CREW_UI_SURFACES.NATIVE;
22
- }
23
- return CREW_UI_SURFACES.UNKNOWN;
24
- }
25
-
26
- export function surfaceResponsibilities(surface) {
27
- const fullControlPlane = surface === CREW_UI_SURFACES.OFFICIAL;
28
- return { fullControlPlane, minimalDiagnostics: !fullControlPlane };
29
- }
30
-
1
+ export const CREW_UI_SURFACES = Object.freeze({
2
+ OFFICIAL: 'official-bridge',
3
+ NATIVE: 'native-crew-harness',
4
+ UNKNOWN: 'unknown',
5
+ });
6
+
7
+ /**
8
+ * Classify the current same-origin UI from explicit backend contracts. The
9
+ * official bridge signal wins because its proxied runtime response correctly
10
+ * describes the 3210 backend, not the browser-facing 3080 surface.
11
+ */
12
+ export function classifyCrewSurface({ bridgeStatus, runtime } = {}) {
13
+ if (bridgeStatus?.ok === true
14
+ && (bridgeStatus.surface === CREW_UI_SURFACES.OFFICIAL
15
+ || bridgeStatus.mode === 'official-3080-isolated-3210')) {
16
+ return CREW_UI_SURFACES.OFFICIAL;
17
+ }
18
+ if (runtime?.ok === true
19
+ && runtime.service === 'dsh-crew-hub'
20
+ && (runtime.surface === CREW_UI_SURFACES.NATIVE || runtime.surface === undefined)) {
21
+ return CREW_UI_SURFACES.NATIVE;
22
+ }
23
+ return CREW_UI_SURFACES.UNKNOWN;
24
+ }
25
+
26
+ /**
27
+ * Surface capability model. The NATIVE 3210 Crew harness is the single full
28
+ * control plane; the OFFICIAL 3080 surface is a narrow quick-controls panel
29
+ * (total switch, flash/pro model priority, vision/imagegen toggles) plus a
30
+ * deep link back to 3210. Unknown surfaces get diagnostics only — never write
31
+ * authority. This replaces the old binary full-vs-minimal assumption.
32
+ */
33
+ export function surfaceResponsibilities(surface) {
34
+ switch (surface) {
35
+ case CREW_UI_SURFACES.NATIVE:
36
+ return { fullControlPlane: true, quickControlPlane: true, diagnostics: true };
37
+ case CREW_UI_SURFACES.OFFICIAL:
38
+ return { fullControlPlane: false, quickControlPlane: true, diagnostics: true };
39
+ default:
40
+ return { fullControlPlane: false, quickControlPlane: false, diagnostics: true };
41
+ }
42
+ }
43
+
@@ -0,0 +1,38 @@
1
+ // Credential references are identifiers, never credential values. Keep this
2
+ // boundary strict because provider metadata is returned to the browser/UI.
3
+
4
+ const ENV_REFERENCE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/u;
5
+ const HANDLE_REFERENCE = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u;
6
+ const SECRET_LIKE = /^(?:sk|pk|rk|sess|tok|token|key|secret)[_-]/iu;
7
+ const EMBEDDED_SECRET = /(?:^|\s)(?:bearer|basic)\s+|(?:https?|data):|=/iu;
8
+
9
+ function rawText(value) {
10
+ if (typeof value === 'string') return value.trim() || null;
11
+ if (value && typeof value === 'object') {
12
+ const candidate = value.name_or_handle ?? value.name ?? value.reference;
13
+ return typeof candidate === 'string' ? candidate.trim() || null : null;
14
+ }
15
+ return null;
16
+ }
17
+
18
+ /**
19
+ * Classify a reference without returning an unsafe value. `kind: env` is the
20
+ * common apiKeyEnv form; non-env stores may use a bounded opaque handle.
21
+ */
22
+ export function classifyCredentialReference(value, { kind = 'env' } = {}) {
23
+ const raw = rawText(value);
24
+ if (!raw) return { present: false, value: null, redacted: false };
25
+ const normalizedKind = typeof kind === 'string' ? kind.trim().toLowerCase() : 'env';
26
+ const valid = normalizedKind === 'env'
27
+ ? ENV_REFERENCE.test(raw) && !SECRET_LIKE.test(raw) && !EMBEDDED_SECRET.test(raw)
28
+ : HANDLE_REFERENCE.test(raw) && !SECRET_LIKE.test(raw) && !EMBEDDED_SECRET.test(raw);
29
+ return { present: true, value: valid ? raw : null, redacted: !valid };
30
+ }
31
+
32
+ export function sanitizeCredentialReference(value, options = {}) {
33
+ return classifyCredentialReference(value, options).value;
34
+ }
35
+
36
+ export function hasCredentialReference(value, options = {}) {
37
+ return classifyCredentialReference(value, options).present;
38
+ }