@unoverse-platform/studio 0.1.97 → 0.1.99
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/bin/studio.mjs +2 -2
- package/local/catalog.ts +5 -1
- package/local/nodes.ts +28 -41
- package/local/plugin.ts +71 -13
- package/local/sample-identity/brand.yaml +24 -0
- package/local/sample-identity/organisation.yaml +14 -0
- package/local/sample-identity/purpose.yaml +14 -0
- package/local/sample-identity/story.yaml +18 -0
- package/local/scaffold.mjs +43 -23
- package/local/source.ts +3 -8
- package/package.json +1 -1
- package/screens/AgentsView.tsx +173 -0
- package/screens/ComponentsView.tsx +2 -2
- package/screens/EmptyLibrary.tsx +1 -1
- package/screens/IdentityView.tsx +3 -3
- package/screens/PromptBlocksView.tsx +1 -1
- package/screens/TemplatesLibraryView.tsx +91 -6
- package/screens/TemplatesView.tsx +24 -9
- package/screens/index.ts +2 -0
- package/src/App.tsx +8 -3
- package/src/host.ts +1 -14
- package/vendor/base/src/agent-mcp/discovery.ts +76 -18
- package/vendor/base/src/agent-mcp/handoff.ts +11 -31
- package/vendor/base/src/agent-mcp/index.ts +5 -5
- package/vendor/base/src/agent-mcp/invoke.ts +37 -19
- package/vendor/base/src/agent-mcp/lean.ts +19 -0
- package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
- package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
- package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
- package/vendor/base/src/agent-mcp/tools.ts +136 -7
- package/vendor/base/src/agent-mcp/types.ts +47 -10
- package/vendor/base/src/auth/auth.ts +35 -5
- package/vendor/base/src/definitions/isolated.ts +37 -0
- package/vendor/base/src/definitions/load/compile/component.ts +59 -4
- package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
- package/vendor/base/src/definitions/load/compile/template.ts +19 -15
- package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
- package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
- package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
- package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
- package/vendor/base/src/definitions/load/load.ts +37 -28
- package/vendor/base/src/definitions/model.ts +8 -12
- package/vendor/base/src/definitions/registry/agents.ts +117 -0
- package/vendor/base/src/definitions/registry/catalog.ts +40 -24
- package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
- package/vendor/base/src/definitions/registry/homes.ts +69 -5
- package/vendor/base/src/definitions/registry/objects.ts +413 -0
- package/vendor/base/src/definitions/registry/orgDocuments.ts +69 -29
- package/vendor/base/src/definitions/registry/orgPack.ts +11 -11
- package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
- package/vendor/base/src/definitions/theme.ts +1 -2
- package/vendor/base/src/items/collect.ts +29 -25
- package/vendor/base/src/items/publish.ts +30 -6
- package/vendor/base/src/lint/context.mjs +17 -0
- package/vendor/base/src/lint/design/file.mjs +199 -2
- package/vendor/base/src/lint/design/index.mjs +66 -73
- package/vendor/base/src/lint/design/vocabulary.mjs +5 -2
- package/vendor/base/src/lint/design/walk.mjs +14 -4
- package/vendor/base/src/lint/index.mjs +15 -9
- package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
- package/vendor/base/src/lint/nodes/index.mjs +5 -0
- package/vendor/base/src/lint/nodes/node.mjs +157 -7
- package/vendor/base/src/lint/objects/index.d.mts +32 -0
- package/vendor/base/src/lint/objects/index.mjs +324 -0
- package/vendor/base/src/lint/objects/rules.d.mts +44 -0
- package/vendor/base/src/lint/objects/rules.mjs +446 -0
- package/vendor/base/src/lint/prompts/index.d.mts +34 -0
- package/vendor/base/src/lint/prompts/index.mjs +198 -0
- package/vendor/base/src/manifests/compose.ts +80 -9
- package/vendor/base/src/manifests/executor/callback.ts +15 -4
- package/vendor/base/src/manifests/executor/context.ts +9 -1
- package/vendor/base/src/manifests/executor/memory.ts +83 -3
- package/vendor/base/src/manifests/executor/session.ts +50 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
- package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
- package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
- package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
- package/vendor/base/src/manifests/runtime/context.ts +18 -6
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
- package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
- package/vendor/base/src/manifests/runtime/events.ts +39 -3
- package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
- package/vendor/base/src/manifests/runtime/index.ts +3 -2
- package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
- package/vendor/base/src/manifests/runtime/usage.ts +10 -0
- package/vendor/base/src/paths.ts +16 -8
- package/vendor/base/src/platform/audioManager.ts +32 -0
- package/vendor/base/src/platform/executionContext.ts +14 -0
- package/vendor/base/src/platform/liveInput.ts +52 -0
- package/vendor/base/src/plugins/discovery.ts +1 -1
- package/vendor/base/src/template/SafeExpression.ts +6 -0
- package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
- package/vendor/sdk/appEngine.tsx +30 -11
- package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
- package/vendor/sdk/lib/app.tsx +70 -95
- package/vendor/sdk/lib/connection.tsx +47 -57
- package/vendor/sdk/lib/core/actions.ts +6 -0
- package/vendor/sdk/lib/core/apps.ts +66 -81
- package/vendor/sdk/lib/core/client.ts +5 -14
- package/vendor/sdk/lib/core/connection.ts +30 -0
- package/vendor/sdk/lib/core/index.ts +2 -2
- package/vendor/sdk/lib/core/store.ts +90 -15
- package/vendor/sdk/lib/core/types.ts +9 -8
- package/vendor/sdk/lib/primitives.tsx +113 -4
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
- package/vendor/sdk/lib/render.tsx +3 -3
- package/vendor/sdk/lib/voice.tsx +27 -13
- package/local/sample-identity/brand/brand.yaml +0 -88
- package/local/sample-identity/brand/manifest.yaml +0 -13
- package/local/sample-identity/organisation/manifest.yaml +0 -15
- package/local/sample-identity/organisation/organisation.yaml +0 -53
- package/local/sample-identity/purpose/manifest.yaml +0 -12
- package/local/sample-identity/purpose/purpose.yaml +0 -46
- package/local/sample-identity/story/manifest.yaml +0 -12
- package/local/sample-identity/story/story.yaml +0 -63
- package/vendor/base/src/definitions/registry/identitySeed.ts +0 -17
package/bin/studio.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* unoverse-studio — the authoring tool, run from a developer's own project.
|
|
4
4
|
*
|
|
5
5
|
* Studio is a TOOL, not a starter kit. A developer's repository holds only their assets
|
|
6
|
-
* (
|
|
6
|
+
* (design/); this is installed alongside and reads them. That is why there
|
|
7
7
|
* is no second repository to keep in step, and why their project contains no Docker, no
|
|
8
8
|
* ansible and no platform source.
|
|
9
9
|
*
|
|
@@ -143,7 +143,7 @@ async function scaffoldIfEmpty() {
|
|
|
143
143
|
// publishes, so a flat rx/ could hold exactly one project and would read `components`
|
|
144
144
|
// and `apps` as orgs.
|
|
145
145
|
createFirstProject(home, name);
|
|
146
|
-
console.log(`\n Created design/${name}
|
|
146
|
+
console.log(`\n Created design/${name}/ in ${cwd}\n`);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
await scaffoldIfEmpty();
|
package/local/catalog.ts
CHANGED
|
@@ -26,6 +26,8 @@ type Loaders = {
|
|
|
26
26
|
listThemeNames: (org: string) => string[];
|
|
27
27
|
/** The identity documents of one org or every org: `<org>/<doc>` items with manifest and briefed props. */
|
|
28
28
|
listIdentity: (org?: string) => unknown[];
|
|
29
|
+
/** The Agents of one org or every org: `<org>/<name>` items with their meta and door. */
|
|
30
|
+
listAgents: (org?: string) => unknown[];
|
|
29
31
|
resolveTheme: (name: string) => unknown;
|
|
30
32
|
DEFAULT_ORG: string;
|
|
31
33
|
/** A project's on-disk home. The writable set is built from these (see source.ts). */
|
|
@@ -46,7 +48,7 @@ export function findProjectRoot(from: string = process.cwd()): string {
|
|
|
46
48
|
// A developer's project (starter kit): rx/ sits at the root.
|
|
47
49
|
if (hasDesignHome(dir)) return dir;
|
|
48
50
|
// This monorepo, where Studio's author works: rx/ lives under apps/unoverse. Same
|
|
49
|
-
// shape underneath (
|
|
51
|
+
// shape underneath (design/), just nested, so return that as the root.
|
|
50
52
|
if (hasDesignHome(resolve(dir, "apps/unoverse"))) return resolve(dir, "apps/unoverse");
|
|
51
53
|
const parent = resolve(dir, "..");
|
|
52
54
|
if (parent === dir) break;
|
|
@@ -98,6 +100,7 @@ export async function loadCatalog(
|
|
|
98
100
|
const inputs = await load(resolve(baseSrc, "definitions/inputs.ts"));
|
|
99
101
|
const homes = await load(resolve(baseSrc, "definitions/registry/homes.ts"));
|
|
100
102
|
const identity = await load(resolve(baseSrc, "definitions/registry/orgDocuments.ts"));
|
|
103
|
+
const agents = await load(resolve(baseSrc, "definitions/registry/agents.ts"));
|
|
101
104
|
|
|
102
105
|
return {
|
|
103
106
|
listDefinitions: defs.listDefinitions as Loaders["listDefinitions"],
|
|
@@ -120,6 +123,7 @@ export async function loadCatalog(
|
|
|
120
123
|
}
|
|
121
124
|
return out;
|
|
122
125
|
}) as Loaders["listIdentity"],
|
|
126
|
+
listAgents: agents.listAgents as Loaders["listAgents"],
|
|
123
127
|
resolveTheme: theme.resolveTheme as Loaders["resolveTheme"],
|
|
124
128
|
DEFAULT_ORG: theme.DEFAULT_ORG as string,
|
|
125
129
|
projectDir: defs.projectDir as Loaders["projectDir"],
|
package/local/nodes.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The LOCAL node catalog and runner.
|
|
3
3
|
*
|
|
4
|
-
* Scope (decided 2026-07-25): the developer's OWN nodes only, the ones in their
|
|
5
|
-
*
|
|
4
|
+
* Scope (decided 2026-07-25): the developer's OWN nodes only, the ones in each of their
|
|
5
|
+
* projects' `design/<project>/nodes/` folder (org-scoped since 2026-09-08, like skills;
|
|
6
|
+
* the workspace-root `nodes/` is gone). Local Studio never reads the installed plugins directory,
|
|
6
7
|
* `installed_plugins`, or the marketplace. Acquiring a node is a universe's job; this
|
|
7
8
|
* is developing the nodes you are writing.
|
|
8
9
|
*
|
|
9
10
|
* As with the definitions (catalog.ts), this reuses the server's REAL registration
|
|
10
|
-
* path rather than scraping source:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* path rather than scraping source: every package goes through `loadManifests` over a
|
|
12
|
+
* disk source, exactly as the server boots. So what Studio lists is what the platform
|
|
13
|
+
* would register, including config schemas and credential types.
|
|
13
14
|
*
|
|
14
15
|
* The bootstrap comment is explicit that this path touches neither Redis nor the
|
|
15
16
|
* workflow service, which is what makes it usable with no backend.
|
|
@@ -29,57 +30,43 @@ export interface LocalNodes {
|
|
|
29
30
|
/**
|
|
30
31
|
* Load the developer's node packages into the in-memory registry.
|
|
31
32
|
*
|
|
32
|
-
* `
|
|
33
|
-
*
|
|
33
|
+
* `nodesDirs` is every project's `design/<project>/nodes/`. Returns null when none of
|
|
34
|
+
* them exists, so Studio can simply omit the section rather than showing an error:
|
|
34
35
|
* plenty of developers only author design assets.
|
|
36
|
+
*
|
|
37
|
+
* YAML manifest nodes (package.yaml + nodes/<Node>/*.yaml) are the ONLY authoring
|
|
38
|
+
* format (DECLARATIVE_NODES.md §10, nothing stays code), so this is one pass over one
|
|
39
|
+
* disk source per project: local Studio reads the developer's folders, never a
|
|
40
|
+
* database, and loadManifests registers into the same registry getAllNodes() reads.
|
|
41
|
+
* The legacy code-package loader that ran ahead of this pass read a `NODES_PATH`
|
|
42
|
+
* variable paths.ts had stopped honouring, so it loaded nothing and is gone.
|
|
35
43
|
*/
|
|
36
44
|
export async function loadLocalNodes(
|
|
37
45
|
baseSrc: string,
|
|
38
46
|
load: (id: string) => Promise<Record<string, unknown>>,
|
|
39
|
-
|
|
47
|
+
nodesDirs: string[],
|
|
40
48
|
): Promise<LocalNodes | null> {
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
43
|
-
|
|
44
|
-
// Set BEFORE the modules load: paths.ts reads process.env at module load time.
|
|
45
|
-
process.env.NODES_PATH = nodesDir;
|
|
49
|
+
const present = nodesDirs.filter((d) => existsSync(d));
|
|
50
|
+
if (!present.length) return null;
|
|
46
51
|
|
|
47
|
-
const discovery = await load(resolve(baseSrc, "plugins/discovery.ts"));
|
|
48
|
-
const loader = await load(resolve(baseSrc, "plugins/loader.ts"));
|
|
49
|
-
const platform = await load(resolve(baseSrc, "platform/index.ts"));
|
|
50
52
|
const registry = await load(resolve(baseSrc, "registry.ts"));
|
|
51
53
|
const executor = await load(resolve(baseSrc, "executor.ts"));
|
|
54
|
+
const manifests = await load(resolve(baseSrc, "manifests/index.ts"));
|
|
55
|
+
const msource = await load(resolve(baseSrc, "manifests/source.ts"));
|
|
52
56
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
for (const name of packages) {
|
|
57
|
+
for (const dir of present) {
|
|
58
|
+
// One broken project must never stop the others loading. Same posture as the
|
|
59
|
+
// server's bootstrap, and the same reason: a bad asset cannot take the tool down.
|
|
57
60
|
try {
|
|
58
|
-
await (
|
|
61
|
+
const r = await (manifests.loadManifests as (s: unknown) => Promise<{ nodes: number; errors: string[] }>)(
|
|
62
|
+
(msource.diskSource as (d: string) => unknown)(dir),
|
|
63
|
+
);
|
|
64
|
+
for (const e of r.errors) console.warn(`[studio] manifest node: ${e}`);
|
|
59
65
|
} catch (err) {
|
|
60
|
-
|
|
61
|
-
// server's bootstrap, and the same reason: a bad asset cannot take the tool down.
|
|
62
|
-
console.warn(`[studio] could not load node package ${name}: ${(err as Error).message}`);
|
|
66
|
+
console.warn(`[studio] nodes in ${dir} did not load: ${(err as Error).message}`);
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|
|
66
|
-
// YAML manifest nodes (package.yaml + nodes/<Node>/*.yaml) — the DEFAULT authoring
|
|
67
|
-
// format, and what the scaffolded sample nodes are. The discovery above only finds
|
|
68
|
-
// legacy CODE packages (a package.json with an entry point), so without this pass a
|
|
69
|
-
// pure-YAML nodes/ folder — every new developer's — listed NOTHING at all. Explicit
|
|
70
|
-
// disk source: local Studio reads the developer's folder, never a database, and
|
|
71
|
-
// loadManifests registers into the same registry getAllNodes() reads.
|
|
72
|
-
try {
|
|
73
|
-
const manifests = await load(resolve(baseSrc, "manifests/index.ts"));
|
|
74
|
-
const msource = await load(resolve(baseSrc, "manifests/source.ts"));
|
|
75
|
-
const r = await (manifests.loadManifests as (s: unknown) => Promise<{ nodes: number; errors: string[] }>)(
|
|
76
|
-
(msource.diskSource as (d: string) => unknown)(nodesDir),
|
|
77
|
-
);
|
|
78
|
-
for (const e of r.errors) console.warn(`[studio] manifest node: ${e}`);
|
|
79
|
-
} catch (err) {
|
|
80
|
-
console.warn(`[studio] manifest nodes did not load: ${(err as Error).message}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
70
|
return {
|
|
84
71
|
list: () => (registry.getAllNodes as () => unknown[])(),
|
|
85
72
|
credentialTypes: () => (registry.getAllCredentialTypes as () => unknown[])(),
|
package/local/plugin.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Routes mirror the server's shapes exactly, because the screens parse those shapes:
|
|
10
10
|
* GET /dev/orgs GET /dev/themes?org= GET /dev/components?org=
|
|
11
|
-
* GET /dev/apps?org= GET /dev/atoms GET /dev/identity?org=
|
|
11
|
+
* GET /dev/apps?org= GET /dev/atoms GET /dev/identity?org= GET /dev/agents?org=
|
|
12
12
|
* GET /local/theme?ref= GET /local/definition?uri=
|
|
13
13
|
* GET /skills /skills/:name GET /prompt-blocks
|
|
14
14
|
* GET|POST /registry/state (no database locally, see below)
|
|
@@ -92,7 +92,12 @@ function json(res: import("http").ServerResponse, body: unknown, status = 200):
|
|
|
92
92
|
interface MarkdownEntry {
|
|
93
93
|
/** Stable key the screens select on. Derived from the filename, like the server's. */
|
|
94
94
|
id: string;
|
|
95
|
+
/** The LOOKUP name, org-qualified for an org's own skill or block. Round-trips verbatim. */
|
|
95
96
|
name: string;
|
|
97
|
+
/** What a person reads. `name` carries the org, and a navigator printing it shows a path. */
|
|
98
|
+
title?: string;
|
|
99
|
+
/** The owning org. ABSENT means the universal tier, exactly as the server tags it. */
|
|
100
|
+
org?: string;
|
|
96
101
|
description: string;
|
|
97
102
|
category?: string;
|
|
98
103
|
version?: string;
|
|
@@ -136,6 +141,7 @@ function readMarkdownLibrary(dir: string): MarkdownEntry[] {
|
|
|
136
141
|
out.push({
|
|
137
142
|
id,
|
|
138
143
|
name: meta.name || id,
|
|
144
|
+
title: meta.title || meta.name || id,
|
|
139
145
|
description: meta.description ?? "",
|
|
140
146
|
category: meta.category,
|
|
141
147
|
version: meta.version,
|
|
@@ -155,6 +161,37 @@ function readMarkdownLibrary(dir: string): MarkdownEntry[] {
|
|
|
155
161
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
156
162
|
}
|
|
157
163
|
|
|
164
|
+
/**
|
|
165
|
+
* A MARKDOWN LIBRARY HAS TWO TIERS, and this host only ever read one.
|
|
166
|
+
*
|
|
167
|
+
* A skill or a prompt block lives in one of two places, and the NAME's shape is what
|
|
168
|
+
* carries which:
|
|
169
|
+
*
|
|
170
|
+
* UNIVERSAL `design/marketplace/<folder>/`, named bare. Published to the marketplace.
|
|
171
|
+
* ORG `design/<org>/<folder>/`, named `<org>/<name>`. Private to that org.
|
|
172
|
+
*
|
|
173
|
+
* The universe server resolves both (the items loaders). This host read the universal
|
|
174
|
+
* home alone, so a developer's OWN skill was missing from local Studio while sitting
|
|
175
|
+
* correctly in their universe, and the two surfaces disagreed about the same folder.
|
|
176
|
+
*
|
|
177
|
+
* Scoping is the server's `?org=`: an org sees the universal tier plus its own and never
|
|
178
|
+
* another org's, the rule RX_ORG_MODEL.md settled for a project's assets.
|
|
179
|
+
*/
|
|
180
|
+
function readLibrary(projectRoot: string, folder: "skills" | "blocks", orgs: string[], org?: string): MarkdownEntry[] {
|
|
181
|
+
const out = readMarkdownLibrary(resolve(designHome(projectRoot), "marketplace", folder));
|
|
182
|
+
for (const o of orgs) {
|
|
183
|
+
if (org && org !== "all" && o !== org) continue;
|
|
184
|
+
for (const e of readMarkdownLibrary(resolve(designHome(projectRoot), o, folder))) {
|
|
185
|
+
// A SKILL's lookup name carries its org, because that is the name every reader
|
|
186
|
+
// addresses it by and two orgs may hold one bare name. A BLOCK's does not: the
|
|
187
|
+
// server tags a block with its org and leaves the name alone, and the shape here
|
|
188
|
+
// has to be the server's or the screens read two different contracts.
|
|
189
|
+
out.push(folder === "skills" ? { ...e, name: `${o}/${e.id}`, org: o } : { ...e, org: o });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
193
|
+
}
|
|
194
|
+
|
|
158
195
|
/**
|
|
159
196
|
* Files this server has just written, so the watcher can tell Studio's own save apart from
|
|
160
197
|
* the developer's other editor. The window only has to outlast the filesystem event, which
|
|
@@ -205,7 +242,12 @@ export function localStudioHost(): Plugin {
|
|
|
205
242
|
// Node packages load lazily: only when the Nodes section is first opened, so a
|
|
206
243
|
// developer authoring only design assets never pays for it at startup.
|
|
207
244
|
let nodesOnce: Promise<LocalNodes | null> | null = null;
|
|
208
|
-
|
|
245
|
+
// Every project's own nodes: design/<project>/nodes/, org-scoped like its skills.
|
|
246
|
+
const nodesDirs = async () => {
|
|
247
|
+
const c = await catalog;
|
|
248
|
+
return c.listOrgs().map((o) => resolve(c.projectDir(o), "nodes"));
|
|
249
|
+
};
|
|
250
|
+
const nodes = () => (nodesOnce ??= nodesDirs().then((dirs) => loadLocalNodes(baseSrc, ssr, dirs)));
|
|
209
251
|
|
|
210
252
|
server.config.logger.info(`\n Studio reading: ${designHome(projectRoot)}\n`);
|
|
211
253
|
|
|
@@ -220,7 +262,7 @@ export function localStudioHost(): Plugin {
|
|
|
220
262
|
*
|
|
221
263
|
* Adding the paths puts them in the same watcher, so the existing hook fires.
|
|
222
264
|
*/
|
|
223
|
-
for (const dir of [designHome(projectRoot)
|
|
265
|
+
for (const dir of [designHome(projectRoot)]) {
|
|
224
266
|
const full = resolve(projectRoot, dir);
|
|
225
267
|
if (existsSync(full)) server.watcher.add(full);
|
|
226
268
|
}
|
|
@@ -255,7 +297,7 @@ export function localStudioHost(): Plugin {
|
|
|
255
297
|
return json(res, {
|
|
256
298
|
projectRoot,
|
|
257
299
|
rx: designHome(projectRoot),
|
|
258
|
-
nodes:
|
|
300
|
+
nodes: await nodesDirs(),
|
|
259
301
|
envFiles: [resolve(projectRoot, "..", ".env"), resolve(projectRoot, ".env")],
|
|
260
302
|
});
|
|
261
303
|
|
|
@@ -360,10 +402,14 @@ export function localStudioHost(): Plugin {
|
|
|
360
402
|
case path === "/dev/apps":
|
|
361
403
|
return json(res, { apps: c.listAppsForWorkbench(org) });
|
|
362
404
|
|
|
363
|
-
// The org's identity documents (design/<org>/identity/<doc
|
|
405
|
+
// The org's identity documents (design/<org>/identity/<doc>.yaml), one org or all.
|
|
364
406
|
case path === "/dev/identity":
|
|
365
407
|
return json(res, { items: c.listIdentity(org) });
|
|
366
408
|
|
|
409
|
+
// Agents: the one file authored for the map, design/<org>/agents/<name>.yaml.
|
|
410
|
+
case path === "/dev/agents":
|
|
411
|
+
return json(res, { items: c.listAgents(org) });
|
|
412
|
+
|
|
367
413
|
case path === "/dev/atoms":
|
|
368
414
|
return json(res, { atoms: c.listDefinitions("atom") });
|
|
369
415
|
|
|
@@ -437,7 +483,14 @@ export function localStudioHost(): Plugin {
|
|
|
437
483
|
if (!p || typeof b.text !== "string") return json(res, { error: "path and text are required" }, 400);
|
|
438
484
|
|
|
439
485
|
type LintModule = { lintDefinitions(designRoot: string): { problems: { level?: string; file?: string }[] } };
|
|
486
|
+
type ObjectsLintModule = { lintObjects(designRoot: string): { problems: { level?: string; file?: string }[] } };
|
|
487
|
+
type PromptsLintModule = { lintPrompts(designRoot: string): { problems: { level?: string; file?: string }[] } };
|
|
440
488
|
const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
|
|
489
|
+
// OBJECTS AND PIPELINES TOO (objects README 9a, 2026-09-10): the same third linter
|
|
490
|
+
// the publish gate runs, so a save here meets the sentence publish would print.
|
|
491
|
+
const objectsMod = (await ssr(`${baseSrc}/lint/objects/index.mjs`)) as unknown as ObjectsLintModule;
|
|
492
|
+
// SKILLS AND PROMPT BLOCKS TOO (2026-09-18): the fourth linter, same gate.
|
|
493
|
+
const promptsMod = (await ssr(`${baseSrc}/lint/prompts/index.mjs`)) as unknown as PromptsLintModule;
|
|
441
494
|
|
|
442
495
|
// Marked BEFORE the write, so the filesystem event can never beat the mark.
|
|
443
496
|
recentSelfWrites.set(resolve(p), Date.now());
|
|
@@ -446,7 +499,7 @@ export function localStudioHost(): Plugin {
|
|
|
446
499
|
b.text as string,
|
|
447
500
|
writableRoots(projectRoot, c),
|
|
448
501
|
projectRoot,
|
|
449
|
-
(designRoot) => lintMod.lintDefinitions(designRoot),
|
|
502
|
+
(designRoot) => ({ problems: [...lintMod.lintDefinitions(designRoot).problems, ...objectsMod.lintObjects(designRoot).problems, ...promptsMod.lintPrompts(designRoot).problems] }),
|
|
450
503
|
);
|
|
451
504
|
return json(res, result, result.saved ? 200 : result.error === "lint errors" ? 422 : 403);
|
|
452
505
|
}
|
|
@@ -470,9 +523,14 @@ export function localStudioHost(): Plugin {
|
|
|
470
523
|
options?: { overlay?: Record<string, string> },
|
|
471
524
|
): { problems: { level?: string; file?: string; line?: number; msg?: string }[] };
|
|
472
525
|
};
|
|
526
|
+
type ObjectsLintModule = { lintObjects(designRoot: string, options?: { overlay?: Record<string, string> }): { problems: { level?: string; file?: string; line?: number; msg?: string }[] } };
|
|
527
|
+
type PromptsLintModule = { lintPrompts(designRoot: string, options?: { overlay?: Record<string, string> }): { problems: { level?: string; file?: string; line?: number; msg?: string }[] } };
|
|
473
528
|
const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
|
|
529
|
+
const objectsMod = (await ssr(`${baseSrc}/lint/objects/index.mjs`)) as unknown as ObjectsLintModule;
|
|
530
|
+
const promptsMod = (await ssr(`${baseSrc}/lint/prompts/index.mjs`)) as unknown as PromptsLintModule;
|
|
474
531
|
const abs = resolve(p);
|
|
475
|
-
const
|
|
532
|
+
const overlay = { [abs]: b.text as string };
|
|
533
|
+
const problems = [...lintMod.lintDefinitions(designHome(projectRoot), { overlay }).problems, ...objectsMod.lintObjects(designHome(projectRoot), { overlay }).problems, ...promptsMod.lintPrompts(designHome(projectRoot), { overlay }).problems];
|
|
476
534
|
return json(res, {
|
|
477
535
|
findings: problems.filter((f) => f.file && resolve(f.file) === abs),
|
|
478
536
|
elsewhere: problems.filter((f) => !f.file || resolve(f.file) !== abs).length,
|
|
@@ -511,17 +569,17 @@ export function localStudioHost(): Plugin {
|
|
|
511
569
|
return json(res, selection ?? { empty: true });
|
|
512
570
|
|
|
513
571
|
case path === "/skills":
|
|
514
|
-
return json(res, { skills:
|
|
572
|
+
return json(res, { skills: readLibrary(projectRoot, "skills", c.listOrgs(), org) });
|
|
515
573
|
|
|
516
574
|
case path.startsWith("/skills/"): {
|
|
517
575
|
const name = decodeURIComponent(path.slice("/skills/".length).replace(/\/file$/, ""));
|
|
518
|
-
const found =
|
|
576
|
+
const found = readLibrary(projectRoot, "skills", c.listOrgs()).find((s) => s.name === name || s.id === name);
|
|
519
577
|
// SkillsView reads `d.skill`, matching the server's shape. Not the bare object.
|
|
520
578
|
return found ? json(res, { skill: found }) : json(res, { error: "skill not found" }, 404);
|
|
521
579
|
}
|
|
522
580
|
|
|
523
581
|
case path === "/prompt-blocks":
|
|
524
|
-
return json(res, { blocks:
|
|
582
|
+
return json(res, { blocks: readLibrary(projectRoot, "blocks", c.listOrgs(), org) });
|
|
525
583
|
|
|
526
584
|
// Registry toggles are a DEPLOYED concept: they persist to installed_plugins
|
|
527
585
|
// and decide what a running universe exposes. Local Studio has no database and
|
|
@@ -532,11 +590,11 @@ export function localStudioHost(): Plugin {
|
|
|
532
590
|
if (req.method === "POST") return json(res, { ok: true });
|
|
533
591
|
return json(res, { entries: [] });
|
|
534
592
|
|
|
535
|
-
// ---- The developer's OWN nodes (
|
|
593
|
+
// ---- The developer's OWN nodes (design/<project>/nodes/). No marketplace. ----
|
|
536
594
|
|
|
537
595
|
case path === "/nodes": {
|
|
538
596
|
const n = await nodes();
|
|
539
|
-
if (!n) return json(res, { nodes: [], credentialTypes: [], reason: "no nodes/ folder in this
|
|
597
|
+
if (!n) return json(res, { nodes: [], credentialTypes: [], reason: "no design/<project>/nodes/ folder in this workspace" });
|
|
540
598
|
return json(res, { nodes: n.list(), credentialTypes: n.credentialTypes() });
|
|
541
599
|
}
|
|
542
600
|
|
|
@@ -601,7 +659,7 @@ export function localStudioHost(): Plugin {
|
|
|
601
659
|
* else, which means the developer's other editor, still reloads.
|
|
602
660
|
*/
|
|
603
661
|
handleHotUpdate({ server, file }) {
|
|
604
|
-
const watched = [designHome(projectRoot)
|
|
662
|
+
const watched = [designHome(projectRoot)];
|
|
605
663
|
if (!watched.some((w) => file.startsWith(w))) return;
|
|
606
664
|
const self = recentSelfWrites.get(resolve(file));
|
|
607
665
|
if (self !== undefined && Date.now() - self < SELF_WRITE_WINDOW_MS) return [];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# The org's brand: its own words, and only those. The briefs, the limits and the shape are the
|
|
2
|
+
# brand object's (design/marketplace/objects/brand/); this file carries the answers as defaults.
|
|
3
|
+
unoverse: "1.0"
|
|
4
|
+
type: identity
|
|
5
|
+
extends: brand
|
|
6
|
+
props:
|
|
7
|
+
name:
|
|
8
|
+
default: ""
|
|
9
|
+
standsFor:
|
|
10
|
+
default: ""
|
|
11
|
+
promise:
|
|
12
|
+
default: ""
|
|
13
|
+
represents:
|
|
14
|
+
default: ""
|
|
15
|
+
positioning:
|
|
16
|
+
default: ""
|
|
17
|
+
differentiators:
|
|
18
|
+
default: []
|
|
19
|
+
brands:
|
|
20
|
+
default: []
|
|
21
|
+
rules:
|
|
22
|
+
default: []
|
|
23
|
+
examples:
|
|
24
|
+
default: []
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The org's organisation: its own words, and only those. The briefs, the limits and the shape are the
|
|
2
|
+
# organisation object's (design/marketplace/objects/organisation/); this file carries the answers as defaults.
|
|
3
|
+
unoverse: "1.0"
|
|
4
|
+
type: identity
|
|
5
|
+
extends: organisation
|
|
6
|
+
props:
|
|
7
|
+
name:
|
|
8
|
+
default: ""
|
|
9
|
+
what:
|
|
10
|
+
default: ""
|
|
11
|
+
units:
|
|
12
|
+
default: []
|
|
13
|
+
vocabulary:
|
|
14
|
+
default: ""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The org's purpose: its own words, and only those. The briefs, the limits and the shape are the
|
|
2
|
+
# purpose object's (design/marketplace/objects/purpose/); this file carries the answers as defaults.
|
|
3
|
+
unoverse: "1.0"
|
|
4
|
+
type: identity
|
|
5
|
+
extends: purpose
|
|
6
|
+
props:
|
|
7
|
+
statement:
|
|
8
|
+
default: ""
|
|
9
|
+
values:
|
|
10
|
+
default: []
|
|
11
|
+
ambition:
|
|
12
|
+
default: ""
|
|
13
|
+
goals:
|
|
14
|
+
default: []
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# The org's story: its own words, and only those. The briefs, the limits and the shape are the
|
|
2
|
+
# story object's (design/marketplace/objects/story/); this file carries the answers as defaults.
|
|
3
|
+
unoverse: "1.0"
|
|
4
|
+
type: identity
|
|
5
|
+
extends: story
|
|
6
|
+
props:
|
|
7
|
+
history:
|
|
8
|
+
default: ""
|
|
9
|
+
milestones:
|
|
10
|
+
default: []
|
|
11
|
+
present:
|
|
12
|
+
default: ""
|
|
13
|
+
vision:
|
|
14
|
+
default: ""
|
|
15
|
+
people:
|
|
16
|
+
default: ""
|
|
17
|
+
meaning:
|
|
18
|
+
default: ""
|
package/local/scaffold.mjs
CHANGED
|
@@ -90,10 +90,10 @@ export function createProject(home, name) {
|
|
|
90
90
|
cpSync(sampleStyles, join(root, "styles"), { recursive: true });
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
// THE IDENTITY (sample-identity: organisation, brand, purpose, story), four
|
|
94
|
-
//
|
|
95
|
-
// place to land from day one.
|
|
96
|
-
//
|
|
93
|
+
// THE IDENTITY (sample-identity: organisation, brand, purpose, story), four instances of
|
|
94
|
+
// the marketplace's identity objects with their defaults empty, so the org's own words
|
|
95
|
+
// have a place to land from day one. Generated by base's instanceText (a guard test holds
|
|
96
|
+
// them equal to what createOrgPack writes). Same rule: copied only into an empty folder.
|
|
97
97
|
const sampleIdentity = join(dirname(fileURLToPath(import.meta.url)), "sample-identity");
|
|
98
98
|
if (existsSync(sampleIdentity) && readdirSync(join(root, "identity")).length === 0) {
|
|
99
99
|
cpSync(sampleIdentity, join(root, "identity"), { recursive: true });
|
|
@@ -124,7 +124,10 @@ export function createProject(home, name) {
|
|
|
124
124
|
* makes that safe.
|
|
125
125
|
*/
|
|
126
126
|
const SAMPLES = [
|
|
127
|
-
|
|
127
|
+
// ORG-SCOPED since 2026-09-08, like the skill and block below: a project's nodes live at
|
|
128
|
+
// design/<project>/nodes/, the same rule as every other kind it authors. The id keeps
|
|
129
|
+
// the old flat string for the same reason theirs do.
|
|
130
|
+
{ id: "nodes/samples", dir: "sample-nodes/samples", to: (p) => `design/${p}/nodes/samples` },
|
|
128
131
|
// ORG-SCOPED since 2026-08-24: a sample skill/block lands in the FIRST project's own
|
|
129
132
|
// folder, because that is where authored skills and blocks now live. `to` is a
|
|
130
133
|
// function of the project for exactly that reason — the id stays the old flat string
|
|
@@ -189,10 +192,10 @@ export function topUpSamples(home, project) {
|
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
/**
|
|
192
|
-
* The FIRST project, plus
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
195
|
+
* The FIRST project, plus what a workspace needs exactly once: editor settings and the
|
|
196
|
+
* worked-example samples. Nothing lives beside the design tree any more: skills, blocks
|
|
197
|
+
* and (since 2026-09-08) nodes are all per-project, made by `createProject` or landed by
|
|
198
|
+
* topUpSamples inside the first project.
|
|
196
199
|
*/
|
|
197
200
|
|
|
198
201
|
/**
|
|
@@ -204,17 +207,34 @@ export function topUpSamples(home, project) {
|
|
|
204
207
|
*/
|
|
205
208
|
const SCHEMAS = "https://docs.unoverse.ai/schemas/nodes";
|
|
206
209
|
const NODE_SCHEMA_MAP = {
|
|
207
|
-
[`${SCHEMAS}/package.schema.json`]: ["nodes/*/package.yaml"],
|
|
208
|
-
[`${SCHEMAS}/credential.schema.json`]: ["nodes/*/credentials/*.yaml"],
|
|
209
|
-
[`${SCHEMAS}/node.schema.json`]: ["nodes/*/nodes/*/node.yaml"],
|
|
210
|
-
[`${SCHEMAS}/interface.schema.json`]: ["nodes/*/nodes/*/interface.yaml"],
|
|
211
|
-
[`${SCHEMAS}/config.schema.json`]: ["nodes/*/nodes/*/config.yaml"],
|
|
212
|
-
[`${SCHEMAS}/test.schema.json`]: ["nodes/*/nodes/*/test.yaml"],
|
|
213
|
-
[`${SCHEMAS}/run.schema.json`]: ["nodes/*/nodes/*/api/run.yaml"],
|
|
214
|
-
[`${SCHEMAS}/events.schema.json`]: ["nodes/*/nodes/*/api/events.yaml"],
|
|
215
|
-
[`${SCHEMAS}/service.schema.json`]: ["nodes/*/nodes/*/api/service.yaml"],
|
|
216
|
-
[`${SCHEMAS}/toolExchange.schema.json`]: ["nodes/*/nodes/*/api/toolExchange.yaml"],
|
|
217
|
-
[`${SCHEMAS}/audio.schema.json`]: ["nodes/*/nodes/*/api/audio.yaml"],
|
|
210
|
+
[`${SCHEMAS}/package.schema.json`]: ["design/*/nodes/*/package.yaml"],
|
|
211
|
+
[`${SCHEMAS}/credential.schema.json`]: ["design/*/nodes/*/credentials/*.yaml"],
|
|
212
|
+
[`${SCHEMAS}/node.schema.json`]: ["design/*/nodes/*/nodes/*/node.yaml"],
|
|
213
|
+
[`${SCHEMAS}/interface.schema.json`]: ["design/*/nodes/*/nodes/*/interface.yaml"],
|
|
214
|
+
[`${SCHEMAS}/config.schema.json`]: ["design/*/nodes/*/nodes/*/config.yaml"],
|
|
215
|
+
[`${SCHEMAS}/test.schema.json`]: ["design/*/nodes/*/nodes/*/test.yaml"],
|
|
216
|
+
[`${SCHEMAS}/run.schema.json`]: ["design/*/nodes/*/nodes/*/api/run.yaml"],
|
|
217
|
+
[`${SCHEMAS}/events.schema.json`]: ["design/*/nodes/*/nodes/*/api/events.yaml"],
|
|
218
|
+
[`${SCHEMAS}/service.schema.json`]: ["design/*/nodes/*/nodes/*/api/service.yaml"],
|
|
219
|
+
[`${SCHEMAS}/toolExchange.schema.json`]: ["design/*/nodes/*/nodes/*/api/toolExchange.yaml"],
|
|
220
|
+
[`${SCHEMAS}/audio.schema.json`]: ["design/*/nodes/*/nodes/*/api/audio.yaml"],
|
|
221
|
+
// OBJECTS AND PIPELINES (objects README 9a, 2026-09-10): folders in the node grammar. The
|
|
222
|
+
// envelope is named for its folder, so its glob excludes the sibling files BY NAME, and
|
|
223
|
+
// api/service.yaml among them: in the YAML extension's matcher a `*` segment can cross
|
|
224
|
+
// `api/`, so without `service` in the exclusion the envelope schema claimed the functions
|
|
225
|
+
// file and red-underlined every key in it (seen live 2026-09-10, redhat.vscode-yaml 1.24.0).
|
|
226
|
+
[`${SCHEMAS.replace(/\/nodes$/, "/objects")}/object.schema.json`]: ["design/*/objects/*/!(props|test|service|make|dataset).yaml"],
|
|
227
|
+
// `dataset` joined the exclusion 2026-09-18: an object that owns a table declares its columns
|
|
228
|
+
// in dataset.yaml, and the envelope schema knows nothing of columns.
|
|
229
|
+
[`${SCHEMAS.replace(/\/nodes$/, "/objects")}/dataset.schema.json`]: ["design/*/objects/*/dataset.yaml"],
|
|
230
|
+
[`${SCHEMAS.replace(/\/nodes$/, "/objects")}/props.schema.json`]: ["design/*/objects/*/props.yaml"],
|
|
231
|
+
[`${SCHEMAS.replace(/\/nodes$/, "/objects")}/service.schema.json`]: ["design/*/objects/*/api/service.yaml"],
|
|
232
|
+
[`${SCHEMAS.replace(/\/nodes$/, "/objects")}/make.schema.json`]: ["design/*/objects/*/api/make.yaml"],
|
|
233
|
+
[`${SCHEMAS.replace(/\/nodes$/, "/objects")}/test.schema.json`]: ["design/*/objects/*/test.yaml"],
|
|
234
|
+
[`${SCHEMAS.replace(/\/nodes$/, "/pipelines")}/pipeline.schema.json`]: ["design/*/pipelines/*/pipeline.yaml"],
|
|
235
|
+
[`${SCHEMAS.replace(/\/nodes$/, "/pipelines")}/config.schema.json`]: ["design/*/pipelines/*/config.yaml"],
|
|
236
|
+
[`${SCHEMAS.replace(/\/nodes$/, "/pipelines")}/interface.schema.json`]: ["design/*/pipelines/*/interface.yaml"],
|
|
237
|
+
[`${SCHEMAS.replace(/\/nodes$/, "/pipelines")}/test.schema.json`]: ["design/*/pipelines/*/test.yaml"],
|
|
218
238
|
};
|
|
219
239
|
export function writeEditorSettings(home) {
|
|
220
240
|
const dir = join(home, ".vscode");
|
|
@@ -227,9 +247,9 @@ export function writeEditorSettings(home) {
|
|
|
227
247
|
|
|
228
248
|
export function createFirstProject(home, name) {
|
|
229
249
|
const root = createProject(home, name);
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
250
|
+
// NOTHING beside the design tree. skills/ and blocks/ moved INTO the org on 2026-08-24
|
|
251
|
+
// and nodes/ followed on 2026-09-08 (the sample package lands at design/<org>/nodes/),
|
|
252
|
+
// so every authored kind sits in one place.
|
|
233
253
|
writeEditorSettings(home);
|
|
234
254
|
|
|
235
255
|
topUpSamples(home, name);
|
package/local/source.ts
CHANGED
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
* install, and §Design-system resolution is what keeps the token contract stable.
|
|
15
15
|
*
|
|
16
16
|
* So the writable set is exactly the developer's own work:
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* nodes/** their own node packages
|
|
17
|
+
* design/<project>/** every project listOrgs() reports, via projectDir(); their own
|
|
18
|
+
* node packages live inside it (design/<project>/nodes/)
|
|
20
19
|
* Everything else is readable and not writable, the marketplace and anything resolved out
|
|
21
20
|
* of node_modules included.
|
|
22
21
|
*
|
|
@@ -51,11 +50,7 @@ export function writableRoots(
|
|
|
51
50
|
projectRoot: string,
|
|
52
51
|
loaders: { listOrgs: () => string[]; projectDir: (name: string) => string },
|
|
53
52
|
): string[] {
|
|
54
|
-
const roots =
|
|
55
|
-
...loaders.listOrgs().map((org) => loaders.projectDir(org)),
|
|
56
|
-
resolve(projectRoot, "prompts"),
|
|
57
|
-
resolve(projectRoot, "nodes"),
|
|
58
|
-
];
|
|
53
|
+
const roots = loaders.listOrgs().map((org) => loaders.projectDir(org));
|
|
59
54
|
return roots.map((r) => resolve(r)).filter((r) => existsSync(r));
|
|
60
55
|
}
|
|
61
56
|
|