dsh-ops 0.2.2 → 0.2.4

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/dsh-plugin.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifestVersion": "0.15",
4
4
  "id": "dsh-ops.fastctx",
5
5
  "name": "dsh-ops",
6
- "version": "0.2.2",
6
+ "version": "0.2.4",
7
7
  "license": "MIT AND Apache-2.0",
8
8
  "facets": {
9
9
  "host": {
@@ -39,9 +39,9 @@
39
39
  "contributes": {
40
40
  "x-display": [
41
41
  {
42
- "icon": "./icon.svg",
43
- "title": "Repository tools",
44
- "description": "Read, search, replace, and run commands through the hosted FastCtx ops_* tool surface instead of shell commands.",
42
+ "icon": "./icon.png",
43
+ "title": "dsh-ops",
44
+ "description": "Provides bash, PowerShell 7, and a suite of high-performance tools written in Rust for Windows DSH to enhance token efficiency",
45
45
  "locales": [
46
46
  "locale/en.json",
47
47
  "locale/zh.json"
@@ -50,9 +50,19 @@
50
50
  ],
51
51
  "x-prompt-sections": [
52
52
  {
53
- "name": "dsh-ops:repository-tooling",
53
+ "name": "dsh-ops:repository-tooling:file",
54
54
  "order": 3250,
55
- "summary": "Compact tools-to-bash-to-PowerShell-7 routing; prefer bash except for Windows-native operations."
55
+ "summary": "File operations only; follows the file component visibility."
56
+ },
57
+ {
58
+ "name": "dsh-ops:repository-tooling:shell",
59
+ "order": 3251,
60
+ "summary": "Prefer bash, use bundled PowerShell 7 for Windows-native functionality; removed when shell component is off."
61
+ },
62
+ {
63
+ "name": "dsh-ops:repository-tooling:background",
64
+ "order": 3252,
65
+ "summary": "Owned background tasks only; absent by default."
56
66
  }
57
67
  ],
58
68
  "x-tool-namespaces": [
@@ -66,21 +76,39 @@
66
76
  "grep",
67
77
  "glob",
68
78
  "replace",
69
- "run",
70
79
  "run_background",
71
80
  "job_output",
72
81
  "job_list",
73
82
  "job_kill"
74
83
  ],
75
- "shellPermission": "enableShellTools AND per-session sandboxPolicy danger-full-access"
84
+ "shellPermission": "background component enabled AND enableShellTools AND per-session sandboxPolicy danger-full-access"
76
85
  },
77
86
  {
78
87
  "serverName": "dsh-ops",
79
88
  "transport": "in-process",
80
89
  "source": "the plugin's bash layer via host subprocess",
81
90
  "publicPrefix": "ops_",
82
- "tools": ["bash"],
83
- "shellPermission": "publishBashTool AND per-session sandboxPolicy danger-full-access"
91
+ "tools": [
92
+ "bash"
93
+ ],
94
+ "shellPermission": "shell component enabled AND publishBashTool AND per-session sandboxPolicy danger-full-access"
95
+ }
96
+ ],
97
+ "x-components": [
98
+ {
99
+ "id": "dsh-ops-shell",
100
+ "entry": "dsh-ops/shell",
101
+ "defaultEnabled": true
102
+ },
103
+ {
104
+ "id": "dsh-ops-file",
105
+ "entry": "dsh-ops/file",
106
+ "defaultEnabled": true
107
+ },
108
+ {
109
+ "id": "dsh-ops-background",
110
+ "entry": "dsh-ops/background",
111
+ "defaultEnabled": false
84
112
  }
85
113
  ]
86
114
  },
package/icon.png ADDED
Binary file
@@ -0,0 +1,3 @@
1
+ import { applyComponent } from "../../index.js"
2
+ export const name = "dsh-ops-background"
3
+ export function apply(ctx, config) { return applyComponent(ctx, config, "background") }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "dsh-ops-background",
4
+ "description": "Manage scientific simulations, model training, and other long-running background tasks."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "dsh-ops-background",
4
+ "description": "为科研仿真、模型训练及其他长进程后台任务提供托管"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "dsh-ops-background",
3
+ "description": "Manage scientific simulations, model training, and other long-running background tasks.",
4
+ "icon": "./icon.png",
5
+ "type": "module"
6
+ }
Binary file
@@ -0,0 +1,3 @@
1
+ import { applyComponent } from "../../index.js"
2
+ export const name = "dsh-ops-file"
3
+ export function apply(ctx, config) { return applyComponent(ctx, config, "file") }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "dsh-ops-file",
4
+ "description": "High-performance Rust file search with faster execution and compact output for token efficiency."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "dsh-ops-file",
4
+ "description": "更快、更高性能、输出更精简更省token的rust文件检索"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "dsh-ops-file",
3
+ "description": "High-performance Rust file search with faster execution and compact output for token efficiency.",
4
+ "icon": "./icon.png",
5
+ "type": "module"
6
+ }
Binary file
@@ -0,0 +1,3 @@
1
+ import { applyComponent } from "../../index.js"
2
+ export const name = "dsh-ops-bash & powershell 7"
3
+ export function apply(ctx, config) { return applyComponent(ctx, config, "shell") }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "dsh-ops-bash & powershell 7",
4
+ "description": "Prefer bash for general commands and provide PowerShell 7 when Windows-native functionality is needed."
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "dsh-ops-bash & powershell 7",
4
+ "description": "将默认终端改为bash,以及bash覆盖不到时提供powershell7"
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "dsh-ops-bash & powershell 7",
3
+ "description": "Prefer bash for general commands and provide PowerShell 7 when Windows-native functionality is needed.",
4
+ "icon": "./icon.png",
5
+ "type": "module"
6
+ }
package/lib/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /** dsh-ops host half. Own registrations only; no @deepseek-ai value imports. */
2
2
  import { PACKAGE_ROOT, probeBinary, resolveBinary } from './binary.js'
3
3
  import { resolveConfig } from './config.js'
4
- import { SECTION_ORDERS, TOOLING_SECTION, hostShellRefusal, renderToolingPolicy } from './policy.js'
4
+ import { SECTION_ORDERS, TOOLING_SECTION, BACKGROUND_TOOLS, FILE_TOOLS, BASH_TOOL, publicToolName, hostShellRefusal, renderToolingPolicy } from './policy.js'
5
5
  import { resolveShells } from './shells.js'
6
6
  import { mountSessionBash } from './session-shells.js'
7
+ import { mountBundledPwsh } from './session-pwsh.js'
7
8
  import { FastCtxTools } from './tools.js'
8
9
 
9
10
  export const name = 'dsh-ops'
@@ -54,30 +55,34 @@ function installHostShellDenial(ctx, config) {
54
55
  }
55
56
  }
56
57
 
57
- async function mountServer(ctx, config, runtime) {
58
+ async function mountServer(ctx, config, runtime, component) {
58
59
  if (!runtime) return
59
- const surface = new FastCtxTools({ ctx, config, runtime, report: (message, level) => report(ctx, message, level) })
60
+ const background = component === 'background'
61
+ const surface = new FastCtxTools({ ctx, config, runtime,
62
+ fileTools: !background, commandTools: background ? BACKGROUND_TOOLS : [], shared: true,
63
+ report: (message, level) => report(ctx, message, level) })
60
64
  ctx.effect(() => () => surface.stop(), 'dsh-ops: fastctx server')
61
- ctx.on('agent/created', ({ agent }) => surface.attachAgent(agent))
62
- ctx.on('agent/disposed', ({ agent }) => surface.detachAgent(agent))
63
- ctx.on('session/event', (session, event) => {
64
- if (event.type === 'sandbox/mode') surface.refreshAgents(session)
65
- })
66
- // Authority may mount after the tools service or be hot-reloaded. Losing it
67
- // immediately retires every command layer; it never falls back to env state.
68
- surface.authorityAvailable = false
69
- ctx.inject(['sandboxPolicy'], authorityCtx => {
70
- surface.authorityAvailable = true
71
- surface.refreshAgents()
72
- authorityCtx.effect(() => () => {
73
- surface.authorityAvailable = false
65
+ if (background) {
66
+ ctx.on('agent/created', ({ agent }) => surface.attachAgent(agent))
67
+ ctx.on('agent/disposed', ({ agent }) => surface.detachAgent(agent))
68
+ ctx.on('session/event', (_session, event) => {
69
+ if (event.type === 'sandbox/mode') surface.refreshAgents()
70
+ })
71
+ // Background command layers follow authoritative per-session permission.
72
+ surface.authorityAvailable = false
73
+ ctx.inject(['sandboxPolicy'], authorityCtx => {
74
+ surface.authorityAvailable = true
74
75
  surface.refreshAgents()
75
- }, 'dsh-ops: permission authority')
76
- })
77
- for (const agent of ctx.get('agents')?.list?.() ?? []) surface.attachAgent(agent)
76
+ authorityCtx.effect(() => () => {
77
+ surface.authorityAvailable = false
78
+ surface.refreshAgents()
79
+ }, 'dsh-ops: permission authority')
80
+ })
81
+ for (const agent of ctx.get('agents')?.list?.() ?? []) surface.attachAgent(agent)
82
+ }
78
83
  try {
79
84
  const tools = await surface.start()
80
- report(ctx, `dsh-ops: FastCtx ${runtime.version} ready; ${tools.length} file tools, session-authorized command layers.`, 'info')
85
+ report(ctx, `dsh-ops: FastCtx ${runtime.version} ready; ${component} component, ${tools.length} global tools.`, 'info')
81
86
  } catch (error) {
82
87
  report(ctx, `dsh-ops: FastCtx unavailable: ${error.message}`)
83
88
  if (config.required) throw error
@@ -85,24 +90,33 @@ async function mountServer(ctx, config, runtime) {
85
90
  }
86
91
  }
87
92
 
88
- export function apply(root, rawConfig) {
93
+ export function applyComponent(root, rawConfig, component) {
89
94
  const config = resolveConfig(rawConfig)
90
- const runtime = prepareRuntime(root, config)
91
- const shells = resolveShells(config)
95
+ const runtime = component === 'shell' ? undefined : prepareRuntime(root, config)
96
+ const shells = component === 'shell' ? resolveShells(config) : undefined
97
+ const owned = component === 'shell' ? [BASH_TOOL, 'pwsh']
98
+ : component === 'file' ? FILE_TOOLS.map(publicToolName) : BACKGROUND_TOOLS.map(publicToolName)
99
+ if (component === 'file') owned.push('read_image')
92
100
  if (config.promptPolicy) root.inject(['systemPrompt'], ctx => {
93
101
  ctx.effect(() => ctx.systemPrompt.section({
94
- name: TOOLING_SECTION,
95
- order: SECTION_ORDERS[TOOLING_SECTION],
102
+ name: `${TOOLING_SECTION}:${component}`,
103
+ order: SECTION_ORDERS[TOOLING_SECTION] + (component === 'shell' ? 1 : component === 'background' ? 2 : 0),
96
104
  interpolate: false,
97
105
  text: context => renderToolingPolicy({
98
- published: visibleTools(ctx, context.agent), pwsh7: shells.pwsh.available,
106
+ published: visibleTools(ctx, context.agent).filter(name => owned.includes(name)),
107
+ shellComponent: component === 'shell', pwsh7: shells?.pwsh.available ?? false,
99
108
  extraGuidance: config.extraGuidance,
100
109
  }),
101
- }), 'dsh-ops: repository tooling')
102
- })
103
- root.inject(['tools'], ctx => {
104
- mountSessionBash(ctx, config, shells)
105
- if (config.shellPolicy === 'deny-host-shell') installHostShellDenial(ctx, config)
106
- return mountServer(ctx, config, runtime)
110
+ }), `dsh-ops: ${component} tooling`)
107
111
  })
112
+ if (component === 'shell') {
113
+ mountBundledPwsh(root, shells)
114
+ root.inject(['tools'], ctx => {
115
+ mountSessionBash(ctx, config, shells)
116
+ if (config.shellPolicy === 'deny-host-shell') installHostShellDenial(ctx, config)
117
+ })
118
+ } else root.inject(['tools'], ctx => mountServer(ctx, config, runtime, component))
108
119
  }
120
+
121
+ // Backward-compatible module entry. The bundle uses independently toggled rows.
122
+ export function apply(root, rawConfig) { return applyComponent(root, rawConfig, 'file') }
package/lib/policy.js CHANGED
@@ -5,7 +5,8 @@ export const HOST_SHELL_SECTION = 'dsh-ops:host-shell-policy'
5
5
  export const SECTION_ORDERS = Object.freeze({ [TOOLING_SECTION]: 3250 })
6
6
  export const TOOL_PREFIX = 'ops_'
7
7
  export const FILE_TOOLS = Object.freeze(['inspect_local_file', 'grep', 'glob', 'replace'])
8
- export const SHELL_TOOLS = Object.freeze(['run', 'run_background', 'job_output', 'job_list', 'job_kill'])
8
+ export const BACKGROUND_TOOLS = Object.freeze(['run_background', 'job_output', 'job_list', 'job_kill'])
9
+ export const SHELL_TOOLS = Object.freeze(['run', ...BACKGROUND_TOOLS])
9
10
  // The plugin's second layer executes POSIX commands; pwsh7 uses the host tool.
10
11
  export const IN_PROCESS_TOOLS = Object.freeze(['bash'])
11
12
  export function publicToolName(rawName) { return `${TOOL_PREFIX}${rawName}` }
@@ -23,17 +24,24 @@ export function ladderLevels({ published, pwsh = false }) {
23
24
  }
24
25
 
25
26
  /** A single compact table; absent tools never get advertised. */
26
- export function renderToolingPolicy({ published, enableShellTools, mounted, levels, pwsh7 = false, extraGuidance = '' }) {
27
+ export function renderToolingPolicy({ published, enableShellTools, mounted, levels, pwsh7 = false, shellComponent = true, extraGuidance = '' }) {
27
28
  const names = new Set(published ?? (mounted || levels?.fastctx
28
29
  ? [...FILE_TOOLS, ...(enableShellTools ? SHELL_TOOLS : []), ...(levels?.bash ? IN_PROCESS_TOOLS : [])].map(publicToolName) : []))
29
30
  if (levels?.pwsh) names.add('pwsh')
30
- if (!FILE_TOOLS.some(name => names.has(publicToolName(name))) && !names.has(BASH_TOOL)) return ''
31
- const lines = ['# Repository operations: tools → bash → PowerShell 7', '', '| Task | Tool |', '| --- | --- |']
31
+ if (!shellComponent) names.delete('pwsh')
32
+ const hasFiles = FILE_TOOLS.some(name => names.has(publicToolName(name)))
33
+ const hasBash = names.has(BASH_TOOL)
34
+ const hasBackground = names.has(publicToolName('run_background'))
35
+ const hasPwsh = shellComponent && names.has('pwsh')
36
+ if (!hasFiles && !hasBash && !hasBackground && !hasPwsh) return ''
37
+ const heading = hasBash ? '# Shell routing: bash → PowerShell 7'
38
+ : hasFiles ? '# Repository file tools' : hasBackground ? '# Background task tools' : '# Windows-native shell'
39
+ const lines = [heading, '', '| Task | Tool |', '| --- | --- |']
32
40
  const row = (raw, task) => {
33
41
  if (names.has(publicToolName(raw))) lines.push(`| ${task} | ${publicToolName(raw)} |`)
34
42
  }
35
43
  row('inspect_local_file', 'Text / multiple ranges / encoding / PDF text / hex')
36
- lines.push('| Images | read_image (host) |')
44
+ if (hasFiles && names.has('read_image')) lines.push('| Images | read_image (host) |')
37
45
  row('grep', 'Search contents')
38
46
  row('glob', 'Find paths')
39
47
  row('replace', 'Batch replacement; use host edit for precise edits')
@@ -41,9 +49,10 @@ export function renderToolingPolicy({ published, enableShellTools, mounted, leve
41
49
  if (names.has('pwsh')) lines.push(`| Windows-native cmdlets / registry / services only | pwsh (${pwsh7 || levels?.pwsh ? 'bundled PowerShell 7 via host' : 'host PowerShell'}) |`)
42
50
  row('run', 'Bounded bash command result (full access only)')
43
51
  if (names.has(publicToolName('run_background'))) {
44
- lines.push(`| Background commands / owned jobs | ${SHELL_TOOLS.slice(1).map(publicToolName).join(', ')} |`)
52
+ lines.push(`| Background commands / owned jobs | ${BACKGROUND_TOOLS.map(publicToolName).filter(name => names.has(name)).join(', ')} |`)
45
53
  }
46
- lines.push('', '- Do not build shell command lines for file operations.',
54
+ lines.push('', '- ops_* arguments are strict: pass only declared fields.')
55
+ if (hasFiles) lines.push('- Use file tools for file operations; use count for totals and batch independent requests.',
47
56
  '- On a tool error, correct the arguments and retry; do not switch to a shell workaround.')
48
57
  if (names.has(BASH_TOOL)) lines.push('- Prefer bash for command execution. Use PowerShell only when Windows-native functionality is necessary; fix bash errors in bash, do not switch shells or mix their syntax.')
49
58
  if (names.has(publicToolName('run_background'))) lines.push('- Only operate on job IDs you started in this session.')
@@ -0,0 +1,42 @@
1
+ /** One plugin-owned FastCtx connection shared by independently toggled components. */
2
+ import { McpStdioClient } from './handshake.js'
3
+ const pools = new WeakMap()
4
+ export async function acquireRuntime(ctx, runtime, env) {
5
+ const root = ctx.root
6
+ let pool = pools.get(root)
7
+ if (!pool) { pool = { clients: new Map(), closing: Promise.resolve() }; pools.set(root, pool) }
8
+ await pool.closing
9
+ let entry = pool.clients.get(runtime.file)
10
+ if (!entry) {
11
+ const client = McpStdioClient.start({ file: runtime.file, args: ['serve', '--enable-shell'], env, timeoutMs: 60000 })
12
+ entry = { client, refs: 0, ready: undefined }
13
+ pool.clients.set(runtime.file, entry)
14
+ entry.ready = (async () => {
15
+ try {
16
+ const handshake = await client.initialize('dsh-ops-components')
17
+ const tools = await client.listTools()
18
+ return { client, handshake, tools }
19
+ } catch (error) {
20
+ if (pool.clients.get(runtime.file) === entry) pool.clients.delete(runtime.file)
21
+ await client.close()
22
+ throw error
23
+ }
24
+ })()
25
+ void client.exited.then(() => { if (pool.clients.get(runtime.file) === entry) pool.clients.delete(runtime.file) })
26
+ }
27
+ entry.refs += 1
28
+ let released = false
29
+ const release = async () => {
30
+ if (released) return
31
+ released = true
32
+ entry.refs -= 1
33
+ if (!entry.refs) {
34
+ if (pool.clients.get(runtime.file) === entry) pool.clients.delete(runtime.file)
35
+ const closing = pool.closing.then(() => entry.client.close())
36
+ pool.closing = closing.catch(() => {})
37
+ await closing
38
+ }
39
+ }
40
+ try { return { ...await entry.ready, release } }
41
+ catch (error) { await release(); throw error }
42
+ }
@@ -0,0 +1,31 @@
1
+ /** A reversible runtime-only config overlay; never edits profile files or services. */
2
+ export function mountBundledPwsh(ctx, shells) {
3
+ if (!shells.pwsh.available || process.platform !== 'win32') return
4
+ ctx.inject(['loader'], loaderCtx => {
5
+ const loader = loaderCtx.get('loader')
6
+ const matches = entry => entry?.options?.id === 'pwsh-sandbox'
7
+ && entry.options.name === '@deepseek-ai/dsh-pwsh-sandbox'
8
+ const refresh = () => {
9
+ for (const entry of loader.entries()) {
10
+ if (matches(entry) && entry.fiber?.state === 2) {
11
+ // Public Fiber.update with noSave recomputes from the owner's latest
12
+ // raw config. No stored config, volatile reference, or method is changed.
13
+ entry.fiber.update(entry.options.config, true)
14
+ }
15
+ }
16
+ }
17
+ const unhook = loaderCtx.on('internal/config', function (_raw, next) {
18
+ const config = next()
19
+ if (!matches(this.entry) || this.parent?.fiber?.entry === this.entry) return config
20
+ return { ...config, pwshPath: shells.pwsh.file }
21
+ }, { global: true, prepend: true })
22
+ let stopped = false
23
+ loaderCtx.effect(() => () => {
24
+ if (stopped) return
25
+ stopped = true
26
+ unhook()
27
+ refresh()
28
+ }, 'dsh-ops: restore owning PowerShell executor config')
29
+ refresh()
30
+ })
31
+ }
package/lib/tools.js CHANGED
@@ -23,6 +23,8 @@ import { McpStdioClient } from './handshake.js'
23
23
  import { FILE_TOOLS, SHELL_TOOLS, publicToolName } from './policy.js'
24
24
  import { projectTool } from './presentation.js'
25
25
  import { cleanBackground, jobEntries, startedJob } from './jobs.js'
26
+ import { acquireRuntime } from './runtime-pool.js'
27
+ import { resolveShells } from './shells.js'
26
28
 
27
29
  /** Deadline for one connection attempt: spawn, initialize, and `tools/list`. */
28
30
  const CONNECT_TIMEOUT_MS = 60_000
@@ -216,10 +218,16 @@ export class FastCtxTools {
216
218
  * @param {{file: string, source: string, version: string}} options.runtime - the resolved FastCtx executable.
217
219
  * @param {(message: string, level?: string) => void} [options.report] - where asynchronous events are reported.
218
220
  */
219
- constructor({ ctx, config, runtime, report }) {
221
+ constructor({ ctx, config, runtime, report, fileTools = true, commandTools = SHELL_TOOLS, shared = false }) {
220
222
  this.ctx = ctx
221
223
  this.config = config
222
224
  this.runtime = runtime
225
+ this.fileTools = fileTools
226
+ this.commandTools = commandTools
227
+ this.shared = shared
228
+ this.leases = new Map()
229
+ this.pendingCalls = new Set()
230
+ this.shuttingDown = false
223
231
  this.report = report ?? (() => {})
224
232
  /** The live connection, or undefined while the surface is down. */
225
233
  this.client = undefined
@@ -282,7 +290,7 @@ export class FastCtxTools {
282
290
  if (!entry?.ctx) return
283
291
  for (const dispose of entry.disposers) dispose()
284
292
  entry.disposers = []
285
- if (this.stopped) return
293
+ if (this.stopped || this.shuttingDown) return
286
294
  if (!this.shellAllowed(agent.session)) {
287
295
  // A child must not inherit an authorized ancestor's command surface.
288
296
  const names = (this.ctx.get('tools')?.schemas(agent) ?? []).map(tool => tool.name)
@@ -293,8 +301,8 @@ export class FastCtxTools {
293
301
  }
294
302
  return
295
303
  }
296
- const commandTools = this.tools.filter(tool => SHELL_TOOLS.includes(tool.name))
297
- if (commandTools.length !== SHELL_TOOLS.length) return
304
+ const commandTools = this.tools.filter(tool => this.commandTools.includes(tool.name))
305
+ if (commandTools.length !== this.commandTools.length) return
298
306
  try {
299
307
  for (const tool of commandTools) {
300
308
  entry.disposers.push(entry.ctx.tools.register(toolDefinition({
@@ -367,6 +375,20 @@ export class FastCtxTools {
367
375
  * @returns {Promise<void>} resolves once the child is gone.
368
376
  */
369
377
  async stop() {
378
+ this.shuttingDown = true
379
+ this.refreshAgents()
380
+ // Drain launches before killing only this component's owned jobs. Closing
381
+ // FastCtx alone does not stop its durable supervisors.
382
+ await Promise.allSettled([...this.pendingCalls])
383
+ const cleanupClient = this.client
384
+ if (cleanupClient) {
385
+ for (const ids of this.ownedJobs.values()) for (const job_id of ids) {
386
+ try {
387
+ const result = await cleanupClient.callTool('job_kill', { job_id }, { timeoutMs: 15000 })
388
+ if (result?.isError) this.report(`dsh-ops: owned job ${job_id} could not be stopped during component shutdown.`, 'warn')
389
+ } catch (error) { this.report(`dsh-ops: owned job ${job_id} shutdown failed: ${messageOf(error)}`, 'warn') }
390
+ }
391
+ }
370
392
  this.stopped = true
371
393
  if (this.timer !== undefined) {
372
394
  clearTimeout(this.timer)
@@ -380,7 +402,14 @@ export class FastCtxTools {
380
402
  this.tools = []
381
403
  this.ownedJobs.clear()
382
404
  await Promise.all([...this.agents.keys()].map(agent => this.detachAgent(agent)))
383
- if (client !== undefined) await client.close()
405
+ if (client !== undefined) await this.#release(client)
406
+ }
407
+
408
+ async #release(client) {
409
+ const lease = this.leases.get(client)
410
+ this.leases.delete(client)
411
+ if (lease) await lease.release()
412
+ else if (!this.shared) await client.close()
384
413
  }
385
414
 
386
415
  /**
@@ -388,7 +417,16 @@ export class FastCtxTools {
388
417
  * @returns {Promise<void>} resolves once the generation is live.
389
418
  */
390
419
  async #connect() {
391
- const client = McpStdioClient.start({
420
+ let lease
421
+ if (this.shared) {
422
+ const env = childEnv()
423
+ const bash = resolveShells(this.config).bash
424
+ if (bash.available) env.FASTCTX_BASH = bash.file
425
+ lease = await acquireRuntime(this.ctx, this.runtime, env)
426
+ if (this.shuttingDown || this.stopped) { await lease.release(); throw new StoppedError() }
427
+ this.leases.set(lease.client, lease)
428
+ }
429
+ const client = lease?.client ?? McpStdioClient.start({
392
430
  file: this.runtime.file,
393
431
  args: serverArgs(this.config.enableShellTools),
394
432
  env: childEnv(),
@@ -401,9 +439,9 @@ export class FastCtxTools {
401
439
  // already replaced or abandoned.
402
440
  void client.exited.then(({ code, signal, error }) => this.#onExit(client, code, signal, error))
403
441
  try {
404
- const handshake = await client.initialize('dsh-ops')
405
- const tools = await client.listTools()
406
- if (this.stopped || client !== this.client) throw new StoppedError()
442
+ const handshake = lease?.handshake ?? await client.initialize('dsh-ops')
443
+ const tools = lease?.tools ?? await client.listTools()
444
+ if (this.stopped || this.shuttingDown || client !== this.client) throw new StoppedError()
407
445
  this.ownedJobs.clear()
408
446
  this.#publish(tools)
409
447
  this.serverInfo = handshake?.serverInfo
@@ -411,7 +449,7 @@ export class FastCtxTools {
411
449
  this.serverInstructions = ''
412
450
  this.connectedAt = Date.now()
413
451
  } catch (error) {
414
- await client.close()
452
+ await this.#release(client)
415
453
  if (this.client === client) this.client = undefined
416
454
  throw error
417
455
  }
@@ -426,7 +464,8 @@ export class FastCtxTools {
426
464
  * @returns {void}
427
465
  */
428
466
  #onExit(client, code, signal, error) {
429
- if (this.stopped || client !== this.client) return
467
+ if (this.stopped || this.shuttingDown || client !== this.client) return
468
+ void this.#release(client)
430
469
  this.client = undefined
431
470
  this.ownedJobs.clear()
432
471
  this.tools = []
@@ -449,7 +488,7 @@ export class FastCtxTools {
449
488
  #scheduleReconnect() {
450
489
  // One pending timer at a time: a crash and a failed first connect can both
451
490
  // land here, and two loops would double every later attempt.
452
- if (this.stopped || this.timer !== undefined) return
491
+ if (this.stopped || this.shuttingDown || this.timer !== undefined) return
453
492
  // A connection that stayed up long enough closed the previous outage.
454
493
  if (this.connectedAt !== undefined && Date.now() - this.connectedAt >= RECONNECT.maxDelayMs) {
455
494
  this.attempts = 0
@@ -486,12 +525,12 @@ export class FastCtxTools {
486
525
  * @returns {Promise<void>} resolves once this attempt settled.
487
526
  */
488
527
  async #reconnect() {
489
- if (this.stopped) return
528
+ if (this.stopped || this.shuttingDown) return
490
529
  try {
491
530
  await this.#connect()
492
531
  this.report(`dsh-ops: ${this.#describe()} reconnected with ${this.names().length} tool(s).`)
493
532
  } catch (error) {
494
- if (this.stopped) return
533
+ if (this.stopped || this.shuttingDown) return
495
534
  this.report(
496
535
  `dsh-ops: FastCtx reconnect attempt ${this.attempts}/${RECONNECT.maxAttempts} `
497
536
  + `failed (${messageOf(error)}).`,
@@ -521,7 +560,7 @@ export class FastCtxTools {
521
560
  throw new Error(`${this.#describe()} listed the tool "${String(tool?.name)}" more than once`)
522
561
  }
523
562
  seen.add(name)
524
- if (!FILE_TOOLS.includes(tool.name)) continue
563
+ if (!this.fileTools || !FILE_TOOLS.includes(tool.name)) continue
525
564
  definitions.push(toolDefinition({
526
565
  tool,
527
566
  call: (args, exec) => this.#call(tool.name, args, exec),
@@ -556,7 +595,15 @@ export class FastCtxTools {
556
595
  * @param {{signal?: AbortSignal, agent?: {session?: object}}} [exec] - the host execution context.
557
596
  * @returns {Promise<{content: unknown[], structuredContent?: unknown}>} the canonical value.
558
597
  */
559
- async #call(rawName, args, exec) {
598
+ #call(rawName, args, exec) {
599
+ if (this.shuttingDown || this.stopped) return Promise.reject(new Error('This dsh-ops component is shutting down.'))
600
+ const call = this.#callActive(rawName, args, exec)
601
+ this.pendingCalls.add(call)
602
+ void call.then(() => this.pendingCalls.delete(call), () => this.pendingCalls.delete(call))
603
+ return call
604
+ }
605
+
606
+ async #callActive(rawName, args, exec) {
560
607
  const label = publicToolName(rawName)
561
608
  const session = exec?.agent?.session
562
609
  if (SHELL_TOOLS.includes(rawName) && !this.shellAllowed(session)) {
@@ -569,6 +616,7 @@ export class FastCtxTools {
569
616
  throw new Error('This is an image; use host read_image.')
570
617
  }
571
618
  }
619
+ if (rawName === 'run_background') args = { ...args, cwd: args?.cwd ?? session?.header?.cwd }
572
620
  const client = this.client
573
621
  if (client === undefined) {
574
622
  // A dead server must be an immediate, explicit failure rather than a
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "Repository tools",
4
- "description": "Read, search, replace, and run commands through the hosted FastCtx ops_* tool surface instead of shell commands."
3
+ "title": "dsh-ops",
4
+ "description": "Provides bash, PowerShell 7, and a suite of high-performance tools written in Rust for Windows DSH to enhance token efficiency"
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "仓库工具",
4
- "description": "通过托管的 FastCtx ops_* 工具完成读取、搜索、替换与命令执行,替代 shell 命令。"
3
+ "title": "dsh-ops",
4
+ "description": "为windows的dsh提供bash、powershell7及一系列rust编写的高性能tools来提高token效率"
5
5
  }
6
6
  }