@oh-my-pi/pi-coding-agent 16.3.0 → 16.3.3
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 +91 -0
- package/dist/cli.js +3855 -3798
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/cli/update-cli.test.d.ts +1 -0
- package/dist/types/commands/update.d.ts +5 -0
- package/dist/types/config/config-file.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/dap/client.d.ts +9 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/edit/file-snapshot-store.d.ts +9 -1
- package/dist/types/edit/hashline/execute.d.ts +1 -1
- package/dist/types/edit/hashline/params.d.ts +3 -6
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/extensibility/plugins/parser.d.ts +2 -1
- package/dist/types/hindsight/client.d.ts +15 -11
- package/dist/types/hindsight/client.test.d.ts +1 -0
- package/dist/types/irc/bus.d.ts +5 -4
- package/dist/types/mcp/smithery-registry.d.ts +1 -0
- package/dist/types/mcp/smithery-registry.test.d.ts +1 -0
- package/dist/types/modes/components/advisor-message.d.ts +4 -2
- package/dist/types/modes/components/tool-execution.d.ts +9 -6
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +13 -13
- package/dist/types/session/indexed-session-storage.d.ts +2 -2
- package/dist/types/session/session-storage.d.ts +31 -3
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +19 -0
- package/dist/types/ssh/sshfs-mount.d.ts +10 -1
- package/dist/types/subprocess/worker-client.d.ts +20 -6
- package/dist/types/task/discovery.d.ts +5 -2
- package/dist/types/task/renderer.d.ts +1 -0
- package/dist/types/tools/ast-grep.d.ts +4 -2
- package/dist/types/tools/bash.d.ts +27 -0
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +1 -0
- package/dist/types/tools/eval-render.d.ts +2 -0
- package/dist/types/tools/glob.d.ts +4 -2
- package/dist/types/tools/grep.d.ts +4 -3
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tools/renderers.d.ts +24 -0
- package/dist/types/tools/ssh.d.ts +4 -1
- package/dist/types/tts/index.d.ts +2 -0
- package/dist/types/tts/speakable.d.ts +47 -0
- package/dist/types/tts/speech-enhancer.d.ts +46 -0
- package/dist/types/tts/streaming-player.d.ts +1 -2
- package/dist/types/tts/tts-client.d.ts +11 -10
- package/dist/types/tts/tts-protocol.d.ts +7 -0
- package/dist/types/tts/vocalizer.d.ts +15 -8
- package/dist/types/utils/fetch-timeout.d.ts +4 -0
- package/dist/types/utils/git.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/package.json +12 -12
- package/scripts/generate-legacy-pi-bundled-registry.ts +8 -2
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +2 -2
- package/src/cli/gallery-fixtures/search.ts +2 -2
- package/src/cli/models-cli.ts +19 -0
- package/src/cli/update-cli.test.ts +28 -0
- package/src/cli/update-cli.ts +35 -8
- package/src/cli.ts +27 -5
- package/src/commands/update.ts +8 -2
- package/src/config/config-file.ts +6 -6
- package/src/config/model-resolver.ts +31 -10
- package/src/config/settings-schema.ts +21 -0
- package/src/cursor.ts +1 -1
- package/src/dap/client.ts +134 -36
- package/src/discovery/helpers.ts +8 -0
- package/src/edit/file-snapshot-store.ts +12 -1
- package/src/edit/hashline/diff.ts +4 -13
- package/src/edit/hashline/execute.ts +1 -1
- package/src/edit/hashline/params.ts +5 -12
- package/src/edit/renderer.ts +4 -2
- package/src/edit/streaming.ts +15 -5
- package/src/export/html/tool-views.generated.js +34 -34
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/extensions/loader.ts +10 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
- package/src/extensibility/plugins/installer.ts +12 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +87 -11
- package/src/extensibility/plugins/loader.ts +30 -1
- package/src/extensibility/plugins/manager.ts +32 -8
- package/src/extensibility/plugins/parser.ts +7 -5
- package/src/extensibility/tool-event-input.ts +1 -1
- package/src/hindsight/client.test.ts +33 -0
- package/src/hindsight/client.ts +42 -22
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/irc/bus.ts +5 -4
- package/src/main.ts +7 -1
- package/src/mcp/oauth-flow.ts +93 -4
- package/src/mcp/smithery-auth.ts +3 -0
- package/src/mcp/smithery-connect.ts +9 -0
- package/src/mcp/smithery-registry.test.ts +51 -0
- package/src/mcp/smithery-registry.ts +27 -4
- package/src/modes/components/__tests__/move-overlay.test.ts +72 -1
- package/src/modes/components/advisor-message.ts +13 -10
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/move-overlay.ts +35 -23
- package/src/modes/components/status-line/component.ts +11 -4
- package/src/modes/components/tool-execution.ts +119 -20
- package/src/modes/components/transcript-container.ts +26 -0
- package/src/modes/components/tree-selector.ts +10 -3
- package/src/modes/controllers/event-controller.ts +23 -3
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/rpc/rpc-client.ts +29 -16
- package/src/modes/theme/shimmer.ts +49 -15
- package/src/modes/theme/theme.ts +7 -0
- package/src/prompts/goals/goal-mode-context.md +4 -0
- package/src/prompts/goals/goal-todo-context.md +10 -2
- package/src/prompts/system/speech-rewrite.md +15 -0
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-system-marker.md +2 -1
- package/src/prompts/system/title-system.md +2 -1
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +329 -79
- package/src/session/indexed-session-storage.ts +40 -3
- package/src/session/session-history-format.ts +6 -2
- package/src/session/session-manager.ts +83 -14
- package/src/session/session-storage.ts +112 -26
- package/src/slash-commands/helpers/usage-report.ts +6 -1
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +61 -0
- package/src/ssh/__tests__/sshfs-mount.test.ts +13 -0
- package/src/ssh/connection-manager.ts +44 -11
- package/src/ssh/sshfs-mount.ts +27 -4
- package/src/subprocess/worker-client.ts +161 -10
- package/src/task/discovery.ts +25 -2
- package/src/task/executor.ts +24 -4
- package/src/task/render.ts +26 -12
- package/src/task/renderer.ts +1 -0
- package/src/task/worktree.ts +144 -16
- package/src/tiny/text.ts +109 -17
- package/src/tools/ast-grep.ts +20 -17
- package/src/tools/bash.ts +46 -9
- package/src/tools/browser/render.ts +2 -0
- package/src/tools/debug.ts +1 -0
- package/src/tools/eval-render.ts +5 -2
- package/src/tools/gh-renderer.ts +3 -0
- package/src/tools/glob.ts +24 -20
- package/src/tools/grep.ts +39 -37
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +23 -6
- package/src/tools/renderers.ts +24 -0
- package/src/tools/ssh.ts +25 -7
- package/src/tts/index.ts +2 -0
- package/src/tts/speakable.ts +382 -0
- package/src/tts/speech-enhancer.ts +204 -0
- package/src/tts/streaming-player.ts +71 -16
- package/src/tts/tts-client.ts +11 -10
- package/src/tts/tts-protocol.ts +14 -5
- package/src/tts/tts-worker.ts +52 -49
- package/src/tts/vocalizer.ts +277 -46
- package/src/utils/clipboard.ts +49 -12
- package/src/utils/fetch-timeout.ts +10 -0
- package/src/utils/git.ts +8 -0
- package/src/web/search/index.ts +8 -0
- package/src/web/search/providers/base.ts +1 -0
- package/src/web/search/providers/gemini.ts +23 -6
|
@@ -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 } from "../utils";
|
|
21
|
+
import { createNoOpUIContext, resolvePath, withExitGuard } from "../utils";
|
|
22
22
|
import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -45,14 +45,14 @@ async function loadTool(
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
try {
|
|
48
|
-
const module = await import(resolvedPath);
|
|
48
|
+
const module = await withExitGuard(() => import(resolvedPath));
|
|
49
49
|
const factory = (module.default ?? module) as CustomToolFactory;
|
|
50
50
|
|
|
51
51
|
if (typeof factory !== "function") {
|
|
52
52
|
return { tools: null, error: { path: toolPath, error: "Tool must export a default function", source } };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const toolResult = await factory(sharedApi);
|
|
55
|
+
const toolResult = await withExitGuard(async () => factory(sharedApi));
|
|
56
56
|
const toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];
|
|
57
57
|
|
|
58
58
|
const loadedTools: LoadedCustomTool[] = toolsArray.map(tool => ({
|
|
@@ -521,10 +521,17 @@ export async function discoverExtensionPaths(
|
|
|
521
521
|
}
|
|
522
522
|
};
|
|
523
523
|
|
|
524
|
-
// 1. Discover extension modules via capability API (native .omp/.pi only)
|
|
525
|
-
|
|
524
|
+
// 1. Discover extension modules via capability API (native .omp/.pi only).
|
|
525
|
+
// Scope the load to the native provider — the extension-module capability
|
|
526
|
+
// also has claude/codex/gemini/opencode providers, and their items were
|
|
527
|
+
// discarded here anyway (see #4198). The provider filter skips the walk
|
|
528
|
+
// entirely instead of running four foreign directory scans and dropping
|
|
529
|
+
// the results.
|
|
530
|
+
const discovered = await loadCapability<ExtensionModule>(extensionModuleCapability.id, {
|
|
531
|
+
...loadOptions,
|
|
532
|
+
providers: ["native"],
|
|
533
|
+
});
|
|
526
534
|
for (const ext of discovered.items) {
|
|
527
|
-
if (ext._source.provider !== "native") continue;
|
|
528
535
|
addPath(ext.path);
|
|
529
536
|
}
|
|
530
537
|
|
|
@@ -500,7 +500,7 @@ export function createGrepToolDefinition(cwd: string, options?: GrepToolOptions)
|
|
|
500
500
|
toolCallId,
|
|
501
501
|
{
|
|
502
502
|
pattern,
|
|
503
|
-
|
|
503
|
+
path: glob ? joinLegacyGlob(searchPath, glob) : searchPath,
|
|
504
504
|
case: booleanField(params, "ignoreCase") ? false : undefined,
|
|
505
505
|
},
|
|
506
506
|
signal,
|
|
@@ -558,7 +558,7 @@ export function createFindToolDefinition(cwd: string, options?: FindToolOptions)
|
|
|
558
558
|
}
|
|
559
559
|
return tool.execute(
|
|
560
560
|
toolCallId,
|
|
561
|
-
{
|
|
561
|
+
{ path: joinLegacyGlob(searchPath, pattern), hidden: true, gitignore: true, limit },
|
|
562
562
|
signal,
|
|
563
563
|
onUpdate,
|
|
564
564
|
);
|
|
@@ -53,9 +53,14 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
|
|
|
53
53
|
windowsHide: true,
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
// Drain both pipes concurrently with proc.exited to avoid a pipe-buffer
|
|
57
|
+
// deadlock if bun install floods stdout/stderr.
|
|
58
|
+
const [exitCode, , stderr] = await Promise.all([
|
|
59
|
+
proc.exited,
|
|
60
|
+
new Response(proc.stdout).text(),
|
|
61
|
+
new Response(proc.stderr).text(),
|
|
62
|
+
]);
|
|
57
63
|
if (exitCode !== 0) {
|
|
58
|
-
const stderr = await new Response(proc.stderr).text();
|
|
59
64
|
throw new Error(`Failed to install ${packageName}: ${stderr}`);
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -95,7 +100,11 @@ export async function uninstallPlugin(name: string): Promise<void> {
|
|
|
95
100
|
windowsHide: true,
|
|
96
101
|
});
|
|
97
102
|
|
|
98
|
-
const exitCode = await
|
|
103
|
+
const [exitCode] = await Promise.all([
|
|
104
|
+
proc.exited,
|
|
105
|
+
new Response(proc.stdout).text(),
|
|
106
|
+
new Response(proc.stderr).text(),
|
|
107
|
+
]);
|
|
99
108
|
if (exitCode !== 0) {
|
|
100
109
|
throw new Error(`Failed to uninstall ${name}`);
|
|
101
110
|
}
|
|
@@ -3,6 +3,7 @@ import { isBuiltin } from "node:module";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
5
|
import { isCompiledBinary, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { registerPluginCacheInvalidator } from "../../discovery/helpers";
|
|
6
7
|
import { BUNDLED_PI_REGISTRY_KEYS } from "./legacy-pi-bundled-keys";
|
|
7
8
|
|
|
8
9
|
const IS_COMPILED_BINARY = isCompiledBinary();
|
|
@@ -202,6 +203,22 @@ const SOURCE_MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx",
|
|
|
202
203
|
const SUPPORTED_PACKAGE_IMPORT_CONDITIONS = new Set(["bun", "node", "import", "default"]);
|
|
203
204
|
const packageRootCache = new Map<string, string | null>();
|
|
204
205
|
const packageImportsCache = new Map<string, Record<string, unknown> | null>();
|
|
206
|
+
const nodePackageRootCache = new Map<string, Promise<string | null>>();
|
|
207
|
+
const packageManifestCache = new Map<string, Promise<Record<string, unknown> | null>>();
|
|
208
|
+
const bareDependencyResolutionCache = new Map<string, Promise<string | null>>();
|
|
209
|
+
const realpathCache = new Map<string, Promise<string>>();
|
|
210
|
+
|
|
211
|
+
function clearLegacyPiResolutionCaches(): void {
|
|
212
|
+
resolvedSpecifierFallbacks.clear();
|
|
213
|
+
packageRootCache.clear();
|
|
214
|
+
packageImportsCache.clear();
|
|
215
|
+
nodePackageRootCache.clear();
|
|
216
|
+
packageManifestCache.clear();
|
|
217
|
+
bareDependencyResolutionCache.clear();
|
|
218
|
+
realpathCache.clear();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
registerPluginCacheInvalidator(clearLegacyPiResolutionCaches);
|
|
205
222
|
const PACKAGE_IMPORT_EXCLUDED = Symbol("packageImportExcluded");
|
|
206
223
|
|
|
207
224
|
// Extensions that imported TypeBox directly used to resolve against a real
|
|
@@ -730,6 +747,16 @@ function splitBarePackageSpecifier(specifier: string): BarePackageSpecifier | nu
|
|
|
730
747
|
}
|
|
731
748
|
|
|
732
749
|
async function findNodePackageRoot(packageName: string, importerPath: string): Promise<string | null> {
|
|
750
|
+
const cacheKey = `${packageName}\0${path.resolve(path.dirname(importerPath))}`;
|
|
751
|
+
const cached = nodePackageRootCache.get(cacheKey);
|
|
752
|
+
if (cached) return cached;
|
|
753
|
+
|
|
754
|
+
const promise = findNodePackageRootUncached(packageName, importerPath);
|
|
755
|
+
nodePackageRootCache.set(cacheKey, promise);
|
|
756
|
+
return promise;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function findNodePackageRootUncached(packageName: string, importerPath: string): Promise<string | null> {
|
|
733
760
|
let dir = path.dirname(importerPath);
|
|
734
761
|
while (true) {
|
|
735
762
|
const candidate = path.join(dir, "node_modules", packageName);
|
|
@@ -745,6 +772,15 @@ async function findNodePackageRoot(packageName: string, importerPath: string): P
|
|
|
745
772
|
}
|
|
746
773
|
|
|
747
774
|
async function readPackageManifest(packageRoot: string): Promise<Record<string, unknown> | null> {
|
|
775
|
+
const cached = packageManifestCache.get(packageRoot);
|
|
776
|
+
if (cached) return cached;
|
|
777
|
+
|
|
778
|
+
const promise = readPackageManifestUncached(packageRoot);
|
|
779
|
+
packageManifestCache.set(packageRoot, promise);
|
|
780
|
+
return promise;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function readPackageManifestUncached(packageRoot: string): Promise<Record<string, unknown> | null> {
|
|
748
784
|
try {
|
|
749
785
|
const manifest = await Bun.file(path.join(packageRoot, "package.json")).json();
|
|
750
786
|
return isRecord(manifest) ? manifest : null;
|
|
@@ -841,6 +877,17 @@ async function resolveExtensionBareDependency(specifier: string, importerPath: s
|
|
|
841
877
|
if (!isBareExtensionDependencySpecifier(specifier)) {
|
|
842
878
|
return null;
|
|
843
879
|
}
|
|
880
|
+
|
|
881
|
+
const cacheKey = `${specifier}\0${path.resolve(path.dirname(importerPath))}`;
|
|
882
|
+
const cached = bareDependencyResolutionCache.get(cacheKey);
|
|
883
|
+
if (cached) return cached;
|
|
884
|
+
|
|
885
|
+
const promise = resolveExtensionBareDependencyUncached(specifier, importerPath);
|
|
886
|
+
bareDependencyResolutionCache.set(cacheKey, promise);
|
|
887
|
+
return promise;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function resolveExtensionBareDependencyUncached(specifier: string, importerPath: string): Promise<string | null> {
|
|
844
891
|
try {
|
|
845
892
|
const resolved = Bun.resolveSync(specifier, path.dirname(importerPath));
|
|
846
893
|
if (resolved && resolved !== specifier && !resolved.startsWith("node:") && !resolved.startsWith("bun:")) {
|
|
@@ -892,6 +939,15 @@ const hookedExtensionEntries = new Set<string>();
|
|
|
892
939
|
|
|
893
940
|
/** Resolve symlinks in a path, falling back to the input if realpath fails. */
|
|
894
941
|
async function realpathOrSelf(p: string): Promise<string> {
|
|
942
|
+
const cached = realpathCache.get(p);
|
|
943
|
+
if (cached) return cached;
|
|
944
|
+
|
|
945
|
+
const promise = realpathOrSelfUncached(p);
|
|
946
|
+
realpathCache.set(p, promise);
|
|
947
|
+
return promise;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function realpathOrSelfUncached(p: string): Promise<string> {
|
|
895
951
|
try {
|
|
896
952
|
return await fs.promises.realpath(p);
|
|
897
953
|
} catch {
|
|
@@ -907,8 +963,8 @@ async function realpathOrSelf(p: string): Promise<string> {
|
|
|
907
963
|
* wherever it physically lives (a `../src` sibling, a symlinked sub-tree, …).
|
|
908
964
|
* This mirrors the module set the old temp-dir mirror tracked, minus the copy.
|
|
909
965
|
*/
|
|
910
|
-
async function collectExtensionModules(entryRealPath: string): Promise<
|
|
911
|
-
const modules = new
|
|
966
|
+
async function collectExtensionModules(entryRealPath: string): Promise<Map<string, string>> {
|
|
967
|
+
const modules = new Map<string, string>();
|
|
912
968
|
const queue = [entryRealPath];
|
|
913
969
|
while (queue.length > 0) {
|
|
914
970
|
const file = queue.pop();
|
|
@@ -921,7 +977,7 @@ async function collectExtensionModules(entryRealPath: string): Promise<Set<strin
|
|
|
921
977
|
} catch {
|
|
922
978
|
continue;
|
|
923
979
|
}
|
|
924
|
-
modules.
|
|
980
|
+
modules.set(file, source);
|
|
925
981
|
const dir = path.dirname(file);
|
|
926
982
|
for (const match of source.matchAll(EXTENSION_GRAPH_SPECIFIER_REGEX)) {
|
|
927
983
|
const specifier = match[1];
|
|
@@ -949,25 +1005,38 @@ async function collectExtensionModules(entryRealPath: string): Promise<Set<strin
|
|
|
949
1005
|
* so the filter is an exact-path alternation of the graph's realpaths — it
|
|
950
1006
|
* never matches the host, other extensions, `node_modules` deps, or unrelated
|
|
951
1007
|
* project source.
|
|
1008
|
+
*
|
|
1009
|
+
* Returns the collected path→source map on first install so the caller can
|
|
1010
|
+
* drop entries the initial import never consumed; `undefined` when the hook
|
|
1011
|
+
* was already installed.
|
|
952
1012
|
*/
|
|
953
|
-
async function ensureExtensionGraphHook(entryRealPath: string): Promise<
|
|
1013
|
+
async function ensureExtensionGraphHook(entryRealPath: string): Promise<Map<string, string> | undefined> {
|
|
954
1014
|
if (hookedExtensionEntries.has(entryRealPath)) {
|
|
955
|
-
return;
|
|
1015
|
+
return undefined;
|
|
956
1016
|
}
|
|
957
1017
|
hookedExtensionEntries.add(entryRealPath);
|
|
958
1018
|
|
|
959
1019
|
const modules = await collectExtensionModules(entryRealPath);
|
|
960
|
-
const alternation = [...modules].map(escapeRegExp).join("|");
|
|
1020
|
+
const alternation = [...modules.keys()].map(escapeRegExp).join("|");
|
|
961
1021
|
const filter = new RegExp(`^(?:${alternation})$`);
|
|
962
1022
|
Bun.plugin({
|
|
963
1023
|
name: `omp:legacy-pi-ext:${Bun.hash(entryRealPath).toString(36)}`,
|
|
964
1024
|
setup(build) {
|
|
965
1025
|
build.onLoad({ filter, namespace: "file" }, async args => {
|
|
966
|
-
const
|
|
1026
|
+
const cached = modules.get(args.path);
|
|
1027
|
+
let raw: string;
|
|
1028
|
+
if (cached !== undefined) {
|
|
1029
|
+
// consume-once: preserves ?mtime edit-pickup for the re-imported entry
|
|
1030
|
+
modules.delete(args.path);
|
|
1031
|
+
raw = cached;
|
|
1032
|
+
} else {
|
|
1033
|
+
raw = await Bun.file(args.path).text();
|
|
1034
|
+
}
|
|
967
1035
|
return { contents: await rewriteLegacyExtensionSource(raw, args.path), loader: getLoader(args.path) };
|
|
968
1036
|
});
|
|
969
1037
|
},
|
|
970
1038
|
});
|
|
1039
|
+
return modules;
|
|
971
1040
|
}
|
|
972
1041
|
|
|
973
1042
|
/**
|
|
@@ -986,9 +1055,16 @@ export async function loadLegacyPiModule(resolvedPath: string): Promise<unknown>
|
|
|
986
1055
|
// `bun link`/pnpm installs) so the rewrite filter matches the path Bun
|
|
987
1056
|
// actually hands the hook.
|
|
988
1057
|
const entryRealPath = await realpathOrSelf(path.resolve(resolvedPath));
|
|
989
|
-
await ensureExtensionGraphHook(entryRealPath);
|
|
990
|
-
|
|
991
|
-
|
|
1058
|
+
const pendingSources = await ensureExtensionGraphHook(entryRealPath);
|
|
1059
|
+
try {
|
|
1060
|
+
// `?mtime` busts Bun's module cache so repeat loads pick up edited source.
|
|
1061
|
+
return await import(`${toImportSpecifier(entryRealPath)}?mtime=${Date.now()}`);
|
|
1062
|
+
} finally {
|
|
1063
|
+
// Drop whatever the initial import didn't consume: graph modules only
|
|
1064
|
+
// reached by lazy dynamic imports must be read from disk at their actual
|
|
1065
|
+
// import time, not served from this load-time snapshot.
|
|
1066
|
+
pendingSources?.clear();
|
|
1067
|
+
}
|
|
992
1068
|
}
|
|
993
1069
|
|
|
994
1070
|
function getLoader(path: string): "js" | "jsx" | "ts" | "tsx" {
|
|
@@ -1061,5 +1137,5 @@ export function installLegacyPiSpecifierShim(): void {
|
|
|
1061
1137
|
|
|
1062
1138
|
/** Test seam: clears the memoized canonical specifier resolutions. */
|
|
1063
1139
|
export function __resetLegacyPiResolutionCache(): void {
|
|
1064
|
-
|
|
1140
|
+
clearLegacyPiResolutionCaches();
|
|
1065
1141
|
}
|
|
@@ -8,7 +8,7 @@ import * as fs from "node:fs";
|
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { getPluginsDir, getPluginsLockfile, isEnoent } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { getConfigDirPaths } from "../../config";
|
|
11
|
-
import { resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
11
|
+
import { registerPluginCacheInvalidator, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
12
12
|
import { installLegacyPiSpecifierShim } from "./legacy-pi-compat";
|
|
13
13
|
import { normalizePluginRuntimeConfig } from "./runtime-config";
|
|
14
14
|
import type { InstalledPlugin, PluginManifest, PluginRuntimeConfig, ProjectPluginOverrides } from "./types";
|
|
@@ -20,6 +20,18 @@ export interface ScopedInstalledPlugin extends InstalledPlugin {
|
|
|
20
20
|
|
|
21
21
|
installLegacyPiSpecifierShim();
|
|
22
22
|
|
|
23
|
+
const enabledPluginsCache = new Map<string, Promise<ScopedInstalledPlugin[]>>();
|
|
24
|
+
|
|
25
|
+
function enabledPluginsCacheKey(cwd: string, home?: string): string {
|
|
26
|
+
return `${path.resolve(cwd)}\0${home === undefined ? "" : path.resolve(home)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clearEnabledPluginsCache(): void {
|
|
30
|
+
enabledPluginsCache.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
registerPluginCacheInvalidator(clearEnabledPluginsCache);
|
|
34
|
+
|
|
23
35
|
// =============================================================================
|
|
24
36
|
// Runtime Config Loading
|
|
25
37
|
// =============================================================================
|
|
@@ -163,6 +175,23 @@ async function collectPluginsAtRoot(
|
|
|
163
175
|
*/
|
|
164
176
|
export async function getEnabledPlugins(cwd: string, opts: { home?: string } = {}): Promise<ScopedInstalledPlugin[]> {
|
|
165
177
|
const { home } = opts;
|
|
178
|
+
const cacheKey = enabledPluginsCacheKey(cwd, home);
|
|
179
|
+
const cached = enabledPluginsCache.get(cacheKey);
|
|
180
|
+
if (cached) return cached;
|
|
181
|
+
|
|
182
|
+
const loadPromise = loadEnabledPlugins(cwd, home);
|
|
183
|
+
enabledPluginsCache.set(cacheKey, loadPromise);
|
|
184
|
+
try {
|
|
185
|
+
return await loadPromise;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if (enabledPluginsCache.get(cacheKey) === loadPromise) {
|
|
188
|
+
enabledPluginsCache.delete(cacheKey);
|
|
189
|
+
}
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function loadEnabledPlugins(cwd: string, home?: string): Promise<ScopedInstalledPlugin[]> {
|
|
166
195
|
const projectOverrides = await loadProjectOverrides(cwd);
|
|
167
196
|
|
|
168
197
|
const userRoot = getPluginsDir(home);
|
|
@@ -458,10 +458,17 @@ export class PluginManager {
|
|
|
458
458
|
stderr: "pipe",
|
|
459
459
|
windowsHide: true,
|
|
460
460
|
});
|
|
461
|
-
|
|
461
|
+
// Drain stdout+stderr concurrently with proc.exited. Awaiting exited
|
|
462
|
+
// before reading either pipe risks a >64 KiB OS-pipe-buffer deadlock
|
|
463
|
+
// once bun install prints enough progress; even where Bun currently
|
|
464
|
+
// buffers eagerly, doing this leaks unbounded memory.
|
|
465
|
+
const [installExit, , installStderr] = await Promise.all([
|
|
466
|
+
installProc.exited,
|
|
467
|
+
new Response(installProc.stdout).text(),
|
|
468
|
+
new Response(installProc.stderr).text(),
|
|
469
|
+
]);
|
|
462
470
|
if (installExit !== 0) {
|
|
463
|
-
|
|
464
|
-
throw new Error(`bun install failed: ${stderr}`);
|
|
471
|
+
throw new Error(`bun install failed: ${installStderr}`);
|
|
465
472
|
}
|
|
466
473
|
// Resolve actual package name. npm specs encode the name (strip version);
|
|
467
474
|
// git specs do not, so diff plugins/package.json deps to find the new entry.
|
|
@@ -508,10 +515,14 @@ export class PluginManager {
|
|
|
508
515
|
stderr: "pipe",
|
|
509
516
|
windowsHide: true,
|
|
510
517
|
});
|
|
511
|
-
|
|
518
|
+
// Same drain-concurrent-with-exit pattern as the bun install above.
|
|
519
|
+
const [updateExit, , updateStderr] = await Promise.all([
|
|
520
|
+
updateProc.exited,
|
|
521
|
+
new Response(updateProc.stdout).text(),
|
|
522
|
+
new Response(updateProc.stderr).text(),
|
|
523
|
+
]);
|
|
512
524
|
if (updateExit !== 0) {
|
|
513
|
-
|
|
514
|
-
throw new Error(`bun update ${actualName} failed: ${stderr}`);
|
|
525
|
+
throw new Error(`bun update ${actualName} failed: ${updateStderr}`);
|
|
515
526
|
}
|
|
516
527
|
}
|
|
517
528
|
|
|
@@ -608,7 +619,13 @@ export class PluginManager {
|
|
|
608
619
|
windowsHide: true,
|
|
609
620
|
});
|
|
610
621
|
|
|
611
|
-
|
|
622
|
+
// Drain both pipes concurrently with proc.exited to avoid a pipe-buffer
|
|
623
|
+
// deadlock if bun uninstall floods stdout/stderr.
|
|
624
|
+
const [exitCode] = await Promise.all([
|
|
625
|
+
proc.exited,
|
|
626
|
+
new Response(proc.stdout).text(),
|
|
627
|
+
new Response(proc.stderr).text(),
|
|
628
|
+
]);
|
|
612
629
|
if (exitCode !== 0) {
|
|
613
630
|
throw new Error(`npm uninstall failed for ${name}`);
|
|
614
631
|
}
|
|
@@ -1007,7 +1024,14 @@ export class PluginManager {
|
|
|
1007
1024
|
stderr: "pipe",
|
|
1008
1025
|
windowsHide: true,
|
|
1009
1026
|
});
|
|
1010
|
-
|
|
1027
|
+
// Drain pipes concurrently with proc.exited; otherwise a chatty
|
|
1028
|
+
// bun install can block on a full OS pipe buffer.
|
|
1029
|
+
const [exit] = await Promise.all([
|
|
1030
|
+
proc.exited,
|
|
1031
|
+
new Response(proc.stdout).text(),
|
|
1032
|
+
new Response(proc.stderr).text(),
|
|
1033
|
+
]);
|
|
1034
|
+
return exit === 0;
|
|
1011
1035
|
} catch {
|
|
1012
1036
|
return false;
|
|
1013
1037
|
}
|
|
@@ -86,20 +86,22 @@ export function formatPluginSpec(spec: ParsedPluginSpec): string {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* Extract the
|
|
89
|
+
* Extract the dependency key from an npm package specifier.
|
|
90
90
|
* Used for path lookups after npm install.
|
|
91
91
|
*
|
|
92
92
|
* @example
|
|
93
93
|
* extractPackageName("lodash@4.17.21") // "lodash"
|
|
94
94
|
* extractPackageName("@scope/pkg@1.0.0") // "@scope/pkg"
|
|
95
95
|
* extractPackageName("@scope/pkg") // "@scope/pkg"
|
|
96
|
+
* extractPackageName("npm:lodash") // "lodash"
|
|
96
97
|
*/
|
|
97
98
|
export function extractPackageName(specifier: string): string {
|
|
99
|
+
const npmSpecifier = specifier.replace(/^npm:/i, "");
|
|
98
100
|
// Handle scoped packages: @scope/name@version -> @scope/name
|
|
99
|
-
if (
|
|
100
|
-
const match =
|
|
101
|
-
return match ? match[1] :
|
|
101
|
+
if (npmSpecifier.startsWith("@")) {
|
|
102
|
+
const match = npmSpecifier.match(/^(@[^/]+\/[^@]+)/);
|
|
103
|
+
return match ? match[1] : npmSpecifier;
|
|
102
104
|
}
|
|
103
105
|
// Unscoped: name@version -> name
|
|
104
|
-
return
|
|
106
|
+
return npmSpecifier.replace(/@[^@]+$/, "");
|
|
105
107
|
}
|
|
@@ -60,7 +60,7 @@ export function normalizeToolEventInput(toolName: string, input: Record<string,
|
|
|
60
60
|
if (toolName !== "edit" || stringField(input, "path")) return input;
|
|
61
61
|
|
|
62
62
|
// Hashline edit mode: the only authoritative target list is the parsed
|
|
63
|
-
// `¶PATH#TAG` headers inside
|
|
63
|
+
// `¶PATH#TAG` headers inside the patch. Trusting a passthrough
|
|
64
64
|
// `_path` here would let a model-supplied field override the real edit
|
|
65
65
|
// target and bypass extension gates that allowlist by path.
|
|
66
66
|
const rawInput = stringField(input, "input") ?? stringField(input, "_input");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
+
import { HindsightApi } from "./client";
|
|
3
|
+
|
|
4
|
+
type FetchInput = string | URL | Request;
|
|
5
|
+
type FetchInit = RequestInit | BunFetchRequestInit;
|
|
6
|
+
|
|
7
|
+
describe("HindsightApi fetch cancellation", () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("combines caller cancellation with the request timeout", async () => {
|
|
13
|
+
let requestSignal: AbortSignal | undefined;
|
|
14
|
+
const fetchStub = Object.assign(
|
|
15
|
+
async (_input: FetchInput, init?: FetchInit) => {
|
|
16
|
+
requestSignal = init?.signal ?? undefined;
|
|
17
|
+
return Response.json({ results: [] });
|
|
18
|
+
},
|
|
19
|
+
{ preconnect: globalThis.fetch.preconnect },
|
|
20
|
+
);
|
|
21
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(fetchStub);
|
|
22
|
+
|
|
23
|
+
const caller = new AbortController();
|
|
24
|
+
const client = new HindsightApi({ baseUrl: "https://hindsight.example" });
|
|
25
|
+
await client.recall("bank", "query", { signal: caller.signal });
|
|
26
|
+
|
|
27
|
+
expect(requestSignal).toBeInstanceOf(AbortSignal);
|
|
28
|
+
expect(requestSignal).not.toBe(caller.signal);
|
|
29
|
+
caller.abort(new Error("caller aborted"));
|
|
30
|
+
expect(requestSignal?.aborted).toBe(true);
|
|
31
|
+
expect(requestSignal?.reason).toBe(caller.signal.reason);
|
|
32
|
+
});
|
|
33
|
+
});
|