@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
package/dist/brand.js ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 品牌/ flavor 单一真源(主进程与 Node 侧共用)。
3
+ *
4
+ * flavor 是开放字符串(ScenarioId,0.3g 起不再是二值联合):`generic`
5
+ * 是无场景包的产品 flavor,其余 flavor 由场景包激活。合法 flavor 集合
6
+ * = 应用层 products/manifest.json 的 flavors 键(best-effort 校验,
7
+ * 读不到清单时不强制)。
8
+ *
9
+ * flavor 在打包时由 electron-builder.js 通过 extraMetadata.flavor
10
+ * 写进包内 package.json;运行时从这里读回。dev 优先读 APP_FLAVOR,
11
+ * 再回退包内 package.json 的 flavor 字段,最终兜底 generic。
12
+ *
13
+ * 品牌数据(3.1 起全部产品注入):产品组装根(products/<id>/active.ts)
14
+ * 在模块求值早期调用 setProductBrand()——数据源是 product.json 的
15
+ * brand 字段(无包产品)或激活包的 pack.json brand(0.3g 机制)。
16
+ * shell 默认品牌是中性占位(Steerable Shell),不注入不出产品名。
17
+ * getBrand() 在覆盖注入后重算。
18
+ *
19
+ * 本模块只允许依赖 node 内置模块——llm-settings.ts 会被 vitest 直接 import,
20
+ * 不能碰 electron / better-sqlite3。
21
+ */
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { getAppRootDir } from './runtime.js';
25
+ /** shell 默认智能体;场景产品里仍可选,但不一定作为首页默认。 */
26
+ export const LOCAL_ASSISTANT_AGENT_ID = 'local-assistant';
27
+ /** 内置「智能助手」:种子时 `loadAllSkills` 开启(无视触发条件全量加载技能)。 */
28
+ export const ALL_ROUND_ASSISTANT_AGENT_ID = 'all-round-assistant';
29
+ /**
30
+ * shell 默认品牌——中性框架占位(3.1 起产品名一律由产品组装根注入,
31
+ * 见 products/<id>/product.json 的 brand 字段或激活包的 pack.json)。
32
+ */
33
+ const SHELL_BRAND = {
34
+ displayName: 'Steerable Shell',
35
+ agentName: 'Agent',
36
+ tagline: '一款本地桌面 AI 伙伴',
37
+ defaultAgentId: LOCAL_ASSISTANT_AGENT_ID,
38
+ };
39
+ let productBrand = null;
40
+ let cached = null;
41
+ /**
42
+ * 注入产品品牌(产品组装根在第一个 import 时调用)。重复注入抛错
43
+ * (组装期笔误,fail fast)。注入后作废缓存,后续 getBrand() 重算。
44
+ */
45
+ export function setProductBrand(brand) {
46
+ if (productBrand) {
47
+ throw new Error('[brand] product brand already set');
48
+ }
49
+ productBrand = brand;
50
+ cached = null;
51
+ }
52
+ /** 合法 flavor 集合(应用层 products/manifest.json 的 flavors 键);读不到返回 null(不强制)。 */
53
+ function knownFlavors() {
54
+ try {
55
+ // 3.2 起 flavor→包 清单是应用层数据(products/manifest.json),由产品
56
+ // 组装根经 setAppRootDir 注入锚点;shell 包内不再携带该清单。
57
+ const manifest = JSON.parse(fs.readFileSync(path.join(getAppRootDir(), 'products', 'manifest.json'), 'utf8'));
58
+ return Object.keys(manifest.flavors ?? {});
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ function detectFlavor() {
65
+ const known = knownFlavors();
66
+ const isKnown = (value) => !known || known.includes(value);
67
+ const fromEnv = process.env.APP_FLAVOR;
68
+ if (fromEnv) {
69
+ if (isKnown(fromEnv))
70
+ return fromEnv;
71
+ throw new Error(`Unknown APP_FLAVOR=${fromEnv}. Known flavors: ${known?.join(', ') ?? '(manifest unavailable)'}`);
72
+ }
73
+ try {
74
+ // 应用根 package.json 的 flavor 字段(打包时 electron-builder 经
75
+ // extraMetadata.flavor 写入包内 package.json;3.2 起应用根由产品注入)。
76
+ const pkgPath = path.join(getAppRootDir(), 'package.json');
77
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
78
+ if (pkg.flavor && isKnown(pkg.flavor))
79
+ return pkg.flavor;
80
+ }
81
+ catch {
82
+ // 读不到 package.json 时用兜底
83
+ }
84
+ return 'generic';
85
+ }
86
+ export function getBrand() {
87
+ if (!cached) {
88
+ const flavor = detectFlavor();
89
+ cached = { flavor, ...(productBrand ?? SHELL_BRAND) };
90
+ }
91
+ return cached;
92
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 场景包 HTTP 路由注册表(BS 宿主扩展点,0.4)。
3
+ *
4
+ * 包把 `/host/<packId>/*` 前缀下的路由声明为 HttpRouteContribution,由
5
+ * server/index.ts 在组装期注册;http-server 在宿主自带路由之后、404 之前
6
+ * 按注册顺序精确/段参数匹配。路径必须带 `/host/<packId>/` 前缀——与 IPC
7
+ * 通道的 `<packId>:` 前缀同一命名空间纪律。
8
+ */
9
+ import type { HttpRouteContribution, HttpRouteRequest } from '../scenario/pack.js';
10
+ export type { HttpRouteContribution, HttpRouteRequest };
11
+ /** 注册包 HTTP 路由;路径越出包前缀直接抛错(组装期失败,不进运行时)。 */
12
+ export declare function registerPackHttpRoutes(packId: string, routes: readonly HttpRouteContribution[]): void;
13
+ /** 测试钩子:清空注册表。 */
14
+ export declare function resetPackHttpRoutes(): void;
15
+ /**
16
+ * 匹配已注册的包路由。段参数(`:id`)逐段捕获进 params;无匹配返回 null。
17
+ */
18
+ export declare function matchPackHttpRoute(method: string, pathname: string): {
19
+ route: HttpRouteContribution;
20
+ params: Record<string, string>;
21
+ } | null;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * 场景包 HTTP 路由注册表(BS 宿主扩展点,0.4)。
3
+ *
4
+ * 包把 `/host/<packId>/*` 前缀下的路由声明为 HttpRouteContribution,由
5
+ * server/index.ts 在组装期注册;http-server 在宿主自带路由之后、404 之前
6
+ * 按注册顺序精确/段参数匹配。路径必须带 `/host/<packId>/` 前缀——与 IPC
7
+ * 通道的 `<packId>:` 前缀同一命名空间纪律。
8
+ */
9
+ const packHttpRoutes = [];
10
+ function assertPackRoutePath(packId, path) {
11
+ if (!path.startsWith(`/host/${packId}/`)) {
12
+ throw new Error(`[packs] HTTP 路由 "${path}" 不合规:包 "${packId}" 的路由必须在 /host/${packId}/ 前缀下`);
13
+ }
14
+ }
15
+ /** 注册包 HTTP 路由;路径越出包前缀直接抛错(组装期失败,不进运行时)。 */
16
+ export function registerPackHttpRoutes(packId, routes) {
17
+ for (const route of routes) {
18
+ assertPackRoutePath(packId, route.path);
19
+ packHttpRoutes.push(route);
20
+ }
21
+ }
22
+ /** 测试钩子:清空注册表。 */
23
+ export function resetPackHttpRoutes() {
24
+ packHttpRoutes.length = 0;
25
+ }
26
+ /**
27
+ * 匹配已注册的包路由。段参数(`:id`)逐段捕获进 params;无匹配返回 null。
28
+ */
29
+ export function matchPackHttpRoute(method, pathname) {
30
+ for (const route of packHttpRoutes) {
31
+ if (route.method !== method)
32
+ continue;
33
+ const params = matchPath(route.path, pathname);
34
+ if (params)
35
+ return { route, params };
36
+ }
37
+ return null;
38
+ }
39
+ function matchPath(pattern, pathname) {
40
+ const patternSegs = pattern.split('/');
41
+ const pathSegs = pathname.split('/');
42
+ if (patternSegs.length !== pathSegs.length)
43
+ return null;
44
+ const params = {};
45
+ for (let i = 0; i < patternSegs.length; i += 1) {
46
+ const seg = patternSegs[i];
47
+ if (seg.startsWith(':')) {
48
+ params[seg.slice(1)] = decodeURIComponent(pathSegs[i]);
49
+ }
50
+ else if (seg !== pathSegs[i]) {
51
+ return null;
52
+ }
53
+ }
54
+ return params;
55
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 包 IPC 绑定器(0.3e):把包的 IpcContribution 注册到 Electron ipcMain。
3
+ *
4
+ * 命名空间规则:channel 必须以 `<packId>:` 开头——包只能占用自己的
5
+ * 命名空间,注册期 fail fast。handler 统一收单个序列化载荷(renderer
6
+ * 经 preload 透传),返回值经 IPC 结构化克隆回 renderer。
7
+ */
8
+ import type { IpcContribution } from '../scenario/pack.js';
9
+ /** 命名空间校验(纯函数,单测直接覆盖)。 */
10
+ export declare function assertPackChannel(packId: string, channel: string): void;
11
+ export declare function registerPackIpc(packId: string, contributions: readonly IpcContribution[]): void;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * 包 IPC 绑定器(0.3e):把包的 IpcContribution 注册到 Electron ipcMain。
3
+ *
4
+ * 命名空间规则:channel 必须以 `<packId>:` 开头——包只能占用自己的
5
+ * 命名空间,注册期 fail fast。handler 统一收单个序列化载荷(renderer
6
+ * 经 preload 透传),返回值经 IPC 结构化克隆回 renderer。
7
+ */
8
+ import { ipcMain } from 'electron';
9
+ /** 命名空间校验(纯函数,单测直接覆盖)。 */
10
+ export function assertPackChannel(packId, channel) {
11
+ if (!channel.startsWith(`${packId}:`)) {
12
+ throw new Error(`[pack-ipc] channel "${channel}" must be namespaced under "${packId}:"`);
13
+ }
14
+ }
15
+ export function registerPackIpc(packId, contributions) {
16
+ for (const contribution of contributions) {
17
+ assertPackChannel(packId, contribution.channel);
18
+ ipcMain.handle(contribution.channel, (_event, payload) => contribution.handler(payload));
19
+ }
20
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * 包装配注册表(阶段 2.3)——纯构建期组合的宿主侧机制。
3
+ *
4
+ * 宿主(host/runtime.ts、main.ts、server/index.ts)不再静态 import 任何
5
+ * 包代码:产品组装根(products/<id>/active.ts)把包的装配函数注册进本
6
+ * 注册表,宿主在装配期逐个调用。产品的 tsc 编译单元只 include 自己的包,
7
+ * 他包代码物理上不进产品 dist。
8
+ *
9
+ * 装配分两个时机:
10
+ * - import 期(active.ts 顶层):迁移/种子/品牌等无宿主依赖的注册;
11
+ * - 装配期(createHostRuntime 内):服务/工具/路由等需要宿主能力的部分,
12
+ * 经 {@link PackAssemblyDeps} 注入。
13
+ * 入口差异:CS(main.ts)额外消费 handle.ipc,BS(server/index.ts)额外
14
+ * 消费 handle.httpRoutes——包的入口面贡献由宿主循环驱动,宿主代码里
15
+ * 不出现任何包名。
16
+ */
17
+ import type Database from 'better-sqlite3';
18
+ import type { HttpRouteContribution, IpcContribution, ToolContribution } from '../scenario/pack.js';
19
+ /** 宿主注入包的装配能力面。包不 import 宿主装配模块,全部经回调注入。 */
20
+ export interface PackAssemblyDeps {
21
+ /** 宿主借出的同一个 SQLite 连接(包只读写自己声明的包前缀表)。 */
22
+ readonly db: Database.Database;
23
+ /** 模型可见工具面(与普通回合同源)。 */
24
+ readonly listTools: () => {
25
+ name: string;
26
+ description: string;
27
+ inputSchema: unknown;
28
+ }[];
29
+ /** chatId → 绑定项目(无项目对话返回 null)。与 router.resolveChatProject 同源。 */
30
+ readonly resolveChatProject: (chatId: string) => {
31
+ name: string;
32
+ folderPath: string;
33
+ } | null;
34
+ /** 面向全部用户面的事件广播(CS=所有窗口 IPC,BS=SSE 总线)。 */
35
+ readonly broadcast: (channel: string, payload: unknown) => void;
36
+ /** 工具贡献注册口(toolRouter.registerToolContributions 的收窄面)。 */
37
+ readonly registerTools: (tools: readonly ToolContribution[]) => void;
38
+ /** 用量归因(包的独立流不经 router 记账路径,不注入会静默漏账)。 */
39
+ readonly recordUsage: (input: {
40
+ chatId: string;
41
+ kind: string;
42
+ provider: string | null;
43
+ model: string | null;
44
+ promptTokens: number;
45
+ completionTokens: number;
46
+ totalTokens: number;
47
+ cachedPromptTokens?: number;
48
+ costUsd?: number | null;
49
+ }) => void;
50
+ /** 行为洞察(与主对话同款 turn 事件)。 */
51
+ readonly recordInsight: (input: {
52
+ chatId: string;
53
+ mode?: string;
54
+ modelId?: string | null;
55
+ completionStatus?: string;
56
+ durationMs?: number | null;
57
+ toolNames?: unknown;
58
+ userText?: string;
59
+ assistantText?: string;
60
+ }) => void;
61
+ /** 装配期日志行(进宿主日志)。 */
62
+ readonly onLog: (line: string) => void;
63
+ }
64
+ /** CS 宿主的窗口能力:打开一个加载应用内路由的独立窗口。 */
65
+ export interface PackHostWindowCapabilities {
66
+ /** route 如 '/<pack>-debug-log';title 为窗口副标题(宿主拼品牌名)。 */
67
+ openRouteWindow(route: string, options: {
68
+ title: string;
69
+ }): Promise<void>;
70
+ }
71
+ /** 装配产物:宿主按入口面消费。全部槽位可选。 */
72
+ export interface PackAssemblyHandle {
73
+ /** 宿主关停钩子(进程退出时按装配逆序调用)。 */
74
+ readonly dispose?: () => void | Promise<void>;
75
+ /** CS:包 IPC 贡献(需要宿主窗口能力时经 caps 注入)。 */
76
+ readonly ipc?: (caps: PackHostWindowCapabilities) => readonly IpcContribution[];
77
+ /** BS:/host/<packId>/* 路由贡献。 */
78
+ readonly httpRoutes?: () => readonly HttpRouteContribution[];
79
+ }
80
+ export type PackAssembly = (deps: PackAssemblyDeps) => PackAssemblyHandle | void;
81
+ /** 注册包的装配函数(产品组装根在 import 期调用;重复注册同包抛错)。 */
82
+ export declare function registerPackAssembly(packId: string, assembly: PackAssembly): void;
83
+ /** 全部已注册装配(按注册序)。宿主装配期消费。 */
84
+ export declare function getPackAssemblies(): ReadonlyMap<string, PackAssembly>;
85
+ /** 测试钩子:清空注册表。 */
86
+ export declare function resetPackAssemblies(): void;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 包装配注册表(阶段 2.3)——纯构建期组合的宿主侧机制。
3
+ *
4
+ * 宿主(host/runtime.ts、main.ts、server/index.ts)不再静态 import 任何
5
+ * 包代码:产品组装根(products/<id>/active.ts)把包的装配函数注册进本
6
+ * 注册表,宿主在装配期逐个调用。产品的 tsc 编译单元只 include 自己的包,
7
+ * 他包代码物理上不进产品 dist。
8
+ *
9
+ * 装配分两个时机:
10
+ * - import 期(active.ts 顶层):迁移/种子/品牌等无宿主依赖的注册;
11
+ * - 装配期(createHostRuntime 内):服务/工具/路由等需要宿主能力的部分,
12
+ * 经 {@link PackAssemblyDeps} 注入。
13
+ * 入口差异:CS(main.ts)额外消费 handle.ipc,BS(server/index.ts)额外
14
+ * 消费 handle.httpRoutes——包的入口面贡献由宿主循环驱动,宿主代码里
15
+ * 不出现任何包名。
16
+ */
17
+ const assemblies = new Map();
18
+ /** 注册包的装配函数(产品组装根在 import 期调用;重复注册同包抛错)。 */
19
+ export function registerPackAssembly(packId, assembly) {
20
+ if (assemblies.has(packId)) {
21
+ throw new Error(`[pack-assembly] duplicate registration for pack: ${packId}`);
22
+ }
23
+ assemblies.set(packId, assembly);
24
+ }
25
+ /** 全部已注册装配(按注册序)。宿主装配期消费。 */
26
+ export function getPackAssemblies() {
27
+ return assemblies;
28
+ }
29
+ /** 测试钩子:清空注册表。 */
30
+ export function resetPackAssemblies() {
31
+ assemblies.clear();
32
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * HostRuntime —— CS(Electron 主进程)与 BS(headless HTTP server)共享的
3
+ * 服务装配与生命周期。
4
+ *
5
+ * 历史上 main.ts 与 server/index.ts 各自装配同一份服务(LocalExecutor /
6
+ * ToolRouter / TaskService / sidecar …),两处逐行重复;BS 曾漏接
7
+ * TaskService 导致模型退化成 local_exec_shell + nohup 的"假后台"。本模块
8
+ * 把装配收敛为一处,两个入口只保留各自的广播机制(IPC webContents.send
9
+ * vs SSE 总线)与生命周期(Electron app 事件 vs SIGINT/SIGTERM)。
10
+ *
11
+ * 这也是阶段 3 上提框架后 `@steerable/agent-shell` 的核心装配件:场景包
12
+ * (ScenarioPack)的服务/工具贡献在 0.3 逐槽接入本模块。
13
+ */
14
+ import { LocalExecutor, type LocalExecRequest, type LocalExecResult } from '../local-executor.js';
15
+ import { LocalScriptRegistry } from '../local-script-registry.js';
16
+ import { TerminalManager } from '../terminal-manager.js';
17
+ import { ToolRouter } from '../tool-router.js';
18
+ import { McpServerRegistry } from '../mcp-server-registry.js';
19
+ import { ProjectRegistry } from '../project-registry.js';
20
+ import { LocalBackendRouter } from '../local-backend/router.js';
21
+ import { WorktreeService } from '../local-backend/worktree-service.js';
22
+ import { TaskService } from '../local-backend/task-service.js';
23
+ import { createApprovalBridge } from '../sidecar/reverse-approval.js';
24
+ import { createAskUserBridge } from '../sidecar/reverse-ask-user.js';
25
+ import { type PackAssemblyHandle } from './pack-assembly.js';
26
+ export interface HostRuntimeOptions {
27
+ /** 面向全部用户面的事件广播(CS=所有窗口 IPC,BS=SSE 总线)。 */
28
+ broadcast: (channel: string, payload: unknown) => void;
29
+ /**
30
+ * 可选的"主窗口限定"广播:CS 的 LocalBackendRouter 历史上只推主窗口
31
+ * (title 是 nice-to-have,启动时序里没拿到主窗就静默丢)。缺省退回
32
+ * broadcast(BS 行为)。
33
+ */
34
+ broadcastMain?: (channel: string, payload: unknown) => void;
35
+ /** 是否有人能应答审批/提问(CS=有窗口,BS=有浏览器连着事件总线)。 */
36
+ hasWindow: () => boolean;
37
+ onLog: (line: string) => void;
38
+ /**
39
+ * 启动清扫 running 任务时写入的失败原因。任务流不跨进程存活,上次
40
+ * 进程留下的 running 是崩溃/重启签名;CS/BS 措辞不同(应用/服务)。
41
+ */
42
+ taskSweepReason: string;
43
+ }
44
+ export interface HostRuntime {
45
+ localExecutor: LocalExecutor;
46
+ localScriptRegistry: LocalScriptRegistry;
47
+ terminalManager: TerminalManager;
48
+ /** 包装配产物(2.3):按产品组装根注册的包逐个装配;宿主不感知包名。 */
49
+ packHandles: ReadonlyMap<string, PackAssemblyHandle>;
50
+ mcpServerRegistry: McpServerRegistry;
51
+ projectRegistry: ProjectRegistry;
52
+ toolRouter: ToolRouter;
53
+ worktreeService: WorktreeService;
54
+ taskService: TaskService;
55
+ localBackendRouter: LocalBackendRouter;
56
+ approvalBridge: ReturnType<typeof createApprovalBridge>;
57
+ askUserBridge: ReturnType<typeof createAskUserBridge>;
58
+ /** 可见终端优先的 shell 执行缝(CS 的 local:exec-shell IPC 也用它)。 */
59
+ maybeExecInTerminal: (request: LocalExecRequest) => Promise<LocalExecResult | null>;
60
+ /**
61
+ * 启动后台部分:任务清扫、sidecar 启动、MCP 工具列表刷新、终端预热。
62
+ * 不阻塞调用方的就绪路径(sidecar python import 慢,竞速的回合由
63
+ * router 回退处理)。
64
+ */
65
+ start(): void;
66
+ /** 关停:杀终端、停 mock、关 sidecar。 */
67
+ shutdown(): Promise<void>;
68
+ }
69
+ export declare function createHostRuntime(options: HostRuntimeOptions): HostRuntime;
@@ -0,0 +1,207 @@
1
+ /**
2
+ * HostRuntime —— CS(Electron 主进程)与 BS(headless HTTP server)共享的
3
+ * 服务装配与生命周期。
4
+ *
5
+ * 历史上 main.ts 与 server/index.ts 各自装配同一份服务(LocalExecutor /
6
+ * ToolRouter / TaskService / sidecar …),两处逐行重复;BS 曾漏接
7
+ * TaskService 导致模型退化成 local_exec_shell + nohup 的"假后台"。本模块
8
+ * 把装配收敛为一处,两个入口只保留各自的广播机制(IPC webContents.send
9
+ * vs SSE 总线)与生命周期(Electron app 事件 vs SIGINT/SIGTERM)。
10
+ *
11
+ * 这也是阶段 3 上提框架后 `@steerable/agent-shell` 的核心装配件:场景包
12
+ * (ScenarioPack)的服务/工具贡献在 0.3 逐槽接入本模块。
13
+ */
14
+ import { LocalExecutor, setDefaultExecTimeoutMs, } from '../local-executor.js';
15
+ import { LocalScriptRegistry } from '../local-script-registry.js';
16
+ import { TerminalManager } from '../terminal-manager.js';
17
+ import { ToolRouter } from '../tool-router.js';
18
+ import { McpServerRegistry, } from '../mcp-server-registry.js';
19
+ import { ProjectRegistry } from '../project-registry.js';
20
+ import { LocalBackendRouter } from '../local-backend/router.js';
21
+ import { WorktreeService } from '../local-backend/worktree-service.js';
22
+ import { TaskService } from '../local-backend/task-service.js';
23
+ import { localStore } from '../storage/index.js';
24
+ import { createApprovalBridge } from '../sidecar/reverse-approval.js';
25
+ import { createAskUserBridge } from '../sidecar/reverse-ask-user.js';
26
+ import { startHostSidecar, shutdownHostSidecar } from '../sidecar/boot.js';
27
+ import { createVisibleTerminalExec } from './visible-terminal-exec.js';
28
+ import { createJsonStore } from '../json-store.js';
29
+ import { bindWorkspaceSkillRoots } from '../local-backend/skill-loader.js';
30
+ import { getChatAttachmentsDir } from '../attachments.js';
31
+ import { recordInsightTurn } from '../insights/record.js';
32
+ import { getPackAssemblies, } from './pack-assembly.js';
33
+ export function createHostRuntime(options) {
34
+ const { broadcast, hasWindow, onLog } = options;
35
+ const broadcastMain = options.broadcastMain ?? broadcast;
36
+ const localExecutor = new LocalExecutor();
37
+ // 恢复设置界面里配置的"命令默认超时"(保存时由 local-backend router 即时
38
+ // 生效,这里负责进程重启后的恢复)。
39
+ {
40
+ const persistedLlmSettings = localStore.getLlmSettings();
41
+ setDefaultExecTimeoutMs(persistedLlmSettings?.execTimeoutSeconds
42
+ ? persistedLlmSettings.execTimeoutSeconds * 1000
43
+ : null);
44
+ }
45
+ const localScriptRegistry = new LocalScriptRegistry();
46
+ const terminalManager = new TerminalManager();
47
+ // 外部 MCP 服务注册表(设置 → MCP 服务里导入),持久化到
48
+ // userData/agent-mcp-servers.json;项目注册表同理(agent-projects.json)。
49
+ const mcpServerRegistry = new McpServerRegistry(createJsonStore({
50
+ name: 'agent-mcp-servers',
51
+ defaults: { mcpServers: [] },
52
+ }));
53
+ const projectRegistry = new ProjectRegistry(createJsonStore({
54
+ name: 'agent-projects',
55
+ defaults: { projects: [] },
56
+ }));
57
+ bindWorkspaceSkillRoots(projectRegistry);
58
+ // 终端输出 / 退出 / 新会话 → 广播(CS=所有窗口,BS=SSE 总线)。
59
+ terminalManager.on('data', (sessionId, chunk) => {
60
+ broadcast('terminal:data', { sessionId, chunk });
61
+ });
62
+ terminalManager.on('exit', (sessionId, code, signal) => {
63
+ broadcast('terminal:exit', { sessionId, code, signal });
64
+ });
65
+ terminalManager.on('spawned', (session) => {
66
+ broadcast('terminal:spawned', session);
67
+ });
68
+ const maybeExecInTerminal = createVisibleTerminalExec({
69
+ localExecutor,
70
+ terminalManager,
71
+ broadcast,
72
+ });
73
+ const toolRouter = new ToolRouter(localExecutor, localScriptRegistry, async (request) => {
74
+ const viaTerminal = await maybeExecInTerminal(request);
75
+ if (viaTerminal)
76
+ return viaTerminal;
77
+ return localExecutor.executeShell(request);
78
+ }, mcpServerRegistry, projectRegistry);
79
+ // 4.6a/4.6b:跨 turn 后台任务 + git worktree 隔离。任务流由宿主独立驱动
80
+ // (不绑父 turn 生命周期,见 task-service.ts 模块头);终态经广播推到
81
+ // 用户面任务面板。
82
+ const resolveChatProjectRoot = (chatId) => {
83
+ const projectId = localStore.getChat(chatId)?.projectId;
84
+ if (!projectId)
85
+ return null;
86
+ const project = projectRegistry.get(projectId);
87
+ return project ? { name: project.name, folderPath: project.folderPath } : null;
88
+ };
89
+ // 场景包装配(2.3):产品组装根(products/<id>/active.ts)在 import 期
90
+ // 把包装配函数注册进 pack-assembly 注册表;这里统一装配。宿主不 import
91
+ // 任何包代码——产品 tsc 编译单元只含自己的包(纯构建期组合)。
92
+ const packHandles = new Map();
93
+ {
94
+ const packDeps = {
95
+ db: localStore.getPackDb(),
96
+ listTools: () => toolRouter.listModelSchemas(),
97
+ resolveChatProject: resolveChatProjectRoot,
98
+ broadcast,
99
+ registerTools: (tools) => toolRouter.registerToolContributions(tools),
100
+ recordUsage: (input) => localStore.recordUsageEvent(input),
101
+ recordInsight: (input) => recordInsightTurn(input),
102
+ onLog,
103
+ };
104
+ // 包迁移容错应用(3.2):包的 import 链可能经 llm/index 等模块提前
105
+ // 触发 storage 单例构造(ESM 深度优先,active.ts 的注册体后于链上
106
+ // 单例求值)——构造时注册表尚空,此处(装配前)补上已注册包迁移。
107
+ // 幂等(按包 id 去重),注册表为空时零开销。
108
+ localStore.applyPackMigrations();
109
+ for (const [packId, assemble] of getPackAssemblies()) {
110
+ const handle = assemble(packDeps);
111
+ if (handle)
112
+ packHandles.set(packId, handle);
113
+ }
114
+ }
115
+ const worktreeService = new WorktreeService({ resolveProject: resolveChatProjectRoot });
116
+ const taskService = new TaskService({
117
+ store: localStore,
118
+ toolRouter,
119
+ worktreeService,
120
+ resolveChatProject: resolveChatProjectRoot,
121
+ broadcast,
122
+ });
123
+ toolRouter.setTaskServices({ taskService, worktreeService });
124
+ const localBackendRouter = new LocalBackendRouter(toolRouter, {
125
+ taskService,
126
+ broadcast: broadcastMain,
127
+ });
128
+ // W4-1 / W8:sidecar 反向通道的审批桥与提问桥。
129
+ const approvalBridge = createApprovalBridge({
130
+ broadcast,
131
+ hasWindow,
132
+ onLog: (line) => onLog(line),
133
+ });
134
+ const askUserBridge = createAskUserBridge({
135
+ broadcast,
136
+ hasWindow,
137
+ onLog: (line) => onLog(line),
138
+ });
139
+ let started = false;
140
+ return {
141
+ localExecutor,
142
+ localScriptRegistry,
143
+ terminalManager,
144
+ packHandles,
145
+ mcpServerRegistry,
146
+ projectRegistry,
147
+ toolRouter,
148
+ worktreeService,
149
+ taskService,
150
+ localBackendRouter,
151
+ approvalBridge,
152
+ askUserBridge,
153
+ maybeExecInTerminal,
154
+ start() {
155
+ if (started)
156
+ return;
157
+ started = true;
158
+ // 4.6a:上次进程崩溃/强杀留下的 running 任务不是真相——任务流随进程
159
+ // 一起死了,启动时落成 failed,任务面板据此显示"重启中断"。
160
+ const sweptTasks = localStore.failRunningTasks(options.taskSweepReason);
161
+ if (sweptTasks > 0) {
162
+ onLog(`[task] swept ${sweptTasks} stale running task(s)`);
163
+ }
164
+ void localExecutor.init();
165
+ // Not awaited: sidecar boot (python import) must not delay readiness.
166
+ // A turn that races the boot falls back for that turn only.
167
+ void startHostSidecar({
168
+ toolRouter,
169
+ resolveProjectRoot: (chatId) => localBackendRouter.resolveChatProject(chatId)?.folderPath ?? null,
170
+ // 项目模式下文件读写被围栏在项目目录内;会话附件目录是额外放行的
171
+ // 只读根,保证用户上传的文件即使在项目会话里也能被 agent 读回。
172
+ resolveAdditionalReadRoots: (chatId) => [getChatAttachmentsDir(chatId)],
173
+ approvalHandler: approvalBridge.handler,
174
+ askUserHandler: askUserBridge.handler,
175
+ // P2b: resume 时 sidecar 把记录里的读证据推给 LocalExecutor 的
176
+ // readFileState(CC seed_read_state)——进程重启后自动 CAS 仍成立。
177
+ readStateSeedHandler: (params) => {
178
+ const state = params?.state;
179
+ const seeded = localExecutor.seedReadState(state && typeof state === 'object' && !Array.isArray(state)
180
+ ? state
181
+ : {});
182
+ return Promise.resolve({ seeded });
183
+ },
184
+ onLogLine: onLog,
185
+ });
186
+ // 后台刷新已启用 MCP 服务的工具列表(连接慢的 server 不阻塞启动)。
187
+ void mcpServerRegistry.refreshAllEnabled();
188
+ // 预热共享可见 PTY:首条 agent 命令不必付 shell 启动成本。
189
+ setTimeout(() => {
190
+ try {
191
+ terminalManager.ensurePrimary();
192
+ }
193
+ catch (err) {
194
+ onLog(`[terminal] pre-warm failed: ${err instanceof Error ? err.message : String(err)}`);
195
+ }
196
+ }, 2_000);
197
+ },
198
+ async shutdown() {
199
+ terminalManager.killAll();
200
+ // 包装配逆序关停(后装配的先停)。
201
+ for (const handle of [...packHandles.values()].reverse()) {
202
+ await handle.dispose?.();
203
+ }
204
+ await shutdownHostSidecar();
205
+ },
206
+ };
207
+ }
@@ -0,0 +1,21 @@
1
+ import { type LocalExecRequest, type LocalExecResult, type LocalExecutor } from '../local-executor.js';
2
+ import type { TerminalManager } from '../terminal-manager.js';
3
+ export type HostBroadcast = (channel: string, payload: unknown) => void;
4
+ export interface VisibleTerminalExecDeps {
5
+ localExecutor: LocalExecutor;
6
+ terminalManager: TerminalManager;
7
+ broadcast: HostBroadcast;
8
+ }
9
+ /**
10
+ * Map a raw PTY shell binary name (e.g. `powershell.exe`, `cmd.exe`, `zsh`) to
11
+ * the {@link LocalExecResult} `shell` enum so the agent sees the *real* dialect
12
+ * it just ran under and can pick the right syntax next turn.
13
+ */
14
+ export declare function mapSessionShell(shell: string): LocalExecResult['shell'];
15
+ /**
16
+ * Decide whether to route a shell command through the visible PTY instead of
17
+ * the headless local-executor. Returns null to fall back headless. No renderer
18
+ * needs to be attached — the exec sentinel capture runs entirely in this
19
+ * process.
20
+ */
21
+ export declare function createVisibleTerminalExec(deps: VisibleTerminalExecDeps): (req: LocalExecRequest) => Promise<LocalExecResult | null>;