@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.
Files changed (85) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +8 -0
  3. package/.mcp.json +8 -0
  4. package/LICENSE +21 -0
  5. package/README.de.md +359 -0
  6. package/README.es.md +359 -0
  7. package/README.fr.md +359 -0
  8. package/README.hi.md +359 -0
  9. package/README.id.md +359 -0
  10. package/README.ja.md +359 -0
  11. package/README.ko.md +359 -0
  12. package/README.md +360 -0
  13. package/README.pt.md +359 -0
  14. package/README.ru.md +359 -0
  15. package/README.th.md +359 -0
  16. package/README.tr.md +359 -0
  17. package/README.vi.md +359 -0
  18. package/README.zh-TW.md +359 -0
  19. package/README.zh.md +305 -0
  20. package/agents/ds-flash.md +26 -0
  21. package/agents/ds-pro.md +32 -0
  22. package/agents/ds-reviewer.md +23 -0
  23. package/agents/ds-worker.md +22 -0
  24. package/codex/agents/ds-flash.toml +30 -0
  25. package/codex/agents/ds-pro.toml +31 -0
  26. package/codex/agents/ds-reviewer.toml +28 -0
  27. package/codex/agents/ds-worker.toml +28 -0
  28. package/codex/prompts/dsh-config.md +3 -0
  29. package/codex/prompts/dsh-status.md +1 -0
  30. package/commands/config.md +11 -0
  31. package/commands/off.md +5 -0
  32. package/commands/on.md +5 -0
  33. package/commands/status.md +5 -0
  34. package/cordis.patch.yml +4 -0
  35. package/docs/images/dsh-crew-host.png +0 -0
  36. package/docs/images/dsh-crew-jobs.png +0 -0
  37. package/docs/images/dsh-crew-logo.png +0 -0
  38. package/docs/images/dsh-crew-overview.png +0 -0
  39. package/lib/client.js +2765 -0
  40. package/package.json +125 -0
  41. package/scripts/build-client.mjs +28 -0
  42. package/scripts/live-crew-smoke.mjs +39 -0
  43. package/scripts/live-policy-matrix.mjs +177 -0
  44. package/scripts/policy-probe.mjs +101 -0
  45. package/scripts/setup.mjs +294 -0
  46. package/scripts/smoke-real.mjs +110 -0
  47. package/scripts/smoke.mjs +78 -0
  48. package/scripts/verify-installer-fix.mjs +26 -0
  49. package/src/adaptive-routing.mjs +260 -0
  50. package/src/client/activation-summary.tsx +64 -0
  51. package/src/client/entry.tsx +236 -0
  52. package/src/client/index.tsx +1120 -0
  53. package/src/config-readiness.mjs +59 -0
  54. package/src/delivery.mjs +205 -0
  55. package/src/dsh-cli-runtime.mjs +251 -0
  56. package/src/failure-classification.mjs +172 -0
  57. package/src/hub/entry.mjs +98 -0
  58. package/src/hub/index.mjs +757 -0
  59. package/src/hub-client.mjs +132 -0
  60. package/src/hub-compatibility.mjs +49 -0
  61. package/src/i18n.mjs +19 -0
  62. package/src/install/cli.mjs +28 -0
  63. package/src/install/install-legacy.mjs +460 -0
  64. package/src/install/install.mjs +451 -0
  65. package/src/jobs.mjs +275 -0
  66. package/src/mcp-runtime.mjs +257 -0
  67. package/src/model-catalog.mjs +173 -0
  68. package/src/model-routing.mjs +391 -0
  69. package/src/multimodal.mjs +0 -0
  70. package/src/policy-legacy.mjs +830 -0
  71. package/src/policy.mjs +197 -0
  72. package/src/readiness-matrix.mjs +169 -0
  73. package/src/runtime-controls.mjs +90 -0
  74. package/src/runtime-identity.mjs +108 -0
  75. package/src/server.mjs +477 -0
  76. package/src/status-shard.mjs +52 -0
  77. package/src/structured-error-code.mjs +39 -0
  78. package/src/vision-route.mjs +138 -0
  79. package/src/workflow-runtime.mjs +567 -0
  80. package/src/workflow.mjs +160 -0
  81. package/src/workspace-audit.mjs +231 -0
  82. package/src/workspace-isolation.mjs +306 -0
  83. package/statusline/statusline.sh +14 -0
  84. package/statusline/worker-segment.sh +35 -0
  85. package/worker.cordis.yml +77 -0
@@ -0,0 +1,138 @@
1
+ // Vision route: lets users paste images into DSH conversations on the
2
+ // text-only DeepSeek models.
3
+ //
4
+ // Two cooperating pieces:
5
+ // 1. A duck-typed LLM adapter registered as provider "deepseek-vision" that
6
+ // advertises image input modality (passing the api-proxy admission gate)
7
+ // and delegates every stream straight back to "deepseek-official".
8
+ // registerAdapter does no instanceof check, so a plain object keeps the
9
+ // zero-@deepseek-imports realm discipline.
10
+ // 2. A global agent/pre-step waterfall that appends a text transcription (via
11
+ // the multimodal describeFile pipeline) after each ImageBlock. The image
12
+ // block itself stays in history so the conversation still shows the picture;
13
+ // the adapter above drops image blocks on the way to the model, which is
14
+ // what keeps the DeepSeek serializer (it throws on any image block) happy.
15
+
16
+ import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { homedir } from 'node:os';
19
+ import { createHash } from 'node:crypto';
20
+ import { describeFile } from './multimodal.mjs';
21
+ import { tr } from './i18n.mjs';
22
+
23
+ const VISION_PROVIDER = 'deepseek-vision';
24
+ const DELEGATE = 'deepseek-official';
25
+ const ATTACH_DIR = join(homedir(), '.config', 'dsh-crew', 'attachments');
26
+
27
+ const EXT = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', 'image/gif': 'gif' };
28
+
29
+ const TRANSCRIPT_PREFIX = (name) => tr(`[图片「${name}」已由视觉桥转写]`, `[Image "${name}" transcribed by the vision bridge]`);
30
+ // Both locales' prefixes must be recognised: the user may switch language after
31
+ // a transcription was written, and re-transcribing would duplicate it.
32
+ const TRANSCRIPT_MARKERS = ['[图片「', '[Image "'];
33
+
34
+ /** True when `block` is a transcription this route wrote for `imageBlock`. */
35
+ function isTranscriptFor(block, imageBlock) {
36
+ if (block?.type !== 'text' || typeof block.text !== 'string') return false;
37
+ const name = imageBlock?.attachment?.name;
38
+ if (name && (block.text.startsWith(`[图片「${name}」`) || block.text.startsWith(`[Image "${name}"`))) return true;
39
+ return TRANSCRIPT_MARKERS.some((m) => block.text.startsWith(m));
40
+ }
41
+
42
+ /**
43
+ * Images live on in history for the UI, but never reach the model: the
44
+ * transcription that follows each one already carries the content, so the image
45
+ * block is dropped rather than replaced with a placeholder.
46
+ */
47
+ function stripImages(content) {
48
+ if (!Array.isArray(content)) return content;
49
+ if (!content.some((b) => b?.type === 'image')) return content;
50
+ const out = content.filter((b) => b?.type !== 'image');
51
+ return out.length > 0 ? out : [{ type: 'text', text: tr('[图片已由视觉桥转写,详见随后的描述]', '[image transcribed by the vision bridge — see the description that follows]') }];
52
+ }
53
+
54
+ export function installVisionRoute(ctx, getConfig) {
55
+ const llm = ctx.llm;
56
+ const disposers = [];
57
+
58
+ const adapter = {
59
+ providerInfo: (p) => ({ id: p, name: tr('DeepSeek (视觉)', 'DeepSeek (Vision)') }),
60
+ providerRetryPolicy: () => undefined,
61
+ async listModels(p) {
62
+ const models = await llm.listModels(DELEGATE);
63
+ return models.map((m) => ({ ...m, provider: p, name: `${m.name} ◉`, inputModalities: ['text', 'image'] }));
64
+ },
65
+ async resolveModel(p, model, signal) {
66
+ const base = await llm.resolveModelInfo(DELEGATE, model, signal);
67
+ return { ...base, provider: p, name: `${base.name} ◉`, inputModalities: ['text', 'image'] };
68
+ },
69
+ async *stream(options) {
70
+ // Safety net: any image block that survived pre-step would make the
71
+ // DeepSeek serializer throw — strip defensively.
72
+ // NOTE: routing reads top-level options.provider (adapterStream uses
73
+ // this.registration(options.provider)); rewriting anything else
74
+ // self-recurses into this adapter.
75
+ const messages = options.messages?.map((m) => ({ ...m, content: stripImages(m.content) })) ?? options.messages;
76
+ yield* llm.stream({ ...options, provider: DELEGATE, ...(messages === undefined ? {} : { messages }) });
77
+ },
78
+ };
79
+ const handle = llm.registerAdapter([VISION_PROVIDER], adapter);
80
+ disposers.push(() => handle());
81
+
82
+ // Transcribe pending images before the step enters history. Applies to every
83
+ // agent in this host: on a DS-only deployment any surviving image block is a
84
+ // hard error downstream, so rewriting is strictly better than crashing.
85
+ disposers.push(ctx.on('agent/pre-step', async (payload, next) => {
86
+ const decision = await next();
87
+ if (decision.kind !== 'enter') return decision;
88
+ if (getConfig().vision_provider === 'off') return decision;
89
+ let changed = false;
90
+ const rewritten = [];
91
+ for (const message of decision.messages) {
92
+ if (!Array.isArray(message.content) || !message.content.some((b) => b?.type === 'image')) {
93
+ rewritten.push(message);
94
+ continue;
95
+ }
96
+ const content = [];
97
+ for (const [i, block] of message.content.entries()) {
98
+ if (block?.type !== 'image') { content.push(block); continue; }
99
+ content.push(block); // keep the image so the conversation still shows it
100
+ // Steps replay the whole history: skip anything already transcribed,
101
+ // otherwise every step would append another copy.
102
+ if (isTranscriptFor(message.content[i + 1], block)) continue;
103
+ changed = true;
104
+ try {
105
+ const stored = await ctx.attachments.readImage(block.attachment, payload.signal);
106
+ const sha = createHash('sha256').update(stored.data).digest('hex').slice(0, 12);
107
+ const ext = EXT[block.attachment?.mediaType] ?? 'png';
108
+ mkdirSync(ATTACH_DIR, { recursive: true });
109
+ const path = join(ATTACH_DIR, `${sha}.${ext}`);
110
+ if (!existsSync(path)) writeFileSync(path, stored.data);
111
+ const name = block.attachment?.name ?? `${sha}.${ext}`;
112
+ const desc = await describeFile(getConfig, path, tr(
113
+ '详细描述这张图片:整体内容、布局结构、可见文字(逐字)、颜色与显著元素。',
114
+ 'Describe this image in detail: overall content, layout, any visible text (verbatim), colours and notable elements.'));
115
+ content.push({
116
+ type: 'text',
117
+ text: `${TRANSCRIPT_PREFIX(name)}\n${desc}\n${tr(
118
+ `[原图: ${path} — 需要更多细节可用 describe_image 工具带具体问题查看]`,
119
+ `[Original: ${path} — for more detail, ask the describe_image tool a specific question]`)}`,
120
+ });
121
+ } catch (err) {
122
+ // Same prefix as a successful transcription so the dedup check above
123
+ // treats it as handled — otherwise a broken provider would append a
124
+ // fresh failure notice on every step.
125
+ content.push({
126
+ type: 'text',
127
+ text: `${TRANSCRIPT_PREFIX(block.attachment?.name ?? tr('图片', 'image'))}\n${tr(`[转写失败: ${err?.message ?? err}]`, `[transcription failed: ${err?.message ?? err}]`)}`,
128
+ });
129
+ }
130
+ }
131
+ rewritten.push({ ...message, content });
132
+ }
133
+ return changed ? { kind: 'enter', messages: rewritten } : decision;
134
+ }));
135
+
136
+ ctx.logger?.info?.('dsh-crew: vision route registered (provider "deepseek-vision" + pre-step transcription)');
137
+ return () => { for (const d of disposers.reverse()) { try { d(); } catch {} } };
138
+ }