Implementation:Openclaw Openclaw InstallPluginFromNpmSpec
InstallPluginFromNpmSpec and InstallPluginFromPath
installPluginFromNpmSpec and installPluginFromPath are the two primary public entry points for installing OpenClaw plugins. The npm installer fetches a package from the npm registry via npm pack and delegates to the archive installer. The path installer auto-detects whether the source is a directory, an archive, or a single file and delegates to the appropriate sub-installer.
Principle:Openclaw_Openclaw_Plugin_Installation
Source Location
| Function | File | Lines |
|---|---|---|
installPluginFromNpmSpec |
src/plugins/install.ts |
453-505 |
installPluginFromPath |
src/plugins/install.ts |
507-554 |
Repository: github.com/openclaw/openclaw
installPluginFromNpmSpec
Signature
export async function installPluginFromNpmSpec(params: {
spec: string;
extensionsDir?: string;
timeoutMs?: number;
logger?: PluginInstallLogger;
mode?: "install" | "update";
dryRun?: boolean;
expectedPluginId?: string;
}): Promise<InstallPluginResult>
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
spec |
string |
(required) | npm package specifier (e.g., "@openclaw/voice-call", "my-plugin@1.2.3").
|
extensionsDir |
string? |
~/.openclaw/extensions |
Custom directory for plugin installation. |
timeoutMs |
number? |
120000 |
Timeout for the npm pack command (minimum 300000ms enforced internally).
|
logger |
PluginInstallLogger? |
no-op logger | Logger for progress and warning messages. |
mode |
"update" | "install" |
Whether to fail on existing install or replace it. |
dryRun |
boolean? |
false |
When true, validates without writing files.
|
expectedPluginId |
string? |
undefined |
If set, installation fails when the derived plugin ID does not match. |
Return Value
Returns Promise<InstallPluginResult>:
export type InstallPluginResult =
| {
ok: true;
pluginId: string;
targetDir: string;
manifestName?: string;
version?: string;
extensions: string[];
}
| { ok: false; error: string };
Behavior
- Validates that the spec is non-empty.
- Creates a temp directory and runs
npm pack <spec>withCOREPACK_ENABLE_DOWNLOAD_PROMPT=0. - Extracts the last line of stdout as the packed archive filename.
- Delegates to
installPluginFromArchive()with the resolved archive path.
Error Conditions
- Empty spec: returns
{ ok: false, error: "missing npm spec" }. npm packnon-zero exit: returns the stderr/stdout as the error.- No archive produced: returns
{ ok: false, error: "npm pack produced no archive" }.
installPluginFromPath
Signature
export async function installPluginFromPath(params: {
path: string;
extensionsDir?: string;
timeoutMs?: number;
logger?: PluginInstallLogger;
mode?: "install" | "update";
dryRun?: boolean;
expectedPluginId?: string;
}): Promise<InstallPluginResult>
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
path |
string |
(required) | Filesystem path to a directory, archive file, or single extension file. |
extensionsDir |
string? |
~/.openclaw/extensions |
Custom directory for plugin installation. |
timeoutMs |
number? |
120000 |
Timeout for dependency installation. |
logger |
PluginInstallLogger? |
no-op logger | Logger for progress messages. |
mode |
"update" | "install" |
Whether to fail on existing install or replace it. |
dryRun |
boolean? |
false |
When true, validates without writing files.
|
expectedPluginId |
string? |
undefined |
If set, installation fails when the derived plugin ID does not match. |
Behavior
- Resolves the path via
resolveUserPath()and checks existence. - If the path is a directory: delegates to
installPluginFromDir(). - If the path is a recognized archive (
.tar.gz,.tgz,.zip): delegates toinstallPluginFromArchive(). - Otherwise, treats it as a single file: delegates to
installPluginFromFile().
Error Conditions
- Path does not exist: returns
{ ok: false, error: "path not found: ..." }.
Internal Helpers
Both public functions depend on the shared internal pipeline in installPluginFromPackageDir() (lines 135-309), which handles:
| Step | Description |
|---|---|
| Manifest validation | Reads package.json, verifies openclaw.extensions is a non-empty array.
|
| Plugin ID derivation | Extracts unscoped package name via unscopedPackageName().
|
| Path traversal prevention | Uses isPathInside() and resolveSafeInstallDir() to ensure all paths stay within bounds.
|
| Security scanning | Calls scanDirectoryWithSummary() with forced scan entries for extension files.
|
| File copy | Recursively copies the package to the target directory. |
| Dependency resolution | Runs npm install --omit=dev --silent if the package has dependencies.
|
| Backup/rollback | In update mode, creates a timestamped backup and restores it on failure. |
Supporting Types
type PluginInstallLogger = {
info?: (message: string) => void;
warn?: (message: string) => void;
};
Usage Example
// Install from npm
const result = await installPluginFromNpmSpec({
spec: "@openclaw/voice-call",
mode: "install",
logger: { info: console.log, warn: console.warn },
});
// Install from local path (auto-detects type)
const result2 = await installPluginFromPath({
path: "./my-plugin",
mode: "update",
dryRun: true,
});