@jini-ai/agent-plugins 0.4.1 → 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.
Files changed (48) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/core/persistent-state.d.ts +25 -0
  3. package/core/persistent-state.js +260 -0
  4. package/dist/lifecycle/activation.js +38 -166
  5. package/dist/lifecycle/bundled-digests.d.ts +6 -0
  6. package/dist/lifecycle/bundled-digests.js +160 -0
  7. package/dist/lifecycle/bundled-source-archive.d.ts +3 -0
  8. package/dist/lifecycle/bundled-source-archive.js +74 -0
  9. package/dist/lifecycle/contribution-loader.d.ts +102 -0
  10. package/dist/lifecycle/contribution-loader.js +92 -0
  11. package/dist/lifecycle/fetch-archive.d.ts +1 -2
  12. package/dist/lifecycle/index.d.ts +24 -4
  13. package/dist/lifecycle/index.js +11 -0
  14. package/dist/lifecycle/install-from-url.d.ts +2 -5
  15. package/dist/lifecycle/install-from-url.js +2 -5
  16. package/dist/lifecycle/install.d.ts +58 -3
  17. package/dist/lifecycle/install.js +301 -23
  18. package/dist/lifecycle/installed-package-roots.d.ts +16 -0
  19. package/dist/lifecycle/installed-package-roots.js +55 -0
  20. package/dist/lifecycle/layout.js +1 -1
  21. package/dist/lifecycle/manifest.d.ts +5 -60
  22. package/dist/lifecycle/manifest.js +12 -20
  23. package/dist/lifecycle/package-paths.d.ts +2 -0
  24. package/dist/lifecycle/package-paths.js +62 -0
  25. package/dist/lifecycle/persistent-state.d.ts +5 -1
  26. package/dist/lifecycle/persistent-state.js +3 -1
  27. package/dist/lifecycle/ports.d.ts +20 -2
  28. package/dist/lifecycle/resolve-agent-plugin-refs.d.ts +13 -1
  29. package/dist/lifecycle/resolve-agent-plugin-refs.js +150 -40
  30. package/dist/lifecycle/retire-bundled.d.ts +50 -1
  31. package/dist/lifecycle/retire-bundled.js +9 -2
  32. package/dist/lifecycle/search.d.ts +65 -0
  33. package/dist/lifecycle/search.js +31 -2
  34. package/dist/lifecycle/seed-bundled.d.ts +18 -1
  35. package/dist/lifecycle/seed-bundled.js +131 -3
  36. package/dist/lifecycle/set-enabled.d.ts +7 -0
  37. package/dist/lifecycle/set-enabled.js +5 -0
  38. package/dist/lifecycle/trusted-plugin-files.d.ts +38 -0
  39. package/dist/lifecycle/trusted-plugin-files.js +18 -0
  40. package/dist/lifecycle/types.d.ts +14 -0
  41. package/dist/lifecycle/uninstall.d.ts +29 -0
  42. package/dist/lifecycle/uninstall.js +204 -4
  43. package/dist/lifecycle/yauzl-archive-reader.d.ts +0 -1
  44. package/dist/manifest.d.ts +3 -0
  45. package/dist/manifest.js +3 -2
  46. package/package.json +22 -8
  47. package/persistent-state.js +2 -260
  48. package/ui-ux-design/plugin.json +15 -1
@@ -1,260 +1,2 @@
1
- /** Build-free Node entry: effects and policy belong to the host; no product namespaces here. */
2
- import path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
4
-
5
- const SAFE_ID = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/;
6
- const DIGEST = /^[a-f0-9]{64}$/;
7
- export const DEFAULT_PLUGIN_MEMORY_LIMITS = Object.freeze({ learned: 1024 * 1024, notes: 16 * 1024, files: 128 });
8
- export function pluginStatePaths({ workspaceRoot, pluginId }, _optional = {}) {
9
- if (!path.isAbsolute(workspaceRoot) || !SAFE_ID.test(pluginId) || pluginId.length > 64 || ['staging', 'packages', 'memory', 'data'].includes(pluginId)) throw new Error('Invalid plugin state root or id');
10
- const root = path.join(workspaceRoot, pluginId);
11
- return { root, packages: path.join(root, 'package', 'sha256'), data: path.join(root, 'data'),
12
- learned: path.join(root, 'memory', 'learned'), notes: path.join(root, 'memory', 'notes') };
13
- }
14
- /** Containment plus ownership: a sibling plugin inside the workspace is still outside this one. */
15
- export async function assertPluginStatePath({ filesystem: fs, contain, workspaceRoot, entryPath }, _optional = {}) {
16
- const absolute = await contain({ root: workspaceRoot, entryPath });
17
- const realWorkspace = await fs.realpath(workspaceRoot);
18
- const relative = path.relative(realWorkspace, absolute);
19
- let current = realWorkspace;
20
- for (const segment of relative.split(path.sep)) {
21
- current = path.join(current, segment);
22
- try { if (await fs.realpath(current) !== current) throw new Error('Plugin state ancestors must not be symlinks'); }
23
- catch (error) { if (code(error) === 'ENOENT') break; throw error; }
24
- }
25
- return absolute;
26
- }
27
- function code(error) { return error && typeof error === 'object' ? error.code : undefined; }
28
-
29
- /** Lock files are workspace scratch so uninstall cannot delete a lock another process holds. */
30
- export async function withPluginStateLock({ filesystem: fs, contain, workspaceRoot, pluginId, withLock, run }, _optional = {}) {
31
- pluginStatePaths({ workspaceRoot, pluginId });
32
- await fs.mkdir(workspaceRoot, { recursive: true, mode: 0o700 });
33
- const lockPath = await assertPluginStatePath({ filesystem: fs, contain, workspaceRoot, entryPath: `staging/.plugin-state-${pluginId}.lock` });
34
- return withLock({ lockPath, run });
35
- }
36
- /** Each instance is bound to one plugin. No read/write method accepts an id or arbitrary root. */
37
- export function createPluginMemory({ filesystem: fs, contain, workspaceRoot, pluginId, withLock }, optional = {}) {
38
- const paths = pluginStatePaths({ workspaceRoot, pluginId });
39
- const limits = { ...DEFAULT_PLUGIN_MEMORY_LIMITS, ...optional.limits };
40
- for (const kind of ['learned', 'notes', 'files']) {
41
- if (!Number.isSafeInteger(limits[kind]) || limits[kind] <= 0 || limits[kind] > DEFAULT_PLUGIN_MEMORY_LIMITS[kind]) throw new Error('Memory caps must be positive and cannot exceed host ceilings');
42
- }
43
- function checkKind(kind) { if (kind !== 'learned' && kind !== 'notes') throw new Error('Invalid memory kind'); }
44
- async function root(kind, create = false) {
45
- checkKind(kind);
46
- if (create) await fs.mkdir(workspaceRoot, { recursive: true, mode: 0o700 });
47
- await assertPluginStatePath({ filesystem: fs, contain, workspaceRoot, entryPath: path.relative(workspaceRoot, paths[kind]) });
48
- if (create) await fs.mkdir(paths[kind], { recursive: true, mode: 0o700 });
49
- return paths[kind];
50
- }
51
- async function readText(filename, cap) {
52
- // A bounded read protects against a concurrent file growing after stat. Invalid UTF-8 is refused.
53
- const handle = await fs.open(filename, 'r');
54
- try {
55
- if (!(await handle.stat()).isFile()) throw new Error('Memory must contain regular UTF-8 files');
56
- const bytes = Buffer.alloc(cap + 1);
57
- let size = 0;
58
- while (size < bytes.length) {
59
- const result = await handle.read(bytes, size, bytes.length - size, size);
60
- if (result.bytesRead === 0) break;
61
- size += result.bytesRead;
62
- }
63
- if (size > cap) throw new Error('Memory byte cap exceeded');
64
- const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, size));
65
- if (text.includes('\0')) throw new Error('Memory must be UTF-8 text');
66
- return text;
67
- } finally { await handle.close(); }
68
- }
69
- async function list({ kind }, _optional = {}) {
70
- const base = await root(kind).catch(error => { if (code(error) === 'ENOENT') return undefined; throw error; });
71
- if (!base) return [];
72
- const files = [];
73
- let total = 0, entriesSeen = 0;
74
- async function walk(prefix) {
75
- let entries;
76
- try { entries = await fs.readdir(path.join(base, prefix), { withFileTypes: true }); }
77
- catch (error) { if (code(error) === 'ENOENT' && prefix === '') return; throw error; }
78
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
79
- if (++entriesSeen > limits.files * 4) throw new Error('Memory entry cap exceeded');
80
- const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
81
- const filename = await contain({ root: base, entryPath: relativePath });
82
- if (entry.isDirectory()) await walk(relativePath);
83
- else if (entry.isFile()) {
84
- if (files.length >= limits.files) throw new Error('Memory file cap exceeded');
85
- const text = await readText(filename, limits[kind] - total);
86
- total += Buffer.byteLength(text, 'utf8');
87
- files.push({ relativePath, text });
88
- } else throw new Error('Memory symlinks and special files are refused');
89
- }
90
- }
91
- await walk('');
92
- return files;
93
- }
94
- async function read({ kind, entryPath }, _optional = {}) {
95
- const base = await root(kind);
96
- return readText(await contain({ root: base, entryPath }), limits[kind]);
97
- }
98
- async function write({ kind, entryPath, text }, _optional = {}) {
99
- checkKind(kind);
100
- if (typeof text !== 'string' || text.includes('\0') || Buffer.from(text, 'utf8').toString('utf8') !== text) throw new Error('Memory must be valid UTF-8 text');
101
- if (Buffer.byteLength(text, 'utf8') > limits[kind]) throw new Error('Memory byte cap exceeded');
102
- // The same lock protects total-size accounting for all processes using this plugin.
103
- return withPluginStateLock({ filesystem: fs, contain, workspaceRoot, pluginId, withLock, run: async () => {
104
- const base = await root(kind, true);
105
- const destination = await contain({ root: base, entryPath });
106
- const normalized = path.relative(base, destination).split(path.sep).join('/');
107
- const files = await list({ kind });
108
- const existing = files.find(file => file.relativePath === normalized);
109
- const total = files.reduce((sum, file) => sum + Buffer.byteLength(file.text, 'utf8'), 0);
110
- if ((!existing && files.length >= limits.files) || total - Buffer.byteLength(existing?.text ?? '', 'utf8') + Buffer.byteLength(text, 'utf8') > limits[kind]) throw new Error('Memory cap exceeded');
111
- await fs.mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
112
- await contain({ root: base, entryPath });
113
- const temporary = path.join(path.dirname(destination), `.memory-${randomUUID()}.tmp`);
114
- const handle = await fs.open(temporary, 'wx', 0o600);
115
- try {
116
- try { await handle.writeFile(text, 'utf8'); await handle.sync(); } finally { await handle.close(); }
117
- await fs.rename(temporary, destination);
118
- } finally { await fs.rm(temporary, { force: true }); }
119
- return { relativePath: normalized, bytes: Buffer.byteLength(text, 'utf8') };
120
- } });
121
- }
122
- // The plugin-facing capability has no notes writer, even if an input tries to supply kind/id.
123
- return { list, read, writeNote: ({ entryPath, text }, optional = {}) => write({ kind: 'notes', entryPath, text }, optional),
124
- learned: { read: ({ entryPath }, optional = {}) => read({ kind: 'learned', entryPath }, optional),
125
- write: ({ entryPath, text }, optional = {}) => write({ kind: 'learned', entryPath, text }, optional) } };
126
- }
127
-
128
- /** Interrupted moves are resumable; refused legacy entries are kept in quarantine and reported. */
129
- export async function migratePluginLayout({ filesystem: fs, contain, workspaceRoot, parsePluginId, withLock, onEvent }, _optional = {}) {
130
- await fs.mkdir(workspaceRoot, { recursive: true, mode: 0o700 });
131
- // Containment returns canonical paths; relative moves must use the same root (e.g. /var -> /private/var).
132
- workspaceRoot = await fs.realpath(workspaceRoot);
133
- const marker = path.join(workspaceRoot, '.agent-plugin-layout-migrated');
134
- const emit = (event, message) => onEvent?.({ event, message });
135
- return withLock({ lockPath: `${marker}.lock`, run: async () => {
136
- await assertPluginStatePath({ filesystem: fs, contain, workspaceRoot, entryPath: path.basename(marker) });
137
- try { if (JSON.parse(await fs.readFile(marker, 'utf8')).layoutVersion === 2) return { complete: true, moved: 0 }; }
138
- catch (error) { if (code(error) !== 'ENOENT') emit('migration-marker-invalid', String(error)); }
139
- let complete = true, moved = 0;
140
- let quarantineRoot;
141
- async function entries(directory) {
142
- try { return await fs.readdir(directory, { withFileTypes: true }); }
143
- catch (error) { if (code(error) === 'ENOENT') return []; throw error; }
144
- }
145
- async function renameEntry({ source, destination, frozen }, _optional = {}) {
146
- await fs.mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
147
- const mode = frozen ? (await fs.stat(source)).mode & 0o777 : undefined;
148
- if (frozen) await fs.chmod(source, mode | 0o700);
149
- try { await fs.rename(source, destination); }
150
- catch (error) { if (frozen) await fs.chmod(source, mode).catch(() => undefined); throw error; }
151
- if (frozen) await fs.chmod(destination, mode);
152
- }
153
- async function quarantine({ source, reason, frozen = false }, _optional = {}) {
154
- // Validate ancestors, then rename the entry itself: even an unsafe symlink is preserved,
155
- // never followed into another plugin or outside the workspace.
156
- await assertPluginStatePath({ filesystem: fs, contain, workspaceRoot, entryPath: path.relative(workspaceRoot, path.dirname(source)) });
157
- if (!quarantineRoot) {
158
- const base = await assertPluginStatePath({ filesystem: fs, contain, workspaceRoot, entryPath: 'staging/legacy-quarantine' });
159
- await fs.mkdir(base, { recursive: true, mode: 0o700 });
160
- const candidate = path.join(base, `${new Date().toISOString().replaceAll(':', '-')}-${randomUUID()}`);
161
- // Exclusive creation prevents a retry or another boot from overwriting saved bytes.
162
- await fs.mkdir(candidate, { mode: 0o700 });
163
- quarantineRoot = candidate;
164
- }
165
- const destinationParent = await assertPluginStatePath({ filesystem: fs, contain, workspaceRoot,
166
- entryPath: path.relative(workspaceRoot, path.join(quarantineRoot, path.relative(workspaceRoot, path.dirname(source)))) });
167
- // Keep the native directory-entry name verbatim, even if it is not a valid portable package
168
- // path (e.g. a literal backslash on POSIX). The fresh quarantine tree has no existing leaf.
169
- const destination = path.join(destinationParent, path.basename(source));
170
- await renameEntry({ source, destination, frozen });
171
- emit('migration-quarantined', `${source} -> ${destination}: ${reason}`);
172
- }
173
- async function move(source, destination, frozen = false) {
174
- await contain({ root: workspaceRoot, entryPath: path.relative(workspaceRoot, source) });
175
- await assertPluginStatePath({ filesystem: fs, contain, workspaceRoot, entryPath: path.relative(workspaceRoot, destination) });
176
- let exists = false;
177
- try { await fs.stat(destination); exists = true; }
178
- catch (error) { if (code(error) !== 'ENOENT') throw error; }
179
- if (exists) {
180
- // The Layout B copy wins. This runs under the plugin lock, so no in-flight move is discarded.
181
- await quarantine({ source, frozen, reason: 'Migration destination already exists; Layout B copy kept' });
182
- return;
183
- }
184
- await renameEntry({ source, destination, frozen });
185
- moved++;
186
- emit('migration-moved', `${source} -> ${destination}`);
187
- }
188
- const oldPackages = path.join(workspaceRoot, 'packages', 'sha256');
189
- for (const entry of await entries(oldPackages)) {
190
- const source = path.join(oldPackages, entry.name);
191
- try {
192
- if (!DIGEST.test(entry.name) || !entry.isDirectory()) {
193
- await quarantine({ source, frozen: entry.isDirectory(), reason: 'Unrecognized legacy package entry' });
194
- continue;
195
- }
196
- let pluginId, paths;
197
- try {
198
- await contain({ root: workspaceRoot, entryPath: `packages/sha256/${entry.name}` });
199
- const manifestPath = await contain({ root: source, entryPath: 'plugin.json' });
200
- pluginId = parsePluginId({ value: JSON.parse(await fs.readFile(manifestPath, 'utf8')) });
201
- paths = pluginStatePaths({ workspaceRoot, pluginId });
202
- } catch (error) {
203
- // Missing/invalid manifests are refused entries; filesystem failures remain resumable.
204
- if (code(error) && !['ENOENT', 'EISDIR'].includes(code(error))) throw error;
205
- await quarantine({ source, frozen: true, reason: String(error) });
206
- continue;
207
- }
208
- await withPluginStateLock({ filesystem: fs, contain, workspaceRoot, pluginId, withLock, run: () => move(source, path.join(paths.packages, entry.name), true) });
209
- } catch (error) { complete = false; emit('migration-failed', `${entry.name}: ${String(error)}`); }
210
- }
211
- // Layout A memory and PLUGIN_DATA may exist without an installed package; migrate them too.
212
- for (const bucket of ['memory', 'data']) {
213
- for (const entry of await entries(path.join(workspaceRoot, bucket))) {
214
- const source = path.join(workspaceRoot, bucket, entry.name);
215
- try {
216
- if (!entry.isDirectory()) {
217
- await quarantine({ source, reason: 'Legacy state is not a directory' });
218
- continue;
219
- }
220
- let paths;
221
- try { paths = pluginStatePaths({ workspaceRoot, pluginId: entry.name }); }
222
- catch (error) { await quarantine({ source, reason: String(error) }); continue; }
223
- await withPluginStateLock({ filesystem: fs, contain, workspaceRoot, pluginId: entry.name, withLock, run: () => move(source, bucket === 'data' ? paths.data : path.join(paths.root, 'memory')) });
224
- } catch (error) { complete = false; emit('migration-failed', `${bucket}/${entry.name}: ${String(error)}`); }
225
- }
226
- }
227
- for (const folder of await entries(workspaceRoot)) {
228
- if (!folder.isDirectory() || !SAFE_ID.test(folder.name) || ['staging', 'packages', 'memory', 'data'].includes(folder.name)) continue;
229
- try {
230
- await withPluginStateLock({ filesystem: fs, contain, workspaceRoot, pluginId: folder.name, withLock, run: async () => {
231
- const paths = pluginStatePaths({ workspaceRoot, pluginId: folder.name });
232
- await assertPluginStatePath({ filesystem: fs, contain, workspaceRoot, entryPath: path.relative(workspaceRoot, paths.packages) });
233
- const digests = (await entries(paths.packages)).filter(digest => digest.isDirectory() && DIGEST.test(digest.name));
234
- for (const digest of digests) {
235
- const packageRoot = await assertPluginStatePath({ filesystem: fs, contain, workspaceRoot, entryPath: path.relative(workspaceRoot, path.join(paths.packages, digest.name)) });
236
- // Legacy install froze every package; a crash after rename can leave only its root writable.
237
- await fs.chmod(packageRoot, 0o555);
238
- }
239
- // Wait until legacy memory/data moves have finished before creating destinations for them.
240
- if (complete && digests.length) for (const directory of [paths.learned, paths.notes, paths.data]) {
241
- await assertPluginStatePath({ filesystem: fs, contain, workspaceRoot, entryPath: path.relative(workspaceRoot, directory) });
242
- await fs.mkdir(directory, { recursive: true, mode: 0o700 });
243
- }
244
- } });
245
- } catch (error) { complete = false; emit('migration-failed', `${folder.name}: ${String(error)}`); }
246
- }
247
- if (complete) {
248
- for (const directory of [oldPackages, path.dirname(oldPackages), path.join(workspaceRoot, 'memory'), path.join(workspaceRoot, 'data')]) {
249
- try { await fs.rmdir(directory); } catch (error) { if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(code(error))) throw error; }
250
- }
251
- const temporary = `${marker}.${randomUUID()}.tmp`;
252
- const handle = await fs.open(temporary, 'wx', 0o600);
253
- try { await handle.writeFile(JSON.stringify({ layoutVersion: 2 }), 'utf8'); await handle.sync(); }
254
- finally { await handle.close(); }
255
- try { await fs.rename(temporary, marker); } finally { await fs.rm(temporary, { force: true }); }
256
- emit('migration-complete', workspaceRoot);
257
- }
258
- return { complete, moved };
259
- } });
260
- }
1
+ // Build-free compatibility entry; lifecycle shares the core implementation.
2
+ export * from './core/persistent-state.js';
@@ -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
  }