@ran-sh/dsh-crew 0.4.2 → 0.5.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.
@@ -0,0 +1,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
+ export function surfaceResponsibilities(surface) {
27
+ const fullControlPlane = surface === CREW_UI_SURFACES.OFFICIAL;
28
+ return { fullControlPlane, minimalDiagnostics: !fullControlPlane };
29
+ }
30
+
@@ -0,0 +1,72 @@
1
+ function clean(value, fallback) {
2
+ const text = typeof value === 'string' ? value.trim() : '';
3
+ return text || fallback;
4
+ }
5
+
6
+ function validTimestamp(value) {
7
+ if (typeof value !== 'string' || !Number.isFinite(Date.parse(value))) return null;
8
+ return value;
9
+ }
10
+
11
+ function sorted(values) {
12
+ return [...values].sort((left, right) => left.localeCompare(right));
13
+ }
14
+
15
+ export const MAX_MODEL_INVOCATION_JOBS = 500;
16
+ export const MAX_MODEL_ACTIVITY_ROWS = 50;
17
+
18
+ function hasInvocationEvidence(job) {
19
+ const tokenCount = Number(job?.tokens?.input ?? 0) + Number(job?.tokens?.output ?? 0);
20
+ return Number(job?.turn ?? 0) > 0 || Number(job?.toolCalls ?? 0) > 0 || tokenCount > 0;
21
+ }
22
+
23
+ /**
24
+ * Build a bounded, presentation-only model activity summary from the jobs the
25
+ * Hub already exposes. No prompts, results, credentials, or new persistence
26
+ * are introduced here.
27
+ */
28
+ export function aggregateModelInvocations(jobs = []) {
29
+ const groups = new Map();
30
+ const recentJobs = Array.isArray(jobs) ? jobs.slice(-MAX_MODEL_INVOCATION_JOBS) : [];
31
+ for (const job of recentJobs) {
32
+ const provider = clean(job?.provider, '');
33
+ const model = clean(job?.model, '');
34
+ if (!provider || !model || !hasInvocationEvidence(job)) continue;
35
+ const key = `${provider}\0${model}`;
36
+ const current = groups.get(key) ?? {
37
+ provider,
38
+ model,
39
+ count: 0,
40
+ taskSources: new Set(),
41
+ selectionSources: new Set(),
42
+ roles: new Set(),
43
+ lastCalledAt: null,
44
+ };
45
+ current.count += 1;
46
+ current.taskSources.add(clean(job?.source, 'api'));
47
+ current.selectionSources.add(clean(job?.selection_source, 'unknown'));
48
+ current.roles.add(job?.role === 'reviewer' ? 'reviewer' : 'worker');
49
+ const calledAt = validTimestamp(job?.startedAt);
50
+ if (calledAt && (!current.lastCalledAt || Date.parse(calledAt) > Date.parse(current.lastCalledAt))) {
51
+ current.lastCalledAt = calledAt;
52
+ }
53
+ groups.set(key, current);
54
+ }
55
+
56
+ return [...groups.values()]
57
+ .sort((left, right) => {
58
+ const timeDelta = (right.lastCalledAt ? Date.parse(right.lastCalledAt) : -1)
59
+ - (left.lastCalledAt ? Date.parse(left.lastCalledAt) : -1);
60
+ return timeDelta || `${left.provider}/${left.model}`.localeCompare(`${right.provider}/${right.model}`);
61
+ })
62
+ .slice(0, MAX_MODEL_ACTIVITY_ROWS)
63
+ .map((entry) => ({
64
+ provider: entry.provider,
65
+ model: entry.model,
66
+ count: entry.count,
67
+ task_sources: sorted(entry.taskSources),
68
+ selection_sources: sorted(entry.selectionSources),
69
+ roles: sorted(entry.roles),
70
+ last_called_at: entry.lastCalledAt,
71
+ }));
72
+ }
@@ -2,7 +2,7 @@
2
2
  // render Codex agent roles with real paths. Called from the CLI entry or the
3
3
  // DSH settings page. All edits are backed up and idempotent.
4
4
 
5
- import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, readdirSync, rmSync } from 'node:fs';
5
+ import { readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync, readdirSync, rmSync, statSync } from 'node:fs';
6
6
  import { dirname, join, resolve, relative } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { homedir } from 'node:os';
@@ -25,9 +25,76 @@ function backup(file) {
25
25
  return null;
26
26
  }
27
27
 
28
- function readJson(file, fallback) {
29
- try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return fallback; }
30
- }
28
+ function readJson(file, fallback) {
29
+ try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return fallback; }
30
+ }
31
+
32
+ function readText(file) {
33
+ try { return readFileSync(file, 'utf8'); } catch { return null; }
34
+ }
35
+
36
+ function tomlSection(text, name) {
37
+ if (typeof text !== 'string') return null;
38
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
+ const header = new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`, 'm').exec(text);
40
+ if (!header) return null;
41
+ const rest = text.slice(header.index + header[0].length);
42
+ const nextHeader = rest.search(/^\s*\[[^\]\r\n]+\]\s*(?:#.*)?$/m);
43
+ return nextHeader === -1 ? rest : rest.slice(0, nextHeader);
44
+ }
45
+
46
+ function existingServerTarget(block) {
47
+ if (typeof block !== 'string' || !/^\s*command\s*=\s*"node"\s*$/m.test(block)) return null;
48
+ const match = /^\s*args\s*=\s*\[\s*"([^"\r\n]*server\.mjs)"\s*\]\s*$/m.exec(block);
49
+ if (!match) return null;
50
+ try {
51
+ if (!statSync(match[1]).isFile()) return null;
52
+ const target = resolve(match[1]);
53
+ return process.platform === 'win32' ? target.toLowerCase() : target;
54
+ } catch { return null; }
55
+ }
56
+
57
+ function codexRoleTarget(file, expectedName) {
58
+ const text = readText(file);
59
+ if (typeof text !== 'string'
60
+ || !new RegExp(`^\\s*name\\s*=\\s*"${expectedName}"\\s*$`, 'm').test(text)) return null;
61
+ return existingServerTarget(tomlSection(text, 'mcp_servers.dsh-crew'));
62
+ }
63
+
64
+ function codexMcpTarget(configText) {
65
+ const section = tomlSection(configText, 'mcp_servers');
66
+ if (!section) return null;
67
+ const entry = /^\s*dsh-crew\s*=\s*\{\s*command\s*=\s*"node"\s*,\s*args\s*=\s*\[\s*"([^"\r\n]*server\.mjs)"\s*\]\s*\}\s*(?:#.*)?$/m.exec(section);
68
+ if (!entry) return null;
69
+ try {
70
+ if (!statSync(entry[1]).isFile()) return null;
71
+ const target = resolve(entry[1]);
72
+ return process.platform === 'win32' ? target.toLowerCase() : target;
73
+ } catch { return null; }
74
+ }
75
+
76
+ function isFile(file) {
77
+ try { return statSync(file).isFile(); } catch { return false; }
78
+ }
79
+
80
+ function claudePluginRootReady(root) {
81
+ return typeof root === 'string' && root.trim() !== ''
82
+ && isFile(join(root, '.claude-plugin', 'plugin.json'))
83
+ && isFile(join(root, '.mcp.json'))
84
+ && isFile(join(root, 'src', 'server.mjs'));
85
+ }
86
+
87
+ function claudeSnapshotReady(home) {
88
+ const installed = readJson(join(home, '.claude', 'plugins', 'installed_plugins.json'), {});
89
+ const record = installed?.plugins?.[PLUGIN_KEY];
90
+ const entries = Array.isArray(record) ? record : [record];
91
+ return entries.some((entry) => claudePluginRootReady(entry?.installPath));
92
+ }
93
+
94
+ function claudePermissionsReady(settings) {
95
+ const allowed = Array.isArray(settings?.permissions?.allow) ? settings.permissions.allow : [];
96
+ return MCP_TOOLS.every((tool) => allowed.includes(`mcp__plugin_dsh-crew_dsh-crew__${tool}`));
97
+ }
31
98
 
32
99
  // One-time migration from the pre-rename config dir (dsh-workers → dsh-crew).
33
100
  try {
@@ -189,16 +256,48 @@ export function writeGlobalConfig(patch) {
189
256
  }
190
257
 
191
258
  /** 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
- }
259
+ export function installStatus({ home = homedir() } = {}) {
260
+ const settings = readJson(join(home, '.claude', 'settings.json'), {});
261
+ const enabled = settings.enabledPlugins;
262
+ const claudeInstalled = !!(enabled && !Array.isArray(enabled) && enabled[PLUGIN_KEY]);
263
+ const hudWired = typeof settings.statusLine?.command === 'string'
264
+ && settings.statusLine.command.includes('worker-segment.sh');
265
+ const claudeComponents = {
266
+ enabled: claudeInstalled,
267
+ marketplace: claudePluginRootReady(settings?.extraKnownMarketplaces?.[MARKETPLACE_NAME]?.source?.path),
268
+ snapshot: claudeSnapshotReady(home),
269
+ permissions: claudePermissionsReady(settings),
270
+ };
271
+ const claudeMissing = Object.entries(claudeComponents).filter(([, present]) => !present).map(([key]) => key);
272
+ const codexRoot = join(home, '.codex');
273
+ const configFile = join(codexRoot, 'config.toml');
274
+ const configText = readText(configFile) ?? '';
275
+ const workerTarget = codexRoleTarget(join(codexRoot, 'agents', 'ds-worker.toml'), 'ds-worker');
276
+ const reviewerTarget = codexRoleTarget(join(codexRoot, 'agents', 'ds-reviewer.toml'), 'ds-reviewer');
277
+ const mcpTarget = codexMcpTarget(configText);
278
+ const components = {
279
+ worker_role: !!workerTarget,
280
+ reviewer_role: !!reviewerTarget,
281
+ config_prompt: !!readText(join(codexRoot, 'prompts', 'dsh-config.md'))?.trim(),
282
+ status_prompt: !!readText(join(codexRoot, 'prompts', 'dsh-status.md'))?.trim(),
283
+ mcp: !!mcpTarget,
284
+ target_alignment: !!workerTarget && workerTarget === reviewerTarget && workerTarget === mcpTarget,
285
+ };
286
+ const codexInstalled = Object.values(components).some(Boolean)
287
+ || existsSync(join(codexRoot, 'agents', 'ds-flash.toml'))
288
+ || existsSync(join(codexRoot, 'agents', 'ds-pro.toml'));
289
+ const missing = Object.entries(components).filter(([, present]) => !present).map(([key]) => key);
290
+ return {
291
+ claude: {
292
+ installed: claudeInstalled,
293
+ ready: claudeMissing.length === 0,
294
+ hud: hudWired,
295
+ components: claudeComponents,
296
+ missing: claudeMissing,
297
+ },
298
+ codex: { installed: codexInstalled, ready: missing.length === 0, components, missing },
299
+ };
300
+ }
202
301
 
203
302
  export function uninstallCodex({ home = homedir() } = {}) {
204
303
  const actions = [];
@@ -194,7 +194,12 @@ export function registerOfficialWebBridge(ctx, options = {}) {
194
194
  path: `${CREW_BRIDGE_PREFIX}/bridge-status`,
195
195
  handler: (req, res) => {
196
196
  if (!isTrustedLocalRequest(req)) return sendJson(res, 403, { ok: false, code: 'LOCAL_SAME_ORIGIN_ONLY' });
197
- return sendJson(res, 200, { ok: true, mode: 'official-3080-isolated-3210' });
197
+ return sendJson(res, 200, {
198
+ ok: true,
199
+ mode: 'official-3080-isolated-3210',
200
+ surface: 'official-bridge',
201
+ ui_role: 'control-plane',
202
+ });
198
203
  },
199
204
  });
200
205
  const disposeProxy = webCtx.webServer.register({
@@ -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.4.2';
11
+ export const RUNTIME_VERSION = '0.5.1';
12
12
  export const HUB_PROTOCOL_VERSION = 1;
13
13
 
14
14
  export const HUB_CAPABILITIES = Object.freeze([
@@ -54,9 +54,11 @@ function normalizedCapabilities(value) {
54
54
  }
55
55
 
56
56
  export function getHubRuntimeIdentity() {
57
- return {
58
- service: 'dsh-crew-hub',
59
- runtime_version: RUNTIME_VERSION,
57
+ return {
58
+ service: 'dsh-crew-hub',
59
+ surface: 'native-crew-harness',
60
+ ui_role: 'runtime',
61
+ runtime_version: RUNTIME_VERSION,
60
62
  protocol_version: HUB_PROTOCOL_VERSION,
61
63
  capabilities: [...HUB_CAPABILITIES],
62
64
  };