@oh-my-pi/pi-coding-agent 13.16.4 → 13.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/package.json +7 -7
- package/src/cli/args.ts +7 -0
- package/src/cli/classify-install-target.ts +50 -0
- package/src/cli/plugin-cli.ts +245 -31
- package/src/commands/plugin.ts +3 -0
- package/src/config/model-registry.ts +37 -0
- package/src/config/model-resolver.ts +18 -3
- package/src/config/settings-schema.ts +24 -13
- package/src/cursor.ts +66 -1
- package/src/discovery/claude-plugins.ts +95 -5
- package/src/discovery/helpers.ts +168 -41
- package/src/discovery/plugin-dir-roots.ts +28 -0
- package/src/discovery/substitute-plugin-root.ts +29 -0
- package/src/extensibility/plugins/index.ts +1 -0
- package/src/extensibility/plugins/marketplace/cache.ts +136 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +354 -0
- package/src/extensibility/plugins/marketplace/index.ts +6 -0
- package/src/extensibility/plugins/marketplace/manager.ts +528 -0
- package/src/extensibility/plugins/marketplace/registry.ts +181 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
- package/src/extensibility/plugins/marketplace/types.ts +177 -0
- package/src/extensibility/skills.ts +3 -3
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/local-protocol.ts +2 -19
- package/src/internal-urls/parse.ts +72 -0
- package/src/internal-urls/router.ts +2 -18
- package/src/lsp/config.ts +9 -0
- package/src/main.ts +50 -1
- package/src/modes/components/plugin-selector.ts +86 -0
- package/src/modes/components/settings-defs.ts +9 -4
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/mcp-command-controller.ts +14 -0
- package/src/modes/controllers/selector-controller.ts +104 -13
- package/src/modes/interactive-mode.ts +9 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/agents/reviewer.md +3 -4
- package/src/prompts/tools/bash.md +3 -3
- package/src/sdk.ts +0 -7
- package/src/session/agent-session.ts +292 -6
- package/src/slash-commands/builtin-registry.ts +273 -0
- package/src/tools/bash-skill-urls.ts +48 -5
- package/src/tools/bash.ts +2 -0
- package/src/tools/read.ts +15 -9
- package/src/web/search/code-search.ts +2 -179
- package/src/web/search/index.ts +2 -3
- package/src/web/search/types.ts +1 -5
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
1
4
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai";
|
|
5
|
+
import { getConfigDirName } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { invalidate as invalidateFsCache } from "../capability/fs";
|
|
2
7
|
import type { SettingPath, SettingValue } from "../config/settings";
|
|
3
8
|
import { settings } from "../config/settings";
|
|
9
|
+
import { clearClaudePluginRootsCache } from "../discovery/helpers.js";
|
|
10
|
+
import { PluginManager } from "../extensibility/plugins";
|
|
11
|
+
import {
|
|
12
|
+
getInstalledPluginsRegistryPath,
|
|
13
|
+
getMarketplacesCacheDir,
|
|
14
|
+
getMarketplacesRegistryPath,
|
|
15
|
+
getPluginsCacheDir,
|
|
16
|
+
MarketplaceManager,
|
|
17
|
+
} from "../extensibility/plugins/marketplace";
|
|
4
18
|
import type { InteractiveModeContext } from "../modes/types";
|
|
5
19
|
|
|
6
20
|
function refreshStatusLine(ctx: InteractiveModeContext): void {
|
|
@@ -543,6 +557,265 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
543
557
|
description: "Exit the application",
|
|
544
558
|
handle: shutdownHandler,
|
|
545
559
|
},
|
|
560
|
+
{
|
|
561
|
+
name: "marketplace",
|
|
562
|
+
description: "Manage marketplace plugin sources and installed plugins",
|
|
563
|
+
subcommands: [
|
|
564
|
+
{ name: "add", description: "Add a marketplace source", usage: "<source>" },
|
|
565
|
+
{ name: "remove", description: "Remove a marketplace source", usage: "<name>" },
|
|
566
|
+
{ name: "update", description: "Update marketplace catalog(s)", usage: "[name]" },
|
|
567
|
+
{ name: "list", description: "List configured marketplaces" },
|
|
568
|
+
{ name: "discover", description: "Browse available plugins", usage: "[marketplace]" },
|
|
569
|
+
{
|
|
570
|
+
name: "install",
|
|
571
|
+
description: "Install a plugin (interactive browser if no args)",
|
|
572
|
+
usage: "[--force] [name@marketplace]",
|
|
573
|
+
},
|
|
574
|
+
{ name: "uninstall", description: "Uninstall a plugin (selector if no args)", usage: "[name@marketplace]" },
|
|
575
|
+
{ name: "installed", description: "List installed marketplace plugins" },
|
|
576
|
+
{ name: "upgrade", description: "Upgrade outdated plugins", usage: "[name@marketplace]" },
|
|
577
|
+
],
|
|
578
|
+
allowArgs: true,
|
|
579
|
+
handle: async (command, runtime) => {
|
|
580
|
+
runtime.ctx.editor.setText("");
|
|
581
|
+
const args = command.args.trim().split(/\s+/);
|
|
582
|
+
const sub = args[0] || "install";
|
|
583
|
+
const rest = args.slice(1).join(" ").trim();
|
|
584
|
+
|
|
585
|
+
// /marketplace (no args) or /marketplace install (no args) → interactive browser
|
|
586
|
+
if ((sub === "install" && !rest) || (!args[0] && !command.args.trim())) {
|
|
587
|
+
try {
|
|
588
|
+
runtime.ctx.showPluginSelector("install");
|
|
589
|
+
} catch (err) {
|
|
590
|
+
runtime.ctx.showStatus(`Marketplace error: ${err}`);
|
|
591
|
+
}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const mgr = new MarketplaceManager({
|
|
596
|
+
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
|
597
|
+
installedRegistryPath: getInstalledPluginsRegistryPath(),
|
|
598
|
+
marketplacesCacheDir: getMarketplacesCacheDir(),
|
|
599
|
+
pluginsCacheDir: getPluginsCacheDir(),
|
|
600
|
+
clearPluginRootsCache: () => {
|
|
601
|
+
const home = os.homedir();
|
|
602
|
+
invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
|
|
603
|
+
invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
|
|
604
|
+
clearClaudePluginRootsCache();
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
switch (sub) {
|
|
610
|
+
case "add": {
|
|
611
|
+
if (!rest) {
|
|
612
|
+
runtime.ctx.showStatus("Usage: /marketplace add <source>");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const entry = await mgr.addMarketplace(rest);
|
|
616
|
+
runtime.ctx.showStatus(`Added marketplace: ${entry.name}`);
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
case "remove":
|
|
620
|
+
case "rm": {
|
|
621
|
+
if (!rest) {
|
|
622
|
+
runtime.ctx.showStatus("Usage: /marketplace remove <name>");
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
await mgr.removeMarketplace(rest);
|
|
626
|
+
runtime.ctx.showStatus(`Removed marketplace: ${rest}`);
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
case "update": {
|
|
630
|
+
if (rest) {
|
|
631
|
+
await mgr.updateMarketplace(rest);
|
|
632
|
+
runtime.ctx.showStatus(`Updated marketplace: ${rest}`);
|
|
633
|
+
} else {
|
|
634
|
+
const results = await mgr.updateAllMarketplaces();
|
|
635
|
+
runtime.ctx.showStatus(`Updated ${results.length} marketplace(s)`);
|
|
636
|
+
}
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
case "discover": {
|
|
640
|
+
const plugins = await mgr.listAvailablePlugins(rest || undefined);
|
|
641
|
+
if (plugins.length === 0) {
|
|
642
|
+
runtime.ctx.showStatus("No plugins available");
|
|
643
|
+
} else {
|
|
644
|
+
const lines = plugins.map(
|
|
645
|
+
p =>
|
|
646
|
+
` ${p.name}${p.version ? `@${p.version}` : ""}${p.description ? ` - ${p.description}` : ""}`,
|
|
647
|
+
);
|
|
648
|
+
runtime.ctx.showStatus(`Available plugins:\n${lines.join("\n")}`);
|
|
649
|
+
}
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
case "install": {
|
|
653
|
+
// Parse: /marketplace install [--force] name@marketplace
|
|
654
|
+
const force = rest.startsWith("--force ");
|
|
655
|
+
const installSpec = force ? rest.slice("--force ".length).trim() : rest;
|
|
656
|
+
if (!installSpec?.includes("@")) {
|
|
657
|
+
runtime.ctx.showStatus("Usage: /marketplace install [--force] <name@marketplace>");
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const atIdx = installSpec.lastIndexOf("@");
|
|
661
|
+
const name = installSpec.slice(0, atIdx);
|
|
662
|
+
const marketplace = installSpec.slice(atIdx + 1);
|
|
663
|
+
await mgr.installPlugin(name, marketplace, { force });
|
|
664
|
+
runtime.ctx.showStatus(`Installed ${name} from ${marketplace}`);
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
case "uninstall": {
|
|
668
|
+
if (!rest) {
|
|
669
|
+
// No args → open interactive uninstall selector
|
|
670
|
+
runtime.ctx.showPluginSelector("uninstall");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
await mgr.uninstallPlugin(rest);
|
|
674
|
+
runtime.ctx.showStatus(`Uninstalled ${rest}`);
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
case "installed": {
|
|
678
|
+
const installed = await mgr.listInstalledPlugins();
|
|
679
|
+
if (installed.length === 0) {
|
|
680
|
+
runtime.ctx.showStatus("No marketplace plugins installed");
|
|
681
|
+
} else {
|
|
682
|
+
const lines = installed.map(p => ` ${p.id} (${p.entries.length} entry)`);
|
|
683
|
+
runtime.ctx.showStatus(`Installed plugins:\n${lines.join("\n")}`);
|
|
684
|
+
}
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
case "upgrade": {
|
|
688
|
+
if (rest) {
|
|
689
|
+
const result = await mgr.upgradePlugin(rest);
|
|
690
|
+
runtime.ctx.showStatus(`Upgraded ${rest} to ${result.version}`);
|
|
691
|
+
} else {
|
|
692
|
+
const results = await mgr.upgradeAllPlugins();
|
|
693
|
+
if (results.length === 0) {
|
|
694
|
+
runtime.ctx.showStatus("All marketplace plugins are up to date");
|
|
695
|
+
} else {
|
|
696
|
+
const lines = results.map(r => ` ${r.pluginId}: ${r.from} -> ${r.to}`);
|
|
697
|
+
runtime.ctx.showStatus(`Upgraded ${results.length} plugin(s):\n${lines.join("\n")}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
default: {
|
|
703
|
+
// Default to list marketplaces
|
|
704
|
+
const marketplaces = await mgr.listMarketplaces();
|
|
705
|
+
if (marketplaces.length === 0) {
|
|
706
|
+
runtime.ctx.showStatus("No marketplaces configured. Use /marketplace add <source>");
|
|
707
|
+
} else {
|
|
708
|
+
const lines = marketplaces.map(m => ` ${m.name} ${m.sourceUri}`);
|
|
709
|
+
runtime.ctx.showStatus(`Marketplaces:\n${lines.join("\n")}`);
|
|
710
|
+
}
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
} catch (err) {
|
|
715
|
+
runtime.ctx.showStatus(`Marketplace error: ${err}`);
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
name: "plugins",
|
|
721
|
+
description: "View and manage installed plugins",
|
|
722
|
+
subcommands: [
|
|
723
|
+
{ name: "list", description: "List all installed plugins (npm + marketplace)" },
|
|
724
|
+
{ name: "enable", description: "Enable a marketplace plugin", usage: "<name@marketplace>" },
|
|
725
|
+
{ name: "disable", description: "Disable a marketplace plugin", usage: "<name@marketplace>" },
|
|
726
|
+
],
|
|
727
|
+
allowArgs: true,
|
|
728
|
+
handle: async (command, runtime) => {
|
|
729
|
+
runtime.ctx.editor.setText("");
|
|
730
|
+
const args = command.args.trim().split(/\s+/);
|
|
731
|
+
const sub = args[0] || "list";
|
|
732
|
+
const rest = args.slice(1).join(" ").trim();
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
const mgr = new MarketplaceManager({
|
|
736
|
+
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
|
737
|
+
installedRegistryPath: getInstalledPluginsRegistryPath(),
|
|
738
|
+
marketplacesCacheDir: getMarketplacesCacheDir(),
|
|
739
|
+
pluginsCacheDir: getPluginsCacheDir(),
|
|
740
|
+
clearPluginRootsCache: () => {
|
|
741
|
+
const home = os.homedir();
|
|
742
|
+
invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
|
|
743
|
+
invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
|
|
744
|
+
clearClaudePluginRootsCache();
|
|
745
|
+
},
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
switch (sub) {
|
|
749
|
+
case "enable": {
|
|
750
|
+
if (!rest) {
|
|
751
|
+
runtime.ctx.showStatus("Usage: /plugins enable <name@marketplace>");
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
await mgr.setPluginEnabled(rest, true);
|
|
755
|
+
runtime.ctx.showStatus(`Enabled ${rest}`);
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
case "disable": {
|
|
759
|
+
if (!rest) {
|
|
760
|
+
runtime.ctx.showStatus("Usage: /plugins disable <name@marketplace>");
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
await mgr.setPluginEnabled(rest, false);
|
|
764
|
+
runtime.ctx.showStatus(`Disabled ${rest}`);
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
default: {
|
|
768
|
+
const lines: string[] = [];
|
|
769
|
+
|
|
770
|
+
const npm = new PluginManager();
|
|
771
|
+
const npmPlugins = await npm.list();
|
|
772
|
+
if (npmPlugins.length > 0) {
|
|
773
|
+
lines.push("npm plugins:");
|
|
774
|
+
for (const p of npmPlugins) {
|
|
775
|
+
const status = p.enabled === false ? " (disabled)" : "";
|
|
776
|
+
lines.push(` ${p.name}@${p.version}${status}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const mktPlugins = await mgr.listInstalledPlugins();
|
|
781
|
+
if (mktPlugins.length > 0) {
|
|
782
|
+
if (lines.length > 0) lines.push("");
|
|
783
|
+
lines.push("marketplace plugins:");
|
|
784
|
+
for (const p of mktPlugins) {
|
|
785
|
+
const entry = p.entries[0];
|
|
786
|
+
const status = entry?.enabled === false ? " (disabled)" : "";
|
|
787
|
+
lines.push(` ${p.id} v${entry?.version ?? "?"}${status}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (lines.length === 0) {
|
|
792
|
+
runtime.ctx.showStatus("No plugins installed");
|
|
793
|
+
} else {
|
|
794
|
+
runtime.ctx.showStatus(lines.join("\n"));
|
|
795
|
+
}
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
} catch (err) {
|
|
800
|
+
runtime.ctx.showStatus(`Plugin error: ${err}`);
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
name: "reload-plugins",
|
|
806
|
+
description: "Reload all plugins (skills, commands, hooks, tools, agents, MCP)",
|
|
807
|
+
handle: async (_command, runtime) => {
|
|
808
|
+
// Invalidate the fs content cache for both registry files so
|
|
809
|
+
// listClaudePluginRoots re-reads from disk on next access.
|
|
810
|
+
const home = os.homedir();
|
|
811
|
+
invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
|
|
812
|
+
invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
|
|
813
|
+
clearClaudePluginRootsCache();
|
|
814
|
+
await runtime.ctx.refreshSlashCommandState();
|
|
815
|
+
runtime.ctx.showStatus("Plugins reloaded.");
|
|
816
|
+
runtime.ctx.editor.setText("");
|
|
817
|
+
},
|
|
818
|
+
},
|
|
546
819
|
{
|
|
547
820
|
name: "quit",
|
|
548
821
|
description: "Quit the application",
|
|
@@ -40,19 +40,29 @@ export function resolveSkillUrlToPath(url: string, skills: readonly Skill[]): st
|
|
|
40
40
|
throw new ToolError(`Invalid skill:// URL: ${url}`);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
if (!
|
|
43
|
+
let rawSkillSegment = parsed[1];
|
|
44
|
+
if (!rawSkillSegment) {
|
|
45
45
|
throw new ToolError(`skill:// URL requires a skill name: ${url}`);
|
|
46
46
|
}
|
|
47
|
+
// Decode percent-encoded colons (%3A) used for namespaced skill names
|
|
48
|
+
try {
|
|
49
|
+
rawSkillSegment = decodeURIComponent(rawSkillSegment);
|
|
50
|
+
} catch {
|
|
51
|
+
// Leave as-is if decoding fails
|
|
52
|
+
}
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
// Resolve skill name by longest-prefix match against registered skills.
|
|
55
|
+
// This handles namespaced skills ("plugin:skill") where the URI may also
|
|
56
|
+
// carry a colon-delimited suffix (e.g., ":1-5" line range).
|
|
57
|
+
const { skill, suffix } = matchSkillName(rawSkillSegment, skills);
|
|
50
58
|
if (!skill) {
|
|
51
59
|
const available = skills.map(s => s.name);
|
|
52
60
|
const availableStr = available.length > 0 ? available.join(", ") : "none";
|
|
53
|
-
throw new ToolError(`Unknown skill: ${
|
|
61
|
+
throw new ToolError(`Unknown skill: ${rawSkillSegment}. Available: ${availableStr}`);
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
// Combine any colon suffix (line range like ":1-5") with the path segment
|
|
65
|
+
const rawPath = (parsed[2] ?? "") + (suffix ? `/${suffix}` : "");
|
|
56
66
|
const hasRelativePath = rawPath !== "" && rawPath !== "/";
|
|
57
67
|
|
|
58
68
|
if (!hasRelativePath) {
|
|
@@ -82,6 +92,39 @@ export function resolveSkillUrlToPath(url: string, skills: readonly Skill[]): st
|
|
|
82
92
|
return resolvedPath;
|
|
83
93
|
}
|
|
84
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Match a raw skill segment against registered skills using longest-prefix match.
|
|
97
|
+
* Handles colons in both skill names (namespacing) and suffixes (line ranges).
|
|
98
|
+
*
|
|
99
|
+
* For "superpowers:brainstorming:1-5" with skill "superpowers:brainstorming":
|
|
100
|
+
* -> skill = superpowers:brainstorming, suffix = "1-5"
|
|
101
|
+
* For "brainstorming" with skill "brainstorming":
|
|
102
|
+
* -> skill = brainstorming, suffix = undefined
|
|
103
|
+
*/
|
|
104
|
+
function matchSkillName(
|
|
105
|
+
rawSegment: string,
|
|
106
|
+
skills: readonly Skill[],
|
|
107
|
+
): { skill: Skill | undefined; suffix: string | undefined } {
|
|
108
|
+
// Exact match first (most common case)
|
|
109
|
+
const exact = skills.find(s => s.name === rawSegment);
|
|
110
|
+
if (exact) return { skill: exact, suffix: undefined };
|
|
111
|
+
|
|
112
|
+
// Try stripping colon-delimited suffixes from the right
|
|
113
|
+
let candidate = rawSegment;
|
|
114
|
+
while (true) {
|
|
115
|
+
const lastColon = candidate.lastIndexOf(":");
|
|
116
|
+
if (lastColon <= 0) break;
|
|
117
|
+
candidate = candidate.slice(0, lastColon);
|
|
118
|
+
const match = skills.find(s => s.name === candidate);
|
|
119
|
+
if (match) {
|
|
120
|
+
const suffix = rawSegment.slice(lastColon + 1);
|
|
121
|
+
return { skill: match, suffix };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { skill: undefined, suffix: undefined };
|
|
126
|
+
}
|
|
127
|
+
|
|
85
128
|
function extractScheme(url: string): SupportedInternalScheme | undefined {
|
|
86
129
|
const match = /^([a-z][a-z0-9+.-]*):\/\//i.exec(url);
|
|
87
130
|
if (!match) return undefined;
|
package/src/tools/bash.ts
CHANGED
|
@@ -222,6 +222,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
222
222
|
asyncEnabled: this.#asyncEnabled,
|
|
223
223
|
hasAstGrep: this.session.settings.get("astGrep.enabled"),
|
|
224
224
|
hasAstEdit: this.session.settings.get("astEdit.enabled"),
|
|
225
|
+
hasGrep: this.session.settings.get("grep.enabled"),
|
|
226
|
+
hasFind: this.session.settings.get("find.enabled"),
|
|
225
227
|
});
|
|
226
228
|
}
|
|
227
229
|
|
package/src/tools/read.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { getRemoteDir, ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
9
9
|
import { type Static, Type } from "@sinclair/typebox";
|
|
10
10
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
|
+
import { parseInternalUrl } from "../internal-urls/parse";
|
|
13
|
+
import type { InternalUrl } from "../internal-urls/types";
|
|
12
14
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
13
15
|
import { computeLineHash } from "../patch/hashline";
|
|
14
16
|
import readDescription from "../prompts/tools/read.md" with { type: "text" };
|
|
@@ -686,18 +688,22 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
686
688
|
async #handleInternalUrl(url: string, offset?: number, limit?: number): Promise<AgentToolResult<ReadToolDetails>> {
|
|
687
689
|
const internalRouter = this.session.internalRouter!;
|
|
688
690
|
|
|
689
|
-
// Check if URL has query extraction (agent:// only)
|
|
690
|
-
|
|
691
|
+
// Check if URL has query extraction (agent:// only).
|
|
692
|
+
// Use parseInternalUrl which handles colons in host (namespaced skills).
|
|
693
|
+
let parsed: InternalUrl;
|
|
691
694
|
try {
|
|
692
|
-
parsed =
|
|
693
|
-
} catch {
|
|
694
|
-
throw new ToolError(
|
|
695
|
+
parsed = parseInternalUrl(url);
|
|
696
|
+
} catch (e) {
|
|
697
|
+
throw new ToolError(e instanceof Error ? e.message : String(e));
|
|
695
698
|
}
|
|
696
699
|
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
700
|
+
let hasExtraction = false;
|
|
701
|
+
if (scheme === "agent") {
|
|
702
|
+
const hasPathExtraction = parsed.pathname && parsed.pathname !== "/" && parsed.pathname !== "";
|
|
703
|
+
const queryParam = parsed.searchParams.get("q");
|
|
704
|
+
const hasQueryExtraction = queryParam !== null && queryParam !== "";
|
|
705
|
+
hasExtraction = hasPathExtraction || hasQueryExtraction;
|
|
706
|
+
}
|
|
701
707
|
|
|
702
708
|
// Reject offset/limit with query extraction
|
|
703
709
|
if (hasExtraction && (offset !== undefined || limit !== undefined)) {
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
replaceTabs,
|
|
12
12
|
truncateToWidth,
|
|
13
13
|
} from "../../tools/render-utils";
|
|
14
|
-
import { decodeHtmlEntities } from "../scrapers/types";
|
|
15
14
|
import type { CodeSearchProviderId } from "./types";
|
|
16
15
|
|
|
17
16
|
export interface CodeSearchToolParams {
|
|
@@ -42,25 +41,6 @@ export interface CodeSearchRenderDetails {
|
|
|
42
41
|
provider: CodeSearchProviderId;
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
interface GrepApiHit {
|
|
46
|
-
repo: string;
|
|
47
|
-
branch: string;
|
|
48
|
-
path: string;
|
|
49
|
-
contentSnippet?: string;
|
|
50
|
-
totalMatches?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface GrepApiResponse {
|
|
54
|
-
totalResults?: number;
|
|
55
|
-
hits: GrepApiHit[];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let preferredCodeSearchProvider: CodeSearchProviderId = "grep";
|
|
59
|
-
|
|
60
|
-
export function setPreferredCodeSearchProvider(provider: CodeSearchProviderId): void {
|
|
61
|
-
preferredCodeSearchProvider = provider;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
44
|
function stringifyExaCodeResponse(payload: unknown): string {
|
|
65
45
|
if (typeof payload === "string") return payload;
|
|
66
46
|
if (typeof payload === "number" || typeof payload === "boolean") return String(payload);
|
|
@@ -91,161 +71,6 @@ function normalizeExaCodeSearchResponse(
|
|
|
91
71
|
};
|
|
92
72
|
}
|
|
93
73
|
|
|
94
|
-
function getStringProperty(value: object, key: string): string | undefined {
|
|
95
|
-
const candidate = Reflect.get(value, key);
|
|
96
|
-
return typeof candidate === "string" ? candidate : undefined;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function getNumberProperty(value: object, key: string): number | undefined {
|
|
100
|
-
const candidate = Reflect.get(value, key);
|
|
101
|
-
return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function getObjectProperty(value: object, key: string): object | undefined {
|
|
105
|
-
const candidate = Reflect.get(value, key);
|
|
106
|
-
return typeof candidate === "object" && candidate !== null ? candidate : undefined;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function getArrayProperty(value: object, key: string): unknown[] | undefined {
|
|
110
|
-
const candidate = Reflect.get(value, key);
|
|
111
|
-
return Array.isArray(candidate) ? candidate : undefined;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function stripHtmlTags(value: string): string {
|
|
115
|
-
return value
|
|
116
|
-
.replace(/<br\s*\/?>/gi, "")
|
|
117
|
-
.replace(/<\/?mark>/gi, "")
|
|
118
|
-
.replace(/<span[^>]*>/gi, "")
|
|
119
|
-
.replace(/<\/span>/gi, "")
|
|
120
|
-
.replace(/<[^>]+>/g, "");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function formatGrepSnippet(snippetHtml: string | undefined): string | undefined {
|
|
124
|
-
if (!snippetHtml) return undefined;
|
|
125
|
-
|
|
126
|
-
const rowPattern = /<tr[^>]*data-line="(\d+)"[^>]*>[\s\S]*?<pre>([\s\S]*?)<\/pre>[\s\S]*?<\/tr>/gi;
|
|
127
|
-
const lines: string[] = [];
|
|
128
|
-
|
|
129
|
-
for (const match of snippetHtml.matchAll(rowPattern)) {
|
|
130
|
-
const lineNumber = match[1];
|
|
131
|
-
const rawCode = match[2] ?? "";
|
|
132
|
-
const text = decodeHtmlEntities(stripHtmlTags(rawCode)).trimEnd();
|
|
133
|
-
if (text.length === 0) continue;
|
|
134
|
-
lines.push(`${lineNumber}: ${text}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (lines.length > 0) {
|
|
138
|
-
return lines.join("\n");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const plainText = decodeHtmlEntities(stripHtmlTags(snippetHtml)).replace(/\s+\n/g, "\n").trim();
|
|
142
|
-
return plainText.length > 0 ? plainText : undefined;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function parseGrepApiResponse(payload: unknown): GrepApiResponse | null {
|
|
146
|
-
if (typeof payload !== "object" || payload === null) return null;
|
|
147
|
-
|
|
148
|
-
const hitsObject = getObjectProperty(payload, "hits");
|
|
149
|
-
if (!hitsObject) return null;
|
|
150
|
-
|
|
151
|
-
const hitValues = getArrayProperty(hitsObject, "hits") ?? [];
|
|
152
|
-
const hits: GrepApiHit[] = [];
|
|
153
|
-
for (const item of hitValues) {
|
|
154
|
-
if (typeof item !== "object" || item === null) continue;
|
|
155
|
-
const repo = getStringProperty(item, "repo");
|
|
156
|
-
const branch = getStringProperty(item, "branch");
|
|
157
|
-
const path = getStringProperty(item, "path");
|
|
158
|
-
if (!repo || !branch || !path) continue;
|
|
159
|
-
|
|
160
|
-
const content = getObjectProperty(item, "content");
|
|
161
|
-
hits.push({
|
|
162
|
-
repo,
|
|
163
|
-
branch,
|
|
164
|
-
path,
|
|
165
|
-
contentSnippet: content ? getStringProperty(content, "snippet") : undefined,
|
|
166
|
-
totalMatches: getStringProperty(item, "total_matches"),
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
if (hitValues.length > 0 && hits.length === 0) return null;
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
totalResults: getNumberProperty(hitsObject, "total"),
|
|
173
|
-
hits,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function buildGrepQuery(params: CodeSearchToolParams): string {
|
|
178
|
-
return params.query.trim();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function tokenizeCodeContext(codeContext: string | undefined): string[] {
|
|
182
|
-
if (!codeContext) return [];
|
|
183
|
-
return codeContext
|
|
184
|
-
.toLowerCase()
|
|
185
|
-
.split(/[^a-z0-9_./:-]+/i)
|
|
186
|
-
.filter(token => token.length >= 2);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function scoreGrepHit(hit: GrepApiHit, contextTokens: string[]): number {
|
|
190
|
-
if (contextTokens.length === 0) return 0;
|
|
191
|
-
const snippet = formatGrepSnippet(hit.contentSnippet)?.toLowerCase() ?? "";
|
|
192
|
-
const repo = hit.repo.toLowerCase();
|
|
193
|
-
const path = hit.path.toLowerCase();
|
|
194
|
-
|
|
195
|
-
let score = 0;
|
|
196
|
-
for (const token of contextTokens) {
|
|
197
|
-
if (repo.includes(token)) score += 4;
|
|
198
|
-
if (path.includes(token)) score += 3;
|
|
199
|
-
if (snippet.includes(token)) score += 2;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return score;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export async function searchCodeWithGrep(params: CodeSearchToolParams): Promise<CodeSearchResponse> {
|
|
206
|
-
const query = buildGrepQuery(params);
|
|
207
|
-
const url = new URL("https://grep.app/api/search");
|
|
208
|
-
url.searchParams.set("q", query);
|
|
209
|
-
|
|
210
|
-
const response = await fetch(url, {
|
|
211
|
-
headers: {
|
|
212
|
-
Accept: "application/json",
|
|
213
|
-
Referer: `https://grep.app/search?q=${encodeURIComponent(query)}`,
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
if (!response.ok) {
|
|
218
|
-
const message = await response.text();
|
|
219
|
-
throw new Error(`grep.app API error (${response.status}): ${message}`);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const payload: unknown = await response.json();
|
|
223
|
-
const parsed = parseGrepApiResponse(payload);
|
|
224
|
-
if (!parsed) {
|
|
225
|
-
throw new Error("grep.app returned an unexpected response shape.");
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const contextTokens = tokenizeCodeContext(params.code_context);
|
|
229
|
-
const rankedHits = [...parsed.hits].sort(
|
|
230
|
-
(left, right) => scoreGrepHit(right, contextTokens) - scoreGrepHit(left, contextTokens),
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
provider: "grep",
|
|
235
|
-
query,
|
|
236
|
-
totalResults: parsed.totalResults,
|
|
237
|
-
sources: rankedHits.map(hit => ({
|
|
238
|
-
title: `${hit.repo}/${hit.path}`,
|
|
239
|
-
url: `https://github.com/${hit.repo}/blob/${hit.branch}/${hit.path}`,
|
|
240
|
-
repository: hit.repo,
|
|
241
|
-
path: hit.path,
|
|
242
|
-
branch: hit.branch,
|
|
243
|
-
snippet: formatGrepSnippet(hit.contentSnippet),
|
|
244
|
-
totalMatches: hit.totalMatches,
|
|
245
|
-
})),
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
74
|
async function searchCodeWithExa(params: CodeSearchToolParams): Promise<CodeSearchResponse> {
|
|
250
75
|
const exaParams = params.code_context
|
|
251
76
|
? { query: params.query, code_context: params.code_context }
|
|
@@ -290,8 +115,7 @@ export async function executeCodeSearch(
|
|
|
290
115
|
params: CodeSearchToolParams,
|
|
291
116
|
): Promise<CustomToolResult<CodeSearchRenderDetails>> {
|
|
292
117
|
try {
|
|
293
|
-
const response =
|
|
294
|
-
preferredCodeSearchProvider === "grep" ? await searchCodeWithGrep(params) : await searchCodeWithExa(params);
|
|
118
|
+
const response = await searchCodeWithExa(params);
|
|
295
119
|
|
|
296
120
|
return {
|
|
297
121
|
content: [{ type: "text", text: formatCodeSearchForLlm(response) }],
|
|
@@ -301,7 +125,7 @@ export async function executeCodeSearch(
|
|
|
301
125
|
const message = error instanceof Error ? error.message : String(error);
|
|
302
126
|
return {
|
|
303
127
|
content: [{ type: "text", text: `Error: ${message}` }],
|
|
304
|
-
details: { provider:
|
|
128
|
+
details: { provider: "exa", error: message },
|
|
305
129
|
};
|
|
306
130
|
}
|
|
307
131
|
}
|
|
@@ -312,7 +136,6 @@ export function renderCodeSearchCall(
|
|
|
312
136
|
theme: Theme,
|
|
313
137
|
): Component {
|
|
314
138
|
let text = `${theme.fg("toolTitle", "Code Search")} ${theme.fg("accent", truncateToWidth(args.query, 80))}`;
|
|
315
|
-
text += ` ${theme.fg("muted", `provider:${preferredCodeSearchProvider}`)}`;
|
|
316
139
|
if (args.code_context) {
|
|
317
140
|
text += ` ${theme.fg("dim", truncateToWidth(args.code_context, 40))}`;
|
|
318
141
|
}
|
package/src/web/search/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Tavily, Kagi, Z.AI, and Synthetic
|
|
5
5
|
* providers with provider-specific parameters exposed conditionally.
|
|
6
6
|
*
|
|
7
|
-
* Code search is also supported via the code_search tool
|
|
7
|
+
* Code search is also supported via the code_search tool.
|
|
8
8
|
*/
|
|
9
9
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
10
10
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
@@ -292,7 +292,6 @@ export function getSearchTools(): CustomTool<any, any>[] {
|
|
|
292
292
|
return [webSearchCustomTool, codeSearchTool];
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
export { setPreferredCodeSearchProvider } from "./code-search";
|
|
296
295
|
export { getSearchProvider, setPreferredSearchProvider } from "./provider";
|
|
297
296
|
export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
|
|
298
|
-
export {
|
|
297
|
+
export { isSearchProviderPreference } from "./types";
|
package/src/web/search/types.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type SearchProviderId =
|
|
|
20
20
|
| "kagi"
|
|
21
21
|
| "synthetic";
|
|
22
22
|
|
|
23
|
-
export type CodeSearchProviderId = "
|
|
23
|
+
export type CodeSearchProviderId = "exa";
|
|
24
24
|
|
|
25
25
|
export function isSearchProviderId(value: string): value is SearchProviderId {
|
|
26
26
|
return [
|
|
@@ -44,10 +44,6 @@ export function isSearchProviderPreference(value: string): value is SearchProvid
|
|
|
44
44
|
return value === "auto" || isSearchProviderId(value);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function isCodeSearchProviderId(value: string): value is CodeSearchProviderId {
|
|
48
|
-
return value === "grep" || value === "exa";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
/** Source returned by search (all providers) */
|
|
52
48
|
export interface SearchSource {
|
|
53
49
|
title: string;
|