@unoverse-platform/studio 0.1.48 → 0.1.50
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/local/sample-nodes/samples/README.md +62 -0
- package/local/sample-nodes/samples/credentials/openAICredential.yaml +36 -0
- package/local/sample-nodes/samples/nodes/SampleChatStream/api/events.yaml +23 -0
- package/local/sample-nodes/samples/nodes/SampleChatStream/api/run.yaml +53 -0
- package/local/sample-nodes/samples/nodes/SampleChatStream/config.yaml +44 -0
- package/local/sample-nodes/samples/nodes/SampleChatStream/interface.yaml +29 -0
- package/local/sample-nodes/samples/nodes/SampleChatStream/node.yaml +39 -0
- package/local/sample-nodes/samples/nodes/SampleChatStream/test.yaml +21 -0
- package/local/sample-nodes/samples/nodes/SampleWeather/api/events.yaml +41 -0
- package/local/sample-nodes/samples/nodes/SampleWeather/api/run.yaml +51 -0
- package/local/sample-nodes/samples/nodes/SampleWeather/config.yaml +39 -0
- package/local/sample-nodes/samples/nodes/SampleWeather/interface.yaml +24 -0
- package/local/sample-nodes/samples/nodes/SampleWeather/node.yaml +56 -0
- package/local/sample-nodes/samples/nodes/SampleWeather/test.yaml +24 -0
- package/local/sample-nodes/samples/package.yaml +41 -0
- package/local/sample-nodes/samples/shared/helpers.yaml +27 -0
- package/local/scaffold.mjs +10 -0
- package/package.json +2 -2
- package/screens/AtomsView.tsx +14 -4
- package/screens/ComponentsView.tsx +16 -3
- package/screens/DesignSystemView.tsx +1 -1
- package/src/App.tsx +1 -1
- package/vendor/skills/unoverse-create/SKILL.md +9 -7
- package/vendor/skills/unoverse-create/references/brand.md +6 -3
- package/vendor/skills/unoverse-create/references/component.md +8 -7
- package/vendor/skills/unoverse-create/references/node.md +6 -21
- package/vendor/skills/unoverse-create/references/template.md +4 -3
- package/vendor/skills/unoverse-create/references/workflow.md +6 -0
- package/vendor/skills/unoverse-prompt-component/SKILL.md +2 -3
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Sample nodes
|
|
2
|
+
|
|
3
|
+
Two worked examples of the Unoverse node format, one of each kind. They are real,
|
|
4
|
+
working nodes: wire them into a workflow and they run.
|
|
5
|
+
|
|
6
|
+
| Node | Kind | What it teaches |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| `nodes/SampleWeather` | `PromiseNode` | Two chained calls, a config form, computed outputs. No API key. |
|
|
9
|
+
| `nodes/SampleChatStream` | `CallbackNode` | Streaming, and how credentials work. Needs an OpenAI key. |
|
|
10
|
+
|
|
11
|
+
## Where to start
|
|
12
|
+
|
|
13
|
+
Read `SampleWeather` first, file by file, in this order:
|
|
14
|
+
|
|
15
|
+
1. `node.yaml` says what the node IS: its identity, its kind, and when a planner
|
|
16
|
+
should pick it.
|
|
17
|
+
2. `interface.yaml` says what it CONNECTS TO: inputs, outputs. Output names become
|
|
18
|
+
what someone types in a template later, so they are named for what they carry.
|
|
19
|
+
3. `config.yaml` is the settings form a builder sees on the canvas. It is a JSON
|
|
20
|
+
schema; the platform draws the form from it.
|
|
21
|
+
4. `api/run.yaml` is the calls the node makes, always a list, each entry named.
|
|
22
|
+
5. `api/events.yaml` is everything that leaves the node: one row per output.
|
|
23
|
+
6. `test.yaml` is a fixture that runs against the real service.
|
|
24
|
+
|
|
25
|
+
Then read `SampleChatStream` for the two things weather cannot show you: a reply
|
|
26
|
+
that arrives as a stream of events rather than one body, and a call that has to
|
|
27
|
+
prove itself to the service with a credential.
|
|
28
|
+
|
|
29
|
+
## Running them
|
|
30
|
+
|
|
31
|
+
Open Studio (`unoverse studio`) and go to the Nodes screen. Pick a node, load its
|
|
32
|
+
sample, and run it. Running is the only proof: a node that lints but has never run
|
|
33
|
+
is not done.
|
|
34
|
+
|
|
35
|
+
`SampleWeather` runs as-is. `SampleChatStream` needs a key: credentials are read
|
|
36
|
+
from your `.env` as `<CREDENTIAL>_<FIELD>` in upper snake case, so
|
|
37
|
+
`openAICredential.apiKey` is:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
OPENAI_API_KEY=sk-...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The credential FILE in this package (`credentials/openAICredential.yaml`) declares
|
|
44
|
+
only the shape. No value ever lives in a node file, which is why the folder is safe
|
|
45
|
+
to commit and to share.
|
|
46
|
+
|
|
47
|
+
## Making your own
|
|
48
|
+
|
|
49
|
+
Copy the closer of the two samples into a new folder under `nodes/<your-package>/`
|
|
50
|
+
and change it one file at a time, running as you go. The guide behind every file:
|
|
51
|
+
|
|
52
|
+
- The format in full: https://docs.unoverse.ai/nodes/manifest-nodes
|
|
53
|
+
- One answer or many: https://docs.unoverse.ai/nodes/node-types
|
|
54
|
+
- Credentials: https://docs.unoverse.ai/nodes/credentials
|
|
55
|
+
- The settings form: https://docs.unoverse.ai/nodes/config-schema
|
|
56
|
+
- Paging, batching, polling: https://docs.unoverse.ai/nodes/calls-that-loop
|
|
57
|
+
- Getting picked by the planner: https://docs.unoverse.ai/nodes/node-discoverability
|
|
58
|
+
- Testing: https://docs.unoverse.ai/nodes/testing-nodes
|
|
59
|
+
- When something goes wrong: https://docs.unoverse.ai/nodes/troubleshooting
|
|
60
|
+
|
|
61
|
+
The condensed rulebook, written for coding agents and just as useful to people:
|
|
62
|
+
https://docs.unoverse.ai/nodes/CLAUDE.md
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# THE SHAPE OF A CREDENTIAL, NEVER ITS VALUE.
|
|
2
|
+
#
|
|
3
|
+
# This file declares what an OpenAI credential LOOKS LIKE: which fields it has, which
|
|
4
|
+
# are secret, what to show in the form that collects it. It never holds a key. Values
|
|
5
|
+
# live where they already live: encrypted in the database on a deployed universe, and
|
|
6
|
+
# in your own .env when running locally.
|
|
7
|
+
#
|
|
8
|
+
# Locally the runner reads .env as <CREDENTIAL>_<FIELD> in upper snake case, so
|
|
9
|
+
# openAICredential.apiKey is the .env line:
|
|
10
|
+
#
|
|
11
|
+
# OPENAI_API_KEY=sk-...
|
|
12
|
+
#
|
|
13
|
+
# Nodes reference this shape BY NAME. At run time every credential in the workflow is
|
|
14
|
+
# available keyed by name, and a call selects the one it means:
|
|
15
|
+
#
|
|
16
|
+
# credential:
|
|
17
|
+
# scheme: bearer
|
|
18
|
+
# token: "{{ credentials.openAICredential.apiKey }}"
|
|
19
|
+
#
|
|
20
|
+
# Full guide: https://docs.unoverse.ai/nodes/credentials
|
|
21
|
+
|
|
22
|
+
name: openAICredential
|
|
23
|
+
displayName: OpenAI
|
|
24
|
+
description: Credentials for the OpenAI API
|
|
25
|
+
documentationUrl: https://platform.openai.com/api-keys
|
|
26
|
+
|
|
27
|
+
properties:
|
|
28
|
+
- name: apiKey
|
|
29
|
+
displayName: API Key
|
|
30
|
+
type: string
|
|
31
|
+
required: true
|
|
32
|
+
# `secret: true` is what keeps the value write-only: it is stored encrypted and
|
|
33
|
+
# never echoed back into any form or log.
|
|
34
|
+
secret: true
|
|
35
|
+
description: Your OpenAI API key
|
|
36
|
+
placeholder: sk-...
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ONE ROW PER OUTPUT, in interface.yaml order. On a streaming call each row names the
|
|
2
|
+
# event type it fires on with `match`, and there are two rows here because a streaming
|
|
3
|
+
# node answers twice: continuously while writing, and once when done.
|
|
4
|
+
# https://docs.unoverse.ai/nodes/node-types
|
|
5
|
+
|
|
6
|
+
# THE LIVE FEED. Two settings here are worth copying into any streaming node you
|
|
7
|
+
# write. `accumulate: true` emits the reply SO FAR rather than the newest fragment: a
|
|
8
|
+
# stream of single words is almost never what a consumer wants. `throttleMs` bounds
|
|
9
|
+
# how often it emits, or a long answer becomes hundreds of events; nothing held back
|
|
10
|
+
# is lost, whatever is pending is flushed when the run ends.
|
|
11
|
+
- emit: stream
|
|
12
|
+
from: response
|
|
13
|
+
match: response.output_text.delta
|
|
14
|
+
value: "return response.delta"
|
|
15
|
+
accumulate: true
|
|
16
|
+
throttleMs: 200
|
|
17
|
+
|
|
18
|
+
# THE SETTLED ANSWER. `from: complete` fires once at the end, over everything the
|
|
19
|
+
# node emitted, which is how a streaming node also produces one finished value.
|
|
20
|
+
# `.at(-1)` and not `.pop()`: expressions are sandboxed and nothing mutates.
|
|
21
|
+
- emit: text
|
|
22
|
+
from: complete
|
|
23
|
+
value: "return events.filter(e => e.emit === 'stream').map(e => e.value).at(-1) ?? ''"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ONE CALL, MANY ANSWERS. `transport: sse` says the reply arrives as a stream of
|
|
2
|
+
# events rather than one body, which is what makes this node a CallbackNode.
|
|
3
|
+
# https://docs.unoverse.ai/nodes/node-types
|
|
4
|
+
|
|
5
|
+
- name: generate
|
|
6
|
+
method: POST
|
|
7
|
+
url: https://api.openai.com/v1/responses
|
|
8
|
+
|
|
9
|
+
# HOW THE NODE PROVES ITSELF TO THE VENDOR. This is OUTBOUND authentication, and it
|
|
10
|
+
# is a different thing from node.yaml's `auth`, which is who may RUN the node.
|
|
11
|
+
#
|
|
12
|
+
# `scheme: bearer` is an executor capability: the executor attaches the header, so
|
|
13
|
+
# no manifest ever handles the secret itself. The token path selects the credential
|
|
14
|
+
# BY NAME from the shapes this package declares. Note also that api.openai.com is
|
|
15
|
+
# in package.yaml's allowedHosts; a manifest cannot send this token anywhere that
|
|
16
|
+
# list does not name. https://docs.unoverse.ai/nodes/credentials
|
|
17
|
+
credential:
|
|
18
|
+
scheme: bearer
|
|
19
|
+
token: "{{ credentials.openAICredential.apiKey }}"
|
|
20
|
+
|
|
21
|
+
body:
|
|
22
|
+
model: "{{ config.model }}"
|
|
23
|
+
# A REQUEST WHOSE SHAPE CHANGES WITH THE SETTINGS. The system message exists only
|
|
24
|
+
# when the builder wrote one: sending an empty message is a 400 from the vendor,
|
|
25
|
+
# so the body is built by an expression rather than a fixed template. A field
|
|
26
|
+
# that is one "{{ path }}" stays a template; a field that must DECIDE something
|
|
27
|
+
# is an expression.
|
|
28
|
+
input: >-
|
|
29
|
+
return [
|
|
30
|
+
config.systemPrompt
|
|
31
|
+
? { type: "message", role: "system", content: config.systemPrompt }
|
|
32
|
+
: null,
|
|
33
|
+
{ type: "message", role: "user", content: config.prompt }
|
|
34
|
+
].filter(Boolean)
|
|
35
|
+
max_output_tokens: "{{ config.maxTokens }}"
|
|
36
|
+
stream: true
|
|
37
|
+
|
|
38
|
+
timeoutMs: 300000
|
|
39
|
+
|
|
40
|
+
retry:
|
|
41
|
+
attempts: 3
|
|
42
|
+
backoff: exponential
|
|
43
|
+
on: [429, 500, 502, 503, 504]
|
|
44
|
+
|
|
45
|
+
# The reply's framing belongs to the call: it streams server-sent events until the
|
|
46
|
+
# terminator line.
|
|
47
|
+
transport: sse
|
|
48
|
+
terminator: "[DONE]"
|
|
49
|
+
|
|
50
|
+
# A 200 carrying an error body must not read as success.
|
|
51
|
+
error:
|
|
52
|
+
when: "return response.type === 'response.failed'"
|
|
53
|
+
message: "return response.response.error.message"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# THE SETTINGS FORM. https://docs.unoverse.ai/nodes/config-schema
|
|
2
|
+
|
|
3
|
+
configSchema:
|
|
4
|
+
type: object
|
|
5
|
+
required: [model, prompt]
|
|
6
|
+
|
|
7
|
+
properties:
|
|
8
|
+
model:
|
|
9
|
+
type: string
|
|
10
|
+
title: Model
|
|
11
|
+
description: The model that writes the reply
|
|
12
|
+
enum: [gpt-5.6, gpt-5.6-terra, gpt-5.6-luna]
|
|
13
|
+
default: gpt-5.6-terra
|
|
14
|
+
"ui:widget": select
|
|
15
|
+
|
|
16
|
+
prompt:
|
|
17
|
+
type: string
|
|
18
|
+
title: Prompt
|
|
19
|
+
description: >-
|
|
20
|
+
What to ask. A template field, so values from earlier nodes compose in
|
|
21
|
+
directly, e.g. Summarize this update. {{signal.weather1.summary}}
|
|
22
|
+
default: ""
|
|
23
|
+
ui:field: template
|
|
24
|
+
|
|
25
|
+
systemPrompt:
|
|
26
|
+
type: string
|
|
27
|
+
title: System Prompt
|
|
28
|
+
description: >-
|
|
29
|
+
Standing instructions for every run: the model's role, tone and limits.
|
|
30
|
+
Optional. NEVER paste instruction text into a manifest file itself; shared
|
|
31
|
+
instruction copy belongs in the prompt block library and is referenced here
|
|
32
|
+
as {{prompt.<blockName>}}, so it keeps tracking the block when the block is
|
|
33
|
+
edited.
|
|
34
|
+
default: ""
|
|
35
|
+
ui:field: template
|
|
36
|
+
|
|
37
|
+
maxTokens:
|
|
38
|
+
type: number
|
|
39
|
+
title: Max output tokens
|
|
40
|
+
description: The ceiling on how long the reply may run
|
|
41
|
+
default: 1024
|
|
42
|
+
minimum: 16
|
|
43
|
+
maximum: 32768
|
|
44
|
+
"ui:order": [model, prompt, systemPrompt, maxTokens]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# WHAT THIS NODE CONNECTS TO. Two outputs is the signature streaming pair: the live
|
|
2
|
+
# feed for whoever wants to watch, and the settled answer for whoever wants the
|
|
3
|
+
# result. Wire `stream` to a streaming text component and the reply appears as it is
|
|
4
|
+
# written; wire `text` onward and downstream nodes get one finished string.
|
|
5
|
+
|
|
6
|
+
inputs:
|
|
7
|
+
- name: signal
|
|
8
|
+
type: object
|
|
9
|
+
description: Data from previous nodes that can be referenced in templates
|
|
10
|
+
|
|
11
|
+
outputs:
|
|
12
|
+
- name: stream
|
|
13
|
+
type: object
|
|
14
|
+
description: The reply so far, emitted repeatedly while the model writes
|
|
15
|
+
|
|
16
|
+
- name: text
|
|
17
|
+
type: string
|
|
18
|
+
description: The complete reply, emitted once at the end
|
|
19
|
+
|
|
20
|
+
# THE CREDENTIAL THIS NODE NEEDS, referenced by NAME. The shape it points at is
|
|
21
|
+
# credentials/openAICredential.yaml in this package: fields, which are secret, what
|
|
22
|
+
# the collecting form shows. The VALUE is never in any file. On a universe it is
|
|
23
|
+
# entered once and stored encrypted; locally it is read from .env as
|
|
24
|
+
# OPENAI_API_KEY. https://docs.unoverse.ai/nodes/credentials
|
|
25
|
+
credentials:
|
|
26
|
+
- name: openAICredential
|
|
27
|
+
required: true
|
|
28
|
+
displayName: OpenAI API
|
|
29
|
+
description: How this node authenticates to OpenAI
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# WHAT THIS NODE IS: the second worked example, a CallbackNode.
|
|
2
|
+
#
|
|
3
|
+
# Read SampleWeather first. This node adds the two things weather cannot show you:
|
|
4
|
+
# a reply that STREAMS in as many events instead of settling once, and a call that
|
|
5
|
+
# must prove itself to the service with a CREDENTIAL.
|
|
6
|
+
# https://docs.unoverse.ai/nodes/node-types
|
|
7
|
+
|
|
8
|
+
type: SampleChatStream
|
|
9
|
+
|
|
10
|
+
# A node that keeps answering. Lint checks this rather than trusting it: the LAST
|
|
11
|
+
# call in api/run.yaml has `transport: sse`, so declaring PromiseNode here would be
|
|
12
|
+
# an error naming exactly that contradiction.
|
|
13
|
+
kind: CallbackNode
|
|
14
|
+
|
|
15
|
+
name: Chat Stream Sample
|
|
16
|
+
category: AI
|
|
17
|
+
color: "#7C5CFF"
|
|
18
|
+
|
|
19
|
+
description: A model reply streamed word by word as it is written, the simplest streaming shape
|
|
20
|
+
|
|
21
|
+
# Ranked against the user's own words, so: outcome first, disqualifiers as
|
|
22
|
+
# properties of the work, wiring fact last, and never the name of another node.
|
|
23
|
+
# https://docs.unoverse.ai/nodes/node-discoverability
|
|
24
|
+
whenToUse: >-
|
|
25
|
+
A worked example for learning the streaming node shape: send a prompt, watch the
|
|
26
|
+
reply arrive word by word, and read the manifest that made it happen. A single
|
|
27
|
+
pass with no tools and no memory, so real workloads that need tool calls,
|
|
28
|
+
reasoning control or multi-turn state want a fuller model node.
|
|
29
|
+
|
|
30
|
+
# WHO MAY RUN THIS. Compulsory, and separate from the credential below: this block
|
|
31
|
+
# is who may press the button, the credential is how the node proves itself to the
|
|
32
|
+
# vendor once pressed. https://docs.unoverse.ai/nodes/who-can-run-it
|
|
33
|
+
auth:
|
|
34
|
+
required: false
|
|
35
|
+
|
|
36
|
+
capabilities:
|
|
37
|
+
# A model completion is non-deterministic: re-running is correct behaviour, and
|
|
38
|
+
# reusing a previous answer would be wrong, not thrifty.
|
|
39
|
+
cacheable: false
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# THE PROOF. This fixture calls the real API, so it needs your key in .env first:
|
|
2
|
+
#
|
|
3
|
+
# OPENAI_API_KEY=sk-...
|
|
4
|
+
#
|
|
5
|
+
# (The name comes from the credential shape: openAICredential.apiKey, upper snake
|
|
6
|
+
# case, so OPENAI_API_KEY.) Then run it from Studio's Nodes screen, or:
|
|
7
|
+
# unoverse node test SampleChatStream
|
|
8
|
+
#
|
|
9
|
+
# Assert the shape, never exact words: the model will not say the same thing twice.
|
|
10
|
+
# https://docs.unoverse.ai/nodes/testing-nodes
|
|
11
|
+
|
|
12
|
+
testData:
|
|
13
|
+
config:
|
|
14
|
+
model: gpt-5.6-luna
|
|
15
|
+
prompt: Say hello in one short sentence.
|
|
16
|
+
systemPrompt: ""
|
|
17
|
+
maxTokens: 64
|
|
18
|
+
inputs:
|
|
19
|
+
signal: {}
|
|
20
|
+
expect:
|
|
21
|
+
text: "return output.text.length > 0"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# EVERYTHING THAT LEAVES THIS NODE: one row per output connector, in the SAME ORDER
|
|
2
|
+
# interface.yaml declares them. Lint holds the order and the coverage: an output with
|
|
3
|
+
# no row here stays silently empty, which is the first thing to check when a
|
|
4
|
+
# downstream template resolves to nothing.
|
|
5
|
+
#
|
|
6
|
+
# In these expressions `response` is the LAST call's reply (the forecast) and every
|
|
7
|
+
# call is also readable by name as `calls.<name>`. The sandbox allows no mutation:
|
|
8
|
+
# `.at(-1)` not `.pop()`, `.toSorted()` not `.sort()`.
|
|
9
|
+
# https://docs.unoverse.ai/nodes/signal-routing
|
|
10
|
+
|
|
11
|
+
# The full picture, shaped HERE so the component that shows it stays dumb. Anything
|
|
12
|
+
# computed (rounding, pairing the daily arrays up, turning code 61 into "light rain")
|
|
13
|
+
# is this node's job: definitions in design/ never compute.
|
|
14
|
+
- emit: weather
|
|
15
|
+
value: >-
|
|
16
|
+
return {
|
|
17
|
+
place: calls.geocode.results[0].name,
|
|
18
|
+
country: calls.geocode.results[0].country || "",
|
|
19
|
+
unit: response.current_units.temperature_2m,
|
|
20
|
+
current: {
|
|
21
|
+
temperature: Math.round(response.current.temperature_2m),
|
|
22
|
+
feelsLike: Math.round(response.current.apparent_temperature),
|
|
23
|
+
humidity: response.current.relative_humidity_2m,
|
|
24
|
+
windSpeed: response.current.wind_speed_10m,
|
|
25
|
+
condition: helpers.describe(response.current.weather_code)
|
|
26
|
+
},
|
|
27
|
+
daily: response.daily.time.map((date, i) => ({
|
|
28
|
+
date: date,
|
|
29
|
+
high: Math.round(response.daily.temperature_2m_max[i]),
|
|
30
|
+
low: Math.round(response.daily.temperature_2m_min[i]),
|
|
31
|
+
condition: helpers.describe(response.daily.weather_code[i])
|
|
32
|
+
}))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# The one-liner, for dropping straight into a sentence or a headline. Same facts,
|
|
36
|
+
# second shape: emitting both is cheaper than making every consumer re-derive one.
|
|
37
|
+
- emit: summary
|
|
38
|
+
value: >-
|
|
39
|
+
return Math.round(response.current.temperature_2m) + "° and " +
|
|
40
|
+
helpers.describe(response.current.weather_code) + " in " +
|
|
41
|
+
calls.geocode.results[0].name
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# THE CALLS THIS NODE MAKES. Always a list, each entry named, even for one call.
|
|
2
|
+
#
|
|
3
|
+
# This node is the "two calls, where the second uses the first" shape: a person types
|
|
4
|
+
# a city NAME, the forecast API wants COORDINATES, so the node looks the place up and
|
|
5
|
+
# then asks about it. A later call reads an earlier reply as `calls.<name>`. The node
|
|
6
|
+
# orchestrates both behind one clean answer; the workflow never sees the seam.
|
|
7
|
+
# https://docs.unoverse.ai/nodes/manifest-nodes
|
|
8
|
+
|
|
9
|
+
# CALL 1: the place, by name. Open-Meteo's geocoder, no key needed.
|
|
10
|
+
- name: geocode
|
|
11
|
+
method: GET
|
|
12
|
+
url: https://geocoding-api.open-meteo.com/v1/search
|
|
13
|
+
# `transport: json` says the reply arrives as ONE body. The streaming sample next
|
|
14
|
+
# door uses `sse`, where the reply arrives as many events.
|
|
15
|
+
transport: json
|
|
16
|
+
query:
|
|
17
|
+
name: "{{ config.city }}"
|
|
18
|
+
count: "1"
|
|
19
|
+
|
|
20
|
+
# ALWAYS write `error` where the API can answer 200 with a failure body. An unknown
|
|
21
|
+
# place is a 200 with no results, and without this rule it would flow downstream as
|
|
22
|
+
# an empty forecast instead of failing here with the true reason.
|
|
23
|
+
error:
|
|
24
|
+
when: "return !(response.results && response.results.length)"
|
|
25
|
+
message: >-
|
|
26
|
+
return 'No place named "' + config.city + '" was found. Check the spelling, or
|
|
27
|
+
try a larger town nearby.'
|
|
28
|
+
|
|
29
|
+
# CALL 2: the forecast, at the coordinates call 1 found. Dot paths into an earlier
|
|
30
|
+
# reply, and array indexes are dot segments too: `results.0.latitude`, never brackets.
|
|
31
|
+
- name: forecast
|
|
32
|
+
method: GET
|
|
33
|
+
url: https://api.open-meteo.com/v1/forecast
|
|
34
|
+
transport: json
|
|
35
|
+
query:
|
|
36
|
+
latitude: "{{ calls.geocode.results.0.latitude }}"
|
|
37
|
+
longitude: "{{ calls.geocode.results.0.longitude }}"
|
|
38
|
+
current: temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,weather_code
|
|
39
|
+
daily: temperature_2m_max,temperature_2m_min,weather_code
|
|
40
|
+
forecast_days: "{{ config.days }}"
|
|
41
|
+
temperature_unit: "{{ config.units }}"
|
|
42
|
+
timezone: auto
|
|
43
|
+
|
|
44
|
+
timeoutMs: 30000
|
|
45
|
+
|
|
46
|
+
# Retry only what retrying can fix: rate limits and transient server errors. A 404
|
|
47
|
+
# or a bad request will fail identically every time, so it is not in the list.
|
|
48
|
+
retry:
|
|
49
|
+
attempts: 3
|
|
50
|
+
backoff: exponential
|
|
51
|
+
on: [429, 500, 502, 503, 504]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# THE SETTINGS FORM. This is a JSON schema; the platform draws the canvas form
|
|
2
|
+
# from it. A `description` says what the setting DOES, not what it is called.
|
|
3
|
+
# Every field type, and the run context that resolves them:
|
|
4
|
+
# https://docs.unoverse.ai/nodes/config-schema
|
|
5
|
+
|
|
6
|
+
configSchema:
|
|
7
|
+
type: object
|
|
8
|
+
required: [city]
|
|
9
|
+
|
|
10
|
+
properties:
|
|
11
|
+
city:
|
|
12
|
+
type: string
|
|
13
|
+
title: City
|
|
14
|
+
description: >-
|
|
15
|
+
The place to report on. A city or town name, in any language. This is a
|
|
16
|
+
template field, so a value from an earlier node works here too, e.g.
|
|
17
|
+
{{signal.inputtrigger1.output.city}}
|
|
18
|
+
default: ""
|
|
19
|
+
# `ui:field: template` is what lets a workflow supply the value at run time
|
|
20
|
+
# instead of it being fixed at build time. Every value the workflow supplies
|
|
21
|
+
# needs it.
|
|
22
|
+
ui:field: template
|
|
23
|
+
|
|
24
|
+
days:
|
|
25
|
+
type: number
|
|
26
|
+
title: Forecast days
|
|
27
|
+
description: How many days of outlook to fetch, today included
|
|
28
|
+
default: 3
|
|
29
|
+
minimum: 1
|
|
30
|
+
maximum: 7
|
|
31
|
+
|
|
32
|
+
units:
|
|
33
|
+
type: string
|
|
34
|
+
title: Units
|
|
35
|
+
description: The temperature scale everything is reported in
|
|
36
|
+
enum: [celsius, fahrenheit]
|
|
37
|
+
default: celsius
|
|
38
|
+
"ui:widget": select
|
|
39
|
+
"ui:order": [city, days, units]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# WHAT THIS NODE CONNECTS TO: the sockets on the box a builder sees on the canvas.
|
|
2
|
+
#
|
|
3
|
+
# Output names become what someone types in a template later
|
|
4
|
+
# ("{{signal.weather1.summary}}"), so they are named for what they CARRY.
|
|
5
|
+
# https://docs.unoverse.ai/nodes/signal-routing
|
|
6
|
+
|
|
7
|
+
inputs:
|
|
8
|
+
- name: signal
|
|
9
|
+
type: object
|
|
10
|
+
description: Data from previous nodes that can be referenced in templates
|
|
11
|
+
|
|
12
|
+
outputs:
|
|
13
|
+
# The full picture, for binding into a component field by field.
|
|
14
|
+
- name: weather
|
|
15
|
+
type: object
|
|
16
|
+
description: Place, current conditions and the daily outlook, as one object
|
|
17
|
+
|
|
18
|
+
# The one-line version, for dropping straight into text.
|
|
19
|
+
- name: summary
|
|
20
|
+
type: string
|
|
21
|
+
description: A single sentence, like "18° and partly cloudy in Lisbon"
|
|
22
|
+
|
|
23
|
+
# No credentials block: Open-Meteo asks for no key. See SampleChatStream for a node
|
|
24
|
+
# that needs one.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# WHAT THIS NODE IS. The one required file.
|
|
2
|
+
#
|
|
3
|
+
# A worked example of a PromiseNode: it makes its calls, settles once, and hands on
|
|
4
|
+
# one answer. Read this folder in order: node.yaml (identity), interface.yaml
|
|
5
|
+
# (connections), config.yaml (the settings form), api/run.yaml (the calls),
|
|
6
|
+
# api/events.yaml (what leaves), test.yaml (the proof).
|
|
7
|
+
# Format in full: https://docs.unoverse.ai/nodes/manifest-nodes
|
|
8
|
+
|
|
9
|
+
# The IDENTITY a saved workflow stores. Renaming it orphans every workflow that
|
|
10
|
+
# uses the node, so never rename lightly.
|
|
11
|
+
type: SampleWeather
|
|
12
|
+
|
|
13
|
+
# One answer or many? A node that calls, settles and hands on ONE reply is a
|
|
14
|
+
# PromiseNode. Lint checks this against the transport of the LAST call in
|
|
15
|
+
# api/run.yaml (json settles; sse or ws streams).
|
|
16
|
+
# https://docs.unoverse.ai/nodes/node-types
|
|
17
|
+
kind: PromiseNode
|
|
18
|
+
|
|
19
|
+
name: Weather Sample
|
|
20
|
+
# The JOB, not the implementation, and from the platform's closed list: lint
|
|
21
|
+
# refuses an invented category, because the canvas palette groups by these.
|
|
22
|
+
category: Storage & Data
|
|
23
|
+
color: "#4A90D9"
|
|
24
|
+
|
|
25
|
+
description: Current conditions and a short forecast for any city, from Open-Meteo, no API key needed
|
|
26
|
+
|
|
27
|
+
# WHENTOUSE DECIDES WHETHER THIS NODE IS EVER OFFERED. A planner ranks nodes by
|
|
28
|
+
# embedding this text against the user's own words: outcome first, then what
|
|
29
|
+
# disqualifies it as a property of the work, never the name of another node.
|
|
30
|
+
# Read https://docs.unoverse.ai/nodes/node-discoverability before writing yours.
|
|
31
|
+
whenToUse: >-
|
|
32
|
+
Get the weather for a named place: current temperature, wind, humidity and a
|
|
33
|
+
day-by-day outlook, ready to show to a person or hand to a model. Takes a city or
|
|
34
|
+
town name, not coordinates. It reports observed forecasts only; it cannot answer
|
|
35
|
+
historical climate questions.
|
|
36
|
+
|
|
37
|
+
# WHO MAY RUN THIS. Compulsory on every node. `required: false` adds no requirement
|
|
38
|
+
# of its own: the node runs for whoever the workflow's trigger admitted. It does NOT
|
|
39
|
+
# mean public; the trigger still owns the door.
|
|
40
|
+
# https://docs.unoverse.ai/nodes/who-can-run-it
|
|
41
|
+
auth:
|
|
42
|
+
required: false
|
|
43
|
+
# Role-based access is one line, and naming a role implies required: true. But a
|
|
44
|
+
# role HERE is only for a claim the platform itself defines (marketplace:publish),
|
|
45
|
+
# because a published node cannot know what roles a stranger's identity provider
|
|
46
|
+
# mints. A business role like finance:approve belongs to the workflow builder: the
|
|
47
|
+
# platform puts a "Require sign-in" / "Require role" section on every node's
|
|
48
|
+
# settings form, so the person who knows their deployment's role names sets one
|
|
49
|
+
# per box. You never author those fields.
|
|
50
|
+
# role: marketplace:publish
|
|
51
|
+
|
|
52
|
+
capabilities:
|
|
53
|
+
# A forecast changes hour to hour, so re-running is the correct behaviour and a
|
|
54
|
+
# cached answer would be stale, not thrifty. `cacheable: true` is ONLY for
|
|
55
|
+
# idempotent, side-effect-free reads of stable data.
|
|
56
|
+
cacheable: false
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# A FIXTURE THAT RUNS, against the real service. Every node needs one: a node that
|
|
2
|
+
# lints but has never run is not done, because only a run proves the manifest
|
|
3
|
+
# describes the real API rather than something merely coherent.
|
|
4
|
+
#
|
|
5
|
+
# Run it from Studio's Nodes screen (Load sample, then Run), or:
|
|
6
|
+
# unoverse node test SampleWeather
|
|
7
|
+
#
|
|
8
|
+
# Assert the SHAPE, never exact words: the temperature in Lisbon is not a constant.
|
|
9
|
+
# https://docs.unoverse.ai/nodes/testing-nodes
|
|
10
|
+
|
|
11
|
+
testData:
|
|
12
|
+
config:
|
|
13
|
+
city: Lisbon
|
|
14
|
+
days: 3
|
|
15
|
+
units: celsius
|
|
16
|
+
inputs:
|
|
17
|
+
signal: {}
|
|
18
|
+
expect:
|
|
19
|
+
weather: >-
|
|
20
|
+
return output.weather.place === "Lisbon" &&
|
|
21
|
+
typeof output.weather.current.temperature === "number" &&
|
|
22
|
+
typeof output.weather.current.condition === "string" &&
|
|
23
|
+
output.weather.daily.length === 3
|
|
24
|
+
summary: "return output.summary.length > 0 && output.summary.includes('Lisbon')"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# THE PACKAGE ENVELOPE. Every node package has exactly one of these.
|
|
2
|
+
#
|
|
3
|
+
# A node is a folder of YAML that the platform interprets. There is no TypeScript,
|
|
4
|
+
# no package.json, no build step and no publish step. The full format:
|
|
5
|
+
# https://docs.unoverse.ai/nodes/package-marketplace
|
|
6
|
+
#
|
|
7
|
+
# This package holds two worked examples, one of each node kind:
|
|
8
|
+
#
|
|
9
|
+
# nodes/SampleWeather/ a PromiseNode. Two chained calls, one settled answer.
|
|
10
|
+
# nodes/SampleChatStream/ a CallbackNode. A reply that streams in as it is written.
|
|
11
|
+
#
|
|
12
|
+
# Read them in that order. Then read the guide they follow:
|
|
13
|
+
# https://docs.unoverse.ai/nodes/overview
|
|
14
|
+
|
|
15
|
+
name: samples
|
|
16
|
+
displayName: Sample Nodes
|
|
17
|
+
description: Two worked examples for learning the node format, one PromiseNode and one CallbackNode
|
|
18
|
+
version: 0.1.0
|
|
19
|
+
category: productivity
|
|
20
|
+
|
|
21
|
+
features:
|
|
22
|
+
- Weather for any city, no API key needed
|
|
23
|
+
- Streaming chat completion, showing how credentials work
|
|
24
|
+
|
|
25
|
+
# THE ONLY HOSTS THIS PACKAGE'S NODES MAY CALL. Deny by default, https only.
|
|
26
|
+
#
|
|
27
|
+
# A manifest cannot execute code, but without this list it could still say "send this
|
|
28
|
+
# credential to evil.example", which is exfiltration with no code at all. One short
|
|
29
|
+
# reviewable list is what makes a node package safe to accept from someone else.
|
|
30
|
+
# The executor refuses any request to a host not named here.
|
|
31
|
+
allowedHosts:
|
|
32
|
+
- geocoding-api.open-meteo.com
|
|
33
|
+
- api.open-meteo.com
|
|
34
|
+
- api.openai.com
|
|
35
|
+
|
|
36
|
+
# EXECUTOR CAPABILITIES these manifests depend on. A deploy is refused if the target
|
|
37
|
+
# universe's executor cannot satisfy them, so a manifest can never name a capability
|
|
38
|
+
# that only exists on a newer executor.
|
|
39
|
+
requires:
|
|
40
|
+
credential: [bearer]
|
|
41
|
+
transport: [json, sse]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# NAMED HELPERS. Any expression in this package can call these as helpers.<name>.
|
|
2
|
+
#
|
|
3
|
+
# An expression past a few lines gets a name here instead. A helper sees ONLY its
|
|
4
|
+
# arguments and its sibling helpers, never config or credentials or the caller's
|
|
5
|
+
# scope: same sandbox, no extra authority.
|
|
6
|
+
# https://docs.unoverse.ai/nodes/manifest-nodes
|
|
7
|
+
|
|
8
|
+
helpers:
|
|
9
|
+
# WMO weather interpretation codes, as words. Open-Meteo answers with a numeric
|
|
10
|
+
# `weather_code`; a person (and a component) wants "light rain". This mapping is
|
|
11
|
+
# LOGIC, and logic lives in the node: a definition in design/ must never compute,
|
|
12
|
+
# so the node sends words and the component just shows them.
|
|
13
|
+
describe:
|
|
14
|
+
args: [code]
|
|
15
|
+
body: >-
|
|
16
|
+
return ({
|
|
17
|
+
0: "clear skies", 1: "mainly clear", 2: "partly cloudy", 3: "overcast",
|
|
18
|
+
45: "fog", 48: "freezing fog",
|
|
19
|
+
51: "light drizzle", 53: "drizzle", 55: "heavy drizzle",
|
|
20
|
+
56: "freezing drizzle", 57: "heavy freezing drizzle",
|
|
21
|
+
61: "light rain", 63: "rain", 65: "heavy rain",
|
|
22
|
+
66: "freezing rain", 67: "heavy freezing rain",
|
|
23
|
+
71: "light snow", 73: "snow", 75: "heavy snow", 77: "snow grains",
|
|
24
|
+
80: "light showers", 81: "showers", 82: "heavy showers",
|
|
25
|
+
85: "snow showers", 86: "heavy snow showers",
|
|
26
|
+
95: "a thunderstorm", 96: "a thunderstorm with hail", 99: "a thunderstorm with heavy hail"
|
|
27
|
+
})[code] || "changeable weather"
|
package/local/scaffold.mjs
CHANGED
|
@@ -84,6 +84,16 @@ export function createFirstProject(home, name) {
|
|
|
84
84
|
const root = createProject(home, name);
|
|
85
85
|
for (const d of ["prompts/skills", "prompts/blocks", "nodes"]) mkdirSync(join(home, d), { recursive: true });
|
|
86
86
|
|
|
87
|
+
// Two worked example nodes (sample-nodes/samples: a PromiseNode and a CallbackNode,
|
|
88
|
+
// heavily commented, linking docs.unoverse.ai/nodes), so `nodes/` is never an empty
|
|
89
|
+
// folder with no hint of what goes in it. Copied only when absent: these are the
|
|
90
|
+
// developer's files to edit or delete, and a later scaffold run must never put back
|
|
91
|
+
// what they threw away or overwrite what they changed.
|
|
92
|
+
const samples = join(dirname(fileURLToPath(import.meta.url)), "sample-nodes/samples");
|
|
93
|
+
if (existsSync(samples) && !existsSync(join(home, "nodes/samples"))) {
|
|
94
|
+
cpSync(samples, join(home, "nodes/samples"), { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
// The Claude Code authoring skills, so "open the folder and describe what you want"
|
|
88
98
|
// works in every Studio project from its first minute. Vendored into the published
|
|
89
99
|
// app (vendor/skills, written by prepack from the monorepo's .claude/skills, the one
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unoverse-platform/studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
|
|
6
6
|
"scripts": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@codemirror/view": "^6.37.1",
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
28
28
|
"@tailwindcss/vite": "^4.0.0",
|
|
29
|
-
"@unoverse-platform/base": "^0.3.
|
|
29
|
+
"@unoverse-platform/base": "^0.3.37",
|
|
30
30
|
"@unoverse-platform/marketplace": "*",
|
|
31
31
|
"@vitejs/plugin-react": "^4.3.4",
|
|
32
32
|
"markdown-to-jsx": "^7.7.17",
|
package/screens/AtomsView.tsx
CHANGED
|
@@ -127,17 +127,27 @@ export function AtomsView({ theme }: { theme: ResolvedTheme }) {
|
|
|
127
127
|
const uri = useMemo(() => (selected ? `unoverse://atoms/${slug(selected)}` : ""), [selected]);
|
|
128
128
|
|
|
129
129
|
return (
|
|
130
|
-
<div className="grid min-h-0 flex-1 grid-cols-[
|
|
130
|
+
<div className="grid min-h-0 flex-1 grid-cols-[240px_1fr] overflow-hidden">
|
|
131
131
|
{/* NAVIGATOR */}
|
|
132
132
|
<nav className="flex min-h-0 min-w-0 flex-col overflow-auto border-r border-gray-700 bg-[#1b2129] p-2">
|
|
133
133
|
{atoms.length === 0 && (
|
|
134
134
|
<div className="p-2 text-xs text-gray-400">No atoms — restart the Unoverse server so /dev/atoms is available.</div>
|
|
135
135
|
)}
|
|
136
|
+
{/* Atoms are ALL base tier (the one shared home): say so once at the top,
|
|
137
|
+
matching the Components sidebar's Base divider. */}
|
|
138
|
+
{atoms.length > 0 && (
|
|
139
|
+
<div className="mb-1 px-2 pt-1 text-[10px] font-semibold uppercase tracking-wider text-gray-300">
|
|
140
|
+
Base <span className="font-normal normal-case tracking-normal text-gray-500">· installed, read-only</span>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
136
143
|
{groups.map((g) => (
|
|
137
144
|
<div key={g.label} className="mb-2">
|
|
138
|
-
<div className="
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
<div className="mb-1 mt-1 flex items-center gap-2 px-2">
|
|
146
|
+
<span className="text-[10px] font-medium uppercase tracking-[0.14em] text-gray-400">
|
|
147
|
+
{g.label}
|
|
148
|
+
<span className="ml-1.5 font-normal lowercase tracking-normal text-gray-500">{g.atoms.length}</span>
|
|
149
|
+
</span>
|
|
150
|
+
<span className="h-px flex-1 bg-gradient-to-r from-gray-500/40 to-transparent" />
|
|
141
151
|
</div>
|
|
142
152
|
{g.atoms.map((a) => (
|
|
143
153
|
<button
|
|
@@ -368,15 +368,21 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
368
368
|
store.apply({ type: "COMPONENT_DATA", chatId: CHAT_ID, nodeId: selected, data: { [key]: val } });
|
|
369
369
|
};
|
|
370
370
|
|
|
371
|
+
// ONE catalog, grouped by CATEGORY — a project's card variants belong beside the
|
|
372
|
+
// base cards, where someone looking for "a card" will look. OWNERSHIP is a per-item
|
|
373
|
+
// mark, not a section: the org tag says "yours, editable"; untagged means base
|
|
374
|
+
// (installed, read-only — the Code view's badge carries the detail). Within a
|
|
375
|
+
// category the developer's own components list first.
|
|
371
376
|
const byCategory = Object.entries(
|
|
372
377
|
components.reduce<Record<string, ComponentDef[]>>((acc, c) => {
|
|
373
378
|
(acc[c.category ?? "Uncategorized"] ??= []).push(c);
|
|
374
379
|
return acc;
|
|
375
380
|
}, {}),
|
|
376
381
|
).sort(([a], [b]) => a.localeCompare(b));
|
|
382
|
+
for (const [, items] of byCategory) items.sort((a, b) => (a.org ? 0 : 1) - (b.org ? 0 : 1));
|
|
377
383
|
|
|
378
384
|
return (
|
|
379
|
-
<div className="grid min-h-0 flex-1 grid-cols-[
|
|
385
|
+
<div className="grid min-h-0 flex-1 grid-cols-[240px_1fr] overflow-hidden">
|
|
380
386
|
{/* NAVIGATOR */}
|
|
381
387
|
<nav className="flex min-h-0 min-w-0 flex-col overflow-auto border-r border-gray-700 bg-[#2c2c2e] p-2">
|
|
382
388
|
{components.length === 0 && (
|
|
@@ -384,7 +390,12 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
384
390
|
)}
|
|
385
391
|
{byCategory.map(([cat, items]) => (
|
|
386
392
|
<div key={cat} className="mb-3">
|
|
387
|
-
|
|
393
|
+
{/* A hairline that fades right lifts the group header without adding weight —
|
|
394
|
+
wider tracking + lighter weight reads as a label, not another item. */}
|
|
395
|
+
<div className="mb-1 mt-1 flex items-center gap-2 px-2">
|
|
396
|
+
<span className="text-[10px] font-medium uppercase tracking-[0.14em] text-gray-400">{cat}</span>
|
|
397
|
+
<span className="h-px flex-1 bg-gradient-to-r from-gray-500/40 to-transparent" />
|
|
398
|
+
</div>
|
|
388
399
|
{items.map((c) => (
|
|
389
400
|
<div key={refOf(c)}>
|
|
390
401
|
<button
|
|
@@ -394,7 +405,9 @@ export function ComponentsView({ theme, org = "all" }: { theme: ResolvedTheme; o
|
|
|
394
405
|
}`}
|
|
395
406
|
>
|
|
396
407
|
{c.name}
|
|
397
|
-
{
|
|
408
|
+
{/* Ownership mark: tagged = the developer's own (editable); untagged =
|
|
409
|
+
base, installed and read-only. Emerald so "mine" reads at a glance. */}
|
|
410
|
+
{c.org ? <span className="ml-1.5 text-[10px] text-emerald-400/80">{c.org}</span> : null}
|
|
398
411
|
</button>
|
|
399
412
|
{/* The component's OWN states (its states/ folder) live HERE in the rail.
|
|
400
413
|
The two MASTER states (inline/focused — the `defaultState` face switch,
|
|
@@ -279,7 +279,7 @@ export function DesignSystemView({
|
|
|
279
279
|
<div className="flex h-full" style={{ background: theme.color["surface.sunken"] ?? "#f9fafb", color: ink }}>
|
|
280
280
|
{/* LEFT NAV — jump between token groups; badge = how many tokens THIS project overrides */}
|
|
281
281
|
<nav className="w-56 shrink-0 overflow-auto border-r p-5" style={{ borderColor: borderC, background: surface, fontFamily: bodyFont }}>
|
|
282
|
-
<h1 className="mb-1 text-lg font-bold">
|
|
282
|
+
<h1 className="mb-1 text-lg font-bold">Styles</h1>
|
|
283
283
|
<p className="mb-5 text-[11px] leading-snug" style={{ color: sub }}>
|
|
284
284
|
{foundation ? (
|
|
285
285
|
<>Viewing <b style={{ color: ink }}>{org}</b></>
|
package/src/App.tsx
CHANGED
|
@@ -33,7 +33,7 @@ const NAV: { key: View; label: string; group: string }[] = [
|
|
|
33
33
|
{ key: "apps", label: "Apps", group: "Build" },
|
|
34
34
|
{ key: "components", label: "Components", group: "Build" },
|
|
35
35
|
{ key: "atoms", label: "Atoms", group: "Build" },
|
|
36
|
-
{ key: "tokens", label: "
|
|
36
|
+
{ key: "tokens", label: "Styles", group: "Build" },
|
|
37
37
|
{ key: "skills", label: "Skills", group: "AI" },
|
|
38
38
|
{ key: "prompts", label: "Prompt Blocks", group: "AI" },
|
|
39
39
|
{ key: "nodes", label: "Nodes", group: "Code" },
|
|
@@ -23,13 +23,15 @@ that is the workspace folder itself; in the platform monorepo it is `apps/unover
|
|
|
23
23
|
"org"). If the workspace has more than one, or the only one looks like a placeholder,
|
|
24
24
|
ask which project the artifact belongs to. Never invent a new project folder to hold
|
|
25
25
|
one artifact.
|
|
26
|
-
2. **The design system is
|
|
27
|
-
developer's own projects; the
|
|
28
|
-
the token foundation
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
2. **The base design system is public. Read it before you write.** A developer
|
|
27
|
+
workspace holds only the developer's own projects; the base components, atoms, and
|
|
28
|
+
the token foundation the platform installs are all published at
|
|
29
|
+
[marketplace/definitions](https://github.com/unoverse-platform/marketplace/tree/main/definitions)
|
|
30
|
+
([components](https://github.com/unoverse-platform/marketplace/tree/main/definitions/components), [atoms](https://github.com/unoverse-platform/marketplace/tree/main/definitions/atoms), [styles](https://github.com/unoverse-platform/marketplace/tree/main/definitions/styles)).
|
|
31
|
+
Study the closest exemplar there, then compose those atoms and reference those
|
|
32
|
+
components; never hand-roll a shape the base already ships. Everything is plain
|
|
33
|
+
YAML files — no connection, no running platform needed. (In the platform monorepo
|
|
34
|
+
the same material is on disk at `design/marketplace/`.)
|
|
33
35
|
|
|
34
36
|
## Step 1: Identify the artifact, read its playbook
|
|
35
37
|
|
|
@@ -87,9 +87,12 @@ Its theme is then served at `unoverse://theme/<neworg>/light`, and a template po
|
|
|
87
87
|
|
|
88
88
|
## 4. Anatomy of an org pack (`design/<org>/styles/`)
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
The foundation's token contract is public — read
|
|
91
|
+
[marketplace/definitions/styles](https://github.com/unoverse-platform/marketplace/tree/main/definitions/styles) (`base/`, `semantic/`, `themes/`); its
|
|
92
|
+
`themes/light.yaml` is the file §3 tells you to copy as your starting theme. (A worked
|
|
93
|
+
client pack lives in the platform monorepo at `design/bpp/styles/`.) Every file in a
|
|
94
|
+
healthy pack carries a `$description` explaining its own job, and those descriptions are
|
|
95
|
+
the spec.
|
|
93
96
|
|
|
94
97
|
| File | Holds | Rule |
|
|
95
98
|
|---|---|---|
|
|
@@ -6,10 +6,11 @@ model). Deep law: [UNOVERSE_AUTHORING](https://docs.unoverse.ai/unoverse/UNOVERS
|
|
|
6
6
|
[UNOVERSE_STATE_MODEL](https://docs.unoverse.ai/unoverse/UNOVERSE_STATE_MODEL.md) §5 (the six rules),
|
|
7
7
|
[UNOVERSE_LAYERS](https://docs.unoverse.ai/unoverse/UNOVERSE_LAYERS.md) (states own layouts).
|
|
8
8
|
**Study an exemplar before writing.** The canonical State Model v2 state tree is inlined
|
|
9
|
-
below
|
|
10
|
-
(
|
|
11
|
-
|
|
12
|
-
is on disk:
|
|
9
|
+
below. For full worked components, read the published base set at
|
|
10
|
+
[marketplace/definitions/components](https://github.com/unoverse-platform/marketplace/tree/main/definitions/components) — flat single-drawing components
|
|
11
|
+
throughout, and [document](https://github.com/unoverse-platform/marketplace/tree/main/definitions/components/document) for the manifest + local-partials
|
|
12
|
+
folder shape. (In the platform monorepo the richest multi-state exemplar is on disk:
|
|
13
|
+
`design/sab/components/product-card`.)
|
|
13
14
|
|
|
14
15
|
## The anatomy
|
|
15
16
|
|
|
@@ -141,8 +142,8 @@ The rules, in the order they bite:
|
|
|
141
142
|
of bound fields). Same shape, different values = ONE state, data-driven: seven wizard
|
|
142
143
|
questions sharing one shape are one `question` state whose data changes, never seven
|
|
143
144
|
files (LAYERS §4, §7).
|
|
144
|
-
5. **Atoms** (the
|
|
145
|
-
|
|
145
|
+
5. **Atoms** (the base design system's shared shapes, used via `Ref`; browse them at
|
|
146
|
+
[marketplace/definitions/atoms](https://github.com/unoverse-platform/marketplace/tree/main/definitions/atoms), or on disk at `design/marketplace/atoms/` in the
|
|
146
147
|
platform monorepo) are for shapes shared across components, authoring-time
|
|
147
148
|
only (the server expands them before serving; no Studio view). `Ref props` remaps FIELDS;
|
|
148
149
|
**`Ref with` passes LITERALS**: `{ "type": "Ref", "ref": "button", "with": { "label":
|
|
@@ -270,7 +271,7 @@ component. Its key nouns/verbs must appear in `whenToUse`'s first sentence.
|
|
|
270
271
|
|
|
271
272
|
## Workflow
|
|
272
273
|
|
|
273
|
-
1. Study the closest
|
|
274
|
+
1. Study the closest published exemplar ([marketplace/definitions/components](https://github.com/unoverse-platform/marketplace/tree/main/definitions/components);
|
|
274
275
|
monorepo: `design/sab/components/product-card`); copy its folder shape.
|
|
275
276
|
2. Declare the state tree first: public states on top, steps nested, each state owning
|
|
276
277
|
its layout (same-name default, `layout:` only when the filename differs).
|
|
@@ -89,27 +89,12 @@ package only for a new integration.
|
|
|
89
89
|
3. **Write `interface.yaml`.** Name outputs for what they carry, because those names become
|
|
90
90
|
what someone types in a template.
|
|
91
91
|
4. **Write `config.yaml`.** Every value the workflow supplies needs `ui:field: template`. A
|
|
92
|
-
`description` says what the setting does, not what it is called.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
title: Require sign-in
|
|
99
|
-
default: false
|
|
100
|
-
"ui:widget": toggle
|
|
101
|
-
authRole:
|
|
102
|
-
type: string
|
|
103
|
-
title: Require role
|
|
104
|
-
default: ""
|
|
105
|
-
"ui:dependencies": { authRequired: true }
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
These are the workflow BUILDER's control, per box on a canvas, separate from
|
|
109
|
-
`node.yaml`'s `auth`, which is your floor. The executor takes the stricter of the two. A
|
|
110
|
-
role usually belongs here rather than in the manifest: `finance:approve` is a claim one
|
|
111
|
-
deployment's identity provider mints, and a published node cannot know it. Add both to
|
|
112
|
-
`ui:order`.
|
|
92
|
+
`description` says what the setting does, not what it is called. NEVER declare
|
|
93
|
+
`authRequired` or `authRole`: they are reserved names, injected by the platform into
|
|
94
|
+
every runnable node's form as the workflow builder's per-box access controls, and lint
|
|
95
|
+
refuses a manifest carrying its own copy. Your floor stays in `node.yaml`'s `auth`; the
|
|
96
|
+
executor takes the stricter of the two. A node with no settings at all needs no
|
|
97
|
+
config.yaml.
|
|
113
98
|
5. **Write `api/run.yaml`.** A list, each entry named. Include `error` wherever the service
|
|
114
99
|
can answer 200 with a failure body. Where one call is really many, name the capability on
|
|
115
100
|
the call rather than trying to express a loop: `paginate`, `chunk`, `poll` or `state`.
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
[UNOVERSE_STATE_MODEL](https://docs.unoverse.ai/unoverse/UNOVERSE_STATE_MODEL.md) §5
|
|
7
7
|
(the six rules), [UNOVERSE_LAYERS](https://docs.unoverse.ai/unoverse/UNOVERSE_LAYERS.md).
|
|
8
8
|
Everything in the component playbook (closed vocabulary, tokens, scale steps) applies
|
|
9
|
-
here too. **Study an exemplar before writing**:
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
here too. **Study an exemplar before writing**: templates are always a project's own
|
|
10
|
+
(none ship in the base set), so your references are the canonical manifest tree inlined
|
|
11
|
+
below, [design/templates](https://docs.unoverse.ai/design/templates.md), and any template
|
|
12
|
+
already in the workspace. (Monorepo: `design/sab/templates/sab-chat-layout`.)
|
|
12
13
|
|
|
13
14
|
## THE model: the component drives; the template reacts by name
|
|
14
15
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Playbook: Build a workflow (via the builder MCP)
|
|
2
2
|
|
|
3
|
+
**A DIFFERENT JOURNEY.** Everything else in this skill is file authoring in the
|
|
4
|
+
developer's own workspace: no connection, no running platform, no pairing code.
|
|
5
|
+
Workflows alone are built on a RUNNING universe's Canvas through the builder MCP, and
|
|
6
|
+
only here does a pairing code exist. Do not carry this playbook's ceremony into any
|
|
7
|
+
other artifact.
|
|
8
|
+
|
|
3
9
|
Workflows are NOT files in the carve-out. They live in the platform and are built
|
|
4
10
|
through the **builder MCP** (`canvas`, auto-registered by this repo's
|
|
5
11
|
`.mcp.json`). You drive the platform's own authoring tools; the canvas updates live
|
|
@@ -32,9 +32,8 @@ It prints every part the agent can fill, its description, and its limits. Read t
|
|
|
32
32
|
output, not the YAML. Then run the design lint
|
|
33
33
|
(`node -e "import('./packages/base/src/lint/design/index.mjs').then(m=>{const r=m.lintDefinitions();console.log(r.problems.length);r.problems.forEach(p=>console.log(p.level,p.file,p.msg))})"`).
|
|
34
34
|
|
|
35
|
-
In a **Studio developer workspace**, the same linter runs as Studio's publish lint
|
|
36
|
-
the
|
|
37
|
-
name. Read what it says rather than silencing it, and always do the final proof in
|
|
35
|
+
In a **Studio developer workspace**, the same linter runs as Studio's publish lint; it
|
|
36
|
+
catches two of the five traps below by name. Read what it says rather than silencing it, and always do the final proof in
|
|
38
37
|
Step 6: one real fill.
|
|
39
38
|
|
|
40
39
|
---
|