@oh-my-pi/pi-coding-agent 17.0.1 → 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 +92 -20
- package/dist/cli.js +3485 -3448
- 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/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- 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/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/mcp/transports/stdio.d.ts +13 -1
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/hook-editor.d.ts +7 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +2 -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/print-mode.d.ts +1 -1
- 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 +34 -10
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- 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/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +1 -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/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 +1304 -42
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +25 -6
- package/src/config/model-resolver.ts +14 -7
- package/src/config/settings-schema.ts +42 -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/helpers.ts +28 -5
- package/src/exec/bash-executor.ts +14 -5
- 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/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/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/components/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/hook-editor.ts +18 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/session-selector.ts +4 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/controllers/command-controller.ts +167 -47
- package/src/modes/controllers/event-controller.ts +5 -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 +191 -31
- package/src/modes/interactive-mode.ts +139 -54
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +19 -4
- 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/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +221 -42
- package/src/session/agent-session.ts +1285 -348
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-manager.ts +48 -0
- package/src/slash-commands/builtin-registry.ts +7 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- 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/essential-tools.ts +45 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +69 -7
- package/src/tools/index.ts +7 -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/providers/kimi.ts +18 -12
- package/src/web/search/types.ts +6 -1
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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
|
@@ -12,7 +12,7 @@ import { loadCapability } from "../../discovery";
|
|
|
12
12
|
import * as PiCodingAgent from "../../index";
|
|
13
13
|
import type { CustomMessagePayload } from "../../session/messages";
|
|
14
14
|
import * as typebox from "../typebox";
|
|
15
|
-
import { resolvePath,
|
|
15
|
+
import { resolvePath, withHostGuard } from "../utils";
|
|
16
16
|
import { execCommand } from "./runner";
|
|
17
17
|
import type { ExecOptions, HookAPI, HookFactory, HookMessageRenderer, RegisteredCommand } from "./types";
|
|
18
18
|
|
|
@@ -149,7 +149,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
|
|
|
149
149
|
|
|
150
150
|
try {
|
|
151
151
|
// Import the module using native Bun import
|
|
152
|
-
const module = await
|
|
152
|
+
const module = await withHostGuard(() => import(resolvedPath));
|
|
153
153
|
const factory = module.default as HookFactory;
|
|
154
154
|
|
|
155
155
|
if (typeof factory !== "function") {
|
|
@@ -164,7 +164,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
|
|
|
164
164
|
);
|
|
165
165
|
|
|
166
166
|
// Call factory to register handlers
|
|
167
|
-
await
|
|
167
|
+
await withHostGuard(async () => factory(api));
|
|
168
168
|
|
|
169
169
|
return {
|
|
170
170
|
hook: {
|
|
@@ -44,9 +44,10 @@ import { ReadTool } from "../tools/read";
|
|
|
44
44
|
import { formatBytes } from "../tools/render-utils";
|
|
45
45
|
import { WriteTool } from "../tools/write";
|
|
46
46
|
import { EventBus } from "../utils/event-bus";
|
|
47
|
-
import { loadExtensionFromFactory, loadExtensions } from "./extensions";
|
|
47
|
+
import { discoverExtensionPaths, loadExtensionFromFactory, loadExtensions } from "./extensions";
|
|
48
48
|
import { ExtensionRuntime } from "./extensions/loader";
|
|
49
49
|
import type { ExtensionFactory, ToolDefinition } from "./extensions/types";
|
|
50
|
+
import { getEnabledPlugins, resolvePluginExtensionPaths, type ScopedInstalledPlugin } from "./plugins/loader";
|
|
50
51
|
import type { Skill } from "./skills";
|
|
51
52
|
import { loadSkillsFromDir } from "./skills";
|
|
52
53
|
import { Type } from "./typebox";
|
|
@@ -655,6 +656,100 @@ export const SettingsManager = {
|
|
|
655
656
|
},
|
|
656
657
|
} as const;
|
|
657
658
|
|
|
659
|
+
/** Scope used by the legacy package manager for discovered resources. */
|
|
660
|
+
export type SourceScope = "user" | "project" | "temporary";
|
|
661
|
+
|
|
662
|
+
/** Discovery metadata exposed alongside a legacy package resource path. */
|
|
663
|
+
export interface PathMetadata {
|
|
664
|
+
source: string;
|
|
665
|
+
scope: SourceScope;
|
|
666
|
+
origin: "package" | "top-level";
|
|
667
|
+
baseDir?: string;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** One extension, skill, prompt, or theme resolved by the legacy package manager. */
|
|
671
|
+
export interface ResolvedResource {
|
|
672
|
+
path: string;
|
|
673
|
+
enabled: boolean;
|
|
674
|
+
metadata: PathMetadata;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Resource groups returned by {@link DefaultPackageManager.resolve}. */
|
|
678
|
+
export interface ResolvedPaths {
|
|
679
|
+
extensions: ResolvedResource[];
|
|
680
|
+
skills: ResolvedResource[];
|
|
681
|
+
prompts: ResolvedResource[];
|
|
682
|
+
themes: ResolvedResource[];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** Action a legacy caller requests when a configured package is unavailable. */
|
|
686
|
+
export type MissingSourceAction = "install" | "skip" | "error";
|
|
687
|
+
|
|
688
|
+
/** Construction inputs accepted by the legacy package manager. */
|
|
689
|
+
export interface DefaultPackageManagerOptions {
|
|
690
|
+
cwd: string;
|
|
691
|
+
agentDir: string;
|
|
692
|
+
settingsManager: Settings | Promise<Settings>;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Enumerates the extensions OMP would load through the historical package
|
|
697
|
+
* manager surface used by legacy extensions.
|
|
698
|
+
*/
|
|
699
|
+
export class DefaultPackageManager {
|
|
700
|
+
#cwd: string;
|
|
701
|
+
#agentDir: string;
|
|
702
|
+
#settingsManager: Settings | Promise<Settings>;
|
|
703
|
+
|
|
704
|
+
constructor(options: DefaultPackageManagerOptions) {
|
|
705
|
+
this.#cwd = options.cwd;
|
|
706
|
+
this.#agentDir = options.agentDir;
|
|
707
|
+
this.#settingsManager = options.settingsManager;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Resolve enabled extension paths with their OMP plugin provenance. */
|
|
711
|
+
async resolve(_onMissing?: (source: string) => Promise<MissingSourceAction>): Promise<ResolvedPaths> {
|
|
712
|
+
const settings = await this.#settingsManager;
|
|
713
|
+
const configuredPaths = settings.get("extensions") ?? [];
|
|
714
|
+
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
|
|
715
|
+
const [extensionPaths, plugins] = await Promise.all([
|
|
716
|
+
discoverExtensionPaths(configuredPaths, this.#cwd, disabledExtensionIds),
|
|
717
|
+
getEnabledPlugins(this.#cwd),
|
|
718
|
+
]);
|
|
719
|
+
const pluginByExtensionPath = new Map<string, ScopedInstalledPlugin>();
|
|
720
|
+
for (const plugin of plugins) {
|
|
721
|
+
for (const extensionPath of resolvePluginExtensionPaths(plugin)) {
|
|
722
|
+
pluginByExtensionPath.set(path.resolve(extensionPath), plugin);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const extensions = extensionPaths.map(extensionPath => {
|
|
727
|
+
const resolvedPath = path.resolve(extensionPath);
|
|
728
|
+
const plugin = pluginByExtensionPath.get(resolvedPath);
|
|
729
|
+
const agentDirRelative = path.relative(path.resolve(this.#agentDir), resolvedPath);
|
|
730
|
+
const metadata: PathMetadata = plugin
|
|
731
|
+
? {
|
|
732
|
+
source: `npm:${plugin.name}`,
|
|
733
|
+
scope: plugin.scope,
|
|
734
|
+
origin: "package",
|
|
735
|
+
baseDir: plugin.path,
|
|
736
|
+
}
|
|
737
|
+
: {
|
|
738
|
+
source: "auto",
|
|
739
|
+
scope:
|
|
740
|
+
agentDirRelative === "" ||
|
|
741
|
+
(!agentDirRelative.startsWith("..") && !path.isAbsolute(agentDirRelative))
|
|
742
|
+
? "user"
|
|
743
|
+
: "project",
|
|
744
|
+
origin: "top-level",
|
|
745
|
+
};
|
|
746
|
+
return { path: resolvedPath, enabled: true, metadata };
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
return { extensions, skills: [], prompts: [], themes: [] };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
658
753
|
/**
|
|
659
754
|
* Resource-loader compatibility layer for legacy pi extensions.
|
|
660
755
|
*
|