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.
- package/CHANGELOG.md +27 -0
- package/README.md +38 -15
- package/README_EN.md +29 -8
- package/dsh.plugin.json +2 -2
- package/package.json +4 -1
- package/src/client.js +170 -0
- package/src/compositions.js +27 -23
- package/src/fastctx-bridge.js +378 -0
- package/src/index.js +224 -8
- package/src/inline-tools.js +434 -0
package/src/compositions.js
CHANGED
|
@@ -27,7 +27,13 @@ function off(value) {
|
|
|
27
27
|
return value === true ? { disabled: true } : {}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
}
|