dsh-gitbash-shell 0.34.2 → 0.35.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.
@@ -27,7 +27,13 @@ function off(value) {
27
27
  return value === true ? { disabled: true } : {}
28
28
  }
29
29
 
30
- const PLAN_SECTION = `You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
30
+ // Plan-mode section, shared body with an era-specific FIRST SENTENCE (each
31
+ // byte-exact from its era's official patch.yml): dsh 0.1.7..0.2.1-alpha.1
32
+ // say "until exit_plan_mode succeeds", 0.2.1-alpha.2 rephrased to "until the
33
+ // user approves your plan through exit_plan_mode". The hostExtras probe
34
+ // already splits the eras for the row set, so the wording rides the same
35
+ // signal — no new probe, and each generation mirrors its own official text.
36
+ const PLAN_SECTION_TAIL = `Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
31
37
 
32
38
  Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
33
39
 
@@ -40,6 +46,10 @@ Make the plan decision-complete: state the goal and success criteria; group impl
40
46
  When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
41
47
  `
42
48
 
49
+ const planSectionFor = (hostEra021) => hostEra021
50
+ ? `You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. ${PLAN_SECTION_TAIL}`
51
+ : `You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. ${PLAN_SECTION_TAIL}`
52
+
43
53
  /**
44
54
  * The shared full-tool rows for standard / ptc / cordis variants (pure).
45
55
  * @param {object} input
@@ -86,7 +96,13 @@ export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hos
86
96
  name: '@deepseek-ai/dsh-persona',
87
97
  config: {
88
98
  prefix: 'You are a coding agent powered by the {{model}} model.',
89
- suffix: 'Your working directory is {{cwd}}.',
99
+ // NO suffix: the host dropped the `cwd` prompt variable on 2026-09-13
100
+ // (upstream 79bd3d8da7 — working directories centralized into the
101
+ // working-directory service) and removed the persona suffix clause
102
+ // from every official preset in 0.2.1-alpha.2 (upstream 2eb058d887).
103
+ // An unregistered variable reference throws BEFORE any model request
104
+ // and kills the whole turn (issue #16, dsh 0.2.1-alpha.1+). The
105
+ // host's working-directory runtime context supplies the directory.
90
106
  },
91
107
  },
92
108
  {
@@ -120,7 +136,7 @@ export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hos
120
136
  {
121
137
  id: 'plan-mode',
122
138
  name: '@deepseek-ai/dsh-plan-mode',
123
- config: { section: PLAN_SECTION },
139
+ config: { section: planSectionFor(timeContext) },
124
140
  },
125
141
  ],
126
142
  },
@@ -150,24 +166,12 @@ export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hos
150
166
  {
151
167
  id: 'tool-subagent',
152
168
  name: '@deepseek-ai/dsh-tool-subagent',
153
- config: { provider: 'spawn', toolName: 'subagent', modelSelectionSettings: true, backgroundMode: 'continuable', ...subagentDeny },
169
+ config: { provider: 'spawn', toolName: 'subagent', modelSelectionSettings: true, ...(timeContext ? {} : { backgroundMode: 'continuable' }), ...subagentDeny },
154
170
  },
155
171
  {
156
172
  id: 'tool-subagent-fork',
157
173
  name: '@deepseek-ai/dsh-tool-subagent',
158
- config: { provider: 'fork', toolName: 'subagent_fork', backgroundMode: 'continuable', ...subagentDeny },
159
- },
160
- {
161
- id: 'tool-subagent-codex',
162
- name: '@deepseek-ai/dsh-tool-subagent',
163
- disabled: true,
164
- config: { provider: 'codex', toolName: 'subagent_codex', backgroundMode: 'one-shot', maxDepth: 'provider-managed' },
165
- },
166
- {
167
- id: 'tool-subagent-claude-code',
168
- name: '@deepseek-ai/dsh-tool-subagent',
169
- disabled: true,
170
- config: { provider: 'claude-code', toolName: 'subagent_claude_code', backgroundMode: 'one-shot', maxDepth: 'provider-managed' },
174
+ config: { provider: 'fork', toolName: 'subagent_fork', ...(timeContext ? {} : { backgroundMode: 'continuable' }), ...subagentDeny },
171
175
  },
172
176
  {
173
177
  id: 'workflow-ptc',
@@ -176,12 +180,12 @@ export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hos
176
180
  config: { provider: 'spawn' },
177
181
  },
178
182
  { id: 'tool-workflow', name: '@deepseek-ai/dsh-tool-workflow', ...off(!workflowOn) },
179
- {
180
- id: 'tool-ralph',
181
- name: '@deepseek-ai/dsh-tool-ralph',
182
- disabled: true,
183
- config: { subagentProvider: 'spawn', maxRounds: 64 },
184
- },
183
+ // NO tool-subagent-codex / tool-subagent-claude-code / tool-ralph:
184
+ // the official presets retired these three disabled placeholder rows
185
+ // in 0.2.1-alpha.2 (upstream 8ed0b530ed — the codex/claude-code
186
+ // provider rows moved to on-demand official plugin bundles). They
187
+ // were `disabled: true` on every host generation, so dropping them
188
+ // loses nothing and re-aligns with the shipped 0.2.1 row split.
185
189
  ],
186
190
  },
187
191
  { id: 'tool-ask-user', name: '@deepseek-ai/dsh-tool-ask-user' },
@@ -0,0 +1,378 @@
1
+ /**
2
+ * dsh-gitbash-shell/fastctx-bridge — the `gb_*` file tools over FastCtx (v0.35.0).
3
+ *
4
+ * FastCtx (github.com/yc-duan/fastctx, Apache-2.0) is a local Rust MCP server
5
+ * with context-efficient file tools: batched ranged reads with an output
6
+ * budget, grep with truncation markers, encoding detection that reports
7
+ * candidate encodings instead of guessing. On a Windows host those four
8
+ * abilities are exactly what the official tools lack, so this bridge publishes
9
+ * them as ADDITIONS (`gb_inspect_local_file`, `gb_grep`, `gb_glob`,
10
+ * `gb_replace`) — the official read/grep/glob/edit tools and their file
11
+ * sandbox stay untouched and remain the default recommendation.
12
+ *
13
+ * TRANSPORT. A minimal MCP stdio client (newline-delimited JSON-RPC over the
14
+ * child's stdin/stdout, ~120 lines). The official @deepseek-ai/dsh-mcp-client
15
+ * exists but is a statically-configured plugin ROW: its `command` must be a
16
+ * literal (no resolver runs in the `!!js` sandbox), its names are locked to
17
+ * `mcp__<server>__<raw>`, and it has no per-server tool allowlist — while this
18
+ * bridge needs a payload path resolved from the profile layout, a closed tool
19
+ * set (FastCtx's shell/background tools must never surface), a dialect
20
+ * translation on every path argument, and a stand-down when dsh-ops is
21
+ * present. When the host one day exposes a runtime bridge API, this module is
22
+ * the first thing to retire — tracked in AGENTS.md.
23
+ *
24
+ * THE PAYLOAD. FastCtx itself is never bundled. The bridge probes, in order,
25
+ * the upstream npm payload (`@fastctx/win32-x64`) and the dsh-ops fork payload
26
+ * (`@dsh-ops/fastctx-win32-x64`, the one this plugin declares as an optional
27
+ * dependency, pinned with a recorded provenance). Neither present -> the
28
+ * bridge mounts nothing and says so once. The server is started WITHOUT
29
+ * `--enable-shell`: FastCtx's command/job tools do not exist in this
30
+ * deployment, and `gb_*` stays a pure file-tool surface.
31
+ *
32
+ * NO SANDBOX, STATED. FastCtx reads the real filesystem with no workspace
33
+ * confinement; that is FastCtx's own contract, so every `gb_` description
34
+ * says the file tools remain the sandboxed default and `gb_replace` warns
35
+ * about its cross-file reach.
36
+ */
37
+
38
+ /** The only FastCtx tools this bridge ever publishes. */
39
+ export const FASTCTX_FILE_TOOLS = Object.freeze(['inspect_local_file', 'grep', 'glob', 'replace'])
40
+
41
+ /** Public names follow the bridge prefix, never the server's own. */
42
+ export const GB_PREFIX = 'gb_'
43
+ export const gbName = (raw) => GB_PREFIX + raw
44
+
45
+ /** Payload packages probed, in order: upstream first, the pinned fork second. */
46
+ export const PAYLOAD_PACKAGES = Object.freeze([
47
+ { pkg: '@fastctx/win32-x64', exe: 'bin/fastctx.exe' },
48
+ { pkg: '@dsh-ops/fastctx-win32-x64', exe: 'bin/fastctx.exe' },
49
+ ])
50
+
51
+ /**
52
+ * Resolve the fastctx executable from the profile's resolution base. Pure in
53
+ * its decisions, I/O through the injected `createRequireFrom`.
54
+ * @returns {{ file: string, source: string } | { file: '', source: '' }}
55
+ */
56
+ export function resolveFastctx(ctx, createRequireFrom) {
57
+ let req
58
+ try {
59
+ req = createRequireFrom(ctx?.baseUrl ?? import.meta.url)
60
+ } catch {
61
+ return { file: '', source: '' }
62
+ }
63
+ for (const { pkg, exe } of PAYLOAD_PACKAGES) {
64
+ try {
65
+ const root = req.resolve(`${pkg}/package.json`)
66
+ const { join, dirname } = req('node:path')
67
+ const { existsSync } = req('node:fs')
68
+ const file = join(dirname(root), exe)
69
+ if (existsSync(file)) return { file, source: pkg }
70
+ } catch { /* probe the next payload */ }
71
+ }
72
+ return { file: '', source: '' }
73
+ }
74
+
75
+ /**
76
+ * One MCP stdio connection. Requests carry incrementing ids; responses are
77
+ * matched by id; the server's stderr is captured for the error report. Every
78
+ * call has a deadline. NOT thread-safe — the harness serializes tool calls.
79
+ */
80
+ export class McpStdio {
81
+ constructor({ file, args = [], timeoutMs = 60_000, log = () => {} }) {
82
+ this.file = file
83
+ this.args = args
84
+ this.timeoutMs = timeoutMs
85
+ this.log = log
86
+ this.child = undefined
87
+ this.nextId = 1
88
+ this.pending = new Map()
89
+ this.buffer = ''
90
+ this.stderrTail = ''
91
+ this.dead = false
92
+ }
93
+
94
+ /** Spawn the server and run the MCP initialize handshake. */
95
+ async start() {
96
+ const { spawn } = await import('node:child_process')
97
+ this.child = spawn(this.file, this.args, { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true })
98
+ this.child.stdout.setEncoding('utf8')
99
+ this.child.stderr.setEncoding('utf8')
100
+ this.child.stdout.on('data', (chunk) => this.#onChunk(chunk))
101
+ this.child.stderr.on('data', (chunk) => {
102
+ this.stderrTail = (this.stderrTail + chunk).slice(-2000)
103
+ })
104
+ this.child.on('exit', (code) => {
105
+ this.dead = true
106
+ for (const [, { reject }] of [...this.pending]) {
107
+ reject(new Error(`the FastCtx server exited (code ${code ?? 'null'})`))
108
+ }
109
+ this.pending.clear()
110
+ })
111
+ await this.#request('initialize', {
112
+ protocolVersion: '2024-11-05',
113
+ capabilities: {},
114
+ clientInfo: { name: 'dsh-gitbash-shell', version: '0.35.0' },
115
+ })
116
+ this.#notify('notifications/initialized')
117
+ return this
118
+ }
119
+
120
+ /** List the server's tools: [{ name, description, inputSchema }]. */
121
+ async listTools() {
122
+ const result = await this.#request('tools/list', {})
123
+ return Array.isArray(result?.tools) ? result.tools : []
124
+ }
125
+
126
+ /** Call one tool; returns the raw MCP result ({ content, isError? }). */
127
+ async callTool(rawName, args) {
128
+ return this.#request('tools/call', { name: rawName, arguments: args ?? {} })
129
+ }
130
+
131
+ /** Whether the transport is gone (a later call re-launches via the mount). */
132
+ get isDead() { return this.dead }
133
+
134
+ async close() {
135
+ this.dead = true
136
+ const child = this.child
137
+ this.child = undefined
138
+ for (const [, { reject }] of [...this.pending]) {
139
+ reject(new Error('the FastCtx bridge was stopped'))
140
+ }
141
+ this.pending.clear()
142
+ if (child && child.exitCode === null && !child.killed) {
143
+ try { child.kill() } catch { /* already gone */ }
144
+ }
145
+ }
146
+
147
+ #onChunk(chunk) {
148
+ this.buffer += chunk
149
+ let at = this.buffer.indexOf('\n')
150
+ while (at !== -1) {
151
+ const line = this.buffer.slice(0, at).trim()
152
+ this.buffer = this.buffer.slice(at + 1)
153
+ if (line !== '') this.#onLine(line)
154
+ at = this.buffer.indexOf('\n')
155
+ }
156
+ }
157
+
158
+ #onLine(line) {
159
+ let message
160
+ try { message = JSON.parse(line) } catch { return /* not JSON: ignore */ }
161
+ const id = message?.id
162
+ if (id !== undefined && this.pending.has(id)) {
163
+ const { resolve, reject, timer } = this.pending.get(id)
164
+ clearTimeout(timer)
165
+ this.pending.delete(id)
166
+ if (message.error) reject(new Error(String(message.error.message ?? JSON.stringify(message.error))))
167
+ else resolve(message.result)
168
+ }
169
+ }
170
+
171
+ #send(frame) {
172
+ if (!this.child || this.dead) throw new Error('the FastCtx server is not running')
173
+ this.child.stdin.write(JSON.stringify(frame) + '\n')
174
+ }
175
+
176
+ #notify(method, params) {
177
+ this.#send({ jsonrpc: '2.0', method, ...params !== undefined ? { params } : {} })
178
+ }
179
+
180
+ #request(method, params) {
181
+ if (!this.child || this.dead) return Promise.reject(new Error('the FastCtx server is not running'))
182
+ const id = this.nextId++
183
+ return new Promise((resolve, reject) => {
184
+ const timer = setTimeout(() => {
185
+ this.pending.delete(id)
186
+ reject(new Error(`${method} timed out after ${this.timeoutMs}ms`))
187
+ }, this.timeoutMs)
188
+ timer.unref?.()
189
+ this.pending.set(id, { resolve, reject, timer })
190
+ try {
191
+ this.#send({ jsonrpc: '2.0', id, method, ...params !== undefined ? { params } : {} })
192
+ } catch (error) {
193
+ clearTimeout(timer)
194
+ this.pending.delete(id)
195
+ reject(error)
196
+ }
197
+ })
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Translate the path-bearing fields of one gb_* argument object, MSYS ->
203
+ * Windows, using the SAME mount table the official file tools get. Handles
204
+ * the fastctx shapes: `path`, `file_path`, `files[].path`, and an absolute
205
+ * glob `pattern`. Pure.
206
+ */
207
+ export function translateFastctxArgs(args, msysToWin) {
208
+ if (!msysToWin || !args || typeof args !== 'object') return args
209
+ const translate = (value) => (typeof value === 'string' ? msysToWin(value) : value)
210
+ const out = { ...args }
211
+ for (const field of ['path', 'file_path', 'workdir']) {
212
+ if (typeof out[field] === 'string') out[field] = translate(out[field])
213
+ }
214
+ if (Array.isArray(out.files)) {
215
+ out.files = out.files.map((entry) => (entry && typeof entry === 'object' && typeof entry.path === 'string'
216
+ ? { ...entry, path: translate(entry.path) }
217
+ : entry))
218
+ }
219
+ // A glob pattern that is absolute (starts with a drive root or /) is
220
+ // translated whole; a relative pattern must stay relative.
221
+ if (typeof out.pattern === 'string' && /^[/~]/.test(out.pattern)) {
222
+ out.pattern = translate(out.pattern)
223
+ }
224
+ return out
225
+ }
226
+
227
+ /**
228
+ * Render one MCP result as the text the model reads, with the dialect echo
229
+ * applied to the text blocks. Mirrors the host's own MCP projection closely
230
+ * enough for these four tools: text blocks joined, non-text blocks named.
231
+ * Pure.
232
+ */
233
+ export function renderFastctxResult(rawName, result, echo) {
234
+ const content = Array.isArray(result?.content) ? result.content : []
235
+ const parts = []
236
+ for (const block of content) {
237
+ if (block && block.type === 'text' && typeof block.text === 'string') parts.push(block.text)
238
+ else parts.push(`[unsupported MCP content block: ${String(block?.type ?? 'unknown')}]`)
239
+ }
240
+ let text = parts.join('\n')
241
+ if (text === '') text = `(${gbName(rawName)} returned no content)`
242
+ if (echo) text = echo(text)
243
+ const header = result?.isError === true ? `${gbName(rawName)} failed: ` : ''
244
+ return header + text
245
+ }
246
+
247
+ /** The sandbox honesty line appended to every gb_* description. */
248
+ const FASTCTX_SANDBOX_NOTE = 'FastCtx reads the real filesystem with no workspace confinement: for sandboxed work prefer the official read/grep/glob/edit tools, which stay the default.'
249
+
250
+ /**
251
+ * Mount the bridge. `deps`:
252
+ * - platform -> process.platform (win32 only mounts)
253
+ * - enabled() -> the row switch (fastctxTools)
254
+ * - createRequireFrom(ctx) -> the profile resolution seam
255
+ * - tools(ctx) -> the host tool registry (or undefined)
256
+ * - msysToWin(path) -> argument dialect translation
257
+ * - echo(text) -> result dialect echo, or undefined (off)
258
+ * - log(message) -> the plugin's tagged logger
259
+ *
260
+ * The surface publishes at the FIRST agent creation and re-decides on every
261
+ * later one: dsh-ops appearing (ops_* tools in the face) withdraws gb_* for
262
+ * the rest of the process — duplicates would only teach the model two names
263
+ * for one job.
264
+ */
265
+ export function mountFastctx(ctx, deps) {
266
+ const { platform, enabled, createRequireFrom, tools, msysToWin, echo, log } = deps
267
+ if (platform !== 'win32') return
268
+ let disposer = undefined
269
+ let client = undefined
270
+ let decided = false
271
+ let withdrawnByOps = false
272
+
273
+ const unpublish = (reason) => {
274
+ if (disposer) {
275
+ try { disposer() } catch { /* already gone */ }
276
+ disposer = undefined
277
+ }
278
+ if (reason) log(reason)
279
+ }
280
+
281
+ const callWithReconnect = async (rawName, args) => {
282
+ if (client === undefined || client.isDead) {
283
+ const { file } = resolveFastctx(ctx, createRequireFrom)
284
+ if (file === '') throw new Error('the FastCtx payload is not installed; repair the profile dependencies (frozen install) and retry')
285
+ client = await new McpStdio({ file, log }).start()
286
+ }
287
+ return client.callTool(rawName, args)
288
+ }
289
+
290
+ const publish = async () => {
291
+ if (disposer || decided && withdrawnByOps) return
292
+ const registry = tools(ctx)
293
+ if (!registry || typeof registry.register !== 'function') return
294
+ const { file, source } = resolveFastctx(ctx, createRequireFrom)
295
+ if (file === '') {
296
+ log('FastCtx payload not found (probed ' + PAYLOAD_PACKAGES.map((p) => p.pkg).join(', ') + '); gb_* tools stay unpublished')
297
+ decided = true
298
+ withdrawnByOps = true // nothing to publish either way; stop re-deciding
299
+ return
300
+ }
301
+ const fresh = new McpStdio({ file, log })
302
+ let toolList
303
+ try {
304
+ await fresh.start()
305
+ toolList = await fresh.listTools()
306
+ } catch (error) {
307
+ try { await fresh.close() } catch { /* best effort */ }
308
+ log(`FastCtx server start failed (${source}): ${error?.message ?? error}; gb_* tools stay unpublished`)
309
+ decided = true
310
+ withdrawnByOps = true
311
+ return
312
+ }
313
+ await fresh.close()
314
+ client = undefined // the first call re-launches lazily; boot stays cheap
315
+ const wanted = toolList.filter((tool) => FASTCTX_FILE_TOOLS.includes(String(tool?.name)))
316
+ const registered = []
317
+ const registeredNames = []
318
+ for (const tool of wanted) {
319
+ const rawName = String(tool.name)
320
+ const schema = tool.inputSchema && typeof tool.inputSchema === 'object' ? tool.inputSchema : { type: 'object', properties: {} }
321
+ try {
322
+ const dispose = registry.register({
323
+ name: gbName(rawName),
324
+ description: `${String(tool.description ?? rawName)}\n${FASTCTX_SANDBOX_NOTE}`,
325
+ parameters: schema,
326
+ async execute(args) {
327
+ const translated = translateFastctxArgs(args, msysToWin)
328
+ const result = await callWithReconnect(rawName, translated)
329
+ return { content: [{ type: 'text', text: renderFastctxResult(rawName, result, echo) }] }
330
+ },
331
+ })
332
+ if (typeof dispose === 'function') {
333
+ registered.push(dispose)
334
+ registeredNames.push(gbName(rawName))
335
+ }
336
+ } catch (error) {
337
+ log(`${gbName(rawName)} registration failed: ${error?.message ?? error}`)
338
+ }
339
+ }
340
+ if (registered.length === 0) {
341
+ log('FastCtx published none of the known file tools; nothing registered')
342
+ decided = true
343
+ withdrawnByOps = true
344
+ return
345
+ }
346
+ disposer = () => { for (const dispose of registered) dispose() }
347
+ log(`FastCtx bridge active (${source}): ${registeredNames.join(', ')}`)
348
+ }
349
+
350
+ const reconcile = (agent) => {
351
+ if (!enabled()) { unpublish('FastCtx bridge disabled by the fastctxTools switch'); decided = true; withdrawnByOps = true; return }
352
+ if (decided && withdrawnByOps) return
353
+ let names = []
354
+ try {
355
+ names = (tools(ctx)?.schemas?.(agent) ?? []).map((schema) => schema.name).filter((name) => typeof name === 'string')
356
+ } catch { /* a broken face reads as empty */ }
357
+ if (names.some((name) => name === 'ops_inspect_local_file' || name === 'ops_grep' || name === 'ops_bash')) {
358
+ if (!withdrawnByOps) {
359
+ withdrawnByOps = true
360
+ decided = true
361
+ unpublish('dsh-ops detected (ops_* tools present): the gb_* surface stands down')
362
+ }
363
+ return
364
+ }
365
+ void publish().catch((error) => log(`FastCtx bridge mount failed: ${error?.message ?? error}`))
366
+ }
367
+
368
+ try {
369
+ ctx.on('agent/created', ({ agent } = {}) => { try { reconcile(agent) } catch { /* never break agent creation */ } })
370
+ } catch (error) {
371
+ log(`FastCtx bridge wiring failed: ${error?.message ?? error}`)
372
+ return
373
+ }
374
+ ctx.effect(() => () => {
375
+ unpublish()
376
+ if (client) { void client.close().catch(() => {}) }
377
+ }, 'dsh-gitbash-shell: FastCtx bridge')
378
+ }