@oh-my-pi/pi-coding-agent 12.7.6 → 12.8.0
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.
- package/CHANGELOG.md +37 -37
- package/README.md +9 -1052
- package/package.json +7 -7
- package/src/cli/args.ts +1 -0
- package/src/cli/update-cli.ts +49 -35
- package/src/cli/web-search-cli.ts +3 -2
- package/src/commands/web-search.ts +1 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/settings-schema.ts +25 -3
- package/src/config/settings.ts +1 -0
- package/src/extensibility/extensions/wrapper.ts +20 -13
- package/src/extensibility/slash-commands.ts +12 -91
- package/src/lsp/client.ts +24 -27
- package/src/lsp/index.ts +92 -42
- package/src/mcp/config-writer.ts +33 -0
- package/src/mcp/config.ts +6 -1
- package/src/mcp/types.ts +1 -0
- package/src/modes/components/custom-editor.ts +8 -5
- package/src/modes/components/settings-defs.ts +2 -1
- package/src/modes/controllers/command-controller.ts +12 -6
- package/src/modes/controllers/input-controller.ts +21 -186
- package/src/modes/controllers/mcp-command-controller.ts +60 -3
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/sdk.ts +23 -1
- package/src/secrets/index.ts +116 -0
- package/src/secrets/obfuscator.ts +269 -0
- package/src/secrets/regex.ts +21 -0
- package/src/session/agent-session.ts +143 -21
- package/src/session/compaction/branch-summarization.ts +2 -2
- package/src/session/compaction/compaction.ts +10 -3
- package/src/session/compaction/utils.ts +25 -1
- package/src/slash-commands/builtin-registry.ts +419 -0
- package/src/web/scrapers/github.ts +50 -12
- package/src/web/search/index.ts +5 -5
- package/src/web/search/provider.ts +13 -2
- package/src/web/search/providers/brave.ts +165 -0
- package/src/web/search/types.ts +1 -1
- package/docs/compaction.md +0 -436
- package/docs/config-usage.md +0 -176
- package/docs/custom-tools.md +0 -585
- package/docs/environment-variables.md +0 -257
- package/docs/extension-loading.md +0 -106
- package/docs/extensions.md +0 -1342
- package/docs/fs-scan-cache-architecture.md +0 -50
- package/docs/hooks.md +0 -906
- package/docs/models.md +0 -234
- package/docs/python-repl.md +0 -110
- package/docs/rpc.md +0 -1173
- package/docs/sdk.md +0 -1039
- package/docs/session-tree-plan.md +0 -84
- package/docs/session.md +0 -368
- package/docs/skills.md +0 -254
- package/docs/theme.md +0 -696
- package/docs/tree.md +0 -206
- package/docs/tui.md +0 -487
|
@@ -2,12 +2,12 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import { copyToClipboard, readImageFromClipboard, sanitizeText } from "@oh-my-pi/pi-natives";
|
|
4
4
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import type { SettingPath, SettingValue } from "../../config/settings";
|
|
6
5
|
import { settings } from "../../config/settings";
|
|
7
6
|
import { theme } from "../../modes/theme/theme";
|
|
8
7
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
9
8
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
10
9
|
import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
|
|
10
|
+
import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
|
|
11
11
|
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
12
12
|
import { resizeImage } from "../../utils/image-resize";
|
|
13
13
|
import { generateSessionTitle, setTerminalTitle } from "../../utils/title-generator";
|
|
@@ -24,6 +24,19 @@ export class InputController {
|
|
|
24
24
|
constructor(private ctx: InteractiveModeContext) {}
|
|
25
25
|
|
|
26
26
|
setupKeyHandlers(): void {
|
|
27
|
+
this.ctx.editor.shouldBypassAutocompleteOnEscape = () =>
|
|
28
|
+
Boolean(
|
|
29
|
+
this.ctx.loadingAnimation ||
|
|
30
|
+
this.ctx.session.isStreaming ||
|
|
31
|
+
this.ctx.session.isCompacting ||
|
|
32
|
+
this.ctx.session.isGeneratingHandoff ||
|
|
33
|
+
this.ctx.session.isBashRunning ||
|
|
34
|
+
this.ctx.session.isPythonRunning ||
|
|
35
|
+
this.ctx.autoCompactionLoader ||
|
|
36
|
+
this.ctx.retryLoader ||
|
|
37
|
+
this.ctx.autoCompactionEscapeHandler ||
|
|
38
|
+
this.ctx.retryEscapeHandler,
|
|
39
|
+
);
|
|
27
40
|
this.ctx.editor.onEscape = () => {
|
|
28
41
|
if (this.ctx.loadingAnimation) {
|
|
29
42
|
this.restoreQueuedMessagesToEditor({ abort: true });
|
|
@@ -172,191 +185,13 @@ export class InputController {
|
|
|
172
185
|
|
|
173
186
|
if (!text) return;
|
|
174
187
|
|
|
175
|
-
// Handle slash commands
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
await this.ctx.handlePlanModeCommand();
|
|
183
|
-
this.ctx.editor.setText("");
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
if (text === "/model" || text === "/models") {
|
|
187
|
-
this.ctx.showModelSelector();
|
|
188
|
-
this.ctx.editor.setText("");
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (text.startsWith("/export")) {
|
|
192
|
-
await this.ctx.handleExportCommand(text);
|
|
193
|
-
this.ctx.editor.setText("");
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (text === "/dump") {
|
|
197
|
-
await this.ctx.handleDumpCommand();
|
|
198
|
-
this.ctx.editor.setText("");
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (text === "/share") {
|
|
202
|
-
await this.ctx.handleShareCommand();
|
|
203
|
-
this.ctx.editor.setText("");
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
if (text === "/browser" || text.startsWith("/browser ")) {
|
|
207
|
-
const arg = text.slice(8).trim().toLowerCase();
|
|
208
|
-
const current = settings.get("browser.headless" as SettingPath) as boolean;
|
|
209
|
-
let next = current;
|
|
210
|
-
if (!(settings.get("browser.enabled" as SettingPath) as boolean)) {
|
|
211
|
-
this.ctx.showWarning("Browser tool is disabled (enable in settings)");
|
|
212
|
-
this.ctx.editor.setText("");
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
if (!arg) {
|
|
216
|
-
next = !current;
|
|
217
|
-
} else if (["headless", "hidden"].includes(arg)) {
|
|
218
|
-
next = true;
|
|
219
|
-
} else if (["visible", "show", "headful"].includes(arg)) {
|
|
220
|
-
next = false;
|
|
221
|
-
} else {
|
|
222
|
-
this.ctx.showStatus("Usage: /browser [headless|visible]");
|
|
223
|
-
this.ctx.editor.setText("");
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
settings.set("browser.headless" as SettingPath, next as SettingValue<SettingPath>);
|
|
227
|
-
const tool = this.ctx.session.getToolByName("browser");
|
|
228
|
-
if (tool && "restartForModeChange" in tool) {
|
|
229
|
-
try {
|
|
230
|
-
await (tool as { restartForModeChange: () => Promise<void> }).restartForModeChange();
|
|
231
|
-
} catch (error) {
|
|
232
|
-
this.ctx.showWarning(
|
|
233
|
-
`Failed to restart browser: ${error instanceof Error ? error.message : String(error)}`,
|
|
234
|
-
);
|
|
235
|
-
this.ctx.editor.setText("");
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
this.ctx.showStatus(`Browser mode: ${next ? "headless" : "visible"}`);
|
|
240
|
-
this.ctx.editor.setText("");
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (text === "/copy") {
|
|
244
|
-
await this.ctx.handleCopyCommand();
|
|
245
|
-
this.ctx.editor.setText("");
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (text === "/session") {
|
|
249
|
-
await this.ctx.handleSessionCommand();
|
|
250
|
-
this.ctx.editor.setText("");
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
if (text === "/usage") {
|
|
254
|
-
await this.ctx.handleUsageCommand();
|
|
255
|
-
this.ctx.editor.setText("");
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
if (text === "/changelog") {
|
|
259
|
-
await this.ctx.handleChangelogCommand();
|
|
260
|
-
this.ctx.editor.setText("");
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (text === "/hotkeys") {
|
|
264
|
-
this.ctx.handleHotkeysCommand();
|
|
265
|
-
this.ctx.editor.setText("");
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
if (text === "/extensions" || text === "/status") {
|
|
269
|
-
this.ctx.showExtensionsDashboard();
|
|
270
|
-
this.ctx.editor.setText("");
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
if (text === "/branch") {
|
|
274
|
-
if (settings.get("doubleEscapeAction") === "tree") {
|
|
275
|
-
this.ctx.showTreeSelector();
|
|
276
|
-
} else {
|
|
277
|
-
this.ctx.showUserMessageSelector();
|
|
278
|
-
}
|
|
279
|
-
this.ctx.editor.setText("");
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
if (text === "/tree") {
|
|
283
|
-
this.ctx.showTreeSelector();
|
|
284
|
-
this.ctx.editor.setText("");
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
if (text === "/login") {
|
|
288
|
-
this.ctx.showOAuthSelector("login");
|
|
289
|
-
this.ctx.editor.setText("");
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
if (text === "/logout") {
|
|
293
|
-
this.ctx.showOAuthSelector("logout");
|
|
294
|
-
this.ctx.editor.setText("");
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
if (text === "/new") {
|
|
298
|
-
this.ctx.editor.setText("");
|
|
299
|
-
await this.ctx.handleClearCommand();
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
if (text === "/fork") {
|
|
303
|
-
this.ctx.editor.setText("");
|
|
304
|
-
await this.ctx.handleForkCommand();
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (text === "/move" || text.startsWith("/move ")) {
|
|
308
|
-
const targetPath = text.slice(6).trim();
|
|
309
|
-
if (!targetPath) {
|
|
310
|
-
this.ctx.showError("Usage: /move <path>");
|
|
311
|
-
this.ctx.editor.setText("");
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
this.ctx.editor.setText("");
|
|
315
|
-
await this.ctx.handleMoveCommand(targetPath);
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
if (text === "/compact" || text.startsWith("/compact ")) {
|
|
319
|
-
const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
|
|
320
|
-
this.ctx.editor.setText("");
|
|
321
|
-
await this.ctx.handleCompactCommand(customInstructions);
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
if (text === "/handoff" || text.startsWith("/handoff ")) {
|
|
325
|
-
const customInstructions = text.startsWith("/handoff ") ? text.slice(9).trim() : undefined;
|
|
326
|
-
this.ctx.editor.setText("");
|
|
327
|
-
await this.ctx.handleHandoffCommand(customInstructions);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (text === "/background" || text === "/bg") {
|
|
331
|
-
this.ctx.editor.setText("");
|
|
332
|
-
this.handleBackgroundCommand();
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
if (text === "/debug") {
|
|
336
|
-
this.ctx.showDebugSelector();
|
|
337
|
-
this.ctx.editor.setText("");
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
if (text === "/memory" || text.startsWith("/memory ")) {
|
|
341
|
-
this.ctx.editor.setText("");
|
|
342
|
-
await this.ctx.handleMemoryCommand(text);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
if (text === "/resume") {
|
|
346
|
-
this.ctx.showSessionSelector();
|
|
347
|
-
this.ctx.editor.setText("");
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (text === "/quit" || text === "/exit") {
|
|
351
|
-
this.ctx.editor.setText("");
|
|
352
|
-
void this.ctx.shutdown();
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
// Handle MCP server management commands
|
|
356
|
-
if (text === "/mcp" || text.startsWith("/mcp ")) {
|
|
357
|
-
this.ctx.editor.addToHistory(text);
|
|
358
|
-
this.ctx.editor.setText("");
|
|
359
|
-
await this.ctx.handleMCPCommand(text);
|
|
188
|
+
// Handle built-in slash commands
|
|
189
|
+
if (
|
|
190
|
+
await executeBuiltinSlashCommand(text, {
|
|
191
|
+
ctx: this.ctx,
|
|
192
|
+
handleBackgroundCommand: () => this.handleBackgroundCommand(),
|
|
193
|
+
})
|
|
194
|
+
) {
|
|
360
195
|
return;
|
|
361
196
|
}
|
|
362
197
|
|
|
@@ -8,7 +8,14 @@ import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
|
8
8
|
import type { SourceMeta } from "../../capability/types";
|
|
9
9
|
import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp";
|
|
10
10
|
import { connectToServer, disconnectServer, listTools } from "../../mcp/client";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
addMCPServer,
|
|
13
|
+
readDisabledServers,
|
|
14
|
+
readMCPConfigFile,
|
|
15
|
+
removeMCPServer,
|
|
16
|
+
setServerDisabled,
|
|
17
|
+
updateMCPServer,
|
|
18
|
+
} from "../../mcp/config-writer";
|
|
12
19
|
import { MCPOAuthFlow } from "../../mcp/oauth-flow";
|
|
13
20
|
import type { MCPServerConfig, MCPServerConnection } from "../../mcp/types";
|
|
14
21
|
import type { OAuthCredential } from "../../session/auth-storage";
|
|
@@ -774,7 +781,12 @@ export class MCPCommandController {
|
|
|
774
781
|
}
|
|
775
782
|
}
|
|
776
783
|
|
|
777
|
-
if (
|
|
784
|
+
if (
|
|
785
|
+
userServers.length === 0 &&
|
|
786
|
+
projectServers.length === 0 &&
|
|
787
|
+
discoveredServers.length === 0 &&
|
|
788
|
+
(userConfig.disabledServers ?? []).length === 0
|
|
789
|
+
) {
|
|
778
790
|
this.#showMessage(
|
|
779
791
|
[
|
|
780
792
|
"",
|
|
@@ -868,6 +880,17 @@ export class MCPCommandController {
|
|
|
868
880
|
lines.push("");
|
|
869
881
|
}
|
|
870
882
|
}
|
|
883
|
+
|
|
884
|
+
// Show servers disabled via /mcp disable (from third-party configs)
|
|
885
|
+
const disabledServers = await readDisabledServers(userPath);
|
|
886
|
+
const relevantDisabled = disabledServers.filter(n => !configServerNames.has(n));
|
|
887
|
+
if (relevantDisabled.length > 0) {
|
|
888
|
+
lines.push(theme.fg("accent", "Disabled") + theme.fg("muted", " (discovered servers):"));
|
|
889
|
+
for (const name of relevantDisabled) {
|
|
890
|
+
lines.push(` ${theme.fg("accent", name)}${theme.fg("warning", " ◌ disabled")}`);
|
|
891
|
+
}
|
|
892
|
+
lines.push("");
|
|
893
|
+
}
|
|
871
894
|
this.#showMessage(lines.join("\n"));
|
|
872
895
|
} catch (error) {
|
|
873
896
|
this.ctx.showError(`Failed to list servers: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1061,7 +1084,41 @@ export class MCPCommandController {
|
|
|
1061
1084
|
try {
|
|
1062
1085
|
const found = await this.#findConfiguredServer(name);
|
|
1063
1086
|
if (!found) {
|
|
1064
|
-
this
|
|
1087
|
+
// Check if this is a discovered server from a third-party config
|
|
1088
|
+
const userConfigPath = getMCPConfigPath("user", getProjectDir());
|
|
1089
|
+
const disabledServers = new Set(await readDisabledServers(userConfigPath));
|
|
1090
|
+
const isDiscovered = this.ctx.mcpManager?.getSource(name);
|
|
1091
|
+
const isCurrentlyDisabled = disabledServers.has(name);
|
|
1092
|
+
if (!isDiscovered && !isCurrentlyDisabled) {
|
|
1093
|
+
this.ctx.showError(`Server "${name}" not found.`);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if (isCurrentlyDisabled === !enabled) {
|
|
1097
|
+
this.#showMessage(
|
|
1098
|
+
["", theme.fg("muted", `Server "${name}" is already ${enabled ? "enabled" : "disabled"}.`), ""].join(
|
|
1099
|
+
"\n",
|
|
1100
|
+
),
|
|
1101
|
+
);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
await setServerDisabled(userConfigPath, name, !enabled);
|
|
1105
|
+
if (enabled) {
|
|
1106
|
+
await this.#reloadMCP();
|
|
1107
|
+
const state = await this.#waitForServerConnectionWithAnimation(name);
|
|
1108
|
+
const status =
|
|
1109
|
+
state === "connected"
|
|
1110
|
+
? theme.fg("success", "Connected")
|
|
1111
|
+
: state === "connecting"
|
|
1112
|
+
? theme.fg("muted", "Connecting")
|
|
1113
|
+
: theme.fg("warning", "Not connected yet");
|
|
1114
|
+
this.#showMessage(
|
|
1115
|
+
["", theme.fg("success", `\u2713 Enabled "${name}"`), "", ` Status: ${status}`, ""].join("\n"),
|
|
1116
|
+
);
|
|
1117
|
+
} else {
|
|
1118
|
+
await this.ctx.mcpManager?.disconnectServer(name);
|
|
1119
|
+
await this.ctx.session.refreshMCPTools(this.ctx.mcpManager?.getTools() ?? []);
|
|
1120
|
+
this.#showMessage(["", theme.fg("success", `\u2713 Disabled "${name}"`), ""].join("\n"));
|
|
1121
|
+
}
|
|
1065
1122
|
return;
|
|
1066
1123
|
}
|
|
1067
1124
|
|
|
@@ -911,8 +911,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
911
911
|
return this.#commandController.handleUsageCommand(reports);
|
|
912
912
|
}
|
|
913
913
|
|
|
914
|
-
async handleChangelogCommand(): Promise<void> {
|
|
915
|
-
await this.#commandController.handleChangelogCommand();
|
|
914
|
+
async handleChangelogCommand(showFull = false): Promise<void> {
|
|
915
|
+
await this.#commandController.handleChangelogCommand(showFull);
|
|
916
916
|
}
|
|
917
917
|
|
|
918
918
|
handleHotkeysCommand(): void {
|
package/src/modes/types.ts
CHANGED
|
@@ -140,7 +140,7 @@ export interface InteractiveModeContext {
|
|
|
140
140
|
handleCopyCommand(): Promise<void>;
|
|
141
141
|
handleSessionCommand(): Promise<void>;
|
|
142
142
|
handleUsageCommand(reports?: UsageReport[] | null): Promise<void>;
|
|
143
|
-
handleChangelogCommand(): Promise<void>;
|
|
143
|
+
handleChangelogCommand(showFull?: boolean): Promise<void>;
|
|
144
144
|
handleHotkeysCommand(): void;
|
|
145
145
|
handleDumpCommand(): Promise<void>;
|
|
146
146
|
handleClearCommand(): Promise<void>;
|
package/src/sdk.ts
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
import { disposeAllKernelSessions } from "./ipy/executor";
|
|
48
48
|
import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
|
|
49
49
|
import { buildMemoryToolDeveloperInstructions, startMemoryStartupTask } from "./memories";
|
|
50
|
+
import { collectEnvSecrets, loadSecrets, obfuscateMessages, SecretObfuscator } from "./secrets";
|
|
50
51
|
import { AgentSession } from "./session/agent-session";
|
|
51
52
|
import { AuthStorage } from "./session/auth-storage";
|
|
52
53
|
import { convertToLlm } from "./session/messages";
|
|
@@ -1127,6 +1128,25 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1127
1128
|
});
|
|
1128
1129
|
};
|
|
1129
1130
|
|
|
1131
|
+
// Load and create secret obfuscator if secrets are enabled
|
|
1132
|
+
let obfuscator: SecretObfuscator | undefined;
|
|
1133
|
+
if (settings.get("secrets.enabled")) {
|
|
1134
|
+
const fileEntries = await loadSecrets(cwd, agentDir);
|
|
1135
|
+
const envEntries = collectEnvSecrets();
|
|
1136
|
+
const allEntries = [...envEntries, ...fileEntries];
|
|
1137
|
+
if (allEntries.length > 0) {
|
|
1138
|
+
obfuscator = new SecretObfuscator(allEntries);
|
|
1139
|
+
}
|
|
1140
|
+
time("loadSecrets");
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Final convertToLlm: chain block-images filter with secret obfuscation
|
|
1144
|
+
const convertToLlmFinal = (messages: AgentMessage[]): Message[] => {
|
|
1145
|
+
const converted = convertToLlmWithBlockImages(messages);
|
|
1146
|
+
if (!obfuscator?.hasSecrets()) return converted;
|
|
1147
|
+
return obfuscateMessages(obfuscator, converted);
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1130
1150
|
const setToolUIContext = (uiContext: ExtensionUIContext, hasUI: boolean) => {
|
|
1131
1151
|
toolContextStore.setUIContext(uiContext, hasUI);
|
|
1132
1152
|
};
|
|
@@ -1146,7 +1166,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1146
1166
|
thinkingLevel,
|
|
1147
1167
|
tools: initialTools,
|
|
1148
1168
|
},
|
|
1149
|
-
convertToLlm:
|
|
1169
|
+
convertToLlm: convertToLlmFinal,
|
|
1150
1170
|
sessionId: sessionManager.getSessionId(),
|
|
1151
1171
|
transformContext: extensionRunner
|
|
1152
1172
|
? async messages => {
|
|
@@ -1171,6 +1191,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1171
1191
|
return key;
|
|
1172
1192
|
},
|
|
1173
1193
|
cursorExecHandlers,
|
|
1194
|
+
transformToolCallArguments: obfuscator?.hasSecrets() ? args => obfuscator!.deobfuscateObject(args) : undefined,
|
|
1174
1195
|
});
|
|
1175
1196
|
cursorEventEmitter = event => agent.emitExternalEvent(event);
|
|
1176
1197
|
debugStartup("sdk:createAgent");
|
|
@@ -1207,6 +1228,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1207
1228
|
rebuildSystemPrompt,
|
|
1208
1229
|
ttsrManager,
|
|
1209
1230
|
forceCopilotAgentInitiator,
|
|
1231
|
+
obfuscator,
|
|
1210
1232
|
});
|
|
1211
1233
|
debugStartup("sdk:createAgentSession");
|
|
1212
1234
|
time("createAgentSession");
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { YAML } from "bun";
|
|
4
|
+
import type { SecretEntry } from "./obfuscator";
|
|
5
|
+
import { compileSecretRegex } from "./regex";
|
|
6
|
+
|
|
7
|
+
export { obfuscateMessages, type SecretEntry, SecretObfuscator } from "./obfuscator";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load secrets from project-local and global secrets.yml files.
|
|
11
|
+
* Project-local entries override global entries with matching content.
|
|
12
|
+
*/
|
|
13
|
+
export async function loadSecrets(cwd: string, agentDir: string): Promise<SecretEntry[]> {
|
|
14
|
+
const projectPath = path.join(cwd, ".omp", "secrets.yml");
|
|
15
|
+
const globalPath = path.join(agentDir, "secrets.yml");
|
|
16
|
+
|
|
17
|
+
const globalEntries = await loadSecretsFile(globalPath);
|
|
18
|
+
const projectEntries = await loadSecretsFile(projectPath);
|
|
19
|
+
|
|
20
|
+
if (globalEntries.length === 0) return projectEntries;
|
|
21
|
+
if (projectEntries.length === 0) return globalEntries;
|
|
22
|
+
|
|
23
|
+
// Merge: project overrides global by content match
|
|
24
|
+
const projectContents = new Set(projectEntries.map(e => e.content));
|
|
25
|
+
const merged = [...globalEntries.filter(e => !projectContents.has(e.content)), ...projectEntries];
|
|
26
|
+
return merged;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Minimum env var value length to consider as a secret. */
|
|
30
|
+
const MIN_ENV_VALUE_LENGTH = 8;
|
|
31
|
+
|
|
32
|
+
/** Env var name patterns that indicate secret values. */
|
|
33
|
+
const SECRET_ENV_PATTERNS = /(?:KEY|SECRET|TOKEN|PASSWORD|PASS|AUTH|CREDENTIAL|PRIVATE|OAUTH)(?:_|$)/i;
|
|
34
|
+
|
|
35
|
+
/** Collect environment variable values that look like secrets. */
|
|
36
|
+
export function collectEnvSecrets(): SecretEntry[] {
|
|
37
|
+
const entries: SecretEntry[] = [];
|
|
38
|
+
const seen = new Set<string>();
|
|
39
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
40
|
+
if (!value || value.length < MIN_ENV_VALUE_LENGTH) continue;
|
|
41
|
+
if (!SECRET_ENV_PATTERNS.test(name)) continue;
|
|
42
|
+
if (seen.has(value)) continue;
|
|
43
|
+
seen.add(value);
|
|
44
|
+
entries.push({ type: "plain", content: value, mode: "obfuscate" });
|
|
45
|
+
}
|
|
46
|
+
return entries;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function loadSecretsFile(filePath: string): Promise<SecretEntry[]> {
|
|
50
|
+
try {
|
|
51
|
+
const text = await Bun.file(filePath).text();
|
|
52
|
+
const raw = YAML.parse(text);
|
|
53
|
+
if (!Array.isArray(raw)) {
|
|
54
|
+
logger.warn("secrets.yml must be a YAML array", { path: filePath });
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const entries: SecretEntry[] = [];
|
|
58
|
+
for (let i = 0; i < raw.length; i++) {
|
|
59
|
+
const entry = raw[i];
|
|
60
|
+
if (!validateEntry(entry, filePath, i)) continue;
|
|
61
|
+
entries.push({
|
|
62
|
+
type: entry.type,
|
|
63
|
+
content: entry.content,
|
|
64
|
+
mode: entry.mode ?? "obfuscate",
|
|
65
|
+
replacement: entry.replacement,
|
|
66
|
+
flags: entry.flags,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return entries;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (isEnoent(err)) return [];
|
|
72
|
+
logger.warn("Failed to load secrets.yml", { path: filePath, error: String(err) });
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateEntry(entry: unknown, filePath: string, index: number): entry is SecretEntry {
|
|
78
|
+
if (entry === null || typeof entry !== "object") {
|
|
79
|
+
logger.warn(`secrets.yml[${index}]: entry must be an object`, { path: filePath });
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const e = entry as Record<string, unknown>;
|
|
83
|
+
if (e.type !== "plain" && e.type !== "regex") {
|
|
84
|
+
logger.warn(`secrets.yml[${index}]: type must be "plain" or "regex"`, { path: filePath });
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (typeof e.content !== "string" || e.content.length === 0) {
|
|
88
|
+
logger.warn(`secrets.yml[${index}]: content must be a non-empty string`, { path: filePath });
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (e.mode !== undefined && e.mode !== "obfuscate" && e.mode !== "replace") {
|
|
92
|
+
logger.warn(`secrets.yml[${index}]: mode must be "obfuscate" or "replace"`, { path: filePath });
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (e.replacement !== undefined && typeof e.replacement !== "string") {
|
|
96
|
+
logger.warn(`secrets.yml[${index}]: replacement must be a string`, { path: filePath });
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (e.flags !== undefined && typeof e.flags !== "string") {
|
|
100
|
+
logger.warn(`secrets.yml[${index}]: flags must be a string`, { path: filePath });
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (e.type === "regex") {
|
|
104
|
+
try {
|
|
105
|
+
compileSecretRegex(e.content as string, e.flags as string | undefined);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
logger.warn(`secrets.yml[${index}]: invalid regex pattern`, {
|
|
108
|
+
path: filePath,
|
|
109
|
+
pattern: e.content,
|
|
110
|
+
error: String(error),
|
|
111
|
+
});
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|