spiega 0.0.0-stage → 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.
Files changed (52) hide show
  1. package/LICENSE +31 -0
  2. package/README.md +408 -2
  3. package/bin/spiega +155 -0
  4. package/docs/README.md +9 -0
  5. package/docs/logo/spiega-mark-on-dark.svg +1 -0
  6. package/docs/logo/spiega-mark.svg +1 -0
  7. package/docs/shapes.md +122 -0
  8. package/docs/skill.md +104 -0
  9. package/docs/vocabulary.md +223 -0
  10. package/examples/order-flow.json +34 -0
  11. package/integrations/README.md +92 -0
  12. package/integrations/cursor/spiega.mdc +68 -0
  13. package/integrations/herdr/README.md +61 -0
  14. package/integrations/herdr/bin/_pane-cwd.sh +19 -0
  15. package/integrations/herdr/bin/focus.sh +11 -0
  16. package/integrations/herdr/bin/list.sh +9 -0
  17. package/integrations/herdr/bin/open-list.sh +3 -0
  18. package/integrations/herdr/bin/open.sh +7 -0
  19. package/integrations/herdr/herdr-plugin.toml +51 -0
  20. package/package.json +58 -4
  21. package/skills/spiega/SKILL.md +268 -0
  22. package/src/agents-block.js +118 -0
  23. package/src/bench.js +246 -0
  24. package/src/board/board.html +876 -0
  25. package/src/board/camera.js +99 -0
  26. package/src/board/draw-on.js +95 -0
  27. package/src/board/log.js +175 -0
  28. package/src/board/narrate.js +291 -0
  29. package/src/board/open.js +144 -0
  30. package/src/board/script.js +166 -0
  31. package/src/board/server.js +644 -0
  32. package/src/board/snapshot.js +108 -0
  33. package/src/board/state.js +499 -0
  34. package/src/cli.js +726 -0
  35. package/src/doctor.js +469 -0
  36. package/src/index.js +7 -0
  37. package/src/install.js +598 -0
  38. package/src/mcp/server.js +304 -0
  39. package/src/migrate.js +271 -0
  40. package/src/render/callout.js +107 -0
  41. package/src/render/edges.js +213 -0
  42. package/src/render/glyphs.js +394 -0
  43. package/src/render/ink.js +227 -0
  44. package/src/render/layout.js +286 -0
  45. package/src/render/math.js +368 -0
  46. package/src/render/render.js +154 -0
  47. package/src/render/theme.js +84 -0
  48. package/src/term.js +79 -0
  49. package/src/toml.js +34 -0
  50. package/test/persist.mjs +362 -0
  51. package/test/saving.mjs +131 -0
  52. package/test/smoke.mjs +1856 -0
package/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pierangelo Cecchetto - TulipLogic
4
+
5
+ The spiega icon (docs/logo/spiega-mark.svg and spiega-mark-on-dark.svg) is
6
+ original work under the copyright above, and not part of the visual language
7
+ credited below.
8
+
9
+ The visual language (palette, hand-drawn ink and grain treatment, flat
10
+ three-band bodies, cobalt cast shadow) follows Colorfigures and ISO-GLOW:
11
+ Copyright (c) 2026 April Zhu — MIT
12
+ and iso-figure, on which ISO-GLOW is based:
13
+ Copyright (c) 2026 Tolga Cohce — MIT
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,409 @@
1
- # Temporary Holding Version
1
+ <img src="https://raw.githubusercontent.com/pierangeloc/spiega/main/docs/logo/spiega-mark.svg" alt="" width="72" align="left" hspace="12">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # spiega
4
+
5
+ A hand-drawn board an AI agent draws on **while it explains**.
6
+
7
+ *spiega* is Italian for *explain* — and for *unfold*, which is what this does
8
+ to a long document: spreads it out until you can see the whole shape at once.
9
+
10
+ Say it **SPYEH-ga** (/ˈspjɛɡa/): two syllables, stress on the first, and the
11
+ `i` is a glide rather than a vowel — *spyeh*, not *spee-eh*. It is the sound
12
+ English keeps missing in *piano*, which Italian says the same way.
13
+
14
+ You ask how something works. The agent answers in the terminal, as usual — and
15
+ beside it, a board fills in: boxes for the services, labelled arrows for the
16
+ calls, a dashed enclosure round the VPC, a note where the gotcha is. Then it
17
+ walks the request through, one numbered hop at a time.
18
+
19
+ It is meant for the thing a terminal is bad at: holding six nouns and the
20
+ arrows between them in your head while you read a paragraph about them.
21
+
22
+ ![a finished board, saved as a snapshot, with step 3 lit](https://raw.githubusercontent.com/pierangeloc/spiega/main/docs/board.png)
23
+
24
+ ## Why not Miro, or Mermaid
25
+
26
+ Miro is a place you go. This is a pane beside the conversation that the agent
27
+ is already having, and the board is gone when you are done unless you keep it.
28
+
29
+ Mermaid renders a finished diagram. This is drawn incrementally, in the order
30
+ the explanation goes, which is most of what makes it followable — and it looks
31
+ drawn, not generated, so it reads as somebody thinking rather than a document.
32
+
33
+ ## Install
34
+
35
+ Node 20 or newer. No dependencies and no build step, so there is nothing to
36
+ compile and nothing to wait for.
37
+
38
+ Developed and used on **macOS**, and tested on macOS and Linux across Node 20,
39
+ 22 and 24. Windows paths are in the code and nobody has run them in anger — if
40
+ you are on Windows, expect to file the first issue, and say so and it will get
41
+ fixed.
42
+
43
+ ### 1. Get it on your PATH
44
+
45
+ ```bash
46
+ npm install -g spiega
47
+ ```
48
+
49
+ Or from a clone, if you want to change it:
50
+
51
+ ```bash
52
+ git clone https://github.com/pierangeloc/spiega.git
53
+ cd spiega
54
+ npm link # `spiega` now points at this checkout
55
+ ```
56
+
57
+ `npm link` rather than a copy on purpose: the CLI, the MCP server and the agent
58
+ skill all resolve to that directory, so `git pull` updates every one of them at
59
+ once.
60
+
61
+ Check it:
62
+
63
+ ```bash
64
+ spiega help
65
+ ```
66
+
67
+ ### 2. Wire it into your agents
68
+
69
+ ```bash
70
+ spiega install --detect # what it would do, touching nothing
71
+ spiega install # do it
72
+ ```
73
+
74
+ `install` with no arguments does only what it finds on this machine, because
75
+ writing a config for a harness you do not use is how you end up with a file you
76
+ never asked for. `--list` shows every target and what it writes.
77
+
78
+ It also links the command into a directory an agent's shell can see. That link
79
+ is the one write it makes outside a config file of its own, and when
80
+ `~/.local/bin` is not already on a non-interactive `PATH` it **asks** before
81
+ adding a line to your `~/.zshenv`:
82
+
83
+ ```
84
+ spiega needs to be where an agent can find it.
85
+ ~/.local/bin is not on the PATH a non-interactive shell gets.
86
+
87
+ This would add one line to ~/.zshenv, after backing it up:
88
+ export PATH="$HOME/.local/bin:$PATH"
89
+
90
+ Add it? [y/N]
91
+ ```
92
+
93
+ Anything but yes is no. Where there is nobody to ask — a script, CI, an agent's
94
+ own tool call — the answer is no and the line is printed instead, so an install
95
+ never hangs on a prompt and never edits a shell file unattended. `--path` and
96
+ `--no-path` answer in advance.
97
+
98
+ **Saying no withholds nothing else.** The skills and the MCP entries still
99
+ install, and MCP runs spiega by its full path, so it works. What you lose is
100
+ the fallback: an agent whose MCP server has died cannot find the command, and
101
+ writes prose instead of drawing with nothing on screen to say it meant to.
102
+ Install says so when you decline, and `spiega doctor` keeps saying so.
103
+
104
+ | target | what it gets |
105
+ |---|---|
106
+ | `claude` | the skill, linked into `~/.claude/skills/` |
107
+ | `codex` | the skill, an `AGENTS.md` block, an `mcp_servers` table |
108
+ | `gemini` | the skill, a `GEMINI.md` block, an `mcpServers` entry |
109
+ | `opencode` | `mcp.spiega` in `opencode.json`, and an `AGENTS.md` block |
110
+ | `copilot` `cursor` `windsurf` `zed` `vscode` `claude_desktop` | the MCP server, in each one's own config shape |
111
+ | `herdr` | a plugin: reach this task's board from its pane |
112
+ | `project` | an `AGENTS.md` block in the repo you are in — ask for it by name |
113
+
114
+ Everything is idempotent and backs up what it rewrites. Skills are
115
+ **symlinked**, so they track the checkout instead of going stale. Instruction
116
+ files get a delimited `SPIEGA_START`/`SPIEGA_END` block that leaves your own
117
+ text alone. For a harness nothing here knows about, `spiega mcp-config`
118
+ prints the entry to paste.
119
+
120
+ ### 3. See that it works
121
+
122
+ ```bash
123
+ spiega draw '[{"op":"title","text":"Hello"},
124
+ {"op":"node","id":"gw","kind":"gateway","label":"API Gateway"},
125
+ {"op":"node","id":"db","kind":"datastore","label":"Postgres"},
126
+ {"op":"edge","from":"gw","to":"db","label":"SQL"}]'
127
+ spiega open # a window of its own
128
+ ```
129
+
130
+ Two boxes and a line, drawn on. If you see them, every layer is working: the
131
+ board, the renderer and the CLI.
132
+
133
+ ### 4. Restart your agent
134
+
135
+ A running agent session holds the MCP process it started, and that process has
136
+ the code it was launched with. **Start a fresh session after installing**, or
137
+ the agent will be reading an older spiega than the one on disk. The symlink
138
+ keeps files current; it cannot do anything about processes already running.
139
+
140
+ Then ask for something architectural — "walk me through how X talks to Y, on
141
+ the board" — and watch the pane.
142
+
143
+ ### Updating, and checking that it took
144
+
145
+ ```bash
146
+ cd spiega && git pull && npm test
147
+ spiega install # only the copies need this: AGENTS.md blocks, the Cursor rule
148
+ spiega doctor # is what is running the thing that is on disk?
149
+ ```
150
+
151
+ spiega reaches an agent through four kinds of thing, and they go stale
152
+ differently:
153
+
154
+ | | goes stale when |
155
+ |---|---|
156
+ | the CLI, via `npm link` | never — it is this checkout |
157
+ | a skill, symlinked | never — but the agent loaded it when its session started |
158
+ | an `AGENTS.md` block, a Cursor rule | the wording changes — re-run `spiega install` |
159
+ | **a running process** | **immediately — it holds the code it was launched with** |
160
+
161
+ The last one is the trap, and it is the one that looks like a feature that was
162
+ never built. `spiega doctor` finds it:
163
+
164
+ ```
165
+ spiega doctor ──────────────────────────────────────────
166
+
167
+ ✓ spiega on PATH ~/.nvm/…/spiega → this checkout
168
+ ✓ claude skill linked — tracks the checkout
169
+ ✓ opencode AGENTS.md block is current
170
+ ✗ agent MCP server 16 running, 16 started before the current commit
171
+
172
+ To fix
173
+ → start a fresh agent session — a running one keeps the server it launched
174
+ ```
175
+
176
+ It exits non-zero when something is wrong, so it can go in a shell prompt or a
177
+ pre-flight check. It makes no network calls and changes nothing.
178
+
179
+ ### If you use herdr
180
+
181
+ ```bash
182
+ spiega install herdr
183
+ ```
184
+
185
+ Then bind a key, in `~/.config/herdr/config.toml`:
186
+
187
+ ```toml
188
+ [[keys.command]]
189
+ key = "prefix+alt+h"
190
+ type = "plugin_action"
191
+ command = "spiega.board.focus"
192
+ ```
193
+
194
+ `herdr server reload-config`, and that key brings the board for whatever pane
195
+ you are in to the front — starting one if there is none.
196
+
197
+ ## Use it yourself
198
+
199
+ ```bash
200
+ spiega draw '{"op":"title","text":"Placing an order"}' # starts a board, prints its URL
201
+ spiega draw '{"op":"node","id":"gw","kind":"gateway","label":"API Gateway","sub":"authn"}'
202
+ spiega draw '{"op":"node","id":"orders","kind":"service","label":"Orders"}'
203
+ spiega draw '{"op":"edge","from":"gw","to":"orders","label":"gRPC"}'
204
+ spiega say "The gateway does **authn** and rate limiting, then hands off."
205
+ ```
206
+
207
+ Give the board a window of its own — chromeless, no tabs, its own Dock entry,
208
+ so it sits beside your terminal like a native panel:
209
+
210
+ ```bash
211
+ spiega open --size 1200x900 --pos 60,60
212
+ ```
213
+
214
+ Render a whole spec with no server, or keep what is on the board:
215
+
216
+ ```bash
217
+ spiega render examples/order-flow.json -o order-flow.html
218
+ spiega snapshot -o docs/board.html
219
+ ```
220
+
221
+ `spiega help` lists the rest.
222
+
223
+ **Replay.** A board holds the order it was drawn in, not just its final state,
224
+ so press **replay** (or `r`) and the session plays back as it happened: the
225
+ line that was said, then the thing it was about being drawn, with the camera
226
+ moving to wherever the work is and the words captioned over the picture. On
227
+ anything complex the finished board is a wall; the replay is the explanation.
228
+ It ends on a wide shot of the whole thing.
229
+
230
+ The log lives on the board, so a replay can also be asked for from outside —
231
+ `spiega replay`, or `board_replay` over MCP. An agent that has just finished
232
+ an explanation can offer to play it back without thinking the drawing out a
233
+ second time, and nothing is re-sent: it is the same session, run again.
234
+
235
+ **Clicking the script.** A line tagged with a step is a way into that step:
236
+ click it and the board walks there, so you can read a sentence and go straight
237
+ to the hop it describes rather than hunting for the number. Clicking the step
238
+ already lit clears it, so one gesture goes both ways. Lines tied to no step are
239
+ not clickable and do not pretend to be.
240
+
241
+ **Keys on the board:** `←` `→` walk the flow, `space` plays it, `r` replays the
242
+ session, `+` `-` zoom a step at a time, `0` or `f` fits, `esc` stops.
243
+
244
+ **Where to put the board.** `spiega open` gives it a real window and is the
245
+ right answer for anything interactive. A terminal pane (`terminal-browser
246
+ open "$(spiega url)"`) is good for *glancing* while you work and for stepping
247
+ a flow with the arrow keys — those repaint once per keypress. It is poor at
248
+ continuous zoom and dragging, because every frame has to be encoded and pushed
249
+ through the TTY however fast the page itself is. Both can watch the same board
250
+ at once.
251
+
252
+ ## One board per conversation
253
+
254
+ A board belongs to an explanation, and people hold more than one at a time. Two
255
+ agents drawing on the same board interleave two explanations into one picture,
256
+ which is worse than either having none — so boards have names.
257
+
258
+ ```bash
259
+ spiega boards # every board, and which one this shell uses
260
+ spiega focus # bring this session's board window forward
261
+ spiega --board api draw '...' # or SPIEGA_BOARD=api
262
+ spiega stop # drop this board; the others keep drawing
263
+ spiega stop --all # all of them, and the server with them
264
+ ```
265
+
266
+ A board names itself after the task: the working directory's own name, which
267
+ for a worktree is the thing you called it — `domain-modeling-for-rights`, not a
268
+ uuid. A session keeps the board it already has, matched on whichever identity
269
+ its harness exposes (the conversation id, else the terminal pane, else the
270
+ directory). Two conversations about one repo get `name` and `name-2`, and
271
+ `spiega boards` prints each board's directory so they can be told apart.
272
+
273
+ Not per harness, per task: four opencode sessions and four Claude Code sessions
274
+ at once is the normal case, and a board per harness would put four
275
+ explanations on one drawing. Note that an MCP server started by a long-lived
276
+ daemon inherits that daemon's environment, so the CLI — which runs in the pane
277
+ — resolves the session correctly where MCP may not.
278
+
279
+ A board is registered by the process serving it and pruned when that process is
280
+ gone, so a name never resolves to a socket nobody is listening on.
281
+
282
+ ## One server, all the boards
283
+
284
+ They all live in one process, on port 4777, each under `/b/<name>`. It used to
285
+ be a process and a port per board, and the arithmetic was indefensible: the
286
+ Node runtime is 55-70 MB and a board with nine boxes is a few KB of state, so
287
+ eight parallel tasks spent ~600 MB holding a few hundred KB of drawing.
288
+
289
+ The second reason is staleness, and it is the one that actually bit. A server
290
+ per board meant a board started before an update kept serving the old page for
291
+ as long as it lived — one of them 404'd on a route the code on disk had — while
292
+ the isolation that a process per board bought had never once been needed.
293
+
294
+ So: one port, no scanning upward, one copy of the renderer served to every
295
+ board, and every board running the code that is on disk. Starting a board is a
296
+ map entry on a process that is almost certainly already up; the port itself is
297
+ the lock, so two agents drawing at the same moment do not end up with two
298
+ servers. `spiega stop` drops one board and leaves the rest drawing;
299
+ `spiega stop --server` ends the process and everything on it.
300
+
301
+ `spiega serve --solo` gives a board its own process and its own port if an
302
+ isolation case ever turns up. Clients cannot tell the difference: the url shape
303
+ is the same either way, and `spiega boards` says which boards are on their own.
304
+
305
+ If you run your sessions in [herdr](https://herdr.dev), `spiega install herdr`
306
+ links a plugin that goes from the pane you are working in to that task's board —
307
+ bind a key to **spiega: show this task's board**. It reads the pane's working
308
+ directory rather than the conversation, so it needs nothing from the agent.
309
+ See [`integrations/herdr/`](integrations/herdr/).
310
+
311
+ ## Compare how models use it
312
+
313
+ ```bash
314
+ spiega bench --list # models you can reach right now
315
+ spiega bench "how a write-through cache works" \
316
+ --models github-copilot/claude-opus-5.5,github-copilot/gpt-6-sol
317
+ ```
318
+
319
+ Runs the same brief through each model via `opencode run`, parses the ops out of
320
+ whatever it replied with, renders each board, and writes `bench/index.html`
321
+ comparing them side by side — boxes, lines, steps, how many edges it bothered to
322
+ label, how many kinds it reached for. Each model's raw reply is kept next to its
323
+ board. Runs are isolated and never touch a live board.
324
+
325
+
326
+
327
+ A Claude skill, a Cursor rule and an MCP server, all driving the same board —
328
+ see [`integrations/`](integrations/). The agent needs no new vocabulary: it
329
+ emits small JSON ops as it talks.
330
+
331
+ Every op, every node kind and every layout hint is in
332
+ [`docs/vocabulary.md`](docs/vocabulary.md), along with what the board does
333
+ *not* model — worth reading before asking for a sequence diagram. The
334
+ instructions the agent itself reads are in [`docs/skill.md`](docs/skill.md).
335
+
336
+ ```jsonc
337
+ {"op":"node","id":"db","kind":"datastore","label":"Postgres","sub":"orders"}
338
+ {"op":"edge","from":"orders","to":"bus","label":"publish","async":true}
339
+ {"op":"note","label":"idempotency key\nlives 24h"}
340
+ {"op":"boundary","label":"our VPC","members":["gw","orders","db"]}
341
+ {"op":"step","edge":"gw>orders","step":"Gateway authenticates and forwards"}
342
+ {"op":"say","text":"The gateway checks the token first.","step":2}
343
+ ```
344
+
345
+ A board has two halves, and a `say` can name the step it explains. Walking the
346
+ flow then lights the matching lines of the script while the others recede, so
347
+ the words and the picture move together. Lines about the board as a whole carry
348
+ no step and never dim; a line said while a step was lit is attributed for free.
349
+ Nothing is inferred beyond that — an untagged script does not highlight rather
350
+ than highlighting the wrong thing.
351
+
352
+ Which lines went with which step is often clear only afterwards, so tags can be
353
+ set on lines already said. The board keeps them; nothing is drawn again.
354
+
355
+ ```bash
356
+ spiega script # the lines, numbered, and what each is tied to
357
+ spiega tag --match "Flyway V25" --step 6
358
+ spiega tag 1 --step off # an opening remark belongs to no step
359
+ ```
360
+
361
+ The rail shows what was said, in the order it was said. So a script narrated
362
+ topic by topic, with each line tagged by whichever step it touches, comes out
363
+ `2, 1, 3, 3, 4, 3, 6` — which looks like a sort gone wrong, though nothing
364
+ sorted anything. `spiega script` says so when it happens, because from the
365
+ board you cannot tell the difference between that and a bug.
366
+
367
+ Nothing is ever placed by hand. Columns are derived from the edges — a node
368
+ sits one column right of whatever points at it — so the agent draws a box, then
369
+ the arrow into it, and the layout follows.
370
+
371
+ ## How it is put together
372
+
373
+ | | |
374
+ |---|---|
375
+ | `src/render/ink.js` | seeded wobble: the geometry that makes a line look drawn |
376
+ | `src/render/glyphs.js` | one drawing per kind — service, cylinder, queue, cloud, note |
377
+ | `src/render/edges.js` | gutter routing, fan-out anchors, numbered tags |
378
+ | `src/render/layout.js` | columns from the flow, order-stable so nothing jumps |
379
+ | `src/board/state.js` | the board as an op log |
380
+ | `src/board/server.js` | one process, every board; HTTP + SSE, and the renderer as plain ES modules |
381
+ | `src/board/board.html` | the pane: transcript rail, step strip, pan and zoom |
382
+ | `src/board/draw-on.js` | the hand: variable pen speed, uneven pauses, ink then colour |
383
+ | `src/mcp/server.js` | the same board as MCP tools |
384
+
385
+ Two separate things make it look hand-drawn, and both are needed. The geometry
386
+ is off true — `waver()` in `ink.js` pushes every point around a seeded wobble,
387
+ so a line bows rather than buzzing. And the *motion* is off true: a stroke is
388
+ drawn with keyframes whose progress runs ahead of and behind the clock, so the
389
+ pen hurries down a straight and slows coming round a corner, strokes start
390
+ before the one before has finished, and the colour only goes on once the
391
+ outline is closed. A constant-speed dashoffset tween reads as a machine tracing
392
+ a path, which is exactly what it is.
393
+
394
+ The wobble lives in the path data rather than in an SVG filter, so the board can
395
+ animate without recomputing a filter every frame. Snapshots, which are rendered
396
+ once and never animate, do get the full ink-and-paper filter on top.
397
+
398
+ ## Credits
399
+
400
+ The look — warm paper, flat three-band bodies, the cobalt cast shadow, the
401
+ ink-and-grain filter — follows [Colorfigures](https://april-zhu.com/colorfigures)
402
+ by April Zhu (MIT), which is built on ISO-GLOW by April Zhu and
403
+ [iso-figure](https://github.com/MrBongoC/ai-iso-skill) by Tolga Cohce, both MIT.
404
+ Colorfigures draws machines in isometric 3D; spiega borrows its palette and
405
+ its hand, and points them at software architecture in 2D.
406
+
407
+ ## License
408
+
409
+ MIT.
package/bin/spiega ADDED
@@ -0,0 +1,155 @@
1
+ #!/bin/sh
2
+ # spiega — a launcher, not a node script.
3
+ #
4
+ # `#!/usr/bin/env node` is a lottery on a machine with nvm: the shebang resolves
5
+ # `node` from the caller's PATH, and an agent's non-interactive shell may find a
6
+ # brew node missing its dylibs or a node from 2016. So this resolves a node that
7
+ # is new enough for the engines floor, preferring the one `spiega install` ran
8
+ # under, and execs the real CLI with it. It is /bin/sh so it runs before any
9
+ # node does.
10
+ #
11
+ # The guesses below are nvm-shaped, which is no use to a node from brew, asdf,
12
+ # volta or mise. The record is what makes the first run after install work
13
+ # there: install only gets that far on a node the engines guard has accepted.
14
+ #
15
+ # The one thing it must never do is guess silently: if no usable node is found,
16
+ # it says so in one sentence and exits non-zero.
17
+
18
+ set -u
19
+
20
+ # Resolve this script through any symlinks. npm and `spiega install` both link
21
+ # to it, so $0 is usually the link and not the file next to src/.
22
+ target=$0
23
+ while [ -h "$target" ]; do
24
+ dir=$(CDPATH= cd -- "$(dirname -- "$target")" && pwd -P)
25
+ link=$(readlink "$target")
26
+ case $link in
27
+ /*) target=$link ;;
28
+ *) target=$dir/$link ;;
29
+ esac
30
+ done
31
+ root=$(CDPATH= cd -- "$(dirname -- "$target")/.." && pwd -P)
32
+ cli=$root/src/cli.js
33
+
34
+ # The floor lives in package.json; this only repeats it if the read fails.
35
+ floor=$(sed -n 's/.*"node"[[:space:]]*:[[:space:]]*">=[[:space:]]*\([0-9][0-9]*\).*/\1/p' \
36
+ "$root/package.json" 2>/dev/null | head -n 1)
37
+ [ -n "${floor:-}" ] || floor=20
38
+
39
+ # A node is usable only if it actually runs and clears the floor. A binary that
40
+ # exists but cannot load its libraries fails here rather than inside the CLI.
41
+ usable() {
42
+ [ -n "${1:-}" ] && [ -x "$1" ] || return 1
43
+ v=$("$1" -p 'process.versions.node' 2>/dev/null) || return 1
44
+ [ -n "$v" ] || return 1
45
+ major=${v%%.*}
46
+ case $major in ''|*[!0-9]*) return 1 ;; esac
47
+ [ "$major" -ge "$floor" ]
48
+ }
49
+
50
+ nvm=${NVM_DIR:-$HOME/.nvm}
51
+
52
+ # The node `spiega install` was running under when it wrote this install. It
53
+ # lives in the state directory rather than in this file, because this is a
54
+ # tracked file: an install that wrote into it would dirty the checkout, and a
55
+ # `git pull` would either revert it or refuse. It is per HOME, so a checkout
56
+ # shared between machines keeps one answer each.
57
+ #
58
+ # A recorded path can be gone — a deleted nvm version, an upgraded brew. It is
59
+ # a candidate like any other and goes through usable() below, so staleness
60
+ # costs a shell call, not the run.
61
+ record_file=$HOME/.spiega/node
62
+ recorded=''
63
+ if [ -f "$record_file" ]; then
64
+ IFS= read -r recorded < "$record_file" 2>/dev/null || true
65
+ fi
66
+
67
+ # nvm's default alias, followed through links like lts/* and node. The alias
68
+ # file may name a version (`26.10.0`, `v26.10.0`) or another alias.
69
+ from_alias() {
70
+ a=$1
71
+ i=0
72
+ while [ "$i" -lt 5 ] && [ -f "$nvm/alias/$a" ]; do
73
+ a=$(cat "$nvm/alias/$a" 2>/dev/null) || return 1
74
+ i=$((i + 1))
75
+ done
76
+ case $a in
77
+ v[0-9]*) printf '%s\n' "$nvm/versions/node/$a/bin/node" ;;
78
+ [0-9]*) printf '%s\n' "$nvm/versions/node/v$a/bin/node" ;;
79
+ esac
80
+ }
81
+
82
+ node=''
83
+ # Which rung answered, so `spiega doctor` can say why rather than only which.
84
+ via=''
85
+
86
+ # 1. An explicit choice, for a machine whose right node is not nvm's default.
87
+ if [ -n "${SPIEGA_NODE:-}" ] && usable "$SPIEGA_NODE"; then
88
+ node=$SPIEGA_NODE
89
+ via=override
90
+ fi
91
+
92
+ # 2. The node that installed this, remembered because install ran under a node
93
+ # it had already checked against the floor. This is the rung that works
94
+ # when the node came from somewhere the guesses below have never heard of.
95
+ if [ -z "$node" ] && [ -n "$recorded" ] && usable "$recorded"; then
96
+ node=$recorded
97
+ via=recorded
98
+ fi
99
+
100
+ # 3. What nvm calls the default — the node someone actually uses in a terminal.
101
+ if [ -z "$node" ]; then
102
+ alias_node=$(from_alias default 2>/dev/null) || alias_node=''
103
+ if [ -n "$alias_node" ] && usable "$alias_node"; then
104
+ node=$alias_node
105
+ via=nvm-default
106
+ fi
107
+ fi
108
+
109
+ # 4. Any installed nvm version that clears the floor, newest first.
110
+ if [ -z "$node" ] && [ -d "$nvm/versions/node" ]; then
111
+ while IFS= read -r candidate; do
112
+ if usable "$candidate"; then node=$candidate; via=nvm-any; break; fi
113
+ done <<EOF
114
+ $(ls -1d "$nvm"/versions/node/*/bin/node 2>/dev/null | sort -r)
115
+ EOF
116
+ fi
117
+
118
+ # 5. Whatever the PATH has, if it is usable.
119
+ if [ -z "$node" ]; then
120
+ candidate=$(command -v node 2>/dev/null) || candidate=''
121
+ if [ -n "$candidate" ] && usable "$candidate"; then node=$candidate; via=path; fi
122
+ fi
123
+
124
+ # 6. The usual places, for a shell whose PATH is as bare as an agent's.
125
+ if [ -z "$node" ]; then
126
+ for candidate in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
127
+ if usable "$candidate"; then node=$candidate; via=known; break; fi
128
+ done
129
+ fi
130
+
131
+ if [ -z "$node" ]; then
132
+ echo "spiega needs Node $floor or newer and found no node on this machine that can run it; install one with nvm (nvm install --lts) or point SPIEGA_NODE at it." >&2
133
+ exit 1
134
+ fi
135
+
136
+ # `spiega doctor` asks the launcher which node an agent would really get,
137
+ # because the node on PATH and the node nvm calls default are not the same
138
+ # thing, and only the second one runs this.
139
+ if [ "${SPIEGA_NODE_VERSION:-}" = "1" ]; then
140
+ printf '%s %s %s\n' "$node" "$("$node" -p 'process.versions.node')" "$via"
141
+ # The record is a second answer to a second question: which node was
142
+ # installed against, and is it still there? It can be stale while the
143
+ # launcher is fine, so doctor cannot infer it from the node above. No line
144
+ # at all means there is no record; that is not the same as a broken one.
145
+ if [ -n "$recorded" ]; then
146
+ if usable "$recorded"; then
147
+ printf 'recorded %s ok\n' "$recorded"
148
+ else
149
+ printf 'recorded %s stale\n' "$recorded"
150
+ fi
151
+ fi
152
+ exit 0
153
+ fi
154
+
155
+ exec "$node" "$cli" "$@"
package/docs/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # docs
2
+
3
+ | page | what is in it |
4
+ |---|---|
5
+ | [skill.md](skill.md) | the instructions an agent reads, and the three routes they travel |
6
+ | [vocabulary.md](vocabulary.md) | every op, kind and layout hint — and what the board does not model |
7
+ | [shapes.md](shapes.md) | what each of them is drawn as, rendered from the renderer itself |
8
+
9
+ The README covers installing and using spiega. These cover steering it.
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="31 31 138 138" role="img" aria-label="Spiega"><rect x="31" y="31" width="138" height="138" rx="28" fill="#1E1C17"/><path d="M139.9 52.5 A62 62 0 1 1 49.2 135.6" fill="none" stroke="#C4823A" stroke-width="14" stroke-linecap="round"/><path d="M41.0 119.2 A62 62 0 0 1 110.8 38.9" fill="none" stroke="#76AE62" stroke-width="14" stroke-linecap="round"/><path d="M142.2 80.4 L142.0 79.9 L141.9 79.4 L141.8 78.9 L141.7 78.3 L141.6 77.8 L141.5 77.2 L141.4 76.6 L141.3 76.1 L141.3 75.5 L141.2 74.9 L141.1 74.3 L141.0 73.7 L140.9 73.1 L140.9 72.5 L140.8 71.9 L140.7 71.3 L140.6 70.7 L140.5 70.0 L140.4 69.4 L140.3 68.8 L140.2 68.1 L140.1 67.5 L140.0 66.8 L139.9 66.2 L139.8 65.5 L139.6 64.9 L139.5 64.2 L139.4 63.5 L139.2 62.8 L139.1 62.2 L138.9 61.5 L138.8 60.8 L138.6 60.1 L138.4 59.4 L138.2 58.7 L138.0 58.0 L137.9 57.3 L137.6 56.6 L137.4 55.8 L137.2 55.1 L137.0 54.4 L136.8 53.7 L136.5 52.9 L136.3 52.2 L136.0 51.4 L135.7 50.7 L135.4 50.0 L135.1 49.2 L134.8 48.5 L134.5 47.7 L134.2 46.9 L133.9 46.2 L133.5 45.4 L133.2 44.6 L132.8 43.9 L132.5 43.1 L132.1 42.3 L131.7 41.6 L131.3 40.8 L130.9 40.0 L121.5 47.6 L122.1 48.1 L122.7 48.5 L123.3 49.0 L123.9 49.5 L124.4 50.0 L124.9 50.5 L125.5 51.0 L126.0 51.5 L126.5 52.0 L127.0 52.5 L127.5 53.0 L128.0 53.6 L128.5 54.1 L128.9 54.6 L129.4 55.2 L129.9 55.7 L130.3 56.2 L130.7 56.8 L131.2 57.3 L131.6 57.9 L132.0 58.5 L132.4 59.0 L132.7 59.6 L133.1 60.2 L133.5 60.7 L133.8 61.3 L134.2 61.9 L134.5 62.4 L134.9 63.0 L135.2 63.6 L135.5 64.2 L135.8 64.8 L136.1 65.3 L136.4 65.9 L136.6 66.5 L136.9 67.1 L137.2 67.7 L137.4 68.3 L137.6 68.8 L137.9 69.4 L138.1 70.0 L138.3 70.6 L138.5 71.2 L138.7 71.7 L138.9 72.3 L139.1 72.9 L139.3 73.5 L139.4 74.0 L139.6 74.6 L139.8 75.2 L139.9 75.7 L140.1 76.3 L140.2 76.9 L140.4 77.4 L140.5 78.0 L140.6 78.5 L140.8 79.1 L140.9 79.6 L141.0 80.2 L141.2 80.7 Z" fill="#B52D6A"/><circle cx="126.2" cy="43.8" r="10" fill="#B52D6A"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="31 31 138 138" role="img" aria-label="Spiega"><path d="M139.9 52.5 A62 62 0 1 1 49.2 135.6" fill="none" stroke="#C4823A" stroke-width="14" stroke-linecap="round"/><path d="M41.0 119.2 A62 62 0 0 1 110.8 38.9" fill="none" stroke="#76AE62" stroke-width="14" stroke-linecap="round"/><path d="M142.2 80.4 L142.0 79.9 L141.9 79.4 L141.8 78.9 L141.7 78.3 L141.6 77.8 L141.5 77.2 L141.4 76.6 L141.3 76.1 L141.3 75.5 L141.2 74.9 L141.1 74.3 L141.0 73.7 L140.9 73.1 L140.9 72.5 L140.8 71.9 L140.7 71.3 L140.6 70.7 L140.5 70.0 L140.4 69.4 L140.3 68.8 L140.2 68.1 L140.1 67.5 L140.0 66.8 L139.9 66.2 L139.8 65.5 L139.6 64.9 L139.5 64.2 L139.4 63.5 L139.2 62.8 L139.1 62.2 L138.9 61.5 L138.8 60.8 L138.6 60.1 L138.4 59.4 L138.2 58.7 L138.0 58.0 L137.9 57.3 L137.6 56.6 L137.4 55.8 L137.2 55.1 L137.0 54.4 L136.8 53.7 L136.5 52.9 L136.3 52.2 L136.0 51.4 L135.7 50.7 L135.4 50.0 L135.1 49.2 L134.8 48.5 L134.5 47.7 L134.2 46.9 L133.9 46.2 L133.5 45.4 L133.2 44.6 L132.8 43.9 L132.5 43.1 L132.1 42.3 L131.7 41.6 L131.3 40.8 L130.9 40.0 L121.5 47.6 L122.1 48.1 L122.7 48.5 L123.3 49.0 L123.9 49.5 L124.4 50.0 L124.9 50.5 L125.5 51.0 L126.0 51.5 L126.5 52.0 L127.0 52.5 L127.5 53.0 L128.0 53.6 L128.5 54.1 L128.9 54.6 L129.4 55.2 L129.9 55.7 L130.3 56.2 L130.7 56.8 L131.2 57.3 L131.6 57.9 L132.0 58.5 L132.4 59.0 L132.7 59.6 L133.1 60.2 L133.5 60.7 L133.8 61.3 L134.2 61.9 L134.5 62.4 L134.9 63.0 L135.2 63.6 L135.5 64.2 L135.8 64.8 L136.1 65.3 L136.4 65.9 L136.6 66.5 L136.9 67.1 L137.2 67.7 L137.4 68.3 L137.6 68.8 L137.9 69.4 L138.1 70.0 L138.3 70.6 L138.5 71.2 L138.7 71.7 L138.9 72.3 L139.1 72.9 L139.3 73.5 L139.4 74.0 L139.6 74.6 L139.8 75.2 L139.9 75.7 L140.1 76.3 L140.2 76.9 L140.4 77.4 L140.5 78.0 L140.6 78.5 L140.8 79.1 L140.9 79.6 L141.0 80.2 L141.2 80.7 Z" fill="#B52D6A"/><circle cx="126.2" cy="43.8" r="10" fill="#B52D6A"/></svg>