@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.12

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 (247) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-k9ghy5sn.md} +53 -0
  3. package/dist/cli.js +10775 -10762
  4. package/dist/types/advisor/runtime.d.ts +1 -1
  5. package/dist/types/capability/mcp.d.ts +11 -0
  6. package/dist/types/capability/skill.d.ts +6 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/custom-models.d.ts +31 -0
  14. package/dist/types/config/model-config-values.d.ts +19 -0
  15. package/dist/types/config/model-patch.d.ts +93 -0
  16. package/dist/types/config/model-provider-discovery.d.ts +51 -0
  17. package/dist/types/config/model-registry.d.ts +9 -52
  18. package/dist/types/config/settings-schema.d.ts +1 -34
  19. package/dist/types/config/settings.d.ts +5 -3
  20. package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
  21. package/dist/types/discovery/agent-plugin-format.d.ts +136 -0
  22. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  23. package/dist/types/discovery/contained-path.d.ts +33 -0
  24. package/dist/types/discovery/index.d.ts +1 -0
  25. package/dist/types/eval/executor-base.d.ts +8 -2
  26. package/dist/types/eval/kernel-session-registry.d.ts +60 -0
  27. package/dist/types/export/share.d.ts +1 -1
  28. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  29. package/dist/types/extensibility/extensions/loader.d.ts +7 -0
  30. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  31. package/dist/types/extensibility/shared-events.d.ts +3 -2
  32. package/dist/types/extensibility/skills.d.ts +5 -0
  33. package/dist/types/lsp/diagnostics.d.ts +96 -0
  34. package/dist/types/lsp/index.d.ts +7 -128
  35. package/dist/types/lsp/servers.d.ts +72 -0
  36. package/dist/types/lsp/tool.d.ts +42 -0
  37. package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
  38. package/dist/types/lsp/writethrough.d.ts +33 -0
  39. package/dist/types/mcp/transports/header-policy.d.ts +46 -0
  40. package/dist/types/mcp/types.d.ts +15 -0
  41. package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
  42. package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
  43. package/dist/types/modes/components/agent-hub.d.ts +2 -0
  44. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  45. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  47. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  48. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  49. package/dist/types/modes/theme/color.d.ts +19 -0
  50. package/dist/types/modes/theme/loader.d.ts +19 -0
  51. package/dist/types/modes/theme/schema.d.ts +175 -0
  52. package/dist/types/modes/theme/symbols.d.ts +28 -0
  53. package/dist/types/modes/theme/theme-class.d.ts +244 -0
  54. package/dist/types/modes/theme/theme.d.ts +9 -280
  55. package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
  56. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  57. package/dist/types/registry/agent-registry.d.ts +1 -3
  58. package/dist/types/secrets/index.d.ts +26 -2
  59. package/dist/types/secrets/message-transform.d.ts +40 -0
  60. package/dist/types/secrets/obfuscator.d.ts +0 -108
  61. package/dist/types/secrets/placeholder-scan.d.ts +95 -0
  62. package/dist/types/secrets/placeholder.d.ts +94 -0
  63. package/dist/types/secrets/replacement.d.ts +82 -0
  64. package/dist/types/session/agent-session-events.d.ts +2 -2
  65. package/dist/types/session/agent-session-types.d.ts +6 -0
  66. package/dist/types/session/agent-session.d.ts +2 -2
  67. package/dist/types/session/messages.d.ts +1 -0
  68. package/dist/types/session/prewalk.d.ts +3 -1
  69. package/dist/types/session/session-handoff.d.ts +3 -3
  70. package/dist/types/session/session-manager.d.ts +32 -0
  71. package/dist/types/session/session-provider-boundary.d.ts +1 -1
  72. package/dist/types/session/session-tools.d.ts +8 -0
  73. package/dist/types/session/turn-recovery.d.ts +9 -4
  74. package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
  75. package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
  76. package/dist/types/slash-commands/builtin-control.d.ts +2 -0
  77. package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
  78. package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
  79. package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
  80. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  81. package/dist/types/slash-commands/builtin-session.d.ts +2 -0
  82. package/dist/types/task/index.d.ts +2 -0
  83. package/dist/types/tools/gh-common.d.ts +69 -0
  84. package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
  85. package/dist/types/tools/gh-pr-diff.d.ts +102 -0
  86. package/dist/types/tools/gh-run-watch.d.ts +78 -0
  87. package/dist/types/tools/gh-search.d.ts +61 -0
  88. package/dist/types/tools/gh-types.d.ts +340 -0
  89. package/dist/types/tools/gh-view.d.ts +88 -0
  90. package/dist/types/tools/gh.d.ts +4 -201
  91. package/dist/types/tools/read-archive.d.ts +16 -0
  92. package/dist/types/tools/read-format.d.ts +104 -0
  93. package/dist/types/tools/read-path-resolution.d.ts +19 -0
  94. package/dist/types/tools/read-pdf-images.d.ts +12 -0
  95. package/dist/types/tools/read-renderer.d.ts +24 -0
  96. package/dist/types/tools/read-selector.d.ts +27 -0
  97. package/dist/types/tools/read-sqlite.d.ts +16 -0
  98. package/dist/types/tools/read-summary.d.ts +19 -0
  99. package/dist/types/tools/read.d.ts +1 -23
  100. package/dist/types/tools/shell-tokenize.d.ts +18 -1
  101. package/dist/types/utils/changelog.d.ts +0 -5
  102. package/package.json +13 -13
  103. package/scripts/legacy-pi-virtual-module.ts +4 -1
  104. package/src/advisor/runtime.ts +12 -7
  105. package/src/capability/mcp.ts +11 -0
  106. package/src/capability/skill.ts +6 -0
  107. package/src/cli/command-help.ts +4 -0
  108. package/src/cli/gallery-cli.ts +1 -1
  109. package/src/cli-commands.ts +5 -0
  110. package/src/commands/commit.ts +16 -3
  111. package/src/commands/share.ts +71 -0
  112. package/src/commit/agentic/index.ts +39 -21
  113. package/src/commit/execute.ts +56 -0
  114. package/src/commit/index.ts +2 -1
  115. package/src/commit/pipeline.ts +20 -7
  116. package/src/config/custom-models.ts +188 -0
  117. package/src/config/model-config-values.ts +128 -0
  118. package/src/config/model-discovery.ts +1 -1
  119. package/src/config/model-patch.ts +252 -0
  120. package/src/config/model-provider-discovery.ts +132 -0
  121. package/src/config/model-registry.ts +84 -704
  122. package/src/config/settings-schema.ts +2 -33
  123. package/src/config/settings.ts +53 -3
  124. package/src/debug/raw-sse-buffer.ts +20 -0
  125. package/src/discovery/agent-plugin-format.ts +551 -0
  126. package/src/discovery/agent-plugins.ts +341 -0
  127. package/src/discovery/claude-plugins.ts +21 -5
  128. package/src/discovery/contained-path.ts +75 -0
  129. package/src/discovery/helpers.ts +23 -9
  130. package/src/discovery/index.ts +1 -0
  131. package/src/discovery/omp-plugins.ts +20 -7
  132. package/src/edit/hashline/diff.ts +5 -1
  133. package/src/eval/executor-base.ts +39 -6
  134. package/src/eval/jl/executor.ts +82 -371
  135. package/src/eval/kernel-session-registry.ts +398 -0
  136. package/src/eval/py/executor.ts +53 -261
  137. package/src/eval/rb/executor.ts +36 -279
  138. package/src/exec/non-interactive-env.ts +1 -0
  139. package/src/export/share.ts +2 -1
  140. package/src/extensibility/custom-tools/types.ts +2 -2
  141. package/src/extensibility/extensions/loader.ts +78 -14
  142. package/src/extensibility/extensions/runner.ts +6 -0
  143. package/src/extensibility/extensions/types.ts +12 -0
  144. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  145. package/src/extensibility/shared-events.ts +3 -2
  146. package/src/extensibility/skills.ts +9 -0
  147. package/src/internal-urls/skill-protocol.ts +14 -0
  148. package/src/internal-urls/vault-protocol.ts +2 -2
  149. package/src/launch/client.ts +11 -1
  150. package/src/launch/protocol.ts +7 -2
  151. package/src/lsp/diagnostics.ts +516 -0
  152. package/src/lsp/index.ts +20 -2819
  153. package/src/lsp/servers.ts +296 -0
  154. package/src/lsp/tool.ts +1352 -0
  155. package/src/lsp/workspace-diagnostics.ts +170 -0
  156. package/src/lsp/writethrough.ts +561 -0
  157. package/src/main.ts +3 -2
  158. package/src/mcp/config.ts +3 -0
  159. package/src/mcp/manager.ts +12 -9
  160. package/src/mcp/transports/header-policy.ts +95 -0
  161. package/src/mcp/transports/http.ts +35 -52
  162. package/src/mcp/transports/sse.ts +20 -27
  163. package/src/mcp/types.ts +15 -0
  164. package/src/modes/acp/acp-agent.ts +15 -4
  165. package/src/modes/components/agent-dashboard.ts +2 -0
  166. package/src/modes/components/agent-hub-projection.ts +2 -2
  167. package/src/modes/components/agent-hub-renderer.ts +3 -7
  168. package/src/modes/components/agent-hub.ts +5 -0
  169. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  170. package/src/modes/components/chat-transcript-builder.ts +3 -0
  171. package/src/modes/components/custom-editor.ts +0 -9
  172. package/src/modes/components/status-line/component.ts +1 -0
  173. package/src/modes/components/status-line/segments.ts +6 -4
  174. package/src/modes/components/status-line/types.ts +2 -0
  175. package/src/modes/components/tool-execution.ts +13 -16
  176. package/src/modes/components/tree-selector.ts +62 -3
  177. package/src/modes/controllers/command-controller.ts +8 -2
  178. package/src/modes/controllers/event-controller.ts +15 -15
  179. package/src/modes/controllers/input-controller.ts +20 -2
  180. package/src/modes/controllers/selector-controller.ts +1 -0
  181. package/src/modes/theme/color.ts +133 -0
  182. package/src/modes/theme/loader.ts +178 -0
  183. package/src/modes/theme/schema.ts +263 -0
  184. package/src/modes/theme/symbols.ts +1000 -0
  185. package/src/modes/theme/theme-class.ts +611 -0
  186. package/src/modes/theme/theme.ts +27 -2453
  187. package/src/modes/theme/tui-adapters.ts +279 -0
  188. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  189. package/src/modes/utils/ui-helpers.ts +1 -0
  190. package/src/registry/agent-registry.ts +2 -2
  191. package/src/sdk.ts +6 -49
  192. package/src/secrets/index.ts +51 -6
  193. package/src/secrets/message-transform.ts +287 -0
  194. package/src/secrets/obfuscator.ts +39 -1303
  195. package/src/secrets/placeholder-scan.ts +506 -0
  196. package/src/secrets/placeholder.ts +309 -0
  197. package/src/secrets/replacement.ts +216 -0
  198. package/src/session/agent-session-events.ts +2 -2
  199. package/src/session/agent-session-types.ts +6 -0
  200. package/src/session/agent-session.ts +146 -21
  201. package/src/session/indexed-session-storage.ts +7 -2
  202. package/src/session/messages.ts +1 -0
  203. package/src/session/prewalk.ts +57 -17
  204. package/src/session/session-advisors.ts +32 -34
  205. package/src/session/session-context.ts +3 -5
  206. package/src/session/session-handoff.ts +39 -16
  207. package/src/session/session-manager.ts +67 -3
  208. package/src/session/session-provider-boundary.ts +3 -6
  209. package/src/session/session-tools.ts +37 -3
  210. package/src/session/todo-tracker.ts +11 -1
  211. package/src/session/turn-recovery.ts +115 -84
  212. package/src/slash-commands/builtin-collaboration.ts +504 -0
  213. package/src/slash-commands/builtin-completions.ts +291 -0
  214. package/src/slash-commands/builtin-control.ts +78 -0
  215. package/src/slash-commands/builtin-lifecycle.ts +506 -0
  216. package/src/slash-commands/builtin-marketplace.ts +567 -0
  217. package/src/slash-commands/builtin-modes.ts +518 -0
  218. package/src/slash-commands/builtin-registry.ts +21 -2996
  219. package/src/slash-commands/builtin-session.ts +592 -0
  220. package/src/task/executor.ts +17 -14
  221. package/src/task/index.ts +21 -6
  222. package/src/tools/bash-skill-urls.ts +15 -0
  223. package/src/tools/bash.ts +10 -11
  224. package/src/tools/debug.ts +1 -1
  225. package/src/tools/gh-common.ts +288 -0
  226. package/src/tools/gh-pr-checkout.ts +679 -0
  227. package/src/tools/gh-pr-diff.ts +473 -0
  228. package/src/tools/gh-run-watch.ts +1015 -0
  229. package/src/tools/gh-search.ts +500 -0
  230. package/src/tools/gh-types.ts +379 -0
  231. package/src/tools/gh-view.ts +612 -0
  232. package/src/tools/gh.ts +109 -3821
  233. package/src/tools/jtd-to-json-schema.ts +123 -21
  234. package/src/tools/read-archive.ts +209 -0
  235. package/src/tools/read-format.ts +593 -0
  236. package/src/tools/read-path-resolution.ts +36 -0
  237. package/src/tools/read-pdf-images.ts +250 -0
  238. package/src/tools/read-renderer.ts +289 -0
  239. package/src/tools/read-selector.ts +84 -0
  240. package/src/tools/read-sqlite.ts +215 -0
  241. package/src/tools/read-summary.ts +199 -0
  242. package/src/tools/read.ts +82 -1824
  243. package/src/tools/shell-tokenize.ts +99 -1
  244. package/src/utils/changelog.ts +1 -1
  245. package/src/utils/title-generator.ts +3 -1
  246. package/src/web/search/providers/exa.ts +1 -1
  247. package/src/web/search/providers/zai.ts +12 -3
@@ -0,0 +1,561 @@
1
+ import * as fs from "node:fs";
2
+ import { isEnoent, logger, once, untilAborted } from "@oh-my-pi/pi-utils";
3
+ import type { BunFile } from "bun";
4
+ import { FileChangeType, notifyWorkspaceWatchedFiles } from "./client";
5
+ import { getServersForFile } from "./config";
6
+ import {
7
+ captureDiagnosticVersions,
8
+ captureOpenFileVersions,
9
+ DEFERRED_DIAGNOSTICS_WAIT_TIMEOUT_MS,
10
+ type FileDiagnosticsResult,
11
+ FileFormatResult,
12
+ formatContent,
13
+ getDiagnosticsForFile,
14
+ INLINE_DIAGNOSTICS_WAIT_TIMEOUT_MS,
15
+ limitDiagnosticMessages,
16
+ type ServerVersionMap,
17
+ } from "./diagnostics";
18
+ import { getConfig, notifyFileSaved, splitServers, syncFileContent } from "./servers";
19
+ import type { ServerConfig } from "./types";
20
+ import { summarizeDiagnosticMessages } from "./utils";
21
+
22
+ /** Options for creating the LSP writethrough callback */
23
+ export interface WritethroughOptions {
24
+ /** Whether to format the file using LSP after writing */
25
+ enableFormat?: boolean;
26
+ /** Whether to get LSP diagnostics after writing */
27
+ enableDiagnostics?: boolean;
28
+ /** Called when diagnostics arrive after the main timeout. */
29
+ onDeferredDiagnostics?: (diagnostics: FileDiagnosticsResult) => void;
30
+ /** Signal to cancel a pending deferred diagnostics fetch. */
31
+ deferredSignal?: AbortSignal;
32
+ /** Transform diagnostics before surfacing them after a successful fetch. */
33
+ transformDiagnostics?: (absPath: string, result: FileDiagnosticsResult) => FileDiagnosticsResult;
34
+ }
35
+
36
+ /** Internal resolved form of {@link WritethroughOptions} that the writethrough machinery operates on. */
37
+ type ResolvedWritethroughOptions = {
38
+ enableFormat: boolean;
39
+ enableDiagnostics: boolean;
40
+ transformDiagnostics?: (absPath: string, result: FileDiagnosticsResult) => FileDiagnosticsResult;
41
+ };
42
+
43
+ /** Per-file deferred LSP diagnostics wiring for {@link WritethroughCallback}. */
44
+ export type WritethroughDeferredHandle = {
45
+ onDeferredDiagnostics: (diagnostics: FileDiagnosticsResult) => void;
46
+ signal: AbortSignal;
47
+ finalize: (diagnostics: FileDiagnosticsResult | undefined) => void;
48
+ };
49
+
50
+ /** Callback type for the LSP writethrough */
51
+ export type WritethroughCallback = (
52
+ dst: string,
53
+ content: string,
54
+ signal?: AbortSignal,
55
+ file?: BunFile,
56
+ batch?: LspWritethroughBatchRequest,
57
+ getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
58
+ ) => Promise<FileDiagnosticsResult | undefined>;
59
+
60
+ /** No-op writethrough callback */
61
+ export async function writethroughNoop(
62
+ dst: string,
63
+ content: string,
64
+ _signal?: AbortSignal,
65
+ file?: BunFile,
66
+ _batch?: LspWritethroughBatchRequest,
67
+ _getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
68
+ ): Promise<FileDiagnosticsResult | undefined> {
69
+ if (file) {
70
+ await file.write(content);
71
+ } else {
72
+ await Bun.write(dst, content);
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ interface PendingWritethrough {
78
+ dst: string;
79
+ file?: BunFile;
80
+ changeType: FileChangeType;
81
+ }
82
+
83
+ interface RunLspWritethroughOptions {
84
+ contentAlreadyWritten?: boolean;
85
+ }
86
+
87
+ interface LspWritethroughBatchRequest {
88
+ id: string;
89
+ flush: boolean;
90
+ }
91
+
92
+ interface LspWritethroughBatchState {
93
+ entries: Map<string, PendingWritethrough>;
94
+ options: ResolvedWritethroughOptions;
95
+ }
96
+
97
+ const writethroughBatches = new Map<string, LspWritethroughBatchState>();
98
+
99
+ function getOrCreateWritethroughBatch(id: string, options: ResolvedWritethroughOptions): LspWritethroughBatchState {
100
+ const existing = writethroughBatches.get(id);
101
+ if (existing) {
102
+ existing.options.enableFormat ||= options.enableFormat;
103
+ existing.options.enableDiagnostics ||= options.enableDiagnostics;
104
+ existing.options.transformDiagnostics ??= options.transformDiagnostics;
105
+ return existing;
106
+ }
107
+ const batch: LspWritethroughBatchState = {
108
+ entries: new Map<string, PendingWritethrough>(),
109
+ options: { ...options },
110
+ };
111
+ writethroughBatches.set(id, batch);
112
+ return batch;
113
+ }
114
+
115
+ export async function flushLspWritethroughBatch(
116
+ id: string,
117
+ cwd: string,
118
+ signal?: AbortSignal,
119
+ ): Promise<FileDiagnosticsResult | undefined> {
120
+ const state = writethroughBatches.get(id);
121
+ if (!state) {
122
+ return undefined;
123
+ }
124
+ writethroughBatches.delete(id);
125
+ return flushWritethroughBatch(Array.from(state.entries.values()), cwd, state.options, signal);
126
+ }
127
+
128
+ function mergeDiagnostics(
129
+ results: Array<FileDiagnosticsResult | undefined>,
130
+ options: ResolvedWritethroughOptions,
131
+ ): FileDiagnosticsResult | undefined {
132
+ const messages: string[] = [];
133
+ const servers = new Set<string>();
134
+ let hasResults = false;
135
+ let hasFormatter = false;
136
+ let formatted = false;
137
+
138
+ for (const result of results) {
139
+ if (!result) continue;
140
+ hasResults = true;
141
+ if (result.server) {
142
+ for (const server of result.server.split(",")) {
143
+ const trimmed = server.trim();
144
+ if (trimmed) {
145
+ servers.add(trimmed);
146
+ }
147
+ }
148
+ }
149
+ if (result.messages.length > 0) {
150
+ messages.push(...result.messages);
151
+ }
152
+ if (result.formatter !== undefined) {
153
+ hasFormatter = true;
154
+ if (result.formatter === FileFormatResult.FORMATTED) {
155
+ formatted = true;
156
+ }
157
+ }
158
+ }
159
+
160
+ if (!hasResults && !hasFormatter) {
161
+ return undefined;
162
+ }
163
+
164
+ let summary = options.enableDiagnostics ? "no issues" : "OK";
165
+ let errored = false;
166
+ let limitedMessages = messages;
167
+ if (messages.length > 0) {
168
+ const summaryInfo = summarizeDiagnosticMessages(messages);
169
+ summary = summaryInfo.summary;
170
+ errored = summaryInfo.errored;
171
+ limitedMessages = limitDiagnosticMessages(messages);
172
+ }
173
+ const formatter = hasFormatter ? (formatted ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED) : undefined;
174
+
175
+ return {
176
+ server: servers.size > 0 ? Array.from(servers).join(", ") : undefined,
177
+ messages: limitedMessages,
178
+ summary,
179
+ errored,
180
+ formatter,
181
+ };
182
+ }
183
+
184
+ async function scheduleDeferredDiagnosticsFetch(args: {
185
+ dst: string;
186
+ cwd: string;
187
+ servers: Array<[string, ServerConfig]>;
188
+ minVersions: ServerVersionMap | undefined;
189
+ expectedDocumentVersions: ServerVersionMap | undefined;
190
+ signal: AbortSignal;
191
+ callback: (diagnostics: FileDiagnosticsResult) => void;
192
+ }): Promise<void> {
193
+ try {
194
+ const deferredTimeout = AbortSignal.timeout(25_000);
195
+ const combined = AbortSignal.any([args.signal, deferredTimeout]);
196
+ const diagnostics = await getDiagnosticsForFile(args.dst, args.cwd, args.servers, {
197
+ signal: combined,
198
+ minVersions: args.minVersions,
199
+ expectedDocumentVersions: args.expectedDocumentVersions,
200
+ timeoutMs: DEFERRED_DIAGNOSTICS_WAIT_TIMEOUT_MS,
201
+ });
202
+ if (args.signal.aborted || diagnostics === undefined) return;
203
+ args.callback(diagnostics);
204
+ } catch {
205
+ // Cancelled or LSP gave up; silently discard.
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Fetch post-write diagnostics without making the edit/write block on a slow
211
+ * language server.
212
+ *
213
+ * Blocks inline only briefly ({@link INLINE_DIAGNOSTICS_WAIT_TIMEOUT_MS}) for a
214
+ * fresh result. Freshness is enforced by the pre-edit `minVersions` baseline:
215
+ * exact document-version matches return immediately, and unversioned/mismatched
216
+ * publishes must settle with no newer publish before inline acceptance. If
217
+ * nothing fresh arrives in the inline window and a deferred
218
+ * channel is available, the in-flight fetch is handed off to deliver late via
219
+ * `onDeferredDiagnostics`, and this returns `undefined` so the tool result
220
+ * lands immediately. Without a deferred channel (direct/CI callers) it blocks
221
+ * for the standard budget so the result is still returned inline.
222
+ */
223
+ async function fetchDiagnosticsWithDeferral(args: {
224
+ dst: string;
225
+ cwd: string;
226
+ servers: Array<[string, ServerConfig]>;
227
+ minVersions: ServerVersionMap | undefined;
228
+ expectedDocumentVersions: ServerVersionMap | undefined;
229
+ transformDiagnostics?: ResolvedWritethroughOptions["transformDiagnostics"];
230
+ deferred?: { onDeferredDiagnostics: (diagnostics: FileDiagnosticsResult) => void; signal: AbortSignal };
231
+ signal?: AbortSignal;
232
+ }): Promise<FileDiagnosticsResult | undefined> {
233
+ const { dst, cwd, servers, minVersions, expectedDocumentVersions, transformDiagnostics, deferred, signal } = args;
234
+ const apply = (d: FileDiagnosticsResult | undefined) =>
235
+ d && transformDiagnostics ? transformDiagnostics(dst, d) : d;
236
+
237
+ if (!deferred) {
238
+ // No late-injection channel: block for the standard budget and return inline.
239
+ return apply(
240
+ await getDiagnosticsForFile(dst, cwd, servers, {
241
+ signal,
242
+ minVersions,
243
+ expectedDocumentVersions,
244
+ }),
245
+ );
246
+ }
247
+
248
+ // One background fetch with a generous inner budget; await it only briefly inline.
249
+ const fetchPromise = getDiagnosticsForFile(dst, cwd, servers, {
250
+ signal: deferred.signal,
251
+ minVersions,
252
+ expectedDocumentVersions,
253
+ timeoutMs: DEFERRED_DIAGNOSTICS_WAIT_TIMEOUT_MS,
254
+ });
255
+ const INLINE_TIMEOUT = Symbol("inline-diagnostics-timeout");
256
+ const raced = await Promise.race([
257
+ fetchPromise,
258
+ Bun.sleep(INLINE_DIAGNOSTICS_WAIT_TIMEOUT_MS).then(() => INLINE_TIMEOUT),
259
+ ]);
260
+ if (raced !== INLINE_TIMEOUT) {
261
+ return apply(raced as FileDiagnosticsResult | undefined);
262
+ }
263
+ // Slow server: deliver late via the deferred channel; nothing inline. The
264
+ // deferred sink (edit tool) applies its own dedup, so pass the raw result.
265
+ void fetchPromise
266
+ .then(diagnostics => {
267
+ if (diagnostics && !deferred.signal.aborted) deferred.onDeferredDiagnostics(diagnostics);
268
+ })
269
+ .catch(() => {});
270
+ return undefined;
271
+ }
272
+
273
+ async function runLspWritethrough(
274
+ dst: string,
275
+ content: string,
276
+ cwd: string,
277
+ options: ResolvedWritethroughOptions,
278
+ changeType: FileChangeType,
279
+ signal?: AbortSignal,
280
+ file?: BunFile,
281
+ deferred?: {
282
+ onDeferredDiagnostics: (diagnostics: FileDiagnosticsResult) => void;
283
+ signal: AbortSignal;
284
+ },
285
+ runOptions?: RunLspWritethroughOptions,
286
+ ): Promise<FileDiagnosticsResult | undefined> {
287
+ const { enableFormat, enableDiagnostics } = options;
288
+ const contentAlreadyWritten = runOptions?.contentAlreadyWritten ?? false;
289
+
290
+ let finalContent = content;
291
+ const writeContent = async (value: string) => (file ? file.write(value) : Bun.write(dst, value));
292
+ const getWritePromise = once(() =>
293
+ contentAlreadyWritten && finalContent === content ? Promise.resolve() : writeContent(finalContent),
294
+ );
295
+ let writeNotified = false;
296
+ const notifyWriteCommitted = async (notifySignal: AbortSignal | undefined = signal) => {
297
+ if (writeNotified) return;
298
+ writeNotified = true;
299
+ try {
300
+ await notifyWorkspaceWatchedFiles(cwd, [{ filePath: dst, type: changeType }], notifySignal);
301
+ } catch (error) {
302
+ if (notifySignal?.aborted && !signal?.aborted) {
303
+ // The operation budget died mid-notify while the caller is still
304
+ // live: allow the post-write retry below to re-announce with the
305
+ // caller's signal (didChangeWatchedFiles is idempotent).
306
+ writeNotified = false;
307
+ return;
308
+ }
309
+ throw error;
310
+ }
311
+ };
312
+ if (!enableFormat && !enableDiagnostics) {
313
+ await getWritePromise();
314
+ await notifyWriteCommitted();
315
+ return undefined;
316
+ }
317
+
318
+ const config = getConfig(cwd);
319
+ const servers = getServersForFile(config, dst);
320
+
321
+ if (servers.length === 0) {
322
+ await getWritePromise();
323
+ await notifyWriteCommitted();
324
+ return undefined;
325
+ }
326
+ const { lspServers, customLinterServers } = splitServers(servers);
327
+ const useCustomFormatter = enableFormat && customLinterServers.length > 0;
328
+
329
+ // Capture diagnostic versions BEFORE syncing to detect stale diagnostics
330
+ // Bound client creation by the writethrough budget: a hung/broken server
331
+ // must not add its full init wait (30s default) to every edit.
332
+ const minVersionsPromise = enableDiagnostics ? captureDiagnosticVersions(cwd, servers, 5_000, signal) : undefined;
333
+ let minVersions = useCustomFormatter ? undefined : await minVersionsPromise;
334
+ let expectedDocumentVersions: ServerVersionMap | undefined;
335
+
336
+ let formatter: FileFormatResult | undefined;
337
+ let diagnostics: FileDiagnosticsResult | undefined;
338
+ let timedOut = false;
339
+ let synced = false;
340
+ let operationSignal: AbortSignal | undefined;
341
+ try {
342
+ const timeoutSignal = AbortSignal.timeout(5_000);
343
+ timeoutSignal.addEventListener(
344
+ "abort",
345
+ () => {
346
+ timedOut = true;
347
+ },
348
+ { once: true },
349
+ );
350
+ operationSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
351
+ await untilAborted(operationSignal, async () => {
352
+ if (useCustomFormatter) {
353
+ // Custom linters operate on on-disk input; the shared pre-write also
354
+ // supports implementations that inspect the file before formatting.
355
+ if (!contentAlreadyWritten) await writeContent(content);
356
+ const [formattedContent, capturedVersions] = await Promise.all([
357
+ formatContent(dst, content, cwd, customLinterServers, operationSignal),
358
+ minVersionsPromise,
359
+ ]);
360
+ finalContent = formattedContent;
361
+ minVersions = capturedVersions;
362
+ formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
363
+ if (!contentAlreadyWritten || finalContent !== content) await writeContent(finalContent);
364
+ await notifyWriteCommitted(operationSignal);
365
+ await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal, enableDiagnostics);
366
+ } else {
367
+ // 1. Sync original content to LSP servers
368
+ await syncFileContent(dst, content, cwd, lspServers, operationSignal);
369
+
370
+ // 2. Format in-memory via LSP
371
+ if (enableFormat) {
372
+ finalContent = await formatContent(dst, content, cwd, lspServers, operationSignal);
373
+ formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
374
+ }
375
+
376
+ // 3. If formatted, sync formatted content to LSP servers
377
+ if (finalContent !== content) {
378
+ await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal);
379
+ }
380
+
381
+ // 4. Write to disk
382
+ await getWritePromise();
383
+ await notifyWriteCommitted(operationSignal);
384
+ }
385
+
386
+ if (enableDiagnostics) {
387
+ expectedDocumentVersions = await captureOpenFileVersions(dst, cwd, lspServers, operationSignal);
388
+ }
389
+
390
+ // 5. Notify saved to LSP servers
391
+ await notifyFileSaved(dst, cwd, lspServers, operationSignal, !useCustomFormatter || enableDiagnostics);
392
+ });
393
+ synced = true;
394
+ } catch {
395
+ if (timedOut) {
396
+ formatter = undefined;
397
+ diagnostics = undefined;
398
+ // Schedule background diagnostic fetch if caller wants deferred results
399
+ if (deferred && !deferred.signal.aborted && enableDiagnostics) {
400
+ void scheduleDeferredDiagnosticsFetch({
401
+ dst,
402
+ cwd,
403
+ servers,
404
+ minVersions,
405
+ expectedDocumentVersions,
406
+ signal: deferred.signal,
407
+ callback: deferred.onDeferredDiagnostics,
408
+ });
409
+ }
410
+ }
411
+ await getWritePromise();
412
+ // The write above committed even though the operation budget elapsed:
413
+ // announce it on the caller's signal — the dead `operationSignal` would
414
+ // abort the notify before it ever reaches the server.
415
+ await notifyWriteCommitted();
416
+ }
417
+
418
+ if (synced && enableDiagnostics) {
419
+ diagnostics = await fetchDiagnosticsWithDeferral({
420
+ dst,
421
+ cwd,
422
+ servers,
423
+ minVersions,
424
+ expectedDocumentVersions,
425
+ transformDiagnostics: options.transformDiagnostics,
426
+ deferred,
427
+ signal,
428
+ });
429
+ }
430
+
431
+ if (formatter !== undefined) {
432
+ diagnostics ??= {
433
+ server: servers.map(([name]) => name).join(", "),
434
+ messages: [],
435
+ summary: "OK",
436
+ errored: false,
437
+ };
438
+ diagnostics.formatter = formatter;
439
+ }
440
+
441
+ return diagnostics;
442
+ }
443
+
444
+ async function flushWritethroughBatch(
445
+ batch: PendingWritethrough[],
446
+ cwd: string,
447
+ options: ResolvedWritethroughOptions,
448
+ signal?: AbortSignal,
449
+ getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
450
+ ): Promise<FileDiagnosticsResult | undefined> {
451
+ if (batch.length === 0) {
452
+ return undefined;
453
+ }
454
+ const results: Array<FileDiagnosticsResult | undefined> = [];
455
+ for (const entry of batch) {
456
+ const bundle = getDeferred?.(entry.dst);
457
+ let content: string;
458
+ try {
459
+ content = await fs.promises.readFile(entry.dst, "utf8");
460
+ } catch (error) {
461
+ if (!isEnoent(error)) throw error;
462
+ bundle?.finalize(undefined);
463
+ continue;
464
+ }
465
+ const deferredInner =
466
+ bundle &&
467
+ ({
468
+ onDeferredDiagnostics: bundle.onDeferredDiagnostics,
469
+ signal: bundle.signal,
470
+ } as const);
471
+ const diag = await runLspWritethrough(
472
+ entry.dst,
473
+ content,
474
+ cwd,
475
+ options,
476
+ entry.changeType,
477
+ signal,
478
+ entry.file,
479
+ deferredInner,
480
+ { contentAlreadyWritten: true },
481
+ );
482
+ bundle?.finalize(diag);
483
+ results.push(diag);
484
+ }
485
+ return mergeDiagnostics(results, options);
486
+ }
487
+
488
+ /** Create a writethrough callback for LSP aware write operations */
489
+ export function createLspWritethrough(cwd: string, options?: WritethroughOptions): WritethroughCallback {
490
+ const resolvedOptions: ResolvedWritethroughOptions = {
491
+ enableFormat: options?.enableFormat ?? false,
492
+ enableDiagnostics: options?.enableDiagnostics ?? false,
493
+ transformDiagnostics: options?.transformDiagnostics,
494
+ };
495
+ return async (
496
+ dst: string,
497
+ content: string,
498
+ signal?: AbortSignal,
499
+ file?: BunFile,
500
+ batch?: LspWritethroughBatchRequest,
501
+ getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
502
+ ) => {
503
+ const changeType = (await Bun.file(dst).exists()) ? FileChangeType.Changed : FileChangeType.Created;
504
+ if (!batch) {
505
+ const bundle = getDeferred?.(dst);
506
+ const deferredInner =
507
+ bundle &&
508
+ ({
509
+ onDeferredDiagnostics: bundle.onDeferredDiagnostics,
510
+ signal: bundle.signal,
511
+ } as const);
512
+ const diagnostics = await runLspWritethrough(
513
+ dst,
514
+ content,
515
+ cwd,
516
+ resolvedOptions,
517
+ changeType,
518
+ signal,
519
+ file,
520
+ deferredInner,
521
+ );
522
+ bundle?.finalize(diagnostics);
523
+ return diagnostics;
524
+ }
525
+
526
+ // File commits are never deferred: the batch owns only LSP post-processing,
527
+ // so a later flush cannot replay an obsolete whole-file snapshot.
528
+ try {
529
+ await writethroughNoop(dst, content, signal, file);
530
+ } catch (error) {
531
+ if (batch.flush) {
532
+ const pending = writethroughBatches.get(batch.id);
533
+ if (pending) {
534
+ writethroughBatches.delete(batch.id);
535
+ try {
536
+ await flushWritethroughBatch(
537
+ Array.from(pending.entries.values()),
538
+ cwd,
539
+ pending.options,
540
+ signal,
541
+ getDeferred,
542
+ );
543
+ } catch (flushError) {
544
+ logger.warn("Failed to flush pending LSP batch after final write failure", {
545
+ batchId: batch.id,
546
+ error: flushError instanceof Error ? flushError.message : String(flushError),
547
+ });
548
+ }
549
+ }
550
+ }
551
+ throw error;
552
+ }
553
+
554
+ const state = getOrCreateWritethroughBatch(batch.id, resolvedOptions);
555
+ state.entries.set(dst, { dst, file, changeType });
556
+ if (!batch.flush) return undefined;
557
+
558
+ writethroughBatches.delete(batch.id);
559
+ return flushWritethroughBatch(Array.from(state.entries.values()), cwd, state.options, signal, getDeferred);
560
+ };
561
+ }
package/src/main.ts CHANGED
@@ -1033,11 +1033,12 @@ export async function buildSessionOptions(
1033
1033
  if (parsed.noPrewalk && (parsed.prewalk || parsed.prewalkInto !== undefined)) {
1034
1034
  throw new Error("--no-prewalk cannot be combined with --prewalk or --prewalk-into");
1035
1035
  }
1036
+ const explicitPrewalk = parsed.prewalk === true || parsed.prewalkInto !== undefined;
1036
1037
  const prewalkEnabled = parsed.noPrewalk
1037
1038
  ? false
1038
- : parsed.prewalk === true || parsed.prewalkInto !== undefined
1039
+ : explicitPrewalk
1039
1040
  ? true
1040
- : activeSettings.get("prewalk.enabled");
1041
+ : !restoringSession && activeSettings.get("prewalk.enabled");
1041
1042
  if (prewalkEnabled) {
1042
1043
  const rolePattern = expandRoleAlias(parsed.prewalkInto ?? DEFAULT_PREWALK_TARGET, activeSettings);
1043
1044
  const resolved = resolveCliModel({ cliModel: rolePattern, modelRegistry, preferences: modelMatchPreferences });
package/src/mcp/config.ts CHANGED
@@ -54,6 +54,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
54
54
  };
55
55
  if (server.args) config.args = server.args;
56
56
  if (server.env) config.env = server.env;
57
+ if (server.envPolicy) config.envPolicy = server.envPolicy;
57
58
  if (server.cwd) config.cwd = server.cwd;
58
59
  return config;
59
60
  }
@@ -65,6 +66,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
65
66
  url: server.url ?? "",
66
67
  };
67
68
  if (server.headers) config.headers = server.headers;
69
+ if (server.headerPolicy) config.headerPolicy = server.headerPolicy;
68
70
  return config;
69
71
  }
70
72
 
@@ -75,6 +77,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
75
77
  url: server.url ?? "",
76
78
  };
77
79
  if (server.headers) config.headers = server.headers;
80
+ if (server.headerPolicy) config.headerPolicy = server.headerPolicy;
78
81
  return config;
79
82
  }
80
83
 
@@ -37,6 +37,7 @@ import type { McpConnectionStatusEvent } from "./startup-events";
37
37
  import type { MCPToolDetails } from "./tool-bridge";
38
38
  import { DeferredMCPTool, MCPTool } from "./tool-bridge";
39
39
  import type { MCPToolCache } from "./tool-cache";
40
+ import { setGeneratedHeader } from "./transports/header-policy";
40
41
  import type {
41
42
  MCPAuthChallenge,
42
43
  MCPGetPromptResult,
@@ -1458,13 +1459,11 @@ export class MCPManager {
1458
1459
 
1459
1460
  if (credential) {
1460
1461
  if (resolved.type === "http" || resolved.type === "sse") {
1461
- resolved = {
1462
- ...resolved,
1463
- headers: {
1464
- ...resolved.headers,
1465
- Authorization: `Bearer ${credential.access}`,
1466
- },
1467
- };
1462
+ // Client-generated authorization wins over any configured header
1463
+ // with the same case-insensitive name (Agent Plugins §7.2.1).
1464
+ const headers = { ...resolved.headers };
1465
+ setGeneratedHeader(headers, "Authorization", `Bearer ${credential.access}`);
1466
+ resolved = { ...resolved, headers };
1468
1467
  } else {
1469
1468
  resolved = {
1470
1469
  ...resolved,
@@ -1481,7 +1480,9 @@ export class MCPManager {
1481
1480
  }
1482
1481
 
1483
1482
  if (resolved.type !== "http" && resolved.type !== "sse") {
1484
- if (resolved.env) {
1483
+ // Literal env values (Agent Plugins §§4.1/9.2) are opaque package data:
1484
+ // no env-name lookup, no `!command` execution, no dropping empty values.
1485
+ if (resolved.env && resolved.envPolicy !== "literal") {
1485
1486
  const nextEnv: Record<string, string> = {};
1486
1487
  for (const [key, value] of Object.entries(resolved.env)) {
1487
1488
  const resolvedValue = await resolveConfigValue(value);
@@ -1490,7 +1491,9 @@ export class MCPManager {
1490
1491
  resolved = { ...resolved, env: nextEnv };
1491
1492
  }
1492
1493
  } else {
1493
- if (resolved.headers) {
1494
+ // Origin-locked servers (Agent Plugins §9.2) carry literal header
1495
+ // values: no placeholder or environment-variable expansion.
1496
+ if (resolved.headers && resolved.headerPolicy !== "origin-locked") {
1494
1497
  const nextHeaders: Record<string, string> = {};
1495
1498
  for (const [key, value] of Object.entries(resolved.headers)) {
1496
1499
  const resolvedValue = await resolveConfigValue(value);