makersclaw 0.1.0
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/README.md +68 -0
- package/bin/makersclaw.mjs +10 -0
- package/package.json +34 -0
- package/registry/blocks/board/README.md +130 -0
- package/registry/blocks/board/block.json +10 -0
- package/registry/blocks/board/board.jsx +435 -0
- package/registry/blocks/board/demo.jsx +50 -0
- package/registry/blocks/board/index.js +4 -0
- package/registry/blocks/job-panel/README.md +68 -0
- package/registry/blocks/job-panel/block.json +10 -0
- package/registry/blocks/job-panel/demo.jsx +75 -0
- package/registry/blocks/job-panel/index.js +3 -0
- package/registry/blocks/job-panel/job-panel.jsx +110 -0
- package/src/cli.mjs +301 -0
- package/src/fsutil.mjs +76 -0
- package/src/install.mjs +457 -0
- package/src/registry.mjs +191 -0
- package/templates/next/files/README.md +626 -0
- package/templates/next/files/app/auto-refresh.jsx +26 -0
- package/templates/next/files/app/error-relay.jsx +53 -0
- package/templates/next/files/app/error.jsx +44 -0
- package/templates/next/files/app/global-error.jsx +36 -0
- package/templates/next/files/app/globals.css +178 -0
- package/templates/next/files/app/layout.jsx +51 -0
- package/templates/next/files/app/not-found.jsx +23 -0
- package/templates/next/files/app/page.jsx +22 -0
- package/templates/next/files/app/s/[key]/page.jsx +47 -0
- package/templates/next/files/app/shell.jsx +64 -0
- package/templates/next/files/check/run.mjs +683 -0
- package/templates/next/files/components/parts/fixture.jsx +60 -0
- package/templates/next/files/components/parts/index.js +5 -0
- package/templates/next/files/components/parts/record-switcher.jsx +49 -0
- package/templates/next/files/components/ui/button.jsx +81 -0
- package/templates/next/files/components/ui/cx.js +4 -0
- package/templates/next/files/components/ui/depiction.jsx +66 -0
- package/templates/next/files/components/ui/field.jsx +126 -0
- package/templates/next/files/components/ui/icon.jsx +87 -0
- package/templates/next/files/components/ui/index.js +12 -0
- package/templates/next/files/components/ui/menu.jsx +123 -0
- package/templates/next/files/components/ui/notice.jsx +86 -0
- package/templates/next/files/components/ui/sheet.jsx +89 -0
- package/templates/next/files/components/ui/tabs.jsx +81 -0
- package/templates/next/files/lib/actions.js +187 -0
- package/templates/next/files/lib/claw.js +219 -0
- package/templates/next/files/lib/data.js +171 -0
- package/templates/next/files/lib/db.js +72 -0
- package/templates/next/files/lib/hooks.js +218 -0
- package/templates/next/files/lib/platform.js +59 -0
- package/templates/next/files/lib/sample.js +12 -0
- package/templates/next/files/middleware.js +42 -0
- package/templates/next/files/next.config.mjs +41 -0
- package/templates/next/files/package.json +19 -0
- package/templates/next/files/postcss.config.mjs +3 -0
- package/templates/next/files/schema.sql +22 -0
- package/templates/next/files/surfaces/README.md +32 -0
- package/templates/next/files/surfaces.js +53 -0
- package/templates/next/template.json +118 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
# artifact template
|
|
2
|
+
|
|
3
|
+
The app every artifact starts from: Next.js 15 (app router, JSX, server
|
|
4
|
+
actions), Postgres via `DATABASE_URL`, Tailwind v4, and a kit of parts
|
|
5
|
+
(`components/ui`, `components/parts`). It carries the behaviour every
|
|
6
|
+
app shares, the parts every app is assembled from, and the workspace's
|
|
7
|
+
colours. You design the surfaces: what is on them, how they are laid
|
|
8
|
+
out, how dense, how they read — never what colour anything is.
|
|
9
|
+
|
|
10
|
+
## 1. What you are inside
|
|
11
|
+
|
|
12
|
+
The app renders in an iframe inside an OS window on the founder's desk.
|
|
13
|
+
The window's title bar already carries the app's name, **Edit**,
|
|
14
|
+
**Pause / Resume**, **Run now** and the status. Under it, the window's
|
|
15
|
+
tab strip carries **App**, **Agent**, **Runs**, **Activity** and
|
|
16
|
+
**Settings**: the OS draws the last four (the chat with the app's
|
|
17
|
+
agent, each run and its report, every activity line, the app's settings).
|
|
18
|
+
You design the **App tab**: the shell and the domain surfaces. Never
|
|
19
|
+
repeat the name, the run controls, or the other tabs.
|
|
20
|
+
|
|
21
|
+
**The colours are the workspace's, not yours.** The founder picks a
|
|
22
|
+
theme once (System Settings → Appearance) and the desk and every app in
|
|
23
|
+
the workspace are painted in it. `app/layout.jsx` links
|
|
24
|
+
`_claw/theme.css`, which sets the palette's variables; `app/globals.css`
|
|
25
|
+
turns them into the tokens you use: `bg-background`, `bg-card`,
|
|
26
|
+
`text-foreground`, `text-muted-foreground`, `text-primary`,
|
|
27
|
+
`bg-primary text-primary-foreground`, `border-border`, `bg-accent`,
|
|
28
|
+
`text-destructive`, `text-success`, `text-warning`, `text-info`,
|
|
29
|
+
`ring-ring`, `rounded-sm|md|lg|xl`, `font-sans|mono|display`. Never a
|
|
30
|
+
hex, `rgb()`, `hsl()`, a raw Tailwind hue (`text-neutral-500`,
|
|
31
|
+
`bg-white`) or a `<style jsx>` block: `npm run check` refuses each, and
|
|
32
|
+
an app that carries its own colour stops matching the workspace the day
|
|
33
|
+
the founder changes theme. Light or dark is a property of the preset;
|
|
34
|
+
the tokens flip with it, so you write no `dark:` variants either.
|
|
35
|
+
|
|
36
|
+
One exception, with rules of its own (§3c): a **depiction** — a picture
|
|
37
|
+
of something that exists elsewhere, a tweet as X will show it, a
|
|
38
|
+
LinkedIn post in the feed — wears THAT place's colours, face and widths,
|
|
39
|
+
in `components/depictions/`, framed on a surface by the kit's
|
|
40
|
+
`<Depiction>`. Colour that is a row's value (`style={{ background:
|
|
41
|
+
row.hex }}`) is neither chrome nor depiction: it is data, and the check
|
|
42
|
+
never sees it.
|
|
43
|
+
|
|
44
|
+
## 2. Files
|
|
45
|
+
|
|
46
|
+
Paths here are relative to this folder — the artifact's `app/`.
|
|
47
|
+
`artifact_files` paths are relative to the artifact folder, so prefix
|
|
48
|
+
`app/`: this file's `app/shell.jsx` is written as `app/app/shell.jsx`,
|
|
49
|
+
`surfaces/<key>.jsx` as `app/surfaces/<key>.jsx`.
|
|
50
|
+
|
|
51
|
+
Yours — write and rewrite freely:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
app/shell.jsx the inside of the window: nav, layout, type, density
|
|
55
|
+
app/DESIGN.md the design, written before the code (§3)
|
|
56
|
+
surfaces/<key>.jsx one file per domain surface
|
|
57
|
+
surfaces.js the manifest entries (import + component when built)
|
|
58
|
+
components/*.jsx anything shared between surfaces
|
|
59
|
+
components/depictions/ pictures of things that exist elsewhere, in their own colours (§3c)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Keep — the contract; do not edit:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
app/layout.jsx <html data-theme>, the _claw/theme.css link, AutoRefresh, ErrorRelay, Shell
|
|
66
|
+
app/globals.css the workspace theme's tokens (§1) — no palette of your own goes here
|
|
67
|
+
app/page.jsx / → the home surface (or "No surfaces yet" while the manifest is empty)
|
|
68
|
+
app/s/[key]/page.jsx renders the surface with that key; planned ones say so
|
|
69
|
+
app/error.jsx, app/global-error.jsx, app/not-found.jsx, app/error-relay.jsx, app/auto-refresh.jsx
|
|
70
|
+
components/ui/ the kit: Button, Field, Input, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction… (§3b, §3c)
|
|
71
|
+
components/parts/ the parts: RecordSwitcher, useFixture + FixtureNotice (§3b)
|
|
72
|
+
check/pins.json the fixed files by hash; `npm run check` refuses one that differs
|
|
73
|
+
lib/* db, data, actions, claw, platform, hooks, sample (§5)
|
|
74
|
+
check/ `npm run check` (§6)
|
|
75
|
+
middleware.js product-token check, x-claw-surface and x-claw-theme headers
|
|
76
|
+
next.config.mjs basePath + Server Actions allowedOrigins, both from env (§4b)
|
|
77
|
+
postcss.config.mjs Tailwind v4
|
|
78
|
+
schema.sql public.artifact_activity, for reference
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 3. Design first
|
|
82
|
+
|
|
83
|
+
Before any code, write `app/DESIGN.md` — one screen of text:
|
|
84
|
+
|
|
85
|
+
- The founder's design rules from the workspace file `/company/design.md`,
|
|
86
|
+
quoted. Read it before every build; if it is absent, write it from this
|
|
87
|
+
first build.
|
|
88
|
+
- Which tokens carry which job on THIS app — what is `bg-card`, where
|
|
89
|
+
`text-primary` appears, what `text-destructive` is reserved for. Not a
|
|
90
|
+
palette: the colours are the workspace's (§1). Colour is a signal, so
|
|
91
|
+
status tones (success, warning, destructive, info) are for status only.
|
|
92
|
+
- Depictions: `<thing> (modes)` or `none` — which pictures of elsewhere
|
|
93
|
+
the app shows (§3c), and in which of the target's modes.
|
|
94
|
+
- Type: scale and weight within the workspace's faces (`font-sans`,
|
|
95
|
+
`font-mono`, `font-display`). Numbers tabular (`data-nums` on the element).
|
|
96
|
+
- Layout and nav: where the surfaces live, what the home surface shows in
|
|
97
|
+
the first second.
|
|
98
|
+
- Density, empty states, error states.
|
|
99
|
+
- What makes this app look like ITS domain and not another.
|
|
100
|
+
|
|
101
|
+
Then `app/shell.jsx`, then the surfaces in the order the founder uses
|
|
102
|
+
them. Every one is a domain surface — the things the app finds, makes,
|
|
103
|
+
sends or receives. There is no agent, runs, activity or settings surface
|
|
104
|
+
to design: Agent, Runs and Activity are tabs the OS draws on the window,
|
|
105
|
+
and the app's settings — model, prompt, skills, connections, schedule —
|
|
106
|
+
are the window's Settings tab, the same for every artifact. The keys
|
|
107
|
+
`agent`, `runs` and `activity` are reserved; a surface may not take one.
|
|
108
|
+
The starter `app/shell.jsx` is a placeholder; replace it first.
|
|
109
|
+
|
|
110
|
+
What the OS does **not** own is asking the agent for something. Those
|
|
111
|
+
tabs are where the founder goes to look back; the app is where they set
|
|
112
|
+
work going and watch it happen (`useJob`, §5). A verb on an object list —
|
|
113
|
+
"Generate a launch plan" beside "New launch plan" — is part of the App
|
|
114
|
+
tab, and so is the panel it opens while the work runs.
|
|
115
|
+
|
|
116
|
+
**The rule: no two apps in a workspace should be mistaken for each other
|
|
117
|
+
at a glance.** The design comes from the domain, not from a default. The
|
|
118
|
+
colours are the same across the workspace by design; what tells the apps
|
|
119
|
+
apart is the shape of each surface — which is your work.
|
|
120
|
+
|
|
121
|
+
### 3b. Parts — assemble, never hand-roll
|
|
122
|
+
|
|
123
|
+
Every control in the app is a part from `components/ui`; the pieces that
|
|
124
|
+
touch this template's data are in `components/parts`. Each is the
|
|
125
|
+
workspace's look over the browser's own behaviour (a `<dialog>` for the
|
|
126
|
+
sheet, the Popover API for the menu, the native `<select>`, roving tabs)
|
|
127
|
+
— which is why they work on first contact and why a hand-rolled one
|
|
128
|
+
rarely does. `npm run check` refuses a raw `<select>`, `<dialog>`,
|
|
129
|
+
`<svg>`, `role="tab"`, `popover` or `window.confirm()` outside the kit.
|
|
130
|
+
|
|
131
|
+
```jsx
|
|
132
|
+
import { Button, IconButton, Card, Field, Input, Textarea, Select, Switch, Checkbox,
|
|
133
|
+
Sheet, Confirm, Tabs, TabPanel, Menu, Notice, Badge, Kicker, Empty, Icon, cx }
|
|
134
|
+
from "../components/ui"; // "./ui" from a component
|
|
135
|
+
import { RecordSwitcher, useFixture, FixtureNotice, isSample } from "../components/parts";
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- `Button` — `variant` primary (one per surface) · secondary · quiet ·
|
|
139
|
+
danger · link; `size` md (44px) · sm; `icon="plus"`; `href` for a link.
|
|
140
|
+
`IconButton` for an icon alone (`label` required).
|
|
141
|
+
- `Field` wraps `Input`, `Textarea`, `Select`, `Switch`, `Checkbox` with
|
|
142
|
+
its label, hint and error. `Select` with empty `options` renders a
|
|
143
|
+
muted line, never a disabled dropdown.
|
|
144
|
+
- `Sheet` — the one overlay, a rail beside the work (`side="end"`), a
|
|
145
|
+
dialog (`center`) or a bottom sheet; on a narrow window every side
|
|
146
|
+
becomes a bottom sheet. `Confirm` is the "delete this?" case.
|
|
147
|
+
- `Tabs` + `TabPanel` — views of ONE thing (a record's notes | history).
|
|
148
|
+
Never for the app's surfaces; those are the shell's nav.
|
|
149
|
+
- `Menu` — the "…" of a row. Empty `items` render nothing: a menu is
|
|
150
|
+
earned by having actions.
|
|
151
|
+
- `Card`, `Notice` (tone neutral · info · success · warning · danger),
|
|
152
|
+
`Badge`, `Kicker` (the small line above a heading), `Empty` (a filter
|
|
153
|
+
or search with no matches — not the landing state, that is the fixture).
|
|
154
|
+
- `Icon name="…"` — one sprite, `ICON_NAMES` lists it. Missing one? Add
|
|
155
|
+
the glyph to `components/ui/icon.jsx`; never an `<svg>` in a surface.
|
|
156
|
+
- `RecordSwitcher` — pick a saved record or make a new one. With nothing
|
|
157
|
+
saved it shows only New. A disabled dropdown reading "No saved…" is the
|
|
158
|
+
failure this part exists to prevent.
|
|
159
|
+
- `useFixture(rows, SAMPLE)` → `{ rows, isFixture, clear }` +
|
|
160
|
+
`FixtureNotice` — the worked example (§3: it opens populated), shown
|
|
161
|
+
only while the real collection is empty and **never INSERTed**. The
|
|
162
|
+
workspace database is shared by every app; an invented row is a real
|
|
163
|
+
row somewhere else. Clearing writes nothing. Editing a fixture row
|
|
164
|
+
copies its values into a new draft and saves THAT.
|
|
165
|
+
|
|
166
|
+
A control with nothing to act on is not drawn.
|
|
167
|
+
|
|
168
|
+
**Blocks — the bigger pieces, added when a surface needs one.** A board
|
|
169
|
+
you drag cards across, the panel that follows a job the agent is doing:
|
|
170
|
+
behaviour that goes wrong when every app draws it by hand. No app has a
|
|
171
|
+
block until it is added — `makersclaw search <what the surface does>`
|
|
172
|
+
finds one, `makersclaw add <name>` lays it in `components/blocks/<name>/`
|
|
173
|
+
and prints its props and an example (in Makersclaw, the `template` tool
|
|
174
|
+
runs both). Import it from there:
|
|
175
|
+
|
|
176
|
+
```jsx
|
|
177
|
+
import { Board } from "../components/blocks/board"; // "./blocks/board" from a component
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
A block is fixed like the kit: compose it through its props — what a card
|
|
181
|
+
says, what a move saves — never edit its files (`npm run check` refuses
|
|
182
|
+
a change; `makersclaw add <name>` restores one). Search before you build
|
|
183
|
+
a board, a table, a timeline, a calendar, a canvas or a job panel by
|
|
184
|
+
hand. When no block fits, build the part from the kit in `components/`,
|
|
185
|
+
with tokens only — except a depiction, §3c.
|
|
186
|
+
|
|
187
|
+
### 3c. Depictions — the one place colour is yours to copy
|
|
188
|
+
|
|
189
|
+
A depiction is a picture of something that exists elsewhere: a tweet as
|
|
190
|
+
X will show it, a LinkedIn post in the feed, the row an email makes in
|
|
191
|
+
Gmail's inbox, a Slack message, a receipt. Its whole job is *this is how
|
|
192
|
+
it will look there*, so it is painted in that place's colours, set in
|
|
193
|
+
that place's face, at that place's width — and it does not move when the
|
|
194
|
+
founder changes theme. The test, per element: **will the founder send,
|
|
195
|
+
post, paste or print THIS and see it there?** Then it is a depiction. Do
|
|
196
|
+
they type into it, save from it, switch records in it, or does it change
|
|
197
|
+
what is stored? Then it is chrome, and it is the workspace's. X's
|
|
198
|
+
composer is not a depiction; the founder is the one typing. Not a
|
|
199
|
+
depiction either: a chart, a code block, a status, a pin on a map, the
|
|
200
|
+
founder's own brand on your buttons, a colour a user picks for a tag, a
|
|
201
|
+
Slack mark on a "Post to Slack" button. Those stay tokens. A published
|
|
202
|
+
thing is an embed (`<iframe>` of the real post), not a depiction.
|
|
203
|
+
|
|
204
|
+
**The file.** `components/depictions/<thing>.jsx` — one thing as it looks
|
|
205
|
+
in one place, one default export, `"use client"` if it holds the target's
|
|
206
|
+
own toggle. It opens with a header comment: the target, the date you
|
|
207
|
+
traced it, its column widths in px, its named modes, then `Exact:`
|
|
208
|
+
(colours, sizes, radii, widths, where the text folds) and `Approximate:`
|
|
209
|
+
(the face when it is proprietary — X's Chirp, Google Sans — and the emoji
|
|
210
|
+
set). A depiction without that header is a guess, not a picture.
|
|
211
|
+
|
|
212
|
+
Inside it, everything is the target's and everything is px: `text-[15px]
|
|
213
|
+
p-[12px] rounded-[16px]`, `style={{ color: "#536471" }}`, its own `<svg>`
|
|
214
|
+
glyphs (the X mark, the verified badge — the kit's stroke icons do not
|
|
215
|
+
look like X's). Never `text-sm`, `p-4`, `rounded-lg` or `font-sans`: rem
|
|
216
|
+
is the workspace's base size, `--spacing` carries the founder's density,
|
|
217
|
+
`rounded-lg` is the preset's radius, and no wrapper can undo them — the
|
|
218
|
+
check refuses each. Never a token, never `dark:`: the target's own modes
|
|
219
|
+
(X: default, dim, lights out) are a `mode` prop and a table in the file,
|
|
220
|
+
and have nothing to do with the workspace being light or dark. The root
|
|
221
|
+
paints the target's page colour and its outer padding; the card sits
|
|
222
|
+
inside. A white tweet floating on the workspace's charcoal is not how it
|
|
223
|
+
will look there.
|
|
224
|
+
|
|
225
|
+
It takes data in and nothing else: text, name, handle, avatar url, counts,
|
|
226
|
+
`mode`. It imports `react`, `../ui/cx` and its siblings — never `../ui`,
|
|
227
|
+
`../parts` or `lib/`. No `<form>`, no `<input>`, no `<a>` (a link there is
|
|
228
|
+
a `<span>` that looks like one), no handler except the target's own
|
|
229
|
+
toggle written in place (`onClick={() => setOpen(true)}` for LinkedIn's
|
|
230
|
+
"…more"), no reads, no writes. When an automation will post the thing,
|
|
231
|
+
the depiction renders the same text the automation sends, so preview and
|
|
232
|
+
post cannot drift.
|
|
233
|
+
|
|
234
|
+
**The frame.** On the surface — or in the client component that holds the
|
|
235
|
+
draft, where a preview can follow typing (`components/<X>.jsx`, importing
|
|
236
|
+
`Depiction` from `"./ui"`) — always this shape:
|
|
237
|
+
|
|
238
|
+
```jsx
|
|
239
|
+
import { Depiction, Tabs } from "../components/ui";
|
|
240
|
+
import XPost from "../components/depictions/x-post";
|
|
241
|
+
|
|
242
|
+
<Depiction of="x-post" scheme="dark" width={600} label="On X · Dim (face approximate)">
|
|
243
|
+
<XPost text={draft.text} name={me.name} handle={me.handle} avatar={me.avatar} mode="dim" />
|
|
244
|
+
</Depiction>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Exactly one depiction inside the frame, self-closing. `of` names the
|
|
248
|
+
file; `scheme` is the target's light or dark (never the workspace's);
|
|
249
|
+
`width` is the target's real column, from the header comment — the frame
|
|
250
|
+
scales the picture to fit a narrow column and never reflows it, because
|
|
251
|
+
line breaks and folds are the preview. The caption is chrome. Two modes
|
|
252
|
+
side by side are two frames. The composer, the record switcher, the mode
|
|
253
|
+
and device toggles, Save — chrome, in tokens, outside. A depiction is a
|
|
254
|
+
default import, framed wherever it is rendered, and is never dropped
|
|
255
|
+
into the shell, the kit or `lib/`: rendered outside its frame it inherits
|
|
256
|
+
the workspace, and the check says so.
|
|
257
|
+
|
|
258
|
+
**One door, one bundle.** The folder is reached one way: a static
|
|
259
|
+
`import XPost from "…/depictions/x-post"` at the top of the file that
|
|
260
|
+
frames it. No `import()`, no `next/dynamic`, no re-export module, no
|
|
261
|
+
alias (`const P = XPost`), no picture passed along as a value. Through
|
|
262
|
+
the frame go data props only — a string, a number, a field of a row;
|
|
263
|
+
never an element, an object, an array, a function or a spread, whatever
|
|
264
|
+
the prop is called. The check reads every `.js`/`.jsx` the app has,
|
|
265
|
+
dot-files included; Tailwind reads exactly those files (`globals.css`
|
|
266
|
+
pins its sources), so a class name in a `.json` or built from pieces
|
|
267
|
+
never reaches the page.
|
|
268
|
+
|
|
269
|
+
## 4. Server and client
|
|
270
|
+
|
|
271
|
+
One split, stated once:
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
lib/data.js, lib/db.js server components and server actions only
|
|
275
|
+
lib/platform.js, lib/hooks.js "use client" files only
|
|
276
|
+
lib/actions.js either (they are server actions)
|
|
277
|
+
surfaces.js server files only (entries import components)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Pass plain data across the boundary — strings, numbers, arrays, objects.
|
|
281
|
+
`plain(row)` already turns dates into ISO strings. A client component can
|
|
282
|
+
receive a server action as a prop, and nothing else that is a function.
|
|
283
|
+
|
|
284
|
+
A surface file that uses hooks or event handlers starts with `"use
|
|
285
|
+
client"` and cannot import `lib/data.js`. Split it: a server file loads
|
|
286
|
+
the rows and renders a client component with props.
|
|
287
|
+
|
|
288
|
+
```jsx
|
|
289
|
+
// surfaces/investors.jsx (server)
|
|
290
|
+
import { loadRows } from "../lib/data";
|
|
291
|
+
import InvestorBoard from "../components/InvestorBoard";
|
|
292
|
+
|
|
293
|
+
export default async function Investors({ surface, searchParams }) {
|
|
294
|
+
const { rows, error } = await loadRows({ source: "public.investors", orderBy: "updated_at desc" });
|
|
295
|
+
return <InvestorBoard rows={rows} error={error} highlight={searchParams.row ?? null} />;
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
```jsx
|
|
300
|
+
// components/InvestorBoard.jsx (client)
|
|
301
|
+
"use client";
|
|
302
|
+
import { moveRow } from "../lib/actions";
|
|
303
|
+
import { Card, Notice } from "./ui";
|
|
304
|
+
import { FixtureNotice, useFixture } from "./parts";
|
|
305
|
+
|
|
306
|
+
const SAMPLE = [{ name: "Ada Ventures", stage: "warm" }, /* … */];
|
|
307
|
+
|
|
308
|
+
export default function InvestorBoard({ rows: real, error, highlight }) {
|
|
309
|
+
const { rows, isFixture, clear } = useFixture(real, SAMPLE);
|
|
310
|
+
/* … */
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Every surface component receives `surface` (`{ key, kind, label, source
|
|
315
|
+
}`) and `searchParams` (the URL query; `?row=<id>` is the convention for
|
|
316
|
+
"highlight this row" — an activity line opens its surface with it).
|
|
317
|
+
|
|
318
|
+
### Linking — `next/link`, never a bare `<a href="/…">`
|
|
319
|
+
|
|
320
|
+
The app is served under a prefix (`/p/<label>` in local dev, nothing in
|
|
321
|
+
production) and `next.config.mjs` puts it in `basePath`. **`<Link>` and
|
|
322
|
+
`router.push()` apply that prefix. A plain `<a href>` does not** — the
|
|
323
|
+
browser resolves it against the ORIGIN, so the click leaves the app
|
|
324
|
+
entirely and loads the Makersclaw dashboard inside the app's own frame.
|
|
325
|
+
|
|
326
|
+
```jsx
|
|
327
|
+
import Link from "next/link";
|
|
328
|
+
<Link href={`/s/${key}`}>Goals</Link> // ✅ becomes /p/<label>/s/goals
|
|
329
|
+
<a href={`/s/${key}`}>Goals</a> // ❌ goes to /s/goals on the dashboard
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Assets hide the mistake: Next writes those itself, so `_next/static/…`
|
|
333
|
+
comes out correctly prefixed while the nav is broken, and the page still
|
|
334
|
+
answers 200. Nothing in the build catches it — only a click does. A write
|
|
335
|
+
under `app/` that contains a root-relative `<a>` comes back with a
|
|
336
|
+
`warnings` entry saying so.
|
|
337
|
+
|
|
338
|
+
Same trap, same fix: `window.location` assignments and `<form
|
|
339
|
+
action="/…">`. A real external URL (`https://`, `mailto:`) is fine.
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
### 4b. Writes are rejected — "Invalid Server Actions request"
|
|
343
|
+
|
|
344
|
+
Already handled. Do not fix it in `next.config.mjs`.
|
|
345
|
+
|
|
346
|
+
Next 15 refuses a Server Action whose `Origin` header disagrees with
|
|
347
|
+
`x-forwarded-host`, and in this stack they always disagree: the sandbox
|
|
348
|
+
ingress rewrites `x-forwarded-host` to its own vercel.run name while the
|
|
349
|
+
browser is on the product host. Untreated, the app builds, loads and
|
|
350
|
+
renders perfectly — and its **first save fails**, which reads like a dead
|
|
351
|
+
button rather than a proxy policy.
|
|
352
|
+
|
|
353
|
+
`next.config.mjs` already reads `CLAW_ALLOWED_ORIGINS`, which `product_run`
|
|
354
|
+
derives per environment (`src/lib/products.ts`, `productAllowedOrigins`):
|
|
355
|
+
the product's own FQDN in production, the dashboard's dev origin under a
|
|
356
|
+
path prefix.
|
|
357
|
+
|
|
358
|
+
**Never hardcode an origin here.** `allowedOrigins: ["localhost:5173"]`
|
|
359
|
+
passes every test you can run in development and refuses every write in
|
|
360
|
+
production. If a write is still rejected, the value is wrong at the
|
|
361
|
+
source — say so and stop; it is not an app-level fix.
|
|
362
|
+
|
|
363
|
+
## 5. API reference
|
|
364
|
+
|
|
365
|
+
### Reads — `lib/data.js` (server)
|
|
366
|
+
|
|
367
|
+
Every read resolves `{ ..., error }` and never throws. `error` is text
|
|
368
|
+
when the table is missing or the database unreachable; it is also
|
|
369
|
+
`console.error`ed, so the log tail carries it to the platform agent.
|
|
370
|
+
`source` is `"table"` or `"schema.table"`; names go through `ident()`.
|
|
371
|
+
|
|
372
|
+
```js
|
|
373
|
+
loadRows({ source, idColumn = "id", orderBy, limit = 500, filter }) → { rows, error }
|
|
374
|
+
loadRow({ source, idColumn = "id", id }) → { row, error } // row null when none
|
|
375
|
+
countRows({ source, filter }) → { count, error }
|
|
376
|
+
countBy({ source, column, filter }) → { counts: { [value]: n }, error }
|
|
377
|
+
loadActivity({ limit = 100, surface, since }) → { rows, error } // public.artifact_activity, this artifact
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
`filter` is `{ column: value }`: `null` → `IS NULL`, an array → `= ANY`,
|
|
381
|
+
else equality. `orderBy` is `"updated_at desc, id"`; default `idColumn
|
|
382
|
+
desc`. `limit` caps at 5000 (activity: 1000).
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
const { rows, error } = await loadRows({ source: "public.drafts", filter: { stage: ["draft", "approved"] } });
|
|
386
|
+
const { counts } = await countBy({ source: "public.investors", column: "stage" });
|
|
387
|
+
const { rows: log } = await loadActivity({ surface: "drafts", limit: 50 });
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Helpers: `plain(row)` (dates → ISO, buffers → base64), `orderClause(orderBy,
|
|
391
|
+
fallback)` (validated `ORDER BY` text), `markSample(sample, idColumn)`
|
|
392
|
+
(stable ids + `_sample: true`), `currentSurface(explicit)` (the surface
|
|
393
|
+
key of the request, from `x-claw-surface`, unless one is passed).
|
|
394
|
+
|
|
395
|
+
### Writes — `lib/actions.js` (server actions, callable from either side)
|
|
396
|
+
|
|
397
|
+
Each quotes names with `ident()`, writes one activity row through
|
|
398
|
+
`logActivity`, calls `revalidatePath("/", "layout")`, and resolves
|
|
399
|
+
`{ ok }` or `{ ok: false, error }`. Never throws. `surface` defaults to
|
|
400
|
+
the request's surface (middleware sets it from `/s/<key>`).
|
|
401
|
+
|
|
402
|
+
```js
|
|
403
|
+
updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data })
|
|
404
|
+
moveRow({ source, idColumn = "id", id, stateColumn, to, surface }) // action "moved"
|
|
405
|
+
insertRow({ source, idColumn = "id", values, surface, action = "added", detail }) → { ok, id }
|
|
406
|
+
deleteRow({ source, idColumn = "id", id, surface, detail }) // action "removed"
|
|
407
|
+
saveSetting({ mode = "table" | "json", source, keyColumn = "key", valueColumn = "value",
|
|
408
|
+
column, idColumn = "id", id, key, value, surface })
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`saveSetting` table mode updates the `(key, value)` row and inserts one
|
|
412
|
+
when missing — a plain `key text, value text` table is enough. JSON mode
|
|
413
|
+
sets `key` inside the jsonb `column` of row `id`.
|
|
414
|
+
|
|
415
|
+
```js
|
|
416
|
+
"use client";
|
|
417
|
+
import { moveRow } from "../lib/actions";
|
|
418
|
+
const r = await moveRow({ source: "public.investors", id, stateColumn: "stage", to: "sent" });
|
|
419
|
+
if (!r.ok) setError(r.error);
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Never write raw SQL from a surface. A write that needs more than these
|
|
423
|
+
is a new function in `lib/actions.js` that also calls `logActivity`.
|
|
424
|
+
|
|
425
|
+
### Platform — `lib/platform.js` (browser)
|
|
426
|
+
|
|
427
|
+
Typed wrappers over `lib/claw.js`, one per `_claw` route an app has a
|
|
428
|
+
reason to call. Every call resolves `{ ok, status, data }` or `{ ok:
|
|
429
|
+
false, status, error, unavailable }`; nothing throws. `unavailable: true`
|
|
430
|
+
(404 or no network) means the gateway has no such route yet: show a
|
|
431
|
+
quiet "not connected yet", not an error.
|
|
432
|
+
|
|
433
|
+
```js
|
|
434
|
+
import { platform } from "../lib/platform"; // or the named functions
|
|
435
|
+
platform.me() GET _claw/me → { artifact: { id, name, vehicle, status, home }, user: { id, email }, blueprint }
|
|
436
|
+
platform.startJob(message, { sessionId, onEvent }) POST _claw/chat → streams session · delta · tool · done · error
|
|
437
|
+
platform.cancelJob(sessionId) POST _claw/cancel
|
|
438
|
+
platform.reportError({ message, stack, url, source }) POST _claw/events
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Named exports: `me, startJob, cancelJob, reportError, platform`. That is
|
|
442
|
+
the whole list. Note what is **not** there: the runs, the chat history,
|
|
443
|
+
Run now, Pause / Resume. Those are MONITORING and they are the OS's — the
|
|
444
|
+
window's tabs and title bar — so an app never calls `_claw/agent`,
|
|
445
|
+
`_claw/runs`, `_claw/run`, `_claw/pause` or `_claw/resume`. Starting work
|
|
446
|
+
is the opposite thing, and it belongs here (see `useJob`). The artifact's
|
|
447
|
+
status lives on `<html data-claw-status>` (`publishStatus` in
|
|
448
|
+
`lib/claw.js`); `useArtifact()` keeps it current.
|
|
449
|
+
|
|
450
|
+
### Hooks — `lib/hooks.js` ("use client")
|
|
451
|
+
|
|
452
|
+
Three hooks: a generic fetch over one `_claw` route, the app's identity,
|
|
453
|
+
and a job. Each owns fetching, polling (while the tab is visible; once a
|
|
454
|
+
minute while `unavailable`), streaming and cleanup on unmount. A surface
|
|
455
|
+
renders what comes back and owns every pixel.
|
|
456
|
+
|
|
457
|
+
```js
|
|
458
|
+
useClaw(path, { every, enabled = true }) → { data, error, loading, unavailable, reload }
|
|
459
|
+
useArtifact() → { artifact, user, blueprint, loading, error, unavailable, reload }
|
|
460
|
+
useJob() → { running, steps, text, progress, error, sessionId, finishedAt, start, cancel, reset }
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
There is no chat, runs or activity hook: the OS draws those tabs.
|
|
464
|
+
Activity has no hook either — it is a Postgres table, read server-side
|
|
465
|
+
with `loadActivity` by any domain surface that wants a slice of it (a
|
|
466
|
+
row's history beside the row, say).
|
|
467
|
+
|
|
468
|
+
### Commissioning work — `useJob()`
|
|
469
|
+
|
|
470
|
+
The founder asks the app's agent to make something, from inside the app,
|
|
471
|
+
and watches it happen. This is SOP §2.3 and the reason it exists is
|
|
472
|
+
blunt: with invocation only on the window's Agent tab, a founder starts
|
|
473
|
+
work and then stares at an app where nothing changes.
|
|
474
|
+
|
|
475
|
+
```jsx
|
|
476
|
+
const job = useJob();
|
|
477
|
+
const products = useProducts(); // your own data hook
|
|
478
|
+
|
|
479
|
+
useEffect(() => { products.reload(); }, [job.progress]); // rows as they land
|
|
480
|
+
|
|
481
|
+
<button onClick={() => job.start("Draft a launch checklist for Rowhouse")}>
|
|
482
|
+
Generate a launch plan
|
|
483
|
+
</button>
|
|
484
|
+
|
|
485
|
+
{job.running || job.finishedAt ? (
|
|
486
|
+
<aside> {/* your design, not ours */}
|
|
487
|
+
{job.steps.map((s, i) => <li key={i} data-status={s.status}>{s.label}</li>)}
|
|
488
|
+
<p>{job.text}</p>
|
|
489
|
+
{job.running ? <button onClick={job.cancel}>Stop</button>
|
|
490
|
+
: <button onClick={job.reset}>Close</button>}
|
|
491
|
+
</aside>
|
|
492
|
+
) : null}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
The panel is a block — `makersclaw add job-panel` — that keeps the four
|
|
496
|
+
rules below for you: `<JobPanel job={job} title="Drafting the launch
|
|
497
|
+
plan" doneTitle="Launch plan drafted" />`. Draw your own only when it
|
|
498
|
+
cannot say what the founder needs to see.
|
|
499
|
+
|
|
500
|
+
Four rules, and a build is wrong without them:
|
|
501
|
+
|
|
502
|
+
1. **The verb sits on the object list, beside the manual one.** "Generate
|
|
503
|
+
a launch plan" next to "New launch plan", on the Launches surface.
|
|
504
|
+
Never a nav item named after the agent, and never the only way to
|
|
505
|
+
create that object — the manual form has to exist too.
|
|
506
|
+
2. **The panel does not exist when no job is running.** Mount it from
|
|
507
|
+
`job.running || job.finishedAt`; `job.reset()` closes it. A panel
|
|
508
|
+
that is always there is the monitoring console (§2.7).
|
|
509
|
+
3. **`job.text` is not the deliverable.** It is the agent's account of
|
|
510
|
+
what it did. The deliverable is ROWS — that is why `job.progress`
|
|
511
|
+
reloads your data hook, and why the panel closing leaves a fuller
|
|
512
|
+
list behind it.
|
|
513
|
+
4. **Show `step.label`, never `step.name`.** `label` is the founder's
|
|
514
|
+
phrase ("Searching the web"); `name` is `web_search`.
|
|
515
|
+
|
|
516
|
+
`job.sessionId` is the thread. Keep it on the row the job produced and
|
|
517
|
+
pass it back as `job.start(msg, { sessionId })` to refine that one thing
|
|
518
|
+
where it lives — one conversation per object, never a global chat.
|
|
519
|
+
|
|
520
|
+
### Activity — the contract
|
|
521
|
+
|
|
522
|
+
`public.artifact_activity` is how the founder sees the work. The OS's
|
|
523
|
+
**Activity tab** on the app window reads it, newest first — and so can
|
|
524
|
+
any domain surface that wants to (`loadActivity`). Every
|
|
525
|
+
`lib/actions.js` write inserts a row through `logActivity()` in
|
|
526
|
+
`lib/db.js`; the artifact's agent writes the same table with the `sql`
|
|
527
|
+
verb, `by = 'agent'` or `'run:<id>'`.
|
|
528
|
+
|
|
529
|
+
```js
|
|
530
|
+
await logActivity({
|
|
531
|
+
action: "drafted", // found, drafted, sent, replied, skipped, edited, moved, added, removed…
|
|
532
|
+
surface: "drafts", // surface key → /s/drafts?row=<row_ref>
|
|
533
|
+
row_ref: String(row.id), // the id in that surface's source table
|
|
534
|
+
detail: "Drafted intro to Weekend Fund",
|
|
535
|
+
data: { to: "ryan@weekend.fund" },
|
|
536
|
+
by: "user:founder@x.com", // 'agent' | 'user:<email>' | 'run:<id>'
|
|
537
|
+
});
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Columns: `id, artifact_id, at, action, surface, row_ref, detail, data
|
|
541
|
+
(jsonb), by`. `loadActivity` returns them without `artifact_id` — every
|
|
542
|
+
row is this artifact's. The OS Activity tab shows the surface's label
|
|
543
|
+
on each line and does not link into the app yet; the convention for
|
|
544
|
+
anything that does (a domain surface's own history slice today, the tab
|
|
545
|
+
later) is `surface` + `row_ref` → `/s/<surface>?row=<row_ref>`, so stamp
|
|
546
|
+
both on every line and make the surface honour `?row=`.
|
|
547
|
+
|
|
548
|
+
### Errors
|
|
549
|
+
|
|
550
|
+
`app/error.jsx` catches a surface that throws while rendering, shows the
|
|
551
|
+
message inside the shell, and posts it to `_claw/events`.
|
|
552
|
+
`app/global-error.jsx` does the same for the layout. `app/error-relay.jsx`
|
|
553
|
+
posts `window.onerror` and `unhandledrejection`. All three go through
|
|
554
|
+
`reportError({ message, stack, url, source })` in `lib/claw.js`, deduped
|
|
555
|
+
and capped per page load; `source` is `"preview"` while the artifact is a
|
|
556
|
+
draft and `"runtime"` once launched (`app/error-relay.jsx` keeps the
|
|
557
|
+
status on `<html data-claw-status>` from `_claw/me`; nothing in your
|
|
558
|
+
files has to). The platform agent reads them as console events. Call
|
|
559
|
+
`reportError` yourself only for a failure the boundaries cannot see.
|
|
560
|
+
|
|
561
|
+
## 6. Acceptance
|
|
562
|
+
|
|
563
|
+
`npm run check` — before `surface_built`, and again before launch. One
|
|
564
|
+
line per rule, under twenty lines, exit 1 on a failure that names the
|
|
565
|
+
file and line and what to use instead. It builds for itself, into
|
|
566
|
+
`.next-check`, and only when a source changed since the last check —
|
|
567
|
+
never run `npm run build` first, and never build into `.next`: the dev
|
|
568
|
+
server product_run started owns that folder as another user, and a
|
|
569
|
+
second build there dies with `EACCES: .next/trace`.
|
|
570
|
+
|
|
571
|
+
```
|
|
572
|
+
check: 5 files · 2 surfaces
|
|
573
|
+
ok colours no colour outside the workspace theme (1 depiction excepted)
|
|
574
|
+
ok controls every control is a kit part
|
|
575
|
+
ok examples fixtures stay in code (useFixture), nothing INSERTs one
|
|
576
|
+
ok manifest 2 surfaces: drafts, sent
|
|
577
|
+
ok icons every icon name exists
|
|
578
|
+
ok kit 33 fixed files match the template
|
|
579
|
+
ok depictions 1 picture of elsewhere, framed on 1 surface: x-post
|
|
580
|
+
ok smoke built in 41s · / → 307 · /s/drafts 200 · /s/sent 200 (0.6s)
|
|
581
|
+
all clear
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
`kit` refuses a fixed file that differs from the template (they are
|
|
585
|
+
hashed in `check/pins.json`) and any file of yours under `components/ui`
|
|
586
|
+
or `components/parts`. `depictions` reads both directions: nothing of
|
|
587
|
+
the workspace inside a depiction (a token, `font-sans`, `dark:`, a rem
|
|
588
|
+
utility), nothing of the depiction outside its frame (a named import, a
|
|
589
|
+
picture in the shell or a `Card`, a dead file nobody renders).
|
|
590
|
+
|
|
591
|
+
The smoke test starts that build on a loopback port and loads every
|
|
592
|
+
surface; `--static` skips the build and the smoke. Then, by eye:
|
|
593
|
+
|
|
594
|
+
- Renders at 360px, 768px and 1280px without horizontal scroll.
|
|
595
|
+
- The fixture shows while the table is empty; a saved row replaces it.
|
|
596
|
+
- Error state shows the text from `{ error }`.
|
|
597
|
+
- Keyboard focus visible on every control.
|
|
598
|
+
- Numbers tabular (`data-nums`).
|
|
599
|
+
- Every write goes through `lib/actions.js`; no raw SQL from a surface.
|
|
600
|
+
- Every surface reachable from the nav.
|
|
601
|
+
|
|
602
|
+
## 7. Run
|
|
603
|
+
|
|
604
|
+
1. `makersclaw init` laid this into the artifact's `app/` (in Makersclaw,
|
|
605
|
+
`artifact_scaffold {dir}` runs it and makes `public.artifact_activity`
|
|
606
|
+
in the workspace database). `makersclaw.json` records the version and
|
|
607
|
+
the blocks; `makersclaw update` moves the fixed files and blocks to a
|
|
608
|
+
newer version and never touches your own files.
|
|
609
|
+
2. `app_database {name, dir}` → `DATABASE_URL` in `.env`. The app's role
|
|
610
|
+
reads and writes every table in `public`.
|
|
611
|
+
3. Make the domain tables with the `sql` verb (they land in `public`).
|
|
612
|
+
4. `product_run` with this directory. While building, run it as a dev
|
|
613
|
+
server — `command "npx next dev -H 127.0.0.1"`, `build ""` — so each
|
|
614
|
+
edit shows in the founder's live preview without a rebuild.
|
|
615
|
+
`product_run` sets `CLAW_BASE_PATH`, `CLAW_PRODUCT`,
|
|
616
|
+
`CLAW_PRODUCT_TOKEN` and `CLAW_ARTIFACT_ID`.
|
|
617
|
+
|
|
618
|
+
Every `artifact_files` write under `app/` comes back with the console's
|
|
619
|
+
state; a 5xx carries the log tail. Fix it before moving on — the founder
|
|
620
|
+
is watching the preview.
|
|
621
|
+
|
|
622
|
+
Every page is `dynamic = "force-dynamic"`: nothing is prerendered, so a
|
|
623
|
+
build never needs the database. `app/auto-refresh.jsx` re-renders from
|
|
624
|
+
the server every 5 s while the tab is visible, so the agent's writes
|
|
625
|
+
appear. The founder reads "artifact", "app", "agent", "automation" —
|
|
626
|
+
nothing else.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
// Rows change under this page from outside it — the agent writes them
|
|
7
|
+
// while the founder watches. Re-render from the server every 5 s while the
|
|
8
|
+
// tab is visible; router.refresh() keeps form state and scroll, a reload
|
|
9
|
+
// would not. Nothing runs while the tab is hidden.
|
|
10
|
+
const EVERY_MS = 5000;
|
|
11
|
+
|
|
12
|
+
export default function AutoRefresh() {
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const tick = () => {
|
|
16
|
+
if (document.visibilityState === "visible") router.refresh();
|
|
17
|
+
};
|
|
18
|
+
const id = setInterval(tick, EVERY_MS);
|
|
19
|
+
document.addEventListener("visibilitychange", tick);
|
|
20
|
+
return () => {
|
|
21
|
+
clearInterval(id);
|
|
22
|
+
document.removeEventListener("visibilitychange", tick);
|
|
23
|
+
};
|
|
24
|
+
}, [router]);
|
|
25
|
+
return null;
|
|
26
|
+
}
|