rhylthyme-cli-runner 0.2.2a0__tar.gz → 0.2.4a0__tar.gz
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.
- {rhylthyme_cli_runner-0.2.2a0/src/rhylthyme_cli_runner.egg-info → rhylthyme_cli_runner-0.2.4a0}/PKG-INFO +135 -2
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/README.md +134 -1
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/setup.py +1 -1
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/__init__.py +1 -1
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/cli.py +10 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/render.py +9 -0
- rhylthyme_cli_runner-0.2.4a0/src/rhylthyme_cli_runner/import_cli.py +250 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/remote/cli.py +58 -8
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0/src/rhylthyme_cli_runner.egg-info}/PKG-INFO +135 -2
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner.egg-info/SOURCES.txt +2 -0
- rhylthyme_cli_runner-0.2.4a0/tests/test_import_cli.py +228 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_remote.py +55 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_skill.py +11 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/LICENSE +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/pyproject.toml +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/setup.cfg +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/environment_icons.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/environment_loader.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/environment_schemas.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/cli.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/compare.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/gold.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/harness.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/llm.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/matcher.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/metrics.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/authoring_guide.md +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.md +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/extract.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/four_turn.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/report.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/expand_replicates.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/calibrate.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/calibrate_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/cli.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/evaluate.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/evaluate_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/factors.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/hash.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/predict.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/recorder.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/replay.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/report.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/report_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/store.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/synth.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/usable.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/instance_checks.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/program_planner.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/program_runner.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/remote/__init__.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/remote/auth.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/remote/checks.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/remote/mcp_client.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/validate_program.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner.egg-info/dependency_links.txt +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner.egg-info/entry_points.txt +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner.egg-info/requires.txt +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner.egg-info/top_level.txt +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_calibrate.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_cli.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_eval_baseline.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_eval_harness.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_eval_providers.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_eval_scorer.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_evaluate.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_example_validation.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_examples_integration.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_expand_replicates.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_hash_parity.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_history_report.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_mcp_checks.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_planner_time_strings.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_predict.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_program_runner.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_replay.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_run_recorder.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_synth.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_usable.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_validate_examples_ci.py +0 -0
- {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_validate_program.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rhylthyme-cli-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4a0
|
|
4
4
|
Summary: CLI runner for Rhylthyme real-time program schedules
|
|
5
5
|
Home-page: https://github.com/rhylthyme/rhylthyme-cli-runner
|
|
6
6
|
Author: Rhylthyme Team
|
|
@@ -69,6 +69,107 @@ rhylthyme login
|
|
|
69
69
|
rhylthyme generate "roast chicken, potatoes and green beans for 6" -e kitchen --by 19:00 --with "one oven"
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
## A timeline in five commands: a birthday party
|
|
73
|
+
|
|
74
|
+
Parents collect at 4:00. Working backwards from that: when does the cake go in
|
|
75
|
+
the oven, when do the balloons go up, and do the pizzas and the cake ever want
|
|
76
|
+
the oven at the same time?
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install rhylthyme-cli-runner
|
|
80
|
+
|
|
81
|
+
# 1. a program: the party, the kitchen and the setup crew, 13 steps
|
|
82
|
+
curl -sO https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.json
|
|
83
|
+
|
|
84
|
+
# 2. is it well formed?
|
|
85
|
+
rhylthyme validate birthday-party.json --strict
|
|
86
|
+
|
|
87
|
+
# 3. any clashes, and what time does everything start if we finish at 4?
|
|
88
|
+
rhylthyme analyze birthday-party.json --finish-at 16:00
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
**Makespan:** 3h 10m across 3 tracks, 13 steps.
|
|
93
|
+
**Critical path:** Guests arrive → Party games → Pizza → Candles and cake → Piñata, until it breaks → Party bags and goodbyes
|
|
94
|
+
**Binding constraints:** step dependencies only — no resource or in-flight limit gates the critical path.
|
|
95
|
+
**Peak concurrency:** 2 steps at 10:00.
|
|
96
|
+
**Resource conflicts:** none.
|
|
97
|
+
|
|
98
|
+
**Tracks that finish early:** Kitchen (1h 10m early), Setup crew (50m early)
|
|
99
|
+
|
|
100
|
+
Start Sat 12:50, finish Sat 16:00 (local time)
|
|
101
|
+
Sat 12:50 Bake the cake
|
|
102
|
+
Sat 13:00 Balloons and banner
|
|
103
|
+
Sat 13:25 Cake cools
|
|
104
|
+
Sat 13:30 Hang the piñata
|
|
105
|
+
Sat 13:50 Guests arrive
|
|
106
|
+
Sat 14:05 Frost and decorate
|
|
107
|
+
Sat 14:20 Party games
|
|
108
|
+
Sat 14:35 Pizzas in the oven
|
|
109
|
+
Sat 14:50 Fill party bags
|
|
110
|
+
Sat 14:50 Pizza
|
|
111
|
+
Sat 15:15 Candles and cake
|
|
112
|
+
Sat 15:30 Piñata, until it breaks
|
|
113
|
+
Sat 15:45 Party bags and goodbyes
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# 4. a live timeline for the fridge door (or a phone): timers, cues, and you end the piñata when it breaks
|
|
118
|
+
rhylthyme publish birthday-party.json --open --image party-preview.png
|
|
119
|
+
|
|
120
|
+
# 5. a picture for the group chat, on the wall clock (needs Node; nothing to install)
|
|
121
|
+
npx -y github:rhylthyme/rhylthyme-timeline birthday-party.json -o birthday-party.png \
|
|
122
|
+
--style web --palette vivid --label-overflow outside --start-at "$(date +%F)T12:50:00"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+

|
|
126
|
+
|
|
127
|
+
The pizzas go in 15 minutes after the games *start*, so they come out as the
|
|
128
|
+
games end; the party bags get filled while everyone is eating. Try
|
|
129
|
+
`--finish-at 17:30`, or turn the cake into a two-hour fruitcake (`"seconds": "2h"`
|
|
130
|
+
on `bake`) and `analyze` reports the clash: `oven needs 2 but max is 1 from
|
|
131
|
+
1:45:00 to 2:00:00 (bake, heat-pizza)`. `rhylthyme run birthday-party.json --time-scale 60` rehearses
|
|
132
|
+
the whole afternoon in the terminal at a minute per second.
|
|
133
|
+
|
|
134
|
+
## This repository and rhylthyme-mcp
|
|
135
|
+
|
|
136
|
+
Two repositories do related things and are easy to confuse. This one is the
|
|
137
|
+
**command a person types**. [rhylthyme-mcp](https://github.com/rhylthyme/rhylthyme-mcp)
|
|
138
|
+
is the **server an AI assistant talks to**.
|
|
139
|
+
|
|
140
|
+
| | [rhylthyme-mcp](https://github.com/rhylthyme/rhylthyme-mcp) | [rhylthyme-cli-runner](https://github.com/rhylthyme/rhylthyme-cli-runner) |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| What it is | The MCP **server**: the tools an AI assistant calls | A command-line **program**: the `rhylthyme` command |
|
|
143
|
+
| Who uses it | Claude, ChatGPT, Cursor or any MCP client, on a person's behalf | A person at a terminal, a script, or CI |
|
|
144
|
+
| Where it runs | Hosted at `mcp.rhylthyme.com`; nothing to install | On your machine: `pip install rhylthyme-cli-runner` |
|
|
145
|
+
| Language | JavaScript (Node 20+) | Python 3.12+ |
|
|
146
|
+
| Input | A program the assistant builds in conversation | A program file on disk (JSON or YAML) |
|
|
147
|
+
| Validate a program | `validate_program` | `rhylthyme validate` (works offline) |
|
|
148
|
+
| Import a recipe or protocol | `import_from_source` | `rhylthyme import` |
|
|
149
|
+
| Timing, conflicts, deadlines | `analyze_schedule` | `rhylthyme analyze` (asks the server) |
|
|
150
|
+
| Publish a live timeline | `visualize_schedule` | `rhylthyme publish` (asks the server) |
|
|
151
|
+
| Run a schedule with timers | no: it hands back a link to the web timeline | `rhylthyme run`, an interactive terminal runner |
|
|
152
|
+
| Recorded runs, calibration | reads runs saved to an account | records runs locally; `rhylthyme runs`, `rhylthyme calibrate` |
|
|
153
|
+
| Catalog search, account library | yes | no |
|
|
154
|
+
| Also in the repository | the `rhylthyme-mcp` PyPI package (a stdio bridge to the hosted server), the Claude plugin marketplace | the Claude skill's source, the prompt-evaluation harness and its results, `rhylthyme mcp-test` |
|
|
155
|
+
|
|
156
|
+
How they fit together: this tool is one of the MCP server's clients.
|
|
157
|
+
`rhylthyme analyze`, `publish`, `generate` and `mcp-test` are MCP calls to
|
|
158
|
+
`mcp.rhylthyme.com`; `rhylthyme validate`, `run`, `runs` and `calibrate` never
|
|
159
|
+
touch the network. Each has its own validator for the same program schema
|
|
160
|
+
(Python here, JavaScript there), so a program is checked again when it is
|
|
161
|
+
published.
|
|
162
|
+
|
|
163
|
+
Use this repository if you have a program file and a terminal, want timers in
|
|
164
|
+
the terminal, or keep run records. Use rhylthyme-mcp to connect Claude, ChatGPT
|
|
165
|
+
or another assistant, or to read or self-host the server.
|
|
166
|
+
|
|
167
|
+
Two names to keep apart: `rhylthyme-cli-runner` on PyPI is this tool (command
|
|
168
|
+
`rhylthyme`); `rhylthyme-mcp` on PyPI is the server's stdio bridge (command
|
|
169
|
+
`rhylthyme-mcp`). The program format itself is defined in
|
|
170
|
+
[rhylthyme-spec](https://github.com/rhylthyme/rhylthyme-spec), with examples in
|
|
171
|
+
[rhylthyme-examples](https://github.com/rhylthyme/rhylthyme-examples).
|
|
172
|
+
|
|
72
173
|
## Installation
|
|
73
174
|
|
|
74
175
|
### From Source
|
|
@@ -257,6 +358,16 @@ it for conflicts, and hand back a live timeline. It follows the layout of
|
|
|
257
358
|
[K-Dense scientific skills](https://github.com/K-Dense-AI/claude-scientific-skills):
|
|
258
359
|
a `SKILL.md` plus `references/`.
|
|
259
360
|
|
|
361
|
+
The easiest install is the Claude Code plugin, which also connects the hosted
|
|
362
|
+
MCP server:
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
/plugin marketplace add rhylthyme/rhylthyme-mcp
|
|
366
|
+
/plugin install rhylthyme@rhylthyme
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Or copy the skill by hand:
|
|
370
|
+
|
|
260
371
|
```bash
|
|
261
372
|
# Claude Code, for one user
|
|
262
373
|
mkdir -p ~/.claude/skills
|
|
@@ -421,15 +532,37 @@ and tracks that finish early. No sign-in; nothing is published.
|
|
|
421
532
|
|
|
422
533
|
### `rhylthyme publish`
|
|
423
534
|
|
|
424
|
-
Publishes a program file
|
|
535
|
+
Publishes a program (a file, an http(s) URL, or `-` for standard input) as a
|
|
536
|
+
live timeline and prints its URL. No sign-in.
|
|
425
537
|
A published timeline is reachable by anyone who has the link.
|
|
426
538
|
|
|
427
539
|
**Options:**
|
|
428
540
|
- `-e, --env`: `generic`, `kitchen`, `lab`, `events`, `gym` (default: from `environmentType`)
|
|
429
541
|
- `-q, --quiet`: print only the URL
|
|
430
542
|
- `--json`: `url`, `shareId`, `imageUrl`, `makespanSeconds`, `warnings`
|
|
543
|
+
- `--image PATH`: also save a PNG of the timeline
|
|
431
544
|
- `--open`: open it in a browser
|
|
432
545
|
|
|
546
|
+
### `rhylthyme import`
|
|
547
|
+
|
|
548
|
+
Imports a recipe, protocol or slide deck as a program, validates it, and
|
|
549
|
+
writes `<programId>.json`. Needs `rhylthyme-importers` (included in
|
|
550
|
+
`pip install rhylthyme`).
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
rhylthyme import https://www.seriouseats.com/the-best-chili-recipe # any of ~580 recipe sites
|
|
554
|
+
rhylthyme import 52772 -i themealdb --publish # a TheMealDB id, straight to a live timeline
|
|
555
|
+
rhylthyme import https://www.protocols.io/view/western-blot-... # needs PROTOCOLS_IO_TOKEN
|
|
556
|
+
rhylthyme import protocol.py -o protocol.json # an Opentrons file
|
|
557
|
+
rhylthyme import "Neapolitan Pizza.cook" -i cooklang
|
|
558
|
+
rhylthyme importers # what is installed
|
|
559
|
+
rhylthyme search "pad thai" -i spoonacular # then import a hit's URL
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Options:** `-i IMPORTER` (default: chosen from the URL), `-o PATH`,
|
|
563
|
+
`--stdout`, `--publish` (and `--open`), `--no-validate`. `-` reads pasted
|
|
564
|
+
source from stdin (with `-i`).
|
|
565
|
+
|
|
433
566
|
### `rhylthyme plan`
|
|
434
567
|
|
|
435
568
|
An older stagger heuristic. It reads the pre-0.2 program format and leaves
|
|
@@ -9,6 +9,107 @@ rhylthyme login
|
|
|
9
9
|
rhylthyme generate "roast chicken, potatoes and green beans for 6" -e kitchen --by 19:00 --with "one oven"
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## A timeline in five commands: a birthday party
|
|
13
|
+
|
|
14
|
+
Parents collect at 4:00. Working backwards from that: when does the cake go in
|
|
15
|
+
the oven, when do the balloons go up, and do the pizzas and the cake ever want
|
|
16
|
+
the oven at the same time?
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install rhylthyme-cli-runner
|
|
20
|
+
|
|
21
|
+
# 1. a program: the party, the kitchen and the setup crew, 13 steps
|
|
22
|
+
curl -sO https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.json
|
|
23
|
+
|
|
24
|
+
# 2. is it well formed?
|
|
25
|
+
rhylthyme validate birthday-party.json --strict
|
|
26
|
+
|
|
27
|
+
# 3. any clashes, and what time does everything start if we finish at 4?
|
|
28
|
+
rhylthyme analyze birthday-party.json --finish-at 16:00
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
**Makespan:** 3h 10m across 3 tracks, 13 steps.
|
|
33
|
+
**Critical path:** Guests arrive → Party games → Pizza → Candles and cake → Piñata, until it breaks → Party bags and goodbyes
|
|
34
|
+
**Binding constraints:** step dependencies only — no resource or in-flight limit gates the critical path.
|
|
35
|
+
**Peak concurrency:** 2 steps at 10:00.
|
|
36
|
+
**Resource conflicts:** none.
|
|
37
|
+
|
|
38
|
+
**Tracks that finish early:** Kitchen (1h 10m early), Setup crew (50m early)
|
|
39
|
+
|
|
40
|
+
Start Sat 12:50, finish Sat 16:00 (local time)
|
|
41
|
+
Sat 12:50 Bake the cake
|
|
42
|
+
Sat 13:00 Balloons and banner
|
|
43
|
+
Sat 13:25 Cake cools
|
|
44
|
+
Sat 13:30 Hang the piñata
|
|
45
|
+
Sat 13:50 Guests arrive
|
|
46
|
+
Sat 14:05 Frost and decorate
|
|
47
|
+
Sat 14:20 Party games
|
|
48
|
+
Sat 14:35 Pizzas in the oven
|
|
49
|
+
Sat 14:50 Fill party bags
|
|
50
|
+
Sat 14:50 Pizza
|
|
51
|
+
Sat 15:15 Candles and cake
|
|
52
|
+
Sat 15:30 Piñata, until it breaks
|
|
53
|
+
Sat 15:45 Party bags and goodbyes
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# 4. a live timeline for the fridge door (or a phone): timers, cues, and you end the piñata when it breaks
|
|
58
|
+
rhylthyme publish birthday-party.json --open --image party-preview.png
|
|
59
|
+
|
|
60
|
+
# 5. a picture for the group chat, on the wall clock (needs Node; nothing to install)
|
|
61
|
+
npx -y github:rhylthyme/rhylthyme-timeline birthday-party.json -o birthday-party.png \
|
|
62
|
+
--style web --palette vivid --label-overflow outside --start-at "$(date +%F)T12:50:00"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
The pizzas go in 15 minutes after the games *start*, so they come out as the
|
|
68
|
+
games end; the party bags get filled while everyone is eating. Try
|
|
69
|
+
`--finish-at 17:30`, or turn the cake into a two-hour fruitcake (`"seconds": "2h"`
|
|
70
|
+
on `bake`) and `analyze` reports the clash: `oven needs 2 but max is 1 from
|
|
71
|
+
1:45:00 to 2:00:00 (bake, heat-pizza)`. `rhylthyme run birthday-party.json --time-scale 60` rehearses
|
|
72
|
+
the whole afternoon in the terminal at a minute per second.
|
|
73
|
+
|
|
74
|
+
## This repository and rhylthyme-mcp
|
|
75
|
+
|
|
76
|
+
Two repositories do related things and are easy to confuse. This one is the
|
|
77
|
+
**command a person types**. [rhylthyme-mcp](https://github.com/rhylthyme/rhylthyme-mcp)
|
|
78
|
+
is the **server an AI assistant talks to**.
|
|
79
|
+
|
|
80
|
+
| | [rhylthyme-mcp](https://github.com/rhylthyme/rhylthyme-mcp) | [rhylthyme-cli-runner](https://github.com/rhylthyme/rhylthyme-cli-runner) |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| What it is | The MCP **server**: the tools an AI assistant calls | A command-line **program**: the `rhylthyme` command |
|
|
83
|
+
| Who uses it | Claude, ChatGPT, Cursor or any MCP client, on a person's behalf | A person at a terminal, a script, or CI |
|
|
84
|
+
| Where it runs | Hosted at `mcp.rhylthyme.com`; nothing to install | On your machine: `pip install rhylthyme-cli-runner` |
|
|
85
|
+
| Language | JavaScript (Node 20+) | Python 3.12+ |
|
|
86
|
+
| Input | A program the assistant builds in conversation | A program file on disk (JSON or YAML) |
|
|
87
|
+
| Validate a program | `validate_program` | `rhylthyme validate` (works offline) |
|
|
88
|
+
| Import a recipe or protocol | `import_from_source` | `rhylthyme import` |
|
|
89
|
+
| Timing, conflicts, deadlines | `analyze_schedule` | `rhylthyme analyze` (asks the server) |
|
|
90
|
+
| Publish a live timeline | `visualize_schedule` | `rhylthyme publish` (asks the server) |
|
|
91
|
+
| Run a schedule with timers | no: it hands back a link to the web timeline | `rhylthyme run`, an interactive terminal runner |
|
|
92
|
+
| Recorded runs, calibration | reads runs saved to an account | records runs locally; `rhylthyme runs`, `rhylthyme calibrate` |
|
|
93
|
+
| Catalog search, account library | yes | no |
|
|
94
|
+
| Also in the repository | the `rhylthyme-mcp` PyPI package (a stdio bridge to the hosted server), the Claude plugin marketplace | the Claude skill's source, the prompt-evaluation harness and its results, `rhylthyme mcp-test` |
|
|
95
|
+
|
|
96
|
+
How they fit together: this tool is one of the MCP server's clients.
|
|
97
|
+
`rhylthyme analyze`, `publish`, `generate` and `mcp-test` are MCP calls to
|
|
98
|
+
`mcp.rhylthyme.com`; `rhylthyme validate`, `run`, `runs` and `calibrate` never
|
|
99
|
+
touch the network. Each has its own validator for the same program schema
|
|
100
|
+
(Python here, JavaScript there), so a program is checked again when it is
|
|
101
|
+
published.
|
|
102
|
+
|
|
103
|
+
Use this repository if you have a program file and a terminal, want timers in
|
|
104
|
+
the terminal, or keep run records. Use rhylthyme-mcp to connect Claude, ChatGPT
|
|
105
|
+
or another assistant, or to read or self-host the server.
|
|
106
|
+
|
|
107
|
+
Two names to keep apart: `rhylthyme-cli-runner` on PyPI is this tool (command
|
|
108
|
+
`rhylthyme`); `rhylthyme-mcp` on PyPI is the server's stdio bridge (command
|
|
109
|
+
`rhylthyme-mcp`). The program format itself is defined in
|
|
110
|
+
[rhylthyme-spec](https://github.com/rhylthyme/rhylthyme-spec), with examples in
|
|
111
|
+
[rhylthyme-examples](https://github.com/rhylthyme/rhylthyme-examples).
|
|
112
|
+
|
|
12
113
|
## Installation
|
|
13
114
|
|
|
14
115
|
### From Source
|
|
@@ -197,6 +298,16 @@ it for conflicts, and hand back a live timeline. It follows the layout of
|
|
|
197
298
|
[K-Dense scientific skills](https://github.com/K-Dense-AI/claude-scientific-skills):
|
|
198
299
|
a `SKILL.md` plus `references/`.
|
|
199
300
|
|
|
301
|
+
The easiest install is the Claude Code plugin, which also connects the hosted
|
|
302
|
+
MCP server:
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
/plugin marketplace add rhylthyme/rhylthyme-mcp
|
|
306
|
+
/plugin install rhylthyme@rhylthyme
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Or copy the skill by hand:
|
|
310
|
+
|
|
200
311
|
```bash
|
|
201
312
|
# Claude Code, for one user
|
|
202
313
|
mkdir -p ~/.claude/skills
|
|
@@ -361,15 +472,37 @@ and tracks that finish early. No sign-in; nothing is published.
|
|
|
361
472
|
|
|
362
473
|
### `rhylthyme publish`
|
|
363
474
|
|
|
364
|
-
Publishes a program file
|
|
475
|
+
Publishes a program (a file, an http(s) URL, or `-` for standard input) as a
|
|
476
|
+
live timeline and prints its URL. No sign-in.
|
|
365
477
|
A published timeline is reachable by anyone who has the link.
|
|
366
478
|
|
|
367
479
|
**Options:**
|
|
368
480
|
- `-e, --env`: `generic`, `kitchen`, `lab`, `events`, `gym` (default: from `environmentType`)
|
|
369
481
|
- `-q, --quiet`: print only the URL
|
|
370
482
|
- `--json`: `url`, `shareId`, `imageUrl`, `makespanSeconds`, `warnings`
|
|
483
|
+
- `--image PATH`: also save a PNG of the timeline
|
|
371
484
|
- `--open`: open it in a browser
|
|
372
485
|
|
|
486
|
+
### `rhylthyme import`
|
|
487
|
+
|
|
488
|
+
Imports a recipe, protocol or slide deck as a program, validates it, and
|
|
489
|
+
writes `<programId>.json`. Needs `rhylthyme-importers` (included in
|
|
490
|
+
`pip install rhylthyme`).
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
rhylthyme import https://www.seriouseats.com/the-best-chili-recipe # any of ~580 recipe sites
|
|
494
|
+
rhylthyme import 52772 -i themealdb --publish # a TheMealDB id, straight to a live timeline
|
|
495
|
+
rhylthyme import https://www.protocols.io/view/western-blot-... # needs PROTOCOLS_IO_TOKEN
|
|
496
|
+
rhylthyme import protocol.py -o protocol.json # an Opentrons file
|
|
497
|
+
rhylthyme import "Neapolitan Pizza.cook" -i cooklang
|
|
498
|
+
rhylthyme importers # what is installed
|
|
499
|
+
rhylthyme search "pad thai" -i spoonacular # then import a hit's URL
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Options:** `-i IMPORTER` (default: chosen from the URL), `-o PATH`,
|
|
503
|
+
`--stdout`, `--publish` (and `--open`), `--no-validate`. `-` reads pasted
|
|
504
|
+
source from stdin (with `-i`).
|
|
505
|
+
|
|
373
506
|
### `rhylthyme plan`
|
|
374
507
|
|
|
375
508
|
An older stagger heuristic. It reads the pre-0.2 program format and leaves
|
|
@@ -19,7 +19,7 @@ def read_readme():
|
|
|
19
19
|
|
|
20
20
|
setup(
|
|
21
21
|
name="rhylthyme-cli-runner",
|
|
22
|
-
version="0.2.
|
|
22
|
+
version="0.2.4a0",
|
|
23
23
|
description="CLI runner for Rhylthyme real-time program schedules",
|
|
24
24
|
long_description=read_readme(),
|
|
25
25
|
long_description_content_type="text/markdown",
|
{rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/__init__.py
RENAMED
|
@@ -6,7 +6,7 @@ This package provides the command-line interface for running and validating
|
|
|
6
6
|
Rhylthyme real-time program schedules.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "0.2.
|
|
9
|
+
__version__ = "0.2.4a0"
|
|
10
10
|
__author__ = "Rhylthyme Team"
|
|
11
11
|
__description__ = "CLI runner for Rhylthyme real-time program schedules"
|
|
12
12
|
|
{rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/cli.py
RENAMED
|
@@ -544,6 +544,16 @@ def _register_remote_commands():
|
|
|
544
544
|
_register_remote_commands()
|
|
545
545
|
|
|
546
546
|
|
|
547
|
+
def _register_import_commands():
|
|
548
|
+
"""Attach `import`, `search`, `importers` (rhylthyme_cli_runner.import_cli)."""
|
|
549
|
+
from .import_cli import register as register_import
|
|
550
|
+
|
|
551
|
+
register_import(cli)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
_register_import_commands()
|
|
555
|
+
|
|
556
|
+
|
|
547
557
|
def main():
|
|
548
558
|
"""Entry point for the CLI."""
|
|
549
559
|
cli()
|
|
@@ -76,6 +76,15 @@ def find_renderer() -> Optional[Path]:
|
|
|
76
76
|
candidate = Path(explicit).expanduser()
|
|
77
77
|
# Absolute: `node -e` has no script directory to resolve against.
|
|
78
78
|
return candidate.resolve() if candidate.is_file() else None
|
|
79
|
+
# The pip package (rhylthyme-timeline) carries the renderer.
|
|
80
|
+
try:
|
|
81
|
+
from rhylthyme_timeline import renderer_path
|
|
82
|
+
|
|
83
|
+
packaged = renderer_path()
|
|
84
|
+
if packaged.is_file():
|
|
85
|
+
return packaged
|
|
86
|
+
except ImportError:
|
|
87
|
+
pass
|
|
79
88
|
here = Path(__file__).resolve()
|
|
80
89
|
for parent in here.parents:
|
|
81
90
|
for relative in _RENDERER_PATHS:
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""``rhylthyme import`` and ``rhylthyme search``: the importers from
|
|
2
|
+
rhylthyme-importers, reached from the one command people already have.
|
|
3
|
+
|
|
4
|
+
``rhylthyme-import`` (the importers' own command) still exists; this is the
|
|
5
|
+
same work with the CLI's validation and publishing on the end of it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
INSTALL_HINT = (
|
|
19
|
+
"The importers are a separate package: pip install rhylthyme-importers "
|
|
20
|
+
"(or pip install rhylthyme, which includes them)."
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _registry():
|
|
25
|
+
try:
|
|
26
|
+
from rhylthyme_importers import ImporterRegistry
|
|
27
|
+
except ImportError:
|
|
28
|
+
raise click.ClickException(INSTALL_HINT)
|
|
29
|
+
return ImporterRegistry
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _pick(registry, source: str, importer_name: Optional[str]):
|
|
33
|
+
if importer_name:
|
|
34
|
+
importer = registry.get(importer_name)
|
|
35
|
+
if importer is None:
|
|
36
|
+
names = ", ".join(sorted(i["name"] for i in registry.list_importers()))
|
|
37
|
+
raise click.ClickException(
|
|
38
|
+
f"Unknown importer '{importer_name}'. Installed: {names}."
|
|
39
|
+
)
|
|
40
|
+
return importer
|
|
41
|
+
importer = registry.find_for_url(source)
|
|
42
|
+
if importer is None:
|
|
43
|
+
names = ", ".join(sorted(i["name"] for i in registry.list_importers()))
|
|
44
|
+
raise click.ClickException(
|
|
45
|
+
f"No importer recognises {source!r}. Pass -i with one of: {names}. "
|
|
46
|
+
"For a file on disk, name its importer (-i opentrons, -i cooklang, -i slidedeck)."
|
|
47
|
+
)
|
|
48
|
+
return importer
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _import_one(importer, source: str, text: Optional[str]):
|
|
52
|
+
if text is not None and hasattr(importer, "import_from_source"):
|
|
53
|
+
return importer.import_from_source(text)
|
|
54
|
+
if text is not None and hasattr(importer, "import_from_content"):
|
|
55
|
+
return importer.import_from_content(
|
|
56
|
+
text, source_name=Path(source).stem if source != "-" else "recipe"
|
|
57
|
+
)
|
|
58
|
+
path = Path(source)
|
|
59
|
+
if path.is_file():
|
|
60
|
+
# A local file was named explicitly; that is the command line's job
|
|
61
|
+
# to allow (a server never does this).
|
|
62
|
+
if hasattr(importer, "allow_local_files"):
|
|
63
|
+
importer.allow_local_files = True
|
|
64
|
+
return importer.import_from_url(str(path))
|
|
65
|
+
return importer.import_from_url(source)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validation(program: dict):
|
|
69
|
+
from .validate_program import validate_program_file_structured
|
|
70
|
+
|
|
71
|
+
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as tmp:
|
|
72
|
+
json.dump(program, tmp)
|
|
73
|
+
tmp_path = tmp.name
|
|
74
|
+
try:
|
|
75
|
+
from .cli import _default_schema_path
|
|
76
|
+
|
|
77
|
+
return validate_program_file_structured(
|
|
78
|
+
tmp_path, schema_file=_default_schema_path()
|
|
79
|
+
)
|
|
80
|
+
finally:
|
|
81
|
+
Path(tmp_path).unlink(missing_ok=True)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@click.command("import")
|
|
85
|
+
@click.argument("source", metavar="URL_OR_ID_OR_FILE")
|
|
86
|
+
@click.option(
|
|
87
|
+
"-i",
|
|
88
|
+
"--importer",
|
|
89
|
+
"importer_name",
|
|
90
|
+
default=None,
|
|
91
|
+
help="Which importer to use (default: chosen from the URL).",
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"-o",
|
|
95
|
+
"--output",
|
|
96
|
+
type=click.Path(dir_okay=False),
|
|
97
|
+
default=None,
|
|
98
|
+
help="Write the program here (default: <name>.json in the current directory).",
|
|
99
|
+
)
|
|
100
|
+
@click.option(
|
|
101
|
+
"--stdout",
|
|
102
|
+
"to_stdout",
|
|
103
|
+
is_flag=True,
|
|
104
|
+
help="Print the program instead of writing a file.",
|
|
105
|
+
)
|
|
106
|
+
@click.option(
|
|
107
|
+
"--publish",
|
|
108
|
+
"do_publish",
|
|
109
|
+
is_flag=True,
|
|
110
|
+
help="Also publish it as a live timeline and print the URL.",
|
|
111
|
+
)
|
|
112
|
+
@click.option(
|
|
113
|
+
"--open",
|
|
114
|
+
"open_url",
|
|
115
|
+
is_flag=True,
|
|
116
|
+
help="With --publish: open the timeline in a browser.",
|
|
117
|
+
)
|
|
118
|
+
@click.option(
|
|
119
|
+
"--no-validate", is_flag=True, help="Skip validation of the imported program."
|
|
120
|
+
)
|
|
121
|
+
def import_command(
|
|
122
|
+
source, importer_name, output, to_stdout, do_publish, open_url, no_validate
|
|
123
|
+
):
|
|
124
|
+
"""Import a recipe, protocol or deck as a program.
|
|
125
|
+
|
|
126
|
+
\b
|
|
127
|
+
URL_OR_ID_OR_FILE is a recipe-site or protocols.io URL, a TheMealDB or
|
|
128
|
+
Spoonacular id, a .cook or GitHub .cook URL, or a local .py (Opentrons),
|
|
129
|
+
.cook or .pptx file. `-` reads the source text from stdin (with -i).
|
|
130
|
+
rhylthyme import https://www.seriouseats.com/the-best-chili-recipe
|
|
131
|
+
rhylthyme import 52772 -i themealdb --publish
|
|
132
|
+
rhylthyme import protocol.py -o protocol.json
|
|
133
|
+
rhylthyme importers # what is installed
|
|
134
|
+
"""
|
|
135
|
+
registry = _registry()
|
|
136
|
+
text = None
|
|
137
|
+
if source == "-":
|
|
138
|
+
if not importer_name:
|
|
139
|
+
raise click.ClickException(
|
|
140
|
+
"Reading from stdin needs -i to say which importer."
|
|
141
|
+
)
|
|
142
|
+
text = sys.stdin.read()
|
|
143
|
+
importer = _pick(registry, source, importer_name)
|
|
144
|
+
click.echo(f"Importing with {importer.name}…", err=True)
|
|
145
|
+
result = _import_one(importer, source, text)
|
|
146
|
+
if not result.success:
|
|
147
|
+
raise click.ClickException(result.error or "import failed")
|
|
148
|
+
program = result.program
|
|
149
|
+
|
|
150
|
+
if not no_validate:
|
|
151
|
+
v = _validation(program)
|
|
152
|
+
errors = v.get("errors") or v.get("logic_errors") or []
|
|
153
|
+
if not v.get("is_valid", v.get("valid", True)):
|
|
154
|
+
for e in errors[:8]:
|
|
155
|
+
click.echo(
|
|
156
|
+
f" ✗ {e if isinstance(e, str) else e.get('message', e)}", err=True
|
|
157
|
+
)
|
|
158
|
+
raise click.ClickException(
|
|
159
|
+
"The imported program does not validate; not published. Re-run with --no-validate to keep it anyway."
|
|
160
|
+
)
|
|
161
|
+
for w in (v.get("warnings") or [])[:5]:
|
|
162
|
+
click.echo(
|
|
163
|
+
f" ! {w if isinstance(w, str) else w.get('message', w)}", err=True
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
payload = json.dumps(program, indent=2, ensure_ascii=False)
|
|
167
|
+
if to_stdout:
|
|
168
|
+
click.echo(payload)
|
|
169
|
+
else:
|
|
170
|
+
target = (
|
|
171
|
+
Path(output)
|
|
172
|
+
if output
|
|
173
|
+
else Path(f"{program.get('programId') or 'program'}.json")
|
|
174
|
+
)
|
|
175
|
+
target.write_text(payload + "\n")
|
|
176
|
+
click.echo(f"Saved {target}", err=True)
|
|
177
|
+
|
|
178
|
+
if do_publish:
|
|
179
|
+
from .remote.cli import publish as publish_command
|
|
180
|
+
|
|
181
|
+
ctx = click.get_current_context()
|
|
182
|
+
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as tmp:
|
|
183
|
+
tmp.write(payload)
|
|
184
|
+
tmp_path = tmp.name
|
|
185
|
+
try:
|
|
186
|
+
ctx.invoke(
|
|
187
|
+
publish_command,
|
|
188
|
+
program_file=tmp_path,
|
|
189
|
+
environment=None,
|
|
190
|
+
open_url=open_url,
|
|
191
|
+
json_output=False,
|
|
192
|
+
quiet=True,
|
|
193
|
+
image_path=None,
|
|
194
|
+
)
|
|
195
|
+
finally:
|
|
196
|
+
Path(tmp_path).unlink(missing_ok=True)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@click.command("search")
|
|
200
|
+
@click.argument("query")
|
|
201
|
+
@click.option(
|
|
202
|
+
"-i",
|
|
203
|
+
"--importer",
|
|
204
|
+
"importer_name",
|
|
205
|
+
default="themealdb",
|
|
206
|
+
show_default=True,
|
|
207
|
+
help="Which catalogue to search.",
|
|
208
|
+
)
|
|
209
|
+
@click.option("--json", "json_output", is_flag=True)
|
|
210
|
+
def search_command(query, importer_name, json_output):
|
|
211
|
+
"""Search a recipe or protocol source; import a hit with `rhylthyme import <url>`."""
|
|
212
|
+
registry = _registry()
|
|
213
|
+
importer = registry.get(importer_name)
|
|
214
|
+
if importer is None:
|
|
215
|
+
names = ", ".join(sorted(i["name"] for i in registry.list_importers()))
|
|
216
|
+
raise click.ClickException(
|
|
217
|
+
f"Unknown importer '{importer_name}'. Installed: {names}."
|
|
218
|
+
)
|
|
219
|
+
hits = importer.search(query)
|
|
220
|
+
if json_output:
|
|
221
|
+
click.echo(json.dumps(hits, indent=2, ensure_ascii=False))
|
|
222
|
+
return
|
|
223
|
+
if not hits:
|
|
224
|
+
click.echo("No results.")
|
|
225
|
+
return
|
|
226
|
+
for h in hits:
|
|
227
|
+
click.echo(f"{h.get('name')}\n {h.get('url')}")
|
|
228
|
+
if h.get("description"):
|
|
229
|
+
click.echo(f" {h['description']}")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@click.command("importers")
|
|
233
|
+
def importers_command():
|
|
234
|
+
"""List the installed importers."""
|
|
235
|
+
registry = _registry()
|
|
236
|
+
for info in registry.list_importers():
|
|
237
|
+
domains = info.get("supported_domains") or []
|
|
238
|
+
where = (
|
|
239
|
+
", ".join(domains[:4])
|
|
240
|
+
+ (f", … ({len(domains)} sites)" if len(domains) > 4 else "")
|
|
241
|
+
if domains
|
|
242
|
+
else "files, ids or pasted text"
|
|
243
|
+
)
|
|
244
|
+
click.echo(f"{info['name']:18} {info['description']} [{where}]")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def register(cli_group: click.Group) -> None:
|
|
248
|
+
cli_group.add_command(import_command)
|
|
249
|
+
cli_group.add_command(search_command)
|
|
250
|
+
cli_group.add_command(importers_command)
|