@jini-ai/agent-plugins 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/dist/lifecycle/bundled-digests.js +4 -0
- package/dist/lifecycle/install.d.ts +1 -1
- package/dist/lifecycle/install.js +115 -38
- package/dist/lifecycle/installed-package-roots.d.ts +16 -0
- package/dist/lifecycle/installed-package-roots.js +55 -0
- package/dist/lifecycle/manifest.d.ts +5 -54
- package/dist/lifecycle/manifest.js +8 -9
- package/dist/lifecycle/persistent-state.d.ts +4 -0
- package/dist/lifecycle/persistent-state.js +2 -0
- package/dist/lifecycle/ports.d.ts +8 -2
- package/dist/lifecycle/resolve-agent-plugin-refs.js +7 -36
- package/dist/lifecycle/search.d.ts +2 -0
- package/dist/lifecycle/search.js +2 -2
- package/dist/lifecycle/types.d.ts +2 -0
- package/dist/manifest.d.ts +3 -0
- package/dist/manifest.js +3 -2
- package/package.json +4 -7
- package/ui-ux-design/plugin.json +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## 0.4.2
|
|
4
4
|
|
|
5
|
+
- Use shared plugin identity metadata with a required `@jini-ai/core` peer at `^0.4.2`. Repair identity-lock publication syntax and isolate unreadable or invalid installed manifests with warnings so one damaged install cannot block healthy installs or bundled seeding.
|
|
6
|
+
|
|
5
7
|
### Patch Changes
|
|
6
8
|
|
|
7
9
|
- Tovu clean-up release (2026-10-08): code Tovu moved into Jini, plus the Jini clean-up (see the commit log since 0.4.1).
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* ships as — written by `seed-bundled.ts` on every boot, read by the surfaces that have to pick one
|
|
4
4
|
* installed package for a plugin id.
|
|
5
5
|
*
|
|
6
|
+
* Current install policy (frpid, 2026-10-09): successful publication prunes superseded revisions.
|
|
7
|
+
* The rationale below describes retained legacy stores and interrupted cleanup. This ledger still
|
|
8
|
+
* selects the build's authoritative digest there; the ledger itself never deletes package bytes.
|
|
9
|
+
*
|
|
6
10
|
* ---------------------------------------------------------------------------
|
|
7
11
|
* The gap this closes
|
|
8
12
|
* ---------------------------------------------------------------------------
|
|
@@ -5,7 +5,7 @@ export type { InstalledAgentPlugin, InstalledAgentPluginSkill } from "./types.js
|
|
|
5
5
|
/** One extraction/install failure reason. A caller (an admin route, a future marketplace installer)
|
|
6
6
|
* branches on `code` rather than parsing `message` — this codebase's own convention
|
|
7
7
|
* (`PluginLoadError.reason` in `plugin-runtime/loader.ts` is the direct precedent). */
|
|
8
|
-
export type AgentPluginInstallErrorCode = "ARCHIVE_TOO_LARGE" | "DIGEST_MISMATCH" | "TOO_MANY_ENTRIES" | "UNSAFE_ENTRY_PATH" | "SYMLINK_ENTRY_REJECTED" | "UNSUPPORTED_ENTRY_KIND" | "DUPLICATE_ENTRY" | "FILE_TOO_LARGE" | "DECOMPRESSION_BOMB" | "TOTAL_SIZE_EXCEEDED" | "MANIFEST_MISSING" | "MANIFEST_INVALID" | "PUBLISH_FAILED";
|
|
8
|
+
export type AgentPluginInstallErrorCode = "PLUGIN_ID_CONFLICT" | "ARCHIVE_TOO_LARGE" | "DIGEST_MISMATCH" | "TOO_MANY_ENTRIES" | "UNSAFE_ENTRY_PATH" | "SYMLINK_ENTRY_REJECTED" | "UNSUPPORTED_ENTRY_KIND" | "DUPLICATE_ENTRY" | "FILE_TOO_LARGE" | "DECOMPRESSION_BOMB" | "TOTAL_SIZE_EXCEEDED" | "MANIFEST_MISSING" | "MANIFEST_INVALID" | "PUBLISH_FAILED";
|
|
9
9
|
export declare class AgentPluginInstallError extends Error {
|
|
10
10
|
readonly code: AgentPluginInstallErrorCode;
|
|
11
11
|
constructor({ code, message }: {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { listInstalledPackageRoots } from "./installed-package-roots.js";
|
|
2
|
+
import { createUninstallModule } from "./uninstall.js";
|
|
3
|
+
import { readPluginIdentity, assertPluginIdentityAvailable } from "@jini-ai/core/plugin-directory";
|
|
1
4
|
/**
|
|
2
5
|
* @file `installAgentPlugin()` — content-addressed, adversarially-hardened extraction of one Agent
|
|
3
6
|
* Plugin (agent-plugins.org) archive into one workspace's own `AgentPluginWorkspaceLayout.packages`.
|
|
@@ -95,7 +98,7 @@ function buildModule(ports) {
|
|
|
95
98
|
const rename = ports.filesystem.rename.bind(ports.filesystem);
|
|
96
99
|
const rm = ports.filesystem.rm.bind(ports.filesystem);
|
|
97
100
|
const stat = ports.filesystem.stat.bind(ports.filesystem);
|
|
98
|
-
const parseAgentPluginManifest = (value) => parseAgentPluginManifestValue({ value });
|
|
101
|
+
const parseAgentPluginManifest = (value) => parseAgentPluginManifestValue({ value }, { identityNamespace: ports.extensionNamespace });
|
|
99
102
|
const { assertContainedOnDisk, normalizePackageEntryPath } = createPackagePathsModule(ports);
|
|
100
103
|
const LIMITS = {
|
|
101
104
|
maxArchiveBytes: 32 * 1024 * 1024,
|
|
@@ -103,6 +106,63 @@ function buildModule(ports) {
|
|
|
103
106
|
maxFileBytes: 16 * 1024 * 1024,
|
|
104
107
|
maxTotalExtractedBytes: 64 * 1024 * 1024,
|
|
105
108
|
};
|
|
109
|
+
/** Publisher ids are metadata, never filesystem segments. The workspace identity lock
|
|
110
|
+
* serializes installs under different local names competing for one qualified id.
|
|
111
|
+
* @complexity O(d) manifest reads for d installed revisions; no executable imports. */
|
|
112
|
+
async function assertIdentityAvailable(workspaceRoot, candidate) {
|
|
113
|
+
// Installed roots are canonical paths; compare against the same root even when the host
|
|
114
|
+
// supplied an OS alias (macOS /var -> /private/var). Containment still rejects escaped links.
|
|
115
|
+
workspaceRoot = await ports.filesystem.realpath(workspaceRoot);
|
|
116
|
+
for (const root of await listInstalledPackageRoots({ ports, workspaceRoot })) {
|
|
117
|
+
const manifestPath = path.join(root.packageRoot, 'plugin.json');
|
|
118
|
+
await persistentState.assertOwnedPath({ workspaceRoot, entryPath: path.relative(workspaceRoot, manifestPath) });
|
|
119
|
+
let raw;
|
|
120
|
+
let identity;
|
|
121
|
+
try {
|
|
122
|
+
raw = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
123
|
+
identity = readPluginIdentity({ manifest: raw, name: root.localId, namespace: ports.extensionNamespace });
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// A damaged unrelated install must not block fresh installs or bundled seeding.
|
|
127
|
+
ports.onEvent?.({ event: 'agent-plugin-identity-warning', message: `Plugin '${root.localId}': installed identity manifest is unreadable or invalid; omitted from identity admission.` });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
assertPluginIdentityAvailable({ candidate: { id: candidate.id, localId: candidate.pluginId, family: 'agent-plugin' },
|
|
132
|
+
installed: [{ ...identity, localId: root.localId, family: 'agent-plugin', version: typeof raw.version === 'string' ? raw.version : null, enabled: false }] });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new AgentPluginInstallError({ code: 'PLUGIN_ID_CONFLICT', message: 'The publisher-qualified plugin id conflicts with an installed package.' });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Retires only this plugin's superseded revisions after accepted publication, under the same
|
|
140
|
+
* identity/plugin locks. Persistent data, learned facts and notes never enter this operation.
|
|
141
|
+
* Reuse uninstall's frozen-tree staging so an interrupted removal is no longer a live digest.
|
|
142
|
+
* There is no previous-revision rollback slot in this lifecycle; failed publication keeps all
|
|
143
|
+
* prior bytes. Cleanup faults warn without turning an accepted install into a failed install.
|
|
144
|
+
* @complexity O(d + f), d workspace revisions, f files in this plugin's superseded packages.
|
|
145
|
+
*/
|
|
146
|
+
async function pruneSupersededRevisions({ workspaceRoot, installed }, _optional = {}) {
|
|
147
|
+
try {
|
|
148
|
+
// Resolve lazily: uninstall's read-side index uses this install module for manifest indexing.
|
|
149
|
+
const removal = createUninstallModule(ports);
|
|
150
|
+
for (const root of await listInstalledPackageRoots({ ports, workspaceRoot })) {
|
|
151
|
+
if (root.localId !== installed.pluginId || root.archiveDigest === installed.archiveDigest)
|
|
152
|
+
continue;
|
|
153
|
+
try {
|
|
154
|
+
const staged = await removal.stageForRemoval(path.dirname(root.packageRoot), root.packageRoot);
|
|
155
|
+
await removal.removeFrozenPackageTree(staged.quarantined);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
ports.onEvent?.({ event: 'agent-plugin-revision-prune-warning', message: `Plugin '${installed.pluginId}': a superseded revision could not be pruned.` });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
ports.onEvent?.({ event: 'agent-plugin-revision-prune-warning', message: `Plugin '${installed.pluginId}': superseded revisions could not be scanned for pruning.` });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
106
166
|
/** Exposes `LIMITS.maxArchiveBytes` for a caller outside this module — concretely, a test asserting
|
|
107
167
|
* `@jini-ai/agent-plugins/lifecycle`'s fetch cap constant stays pinned to this one. The local fetch-archive.ts fork was deleted; see the package's fetch module
|
|
108
168
|
* for why it duplicates the number instead of importing it (no runtime dependency on `install.ts`);
|
|
@@ -150,43 +210,56 @@ function buildModule(ports) {
|
|
|
150
210
|
const indexed = await indexInstalledRoot(extractionRoot, digest);
|
|
151
211
|
if (optional.previewOnly)
|
|
152
212
|
return indexed;
|
|
153
|
-
return await persistentState.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
213
|
+
return await persistentState.withIdentityLock({ workspaceRoot: workspaceLayout.root, run: async () => {
|
|
214
|
+
await assertIdentityAvailable(workspaceLayout.root, indexed);
|
|
215
|
+
const publishWithinPluginLock = () => persistentState.withPluginLock({ workspaceRoot: workspaceLayout.root, pluginId: indexed.pluginId, run: async () => {
|
|
216
|
+
// Layout B needs the manifest id before choosing the package destination. Dedup still
|
|
217
|
+
// skips publication and freezing, after one bounded staging extraction.
|
|
218
|
+
// Owner decision 2026-10-04: learned facts begin empty for each site; author guidance stays
|
|
219
|
+
// in package docs. Package extension files never seed learned memory or the user's notes.
|
|
220
|
+
// Content-addressed dedup, scoped to THIS workspace's own tree (`workspaceLayout` was just
|
|
221
|
+
// resolved above, atomically, from this same `workspaceId`): identical bytes were already
|
|
222
|
+
// extracted, verified, and frozen by a prior install of this same workspace's — never
|
|
223
|
+
// re-extracted for a different workspace, by construction, since a different workspace's
|
|
224
|
+
// `workspaceLayout.packages` is a different path entirely. one bounded staging extraction precedes this check.
|
|
225
|
+
// Publish BEFORE freezing permissions, not after: `rename()` moves `extractionRoot` as a single
|
|
226
|
+
// directory-entry operation, and on this filesystem renaming an already-read-only (0o555)
|
|
227
|
+
// directory itself fails EACCES (observed, not merely theoretical — see the regression test this
|
|
228
|
+
// ordering fixed). Freezing after publish also closes a smaller race for free: nothing outside
|
|
229
|
+
// this function can observe the package at its live path while it is still writable, because the
|
|
230
|
+
// live path does not exist until `publish` returns.
|
|
231
|
+
let publishedPlugin;
|
|
232
|
+
const publishPackage = async () => {
|
|
233
|
+
// The manifest identifies the plugin only after bounded staging extraction (Layout B).
|
|
234
|
+
const pluginId = indexed.pluginId;
|
|
235
|
+
const packagesDir = workspaceLayout.pluginPackagesDir({ pluginId });
|
|
236
|
+
await persistentState.assertOwnedPath({ workspaceRoot: workspaceLayout.root, entryPath: path.relative(workspaceLayout.root, packagesDir) });
|
|
237
|
+
await mkdir(packagesDir, { recursive: true, mode: 0o700 });
|
|
238
|
+
const finalRoot = await assertContainedOnDisk(packagesDir, digest);
|
|
239
|
+
for (const directory of [workspaceLayout.pluginDataDir({ pluginId }),
|
|
240
|
+
workspaceLayout.pluginMemoryDir({ pluginId, kind: 'learned' }),
|
|
241
|
+
workspaceLayout.pluginMemoryDir({ pluginId, kind: 'notes' })]) {
|
|
242
|
+
await persistentState.assertOwnedPath({ workspaceRoot: workspaceLayout.root, entryPath: path.relative(workspaceLayout.root, directory) });
|
|
243
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
244
|
+
}
|
|
245
|
+
if (await isRealDirectory(finalRoot)) {
|
|
246
|
+
publishedPlugin = await indexInstalledRoot(finalRoot, digest);
|
|
247
|
+
return publishedPlugin;
|
|
248
|
+
}
|
|
249
|
+
await publish(extractionRoot, finalRoot);
|
|
250
|
+
await freezeTree(finalRoot, executablePaths);
|
|
251
|
+
/** Absolute, read-only (frozen) path to the extracted package root. */
|
|
252
|
+
publishedPlugin = { ...indexed, packageRoot: finalRoot };
|
|
253
|
+
return publishedPlugin;
|
|
254
|
+
};
|
|
255
|
+
const installed = await (optional.publishGuard ? optional.publishGuard({ plugin: indexed, publish: publishPackage }, {}) : publishPackage());
|
|
256
|
+
if (publishedPlugin)
|
|
257
|
+
await pruneSupersededRevisions({ workspaceRoot: workspaceLayout.root, installed: publishedPlugin });
|
|
258
|
+
return installed;
|
|
259
|
+
} });
|
|
260
|
+
if (ports.identityAdmission)
|
|
261
|
+
return ports.identityAdmission({ id: indexed.id, localId: indexed.pluginId, run: publishWithinPluginLock }, {});
|
|
262
|
+
return publishWithinPluginLock();
|
|
190
263
|
} });
|
|
191
264
|
}
|
|
192
265
|
finally {
|
|
@@ -379,6 +452,8 @@ function buildModule(ports) {
|
|
|
379
452
|
throw new AgentPluginInstallError({ code: "MANIFEST_INVALID", message: `plugin.json failed validation: ${parsed.errors.join("; ")}` });
|
|
380
453
|
}
|
|
381
454
|
const files = [];
|
|
455
|
+
for (const message of parsed.warnings)
|
|
456
|
+
ports.onEvent?.({ event: 'agent-plugin-identity-warning', message });
|
|
382
457
|
const skills = [];
|
|
383
458
|
async function walk(absolute, prefix) {
|
|
384
459
|
for (const entry of await readdir(absolute, { withFileTypes: true })) {
|
|
@@ -401,6 +476,8 @@ function buildModule(ports) {
|
|
|
401
476
|
await walk(packageRoot, "");
|
|
402
477
|
return {
|
|
403
478
|
pluginId: parsed.manifest.name,
|
|
479
|
+
id: parsed.manifest.id,
|
|
480
|
+
provides: parsed.manifest.provides,
|
|
404
481
|
version: parsed.manifest.version,
|
|
405
482
|
...( /**
|
|
406
483
|
* `plugin.json`'s own `description`/`keywords`/`author`/`license` (`manifest.ts`'s
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentPluginLifecyclePorts } from './ports.js';
|
|
2
|
+
export declare const INSTALLED_PLUGIN_DIGEST_PATTERN: RegExp;
|
|
3
|
+
export interface InstalledPackageRoot {
|
|
4
|
+
readonly localId: string;
|
|
5
|
+
readonly archiveDigest: string;
|
|
6
|
+
readonly packageRoot: string;
|
|
7
|
+
}
|
|
8
|
+
/** Enumerates contained installed revisions; missing stores are empty. A corrupt slot is
|
|
9
|
+
* isolated, matching the full installed-plugin index's existing recovery contract.
|
|
10
|
+
* @complexity O(d) directory/containment operations for d revisions; no executable imports.
|
|
11
|
+
*/
|
|
12
|
+
export declare function listInstalledPackageRoots({ ports, workspaceRoot }: {
|
|
13
|
+
ports: AgentPluginLifecyclePorts;
|
|
14
|
+
workspaceRoot: string;
|
|
15
|
+
}, _optional?: Record<string, never>): Promise<readonly InstalledPackageRoot[]>;
|
|
16
|
+
//# sourceMappingURL=installed-package-roots.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** The installed-package walk shared by full indexing and identity admission. Reusing this
|
|
2
|
+
* traversal preserves the same digest grammar, memory-folder exclusion and containment checks. */
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createPersistentStateModule } from './persistent-state.js';
|
|
5
|
+
import { createPackagePathsModule } from './package-paths.js';
|
|
6
|
+
export const INSTALLED_PLUGIN_DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
7
|
+
/** Enumerates contained installed revisions; missing stores are empty. A corrupt slot is
|
|
8
|
+
* isolated, matching the full installed-plugin index's existing recovery contract.
|
|
9
|
+
* @complexity O(d) directory/containment operations for d revisions; no executable imports.
|
|
10
|
+
*/
|
|
11
|
+
export async function listInstalledPackageRoots({ ports, workspaceRoot }, _optional = {}) {
|
|
12
|
+
const persistentState = createPersistentStateModule(ports);
|
|
13
|
+
const { assertContainedOnDisk } = createPackagePathsModule(ports);
|
|
14
|
+
const readdir = ports.filesystem.readdir.bind(ports.filesystem);
|
|
15
|
+
let folders;
|
|
16
|
+
try {
|
|
17
|
+
folders = await readdir(workspaceRoot, { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (isEnoent(error))
|
|
21
|
+
return [];
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
const roots = [];
|
|
25
|
+
for (const folder of folders) {
|
|
26
|
+
if (!folder.isDirectory() || !/^[a-z0-9]+(?:[-.][a-z0-9]+)*$/.test(folder.name) || folder.name.length > 64)
|
|
27
|
+
continue;
|
|
28
|
+
const packagesDir = path.join(workspaceRoot, folder.name, 'package', 'sha256');
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
await persistentState.assertOwnedPath({ workspaceRoot, entryPath: path.relative(workspaceRoot, packagesDir) });
|
|
32
|
+
entries = await readdir(packagesDir, { withFileTypes: true });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (!entry.isDirectory() || !INSTALLED_PLUGIN_DIGEST_PATTERN.test(entry.name))
|
|
39
|
+
continue;
|
|
40
|
+
try {
|
|
41
|
+
const packageRoot = await assertContainedOnDisk(workspaceRoot, path.relative(workspaceRoot, path.join(packagesDir, entry.name)));
|
|
42
|
+
roots.push({ localId: folder.name, archiveDigest: entry.name, packageRoot });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// One corrupt package never hides another plugin. Memory-only folders are not installs.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return roots;
|
|
50
|
+
}
|
|
51
|
+
/** Only a genuinely missing store is empty; other filesystem faults propagate unchanged. */
|
|
52
|
+
function isEnoent(error) {
|
|
53
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=installed-package-roots.js.map
|
|
@@ -1,57 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file `parseAgentPluginManifest()` / `parseAgentPluginMcpConfig()` — the Agent Plugins v1.0.0
|
|
3
|
-
* `plugin.json` / `mcp.json` grammar (agent-plugins.org/specification), verified against the live
|
|
4
|
-
* spec rather than inferred from an example manifest.
|
|
5
|
-
*
|
|
6
|
-
* Deliberately a SEPARATE, smaller validator from `src/features/plugin-runtime/manifest.ts`'s
|
|
7
|
-
* `validateManifest()`. That file validates `.host-plugin`'s own tarball format — `integrity`
|
|
8
|
-
* (per-file SHA-256), `sdkRange`, `tier`, an exactly-three-token capability vocabulary, a single
|
|
9
|
-
* `content.entry.beforeSave` hook. This file validates the OPEN standard's `plugin.json`, which
|
|
10
|
-
* shares none of those fields and defines no execution/trust/capability model at all — see this
|
|
11
|
-
* feature's own header comment in `install.ts` for why the two formats cannot be unified.
|
|
12
|
-
*
|
|
13
|
-
* Pinned v1.0.0 rules from agent-plugins.org/specification:
|
|
14
|
-
* - `plugin.json`'s `name`: 1-64 chars, lowercase alphanumeric/hyphen/period only, must start and
|
|
15
|
-
* end alphanumeric, no `--` or `..` consecutive delimiters (§5.5). Valid: `my-plugin`,
|
|
16
|
-
* `acme.tools`, `lint3r`, `a`. Invalid: `My-Plugin`, `-start`, `has--double`.
|
|
17
|
-
* - `plugin.json`'s `$schema` MUST equal `https://agent-plugins.org/schemas/1.0.0/plugin.schema.json`
|
|
18
|
-
* for this pinned version; `mcp.json`'s MUST equal
|
|
19
|
-
* `https://agent-plugins.org/schemas/1.0.0/mcp.schema.json`.
|
|
20
|
-
* - Unknown top-level `plugin.json` fields produce warnings but do not block loading — this module
|
|
21
|
-
* returns them as `warnings`, never as a rejection reason.
|
|
22
|
-
* - `mcp.json`'s top-level shape is `{ "$schema": ..., "mcpServers": { "<server-id>": {...} } }` —
|
|
23
|
-
* `mcpServers` MUST be an object whose member names identify servers.
|
|
24
|
-
* - Each server entry MUST carry a `type` discriminator matching one of three CLOSED variants.
|
|
25
|
-
* Launching requires the actual transport config, not just a server id:
|
|
26
|
-
* - `stdio`: requires `command` (non-empty string); optional `args` (string[]), `env`
|
|
27
|
-
* (string-valued object — MUST NOT set `PLUGIN_ROOT`/`PLUGIN_DATA`, which the spec reserves),
|
|
28
|
-
* `cwd` (plugin-relative path).
|
|
29
|
-
* - `streamable-http` / `sse`: requires `url` (non-empty string); optional `headers`
|
|
30
|
-
* (string-valued object).
|
|
31
|
-
* - **The spec defines NO auth-related field on a server entry at all.** Verbatim from
|
|
32
|
-
* `/specification`: "Agent Plugins v1 defines no OAuth configuration or portable
|
|
33
|
-
* credential-reference fields. Authorization discovery, user interaction, and credential storage
|
|
34
|
-
* are client-managed." A plugin therefore cannot declare "this server needs OAuth" using any
|
|
35
|
-
* spec-defined field — the host is the client the spec defers that decision to. This module accepts
|
|
36
|
-
* the host-specific metadata in `plugin.json`'s `extensions.<host-namespace>.mcpServers[serverId]`, keeping
|
|
37
|
-
* `mcp.json` transport entries closed per §7.2.1. Hosts may translate legacy auth and sign-in defaults in MCP
|
|
38
|
-
* entries through readServerMetadata for existing packages, but those cannot supply operator read trust.
|
|
39
|
-
* `authMode: "oauth" | "none"` selects client auth; absent defaults to "none".
|
|
40
|
-
* `defaultTools: { allow?, write?, read? }` names sign-in grants and reviewed read tools.
|
|
41
|
-
* `write` requires `allow` and must be its subset; `read` must be a subset when `allow` is
|
|
42
|
-
* present. A read-only declaration without `allow` leaves sign-in grants unchanged.
|
|
43
|
-
* Remote hints may veto a read declaration but never supply it. A third,
|
|
44
|
-
* `tokenAuth: { helpUrl, probeUrl }`, offers a pasted access token as the sign-in fallback
|
|
45
|
-
* (`access-token-tool.ts`). A fourth, `renamedTools: { oldName: newName }`, carries saved tool
|
|
46
|
-
* selections across a vendor's tool rename (`apply-tool-renames.ts`). A malformed per-server entry
|
|
47
|
-
* (wrong/missing `type`, missing required field) does not fail the whole file — see
|
|
48
|
-
* `ParseAgentPluginMcpConfigResult`'s own doc for the fail-open contract this preserves.
|
|
49
|
-
*
|
|
50
|
-
* Architectural role:
|
|
51
|
-
* Pure, no-I/O parsing over an already-`JSON.parse`d value — mirrors `plugin-runtime/manifest.ts`'s
|
|
52
|
-
* own "receives a JS value, never a file path or raw bytes" contract. `install.ts` is the only
|
|
53
|
-
* caller that reads the file and hands the parsed value here.
|
|
54
|
-
*/
|
|
55
1
|
export { validatePluginManifest, validateMcpManifest } from '../manifest.js';
|
|
56
2
|
export type { PluginManifest, McpManifest, McpServerEntry } from '../manifest.js';
|
|
57
3
|
import type { PluginManifestAuthor } from '../manifest.js';
|
|
@@ -70,6 +16,9 @@ export interface AgentPluginManifest {
|
|
|
70
16
|
readonly license?: (string) | undefined;
|
|
71
17
|
readonly keywords?: (readonly string[]) | undefined;
|
|
72
18
|
readonly extensions?: (Readonly<Record<string, unknown>>) | undefined;
|
|
19
|
+
/** Normalized publisher identity, independent of name/storage key and version. */
|
|
20
|
+
readonly id?: string | undefined;
|
|
21
|
+
readonly provides?: readonly string[] | undefined;
|
|
73
22
|
}
|
|
74
23
|
export type ParseAgentPluginManifestResult = {
|
|
75
24
|
readonly ok: true;
|
|
@@ -89,6 +38,8 @@ export type ParseAgentPluginManifestResult = {
|
|
|
89
38
|
*/
|
|
90
39
|
export declare function parseAgentPluginManifest({ value }: {
|
|
91
40
|
readonly value: unknown;
|
|
41
|
+
}, { identityNamespace }?: {
|
|
42
|
+
readonly identityNamespace?: string;
|
|
92
43
|
}): ParseAgentPluginManifestResult;
|
|
93
44
|
/**
|
|
94
45
|
* The three closed transport variants `mcp.schema.json` v1.0.0 defines. See this file's header for
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
* own "receives a JS value, never a file path or raw bytes" contract. `install.ts` is the only
|
|
53
53
|
* caller that reads the file and hands the parsed value here.
|
|
54
54
|
*/
|
|
55
|
+
import { readPluginIdentity, validatePluginIdentity } from "@jini-ai/core/plugin-directory";
|
|
55
56
|
export { validatePluginManifest, validateMcpManifest } from '../manifest.js';
|
|
56
57
|
/**
|
|
57
58
|
* The one plugin.json schema version this loader understands. A manifest declaring any other
|
|
@@ -84,13 +85,6 @@ function isJsonObject(value) {
|
|
|
84
85
|
function coerceOptionalString(value) {
|
|
85
86
|
return typeof value === "string" ? value : undefined;
|
|
86
87
|
}
|
|
87
|
-
/**
|
|
88
|
-
* `value` narrowed to a `string[]` (non-string entries dropped), or `undefined` when `value`
|
|
89
|
-
* itself isn't an array — `keywords`'s own coercion.
|
|
90
|
-
*/
|
|
91
|
-
function coerceStringArray(value) {
|
|
92
|
-
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : undefined;
|
|
93
|
-
}
|
|
94
88
|
function parseManifestAuthor(value) {
|
|
95
89
|
if (typeof value === 'string')
|
|
96
90
|
return value; // Existing installed manifests may carry a string.
|
|
@@ -127,7 +121,7 @@ function resolveManifestName(raw) {
|
|
|
127
121
|
* `plugin-runtime/discovery.ts`).
|
|
128
122
|
* @complexity O(k + n) time and space in the input keys and keyword entries.
|
|
129
123
|
*/
|
|
130
|
-
export function parseAgentPluginManifest({ value }) {
|
|
124
|
+
export function parseAgentPluginManifest({ value }, { identityNamespace = "tovu" } = {}) {
|
|
131
125
|
if (!isJsonObject(value))
|
|
132
126
|
return { ok: false, errors: ["plugin.json must be a JSON object"] };
|
|
133
127
|
const raw = value;
|
|
@@ -145,15 +139,20 @@ export function parseAgentPluginManifest({ value }) {
|
|
|
145
139
|
if (!KNOWN_MANIFEST_KEYS.has(key))
|
|
146
140
|
warnings.push(`unrecognized plugin.json field '${key}' (ignored per spec)`);
|
|
147
141
|
}
|
|
142
|
+
errors.push(...validatePluginIdentity({ manifest: raw, namespace: identityNamespace }).errors);
|
|
148
143
|
if (errors.length > 0)
|
|
149
144
|
return { ok: false, errors };
|
|
145
|
+
const identity = readPluginIdentity({ manifest: raw, namespace: identityNamespace });
|
|
146
|
+
warnings.push(...identity.warnings);
|
|
150
147
|
const manifest = {
|
|
151
148
|
name: nameResult.name,
|
|
149
|
+
id: identity.id,
|
|
150
|
+
provides: identity.provides,
|
|
152
151
|
version: coerceOptionalString(raw.version),
|
|
153
152
|
description: coerceOptionalString(raw.description),
|
|
154
153
|
author: parseManifestAuthor(raw.author),
|
|
155
154
|
license: coerceOptionalString(raw.license),
|
|
156
|
-
keywords:
|
|
155
|
+
keywords: raw.keywords === undefined ? undefined : identity.keywords,
|
|
157
156
|
...(isJsonObject(raw.extensions) ? { extensions: raw.extensions } : {}),
|
|
158
157
|
};
|
|
159
158
|
return { ok: true, manifest, warnings };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { AgentPluginLifecyclePorts } from './ports.js';
|
|
2
2
|
/** Reuses the platform lock with injected effects, never age-stealing a living migration. */
|
|
3
3
|
export declare function createPersistentStateModule(ports: AgentPluginLifecyclePorts, _optional?: Record<string, never>): {
|
|
4
|
+
withIdentityLock: <T>(required: {
|
|
5
|
+
workspaceRoot: string;
|
|
6
|
+
run(): Promise<T>;
|
|
7
|
+
}, _optional?: {}) => Promise<T>;
|
|
4
8
|
withPluginLock: <T>(required: {
|
|
5
9
|
workspaceRoot: string;
|
|
6
10
|
pluginId: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createPluginMemory, migratePluginLayout, assertPluginStatePath, withPluginStateLock } from '../../core/persistent-state.js';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { withFileLock } from '@jini-ai/platform/fs/file-lock';
|
|
3
4
|
import { createPackagePathsModule } from './package-paths.js';
|
|
4
5
|
import { parseAgentPluginManifest } from './manifest.js';
|
|
@@ -16,6 +17,7 @@ export function createPersistentStateModule(ports, _optional = {}) {
|
|
|
16
17
|
}),
|
|
17
18
|
};
|
|
18
19
|
return {
|
|
20
|
+
withIdentityLock: (required, _optional = {}) => effects.withLock({ lockPath: path.join(required.workspaceRoot, '.publisher-identity.lock'), run: required.run }),
|
|
19
21
|
withPluginLock: (required, optional = {}) => withPluginStateLock({ ...effects, ...required }, optional),
|
|
20
22
|
memory: (required, optional = {}) => createPluginMemory({ ...effects, ...required }, optional),
|
|
21
23
|
assertOwnedPath: (required, optional = {}) => assertPluginStatePath({ ...effects, ...required }, optional),
|
|
@@ -52,6 +52,12 @@ export interface AgentPluginOutboundGuardPort {
|
|
|
52
52
|
}): Promise<void>;
|
|
53
53
|
}
|
|
54
54
|
export interface AgentPluginLifecyclePorts {
|
|
55
|
+
/** Optional host admission serializes cross-kind identity checking and publication. */
|
|
56
|
+
readonly identityAdmission?: (<T>(required: {
|
|
57
|
+
id: string;
|
|
58
|
+
localId: string;
|
|
59
|
+
run: () => Promise<T>;
|
|
60
|
+
}, optional?: Record<string, never>) => Promise<T>) | undefined;
|
|
55
61
|
readonly onEvent?: ((required: {
|
|
56
62
|
readonly event: string;
|
|
57
63
|
readonly message: string;
|
|
@@ -88,6 +94,6 @@ export interface AgentPluginLifecyclePorts {
|
|
|
88
94
|
readonly outboundGuard: AgentPluginOutboundGuardPort;
|
|
89
95
|
}
|
|
90
96
|
/** Application policy/effects are required; observers and metadata translators are optional. */
|
|
91
|
-
export type AgentPluginLifecycleRequired = Omit<AgentPluginLifecyclePorts, 'onEvent' | 'readServerMetadata' | 'migrateBeforeSeed' | 'formatBundledUninstallRecovery'>;
|
|
92
|
-
export type AgentPluginLifecycleOptional = Pick<AgentPluginLifecyclePorts, 'onEvent' | 'readServerMetadata' | 'migrateBeforeSeed' | 'formatBundledUninstallRecovery'>;
|
|
97
|
+
export type AgentPluginLifecycleRequired = Omit<AgentPluginLifecyclePorts, 'onEvent' | 'readServerMetadata' | 'migrateBeforeSeed' | 'formatBundledUninstallRecovery' | 'identityAdmission'>;
|
|
98
|
+
export type AgentPluginLifecycleOptional = Pick<AgentPluginLifecyclePorts, 'onEvent' | 'readServerMetadata' | 'migrateBeforeSeed' | 'formatBundledUninstallRecovery' | 'identityAdmission'>;
|
|
93
99
|
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { listInstalledPackageRoots, INSTALLED_PLUGIN_DIGEST_PATTERN } from "./installed-package-roots.js";
|
|
1
2
|
/**
|
|
2
3
|
* @file Resolves a run's pinned Agent Plugin refs (`pluginRefIds` — `run-start-context.ts`'s
|
|
3
4
|
* decoded `contextRef` field, itself sourced from the composer's `pluginRefId` chips,
|
|
@@ -65,13 +66,11 @@ import { createCapabilityProjectionModule } from "./capability-projection.js";
|
|
|
65
66
|
function buildModule(ports) {
|
|
66
67
|
const persistentState = createPersistentStateModule(ports);
|
|
67
68
|
const { assertContainedOnDisk } = createPackagePathsModule(ports);
|
|
68
|
-
const readdir = ports.filesystem.readdir.bind(ports.filesystem);
|
|
69
69
|
const stat = ports.filesystem.stat.bind(ports.filesystem);
|
|
70
70
|
const { resolveAgentPluginActivation } = createActivationModule(ports);
|
|
71
71
|
const { preferBundledAgentPluginDigests, readBundledAgentPluginDigests } = createBundledDigestsModule(ports);
|
|
72
72
|
const { indexInstalledRoot } = createInstallModule(ports);
|
|
73
73
|
const { readInstalledSkillMarkdown } = createCapabilityProjectionModule(ports);
|
|
74
|
-
const SHA256_DIGEST_DIRNAME_PATTERN = /^[a-f0-9]{64}$/;
|
|
75
74
|
/** Reads the delivery mode for this process. An unset or unrecognised value is `inject` — an A/B
|
|
76
75
|
* affordance must never be able to change production behaviour by typo. */
|
|
77
76
|
function resolveAgentPluginDeliveryMode() { return ports.deliveryMode; }
|
|
@@ -147,40 +146,15 @@ function buildModule(ports) {
|
|
|
147
146
|
* re-walking `packages/sha256/*` a second, less-validated way keeps the digest-directory-name check
|
|
148
147
|
* and the per-digest failure isolation in exactly one place. */
|
|
149
148
|
async function listInstalledPlugins(workspaceRoot) {
|
|
150
|
-
let folders;
|
|
151
|
-
try {
|
|
152
|
-
folders = await readdir(workspaceRoot, { withFileTypes: true });
|
|
153
|
-
}
|
|
154
|
-
catch (error) {
|
|
155
|
-
if (isEnoent(error))
|
|
156
|
-
return [];
|
|
157
|
-
throw error;
|
|
158
|
-
}
|
|
159
149
|
const installed = [];
|
|
160
|
-
for (const
|
|
161
|
-
if (!folder.isDirectory() || !/^[a-z0-9]+(?:[-.][a-z0-9]+)*$/.test(folder.name) || folder.name.length > 64)
|
|
162
|
-
continue;
|
|
163
|
-
const packagesDir = path.join(workspaceRoot, folder.name, "package", "sha256");
|
|
164
|
-
let entries;
|
|
150
|
+
for (const root of await listInstalledPackageRoots({ ports, workspaceRoot })) {
|
|
165
151
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
152
|
+
const plugin = await indexInstalledRoot(root.packageRoot, root.archiveDigest);
|
|
153
|
+
if (plugin.pluginId === root.localId)
|
|
154
|
+
installed.push(plugin);
|
|
168
155
|
}
|
|
169
156
|
catch {
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
for (const entry of entries) {
|
|
173
|
-
if (!entry.isDirectory() || !SHA256_DIGEST_DIRNAME_PATTERN.test(entry.name))
|
|
174
|
-
continue;
|
|
175
|
-
try {
|
|
176
|
-
const packageRoot = await assertContainedOnDisk(workspaceRoot, path.relative(workspaceRoot, path.join(packagesDir, entry.name)));
|
|
177
|
-
const plugin = await indexInstalledRoot(packageRoot, entry.name);
|
|
178
|
-
if (plugin.pluginId === folder.name)
|
|
179
|
-
installed.push(plugin);
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
// One corrupt package never hides another plugin. Memory-only folders are not installs.
|
|
183
|
-
}
|
|
157
|
+
// One corrupt package never hides another plugin. Memory-only folders are not installs.
|
|
184
158
|
}
|
|
185
159
|
}
|
|
186
160
|
return installed;
|
|
@@ -208,7 +182,7 @@ function buildModule(ports) {
|
|
|
208
182
|
* @complexity One `stat`.
|
|
209
183
|
*/
|
|
210
184
|
async function isInstalledDigestPresent(packagesDir, archiveDigest) {
|
|
211
|
-
if (!
|
|
185
|
+
if (!INSTALLED_PLUGIN_DIGEST_PATTERN.test(archiveDigest))
|
|
212
186
|
return false;
|
|
213
187
|
try {
|
|
214
188
|
return (await stat(path.join(packagesDir, archiveDigest))).isDirectory();
|
|
@@ -217,9 +191,6 @@ function buildModule(ports) {
|
|
|
217
191
|
return false;
|
|
218
192
|
}
|
|
219
193
|
}
|
|
220
|
-
function isEnoent(error) {
|
|
221
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
222
|
-
}
|
|
223
194
|
/**
|
|
224
195
|
* Resolves one `pluginRefId`, applying the zero/one/many-match rules described in this module's
|
|
225
196
|
* own header.
|
|
@@ -67,6 +67,8 @@ export interface AgentPluginSearchCandidate {
|
|
|
67
67
|
readonly summary?: string | undefined;
|
|
68
68
|
readonly description?: (string) | undefined;
|
|
69
69
|
readonly keywords: readonly string[];
|
|
70
|
+
readonly id?: string | undefined;
|
|
71
|
+
readonly provides?: readonly string[] | undefined;
|
|
70
72
|
/** Whether this workspace currently has the plugin enabled — reflects `activation.ts`'s
|
|
71
73
|
* absent-means-active default, same as everywhere else this codebase reads activation state. */
|
|
72
74
|
readonly enabled: boolean;
|
package/dist/lifecycle/search.js
CHANGED
|
@@ -18,8 +18,8 @@ function buildModule(ports) {
|
|
|
18
18
|
* plain, independently-readable list rather than interleaved with the scoring loop itself. */
|
|
19
19
|
function fieldHaystacks(candidate) {
|
|
20
20
|
return [
|
|
21
|
-
{ text: candidate.pluginId.toLowerCase(), weight: ID_FIELD_WEIGHT },
|
|
22
|
-
{ text: candidate.keywords.join(" ").toLowerCase(), weight: KEYWORD_FIELD_WEIGHT },
|
|
21
|
+
{ text: `${candidate.pluginId} ${candidate.id ?? ""}`.toLowerCase(), weight: ID_FIELD_WEIGHT },
|
|
22
|
+
{ text: [...candidate.keywords, ...(candidate.provides ?? [])].join(" ").toLowerCase(), weight: KEYWORD_FIELD_WEIGHT },
|
|
23
23
|
{ text: (candidate.description ?? "").toLowerCase(), weight: DESCRIPTION_FIELD_WEIGHT },
|
|
24
24
|
{
|
|
25
25
|
text: candidate.skills.map((skill) => `${skill.name} ${skill.summary}`).join(" ").toLowerCase(),
|
|
@@ -6,6 +6,8 @@ export interface InstalledAgentPluginSkill {
|
|
|
6
6
|
readonly skillPath: string;
|
|
7
7
|
}
|
|
8
8
|
export interface InstalledAgentPlugin {
|
|
9
|
+
readonly id?: string | undefined;
|
|
10
|
+
readonly provides?: readonly string[] | undefined;
|
|
9
11
|
readonly pluginId: string;
|
|
10
12
|
readonly version?: (string) | undefined;
|
|
11
13
|
/** the host extension namespace in `plugin.json`: `displayName` — the human-facing title when the id alone reads
|
package/dist/manifest.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface PluginManifestAuthor {
|
|
|
20
20
|
readonly email?: string;
|
|
21
21
|
readonly url?: string;
|
|
22
22
|
}
|
|
23
|
+
export type { PluginIdentityExtension } from "@jini-ai/core/plugin-directory";
|
|
23
24
|
export interface PluginManifest {
|
|
24
25
|
readonly $schema?: string;
|
|
25
26
|
/** 1-64 chars, lowercase alphanumeric/dots/hyphens, no leading/trailing/doubled separators. */
|
|
@@ -60,6 +61,8 @@ export declare function isMcpManifest(value: unknown): value is McpManifest;
|
|
|
60
61
|
/** Object-argument facade for the legacy structural validator. */
|
|
61
62
|
export declare function validatePluginManifest(required: {
|
|
62
63
|
readonly value: unknown;
|
|
64
|
+
}, optional?: {
|
|
65
|
+
readonly identityNamespace?: string;
|
|
63
66
|
}): boolean;
|
|
64
67
|
/** Object-argument facade for the legacy structural validator. */
|
|
65
68
|
export declare function validateMcpManifest(required: {
|
package/dist/manifest.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validatePluginIdentity } from "@jini-ai/core/plugin-directory";
|
|
1
2
|
/**
|
|
2
3
|
* Types for the Agent Plugins 1.0.0 open standard (published by a Technical Steering Committee
|
|
3
4
|
* with maintainers from Amazon, Cursor, Google, Microsoft, OpenAI, and Vercel). A plugin is a
|
|
@@ -28,8 +29,8 @@ export function isMcpManifest(value) {
|
|
|
28
29
|
return typeof servers === 'object' && servers !== null && !Array.isArray(servers);
|
|
29
30
|
}
|
|
30
31
|
/** Object-argument facade for the legacy structural validator. */
|
|
31
|
-
export function validatePluginManifest(required) {
|
|
32
|
-
return isPluginManifest(required.value);
|
|
32
|
+
export function validatePluginManifest(required, optional = {}) {
|
|
33
|
+
return isPluginManifest(required.value) && validatePluginIdentity({ manifest: required.value, namespace: optional.identityNamespace ?? "tovu" }).errors.length === 0;
|
|
33
34
|
}
|
|
34
35
|
/** Object-argument facade for the legacy structural validator. */
|
|
35
36
|
export function validateMcpManifest(required) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/agent-plugins",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Support for Agent Plugins, the open, vendor-neutral spec (v1.0.0, published 2026-08-06, Technical Steering Committee with maintainers from Amazon, Cursor, Google, Microsoft, OpenAI, and Vercel) for bundling Agent Skills and MCP servers into one portable directory: PluginManifest/McpManifest types + structural validators on the package root, plus the bundled plugins themselves, each on a subpath named for itself (./ui-ux-design). Extracted 2026-08-18 from @jini-ai/plugins, which now only reserves the unrelated, unimplemented ./host format.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -89,21 +89,18 @@
|
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@vitest/coverage-v8": "^2.1.9",
|
|
91
91
|
"yazl": "^3.3.1",
|
|
92
|
-
"@jini-ai/core": "^0.4.
|
|
92
|
+
"@jini-ai/core": "^0.4.2",
|
|
93
93
|
"@jini-ai/platform": "^0.5.0"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
96
|
"yauzl": "^3.4.0",
|
|
97
|
-
"@jini-ai/core": "^0.4.
|
|
98
|
-
"@jini-ai/platform": "^0.5.
|
|
97
|
+
"@jini-ai/core": "^0.4.3",
|
|
98
|
+
"@jini-ai/platform": "^0.5.3"
|
|
99
99
|
},
|
|
100
100
|
"peerDependenciesMeta": {
|
|
101
101
|
"yauzl": {
|
|
102
102
|
"optional": true
|
|
103
103
|
},
|
|
104
|
-
"@jini-ai/core": {
|
|
105
|
-
"optional": true
|
|
106
|
-
},
|
|
107
104
|
"@jini-ai/platform": {
|
|
108
105
|
"optional": true
|
|
109
106
|
}
|
package/ui-ux-design/plugin.json
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "ui-ux-design",
|
|
4
|
-
"description": "UI/UX design, interface-design-system, accessibility, and shadcn/ui component skills bundled as one portable Agent Plugin."
|
|
4
|
+
"description": "UI/UX design, interface-design-system, accessibility, and shadcn/ui component skills bundled as one portable Agent Plugin.",
|
|
5
|
+
"extensions": {
|
|
6
|
+
"tovu": {
|
|
7
|
+
"id": "jini/ui-ux-design",
|
|
8
|
+
"provides": [
|
|
9
|
+
"ui-design"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"design",
|
|
15
|
+
"ui",
|
|
16
|
+
"ux",
|
|
17
|
+
"ui-design"
|
|
18
|
+
]
|
|
5
19
|
}
|