@unoverse-platform/studio 0.1.52 → 0.1.54
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 +49 -0
- package/local/sample-prompts/blocks/formatting/sample-concise-answers.md +22 -0
- package/local/sample-prompts/skills/sample-complaint-handling/SKILL.md +52 -0
- package/local/sample-styles/README.md +23 -0
- package/local/sample-styles/base/color.yaml +18 -0
- package/local/scaffold.mjs +21 -0
- package/package.json +1 -1
- package/src/NodesView.tsx +6 -1
package/bin/studio.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { dirname, resolve, join, basename } from "path";
|
|
|
21
21
|
import { fileURLToPath } from "url";
|
|
22
22
|
import { existsSync, readdirSync } from "fs";
|
|
23
23
|
import { createInterface } from "readline/promises";
|
|
24
|
+
import { execSync } from "child_process";
|
|
24
25
|
import { designHome, hasDesignHome } from "../local/design-home.mjs";
|
|
25
26
|
import { createFirstProject, checkProjectName } from "../local/scaffold.mjs";
|
|
26
27
|
|
|
@@ -163,6 +164,54 @@ const projectRoot = findProjectRootFromCwd();
|
|
|
163
164
|
process.env.UNOVERSE_HOME = projectRoot;
|
|
164
165
|
console.log(`\n Studio reading: ${designHome(projectRoot)}\n`);
|
|
165
166
|
|
|
167
|
+
/**
|
|
168
|
+
* LAUNCHING STUDIO IS THE DECISION; A STALE STUDIO YIELDS THE PORT.
|
|
169
|
+
*
|
|
170
|
+
* Studio is always 4108 with strictPort, so the address in your browser's history is
|
|
171
|
+
* always the right one. The price of one fixed port used to be a raw vite "Port 4108
|
|
172
|
+
* is already in use" whenever a previous Studio was still running — the tab from the
|
|
173
|
+
* last session, forgotten, holding the port for a project you have moved on from.
|
|
174
|
+
* Typing `unoverse studio` again IS the answer to "which one do you want": the old
|
|
175
|
+
* instance is stopped and this one takes the port.
|
|
176
|
+
*
|
|
177
|
+
* ONLY a recognisable Studio is stopped. Anything else on 4108 is somebody's own
|
|
178
|
+
* process, and killing it on sight would be worse than the error, so that case names
|
|
179
|
+
* the process and stops.
|
|
180
|
+
*/
|
|
181
|
+
function freePortOfStaleStudio(port) {
|
|
182
|
+
let pids = [];
|
|
183
|
+
try {
|
|
184
|
+
pids = execSync(`lsof -nP -ti tcp:${port} -s tcp:LISTEN`, { stdio: ["ignore", "pipe", "ignore"] })
|
|
185
|
+
.toString().trim().split("\n").filter(Boolean);
|
|
186
|
+
} catch {
|
|
187
|
+
return; // nothing listening, or no lsof: let vite find out
|
|
188
|
+
}
|
|
189
|
+
for (const pid of pids) {
|
|
190
|
+
if (pid === String(process.pid)) continue;
|
|
191
|
+
let command = "";
|
|
192
|
+
try {
|
|
193
|
+
command = execSync(`ps -p ${pid} -o command=`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
194
|
+
} catch { continue; } // already gone
|
|
195
|
+
if (/unoverse-studio|@unoverse-platform\/studio|packages\/studio/.test(command)) {
|
|
196
|
+
console.log(` A previous Studio was still holding :${port}. Stopping it and taking over.`);
|
|
197
|
+
try { process.kill(Number(pid), "SIGTERM"); } catch { /* already gone */ }
|
|
198
|
+
// SIGTERM first so it closes cleanly; escalate only if it lingers.
|
|
199
|
+
const deadline = Date.now() + 3000;
|
|
200
|
+
for (;;) {
|
|
201
|
+
try { process.kill(Number(pid), 0); } catch { break; } // no longer exists
|
|
202
|
+
if (Date.now() > deadline) { try { process.kill(Number(pid), "SIGKILL"); } catch { /* gone */ } break; }
|
|
203
|
+
execSync("sleep 0.2");
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
console.error(`\n Port ${port} is held by something that is not Studio:\n ${command}\n`);
|
|
207
|
+
console.error(` Stop it (kill ${pid}) or find out what it is (lsof -nP -i :${port}), then run unoverse studio again.\n`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
freePortOfStaleStudio(4108);
|
|
214
|
+
|
|
166
215
|
const server = await createServer({
|
|
167
216
|
// The APP is the vite root; the developer's project is found by walking up from the
|
|
168
217
|
// working directory (findProjectRoot), so Studio works from anywhere inside it.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Concise Answers
|
|
3
|
+
description: Keeps replies short, direct, and free of filler — the answer first, detail only on request
|
|
4
|
+
tags: [formatting, tone]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- A WORKED EXAMPLE prompt block, shipped with Studio so prompts/blocks/ is never an
|
|
8
|
+
empty folder. Edit it, copy it, or delete it — it is yours. A block is a reusable
|
|
9
|
+
prompt fragment workflows compose into an agent's system prompt: smaller than a
|
|
10
|
+
skill, no routing of its own, and it must stand alone (no references to other
|
|
11
|
+
blocks). The folder above it is its category. -->
|
|
12
|
+
|
|
13
|
+
Answer the question first, in one or two sentences, before any background.
|
|
14
|
+
|
|
15
|
+
Keep every reply as short as it can be while still being complete. Cut greetings,
|
|
16
|
+
restatements of the question, and closing filler ("I hope this helps").
|
|
17
|
+
|
|
18
|
+
Use plain words over formal ones. Prefer an example over an explanation when one
|
|
19
|
+
example makes the point.
|
|
20
|
+
|
|
21
|
+
If the full answer would be long, give the short version and offer the detail:
|
|
22
|
+
"Want the full breakdown?"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sample-complaint-handling
|
|
3
|
+
title: Complaint Handling
|
|
4
|
+
description: How the assistant responds when a customer is unhappy, from first reply to escalation
|
|
5
|
+
whenToUse: Handle an unhappy customer — a complaint, frustration with the product or service, a request for a refund, or a demand to speak to a person. Not for neutral questions or how-to requests.
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
category: ai
|
|
8
|
+
triggers:
|
|
9
|
+
- complaint
|
|
10
|
+
- refund
|
|
11
|
+
- unhappy
|
|
12
|
+
- speak to a human
|
|
13
|
+
- escalate
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Complaint Handling
|
|
17
|
+
|
|
18
|
+
<!-- A WORKED EXAMPLE, shipped with Studio so prompts/skills/ is never an empty folder.
|
|
19
|
+
Edit it, copy it, or delete it — it is yours. An agent skill is prose the
|
|
20
|
+
platform's AI agents discover at runtime and follow: the frontmatter above is the
|
|
21
|
+
routing (whenToUse is ranked against what the user actually says), the body below
|
|
22
|
+
is the behavior. Format + rules: https://docs.unoverse.ai/nodes/node-discoverability.md -->
|
|
23
|
+
|
|
24
|
+
## When to Use This Skill
|
|
25
|
+
|
|
26
|
+
The customer is expressing dissatisfaction: something broke, something was late, they
|
|
27
|
+
were charged wrongly, or they are simply frustrated. The goal of every reply is that
|
|
28
|
+
the customer feels heard first and helped second.
|
|
29
|
+
|
|
30
|
+
## Approach
|
|
31
|
+
|
|
32
|
+
1. **Acknowledge before anything else.** One sentence that names their problem in
|
|
33
|
+
their words. Never open with policy.
|
|
34
|
+
2. **Ask for the one detail you need**, if any, and only one at a time.
|
|
35
|
+
3. **Offer the concrete next step you can actually do.** Do not promise outcomes you
|
|
36
|
+
cannot see through.
|
|
37
|
+
4. **Escalate when asked, and say so plainly.** If the customer asks for a person
|
|
38
|
+
twice, or mentions legal action, stop resolving and hand off: "I'm passing this to
|
|
39
|
+
our team; you'll hear from a person."
|
|
40
|
+
|
|
41
|
+
## Do / Don't
|
|
42
|
+
|
|
43
|
+
- Do keep replies to three sentences or fewer while the customer is upset.
|
|
44
|
+
- Do use their name if you have it.
|
|
45
|
+
- Don't defend the company or explain why the problem happened, unless they ask.
|
|
46
|
+
- Don't offer discounts or refunds beyond what the workflow's tools allow.
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
Customer: "This is the second time my order arrived broken. Ridiculous."
|
|
51
|
+
Reply: "Twice in a row is genuinely not okay — I'm sorry. I can send a replacement
|
|
52
|
+
today or refund the order in full: which would you prefer?"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Your project's styles
|
|
2
|
+
|
|
3
|
+
This folder restyles the whole design system for your brand — without editing a
|
|
4
|
+
single component. Three rules:
|
|
5
|
+
|
|
6
|
+
1. **Token NAMES are the contract, VALUES are yours.** Components reference names
|
|
7
|
+
like `text.primary` and `action.primary`; you change what those names mean.
|
|
8
|
+
2. **Name a token to override it; omit it to inherit.** The cascade is per token,
|
|
9
|
+
never per file. Never copy a whole file to change one line — the untouched lines
|
|
10
|
+
would stop tracking platform improvements.
|
|
11
|
+
3. **Ingredients in `base/`, meaning in `themes/`.** `base/color.yaml` names your
|
|
12
|
+
brand's raw colors (`color.brand.ink`); a theme file assigns them to roles
|
|
13
|
+
(`text.primary: {color.brand.ink}`).
|
|
14
|
+
|
|
15
|
+
Start here:
|
|
16
|
+
|
|
17
|
+
- `base/color.yaml` — name your brand ingredients (a worked example is inside).
|
|
18
|
+
- Want your own theme? Copy the foundation's theme file whole — the ONE place
|
|
19
|
+
copying is correct, because a theme must assign every role:
|
|
20
|
+
https://github.com/unoverse-platform/marketplace/tree/main/definitions/styles/themes
|
|
21
|
+
→ save as `themes/light.yaml` here, then point roles at your `color.brand.*`.
|
|
22
|
+
|
|
23
|
+
Full guide: https://docs.unoverse.ai/design/styles-and-tokens.md
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# BRAND INGREDIENTS — raw colors named for what they ARE (ink, paper, a signature
|
|
2
|
+
# hue), never for where they are used ("button-blue" is a decision, not an ingredient;
|
|
3
|
+
# a theme file makes decisions). These are inert until a theme assigns them to roles,
|
|
4
|
+
# so this file is safe to grow before you have a theme at all.
|
|
5
|
+
#
|
|
6
|
+
# Delete the examples and name your own. Guide: see README.md in this folder.
|
|
7
|
+
color:
|
|
8
|
+
$type: color
|
|
9
|
+
brand:
|
|
10
|
+
ink:
|
|
11
|
+
$value: "#1a1a2e"
|
|
12
|
+
$description: "Near-black with a hint of the brand hue — body text and headings"
|
|
13
|
+
paper:
|
|
14
|
+
$value: "#fbfaf8"
|
|
15
|
+
$description: "Warm off-white — page and card backgrounds"
|
|
16
|
+
signature:
|
|
17
|
+
$value: "#4f46e5"
|
|
18
|
+
$description: "The brand's signature hue — actions and accents"
|
package/local/scaffold.mjs
CHANGED
|
@@ -71,6 +71,16 @@ export function createProject(home, name) {
|
|
|
71
71
|
|
|
72
72
|
for (const d of ["components/welcome", "templates", "styles"]) mkdirSync(join(root, d), { recursive: true });
|
|
73
73
|
writeFileSync(join(root, "components/welcome/welcome.yaml"), WELCOME);
|
|
74
|
+
|
|
75
|
+
// Worked example styles (sample-styles: brand ingredients + the override rules), so
|
|
76
|
+
// styles/ is never an empty folder with no hint of the token cascade. Per PROJECT,
|
|
77
|
+
// because every project is a publishable org with its own pack. Copied only when the
|
|
78
|
+
// folder is empty: these are the developer's files, and a later run must never put
|
|
79
|
+
// back what they threw away.
|
|
80
|
+
const sampleStyles = join(dirname(fileURLToPath(import.meta.url)), "sample-styles");
|
|
81
|
+
if (existsSync(sampleStyles) && readdirSync(join(root, "styles")).length === 0) {
|
|
82
|
+
cpSync(sampleStyles, join(root, "styles"), { recursive: true });
|
|
83
|
+
}
|
|
74
84
|
return root;
|
|
75
85
|
}
|
|
76
86
|
|
|
@@ -94,6 +104,17 @@ export function createFirstProject(home, name) {
|
|
|
94
104
|
cpSync(samples, join(home, "nodes/samples"), { recursive: true });
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
// A worked example agent skill and prompt block, same reason and same rule as the
|
|
108
|
+
// sample nodes: prompts/ must never be two empty folders, and the samples are the
|
|
109
|
+
// developer's to edit or delete — copied only when absent, never restored.
|
|
110
|
+
const samplePrompts = join(dirname(fileURLToPath(import.meta.url)), "sample-prompts");
|
|
111
|
+
if (existsSync(join(samplePrompts, "skills")) && !existsSync(join(home, "prompts/skills/sample-complaint-handling"))) {
|
|
112
|
+
cpSync(join(samplePrompts, "skills"), join(home, "prompts/skills"), { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
if (existsSync(join(samplePrompts, "blocks")) && !existsSync(join(home, "prompts/blocks/formatting/sample-concise-answers.md"))) {
|
|
115
|
+
cpSync(join(samplePrompts, "blocks"), join(home, "prompts/blocks"), { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
// The Claude Code authoring skills, so "open the folder and describe what you want"
|
|
98
119
|
// works in every Studio project from its first minute. Vendored into the published
|
|
99
120
|
// app (vendor/skills, written by prepack from the monorepo's .claude/skills, the one
|
package/package.json
CHANGED
package/src/NodesView.tsx
CHANGED
|
@@ -383,7 +383,12 @@ export function NodesView() {
|
|
|
383
383
|
</div>
|
|
384
384
|
<div className="ml-auto flex shrink-0 items-center gap-2">
|
|
385
385
|
{current.testData && (
|
|
386
|
-
<button
|
|
386
|
+
<button
|
|
387
|
+
onClick={loadSample}
|
|
388
|
+
disabled={!current?.testData}
|
|
389
|
+
title={current?.testData ? "Fill the form from the node's test fixture (test.yaml)" : "This node declares no test fixture"}
|
|
390
|
+
className="rounded border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40"
|
|
391
|
+
>
|
|
387
392
|
Load sample
|
|
388
393
|
</button>
|
|
389
394
|
)}
|