feat: add plugin manager and official plugins - #119
Conversation
🦋 Changeset detectedLatest commit: 460ac4e The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ba9d24bc92
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| "description": "Planning, TDD, debugging, and delivery workflows for coding agents.", | ||
| "homepage": "https://github.com/obra/superpowers", | ||
| "keywords": ["skills", "planning", "tdd", "debugging", "code-review"], | ||
| "source": "./curated/superpowers" |
There was a problem hiding this comment.
Remove missing plugin marketplace source
This local relative source is consumed by build-plugin-marketplace-cdn.mjs, which stats every local marketplace source and throws when it is absent. I checked the plugin tree and only plugins/official/kimi-datasource exists, so the root build:plugin-marketplace target will fail for the committed marketplace until this entry is either backed by a checked-in/available plugin archive or changed to a reachable remote source.
Useful? React with 👍 / 👎.
| if (registry === undefined) return undefined; | ||
| const blocks: string[] = []; | ||
| for (const sessionStart of sessionStarts) { | ||
| const skill = registry.getSkill(sessionStart.skillName); |
There was a problem hiding this comment.
Resolve session-start skills by plugin identity
When a plugin's sessionStart.skill name is also defined by a project/user skill, this name-only lookup can inject the wrong skill: plugin skill roots are appended after project/user roots and the registry keeps the first skill for a normalized name. In that collision scenario, startup will render the non-plugin skill under the plugin's <plugin_session_start> block instead of the plugin-owned session-start content, so this should verify skill.plugin?.id === sessionStart.pluginId or otherwise resolve the specific plugin skill.
Useful? React with 👍 / 👎.
| .then(() => pipeline(stream, createWriteStream(destPath))) | ||
| .then(() => restoreFilePermissions(destPath, entry)) |
There was a problem hiding this comment.
Bound plugin archive extraction size
Installing a plugin from a marketplace or direct zip URL currently streams every archive entry to disk without any compressed or uncompressed size limit, so a small zip bomb or simply an oversized plugin archive can exhaust the user's disk during /plugins install. Please enforce a maximum download/extracted byte budget before writing entries, or reject archives whose declared uncompressed sizes exceed the budget.
Useful? React with 👍 / 👎.
| return undefined; | ||
| } | ||
|
|
||
| const found = await search(dir); |
There was a problem hiding this comment.
Prefer the archive root manifest first
For zip installs where the archive root itself contains kimi.plugin.json or .kimi-plugin/plugin.json, this starts by searching child directories and can return a nested example/subplugin manifest instead of the actual root plugin if one exists. Check hasManifest(dir) before descending so a valid top-level plugin archive is not accidentally installed as a nested plugin.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| function pluginMcpRuntimeName(pluginId: string, serverName: string): string { | ||
| return `plugin-${pluginId}-${serverName}`; |
There was a problem hiding this comment.
Disambiguate plugin MCP runtime names
Because plugin ids may contain hyphens and MCP server names are only checked for non-empty strings, different plugin/server pairs can produce the same runtime key here, e.g. plugin a-b server c and plugin a server b-c both become plugin-a-b-c. When both are enabled, enabledMcpServers() writes them into the same object key and one server silently overwrites the other; use an escaping scheme or structured separator that cannot collide.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 34bca195fc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| "description": "Planning, TDD, debugging, and delivery workflows for coding agents.", | ||
| "homepage": "https://github.com/obra/superpowers", | ||
| "keywords": ["skills", "planning", "tdd", "debugging", "code-review"], | ||
| "source": "./curated/superpowers" |
There was a problem hiding this comment.
Remove absent Superpowers source from marketplace
In a clean checkout this marketplace entry points at plugins/curated/superpowers, but that directory is not tracked in this commit (git ls-tree for plugins/curated/superpowers is empty). The new build:plugin-marketplace script materializes every local relative source and throws Marketplace source does not exist when the path is missing, so the CDN marketplace build cannot succeed unless a developer has an uncommitted local clone.
Useful? React with 👍 / 👎.
| for (const entry of subdirs) { | ||
| await walkSkillDir(path.join(dirPath, entry), source, false, depth + 1); | ||
| await walkSkillDir(path.join(dirPath, entry), root, false, depth + 1); |
There was a problem hiding this comment.
Stop descending after root plugin SKILL.md
When a plugin uses the root SKILL.md fallback, it is registered as a single skill bundle above, but this unconditional subdirectory walk still scans its child directories. For plugins that keep bundled references/examples under the root, any nested */SKILL.md will be exposed as an extra public plugin skill, unlike the existing directory-bundle rule that deliberately does not descend into a directory containing SKILL.md.
Useful? React with 👍 / 👎.
| }); | ||
| if (resolvedCommand === undefined) return undefined; | ||
| command = resolvedCommand; | ||
| } else if (command.includes('/') || path.isAbsolute(command)) { |
There was a problem hiding this comment.
Reject Windows backslash plugin commands
On Windows, a plugin MCP command such as ..\..\outside.exe or .\bin\server.exe contains no /, so it falls through this check as if it were a PATH command instead of being resolved and confined by resolvePluginPathField. Since plugin MCP servers are later launched with the plugin root as cwd, backslash relative paths can escape the managed plugin directory or bypass the intended inside-plugin validation.
Useful? React with 👍 / 👎.
| if ( | ||
| matchesKey(data, Key.enter) || | ||
| matchesKey(data, Key.space) || | ||
| printableChar(data) === ' ' |
There was a problem hiding this comment.
Preserve spaces while searching choice pickers
For searchable ChoicePickerComponent instances, such as the provider selector in the /connect flow, pressing space now selects the highlighted item before SearchableList can append the space to the query. Users trying to search multi-word provider names can accidentally accept the current selection instead of continuing their search; space-to-select should be limited to non-searchable pickers or empty search state.
Useful? React with 👍 / 👎.
| if ( | ||
| trimmed.startsWith('http://') || | ||
| trimmed.startsWith('https://') || | ||
| trimmed.startsWith('~/') || | ||
| trimmed === '~' || | ||
| isAbsolute(trimmed) | ||
| ) { | ||
| return trimmed; |
There was a problem hiding this comment.
Resolve root-relative remote marketplace sources
For a remote marketplace, an entry like "source": "/plugins/demo.zip" is a normal origin-relative URL, but this branch returns it as a local absolute filesystem path before the new URL(..., location.resolved) fallback runs. Installing that marketplace entry then calls the plugin installer with /plugins/demo.zip and fails with a local-path error instead of downloading from the marketplace host.
Useful? React with 👍 / 👎.
| return { | ||
| ...config, | ||
| command: process.execPath, | ||
| args: [KIMI_NODE_FALLBACK_SUBCOMMAND, ...(config.args ?? [])], |
There was a problem hiding this comment.
Preserve Node flags in native plugin fallback
In native builds, any plugin MCP server declared as command: "node" is rewritten to the hidden __plugin_run_node command, but the first original Node argument becomes the required entry path. A valid server command such as node --enable-source-maps ./server.mjs therefore tries to realpath("--enable-source-maps") and fails only in native distributions, even though it works when node is available on PATH.
Useful? React with 👍 / 👎.
Co-authored-by: liruifengv <liruifeng1024@gmail.com> Signed-off-by: qer <wbxl2000@outlook.com>
Summary
Verification