@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.
Files changed (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. 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
+ }