@junoflow/cli 0.3.0 → 0.6.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 +579 -54
- package/bin/juno.js +7 -0
- package/eslint.flow.config.js +33 -0
- package/package.json +7 -4
- package/src/api.js +395 -27
- package/src/authoring-skills.js +42 -0
- package/src/commands/agent-skill-install.js +64 -0
- package/src/commands/ask.js +60 -0
- package/src/commands/check.js +44 -16
- package/src/commands/config.js +26 -0
- package/src/commands/credential-set.js +68 -0
- package/src/commands/deploy.js +41 -50
- package/src/commands/flow-check.js +93 -0
- package/src/commands/flow-deploy.js +107 -0
- package/src/commands/flow-gen-types.js +30 -0
- package/src/commands/flow-init.js +59 -0
- package/src/commands/flow-pull.js +86 -0
- package/src/commands/{types.js → gen-types.js} +12 -5
- package/src/commands/init.js +28 -16
- package/src/commands/pull.js +70 -16
- package/src/commands/session-approve.js +73 -0
- package/src/commands/skill.js +73 -0
- package/src/commands/tool.js +249 -0
- package/src/commands/verify-types.js +4 -2
- package/src/config.js +34 -0
- package/src/diagnostics.js +14 -0
- package/src/errors.js +40 -0
- package/src/exit.js +22 -0
- package/src/flow-project.js +128 -0
- package/src/help.js +199 -0
- package/src/index.js +506 -75
- package/src/installed-skill.js +55 -0
- package/src/prompt.js +196 -0
- package/src/stamp.js +10 -9
- package/src/suggest.js +67 -0
- package/src/target.js +22 -7
- package/src/verbs.js +34 -0
- package/src/watch.js +50 -0
- package/verbs.json +227 -0
package/README.md
CHANGED
|
@@ -1,52 +1,575 @@
|
|
|
1
1
|
# @junoflow/cli
|
|
2
2
|
|
|
3
|
-
Develop [Juno](https://github.com/boivie/juno)
|
|
4
|
-
|
|
3
|
+
Develop [Juno](https://github.com/boivie/juno) flows and integrations, and
|
|
4
|
+
operate a Juno instance, from the command line.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
-
npm install --save-dev @junoflow/cli
|
|
8
|
-
|
|
7
|
+
npm install --save-dev @junoflow/cli # in a flow or integration working copy
|
|
8
|
+
npm install --global @junoflow/cli # to operate an instance from anywhere
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
fetches it at a tag, compiles it, and runs it in a sandbox. There is no
|
|
13
|
-
packaging format and nothing is published to npm.
|
|
11
|
+
The package installs one command, `juno`, with two sets of commands:
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
- **Operating an instance**: `juno <group> <verb> [id]` puts a named command in
|
|
14
|
+
front of every tool Juno's in-app assistant uses, so a coding agent (Claude
|
|
15
|
+
Code and similar), a shell script or a person can find things and act on them.
|
|
16
|
+
`juno ask` talks to the assistant itself.
|
|
17
|
+
- **Working copies**: `juno flow init`, `juno integration init` and the commands
|
|
18
|
+
beside them write a flow or an integration to disk as files, check it, and
|
|
19
|
+
deploy it. See [Working copies](#working-copies).
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| `juno init <name>` | fetch a starting project from a Juno and write it down |
|
|
20
|
-
| `juno types` | POST `manifest.json`, write `types/` |
|
|
21
|
-
| `juno verify-types` | check `types/` still matches `manifest.json` — talks to nothing |
|
|
22
|
-
| `juno check` | local `verify-types` + `tsc --noEmit` + `eslint`, then validate against a Juno |
|
|
23
|
-
| `juno deploy [--watch]` | upload, verify, activate — and write back the returned types |
|
|
24
|
-
| `juno pull` | write back the manifest and sources the instance holds |
|
|
21
|
+
Juno has **no authentication on any endpoint**. An instance is a trusted-LAN
|
|
22
|
+
appliance; do not expose one to the internet.
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
## Operating an instance
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
juno config set url juno.local:8080 # once, outside a project
|
|
28
|
+
juno discover porch --kind state
|
|
29
|
+
juno state get porch-dark --jq .value
|
|
30
|
+
juno related endpoint:ha:light.hallway --degree 2
|
|
31
|
+
juno state preview-layers porch-dark --input - <<'EOF'
|
|
32
|
+
{"ops":[{"op":"insert", …}]}
|
|
33
|
+
EOF
|
|
34
|
+
juno ha call-service --input '{"domain":"light","service":"turn_off"}'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The grammar is `juno <group> <verb> [id]`. The group is the kind of thing a
|
|
38
|
+
command acts on, so a `discover` hit's `{kind, id}` leads straight to
|
|
39
|
+
`juno <kind> <verb> <id>` and `juno related <kind>:<id>` (or `<kind> <id>`). A
|
|
40
|
+
verb may also be spelled with `_` (`juno state get_history`).
|
|
41
|
+
|
|
42
|
+
Every tool command sends the tool's own JSON input, and prints the tool's JSON
|
|
43
|
+
answer. The CLI holds no tool semantics: the instance validates the input,
|
|
44
|
+
writes the descriptions and classifies each command's effect.
|
|
45
|
+
|
|
46
|
+
### Help
|
|
47
|
+
|
|
48
|
+
| Command | Shows |
|
|
49
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `juno --help` | the groups and top-level commands. Offline. |
|
|
51
|
+
| `juno <group> --help` | the group's commands, each with its effect and the first sentence of its description. Offline, it lists the commands alone. |
|
|
52
|
+
| `juno <group> <verb> --help` | the tool's full description, its effect, its input schema, and what the positional and flags fill in. Needs the instance. |
|
|
53
|
+
|
|
54
|
+
`juno <group>` on its own prints the group's help and exits 2. An unknown
|
|
55
|
+
command suggests the closest one: `juno stat get` answers
|
|
56
|
+
`did you mean "juno state get"?`.
|
|
57
|
+
|
|
58
|
+
### Effects
|
|
59
|
+
|
|
60
|
+
Every command has one effect:
|
|
61
|
+
|
|
62
|
+
- **read** changes nothing.
|
|
63
|
+
- **change** writes stored configuration that keeps a version timeline, so it
|
|
64
|
+
can be reverted.
|
|
65
|
+
- **act** does something now (calls a service, presses a button, sets a value,
|
|
66
|
+
starts a run, executes code), with nothing to roll back to.
|
|
67
|
+
|
|
68
|
+
`config set` and `agent-skill install` are **local**: they write a file on your
|
|
69
|
+
machine and change nothing on the instance.
|
|
70
|
+
|
|
71
|
+
No command of a lower effect is a string prefix of a command of a higher one.
|
|
72
|
+
Claude Code matches a Bash permission rule by prefix, so an allowlist can list
|
|
73
|
+
the read commands, and optionally the change commands, and every act still
|
|
74
|
+
prompts.
|
|
75
|
+
|
|
76
|
+
### Commands
|
|
77
|
+
|
|
78
|
+
The positional fills the named input field. A dash means the command takes no
|
|
79
|
+
positional; its input is a whole document (`device write`) or has no id
|
|
80
|
+
(`run list`).
|
|
81
|
+
|
|
82
|
+
**Top level**
|
|
83
|
+
|
|
84
|
+
| Command | Tool | Positional | Effect |
|
|
85
|
+
| --------------------------------------- | ------------- | ----------------------------------- | ------ |
|
|
86
|
+
| `juno discover <query> [--kind k]…` | `discover` | `query`; `--kind` → `kinds` | read |
|
|
87
|
+
| `juno related <kind>:<id> [--degree n]` | `get_related` | `kind`, `id`; `--degree` → `degree` | read |
|
|
88
|
+
| `juno skill [<name>] [--json]` | none | skill name | read |
|
|
89
|
+
| `juno ask ["<prompt>"]` | none | the prompt | act |
|
|
90
|
+
| `juno agent-skill install` | none | none | local |
|
|
91
|
+
|
|
92
|
+
`related` splits its positional on the first `:` only, so
|
|
93
|
+
`endpoint:ha:light.hallway` is kind `endpoint`, id `ha:light.hallway`.
|
|
94
|
+
|
|
95
|
+
**`state`**
|
|
96
|
+
|
|
97
|
+
| Command | Tool | Positional | Effect |
|
|
98
|
+
| --------------------------- | ------------------------------------- | ---------- | ------ |
|
|
99
|
+
| `juno state get` | `get_state` | `id` | read |
|
|
100
|
+
| `juno state get-history` | `get_state_history` | `id` | read |
|
|
101
|
+
| `juno state get-layers` | `get_state_layers` | `id` | read |
|
|
102
|
+
| `juno state evaluate-level` | `evaluate_level` | — | read |
|
|
103
|
+
| `juno state preview-layers` | `edit_state_layers`, `dry_run: true` | `id` | read |
|
|
104
|
+
| `juno state create` | `create_state` | `id` | change |
|
|
105
|
+
| `juno state edit-layers` | `edit_state_layers`, `dry_run: false` | `id` | change |
|
|
106
|
+
| `juno state set-value` | `set_state_value` | `id` | act |
|
|
107
|
+
| `juno state claim` | `claim_state` | `id` | act |
|
|
108
|
+
| `juno state release` | `release_state` | `id` | act |
|
|
109
|
+
|
|
110
|
+
`preview-layers` and `edit-layers` call the same tool. The preview always sends
|
|
111
|
+
`dry_run: true`, so a permission rule can allow the preview and not the edit; an
|
|
112
|
+
`--input` that says `"dry_run": false` is refused.
|
|
113
|
+
|
|
114
|
+
**`flow`**
|
|
115
|
+
|
|
116
|
+
| Command | Tool | Positional | Effect |
|
|
117
|
+
| ------------------------ | ------------------ | ---------- | ------ |
|
|
118
|
+
| `juno flow get` | `get_flow` | `id` | read |
|
|
119
|
+
| `juno flow set-enabled` | `set_flow_enabled` | `flow_id` | change |
|
|
120
|
+
| `juno flow trigger` | `trigger_flow` | `flow_id` | act |
|
|
121
|
+
| `juno flow fire-trigger` | `fire_trigger` | `flow_id` | act |
|
|
122
|
+
|
|
123
|
+
The group's working-copy commands (`flow init`, `gen-types`, `check`, `deploy`,
|
|
124
|
+
`pull`) are in [Flows as files](#flows-as-files).
|
|
125
|
+
|
|
126
|
+
**`run`**
|
|
127
|
+
|
|
128
|
+
| Command | Tool | Positional | Effect |
|
|
129
|
+
| --------------- | ----------- | ---------- | ------ |
|
|
130
|
+
| `juno run list` | `list_runs` | — | read |
|
|
131
|
+
| `juno run get` | `get_run` | `run_id` | read |
|
|
132
|
+
|
|
133
|
+
**`integration`**
|
|
134
|
+
|
|
135
|
+
| Command | Tool | Positional | Effect |
|
|
136
|
+
| ------------------------------- | -------------------------- | ------------- | ------ |
|
|
137
|
+
| `juno integration get` | `get_integration` | `id` | read |
|
|
138
|
+
| `juno integration configure` | `configure_integration` | `id` | change |
|
|
139
|
+
| `juno integration install` | `install_integration` | — | act |
|
|
140
|
+
| `juno integration run-schedule` | `run_integration_schedule` | `integration` | act |
|
|
141
|
+
|
|
142
|
+
The group's working-copy commands (`integration init`, `gen-types`,
|
|
143
|
+
`verify-types`, `check`, `deploy`, `pull`) are in
|
|
144
|
+
[Developing integrations](#developing-integrations).
|
|
145
|
+
|
|
146
|
+
**`device`**
|
|
147
|
+
|
|
148
|
+
| Command | Tool | Positional | Effect |
|
|
149
|
+
| --------------------- | ---------------- | ---------- | ------ |
|
|
150
|
+
| `juno device list` | `list_devices` | — | read |
|
|
151
|
+
| `juno device get` | `get_device` | `id` | read |
|
|
152
|
+
| `juno device preview` | `preview_device` | — | read |
|
|
153
|
+
| `juno device write` | `write_device` | — | change |
|
|
154
|
+
| `juno device suspend` | `suspend_device` | `id` | act |
|
|
155
|
+
| `juno device release` | `release_device` | `id` | act |
|
|
156
|
+
|
|
157
|
+
**`control`**
|
|
158
|
+
|
|
159
|
+
| Command | Tool | Positional | Effect |
|
|
160
|
+
| ---------------------- | ----------------- | ---------- | ------ |
|
|
161
|
+
| `juno control write` | `write_control` | — | change |
|
|
162
|
+
| `juno control use` | `use_control` | `id` | act |
|
|
163
|
+
| `juno control release` | `release_control` | `id` | act |
|
|
164
|
+
|
|
165
|
+
**`finding`**
|
|
166
|
+
|
|
167
|
+
| Command | Tool | Positional | Effect |
|
|
168
|
+
| ------------------ | ------------- | ---------- | ------ |
|
|
169
|
+
| `juno finding get` | `get_finding` | `slug` | read |
|
|
170
|
+
|
|
171
|
+
**`webhook`**
|
|
172
|
+
|
|
173
|
+
| Command | Tool | Positional | Effect |
|
|
174
|
+
| ----------------------------- | ------------------------ | ---------- | ------ |
|
|
175
|
+
| `juno webhook list` | `list_webhooks` | — | read |
|
|
176
|
+
| `juno webhook get-deliveries` | `get_webhook_deliveries` | `id` | read |
|
|
177
|
+
| `juno webhook write` | `write_webhook` | `id` | change |
|
|
178
|
+
| `juno webhook fire` | `fire_webhook` | `id` | act |
|
|
179
|
+
|
|
180
|
+
**`credential`**
|
|
181
|
+
|
|
182
|
+
| Command | Tool | Positional | Effect |
|
|
183
|
+
| -------------------------- | ------------------ | ------------- | ------ |
|
|
184
|
+
| `juno credential list` | `list_credentials` | — | read |
|
|
185
|
+
| `juno credential set <id>` | none | credential id | act |
|
|
186
|
+
|
|
187
|
+
**`ha`** (Home Assistant)
|
|
188
|
+
|
|
189
|
+
| Command | Tool | Positional | Effect |
|
|
190
|
+
| ---------------------------- | -------------------- | ----------- | ------ |
|
|
191
|
+
| `juno ha get-entity` | `get_entity` | `entity_id` | read |
|
|
192
|
+
| `juno ha get-entity-history` | `get_entity_history` | `entity_id` | read |
|
|
193
|
+
| `juno ha list-services` | `list_services` | — | read |
|
|
194
|
+
| `juno ha call-service` | `call_service` | — | act |
|
|
195
|
+
|
|
196
|
+
**`script`**
|
|
197
|
+
|
|
198
|
+
| Command | Tool | Positional | Effect |
|
|
199
|
+
| ------------------------------------- | ------------ | ----------------------------------------------------- | ------ |
|
|
200
|
+
| `juno script run <file> [--cred id]…` | `run_script` | `<file>` → `files`, `entry`; `--cred` → `credentials` | act |
|
|
201
|
+
|
|
202
|
+
`script run` reads the file relative to the working directory, puts its content
|
|
203
|
+
under `files` by its basename and makes it the `entry`.
|
|
204
|
+
|
|
205
|
+
**`session`** and **`config`**
|
|
206
|
+
|
|
207
|
+
| Command | Tool | Positional | Effect |
|
|
208
|
+
| --------------------------- | ---- | ----------- | ------ |
|
|
209
|
+
| `juno session approve <id>` | none | session id | act |
|
|
210
|
+
| `juno config set url <url>` | none | `url <url>` | local |
|
|
211
|
+
|
|
212
|
+
There is no command that calls a tool by name. One permission rule for such a
|
|
213
|
+
command would allow every act, so each tool has its own command instead, and a
|
|
214
|
+
tool added to the server needs a CLI release to reach.
|
|
215
|
+
|
|
216
|
+
### Input
|
|
217
|
+
|
|
218
|
+
The id, when the command has one, is the single positional argument. Everything
|
|
219
|
+
else is the tool's input JSON, given with `--input`:
|
|
220
|
+
|
|
221
|
+
| Form | Reads |
|
|
222
|
+
| ------------------ | ----------------------------------------- |
|
|
223
|
+
| `--input '<json>'` | the JSON itself |
|
|
224
|
+
| `--input -` | standard input |
|
|
225
|
+
| `--input @<path>` | a file, relative to the working directory |
|
|
226
|
+
|
|
227
|
+
The input must be a JSON object. Without `--input` it is `{}`. The CLI never
|
|
228
|
+
reads standard input unless `--input -` asks for it, because an open, empty
|
|
229
|
+
stdin would hang an agent, and it never prompts off a terminal.
|
|
230
|
+
|
|
231
|
+
The input is built in this order: the `--input` object, then the positional into
|
|
232
|
+
its field, then the command's flags, then the fixed input (`preview-layers`'
|
|
233
|
+
`dry_run`). A positional or flag that disagrees with a value already in
|
|
234
|
+
`--input` is a usage error naming both. The CLI checks nothing else: the
|
|
235
|
+
instance validates the input against the tool's schema and answers with the
|
|
236
|
+
failing field (`/id`, `/ops/0/when`, `/` for the input as a whole).
|
|
237
|
+
|
|
238
|
+
### Output
|
|
239
|
+
|
|
240
|
+
Standard output carries the tool's answer and nothing else:
|
|
241
|
+
|
|
242
|
+
- off a terminal, the instance's JSON byte for byte;
|
|
243
|
+
- on a terminal, the same JSON indented by two spaces. Never coloured.
|
|
244
|
+
|
|
245
|
+
The choice follows whether stdout is a terminal, so a script and an agent see
|
|
246
|
+
the same bytes. A tool that fails in its own terms answers `{"error": "…"}`;
|
|
247
|
+
that is printed on stdout like any answer, and the exit code is 1. Warnings,
|
|
248
|
+
progress and notes go to standard error.
|
|
249
|
+
|
|
250
|
+
`--jq <expr>` runs the answer through [`jq`](https://jqlang.org), which must be
|
|
251
|
+
on `PATH` (`jq -c` off a terminal, plain `jq` on one). A filter given this way
|
|
252
|
+
stays inside the command's own permission rule, where a pipe to `jq` would need
|
|
253
|
+
its own. A missing `jq` or an expression `jq` rejects exits 2.
|
|
254
|
+
|
|
255
|
+
### Exit codes
|
|
256
|
+
|
|
257
|
+
One table for the whole CLI, the working-copy commands included:
|
|
258
|
+
|
|
259
|
+
| Code | When |
|
|
260
|
+
| ---- | ----------------------------------------------------------------------------------------------------- |
|
|
261
|
+
| 0 | Success |
|
|
262
|
+
| 1 | A tool answer with a top-level string `error`, a failed check or deploy, or any other failure |
|
|
263
|
+
| 2 | Usage: an unknown command or flag, bad `--input`, input the instance rejects, a bad `--jq` |
|
|
264
|
+
| 3 | The instance is unreachable |
|
|
265
|
+
| 4 | `juno flow deploy` / `flow check`: the flow changed on the instance since the working copy was pulled |
|
|
266
|
+
| 5 | `juno ask` / `session approve`: the turn is still running |
|
|
267
|
+
| 6 | `juno ask` / `session approve`: the assistant is waiting for an answer or an approval |
|
|
268
|
+
|
|
269
|
+
An instance that does not serve a tool the CLI names is older than the CLI; that
|
|
270
|
+
is also exit 2, and the message says so.
|
|
271
|
+
|
|
272
|
+
### `juno ask` and `juno session approve`
|
|
273
|
+
|
|
274
|
+
`juno ask` sends a prompt to Juno's in-app assistant through the
|
|
275
|
+
[Agent Prompt API](https://boivie.github.io/juno/reference/agent-api/) and
|
|
276
|
+
prints the turn's final event. It is for scripts, chat bridges and a person at a
|
|
277
|
+
terminal. Its effect is **act**, because the assistant can do anything its tools
|
|
278
|
+
allow.
|
|
27
279
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
280
|
+
```bash
|
|
281
|
+
juno ask "Why is the porch lamp on?"
|
|
282
|
+
juno ask --session <id> "And the hallway?" # continue a conversation
|
|
283
|
+
juno ask --session <id> # keep waiting on a running turn
|
|
284
|
+
juno session approve <id> # let a pending tool call through
|
|
285
|
+
juno ask --session <id> "not while I'm away" # deny it, with a reason
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
| Option | Meaning |
|
|
289
|
+
| ------------------ | -------------------------------------------------------------------------------- |
|
|
290
|
+
| `--session <id>` | resume that session. With no prompt, observe it: wait for its turn, send nothing |
|
|
291
|
+
| `--wait <seconds>` | off a terminal, how long one call waits before answering pending (default 60) |
|
|
292
|
+
|
|
293
|
+
Off a terminal each call is one request that waits up to `--wait` seconds, and
|
|
294
|
+
the final event is printed unchanged. On a terminal the answer streams: progress
|
|
295
|
+
and the session id go to standard error, and the final event is printed
|
|
296
|
+
indented. The exit code is the outcome:
|
|
297
|
+
|
|
298
|
+
| Event | Exit |
|
|
299
|
+
| ----------------------------------------------- | ---- |
|
|
300
|
+
| `message`, or `done` with `completed` | 0 |
|
|
301
|
+
| `pending` (still running) | 5 |
|
|
302
|
+
| `awaiting_input` (question or approval) | 6 |
|
|
303
|
+
| `done` with `failed` or `abandoned`, or `error` | 1 |
|
|
304
|
+
|
|
305
|
+
So "keep waiting" is `juno ask --session <id>` until the exit code is not 5. A
|
|
306
|
+
new conversation's session id is minted by the CLI, so a retry after a dropped
|
|
307
|
+
connection resumes the same session; a failed connection is retried twice before
|
|
308
|
+
exit 3. When the session is busy with another turn, `juno ask` waits for that
|
|
309
|
+
turn to end and sends again (up to three times), unless the turn ends waiting
|
|
310
|
+
for input, since the message would then be taken as the answer.
|
|
311
|
+
|
|
312
|
+
`juno ask approve` is refused: approving is `juno session approve <id>`, a
|
|
313
|
+
separate command so that a permission rule allowing `juno ask` does not also let
|
|
314
|
+
an agent approve the assistant's `call_service` on your behalf.
|
|
315
|
+
`session approve` first checks that the session is waiting for an approval and
|
|
316
|
+
exits 1 without sending anything otherwise. Any other message sent to a session
|
|
317
|
+
waiting for an approval denies it, and the assistant sees the message as the
|
|
318
|
+
reason.
|
|
319
|
+
|
|
320
|
+
### `juno credential set`
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
juno credential set pushover_token # update an existing one
|
|
324
|
+
juno credential set pushover_token --name Pushover --description "App token"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
It reads the secret from the terminal with echo off and stores it. An existing
|
|
328
|
+
credential keeps its name and description unless `--name` or `--description`
|
|
329
|
+
replaces them; a new one needs both. It never prints the value.
|
|
330
|
+
|
|
331
|
+
It **refuses to run when standard input is not a terminal** (exit 2), so an
|
|
332
|
+
agent cannot pipe a value into it: a secret an agent types lands in its
|
|
333
|
+
transcript. A person runs it, directly or as `! juno credential set <id>` inside
|
|
334
|
+
Claude Code. For the same reason there is no tool that writes a credential.
|
|
335
|
+
|
|
336
|
+
### `juno skill`
|
|
337
|
+
|
|
338
|
+
`juno skill` lists the skills the assistant can load, one
|
|
339
|
+
`<name> — <description>` line each. `juno skill <name>` prints one skill's body.
|
|
340
|
+
`--json` prints the instance's JSON instead. The skills are the in-app
|
|
341
|
+
assistant's, rendered for a reader outside Juno: where the assistant reads a
|
|
342
|
+
tool name (`edit_state_layers`), you read the command in front of it
|
|
343
|
+
(`juno state edit-layers`), or what to do in a working copy instead
|
|
344
|
+
(`npm run deploy` for `submit_code`).
|
|
345
|
+
|
|
346
|
+
### `juno agent-skill install`
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
juno agent-skill install # .claude/skills/juno/SKILL.md
|
|
350
|
+
juno agent-skill install --dir ~/.claude/skills/juno # for every project
|
|
351
|
+
juno agent-skill install --allow-change # + every change command
|
|
352
|
+
juno agent-skill install --working-copy # + the npm scripts (run it in a working copy)
|
|
353
|
+
juno agent-skill install --agents-md >> AGENTS.md # for a harness that reads AGENTS.md
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Writes the skill that teaches a coding agent to use Juno — `SKILL.md` in the
|
|
357
|
+
Agent Skills format, rendered by the instance (`GET /api/agent-skill`) — and
|
|
358
|
+
prints a Claude Code permission block on stdout:
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
{"permissions":{"allow":["Bash(juno discover:*)","Bash(juno related:*)","Bash(juno state get:*)",…]}}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The block is generated from the command table and each command's effect. By
|
|
365
|
+
default it allows every `read` command. `--allow-change` adds every `change`
|
|
366
|
+
command (layer stacks, devices, controls, `flow deploy`, …), for when you want
|
|
367
|
+
the agent to reconfigure freely and use version history to review or undo.
|
|
368
|
+
`--working-copy` adds the working copy's npm scripts of the same tiers
|
|
369
|
+
(`npm test`, `npm run check`, and with `--allow-change` `npm run deploy`); those
|
|
370
|
+
trust whatever that `package.json` says the script runs, so put them in the
|
|
371
|
+
working copy's `.claude/settings.json`, never in user-wide settings. **An `act`
|
|
372
|
+
command is never in the block**: it always prompts. Add one by hand if you want
|
|
373
|
+
to, for example, let the agent press a test button. A command added in a later
|
|
374
|
+
version prompts until you regenerate the block.
|
|
375
|
+
|
|
376
|
+
The skill is short and stable; what changes per instance comes from `juno skill`
|
|
377
|
+
and `--help` at run time. It records the version it was rendered at, and
|
|
378
|
+
`juno skill`, `juno flow init` and `juno integration init` say on stderr when
|
|
379
|
+
the instance serves a newer one. `--agents-md` prints the same text as an
|
|
380
|
+
`AGENTS.md` section and writes nothing; `--json` prints the instance's answer.
|
|
36
381
|
|
|
37
382
|
## Choosing an instance
|
|
38
383
|
|
|
39
|
-
In order:
|
|
40
|
-
|
|
384
|
+
In order:
|
|
385
|
+
|
|
386
|
+
1. `--url <url>`, before or after the command words;
|
|
387
|
+
2. `$JUNO_URL`;
|
|
388
|
+
3. `"juno": { "target": … }` in `package.json` in the working directory;
|
|
389
|
+
4. the user config file, `$XDG_CONFIG_HOME/juno/config.json`
|
|
390
|
+
(`~/.config/juno/config.json` when `XDG_CONFIG_HOME` is unset).
|
|
391
|
+
|
|
392
|
+
A project downloaded from a Juno already has the third, so a fresh clone deploys
|
|
393
|
+
with no setup. `juno config set url <url>` writes the fourth, which is what
|
|
394
|
+
makes `juno` useful outside a project. A bare host such as `juno.local:8080`
|
|
395
|
+
means `http://juno.local:8080`.
|
|
396
|
+
|
|
397
|
+
Every request carries an `X-Juno-Client` header: `cli`, or `cli/claude-code`
|
|
398
|
+
when `CLAUDECODE=1` is set, as Claude Code sets it in the shells it starts. Juno
|
|
399
|
+
records a write made through a tool as `operator:<client>`. The header names who
|
|
400
|
+
made a change; it is not a security control.
|
|
401
|
+
|
|
402
|
+
## Working copies
|
|
403
|
+
|
|
404
|
+
A working copy is a directory on your machine holding one flow or one
|
|
405
|
+
integration as files. Juno keeps every version, so the instance is the source of
|
|
406
|
+
truth and a working copy is where you edit the next version. Its `package.json`
|
|
407
|
+
names the instance (`juno.target`) and the flow or install it belongs to, so a
|
|
41
408
|
fresh clone deploys with no setup.
|
|
42
409
|
|
|
43
|
-
|
|
44
|
-
|
|
410
|
+
Both kinds have the same loop and the same npm scripts:
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
npm install
|
|
414
|
+
npm test # local tsc and eslint
|
|
415
|
+
npm run check # ... then verify against the instance, which writes nothing
|
|
416
|
+
npm run deploy # verify, commit and activate the next version
|
|
417
|
+
npm run pull # overwrite the working copy with the active version
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
| Script | Flow working copy | Integration working copy |
|
|
421
|
+
| -------------- | ----------------------------------- | ------------------------------------------------------------------------- |
|
|
422
|
+
| `typecheck` | `tsc --noEmit` | `tsc --noEmit` |
|
|
423
|
+
| `lint` | `eslint .` | `eslint .` |
|
|
424
|
+
| `verify-types` | (absent) | `juno integration verify-types` |
|
|
425
|
+
| `test` | `npm run typecheck && npm run lint` | `npm run verify-types && npm run typecheck && npm run lint && vitest run` |
|
|
426
|
+
| `types` | `juno flow gen-types` | `juno integration gen-types` |
|
|
427
|
+
| `check` | `juno flow check` | `juno integration check` |
|
|
428
|
+
| `deploy` | `juno flow deploy` | `juno integration deploy` |
|
|
429
|
+
| `watch` | `juno flow deploy --watch` | `juno integration deploy --watch` |
|
|
430
|
+
| `pull` | `juno flow pull` | `juno integration pull` |
|
|
431
|
+
|
|
432
|
+
The instance writes these lines, and `pull` rewrites them, so a working copy
|
|
433
|
+
picks up a new command without your editing `package.json`.
|
|
434
|
+
|
|
435
|
+
The working-copy commands sit in the `flow` and `integration` groups beside the
|
|
436
|
+
commands that act on the instance, and `juno flow --help` and
|
|
437
|
+
`juno integration --help` list them under their own heading. Every one except
|
|
438
|
+
`init` acts on the working copy in the current directory and refuses an
|
|
439
|
+
argument: `juno flow deploy porch-light` exits 2 and tells you to run
|
|
440
|
+
`juno flow init porch-light` and deploy from inside it.
|
|
441
|
+
|
|
442
|
+
| Effect | Commands |
|
|
443
|
+
| -------- | -------------------------------------------------------------- |
|
|
444
|
+
| `read` | `init`, `gen-types`, `verify-types`, `check`, `pull` |
|
|
445
|
+
| `change` | `deploy`: it adds a version to the flow's or install's history |
|
|
446
|
+
|
|
447
|
+
`init` and `pull` write local files, but change nothing in Juno, so their effect
|
|
448
|
+
is `read`.
|
|
449
|
+
|
|
450
|
+
After writing a working copy, `init` prints the skills for writing that kind,
|
|
451
|
+
one `<name> — <description>` line each, the same index Juno's in-app flow or
|
|
452
|
+
integration author is given; load one with `juno skill <name>`.
|
|
453
|
+
`juno flow --help` and `juno integration --help` list them too. `pull` refuses
|
|
454
|
+
to overwrite uncommitted changes in a git working tree unless you pass
|
|
455
|
+
`--force`.
|
|
456
|
+
|
|
457
|
+
## Flows as files
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
juno flow init porch-light # the flow's active version, or a stub for a new id
|
|
461
|
+
cd porch-light && npm install
|
|
462
|
+
$EDITOR flow.ts manifest.json
|
|
463
|
+
npm test
|
|
464
|
+
npm run check
|
|
465
|
+
npm run deploy
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`juno flow init <id>` writes `./<id>` (or the current directory, with `--here`).
|
|
469
|
+
For a flow that has an active version, it is that version. For a new id it is a
|
|
470
|
+
stub whose only trigger is a `call` trigger, so a stub deployed by accident
|
|
471
|
+
never fires on its own. A working copy is flat:
|
|
472
|
+
|
|
473
|
+
| File | What it is |
|
|
474
|
+
| ------------------ | ---------------------------------------------------------------------------------- |
|
|
475
|
+
| `manifest.json` | the flow's manifest |
|
|
476
|
+
| `flow.ts` | the flow's source |
|
|
477
|
+
| `flow-env.d.ts` | types generated from the manifest and the instance's states, integrations, etc. |
|
|
478
|
+
| `flow-api.d.ts` | the SDK's declarations; it and `flow-env.d.ts` only typecheck together |
|
|
479
|
+
| `tsconfig.json` | extends `@junoflow/cli/tsconfig.base.json` |
|
|
480
|
+
| `eslint.config.js` | `@junoflow/cli/eslint.flow.config.js`: the rules Juno's verify step lints flows by |
|
|
481
|
+
| `package.json` | the scripts, the dependencies, and `juno.target` / `flow_id` / `base_version` |
|
|
482
|
+
| `.gitignore` | `node_modules/` |
|
|
483
|
+
|
|
484
|
+
| Command | What it does |
|
|
485
|
+
| ----------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
486
|
+
| `juno flow init <id> [--here]` | write a working copy from the instance |
|
|
487
|
+
| `juno flow gen-types` | regenerate `flow-env.d.ts` and `flow-api.d.ts` from `manifest.json` |
|
|
488
|
+
| `juno flow check [--skip-local] [--json]` | local `tsc --noEmit` and `eslint .`, then verify against the instance, writing nothing |
|
|
489
|
+
| `juno flow deploy [--watch] [--force] [--json]` | verify, commit and activate; write back the manifest, the types and `base_version` |
|
|
490
|
+
| `juno flow pull [--force]` | overwrite the working copy with the active version |
|
|
491
|
+
|
|
492
|
+
`deploy` writes back what Juno stored: the canonical `manifest.json`, freshly
|
|
493
|
+
generated types, and the new version number. `--watch` redeploys whenever
|
|
494
|
+
`flow.ts` or `manifest.json` is saved, skips a save that changed nothing, and
|
|
495
|
+
keeps watching after a failure. `--json` on `check` and `deploy` prints the
|
|
496
|
+
instance's JSON answer (or, when verification fails, its diagnostics) on stdout
|
|
497
|
+
instead, for CI; the integration commands take it too.
|
|
498
|
+
|
|
499
|
+
### The flow id and `base_version`
|
|
500
|
+
|
|
501
|
+
`package.json`'s `juno.flow_id` binds the working copy to one flow. The
|
|
502
|
+
manifest's `id` is the flow's identity, so a manifest whose `id` differs from
|
|
503
|
+
`flow_id` is refused, naming both. To change what a flow is called, edit `name`;
|
|
504
|
+
renaming a flow is done in the UI.
|
|
505
|
+
|
|
506
|
+
`juno.base_version` is the version the working copy came from. `deploy` sends
|
|
507
|
+
it, and when the flow's active version is anything else (someone edited the flow
|
|
508
|
+
in the UI, asked the assistant to change it, or rolled it back), Juno refuses
|
|
509
|
+
the deploy and the CLI exits **4**, naming both versions. Run `npm run pull` to
|
|
510
|
+
start from the active version, or `juno flow deploy --force` to replace it.
|
|
511
|
+
`check` makes the same comparison, so a stale working copy shows up before you
|
|
512
|
+
deploy. A new flow's working copy has no `base_version`. `juno flow pull` on a
|
|
513
|
+
flow with no active version exits 1: there is nothing to pull.
|
|
514
|
+
|
|
515
|
+
### Dependencies
|
|
516
|
+
|
|
517
|
+
`manifest.dependencies` is the list Juno installs when it verifies the flow.
|
|
518
|
+
`init` copies it into `package.json`'s `dependencies` so `npm install` gives
|
|
519
|
+
`tsc` the same modules. Add a package to the manifest, then to `package.json`
|
|
520
|
+
(or deploy and `npm run pull`). `juno flow check` warns when the two lists
|
|
521
|
+
differ, naming the packages.
|
|
522
|
+
|
|
523
|
+
### Why there is no `flow verify-types`
|
|
524
|
+
|
|
525
|
+
`flow-env.d.ts` describes the instance as well as the manifest: the states the
|
|
526
|
+
flow binds, the integrations and controls it calls. It goes stale whenever the
|
|
527
|
+
instance changes, and Juno regenerates it on every check and deploy, so stale
|
|
528
|
+
local types never ship. `npm run types` refreshes your copy when the editor
|
|
529
|
+
disagrees with the instance.
|
|
530
|
+
|
|
531
|
+
## Developing integrations
|
|
532
|
+
|
|
533
|
+
An integration is a git repository holding `manifest.json` and TypeScript. Juno
|
|
534
|
+
fetches it at a tag, compiles it, and runs it in a sandbox. There is no
|
|
535
|
+
packaging format and nothing is published to npm.
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
npm install --global @junoflow/cli
|
|
539
|
+
juno integration init my-integration --url http://juno.local:8080
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Commands
|
|
543
|
+
|
|
544
|
+
| Command | What it does |
|
|
545
|
+
| ------------------------------------------------------------ | ------------------------------------------------------------------------------ |
|
|
546
|
+
| `juno integration init <id> [--name <name>] [--here]` | write the install's project from a Juno, or a starter for a new id |
|
|
547
|
+
| `juno integration gen-types` | POST `manifest.json`, write `types/` |
|
|
548
|
+
| `juno integration verify-types` | check `types/` still matches `manifest.json`; talks to nothing |
|
|
549
|
+
| `juno integration check [--skip-local] [--id <id>] [--json]` | local `verify-types` + `tsc --noEmit` + `eslint`, then validate against a Juno |
|
|
550
|
+
| `juno integration deploy [--watch] [--id <id>] [--json]` | upload, verify, activate, and write back the returned types |
|
|
551
|
+
| `juno integration pull [--force] [--id <id>]` | write back the manifest, sources and `package.json` scripts the instance holds |
|
|
552
|
+
|
|
553
|
+
They exit as the [exit-code table](#exit-codes) says: 1 for a failed check or
|
|
554
|
+
deploy, 2 for a usage error, 3 when the instance is unreachable.
|
|
555
|
+
|
|
556
|
+
### Options
|
|
557
|
+
|
|
558
|
+
| Flag | Meaning |
|
|
559
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
560
|
+
| `--url <url>` | the instance to talk to |
|
|
561
|
+
| `--id <id>` | address an install under a different id than the manifest declares (`check`, `deploy`, `pull`) |
|
|
562
|
+
| `--name <name>` | display name for a new integration (`init`) |
|
|
563
|
+
| `--watch` | redeploy on every save (`deploy`) |
|
|
564
|
+
| `--force` | overwrite a dirty working tree (`pull`) |
|
|
565
|
+
| `--here` | scaffold into the current directory (`init`) |
|
|
566
|
+
| `--skip-local` | skip the local verify-types/tsc/eslint pass (`check`) |
|
|
567
|
+
| `--json` | print the instance's JSON answer (or the failed verify's diagnostics) on stdout, for CI (`check`, `deploy`) |
|
|
45
568
|
|
|
46
|
-
|
|
569
|
+
### The loop
|
|
47
570
|
|
|
48
571
|
```bash
|
|
49
|
-
npm run watch # juno deploy --watch
|
|
572
|
+
npm run watch # juno integration deploy --watch
|
|
50
573
|
```
|
|
51
574
|
|
|
52
575
|
Add an action to `manifest.json`. The types regenerate, and your editor
|
|
@@ -54,7 +577,7 @@ immediately red-squiggles `src/integration.ts` because `IntegrationActions` now
|
|
|
54
577
|
requires a method you have not written — with the parameter and return types
|
|
55
578
|
your JSON Schema implies. The manifest drives the types drive the compiler.
|
|
56
579
|
|
|
57
|
-
|
|
580
|
+
### Why the types are committed
|
|
58
581
|
|
|
59
582
|
`types/` is generated and checked in. CI needs no Juno, a fresh clone compiles
|
|
60
583
|
offline, and the diff in a pull request shows that adding an action changed the
|
|
@@ -69,40 +592,42 @@ contract you no longer declare. `tsc` compiles happily against stale types,
|
|
|
69
592
|
with your _module_, never with `types/` — so the mismatch surfaces at deploy, or
|
|
70
593
|
later, as a flow author calling an action whose shape changed.
|
|
71
594
|
|
|
72
|
-
`juno verify-types` closes it. `juno types` records
|
|
73
|
-
`manifest.json` it generated from, as a comment in
|
|
595
|
+
`juno integration verify-types` closes it. `juno integration gen-types` records
|
|
596
|
+
a fingerprint of the `manifest.json` it generated from, as a comment in
|
|
597
|
+
`types/integration-env.d.ts`:
|
|
74
598
|
|
|
75
599
|
```ts
|
|
76
600
|
// integration-env.d.ts for 'pushover'
|
|
77
|
-
// DO NOT EDIT — generated by 'juno types' from 'manifest.json'.
|
|
601
|
+
// DO NOT EDIT — generated by 'juno integration gen-types' from 'manifest.json'.
|
|
78
602
|
// juno:manifest-sha256 5c5ca8eabe12cf44
|
|
79
603
|
```
|
|
80
604
|
|
|
81
605
|
`verify-types` recomputes it and compares. It runs first in `npm test` and
|
|
82
|
-
inside `juno check`, and it contacts nothing — which is the point:
|
|
83
|
-
is caught in a contributor's CI, and that CI deliberately has no
|
|
84
|
-
ask. Reformatting `manifest.json` also invalidates the stamp; the
|
|
85
|
-
same `npm run types` either way.
|
|
606
|
+
inside `juno integration check`, and it contacts nothing — which is the point:
|
|
607
|
+
the mistake is caught in a contributor's CI, and that CI deliberately has no
|
|
608
|
+
instance to ask. Reformatting `manifest.json` also invalidates the stamp; the
|
|
609
|
+
fix is the same `npm run types` either way.
|
|
86
610
|
|
|
87
|
-
|
|
611
|
+
### What runs where
|
|
88
612
|
|
|
89
|
-
| | offline | needs an instance
|
|
90
|
-
| --------------------------------------------------------------- | ------- |
|
|
91
|
-
| `tsc --noEmit`, `eslint`, `vitest` | ✅ |
|
|
92
|
-
| `juno verify-types` (is `types/` current?)
|
|
93
|
-
| structural manifest check via `"$schema"` in your editor | ✅ |
|
|
94
|
-
| reserved names, cross-surface collisions, JSON Schema compiling | | `juno check`
|
|
95
|
-
| `juno init`, `
|
|
613
|
+
| | offline | needs an instance |
|
|
614
|
+
| --------------------------------------------------------------- | ------- | ------------------------ |
|
|
615
|
+
| `tsc --noEmit`, `eslint`, `vitest` | ✅ | |
|
|
616
|
+
| `juno integration verify-types` (is `types/` current?) | ✅ | |
|
|
617
|
+
| structural manifest check via `"$schema"` in your editor | ✅ | |
|
|
618
|
+
| reserved names, cross-surface collisions, JSON Schema compiling | | `juno integration check` |
|
|
619
|
+
| `juno integration init`, `gen-types`, `deploy` | | ✅ |
|
|
96
620
|
|
|
97
621
|
Everything semantic lives in the Go server. This package knows some HTTP
|
|
98
622
|
endpoints and a config preset — that is deliberate, so there is one answer to
|
|
99
623
|
"is this manifest valid" rather than two that can disagree.
|
|
100
624
|
|
|
101
|
-
|
|
625
|
+
### `init` vs. downloading a project
|
|
102
626
|
|
|
103
|
-
Both produce the same project, from the same generator on the instance
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
template
|
|
107
|
-
from what the server ejects. To refresh an
|
|
108
|
-
|
|
627
|
+
Both produce the same project, from the same generator on the instance. `init`
|
|
628
|
+
of an id that is already installed writes that install's project, the one the
|
|
629
|
+
**Develop locally → Download project** button gives you; `init` of a new id
|
|
630
|
+
writes a starter. Neither is a local template: the CLI writes the files it is
|
|
631
|
+
handed, so a scaffold cannot drift from what the server ejects. To refresh an
|
|
632
|
+
existing checkout use `juno integration pull`, which also rewrites
|
|
633
|
+
`package.json`'s `scripts`; a second download would clobber your working tree.
|