Compare commits
6 Commits
6eb8db3791
...
e3a0f46f9d
| Author | SHA1 | Date | |
|---|---|---|---|
| e3a0f46f9d | |||
| 9ce3a557a9 | |||
| 68019dec58 | |||
| 5f9c2f9193 | |||
| 7db819c647 | |||
| 1b5ab2fa86 |
@ -17,6 +17,7 @@ exports `activate(ctx)`.
|
||||
- [Services](docs/services.md): workspace, tasks, tooling, events, config, terminal and windows facades.
|
||||
- [Packaging And Distribution](docs/packaging-distribution.md): build output, archive layout, hot reload and dependency rules.
|
||||
- [Codex Skills And IdeA Plugins](docs/codex-skills-and-idea-plugins.md): separate IdeA-native plugin skills from Codex plugin discovery.
|
||||
- [Project Plugin Assignments](docs/project-plugin-assignments.md): enable plugins per project and assign skills and MCP tools to agents.
|
||||
|
||||
The installable example lives in [`examples/hello-plugin`](examples/hello-plugin).
|
||||
It demonstrates a menu command, storage, background-task feedback, a React layout
|
||||
|
||||
@ -6,7 +6,10 @@ The plugin entrypoint exports `activate(ctx)`.
|
||||
import type { ActivateContext, IdeAPluginModule } from "@idea/plugin-sdk";
|
||||
|
||||
export function activate(ctx: ActivateContext): void {
|
||||
ctx.logger.info("activated", { pluginId: ctx.pluginId });
|
||||
ctx.logger.info("activated", {
|
||||
pluginId: ctx.pluginId,
|
||||
pluginRoot: ctx.pluginRoot
|
||||
});
|
||||
}
|
||||
|
||||
export default { activate } satisfies IdeAPluginModule;
|
||||
@ -27,7 +30,8 @@ focused project at invocation/render time.
|
||||
|
||||
## Context Fields
|
||||
|
||||
- `pluginId`, `pluginDisplayName`, `version`: host-provided identity.
|
||||
- `pluginId`: host-provided plugin identity.
|
||||
- `pluginRoot`: absolute host-local path to the active installed plugin package.
|
||||
- `logger`: `debug`, `info`, `warn`, `error`.
|
||||
- `subscriptions`: push disposables returned by command/layout/watch
|
||||
registrations.
|
||||
@ -41,6 +45,12 @@ focused project at invocation/render time.
|
||||
The context never exposes internal IdeA gateways or Tauri commands. Use
|
||||
`ctx.services` and the registration APIs instead.
|
||||
|
||||
`pluginRoot` identifies the committed package currently loaded by IdeA. It is
|
||||
not the original source directory or archive path and may change after reinstall.
|
||||
Treat it as read-only, do not persist it, and resolve packaged scripts/assets
|
||||
under it only while the plugin is active. Workspace services remain confined to
|
||||
project-owned paths.
|
||||
|
||||
## Disposal
|
||||
|
||||
Push every returned disposable to `ctx.subscriptions`:
|
||||
|
||||
@ -40,7 +40,9 @@ loading.
|
||||
For multi-harness IdeA agent skills, declare `contributes.skills` in
|
||||
`idea-plugin.json`, ship the referenced Markdown files inside the package,
|
||||
install the plugin in IdeA, enable it in the target project, and assign the
|
||||
skills to the target agents.
|
||||
skills to the target agents. See
|
||||
[Project Plugin Assignments](project-plugin-assignments.md) for the project
|
||||
settings schema and the complete install, enable, assign, and relaunch workflow.
|
||||
|
||||
For Codex-specific plugin skills, install the Codex plugin through the Codex
|
||||
plugin mechanism for the intended scope, then assign or enable those skills
|
||||
|
||||
@ -43,6 +43,10 @@ Check preconditions before calling `runCommand()`:
|
||||
- `ownerAgentId` is a real agent id when the work is owned by an agent workflow.
|
||||
Do not use all-zero placeholders in production.
|
||||
- `cwd` is relative to the project root.
|
||||
- `command` and `args` do not resolve package-relative plugin resources
|
||||
implicitly. Build an explicit absolute script path from `ctx.pluginRoot` when
|
||||
launching read-only files shipped in the installed package.
|
||||
- Do not use `ctx.pluginRoot` as `cwd`; `cwd` remains relative to the project root.
|
||||
- `command` and `args` are separate values. Do not shell-join user input.
|
||||
|
||||
If any required precondition fails, return a skipped result:
|
||||
@ -196,3 +200,20 @@ await ctx.services?.tasks.runCommand({
|
||||
|
||||
This creates misleading Work history, uses a placeholder owner and turns a
|
||||
precondition failure into a fake task. Return `status: "skipped"` instead.
|
||||
|
||||
Do not try to reach packaged plugin scripts through workspace-relative paths:
|
||||
|
||||
```ts
|
||||
await ctx.services?.tasks.runCommand({
|
||||
projectId: project.id,
|
||||
ownerAgentId,
|
||||
label: "Run packaged tests",
|
||||
command: "scripts/run-unity-tests.sh",
|
||||
cwd: "."
|
||||
});
|
||||
```
|
||||
|
||||
`scripts/run-unity-tests.sh` above is resolved like any other executable visible
|
||||
from the project task environment; it is not resolved relative to the plugin
|
||||
package. Use a project-owned script or an executable available on `PATH` until a
|
||||
dedicated packaged-resource API exists.
|
||||
|
||||
@ -177,6 +177,10 @@ Skill contribution rules:
|
||||
- Installing a plugin globally is not enough: a project must enable the plugin,
|
||||
then assign the skill to the target agent.
|
||||
|
||||
See [Project Plugin Assignments](project-plugin-assignments.md) for the
|
||||
`.ideai/project-plugins.json` schema, validation rules, assignment order, and
|
||||
agent relaunch requirements.
|
||||
|
||||
`contributes.layouts` is the only surface for plugin layouts. The same layout
|
||||
contribution can be mounted inside an IdeA layout cell or opened in a detached OS
|
||||
window. Do not add a separate manifest contribution type for windows.
|
||||
@ -243,6 +247,19 @@ Contribution ids are part of the runtime contract:
|
||||
- `services.windows.open({ layoutType })` only accepts a layout type declared by
|
||||
the calling plugin.
|
||||
|
||||
## MCP Server Paths
|
||||
|
||||
`contributes.mcpServers` entries are resolved by the host before starting a
|
||||
declared MCP server. In `command`, `args`, `env` and `cwd`, the host expands:
|
||||
|
||||
- `${pluginRoot}` to the installed plugin package root.
|
||||
- `${appDataDir}` to the host-owned application data directory.
|
||||
|
||||
This string substitution is limited to manifest-declared MCP server startup.
|
||||
Runtime handlers receive the same installed package location separately as
|
||||
`ctx.pluginRoot`; `ctx.services.tasks.runCommand()` does not perform placeholder
|
||||
substitution, and workspace APIs remain project-confined.
|
||||
|
||||
## Validation
|
||||
|
||||
Use the SDK validator in tests or build tooling:
|
||||
|
||||
154
docs/project-plugin-assignments.md
Normal file
154
docs/project-plugin-assignments.md
Normal file
@ -0,0 +1,154 @@
|
||||
# Project Plugin Assignments
|
||||
|
||||
IdeA installs plugin packages globally, but enables their capabilities per
|
||||
project and assigns agent-facing capabilities per agent. Project choices are
|
||||
stored in the project-owned `.ideai/project-plugins.json` file.
|
||||
|
||||
The normal workflow is:
|
||||
|
||||
1. Install the plugin globally in IdeA.
|
||||
2. Enable the installed plugin for the target project.
|
||||
3. Assign selected plugin skills and MCP tools to each target agent.
|
||||
4. Relaunch the agent so its capability snapshot includes the new assignments.
|
||||
|
||||
Installation alone does not enable a plugin in a project. Enabling a plugin does
|
||||
not automatically grant all of its capabilities to every agent.
|
||||
|
||||
## Version 1 Schema
|
||||
|
||||
The current schema version is `1`. A complete example is:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"plugins": [
|
||||
{
|
||||
"pluginId": "com.example.unity-plugin",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "2a0e42e2-3c5f-4f30-a679-034b460cd59e",
|
||||
"skills": [
|
||||
{
|
||||
"pluginId": "com.example.unity-plugin",
|
||||
"skillId": "unity.build-debug"
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"pluginId": "com.example.unity-plugin",
|
||||
"serverId": "unity-editor",
|
||||
"toolName": "unity_build"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The top-level fields are:
|
||||
|
||||
- `version`: required schema version. It must currently be `1`; unsupported
|
||||
versions are rejected.
|
||||
- `plugins`: project activation entries. Each entry contains the stable global
|
||||
`pluginId` and an `enabled` boolean.
|
||||
- `agents`: per-agent assignments. Each entry identifies an IdeA project agent
|
||||
by `agentId` and contains ordered `skills` and `tools` arrays.
|
||||
|
||||
An agent skill reference contains:
|
||||
|
||||
- `pluginId`: the id from the owning plugin's `idea-plugin.json`.
|
||||
- `skillId`: the stable `contributes.skills[].id` within that plugin.
|
||||
|
||||
An agent MCP tool reference contains:
|
||||
|
||||
- `pluginId`: the id from the owning plugin's `idea-plugin.json`.
|
||||
- `serverId`: the stable `contributes.mcpServers[].id` within that plugin.
|
||||
- `toolName`: the exact tool name advertised by that MCP server. The UI accepts
|
||||
this name as text because a dynamic MCP tool catalogue is not yet available.
|
||||
|
||||
Missing `plugins`, `agents`, `skills`, or `tools` arrays default to empty arrays
|
||||
when IdeA reads the document. Prefer writing them explicitly for clarity and
|
||||
forward-compatible review.
|
||||
|
||||
## Assignment And Injection Order
|
||||
|
||||
Array order is significant. IdeA keeps plugin skill assignments in the order in
|
||||
which they were added. At agent launch, IdeA resolves the agent's native skills
|
||||
first, in agent-manifest order, then its plugin skills in the order stored in
|
||||
`agents[].skills`. Unavailable assignments are skipped without reordering the
|
||||
remaining skills.
|
||||
|
||||
The resolved skill catalogue is injected into the agent's orchestration context
|
||||
before the agent persona. Each supported harness receives the same ordered
|
||||
IdeA-native capability snapshot. The skill body remains read-only package
|
||||
content and is available through `idea_skill_read` only when that skill was
|
||||
resolved for the launched agent.
|
||||
|
||||
## Tombstones
|
||||
|
||||
Plugin activation entries and agent assignments are intentionally retained when
|
||||
a plugin, skill, or MCP server is temporarily missing. These dangling entries
|
||||
are tombstones: they preserve project intent across uninstall/reinstall or a
|
||||
temporary manifest change.
|
||||
|
||||
A tombstone is inert. IdeA skips it when resolving capabilities, and the project
|
||||
settings UI marks stale skill or server references as missing or obsolete. The
|
||||
UI lets users remove those references explicitly. Reinstalling and activating a
|
||||
matching plugin makes a retained assignment resolvable again.
|
||||
|
||||
Disabling a plugin also keeps its assignments. They become effective again only
|
||||
after the plugin is enabled and runtime-active for the project.
|
||||
|
||||
## UI And API Validation
|
||||
|
||||
Prefer the project plugin settings UI or its backing API commands over manual
|
||||
editing. They apply the following rules:
|
||||
|
||||
- Enabling requires the plugin package to be installed globally. Disabling a
|
||||
missing plugin is allowed so a retained activation can be cleared.
|
||||
- Assigning a skill requires an existing project agent, an enabled and
|
||||
runtime-active plugin, and a matching `contributes.skills[].id`.
|
||||
- Assigning a tool requires an existing project agent, an enabled and
|
||||
runtime-active plugin, a matching `contributes.mcpServers[].id`, and a
|
||||
non-empty `toolName`.
|
||||
- Assign and unassign operations are idempotent. Assigning an existing reference
|
||||
does not duplicate it; unassigning a missing reference is harmless.
|
||||
- Unassign operations remain available for tombstones, so stale references can
|
||||
be removed even when their plugin capability no longer resolves.
|
||||
|
||||
The project settings read API returns both the persisted document and the
|
||||
currently installed plugin manifests. The UI uses this catalogue to offer only
|
||||
skills and MCP servers from plugins enabled for the project, while still
|
||||
surfacing persisted tombstones.
|
||||
|
||||
## Manual Editing
|
||||
|
||||
The file is project-owned and may be edited manually, for example in version
|
||||
control or automation. Stop or relaunch affected agents around the edit, keep
|
||||
the document valid JSON, preserve `version: 1`, use the real project agent UUID,
|
||||
and copy plugin, skill, and server ids exactly from the plugin manifest.
|
||||
|
||||
Manual editing bypasses the mutation checks described above. A syntactically
|
||||
valid document may therefore contain duplicates, unknown agents, disabled or
|
||||
missing plugins, unknown capability ids, or an invalid tool name. Such entries
|
||||
may remain inert or fail later operations. Use the UI/API whenever possible to
|
||||
receive immediate validation and idempotent updates.
|
||||
|
||||
## Reloading Plugins And Relaunching Agents
|
||||
|
||||
These actions solve different problems:
|
||||
|
||||
- Reload the plugin after rebuilding or replacing its installed package. Reload
|
||||
refreshes the plugin manifest, runtime bundle, contributions, and packaged
|
||||
skill Markdown. It does not change project activation or agent assignments.
|
||||
- Relaunch the agent after enabling or disabling a project plugin, changing its
|
||||
skill/tool assignments, or manually editing `.ideai/project-plugins.json`.
|
||||
Agent capabilities are resolved into a launch-time snapshot, so an already
|
||||
running agent does not acquire assignment changes retroactively.
|
||||
|
||||
When both package content and assignments changed, reload the plugin first,
|
||||
confirm it is enabled for the project, update the assignments, then relaunch the
|
||||
agent.
|
||||
@ -37,6 +37,25 @@ Key APIs:
|
||||
Use `runCommand()` only after preconditions are satisfied. `ownerAgentId` must be
|
||||
a real agent id when work belongs to an agent workflow.
|
||||
|
||||
`runCommand()` keeps its `cwd` project/workspace scoped: `cwd` must be a relative
|
||||
path under the project root. The host does not implicitly resolve `command` or
|
||||
`args` against the plugin package. Use the absolute `ctx.pluginRoot` supplied at
|
||||
activation time to construct an explicit path to a packaged script, and pass
|
||||
that path as `command` or as an argument to its interpreter. Treat package files
|
||||
as read-only.
|
||||
|
||||
```ts
|
||||
const script = `${ctx.pluginRoot.replace(/[\\/]+$/, "")}/scripts/check.mjs`;
|
||||
await ctx.services.tasks.runCommand({
|
||||
projectId,
|
||||
ownerAgentId,
|
||||
label: "Run packaged check",
|
||||
command: "node",
|
||||
args: [script],
|
||||
cwd: "."
|
||||
});
|
||||
```
|
||||
|
||||
## Tooling
|
||||
|
||||
`services.tooling.diagnose()` checks executables, environment values and files
|
||||
|
||||
@ -16,6 +16,8 @@ It exercises the current plugin primitives end to end:
|
||||
`ctx.services.windows.open(...)` when services are available.
|
||||
- plugin-owned storage: activation count, command run count and initialization flag;
|
||||
- tooling capability: logs the focused workspace project when `ctx.services` is available.
|
||||
- packaged command script: resolves `scripts/hello-task.mjs` from the official
|
||||
`ctx.pluginRoot` and launches it with the project kept as the task working directory.
|
||||
|
||||
The source is intentionally split across multiple TypeScript modules:
|
||||
|
||||
@ -23,6 +25,7 @@ The source is intentionally split across multiple TypeScript modules:
|
||||
- `src/constants.ts` owns shared command/layout identifiers;
|
||||
- `src/core/layout.tsx` and `src/core/workspace.ts` hold feature logic.
|
||||
- `src/core/storage.ts` keeps plugin-owned counters and flags in `ctx.storage`.
|
||||
- `scripts/hello-task.mjs` is a read-only runtime resource included in the archive.
|
||||
|
||||
The build uses plain `tsc`; it does not bundle the plugin into one file. The archive includes all
|
||||
compiled `dist/**/*.js` files so IdeA can load `dist/index.js` and serve its package-relative imports
|
||||
@ -36,8 +39,8 @@ npm run package:hello-plugin
|
||||
```
|
||||
|
||||
The installable archive is emitted at `examples/hello-plugin/build/hello-plugin-0.1.0.zip`.
|
||||
It contains `idea-plugin.json` at the ZIP root and the compiled multi-file ESM output under `dist/`,
|
||||
including `dist/index.js`, matching the manifest `main` field.
|
||||
It contains `idea-plugin.json` at the ZIP root, the compiled multi-file ESM output under `dist/`,
|
||||
including `dist/index.js`, and the packaged handler under `scripts/hello-task.mjs`.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
|
||||
1
examples/hello-plugin/scripts/hello-task.mjs
Normal file
1
examples/hello-plugin/scripts/hello-task.mjs
Normal file
@ -0,0 +1 @@
|
||||
console.log("hello from a script packaged with @idea/plugin-sdk");
|
||||
@ -122,12 +122,13 @@ export async function runHelloCommandTask(ctx: ActivateContext): Promise<HelloCo
|
||||
return feedback;
|
||||
}
|
||||
|
||||
const pluginRoot = ctx.pluginRoot.replace(/[\\/]+$/, "");
|
||||
const task = await ctx.services.tasks.runCommand({
|
||||
projectId: project.id,
|
||||
ownerAgentId,
|
||||
label: "Hello plugin command",
|
||||
command: "echo",
|
||||
args: ["hello from @idea/plugin-sdk"],
|
||||
command: "node",
|
||||
args: [`${pluginRoot}/scripts/hello-task.mjs`],
|
||||
cwd: ".",
|
||||
recordOnly: true
|
||||
});
|
||||
|
||||
@ -9,12 +9,14 @@ const main = requireString(manifest, "main");
|
||||
const version = requireString(manifest, "version");
|
||||
const archivePath = join(pluginRoot, "build", `hello-plugin-${version}.zip`);
|
||||
const distEntries = await collectFiles(join(pluginRoot, "dist"), "dist");
|
||||
const scriptEntries = await collectFiles(join(pluginRoot, "scripts"), "scripts");
|
||||
if (!distEntries.some((entry) => entry.archivePath === main)) {
|
||||
throw new Error(`Built plugin dist does not contain manifest main: ${main}`);
|
||||
}
|
||||
const archiveEntries = [
|
||||
{ archivePath: "idea-plugin.json", sourcePath: manifestPath },
|
||||
...distEntries,
|
||||
...scriptEntries,
|
||||
{ archivePath: "README.md", sourcePath: join(pluginRoot, "README.md") }
|
||||
];
|
||||
const DOS_TIME_MIDNIGHT = 0;
|
||||
|
||||
@ -2,6 +2,8 @@ import type { ComponentType, ReactNode } from "react";
|
||||
|
||||
export interface ActivateContext {
|
||||
pluginId: string;
|
||||
/** Absolute host-local root of this active installed plugin package. */
|
||||
pluginRoot: string;
|
||||
logger: PluginLogger;
|
||||
subscriptions: CommandDisposable[];
|
||||
commands?: CommandRegistry;
|
||||
|
||||
Reference in New Issue
Block a user