@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.2
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 +128 -0
- package/dist/cli.js +4006 -3966
- package/dist/types/advisor/advise-tool.d.ts +5 -2
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/cli-commands.d.ts +14 -1
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +32 -7
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
- package/dist/types/lib/xai-http.d.ts +11 -0
- package/dist/types/mcp/transports/stdio.d.ts +13 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -0
- package/dist/types/modes/acp/acp-mode.d.ts +6 -2
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/assistant-message.d.ts +2 -0
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +23 -1
- package/dist/types/modes/components/hook-editor.d.ts +16 -2
- package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +4 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/sdk.d.ts +13 -2
- package/dist/types/session/agent-session.d.ts +43 -11
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/session/streaming-output.d.ts +2 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/slash-commands/types.d.ts +4 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/browser/registry.d.ts +7 -3
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +4 -1
- package/dist/types/tools/manage-skill.d.ts +2 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +3 -0
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1358 -86
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/advise-tool.ts +7 -3
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/bench-cli.ts +4 -1
- package/src/cli/grep-cli.ts +2 -1
- package/src/cli-commands.ts +57 -23
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +34 -6
- package/src/config/model-resolver.ts +57 -12
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +43 -6
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/codex.ts +14 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/discovery/substitute-plugin-root.ts +23 -7
- package/src/exec/bash-executor.ts +14 -5
- package/src/export/html/template.js +2 -0
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
- package/src/internal-urls/agent-protocol.ts +73 -39
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lib/xai-http.ts +28 -2
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/transports/stdio.ts +103 -23
- package/src/modes/acp/acp-agent.ts +28 -9
- package/src/modes/acp/acp-event-mapper.ts +8 -0
- package/src/modes/acp/acp-mode.ts +18 -4
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/assistant-message.ts +35 -14
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
- package/src/modes/components/custom-editor.ts +62 -1
- package/src/modes/components/hook-editor.ts +35 -5
- package/src/modes/components/login-dialog.test.ts +56 -0
- package/src/modes/components/login-dialog.ts +7 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/plan-review-overlay.ts +51 -10
- package/src/modes/components/session-selector.ts +10 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/component.ts +2 -4
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/components/tool-execution.test.ts +101 -0
- package/src/modes/components/tool-execution.ts +117 -9
- package/src/modes/components/transcript-container.ts +30 -2
- package/src/modes/controllers/command-controller-shared.ts +1 -1
- package/src/modes/controllers/command-controller.ts +168 -48
- package/src/modes/controllers/event-controller.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +208 -46
- package/src/modes/interactive-mode.ts +205 -65
- package/src/modes/noninteractive-dispose.test.ts +60 -0
- package/src/modes/print-mode.ts +10 -5
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +31 -7
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +10 -0
- package/src/modes/utils/interactive-context-helpers.ts +3 -1
- package/src/modes/utils/transcript-render-helpers.ts +3 -2
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +227 -44
- package/src/session/agent-session.ts +1364 -382
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-listing.ts +6 -5
- package/src/session/session-loader.ts +9 -0
- package/src/session/session-manager.ts +48 -0
- package/src/session/session-persistence.ts +11 -0
- package/src/session/streaming-output.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +9 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/slash-commands/types.ts +4 -4
- package/src/system-prompt.ts +2 -2
- package/src/task/executor.ts +1 -1
- package/src/task/label.ts +2 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/browser/registry.ts +9 -2
- package/src/tools/browser/tab-supervisor.ts +65 -10
- package/src/tools/browser.ts +4 -3
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/eval.ts +3 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +624 -513
- package/src/tools/index.ts +10 -6
- package/src/tools/manage-skill.ts +5 -3
- package/src/tools/output-meta.ts +7 -0
- package/src/tools/path-utils.ts +8 -5
- package/src/tools/read.ts +48 -3
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/utils/title-generator.ts +15 -4
- package/src/web/search/index.ts +21 -6
- package/src/web/search/providers/base.ts +3 -0
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/providers/xai.ts +40 -9
- package/src/web/search/types.ts +6 -1
package/src/cursor.ts
CHANGED
|
@@ -18,9 +18,20 @@ import { resolveToCwd } from "./tools/path-utils";
|
|
|
18
18
|
|
|
19
19
|
interface CursorExecBridgeOptions {
|
|
20
20
|
cwd: string;
|
|
21
|
+
getCwd?: () => string;
|
|
21
22
|
tools: Map<string, AgentTool>;
|
|
23
|
+
getTool?: (name: string) => AgentTool | undefined;
|
|
22
24
|
getToolContext?: () => AgentToolContext | undefined;
|
|
23
25
|
emitEvent?: (event: AgentEvent) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the Cursor native `delete` frame may remove files. Unlike every
|
|
28
|
+
* other exec handler, `executeDelete` mutates the filesystem directly instead
|
|
29
|
+
* of consulting {@link tools}, so a background read-only advisor could delete
|
|
30
|
+
* workspace files it was never granted a mutating tool for (issue #5680
|
|
31
|
+
* review). Defaults to allowed to preserve the primary agent's behavior;
|
|
32
|
+
* callers with a restricted tool set (advisors) opt out.
|
|
33
|
+
*/
|
|
34
|
+
allowNativeDelete?: boolean;
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
function createToolResultMessage(
|
|
@@ -53,7 +64,7 @@ async function executeTool(
|
|
|
53
64
|
toolCallId: string,
|
|
54
65
|
args: Record<string, unknown>,
|
|
55
66
|
): Promise<ToolResultMessage> {
|
|
56
|
-
const tool = options.tools.get(toolName);
|
|
67
|
+
const tool = options.tools.get(toolName) ?? options.getTool?.(toolName);
|
|
57
68
|
if (!tool) {
|
|
58
69
|
const result = buildToolErrorResult(`Tool "${toolName}" not available`);
|
|
59
70
|
return createToolResultMessage(toolCallId, toolName, result, true);
|
|
@@ -106,9 +117,15 @@ async function executeTool(
|
|
|
106
117
|
|
|
107
118
|
async function executeDelete(options: CursorExecBridgeOptions, pathArg: string, toolCallId: string) {
|
|
108
119
|
const toolName = "delete";
|
|
120
|
+
|
|
121
|
+
if (options.allowNativeDelete === false) {
|
|
122
|
+
const result = buildToolErrorResult(`Tool "${toolName}" not available`);
|
|
123
|
+
return createToolResultMessage(toolCallId, toolName, result, true);
|
|
124
|
+
}
|
|
125
|
+
|
|
109
126
|
options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: { path: pathArg } });
|
|
110
127
|
|
|
111
|
-
const absolutePath = resolveToCwd(pathArg, options.cwd);
|
|
128
|
+
const absolutePath = resolveToCwd(pathArg, options.getCwd?.() ?? options.cwd);
|
|
112
129
|
let isError = false;
|
|
113
130
|
let result: AgentToolResult<unknown>;
|
|
114
131
|
|
|
@@ -327,7 +344,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
327
344
|
async mcp(call: CursorMcpCall) {
|
|
328
345
|
const toolName = call.toolName || call.name;
|
|
329
346
|
const toolCallId = decodeToolCallId(call.toolCallId);
|
|
330
|
-
const tool = this.options.tools.get(toolName);
|
|
347
|
+
const tool = this.options.tools.get(toolName) ?? this.options.getTool?.(toolName);
|
|
331
348
|
if (!tool) {
|
|
332
349
|
const availableTools = Array.from(this.options.tools.keys()).filter(name => name.startsWith("mcp__"));
|
|
333
350
|
const message = formatMcpToolErrorMessage(toolName, availableTools);
|
|
@@ -105,9 +105,10 @@ export async function createReportBundle(options: ReportBundleOptions): Promise<
|
|
|
105
105
|
files.push("config.json");
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
// Recent logs (last 1000 lines)
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
// Recent logs (last 1000 lines) across every same-day process. PID-qualified
|
|
109
|
+
// filenames mean a report generated from a later invocation must still gather
|
|
110
|
+
// the crashed process's log, so read all of today's files, not just our own.
|
|
111
|
+
const logs = await collectSameDayLogs(1000);
|
|
111
112
|
if (logs) {
|
|
112
113
|
data["logs.txt"] = logs;
|
|
113
114
|
files.push("logs.txt");
|
|
@@ -241,7 +242,42 @@ export async function getLogText(): Promise<string> {
|
|
|
241
242
|
return readLastLines(getLogPath(), MAX_LOG_LINES);
|
|
242
243
|
}
|
|
243
244
|
|
|
244
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Concatenate the tail of every same-day process log so a report generated
|
|
247
|
+
* after a crash still captures the fatal PID's `omp.<date>.<pid>.log`. Files
|
|
248
|
+
* are ordered oldest-first by mtime and separated by a filename header.
|
|
249
|
+
*/
|
|
250
|
+
async function collectSameDayLogs(linesPerFile: number): Promise<string> {
|
|
251
|
+
const logsDir = getLogsDir();
|
|
252
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
253
|
+
const sameDay: Array<{ name: string; mtimeMs: number }> = [];
|
|
254
|
+
try {
|
|
255
|
+
const entries = await fs.readdir(logsDir, { withFileTypes: true });
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
if (!entry.isFile()) continue;
|
|
258
|
+
const match = LOG_FILE_PATTERN.exec(entry.name);
|
|
259
|
+
if (!match || match[1] !== today) continue;
|
|
260
|
+
try {
|
|
261
|
+
const stat = await fs.stat(path.join(logsDir, entry.name));
|
|
262
|
+
sameDay.push({ name: entry.name, mtimeMs: stat.mtimeMs });
|
|
263
|
+
} catch {
|
|
264
|
+
// File may have rotated away between readdir and stat.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
return "";
|
|
269
|
+
}
|
|
270
|
+
sameDay.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
271
|
+
|
|
272
|
+
const chunks: string[] = [];
|
|
273
|
+
for (const { name } of sameDay) {
|
|
274
|
+
const text = await readLastLines(path.join(logsDir, name), linesPerFile);
|
|
275
|
+
if (text) chunks.push(`===== ${name} =====\n${text}`);
|
|
276
|
+
}
|
|
277
|
+
return chunks.join("\n\n");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const LOG_FILE_PATTERN = new RegExp(`^${APP_NAME}\\.(\\d{4}-\\d{2}-\\d{2})\\.\\d+\\.log(?:\\.\\d+)?$`);
|
|
245
281
|
|
|
246
282
|
export async function createDebugLogSource(): Promise<DebugLogSource> {
|
|
247
283
|
const logsDir = getLogsDir();
|
package/src/discovery/codex.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
SOURCE_PATHS,
|
|
38
38
|
scanSkillsFromDir,
|
|
39
39
|
} from "./helpers";
|
|
40
|
+
import { resolvePluginStdioPaths } from "./substitute-plugin-root";
|
|
40
41
|
|
|
41
42
|
const PROVIDER_ID = "codex";
|
|
42
43
|
const DISPLAY_NAME = "OpenAI Codex";
|
|
@@ -87,7 +88,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
87
88
|
|
|
88
89
|
const items: MCPServer[] = [];
|
|
89
90
|
if (userConfig) {
|
|
90
|
-
const servers = extractMCPServersFromToml(userConfig);
|
|
91
|
+
const servers = extractMCPServersFromToml(userConfig, path.dirname(userConfigPath));
|
|
91
92
|
for (const [name, config] of Object.entries(servers)) {
|
|
92
93
|
items.push({
|
|
93
94
|
name,
|
|
@@ -97,7 +98,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
if (projectConfig) {
|
|
100
|
-
const servers = extractMCPServersFromToml(projectConfig);
|
|
101
|
+
const servers = extractMCPServersFromToml(projectConfig, path.dirname(projectConfigPath));
|
|
101
102
|
for (const [name, config] of Object.entries(servers)) {
|
|
102
103
|
items.push({
|
|
103
104
|
name,
|
|
@@ -139,7 +140,10 @@ interface CodexMCPConfig {
|
|
|
139
140
|
disabled_tools?: string[];
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
function extractMCPServersFromToml(
|
|
143
|
+
function extractMCPServersFromToml(
|
|
144
|
+
toml: Record<string, unknown>,
|
|
145
|
+
configDir: string,
|
|
146
|
+
): Record<string, Partial<MCPServer>> {
|
|
143
147
|
// Check for [mcp_servers.*] sections (Codex format)
|
|
144
148
|
if (!toml.mcp_servers || typeof toml.mcp_servers !== "object") {
|
|
145
149
|
return {};
|
|
@@ -149,10 +153,16 @@ function extractMCPServersFromToml(toml: Record<string, unknown>): Record<string
|
|
|
149
153
|
const result: Record<string, Partial<MCPServer>> = {};
|
|
150
154
|
|
|
151
155
|
for (const [name, config] of Object.entries(codexServers)) {
|
|
156
|
+
// Root relative cwd/command against the Codex config directory. Codex
|
|
157
|
+
// spawns the process with the resolved cwd, so a relative command is
|
|
158
|
+
// resolved by the OS from there — pass "cwd" so e.g. cwd="server",
|
|
159
|
+
// command="./bin/mcp" resolves to <configDir>/server/bin/mcp.
|
|
160
|
+
const rooted = resolvePluginStdioPaths({ command: config.command, cwd: config.cwd }, configDir, "cwd");
|
|
152
161
|
const server: Partial<MCPServer> = {
|
|
153
|
-
command:
|
|
162
|
+
...(rooted.command !== undefined && { command: rooted.command }),
|
|
154
163
|
args: config.args,
|
|
155
164
|
url: config.url,
|
|
165
|
+
...(rooted.cwd !== undefined && { cwd: rooted.cwd }),
|
|
156
166
|
};
|
|
157
167
|
|
|
158
168
|
// Build env by merging explicit env and forwarded env_vars
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -741,13 +741,16 @@ export function buildExtensionModuleItems(
|
|
|
741
741
|
* Entry for an installed Claude Code plugin.
|
|
742
742
|
*/
|
|
743
743
|
export interface ClaudePluginEntry {
|
|
744
|
-
scope
|
|
744
|
+
/** Claude registry scope; local entries are restricted to their project path. */
|
|
745
|
+
scope?: "user" | "project" | "local";
|
|
745
746
|
installPath: string;
|
|
746
747
|
version: string;
|
|
747
748
|
installedAt: string;
|
|
748
749
|
lastUpdated: string;
|
|
749
750
|
gitCommitSha?: string;
|
|
750
751
|
enabled?: boolean;
|
|
752
|
+
/** Project root recorded by Claude for a local installation. */
|
|
753
|
+
projectPath?: string;
|
|
751
754
|
}
|
|
752
755
|
|
|
753
756
|
/**
|
|
@@ -862,6 +865,14 @@ export async function resolveOrDefaultProjectRegistryPath(cwd: string): Promise<
|
|
|
862
865
|
return path.join(cwd, getConfigDirName(), "plugins", "installed_plugins.json");
|
|
863
866
|
}
|
|
864
867
|
|
|
868
|
+
async function canonicalClaudeProjectPath(projectPath: string): Promise<string | null> {
|
|
869
|
+
try {
|
|
870
|
+
return await fs.promises.realpath(path.resolve(projectPath));
|
|
871
|
+
} catch {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
865
876
|
const pluginRootsCache = new Map<string, { roots: ClaudePluginRoot[]; warnings: string[] }>();
|
|
866
877
|
|
|
867
878
|
const pluginCacheInvalidators = new Set<() => void>();
|
|
@@ -876,20 +887,23 @@ export function registerPluginCacheInvalidator(invalidator: () => void): void {
|
|
|
876
887
|
* Reads ~/.claude/plugins/installed_plugins.json and ~/.omp/plugins/installed_plugins.json,
|
|
877
888
|
* and optionally the nearest project-scoped registry resolved from `cwd`.
|
|
878
889
|
*
|
|
879
|
-
* Results are cached per
|
|
890
|
+
* Results are cached per home, project registry, and canonical active project.
|
|
880
891
|
*/
|
|
881
892
|
export async function listClaudePluginRoots(
|
|
882
893
|
home: string,
|
|
883
894
|
cwd?: string,
|
|
884
895
|
): Promise<{ roots: ClaudePluginRoot[]; warnings: string[] }> {
|
|
885
896
|
const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd) : null;
|
|
886
|
-
const
|
|
897
|
+
const projectRoot = resolvedProjectPath ? path.dirname(path.dirname(path.dirname(resolvedProjectPath))) : cwd;
|
|
898
|
+
const activeClaudeProjectPath = projectRoot ? await canonicalClaudeProjectPath(projectRoot) : null;
|
|
899
|
+
const cacheKey = `${home}:${resolvedProjectPath ?? ""}:${activeClaudeProjectPath ?? ""}`;
|
|
887
900
|
const cached = pluginRootsCache.get(cacheKey);
|
|
888
901
|
if (cached) return cached;
|
|
889
902
|
|
|
890
903
|
const roots: ClaudePluginRoot[] = [];
|
|
891
904
|
const warnings: string[] = [];
|
|
892
905
|
const projectRoots: ClaudePluginRoot[] = [];
|
|
906
|
+
const canonicalClaudeProjectPaths = new Map<string, string | null>();
|
|
893
907
|
|
|
894
908
|
// ── Claude Code registry ──────────────────────────────────────────────────
|
|
895
909
|
const registryPath = path.join(home, ".claude", "plugins", "installed_plugins.json");
|
|
@@ -921,6 +935,15 @@ export async function listClaudePluginRoots(
|
|
|
921
935
|
continue;
|
|
922
936
|
}
|
|
923
937
|
if (entry.enabled === false) continue;
|
|
938
|
+
if (entry.scope === "local") {
|
|
939
|
+
if (!entry.projectPath || !activeClaudeProjectPath) continue;
|
|
940
|
+
let entryProjectPath = canonicalClaudeProjectPaths.get(entry.projectPath);
|
|
941
|
+
if (entryProjectPath === undefined) {
|
|
942
|
+
entryProjectPath = await canonicalClaudeProjectPath(entry.projectPath);
|
|
943
|
+
canonicalClaudeProjectPaths.set(entry.projectPath, entryProjectPath);
|
|
944
|
+
}
|
|
945
|
+
if (entryProjectPath !== activeClaudeProjectPath) continue;
|
|
946
|
+
}
|
|
924
947
|
|
|
925
948
|
roots.push({
|
|
926
949
|
id: pluginId,
|
|
@@ -928,7 +951,7 @@ export async function listClaudePluginRoots(
|
|
|
928
951
|
plugin: pluginName,
|
|
929
952
|
version: entry.version || "unknown",
|
|
930
953
|
path: entry.installPath,
|
|
931
|
-
scope: entry.scope || "user",
|
|
954
|
+
scope: entry.scope === "local" ? "project" : entry.scope || "user",
|
|
932
955
|
});
|
|
933
956
|
}
|
|
934
957
|
}
|
|
@@ -976,7 +999,7 @@ export async function listClaudePluginRoots(
|
|
|
976
999
|
plugin: pluginName,
|
|
977
1000
|
version: entry.version || "unknown",
|
|
978
1001
|
path: entry.installPath,
|
|
979
|
-
scope: entry.scope || "user",
|
|
1002
|
+
scope: entry.scope === "local" ? "project" : entry.scope || "user",
|
|
980
1003
|
});
|
|
981
1004
|
}
|
|
982
1005
|
}
|
|
@@ -31,23 +31,38 @@ export function substitutePluginRoot<T>(value: T, rootPath: string): T {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
* the directory of the `.mcp.json` that declared them.
|
|
34
|
+
* Where a relative, path-like `command` is rooted by {@link resolvePluginStdioPaths}.
|
|
36
35
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
36
|
+
* - `"config-dir"` (default): the directory of the config file that declared the
|
|
37
|
+
* server — the plugin package root for `.mcp.json`. A plugin can ship its
|
|
38
|
+
* executable at the package root (`command: "./bin/server"`) yet run from a
|
|
39
|
+
* data subdir (`cwd: "work"`); the command stays `<pkg>/bin/server`.
|
|
40
|
+
* - `"cwd"`: the rooted `cwd`, falling back to the config dir when no `cwd` is
|
|
41
|
+
* set. This matches how the OS resolves a relative command against the
|
|
42
|
+
* subprocess's working directory, which is the Codex `config.toml` contract:
|
|
43
|
+
* `cwd = "server"`, `command = "./bin/mcp"` → `<configDir>/server/bin/mcp`.
|
|
44
|
+
*/
|
|
45
|
+
export type StdioCommandBase = "config-dir" | "cwd";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Rebase relative filesystem values in a discovered stdio server config against
|
|
49
|
+
* the directory of the config file (`.mcp.json`/`config.toml`) that declared them.
|
|
50
|
+
*
|
|
51
|
+
* External configs (bundled ChatGPT/Codex plugins, Claude marketplace plugins)
|
|
52
|
+
* express `command`/`cwd` relative to their own config file, but MCP stdio
|
|
53
|
+
* spawning roots relative values at the session cwd — so a server shipping
|
|
40
54
|
* `command: "./bin/server"`, `cwd: "."` launches from the wrong directory and
|
|
41
55
|
* fails with ENOENT. This resolves those against `configDir` instead:
|
|
42
56
|
*
|
|
43
57
|
* - relative `cwd` → resolved against `configDir`;
|
|
44
58
|
* - path-like `command` (`./`, `../`, or the Windows `.\`/`..\` forms) →
|
|
45
|
-
* resolved against `
|
|
59
|
+
* resolved against the base selected by `commandBase` (see {@link StdioCommandBase});
|
|
46
60
|
* - bare executables (`npx`, `uvx`, …) and absolute paths are left untouched.
|
|
47
61
|
*/
|
|
48
62
|
export function resolvePluginStdioPaths(
|
|
49
63
|
config: { command?: string; cwd?: string },
|
|
50
64
|
configDir: string,
|
|
65
|
+
commandBase: StdioCommandBase = "config-dir",
|
|
51
66
|
): { command?: string; cwd?: string } {
|
|
52
67
|
const resolved: { command?: string; cwd?: string } = {};
|
|
53
68
|
if (typeof config.cwd === "string") {
|
|
@@ -55,7 +70,8 @@ export function resolvePluginStdioPaths(
|
|
|
55
70
|
}
|
|
56
71
|
if (config.command !== undefined) {
|
|
57
72
|
const isPathLike = /^\.\.?[/\\]/.test(config.command);
|
|
58
|
-
|
|
73
|
+
const base = commandBase === "cwd" ? (resolved.cwd ?? configDir) : configDir;
|
|
74
|
+
resolved.command = isPathLike ? path.resolve(base, config.command) : config.command;
|
|
59
75
|
}
|
|
60
76
|
return resolved;
|
|
61
77
|
}
|
|
@@ -45,6 +45,8 @@ export interface BashResult {
|
|
|
45
45
|
output: string;
|
|
46
46
|
exitCode: number | undefined;
|
|
47
47
|
cancelled: boolean;
|
|
48
|
+
/** True when the command was killed by its timeout deadline (not a user abort). */
|
|
49
|
+
timedOut?: boolean;
|
|
48
50
|
truncated: boolean;
|
|
49
51
|
totalLines: number;
|
|
50
52
|
totalBytes: number;
|
|
@@ -70,6 +72,9 @@ const shellSessionsInUse = new Set<string>();
|
|
|
70
72
|
*/
|
|
71
73
|
const retainedShells = new Set<Shell>();
|
|
72
74
|
const RETAIN_REAP_INTERVAL_MS = 5_000;
|
|
75
|
+
// Native cancellation may spend two seconds unwinding the shell before its
|
|
76
|
+
// N-API chunk bridge drains. The JS watchdog must not race that teardown.
|
|
77
|
+
const NATIVE_TIMEOUT_FALLBACK_GRACE_MS = 5_000;
|
|
73
78
|
|
|
74
79
|
async function retainShellWithLiveBackgroundJobs(shell: Shell): Promise<void> {
|
|
75
80
|
let live: number;
|
|
@@ -306,16 +311,18 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
306
311
|
const nativeTimeoutMs = requestedTimeoutMs !== undefined && requestedTimeoutMs > 0 ? requestedTimeoutMs : undefined;
|
|
307
312
|
const nativeOwnsTimeout = nativeTimeoutMs !== undefined;
|
|
308
313
|
if (deadlineTimeoutMs !== undefined) {
|
|
314
|
+
const fallbackTimeoutMs = nativeOwnsTimeout
|
|
315
|
+
? deadlineTimeoutMs + NATIVE_TIMEOUT_FALLBACK_GRACE_MS
|
|
316
|
+
: deadlineTimeoutMs;
|
|
309
317
|
timeoutTimer = setTimeout(() => {
|
|
310
|
-
// Explicit timeouts are
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
// can terminate the Bun host before the native timeout result resolves.
|
|
318
|
+
// Explicit timeouts are enforced inside pi-natives via `timeoutMs`.
|
|
319
|
+
// Give native cancellation time to flush pipeline output and drain the
|
|
320
|
+
// N-API bridge before this result-only watchdog quarantines the run.
|
|
314
321
|
if (!nativeOwnsTimeout) {
|
|
315
322
|
abortCurrentExecution();
|
|
316
323
|
}
|
|
317
324
|
timeoutDeferred.resolve("timeout");
|
|
318
|
-
},
|
|
325
|
+
}, fallbackTimeoutMs);
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
let resetSession = false;
|
|
@@ -357,6 +364,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
357
364
|
return {
|
|
358
365
|
exitCode: undefined,
|
|
359
366
|
cancelled: true,
|
|
367
|
+
...(winner.kind === "timeout" ? { timedOut: true } : {}),
|
|
360
368
|
...(await sink.dump(
|
|
361
369
|
winner.kind === "timeout" && deadlineTimeoutMs !== undefined
|
|
362
370
|
? `Command timed out after ${Math.round(deadlineTimeoutMs / 1000)} seconds`
|
|
@@ -381,6 +389,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
381
389
|
return {
|
|
382
390
|
exitCode: undefined,
|
|
383
391
|
cancelled: true,
|
|
392
|
+
timedOut: true,
|
|
384
393
|
...(await sink.dump(annotation)),
|
|
385
394
|
};
|
|
386
395
|
}
|
|
@@ -1082,6 +1082,8 @@
|
|
|
1082
1082
|
<div class="thinking-text">${escapeHtml(thinking)}</div>
|
|
1083
1083
|
<div class="thinking-collapsed">Thinking ...</div>
|
|
1084
1084
|
</div>`;
|
|
1085
|
+
} else if (block.type === 'image') {
|
|
1086
|
+
html += `<div class="message-images"><img src="data:${block.mimeType};base64,${block.data}" class="message-image" /></div>`;
|
|
1085
1087
|
}
|
|
1086
1088
|
}
|
|
1087
1089
|
for (const block of msg.content) {
|
|
@@ -18,7 +18,7 @@ import { getAllPluginToolPaths } from "../../extensibility/plugins/loader";
|
|
|
18
18
|
// Runtime self-reference: dereference this namespace only inside loader functions to keep the index.ts cycle safe.
|
|
19
19
|
import * as PiCodingAgent from "../../index";
|
|
20
20
|
import * as typebox from "../typebox";
|
|
21
|
-
import { createNoOpUIContext, resolvePath,
|
|
21
|
+
import { createNoOpUIContext, resolvePath, withHostGuard } from "../utils";
|
|
22
22
|
import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
|
|
23
23
|
|
|
24
24
|
interface LoadToolResult {
|
|
@@ -75,14 +75,14 @@ async function loadTool(
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
try {
|
|
78
|
-
const module = await
|
|
78
|
+
const module = await withHostGuard(() => import(resolvedPath));
|
|
79
79
|
const factory = (module.default ?? module) as CustomToolFactory;
|
|
80
80
|
|
|
81
81
|
if (typeof factory !== "function") {
|
|
82
82
|
return { tools: [], errors: [{ path: toolPath, error: "Tool must export a default function", source }] };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const toolResult: unknown = await
|
|
85
|
+
const toolResult: unknown = await withHostGuard(async () => factory(sharedApi));
|
|
86
86
|
const toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];
|
|
87
87
|
|
|
88
88
|
const loadedTools: LoadedCustomTool[] = [];
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { AgentTool, AgentToolUpdateCallback, ToolLoadMode } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { Static, TSchema } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import type { Theme } from "../../modes/theme/theme";
|
|
7
|
+
import { defaultLoadModeForToolName } from "../../tools/essential-tools";
|
|
7
8
|
import { applyToolProxy } from "../tool-proxy";
|
|
8
9
|
import type { CustomTool, CustomToolContext } from "./types";
|
|
9
10
|
|
|
@@ -23,7 +24,7 @@ export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any
|
|
|
23
24
|
) {
|
|
24
25
|
applyToolProxy(tool, this);
|
|
25
26
|
this.strict = tool.strict;
|
|
26
|
-
this.loadMode = tool.loadMode
|
|
27
|
+
this.loadMode = defaultLoadModeForToolName(tool.name, tool.loadMode);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
execute(
|
|
@@ -24,7 +24,7 @@ import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "../plugins/leg
|
|
|
24
24
|
import { getAllPluginExtensionPaths } from "../plugins/loader";
|
|
25
25
|
import * as TypeBox from "../typebox";
|
|
26
26
|
|
|
27
|
-
import { resolvePath,
|
|
27
|
+
import { resolvePath, withHostGuard } from "../utils";
|
|
28
28
|
import type {
|
|
29
29
|
AssistantThinkingRenderer,
|
|
30
30
|
Extension,
|
|
@@ -290,7 +290,7 @@ async function loadExtension(
|
|
|
290
290
|
): Promise<{ extension: Extension | null; error: string | null }> {
|
|
291
291
|
const resolvedPath = resolvePath(extensionPath, cwd);
|
|
292
292
|
try {
|
|
293
|
-
const module = (await
|
|
293
|
+
const module = (await withHostGuard(() => loadLegacyPiModule(resolvedPath))) as LoadedExtensionModule;
|
|
294
294
|
const factory = getExtensionFactory(module);
|
|
295
295
|
|
|
296
296
|
if (typeof factory !== "function") {
|
|
@@ -302,7 +302,7 @@ async function loadExtension(
|
|
|
302
302
|
|
|
303
303
|
const extension = createExtension(extensionPath, resolvedPath);
|
|
304
304
|
const api = new ConcreteExtensionAPI(PiCodingAgent, extension, runtime, cwd, eventBus);
|
|
305
|
-
await
|
|
305
|
+
await withHostGuard(async () => {
|
|
306
306
|
await factory(api);
|
|
307
307
|
});
|
|
308
308
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed timers for extensions.
|
|
3
|
+
*
|
|
4
|
+
* Extensions scheduling their own background work through raw `setInterval` /
|
|
5
|
+
* `setTimeout` used to be able to take down the whole session: a throw inside
|
|
6
|
+
* the callback runs on a fresh stack outside the handler-dispatch try/catch,
|
|
7
|
+
* surfaces as a process-level `uncaughtException`, and the global postmortem
|
|
8
|
+
* handler treats that as fatal (issue #5664).
|
|
9
|
+
*
|
|
10
|
+
* {@link ManagedTimers} backs the sanctioned `ctx.setInterval` /
|
|
11
|
+
* `ctx.setTimeout` helpers. Each callback runs inside the same isolation the
|
|
12
|
+
* runner already applies to handler dispatch — a synchronous throw or a
|
|
13
|
+
* rejected promise is reported through `onError` and swallowed — and every
|
|
14
|
+
* outstanding handle is `unref`'d (never keeps the process alive) and cleared
|
|
15
|
+
* on session teardown via {@link clearAll}.
|
|
16
|
+
*/
|
|
17
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
18
|
+
|
|
19
|
+
/** Callback invoked when a managed timer's callback throws or rejects. */
|
|
20
|
+
export type ManagedTimerErrorHandler = (event: string, error: string, stack?: string) => void;
|
|
21
|
+
|
|
22
|
+
export class ManagedTimers {
|
|
23
|
+
readonly #timers = new Set<Timer>();
|
|
24
|
+
|
|
25
|
+
constructor(private readonly onError: ManagedTimerErrorHandler) {}
|
|
26
|
+
|
|
27
|
+
/** Schedule a repeating callback whose throws are contained. */
|
|
28
|
+
setInterval(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): Timer {
|
|
29
|
+
const timer = setInterval(() => this.#run("interval", callback, args), ms, ...args);
|
|
30
|
+
timer.unref?.();
|
|
31
|
+
this.#timers.add(timer);
|
|
32
|
+
return timer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Schedule a one-shot callback whose throws are contained. Deregisters after it fires. */
|
|
36
|
+
setTimeout(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): Timer {
|
|
37
|
+
const timer = setTimeout(
|
|
38
|
+
() => {
|
|
39
|
+
this.#timers.delete(timer);
|
|
40
|
+
this.#run("timeout", callback, args);
|
|
41
|
+
},
|
|
42
|
+
ms,
|
|
43
|
+
...args,
|
|
44
|
+
);
|
|
45
|
+
timer.unref?.();
|
|
46
|
+
this.#timers.add(timer);
|
|
47
|
+
return timer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Clear one managed timer. Accepts an interval or timeout handle. */
|
|
51
|
+
clear(timer: Timer): void {
|
|
52
|
+
if (!this.#timers.delete(timer)) return;
|
|
53
|
+
clearInterval(timer);
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Clear every outstanding managed timer. Called on session teardown. */
|
|
58
|
+
clearAll(): void {
|
|
59
|
+
for (const timer of this.#timers) {
|
|
60
|
+
clearInterval(timer);
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
}
|
|
63
|
+
this.#timers.clear();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#run(kind: "interval" | "timeout", callback: (...args: unknown[]) => void, args: unknown[]): void {
|
|
67
|
+
try {
|
|
68
|
+
const result = callback(...args) as unknown;
|
|
69
|
+
if (result instanceof Promise) {
|
|
70
|
+
result.catch((err: unknown) => this.#report(kind, err));
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
this.#report(kind, err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#report(kind: "interval" | "timeout", err: unknown): void {
|
|
78
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
79
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
80
|
+
logger.warn("Extension timer callback threw", { event: `${kind}_callback`, error: message });
|
|
81
|
+
this.onError(`${kind}_callback`, message, stack);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -12,6 +12,7 @@ import type { MemoryRuntimeContext } from "../../memory-backend";
|
|
|
12
12
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
13
13
|
import type { SessionManager } from "../../session/session-manager";
|
|
14
14
|
import type { BranchHandler, NavigateTreeHandler, NewSessionHandler } from "../session-handler-types";
|
|
15
|
+
import { ManagedTimers } from "./managed-timers";
|
|
15
16
|
import { createExtensionModelQuery } from "./model-api";
|
|
16
17
|
import type {
|
|
17
18
|
AfterProviderResponseEvent,
|
|
@@ -249,6 +250,18 @@ export class ExtensionRunner {
|
|
|
249
250
|
*/
|
|
250
251
|
#pendingCredentialDisabled: CredentialDisabledEvent[] = [];
|
|
251
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Timers scheduled by extensions through the sanctioned `ctx.setInterval` /
|
|
255
|
+
* `ctx.setTimeout` helpers. Callbacks run with the same isolation as handler
|
|
256
|
+
* dispatch — a throw is logged and routed through {@link onError} instead of
|
|
257
|
+
* escaping to the process `uncaughtException` handler and tearing down the
|
|
258
|
+
* whole session (issue #5664). Handles are `unref`'d and every outstanding
|
|
259
|
+
* timer is cleared on session teardown via {@link clearManagedTimers}.
|
|
260
|
+
*/
|
|
261
|
+
#managedTimers = new ManagedTimers((event, error, stack) =>
|
|
262
|
+
this.emitError({ extensionPath: "<timer>", event, error, stack }),
|
|
263
|
+
);
|
|
264
|
+
|
|
252
265
|
constructor(
|
|
253
266
|
private readonly extensions: Extension[],
|
|
254
267
|
private readonly runtime: ExtensionRuntime,
|
|
@@ -542,6 +555,9 @@ export class ExtensionRunner {
|
|
|
542
555
|
getSystemPrompt: () => this.#getSystemPromptFn(),
|
|
543
556
|
localProtocolOptions: this.localProtocolOptions,
|
|
544
557
|
memory: this.#getMemoryFn?.(),
|
|
558
|
+
setInterval: (callback, ms, ...args) => this.#managedTimers.setInterval(callback, ms, ...args),
|
|
559
|
+
setTimeout: (callback, ms, ...args) => this.#managedTimers.setTimeout(callback, ms, ...args),
|
|
560
|
+
clearTimer: timer => this.#managedTimers.clear(timer),
|
|
545
561
|
};
|
|
546
562
|
}
|
|
547
563
|
|
|
@@ -552,6 +568,16 @@ export class ExtensionRunner {
|
|
|
552
568
|
this.#shutdownHandler();
|
|
553
569
|
}
|
|
554
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Clear every timer scheduled through `ctx.setInterval` / `ctx.setTimeout`.
|
|
573
|
+
* Called during session teardown so extension background work does not
|
|
574
|
+
* outlive the session (a self-scheduling interval would otherwise keep
|
|
575
|
+
* firing against a disposed session).
|
|
576
|
+
*/
|
|
577
|
+
clearManagedTimers(): void {
|
|
578
|
+
this.#managedTimers.clearAll();
|
|
579
|
+
}
|
|
580
|
+
|
|
555
581
|
createCommandContext(): ExtensionCommandContext {
|
|
556
582
|
return {
|
|
557
583
|
...this.createContext(),
|
|
@@ -440,6 +440,24 @@ export interface ExtensionContext {
|
|
|
440
440
|
getSystemPrompt(): string[];
|
|
441
441
|
/** Structured memory runtime for status/search/save across the configured backend. */
|
|
442
442
|
memory?: MemoryRuntimeContext;
|
|
443
|
+
/**
|
|
444
|
+
* Schedule a repeating callback whose throws are contained. Unlike raw
|
|
445
|
+
* `setInterval`, a synchronous throw or rejected promise from `callback` is
|
|
446
|
+
* logged and surfaced through the extension error channel instead of
|
|
447
|
+
* escaping as a process-fatal `uncaughtException` — one misbehaving timer
|
|
448
|
+
* can no longer take down the whole session. The handle is `unref`'d and
|
|
449
|
+
* cleared automatically on `session_shutdown`. Prefer this over raw
|
|
450
|
+
* `setInterval` for any extension background work.
|
|
451
|
+
*/
|
|
452
|
+
setInterval(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): Timer;
|
|
453
|
+
/**
|
|
454
|
+
* Schedule a one-shot callback whose throws are contained, mirroring
|
|
455
|
+
* {@link setInterval}. Cleared automatically on `session_shutdown` if it has
|
|
456
|
+
* not yet fired.
|
|
457
|
+
*/
|
|
458
|
+
setTimeout(callback: (...args: unknown[]) => void, ms?: number, ...args: unknown[]): Timer;
|
|
459
|
+
/** Clear a timer scheduled via {@link setInterval} or {@link setTimeout}. */
|
|
460
|
+
clearTimer(timer: Timer): void;
|
|
443
461
|
}
|
|
444
462
|
|
|
445
463
|
/**
|
|
@@ -12,6 +12,7 @@ import type { ImageContent, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai
|
|
|
12
12
|
import type { Settings } from "../../config/settings";
|
|
13
13
|
import type { Theme } from "../../modes/theme/theme";
|
|
14
14
|
import { type ApprovalMode, formatApprovalPrompt, resolveApproval } from "../../tools/approval";
|
|
15
|
+
import { defaultLoadModeForToolName } from "../../tools/essential-tools";
|
|
15
16
|
import { normalizeToolEventInput, resolveToolEventInput } from "../tool-event-input";
|
|
16
17
|
import { applyToolProxy } from "../tool-proxy";
|
|
17
18
|
import type { ExtensionRunner } from "./runner";
|
|
@@ -36,7 +37,7 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
|
|
|
36
37
|
private runner: ExtensionRunner,
|
|
37
38
|
) {
|
|
38
39
|
applyToolProxy(registeredTool.definition, this);
|
|
39
|
-
this.loadMode = registeredTool.definition.loadMode
|
|
40
|
+
this.loadMode = defaultLoadModeForToolName(registeredTool.definition.name, registeredTool.definition.loadMode);
|
|
40
41
|
|
|
41
42
|
// Only define render methods when the underlying definition provides them.
|
|
42
43
|
// If these exist unconditionally on the prototype, ToolExecutionComponent
|