@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,151 @@
1
+ /**
2
+ * 可见 PTY 执行路由(Codex 式"看 agent 打字"):shell 命令优先打进共享的
3
+ * 可见终端,多行/heredoc 等 sentinel 抓不住的形态回退 headless。
4
+ * Electron main 与 BS server 共用;`terminal:reveal` 等事件经注入的
5
+ * broadcast 出口(Electron 窗口广播 / BS SSE 总线)。
6
+ */
7
+ import log from 'electron-log';
8
+ import { GUI_LAUNCH_WAIT_MS, getConfiguredExecTimeoutMs, isGuiLaunchCommand, rewriteExeCommandIfNeeded, } from '../local-executor.js';
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 function mapSessionShell(shell) {
15
+ const s = (shell || '').toLowerCase();
16
+ if (s.includes('powershell') || s.includes('pwsh'))
17
+ return 'powershell';
18
+ if (s.includes('cmd'))
19
+ return 'cmd';
20
+ if (s.includes('wsl'))
21
+ return 'wsl';
22
+ if (s.includes('bash'))
23
+ return 'bash';
24
+ if (s.includes('zsh'))
25
+ return 'zsh';
26
+ return process.platform === 'win32' ? 'powershell' : 'zsh';
27
+ }
28
+ /**
29
+ * Decide whether to route a shell command through the visible PTY instead of
30
+ * the headless local-executor. Returns null to fall back headless. No renderer
31
+ * needs to be attached — the exec sentinel capture runs entirely in this
32
+ * process.
33
+ */
34
+ export function createVisibleTerminalExec(deps) {
35
+ const { localExecutor, terminalManager, broadcast } = deps;
36
+ return async function maybeExecInTerminal(req) {
37
+ if (req.command) {
38
+ req.command = rewriteExeCommandIfNeeded(req.command);
39
+ }
40
+ // Multi-line / heredoc commands break the sentinel pattern; fall back to
41
+ // headless local-executor for those.
42
+ if (req.command && req.command.includes('\n')) {
43
+ log.info('[terminal-exec] skip multiline command, fallback headless');
44
+ return null;
45
+ }
46
+ // The visible-terminal path must run the same dangerous-command check as
47
+ // headless `executeShell` — both paths enforce the same safety policy.
48
+ if (req.command) {
49
+ const dangerousReason = localExecutor.detectDangerousCommand(req.command);
50
+ if (dangerousReason) {
51
+ log.warn('[terminal-exec] blocked dangerous command', { pattern: dangerousReason });
52
+ return {
53
+ success: false,
54
+ error: `Blocked dangerous command: ${dangerousReason}`,
55
+ platform: process.platform,
56
+ };
57
+ }
58
+ }
59
+ // GUI/交互式命令(命令行带 "gui" token——场景应用启动图形图件的
60
+ // 命令行约定):没显式 timeout 时只等一小段,到点按"已启动"处理
61
+ // (见下方 catch)。
62
+ const guiCommand = isGuiLaunchCommand(req.command || '');
63
+ let timeoutMs = req.timeout;
64
+ if (timeoutMs !== undefined) {
65
+ if (timeoutMs > 0 && timeoutMs < 1000) {
66
+ timeoutMs *= 1000;
67
+ }
68
+ }
69
+ else if (guiCommand) {
70
+ timeoutMs = GUI_LAUNCH_WAIT_MS;
71
+ }
72
+ else {
73
+ timeoutMs = getConfiguredExecTimeoutMs() ?? undefined;
74
+ }
75
+ const session = terminalManager.ensurePrimary({ cwd: req.cwd });
76
+ // Codex 式面板自动展开:命令进入可见 PTY 时通知 renderer 打开内嵌终端
77
+ // 面板。幂等——面板已打开时 renderer 端是 no-op。
78
+ broadcast('terminal:reveal', { sessionId: session.id });
79
+ const resolvedShell = mapSessionShell(session.shell);
80
+ // 项目模式(ToolRouter 会把项目根塞进 req.cwd):可见 PTY 是共享交互
81
+ // 会话,已有 session 不会随 ensurePrimary 改目录——显式 cd 过去。
82
+ const commandForPty = req.cwd
83
+ ? `cd "${req.cwd.replace(/"/g, '\\"')}" && ${req.command}`
84
+ : req.command;
85
+ log.info('[terminal-exec] start', {
86
+ sessionId: session.id,
87
+ cwd: req.cwd || session.cwd,
88
+ shell: resolvedShell,
89
+ timeout: timeoutMs,
90
+ commandPreview: (req.command || '').slice(0, 200),
91
+ });
92
+ try {
93
+ // killOnTimeout: a timed-out non-GUI command is SIGINTed so it can't jam
94
+ // the shared visible terminal for every subsequent exec (GUI launches
95
+ // keep the "launched, still running" semantics — never killed).
96
+ const r = await terminalManager.exec(session.id, commandForPty, timeoutMs, !guiCommand);
97
+ log.info('[terminal-exec] done', {
98
+ sessionId: session.id,
99
+ success: r.success,
100
+ exitCode: r.exitCode,
101
+ stdoutBytes: (r.stdout || '').length,
102
+ stderrBytes: (r.stderr || '').length,
103
+ durationMs: r.durationMs,
104
+ truncated: r.truncated,
105
+ });
106
+ return {
107
+ success: r.success,
108
+ stdout: r.stdout,
109
+ stderr: r.stderr,
110
+ exitCode: r.exitCode,
111
+ truncated: r.truncated,
112
+ shell: resolvedShell,
113
+ platform: process.platform,
114
+ };
115
+ }
116
+ catch (err) {
117
+ const message = err instanceof Error ? err.message : String(err);
118
+ // 超时绝不能 fallback headless——那会把同一条命令原样再跑一遍
119
+ // (GUI/启动类命令会被重复拉起,正是"Qt 图件被启动多次"的根源)。
120
+ if (message.includes('exec timeout after')) {
121
+ if (guiCommand) {
122
+ log.info('[terminal-exec] gui command still running after wait, treat as launched', {
123
+ commandPreview: (req.command || '').slice(0, 120),
124
+ });
125
+ return {
126
+ success: true,
127
+ timedOut: true,
128
+ stillRunning: true,
129
+ stdout: `[GUI 程序已启动,仍在终端中运行(等待 ${timeoutMs}ms 后未退出,进程未被终止)。` +
130
+ `请在打开的界面中继续操作;不要重新执行同一条启动命令。]`,
131
+ stderr: '',
132
+ shell: mapSessionShell(session.shell),
133
+ platform: process.platform,
134
+ };
135
+ }
136
+ log.warn('[terminal-exec] timed out, NOT falling back (would re-run command)', message);
137
+ return {
138
+ success: false,
139
+ timedOut: true,
140
+ error: `Command timed out after ${timeoutMs}ms in the visible terminal (the process may still be running). ` +
141
+ `DO NOT blindly re-run the same command — if it launched a GUI/long-running program, ` +
142
+ `first check whether it is already running, or re-run with a larger \`timeout\`.`,
143
+ shell: mapSessionShell(session.shell),
144
+ platform: process.platform,
145
+ };
146
+ }
147
+ log.warn('[terminal-exec] failed, fallback headless', message);
148
+ return null;
149
+ }
150
+ };
151
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * OpenAI hosted web search using the existing chat credential.
3
+ *
4
+ * The sidecar must not hold the chat key under credential-broker mode.
5
+ * This runs in the Electron host. Providers without hosted search
6
+ * (GLM, OpenRouter, DeepSeek, Ollama) stay on the Tavily settings key.
7
+ */
8
+ import type { LlmSettings } from './storage/llm-settings.js';
9
+ export declare function executeHostedWebSearch(query: string, maxResults: number, settings: LlmSettings): Promise<{
10
+ success: boolean;
11
+ data?: Record<string, unknown>;
12
+ error?: string;
13
+ }>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * OpenAI hosted web search using the existing chat credential.
3
+ *
4
+ * The sidecar must not hold the chat key under credential-broker mode.
5
+ * This runs in the Electron host. Providers without hosted search
6
+ * (GLM, OpenRouter, DeepSeek, Ollama) stay on the Tavily settings key.
7
+ */
8
+ import { hostedSearchAvailable } from './storage/web-search-settings.js';
9
+ export async function executeHostedWebSearch(query, maxResults, settings) {
10
+ const q = query.trim();
11
+ if (!q) {
12
+ return { success: false, error: 'query is empty' };
13
+ }
14
+ if (!hostedSearchAvailable(settings.baseUrl)) {
15
+ return {
16
+ success: false,
17
+ error: 'this LLM provider has no hosted web search; set a Tavily key in settings',
18
+ };
19
+ }
20
+ if (!settings.apiKey) {
21
+ return { success: false, error: 'hosted web search needs the chat API key' };
22
+ }
23
+ const base = (settings.baseUrl || 'https://api.openai.com').replace(/\/+$/, '');
24
+ const url = `${base}/chat/completions`;
25
+ const cap = Math.min(Math.max(maxResults, 1), 20);
26
+ let response;
27
+ try {
28
+ response = await fetch(url, {
29
+ method: 'POST',
30
+ headers: {
31
+ Authorization: `Bearer ${settings.apiKey}`,
32
+ 'Content-Type': 'application/json',
33
+ },
34
+ body: JSON.stringify({
35
+ model: settings.model,
36
+ messages: [{ role: 'user', content: q }],
37
+ web_search_options: {},
38
+ }),
39
+ });
40
+ }
41
+ catch (err) {
42
+ return {
43
+ success: false,
44
+ error: `hosted web search request failed: ${err instanceof Error ? err.message : String(err)}`,
45
+ };
46
+ }
47
+ if (!response.ok) {
48
+ const text = await response.text().catch(() => '');
49
+ return {
50
+ success: false,
51
+ error: `hosted web search returned HTTP ${response.status}: ${text.slice(0, 200)}` +
52
+ (response.status === 400
53
+ ? ' — this model may not support hosted search; set a Tavily key'
54
+ : ''),
55
+ };
56
+ }
57
+ const payload = (await response.json());
58
+ const message = payload.choices?.[0]?.message;
59
+ const hits = [];
60
+ for (const annotation of message?.annotations ?? []) {
61
+ const cite = annotation.url_citation;
62
+ if (cite?.url) {
63
+ hits.push({
64
+ title: cite.title || '',
65
+ url: cite.url,
66
+ snippet: '',
67
+ });
68
+ }
69
+ if (hits.length >= cap)
70
+ break;
71
+ }
72
+ const snippet = typeof message?.content === 'string' ? message.content : '';
73
+ return {
74
+ success: true,
75
+ data: {
76
+ query: q,
77
+ result_count: hits.length,
78
+ results: hits,
79
+ snippet: hits.length ? undefined : snippet.slice(0, 2000),
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,39 @@
1
+ import type { LlmImage } from './llm/types.js';
2
+ /** Refuse to read source files larger than this (10 MB). */
3
+ export declare const IMAGE_MAX_SOURCE_BYTES: number;
4
+ /** Downscale so the long edge is at most this many px (matches common vision limits). */
5
+ export declare const IMAGE_MAX_DIMENSION = 1568;
6
+ /** Refuse the encoded payload if it still exceeds this after re-encode (5 MB). */
7
+ export declare const IMAGE_MAX_ENCODED_BYTES: number;
8
+ export interface ImageAttachmentInput {
9
+ path: string;
10
+ name?: string;
11
+ }
12
+ export interface ProcessedImageAttachments {
13
+ images: LlmImage[];
14
+ /** One line per input describing the outcome, for the model-visible note. */
15
+ notes: string[];
16
+ }
17
+ export declare function isImagePath(path: string): boolean;
18
+ /**
19
+ * Validate the wire value (`metadata.images` from the renderer) into a clean
20
+ * input list. Anything that isn't an object with a non-empty string `path`
21
+ * is dropped; non-image extensions are dropped here too so a renamed file
22
+ * can't smuggle arbitrary bytes into the decoder.
23
+ */
24
+ export declare function parseImageAttachments(value: unknown): ImageAttachmentInput[];
25
+ /**
26
+ * Pure resize decision, exported for tests: given source dimensions, return
27
+ * the target dimensions honoring `IMAGE_MAX_DIMENSION` (aspect preserved,
28
+ * never upscale, never below 1px).
29
+ */
30
+ export declare function computeTargetSize(width: number, height: number, maxDimension?: number): {
31
+ width: number;
32
+ height: number;
33
+ resized: boolean;
34
+ };
35
+ /**
36
+ * Process a batch of image attachments. Synchronous: `nativeImage` decode /
37
+ * resize / encode are all synchronous, and the byte check uses `statSync`.
38
+ */
39
+ export declare function processImageAttachments(files: ImageAttachmentInput[]): ProcessedImageAttachments;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * W6-3: turn an attached image file into a base64 `LlmImage` the model can
3
+ * actually see, replacing the old behavior of dropping only the file *path*
4
+ * into the prompt text.
5
+ *
6
+ * Runs in the Electron main process and uses `nativeImage` to decode /
7
+ * downscale — no native dependency, works in the packaged app. Two caps are
8
+ * enforced before the image ever reaches the model:
9
+ *
10
+ * - source bytes (`IMAGE_MAX_SOURCE_BYTES`) — refuse to read huge files;
11
+ * - long-edge px (`IMAGE_MAX_DIMENSION`) — downscale so the provider's
12
+ * vision input limit and our token budget aren't blown by a 4K screenshot;
13
+ * - encoded bytes (`IMAGE_MAX_ENCODED_BYTES`) — re-encode PNG->JPEG and
14
+ * finally refuse if still too large.
15
+ *
16
+ * Every decision (attached / resized / skipped + why) is returned as a note
17
+ * line so the caller can inject it into the model-visible context — the model
18
+ * should know an image was attached even when it was too large to send.
19
+ */
20
+ import { getNativeImage } from './runtime.js';
21
+ import { statSync } from 'fs';
22
+ import { basename, extname } from 'path';
23
+ /** Refuse to read source files larger than this (10 MB). */
24
+ export const IMAGE_MAX_SOURCE_BYTES = 10 * 1024 * 1024;
25
+ /** Downscale so the long edge is at most this many px (matches common vision limits). */
26
+ export const IMAGE_MAX_DIMENSION = 1568;
27
+ /** Refuse the encoded payload if it still exceeds this after re-encode (5 MB). */
28
+ export const IMAGE_MAX_ENCODED_BYTES = 5 * 1024 * 1024;
29
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']);
30
+ export function isImagePath(path) {
31
+ return IMAGE_EXTENSIONS.has(extname(path).toLowerCase());
32
+ }
33
+ /**
34
+ * Validate the wire value (`metadata.images` from the renderer) into a clean
35
+ * input list. Anything that isn't an object with a non-empty string `path`
36
+ * is dropped; non-image extensions are dropped here too so a renamed file
37
+ * can't smuggle arbitrary bytes into the decoder.
38
+ */
39
+ export function parseImageAttachments(value) {
40
+ if (!Array.isArray(value))
41
+ return [];
42
+ const out = [];
43
+ for (const item of value) {
44
+ if (!item || typeof item !== 'object')
45
+ continue;
46
+ const path = item.path;
47
+ if (typeof path !== 'string' || !path.trim() || !isImagePath(path))
48
+ continue;
49
+ const name = item.name;
50
+ out.push({ path, name: typeof name === 'string' ? name : undefined });
51
+ }
52
+ return out;
53
+ }
54
+ /**
55
+ * Pure resize decision, exported for tests: given source dimensions, return
56
+ * the target dimensions honoring `IMAGE_MAX_DIMENSION` (aspect preserved,
57
+ * never upscale, never below 1px).
58
+ */
59
+ export function computeTargetSize(width, height, maxDimension = IMAGE_MAX_DIMENSION) {
60
+ if (width <= 0 || height <= 0)
61
+ return { width: 0, height: 0, resized: false };
62
+ if (width <= maxDimension && height <= maxDimension) {
63
+ return { width, height, resized: false };
64
+ }
65
+ const scale = Math.min(maxDimension / width, maxDimension / height);
66
+ return {
67
+ width: Math.max(1, Math.round(width * scale)),
68
+ height: Math.max(1, Math.round(height * scale)),
69
+ resized: true,
70
+ };
71
+ }
72
+ function formatMb(bytes) {
73
+ return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, '');
74
+ }
75
+ /**
76
+ * Process a batch of image attachments. Synchronous: `nativeImage` decode /
77
+ * resize / encode are all synchronous, and the byte check uses `statSync`.
78
+ */
79
+ export function processImageAttachments(files) {
80
+ const images = [];
81
+ const notes = [];
82
+ for (const file of files) {
83
+ const label = file.name || basename(file.path);
84
+ // Existence + source-size guards run before the decoder so they're
85
+ // exercisable in a non-Electron (test) host too.
86
+ let sourceBytes = 0;
87
+ try {
88
+ sourceBytes = statSync(file.path).size;
89
+ }
90
+ catch {
91
+ notes.push(`- ${label}:文件不存在或不可读,未附加`);
92
+ continue;
93
+ }
94
+ if (sourceBytes > IMAGE_MAX_SOURCE_BYTES) {
95
+ notes.push(`- ${label}:源文件 ${formatMb(sourceBytes)}MB 超过 ${formatMb(IMAGE_MAX_SOURCE_BYTES)}MB 上限,未附加`);
96
+ continue;
97
+ }
98
+ const nativeImage = getNativeImage();
99
+ if (!nativeImage) {
100
+ // Non-Electron host (BS server, unit tests, headless) — can't decode images.
101
+ notes.push(`- ${label}:当前运行环境不支持图片解码,未附加`);
102
+ continue;
103
+ }
104
+ const image = nativeImage.createFromPath(file.path);
105
+ if (image.isEmpty()) {
106
+ notes.push(`- ${label}:不是可识别的图片,未附加`);
107
+ continue;
108
+ }
109
+ const { width, height } = image.getSize();
110
+ const target = computeTargetSize(width, height);
111
+ const rendered = target.resized
112
+ ? image.resize({ width: target.width, height: target.height, quality: 'good' })
113
+ : image;
114
+ // PNG for graphics/screenshots (lossless), JPEG for photos (smaller).
115
+ const preferJpeg = ['.jpg', '.jpeg'].includes(extname(file.path).toLowerCase());
116
+ let mediaType = preferJpeg ? 'image/jpeg' : 'image/png';
117
+ let buffer = preferJpeg ? rendered.toJPEG(85) : rendered.toPNG();
118
+ if (buffer.length > IMAGE_MAX_ENCODED_BYTES && mediaType !== 'image/jpeg') {
119
+ mediaType = 'image/jpeg';
120
+ buffer = rendered.toJPEG(80);
121
+ }
122
+ if (buffer.length > IMAGE_MAX_ENCODED_BYTES) {
123
+ notes.push(`- ${label}:压缩后仍超过 ${formatMb(IMAGE_MAX_ENCODED_BYTES)}MB,未附加`);
124
+ continue;
125
+ }
126
+ images.push({ data: buffer.toString('base64'), mediaType });
127
+ const sizeText = target.resized
128
+ ? `${width}×${height},已缩放至 ${target.width}×${target.height}`
129
+ : `${width}×${height}`;
130
+ notes.push(`- ${label}(${sizeText})`);
131
+ }
132
+ return { images, notes };
133
+ }
@@ -0,0 +1,10 @@
1
+ export declare function insightsNetworkDisabled(): boolean;
2
+ export declare function buildInsightsExportPayload(): Record<string, unknown>;
3
+ export declare function postInsightsJson(url: string, body: unknown): Promise<boolean>;
4
+ export declare function flushInsightsOutbox(): Promise<{
5
+ uploaded: number;
6
+ skipped: number;
7
+ failed: number;
8
+ }>;
9
+ export declare function uploadInsightsBundle(): Promise<boolean>;
10
+ export declare function scheduleInsightsFlush(): void;
@@ -0,0 +1,139 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { getBrand } from '../brand.js';
5
+ import { resolveInsightsApiBase, rowsEligibleForAutoUpload, } from '../storage/insights-settings.js';
6
+ import { localStore } from '../storage/index.js';
7
+ const FLUSH_TIMEOUT_MS = 8_000;
8
+ const MAX_BATCH = 20;
9
+ export function insightsNetworkDisabled() {
10
+ return Boolean(process.env.VITEST) || process.env.DEEPPATH_INSIGHTS_DISABLE_UPLOAD === '1';
11
+ }
12
+ let cachedVersion = null;
13
+ /** Packaged Electron has no npm_package_version — read package.json like brand.ts does. */
14
+ function detectAppVersion() {
15
+ if (cachedVersion)
16
+ return cachedVersion;
17
+ const fromEnv = process.env.npm_package_version;
18
+ if (fromEnv) {
19
+ cachedVersion = fromEnv;
20
+ return fromEnv;
21
+ }
22
+ try {
23
+ const here = path.dirname(fileURLToPath(import.meta.url));
24
+ const pkgPath = path.join(here, '..', '..', 'package.json');
25
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
26
+ if (typeof pkg.version === 'string' && pkg.version) {
27
+ cachedVersion = pkg.version;
28
+ return cachedVersion;
29
+ }
30
+ }
31
+ catch {
32
+ // fall through
33
+ }
34
+ cachedVersion = '0.0.0';
35
+ return cachedVersion;
36
+ }
37
+ function clientMeta(settings) {
38
+ return {
39
+ installId: settings.installId,
40
+ flavor: getBrand().flavor,
41
+ appVersion: detectAppVersion(),
42
+ };
43
+ }
44
+ export function buildInsightsExportPayload() {
45
+ const settings = localStore.ensureInsightsSettings();
46
+ return {
47
+ ...localStore.exportInsightsBundle(),
48
+ ...clientMeta(settings),
49
+ };
50
+ }
51
+ function ingestHeaders() {
52
+ const headers = { 'Content-Type': 'application/json' };
53
+ const token = (process.env.DEEPPATH_INSIGHTS_INGEST_TOKEN || '').trim();
54
+ // 线协议常量:header 名由遥测接收端(产品注入的 insightsApiBase)定义,
55
+ // shell 只是按对端契约发送——不属产品硬编码。
56
+ if (token)
57
+ headers['X-DeepPath-Insights-Key'] = token; // shell-neutral:allow
58
+ return headers;
59
+ }
60
+ export async function postInsightsJson(url, body) {
61
+ const controller = new AbortController();
62
+ const timer = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
63
+ try {
64
+ const res = await fetch(url, {
65
+ method: 'POST',
66
+ headers: ingestHeaders(),
67
+ body: JSON.stringify(body),
68
+ signal: controller.signal,
69
+ });
70
+ return res.ok;
71
+ }
72
+ catch (err) {
73
+ console.warn('[insights] upload failed', err instanceof Error ? err.message : err);
74
+ return false;
75
+ }
76
+ finally {
77
+ clearTimeout(timer);
78
+ }
79
+ }
80
+ function pathForKind(kind) {
81
+ if (kind === 'event')
82
+ return '/api/v2/agent-insights/events';
83
+ if (kind === 'turn')
84
+ return '/api/v2/agent-insights/turns';
85
+ return '/api/v2/agent-insights/profile';
86
+ }
87
+ export async function flushInsightsOutbox() {
88
+ if (insightsNetworkDisabled()) {
89
+ return { uploaded: 0, skipped: 0, failed: 0 };
90
+ }
91
+ const settings = localStore.ensureInsightsSettings();
92
+ const pending = localStore.listInsightOutbox({ uploaded: false, limit: MAX_BATCH });
93
+ const { upload, skipped } = rowsEligibleForAutoUpload(settings, pending);
94
+ let uploaded = 0;
95
+ let failed = 0;
96
+ const base = resolveInsightsApiBase(settings, process.env.DEEPPATH_INSIGHTS_API_BASE);
97
+ // 无端点(中性 shell 未注入遥测配置)= 不上报,outbox 留本地待导出。
98
+ if (!base)
99
+ return { uploaded, skipped, failed };
100
+ const meta = clientMeta(settings);
101
+ for (const row of upload) {
102
+ const ok = await postInsightsJson(`${base}${pathForKind(row.kind)}`, {
103
+ ...meta,
104
+ ...row.payload,
105
+ clientEventId: row.id,
106
+ });
107
+ if (ok) {
108
+ localStore.markInsightUploaded(row.id);
109
+ uploaded += 1;
110
+ }
111
+ else {
112
+ localStore.markInsightUploadError(row.id, 'upload_failed');
113
+ failed += 1;
114
+ }
115
+ }
116
+ return { uploaded, skipped, failed };
117
+ }
118
+ export async function uploadInsightsBundle() {
119
+ if (insightsNetworkDisabled()) {
120
+ return false;
121
+ }
122
+ const settings = localStore.ensureInsightsSettings();
123
+ const bundle = buildInsightsExportPayload();
124
+ const base = resolveInsightsApiBase(settings, process.env.DEEPPATH_INSIGHTS_API_BASE);
125
+ if (!base)
126
+ return false;
127
+ const ok = await postInsightsJson(`${base}/api/v2/agent-insights/bundle`, bundle);
128
+ if (ok) {
129
+ const records = Array.isArray(bundle.records) ? bundle.records : [];
130
+ for (const row of records)
131
+ localStore.markInsightUploaded(row.id);
132
+ }
133
+ return ok;
134
+ }
135
+ export function scheduleInsightsFlush() {
136
+ void flushInsightsOutbox().catch((err) => {
137
+ console.warn('[insights] flush crashed', err);
138
+ });
139
+ }
@@ -0,0 +1,17 @@
1
+ export declare function recordInsightEvent(eventName: string, properties?: Record<string, unknown>): void;
2
+ export declare function recordInsightTurn(input: {
3
+ chatId: string;
4
+ mode?: string;
5
+ modelId?: string | null;
6
+ completionStatus?: string;
7
+ durationMs?: number | null;
8
+ toolNames?: unknown;
9
+ userText?: string;
10
+ assistantText?: string;
11
+ }): void;
12
+ export declare function recordInsightProfile(profile: {
13
+ displayName?: string;
14
+ email?: string;
15
+ company?: string;
16
+ note?: string;
17
+ }): void;
@@ -0,0 +1,44 @@
1
+ import { redactInsightText, toolNamesOnly } from '../storage/insights-redact.js';
2
+ import { localStore } from '../storage/index.js';
3
+ import { scheduleInsightsFlush } from './flush.js';
4
+ export function recordInsightEvent(eventName, properties = {}) {
5
+ try {
6
+ localStore.enqueueInsight('event', { eventName, properties });
7
+ scheduleInsightsFlush();
8
+ }
9
+ catch (err) {
10
+ console.warn('[insights] event enqueue failed', err);
11
+ }
12
+ }
13
+ export function recordInsightTurn(input) {
14
+ try {
15
+ localStore.enqueueInsight('turn', {
16
+ chatId: input.chatId,
17
+ mode: input.mode === 'plan' ? 'plan' : 'agent',
18
+ modelId: typeof input.modelId === 'string' ? input.modelId.slice(0, 128) : undefined,
19
+ completionStatus: input.completionStatus,
20
+ durationMs: typeof input.durationMs === 'number' ? input.durationMs : undefined,
21
+ toolNames: toolNamesOnly(input.toolNames),
22
+ userText: redactInsightText(input.userText, 4000),
23
+ assistantText: redactInsightText(input.assistantText, 8000),
24
+ });
25
+ scheduleInsightsFlush();
26
+ }
27
+ catch (err) {
28
+ console.warn('[insights] turn enqueue failed', err);
29
+ }
30
+ }
31
+ export function recordInsightProfile(profile) {
32
+ try {
33
+ localStore.enqueueInsight('profile', {
34
+ displayName: redactInsightText(profile.displayName, 128),
35
+ email: redactInsightText(profile.email, 191),
36
+ company: redactInsightText(profile.company, 191),
37
+ note: redactInsightText(profile.note, 500),
38
+ });
39
+ scheduleInsightsFlush();
40
+ }
41
+ catch (err) {
42
+ console.warn('[insights] profile enqueue failed', err);
43
+ }
44
+ }
@@ -0,0 +1,9 @@
1
+ export interface JsonStore<T extends Record<string, unknown>> {
2
+ get<Key extends keyof T>(key: Key): T[Key];
3
+ get<Key extends keyof T>(key: Key, defaultValue: Required<T>[Key]): Required<T>[Key];
4
+ set<Key extends keyof T>(key: Key, value?: T[Key]): void;
5
+ }
6
+ export declare function createJsonStore<T extends Record<string, unknown>>(options: {
7
+ name: string;
8
+ defaults?: Partial<T>;
9
+ }): JsonStore<T>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * JSON 键值存储工厂(agent-mcp-servers.json / agent-projects.json / window-state)。
3
+ *
4
+ * 历史上用 electron-store;它只在构造时读 `app.getPath('userData')` 当 cwd,
5
+ * 而 conf(electron-store 的底层,已是本项目直接依赖)API 完全覆盖用到的
6
+ * get/set + name + defaults 子集。统一走 conf + getUserDataDir(),Electron
7
+ * 与 BS server 得到同一个文件路径,两种模式共享同一份数据。
8
+ *
9
+ * conf@10 的 d.ts 是 ESM 风格但包本身没有 `type: module`,NodeNext 下类型
10
+ * 解析会塌成 namespace——所以这里声明用到的最小接口并做一次强转,不依赖
11
+ * 它的声明文件。
12
+ */
13
+ import ConfImport from 'conf';
14
+ import { getUserDataDir } from './runtime.js';
15
+ const Conf = ConfImport;
16
+ export function createJsonStore(options) {
17
+ return new Conf({
18
+ cwd: getUserDataDir(),
19
+ name: options.name,
20
+ defaults: options.defaults,
21
+ });
22
+ }