dsh-gitbash-shell 0.39.0 → 0.39.2
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 +78 -0
- package/dsh.plugin.json +1 -1
- package/package.json +3 -2
- package/src/bash-tool.js +28 -17
- package/src/client.js +36 -25
- package/src/compositions.js +11 -7
- package/src/fastctx-bridge.js +125 -39
- package/src/index.js +163 -141
- package/src/inline-tools.js +151 -64
- package/src/shell.js +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,84 @@
|
|
|
3
3
|
> 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
|
|
4
4
|
> 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。
|
|
5
5
|
|
|
6
|
+
## v0.39.2 — 2026-10-13
|
|
7
|
+
|
|
8
|
+
**类型**:fix(route B 工具/预设生命周期、动态配置与客户端回归) / test。
|
|
9
|
+
|
|
10
|
+
- **复现与裁定**:两名 Sol 分区审查、Grok 交叉反证与主代理隔离复验确认了工具面、生命周期、
|
|
11
|
+
组合、代码扫描和客户端状态转换的问题;普通子 scope 的原始冲突仍由 v0.39.1 的 `shell` 隔离修复。
|
|
12
|
+
FastCtx 全文路径回显是既定策略,不误当文件工具替换问题;minimal 的 persistent PTY 不与全工具
|
|
13
|
+
变体的执行器来源混为一谈。只修既有功能,不新增功能、改 root shell 或市场 patch。
|
|
14
|
+
- **变体接线**:Windows 全工具 Git Bash 变体不再启用依赖 root PowerShell 的官方 `tool-bash`;
|
|
15
|
+
保留官方 pwsh 面,经同一行内补位通道接入私有 Git Bash 工具。minimal 的 persistent 通道保持。
|
|
16
|
+
- **工具生命周期**:存活 agent 的 `pwshTool` 双向翻转同步解除/重建自身的 restrict;异步导入/
|
|
17
|
+
挂载在插件或 agent 退役后不再迟到注册;jobs 服务卸载按真实 Cordis 活跃状态恢复前台 bash。
|
|
18
|
+
- **降级分支**:plain definition 的 required 标志递归转换为合法 JSON Schema;升级助手不可用
|
|
19
|
+
时仍解析并传递每会话 standing policy,不错误回退部署默认。两项为模块解析降级的受控复现,
|
|
20
|
+
不宣称本机生产已发生导入失败,也不改变 Windows 受限模式明示 unconfined 的既定取舍。
|
|
21
|
+
- **预设与默认值**:串行 reconcile 退役时关闭新任务、清理晚到 register 并等待 unregister;
|
|
22
|
+
动态关闭变体在退役前迁回部署默认,避免后续新会话引用不存在的 preset。既有钉住会话不迁移。
|
|
23
|
+
- **路径与终端**:run_code 扫描辨认关键字后的正则并在不确定时整份 no-op,不改正则数据;
|
|
24
|
+
FastCtx 入/出参获得 execution 上下文并服从总方言、委托与虚拟挂载开关;better-sidebar 的
|
|
25
|
+
adoptSidebar 动态翻转恢复接管前值并保护接管后的用户手选,不改变官方 autoTerminalShell
|
|
26
|
+
关后只停止未来写入的独立语义。
|
|
27
|
+
- **FastCtx 错误**:子进程/写管道 error 进入初始化 Promise,失败清理 pending 计时器与连接;
|
|
28
|
+
MCP `isError:true` 按官方桥约定抛错,不再把失败包装成宿主成功 value。
|
|
29
|
+
发现与连接启动采用 single-flight,拥有在途连接并在卸载时取消/等待,不让并发 agent 的发现流程
|
|
30
|
+
覆盖已在使用的 lazy client。该丢失所有权链已用受控 MCP 传输复现,并新增关闭/晚注册回归。
|
|
31
|
+
- **客户端**:设置卡所有 Hooks 在 readiness early-return 前调用;工具链进度轮询在网络/HTTP
|
|
32
|
+
暂态失败后重试,卸载或失效时取消,不因单次请求失败永久卡在忙碌状态。
|
|
33
|
+
- **回归入口**:新增可移植客户端/桥/运行时/宿主回归与实际 Core preview 集成用例,接入现有
|
|
34
|
+
`npm test`/`npm run test:preview`;不新增依赖、不跳过旧断言或 Node 22 矩阵。对齐上游 v0.39.1
|
|
35
|
+
(`521be01`)后完整测试 **156/156**、实际 Core 无头 preview **14/14**、打包 dry-run 和 diff 检查
|
|
36
|
+
通过。新增执行级回归保留 #21 的 real-array glob 契约与 stringified-array 响亮拒绝;不改已发布
|
|
37
|
+
版本号、历史记录或 npm 发布流程。Grok 对修复 diff 的只读反证未发现确定遗漏。
|
|
38
|
+
首轮全量检查发现三条旧源码正则仍锁旧实现,改为对应新保护条件并补同值/手选行为测试,没有删掉
|
|
39
|
+
保护断言。原 [Issue #19](https://github.com/KannaKuron/dsh-gitbash-shell/issues/19) 和
|
|
40
|
+
[PR #20](https://github.com/KannaKuron/dsh-gitbash-shell/pull/20) 已完成;这些后续修复由新 PR 承载,
|
|
41
|
+
Windows/Linux × Node 22/24 的远端结果见其检查。preview 不等同于完整 Electron GUI 或真 MCP
|
|
42
|
+
exe 并发验收,模块解析降级使用受控夹具复现。
|
|
43
|
+
|
|
44
|
+
## v0.39.1 — 2026-10-13
|
|
45
|
+
|
|
46
|
+
**类型**:fix(route B 私有执行器的真实服务隔离与读取授权) / test / ci。
|
|
47
|
+
|
|
48
|
+
- **根因**:普通 `ctx.plugin()` 子 fiber 只隔离副作用所有权,不隔离服务命名空间。
|
|
49
|
+
`GitBashSandboxExecutor` 仍经 `SandboxBashExecutor → LocalBashExecutor → ShellExecutor → Service`
|
|
50
|
+
注册 `shell`,与 route B 保留的 `SandboxPwshExecutor` 冲突,报
|
|
51
|
+
`service "shell" has been registered at <SandboxPwshExecutor>`;bash 工具尚未注册即中断。
|
|
52
|
+
Git Bash 路径已成功解析时同样发生,不是工具发现或路径探测问题。
|
|
53
|
+
- **修复**:`mountPrivateExecutor` 仅对 `shell` 调用 `ctx.isolate('shell')`,其余宿主依赖原样继承;
|
|
54
|
+
类插件仍走官方 `plugin()` 通道,Config/volatile/static inject 与生命周期不变。
|
|
55
|
+
空 scope 通过 `get('shell')` 读取活跃私有实例,工具就绪检查用 `get('tools')`,避免
|
|
56
|
+
缺少 inject 声明的属性访问再度中断补位。失败时销毁已建 scope,成功/失败均清理挂载计时器。
|
|
57
|
+
- **边界**:不撤官方 PowerShell 执行器、不改 bundle patch、不改 shell 执行/沙箱/后台协议,
|
|
58
|
+
不抬 Core 兼容下限,不发版、不触发 npm 发布。
|
|
59
|
+
- **回归**:新增 5 项真实 Cordis 测试,覆盖普通子 scope 冲突、显式隔离后的 Config/inject 与
|
|
60
|
+
根 shell 保留、卸载、构造失败清理/再挂载、缺服务失败与挂载超时清理;不是仅正则源码检查。
|
|
61
|
+
既有 minimal PTY 测试的非 Windows 断言现在显式模拟 Linux,不再在恢复 win32 后误断言 `/bin/bash`。
|
|
62
|
+
首轮远端 CI 发现两项独立 error-content 测试误嵌套在父测试中,Node 22 会取消未等待的子测试;
|
|
63
|
+
恢复独立结构并保留全部断言,不跳过测试、不移除 Node 22 矩阵。
|
|
64
|
+
- **验证入口**:`npm test` 合并 smoke 与真实 Cordis 回归;新增 `npm run test:preview`,通过
|
|
65
|
+
显式 `DSH_PREVIEW_HOST_DIR` 只读解析目标宿主包,在新 Context + 临时 cwd 做无头运行时 preview。
|
|
66
|
+
Windows 本机已通过:Core **0.2.1-alpha.2**,宿主 Cordis **4.0.5-alpha.1**,Node **24.13.0**;
|
|
67
|
+
原路径真实重现服务冲突,修复后 bash 出现在实际 ToolRuntime schema 中并执行
|
|
68
|
+
`command -v bash`/`uname -s`/sentinel(退出码 0),官方 pwsh 执行 `Get-Process`,
|
|
69
|
+
`pwshTool` 开/关两态与卸载恢复均通过。**不等同于完整 GUI、PTY、后台任务或受限模式验收**。
|
|
70
|
+
- **CI**:新增非发布 `ci` 工作流(push 修复分支 / PR / 手动触发),Windows/Linux × Node 22/24,
|
|
71
|
+
锁定依赖、禁止生命周期脚本与可选载荷、完整测试及 `npm pack --dry-run --ignore-scripts`。
|
|
72
|
+
本机完整测试 **127/127 通过**,打包预检与 `git diff --check` 通过;远端矩阵结果见对应 PR 检查。
|
|
73
|
+
复验命令和覆盖边界见 `tests/README.md`。
|
|
74
|
+
- **gb_glob 守卫封杀了上游主用法([#21](https://github.com/KannaKuron/dsh-gitbash-shell/issues/21),同日报告)**:
|
|
75
|
+
上游 `tools/list` 的 glob `pattern` 是 `string | string[]` 的 untagged enum——**数组才是主形态**
|
|
76
|
+
(schema 示例本身就是数组),v0.38.5 的只认字符串守卫把它一并拒绝;且模型常把数组 JSON 序列化后
|
|
77
|
+
传入(实测 25 次调用 24 次为 `["*.txt"]` 字符串),上游按**一个字面 glob** 匹配 → 静默返回空
|
|
78
|
+
——恰好是守卫要防的「静默错误答案」。修法(`validateFastctxPattern`,提取为纯导出函数):
|
|
79
|
+
glob 接受非空字符串或非空字符串数组;**字符串化数组响亮报错并给出正确形状**(绝不代模型反序列化
|
|
80
|
+
——那会掩盖错误);replace 维持单正则字符串契约;其他类型报错带类型名。gb_glob 的 description
|
|
81
|
+
增补 pattern 形状声明(模型侧修正源)。**教训**:给上游加防御守卫必须从 `tools/list` 的
|
|
82
|
+
`inputSchema` 原文出发,单一探针会误判(v0.38.5 的「静默当全部文件」实测即单一探针结论)。
|
|
83
|
+
|
|
6
84
|
## v0.39.0 — 2026-10-13
|
|
7
85
|
|
|
8
86
|
**类型**:feat(架构转向:route B——插件自带 bash 工具,不再替换宿主执行器;DSH STORE 收录合规)。
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-gitbash-shell",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.2",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "Windows 全模式 Git Bash:自带 bash 工具(私有 Git Bash 执行器实例,官方 PowerShell 执行器原样保留),官方四个模式经行内工具补位直接获得 Git Bash(v0.35.0 起;v0.39.0 起不再替换宿主执行器),四个 *·Git Bash 变体 preset 可开关;官方 pwsh 工具默认隐藏、开关可见(真 PowerShell);附带 gb_* FastCtx 文件工具,检测到 dsh-ops 自动让位。联动:dsh-ptc-cordis-preset(PTC 创造模式带 Git Bash 行且可去重)+ dsh-better-sidebar(终端标签自动切 Git Bash)。",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gitbash-shell",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.2",
|
|
4
4
|
"description": "Windows 上给 agent 一个真正的 Git Bash 命令工具:私有 Git Bash 执行器实例(官方 PowerShell 执行器原样保留),统一 MSYS 盘根路径方言,官方 pwsh 工具默认隐藏可开关,物化四个内置预设的 Git Bash 变体,侧栏终端接管可开关。· Give agents a real Git Bash tool on Windows: a private Git Bash executor instance (the official PowerShell executor stays untouched), a unified MSYS drive-root path dialect, an opt-in official pwsh tool, and four Git Bash preset variants.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"icon.svg"
|
|
53
53
|
],
|
|
54
54
|
"scripts": {
|
|
55
|
-
"test": "node --test tests/smoke.mjs",
|
|
55
|
+
"test": "node --test tests/smoke.mjs tests/private-executor.mjs tests/client-regressions.mjs tests/bridge-regressions.mjs tests/runtime-regressions.mjs tests/host-regressions.mjs",
|
|
56
|
+
"test:preview": "node --import ./tests/host-loader.mjs --test tests/preview.mjs tests/bridge-preview.mjs tests/runtime-preview.mjs tests/host-preview.mjs",
|
|
56
57
|
"version": "node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dsh.plugin.json','utf8'));const v=JSON.parse(fs.readFileSync('package.json','utf8')).version;m.version=v;fs.writeFileSync('dsh.plugin.json',JSON.stringify(m,null,2)+'\\n')\" && git add dsh.plugin.json"
|
|
57
58
|
},
|
|
58
59
|
"keywords": [
|
package/src/bash-tool.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* instead ships an OWN tool:
|
|
12
12
|
*
|
|
13
13
|
* - the executor is a private `GitBashSandboxExecutor` instance constructed
|
|
14
|
-
* through cordis's own plugin path in a
|
|
15
|
-
* `ctx.shell`, which stays
|
|
14
|
+
* through cordis's own plugin path in a `shell`-isolated scope (never
|
|
15
|
+
* registered as the ROOT `ctx.shell`, which stays PowerShell); it inherits
|
|
16
16
|
* the shipped bash-sandbox semantics (deadline, output capture, argv
|
|
17
17
|
* execution) and points them at Git for Windows' bash.exe;
|
|
18
18
|
* - the tool definition mirrors `@deepseek-ai/dsh-tool-bash` call-for-call
|
|
@@ -248,20 +248,29 @@ function outputSchemaSpec() {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
/**
|
|
252
|
-
function
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
out.properties[key] =
|
|
251
|
+
/** Convert key-level spec flags at every depth, including union branches. */
|
|
252
|
+
function schemaSpecToJson(spec) {
|
|
253
|
+
const { required: marker, ...out } = spec
|
|
254
|
+
if (Array.isArray(marker)) out.required = [...marker]
|
|
255
|
+
if (spec.properties !== undefined) {
|
|
256
|
+
const required = Array.isArray(marker) ? [...marker] : []
|
|
257
|
+
out.properties = {}
|
|
258
|
+
for (const [key, value] of Object.entries(spec.properties)) {
|
|
259
|
+
if (value.required === true && !required.includes(key)) required.push(key)
|
|
260
|
+
out.properties[key] = schemaSpecToJson(value)
|
|
261
261
|
}
|
|
262
|
-
|
|
262
|
+
if (required.length > 0) out.required = required
|
|
263
263
|
}
|
|
264
|
-
|
|
264
|
+
for (const key of ['oneOf', 'anyOf', 'allOf']) {
|
|
265
|
+
if (Array.isArray(spec[key])) out[key] = spec[key].map(schemaSpecToJson)
|
|
266
|
+
}
|
|
267
|
+
if (spec.items !== undefined) out.items = schemaSpecToJson(spec.items)
|
|
268
|
+
return out
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Plain JSON-schema variant of the same union (fallback when defineTool is absent). */
|
|
272
|
+
function outputSchemaJson() {
|
|
273
|
+
return schemaSpecToJson(outputSchemaSpec())
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
/** The official args validation (index.ts:73). */
|
|
@@ -320,8 +329,9 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
|
|
|
320
329
|
const defaultMode = executor.sandboxMode
|
|
321
330
|
const escalationReady = defaultMode !== undefined && typeof approveEscalation === 'function'
|
|
322
331
|
const escalationModes = escalationReady ? ESCALATION_TARGETS : []
|
|
332
|
+
// Missing approval helpers narrow escalation, not the session's standing policy.
|
|
323
333
|
const resolveSandboxPolicy = (exec) => {
|
|
324
|
-
if (
|
|
334
|
+
if (defaultMode === undefined) return undefined
|
|
325
335
|
const policy = ctx.get('sandboxPolicy')
|
|
326
336
|
return policy ? policy.resolve(exec?.agent === undefined ? {} : { session: exec.agent.session }) : undefined
|
|
327
337
|
}
|
|
@@ -548,7 +558,7 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
|
|
|
548
558
|
// keys reach execute too, where enableRunInBackground enforces them
|
|
549
559
|
// ("Undeclared keys are allowed, so schema omission also needs
|
|
550
560
|
// enforcement", index.ts:498).
|
|
551
|
-
parameters: { type: 'object', properties: parametersSpec()
|
|
561
|
+
parameters: schemaSpecToJson({ type: 'object', properties: parametersSpec() }),
|
|
552
562
|
output: {
|
|
553
563
|
schema: outputSchemaJson(),
|
|
554
564
|
render: (_args, value) => [{
|
|
@@ -580,7 +590,8 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
|
|
|
580
590
|
jobCtx.effect(() => () => {
|
|
581
591
|
unregister()
|
|
582
592
|
try {
|
|
583
|
-
|
|
593
|
+
// Cordis FiberState.ACTIVE is the numeric enum member 2.
|
|
594
|
+
if (ctx.fiber?.state === 2) foregroundOnly = ctx.tools.register(bashTool(undefined))
|
|
584
595
|
} catch { /* the scope is going away */ }
|
|
585
596
|
})
|
|
586
597
|
})
|
package/src/client.js
CHANGED
|
@@ -2063,6 +2063,42 @@ window.__ModuleLoader__.load({
|
|
|
2063
2063
|
if (scope && typeof scope.getSnapshot === "function") snap = scope.getSnapshot();
|
|
2064
2064
|
} catch (error_) { /* keep unavailable */ }
|
|
2065
2065
|
|
|
2066
|
+
var pageView = props.view === "page";
|
|
2067
|
+
|
|
2068
|
+
// Hooks run for every snapshot state; readiness only gates their work.
|
|
2069
|
+
useEffect(function () {
|
|
2070
|
+
if (snap.status !== "ready" || (!open && !pageView)) return undefined;
|
|
2071
|
+
if (toolsLoaded || toolBusy) return undefined;
|
|
2072
|
+
loadTools();
|
|
2073
|
+
return undefined;
|
|
2074
|
+
}, [open, pageView, snap.status]);
|
|
2075
|
+
|
|
2076
|
+
/* Poll ONLY while a job is unfinished, and through the cheap ?job=1
|
|
2077
|
+
form. A transient failure retries without discarding progress. */
|
|
2078
|
+
useEffect(function () {
|
|
2079
|
+
if (snap.status !== "ready" || toolJob === null || toolJob.finished) return undefined;
|
|
2080
|
+
var alive = true;
|
|
2081
|
+
var timer;
|
|
2082
|
+
function schedule() {
|
|
2083
|
+
timer = setTimeout(function () {
|
|
2084
|
+
fetch(TOOLS_URL + "?job=1", { headers: { accept: "application/json" } })
|
|
2085
|
+
.then(function (res) {
|
|
2086
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
2087
|
+
return res.json();
|
|
2088
|
+
})
|
|
2089
|
+
.then(function (data) {
|
|
2090
|
+
if (!alive) return;
|
|
2091
|
+
var next = data !== null && typeof data === "object" && data.job ? data.job : null;
|
|
2092
|
+
setToolJob(next);
|
|
2093
|
+
if (next !== null && next.finished) loadTools();
|
|
2094
|
+
})
|
|
2095
|
+
.catch(function () { if (alive) schedule(); });
|
|
2096
|
+
}, 1200);
|
|
2097
|
+
}
|
|
2098
|
+
schedule();
|
|
2099
|
+
return function () { alive = false; clearTimeout(timer); };
|
|
2100
|
+
}, [toolJob, snap.status]);
|
|
2101
|
+
|
|
2066
2102
|
if (snap.status !== "ready") return null;
|
|
2067
2103
|
|
|
2068
2104
|
var value = snap.value || {};
|
|
@@ -2135,7 +2171,6 @@ window.__ModuleLoader__.load({
|
|
|
2135
2171
|
// dsh 0.1.6-alpha.2: the Plugins page renders this card through the
|
|
2136
2172
|
// `plugins.bundle.config` slot with view="page" — the page draws the
|
|
2137
2173
|
// title itself; the collapsible shell only shows on the popup seat.
|
|
2138
|
-
var pageView = props.view === "page";
|
|
2139
2174
|
|
|
2140
2175
|
var mounts = snap.value.virtualMounts !== false;
|
|
2141
2176
|
var errs = snap.value.errorDialect !== false;
|
|
@@ -2469,30 +2504,6 @@ window.__ModuleLoader__.load({
|
|
|
2469
2504
|
.then(function () { setToolBusy(false); });
|
|
2470
2505
|
}
|
|
2471
2506
|
|
|
2472
|
-
useEffect(function () {
|
|
2473
|
-
if (!open && !pageView) return undefined;
|
|
2474
|
-
if (toolsLoaded || toolBusy) return undefined;
|
|
2475
|
-
loadTools();
|
|
2476
|
-
return undefined;
|
|
2477
|
-
}, [open]);
|
|
2478
|
-
|
|
2479
|
-
/* Poll ONLY while a job is unfinished, and through the cheap ?job=1
|
|
2480
|
-
form: a full probe re-runs three winget commands per second. */
|
|
2481
|
-
useEffect(function () {
|
|
2482
|
-
if (toolJob === null || toolJob.finished) return undefined;
|
|
2483
|
-
var timer = setTimeout(function () {
|
|
2484
|
-
fetch(TOOLS_URL + "?job=1", { headers: { accept: "application/json" } })
|
|
2485
|
-
.then(function (res) { return res.json(); })
|
|
2486
|
-
.then(function (data) {
|
|
2487
|
-
var next = data !== null && typeof data === "object" && data.job ? data.job : null;
|
|
2488
|
-
setToolJob(next);
|
|
2489
|
-
if (next !== null && next.finished) loadTools();
|
|
2490
|
-
})
|
|
2491
|
-
.catch(function () { /* keep the last known progress */ });
|
|
2492
|
-
}, 1200);
|
|
2493
|
-
return function () { clearTimeout(timer); };
|
|
2494
|
-
}, [toolJob]);
|
|
2495
|
-
|
|
2496
2507
|
var toolsSection = fold("sec.tools",
|
|
2497
2508
|
toolsData === null && toolErr === "" ? E("p", { className: "gb-hint" }, t("tools.loading")) : null,
|
|
2498
2509
|
toolErr !== "" ? E("p", { className: "gb-error" }, toolErr) : null,
|
package/src/compositions.js
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Every variant mirrors the official 0.1.7 preset it replaces (packages/
|
|
11
11
|
* bundle/web-app/presets/{standard,minimal,ptc,cordis}.patch.yml) with the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Git Bash routing: full variants keep the official platform-gated shell
|
|
13
|
+
* rows, and the inline patch adds OUR bash over a private executor on Windows.
|
|
14
|
+
* The minimal variant instead keeps its persistent Git Bash terminal rows.
|
|
15
|
+
* On non-Windows hosts the official stack stays native. A conditional delta exists while
|
|
16
16
|
* dsh-ptc-cordis-preset reports its experimental-CPython backend ACTIVE: the
|
|
17
17
|
* two workflow rows follow the official Python composition and go off (see
|
|
18
18
|
* `pythonActive` below). While that backend is off — the default, and also
|
|
@@ -56,7 +56,8 @@ const planSectionFor = (hostEra021) => hostEra021
|
|
|
56
56
|
* @param {'standard'|'ptc'|'cordis'} input.kind - which official preset this
|
|
57
57
|
* variant mirrors (workflow rows follow it: enabled on standard/cordis,
|
|
58
58
|
* disabled on ptc).
|
|
59
|
-
* @param {boolean} input.gitBash -
|
|
59
|
+
* @param {boolean} input.gitBash - retained for composition-call compatibility;
|
|
60
|
+
* full variants obtain Git Bash from the per-agent inline patch on Windows.
|
|
60
61
|
* @param {string|undefined} input.skillsDir - Creation authoring skills
|
|
61
62
|
* directory (cordis variant only; resolved beside the agent-preset package).
|
|
62
63
|
* @param {boolean} [input.pythonActive] - whether the experimental CPython
|
|
@@ -75,8 +76,11 @@ const planSectionFor = (hostEra021) => hostEra021
|
|
|
75
76
|
*/
|
|
76
77
|
export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hostExtras = {} }) {
|
|
77
78
|
const win = typeof process !== 'undefined' && process.platform === 'win32'
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// Route B preserves the root PowerShell executor. On Windows the official
|
|
80
|
+
// tool-bash would execute through that executor, so full variants retain the
|
|
81
|
+
// official pwsh face and let the same per-agent inline patch add OUR bash.
|
|
82
|
+
const bashDisabled = win
|
|
83
|
+
const pwshDisabled = !win
|
|
80
84
|
const workflowOn = kind !== 'ptc' && pythonActive !== true
|
|
81
85
|
// dsh 0.2.1 added two rows to every full-tool official preset — time-context
|
|
82
86
|
// (durable clock readings) and tool-schedule (the reminder tools) — plus a
|
package/src/fastctx-bridge.js
CHANGED
|
@@ -106,20 +106,21 @@ export class McpStdio {
|
|
|
106
106
|
this.child.stderr.on('data', (chunk) => {
|
|
107
107
|
this.stderrTail = (this.stderrTail + chunk).slice(-2000)
|
|
108
108
|
})
|
|
109
|
-
this.child.on('
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
|
|
109
|
+
this.child.on('error', (error) => this.#fail(error))
|
|
110
|
+
this.child.stdin.on('error', (error) => this.#fail(error))
|
|
111
|
+
this.child.on('exit', (code) => this.#fail(new Error(`the FastCtx server exited (code ${code ?? 'null'})`)))
|
|
112
|
+
try {
|
|
113
|
+
await this.#request('initialize', {
|
|
114
|
+
protocolVersion: '2024-11-05',
|
|
115
|
+
capabilities: {},
|
|
116
|
+
clientInfo: { name: 'dsh-gitbash-shell', version: '0.35.0' },
|
|
117
|
+
})
|
|
118
|
+
this.#notify('notifications/initialized')
|
|
119
|
+
return this
|
|
120
|
+
} catch (error) {
|
|
121
|
+
await this.close()
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
/** List the server's tools: [{ name, description, inputSchema }]. */
|
|
@@ -140,15 +141,21 @@ export class McpStdio {
|
|
|
140
141
|
this.dead = true
|
|
141
142
|
const child = this.child
|
|
142
143
|
this.child = undefined
|
|
143
|
-
|
|
144
|
-
reject(new Error('the FastCtx bridge was stopped'))
|
|
145
|
-
}
|
|
146
|
-
this.pending.clear()
|
|
144
|
+
this.#fail(new Error('the FastCtx bridge was stopped'))
|
|
147
145
|
if (child && child.exitCode === null && !child.killed) {
|
|
148
146
|
try { child.kill() } catch { /* already gone */ }
|
|
149
147
|
}
|
|
150
148
|
}
|
|
151
149
|
|
|
150
|
+
#fail(error) {
|
|
151
|
+
this.dead = true
|
|
152
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
153
|
+
clearTimeout(timer)
|
|
154
|
+
reject(error)
|
|
155
|
+
}
|
|
156
|
+
this.pending.clear()
|
|
157
|
+
}
|
|
158
|
+
|
|
152
159
|
#onChunk(chunk) {
|
|
153
160
|
this.buffer += chunk
|
|
154
161
|
let at = this.buffer.indexOf('\n')
|
|
@@ -209,6 +216,42 @@ export class McpStdio {
|
|
|
209
216
|
* the fastctx shapes: `path`, `file_path`, `files[].path`, and an absolute
|
|
210
217
|
* glob `pattern`. Pure.
|
|
211
218
|
*/
|
|
219
|
+
/**
|
|
220
|
+
* Validate one pattern argument BEFORE dispatch (v0.38.5 guard, reshaped in
|
|
221
|
+
* v0.39.1 around the upstream SCHEMA — issue #21). glob's `pattern` is an
|
|
222
|
+
* untagged enum of string | string[] in tools/list (the array is the PRIMARY
|
|
223
|
+
* form — the schema's own example is an array), so a string-only check bans
|
|
224
|
+
* the main usage; and a JSON-stringified array ("[\"*.txt\"]") is matched by
|
|
225
|
+
* the upstream as ONE literal glob and silently returns nothing — the exact
|
|
226
|
+
* silent-wrong-answer this guard exists to prevent. replace's pattern is a
|
|
227
|
+
* single regex string. Pure; returns an error message or undefined.
|
|
228
|
+
* @param {string} rawName - upstream tool name ('glob' | 'replace' | ...).
|
|
229
|
+
* @param {unknown} pattern - the raw pattern argument.
|
|
230
|
+
* @returns {string|undefined}
|
|
231
|
+
*/
|
|
232
|
+
export function validateFastctxPattern(rawName, pattern) {
|
|
233
|
+
if (rawName !== 'glob' && rawName !== 'replace') return undefined
|
|
234
|
+
const describe = (v) => (v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v)
|
|
235
|
+
if (rawName === 'replace') {
|
|
236
|
+
if (typeof pattern !== 'string' || pattern.trim() === '') {
|
|
237
|
+
return `invalid arguments: ${gbName(rawName)} pattern must be a non-empty string (got ${describe(pattern)})`
|
|
238
|
+
}
|
|
239
|
+
return undefined
|
|
240
|
+
}
|
|
241
|
+
// A stringified array would match as one literal glob and return an empty
|
|
242
|
+
// set: name the mistake, never guess the intent (un-serializing on the
|
|
243
|
+
// model's behalf would hide the error this guard is here to surface).
|
|
244
|
+
if (typeof pattern === 'string' && pattern.length >= 2 && pattern.startsWith('[') && pattern.endsWith(']')) {
|
|
245
|
+
return `invalid arguments: ${gbName(rawName)} pattern looks like a JSON-stringified array (${String(pattern).slice(0, 60)}). Pass a real array of strings (e.g. ["*.js","*.py"]) or one plain glob string — a stringified array matches nothing.`
|
|
246
|
+
}
|
|
247
|
+
const ok = (typeof pattern === 'string' && pattern.trim() !== '')
|
|
248
|
+
|| (Array.isArray(pattern) && pattern.length > 0 && pattern.every((p) => typeof p === 'string' && p.trim() !== ''))
|
|
249
|
+
if (!ok) {
|
|
250
|
+
return `invalid arguments: ${gbName(rawName)} pattern must be a non-empty string or a non-empty array of non-empty strings (got ${describe(pattern)})`
|
|
251
|
+
}
|
|
252
|
+
return undefined
|
|
253
|
+
}
|
|
254
|
+
|
|
212
255
|
export function translateFastctxArgs(args, msysToWin) {
|
|
213
256
|
if (!msysToWin || !args || typeof args !== 'object') return args
|
|
214
257
|
const translate = (value) => (typeof value === 'string' ? msysToWin(value) : value)
|
|
@@ -274,6 +317,10 @@ export function mountFastctx(ctx, deps) {
|
|
|
274
317
|
let client = undefined
|
|
275
318
|
let decided = false
|
|
276
319
|
let withdrawnByOps = false
|
|
320
|
+
let closed = false
|
|
321
|
+
let publishing
|
|
322
|
+
let connecting
|
|
323
|
+
const starting = new Set()
|
|
277
324
|
|
|
278
325
|
const unpublish = (reason) => {
|
|
279
326
|
if (disposer) {
|
|
@@ -284,16 +331,34 @@ export function mountFastctx(ctx, deps) {
|
|
|
284
331
|
}
|
|
285
332
|
|
|
286
333
|
const callWithReconnect = async (rawName, args) => {
|
|
334
|
+
if (closed || withdrawnByOps) throw new Error('the FastCtx bridge was stopped')
|
|
287
335
|
if (client === undefined || client.isDead) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
336
|
+
if (!connecting) {
|
|
337
|
+
connecting = (async () => {
|
|
338
|
+
const { file } = resolveFastctx(ctx, createRequireFrom)
|
|
339
|
+
if (file === '') throw new Error('the FastCtx payload is not installed; repair the profile dependencies (frozen install) and retry')
|
|
340
|
+
const fresh = new McpStdio({ file, log })
|
|
341
|
+
starting.add(fresh)
|
|
342
|
+
try {
|
|
343
|
+
await fresh.start()
|
|
344
|
+
if (closed || withdrawnByOps) throw new Error('the FastCtx bridge was stopped')
|
|
345
|
+
client = fresh
|
|
346
|
+
} catch (error) {
|
|
347
|
+
await fresh.close()
|
|
348
|
+
throw error
|
|
349
|
+
} finally {
|
|
350
|
+
starting.delete(fresh)
|
|
351
|
+
}
|
|
352
|
+
})().finally(() => { connecting = undefined })
|
|
353
|
+
}
|
|
354
|
+
await connecting
|
|
291
355
|
}
|
|
356
|
+
if (closed || withdrawnByOps) throw new Error('the FastCtx bridge was stopped')
|
|
292
357
|
return client.callTool(rawName, args)
|
|
293
358
|
}
|
|
294
359
|
|
|
295
|
-
const
|
|
296
|
-
if (disposer || decided && withdrawnByOps) return
|
|
360
|
+
const publishOnce = async () => {
|
|
361
|
+
if (closed || disposer || decided && withdrawnByOps) return
|
|
297
362
|
const registry = tools(ctx)
|
|
298
363
|
if (!registry || typeof registry.register !== 'function') return
|
|
299
364
|
const { file, source } = resolveFastctx(ctx, createRequireFrom)
|
|
@@ -304,6 +369,7 @@ export function mountFastctx(ctx, deps) {
|
|
|
304
369
|
return
|
|
305
370
|
}
|
|
306
371
|
const fresh = new McpStdio({ file, log })
|
|
372
|
+
starting.add(fresh)
|
|
307
373
|
let toolList
|
|
308
374
|
try {
|
|
309
375
|
await fresh.start()
|
|
@@ -314,9 +380,12 @@ export function mountFastctx(ctx, deps) {
|
|
|
314
380
|
decided = true
|
|
315
381
|
withdrawnByOps = true
|
|
316
382
|
return
|
|
383
|
+
} finally {
|
|
384
|
+
starting.delete(fresh)
|
|
317
385
|
}
|
|
318
386
|
await fresh.close()
|
|
319
|
-
|
|
387
|
+
if (closed || withdrawnByOps) return
|
|
388
|
+
// Discovery never overwrites the independently owned lazy call client.
|
|
320
389
|
const wanted = toolList.filter((tool) => FASTCTX_FILE_TOOLS.includes(String(tool?.name)))
|
|
321
390
|
const registered = []
|
|
322
391
|
const registeredNames = []
|
|
@@ -334,29 +403,36 @@ export function mountFastctx(ctx, deps) {
|
|
|
334
403
|
: rawName === 'grep'
|
|
335
404
|
? '\nIgnore rules: this tool honors .gitignore and skips .git — unlike gb_glob (bare .ignore only) and the official glob (ignores nothing).'
|
|
336
405
|
: ''
|
|
406
|
+
// Pattern-shape transparency (v0.39.1, issue #21): models keep
|
|
407
|
+
// JSON-stringifying the glob array (24 of 25 measured calls), which the
|
|
408
|
+
// upstream matches as one literal glob -> silent empty. State the
|
|
409
|
+
// accepted shapes on the tool.
|
|
410
|
+
const patternNote = rawName === 'glob'
|
|
411
|
+
? '\npattern accepts one glob string OR an array of glob strings (e.g. ["**/*.rs", "!tests/**"]). Pass a real array — never a JSON-stringified array.'
|
|
412
|
+
: ''
|
|
337
413
|
try {
|
|
338
414
|
const dispose = registry.register({
|
|
339
415
|
name: gbName(rawName),
|
|
340
|
-
description: `${String(tool.description ?? rawName)}\n${FASTCTX_SANDBOX_NOTE}${ignoreNote}`,
|
|
416
|
+
description: `${String(tool.description ?? rawName)}\n${FASTCTX_SANDBOX_NOTE}${ignoreNote}${patternNote}`,
|
|
341
417
|
parameters: schema,
|
|
342
418
|
output: {
|
|
343
419
|
schema: TOOL_OUTPUT_SCHEMA,
|
|
344
420
|
render: (_args, value) => renderTextContent(value),
|
|
345
421
|
},
|
|
346
|
-
async execute(args) {
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const pattern = args.pattern
|
|
353
|
-
if (typeof pattern !== 'string' || pattern.trim() === '') {
|
|
354
|
-
throw new Error(`invalid arguments: ${gbName(rawName)} pattern must be a non-empty string (got ${pattern === null ? 'null' : typeof pattern})`)
|
|
355
|
-
}
|
|
422
|
+
async execute(args, execution) {
|
|
423
|
+
// Preserve v0.39.1 / #21: glob accepts real string arrays,
|
|
424
|
+
// while stringified arrays and malformed patterns fail loudly.
|
|
425
|
+
if (args !== null && typeof args === 'object' && 'pattern' in args) {
|
|
426
|
+
const invalid = validateFastctxPattern(rawName, args.pattern)
|
|
427
|
+
if (invalid) throw new Error(invalid)
|
|
356
428
|
}
|
|
357
|
-
const translated = translateFastctxArgs(args, msysToWin)
|
|
429
|
+
const translated = translateFastctxArgs(args, msysToWin ? (path) => msysToWin(path, execution) : undefined)
|
|
358
430
|
const result = await callWithReconnect(rawName, translated)
|
|
359
|
-
|
|
431
|
+
const text = renderFastctxResult(rawName, result, echo ? (value) => echo(value, execution) : undefined)
|
|
432
|
+
// The host treats a returned value as success; MCP failures must
|
|
433
|
+
// throw, just as its official MCP client does.
|
|
434
|
+
if (result?.isError === true) throw new Error(text)
|
|
435
|
+
return { content: [{ type: 'text', text }] }
|
|
360
436
|
},
|
|
361
437
|
})
|
|
362
438
|
if (typeof dispose === 'function') {
|
|
@@ -377,7 +453,13 @@ export function mountFastctx(ctx, deps) {
|
|
|
377
453
|
log(`FastCtx bridge active (${source}): ${registeredNames.join(', ')}`)
|
|
378
454
|
}
|
|
379
455
|
|
|
456
|
+
const publish = () => {
|
|
457
|
+
if (!publishing) publishing = publishOnce().finally(() => { publishing = undefined })
|
|
458
|
+
return publishing
|
|
459
|
+
}
|
|
460
|
+
|
|
380
461
|
const reconcile = (agent) => {
|
|
462
|
+
if (closed) return
|
|
381
463
|
if (!enabled()) { unpublish('FastCtx bridge disabled by the fastctxTools switch'); decided = true; withdrawnByOps = true; return }
|
|
382
464
|
if (decided && withdrawnByOps) return
|
|
383
465
|
let names = []
|
|
@@ -401,8 +483,12 @@ export function mountFastctx(ctx, deps) {
|
|
|
401
483
|
log(`FastCtx bridge wiring failed: ${error?.message ?? error}`)
|
|
402
484
|
return
|
|
403
485
|
}
|
|
404
|
-
ctx.effect(() => () => {
|
|
486
|
+
ctx.effect(() => async () => {
|
|
487
|
+
closed = true
|
|
405
488
|
unpublish()
|
|
406
|
-
|
|
489
|
+
const owned = new Set([...starting, client].filter(Boolean))
|
|
490
|
+
await Promise.allSettled([...owned].map((connection) => connection.close()))
|
|
491
|
+
await Promise.allSettled([publishing, connecting].filter(Boolean))
|
|
492
|
+
client = undefined
|
|
407
493
|
}, 'dsh-gitbash-shell: FastCtx bridge')
|
|
408
494
|
}
|