How pi-web Manages Plugins Using pi's SettingsManager
pi-web orchestrates plugin discovery, installation, and state management by combining pi's SettingsManager with DefaultPackageManager to persist configuration changes and resolve package metadata.
The agegr/pi-web repository implements a Next.js-based API layer that controls extensions, skills, prompts, and themes through pi's configuration system. It delegates persistence to SettingsManager (provided by @earendil-works/pi-coding-agent) while delegating package operations to DefaultPackageManager, creating a clean separation between configuration state and package resolution logic.
Core Architecture: SettingsManager and DefaultPackageManager
The plugin system relies on two coordinated components that handle distinct responsibilities.
SettingsManager as the Configuration Authority
The SettingsManager serves as the single source of truth for plugin configuration. It reads global and project-level ~/.pi/agent/settings.json files and maintains a mutable in-memory copy that tracks which packages are installed, enabled, or disabled.
In app/api/plugins/route.ts, the manager initializes with the current working directory and agent directory:
const settingsManager = SettingsManager.create(cwd, getAgentDir());
This instance persists throughout the request lifecycle and ensures that all modifications remain atomic until explicitly flushed to disk.
DefaultPackageManager for Package Resolution
The DefaultPackageManager handles the heavy lifting of locating, installing, and updating npm, git, or URL-based packages. It receives the SettingsManager instance so it can immediately persist structural changes after mutating operations.
The instantiation pattern appears at lines 20-24 of app/api/plugins/route.ts:
const packageManager = new DefaultPackageManager({
cwd,
agentDir: getAgentDir(),
settingsManager,
});
By injecting the settings manager into the package manager, the system guarantees that every install, remove, or update operation automatically updates the configuration files before the API returns a response.
Reading Plugin State
The GET /api/plugins endpoint exposes the current plugin configuration through the readPlugins function (lines 20-64 in app/api/plugins/route.ts).
Resource Collection and Status Detection
The function performs three critical operations:
- Resolves packages against the configured sources to verify availability and metadata
- Collects resources (
collectResources) to enumerate extensions, skills, prompts, and themes provided by each package - Detects disabled state (
isDisabledPackage) by checking if every resource list is empty—a signal that the package entry was rewritten to disable it without uninstalling
The response includes per-package metadata such as source identifier, scope (global or project), installed version, resource counts, and diagnostic information.
Modifying Plugin State via POST Actions
The POST /api/plugins endpoint handles mutations through a structured action protocol. The request body specifies an action (install, remove, update, disable, enable), the package source, and the scope.
Installation and Removal
For install and remove operations, the route delegates to DefaultPackageManager.installAndPersist and removeAndPersist respectively. These methods handle npm installation or directory removal and immediately update the settings to reflect the new package list.
Updating Packages
The update action triggers packageManager.update(source), which checks for newer versions of the specified package and applies updates while preserving the existing configuration scope.
Enabling and Disabling Plugins
Toggle operations use the helper setPackageDisabled to manipulate the settings without touching the filesystem:
- Disabling rewrites the package entry to contain empty resource arrays
- Enabling restores the original resource list from package metadata
After modifying the in-memory settings, the code calls settingsManager.flush() to write changes to ~/.pi/agent/settings.json before returning the updated state to the client.
Filesystem Security Controls
The API enforces strict filesystem access through utilities in lib/file-access.ts. Before executing any operation, the route validates the cwd parameter against getAllowedFileRoots and isExistingFilePathAllowed to prevent path traversal attacks. This ensures plugin management only occurs within authorized directories.
Client Integration Examples
Fetch the current plugin list for a project:
const cwd = "/home/user/project";
const resp = await fetch(`/api/plugins?cwd=${encodeURIComponent(cwd)}`);
const { packages, totals, diagnostics } = await resp.json();
console.log("Loaded packages:", packages);
console.log("Total resources:", totals);
Install a package to project scope:
await fetch("/api/plugins", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "install",
source: "npm:@pi/awesome-skill",
scope: "project",
cwd: "/home/user/project"
})
});
Disable a plugin without removing it:
await fetch("/api/plugins", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "disable",
source: "npm:@pi/awesome-skill",
scope: "project",
cwd: "/home/user/project"
})
});
Enable a previously disabled plugin:
await fetch("/api/plugins", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "enable",
source: "npm:@pi/awesome-skill",
scope: "project",
cwd: "/home/user/project"
})
});
Update all configured plugins:
await fetch("/api/plugins", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "update",
cwd: "/home/user/project"
})
});
Summary
- SettingsManager acts as the configuration authority, maintaining a mutable copy of
settings.jsonand persisting changes viaflush(). - DefaultPackageManager handles package resolution and installation, receiving the settings manager to ensure atomic configuration updates.
- The
readPluginsfunction inapp/api/plugins/route.tsdetects disabled packages by checking for empty resource arrays without querying the filesystem. - Plugin state changes (install, remove, update, enable, disable) are processed through
POST /api/pluginsand persist immediately to disk. - Filesystem access is guarded by
getAllowedFileRootsandisExistingFilePathAllowedfromlib/file-access.tsto prevent unauthorized directory traversal.
Frequently Asked Questions
How does pi-web distinguish between enabled and disabled plugins?
pi-web checks the resource arrays within the package configuration. When isDisabledPackage returns true because all resource lists (extensions, skills, prompts, themes) are empty, the UI treats the package as disabled even though the entry remains in settings.json. This allows toggling without reinstalling npm packages.
Where does pi-web store plugin configuration?
Configuration persists to ~/.pi/agent/settings.json, managed by SettingsManager. The system supports both global and project-scoped entries, with the scope parameter determining which configuration section receives updates.
What package sources does pi-web support?
The DefaultPackageManager accepts npm registry identifiers (e.g., npm:@pi/awesome-skill), git URLs, and direct HTTP URLs. The package manager resolves these sources during installation and update operations.
How does the API prevent unauthorized filesystem access?
Every request validates the cwd parameter against allowed roots using isExistingFilePathAllowed from lib/file-access.ts. This ensures plugin operations can only occur within directories explicitly permitted by the server configuration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →