@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,22 @@
1
+ /**
2
+ * 包迁移注册表 —— ScenarioPack 的存储槽位(0.3b)。
3
+ *
4
+ * `storage/index.ts` 的 `LocalStore` 单例在模块求值时即构造并跑 `migrate()`,
5
+ * 所以包迁移必须在任何 `storage/index.js` import 之前注册。入口
6
+ * (main.ts / server/index.ts)把 `packs/active.ts` 作为第一个 import
7
+ * 来保证这个顺序。
8
+ *
9
+ * 本模块刻意不 import `storage/index.js`,避免反向依赖。
10
+ */
11
+ import type { MigrationContribution } from '../scenario/pack.js';
12
+ interface RegisteredMigration {
13
+ packId: string;
14
+ migration: MigrationContribution;
15
+ }
16
+ /**
17
+ * 注册一个包的迁移贡献。重复注册同一包会抛错(组装期笔误,fail fast)。
18
+ */
19
+ export declare function registerPackMigrations(packId: string, migration: MigrationContribution): void;
20
+ /** 已注册的全部迁移,按注册顺序返回。 */
21
+ export declare function getPackMigrations(): readonly RegisteredMigration[];
22
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 包迁移注册表 —— ScenarioPack 的存储槽位(0.3b)。
3
+ *
4
+ * `storage/index.ts` 的 `LocalStore` 单例在模块求值时即构造并跑 `migrate()`,
5
+ * 所以包迁移必须在任何 `storage/index.js` import 之前注册。入口
6
+ * (main.ts / server/index.ts)把 `packs/active.ts` 作为第一个 import
7
+ * 来保证这个顺序。
8
+ *
9
+ * 本模块刻意不 import `storage/index.js`,避免反向依赖。
10
+ */
11
+ const registered = [];
12
+ /**
13
+ * 注册一个包的迁移贡献。重复注册同一包会抛错(组装期笔误,fail fast)。
14
+ */
15
+ export function registerPackMigrations(packId, migration) {
16
+ if (registered.some((r) => r.packId === packId)) {
17
+ throw new Error(`[pack-migrations] duplicate registration for pack: ${packId}`);
18
+ }
19
+ registered.push({ packId, migration });
20
+ }
21
+ /** 已注册的全部迁移,按注册顺序返回。 */
22
+ export function getPackMigrations() {
23
+ return registered;
24
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 包智能体种子注册表 —— ScenarioPack 的种子槽位(0.3d)。
3
+ *
4
+ * 与 pack-migrations 同一模式:`LocalStore` 单例在模块求值时构造并跑
5
+ * `seedDefaults()`,所以种子必须在任何 `storage/index.js` import 之前
6
+ * 由 `packs/active.ts` 注册。
7
+ *
8
+ * 与迁移不同的一点:**未激活的包也要注册**(active=false)——已有库
9
+ * 里「从未定制过」的包内置智能体需要在包缺席的产品里归档,shell 只有
10
+ * 拿到种子数据才能识别「未定制」。
11
+ *
12
+ * 本模块刻意不 import `storage/index.js`,避免反向依赖。判定函数是纯
13
+ * 函数,单测直接覆盖(tests/pack-seeds.test.ts)。
14
+ */
15
+ import type { AgentSeed } from '../scenario/pack.js';
16
+ export interface PackAgentSeedRegistration {
17
+ packId: string;
18
+ seeds: readonly AgentSeed[];
19
+ /** 包在当前产品是否激活;未激活包的种子只跑「未定制则归档」。 */
20
+ active: boolean;
21
+ }
22
+ /** 注册一个包的智能体种子。重复注册同一包抛错(组装期笔误,fail fast)。 */
23
+ export declare function registerPackAgentSeeds(packId: string, seeds: readonly AgentSeed[], active: boolean): void;
24
+ /** 已注册的全部种子,按注册顺序返回。 */
25
+ export declare function getPackAgentSeeds(): readonly PackAgentSeedRegistration[];
26
+ /** chat_agents 行中种子判定需要的最小列。 */
27
+ export interface AgentSeedRow {
28
+ name: string;
29
+ description: string | null;
30
+ role_prompt: string;
31
+ is_archived: number;
32
+ }
33
+ /** 命中任一历史代文案(提示词前缀匹配,描述缺省按当前代)。 */
34
+ export declare function matchesPreviousIdentity(seed: AgentSeed, row: AgentSeedRow): boolean;
35
+ /** 当前代或任一历史代全中 = 用户从未定制过这行内置智能体。 */
36
+ export declare function isUntouchedSeed(seed: AgentSeed, row: AgentSeedRow): boolean;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 包智能体种子注册表 —— ScenarioPack 的种子槽位(0.3d)。
3
+ *
4
+ * 与 pack-migrations 同一模式:`LocalStore` 单例在模块求值时构造并跑
5
+ * `seedDefaults()`,所以种子必须在任何 `storage/index.js` import 之前
6
+ * 由 `packs/active.ts` 注册。
7
+ *
8
+ * 与迁移不同的一点:**未激活的包也要注册**(active=false)——已有库
9
+ * 里「从未定制过」的包内置智能体需要在包缺席的产品里归档,shell 只有
10
+ * 拿到种子数据才能识别「未定制」。
11
+ *
12
+ * 本模块刻意不 import `storage/index.js`,避免反向依赖。判定函数是纯
13
+ * 函数,单测直接覆盖(tests/pack-seeds.test.ts)。
14
+ */
15
+ const registrations = [];
16
+ /** 注册一个包的智能体种子。重复注册同一包抛错(组装期笔误,fail fast)。 */
17
+ export function registerPackAgentSeeds(packId, seeds, active) {
18
+ if (registrations.some((r) => r.packId === packId)) {
19
+ throw new Error(`[pack-seeds] duplicate registration for pack: ${packId}`);
20
+ }
21
+ registrations.push({ packId, seeds, active });
22
+ }
23
+ /** 已注册的全部种子,按注册顺序返回。 */
24
+ export function getPackAgentSeeds() {
25
+ return registrations;
26
+ }
27
+ /** 命中当前代文案:名字 + 描述全等、提示词全等。 */
28
+ function matchesCurrent(seed, row) {
29
+ return (row.name === seed.name &&
30
+ (row.description ?? '') === (seed.description ?? '') &&
31
+ row.role_prompt === seed.rolePrompt);
32
+ }
33
+ /** 命中任一历史代文案(提示词前缀匹配,描述缺省按当前代)。 */
34
+ export function matchesPreviousIdentity(seed, row) {
35
+ return (seed.previousIdentities ?? []).some((prev) => row.name === prev.name &&
36
+ (row.description ?? '') === (prev.description ?? seed.description ?? '') &&
37
+ row.role_prompt.startsWith(prev.rolePromptHead));
38
+ }
39
+ /** 当前代或任一历史代全中 = 用户从未定制过这行内置智能体。 */
40
+ export function isUntouchedSeed(seed, row) {
41
+ return matchesCurrent(seed, row) || matchesPreviousIdentity(seed, row);
42
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * W6-6 遥测合规化:OTLP collector 连接 + 隐私模式的纯逻辑(默认值/校验/归一)。
3
+ *
4
+ * 与 `llm-settings.ts` 同样的拆分理由:不依赖 electron / better-sqlite3,
5
+ * 可在纯 Node/vitest 下直接单测。持久化在 `storage/index.ts` 的
6
+ * `settings_kv` 表(key = 'telemetry_settings')。
7
+ *
8
+ * 隐私模型(与 framework `otel.py` 的 PrivacyMode 对齐):
9
+ * - 未配置 endpoint = 关。一条 trace 都不出进程——这是默认,也是隐私安全默认。
10
+ * - 'metadata':只导出结构/时延/状态,事件 payload 正文与自由属性一律丢弃。
11
+ * - 'full':导出(已脱敏的)payload 与属性。仅当 collector 可信时选。
12
+ */
13
+ export type TelemetryPrivacyMode = 'full' | 'metadata';
14
+ export interface TelemetrySettings {
15
+ /**
16
+ * OTLP/HTTP collector 的 /v1/traces URL,例如
17
+ * `http://127.0.0.1:4318/v1/traces`。空/缺省 = 遥测关闭(默认)。
18
+ */
19
+ endpoint?: string;
20
+ /** 隐私档位;缺省 'metadata'(可观测但不外发内容)。 */
21
+ privacyMode: TelemetryPrivacyMode;
22
+ /** 资源属性 service.name;缺省 'steerable-agent-desktop'。 */
23
+ serviceName?: string;
24
+ }
25
+ export declare const DEFAULT_TELEMETRY_SETTINGS: TelemetrySettings;
26
+ /** 遥测是否启用:有合法 endpoint 才启用(privacyMode 不影响开关)。 */
27
+ export declare function telemetryEnabled(s: TelemetrySettings | null | undefined): boolean;
28
+ /**
29
+ * 归一 endpoint:trim;空串视为未配置。只接受 http(s) URL——别的协议
30
+ * (file: 等)直接拒绝,避免把 trace POST 到非预期 scheme。返回 undefined
31
+ * 表示无效/未配置。
32
+ */
33
+ export declare function normalizeTelemetryEndpoint(raw: unknown): string | undefined;
34
+ /**
35
+ * 合并/校验一份(可能残缺的)incoming 设置,落到合法 TelemetrySettings。
36
+ * endpoint 非法一律归一为 undefined(=关),绝不让一个坏地址把遥测打开。
37
+ */
38
+ export declare function mergeTelemetrySettings(settings?: Partial<TelemetrySettings> | null): TelemetrySettings;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * W6-6 遥测合规化:OTLP collector 连接 + 隐私模式的纯逻辑(默认值/校验/归一)。
3
+ *
4
+ * 与 `llm-settings.ts` 同样的拆分理由:不依赖 electron / better-sqlite3,
5
+ * 可在纯 Node/vitest 下直接单测。持久化在 `storage/index.ts` 的
6
+ * `settings_kv` 表(key = 'telemetry_settings')。
7
+ *
8
+ * 隐私模型(与 framework `otel.py` 的 PrivacyMode 对齐):
9
+ * - 未配置 endpoint = 关。一条 trace 都不出进程——这是默认,也是隐私安全默认。
10
+ * - 'metadata':只导出结构/时延/状态,事件 payload 正文与自由属性一律丢弃。
11
+ * - 'full':导出(已脱敏的)payload 与属性。仅当 collector 可信时选。
12
+ */
13
+ export const DEFAULT_TELEMETRY_SETTINGS = {
14
+ endpoint: undefined, // 默认关闭:没有 collector 地址就一条都不发
15
+ privacyMode: 'metadata',
16
+ serviceName: 'steerable-agent-desktop',
17
+ };
18
+ /** 遥测是否启用:有合法 endpoint 才启用(privacyMode 不影响开关)。 */
19
+ export function telemetryEnabled(s) {
20
+ return !!normalizeTelemetryEndpoint(s?.endpoint);
21
+ }
22
+ /**
23
+ * 归一 endpoint:trim;空串视为未配置。只接受 http(s) URL——别的协议
24
+ * (file: 等)直接拒绝,避免把 trace POST 到非预期 scheme。返回 undefined
25
+ * 表示无效/未配置。
26
+ */
27
+ export function normalizeTelemetryEndpoint(raw) {
28
+ if (typeof raw !== 'string')
29
+ return undefined;
30
+ const trimmed = raw.trim();
31
+ if (!trimmed)
32
+ return undefined;
33
+ try {
34
+ const url = new URL(trimmed);
35
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
36
+ return undefined;
37
+ return trimmed;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ function normalizePrivacyMode(raw) {
44
+ return raw === 'full' ? 'full' : 'metadata';
45
+ }
46
+ /**
47
+ * 合并/校验一份(可能残缺的)incoming 设置,落到合法 TelemetrySettings。
48
+ * endpoint 非法一律归一为 undefined(=关),绝不让一个坏地址把遥测打开。
49
+ */
50
+ export function mergeTelemetrySettings(settings) {
51
+ const incoming = settings ?? {};
52
+ return {
53
+ endpoint: normalizeTelemetryEndpoint(incoming.endpoint),
54
+ privacyMode: normalizePrivacyMode(incoming.privacyMode),
55
+ serviceName: typeof incoming.serviceName === 'string' && incoming.serviceName.trim()
56
+ ? incoming.serviceName.trim()
57
+ : DEFAULT_TELEMETRY_SETTINGS.serviceName,
58
+ };
59
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * W6-9 用量与成本归因 —— 用量聚合的纯逻辑部分。
3
+ *
4
+ * `LocalStore.getUsageSummary` 负责 SQL(按 model/provider 分桶 GROUP BY),
5
+ * 本模块负责把 SQL 行映射成 `UsageSummary`(逐桶 null-cost 处理 + 总计 reduce)。
6
+ * 拆成纯函数是为了可测:`LocalStore` 顶层加载的 better-sqlite3 native module
7
+ * 与 vitest 的 node 运行时二进制不兼容,无法直接实例化测试。
8
+ *
9
+ * 成本口径:只统计有单价的模型(framework `MODEL_PRICES`);无单价模型的
10
+ * `cost_usd` 存 NULL,SQL `SUM` 全 NULL 得 NULL → 该桶 `costUsd = null`
11
+ * (面板渲染 "—"),且不计入总成本。
12
+ */
13
+ /** 一个 model(+provider) 分桶的用量聚合。 */
14
+ export interface UsageModelBucket {
15
+ model: string;
16
+ provider: string;
17
+ turns: number;
18
+ promptTokens: number;
19
+ completionTokens: number;
20
+ totalTokens: number;
21
+ cachedPromptTokens: number;
22
+ /** 该桶成本合计(USD);桶内无一条有单价时为 null(渲染为 "—")。 */
23
+ costUsd: number | null;
24
+ }
25
+ /** 用量面板数据 —— 按 model 分桶 + 总计。 */
26
+ export interface UsageSummary {
27
+ sinceDays: number;
28
+ byModel: UsageModelBucket[];
29
+ totals: {
30
+ turns: number;
31
+ promptTokens: number;
32
+ completionTokens: number;
33
+ totalTokens: number;
34
+ cachedPromptTokens: number;
35
+ /** 所有有单价模型的成本合计(无单价模型不计入)。 */
36
+ costUsd: number;
37
+ };
38
+ }
39
+ /** `getUsageSummary` SQL 查询返回的原始行(snake_case 列)。 */
40
+ export interface UsageSummaryRow {
41
+ model: string;
42
+ provider: string;
43
+ turns: number;
44
+ prompt_tokens: number;
45
+ completion_tokens: number;
46
+ total_tokens: number;
47
+ cached_prompt_tokens: number;
48
+ cost_usd: number | null;
49
+ }
50
+ /**
51
+ * 把 SQL 分桶行聚合成 `UsageSummary`。
52
+ * 逐桶:`cost_usd` 为 NULL/undefined → `costUsd = null`;否则转 number。
53
+ * 总计:token/轮次直接相加;成本只累加非 null 桶(无单价模型不摊入)。
54
+ */
55
+ export declare function buildUsageSummary(rows: UsageSummaryRow[], sinceDays: number): UsageSummary;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * W6-9 用量与成本归因 —— 用量聚合的纯逻辑部分。
3
+ *
4
+ * `LocalStore.getUsageSummary` 负责 SQL(按 model/provider 分桶 GROUP BY),
5
+ * 本模块负责把 SQL 行映射成 `UsageSummary`(逐桶 null-cost 处理 + 总计 reduce)。
6
+ * 拆成纯函数是为了可测:`LocalStore` 顶层加载的 better-sqlite3 native module
7
+ * 与 vitest 的 node 运行时二进制不兼容,无法直接实例化测试。
8
+ *
9
+ * 成本口径:只统计有单价的模型(framework `MODEL_PRICES`);无单价模型的
10
+ * `cost_usd` 存 NULL,SQL `SUM` 全 NULL 得 NULL → 该桶 `costUsd = null`
11
+ * (面板渲染 "—"),且不计入总成本。
12
+ */
13
+ /**
14
+ * 把 SQL 分桶行聚合成 `UsageSummary`。
15
+ * 逐桶:`cost_usd` 为 NULL/undefined → `costUsd = null`;否则转 number。
16
+ * 总计:token/轮次直接相加;成本只累加非 null 桶(无单价模型不摊入)。
17
+ */
18
+ export function buildUsageSummary(rows, sinceDays) {
19
+ const byModel = rows.map((row) => ({
20
+ model: String(row.model),
21
+ provider: String(row.provider),
22
+ turns: Number(row.turns),
23
+ promptTokens: Number(row.prompt_tokens),
24
+ completionTokens: Number(row.completion_tokens),
25
+ totalTokens: Number(row.total_tokens),
26
+ cachedPromptTokens: Number(row.cached_prompt_tokens),
27
+ costUsd: row.cost_usd === null || row.cost_usd === undefined ? null : Number(row.cost_usd),
28
+ }));
29
+ const totals = byModel.reduce((acc, b) => ({
30
+ turns: acc.turns + b.turns,
31
+ promptTokens: acc.promptTokens + b.promptTokens,
32
+ completionTokens: acc.completionTokens + b.completionTokens,
33
+ totalTokens: acc.totalTokens + b.totalTokens,
34
+ cachedPromptTokens: acc.cachedPromptTokens + b.cachedPromptTokens,
35
+ costUsd: acc.costUsd + (b.costUsd ?? 0),
36
+ }), { turns: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedPromptTokens: 0, costUsd: 0 });
37
+ return { sinceDays, byModel, totals };
38
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Web search settings: Tavily key and/or an explicit free backend.
3
+ *
4
+ * Empty Tavily key → sidecar does not register `web_search` unless:
5
+ * - stored provider is `ddg` (DuckDuckGo lite, no key), or
6
+ * - the current LLM has hosted search (OpenAI) and the host injects
7
+ * `STEERABLE_WEB_SEARCH_PROVIDER=host`.
8
+ * DuckDuckGo is never a silent fallback for an empty Tavily key.
9
+ */
10
+ export type WebSearchProviderId = 'tavily' | 'ddg';
11
+ export interface WebSearchSettings {
12
+ /** Search backend. Default `tavily` keeps the previous empty-key = off behavior. */
13
+ provider: WebSearchProviderId;
14
+ /** Search backend key (Tavily). Empty/undefined = not configured. */
15
+ apiKey?: string;
16
+ }
17
+ export declare const DEFAULT_WEB_SEARCH_SETTINGS: WebSearchSettings;
18
+ export declare function normalizeWebSearchProvider(raw: unknown): WebSearchProviderId;
19
+ export declare function mergeWebSearchSettings(settings?: Partial<WebSearchSettings> | null): WebSearchSettings;
20
+ /** Hostnames whose chat credential can run hosted web search (OpenAI). */
21
+ export declare function hostedSearchAvailable(baseUrl?: string): boolean;
22
+ /**
23
+ * Origin for the in-process search backend. Mirrors
24
+ * `WebToolsConfig` / `_default_search_base_url` in the sidecar.
25
+ */
26
+ export declare function defaultSearchBaseUrl(provider: string): string;
27
+ /**
28
+ * Sidecar env the desktop injects at spawn. Process env wins over stored
29
+ * settings. `ddg` is an explicit stored (or operator) choice. `host` is
30
+ * only set when there is no search key, the stored provider is not `ddg`,
31
+ * and the LLM base URL is OpenAI.
32
+ */
33
+ export declare function sidecarWebSearchEnv(opts: {
34
+ processEnv: NodeJS.ProcessEnv;
35
+ storedApiKey?: string;
36
+ storedProvider?: WebSearchProviderId;
37
+ llmBaseUrl?: string;
38
+ }): Record<string, string>;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Web search settings: Tavily key and/or an explicit free backend.
3
+ *
4
+ * Empty Tavily key → sidecar does not register `web_search` unless:
5
+ * - stored provider is `ddg` (DuckDuckGo lite, no key), or
6
+ * - the current LLM has hosted search (OpenAI) and the host injects
7
+ * `STEERABLE_WEB_SEARCH_PROVIDER=host`.
8
+ * DuckDuckGo is never a silent fallback for an empty Tavily key.
9
+ */
10
+ export const DEFAULT_WEB_SEARCH_SETTINGS = {
11
+ provider: 'tavily',
12
+ apiKey: undefined,
13
+ };
14
+ export function normalizeWebSearchProvider(raw) {
15
+ return raw === 'ddg' ? 'ddg' : 'tavily';
16
+ }
17
+ export function mergeWebSearchSettings(settings) {
18
+ const incoming = settings ?? {};
19
+ const key = typeof incoming.apiKey === 'string' ? incoming.apiKey.trim() : '';
20
+ return {
21
+ provider: normalizeWebSearchProvider(incoming.provider),
22
+ apiKey: key || undefined,
23
+ };
24
+ }
25
+ /** Hostnames whose chat credential can run hosted web search (OpenAI). */
26
+ export function hostedSearchAvailable(baseUrl) {
27
+ if (!baseUrl || !baseUrl.trim())
28
+ return false;
29
+ try {
30
+ const host = new URL(baseUrl.includes('://') ? baseUrl : `https://${baseUrl}`).hostname;
31
+ return host === 'api.openai.com' || host.endsWith('.openai.com');
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ /**
38
+ * Origin for the in-process search backend. Mirrors
39
+ * `WebToolsConfig` / `_default_search_base_url` in the sidecar.
40
+ */
41
+ export function defaultSearchBaseUrl(provider) {
42
+ if (provider === 'brave')
43
+ return 'https://api.search.brave.com';
44
+ if (provider === 'ddg')
45
+ return 'https://html.duckduckgo.com';
46
+ return 'https://api.tavily.com';
47
+ }
48
+ /**
49
+ * Sidecar env the desktop injects at spawn. Process env wins over stored
50
+ * settings. `ddg` is an explicit stored (or operator) choice. `host` is
51
+ * only set when there is no search key, the stored provider is not `ddg`,
52
+ * and the LLM base URL is OpenAI.
53
+ */
54
+ export function sidecarWebSearchEnv(opts) {
55
+ const fromEnv = ((opts.processEnv.STEERABLE_WEB_SEARCH_API_KEY || '').trim()
56
+ || (opts.processEnv.TAVILY_API_KEY || '').trim());
57
+ if (fromEnv) {
58
+ return { STEERABLE_WEB_SEARCH_API_KEY: fromEnv };
59
+ }
60
+ if ((opts.processEnv.STEERABLE_WEB_SEARCH_PROVIDER || '').trim()) {
61
+ return {};
62
+ }
63
+ if (opts.storedProvider === 'ddg') {
64
+ return { STEERABLE_WEB_SEARCH_PROVIDER: 'ddg' };
65
+ }
66
+ const stored = (opts.storedApiKey || '').trim();
67
+ if (stored) {
68
+ return { STEERABLE_WEB_SEARCH_API_KEY: stored };
69
+ }
70
+ if (hostedSearchAvailable(opts.llmBaseUrl)) {
71
+ return { STEERABLE_WEB_SEARCH_PROVIDER: 'host' };
72
+ }
73
+ return {};
74
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Cross-process write lease for one sqlite database.
3
+ *
4
+ * Sibling lock file: `<dbFileName>` → 同路径换 `.lock` 后缀。
5
+ *
6
+ * The lease is an open `BEGIN EXCLUSIVE` transaction on the lock file,
7
+ * which is a kernel lock in SQLite's VFS (fcntl on POSIX, LockFileEx on
8
+ * Windows). That gives the three properties the desktop needs: a second
9
+ * process fails loud (`StoreAlreadyOwnedError`) instead of corrupting a
10
+ * shared WAL, process death releases the lock without a TTL, and no
11
+ * caller can steal a live lease.
12
+ *
13
+ * Node exposes no portable `flock`, and `fs.constants.O_EXLOCK` does not
14
+ * exist on any platform, so SQLite's own locking is the only kernel lock
15
+ * reachable from here — and it is already a shipped dependency because
16
+ * the database it guards is sqlite.
17
+ */
18
+ export declare class StoreAlreadyOwnedError extends Error {
19
+ readonly lockPath: string;
20
+ constructor(lockPath: string);
21
+ }
22
+ export interface HeldWriteLease {
23
+ release(): void;
24
+ }
25
+ export declare function lockPathForDb(dbPath: string): string;
26
+ export declare function acquireWriteLease(lockPath: string): HeldWriteLease;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Cross-process write lease for one sqlite database.
3
+ *
4
+ * Sibling lock file: `<dbFileName>` → 同路径换 `.lock` 后缀。
5
+ *
6
+ * The lease is an open `BEGIN EXCLUSIVE` transaction on the lock file,
7
+ * which is a kernel lock in SQLite's VFS (fcntl on POSIX, LockFileEx on
8
+ * Windows). That gives the three properties the desktop needs: a second
9
+ * process fails loud (`StoreAlreadyOwnedError`) instead of corrupting a
10
+ * shared WAL, process death releases the lock without a TTL, and no
11
+ * caller can steal a live lease.
12
+ *
13
+ * Node exposes no portable `flock`, and `fs.constants.O_EXLOCK` does not
14
+ * exist on any platform, so SQLite's own locking is the only kernel lock
15
+ * reachable from here — and it is already a shipped dependency because
16
+ * the database it guards is sqlite.
17
+ */
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { createRequire } from 'node:module';
21
+ const require = createRequire(import.meta.url);
22
+ export class StoreAlreadyOwnedError extends Error {
23
+ lockPath;
24
+ constructor(lockPath) {
25
+ super(`store already owned: ${lockPath} (another process has this database open for write)`);
26
+ this.name = 'StoreAlreadyOwnedError';
27
+ this.lockPath = lockPath;
28
+ }
29
+ }
30
+ /** SQLite reports a live incumbent as SQLITE_BUSY / SQLITE_BUSY_SNAPSHOT. */
31
+ function isSqliteBusy(err) {
32
+ const code = err?.code;
33
+ return typeof code === 'string' && code.startsWith('SQLITE_BUSY');
34
+ }
35
+ export function lockPathForDb(dbPath) {
36
+ const parsed = path.parse(path.resolve(dbPath));
37
+ return path.join(parsed.dir, `${parsed.name}.lock`);
38
+ }
39
+ export function acquireWriteLease(lockPath) {
40
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
41
+ // Loaded lazily so tests that only exercise lockPathForDb do not need
42
+ // better-sqlite3's Electron ABI.
43
+ const Database = require('better-sqlite3');
44
+ const db = new Database(lockPath, { timeout: 0 });
45
+ try {
46
+ // Every statement here can hit the incumbent's lock, including the
47
+ // journal_mode pragma — it rewrites the header and so needs the same
48
+ // write lock. busy_timeout 0 makes that immediate instead of a stall.
49
+ db.pragma('busy_timeout = 0');
50
+ db.pragma('journal_mode = DELETE');
51
+ db.exec('CREATE TABLE IF NOT EXISTS lease (id INTEGER PRIMARY KEY)');
52
+ db.exec('BEGIN EXCLUSIVE');
53
+ }
54
+ catch (err) {
55
+ db.close();
56
+ // Only contention becomes StoreAlreadyOwnedError; a corrupt or
57
+ // unreadable lock file must not masquerade as a second instance.
58
+ if (isSqliteBusy(err)) {
59
+ throw new StoreAlreadyOwnedError(lockPath);
60
+ }
61
+ throw err;
62
+ }
63
+ let released = false;
64
+ return {
65
+ release() {
66
+ if (released)
67
+ return;
68
+ released = true;
69
+ // Closing rolls the transaction back and drops the kernel lock. The
70
+ // lock file itself is never deleted.
71
+ db.close();
72
+ },
73
+ };
74
+ }
@@ -0,0 +1,83 @@
1
+ import { EventEmitter } from 'events';
2
+ export type TerminalShell = 'zsh' | 'bash' | 'sh' | 'powershell.exe' | 'cmd.exe';
3
+ export interface TerminalSpawnOptions {
4
+ shell?: TerminalShell;
5
+ cwd?: string;
6
+ env?: Record<string, string>;
7
+ cols?: number;
8
+ rows?: number;
9
+ }
10
+ export interface TerminalSession {
11
+ id: string;
12
+ shell: string;
13
+ pid: number;
14
+ cwd: string;
15
+ cols: number;
16
+ rows: number;
17
+ }
18
+ export interface TerminalExecResult {
19
+ success: boolean;
20
+ exitCode: number;
21
+ stdout: string;
22
+ stderr: string;
23
+ truncated: boolean;
24
+ durationMs: number;
25
+ }
26
+ export interface TerminalManagerEvents {
27
+ data: (sessionId: string, chunk: string) => void;
28
+ exit: (sessionId: string, code: number, signal: string | null) => void;
29
+ spawned: (session: TerminalSession) => void;
30
+ }
31
+ /**
32
+ * Owns interactive PTY sessions and provides two complementary APIs:
33
+ *
34
+ * 1. **Streaming**: spawn a PTY, push raw bytes to subscribers (xterm UI),
35
+ * accept user input via `write()`. Standard interactive terminal model.
36
+ *
37
+ * 2. **Programmatic exec**: `exec(id, command)` writes a command to the PTY
38
+ * wrapped with a unique end-of-command sentinel, captures the output
39
+ * between write and sentinel, and resolves with stdout + exit code.
40
+ * This is what the agent uses when it wants to "drive" the user's
41
+ * visible terminal — the user sees the command being typed and the
42
+ * output streaming live, while the agent gets a structured result back.
43
+ *
44
+ * Concurrency note: only one `exec()` may be in flight per session. Callers
45
+ * must serialize their requests; this class throws if exec is invoked while
46
+ * another exec is still pending on the same session.
47
+ */
48
+ export declare class TerminalManager extends EventEmitter {
49
+ private sessions;
50
+ spawn(options?: TerminalSpawnOptions): TerminalSession;
51
+ list(): TerminalSession[];
52
+ primarySession(): TerminalSession | null;
53
+ ensurePrimary(options?: TerminalSpawnOptions): TerminalSession;
54
+ /**
55
+ * Snapshot of recent PTY output, used to "catch up" a renderer that
56
+ * subscribed after the session started producing output.
57
+ */
58
+ getReplayBuffer(id: string): string;
59
+ write(id: string, data: string): boolean;
60
+ resize(id: string, cols: number, rows: number): boolean;
61
+ kill(id: string): boolean;
62
+ killAll(): void;
63
+ /**
64
+ * Execute a single command in the visible PTY and wait for it to finish.
65
+ * The user sees the command typed and output streaming live. The returned
66
+ * result is parsed from the captured stream by stripping the sentinel.
67
+ *
68
+ * Caller must ensure no other exec() is pending on the same session.
69
+ *
70
+ * `killOnTimeout`: on timeout, send SIGINT (Ctrl-C) to the PTY instead of
71
+ * just giving up on the capture. A timed-out command otherwise keeps
72
+ * running in the shared visible terminal — every subsequent exec jams
73
+ * behind it (typed into the busy terminal, never reaching a shell prompt)
74
+ * and times out in cascade. The wrapper's `; printf <sentinel>` survives
75
+ * the interrupt (SIGINT kills the foreground pipeline, not the shell's
76
+ * command list), so after Ctrl-C we keep waiting briefly for the sentinel
77
+ * and resolve with the interrupted exit code (130) instead of rejecting.
78
+ * GUI-launch commands pass false: their timeout means "launched, still
79
+ * running", and killing would terminate the app the user asked for.
80
+ */
81
+ exec(id: string, command: string, timeoutMs?: number, killOnTimeout?: boolean): Promise<TerminalExecResult>;
82
+ private defaultShell;
83
+ }