@unoverse-platform/studio 0.1.90 → 0.1.91

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 (130) hide show
  1. package/local/plugin.ts +20 -12
  2. package/package.json +13 -3
  3. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  4. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  5. package/vendor/base/src/agent-mcp/index.ts +44 -0
  6. package/vendor/base/src/agent-mcp/invoke.ts +140 -0
  7. package/vendor/base/src/agent-mcp/lean.ts +111 -0
  8. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  9. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  10. package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
  11. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  12. package/vendor/base/src/agent-mcp/types.ts +130 -0
  13. package/vendor/base/src/auth/auth.ts +278 -0
  14. package/vendor/base/src/auth/authConfig.ts +73 -0
  15. package/vendor/base/src/boot.ts +245 -0
  16. package/vendor/base/src/definitions/definitions.ts +18 -0
  17. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  18. package/vendor/base/src/definitions/fsCache.ts +128 -0
  19. package/vendor/base/src/definitions/index.ts +19 -0
  20. package/vendor/base/src/definitions/inputs.ts +30 -0
  21. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  22. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  23. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  24. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  25. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  26. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  27. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  28. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  29. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  30. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  31. package/vendor/base/src/definitions/load/load.ts +487 -0
  32. package/vendor/base/src/definitions/model.ts +238 -0
  33. package/vendor/base/src/definitions/registry/catalog.ts +643 -0
  34. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  35. package/vendor/base/src/definitions/theme.ts +383 -0
  36. package/vendor/base/src/executor.ts +284 -0
  37. package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
  38. package/vendor/base/src/items/baseVersion.ts +96 -0
  39. package/vendor/base/src/items/collect.ts +199 -0
  40. package/vendor/base/src/items/fingerprint.ts +34 -0
  41. package/vendor/base/src/items/publish.ts +198 -0
  42. package/vendor/base/src/lint/context.mjs +56 -0
  43. package/vendor/base/src/lint/design/defs.mjs +46 -0
  44. package/vendor/base/src/lint/design/file.mjs +737 -0
  45. package/vendor/base/src/lint/design/index.d.mts +30 -0
  46. package/vendor/base/src/lint/design/index.mjs +996 -0
  47. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  48. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  49. package/vendor/base/src/lint/design/walk.mjs +444 -0
  50. package/vendor/base/src/lint/index.mjs +28 -0
  51. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  52. package/vendor/base/src/lint/nodes/index.mjs +64 -0
  53. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  54. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  55. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  56. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  57. package/vendor/base/src/logger.ts +33 -0
  58. package/vendor/base/src/manifests/compose.ts +539 -0
  59. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  60. package/vendor/base/src/manifests/executor/context.ts +107 -0
  61. package/vendor/base/src/manifests/executor/index.ts +47 -0
  62. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  63. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  64. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  65. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  66. package/vendor/base/src/manifests/executor/send.ts +65 -0
  67. package/vendor/base/src/manifests/executor/session.ts +46 -0
  68. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  69. package/vendor/base/src/manifests/index.ts +280 -0
  70. package/vendor/base/src/manifests/integrity.ts +93 -0
  71. package/vendor/base/src/manifests/rows.ts +33 -0
  72. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  73. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  74. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  75. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  76. package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
  77. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  78. package/vendor/base/src/manifests/runtime/context.ts +150 -0
  79. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  80. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  81. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  82. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  83. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  84. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  85. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  86. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  87. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  88. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  89. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  90. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  91. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  92. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  93. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  94. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  95. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  96. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  97. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  98. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  99. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  100. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
  101. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  102. package/vendor/base/src/manifests/source.ts +187 -0
  103. package/vendor/base/src/manifests/watch.ts +67 -0
  104. package/vendor/base/src/markdown/document.ts +141 -0
  105. package/vendor/base/src/paths.ts +180 -0
  106. package/vendor/base/src/platform/audioManager.ts +100 -0
  107. package/vendor/base/src/platform/clientTransport.ts +65 -0
  108. package/vendor/base/src/platform/executionContext.ts +154 -0
  109. package/vendor/base/src/platform/index.ts +10 -0
  110. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  111. package/vendor/base/src/platform/redis.ts +54 -0
  112. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  113. package/vendor/base/src/pluginBase.ts +248 -0
  114. package/vendor/base/src/plugins/README.md +57 -0
  115. package/vendor/base/src/plugins/discovery.ts +87 -0
  116. package/vendor/base/src/plugins/index.ts +16 -0
  117. package/vendor/base/src/plugins/loader.ts +179 -0
  118. package/vendor/base/src/plugins/redis.ts +36 -0
  119. package/vendor/base/src/plugins/state.ts +53 -0
  120. package/vendor/base/src/registry.ts +95 -0
  121. package/vendor/base/src/security/permissions.ts +93 -0
  122. package/vendor/base/src/security/rules.ts +73 -0
  123. package/vendor/base/src/template/SafeExpression.ts +556 -0
  124. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  125. package/vendor/base/src/template/index.ts +22 -0
  126. package/vendor/base/src/types.ts +50 -0
  127. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  128. package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
  129. package/vendor/base/src/workflow/routing.ts +122 -0
  130. package/vendor/sdk/lib/core/store.ts +26 -6
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Plugin Base Types and Classes
3
+ *
4
+ * These are copied from @unoverse-platform/plugin-base to remove the
5
+ * compile-time dependency. This allows node-service to start without
6
+ * packages/ being mounted.
7
+ *
8
+ * SINCE 2026-07-29 THIS IS THE ONLY IN-REPO COPY: the plugin-base source retired to
9
+ * _legacy/plugin-base once the last code node (postgres-toolkit) did, and everything
10
+ * in-repo that still speaks the plugin protocol — packages/marketplace, the server's
11
+ * component runtime — imports from here. The PUBLISHED @unoverse-platform/plugin-base
12
+ * stays on npm, frozen, for externally installed plugins; the loader still names it by
13
+ * that string.
14
+ *
15
+ * A FAITHFUL transcription, including its loosest part: plugin-base's own index exported
16
+ * `export type EnhancedNodeDefinition = any` (and friends) as LOCAL declarations, which
17
+ * SHADOW its `export * from "./types"` star export — so every consumer's "typed" imports
18
+ * were `any` all along. The aliases below reproduce that, rather than pretending the old
19
+ * package gave marketplace type-safety it never had.
20
+ */
21
+
22
+ /**
23
+ * Platform dependencies for internal use
24
+ */
25
+ export interface PlatformDependencies {
26
+ PromiseNode: any;
27
+ CallbackNode: any;
28
+ NodeInputType: any;
29
+ NodeConcurrency: any;
30
+ getNodeCredentials: (context: any, credentialName: string) => Promise<any>;
31
+ getConfig: () => any;
32
+ createLogger: (name: string) => any;
33
+ saveTokenUsage: (usage: any) => Promise<void>;
34
+ callService: (method: string, params: any, context: any) => Promise<any>;
35
+ getRedisClient: () => any;
36
+ gravityPublish: (channel: string, message: any) => Promise<void>;
37
+ executeNodeWithRouting?: (
38
+ executeNode: (inputs: any, config: any, context: any) => Promise<any>,
39
+ params: any,
40
+ config: any,
41
+ context: any
42
+ ) => Promise<any>;
43
+ deliverToNode?: (
44
+ executionId: string,
45
+ nodeId: string,
46
+ handle: string,
47
+ value: unknown,
48
+ signal?: string,
49
+ ) => Promise<void>;
50
+ getAudioWebSocketManager?: () => any;
51
+ [key: string]: any;
52
+ }
53
+
54
+ /**
55
+ * Base class for Promise-based nodes
56
+ */
57
+ export class PromiseNode {
58
+ nodeType: string;
59
+ logger: any = { info: () => {}, error: () => {}, debug: () => {} };
60
+
61
+ constructor(name: string) {
62
+ this.nodeType = name || "stub";
63
+ }
64
+
65
+ protected async validateConfig(config: any): Promise<any> {
66
+ return { success: true };
67
+ }
68
+
69
+ protected async executeNode(inputs: any, config: any, context: any): Promise<any> {
70
+ return {};
71
+ }
72
+
73
+ protected validateAndGetContext(context: any) {
74
+ return {
75
+ workflowId: context?.workflowId || context?.workflow?.id || "",
76
+ executionId: context?.executionId || context?.workflow?.runId || "",
77
+ nodeId: context?.nodeId || "",
78
+ };
79
+ }
80
+
81
+ protected getExecutionContext(context: any) {
82
+ return {
83
+ workflowId: context?.workflowId || context?.workflow?.id || "",
84
+ executionId: context?.executionId || context?.workflow?.runId || "",
85
+ nodeId: context?.nodeId || "",
86
+ nodeType: context?.nodeType || this.nodeType || "",
87
+ config: context?.config || {},
88
+ credentials: context?.credentials || {},
89
+ };
90
+ }
91
+
92
+ async execute(inputs: any, config: any, context: any): Promise<any> {
93
+ return this.executeNode(inputs, config, context);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Base class for Callback-based nodes
99
+ */
100
+ export class CallbackNode {
101
+ constructor(name: string) {}
102
+ }
103
+
104
+ /**
105
+ * Node input type enum
106
+ */
107
+ export const NodeInputType = {
108
+ STRING: "string",
109
+ OBJECT: "object",
110
+ ARRAY: "array",
111
+ NUMBER: "number",
112
+ BOOLEAN: "boolean",
113
+ };
114
+
115
+ // Global platform instance
116
+ let platformDeps: PlatformDependencies | null = null;
117
+
118
+ /**
119
+ * Set platform dependencies (called by plugin setup)
120
+ */
121
+ export function setPlatformDependencies(deps: PlatformDependencies) {
122
+ if (platformDeps !== null) {
123
+ return;
124
+ }
125
+ platformDeps = deps;
126
+ }
127
+
128
+ /** The loose compile-time aliases plugin-base actually exported (see the header). */
129
+ export type EnhancedNodeDefinition = any;
130
+ export type ValidationResult = any;
131
+ export type NodeExecutionContext = any;
132
+
133
+ /** Plugin interface that packages implement. Transcribed from plugin-base index.ts. */
134
+ export interface GravityPlugin {
135
+ name: string;
136
+ version?: string;
137
+ description?: string;
138
+ setup(api: GravityPluginAPI): void | Promise<void>;
139
+ }
140
+
141
+ /** The API the platform hands a plugin's setup(). Transcribed from plugin-base index.ts. */
142
+ export interface GravityPluginAPI {
143
+ registerNode(node: PluginNodeDefinition): void;
144
+ registerService(name: string, service: any): void;
145
+ registerCredential(credential: any): void;
146
+ registerComponentPath?(packagePath: string): void;
147
+ createLogger(name: string): any;
148
+ getConfig(): any;
149
+ saveTokenUsage(usage: any): Promise<void>;
150
+ saveMCPTrace(trace: any): Promise<string>;
151
+ getNodeCredentials(context: any, credentialName: string): Promise<any>;
152
+ callService(method: string, params: any, context: any): Promise<any>;
153
+ getRedisClient(): any;
154
+ gravityPublish(channel: string, message: any): Promise<void>;
155
+ getAudioWebSocketManager?: () => any;
156
+ getWebSocketManager?: () => any;
157
+ publishStreamingUpdate?: (config: { componentType: string; props: Record<string, any> }) => void;
158
+ executeNodeWithRouting?: (
159
+ executeNode: (inputs: any, config: any, context: any) => Promise<any>,
160
+ params: any,
161
+ config: any,
162
+ context: any,
163
+ ) => Promise<any>;
164
+ deliverToNode?: (
165
+ executionId: string,
166
+ nodeId: string,
167
+ handle: string,
168
+ value: unknown,
169
+ signal?: string,
170
+ ) => Promise<void>;
171
+ classes: { PromiseNode: any; CallbackNode: any };
172
+ types: { NodeInputType: any; NodeConcurrency: any };
173
+ }
174
+
175
+ export interface PluginNodeDefinition {
176
+ definition: any;
177
+ executor: any;
178
+ }
179
+
180
+ /** Helper to create a plugin — the identity function plugin-base always was. */
181
+ export function createPlugin(config: {
182
+ name: string;
183
+ version?: string;
184
+ description?: string;
185
+ setup: (api: GravityPluginAPI) => void | Promise<void>;
186
+ }): GravityPlugin {
187
+ return config;
188
+ }
189
+
190
+ /**
191
+ * Initialize platform dependencies from the plugin API — transcribed from plugin-base
192
+ * index.ts, the same field mapping, so a plugin moving its import here behaves
193
+ * identically at setup time.
194
+ */
195
+ export function initializePlatformFromAPI(api: GravityPluginAPI) {
196
+ setPlatformDependencies({
197
+ PromiseNode: api.classes.PromiseNode,
198
+ CallbackNode: api.classes.CallbackNode,
199
+ NodeInputType: api.types.NodeInputType,
200
+ NodeConcurrency: api.types.NodeConcurrency,
201
+ getNodeCredentials: api.getNodeCredentials,
202
+ getConfig: api.getConfig,
203
+ createLogger: api.createLogger,
204
+ saveTokenUsage: api.saveTokenUsage,
205
+ saveMCPTrace: api.saveMCPTrace,
206
+ callService: api.callService,
207
+ getRedisClient: api.getRedisClient,
208
+ gravityPublish: api.gravityPublish,
209
+ executeNodeWithRouting: api.executeNodeWithRouting,
210
+ deliverToNode: api.deliverToNode,
211
+ getAudioWebSocketManager: api.getAudioWebSocketManager,
212
+ } as PlatformDependencies);
213
+ }
214
+
215
+ // The prop-key reader marketplace's synthesizer uses — base's own copy, re-exported so a
216
+ // plugin needs ONE import root here rather than knowing base's internal layout.
217
+ export { inputPropKeys, type InputPropLike } from "./definitions/inputs.js";
218
+
219
+ /**
220
+ * Get platform dependencies
221
+ */
222
+ export function getPlatformDependencies(): PlatformDependencies {
223
+ if (!platformDeps) {
224
+ return {
225
+ packageVersion: "1.1.1",
226
+ PromiseNode,
227
+ CallbackNode,
228
+ NodeInputType,
229
+ NodeConcurrency: {},
230
+ getConfig: () => ({}),
231
+ createLogger: () => ({ info: () => {}, error: () => {}, debug: () => {}, warn: () => {} }),
232
+ saveTokenUsage: () => Promise.resolve(),
233
+ saveMCPTrace: () => Promise.resolve(),
234
+ callService: () => Promise.resolve(null),
235
+ getRedisClient: () => null,
236
+ gravityPublish: async () => {},
237
+ executeNodeWithRouting: async () => ({}),
238
+ getAudioWebSocketManager: () => null,
239
+ getNodeCredentials: async (context: any, credentialName: string) => {
240
+ if (platformDeps?.getNodeCredentials) {
241
+ return platformDeps.getNodeCredentials(context, credentialName);
242
+ }
243
+ return context?.credentials?.[credentialName] || {};
244
+ },
245
+ } as any;
246
+ }
247
+ return platformDeps;
248
+ }
@@ -0,0 +1,57 @@
1
+ # plugins/ — LEGACY. Nothing new goes here.
2
+
3
+ This folder loads **JavaScript** nodes: a package with a `src/`, a `setup(api)` call, and
4
+ classes extending `PromiseNode` or `CallbackNode`. Every node becomes a YAML manifest
5
+ (`../manifests/`), so this whole folder is scheduled for deletion.
6
+
7
+ **Do not add to it. Do not tidy it. Do not move it.** Work spent here is work spent twice.
8
+
9
+ ## What is still holding it open
10
+
11
+ 17 packages still ship `src/`. Most are vendor integrations and are pure migration work,
12
+ but three are platform capability and are why this cannot simply be switched off:
13
+
14
+ | Package | Why it matters |
15
+ |---|---|
16
+ | `flow` | `IfElse`, `LoopStart`, `LoopEnd`, `Code`, `Context`, `FieldValidator`. **Control flow.** Deleting this folder today breaks every workflow with a loop or a condition |
17
+ | `ingest` | the content pipeline (document parsing, sheets, connectors) |
18
+ | `spatial` | `SpatialSearch` |
19
+
20
+ Two of the remaining ones also need the executor to grow first
21
+ (`DECLARATIVE_NODES.md` §10): a socket transport with audio framing for `openai-realtime`
22
+ and the streaming half of `elevenlabs`, and catalog matching for the hubspot and salesforce
23
+ attribute pull. Everything else is conversion, not invention.
24
+
25
+ ## The folder is two jobs, dying on two timelines
26
+
27
+ ```
28
+ discovery.ts loader.ts redis.ts state.ts ← code-node loading. Dies with the last src/ package
29
+ ```
30
+
31
+ **The npm half is GONE.** `install.ts` and `startup.ts` are deleted, and with them
32
+ `CORE_PACKAGES` — the boot step that installed `@unoverse-platform/marketplace` so the
33
+ design system could resolve out of that package. `013_items.sql` said where it was going:
34
+ "Installing something from the marketplace is an INSERT... no npm, no image, no rsync, no
35
+ restart." Rows replaced npm for content exactly as manifests replaced code for nodes, and
36
+ MARKETPLACE.md §5 is now the whole story — a universe fetches from the marketplace URL at
37
+ install time and writes a row.
38
+
39
+ The last traces went with it (2026-08-30): `apps/unoverse` no longer declares
40
+ `@unoverse-platform/marketplace` as a dependency (nothing ever imported it — the only
41
+ mention was a string label), and `definitions/registry/homes.ts` no longer lists the
42
+ `PLUGINS_DIR/node_modules/...` bundle as a candidate. The core platform has no
43
+ relationship with the marketplace package at all; it talks to the marketplace over HTTP.
44
+
45
+ What remains here is one job, not two: loading code nodes.
46
+
47
+ ## How it ends
48
+
49
+ No flag day. Each package that converts to YAML removes one reason for this folder to exist.
50
+ When no package ships `src/`, delete `discovery.ts`, `loader.ts`, `redis.ts` and
51
+ `../pluginBase.ts`. When installs are rows, delete the rest and the folder with it.
52
+
53
+ The two Vite warnings a developer sees when starting Studio come from `discovery.ts` and
54
+ `loader.ts` (dynamic `import()` of a package by name). They are correct, they are
55
+ unfixable while this folder exists, and they leave when it does.
56
+
57
+ See `docs/architecture/DECLARATIVE_NODES.md` §7a and §10.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Plugin Discovery — finds plugins from /packages folder
3
+ */
4
+
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import type { PluginMetadata } from "../types.js";
8
+ import { PACKAGES_PATH, PLUGINS_DIR, NODES_HOME } from "../paths.js";
9
+
10
+ let ALL_PLUGINS: string[] = [];
11
+
12
+ export function getAllPlugins(): string[] {
13
+ return ALL_PLUGINS;
14
+ }
15
+
16
+ export function setAllPlugins(plugins: string[]): void {
17
+ ALL_PLUGINS = plugins;
18
+ }
19
+
20
+ /** Scan a directory for plugin packages (a package.json whose name is in `scopes` and has an entry). */
21
+ function scanPluginDir(dir: string, scopes: string[]): string[] {
22
+ const plugins: string[] = [];
23
+ if (!fs.existsSync(dir)) return plugins;
24
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
25
+ if (!entry.isDirectory()) continue;
26
+ const pkgJsonPath = path.join(dir, entry.name, "package.json");
27
+ if (!fs.existsSync(pkgJsonPath)) continue;
28
+ try {
29
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
30
+ const pkgName = pkgJson.name;
31
+ if (!pkgName || pkgName.includes("/docs")) continue;
32
+ if (scopes.some((s) => pkgName.startsWith(s)) && (pkgJson.main || pkgJson.exports)) {
33
+ plugins.push(pkgName);
34
+ }
35
+ } catch {
36
+ // Skip invalid package.json files
37
+ }
38
+ }
39
+ return plugins;
40
+ }
41
+
42
+ /** Nodes that have been moved INTO Unoverse (apps/unoverse/nodes). */
43
+ export function discoverNodesHome(): string[] {
44
+ return scanPluginDir(NODES_HOME, ["@unoverse-platform/"]);
45
+ }
46
+
47
+ /**
48
+ * LEGACY starter-kit packages. The fallback from before nodes moved to `nodes/`.
49
+ *
50
+ * Absent is the NORMAL state now, so its absence is not worth a line at boot. It used to
51
+ * warn, which was right when the folder was expected and missing meant something was wrong.
52
+ */
53
+ export function discoverPlugins(): string[] {
54
+ return scanPluginDir(PACKAGES_PATH, ["@unoverse-platform/"]);
55
+ }
56
+
57
+ export async function getPluginMetadata(pluginName: string): Promise<PluginMetadata> {
58
+ try {
59
+ const packagesPath = PACKAGES_PATH;
60
+ const shortName = pluginName.replace("@unoverse-platform/", "");
61
+ const pkgJsonPath = path.join(packagesPath, shortName, "package.json");
62
+
63
+ const pluginsDir = PLUGINS_DIR;
64
+ const npmPkgJsonPath = path.join(pluginsDir, "node_modules", pluginName, "package.json");
65
+
66
+ let pkg: any;
67
+ if (fs.existsSync(pkgJsonPath)) {
68
+ pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
69
+ } else if (fs.existsSync(npmPkgJsonPath)) {
70
+ pkg = JSON.parse(fs.readFileSync(npmPkgJsonPath, "utf-8"));
71
+ } else {
72
+ pkg = await import(/* @vite-ignore */ `${pluginName}/package.json`);
73
+ }
74
+
75
+ const gravity = pkg.gravity || {};
76
+ return {
77
+ version: pkg.version || "1.0.0",
78
+ description: pkg.description || "",
79
+ category: gravity.category || undefined,
80
+ displayName: gravity.displayName || undefined,
81
+ features: gravity.features || undefined,
82
+ credentials: gravity.credentials || undefined,
83
+ };
84
+ } catch {
85
+ return { version: "1.0.0", description: "" };
86
+ }
87
+ }
@@ -0,0 +1,16 @@
1
+ export { getPluginState, setPluginState, isPluginEnabled, persistPluginState, removePluginState, listPluginState } from "./state.js";
2
+ export { getAllPlugins, getPluginMetadata, discoverPlugins } from "./discovery.js";
3
+ export { loadPlugin, unloadPlugin } from "./loader.js";
4
+ /**
5
+ * NO INSTALL EXPORT, DELIBERATELY. `install.ts` held `installPlugin`/`uninstallPlugin`,
6
+ * which shelled out to `npm install` at runtime and imported whatever came back, gated by
7
+ * an `@unoverse-platform` scope allowlist. That whole lane is retired (MARKETPLACE.md §10
8
+ * q3, removed 2026-08-30): a universe takes content from the marketplace as ROWS via
9
+ * `POST /marketplace/install`, and node packages are declarative YAML already on the box.
10
+ * Nothing fetches or executes code from a package registry any more.
11
+ */
12
+ export { warnMissingNodeMeta } from "./redis.js";
13
+ // startup.ts (loadInstalledNpmPlugins) retired 2026-08-06: it survived the npm-plugin
14
+ // plane only as the bridge that loaded @unoverse-platform/marketplace's duplicate of the
15
+ // Component-node executor. The server registers the node from its own code
16
+ // (runtime/components/registerComponentNode); the package is content only.
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Plugin Loader — loads plugins and registers nodes into the in-memory registry.
3
+ */
4
+
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import { execSync } from "child_process";
8
+ import { pathToFileURL } from "url";
9
+ import { getNodeRegistry, getPluginNodeMap, deleteNode } from "../registry.js";
10
+ import { PACKAGES_PATH, MARKETPLACE_PATH, PLUGINS_DIR, NODES_HOME } from "../paths.js";
11
+
12
+ /**
13
+ * Resolve a package directory's entry file from its package.json.
14
+ *
15
+ * CommonJS `require(dir)` reads package.json "main" and loads it; ESM `import(dir)`
16
+ * does NOT do directory/main resolution for a filesystem path (it only does so for
17
+ * bare package specifiers). Since the engine moved to ESM we resolve the entry
18
+ * ourselves — supporting `exports["."]` and `main`, falling back to index.js.
19
+ */
20
+ function resolveEntry(dir: string): string {
21
+ // Unoverse runs under tsx in BOTH dev and prod (see Dockerfile), so it can import a
22
+ // package's TypeScript source directly. For the in-Unoverse node packages (NODES_HOME),
23
+ // prefer `src/index.ts` when it exists: this sidesteps the whole "stale/missing dist"
24
+ // failure class — most painfully the `components` package, which is regenerated by
25
+ // `nodegen:local` and whose gitignored dist silently goes missing (the runtime then
26
+ // boots without those nodes and the workflow pulls a partial catalog → "Unknown node
27
+ // type: CardFinder"). Loading from src also means a regenerated node takes effect on
28
+ // restart with no separate build. Scoped to NODES_HOME so legacy /packages and
29
+ // marketplace plugins keep their existing (bundled-dist) resolution untouched.
30
+ if (path.resolve(dir).startsWith(path.resolve(NODES_HOME) + path.sep)) {
31
+ const srcTs = path.join(dir, "src", "index.ts");
32
+ if (fs.existsSync(srcTs)) return srcTs;
33
+ }
34
+
35
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8"));
36
+ let entry: string = pkg.main || "index.js";
37
+ const exp = pkg.exports;
38
+ if (exp) {
39
+ const dot = typeof exp === "string" ? exp : exp["."] ?? exp;
40
+ if (typeof dot === "string") entry = dot;
41
+ else if (dot && typeof dot === "object") entry = dot.import || dot.require || dot.default || entry;
42
+ }
43
+ return path.join(dir, entry);
44
+ }
45
+
46
+ /**
47
+ * Import a plugin from a directory (resolving its entry file as a file:// URL) or,
48
+ * if no local directory exists, from its bare package specifier (npm-resolved).
49
+ */
50
+ export async function importPluginModule(dirOrName: string): Promise<any> {
51
+ let hasPkg = false;
52
+ try {
53
+ hasPkg = fs.existsSync(path.join(dirOrName, "package.json"));
54
+ } catch {
55
+ hasPkg = false;
56
+ }
57
+ // @vite-ignore: loading a plugin by name IS the job here, so the specifier cannot be
58
+ // static. Vite is right to warn and there is nothing to fix while this folder exists;
59
+ // the warning printed on every Studio start (see ./README.md, this folder is legacy).
60
+ const target = hasPkg ? pathToFileURL(resolveEntry(dirOrName)).href : dirOrName;
61
+ return import(/* @vite-ignore */ target);
62
+ }
63
+
64
+ /**
65
+ * Clear all cached modules under a given path so re-import loads fresh code.
66
+ *
67
+ * Under ESM there is no `require.cache`, and `import()` caches by URL with no
68
+ * public API to evict — so plugin hot-reload is a no-op here and is handled by
69
+ * a process restart instead (see the marketplace upgrade docs). Guarded so the
70
+ * call site stays identical to the CJS original.
71
+ */
72
+ export function clearModuleCache(modulePath: string): void {
73
+ const req = (globalThis as { require?: { cache?: Record<string, unknown> } }).require;
74
+ if (!req?.cache) return;
75
+ const resolved = path.resolve(modulePath);
76
+ for (const key of Object.keys(req.cache)) {
77
+ if (key.startsWith(resolved)) {
78
+ delete req.cache[key];
79
+ }
80
+ }
81
+ }
82
+
83
+ export async function loadPlugin(pluginName: string, api: any): Promise<string[]> {
84
+ const nodeRegistry = getNodeRegistry();
85
+ const nodeTypes: string[] = [];
86
+ const nodesBefore = new Set(nodeRegistry.keys());
87
+
88
+ try {
89
+ const shortName = pluginName.replace(/@[^/]+\//, "");
90
+ // Resolution order: the in-Unoverse node home wins, then legacy /packages,
91
+ // then /packages-marketplace, then the bare npm specifier.
92
+ const homePath = path.join(NODES_HOME, shortName);
93
+ const pkgPath = path.join(PACKAGES_PATH, shortName);
94
+ const mktPath = path.join(MARKETPLACE_PATH, shortName);
95
+ const resolvedPath = fs.existsSync(homePath)
96
+ ? homePath
97
+ : fs.existsSync(pkgPath)
98
+ ? pkgPath
99
+ : fs.existsSync(mktPath)
100
+ ? mktPath
101
+ : pluginName;
102
+
103
+ const pkg = await importPluginModule(resolvedPath);
104
+ const plugin = pkg.default || pkg;
105
+
106
+ if (plugin && typeof plugin.setup === "function") {
107
+ await plugin.setup(api);
108
+
109
+ for (const [type] of nodeRegistry) {
110
+ if (!nodesBefore.has(type)) {
111
+ nodeTypes.push(type);
112
+ }
113
+ }
114
+
115
+ // Stamp package-level marketplace metadata (package.json `gravity`) onto the nodes
116
+ // this plugin just registered. By the authoring convention (packages/docs/nodes), a
117
+ // package's icon lives in `gravity.logoUrl` and applies to every node in the package
118
+ // — it is NOT repeated on each createNodeDefinition(). node-service merged this; the
119
+ // Unoverse runtime didn't, so the Canvas drew these nodes with no icon. A node that
120
+ // sets its own logoUrl wins; the package value is only a fallback.
121
+ try {
122
+ const pkgJsonPath = path.join(resolvedPath, "package.json");
123
+ if (fs.existsSync(pkgJsonPath)) {
124
+ const gravity = (JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")).gravity ?? {}) as {
125
+ logoUrl?: string;
126
+ };
127
+ if (gravity.logoUrl) {
128
+ for (const type of nodeTypes) {
129
+ const def = nodeRegistry.get(type) as { logoUrl?: string } | undefined;
130
+ if (def && !def.logoUrl) def.logoUrl = gravity.logoUrl;
131
+ }
132
+ }
133
+ }
134
+ } catch {
135
+ // Marketplace metadata is best-effort — a missing/!malformed manifest must not
136
+ // fail node loading.
137
+ }
138
+ }
139
+ } catch (error: any) {
140
+ console.log(` ⚠ Failed to load ${pluginName}: ${error.message}`);
141
+ }
142
+
143
+ return nodeTypes;
144
+ }
145
+
146
+ export function unloadPlugin(pluginName: string): void {
147
+ const pluginNodeMap = getPluginNodeMap();
148
+ const nodeTypes = pluginNodeMap.get(pluginName) || [];
149
+
150
+ // Drop from the in-memory registry. Workflow re-pulls the catalog (GET /nodes),
151
+ // so there is no Redis catalog to purge.
152
+ for (const nodeType of nodeTypes) {
153
+ deleteNode(nodeType);
154
+ }
155
+
156
+ pluginNodeMap.delete(pluginName);
157
+ }
158
+
159
+ export function getPluginsDir(): string {
160
+ return PLUGINS_DIR;
161
+ }
162
+
163
+ export function ensurePluginsDir(): void {
164
+ if (!fs.existsSync(PLUGINS_DIR)) {
165
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
166
+ }
167
+ // No seeded dependencies. plugin-base retired 2026-07-29 with the last code node —
168
+ // an installed package that needs it (an OLD published version) declares it as an
169
+ // ordinary npm dependency and npm installs it transitively; the self-heal that used
170
+ // to force-upgrade it existed for PEER-dep node packages, and none remain.
171
+ const pkgJsonPath = path.join(PLUGINS_DIR, "package.json");
172
+ if (!fs.existsSync(pkgJsonPath)) {
173
+ fs.writeFileSync(pkgJsonPath, JSON.stringify({
174
+ name: "gravity-plugins",
175
+ private: true,
176
+ dependencies: {},
177
+ }, null, 2));
178
+ }
179
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Node-meta guard. (Formerly also held the Redis node/credential catalog writers —
3
+ * removed with the catalog repoint: the catalog now lives in the in-memory registry
4
+ * and is pulled by workflow over HTTP, so there is nothing to write to Redis.)
5
+ */
6
+
7
+ import { boot } from "../boot.js";
8
+
9
+ /**
10
+ * Meta sync guard — LOUD signal whenever a batch of node defs is registered.
11
+ *
12
+ * `whenToUse` is the field the node ranker (CatalogService / semantic search)
13
+ * embeds; if it's empty the search silently degrades to name+description. A stale
14
+ * build or a freshly imported package that never authored it leaves the meta
15
+ * unsynced with NO error. Call this right after registering a batch (startup,
16
+ * import/update, reload) so a gap is visible at the moment it enters the registry.
17
+ *
18
+ * Returns the missing node types so callers can surface them in an API response too.
19
+ */
20
+ export function warnMissingNodeMeta(defs: any[], context: string): { total: number; missing: string[] } {
21
+ const missing = defs
22
+ .filter((d: any) => !d?.whenToUse || !String(d.whenToUse).trim())
23
+ .map((d: any) => d?.type)
24
+ .filter(Boolean);
25
+ // Only the shortfall is worth saying. "All 84 nodes have whenToUse" is the expected
26
+ // state and a line that only ever reports success trains you to skim past it.
27
+ if (missing.length > 0) {
28
+ const named = `${missing.slice(0, 10).join(", ")}${missing.length > 10 ? `, and ${missing.length - 10} more` : ""}`;
29
+ const text =
30
+ `${missing.length} of ${defs.length} nodes have no whenToUse, so catalog ranking is degraded for them: ` +
31
+ `${named}. Author whenToUse on the node, then rebuild and restart.`;
32
+ if (boot.isBooting()) boot.notice("warn", text);
33
+ else console.warn(`[${context}] ${text}`);
34
+ }
35
+ return { total: defs.length, missing };
36
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Plugin State — in-memory cache sourced from PostgreSQL
3
+ */
4
+
5
+ import { persistPluginState, removePluginState, fetchPluginState } from "../platform/serviceCalls.js";
6
+ import { boot } from "../boot.js";
7
+
8
+ export type PluginStateEntry = {
9
+ enabled: boolean;
10
+ version: string;
11
+ source: string;
12
+ metadata?: any;
13
+ };
14
+
15
+ let PLUGIN_STATE: Map<string, PluginStateEntry> = new Map();
16
+
17
+ export function getPluginState(name: string): PluginStateEntry | undefined {
18
+ return PLUGIN_STATE.get(name);
19
+ }
20
+
21
+ export function setPluginState(name: string, state: PluginStateEntry): void {
22
+ PLUGIN_STATE.set(name, state);
23
+ }
24
+
25
+ export function isPluginEnabled(name: string): boolean {
26
+ const state = PLUGIN_STATE.get(name);
27
+ if (!state) return true;
28
+ return state.enabled;
29
+ }
30
+
31
+ export async function loadStateFromPostgres(): Promise<void> {
32
+ const pgState = await fetchPluginState();
33
+ PLUGIN_STATE.clear();
34
+ if (pgState.length > 0) {
35
+ boot.pluginStates(pgState.length);
36
+ for (const plugin of pgState) {
37
+ PLUGIN_STATE.set(plugin.name, {
38
+ enabled: plugin.enabled,
39
+ version: plugin.version,
40
+ source: plugin.source,
41
+ metadata: plugin.metadata,
42
+ });
43
+ }
44
+ }
45
+ }
46
+
47
+ /** All state entries (the in-memory mirror of installed_plugins). Used by the
48
+ * registry-toggle routes to read enable/spatial state for skills/apps/nodes. */
49
+ export function listPluginState(): Array<{ name: string } & PluginStateEntry> {
50
+ return [...PLUGIN_STATE.entries()].map(([name, state]) => ({ name, ...state }));
51
+ }
52
+
53
+ export { persistPluginState, removePluginState };