@hydraharness/harness-host-plugin-inventory 0.1.1-rc.10

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/lib/index.js ADDED
@@ -0,0 +1,1375 @@
1
+ import { createRequire } from "node:module";
2
+ import { execFile } from "node:child_process";
3
+ import { lstat, mkdtemp, readFile, realpath, rm, stat } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { basename, dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { Service } from "@hydraharness/cordis";
8
+ import { profilePluginEnablement, readProfileManifest, resolveProfileDir } from "@hydraharness/harness-app-boot";
9
+ import { withFileLock, writeFileAtomic } from "@hydraharness/harness-atomic-write";
10
+ import { settingsNamespace } from "@hydraharness/harness-settings";
11
+ import { Remote, TypertRemoteService } from "@hydraharness/harness-typert-protocol";
12
+ import z from "@hydraharness/schemastery";
13
+ import { z as z$1 } from "zod";
14
+ //#region lib/types/index.js
15
+ /** Runtime plugin inventory plus shared user-setting enablement controls. */
16
+ var __runInitializers = function(thisArg, initializers, value) {
17
+ var useValue = arguments.length > 2;
18
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
19
+ return useValue ? value : void 0;
20
+ };
21
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
22
+ function accept(f) {
23
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
24
+ return f;
25
+ }
26
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
27
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
28
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
29
+ var _, done = false;
30
+ for (var i = decorators.length - 1; i >= 0; i--) {
31
+ var context = {};
32
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
33
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
34
+ context.addInitializer = function(f) {
35
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
36
+ extraInitializers.push(accept(f || null));
37
+ };
38
+ var result = (0, decorators[i])(kind === "accessor" ? {
39
+ get: descriptor.get,
40
+ set: descriptor.set
41
+ } : descriptor[key], context);
42
+ if (kind === "accessor") {
43
+ if (result === void 0) continue;
44
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
45
+ if (_ = accept(result.get)) descriptor.get = _;
46
+ if (_ = accept(result.set)) descriptor.set = _;
47
+ if (_ = accept(result.init)) initializers.unshift(_);
48
+ } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
49
+ else descriptor[key] = _;
50
+ }
51
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
52
+ done = true;
53
+ };
54
+ /** Brand an existing Loader-tree entry id at the owning boundary. */
55
+ function pluginEntryId(value) {
56
+ return value;
57
+ }
58
+ /** Runtime mirror: FiberState is a cross-package const enum. */
59
+ const FIBER_STATE = {
60
+ PENDING: 0,
61
+ LOADING: 1,
62
+ ACTIVE: 2,
63
+ FAILED: 3,
64
+ DISPOSED: 4,
65
+ UNLOADING: 5
66
+ };
67
+ /** Complete public projection of Cordis Fiber states. */
68
+ const FIBER_PHASE = {
69
+ [FIBER_STATE.PENDING]: "pending",
70
+ [FIBER_STATE.LOADING]: "loading",
71
+ [FIBER_STATE.ACTIVE]: "active",
72
+ [FIBER_STATE.FAILED]: "failed",
73
+ [FIBER_STATE.DISPOSED]: null,
74
+ [FIBER_STATE.UNLOADING]: "unloading"
75
+ };
76
+ const PLUGIN_SETTINGS_NAMESPACE = settingsNamespace("plugins");
77
+ const MARKETPLACE_SETTINGS_NAMESPACE = settingsNamespace("plugin-marketplaces");
78
+ const HMR_MODULE = "@hydraharness/cordis-plugin-hmr";
79
+ const AGENT_PRESET_ENTRY_PREFIX = "agent-preset:";
80
+ const MAX_MARKETPLACE_BYTES = 1024 * 1024;
81
+ const MARKETPLACE_GIT_TIMEOUT_MS = 3e4;
82
+ const MAX_MARKETPLACE_SPARSE_PATHS = 20;
83
+ const MAX_MARKETPLACE_SPARSE_PATH_LENGTH = 512;
84
+ const GITHUB_SHORTHAND_PATTERN = /^([A-Za-z0-9][A-Za-z0-9._-]*)\/([A-Za-z0-9][A-Za-z0-9._-]*)$/u;
85
+ const MarketplaceDocumentSchema = z$1.object({ plugins: z$1.array(z$1.record(z$1.string(), z$1.unknown())).max(500) }).loose();
86
+ const PluginMetadataSchema = z$1.object({
87
+ description: z$1.string().trim().min(1).optional(),
88
+ application: z$1.string().trim().min(1).optional()
89
+ });
90
+ const PluginPackageSchema = z$1.object({
91
+ description: PluginMetadataSchema.shape.description,
92
+ hydra: z$1.object({ plugin: z$1.object({ application: z$1.string().trim().min(1).optional() }).optional() }).optional()
93
+ });
94
+ /** Read presentation metadata without importing or activating the plugin. */
95
+ async function pluginMetadata(moduleName, baseUrl, options = {}) {
96
+ const overrides = PluginMetadataSchema.parse(options);
97
+ const metadata = {
98
+ ...overrides.description === void 0 ? {} : { description: overrides.description },
99
+ ...overrides.application === void 0 ? {} : { application: overrides.application }
100
+ };
101
+ if (baseUrl === void 0 || moduleName.startsWith(".") || moduleName.startsWith("/") || moduleName.includes(":") || moduleName.includes("\\")) return metadata;
102
+ const packageName = moduleName.split("/").slice(0, moduleName.startsWith("@") ? 2 : 1).join("/");
103
+ let path;
104
+ try {
105
+ path = createRequire(baseUrl).resolve(`${packageName}/package.json`);
106
+ } catch (error) {
107
+ if (error instanceof Error && "code" in error && (error.code === "MODULE_NOT_FOUND" || error.code === "ERR_PACKAGE_PATH_NOT_EXPORTED")) return metadata;
108
+ throw error;
109
+ }
110
+ const result = PluginPackageSchema.safeParse(JSON.parse(await readFile(path, "utf8")));
111
+ if (!result.success) throw new Error(`pluginInventory: invalid plugin metadata in ${path}`, { cause: result.error });
112
+ const { description, hydra } = result.data;
113
+ return {
114
+ ...description === void 0 ? {} : { description },
115
+ ...hydra?.plugin?.application === void 0 ? {} : { application: hydra.plugin.application },
116
+ ...metadata
117
+ };
118
+ }
119
+ const PluginSettingsSchema = z.object({ enabled: z.dict(z.boolean()).default({}) });
120
+ const MarketplaceSourceSchema = z.object({
121
+ source: z.string(),
122
+ gitRef: z.string().default(""),
123
+ sparsePaths: z.array(z.string()).max(MAX_MARKETPLACE_SPARSE_PATHS).default([]),
124
+ enabled: z.boolean().default(true)
125
+ });
126
+ const MarketplaceSettingsSchema = z.object({ sources: z.array(MarketplaceSourceSchema).max(20).default([]) });
127
+ function missingPath(error) {
128
+ return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
129
+ }
130
+ function normalizeGitRef(value) {
131
+ const gitRef = value?.trim();
132
+ if (gitRef === void 0 || gitRef === "") return void 0;
133
+ if (gitRef.length > 255 || gitRef.startsWith("-") || /[\u0000-\u0020\u007F]/u.test(gitRef)) throw new Error("pluginInventory: Git ref is invalid");
134
+ return gitRef;
135
+ }
136
+ function normalizeSparsePaths(values) {
137
+ if ((values?.length ?? 0) > MAX_MARKETPLACE_SPARSE_PATHS) throw new Error(`pluginInventory: at most ${String(MAX_MARKETPLACE_SPARSE_PATHS)} sparse paths are allowed`);
138
+ const paths = (values ?? []).map((value) => {
139
+ const path = value.trim().replaceAll("\\", "/").replace(/\/+$/u, "");
140
+ if (path.length > MAX_MARKETPLACE_SPARSE_PATH_LENGTH) throw new Error(`pluginInventory: sparse paths may contain at most ${String(MAX_MARKETPLACE_SPARSE_PATH_LENGTH)} characters`);
141
+ const segments = path.split("/");
142
+ if (path === "" || path.startsWith("/") || path.startsWith("-") || /^[A-Za-z]:\//u.test(path) || segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error(`pluginInventory: sparse path ${JSON.stringify(value)} must be repository-relative`);
143
+ return path;
144
+ });
145
+ return [...new Set(paths)].sort();
146
+ }
147
+ function normalizeGitSource(source) {
148
+ const shorthand = GITHUB_SHORTHAND_PATTERN.exec(source);
149
+ if (shorthand !== null) return `https://github.com/${shorthand[1] ?? ""}/${(shorthand[2] ?? "").replace(/\.git$/u, "")}.git`;
150
+ if (/^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:[A-Za-z0-9._~/-]+$/u.test(source)) return source;
151
+ let url;
152
+ try {
153
+ url = new URL(source);
154
+ } catch (cause) {
155
+ throw new Error("pluginInventory: source must be a GitHub repo, Git URL, or existing local folder", { cause });
156
+ }
157
+ if (url.protocol !== "https:" && url.protocol !== "ssh:") throw new Error("pluginInventory: remote marketplace source must use HTTPS or SSH");
158
+ if (url.password !== "" || url.hash !== "" || url.search !== "" || url.protocol === "https:" && url.username !== "") throw new Error("pluginInventory: marketplace source cannot contain credentials, a query, or a fragment");
159
+ return url.href;
160
+ }
161
+ async function normalizeMarketplaceSource(request) {
162
+ const source = request.source.trim();
163
+ if (source === "" || source.length > 2048 || /[\u0000-\u001F\u007F]/u.test(source)) throw new Error("pluginInventory: marketplace source is invalid");
164
+ const gitRef = normalizeGitRef(request.gitRef);
165
+ const sparsePaths = normalizeSparsePaths(request.sparsePaths);
166
+ const localPath = resolve(source);
167
+ try {
168
+ if (!GITHUB_SHORTHAND_PATTERN.test(source) && (await stat(localPath)).isDirectory()) {
169
+ if (gitRef !== void 0 || sparsePaths.length > 0) throw new Error("pluginInventory: Git ref and sparse paths require a Git source");
170
+ return {
171
+ kind: "local",
172
+ source: await realpath(localPath),
173
+ gitRef: "",
174
+ sparsePaths: [],
175
+ enabled: true
176
+ };
177
+ }
178
+ } catch (error) {
179
+ if (!missingPath(error)) throw error;
180
+ }
181
+ return {
182
+ kind: "git",
183
+ source: normalizeGitSource(source),
184
+ gitRef: gitRef ?? "",
185
+ sparsePaths,
186
+ enabled: true
187
+ };
188
+ }
189
+ function marketplaceGitEnv() {
190
+ const env = {};
191
+ for (const [name, value] of Object.entries(process.env)) if (!/(?:KEY|SECRET|TOKEN|PASSWORD)/iu.test(name)) env[name] = value;
192
+ env.GCM_INTERACTIVE = "Never";
193
+ env.GIT_TERMINAL_PROMPT = "0";
194
+ return env;
195
+ }
196
+ function runGit(cwd, args) {
197
+ return new Promise((resolvePromise, reject) => {
198
+ execFile("git", [
199
+ "-c",
200
+ "protocol.ext.allow=never",
201
+ "-c",
202
+ "protocol.file.allow=never",
203
+ ...args
204
+ ], {
205
+ cwd,
206
+ encoding: "utf8",
207
+ env: marketplaceGitEnv(),
208
+ maxBuffer: MAX_MARKETPLACE_BYTES,
209
+ timeout: MARKETPLACE_GIT_TIMEOUT_MS,
210
+ windowsHide: true
211
+ }, (error, _stdout, stderr) => {
212
+ if (error === null) {
213
+ resolvePromise();
214
+ return;
215
+ }
216
+ const detail = stderr.trim().split(/\r?\n/u)[0];
217
+ reject(new Error(`pluginInventory: failed to load Git marketplace${detail === void 0 || detail === "" ? "" : `: ${detail}`}`, { cause: error }));
218
+ });
219
+ });
220
+ }
221
+ async function readMarketplaceDocument(root) {
222
+ for (const relativePath of [
223
+ ".agents/plugins/marketplace.json",
224
+ ".agents/plugins/api_marketplace.json",
225
+ "marketplace.json"
226
+ ]) {
227
+ const filename = join(root, relativePath);
228
+ try {
229
+ const metadata = await lstat(filename);
230
+ if (!metadata.isFile()) continue;
231
+ if (metadata.size > MAX_MARKETPLACE_BYTES) throw new Error(`pluginInventory: marketplace exceeds ${String(MAX_MARKETPLACE_BYTES)} bytes`);
232
+ const body = await readFile(filename);
233
+ if (body.byteLength > MAX_MARKETPLACE_BYTES) throw new Error(`pluginInventory: marketplace exceeds ${String(MAX_MARKETPLACE_BYTES)} bytes`);
234
+ return MarketplaceDocumentSchema.parse(JSON.parse(body.toString("utf8")));
235
+ } catch (error) {
236
+ if (missingPath(error)) continue;
237
+ throw error;
238
+ }
239
+ }
240
+ throw new Error("pluginInventory: OpenAI/Codex marketplace.json is missing");
241
+ }
242
+ /** Load and validate one marketplace root without retaining an executable checkout. */
243
+ async function loadMarketplace(request) {
244
+ const { kind, ...source } = await normalizeMarketplaceSource(request);
245
+ if (kind === "local") return {
246
+ source,
247
+ document: await readMarketplaceDocument(source.source)
248
+ };
249
+ const tempRoot = await mkdtemp(join(tmpdir(), "hydra-marketplace-"));
250
+ const checkout = join(tempRoot, "repository");
251
+ try {
252
+ if (source.gitRef !== "") try {
253
+ await runGit(tempRoot, [
254
+ "check-ref-format",
255
+ "--allow-onelevel",
256
+ source.gitRef
257
+ ]);
258
+ } catch (cause) {
259
+ throw new Error("pluginInventory: Git ref is invalid", { cause });
260
+ }
261
+ const cloneArgs = [
262
+ "clone",
263
+ "--depth",
264
+ "1",
265
+ "--filter=blob:none"
266
+ ];
267
+ if (source.sparsePaths.length > 0) cloneArgs.push("--sparse");
268
+ if (source.gitRef !== "") cloneArgs.push("--no-checkout");
269
+ cloneArgs.push("--", source.source, checkout);
270
+ await runGit(tempRoot, cloneArgs);
271
+ if (source.gitRef !== "") await runGit(tempRoot, [
272
+ "-C",
273
+ checkout,
274
+ "fetch",
275
+ "--depth",
276
+ "1",
277
+ "origin",
278
+ source.gitRef
279
+ ]);
280
+ if (source.sparsePaths.length > 0) await runGit(tempRoot, [
281
+ "-C",
282
+ checkout,
283
+ "sparse-checkout",
284
+ "set",
285
+ "--cone",
286
+ "--sparse-index",
287
+ "--",
288
+ ".agents/plugins",
289
+ ...source.sparsePaths
290
+ ]);
291
+ if (source.gitRef !== "") await runGit(tempRoot, [
292
+ "-C",
293
+ checkout,
294
+ "checkout",
295
+ "--detach",
296
+ "FETCH_HEAD"
297
+ ]);
298
+ return {
299
+ source,
300
+ document: await readMarketplaceDocument(checkout)
301
+ };
302
+ } finally {
303
+ await rm(tempRoot, {
304
+ recursive: true,
305
+ force: true
306
+ });
307
+ }
308
+ }
309
+ /** Remote-only service exposing the Loader's current non-group entry state. */
310
+ let PluginInventoryGateway = (() => {
311
+ let _classSuper = TypertRemoteService;
312
+ let _instanceExtraInitializers = [];
313
+ let _list_decorators;
314
+ let _setEnabled_decorators;
315
+ let _listMarketplaces_decorators;
316
+ let _addMarketplace_decorators;
317
+ let _setMarketplaceEnabled_decorators;
318
+ let _removeMarketplace_decorators;
319
+ let _listImportedPlugins_decorators;
320
+ let _importPlugin_decorators;
321
+ let _infoPlugin_decorators;
322
+ let _enablePlugin_decorators;
323
+ let _disablePlugin_decorators;
324
+ let _setPluginMcpServerEnabled_decorators;
325
+ let _setPluginMcpToolApproval_decorators;
326
+ let _trustPlugin_decorators;
327
+ let _untrustPlugin_decorators;
328
+ let _removePlugin_decorators;
329
+ let _listMcpServers_decorators;
330
+ let _defineMcpServer_decorators;
331
+ let _setMcpServerEnabled_decorators;
332
+ let _removeMcpServer_decorators;
333
+ let _listHookRecords_decorators;
334
+ let _defineHookRecord_decorators;
335
+ let _setHookRecordEnabled_decorators;
336
+ let _removeHookRecord_decorators;
337
+ return class PluginInventoryGateway extends _classSuper {
338
+ static {
339
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
340
+ _list_decorators = [Remote("list")];
341
+ _setEnabled_decorators = [Remote("setEnabled")];
342
+ _listMarketplaces_decorators = [Remote("listMarketplaces")];
343
+ _addMarketplace_decorators = [Remote("addMarketplace")];
344
+ _setMarketplaceEnabled_decorators = [Remote("setMarketplaceEnabled")];
345
+ _removeMarketplace_decorators = [Remote("removeMarketplace")];
346
+ _listImportedPlugins_decorators = [Remote("listImportedPlugins")];
347
+ _importPlugin_decorators = [Remote("importPlugin")];
348
+ _infoPlugin_decorators = [Remote("infoPlugin")];
349
+ _enablePlugin_decorators = [Remote("enablePlugin")];
350
+ _disablePlugin_decorators = [Remote("disablePlugin")];
351
+ _setPluginMcpServerEnabled_decorators = [Remote("setPluginMcpServerEnabled")];
352
+ _setPluginMcpToolApproval_decorators = [Remote("setPluginMcpToolApproval")];
353
+ _trustPlugin_decorators = [Remote("trustPlugin")];
354
+ _untrustPlugin_decorators = [Remote("untrustPlugin")];
355
+ _removePlugin_decorators = [Remote("removePlugin")];
356
+ _listMcpServers_decorators = [Remote("listMcpServers")];
357
+ _defineMcpServer_decorators = [Remote("defineMcpServer")];
358
+ _setMcpServerEnabled_decorators = [Remote("setMcpServerEnabled")];
359
+ _removeMcpServer_decorators = [Remote("removeMcpServer")];
360
+ _listHookRecords_decorators = [Remote("listHookRecords")];
361
+ _defineHookRecord_decorators = [Remote("defineHookRecord")];
362
+ _setHookRecordEnabled_decorators = [Remote("setHookRecordEnabled")];
363
+ _removeHookRecord_decorators = [Remote("removeHookRecord")];
364
+ __esDecorate(this, null, _list_decorators, {
365
+ kind: "method",
366
+ name: "list",
367
+ static: false,
368
+ private: false,
369
+ access: {
370
+ has: (obj) => "list" in obj,
371
+ get: (obj) => obj.list
372
+ },
373
+ metadata: _metadata
374
+ }, null, _instanceExtraInitializers);
375
+ __esDecorate(this, null, _setEnabled_decorators, {
376
+ kind: "method",
377
+ name: "setEnabled",
378
+ static: false,
379
+ private: false,
380
+ access: {
381
+ has: (obj) => "setEnabled" in obj,
382
+ get: (obj) => obj.setEnabled
383
+ },
384
+ metadata: _metadata
385
+ }, null, _instanceExtraInitializers);
386
+ __esDecorate(this, null, _listMarketplaces_decorators, {
387
+ kind: "method",
388
+ name: "listMarketplaces",
389
+ static: false,
390
+ private: false,
391
+ access: {
392
+ has: (obj) => "listMarketplaces" in obj,
393
+ get: (obj) => obj.listMarketplaces
394
+ },
395
+ metadata: _metadata
396
+ }, null, _instanceExtraInitializers);
397
+ __esDecorate(this, null, _addMarketplace_decorators, {
398
+ kind: "method",
399
+ name: "addMarketplace",
400
+ static: false,
401
+ private: false,
402
+ access: {
403
+ has: (obj) => "addMarketplace" in obj,
404
+ get: (obj) => obj.addMarketplace
405
+ },
406
+ metadata: _metadata
407
+ }, null, _instanceExtraInitializers);
408
+ __esDecorate(this, null, _setMarketplaceEnabled_decorators, {
409
+ kind: "method",
410
+ name: "setMarketplaceEnabled",
411
+ static: false,
412
+ private: false,
413
+ access: {
414
+ has: (obj) => "setMarketplaceEnabled" in obj,
415
+ get: (obj) => obj.setMarketplaceEnabled
416
+ },
417
+ metadata: _metadata
418
+ }, null, _instanceExtraInitializers);
419
+ __esDecorate(this, null, _removeMarketplace_decorators, {
420
+ kind: "method",
421
+ name: "removeMarketplace",
422
+ static: false,
423
+ private: false,
424
+ access: {
425
+ has: (obj) => "removeMarketplace" in obj,
426
+ get: (obj) => obj.removeMarketplace
427
+ },
428
+ metadata: _metadata
429
+ }, null, _instanceExtraInitializers);
430
+ __esDecorate(this, null, _listImportedPlugins_decorators, {
431
+ kind: "method",
432
+ name: "listImportedPlugins",
433
+ static: false,
434
+ private: false,
435
+ access: {
436
+ has: (obj) => "listImportedPlugins" in obj,
437
+ get: (obj) => obj.listImportedPlugins
438
+ },
439
+ metadata: _metadata
440
+ }, null, _instanceExtraInitializers);
441
+ __esDecorate(this, null, _importPlugin_decorators, {
442
+ kind: "method",
443
+ name: "importPlugin",
444
+ static: false,
445
+ private: false,
446
+ access: {
447
+ has: (obj) => "importPlugin" in obj,
448
+ get: (obj) => obj.importPlugin
449
+ },
450
+ metadata: _metadata
451
+ }, null, _instanceExtraInitializers);
452
+ __esDecorate(this, null, _infoPlugin_decorators, {
453
+ kind: "method",
454
+ name: "infoPlugin",
455
+ static: false,
456
+ private: false,
457
+ access: {
458
+ has: (obj) => "infoPlugin" in obj,
459
+ get: (obj) => obj.infoPlugin
460
+ },
461
+ metadata: _metadata
462
+ }, null, _instanceExtraInitializers);
463
+ __esDecorate(this, null, _enablePlugin_decorators, {
464
+ kind: "method",
465
+ name: "enablePlugin",
466
+ static: false,
467
+ private: false,
468
+ access: {
469
+ has: (obj) => "enablePlugin" in obj,
470
+ get: (obj) => obj.enablePlugin
471
+ },
472
+ metadata: _metadata
473
+ }, null, _instanceExtraInitializers);
474
+ __esDecorate(this, null, _disablePlugin_decorators, {
475
+ kind: "method",
476
+ name: "disablePlugin",
477
+ static: false,
478
+ private: false,
479
+ access: {
480
+ has: (obj) => "disablePlugin" in obj,
481
+ get: (obj) => obj.disablePlugin
482
+ },
483
+ metadata: _metadata
484
+ }, null, _instanceExtraInitializers);
485
+ __esDecorate(this, null, _setPluginMcpServerEnabled_decorators, {
486
+ kind: "method",
487
+ name: "setPluginMcpServerEnabled",
488
+ static: false,
489
+ private: false,
490
+ access: {
491
+ has: (obj) => "setPluginMcpServerEnabled" in obj,
492
+ get: (obj) => obj.setPluginMcpServerEnabled
493
+ },
494
+ metadata: _metadata
495
+ }, null, _instanceExtraInitializers);
496
+ __esDecorate(this, null, _setPluginMcpToolApproval_decorators, {
497
+ kind: "method",
498
+ name: "setPluginMcpToolApproval",
499
+ static: false,
500
+ private: false,
501
+ access: {
502
+ has: (obj) => "setPluginMcpToolApproval" in obj,
503
+ get: (obj) => obj.setPluginMcpToolApproval
504
+ },
505
+ metadata: _metadata
506
+ }, null, _instanceExtraInitializers);
507
+ __esDecorate(this, null, _trustPlugin_decorators, {
508
+ kind: "method",
509
+ name: "trustPlugin",
510
+ static: false,
511
+ private: false,
512
+ access: {
513
+ has: (obj) => "trustPlugin" in obj,
514
+ get: (obj) => obj.trustPlugin
515
+ },
516
+ metadata: _metadata
517
+ }, null, _instanceExtraInitializers);
518
+ __esDecorate(this, null, _untrustPlugin_decorators, {
519
+ kind: "method",
520
+ name: "untrustPlugin",
521
+ static: false,
522
+ private: false,
523
+ access: {
524
+ has: (obj) => "untrustPlugin" in obj,
525
+ get: (obj) => obj.untrustPlugin
526
+ },
527
+ metadata: _metadata
528
+ }, null, _instanceExtraInitializers);
529
+ __esDecorate(this, null, _removePlugin_decorators, {
530
+ kind: "method",
531
+ name: "removePlugin",
532
+ static: false,
533
+ private: false,
534
+ access: {
535
+ has: (obj) => "removePlugin" in obj,
536
+ get: (obj) => obj.removePlugin
537
+ },
538
+ metadata: _metadata
539
+ }, null, _instanceExtraInitializers);
540
+ __esDecorate(this, null, _listMcpServers_decorators, {
541
+ kind: "method",
542
+ name: "listMcpServers",
543
+ static: false,
544
+ private: false,
545
+ access: {
546
+ has: (obj) => "listMcpServers" in obj,
547
+ get: (obj) => obj.listMcpServers
548
+ },
549
+ metadata: _metadata
550
+ }, null, _instanceExtraInitializers);
551
+ __esDecorate(this, null, _defineMcpServer_decorators, {
552
+ kind: "method",
553
+ name: "defineMcpServer",
554
+ static: false,
555
+ private: false,
556
+ access: {
557
+ has: (obj) => "defineMcpServer" in obj,
558
+ get: (obj) => obj.defineMcpServer
559
+ },
560
+ metadata: _metadata
561
+ }, null, _instanceExtraInitializers);
562
+ __esDecorate(this, null, _setMcpServerEnabled_decorators, {
563
+ kind: "method",
564
+ name: "setMcpServerEnabled",
565
+ static: false,
566
+ private: false,
567
+ access: {
568
+ has: (obj) => "setMcpServerEnabled" in obj,
569
+ get: (obj) => obj.setMcpServerEnabled
570
+ },
571
+ metadata: _metadata
572
+ }, null, _instanceExtraInitializers);
573
+ __esDecorate(this, null, _removeMcpServer_decorators, {
574
+ kind: "method",
575
+ name: "removeMcpServer",
576
+ static: false,
577
+ private: false,
578
+ access: {
579
+ has: (obj) => "removeMcpServer" in obj,
580
+ get: (obj) => obj.removeMcpServer
581
+ },
582
+ metadata: _metadata
583
+ }, null, _instanceExtraInitializers);
584
+ __esDecorate(this, null, _listHookRecords_decorators, {
585
+ kind: "method",
586
+ name: "listHookRecords",
587
+ static: false,
588
+ private: false,
589
+ access: {
590
+ has: (obj) => "listHookRecords" in obj,
591
+ get: (obj) => obj.listHookRecords
592
+ },
593
+ metadata: _metadata
594
+ }, null, _instanceExtraInitializers);
595
+ __esDecorate(this, null, _defineHookRecord_decorators, {
596
+ kind: "method",
597
+ name: "defineHookRecord",
598
+ static: false,
599
+ private: false,
600
+ access: {
601
+ has: (obj) => "defineHookRecord" in obj,
602
+ get: (obj) => obj.defineHookRecord
603
+ },
604
+ metadata: _metadata
605
+ }, null, _instanceExtraInitializers);
606
+ __esDecorate(this, null, _setHookRecordEnabled_decorators, {
607
+ kind: "method",
608
+ name: "setHookRecordEnabled",
609
+ static: false,
610
+ private: false,
611
+ access: {
612
+ has: (obj) => "setHookRecordEnabled" in obj,
613
+ get: (obj) => obj.setHookRecordEnabled
614
+ },
615
+ metadata: _metadata
616
+ }, null, _instanceExtraInitializers);
617
+ __esDecorate(this, null, _removeHookRecord_decorators, {
618
+ kind: "method",
619
+ name: "removeHookRecord",
620
+ static: false,
621
+ private: false,
622
+ access: {
623
+ has: (obj) => "removeHookRecord" in obj,
624
+ get: (obj) => obj.removeHookRecord
625
+ },
626
+ metadata: _metadata
627
+ }, null, _instanceExtraInitializers);
628
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
629
+ enumerable: true,
630
+ configurable: true,
631
+ writable: true,
632
+ value: _metadata
633
+ });
634
+ }
635
+ static inject = ["loader", "settings"];
636
+ static Config = z.object({ compositionEntryIds: z.array(z.string()).default([]) });
637
+ compositionEntryIds = __runInitializers(this, _instanceExtraInitializers);
638
+ settings;
639
+ marketplaceSettings;
640
+ defaultEnabled = /* @__PURE__ */ new Map();
641
+ initialEnabled = /* @__PURE__ */ new Map();
642
+ originalConfigs = /* @__PURE__ */ new Map();
643
+ suspended = /* @__PURE__ */ new Map();
644
+ required = /* @__PURE__ */ new Set();
645
+ appliedSettings = /* @__PURE__ */ new Set();
646
+ restartEnablement = {};
647
+ mutationTail = Promise.resolve();
648
+ constructor(ctx, config = {}) {
649
+ super(ctx, "pluginInventory");
650
+ this.compositionEntryIds = new Set(config.compositionEntryIds);
651
+ this.settings = ctx.settings.register(PLUGIN_SETTINGS_NAMESPACE, PluginSettingsSchema);
652
+ this.marketplaceSettings = ctx.settings.register(MARKETPLACE_SETTINGS_NAMESPACE, MarketplaceSettingsSchema);
653
+ ctx.inject(["optionalPlugins"], (pctx) => {
654
+ pctx.effect(() => pctx.optionalPlugins.subscribe((enabled) => {
655
+ this.enqueue(() => this.applyOptionalGate(enabled)).catch((error) => {
656
+ this.ctx.logger.error(error);
657
+ });
658
+ }), "pluginInventory: optional policy");
659
+ this.enqueue(() => this.applyOptionalGate(pctx.optionalPlugins.enabled)).catch((error) => {
660
+ this.ctx.logger.error(error);
661
+ });
662
+ });
663
+ ctx.on("loader/config-update", () => {
664
+ const policy = ctx.get("optionalPlugins");
665
+ if (policy !== void 0) this.enqueue(() => this.applyOptionalGate(policy.enabled)).catch((error) => {
666
+ ctx.logger.error(error);
667
+ });
668
+ });
669
+ }
670
+ async [Service.init]() {
671
+ const rootInclude = this.rootInclude();
672
+ if (rootInclude === void 0) return;
673
+ const representatives = this.representatives(rootInclude);
674
+ for (const entry of representatives.values()) {
675
+ this.defaultEnabled.set(entry.options.name, !entry.disabled && !this.isHmrWatchOnly(entry));
676
+ this.originalConfigs.set(entry.options.name, structuredClone(entry.options.config));
677
+ }
678
+ if ([...representatives.values()].some((entry) => this.isRestartOnly(entry))) this.restartEnablement = profilePluginEnablement(readProfileManifest("pluginInventory", this.profileDir()));
679
+ await this.applySettings(this.settings.get().enabled);
680
+ for (const entry of representatives.values()) this.initialEnabled.set(entry.id, this.enabled(entry));
681
+ this.ctx.effect(() => this.settings.watch((next) => this.enqueue(() => this.applySettings(next.enabled))), "pluginInventory.settingsWatch");
682
+ }
683
+ /** Protect required modules and their declared dependency closure before suspending root-owned features. */
684
+ async applyOptionalGate(enabled) {
685
+ const policy = this.ctx.get("optionalPlugins");
686
+ if (policy === void 0) return;
687
+ if (!enabled) await policy.drain();
688
+ if (policy.enabled !== enabled) return;
689
+ const entries = [...this.representatives(this.rootInclude()).values()];
690
+ const byModule = new Map(entries.map((entry) => [entry.options.name, entry]));
691
+ const pending = entries.filter((entry) => entry.options.pluginRequired === true).map((entry) => entry.options.name);
692
+ this.required.clear();
693
+ for (const name of pending) {
694
+ if (this.required.has(name)) continue;
695
+ this.required.add(name);
696
+ const baseUrl = byModule.get(name)?.parent.tree.ctx.baseUrl;
697
+ if (baseUrl === void 0 || !name.startsWith("@")) continue;
698
+ const packageName = name.split("/").slice(0, 2).join("/");
699
+ const manifest = createRequire(baseUrl).resolve(`${packageName}/package.json`);
700
+ const metadata = z$1.object({
701
+ dependencies: z$1.record(z$1.string(), z$1.string()).optional(),
702
+ peerDependencies: z$1.record(z$1.string(), z$1.string()).optional(),
703
+ hydra: z$1.object({ bundle: z$1.unknown().optional() }).optional()
704
+ }).parse(JSON.parse(await readFile(manifest, "utf8")));
705
+ if (metadata.hydra?.bundle !== void 0) continue;
706
+ for (const dependency of Object.keys({
707
+ ...metadata.dependencies,
708
+ ...metadata.peerDependencies
709
+ })) for (const candidate of entries) if (candidate.options.name === dependency || candidate.options.name.startsWith(`${dependency}/`)) pending.push(candidate.options.name);
710
+ }
711
+ for (const entry of entries) {
712
+ if (entry.subtree !== void 0 || entry.subgroup !== void 0 || this.required.has(entry.options.name)) continue;
713
+ const previous = this.suspended.get(entry);
714
+ if (!enabled) {
715
+ if (!this.suspended.has(entry)) this.suspended.set(entry, !entry.disabled);
716
+ await entry.update({ disabled: true });
717
+ } else if (previous !== void 0) {
718
+ const nextEnabled = this.isRestartOnly(entry) ? previous : this.settings.get().enabled[entry.options.name] ?? this.defaultEnabled.get(entry.options.name) ?? previous;
719
+ this.suspended.delete(entry);
720
+ await entry.update({ disabled: !nextEnabled });
721
+ }
722
+ }
723
+ }
724
+ rootInclude() {
725
+ return Object.values(this.ctx.loader.store).map((entry) => entry.subtree).find((tree) => tree !== void 0 && "filename" in tree && typeof tree.filename === "string");
726
+ }
727
+ /** Project persisted OpenAI/Codex marketplace sources without exposing their entries here. */
728
+ async marketplaceSnapshot() {
729
+ const sources = this.marketplaceSettings.get().sources;
730
+ if (sources.length === 0) return { marketplaces: [] };
731
+ const marketplaces = [];
732
+ for (const source of sources) {
733
+ const sourceView = {
734
+ source: source.source,
735
+ ...source.gitRef === "" ? {} : { gitRef: source.gitRef },
736
+ sparsePaths: source.sparsePaths,
737
+ enabled: source.enabled
738
+ };
739
+ try {
740
+ await loadMarketplace(source);
741
+ marketplaces.push({
742
+ status: "ready",
743
+ ...sourceView
744
+ });
745
+ } catch {
746
+ marketplaces.push({
747
+ status: "unavailable",
748
+ ...sourceView
749
+ });
750
+ }
751
+ }
752
+ return { marketplaces };
753
+ }
754
+ moduleEntries(moduleName) {
755
+ const entries = [];
756
+ for (const entry of this.ctx.loader.entries()) if (!entry.options.group && !this.compositionEntryIds.has(entry.options.id) && entry.options.name === moduleName) entries.push(entry);
757
+ return entries;
758
+ }
759
+ /** One logical row per module, preferring the profile-owned entry. */
760
+ representatives(rootInclude) {
761
+ const entries = /* @__PURE__ */ new Map();
762
+ for (const entry of this.ctx.loader.entries()) {
763
+ if (entry.options.group || this.compositionEntryIds.has(entry.options.id)) continue;
764
+ this.isRestartOnly(entry);
765
+ if (entry.options.pluginRequired !== void 0 && typeof entry.options.pluginRequired !== "boolean") throw new Error(`pluginInventory: ${entry.id}.pluginRequired must be boolean`);
766
+ const group = entry.options.pluginGroup;
767
+ if (group !== void 0 && (typeof group !== "string" || group.trim() === "")) throw new Error(`pluginInventory: ${entry.id}.pluginGroup must be a non-empty string`);
768
+ if ("plugin_type" in entry.options) throw new Error(`pluginInventory: ${entry.id}.plugin_type: use pluginType`);
769
+ const current = entries.get(entry.options.name);
770
+ if (current === void 0 || entry.parent.tree === rootInclude && current.parent.tree !== rootInclude || this.isRestartOnly(entry) && !this.isRestartOnly(current)) entries.set(entry.options.name, entry);
771
+ }
772
+ return entries;
773
+ }
774
+ /** Whether changing this root entry must wait for the next profile boot. */
775
+ isRestartOnly(entry) {
776
+ const value = entry.options.pluginType;
777
+ if (value !== void 0 && value !== "core" && value !== "normal") throw new Error(`pluginInventory: ${entry.id}.pluginType must be core or normal`);
778
+ return value === "core" || entry.options.pluginGroup !== void 0 && [...this.ctx.loader.entries()].some((candidate) => candidate.options.pluginGroup === entry.options.pluginGroup && candidate.options.pluginType === "core");
779
+ }
780
+ isToggleable(entry, rootInclude = this.rootInclude()) {
781
+ return rootInclude !== void 0 && entry.parent.tree === rootInclude && entry.options.pluginRequired !== true && !this.required.has(entry.options.name);
782
+ }
783
+ enabled(entry) {
784
+ if (this.isRestartOnly(entry) || !this.isToggleable(entry)) return this.suspended.get(entry) ?? !entry.disabled;
785
+ return this.settings.get().enabled[entry.options.name] ?? this.defaultEnabled.get(entry.options.name) ?? !entry.disabled;
786
+ }
787
+ /** Desired state used by the switch; restart-only entries retain the live Loader state until reboot. */
788
+ desiredEnabled(entry) {
789
+ return this.isRestartOnly(entry) ? this.restartEnablement[entry.options.id] ?? !entry.disabled : this.enabled(entry);
790
+ }
791
+ /** Whether the currently loaded entry still differs from its persisted desired state. */
792
+ restartRequired(entry) {
793
+ const pending = this.restartEnablement[entry.options.id];
794
+ return this.isRestartOnly(entry) && pending !== void 0 && pending !== !entry.disabled;
795
+ }
796
+ /** Resolve the active root Include to the profile directory the launcher owns. */
797
+ profileDir() {
798
+ const filename = this.rootInclude()?.filename;
799
+ /* v8 ignore next -- Restart updates only follow a synchronous rootInclude-backed toggleability check. */
800
+ if (typeof filename !== "string") throw new Error("pluginInventory: active profile root is unavailable");
801
+ /* v8 ignore next -- The root Include stores filename as the filesystem path returned by fileURLToPath. */
802
+ const path = filename.startsWith("file:") ? fileURLToPath(filename) : filename;
803
+ const actual = resolve(dirname(path));
804
+ const expected = resolve(resolveProfileDir(basename(actual)));
805
+ if (actual !== expected) throw new Error(`pluginInventory: active root ${path} is not a managed hydra profile`);
806
+ return expected;
807
+ }
808
+ /** Persist one restart-only root switch without changing the live Loader tree. */
809
+ async setRestartEnabled(entries, enabled) {
810
+ const profileDir = this.profileDir();
811
+ const manifestPath = join(profileDir, "package.json");
812
+ await withFileLock(manifestPath, async () => {
813
+ const manifest = readProfileManifest("pluginInventory", profileDir);
814
+ const pluginEnablement = {
815
+ ...profilePluginEnablement(manifest),
816
+ ...Object.fromEntries(entries.map((entry) => [entry.options.id, enabled]))
817
+ };
818
+ const next = {
819
+ ...manifest,
820
+ hydra: {
821
+ ...manifest.hydra,
822
+ profile: {
823
+ ...manifest.hydra?.profile,
824
+ pluginEnablement
825
+ }
826
+ }
827
+ };
828
+ await writeFileAtomic(manifestPath, `${JSON.stringify(next, void 0, 2)}\n`, { mode: 384 });
829
+ this.restartEnablement = pluginEnablement;
830
+ });
831
+ }
832
+ isHmrWatchOnly(entry) {
833
+ const config = entry.options.config;
834
+ return entry.options.name === HMR_MODULE && Array.isArray(config?.root) && config.root.length === 0;
835
+ }
836
+ enqueue(task) {
837
+ const run = this.mutationTail.then(task, task);
838
+ this.mutationTail = run.then(() => void 0, () => void 0);
839
+ return run;
840
+ }
841
+ async restore(states) {
842
+ for (const state of states.toReversed()) await state.entry.update({
843
+ disabled: state.disabled ?? null,
844
+ config: state.config ?? null
845
+ });
846
+ }
847
+ /** Keep one configured instance active; HMR's disabled state remains watch-only. */
848
+ async updateModule(entry, enabled) {
849
+ const candidates = this.moduleEntries(entry.options.name);
850
+ const states = candidates.map((candidate) => ({
851
+ entry: candidate,
852
+ disabled: candidate.options.disabled,
853
+ config: candidate.options.config
854
+ }));
855
+ try {
856
+ for (const candidate of candidates) if (candidate !== entry && !candidate.disabled) await candidate.update({ disabled: true });
857
+ if (entry.options.name === HMR_MODULE) {
858
+ const config = { ...this.originalConfigs.get(entry.options.name) };
859
+ if (enabled) delete config.root;
860
+ else config.root = [];
861
+ await entry.update({
862
+ disabled: null,
863
+ config
864
+ });
865
+ } else await entry.update({ disabled: enabled ? null : true });
866
+ return states;
867
+ } catch (error) {
868
+ try {
869
+ await this.restore(states);
870
+ } catch (rollbackError) {
871
+ throw new AggregateError([error, rollbackError], `pluginInventory: failed to update and roll back ${entry.options.name}`);
872
+ }
873
+ throw error;
874
+ }
875
+ }
876
+ async applySettings(enabled) {
877
+ const rootInclude = this.rootInclude();
878
+ const representatives = this.representatives(rootInclude);
879
+ const modules = new Set([...this.appliedSettings, ...Object.keys(enabled)]);
880
+ this.appliedSettings.clear();
881
+ for (const moduleName of Object.keys(enabled)) this.appliedSettings.add(moduleName);
882
+ const failures = [];
883
+ for (const moduleName of modules) {
884
+ const entry = representatives.get(moduleName);
885
+ if (entry === void 0) continue;
886
+ if (!this.isToggleable(entry, rootInclude) || this.isRestartOnly(entry)) continue;
887
+ const desired = enabled[moduleName] ?? this.defaultEnabled.get(moduleName) ?? !entry.disabled;
888
+ try {
889
+ await this.updateModule(entry, this.ctx.get("optionalPlugins")?.enabled === false && !this.required.has(moduleName) ? false : desired);
890
+ } catch (error) {
891
+ failures.push(error);
892
+ }
893
+ }
894
+ if (failures.length === 1) throw failures[0];
895
+ if (failures.length > 1) throw new AggregateError(failures, "pluginInventory: failed to apply plugin settings");
896
+ }
897
+ /**
898
+ * Read the Loader and optional preset roster directly on every call. Cordis's
899
+ * internal plugin/status events already maintain Entry.fiber and Fiber state,
900
+ * so a second Host cache would only add another lifecycle truth to synchronize.
901
+ * @returns Current Host entries followed by preset-owned leaf entries.
902
+ */
903
+ async hostEntries() {
904
+ const rootInclude = this.rootInclude();
905
+ return Promise.all(this.inventoryGroups(rootInclude).map(async (group) => {
906
+ const entry = group[0];
907
+ const members = group.filter((candidate) => this.isToggleable(candidate, rootInclude));
908
+ const controlled = members.length > 0 ? members : group;
909
+ const initialStates = controlled.map((candidate) => this.initialEnabled.get(candidate.id) ?? this.enabled(candidate));
910
+ const desiredStates = controlled.map((candidate) => this.desiredEnabled(candidate));
911
+ const enabled = controlled.every((candidate) => this.enabled(candidate));
912
+ const restartRequired = controlled.some((candidate) => this.restartRequired(candidate));
913
+ const pendingEnabled = restartRequired ? controlled.every((candidate) => this.desiredEnabled(candidate)) : void 0;
914
+ const fiber = (enabled ? this.moduleEntries(entry.options.name).find((candidate) => !candidate.disabled && candidate.fiber !== void 0) : void 0)?.fiber;
915
+ return {
916
+ entryId: pluginEntryId(entry.id),
917
+ moduleName: entry.options.name,
918
+ ...await pluginMetadata(entry.options.name, entry.parent.tree.ctx.baseUrl, entry.options),
919
+ pluginType: group.some((candidate) => this.isRestartOnly(candidate)) ? "core" : "normal",
920
+ ...entry.options.pluginRequired === true || this.required.has(entry.options.name) ? { required: true } : {},
921
+ initialEnabled: initialStates.every(Boolean) ? true : initialStates.some(Boolean) ? null : false,
922
+ changedSinceStart: controlled.some((candidate, index) => this.desiredEnabled(candidate) !== initialStates[index]),
923
+ ...desiredStates.some(Boolean) && !desiredStates.every(Boolean) ? { mixedEnabled: true } : {},
924
+ ...group.length > 1 ? { relatedModules: group.slice(1).map((candidate) => candidate.options.name) } : {},
925
+ enabled,
926
+ ...pendingEnabled === void 0 ? {} : { pendingEnabled },
927
+ restartRequired,
928
+ toggleable: this.isToggleable(entry, rootInclude),
929
+ fiberPhase: fiber === void 0 ? null : FIBER_PHASE[fiber.state]
930
+ };
931
+ }));
932
+ }
933
+ async presetEntries(presets, settings, target = {}) {
934
+ if (presets === void 0) return [];
935
+ const entries = settings === void 0 ? await presets.listPluginEntries() : await presets.listPluginEntries(settings);
936
+ return Promise.all(entries.map(async (entry) => ({
937
+ entryId: pluginEntryId(entry.entryId),
938
+ moduleName: entry.moduleName,
939
+ ...await pluginMetadata(entry.moduleName, this.ctx.baseUrl, entry),
940
+ enabled: entry.enabled,
941
+ presetId: entry.presetId,
942
+ ...entry.rowId === void 0 ? {} : { presetRowId: entry.rowId },
943
+ newSessionsOnly: true,
944
+ initialEnabled: this.initialEnabled.get(this.initialKey(entry.entryId, target)) ?? entry.enabled,
945
+ restartRequired: false,
946
+ toggleable: true,
947
+ fiberPhase: null
948
+ })));
949
+ }
950
+ /**
951
+ * List Host entries and preset leaves, or only the selected workspace's preset leaves.
952
+ * @param request - Workspace and editable settings layer; omission selects global inventory.
953
+ * @returns the current inventory projection.
954
+ */
955
+ async list(request) {
956
+ const target = request ?? {};
957
+ this.assertTarget(target);
958
+ const workspace = target.workspaceId === void 0 ? void 0 : await this.workspaceTarget(target.workspaceId, target.layer);
959
+ const selected = workspace?.target ?? {};
960
+ const entries = workspace === void 0 ? [...await this.hostEntries(), ...await this.presetEntries(this.agentPresets())] : await this.presetEntries(workspace.presets, workspace.settings, selected);
961
+ for (const entry of entries) {
962
+ const key = this.initialKey(entry.entryId, selected);
963
+ if (!this.initialEnabled.has(key)) this.initialEnabled.set(key, entry.enabled);
964
+ }
965
+ return {
966
+ entries,
967
+ ...selected
968
+ };
969
+ }
970
+ /** Keep initial preset states separate for global and workspace settings files. */
971
+ initialKey(entryId, target) {
972
+ return target.workspaceId === void 0 ? entryId : `${target.workspaceId}:${target.layer ?? "local"}:${entryId}`;
973
+ }
974
+ /** Group only explicitly related entries, preferring a persistently editable owner. */
975
+ inventoryGroups(rootInclude) {
976
+ const groups = /* @__PURE__ */ new Map();
977
+ for (const entry of this.representatives(rootInclude).values()) {
978
+ const key = entry.options.pluginGroup === void 0 ? `module:${entry.options.name}` : `group:${entry.options.pluginGroup}:${entry.options.pluginRequired === true || this.required.has(entry.options.name) ? "required" : "optional"}`;
979
+ const group = groups.get(key) ?? [];
980
+ if (this.isToggleable(entry, rootInclude) && group[0] !== void 0 && !this.isToggleable(group[0], rootInclude)) group.unshift(entry);
981
+ else group.push(entry);
982
+ groups.set(key, group);
983
+ }
984
+ return [...groups.values()];
985
+ }
986
+ /**
987
+ * Persist a restart-only root state without live unload, or apply one normal
988
+ * module-wide live state and persist it in the shared settings document.
989
+ * @param request - Target entry, desired enablement, and optional workspace settings file.
990
+ * @returns The fresh snapshot and whether this mutation needs a restart.
991
+ */
992
+ setEnabled(request) {
993
+ this.assertTarget(request);
994
+ return this.enqueue(async () => {
995
+ const workspace = request.workspaceId === void 0 ? void 0 : await this.workspaceTarget(request.workspaceId, request.layer);
996
+ if (workspace !== void 0 && !request.entryId.startsWith(AGENT_PRESET_ENTRY_PREFIX)) throw new Error("pluginInventory: workspace settings can change only agent preset entries");
997
+ if (request.entryId.startsWith(AGENT_PRESET_ENTRY_PREFIX)) {
998
+ const presets = workspace === void 0 ? this.agentPresets() : workspace.presets;
999
+ if (presets === void 0) throw new Error("pluginInventory: agent preset controls are unavailable");
1000
+ const entry = (workspace === void 0 ? await presets.listPluginEntries() : await presets.listPluginEntries(workspace.settings)).find((candidate) => candidate.entryId === request.entryId);
1001
+ if (entry === void 0) throw new Error(`pluginInventory: preset entry ${request.entryId} cannot be toggled in-app`);
1002
+ const key = this.initialKey(entry.entryId, workspace?.target ?? {});
1003
+ if (!this.initialEnabled.has(key)) this.initialEnabled.set(key, entry.enabled);
1004
+ if (entry.enabled !== request.enabled) if (workspace === void 0) await presets.setPluginEnabled(entry.entryId, request.enabled);
1005
+ else await presets.setPluginEnabled(entry.entryId, request.enabled, workspace.settings);
1006
+ return {
1007
+ snapshot: await this.list(workspace?.target),
1008
+ restartRequired: false
1009
+ };
1010
+ }
1011
+ const resolved = this.ctx.loader.resolve(request.entryId);
1012
+ const rootInclude = this.rootInclude();
1013
+ const group = this.inventoryGroups(rootInclude).find((candidates) => candidates.some((candidate) => candidate.options.name === resolved.options.name)) ?? [];
1014
+ const entry = group[0];
1015
+ if (entry === void 0 || !this.isToggleable(entry, rootInclude)) throw new Error(`pluginInventory: entry ${request.entryId} cannot be toggled in-app`);
1016
+ const members = group.filter((candidate) => this.isToggleable(candidate, rootInclude));
1017
+ if (members.every((candidate) => request.enabled === this.desiredEnabled(candidate))) return {
1018
+ snapshot: await this.list(),
1019
+ restartRequired: members.some((candidate) => this.restartRequired(candidate))
1020
+ };
1021
+ if (group.some((candidate) => this.isRestartOnly(candidate))) {
1022
+ await this.setRestartEnabled(members, request.enabled);
1023
+ return {
1024
+ snapshot: await this.list(),
1025
+ restartRequired: members.some((candidate) => this.restartRequired(candidate))
1026
+ };
1027
+ }
1028
+ const states = [];
1029
+ try {
1030
+ for (const member of members) states.push(...await this.updateModule(member, request.enabled));
1031
+ await this.settings.update({ enabled: Object.fromEntries(members.map((member) => [member.options.name, request.enabled])) });
1032
+ } catch (error) {
1033
+ try {
1034
+ await this.restore(states);
1035
+ } catch (rollbackError) {
1036
+ throw new AggregateError([error, rollbackError], `pluginInventory: failed to persist and roll back ${request.entryId}`);
1037
+ }
1038
+ throw error;
1039
+ }
1040
+ return {
1041
+ snapshot: await this.list(),
1042
+ restartRequired: false
1043
+ };
1044
+ });
1045
+ }
1046
+ /**
1047
+ * Project every persisted OpenAI/Codex marketplace source.
1048
+ * @returns OpenAI/Codex marketplace sources in persisted order.
1049
+ */
1050
+ listMarketplaces() {
1051
+ return this.marketplaceSnapshot();
1052
+ }
1053
+ /**
1054
+ * Persist one validated OpenAI/Codex marketplace source, replacing any source of the same location.
1055
+ * @param request - marketplace location with its optional Git ref and sparse paths.
1056
+ * @returns The refreshed OpenAI/Codex marketplace source list.
1057
+ */
1058
+ addMarketplace(request) {
1059
+ return this.enqueue(async () => {
1060
+ const loaded = await loadMarketplace(request);
1061
+ const sources = this.marketplaceSettings.get().sources;
1062
+ const existing = sources.findIndex((source) => source.source === loaded.source.source);
1063
+ const entry = {
1064
+ ...loaded.source,
1065
+ enabled: sources[existing]?.enabled ?? true
1066
+ };
1067
+ const next = existing === -1 ? [...sources, entry] : sources.map((source, index) => index === existing ? entry : source);
1068
+ await this.marketplaceSettings.update({ sources: next });
1069
+ return this.marketplaceSnapshot();
1070
+ });
1071
+ }
1072
+ /**
1073
+ * Enable or disable one marketplace slot, cascading to its imported plugins.
1074
+ * @param request - marketplace source and its desired enablement.
1075
+ * @returns The refreshed OpenAI/Codex marketplace source list.
1076
+ */
1077
+ setMarketplaceEnabled(request) {
1078
+ return this.enqueue(async () => {
1079
+ const sources = this.marketplaceSettings.get().sources;
1080
+ const index = sources.findIndex((candidate) => candidate.source === request.source);
1081
+ if (index === -1) throw new Error(`pluginInventory: marketplace ${request.source} is not configured`);
1082
+ const next = sources.map((candidate, position) => position === index ? {
1083
+ ...candidate,
1084
+ enabled: request.enabled
1085
+ } : candidate);
1086
+ await this.marketplaceSettings.update({ sources: next });
1087
+ await this.cascadeMarketplacePlugins(request.source, (runtime, identity) => request.enabled ? runtime.enable(identity) : runtime.disable(identity));
1088
+ return this.marketplaceSnapshot();
1089
+ });
1090
+ }
1091
+ /**
1092
+ * Remove one persisted OpenAI/Codex marketplace source and uninstall its imported plugins.
1093
+ * @param source - the persisted marketplace location to delete.
1094
+ * @returns The refreshed OpenAI/Codex marketplace source list.
1095
+ */
1096
+ removeMarketplace(source) {
1097
+ return this.enqueue(async () => {
1098
+ if (!this.marketplaceSettings.get().sources.some((candidate) => candidate.source === source)) throw new Error(`pluginInventory: marketplace ${source} is not configured`);
1099
+ await this.cascadeMarketplacePlugins(source, (runtime, identity) => runtime.remove(identity));
1100
+ const next = this.marketplaceSettings.get().sources.filter((candidate) => candidate.source !== source);
1101
+ await this.marketplaceSettings.update({ sources: next });
1102
+ return await this.marketplaceSnapshot();
1103
+ });
1104
+ }
1105
+ /**
1106
+ * Apply one lifecycle action to every plugin imported from this marketplace root.
1107
+ * A no-op when the imported-plugin runtime is not part of this composition.
1108
+ */
1109
+ async cascadeMarketplacePlugins(marketplace, action) {
1110
+ const runtime = this.ctx.get("importedPlugins");
1111
+ if (runtime === void 0) return;
1112
+ const plugins = (await runtime.list()).plugins.filter((plugin) => plugin.source.marketplace === marketplace);
1113
+ const failures = [];
1114
+ for (const plugin of plugins) try {
1115
+ await action(runtime, plugin.identity);
1116
+ } catch (error) {
1117
+ failures.push(error);
1118
+ }
1119
+ if (failures.length === 1) throw failures[0];
1120
+ if (failures.length > 1) throw new AggregateError(failures, `pluginInventory: failed to update plugins for marketplace ${marketplace}`);
1121
+ }
1122
+ /**
1123
+ * List source-qualified imported OpenAI/Codex plugin bundles.
1124
+ * @returns Current imported-plugin projection.
1125
+ */
1126
+ listImportedPlugins() {
1127
+ return this.importedPlugins().list();
1128
+ }
1129
+ /**
1130
+ * Stage a direct local/Git source or marketplace bundle through the shared runtime.
1131
+ * @param source - Plugin source to import.
1132
+ * @returns Current imported-plugin projection.
1133
+ */
1134
+ importPlugin(source) {
1135
+ return this.importedPlugins().import(source);
1136
+ }
1137
+ /**
1138
+ * Read one imported bundle by source-qualified identity or an unambiguous name.
1139
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1140
+ * @returns Current plugin projection.
1141
+ */
1142
+ infoPlugin(identityOrName) {
1143
+ return this.importedPlugins().info(identityOrName);
1144
+ }
1145
+ /**
1146
+ * Enable and load one imported bundle without changing native Hydra package state.
1147
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1148
+ * @returns Current imported-plugin projection.
1149
+ */
1150
+ enablePlugin(identityOrName) {
1151
+ return this.importedPlugins().enable(identityOrName);
1152
+ }
1153
+ /**
1154
+ * Disable and deterministically unload one imported bundle.
1155
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1156
+ * @returns Current imported-plugin projection.
1157
+ */
1158
+ disablePlugin(identityOrName) {
1159
+ return this.importedPlugins().disable(identityOrName);
1160
+ }
1161
+ /**
1162
+ * Change one imported plugin MCP server without changing hook trust.
1163
+ * @param request - plugin identity, server name, and desired enablement.
1164
+ * @returns Current imported-plugin projection.
1165
+ */
1166
+ setPluginMcpServerEnabled(request) {
1167
+ return this.importedPlugins().setMcpServerEnabled(request.identity, request.server, request.enabled);
1168
+ }
1169
+ /**
1170
+ * Set one imported MCP tool's approval mode without changing server enablement.
1171
+ * @param request - Plugin identity, server, tool, and approval mode.
1172
+ * @returns Current imported-plugin projection.
1173
+ */
1174
+ setPluginMcpToolApproval(request) {
1175
+ return this.importedPlugins().setMcpToolApproval(request.identity, request.server, request.tool, request.approval);
1176
+ }
1177
+ /**
1178
+ * Trust only the current hook definitions; MCP/tool approval remains independent.
1179
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1180
+ * @returns Current imported-plugin projection.
1181
+ */
1182
+ trustPlugin(identityOrName) {
1183
+ return this.importedPlugins().trustHooks(identityOrName);
1184
+ }
1185
+ /**
1186
+ * Revoke hook trust without disabling other plugin components.
1187
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1188
+ * @returns Current imported-plugin projection.
1189
+ */
1190
+ untrustPlugin(identityOrName) {
1191
+ return this.importedPlugins().untrustHooks(identityOrName);
1192
+ }
1193
+ /**
1194
+ * Remove one imported bundle and its writable plugin data.
1195
+ * @param identityOrName - Source-qualified identity or unambiguous plugin name.
1196
+ * @returns Current imported-plugin projection.
1197
+ */
1198
+ removePlugin(identityOrName) {
1199
+ return this.importedPlugins().remove(identityOrName);
1200
+ }
1201
+ /**
1202
+ * List the user's own MCP server records with their live state.
1203
+ * @param request - Workspace to inspect; omission selects the user's global records.
1204
+ * @returns Selected settings records with runtime status and the workspace target.
1205
+ */
1206
+ listMcpServers(request) {
1207
+ const target = request ?? {};
1208
+ this.assertTarget(target);
1209
+ if (target.workspaceId !== void 0) return this.workspaceTarget(target.workspaceId, target.layer).then(({ ctx, settings, target: selected }) => ({
1210
+ ...this.mcpServers(ctx).list(settings),
1211
+ ...selected
1212
+ }));
1213
+ return Promise.resolve(this.mcpServers().list());
1214
+ }
1215
+ /**
1216
+ * Store one complete MCP server definition and converge its mount.
1217
+ * @param request - Complete server definition and optional workspace file to edit.
1218
+ * @returns Refreshed MCP record projection.
1219
+ */
1220
+ defineMcpServer(request) {
1221
+ this.assertTarget(request);
1222
+ if (request.workspaceId !== void 0) return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1223
+ const registry = this.mcpServers(ctx);
1224
+ await registry.define(request, settings);
1225
+ return {
1226
+ ...registry.list(settings),
1227
+ ...target
1228
+ };
1229
+ });
1230
+ return this.mcpServers().define(request);
1231
+ }
1232
+ /**
1233
+ * Change one MCP record's desired state without touching its definition.
1234
+ * @param request - Target record, desired state, and optional workspace file to edit.
1235
+ * @returns Refreshed MCP record projection.
1236
+ */
1237
+ setMcpServerEnabled(request) {
1238
+ this.assertTarget(request);
1239
+ if (request.workspaceId !== void 0) return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1240
+ const registry = this.mcpServers(ctx);
1241
+ await registry.setEnabled(request, settings);
1242
+ return {
1243
+ ...registry.list(settings),
1244
+ ...target
1245
+ };
1246
+ });
1247
+ return this.mcpServers().setEnabled(request);
1248
+ }
1249
+ /**
1250
+ * Remove one MCP record and unmount its server.
1251
+ * @param request - Stored record name and optional workspace file to edit; a string selects global records.
1252
+ * @returns Refreshed MCP record projection.
1253
+ */
1254
+ removeMcpServer(request) {
1255
+ if (typeof request !== "string") this.assertTarget(request);
1256
+ if (typeof request !== "string" && request.workspaceId !== void 0) return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1257
+ const registry = this.mcpServers(ctx);
1258
+ await registry.remove(request.name, settings);
1259
+ return {
1260
+ ...registry.list(settings),
1261
+ ...target
1262
+ };
1263
+ });
1264
+ return this.mcpServers().remove(typeof request === "string" ? request : request.name);
1265
+ }
1266
+ /**
1267
+ * List the user's own hook records with their live state.
1268
+ * @param request - Workspace to inspect; omission selects the user's global records.
1269
+ * @returns Selected settings records with runtime status and the workspace target.
1270
+ */
1271
+ listHookRecords(request) {
1272
+ const target = request ?? {};
1273
+ this.assertTarget(target);
1274
+ if (target.workspaceId !== void 0) return this.workspaceTarget(target.workspaceId, target.layer).then(({ ctx, settings, target: selected }) => ({
1275
+ ...this.hookRecords(ctx).list(settings),
1276
+ ...selected
1277
+ }));
1278
+ return Promise.resolve(this.hookRecords().list());
1279
+ }
1280
+ /**
1281
+ * Store one complete hook record definition and converge its bridge mount.
1282
+ * @param request - Complete record definition and optional workspace file to edit.
1283
+ * @returns Refreshed hook record projection.
1284
+ */
1285
+ defineHookRecord(request) {
1286
+ this.assertTarget(request);
1287
+ if (request.workspaceId !== void 0) return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1288
+ const registry = this.hookRecords(ctx);
1289
+ await registry.define(request, settings);
1290
+ return {
1291
+ ...registry.list(settings),
1292
+ ...target
1293
+ };
1294
+ });
1295
+ return this.hookRecords().define(request);
1296
+ }
1297
+ /**
1298
+ * Change one hook record's desired state without touching its definition.
1299
+ * @param request - Target record, desired state, and optional workspace file to edit.
1300
+ * @returns Refreshed hook record projection.
1301
+ */
1302
+ setHookRecordEnabled(request) {
1303
+ this.assertTarget(request);
1304
+ if (request.workspaceId !== void 0) return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1305
+ const registry = this.hookRecords(ctx);
1306
+ await registry.setEnabled(request, settings);
1307
+ return {
1308
+ ...registry.list(settings),
1309
+ ...target
1310
+ };
1311
+ });
1312
+ return this.hookRecords().setEnabled(request);
1313
+ }
1314
+ /**
1315
+ * Remove one hook record and unmount its bridge.
1316
+ * @param request - Stored record name and optional workspace file to edit; a string selects global records.
1317
+ * @returns Refreshed hook record projection.
1318
+ */
1319
+ removeHookRecord(request) {
1320
+ if (typeof request !== "string") this.assertTarget(request);
1321
+ if (typeof request !== "string" && request.workspaceId !== void 0) return this.workspaceTarget(request.workspaceId, request.layer).then(async ({ ctx, settings, target }) => {
1322
+ const registry = this.hookRecords(ctx);
1323
+ await registry.remove(request.name, settings);
1324
+ return {
1325
+ ...registry.list(settings),
1326
+ ...target
1327
+ };
1328
+ });
1329
+ return this.hookRecords().remove(typeof request === "string" ? request : request.name);
1330
+ }
1331
+ /** Reject a workspace file selector that cannot identify a workspace. */
1332
+ assertTarget(target) {
1333
+ if (target.workspaceId === void 0 && target.layer !== void 0) throw new Error("pluginInventory: layer requires workspaceId");
1334
+ }
1335
+ /** Resolve one workspace's runtime services and selected editable settings file. */
1336
+ async workspaceTarget(workspaceId, layer = "local") {
1337
+ const service = this.ctx.get("workspaceConfig");
1338
+ if (service === void 0) throw new Error("pluginInventory: workspace configuration is unavailable");
1339
+ const environment = await service.environmentFor(workspaceId);
1340
+ const settings = await service.settingsFor(workspaceId, layer);
1341
+ return {
1342
+ ctx: environment.ctx,
1343
+ presets: environment.presets,
1344
+ settings,
1345
+ target: {
1346
+ workspaceId,
1347
+ layer
1348
+ }
1349
+ };
1350
+ }
1351
+ importedPlugins() {
1352
+ const runtime = this.ctx.get("importedPlugins");
1353
+ if (runtime === void 0) throw new Error("pluginInventory: imported plugin runtime is unavailable");
1354
+ return runtime;
1355
+ }
1356
+ /** The MCP record registry is optional in compositions without user MCP servers. */
1357
+ mcpServers(ctx = this.ctx) {
1358
+ const registry = ctx.get("mcpServers");
1359
+ if (registry === void 0) throw new Error("pluginInventory: MCP server registry is unavailable");
1360
+ return registry;
1361
+ }
1362
+ /** The hook record registry is optional in compositions without user hooks. */
1363
+ hookRecords(ctx = this.ctx) {
1364
+ const registry = ctx.get("hookRecords");
1365
+ if (registry === void 0) throw new Error("pluginInventory: hook record registry is unavailable");
1366
+ return registry;
1367
+ }
1368
+ /** Agent-preset ownership is optional in headless and non-roster compositions. */
1369
+ agentPresets() {
1370
+ return this.ctx.get("agentPresets");
1371
+ }
1372
+ };
1373
+ })();
1374
+ //#endregion
1375
+ export { PluginInventoryGateway, PluginInventoryGateway as default };