# How pi-web Manages Plugins Using pi's SettingsManager

> Discover how pi-web manages plugins with SettingsManager and DefaultPackageManager for seamless discovery installation and state management. Persist configurations and resolve package metadata.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: internals
- Published: 2026-07-26

---

**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`](https://github.com/agegr/pi-web/blob/main/app/api/plugins/route.ts), the manager initializes with the current working directory and agent directory:

```typescript
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`](https://github.com/agegr/pi-web/blob/main/app/api/plugins/route.ts):

```typescript
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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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.json`](https://github.com/agegr/pi-web/blob/main/settings.json) and persisting changes via `flush()`.
- **DefaultPackageManager** handles package resolution and installation, receiving the settings manager to ensure atomic configuration updates.
- The `readPlugins` function in [`app/api/plugins/route.ts`](https://github.com/agegr/pi-web/blob/main/app/api/plugins/route.ts) detects 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/plugins` and persist immediately to disk.
- Filesystem access is guarded by `getAllowedFileRoots` and `isExistingFilePathAllowed` from [`lib/file-access.ts`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts) to 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`](https://github.com/agegr/pi-web/blob/main/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`](https://github.com/agegr/pi-web/blob/main/lib/file-access.ts). This ensures plugin operations can only occur within directories explicitly permitted by the server configuration.