@steerable/agent-shell 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/LICENSE +91 -0
  2. package/contracts/tool-contract.json +326 -0
  3. package/dist/attachments.d.ts +41 -0
  4. package/dist/attachments.js +147 -0
  5. package/dist/brand.d.ts +24 -0
  6. package/dist/brand.js +92 -0
  7. package/dist/host/http-routes.d.ts +21 -0
  8. package/dist/host/http-routes.js +55 -0
  9. package/dist/host/ipc.d.ts +11 -0
  10. package/dist/host/ipc.js +20 -0
  11. package/dist/host/pack-assembly.d.ts +86 -0
  12. package/dist/host/pack-assembly.js +32 -0
  13. package/dist/host/runtime.d.ts +69 -0
  14. package/dist/host/runtime.js +207 -0
  15. package/dist/host/visible-terminal-exec.d.ts +21 -0
  16. package/dist/host/visible-terminal-exec.js +151 -0
  17. package/dist/hosted-web-search.d.ts +13 -0
  18. package/dist/hosted-web-search.js +82 -0
  19. package/dist/image-attachment.d.ts +39 -0
  20. package/dist/image-attachment.js +133 -0
  21. package/dist/insights/flush.d.ts +10 -0
  22. package/dist/insights/flush.js +139 -0
  23. package/dist/insights/record.d.ts +17 -0
  24. package/dist/insights/record.js +44 -0
  25. package/dist/json-store.d.ts +9 -0
  26. package/dist/json-store.js +22 -0
  27. package/dist/llm/index.d.ts +23 -0
  28. package/dist/llm/index.js +105 -0
  29. package/dist/llm/ollama.d.ts +23 -0
  30. package/dist/llm/ollama.js +242 -0
  31. package/dist/llm/openai-compat.d.ts +20 -0
  32. package/dist/llm/openai-compat.js +199 -0
  33. package/dist/llm/sidecar-provider.d.ts +37 -0
  34. package/dist/llm/sidecar-provider.js +163 -0
  35. package/dist/llm/tool-choice.d.ts +35 -0
  36. package/dist/llm/tool-choice.js +85 -0
  37. package/dist/llm/types.d.ts +122 -0
  38. package/dist/llm/types.js +1 -0
  39. package/dist/local-backend/agent-capability.d.ts +101 -0
  40. package/dist/local-backend/agent-capability.js +174 -0
  41. package/dist/local-backend/ai-title.d.ts +43 -0
  42. package/dist/local-backend/ai-title.js +173 -0
  43. package/dist/local-backend/auto-continue-helper.d.ts +80 -0
  44. package/dist/local-backend/auto-continue-helper.js +83 -0
  45. package/dist/local-backend/branch-helper.d.ts +24 -0
  46. package/dist/local-backend/branch-helper.js +27 -0
  47. package/dist/local-backend/context-compactor.d.ts +81 -0
  48. package/dist/local-backend/context-compactor.js +213 -0
  49. package/dist/local-backend/coreloop-stream.d.ts +245 -0
  50. package/dist/local-backend/coreloop-stream.js +277 -0
  51. package/dist/local-backend/deferred-detector.d.ts +15 -0
  52. package/dist/local-backend/deferred-detector.js +124 -0
  53. package/dist/local-backend/history-helper.d.ts +30 -0
  54. package/dist/local-backend/history-helper.js +34 -0
  55. package/dist/local-backend/interrupted-helper.d.ts +29 -0
  56. package/dist/local-backend/interrupted-helper.js +25 -0
  57. package/dist/local-backend/live-stream.d.ts +36 -0
  58. package/dist/local-backend/live-stream.js +23 -0
  59. package/dist/local-backend/llm-diagnose.d.ts +37 -0
  60. package/dist/local-backend/llm-diagnose.js +284 -0
  61. package/dist/local-backend/message-triggers.d.ts +27 -0
  62. package/dist/local-backend/message-triggers.js +67 -0
  63. package/dist/local-backend/pack-backend-routes.d.ts +31 -0
  64. package/dist/local-backend/pack-backend-routes.js +62 -0
  65. package/dist/local-backend/pack-turn-hooks.d.ts +37 -0
  66. package/dist/local-backend/pack-turn-hooks.js +67 -0
  67. package/dist/local-backend/prompt-builder.d.ts +101 -0
  68. package/dist/local-backend/prompt-builder.js +246 -0
  69. package/dist/local-backend/regenerate-helper.d.ts +62 -0
  70. package/dist/local-backend/regenerate-helper.js +75 -0
  71. package/dist/local-backend/router.d.ts +175 -0
  72. package/dist/local-backend/router.js +3139 -0
  73. package/dist/local-backend/skill-install.d.ts +19 -0
  74. package/dist/local-backend/skill-install.js +71 -0
  75. package/dist/local-backend/skill-loader.d.ts +92 -0
  76. package/dist/local-backend/skill-loader.js +146 -0
  77. package/dist/local-backend/skills/00-identity/SKILL.md +32 -0
  78. package/dist/local-backend/skills/10-goal/SKILL.md +59 -0
  79. package/dist/local-backend/skills/11-loop/SKILL.md +71 -0
  80. package/dist/local-backend/skills/12-create-skill/SKILL.md +88 -0
  81. package/dist/local-backend/skills/70-plan-mode/SKILL.md +58 -0
  82. package/dist/local-backend/skills/80-tool-usage/SKILL.md +70 -0
  83. package/dist/local-backend/skills/81-anti-deferred/SKILL.md +53 -0
  84. package/dist/local-backend/skills/82-data-grounding/SKILL.md +56 -0
  85. package/dist/local-backend/skills/85-local-exec/SKILL.md +86 -0
  86. package/dist/local-backend/skills/86-proactive-coding/SKILL.md +51 -0
  87. package/dist/local-backend/subagent-profiles.d.ts +30 -0
  88. package/dist/local-backend/subagent-profiles.js +74 -0
  89. package/dist/local-backend/task-process.d.ts +12 -0
  90. package/dist/local-backend/task-process.js +176 -0
  91. package/dist/local-backend/task-service.d.ts +135 -0
  92. package/dist/local-backend/task-service.js +565 -0
  93. package/dist/local-backend/turn-duration.d.ts +2 -0
  94. package/dist/local-backend/turn-duration.js +9 -0
  95. package/dist/local-backend/turn-timeline.d.ts +16 -0
  96. package/dist/local-backend/turn-timeline.js +42 -0
  97. package/dist/local-backend/worktree-service.d.ts +84 -0
  98. package/dist/local-backend/worktree-service.js +243 -0
  99. package/dist/local-edit.d.ts +48 -0
  100. package/dist/local-edit.js +44 -0
  101. package/dist/local-executor.d.ts +255 -0
  102. package/dist/local-executor.js +881 -0
  103. package/dist/local-script-registry.d.ts +28 -0
  104. package/dist/local-script-registry.js +63 -0
  105. package/dist/log.d.ts +13 -0
  106. package/dist/log.js +12 -0
  107. package/dist/main.d.ts +1 -0
  108. package/dist/main.js +855 -0
  109. package/dist/mcp-executor.d.ts +45 -0
  110. package/dist/mcp-executor.js +241 -0
  111. package/dist/mcp-server-registry.d.ts +104 -0
  112. package/dist/mcp-server-registry.js +234 -0
  113. package/dist/preload-default.d.ts +1 -0
  114. package/dist/preload-default.js +9 -0
  115. package/dist/preload.cjs +395 -0
  116. package/dist/preload.d.ts +20 -0
  117. package/dist/preload.js +411 -0
  118. package/dist/product-config.d.ts +43 -0
  119. package/dist/product-config.js +26 -0
  120. package/dist/project-registry.d.ts +55 -0
  121. package/dist/project-registry.js +106 -0
  122. package/dist/project-rules.d.ts +15 -0
  123. package/dist/project-rules.js +102 -0
  124. package/dist/runtime.d.ts +62 -0
  125. package/dist/runtime.js +217 -0
  126. package/dist/scenario/pack.d.ts +8 -0
  127. package/dist/scenario/pack.js +1 -0
  128. package/dist/scenario/registry.d.ts +24 -0
  129. package/dist/scenario/registry.js +31 -0
  130. package/dist/server/http-server.d.ts +39 -0
  131. package/dist/server/http-server.js +361 -0
  132. package/dist/server/index.d.ts +1 -0
  133. package/dist/server/index.js +107 -0
  134. package/dist/server/sse-bus.d.ts +14 -0
  135. package/dist/server/sse-bus.js +31 -0
  136. package/dist/shell-adapt.d.ts +21 -0
  137. package/dist/shell-adapt.js +104 -0
  138. package/dist/sidecar/boot.d.ts +36 -0
  139. package/dist/sidecar/boot.js +343 -0
  140. package/dist/sidecar/egress-hint.d.ts +15 -0
  141. package/dist/sidecar/egress-hint.js +46 -0
  142. package/dist/sidecar/egress-proxy.d.ts +183 -0
  143. package/dist/sidecar/egress-proxy.js +419 -0
  144. package/dist/sidecar/errors.d.ts +22 -0
  145. package/dist/sidecar/errors.js +38 -0
  146. package/dist/sidecar/exec-sandbox.d.ts +48 -0
  147. package/dist/sidecar/exec-sandbox.js +94 -0
  148. package/dist/sidecar/handle.d.ts +32 -0
  149. package/dist/sidecar/handle.js +53 -0
  150. package/dist/sidecar/index.d.ts +14 -0
  151. package/dist/sidecar/index.js +13 -0
  152. package/dist/sidecar/proxy-detect.d.ts +50 -0
  153. package/dist/sidecar/proxy-detect.js +182 -0
  154. package/dist/sidecar/reverse-approval.d.ts +55 -0
  155. package/dist/sidecar/reverse-approval.js +86 -0
  156. package/dist/sidecar/reverse-ask-user.d.ts +34 -0
  157. package/dist/sidecar/reverse-ask-user.js +59 -0
  158. package/dist/sidecar/reverse-spawn.d.ts +19 -0
  159. package/dist/sidecar/reverse-spawn.js +161 -0
  160. package/dist/sidecar/reverse-tools.d.ts +29 -0
  161. package/dist/sidecar/reverse-tools.js +106 -0
  162. package/dist/sidecar/safety-patterns.d.ts +41 -0
  163. package/dist/sidecar/safety-patterns.js +157 -0
  164. package/dist/sidecar/storage-path.d.ts +14 -0
  165. package/dist/sidecar/storage-path.js +35 -0
  166. package/dist/sidecar/supervisor.d.ts +218 -0
  167. package/dist/sidecar/supervisor.js +932 -0
  168. package/dist/sidecar/types.d.ts +601 -0
  169. package/dist/sidecar/types.js +1 -0
  170. package/dist/single-instance.d.ts +11 -0
  171. package/dist/single-instance.js +21 -0
  172. package/dist/storage/empty-chats.d.ts +9 -0
  173. package/dist/storage/empty-chats.js +16 -0
  174. package/dist/storage/index.d.ts +373 -0
  175. package/dist/storage/index.js +1158 -0
  176. package/dist/storage/insights-redact.d.ts +2 -0
  177. package/dist/storage/insights-redact.js +30 -0
  178. package/dist/storage/insights-settings.d.ts +53 -0
  179. package/dist/storage/insights-settings.js +92 -0
  180. package/dist/storage/llm-settings.d.ts +120 -0
  181. package/dist/storage/llm-settings.js +233 -0
  182. package/dist/storage/local-store-singleton.d.ts +28 -0
  183. package/dist/storage/local-store-singleton.js +38 -0
  184. package/dist/storage/message-order.d.ts +25 -0
  185. package/dist/storage/message-order.js +27 -0
  186. package/dist/storage/pack-migrations.d.ts +22 -0
  187. package/dist/storage/pack-migrations.js +24 -0
  188. package/dist/storage/pack-seeds.d.ts +36 -0
  189. package/dist/storage/pack-seeds.js +42 -0
  190. package/dist/storage/telemetry-settings.d.ts +38 -0
  191. package/dist/storage/telemetry-settings.js +59 -0
  192. package/dist/storage/usage-summary.d.ts +55 -0
  193. package/dist/storage/usage-summary.js +38 -0
  194. package/dist/storage/web-search-settings.d.ts +38 -0
  195. package/dist/storage/web-search-settings.js +74 -0
  196. package/dist/storage/write-lease.d.ts +26 -0
  197. package/dist/storage/write-lease.js +74 -0
  198. package/dist/terminal-manager.d.ts +83 -0
  199. package/dist/terminal-manager.js +506 -0
  200. package/dist/tool-router.d.ts +228 -0
  201. package/dist/tool-router.js +930 -0
  202. package/dist/tool-search-rank.d.ts +42 -0
  203. package/dist/tool-search-rank.js +96 -0
  204. package/package.json +67 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 4.6b Worktree(git worktree 隔离)。
3
+ *
4
+ * 在绑定项目的仓库里管理隔离工作区:`<项目根>/.steerable/worktrees/<name>`
5
+ * + 分支 `steerable/<name>`。任务(task_run worktree:true)在隔离区里跑,
6
+ * 主检出不被触碰;完成后由用户选择「合并到主仓」(worktree 内提交 +
7
+ * 主仓 merge)或「丢弃」(remove + 删分支)。
8
+ *
9
+ * 位置选择的取舍:worktree 放在项目根**之内**(而不是 sibling 目录),
10
+ * 这样既有的项目围栏(writableRoots = 项目根、applyProjectCwdSandbox)
11
+ * 天然覆盖它,无需为主检出之外的目录开洞;`.steerable/` 写进
12
+ * `.git/info/exclude`(仓库本地、不动用户的 .gitignore),主检出的
13
+ * `git status` 保持干净。
14
+ *
15
+ * 本模块只依赖注入的 git 执行器与项目根解析器——不 import electron,
16
+ * 保持纯 Node 可测(与 project-registry.ts 同一模式)。
17
+ */
18
+ export interface WorktreeInfo {
19
+ /** worktree 目录名(也是分支名的 slug 部分)。 */
20
+ name: string;
21
+ path: string;
22
+ branch: string;
23
+ }
24
+ export interface WorktreeProject {
25
+ name: string;
26
+ folderPath: string;
27
+ }
28
+ /** git 执行缝:默认 execFile;测试可注入假实现。 */
29
+ export type GitRunner = (args: string[], cwd: string) => Promise<{
30
+ stdout: string;
31
+ stderr: string;
32
+ }>;
33
+ /** worktree 名 → 目录/分支可用的 slug(小写字母数字中划线,防爆目录注入)。 */
34
+ export declare function slugifyWorktreeName(raw: string): string;
35
+ export declare class WorktreeService {
36
+ private readonly deps;
37
+ constructor(deps: {
38
+ resolveProject: (chatId: string) => WorktreeProject | null;
39
+ runGit?: GitRunner;
40
+ });
41
+ private git;
42
+ /** 项目根下的托管 worktree 根目录。 */
43
+ private managedRoot;
44
+ private requireProject;
45
+ /** 校验目标确是 git 仓库(worktree 子命令只在仓库里才有意义)。 */
46
+ private requireGitRepo;
47
+ /**
48
+ * 把 `.steerable/` 写进 `.git/info/exclude`(仓库本地排除,不动用户的
49
+ * .gitignore),让托管 worktree 目录不出现在主检出的 git status 里。
50
+ * 幂等。
51
+ */
52
+ private ensureGitInfoExclude;
53
+ /**
54
+ * 创建隔离 worktree:分支 `steerable/<name>` 从当前 HEAD 切出。
55
+ * 同名已存在时直接复用(幂等——模型重试不该炸掉已建好的工作区)。
56
+ */
57
+ createWorktree(chatId: string, rawName?: string): Promise<WorktreeInfo>;
58
+ /** 列出本项目的托管 worktree(只认 .steerable/worktrees 下的)。 */
59
+ listWorktrees(chatId: string): Promise<WorktreeInfo[]>;
60
+ /**
61
+ * 移除 worktree。`deleteBranch` 同时删掉 `steerable/<name>` 分支
62
+ * (丢弃语义);保留分支用于合并后的清理(合并后分支引用留着无害,
63
+ * 但默认删掉保持分支列表干净——已合并的分支删除是安全的)。
64
+ */
65
+ removeWorktree(chatId: string, name: string, options?: {
66
+ deleteBranch?: boolean;
67
+ }): Promise<{
68
+ removed: string;
69
+ branchDeleted: string | null;
70
+ }>;
71
+ /**
72
+ * 把 worktree 里的改动合并回主仓当前分支:
73
+ * 1. worktree 内 `git add -A` + 提交(任务 agent 只改文件,不提交);
74
+ * 2. 主仓 `git merge --no-ff <branch>`;
75
+ * 3. 移除 worktree 并删除已合并分支。
76
+ * 冲突时 `merge --abort` 把主检出还原到干净状态(主检出是用户的工作区,
77
+ * 不能留半合并的冲突现场),worktree 与分支保留,错误如实上抛——用户
78
+ * 可稍后重试或手工处理。
79
+ */
80
+ mergeWorktree(chatId: string, name: string, commitMessage: string): Promise<{
81
+ merged: string;
82
+ commit: string | null;
83
+ }>;
84
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * 4.6b Worktree(git worktree 隔离)。
3
+ *
4
+ * 在绑定项目的仓库里管理隔离工作区:`<项目根>/.steerable/worktrees/<name>`
5
+ * + 分支 `steerable/<name>`。任务(task_run worktree:true)在隔离区里跑,
6
+ * 主检出不被触碰;完成后由用户选择「合并到主仓」(worktree 内提交 +
7
+ * 主仓 merge)或「丢弃」(remove + 删分支)。
8
+ *
9
+ * 位置选择的取舍:worktree 放在项目根**之内**(而不是 sibling 目录),
10
+ * 这样既有的项目围栏(writableRoots = 项目根、applyProjectCwdSandbox)
11
+ * 天然覆盖它,无需为主检出之外的目录开洞;`.steerable/` 写进
12
+ * `.git/info/exclude`(仓库本地、不动用户的 .gitignore),主检出的
13
+ * `git status` 保持干净。
14
+ *
15
+ * 本模块只依赖注入的 git 执行器与项目根解析器——不 import electron,
16
+ * 保持纯 Node 可测(与 project-registry.ts 同一模式)。
17
+ */
18
+ import { execFile } from 'node:child_process';
19
+ import { existsSync, realpathSync } from 'node:fs';
20
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
21
+ import path from 'node:path';
22
+ const defaultGitRunner = (args, cwd) => new Promise((resolve, reject) => {
23
+ execFile('git', args, { cwd, maxBuffer: 8 * 1024 * 1024 }, (error, stdout, stderr) => {
24
+ if (error) {
25
+ reject(new Error(`git ${args.join(' ')} 失败: ${stderr.trim() || error.message}`));
26
+ return;
27
+ }
28
+ resolve({ stdout, stderr });
29
+ });
30
+ });
31
+ /** worktree 名 → 目录/分支可用的 slug(小写字母数字中划线,防爆目录注入)。 */
32
+ export function slugifyWorktreeName(raw) {
33
+ const slug = raw
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9一-鿿]+/g, '-')
36
+ .replace(/^-+|-+$/g, '')
37
+ .slice(0, 40);
38
+ return slug || 'wt';
39
+ }
40
+ export class WorktreeService {
41
+ deps;
42
+ constructor(deps) {
43
+ this.deps = deps;
44
+ }
45
+ git(cwd, args) {
46
+ return (this.deps.runGit ?? defaultGitRunner)(args, cwd);
47
+ }
48
+ /** 项目根下的托管 worktree 根目录。 */
49
+ managedRoot(projectRoot) {
50
+ return path.join(projectRoot, '.steerable', 'worktrees');
51
+ }
52
+ requireProject(chatId) {
53
+ const project = this.deps.resolveProject(chatId);
54
+ if (!project) {
55
+ throw new Error('worktree 需要对话绑定项目(git 仓库)——请先把本对话关联到一个项目。');
56
+ }
57
+ return project;
58
+ }
59
+ /** 校验目标确是 git 仓库(worktree 子命令只在仓库里才有意义)。 */
60
+ async requireGitRepo(projectRoot) {
61
+ try {
62
+ await this.git(projectRoot, ['rev-parse', '--git-dir']);
63
+ }
64
+ catch {
65
+ throw new Error(`项目目录不是 git 仓库:${projectRoot}`);
66
+ }
67
+ }
68
+ /**
69
+ * 把 `.steerable/` 写进 `.git/info/exclude`(仓库本地排除,不动用户的
70
+ * .gitignore),让托管 worktree 目录不出现在主检出的 git status 里。
71
+ * 幂等。
72
+ */
73
+ async ensureGitInfoExclude(projectRoot) {
74
+ const { stdout } = await this.git(projectRoot, [
75
+ 'rev-parse',
76
+ '--git-path',
77
+ 'info/exclude',
78
+ ]);
79
+ const excludePath = path.join(projectRoot, stdout.trim());
80
+ const existing = existsSync(excludePath)
81
+ ? await readFile(excludePath, 'utf8')
82
+ : '';
83
+ if (existing.split('\n').some((line) => line.trim() === '.steerable/')) {
84
+ return;
85
+ }
86
+ await mkdir(path.dirname(excludePath), { recursive: true });
87
+ await appendFile(excludePath, '.steerable/\n', 'utf8');
88
+ }
89
+ /**
90
+ * 创建隔离 worktree:分支 `steerable/<name>` 从当前 HEAD 切出。
91
+ * 同名已存在时直接复用(幂等——模型重试不该炸掉已建好的工作区)。
92
+ */
93
+ async createWorktree(chatId, rawName) {
94
+ const project = this.requireProject(chatId);
95
+ await this.requireGitRepo(project.folderPath);
96
+ const name = slugifyWorktreeName(rawName ?? '');
97
+ const suffix = name === 'wt' ? `-${Date.now().toString(36)}` : '';
98
+ const finalName = `${name}${suffix}`;
99
+ const branch = `steerable/${finalName}`;
100
+ const worktreePath = path.join(this.managedRoot(project.folderPath), finalName);
101
+ const existing = await this.listWorktrees(chatId);
102
+ const found = existing.find((w) => w.name === finalName);
103
+ if (found)
104
+ return found;
105
+ await this.ensureGitInfoExclude(project.folderPath);
106
+ await mkdir(this.managedRoot(project.folderPath), { recursive: true });
107
+ await this.git(project.folderPath, [
108
+ 'worktree',
109
+ 'add',
110
+ worktreePath,
111
+ '-b',
112
+ branch,
113
+ ]);
114
+ // 返回 realpath(与 listWorktrees 一致)——任务表存它、沙箱围栏用它,
115
+ // 字符串前缀比较不能落在未解析的符号链接形式上(macOS /var 软链)。
116
+ return { name: finalName, path: realpathSync(worktreePath), branch };
117
+ }
118
+ /** 列出本项目的托管 worktree(只认 .steerable/worktrees 下的)。 */
119
+ async listWorktrees(chatId) {
120
+ const project = this.requireProject(chatId);
121
+ await this.requireGitRepo(project.folderPath);
122
+ const { stdout } = await this.git(project.folderPath, [
123
+ 'worktree',
124
+ 'list',
125
+ '--porcelain',
126
+ ]);
127
+ // git 报的是 realpath(macOS 上 /var → /private/var),而项目根可能
128
+ // 是未解析的符号链接路径——项目根(必然存在)过 realpath 再拼托管后缀,
129
+ // 否则前缀匹配在 macOS 上永远落空(managedRoot 本身可能还没建,
130
+ // 不能直接 realpath)。
131
+ const root = path.join(realpathSync(project.folderPath), '.steerable', 'worktrees');
132
+ const out = [];
133
+ let currentPath = null;
134
+ let currentBranch = '';
135
+ const flush = () => {
136
+ if (!currentPath)
137
+ return;
138
+ const normalized = realpathSync(currentPath);
139
+ if (normalized.startsWith(root + path.sep) &&
140
+ currentBranch.startsWith('refs/heads/steerable/')) {
141
+ out.push({
142
+ name: path.basename(normalized),
143
+ path: normalized,
144
+ branch: currentBranch.slice('refs/heads/'.length),
145
+ });
146
+ }
147
+ currentPath = null;
148
+ currentBranch = '';
149
+ };
150
+ for (const line of stdout.split('\n')) {
151
+ if (line.startsWith('worktree ')) {
152
+ flush();
153
+ currentPath = line.slice('worktree '.length);
154
+ }
155
+ else if (line.startsWith('branch ')) {
156
+ currentBranch = line.slice('branch '.length);
157
+ }
158
+ else if (line === '') {
159
+ flush();
160
+ }
161
+ }
162
+ flush();
163
+ return out;
164
+ }
165
+ /**
166
+ * 移除 worktree。`deleteBranch` 同时删掉 `steerable/<name>` 分支
167
+ * (丢弃语义);保留分支用于合并后的清理(合并后分支引用留着无害,
168
+ * 但默认删掉保持分支列表干净——已合并的分支删除是安全的)。
169
+ */
170
+ async removeWorktree(chatId, name, options = {}) {
171
+ const project = this.requireProject(chatId);
172
+ const worktrees = await this.listWorktrees(chatId);
173
+ const target = worktrees.find((w) => w.name === name || w.path === name);
174
+ if (!target) {
175
+ throw new Error(`未找到托管 worktree:${name}`);
176
+ }
177
+ // --force:worktree 里可能躺着任务留下的未提交改动,移除语义就是不要了。
178
+ await this.git(project.folderPath, [
179
+ 'worktree',
180
+ 'remove',
181
+ '--force',
182
+ target.path,
183
+ ]);
184
+ let branchDeleted = null;
185
+ if (options.deleteBranch ?? true) {
186
+ try {
187
+ await this.git(project.folderPath, ['branch', '-D', target.branch]);
188
+ branchDeleted = target.branch;
189
+ }
190
+ catch {
191
+ // 分支可能已被删/从未创建成功——移除 worktree 本身已成功,不因此翻车。
192
+ }
193
+ }
194
+ return { removed: target.path, branchDeleted };
195
+ }
196
+ /**
197
+ * 把 worktree 里的改动合并回主仓当前分支:
198
+ * 1. worktree 内 `git add -A` + 提交(任务 agent 只改文件,不提交);
199
+ * 2. 主仓 `git merge --no-ff <branch>`;
200
+ * 3. 移除 worktree 并删除已合并分支。
201
+ * 冲突时 `merge --abort` 把主检出还原到干净状态(主检出是用户的工作区,
202
+ * 不能留半合并的冲突现场),worktree 与分支保留,错误如实上抛——用户
203
+ * 可稍后重试或手工处理。
204
+ */
205
+ async mergeWorktree(chatId, name, commitMessage) {
206
+ const project = this.requireProject(chatId);
207
+ const worktrees = await this.listWorktrees(chatId);
208
+ const target = worktrees.find((w) => w.name === name || w.path === name);
209
+ if (!target) {
210
+ throw new Error(`未找到托管 worktree:${name}`);
211
+ }
212
+ let commit = null;
213
+ const { stdout: status } = await this.git(target.path, [
214
+ 'status',
215
+ '--porcelain',
216
+ ]);
217
+ if (status.trim()) {
218
+ await this.git(target.path, ['add', '-A']);
219
+ await this.git(target.path, ['commit', '-m', commitMessage]);
220
+ const { stdout: sha } = await this.git(target.path, [
221
+ 'rev-parse',
222
+ 'HEAD',
223
+ ]);
224
+ commit = sha.trim();
225
+ }
226
+ try {
227
+ await this.git(project.folderPath, [
228
+ 'merge',
229
+ '--no-ff',
230
+ '-m',
231
+ `Merge ${target.branch}`,
232
+ target.branch,
233
+ ]);
234
+ }
235
+ catch (err) {
236
+ // 主检出是用户的工作区——冲突时还原干净,绝不留 MERGE_HEAD/冲突标记。
237
+ await this.git(project.folderPath, ['merge', '--abort']).catch(() => { });
238
+ throw err;
239
+ }
240
+ await this.removeWorktree(chatId, target.name, { deleteBranch: true });
241
+ return { merged: target.branch, commit };
242
+ }
243
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 结构化文件编辑(W6-1)的桌面侧薄客户端。
3
+ *
4
+ * 定位/替换/统一 diff 算法已下沉到框架,单一真源是 Python 的
5
+ * `steerable_sidecar/file_edit.py`(headless 与 ACP 工作区工具共用同一实现),
6
+ * 通过 sidecar 的 `workspace.apply_edits` RPC 调用。本模块只做线协议转换与
7
+ * 错误映射;文件读写、版本令牌、原子落盘、同文件串行队列仍在 local-executor。
8
+ */
9
+ export interface EditOp {
10
+ /** 定位锚点(要被替换掉的原文)。 */
11
+ oldText: string;
12
+ /** 替换后的新文。 */
13
+ newText: string;
14
+ }
15
+ export type MatchLevel = 'exact' | 'trim' | 'unicode';
16
+ /** 一次命中在原文中的位置与跨度(供工具卡/测试断言用)。 */
17
+ export interface EditMatch {
18
+ level: MatchLevel;
19
+ startLine: number;
20
+ oldLineCount: number;
21
+ }
22
+ export interface ApplyEditsResult {
23
+ /** 应用全部编辑后的新文件内容。 */
24
+ content: string;
25
+ /** 统一 diff(unified diff,3 行上下文),供工具卡渲染。 */
26
+ diff: string;
27
+ matches: EditMatch[];
28
+ }
29
+ export declare class EditError extends Error {
30
+ readonly code: 'empty_old' | 'not_found' | 'ambiguous' | 'overlap' | 'no_edits';
31
+ constructor(message: string, code: 'empty_old' | 'not_found' | 'ambiguous' | 'overlap' | 'no_edits');
32
+ }
33
+ /** 把 sidecar 的 `workspace.apply_edits` 响应映射为桌面结果形状。 */
34
+ export declare function buildApplyEditsResult(result: {
35
+ content: string;
36
+ diff: string;
37
+ matches: Array<{
38
+ level: MatchLevel;
39
+ startLine: number;
40
+ oldLineCount: number;
41
+ }>;
42
+ }): ApplyEditsResult;
43
+ /**
44
+ * 在 sidecar(Python 真源)里对 `content` 应用 `edits`。只在回合内可达——
45
+ * 编辑工具经 reverse `tool.invoke` 触发,此刻 sidecar 必在运行;sidecar 不可用
46
+ * 时抛出 EditError 之外的错误,由 local-executor 的兜底分支转成失败结果。
47
+ */
48
+ export declare function applyEdits(content: string, edits: EditOp[], filePath?: string): Promise<ApplyEditsResult>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 结构化文件编辑(W6-1)的桌面侧薄客户端。
3
+ *
4
+ * 定位/替换/统一 diff 算法已下沉到框架,单一真源是 Python 的
5
+ * `steerable_sidecar/file_edit.py`(headless 与 ACP 工作区工具共用同一实现),
6
+ * 通过 sidecar 的 `workspace.apply_edits` RPC 调用。本模块只做线协议转换与
7
+ * 错误映射;文件读写、版本令牌、原子落盘、同文件串行队列仍在 local-executor。
8
+ */
9
+ import { getSidecarSupervisor } from './sidecar/handle.js';
10
+ import { SidecarMethodError } from './sidecar/errors.js';
11
+ export class EditError extends Error {
12
+ code;
13
+ constructor(message, code) {
14
+ super(message);
15
+ this.code = code;
16
+ this.name = 'EditError';
17
+ }
18
+ }
19
+ /** 把 sidecar 的 `workspace.apply_edits` 响应映射为桌面结果形状。 */
20
+ export function buildApplyEditsResult(result) {
21
+ return { content: result.content, diff: result.diff, matches: result.matches };
22
+ }
23
+ /**
24
+ * 在 sidecar(Python 真源)里对 `content` 应用 `edits`。只在回合内可达——
25
+ * 编辑工具经 reverse `tool.invoke` 触发,此刻 sidecar 必在运行;sidecar 不可用
26
+ * 时抛出 EditError 之外的错误,由 local-executor 的兜底分支转成失败结果。
27
+ */
28
+ export async function applyEdits(content, edits, filePath = 'file') {
29
+ const supervisor = getSidecarSupervisor();
30
+ if (!supervisor) {
31
+ throw new Error('sidecar 不可用——结构化编辑算法在框架侧,需 sidecar 运行。');
32
+ }
33
+ try {
34
+ const result = await supervisor.applyEdits({ content, edits, filePath });
35
+ return buildApplyEditsResult(result);
36
+ }
37
+ catch (error) {
38
+ if (error instanceof SidecarMethodError && error.kind === 'edit_failed') {
39
+ const code = error.data?.code;
40
+ throw new EditError(error.message, code ?? 'not_found');
41
+ }
42
+ throw error;
43
+ }
44
+ }
@@ -0,0 +1,255 @@
1
+ import { type ApplyEditsResult, type EditOp } from './local-edit.js';
2
+ export declare function getWineCommand(): string | null;
3
+ export declare function rewriteExeCommandIfNeeded(command: string): string;
4
+ export type ShellType = 'zsh' | 'bash' | 'wsl' | 'powershell' | 'cmd';
5
+ export interface LocalExecRequest {
6
+ command: string;
7
+ cwd?: string;
8
+ timeout?: number;
9
+ env?: Record<string, string>;
10
+ shell?: ShellType;
11
+ }
12
+ export interface LocalExecResult {
13
+ success: boolean;
14
+ stdout?: string;
15
+ stderr?: string;
16
+ exitCode?: number;
17
+ error?: string;
18
+ truncated?: boolean;
19
+ shell?: ShellType;
20
+ platform?: string;
21
+ /** 命令因超时被终止(不代表命令本身出错——长任务 / GUI 程序常见)。 */
22
+ timedOut?: boolean;
23
+ /**
24
+ * GUI/长交互命令超时后进程未被终止,仍在后台运行。此时 success=true:
25
+ * "程序已启动并在运行" 对启动类命令就是成功,不能让 LLM 当失败重试。
26
+ */
27
+ stillRunning?: boolean;
28
+ }
29
+ export interface LocalFileReadRequest {
30
+ path: string;
31
+ encoding?: BufferEncoding;
32
+ maxSize?: number;
33
+ }
34
+ export interface LocalFileReadResult {
35
+ success: boolean;
36
+ content?: string;
37
+ /** 内容版本令牌(SHA-256)。读后写场景把它回传给 edit/write 做冲突检测。 */
38
+ version?: string;
39
+ error?: string;
40
+ }
41
+ export interface LocalFileWriteRequest {
42
+ path: string;
43
+ content: string;
44
+ encoding?: BufferEncoding;
45
+ createDirs?: boolean;
46
+ /** 可选:期望的当前版本令牌(read 返回的 version)。不匹配则拒绝写入。 */
47
+ expectedVersion?: string;
48
+ }
49
+ export interface LocalFileWriteResult {
50
+ success: boolean;
51
+ /** 写入后的新版本令牌。 */
52
+ version?: string;
53
+ error?: string;
54
+ }
55
+ /** local_edit_file 的单条编辑。 */
56
+ export interface LocalFileEditOp extends EditOp {
57
+ }
58
+ export interface LocalFileEditRequest {
59
+ path: string;
60
+ edits: LocalFileEditOp[];
61
+ encoding?: BufferEncoding;
62
+ /** 可选:期望的当前版本令牌(read 返回的 version)。不匹配则拒绝(冲突)。 */
63
+ expectedVersion?: string;
64
+ }
65
+ export interface LocalFileEditResult {
66
+ success: boolean;
67
+ /** 编辑后的新版本令牌。 */
68
+ version?: string;
69
+ /** 统一 diff 预览(供工具卡渲染)。 */
70
+ diff?: string;
71
+ /** 每条编辑的命中信息(定位级别 / 起始行)。 */
72
+ matches?: Array<{
73
+ level: string;
74
+ startLine: number;
75
+ oldLineCount: number;
76
+ }>;
77
+ /** 命中的编辑条数。 */
78
+ applied?: number;
79
+ error?: string;
80
+ }
81
+ export interface LocalOpenRequest {
82
+ target: string;
83
+ }
84
+ export interface LocalOpenResult {
85
+ success: boolean;
86
+ error?: string;
87
+ }
88
+ export type RunCodeLanguage = 'python' | 'node';
89
+ export interface LocalRunCodeRequest {
90
+ language: string;
91
+ code: string;
92
+ cwd?: string;
93
+ timeout?: number;
94
+ /** 运行前要 pip 安装的依赖(如 ["pandas", "openpyxl"])。 */
95
+ pipPackages?: string[];
96
+ /** 运行前要 npm 安装到 scratch 目录的依赖(如 ["xlsx"])。 */
97
+ npmPackages?: string[];
98
+ }
99
+ export interface LocalRunCodeResult extends LocalExecResult {
100
+ language?: RunCodeLanguage;
101
+ /** 实际使用的解释器命令/路径(venv 兜底时是 venv 里的 python)。 */
102
+ interpreter?: string;
103
+ /** 代码片段落盘位置(保留不删,便于排查/复用)。 */
104
+ scriptPath?: string;
105
+ /** 本次调用实际安装成功的依赖。 */
106
+ installedPackages?: string[];
107
+ }
108
+ /** 内容版本令牌:UTF-8 内容的 SHA-256。用于 read-before-write 冲突检测。 */
109
+ /**
110
+ * read-before-write 冲突令牌:内容的 SHA-256(UTF-8)十六进制。与框架
111
+ * `steerable_sidecar.file_edit.content_version` 是同一算法(WS4 契约
112
+ * `versionAlgorithm: sha256-utf8-hex`),两侧由共享测试向量防漂移。
113
+ * 导出供 `tests/tool-contract.test.ts` 断言算法一致性。
114
+ */
115
+ export declare function hashContent(content: string): string;
116
+ /**
117
+ * GUI 命令(见 isGuiLaunchCommand)没显式给 timeout 时的等待时长:只等一小段
118
+ * 时间捕获"立即启动失败"类错误;到点后按"已启动、仍在运行"成功返回,不杀进程。
119
+ */
120
+ export declare const GUI_LAUNCH_WAIT_MS = 15000;
121
+ export declare function setDefaultExecTimeoutMs(ms: number | null | undefined): void;
122
+ export declare function getDefaultExecTimeoutMs(): number;
123
+ /** 用户配置的默认超时;未配置返回 null(调用方可用各自的内置默认)。 */
124
+ export declare function getConfiguredExecTimeoutMs(): number | null;
125
+ /**
126
+ * 判断命令是否在启动 GUI/交互式程序。场景应用启动图形图件的命令行约定
127
+ * 带有 "gui" 字样(如 `xxx.exe gui replay ...`、`--gui`、`start_gui.bat`),
128
+ * 以非字母数字为边界匹配独立的 "gui" token,避免误伤 guide/guid 之类的词。
129
+ *
130
+ * 这类命令的语义是"把程序拉起来给用户操作",等不到退出码是常态——超时后
131
+ * 不应杀进程、更不应判失败重跑(那会把 Qt 图件重复启动多次)。
132
+ */
133
+ export declare function isGuiLaunchCommand(command: string): boolean;
134
+ /**
135
+ * 项目模式的路径围栏:判断 resolvedPath 是否落在 root 之内(含 root 本身)。
136
+ * 两个入参都应先经过 resolve(`~` 展开 + path.resolve)——本函数只做纯
137
+ * 字符串/相对路径判断,不碰 fs,方便 ToolRouter 与 LocalExecutor 共用。
138
+ */
139
+ export declare function isPathWithinRoot(resolvedPath: string, resolvedRoot: string): boolean;
140
+ /** 越界错误消息(给模型看,引导它把操作收回项目目录内)。 */
141
+ export declare function buildProjectRootViolation(resolvedPath: string, resolvedRoot: string): string;
142
+ export interface CommandSafetyConfigPayload {
143
+ disabledPatternIds: string[];
144
+ customPatterns: Array<{
145
+ id: string;
146
+ label: string;
147
+ pattern: string;
148
+ category: string;
149
+ enabled: boolean;
150
+ }>;
151
+ }
152
+ /** 结构化编辑算法(默认走 sidecar 的 Python 真源;测试可注入 stub)。 */
153
+ type ApplyEditsFn = (content: string, edits: EditOp[], filePath?: string) => Promise<ApplyEditsResult>;
154
+ export declare class LocalExecutor {
155
+ private readonly maxOutputBytes;
156
+ private readonly defaultShell;
157
+ private readonly applyEditsFn;
158
+ private wslAvailable;
159
+ private shellCacheInitialized;
160
+ private dangerousPatterns;
161
+ /** 同文件读-改-写的串行队列(key = resolvedPath)。 */
162
+ private readonly fileQueues;
163
+ /**
164
+ * P2b read-before-write 证据:resolvedPath → 读取时的内容版本
165
+ * (CC `seed_read_state` 对齐)。read 成功时记录;write/edit 缺省
166
+ * expectedVersion 时用它做自动 CAS;写成功后回写新版本。会话恢复时
167
+ * 由 sidecar 的 `read_state.seed` 反向调用重灌(seedReadState)。
168
+ */
169
+ private readonly readFileState;
170
+ constructor(maxOutputBytes?: number, applyEditsFn?: ApplyEditsFn);
171
+ updateSafetyConfig(config: CommandSafetyConfigPayload): void;
172
+ /**
173
+ * P2b:会话恢复时重灌 read-before-write 证据。sidecar 扫描持久记录里的
174
+ * 文件工具结果,把 path → version 经 `read_state.seed` 推到这里——进程
175
+ * 重启后自动 CAS 仍然成立(CC `seed_read_state` 语义)。键是 sidecar 侧
176
+ * 已解析的绝对路径,原样存储;resolvePath 对绝对路径是恒等,查询能命中。
177
+ * 返回实际收录的条目数(畸形条目跳过)。
178
+ */
179
+ seedReadState(state: Record<string, unknown>): number;
180
+ init(): Promise<void>;
181
+ getPlatformInfo(): {
182
+ platform: string;
183
+ shell: ShellType;
184
+ wslAvailable: boolean;
185
+ osVersion: string;
186
+ osArch: string;
187
+ };
188
+ executeShell(request: LocalExecRequest): Promise<LocalExecResult>;
189
+ readLocalFile(request: LocalFileReadRequest, projectRoot?: string | null, additionalReadRoots?: string[] | null): Promise<LocalFileReadResult>;
190
+ writeLocalFile(request: LocalFileWriteRequest, projectRoot?: string | null): Promise<LocalFileWriteResult>;
191
+ /**
192
+ * local_edit_file 的实现(W6-1):结构化批量编辑。先按 expectedVersion 做
193
+ * read-before-write 冲突检测,再在原文上做三级模糊定位 + 逆序替换,最后
194
+ * 原子落盘。返回新版本令牌与统一 diff。
195
+ */
196
+ editLocalFile(request: LocalFileEditRequest, projectRoot?: string | null): Promise<LocalFileEditResult>;
197
+ /**
198
+ * read-before-write 冲突检测:当前内容版本与调用方期望不一致时返回错误
199
+ * 消息(引导重新读取),一致返回 null。文件不存在且期望存在也算冲突,
200
+ * 除非 `missingOk`(隐式期望来自读证据:读后被删的文件再写是新建,
201
+ * 没有可覆盖的他人改动)。
202
+ */
203
+ private checkVersion;
204
+ /** 原子写:先写同目录临时文件再 rename(同卷 rename 原子),失败清理临时文件。 */
205
+ private atomicWrite;
206
+ /**
207
+ * 同文件操作串行队列:把同一 resolvedPath 的读-改-写串成链,避免并发
208
+ * 写互相覆盖。不同文件互不影响。
209
+ */
210
+ private serializeFileOp;
211
+ /**
212
+ * local_run_snippet 的实现:把代码片段写到 scratch 目录(os.tmpdir()/
213
+ * agent-shell-code),用本机解释器跑起来;可选先装 pip/npm 依赖。
214
+ *
215
+ * 设计要点:
216
+ * - 解释器探测结果缓存(python3 → python → py;node);缺失时返回可操作错误。
217
+ * - pip 优先 `--user`;被 PEP 668(externally-managed,Homebrew Python 常见)
218
+ * 拒绝时在 scratch 目录建一次性 venv,之后所有 python 运行都用 venv 解释器。
219
+ * - npm 依赖装进 scratchDir,node 脚本也落盘在 scratchDir——node 的
220
+ * node_modules 解析会沿脚本所在目录向上找到依赖,无需 NODE_PATH。
221
+ * - cwd 的项目沙箱在 ToolRouter 层完成,这里只负责跑。
222
+ */
223
+ runCode(request: LocalRunCodeRequest): Promise<LocalRunCodeResult>;
224
+ private getCodeScratchDir;
225
+ /** 解释器探测缓存:undefined=未探测,null=探测过但没有。 */
226
+ private pythonCommand;
227
+ private nodeCommand;
228
+ /** PEP 668 兜底 venv 的解释器路径;一旦建立,后续 python 运行都用它。 */
229
+ private pythonVenvPath;
230
+ private resolveInterpreter;
231
+ private probeCommand;
232
+ /**
233
+ * pip 安装:`--user` 优先;遇到 PEP 668(externally-managed-environment,
234
+ * Homebrew/系统 Python 的默认限制)就在 scratch 目录建 venv 兜底,
235
+ * 并把 venv 解释器缓存下来供后续运行使用。
236
+ *
237
+ * 返回的 `python` 字段是后续运行脚本应使用的解释器(可能已被 venv 替换)。
238
+ */
239
+ private installPythonPackages;
240
+ openLocalTarget(request: LocalOpenRequest): Promise<LocalOpenResult>;
241
+ /**
242
+ * Public wrapper so other execution paths (e.g. the visible-terminal path
243
+ * in main.ts) can run the same dangerous-command check that headless
244
+ * `executeShell` applies, instead of silently bypassing it.
245
+ */
246
+ detectDangerousCommand(command: string): string | null;
247
+ private detectDangerousCommandInternal;
248
+ private getDefaultPatterns;
249
+ private resolvePath;
250
+ private resolveCwd;
251
+ private resolveShell;
252
+ private computeDefaultShell;
253
+ private detectWslAvailable;
254
+ }
255
+ export {};