@jini-ai/core 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 (131) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +19 -1
  3. package/dist/agent-tools.d.ts +1 -0
  4. package/dist/approval.d.ts +134 -0
  5. package/dist/approval.js +141 -0
  6. package/dist/fetch/cache.d.ts +129 -0
  7. package/dist/fetch/cache.js +243 -0
  8. package/dist/fetch/index.d.ts +5 -0
  9. package/dist/fetch/index.js +3 -0
  10. package/dist/fetch/types.d.ts +93 -0
  11. package/dist/fetch/types.js +2 -0
  12. package/dist/index.d.ts +6 -1
  13. package/dist/index.js +6 -1
  14. package/dist/model-facing-tool-errors.d.ts +152 -0
  15. package/dist/model-facing-tool-errors.js +124 -0
  16. package/dist/native-regexp.d.ts +10 -0
  17. package/dist/native-regexp.js +2 -0
  18. package/dist/primitives/index.d.ts +2 -0
  19. package/dist/primitives/index.js +1 -0
  20. package/dist/primitives/sse.d.ts +60 -0
  21. package/dist/primitives/sse.js +122 -0
  22. package/dist/redact.js +4 -3
  23. package/dist/registration-kit.d.ts +13 -0
  24. package/dist/registration-kit.js +12 -0
  25. package/dist/settings/agent-tools.d.ts +10 -0
  26. package/dist/settings/agent-tools.js +204 -0
  27. package/dist/settings/agent-value-write-tools.d.ts +22 -0
  28. package/dist/settings/agent-value-write-tools.js +157 -0
  29. package/dist/settings/agent-writable-preferences.d.ts +67 -0
  30. package/dist/settings/agent-writable-preferences.js +132 -0
  31. package/dist/settings/agent-write-denylist.d.ts +19 -0
  32. package/dist/settings/agent-write-denylist.js +15 -0
  33. package/dist/settings/change-feed.d.ts +83 -0
  34. package/dist/settings/change-feed.js +61 -0
  35. package/dist/settings/definitions-dispatch.d.ts +56 -0
  36. package/dist/settings/definitions-dispatch.js +86 -0
  37. package/dist/settings/dictionaries/index.d.ts +48 -0
  38. package/dist/settings/dictionaries/index.js +69 -0
  39. package/dist/settings/dictionaries/settings-dialog.ar.d.ts +3 -0
  40. package/dist/settings/dictionaries/settings-dialog.ar.js +27 -0
  41. package/dist/settings/dictionaries/settings-dialog.bn.d.ts +3 -0
  42. package/dist/settings/dictionaries/settings-dialog.bn.js +27 -0
  43. package/dist/settings/dictionaries/settings-dialog.de.d.ts +3 -0
  44. package/dist/settings/dictionaries/settings-dialog.de.js +27 -0
  45. package/dist/settings/dictionaries/settings-dialog.es.d.ts +3 -0
  46. package/dist/settings/dictionaries/settings-dialog.es.js +27 -0
  47. package/dist/settings/dictionaries/settings-dialog.fa.d.ts +3 -0
  48. package/dist/settings/dictionaries/settings-dialog.fa.js +27 -0
  49. package/dist/settings/dictionaries/settings-dialog.fr.d.ts +3 -0
  50. package/dist/settings/dictionaries/settings-dialog.fr.js +27 -0
  51. package/dist/settings/dictionaries/settings-dialog.hi.d.ts +3 -0
  52. package/dist/settings/dictionaries/settings-dialog.hi.js +27 -0
  53. package/dist/settings/dictionaries/settings-dialog.hu.d.ts +3 -0
  54. package/dist/settings/dictionaries/settings-dialog.hu.js +27 -0
  55. package/dist/settings/dictionaries/settings-dialog.id.d.ts +3 -0
  56. package/dist/settings/dictionaries/settings-dialog.id.js +27 -0
  57. package/dist/settings/dictionaries/settings-dialog.it.d.ts +3 -0
  58. package/dist/settings/dictionaries/settings-dialog.it.js +27 -0
  59. package/dist/settings/dictionaries/settings-dialog.ja.d.ts +3 -0
  60. package/dist/settings/dictionaries/settings-dialog.ja.js +27 -0
  61. package/dist/settings/dictionaries/settings-dialog.ko.d.ts +3 -0
  62. package/dist/settings/dictionaries/settings-dialog.ko.js +27 -0
  63. package/dist/settings/dictionaries/settings-dialog.pl.d.ts +3 -0
  64. package/dist/settings/dictionaries/settings-dialog.pl.js +27 -0
  65. package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +3 -0
  66. package/dist/settings/dictionaries/settings-dialog.pt-BR.js +27 -0
  67. package/dist/settings/dictionaries/settings-dialog.ru.d.ts +3 -0
  68. package/dist/settings/dictionaries/settings-dialog.ru.js +27 -0
  69. package/dist/settings/dictionaries/settings-dialog.th.d.ts +3 -0
  70. package/dist/settings/dictionaries/settings-dialog.th.js +27 -0
  71. package/dist/settings/dictionaries/settings-dialog.tr.d.ts +3 -0
  72. package/dist/settings/dictionaries/settings-dialog.tr.js +27 -0
  73. package/dist/settings/dictionaries/settings-dialog.uk.d.ts +3 -0
  74. package/dist/settings/dictionaries/settings-dialog.uk.js +27 -0
  75. package/dist/settings/dictionaries/settings-dialog.ur.d.ts +3 -0
  76. package/dist/settings/dictionaries/settings-dialog.ur.js +27 -0
  77. package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +3 -0
  78. package/dist/settings/dictionaries/settings-dialog.zh-CN.js +27 -0
  79. package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +3 -0
  80. package/dist/settings/dictionaries/settings-dialog.zh-TW.js +27 -0
  81. package/dist/settings/ensure-definitions.d.ts +101 -0
  82. package/dist/settings/ensure-definitions.js +101 -0
  83. package/dist/settings/errors.d.ts +42 -0
  84. package/dist/settings/errors.js +46 -0
  85. package/dist/settings/express/cms-adapter.d.ts +20 -0
  86. package/dist/settings/express/cms-adapter.js +124 -0
  87. package/dist/settings/express/contracts.d.ts +135 -0
  88. package/dist/settings/express/contracts.js +2 -0
  89. package/dist/settings/express/http.d.ts +10 -0
  90. package/dist/settings/express/http.js +211 -0
  91. package/dist/settings/express/index.d.ts +5 -0
  92. package/dist/settings/express/index.js +4 -0
  93. package/dist/settings/express/shared.d.ts +68 -0
  94. package/dist/settings/express/shared.js +54 -0
  95. package/dist/settings/express/stream.d.ts +24 -0
  96. package/dist/settings/express/stream.js +147 -0
  97. package/dist/settings/index.d.ts +40 -0
  98. package/dist/settings/index.js +37 -0
  99. package/dist/settings/ports.d.ts +118 -0
  100. package/dist/settings/ports.js +2 -0
  101. package/dist/settings/principal-lookup.d.ts +9 -0
  102. package/dist/settings/principal-lookup.js +2 -0
  103. package/dist/settings/purge-service.d.ts +54 -0
  104. package/dist/settings/purge-service.js +106 -0
  105. package/dist/settings/repo.memory.d.ts +119 -0
  106. package/dist/settings/repo.memory.js +199 -0
  107. package/dist/settings/settings.d.ts +139 -0
  108. package/dist/settings/settings.js +259 -0
  109. package/dist/settings/tool-registrations.d.ts +54 -0
  110. package/dist/settings/tool-registrations.js +250 -0
  111. package/dist/settings/types.d.ts +125 -0
  112. package/dist/settings/types.js +3 -0
  113. package/dist/settings/ui-tab-definitions.d.ts +41 -0
  114. package/dist/settings/ui-tab-definitions.js +148 -0
  115. package/dist/settings/write-service.d.ts +320 -0
  116. package/dist/settings/write-service.js +683 -0
  117. package/dist/surface-exchanges.d.ts +209 -0
  118. package/dist/surface-exchanges.js +55 -0
  119. package/dist/text/escape.d.ts +20 -0
  120. package/dist/text/escape.js +23 -0
  121. package/dist/text/index.d.ts +1 -0
  122. package/dist/text/index.js +1 -0
  123. package/dist/tool-metadata.d.ts +71 -0
  124. package/dist/tool-metadata.js +56 -0
  125. package/dist/tool-permissions.d.ts +76 -0
  126. package/dist/tool-permissions.js +57 -0
  127. package/dist/tool-registry.d.ts +19 -1
  128. package/dist/tool-registry.js +44 -0
  129. package/package.json +79 -4
  130. package/plugin-directory.d.ts +32 -0
  131. package/plugin-directory.js +140 -0
@@ -33,6 +33,48 @@ export class ToolInputError extends Error {
33
33
  }
34
34
  }
35
35
  const registrationsByRegistry = new WeakMap();
36
+ // Static register() remains append-only; scoped host contributions below explicitly own removal.
37
+ const registryRevisions = new WeakMap();
38
+ /** A host can invalidate its disposable discovery snapshot without exposing handlers. */
39
+ export function toolRegistryRevision(required, _optional = {}) {
40
+ return registryRevisions.get(required.registry);
41
+ }
42
+ /** Revocable registration scope on the SAME registry the executor owns. Collision validation
43
+ * completes before mutation, so a failed replacement cannot lose the working generation.
44
+ * Only this closure may remove its own registrations; native and other scopes remain protected.
45
+ * Already-authorized closures are revoked too: confirmation can await a human across disable. */
46
+ export function createToolRegistrationScope(required, _optional = {}) {
47
+ const registrations = registrationsByRegistry.get(required.registry);
48
+ if (!registrations)
49
+ throw new Error('registration scopes require a core ToolRegistry');
50
+ let owned = new Set();
51
+ let generation = 0;
52
+ return {
53
+ replace({ registrations: next }, _optional = {}) {
54
+ const ids = new Set();
55
+ for (const registration of next) {
56
+ const id = registration.descriptor.id;
57
+ if (ids.has(id) || (registrations.has(id) && !owned.has(id)))
58
+ throw new Error(`ToolRegistry: tool "${id}" is already registered`);
59
+ ids.add(id);
60
+ }
61
+ generation++;
62
+ const current = generation;
63
+ for (const id of owned)
64
+ registrations.delete(id);
65
+ for (const registration of next)
66
+ registrations.set(registration.descriptor.id, { ...registration,
67
+ handler: (input, options) => {
68
+ if (generation !== current)
69
+ throw new Error('tool registration was revoked');
70
+ return registration.handler(input, options);
71
+ },
72
+ });
73
+ owned = ids;
74
+ registryRevisions.set(required.registry, (registryRevisions.get(required.registry) ?? 0) + 1);
75
+ },
76
+ };
77
+ }
36
78
  /**
37
79
  * Creates an empty, append-only `ToolRegistry`.
38
80
  *
@@ -51,6 +93,7 @@ export function createToolRegistry(_required) {
51
93
  throw new Error(`ToolRegistry: tool "${registration.descriptor.id}" is already registered`);
52
94
  }
53
95
  registrations.set(registration.descriptor.id, registration);
96
+ registryRevisions.set(registry, (registryRevisions.get(registry) ?? 0) + 1);
54
97
  },
55
98
  has({ toolId }) {
56
99
  return registrations.has(toolId);
@@ -60,6 +103,7 @@ export function createToolRegistry(_required) {
60
103
  },
61
104
  };
62
105
  registrationsByRegistry.set(registry, registrations);
106
+ registryRevisions.set(registry, 0);
63
107
  return registry;
64
108
  }
65
109
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/core",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "The Jini kernel: typed DI tokens, pack composition and lifecycle, the tool registry, redaction, and origin/API-token auth primitives. Zero dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -20,7 +20,50 @@
20
20
  "./contribution-registry": "universal",
21
21
  "./naming": "universal",
22
22
  "./primitives": "universal",
23
- "./text": "universal"
23
+ "./fetch": "universal",
24
+ "./text": "universal",
25
+ "./settings": "universal",
26
+ "./settings/express": "node",
27
+ "./plugin-directory": "universal"
28
+ },
29
+ "isolation": {
30
+ "entries": {
31
+ "./primitives": "core",
32
+ "./fetch": "fetch",
33
+ "./settings": "settings",
34
+ "./settings/express": "settings"
35
+ },
36
+ "domains": {
37
+ "fetch": [
38
+ "src/fetch"
39
+ ],
40
+ "settings": [
41
+ "src/settings"
42
+ ]
43
+ },
44
+ "shared": [
45
+ "src/tool-permissions.ts"
46
+ ],
47
+ "forbidden": {
48
+ "universal": [
49
+ "node:",
50
+ "express",
51
+ "react",
52
+ "@jini-ai/cms"
53
+ ],
54
+ ".": [
55
+ "node:",
56
+ "express",
57
+ "react",
58
+ "@jini-ai/cms",
59
+ "@jini-ai/core/settings",
60
+ "./settings",
61
+ "../settings"
62
+ ],
63
+ "./settings/express": [
64
+ "@jini-ai/cms"
65
+ ]
66
+ }
24
67
  }
25
68
  },
26
69
  "type": "module",
@@ -63,10 +106,30 @@
63
106
  "import": "./dist/primitives/index.js",
64
107
  "default": "./dist/primitives/index.js"
65
108
  },
109
+ "./fetch": {
110
+ "types": "./dist/fetch/index.d.ts",
111
+ "import": "./dist/fetch/index.js",
112
+ "default": "./dist/fetch/index.js"
113
+ },
66
114
  "./text": {
67
115
  "types": "./dist/text/index.d.ts",
68
116
  "import": "./dist/text/index.js",
69
117
  "default": "./dist/text/index.js"
118
+ },
119
+ "./settings": {
120
+ "types": "./dist/settings/index.d.ts",
121
+ "import": "./dist/settings/index.js",
122
+ "default": "./dist/settings/index.js"
123
+ },
124
+ "./settings/express": {
125
+ "types": "./dist/settings/express/index.d.ts",
126
+ "import": "./dist/settings/express/index.js",
127
+ "default": "./dist/settings/express/index.js"
128
+ },
129
+ "./plugin-directory": {
130
+ "types": "./plugin-directory.d.ts",
131
+ "import": "./plugin-directory.js",
132
+ "default": "./plugin-directory.js"
70
133
  }
71
134
  },
72
135
  "files": [
@@ -80,14 +143,26 @@
80
143
  "!dist/**/*.test.d.ts.map",
81
144
  "!dist/**/__tests__/**",
82
145
  "!dist/**/*.typecheck.*",
83
- "!dist/**/*.map"
146
+ "!dist/**/*.map",
147
+ "plugin-directory.js",
148
+ "plugin-directory.d.ts"
84
149
  ],
85
150
  "publishConfig": {
86
151
  "access": "public",
87
152
  "registry": "https://registry.npmjs.org"
88
153
  },
89
154
  "devDependencies": {
90
- "@vitest/coverage-v8": "^2.1.9"
155
+ "@vitest/coverage-v8": "^2.1.9",
156
+ "express": "^4.21.0",
157
+ "@types/express": "^4.17.21"
158
+ },
159
+ "peerDependencies": {
160
+ "express": "^4.21.0"
161
+ },
162
+ "peerDependenciesMeta": {
163
+ "express": {
164
+ "optional": true
165
+ }
91
166
  },
92
167
  "scripts": {
93
168
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,32 @@
1
+ /** Identical identity metadata for portable agent packages and host site plugins. */
2
+ export interface PluginIdentityExtension {
3
+ readonly id?: string;
4
+ readonly provides?: readonly string[];
5
+ readonly [key: string]: unknown;
6
+ }
7
+ export interface PluginIdentity {
8
+ readonly id: string;
9
+ readonly keywords: readonly string[];
10
+ readonly provides: readonly string[];
11
+ readonly warnings: readonly string[];
12
+ }
13
+ export interface PluginProvider {
14
+ readonly id: string;
15
+ readonly version: string | null;
16
+ readonly enabled: boolean;
17
+ readonly family: string;
18
+ /** Stable storage/activation key; publisher identity never becomes a filesystem path. */
19
+ readonly localId: string;
20
+ readonly keywords: readonly string[];
21
+ readonly provides: readonly string[];
22
+ }
23
+ export interface PluginLookupPort {
24
+ getPlugin(required: { readonly id: string; readonly family?: string }, optional?: Record<string, never>): Promise<PluginProvider | null>;
25
+ findProviders(required: { readonly capability: string }, optional?: { readonly enabledOnly?: boolean }): Promise<readonly PluginProvider[]>;
26
+ }
27
+ export type PluginIdentityCandidate = Pick<PluginProvider, 'id' | 'family' | 'localId'>;
28
+ export function assertPluginIdentityAvailable(required: { readonly candidate: PluginIdentityCandidate; readonly installed: readonly PluginProvider[] }, optional?: Record<string, never>): void;
29
+ export function createPluginLookupFacade(required: { readonly lookup: PluginLookupPort }, optional?: Record<string, never>): PluginLookupPort;
30
+ export function validatePluginIdentity(required: { readonly manifest: unknown; readonly namespace: string }, optional?: Record<string, never>): { readonly errors: readonly string[] };
31
+ export function readPluginIdentity(required: { readonly manifest: Readonly<Record<string, unknown>>; readonly namespace: string; readonly name?: string }, optional?: Record<string, never>): PluginIdentity;
32
+ export function createPluginLookup(required: { readonly listPlugins: (required: Record<string, never>, optional?: Record<string, never>) => Promise<readonly PluginProvider[]> }, optional?: { readonly warn?: (message: string) => void }): PluginLookupPort;
@@ -0,0 +1,140 @@
1
+ /** Shared publisher identity and live plugin lookup. Source-backed, like persistent-state:
2
+ * hosts can adopt the additive entry without replacing a running server's compiled packages. */
3
+ const SEGMENT = '[a-z0-9]+(?:[.-][a-z0-9]+)*';
4
+ const ID_PATTERN = new RegExp(`^${SEGMENT}/${SEGMENT}$`);
5
+ // Legacy site ids used a looser grammar than portable Agent Plugin names. Preserve those keys
6
+ // in unknown/<name>; only explicitly declared publisher ids use the new strict grammar.
7
+ const LEGACY_NAME_PATTERN = /^[a-z0-9.-]+$/;
8
+ const CAPABILITY_PATTERN = new RegExp(`^${SEGMENT}(?::${SEGMENT})?$`);
9
+ const object = value => typeof value === 'object' && value !== null && !Array.isArray(value);
10
+
11
+ /** Collects identity errors without mutating the portable manifest. Absent metadata is legacy.
12
+ * @param {{manifest: unknown, namespace: string}} required
13
+ * @param {Record<string, never>} [optional]
14
+ * @returns {{errors: string[]}}
15
+ * @complexity O(k + p) in keyword and provides entries; no I/O.
16
+ */
17
+ export function validatePluginIdentity({ manifest, namespace }, optional = {}) {
18
+ const errors = [];
19
+ if (!object(manifest)) return { errors: ['Plugin manifest must be an object.'] };
20
+ if (manifest.extensions !== undefined && !object(manifest.extensions)) return { errors: [...errors, 'Plugin extensions must be an object.'] };
21
+ const extension = manifest.extensions?.[namespace];
22
+ if (extension === undefined) return { errors };
23
+ if (!object(extension)) return { errors: [...errors, 'Plugin identity extension must be an object.'] };
24
+ if (extension.id !== undefined && (typeof extension.id !== 'string' || extension.id.length > 129 || !ID_PATTERN.test(extension.id))) {
25
+ errors.push('Plugin id must have the form publisher/name (lowercase letters, digits, dots and hyphens).');
26
+ } else if (typeof extension.id === 'string' && extension.id.startsWith('unknown/')) {
27
+ // Only the host may infer a legacy identity; a publisher cannot claim its upgrade exemption.
28
+ errors.push("Plugin publisher 'unknown' is reserved for legacy identities.");
29
+ }
30
+ if (extension.provides !== undefined && (!Array.isArray(extension.provides) || !extension.provides.every(p => typeof p === 'string' && p.length <= 129 && CAPABILITY_PATTERN.test(p)))) {
31
+ errors.push('Plugin provides must be an array of capability names.');
32
+ }
33
+ return { errors };
34
+ }
35
+
36
+ /** Resolves one identity; warns on missing ids and refuses malformed declarations.
37
+ * @param {{manifest: Record<string, unknown>, namespace: string, name?: string}} required
38
+ * @param {Record<string, never>} [optional]
39
+ * @returns {{id: string, keywords: string[], provides: string[], warnings: string[]}}
40
+ * @throws {TypeError} Malformed identity metadata or missing legacy name.
41
+ * @example readPluginIdentity({ manifest: { name: 'example' }, namespace: 'host' })
42
+ * @complexity O(k + p), copying arrays so a caller cannot mutate manifest metadata.
43
+ */
44
+ export function readPluginIdentity({ manifest, namespace, name }, optional = {}) {
45
+ const { errors } = validatePluginIdentity({ manifest, namespace });
46
+ if (errors.length) throw new TypeError(errors.join(' '));
47
+ const extension = manifest.extensions?.[namespace];
48
+ const localName = name ?? manifest.name;
49
+ if (extension?.id === undefined && (typeof localName !== 'string' || !LEGACY_NAME_PATTERN.test(localName))) {
50
+ throw new TypeError('A valid plugin name is required for a legacy identity.');
51
+ }
52
+ const id = extension?.id ?? `unknown/${localName}`;
53
+ // Keywords are advisory portable metadata: older manifests accepted mixed or non-array values.
54
+ // The portable parser's coercion (non-string entries dropped, non-arrays ignored) lives here
55
+ // so validation and lookup normalize advisory metadata through the same owner.
56
+ const keywords = Array.isArray(manifest.keywords) ? manifest.keywords.filter(k => typeof k === 'string' && k.trim().length > 0) : [];
57
+ const warnings = extension?.id === undefined ? [`Plugin '${localName}' has no publisher-qualified id; using '${id}'.`] : [];
58
+ if (manifest.keywords !== undefined && (!Array.isArray(manifest.keywords) || keywords.length !== manifest.keywords.length)) {
59
+ warnings.push('Plugin keywords must be an array of nonempty strings; invalid keywords were ignored.');
60
+ }
61
+ return { id, keywords, provides: [...(extension?.provides ?? [])], warnings };
62
+ }
63
+
64
+ /** Rejects id reuse and publisher changes at an existing storage slot. Legacy metadata may
65
+ * acquire its first publisher id during an upgrade; a declared publisher can never be replaced.
66
+ * @param {{candidate: {id: string, family: string, localId: string}, installed: import('./plugin-directory.js').PluginProvider[]}} required
67
+ * @param {Record<string, never>} [optional]
68
+ * @returns {void}
69
+ * @throws {Error} Conflicting publisher identity.
70
+ * @complexity O(n) for n installed providers.
71
+ */
72
+ export function assertPluginIdentityAvailable({ candidate, installed }, optional = {}) {
73
+ for (const plugin of installed) {
74
+ const sameSlot = plugin.family === candidate.family && plugin.localId === candidate.localId;
75
+ if (sameSlot) {
76
+ if (plugin.id !== candidate.id && plugin.id !== `unknown/${candidate.localId}`) {
77
+ throw new Error(`Plugin '${candidate.localId}' belongs to a different publisher identity.`);
78
+ }
79
+ } else if (plugin.id === candidate.id && !(plugin.family !== candidate.family && candidate.id === `unknown/${candidate.localId}` && plugin.id === `unknown/${plugin.localId}`)) {
80
+ throw new Error(`Duplicate plugin id '${candidate.id}'.`);
81
+ }
82
+ }
83
+ }
84
+
85
+ /** Restricts an SDK handle at runtime, not just through a TypeScript interface. A host may carry
86
+ * lifecycle mutation ports, but a plugin's discovery surface exposes only these metadata reads.
87
+ * @param {{lookup: import('./plugin-directory.js').PluginLookupPort}} required
88
+ * @param {Record<string, never>} [optional]
89
+ * @returns {import('./plugin-directory.js').PluginLookupPort}
90
+ * @complexity O(1); delegates live reads without copying host methods.
91
+ */
92
+ export function createPluginLookupFacade({ lookup }, optional = {}) {
93
+ return {
94
+ getPlugin: (required, options = {}) => lookup.getPlugin(required, options),
95
+ findProviders: (required, options = {}) => lookup.findProviders(required, options),
96
+ };
97
+ }
98
+
99
+ /** Live, metadata-only directory over injected workspace-scoped sources. No plugin imports.
100
+ * Later duplicate providers are omitted with a warning, preserving the source's incumbent order.
101
+ * Admission checks the unfiltered installs separately, including disabled/cross-family collisions.
102
+ * Legacy fallback ids are family-scoped; an unscoped ambiguous get returns null.
103
+ * @param {{listPlugins: (required: {}, optional?: {}) => Promise<import('./plugin-directory.js').PluginProvider[]>}} required
104
+ * @param {{warn?: (message: string) => void}} [optional]
105
+ * @returns {import('./plugin-directory.js').PluginLookupPort}
106
+ * @example createPluginLookup({ listPlugins: async () => [] })
107
+ * @complexity O(n log n) per lookup, n installed plugins. Fresh reads preserve revocation.
108
+ */
109
+ export function createPluginLookup({ listPlugins }, optional = {}) {
110
+ const warn = optional.warn ?? console.warn;
111
+ async function snapshot() {
112
+ const plugins = await listPlugins({}, {});
113
+ const seen = new Set();
114
+ const unique = [];
115
+ for (const plugin of plugins) {
116
+ const key = plugin.id === `unknown/${plugin.localId}` ? `${plugin.family}:${plugin.id}` : plugin.id;
117
+ if (seen.has(key)) {
118
+ warn(`Duplicate plugin id '${plugin.id}'; later provider omitted from metadata lookup.`);
119
+ continue;
120
+ }
121
+ seen.add(key);
122
+ unique.push(plugin);
123
+ }
124
+ // Project metadata explicitly: host paths, services and additional source properties never
125
+ // leave this boundary, and a consumer cannot mutate the source's discovery arrays.
126
+ return unique.map(plugin => ({
127
+ id: plugin.id, version: plugin.version, enabled: plugin.enabled, family: plugin.family,
128
+ localId: plugin.localId, keywords: [...plugin.keywords], provides: [...plugin.provides],
129
+ })).sort((a, b) => a.id.localeCompare(b.id));
130
+ }
131
+ return {
132
+ async getPlugin({ id, family }, options = {}) {
133
+ const matches = (await snapshot()).filter(plugin => plugin.id === id && (family === undefined || plugin.family === family));
134
+ return matches.length === 1 ? matches[0] : null;
135
+ },
136
+ async findProviders({ capability }, { enabledOnly = false } = {}) {
137
+ return (await snapshot()).filter(plugin => (!enabledOnly || plugin.enabled) && plugin.provides.includes(capability));
138
+ },
139
+ };
140
+ }