@oh-my-pi/pi-coding-agent 1.337.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 +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { DoctorCheck } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export async function runDoctorChecks(): Promise<DoctorCheck[]> {
|
|
4
|
+
const checks: DoctorCheck[] = [];
|
|
5
|
+
|
|
6
|
+
// Check external tools
|
|
7
|
+
const tools = [
|
|
8
|
+
{ name: "fd", description: "File finder" },
|
|
9
|
+
{ name: "rg", description: "Ripgrep" },
|
|
10
|
+
{ name: "sd", description: "Find-replace" },
|
|
11
|
+
{ name: "sg", description: "AST-grep" },
|
|
12
|
+
{ name: "git", description: "Version control" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
for (const tool of tools) {
|
|
16
|
+
const path = Bun.which(tool.name);
|
|
17
|
+
checks.push({
|
|
18
|
+
name: tool.name,
|
|
19
|
+
status: path ? "ok" : "warning",
|
|
20
|
+
message: path ? `Found at ${path}` : `${tool.description} not found - some features may be limited`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check API keys
|
|
25
|
+
const apiKeys = [
|
|
26
|
+
{ name: "ANTHROPIC_API_KEY", description: "Anthropic API" },
|
|
27
|
+
{ name: "OPENAI_API_KEY", description: "OpenAI API" },
|
|
28
|
+
{ name: "PERPLEXITY_API_KEY", description: "Perplexity search" },
|
|
29
|
+
{ name: "EXA_API_KEY", description: "Exa search" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const key of apiKeys) {
|
|
33
|
+
const hasKey = !!process.env[key.name];
|
|
34
|
+
checks.push({
|
|
35
|
+
name: key.name,
|
|
36
|
+
status: hasKey ? "ok" : "warning",
|
|
37
|
+
message: hasKey ? "Configured" : `Not set - ${key.description} unavailable`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return checks;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function formatDoctorResults(checks: DoctorCheck[]): string {
|
|
45
|
+
const lines: string[] = ["System Health Check", "=".repeat(40), ""];
|
|
46
|
+
|
|
47
|
+
for (const check of checks) {
|
|
48
|
+
const icon = check.status === "ok" ? "✓" : check.status === "warning" ? "!" : "✗";
|
|
49
|
+
lines.push(`${icon} ${check.name}: ${check.message}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const errors = checks.filter((c) => c.status === "error").length;
|
|
53
|
+
const warnings = checks.filter((c) => c.status === "warning").length;
|
|
54
|
+
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push(`Summary: ${checks.length - errors - warnings} ok, ${warnings} warnings, ${errors} errors`);
|
|
57
|
+
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Plugin system exports
|
|
2
|
+
export { formatDoctorResults, runDoctorChecks } from "./doctor.js";
|
|
3
|
+
export {
|
|
4
|
+
getAllPluginCommandPaths,
|
|
5
|
+
getAllPluginHookPaths,
|
|
6
|
+
getAllPluginToolPaths,
|
|
7
|
+
getEnabledPlugins,
|
|
8
|
+
getPluginSettings,
|
|
9
|
+
resolvePluginCommandPaths,
|
|
10
|
+
resolvePluginHookPaths,
|
|
11
|
+
resolvePluginToolPaths,
|
|
12
|
+
} from "./loader.js";
|
|
13
|
+
export { PluginManager, parseSettingValue, validateSetting } from "./manager.js";
|
|
14
|
+
export { extractPackageName, formatPluginSpec, parsePluginSpec } from "./parser.js";
|
|
15
|
+
export {
|
|
16
|
+
getPluginsDir,
|
|
17
|
+
getPluginsLockfile,
|
|
18
|
+
getPluginsNodeModules,
|
|
19
|
+
getPluginsPackageJson,
|
|
20
|
+
getProjectPluginOverrides,
|
|
21
|
+
} from "./paths.js";
|
|
22
|
+
export type {
|
|
23
|
+
BooleanSetting,
|
|
24
|
+
DoctorCheck,
|
|
25
|
+
DoctorOptions,
|
|
26
|
+
EnumSetting,
|
|
27
|
+
InstalledPlugin,
|
|
28
|
+
InstallOptions,
|
|
29
|
+
NumberSetting,
|
|
30
|
+
PluginFeature,
|
|
31
|
+
PluginManifest,
|
|
32
|
+
PluginRuntimeConfig,
|
|
33
|
+
PluginRuntimeState,
|
|
34
|
+
PluginSettingSchema,
|
|
35
|
+
PluginSettingType,
|
|
36
|
+
ProjectPluginOverrides,
|
|
37
|
+
StringSetting,
|
|
38
|
+
} from "./types.js";
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { mkdir } from "fs/promises";
|
|
2
|
+
import { join, resolve } from "path";
|
|
3
|
+
import { getAgentDir } from "../../config.js";
|
|
4
|
+
import type { InstalledPlugin } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const PLUGINS_DIR = join(getAgentDir(), "plugins");
|
|
7
|
+
|
|
8
|
+
// Valid npm package name pattern (scoped and unscoped)
|
|
9
|
+
const VALID_PACKAGE_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*(@[a-z0-9-._^~>=<]+)?$/i;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate package name to prevent command injection
|
|
13
|
+
*/
|
|
14
|
+
function validatePackageName(name: string): void {
|
|
15
|
+
if (!VALID_PACKAGE_NAME.test(name)) {
|
|
16
|
+
throw new Error(`Invalid package name: ${name}`);
|
|
17
|
+
}
|
|
18
|
+
// Extra safety: no shell metacharacters
|
|
19
|
+
if (/[;&|`$(){}[\]<>\\]/.test(name)) {
|
|
20
|
+
throw new Error(`Invalid characters in package name: ${name}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensure the plugins directory exists
|
|
26
|
+
*/
|
|
27
|
+
async function ensurePluginsDir(): Promise<void> {
|
|
28
|
+
await mkdir(PLUGINS_DIR, { recursive: true });
|
|
29
|
+
await mkdir(join(PLUGINS_DIR, "node_modules"), { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function installPlugin(packageName: string): Promise<InstalledPlugin> {
|
|
33
|
+
// Validate package name to prevent command injection
|
|
34
|
+
validatePackageName(packageName);
|
|
35
|
+
|
|
36
|
+
// Ensure plugins directory exists
|
|
37
|
+
await ensurePluginsDir();
|
|
38
|
+
|
|
39
|
+
// Initialize package.json if it doesn't exist
|
|
40
|
+
const pkgJsonPath = join(PLUGINS_DIR, "package.json");
|
|
41
|
+
if (!(await Bun.file(pkgJsonPath).exists())) {
|
|
42
|
+
await Bun.write(pkgJsonPath, JSON.stringify({ name: "pi-plugins", private: true, dependencies: {} }, null, 2));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Run npm install in plugins directory
|
|
46
|
+
const proc = Bun.spawn(["npm", "install", packageName], {
|
|
47
|
+
cwd: PLUGINS_DIR,
|
|
48
|
+
stdin: "ignore",
|
|
49
|
+
stdout: "pipe",
|
|
50
|
+
stderr: "pipe",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const exitCode = await proc.exited;
|
|
54
|
+
if (exitCode !== 0) {
|
|
55
|
+
const stderr = await new Response(proc.stderr).text();
|
|
56
|
+
throw new Error(`Failed to install ${packageName}: ${stderr}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extract the actual package name (without version specifier) for path lookup
|
|
60
|
+
const actualName = packageName.replace(/@[^/]+$/, "").replace(/^(@[^/]+\/[^@]+).*$/, "$1");
|
|
61
|
+
|
|
62
|
+
// Read the installed package's package.json
|
|
63
|
+
const pkgPath = join(PLUGINS_DIR, "node_modules", actualName, "package.json");
|
|
64
|
+
const pkgFile = Bun.file(pkgPath);
|
|
65
|
+
if (!(await pkgFile.exists())) {
|
|
66
|
+
throw new Error(`Package installed but package.json not found at ${pkgPath}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pkg = await pkgFile.json();
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
name: pkg.name,
|
|
73
|
+
version: pkg.version,
|
|
74
|
+
path: join(PLUGINS_DIR, "node_modules", actualName),
|
|
75
|
+
manifest: pkg.omp || pkg.pi || { version: pkg.version },
|
|
76
|
+
enabledFeatures: null,
|
|
77
|
+
enabled: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function uninstallPlugin(name: string): Promise<void> {
|
|
82
|
+
// Validate package name
|
|
83
|
+
validatePackageName(name);
|
|
84
|
+
|
|
85
|
+
await ensurePluginsDir();
|
|
86
|
+
|
|
87
|
+
const proc = Bun.spawn(["npm", "uninstall", name], {
|
|
88
|
+
cwd: PLUGINS_DIR,
|
|
89
|
+
stdin: "ignore",
|
|
90
|
+
stdout: "pipe",
|
|
91
|
+
stderr: "pipe",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const exitCode = await proc.exited;
|
|
95
|
+
if (exitCode !== 0) {
|
|
96
|
+
throw new Error(`Failed to uninstall ${name}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function listPlugins(): Promise<InstalledPlugin[]> {
|
|
101
|
+
const pkgJsonPath = join(PLUGINS_DIR, "package.json");
|
|
102
|
+
if (!(await Bun.file(pkgJsonPath).exists())) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const pkg = await Bun.file(pkgJsonPath).json();
|
|
107
|
+
const deps = pkg.dependencies || {};
|
|
108
|
+
|
|
109
|
+
const plugins: InstalledPlugin[] = [];
|
|
110
|
+
for (const [name, _version] of Object.entries(deps)) {
|
|
111
|
+
const pluginPkgPath = join(PLUGINS_DIR, "node_modules", name, "package.json");
|
|
112
|
+
if (await Bun.file(pluginPkgPath).exists()) {
|
|
113
|
+
const pluginPkg = await Bun.file(pluginPkgPath).json();
|
|
114
|
+
plugins.push({
|
|
115
|
+
name,
|
|
116
|
+
version: pluginPkg.version,
|
|
117
|
+
path: join(PLUGINS_DIR, "node_modules", name),
|
|
118
|
+
manifest: pluginPkg.omp || pluginPkg.pi || { version: pluginPkg.version },
|
|
119
|
+
enabledFeatures: null,
|
|
120
|
+
enabled: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return plugins;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function linkPlugin(localPath: string): Promise<void> {
|
|
129
|
+
const cwd = process.cwd();
|
|
130
|
+
const absolutePath = resolve(cwd, localPath);
|
|
131
|
+
|
|
132
|
+
// Validate that resolved path is within cwd to prevent path traversal
|
|
133
|
+
const normalizedCwd = resolve(cwd);
|
|
134
|
+
const normalizedPath = resolve(absolutePath);
|
|
135
|
+
if (!normalizedPath.startsWith(`${normalizedCwd}/`) && normalizedPath !== normalizedCwd) {
|
|
136
|
+
throw new Error(`Invalid path: ${localPath} resolves outside working directory`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Validate package.json exists
|
|
140
|
+
const pkgFile = Bun.file(join(absolutePath, "package.json"));
|
|
141
|
+
if (!(await pkgFile.exists())) {
|
|
142
|
+
throw new Error(`package.json not found at ${absolutePath}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let pkg: { name?: string };
|
|
146
|
+
try {
|
|
147
|
+
pkg = await pkgFile.json();
|
|
148
|
+
} catch (err) {
|
|
149
|
+
throw new Error(`Invalid package.json at ${absolutePath}: ${err}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!pkg.name || typeof pkg.name !== "string") {
|
|
153
|
+
throw new Error("package.json must have a valid name field");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Validate package name to prevent path traversal via pkg.name
|
|
157
|
+
if (pkg.name.includes("..") || pkg.name.includes("/") || pkg.name.includes("\\")) {
|
|
158
|
+
// Exception: scoped packages have one slash
|
|
159
|
+
if (!pkg.name.startsWith("@") || (pkg.name.match(/\//g) || []).length !== 1) {
|
|
160
|
+
throw new Error(`Invalid package name in package.json: ${pkg.name}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await ensurePluginsDir();
|
|
165
|
+
|
|
166
|
+
// Create symlink in plugins/node_modules
|
|
167
|
+
const linkPath = join(PLUGINS_DIR, "node_modules", pkg.name);
|
|
168
|
+
|
|
169
|
+
// For scoped packages, ensure the scope directory exists
|
|
170
|
+
if (pkg.name.startsWith("@")) {
|
|
171
|
+
const scopeDir = join(PLUGINS_DIR, "node_modules", pkg.name.split("/")[0]);
|
|
172
|
+
await mkdir(scopeDir, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Remove existing if present
|
|
176
|
+
try {
|
|
177
|
+
const { unlinkSync, lstatSync } = await import("fs");
|
|
178
|
+
const stat = lstatSync(linkPath);
|
|
179
|
+
if (stat.isSymbolicLink() || stat.isDirectory()) {
|
|
180
|
+
unlinkSync(linkPath);
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Doesn't exist, that's fine
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Create symlink using fs instead of shell command
|
|
187
|
+
const { symlinkSync } = await import("fs");
|
|
188
|
+
symlinkSync(absolutePath, linkPath);
|
|
189
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin loader - discovers and loads tools/hooks from installed plugins.
|
|
3
|
+
*
|
|
4
|
+
* Reads enabled plugins from the runtime config and loads their tools/hooks
|
|
5
|
+
* based on manifest entries and enabled features.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import {
|
|
11
|
+
getPluginsLockfile,
|
|
12
|
+
getPluginsNodeModules,
|
|
13
|
+
getPluginsPackageJson,
|
|
14
|
+
getProjectPluginOverrides,
|
|
15
|
+
} from "./paths.js";
|
|
16
|
+
import type { InstalledPlugin, PluginManifest, PluginRuntimeConfig, ProjectPluginOverrides } from "./types.js";
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Runtime Config Loading
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load plugin runtime config from lock file.
|
|
24
|
+
*/
|
|
25
|
+
function loadRuntimeConfig(): PluginRuntimeConfig {
|
|
26
|
+
const lockPath = getPluginsLockfile();
|
|
27
|
+
if (!existsSync(lockPath)) {
|
|
28
|
+
return { plugins: {}, settings: {} };
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
32
|
+
} catch {
|
|
33
|
+
return { plugins: {}, settings: {} };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load project-local plugin overrides.
|
|
39
|
+
*/
|
|
40
|
+
function loadProjectOverrides(cwd: string): ProjectPluginOverrides {
|
|
41
|
+
const overridesPath = getProjectPluginOverrides(cwd);
|
|
42
|
+
if (!existsSync(overridesPath)) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(readFileSync(overridesPath, "utf-8"));
|
|
47
|
+
} catch {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Plugin Discovery
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get list of enabled plugins with their resolved configurations.
|
|
58
|
+
* Respects both global runtime config and project overrides.
|
|
59
|
+
*/
|
|
60
|
+
export function getEnabledPlugins(cwd: string): InstalledPlugin[] {
|
|
61
|
+
const pkgJsonPath = getPluginsPackageJson();
|
|
62
|
+
if (!existsSync(pkgJsonPath)) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nodeModulesPath = getPluginsNodeModules();
|
|
67
|
+
if (!existsSync(nodeModulesPath)) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
72
|
+
const deps = pkg.dependencies || {};
|
|
73
|
+
const runtimeConfig = loadRuntimeConfig();
|
|
74
|
+
const projectOverrides = loadProjectOverrides(cwd);
|
|
75
|
+
const plugins: InstalledPlugin[] = [];
|
|
76
|
+
|
|
77
|
+
for (const [name] of Object.entries(deps)) {
|
|
78
|
+
const pluginPkgPath = join(nodeModulesPath, name, "package.json");
|
|
79
|
+
if (!existsSync(pluginPkgPath)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pluginPkg = JSON.parse(readFileSync(pluginPkgPath, "utf-8"));
|
|
84
|
+
const manifest: PluginManifest | undefined = pluginPkg.omp || pluginPkg.pi;
|
|
85
|
+
|
|
86
|
+
if (!manifest) {
|
|
87
|
+
// Not a pi plugin, skip
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
manifest.version = pluginPkg.version;
|
|
92
|
+
|
|
93
|
+
const runtimeState = runtimeConfig.plugins[name];
|
|
94
|
+
|
|
95
|
+
// Check if disabled globally
|
|
96
|
+
if (runtimeState && !runtimeState.enabled) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if disabled in project
|
|
101
|
+
if (projectOverrides.disabled?.includes(name)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Resolve enabled features (project overrides take precedence)
|
|
106
|
+
const enabledFeatures = projectOverrides.features?.[name] ?? runtimeState?.enabledFeatures ?? null;
|
|
107
|
+
|
|
108
|
+
plugins.push({
|
|
109
|
+
name,
|
|
110
|
+
version: pluginPkg.version,
|
|
111
|
+
path: join(nodeModulesPath, name),
|
|
112
|
+
manifest,
|
|
113
|
+
enabledFeatures,
|
|
114
|
+
enabled: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return plugins;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// Path Resolution
|
|
123
|
+
// =============================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve tool entry points for a plugin based on manifest and enabled features.
|
|
127
|
+
* Returns absolute paths to tool modules.
|
|
128
|
+
*/
|
|
129
|
+
export function resolvePluginToolPaths(plugin: InstalledPlugin): string[] {
|
|
130
|
+
const paths: string[] = [];
|
|
131
|
+
const manifest = plugin.manifest;
|
|
132
|
+
|
|
133
|
+
// Base tools entry (always included if exists)
|
|
134
|
+
if (manifest.tools) {
|
|
135
|
+
const toolPath = join(plugin.path, manifest.tools);
|
|
136
|
+
if (existsSync(toolPath)) {
|
|
137
|
+
paths.push(toolPath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Feature-specific tools
|
|
142
|
+
if (manifest.features && plugin.enabledFeatures) {
|
|
143
|
+
const enabledSet = new Set(plugin.enabledFeatures);
|
|
144
|
+
|
|
145
|
+
for (const [featName, feat] of Object.entries(manifest.features)) {
|
|
146
|
+
if (!enabledSet.has(featName)) continue;
|
|
147
|
+
|
|
148
|
+
if (feat.tools) {
|
|
149
|
+
for (const toolEntry of feat.tools) {
|
|
150
|
+
const toolPath = join(plugin.path, toolEntry);
|
|
151
|
+
if (existsSync(toolPath)) {
|
|
152
|
+
paths.push(toolPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} else if (manifest.features && plugin.enabledFeatures === null) {
|
|
158
|
+
// null means use defaults - enable features with default: true
|
|
159
|
+
for (const [_featName, feat] of Object.entries(manifest.features)) {
|
|
160
|
+
if (!feat.default) continue;
|
|
161
|
+
|
|
162
|
+
if (feat.tools) {
|
|
163
|
+
for (const toolEntry of feat.tools) {
|
|
164
|
+
const toolPath = join(plugin.path, toolEntry);
|
|
165
|
+
if (existsSync(toolPath)) {
|
|
166
|
+
paths.push(toolPath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return paths;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Resolve hook entry points for a plugin based on manifest and enabled features.
|
|
178
|
+
* Returns absolute paths to hook modules.
|
|
179
|
+
*/
|
|
180
|
+
export function resolvePluginHookPaths(plugin: InstalledPlugin): string[] {
|
|
181
|
+
const paths: string[] = [];
|
|
182
|
+
const manifest = plugin.manifest;
|
|
183
|
+
|
|
184
|
+
// Base hooks entry (always included if exists)
|
|
185
|
+
if (manifest.hooks) {
|
|
186
|
+
const hookPath = join(plugin.path, manifest.hooks);
|
|
187
|
+
if (existsSync(hookPath)) {
|
|
188
|
+
paths.push(hookPath);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Feature-specific hooks
|
|
193
|
+
if (manifest.features && plugin.enabledFeatures) {
|
|
194
|
+
const enabledSet = new Set(plugin.enabledFeatures);
|
|
195
|
+
|
|
196
|
+
for (const [featName, feat] of Object.entries(manifest.features)) {
|
|
197
|
+
if (!enabledSet.has(featName)) continue;
|
|
198
|
+
|
|
199
|
+
if (feat.hooks) {
|
|
200
|
+
for (const hookEntry of feat.hooks) {
|
|
201
|
+
const hookPath = join(plugin.path, hookEntry);
|
|
202
|
+
if (existsSync(hookPath)) {
|
|
203
|
+
paths.push(hookPath);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else if (manifest.features && plugin.enabledFeatures === null) {
|
|
209
|
+
// null means use defaults - enable features with default: true
|
|
210
|
+
for (const [_featName, feat] of Object.entries(manifest.features)) {
|
|
211
|
+
if (!feat.default) continue;
|
|
212
|
+
|
|
213
|
+
if (feat.hooks) {
|
|
214
|
+
for (const hookEntry of feat.hooks) {
|
|
215
|
+
const hookPath = join(plugin.path, hookEntry);
|
|
216
|
+
if (existsSync(hookPath)) {
|
|
217
|
+
paths.push(hookPath);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return paths;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Resolve command file paths for a plugin based on manifest and enabled features.
|
|
229
|
+
* Returns absolute paths to command files (.md).
|
|
230
|
+
*/
|
|
231
|
+
export function resolvePluginCommandPaths(plugin: InstalledPlugin): string[] {
|
|
232
|
+
const paths: string[] = [];
|
|
233
|
+
const manifest = plugin.manifest;
|
|
234
|
+
|
|
235
|
+
// Base commands (always included if exists)
|
|
236
|
+
if (manifest.commands) {
|
|
237
|
+
for (const cmdEntry of manifest.commands) {
|
|
238
|
+
const cmdPath = join(plugin.path, cmdEntry);
|
|
239
|
+
if (existsSync(cmdPath)) {
|
|
240
|
+
paths.push(cmdPath);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Feature-specific commands
|
|
246
|
+
if (manifest.features && plugin.enabledFeatures) {
|
|
247
|
+
const enabledSet = new Set(plugin.enabledFeatures);
|
|
248
|
+
|
|
249
|
+
for (const [featName, feat] of Object.entries(manifest.features)) {
|
|
250
|
+
if (!enabledSet.has(featName)) continue;
|
|
251
|
+
|
|
252
|
+
if (feat.commands) {
|
|
253
|
+
for (const cmdEntry of feat.commands) {
|
|
254
|
+
const cmdPath = join(plugin.path, cmdEntry);
|
|
255
|
+
if (existsSync(cmdPath)) {
|
|
256
|
+
paths.push(cmdPath);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} else if (manifest.features && plugin.enabledFeatures === null) {
|
|
262
|
+
// null means use defaults - enable features with default: true
|
|
263
|
+
for (const [_featName, feat] of Object.entries(manifest.features)) {
|
|
264
|
+
if (!feat.default) continue;
|
|
265
|
+
|
|
266
|
+
if (feat.commands) {
|
|
267
|
+
for (const cmdEntry of feat.commands) {
|
|
268
|
+
const cmdPath = join(plugin.path, cmdEntry);
|
|
269
|
+
if (existsSync(cmdPath)) {
|
|
270
|
+
paths.push(cmdPath);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return paths;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// =============================================================================
|
|
281
|
+
// Aggregated Discovery
|
|
282
|
+
// =============================================================================
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get all tool paths from all enabled plugins.
|
|
286
|
+
*/
|
|
287
|
+
export function getAllPluginToolPaths(cwd: string): string[] {
|
|
288
|
+
const plugins = getEnabledPlugins(cwd);
|
|
289
|
+
const paths: string[] = [];
|
|
290
|
+
|
|
291
|
+
for (const plugin of plugins) {
|
|
292
|
+
paths.push(...resolvePluginToolPaths(plugin));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return paths;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get all hook paths from all enabled plugins.
|
|
300
|
+
*/
|
|
301
|
+
export function getAllPluginHookPaths(cwd: string): string[] {
|
|
302
|
+
const plugins = getEnabledPlugins(cwd);
|
|
303
|
+
const paths: string[] = [];
|
|
304
|
+
|
|
305
|
+
for (const plugin of plugins) {
|
|
306
|
+
paths.push(...resolvePluginHookPaths(plugin));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return paths;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get all command paths from all enabled plugins.
|
|
314
|
+
*/
|
|
315
|
+
export function getAllPluginCommandPaths(cwd: string): string[] {
|
|
316
|
+
const plugins = getEnabledPlugins(cwd);
|
|
317
|
+
const paths: string[] = [];
|
|
318
|
+
|
|
319
|
+
for (const plugin of plugins) {
|
|
320
|
+
paths.push(...resolvePluginCommandPaths(plugin));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return paths;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get plugin settings for use in tool/hook contexts.
|
|
328
|
+
* Merges global settings with project overrides.
|
|
329
|
+
*/
|
|
330
|
+
export function getPluginSettings(pluginName: string, cwd: string): Record<string, unknown> {
|
|
331
|
+
const runtimeConfig = loadRuntimeConfig();
|
|
332
|
+
const projectOverrides = loadProjectOverrides(cwd);
|
|
333
|
+
|
|
334
|
+
const global = runtimeConfig.settings[pluginName] || {};
|
|
335
|
+
const project = projectOverrides.settings?.[pluginName] || {};
|
|
336
|
+
|
|
337
|
+
return { ...global, ...project };
|
|
338
|
+
}
|