@oh-my-pi/pi-coding-agent 13.17.0 → 13.17.5
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 +102 -0
- package/package.json +10 -8
- package/scripts/format-prompts.ts +3 -3
- package/src/cli/plugin-cli.ts +114 -25
- package/src/commands/plugin.ts +5 -0
- package/src/commit/agentic/prompts/analyze-file.md +1 -1
- package/src/commit/agentic/prompts/session-user.md +1 -1
- package/src/commit/agentic/prompts/split-confirm.md +1 -1
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/git/index.ts +3 -4
- package/src/commit/model-selection.ts +1 -19
- package/src/commit/prompts/analysis-system.md +1 -1
- package/src/commit/prompts/analysis-user.md +1 -1
- package/src/commit/prompts/changelog-system.md +1 -1
- package/src/commit/prompts/changelog-user.md +1 -1
- package/src/commit/prompts/file-observer-system.md +1 -1
- package/src/commit/prompts/file-observer-user.md +1 -1
- package/src/commit/prompts/reduce-system.md +1 -1
- package/src/commit/prompts/reduce-user.md +1 -1
- package/src/commit/prompts/summary-retry.md +1 -1
- package/src/commit/prompts/summary-system.md +1 -1
- package/src/commit/prompts/summary-user.md +1 -1
- package/src/commit/prompts/types-description.md +1 -1
- package/src/config/model-registry.ts +19 -3
- package/src/config/model-resolver.ts +21 -0
- package/src/config/settings-schema.ts +16 -0
- package/src/discovery/claude-plugins.ts +5 -5
- package/src/discovery/helpers.ts +144 -24
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +37 -0
- package/src/extensibility/custom-commands/loader.ts +7 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +55 -37
- package/src/extensibility/plugins/marketplace/manager.ts +296 -73
- package/src/extensibility/plugins/marketplace/registry.ts +15 -0
- package/src/extensibility/plugins/marketplace/types.ts +17 -3
- package/src/internal-urls/docs-index.generated.ts +2 -1
- package/src/main.ts +14 -4
- package/src/modes/components/assistant-message.ts +2 -1
- package/src/modes/components/plugin-selector.ts +20 -11
- package/src/modes/components/tool-execution.ts +2 -3
- package/src/modes/controllers/command-controller.ts +19 -7
- package/src/modes/controllers/selector-controller.ts +7 -4
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/tools-markdown.ts +27 -0
- package/src/patch/shared.ts +28 -3
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/frontmatter.md +1 -1
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/oracle.md +1 -1
- package/src/prompts/agents/plan.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +1 -1
- package/src/prompts/ci-green-request.md +36 -0
- package/src/prompts/compaction/branch-summary-context.md +1 -1
- package/src/prompts/compaction/branch-summary-preamble.md +1 -1
- package/src/prompts/compaction/branch-summary.md +1 -1
- package/src/prompts/compaction/compaction-short-summary.md +1 -1
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +1 -1
- package/src/prompts/compaction/compaction-turn-prefix.md +1 -1
- package/src/prompts/compaction/compaction-update-summary.md +1 -1
- package/src/prompts/memories/consolidation.md +1 -1
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +1 -1
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +1 -1
- package/src/prompts/system/agent-creation-user.md +1 -1
- package/src/prompts/system/auto-handoff-threshold-focus.md +1 -1
- package/src/prompts/system/btw-user.md +1 -1
- package/src/prompts/system/commit-message-system.md +1 -1
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/eager-todo.md +1 -1
- package/src/prompts/system/file-operations.md +1 -1
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/plan-mode-reference.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +1 -1
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/system/subagent-submit-reminder.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +1 -1
- package/src/prompts/system/subagent-user-prompt.md +1 -1
- package/src/prompts/system/summarization-system.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/title-system.md +1 -1
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/system/web-search.md +1 -1
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/async-result.md +1 -1
- package/src/prompts/tools/await.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/calculator.md +1 -1
- package/src/prompts/tools/cancel-job.md +1 -1
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/exit-plan-mode.md +1 -1
- package/src/prompts/tools/fetch.md +1 -1
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/gemini-image.md +1 -1
- package/src/prompts/tools/gh-issue-view.md +11 -0
- package/src/prompts/tools/gh-pr-checkout.md +12 -0
- package/src/prompts/tools/gh-pr-diff.md +12 -0
- package/src/prompts/tools/gh-pr-push.md +11 -0
- package/src/prompts/tools/gh-pr-view.md +11 -0
- package/src/prompts/tools/gh-repo-view.md +11 -0
- package/src/prompts/tools/gh-run-watch.md +12 -0
- package/src/prompts/tools/gh-search-issues.md +11 -0
- package/src/prompts/tools/gh-search-prs.md +11 -0
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/hashline.md +1 -1
- package/src/prompts/tools/inspect-image-system.md +1 -1
- package/src/prompts/tools/inspect-image.md +1 -1
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/patch.md +1 -1
- package/src/prompts/tools/python.md +1 -1
- package/src/prompts/tools/read.md +6 -3
- package/src/prompts/tools/render-mermaid.md +1 -1
- package/src/prompts/tools/replace.md +1 -1
- package/src/prompts/tools/resolve.md +1 -1
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +1 -1
- package/src/prompts/tools/ssh.md +1 -1
- package/src/prompts/tools/task-summary.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/web-search.md +1 -1
- package/src/prompts/tools/write.md +2 -1
- package/src/sdk.ts +3 -1
- package/src/session/messages.ts +11 -7
- package/src/session/session-manager.ts +13 -3
- package/src/slash-commands/builtin-registry.ts +109 -37
- package/src/slash-commands/marketplace-install-parser.ts +99 -0
- package/src/task/discovery.ts +1 -1
- package/src/tools/archive-reader.ts +315 -0
- package/src/tools/auto-generated-guard.ts +1 -1
- package/src/tools/fetch.ts +21 -19
- package/src/tools/gh-cli.ts +125 -0
- package/src/tools/gh-renderer.ts +305 -0
- package/src/tools/gh.ts +2719 -0
- package/src/tools/index.ts +22 -0
- package/src/tools/read.ts +286 -34
- package/src/tools/render-utils.ts +22 -2
- package/src/tools/renderers.ts +2 -0
- package/src/tools/write.ts +175 -4
- package/src/utils/markit.ts +81 -0
- package/src/utils/title-generator.ts +4 -8
- package/src/utils/tools-manager.ts +1 -6
- package/src/web/scrapers/arxiv.ts +3 -3
- package/src/web/scrapers/iacr.ts +3 -3
- package/src/web/scrapers/utils.ts +6 -34
- package/src/web/search/index.ts +1 -36
- package/src/web/search/types.ts +0 -2
- package/src/prompts/tools/code-search.md +0 -45
- package/src/web/search/code-search.ts +0 -208
|
@@ -31,7 +31,7 @@ import { type ConfigError, ConfigFile } from "../config";
|
|
|
31
31
|
import { parseModelString } from "../config/model-resolver";
|
|
32
32
|
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
|
33
33
|
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
34
|
-
import type
|
|
34
|
+
import { type Settings, settings } from "./settings";
|
|
35
35
|
|
|
36
36
|
export const kNoAuth = "N/A";
|
|
37
37
|
|
|
@@ -730,6 +730,14 @@ function normalizeSuppressedSelector(selector: string): string {
|
|
|
730
730
|
return `${parsed.provider}/${parsed.id}`;
|
|
731
731
|
}
|
|
732
732
|
|
|
733
|
+
function getDisabledProviderIdsFromSettings(): Set<string> {
|
|
734
|
+
try {
|
|
735
|
+
return new Set(settings.get("disabledProviders"));
|
|
736
|
+
} catch {
|
|
737
|
+
return new Set();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
733
741
|
/**
|
|
734
742
|
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
735
743
|
*/
|
|
@@ -1670,11 +1678,19 @@ export class ModelRegistry {
|
|
|
1670
1678
|
* This is a fast check that doesn't refresh OAuth tokens.
|
|
1671
1679
|
*/
|
|
1672
1680
|
getAvailable(): Model<Api>[] {
|
|
1673
|
-
|
|
1681
|
+
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
1682
|
+
return this.#models.filter(
|
|
1683
|
+
m =>
|
|
1684
|
+
!disabledProviders.has(m.provider) &&
|
|
1685
|
+
(this.#keylessProviders.has(m.provider) || this.authStorage.hasAuth(m.provider)),
|
|
1686
|
+
);
|
|
1674
1687
|
}
|
|
1675
1688
|
|
|
1676
1689
|
getDiscoverableProviders(): string[] {
|
|
1677
|
-
|
|
1690
|
+
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
1691
|
+
return this.#discoverableProviders
|
|
1692
|
+
.filter(provider => !disabledProviders.has(provider.provider))
|
|
1693
|
+
.map(provider => provider.provider);
|
|
1678
1694
|
}
|
|
1679
1695
|
|
|
1680
1696
|
getProviderDiscoveryState(provider: string): ProviderDiscoveryState | undefined {
|
|
@@ -587,6 +587,27 @@ export function resolveModelOverride(
|
|
|
587
587
|
return { explicitThinkingLevel: false };
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Resolve a list of role patterns to the first matching model.
|
|
592
|
+
*/
|
|
593
|
+
export function resolveRoleSelection(
|
|
594
|
+
roles: readonly string[],
|
|
595
|
+
settings: Settings,
|
|
596
|
+
availableModels: Model<Api>[],
|
|
597
|
+
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
|
|
598
|
+
const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
|
|
599
|
+
for (const role of roles) {
|
|
600
|
+
const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
|
|
601
|
+
settings,
|
|
602
|
+
matchPreferences,
|
|
603
|
+
});
|
|
604
|
+
if (resolved.model) {
|
|
605
|
+
return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
610
|
+
|
|
590
611
|
/**
|
|
591
612
|
* Resolve model patterns to actual Model objects with optional thinking levels
|
|
592
613
|
* Format: "pattern:level" where :level is optional
|
|
@@ -372,6 +372,12 @@ export const SETTINGS_SCHEMA = {
|
|
|
372
372
|
"Maximum width in terminal columns for inline images (default 100). Set to 0 for unlimited (bounded only by terminal width).",
|
|
373
373
|
},
|
|
374
374
|
|
|
375
|
+
"tui.maxInlineImageRows": {
|
|
376
|
+
type: "number",
|
|
377
|
+
default: 20,
|
|
378
|
+
description:
|
|
379
|
+
"Maximum height in terminal rows for inline images (default 20). Set to 0 to use only the viewport-based limit (60% of terminal height).",
|
|
380
|
+
},
|
|
375
381
|
// Display rendering
|
|
376
382
|
"display.tabWidth": {
|
|
377
383
|
type: "number",
|
|
@@ -1204,6 +1210,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
1204
1210
|
ui: { tab: "tools", label: "Fetch", description: "Enable the fetch tool for URL fetching" },
|
|
1205
1211
|
},
|
|
1206
1212
|
|
|
1213
|
+
"github.enabled": {
|
|
1214
|
+
type: "boolean",
|
|
1215
|
+
default: false,
|
|
1216
|
+
ui: {
|
|
1217
|
+
tab: "tools",
|
|
1218
|
+
label: "GitHub CLI",
|
|
1219
|
+
description: "Enable read-only gh_* tools for GitHub repository, issue, pull request, diff, and search access",
|
|
1220
|
+
},
|
|
1221
|
+
},
|
|
1222
|
+
|
|
1207
1223
|
"web_search.enabled": {
|
|
1208
1224
|
type: "boolean",
|
|
1209
1225
|
default: true,
|
|
@@ -36,7 +36,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
|
36
36
|
const items: Skill[] = [];
|
|
37
37
|
const warnings: string[] = [];
|
|
38
38
|
|
|
39
|
-
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home);
|
|
39
|
+
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
|
|
40
40
|
warnings.push(...rootWarnings);
|
|
41
41
|
|
|
42
42
|
const results = await Promise.all(
|
|
@@ -70,7 +70,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
|
|
|
70
70
|
const items: SlashCommand[] = [];
|
|
71
71
|
const warnings: string[] = [];
|
|
72
72
|
|
|
73
|
-
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home);
|
|
73
|
+
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
|
|
74
74
|
warnings.push(...rootWarnings);
|
|
75
75
|
|
|
76
76
|
const results = await Promise.all(
|
|
@@ -108,7 +108,7 @@ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
|
|
|
108
108
|
const items: Hook[] = [];
|
|
109
109
|
const warnings: string[] = [];
|
|
110
110
|
|
|
111
|
-
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home);
|
|
111
|
+
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
|
|
112
112
|
warnings.push(...rootWarnings);
|
|
113
113
|
|
|
114
114
|
const hookTypes = ["pre", "post"] as const;
|
|
@@ -155,7 +155,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
|
|
|
155
155
|
const items: CustomTool[] = [];
|
|
156
156
|
const warnings: string[] = [];
|
|
157
157
|
|
|
158
|
-
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home);
|
|
158
|
+
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
|
|
159
159
|
warnings.push(...rootWarnings);
|
|
160
160
|
|
|
161
161
|
const results = await Promise.all(
|
|
@@ -192,7 +192,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
192
192
|
const items: MCPServer[] = [];
|
|
193
193
|
const warnings: string[] = [];
|
|
194
194
|
|
|
195
|
-
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home);
|
|
195
|
+
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
|
|
196
196
|
warnings.push(...rootWarnings);
|
|
197
197
|
|
|
198
198
|
for (const root of roots) {
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
4
5
|
import { FileType, glob } from "@oh-my-pi/pi-natives";
|
|
5
|
-
import { CONFIG_DIR_NAME, getConfigDirName, tryParseJson } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { CONFIG_DIR_NAME, getConfigDirName, getProjectDir, tryParseJson } from "@oh-my-pi/pi-utils";
|
|
6
7
|
import { readDirEntries, readFile } from "../capability/fs";
|
|
7
8
|
import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
|
|
8
9
|
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
@@ -640,19 +641,96 @@ export function parseClaudePluginsRegistry(content: string): ClaudePluginsRegist
|
|
|
640
641
|
}
|
|
641
642
|
|
|
642
643
|
/**
|
|
643
|
-
*
|
|
644
|
-
*
|
|
644
|
+
* Resolve the active project registry path by walking up from `cwd`.
|
|
645
|
+
*
|
|
646
|
+
* Walk order:
|
|
647
|
+
* 1. Walk up from `cwd` looking for the nearest directory containing `.omp/`.
|
|
648
|
+
* The first match returns `<dir>/.omp/plugins/installed_plugins.json`.
|
|
649
|
+
* 2. If no `.omp/` is found, rescan from `cwd` upward looking for `.git`.
|
|
650
|
+
* The git root is used as an anchor: `<gitRoot>/.omp/plugins/installed_plugins.json`.
|
|
651
|
+
* 3. If neither is found, return `null` — no project context is active.
|
|
645
652
|
*
|
|
646
|
-
*
|
|
653
|
+
* This is the single source of truth for "active project root" used by install,
|
|
654
|
+
* uninstall, list, upgrade, discovery, and doctor. Deterministic for a given `cwd`.
|
|
647
655
|
*/
|
|
656
|
+
export async function resolveActiveProjectRegistryPath(cwd: string): Promise<string | null> {
|
|
657
|
+
// Pass 1: walk up looking for an existing .omp/ directory (nearest wins).
|
|
658
|
+
// Stop before os.homedir() — ~/.omp/ is the user-level config dir, not a project root.
|
|
659
|
+
const homeDir = os.homedir();
|
|
660
|
+
let dir = path.resolve(cwd);
|
|
661
|
+
while (dir !== homeDir) {
|
|
662
|
+
try {
|
|
663
|
+
const stat = await fs.promises.stat(path.join(dir, getConfigDirName()));
|
|
664
|
+
if (stat.isDirectory()) {
|
|
665
|
+
return path.join(dir, getConfigDirName(), "plugins", "installed_plugins.json");
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
// not found at this level — continue up
|
|
669
|
+
}
|
|
670
|
+
const parent = path.dirname(dir);
|
|
671
|
+
if (parent === dir) break; // filesystem root
|
|
672
|
+
dir = parent;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Pass 2: walk up looking for .git as a fallback anchor.
|
|
676
|
+
dir = path.resolve(cwd);
|
|
677
|
+
while (dir !== homeDir) {
|
|
678
|
+
try {
|
|
679
|
+
await fs.promises.stat(path.join(dir, ".git"));
|
|
680
|
+
return path.join(dir, getConfigDirName(), "plugins", "installed_plugins.json");
|
|
681
|
+
} catch {
|
|
682
|
+
// not found at this level — continue up
|
|
683
|
+
}
|
|
684
|
+
const parent = path.dirname(dir);
|
|
685
|
+
if (parent === dir) break; // filesystem root
|
|
686
|
+
dir = parent;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return null; // not inside any project
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Like resolveActiveProjectRegistryPath, but falls back to `<cwd>/.omp/plugins/installed_plugins.json`
|
|
694
|
+
* when no project anchor (.omp/ or .git/) is found.
|
|
695
|
+
*
|
|
696
|
+
* Use this when the caller accepts an explicit --scope project so that installing into a freshly
|
|
697
|
+
* bootstrapped directory (no .omp/ or .git/ yet) works: writeInstalledPluginsRegistry auto-creates
|
|
698
|
+
* the directory tree on first write.
|
|
699
|
+
*
|
|
700
|
+
* Returns undefined when cwd is os.homedir() — that path is already the user registry and must
|
|
701
|
+
* never alias as the project registry.
|
|
702
|
+
*/
|
|
703
|
+
export async function resolveOrDefaultProjectRegistryPath(cwd: string): Promise<string | undefined> {
|
|
704
|
+
const resolved = await resolveActiveProjectRegistryPath(cwd);
|
|
705
|
+
if (resolved) return resolved;
|
|
706
|
+
// Home directory must not be treated as a project root: the fallback path would alias
|
|
707
|
+
// getInstalledPluginsRegistryPath(), causing MarketplaceManager to load the same file
|
|
708
|
+
// as both user and project registry and producing duplicates / disambiguation errors.
|
|
709
|
+
if (path.resolve(cwd) === os.homedir()) return undefined;
|
|
710
|
+
return path.join(cwd, getConfigDirName(), "plugins", "installed_plugins.json");
|
|
711
|
+
}
|
|
712
|
+
|
|
648
713
|
const pluginRootsCache = new Map<string, { roots: ClaudePluginRoot[]; warnings: string[] }>();
|
|
649
714
|
|
|
650
|
-
|
|
651
|
-
|
|
715
|
+
/**
|
|
716
|
+
* List all installed Claude Code plugin roots from the plugin cache.
|
|
717
|
+
* Reads ~/.claude/plugins/installed_plugins.json and ~/.omp/plugins/installed_plugins.json,
|
|
718
|
+
* and optionally the nearest project-scoped registry resolved from `cwd`.
|
|
719
|
+
*
|
|
720
|
+
* Results are cached per `home:resolvedProjectPath` key to avoid repeated parsing.
|
|
721
|
+
*/
|
|
722
|
+
export async function listClaudePluginRoots(
|
|
723
|
+
home: string,
|
|
724
|
+
cwd?: string,
|
|
725
|
+
): Promise<{ roots: ClaudePluginRoot[]; warnings: string[] }> {
|
|
726
|
+
const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd) : null;
|
|
727
|
+
const cacheKey = `${home}:${resolvedProjectPath ?? ""}`;
|
|
728
|
+
const cached = pluginRootsCache.get(cacheKey);
|
|
652
729
|
if (cached) return cached;
|
|
653
730
|
|
|
654
731
|
const roots: ClaudePluginRoot[] = [];
|
|
655
732
|
const warnings: string[] = [];
|
|
733
|
+
const projectRoots: ClaudePluginRoot[] = [];
|
|
656
734
|
|
|
657
735
|
// ── Claude Code registry ──────────────────────────────────────────────────
|
|
658
736
|
const registryPath = path.join(home, ".claude", "plugins", "installed_plugins.json");
|
|
@@ -746,6 +824,53 @@ export async function listClaudePluginRoots(home: string): Promise<{ roots: Clau
|
|
|
746
824
|
}
|
|
747
825
|
}
|
|
748
826
|
|
|
827
|
+
// ── Project-scoped OMP registry ────────────────────────────────────────
|
|
828
|
+
// Loaded from the nearest .omp/plugins/installed_plugins.json relative to cwd.
|
|
829
|
+
// Project entries take precedence over user entries for the same plugin ID.
|
|
830
|
+
if (resolvedProjectPath) {
|
|
831
|
+
const projectContent = await readFile(resolvedProjectPath);
|
|
832
|
+
if (projectContent) {
|
|
833
|
+
const projectRegistry = parseClaudePluginsRegistry(projectContent);
|
|
834
|
+
if (projectRegistry) {
|
|
835
|
+
for (const [pluginId, entries] of Object.entries(projectRegistry.plugins)) {
|
|
836
|
+
if (!Array.isArray(entries) || entries.length === 0) continue;
|
|
837
|
+
const atIndex = pluginId.lastIndexOf("@");
|
|
838
|
+
if (atIndex === -1) {
|
|
839
|
+
warnings.push(`Invalid plugin ID format (missing @marketplace): ${pluginId}`);
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
const pluginName = pluginId.slice(0, atIndex);
|
|
843
|
+
const marketplace = pluginId.slice(atIndex + 1);
|
|
844
|
+
for (const entry of entries) {
|
|
845
|
+
if (!entry.installPath || typeof entry.installPath !== "string") {
|
|
846
|
+
warnings.push(`Plugin ${pluginId} entry has no installPath`);
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (entry.enabled === false) continue;
|
|
850
|
+
projectRoots.push({
|
|
851
|
+
id: pluginId,
|
|
852
|
+
marketplace,
|
|
853
|
+
plugin: pluginName,
|
|
854
|
+
version: entry.version || "unknown",
|
|
855
|
+
path: entry.installPath,
|
|
856
|
+
scope: "project",
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
warnings.push(`Failed to parse project plugin registry: ${resolvedProjectPath}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Project entries shadow user entries for the same plugin ID.
|
|
867
|
+
if (projectRoots.length > 0) {
|
|
868
|
+
const projectIds = new Set(projectRoots.map(r => r.id));
|
|
869
|
+
const deduped = roots.filter(r => !projectIds.has(r.id));
|
|
870
|
+
roots.length = 0;
|
|
871
|
+
roots.push(...projectRoots, ...deduped);
|
|
872
|
+
}
|
|
873
|
+
|
|
749
874
|
// Merge --plugin-dir roots (highest precedence) on every fresh load
|
|
750
875
|
if (injectedPluginDirRoots.length > 0) {
|
|
751
876
|
const injectedIds = new Set(injectedPluginDirRoots.map(r => r.id));
|
|
@@ -755,7 +880,7 @@ export async function listClaudePluginRoots(home: string): Promise<{ roots: Clau
|
|
|
755
880
|
}
|
|
756
881
|
|
|
757
882
|
const result = { roots, warnings };
|
|
758
|
-
pluginRootsCache.set(
|
|
883
|
+
pluginRootsCache.set(cacheKey, result);
|
|
759
884
|
return result;
|
|
760
885
|
}
|
|
761
886
|
|
|
@@ -767,7 +892,7 @@ export function clearClaudePluginRootsCache(): void {
|
|
|
767
892
|
preloadedPluginRoots = [...injectedPluginDirRoots];
|
|
768
893
|
// Re-warm preloaded roots asynchronously so sync LSP config reads stay valid
|
|
769
894
|
if (lastPreloadHome) {
|
|
770
|
-
void preloadPluginRoots(lastPreloadHome);
|
|
895
|
+
void preloadPluginRoots(lastPreloadHome, getProjectDir());
|
|
771
896
|
}
|
|
772
897
|
}
|
|
773
898
|
|
|
@@ -784,9 +909,9 @@ let lastPreloadHome: string | undefined;
|
|
|
784
909
|
* Call during session initialization, after dir resolution completes
|
|
785
910
|
* but before any LSP config is read.
|
|
786
911
|
*/
|
|
787
|
-
export async function preloadPluginRoots(home: string): Promise<void> {
|
|
912
|
+
export async function preloadPluginRoots(home: string, cwd?: string): Promise<void> {
|
|
788
913
|
lastPreloadHome = home;
|
|
789
|
-
const { roots } = await listClaudePluginRoots(home);
|
|
914
|
+
const { roots } = await listClaudePluginRoots(home, cwd);
|
|
790
915
|
preloadedPluginRoots = roots;
|
|
791
916
|
}
|
|
792
917
|
|
|
@@ -805,10 +930,7 @@ export function getPreloadedPluginRoots(): readonly ClaudePluginRoot[] {
|
|
|
805
930
|
* These are prepended to the cache with highest precedence (before OMP/Claude entries).
|
|
806
931
|
* Must be called before any listClaudePluginRoots() access.
|
|
807
932
|
*/
|
|
808
|
-
export async function injectPluginDirRoots(home: string, dirs: string[]): Promise<void> {
|
|
809
|
-
// Ensure the base cache is populated first
|
|
810
|
-
const { roots, warnings } = await listClaudePluginRoots(home);
|
|
811
|
-
|
|
933
|
+
export async function injectPluginDirRoots(home: string, dirs: string[], cwd?: string): Promise<void> {
|
|
812
934
|
const injected: ClaudePluginRoot[] = [];
|
|
813
935
|
for (const dir of dirs) {
|
|
814
936
|
const resolved = path.resolve(dir);
|
|
@@ -828,15 +950,13 @@ export async function injectPluginDirRoots(home: string, dirs: string[]): Promis
|
|
|
828
950
|
injected.push(buildPluginDirRoot(resolved, pluginName));
|
|
829
951
|
}
|
|
830
952
|
|
|
831
|
-
//
|
|
832
|
-
// removing any existing entries with the same plugin ID.
|
|
953
|
+
// Set injected roots BEFORE populating cache so listClaudePluginRoots merges them.
|
|
833
954
|
injectedPluginDirRoots = injected;
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
preloadedPluginRoots = merged;
|
|
955
|
+
lastPreloadHome = home; // ensure cache-clear re-warm fires even when injectPluginDirRoots was the startup path
|
|
956
|
+
// Clear any stale cache entries (populated before injected roots were set).
|
|
957
|
+
pluginRootsCache.clear();
|
|
958
|
+
// Rebuild — cache miss triggers fresh load that includes both user+project registries
|
|
959
|
+
// and prepends injectedPluginDirRoots at highest precedence.
|
|
960
|
+
const { roots } = await listClaudePluginRoots(home, cwd);
|
|
961
|
+
preloadedPluginRoots = roots;
|
|
842
962
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { renderPromptTemplate } from "../../../../config/prompt-templates";
|
|
2
|
+
import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
|
|
3
|
+
import type { HookCommandContext } from "../../../../extensibility/hooks/types";
|
|
4
|
+
import ciGreenRequestTemplate from "../../../../prompts/ci-green-request.md" with { type: "text" };
|
|
5
|
+
|
|
6
|
+
async function getHeadTag(api: CustomCommandAPI): Promise<string | undefined> {
|
|
7
|
+
const result = await api.exec("git", [
|
|
8
|
+
"for-each-ref",
|
|
9
|
+
"--points-at",
|
|
10
|
+
"HEAD",
|
|
11
|
+
"--sort=-version:refname",
|
|
12
|
+
"--format=%(refname:strip=2)",
|
|
13
|
+
"refs/tags",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
if (result.code !== 0 || result.killed) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tag = result.stdout
|
|
21
|
+
.split("\n")
|
|
22
|
+
.map(line => line.trim())
|
|
23
|
+
.find(Boolean);
|
|
24
|
+
return tag || undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class GreenCommand implements CustomCommand {
|
|
28
|
+
name = "green";
|
|
29
|
+
description = "Generate a prompt to iterate on CI failures until the branch is green";
|
|
30
|
+
|
|
31
|
+
constructor(private api: CustomCommandAPI) {}
|
|
32
|
+
|
|
33
|
+
async execute(_args: string[], _ctx: HookCommandContext): Promise<string> {
|
|
34
|
+
const headTag = await getHeadTag(this.api);
|
|
35
|
+
return renderPromptTemplate(ciGreenRequestTemplate, { headTag });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -11,6 +11,7 @@ import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils
|
|
|
11
11
|
import * as typebox from "@sinclair/typebox";
|
|
12
12
|
import { getConfigDirs } from "../../config";
|
|
13
13
|
import { execCommand } from "../../exec/exec";
|
|
14
|
+
import { GreenCommand } from "./bundled/ci-green";
|
|
14
15
|
import { ReviewCommand } from "./bundled/review";
|
|
15
16
|
import type {
|
|
16
17
|
CustomCommand,
|
|
@@ -148,6 +149,12 @@ function loadBundledCommands(sharedApi: CustomCommandAPI): LoadedCustomCommand[]
|
|
|
148
149
|
const bundled: LoadedCustomCommand[] = [];
|
|
149
150
|
|
|
150
151
|
// Add bundled commands here
|
|
152
|
+
bundled.push({
|
|
153
|
+
path: "bundled:green",
|
|
154
|
+
resolvedPath: "bundled:green",
|
|
155
|
+
command: new GreenCommand(sharedApi),
|
|
156
|
+
source: "bundled",
|
|
157
|
+
});
|
|
151
158
|
bundled.push({
|
|
152
159
|
path: "bundled:review",
|
|
153
160
|
resolvedPath: "bundled:review",
|
|
@@ -125,48 +125,66 @@ export function parseMarketplaceCatalog(content: string, filePath: string): Mark
|
|
|
125
125
|
assertField(Array.isArray(obj.plugins), "plugins", filePath);
|
|
126
126
|
|
|
127
127
|
const plugins = obj.plugins as unknown[];
|
|
128
|
+
const validPlugins: unknown[] = [];
|
|
128
129
|
for (let i = 0; i < plugins.length; i++) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
p.source
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
130
|
+
try {
|
|
131
|
+
const entry = plugins[i];
|
|
132
|
+
assertField(typeof entry === "object" && entry !== null && !Array.isArray(entry), `plugins[${i}]`, filePath);
|
|
133
|
+
const p = entry as Record<string, unknown>;
|
|
134
|
+
assertField(typeof p.name === "string" && isValidNameSegment(p.name), `plugins[${i}].name`, filePath);
|
|
135
|
+
// source can be a string path or a typed object (github/url/git-subdir/npm)
|
|
136
|
+
// all typed objects carry a "source" discriminant string field
|
|
137
|
+
assertField(
|
|
138
|
+
typeof p.source === "string" ||
|
|
139
|
+
(typeof p.source === "object" &&
|
|
140
|
+
p.source !== null &&
|
|
141
|
+
!Array.isArray(p.source) &&
|
|
142
|
+
typeof (p.source as Record<string, unknown>).source === "string"),
|
|
143
|
+
`plugins[${i}].source`,
|
|
144
|
+
filePath,
|
|
145
|
+
);
|
|
146
|
+
// String sources must be relative paths starting with "./"
|
|
147
|
+
if (typeof p.source === "string") {
|
|
148
|
+
assertField((p.source as string).startsWith("./"), `plugins[${i}].source (must start with "./")`, filePath);
|
|
149
|
+
}
|
|
150
|
+
// Validate required fields for typed source variants
|
|
151
|
+
if (typeof p.source === "object" && p.source !== null) {
|
|
152
|
+
const src = p.source as Record<string, unknown>;
|
|
153
|
+
const variant = src.source as string;
|
|
154
|
+
if (variant === "github") {
|
|
155
|
+
assertField(typeof src.repo === "string" && src.repo.length > 0, `plugins[${i}].source.repo`, filePath);
|
|
156
|
+
} else if (variant === "url" || variant === "git-subdir") {
|
|
157
|
+
assertField(typeof src.url === "string" && src.url.length > 0, `plugins[${i}].source.url`, filePath);
|
|
158
|
+
if (variant === "git-subdir") {
|
|
159
|
+
assertField(
|
|
160
|
+
typeof src.path === "string" && src.path.length > 0,
|
|
161
|
+
`plugins[${i}].source.path`,
|
|
162
|
+
filePath,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
} else if (variant === "npm") {
|
|
166
|
+
assertField(
|
|
167
|
+
typeof src.package === "string" && src.package.length > 0,
|
|
168
|
+
`plugins[${i}].source.package`,
|
|
169
|
+
filePath,
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
assertField(false, `plugins[${i}].source.source (unknown variant: "${variant}")`, filePath);
|
|
158
173
|
}
|
|
159
|
-
} else if (variant === "npm") {
|
|
160
|
-
assertField(
|
|
161
|
-
typeof src.package === "string" && src.package.length > 0,
|
|
162
|
-
`plugins[${i}].source.package`,
|
|
163
|
-
filePath,
|
|
164
|
-
);
|
|
165
|
-
} else {
|
|
166
|
-
assertField(false, `plugins[${i}].source.source (unknown variant: "${variant}")`, filePath);
|
|
167
174
|
}
|
|
175
|
+
validPlugins.push(entry);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
// Warn and skip invalid plugin entries instead of failing the entire catalog.
|
|
178
|
+
// This lets the rest of the marketplace load even if one entry has a bad name/source.
|
|
179
|
+
const name =
|
|
180
|
+
typeof plugins[i] === "object" && plugins[i] !== null
|
|
181
|
+
? ((plugins[i] as Record<string, unknown>).name ?? `[${i}]`)
|
|
182
|
+
: `[${i}]`;
|
|
183
|
+
logger.warn(`Skipping invalid plugin ${name}: ${(err as Error).message}`);
|
|
168
184
|
}
|
|
169
185
|
}
|
|
186
|
+
// Replace the plugins array with only valid entries
|
|
187
|
+
obj.plugins = validPlugins;
|
|
170
188
|
|
|
171
189
|
// Extra fields are preserved — cast through unknown for type safety
|
|
172
190
|
return obj as unknown as MarketplaceCatalog;
|