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.
Files changed (85) hide show
  1. {rhylthyme_cli_runner-0.2.2a0/src/rhylthyme_cli_runner.egg-info → rhylthyme_cli_runner-0.2.4a0}/PKG-INFO +135 -2
  2. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/README.md +134 -1
  3. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/setup.py +1 -1
  4. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/__init__.py +1 -1
  5. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/cli.py +10 -0
  6. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/render.py +9 -0
  7. rhylthyme_cli_runner-0.2.4a0/src/rhylthyme_cli_runner/import_cli.py +250 -0
  8. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/remote/cli.py +58 -8
  9. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0/src/rhylthyme_cli_runner.egg-info}/PKG-INFO +135 -2
  10. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner.egg-info/SOURCES.txt +2 -0
  11. rhylthyme_cli_runner-0.2.4a0/tests/test_import_cli.py +228 -0
  12. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_remote.py +55 -0
  13. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_skill.py +11 -0
  14. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/LICENSE +0 -0
  15. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/pyproject.toml +0 -0
  16. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/setup.cfg +0 -0
  17. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/environment_icons.py +0 -0
  18. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/environment_loader.py +0 -0
  19. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/environment_schemas.py +0 -0
  20. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/__init__.py +0 -0
  21. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/cli.py +0 -0
  22. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/compare.py +0 -0
  23. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/gold.py +0 -0
  24. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/harness.py +0 -0
  25. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/llm.py +0 -0
  26. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/matcher.py +0 -0
  27. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/metrics.py +0 -0
  28. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/__init__.py +0 -0
  29. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/authoring_guide.md +0 -0
  30. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.md +0 -0
  31. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/baseline.py +0 -0
  32. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/extract.py +0 -0
  33. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/patterns/four_turn.py +0 -0
  34. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/eval/report.py +0 -0
  35. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/expand_replicates.py +0 -0
  36. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/__init__.py +0 -0
  37. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/calibrate.py +0 -0
  38. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/calibrate_cli.py +0 -0
  39. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/cli.py +0 -0
  40. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/evaluate.py +0 -0
  41. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/evaluate_cli.py +0 -0
  42. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/factors.py +0 -0
  43. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/hash.py +0 -0
  44. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/predict.py +0 -0
  45. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/recorder.py +0 -0
  46. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/replay.py +0 -0
  47. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/report.py +0 -0
  48. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/report_cli.py +0 -0
  49. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/store.py +0 -0
  50. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/synth.py +0 -0
  51. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/history/usable.py +0 -0
  52. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/instance_checks.py +0 -0
  53. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/program_planner.py +0 -0
  54. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/program_runner.py +0 -0
  55. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/remote/__init__.py +0 -0
  56. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/remote/auth.py +0 -0
  57. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/remote/checks.py +0 -0
  58. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/remote/mcp_client.py +0 -0
  59. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner/validate_program.py +0 -0
  60. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner.egg-info/dependency_links.txt +0 -0
  61. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner.egg-info/entry_points.txt +0 -0
  62. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner.egg-info/requires.txt +0 -0
  63. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/src/rhylthyme_cli_runner.egg-info/top_level.txt +0 -0
  64. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_calibrate.py +0 -0
  65. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_cli.py +0 -0
  66. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_eval_baseline.py +0 -0
  67. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_eval_harness.py +0 -0
  68. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_eval_providers.py +0 -0
  69. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_eval_scorer.py +0 -0
  70. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_evaluate.py +0 -0
  71. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_example_validation.py +0 -0
  72. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_examples_integration.py +0 -0
  73. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_expand_replicates.py +0 -0
  74. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_hash_parity.py +0 -0
  75. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_history_report.py +0 -0
  76. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_mcp_checks.py +0 -0
  77. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_planner_time_strings.py +0 -0
  78. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_predict.py +0 -0
  79. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_program_runner.py +0 -0
  80. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_replay.py +0 -0
  81. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_run_recorder.py +0 -0
  82. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_synth.py +0 -0
  83. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_usable.py +0 -0
  84. {rhylthyme_cli_runner-0.2.2a0 → rhylthyme_cli_runner-0.2.4a0}/tests/test_validate_examples_ci.py +0 -0
  85. {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.2a0
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
+ ![Eighth birthday party: the party, the kitchen and the setup crew on a wall-clock axis](https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.png)
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 as a live timeline and prints its URL. No sign-in.
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
+ ![Eighth birthday party: the party, the kitchen and the setup crew on a wall-clock axis](https://raw.githubusercontent.com/rhylthyme/rhylthyme-cli-runner/main/examples/recipe/birthday-party.png)
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 as a live timeline and prints its URL. No sign-in.
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.2a0",
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",
@@ -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.2a0"
9
+ __version__ = "0.2.4a0"
10
10
  __author__ = "Rhylthyme Team"
11
11
  __description__ = "CLI runner for Rhylthyme real-time program schedules"
12
12
 
@@ -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)