@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,2 @@
1
+ export declare function redactInsightText(value: unknown, limit: number): string;
2
+ export declare function toolNamesOnly(raw: unknown): string[];
@@ -0,0 +1,30 @@
1
+ const SECRET = /(sk-[A-Za-z0-9_\-]{8,}|api[_-]?key\s*[:=]\s*\S+|bearer\s+[A-Za-z0-9._\-]{8,})/gi;
2
+ const WIN_HOME = /[A-Za-z]:\\(?:Users|home)\\[^\\\s]+/gi;
3
+ const UNIX_HOME = /\/(?:Users|home)\/[^/\s]+/g;
4
+ export function redactInsightText(value, limit) {
5
+ if (typeof value !== 'string' || !value)
6
+ return '';
7
+ let text = value.replace(/\u0000/g, '');
8
+ text = text.replace(SECRET, '[redacted]');
9
+ text = text.replace(WIN_HOME, 'C:\\Users\\[redacted]');
10
+ text = text.replace(UNIX_HOME, '/Users/[redacted]'); // shell-neutral:allow — 脱敏替换目标,非机器路径
11
+ if (text.length > limit)
12
+ return `${text.slice(0, limit)}…`;
13
+ return text;
14
+ }
15
+ export function toolNamesOnly(raw) {
16
+ if (!Array.isArray(raw))
17
+ return [];
18
+ const names = [];
19
+ for (const item of raw.slice(0, 40)) {
20
+ if (typeof item === 'string' && item.trim())
21
+ names.push(item.trim().slice(0, 64));
22
+ else if (item && typeof item === 'object') {
23
+ const rec = item;
24
+ const name = rec.name ?? rec.tool;
25
+ if (typeof name === 'string' && name.trim())
26
+ names.push(name.trim().slice(0, 64));
27
+ }
28
+ }
29
+ return names;
30
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Product-insight consent + install identity. Pure logic (no sqlite/electron)
3
+ * so vitest can cover the three separate confirmation flags.
4
+ *
5
+ * - shareBehavior: 行为事件可否自动上传
6
+ * - shareConversation: 问答可否自动上传
7
+ * - shareProfile: 用户资料可否自动上传
8
+ * 三者默认全关。关掉的种类仍写入本地 outbox,用户可导出后手动发给开发者。
9
+ *
10
+ * 遥测端点(3.1 起产品注入):优先级 用户设置 apiBase > 环境变量
11
+ * DEEPPATH_INSIGHTS_API_BASE > 产品注入(product.json insightsApiBase)。
12
+ * 三者皆空 = 无上报端点(中性 shell 默认),flush 直接跳过。
13
+ */
14
+ export interface InsightsProfile {
15
+ displayName: string;
16
+ email: string;
17
+ company: string;
18
+ note: string;
19
+ }
20
+ export interface InsightsSettings {
21
+ installId: string;
22
+ shareBehavior: boolean;
23
+ shareConversation: boolean;
24
+ shareProfile: boolean;
25
+ /** ISO time of first consent prompt answer; empty = not prompted yet. */
26
+ promptedAt?: string;
27
+ /** Optional override. Empty = env DEEPPATH_INSIGHTS_API_BASE or 产品注入端点。 */
28
+ apiBase?: string;
29
+ profile: InsightsProfile;
30
+ }
31
+ export declare const DEFAULT_INSIGHTS_PROFILE: InsightsProfile;
32
+ export declare function isUuid(value: unknown): value is string;
33
+ export declare function normalizeInsightsApiBase(raw: unknown): string | undefined;
34
+ /**
35
+ * 解析遥测端点:用户设置 > 环境变量 > 产品注入。皆空返回 ''(调用方
36
+ * 视为空端点 = 遥测未配置,跳过上报)。
37
+ */
38
+ export declare function resolveInsightsApiBase(settings: InsightsSettings | null | undefined, envBase?: string | null): string;
39
+ export declare function mergeInsightsProfile(raw?: Partial<InsightsProfile> | null): InsightsProfile;
40
+ export declare function mergeInsightsSettings(settings?: InsightsSettingsPatch | null, generateId?: () => string): InsightsSettings;
41
+ export declare function insightsNeedsPrompt(settings: InsightsSettings | null | undefined): boolean;
42
+ export type InsightKind = 'event' | 'turn' | 'profile';
43
+ /** PATCH-style input: flags optional, profile fields individually optional. */
44
+ export type InsightsSettingsPatch = Partial<Omit<InsightsSettings, 'profile'>> & {
45
+ profile?: Partial<InsightsProfile>;
46
+ };
47
+ export declare function canAutoUpload(settings: InsightsSettings, kind: InsightKind): boolean;
48
+ export declare function rowsEligibleForAutoUpload<T extends {
49
+ kind: InsightKind;
50
+ }>(settings: InsightsSettings, rows: T[]): {
51
+ upload: T[];
52
+ skipped: number;
53
+ };
@@ -0,0 +1,92 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { getProductConfig } from '../product-config.js';
3
+ export const DEFAULT_INSIGHTS_PROFILE = {
4
+ displayName: '',
5
+ email: '',
6
+ company: '',
7
+ note: '',
8
+ };
9
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
+ export function isUuid(value) {
11
+ return typeof value === 'string' && UUID_RE.test(value.trim());
12
+ }
13
+ export function normalizeInsightsApiBase(raw) {
14
+ if (typeof raw !== 'string')
15
+ return undefined;
16
+ const trimmed = raw.trim().replace(/\/+$/, '');
17
+ if (!trimmed)
18
+ return undefined;
19
+ try {
20
+ const url = new URL(trimmed);
21
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
22
+ return undefined;
23
+ return trimmed;
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ /**
30
+ * 解析遥测端点:用户设置 > 环境变量 > 产品注入。皆空返回 ''(调用方
31
+ * 视为空端点 = 遥测未配置,跳过上报)。
32
+ */
33
+ export function resolveInsightsApiBase(settings, envBase) {
34
+ return (normalizeInsightsApiBase(settings?.apiBase) ||
35
+ normalizeInsightsApiBase(envBase) ||
36
+ normalizeInsightsApiBase(getProductConfig().insightsApiBase) ||
37
+ '');
38
+ }
39
+ function asBool(value) {
40
+ return value === true;
41
+ }
42
+ function asTrimmed(value, max) {
43
+ if (typeof value !== 'string')
44
+ return '';
45
+ return value.trim().slice(0, max);
46
+ }
47
+ export function mergeInsightsProfile(raw) {
48
+ const incoming = raw ?? {};
49
+ return {
50
+ displayName: asTrimmed(incoming.displayName, 128),
51
+ email: asTrimmed(incoming.email, 191),
52
+ company: asTrimmed(incoming.company, 191),
53
+ note: asTrimmed(incoming.note, 500),
54
+ };
55
+ }
56
+ export function mergeInsightsSettings(settings, generateId = () => randomUUID()) {
57
+ const incoming = settings ?? {};
58
+ const installId = isUuid(incoming.installId) ? incoming.installId.trim().toLowerCase() : generateId();
59
+ const promptedAt = typeof incoming.promptedAt === 'string' && incoming.promptedAt.trim()
60
+ ? incoming.promptedAt.trim()
61
+ : undefined;
62
+ return {
63
+ installId,
64
+ shareBehavior: asBool(incoming.shareBehavior),
65
+ shareConversation: asBool(incoming.shareConversation),
66
+ shareProfile: asBool(incoming.shareProfile),
67
+ promptedAt,
68
+ apiBase: normalizeInsightsApiBase(incoming.apiBase),
69
+ profile: mergeInsightsProfile(incoming.profile),
70
+ };
71
+ }
72
+ export function insightsNeedsPrompt(settings) {
73
+ return !settings?.promptedAt;
74
+ }
75
+ export function canAutoUpload(settings, kind) {
76
+ if (kind === 'event')
77
+ return settings.shareBehavior;
78
+ if (kind === 'turn')
79
+ return settings.shareConversation;
80
+ return settings.shareProfile;
81
+ }
82
+ export function rowsEligibleForAutoUpload(settings, rows) {
83
+ const upload = [];
84
+ let skipped = 0;
85
+ for (const row of rows) {
86
+ if (canAutoUpload(settings, row.kind))
87
+ upload.push(row);
88
+ else
89
+ skipped += 1;
90
+ }
91
+ return { upload, skipped };
92
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Pure LLM-settings defaults/migration logic, split out of `storage/index.ts`
3
+ * so it can be unit-tested without touching `better-sqlite3` / `electron`
4
+ * (importing `storage/index.ts` eagerly constructs a `LocalStore`, which
5
+ * calls `app.getPath()` and loads the Electron-ABI-compiled native module —
6
+ * neither works under plain Node/vitest).
7
+ */
8
+ /**
9
+ * OpenAI 兼容旗标的宿主覆盖(wire 形状与框架 compat.py 的
10
+ * `OpenAICompatFlags.from_dict` 对齐——camelCase 键、未知键在框架侧
11
+ * fail loud)。字段全部可选:缺省 = 不覆盖,由框架按 baseUrl 主机名
12
+ * 自动探测(PROVIDER_COMPAT_HOSTS)兜底。旗标词汇表由 sidecar 的
13
+ * `compat.describe` RPC 服务化,设置页按它渲染,不在前端硬编码键名。
14
+ */
15
+ export interface OpenAICompatOverrides {
16
+ supportsUsageInStreaming?: boolean;
17
+ maxTokensField?: 'max_tokens' | 'max_completion_tokens';
18
+ supportsReasoningEffort?: boolean;
19
+ supportsTemperature?: boolean;
20
+ reasoningDeltaFields?: string[];
21
+ reasoningEchoField?: string;
22
+ supportsForcedToolChoice?: boolean;
23
+ /**
24
+ * thinking 模式下带 tool_calls 的 assistant 消息即使没产出思考,也必须
25
+ * 回传 reasoning 字段(空串)——DeepSeek 缺键即 400;框架按 baseUrl
26
+ * 自动为 api.deepseek.com 打开,这里只是允许设置页显式覆盖。
27
+ */
28
+ echoEmptyReasoningForToolCalls?: boolean;
29
+ cachedTokensFields?: string[];
30
+ }
31
+ /**
32
+ * 厂商参数预制(框架 llm.presets)的宿主选择。缺省 / `{enabled: true}` =
33
+ * 自动:框架按 baseUrl 主机名 + 模型名匹配注册表,填充厂商文档最优的
34
+ * temperature/top_p/top_k 等(显式请求字段永远优先);`enabled: false` =
35
+ * 整层关闭;`override` = 钉死一条显式预制(设置页从 sidecar 的
36
+ * `presets.describe` 选条目)。wire 形状与框架 `ProviderPreset.from_dict`
37
+ * 对齐——camelCase 键、未知键在框架侧 fail loud。
38
+ */
39
+ export interface ProviderPresetOverride {
40
+ temperature?: number;
41
+ topP?: number;
42
+ maxTokens?: number;
43
+ reasoningEffort?: string;
44
+ extraBody?: Record<string, unknown>;
45
+ }
46
+ export interface ProviderPresetsChoice {
47
+ enabled?: boolean;
48
+ override?: ProviderPresetOverride;
49
+ }
50
+ export type LlmProvider = 'ollama' | 'openai-compat' | 'anthropic' | 'google' | 'openai-responses';
51
+ export interface LlmSettings {
52
+ provider: LlmProvider;
53
+ /**
54
+ * 设置页选中的服务商目录 id(deepseek / anthropic / custom 等)。
55
+ * 缺省时由 baseUrl 主机名回推。只影响表单回显,不进 sidecar wire。
56
+ */
57
+ vendorId?: string;
58
+ model: string;
59
+ baseUrl?: string;
60
+ apiKey?: string;
61
+ /** 缺省 = 自动:命中厂商预制时用预制值,否则不下发(厂商服务端默认)。 */
62
+ temperature?: number;
63
+ systemPrompt?: string;
64
+ maxTotalTokens?: number;
65
+ /** 见 OpenAICompatOverrides;仅 openai-compat provider 生效。 */
66
+ compat?: OpenAICompatOverrides;
67
+ /** 见 ProviderPresetsChoice;仅 openai-compat / openai-responses 生效。 */
68
+ presets?: ProviderPresetsChoice;
69
+ /**
70
+ * 本地命令(local_exec_shell)的默认超时(秒)。不设 = 用内置默认
71
+ * (headless 30s / 可见终端 60s)。带 "gui" 的启动命令不受它限制——
72
+ * 超时后按"已启动、仍在运行"处理,见 local-executor.isGuiLaunchCommand。
73
+ */
74
+ execTimeoutSeconds?: number;
75
+ }
76
+ export declare const DEFAULT_SYSTEM_PROMPT: string;
77
+ export declare const OPENAI_COMPAT_DEFAULTS: LlmSettings;
78
+ export declare const EXPIRED_BAKED_API_KEY = "sk-b1383d225ee44f118ad99e359ec9e5d8";
79
+ /** 已存设置是否还挂着作废的出厂内置 key。 */
80
+ export declare function llmSettingsCarryExpiredBakedKey(s: LlmSettings): boolean;
81
+ export declare const OLLAMA_DEFAULTS: LlmSettings;
82
+ export declare function sanitizeLlmProvider(raw: unknown): LlmProvider;
83
+ export declare function sanitizeVendorId(raw: unknown): string | undefined;
84
+ /** Sidecar `agent.chat.stream` 的 provider 字段。 */
85
+ export declare function sidecarWireProvider(provider: LlmProvider): string;
86
+ export declare function usesOpenAiCompatExtras(provider: LlmProvider): boolean;
87
+ /** Ollama OpenAI 兼容面在 /v1;本机设置存的是 daemon 根路径。 */
88
+ export declare function listingBaseUrl(provider: LlmProvider, baseUrl: string | undefined): string | undefined;
89
+ /** The vendor listing URL sidecar will GET (must match gateway_catalog.listing_request). */
90
+ export declare function vendorModelsListUrl(provider: LlmProvider, baseUrl: string): string;
91
+ export declare const DEFAULT_LLM_SETTINGS: LlmSettings;
92
+ export declare function llmSettingsMatchOllamaDefaults(s: LlmSettings): boolean;
93
+ /**
94
+ * Merge a (possibly partial-looking, but typed as complete) incoming
95
+ * `LlmSettings` against the preset for *its own* `provider`, not always
96
+ * against the OpenAI-compat/DeepSeek preset.
97
+ *
98
+ * Previously `setLlmSettings` always merged against
99
+ * `{...DEFAULT_LLM_SETTINGS, ...settings}` (`DEFAULT_LLM_SETTINGS` ==
100
+ * `OPENAI_COMPAT_DEFAULTS`). Switching the provider to Ollama without
101
+ * explicitly clearing `apiKey` left the baked-in DeepSeek key sitting in the
102
+ * merged Ollama settings, since a key that's simply absent from `settings`
103
+ * doesn't shadow the spread base — it silently falls through to whatever
104
+ * `apiKey` the base object happens to carry.
105
+ */
106
+ export declare function mergeLlmSettings(settings: LlmSettings): LlmSettings;
107
+ /**
108
+ * 把设置页/POST 体里的 compat 载荷收敛成合法的 `OpenAICompatOverrides`:
109
+ * 只保留框架 `from_dict` 认识的键、按旗标种类校正类型,空数组与非法值
110
+ * 丢弃(缺省 = 不覆盖)。框架侧对未知键 fail loud,这里先收敛一遍,
111
+ * 让设置页的旧版本载荷不会因为框架新增旗标而报错。
112
+ */
113
+ export declare function sanitizeCompatOverrides(input: unknown): OpenAICompatOverrides | undefined;
114
+ /**
115
+ * 把设置页/POST 体里的 presets 载荷收敛成合法的 `ProviderPresetsChoice`:
116
+ * 与 sanitizeCompatOverrides 同一纪律——只保留框架 `ProviderPreset.from_dict`
117
+ * 认识的键、按类型校正,非法值丢弃(缺省 = 自动匹配)。框架侧对未知键
118
+ * fail loud,这里先收敛一遍。
119
+ */
120
+ export declare function sanitizePresetsChoice(input: unknown): ProviderPresetsChoice | undefined;
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Pure LLM-settings defaults/migration logic, split out of `storage/index.ts`
3
+ * so it can be unit-tested without touching `better-sqlite3` / `electron`
4
+ * (importing `storage/index.ts` eagerly constructs a `LocalStore`, which
5
+ * calls `app.getPath()` and loads the Electron-ABI-compiled native module —
6
+ * neither works under plain Node/vitest).
7
+ */
8
+ import { getBrand } from '../brand.js';
9
+ export const DEFAULT_SYSTEM_PROMPT = `你是 ${getBrand().agentName},${getBrand().tagline},优先给出可执行建议并在需要时调用本地工具。`;
10
+ // ─── Per-provider 预设 ──────────────────────────────────────────────────────
11
+ // 每套预设描述"用户切到该 provider 时一组合理的开箱即用值"。两套预设地位
12
+ // 对等——挑哪个当 first-install 默认由下面 DEFAULT_LLM_SETTINGS 显式声明。
13
+ //
14
+ // 前端设置页的服务商列表由 sidecar catalog.describe 提供;这里只保留
15
+ // 切 provider 时的连接缺省。
16
+ // OpenAI 兼容预设:指向 DeepSeek 官方 API。API Key 默认留空——不再打包
17
+ // 内置 key:打包进客户端 = 任何拿到 .exe 的人都能解出来,且 key 过期后
18
+ // 新用户开箱即 401(0.0.35 事故)。新用户由设置页引导填自己的 key。
19
+ export const OPENAI_COMPAT_DEFAULTS = {
20
+ provider: 'openai-compat',
21
+ vendorId: 'deepseek',
22
+ model: 'deepseek-chat',
23
+ baseUrl: 'https://api.deepseek.com',
24
+ // temperature 缺省 = 自动:命中厂商预制(llm.presets)时用厂商文档最优
25
+ // 值,未命中则不下发。设置页可切手动。
26
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
27
+ maxTotalTokens: 60000,
28
+ };
29
+ // 0.0.35 及之前打包进客户端的内置 DeepSeek key(已作废,上游返回 401)。
30
+ // 保留常量仅作迁移指纹:老安装的已存设置若还带着它,升级后静默清空,
31
+ // 否则用户永远拿着一把死 key,报 401 也不知道为什么。
32
+ export const EXPIRED_BAKED_API_KEY = 'sk-b1383d225ee44f118ad99e359ec9e5d8';
33
+ /** 已存设置是否还挂着作废的出厂内置 key。 */
34
+ export function llmSettingsCarryExpiredBakedKey(s) {
35
+ return !!s.apiKey && s.apiKey === EXPIRED_BAKED_API_KEY;
36
+ }
37
+ // Ollama 预设:用户切到 Ollama provider 时的默认连接参数(指向本机默认端口
38
+ // + 常用的 llama3.1:8b 镜像)。同时也充当"判断老安装是否还停留在出厂 Ollama
39
+ // 配置"的迁移指纹——见 llmSettingsMatchOllamaDefaults。
40
+ export const OLLAMA_DEFAULTS = {
41
+ provider: 'ollama',
42
+ vendorId: 'ollama',
43
+ model: 'llama3.1:8b',
44
+ baseUrl: 'http://127.0.0.1:11434',
45
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
46
+ maxTotalTokens: 60000,
47
+ };
48
+ const BARE_PROVIDER_DEFAULTS = {
49
+ provider: 'openai-compat',
50
+ model: '',
51
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
52
+ maxTotalTokens: 60000,
53
+ };
54
+ export function sanitizeLlmProvider(raw) {
55
+ if (raw === 'ollama' ||
56
+ raw === 'openai-compat' ||
57
+ raw === 'anthropic' ||
58
+ raw === 'google' ||
59
+ raw === 'openai-responses') {
60
+ return raw;
61
+ }
62
+ return 'openai-compat';
63
+ }
64
+ export function sanitizeVendorId(raw) {
65
+ if (typeof raw !== 'string')
66
+ return undefined;
67
+ const id = raw.trim();
68
+ return id || undefined;
69
+ }
70
+ /** Sidecar `agent.chat.stream` 的 provider 字段。 */
71
+ export function sidecarWireProvider(provider) {
72
+ switch (provider) {
73
+ case 'ollama':
74
+ return 'ollama';
75
+ case 'anthropic':
76
+ return 'anthropic';
77
+ case 'google':
78
+ return 'google';
79
+ case 'openai-responses':
80
+ return 'openai-responses';
81
+ default:
82
+ return 'openai_compat';
83
+ }
84
+ }
85
+ export function usesOpenAiCompatExtras(provider) {
86
+ return provider === 'openai-compat' || provider === 'openai-responses';
87
+ }
88
+ /** Ollama OpenAI 兼容面在 /v1;本机设置存的是 daemon 根路径。 */
89
+ export function listingBaseUrl(provider, baseUrl) {
90
+ if (!baseUrl)
91
+ return undefined;
92
+ const trimmed = baseUrl.replace(/\/+$/, '');
93
+ if (provider === 'ollama' && !trimmed.endsWith('/v1')) {
94
+ return `${trimmed}/v1`;
95
+ }
96
+ return trimmed;
97
+ }
98
+ /** The vendor listing URL sidecar will GET (must match gateway_catalog.listing_request). */
99
+ export function vendorModelsListUrl(provider, baseUrl) {
100
+ const base = listingBaseUrl(provider, baseUrl) ?? baseUrl.replace(/\/+$/, '');
101
+ if (provider === 'anthropic') {
102
+ return base.endsWith('/v1') ? `${base}/models` : `${base}/v1/models`;
103
+ }
104
+ if (provider === 'google') {
105
+ if (base.endsWith('/models'))
106
+ return base;
107
+ if (base.includes('/v1beta'))
108
+ return `${base}/models`;
109
+ return `${base}/v1beta/models`;
110
+ }
111
+ return `${base}/models`;
112
+ }
113
+ // 全新安装的出厂默认。当前选 OpenAI 兼容(DeepSeek)—— Windows 用户大多没装
114
+ // 过 Ollama,开箱用云端 API 失败率最低。
115
+ export const DEFAULT_LLM_SETTINGS = OPENAI_COMPAT_DEFAULTS;
116
+ // 用作迁移指纹:老安装用户停留在出厂 Ollama 默认(从未改过任何一项),新
117
+ // 版本启动时静默迁移到新默认。任意一项被改过就视为"用户已自定义",不动。
118
+ //
119
+ // 之前漏了 maxTotalTokens:只要 provider/model/baseUrl/apiKey/temperature/
120
+ // systemPrompt 都命中出厂值,哪怕用户特意把 maxTotalTokens 调大/调小过,也
121
+ // 会被这次静默迁移覆盖回默认值——这里补齐,让指纹覆盖 LlmSettings 的全部字段。
122
+ export function llmSettingsMatchOllamaDefaults(s) {
123
+ return (s.provider === OLLAMA_DEFAULTS.provider &&
124
+ s.model === OLLAMA_DEFAULTS.model &&
125
+ s.baseUrl === OLLAMA_DEFAULTS.baseUrl &&
126
+ !s.apiKey &&
127
+ // 0.3 是出厂默认改为"自动"之前的遗留值:当时保存过的安装会把 0.3
128
+ // 持久化下来,指纹必须继续认它,否则老用户丢失静默迁移。
129
+ (s.temperature === undefined || s.temperature === null || s.temperature === 0.3) &&
130
+ (s.systemPrompt === OLLAMA_DEFAULTS.systemPrompt || !s.systemPrompt) &&
131
+ (s.maxTotalTokens === OLLAMA_DEFAULTS.maxTotalTokens ||
132
+ s.maxTotalTokens === undefined ||
133
+ s.maxTotalTokens === null));
134
+ }
135
+ /**
136
+ * Merge a (possibly partial-looking, but typed as complete) incoming
137
+ * `LlmSettings` against the preset for *its own* `provider`, not always
138
+ * against the OpenAI-compat/DeepSeek preset.
139
+ *
140
+ * Previously `setLlmSettings` always merged against
141
+ * `{...DEFAULT_LLM_SETTINGS, ...settings}` (`DEFAULT_LLM_SETTINGS` ==
142
+ * `OPENAI_COMPAT_DEFAULTS`). Switching the provider to Ollama without
143
+ * explicitly clearing `apiKey` left the baked-in DeepSeek key sitting in the
144
+ * merged Ollama settings, since a key that's simply absent from `settings`
145
+ * doesn't shadow the spread base — it silently falls through to whatever
146
+ * `apiKey` the base object happens to carry.
147
+ */
148
+ export function mergeLlmSettings(settings) {
149
+ const base = settings.provider === 'ollama'
150
+ ? OLLAMA_DEFAULTS
151
+ : settings.provider === 'openai-compat'
152
+ ? OPENAI_COMPAT_DEFAULTS
153
+ : { ...BARE_PROVIDER_DEFAULTS, provider: settings.provider };
154
+ const merged = { ...base, ...settings };
155
+ if (!settings.vendorId)
156
+ merged.vendorId = base.vendorId;
157
+ return merged;
158
+ }
159
+ const COMPAT_BOOL_KEYS = [
160
+ 'supportsUsageInStreaming',
161
+ 'supportsReasoningEffort',
162
+ 'supportsTemperature',
163
+ 'supportsForcedToolChoice',
164
+ 'echoEmptyReasoningForToolCalls',
165
+ ];
166
+ const COMPAT_LIST_KEYS = ['reasoningDeltaFields', 'cachedTokensFields'];
167
+ const COMPAT_STRING_KEYS = ['reasoningEchoField'];
168
+ /**
169
+ * 把设置页/POST 体里的 compat 载荷收敛成合法的 `OpenAICompatOverrides`:
170
+ * 只保留框架 `from_dict` 认识的键、按旗标种类校正类型,空数组与非法值
171
+ * 丢弃(缺省 = 不覆盖)。框架侧对未知键 fail loud,这里先收敛一遍,
172
+ * 让设置页的旧版本载荷不会因为框架新增旗标而报错。
173
+ */
174
+ export function sanitizeCompatOverrides(input) {
175
+ if (!input || typeof input !== 'object' || Array.isArray(input))
176
+ return undefined;
177
+ const record = input;
178
+ const out = {};
179
+ for (const key of COMPAT_BOOL_KEYS) {
180
+ if (typeof record[key] === 'boolean')
181
+ out[key] = record[key];
182
+ }
183
+ if (record.maxTokensField === 'max_tokens' || record.maxTokensField === 'max_completion_tokens') {
184
+ out.maxTokensField = record.maxTokensField;
185
+ }
186
+ for (const key of COMPAT_LIST_KEYS) {
187
+ const value = record[key];
188
+ if (Array.isArray(value)) {
189
+ const list = value.map((v) => String(v).trim()).filter(Boolean);
190
+ if (list.length > 0)
191
+ out[key] = list;
192
+ }
193
+ }
194
+ for (const key of COMPAT_STRING_KEYS) {
195
+ const value = record[key];
196
+ if (typeof value === 'string' && value.trim())
197
+ out[key] = value.trim();
198
+ }
199
+ return Object.keys(out).length > 0 ? out : undefined;
200
+ }
201
+ /**
202
+ * 把设置页/POST 体里的 presets 载荷收敛成合法的 `ProviderPresetsChoice`:
203
+ * 与 sanitizeCompatOverrides 同一纪律——只保留框架 `ProviderPreset.from_dict`
204
+ * 认识的键、按类型校正,非法值丢弃(缺省 = 自动匹配)。框架侧对未知键
205
+ * fail loud,这里先收敛一遍。
206
+ */
207
+ export function sanitizePresetsChoice(input) {
208
+ if (!input || typeof input !== 'object' || Array.isArray(input))
209
+ return undefined;
210
+ const record = input;
211
+ const out = {};
212
+ if (typeof record.enabled === 'boolean')
213
+ out.enabled = record.enabled;
214
+ if (record.override && typeof record.override === 'object' && !Array.isArray(record.override)) {
215
+ const raw = record.override;
216
+ const override = {};
217
+ if (typeof raw.temperature === 'number')
218
+ override.temperature = raw.temperature;
219
+ if (typeof raw.topP === 'number')
220
+ override.topP = raw.topP;
221
+ if (typeof raw.maxTokens === 'number')
222
+ override.maxTokens = raw.maxTokens;
223
+ if (typeof raw.reasoningEffort === 'string' && raw.reasoningEffort) {
224
+ override.reasoningEffort = raw.reasoningEffort;
225
+ }
226
+ if (raw.extraBody && typeof raw.extraBody === 'object' && !Array.isArray(raw.extraBody)) {
227
+ override.extraBody = raw.extraBody;
228
+ }
229
+ if (Object.keys(override).length > 0)
230
+ out.override = override;
231
+ }
232
+ return Object.keys(out).length > 0 ? out : undefined;
233
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The `localStore` singleton's friendly handling of a held write lease.
3
+ *
4
+ * `createLocalStore` turns a `StoreAlreadyOwnedError` into a readable dialog
5
+ * + exit instead of a raw stack. The single-instance lock (single-instance.ts)
6
+ * already stops a second OS process earlier; this is the deeper kernel-lock
7
+ * backstop for the cases it cannot reach (a bypassed lock, two userData dirs
8
+ * pointed at one database, a crashed-then-relaunched app racing its own
9
+ * cleanup).
10
+ *
11
+ * This lives in its own module — and takes an injectable constructor — so the
12
+ * error-mapping path is testable in a plain-node vitest worker. Importing
13
+ * `storage/index.ts` constructs the real `LocalStore`, which loads
14
+ * better-sqlite3 (Electron ABI) and cannot resolve outside Electron; this
15
+ * module imports only the error class, so it loads anywhere. `dialog`/`app`
16
+ * are loaded lazily for the same reason.
17
+ */
18
+ /** The slice of electron's app/dialog the friendly exit uses. */
19
+ interface ElectronExit {
20
+ app: {
21
+ exit(code: number): void;
22
+ };
23
+ dialog: {
24
+ showErrorBox(title: string, message: string): void;
25
+ };
26
+ }
27
+ export declare function createLocalStore<T>(construct: () => T, loadElectron?: () => Promise<ElectronExit>, exitProcess?: (code: number) => void): T;
28
+ export {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The `localStore` singleton's friendly handling of a held write lease.
3
+ *
4
+ * `createLocalStore` turns a `StoreAlreadyOwnedError` into a readable dialog
5
+ * + exit instead of a raw stack. The single-instance lock (single-instance.ts)
6
+ * already stops a second OS process earlier; this is the deeper kernel-lock
7
+ * backstop for the cases it cannot reach (a bypassed lock, two userData dirs
8
+ * pointed at one database, a crashed-then-relaunched app racing its own
9
+ * cleanup).
10
+ *
11
+ * This lives in its own module — and takes an injectable constructor — so the
12
+ * error-mapping path is testable in a plain-node vitest worker. Importing
13
+ * `storage/index.ts` constructs the real `LocalStore`, which loads
14
+ * better-sqlite3 (Electron ABI) and cannot resolve outside Electron; this
15
+ * module imports only the error class, so it loads anywhere. `dialog`/`app`
16
+ * are loaded lazily for the same reason.
17
+ */
18
+ import { StoreAlreadyOwnedError } from './write-lease.js';
19
+ export function createLocalStore(construct, loadElectron = () => import('electron'), exitProcess = (code) => process.exit(code)) {
20
+ try {
21
+ return construct();
22
+ }
23
+ catch (err) {
24
+ if (err instanceof StoreAlreadyOwnedError) {
25
+ void loadElectron()
26
+ .then(({ app, dialog }) => {
27
+ dialog.showErrorBox('无法启动', '另一个实例正在运行并占用本地数据库。请先关闭该实例,再重新打开应用。');
28
+ app.exit(1);
29
+ })
30
+ .catch(() => exitProcess(1));
31
+ // Unreachable once electron resolves (app.exit ends the process); the
32
+ // rethrow is the non-electron fallback so a plain-node import still
33
+ // fails loud rather than returning a half-built store.
34
+ throw err;
35
+ }
36
+ throw err;
37
+ }
38
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The single ordering key for a chat's messages, shared by the read and the
3
+ * regenerate cut so "after" means the same thing to both.
4
+ *
5
+ * Every writer stores `new Date(...).toISOString()`, whose fixed-width UTC form
6
+ * sorts lexicographically exactly as it sorts chronologically, so the raw text
7
+ * is the key. Wrapping it in SQLite's `datetime()` truncates to whole seconds
8
+ * and collapses sub-second writes into a tie group with no defined order;
9
+ * `rowid` then settles exact-timestamp ties toward insertion order.
10
+ *
11
+ * Sub-second writes are normal here, not an edge case: a mid-turn steer lands
12
+ * milliseconds after the message it interrupts, and `replaceChatMessages`
13
+ * synthesizes a whole branch projection at `base + i` milliseconds.
14
+ */
15
+ export declare const MESSAGE_ORDER_DESC = "created_at DESC, rowid DESC";
16
+ /** Ascending form of {@link MESSAGE_ORDER_DESC}, for transcript order. */
17
+ export declare const MESSAGE_ORDER_ASC = "created_at ASC, rowid ASC";
18
+ /**
19
+ * Predicate selecting the message bound to `?2` in chat `?3` plus everything
20
+ * ordered after it under {@link MESSAGE_ORDER_DESC}. An unknown id makes the
21
+ * subquery NULL, so nothing matches and no rows are touched.
22
+ *
23
+ * Placeholders are positional: the caller binds (chatId, messageId, chatId).
24
+ */
25
+ export declare const MESSAGE_CUT_FROM_ID = "(created_at, rowid) >= (\n SELECT created_at, rowid FROM chat_messages WHERE id = ? AND chat_id = ?\n )";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The single ordering key for a chat's messages, shared by the read and the
3
+ * regenerate cut so "after" means the same thing to both.
4
+ *
5
+ * Every writer stores `new Date(...).toISOString()`, whose fixed-width UTC form
6
+ * sorts lexicographically exactly as it sorts chronologically, so the raw text
7
+ * is the key. Wrapping it in SQLite's `datetime()` truncates to whole seconds
8
+ * and collapses sub-second writes into a tie group with no defined order;
9
+ * `rowid` then settles exact-timestamp ties toward insertion order.
10
+ *
11
+ * Sub-second writes are normal here, not an edge case: a mid-turn steer lands
12
+ * milliseconds after the message it interrupts, and `replaceChatMessages`
13
+ * synthesizes a whole branch projection at `base + i` milliseconds.
14
+ */
15
+ export const MESSAGE_ORDER_DESC = 'created_at DESC, rowid DESC';
16
+ /** Ascending form of {@link MESSAGE_ORDER_DESC}, for transcript order. */
17
+ export const MESSAGE_ORDER_ASC = 'created_at ASC, rowid ASC';
18
+ /**
19
+ * Predicate selecting the message bound to `?2` in chat `?3` plus everything
20
+ * ordered after it under {@link MESSAGE_ORDER_DESC}. An unknown id makes the
21
+ * subquery NULL, so nothing matches and no rows are touched.
22
+ *
23
+ * Placeholders are positional: the caller binds (chatId, messageId, chatId).
24
+ */
25
+ export const MESSAGE_CUT_FROM_ID = `(created_at, rowid) >= (
26
+ SELECT created_at, rowid FROM chat_messages WHERE id = ? AND chat_id = ?
27
+ )`;