chartremotely 0.3.1__tar.gz → 0.3.2__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 (65) hide show
  1. {chartremotely-0.3.1 → chartremotely-0.3.2}/PKG-INFO +24 -15
  2. {chartremotely-0.3.1 → chartremotely-0.3.2}/README.md +23 -14
  3. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/mcpclient.py +4 -1
  4. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely.egg-info/PKG-INFO +24 -15
  5. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely.egg-info/SOURCES.txt +1 -0
  6. {chartremotely-0.3.1 → chartremotely-0.3.2}/pyproject.toml +1 -1
  7. chartremotely-0.3.2/tests/test_changelog.py +85 -0
  8. {chartremotely-0.3.1 → chartremotely-0.3.2}/LICENSE +0 -0
  9. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/__init__.py +0 -0
  10. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/answer.py +0 -0
  11. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/assets/shortcut.plist +0 -0
  12. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/ax.py +0 -0
  13. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/cli.py +0 -0
  14. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/config.py +0 -0
  15. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/displays.py +0 -0
  16. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/doctor.py +0 -0
  17. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/guilock.py +0 -0
  18. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/hearing.py +0 -0
  19. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/keystore.py +0 -0
  20. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/layout.py +0 -0
  21. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/patron.py +0 -0
  22. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/popups.py +0 -0
  23. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/push.py +0 -0
  24. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/recent.py +0 -0
  25. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/registry.py +0 -0
  26. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/relay.py +0 -0
  27. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/requestlog.py +0 -0
  28. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/resolve.py +0 -0
  29. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/scales.py +0 -0
  30. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/screen.py +0 -0
  31. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/server.py +0 -0
  32. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/services.py +0 -0
  33. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/setup.py +0 -0
  34. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/shortcut.py +0 -0
  35. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/snapshot.py +0 -0
  36. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/studies.py +0 -0
  37. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/symbol.py +0 -0
  38. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/tailnet.py +0 -0
  39. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/terminal.py +0 -0
  40. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/timeframe.py +0 -0
  41. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/understand.py +0 -0
  42. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/vocab.py +0 -0
  43. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/voice.py +0 -0
  44. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely/window.py +0 -0
  45. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely.egg-info/dependency_links.txt +0 -0
  46. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely.egg-info/entry_points.txt +0 -0
  47. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely.egg-info/requires.txt +0 -0
  48. {chartremotely-0.3.1 → chartremotely-0.3.2}/chartremotely.egg-info/top_level.txt +0 -0
  49. {chartremotely-0.3.1 → chartremotely-0.3.2}/setup.cfg +0 -0
  50. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_hearing.py +0 -0
  51. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_patron.py +0 -0
  52. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_popups.py +0 -0
  53. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_push.py +0 -0
  54. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_recent.py +0 -0
  55. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_relay.py +0 -0
  56. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_requestlog.py +0 -0
  57. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_resolve.py +0 -0
  58. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_scales.py +0 -0
  59. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_screen.py +0 -0
  60. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_setup.py +0 -0
  61. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_snapshot.py +0 -0
  62. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_stamp_changelog.py +0 -0
  63. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_timeframe.py +0 -0
  64. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_understand.py +0 -0
  65. {chartremotely-0.3.1 → chartremotely-0.3.2}/tests/test_voice.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartremotely
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Remote control of desktop charting applications on macOS
5
5
  Author: Lonnie VanZandt
6
6
  License:
@@ -305,6 +305,7 @@ chartremotely serve run the local listener
305
305
  chartremotely pair adopt this display to an operator
306
306
  chartremotely relay hold a connection open for the operator
307
307
  chartremotely doctor check everything the agent needs
308
+ chartremotely token print the local listener's access token
308
309
  ```
309
310
 
310
311
  `serve` and `relay` are two different transports and most setups want both.
@@ -421,19 +422,23 @@ curl -fsSL https://chartremotely.tollbooth-dpyc.com/install.sh | sh
421
422
 
422
423
  That installs [uv](https://docs.astral.sh/uv/) if it is missing, installs this
423
424
  agent from PyPI (`uv tool install --python 3.12 'chartremotely[macos,setup]'`),
424
- and runs `chartremotely setup`, which:
425
-
426
- 1. proves who owns the display — your existing npub by answering a Nostr DM,
427
- a key already saved on this Mac, or a new key it makes and saves for you
428
- in the Keychain and, through Safari, in iCloud Passwords;
429
- 2. pairs this Mac with that identity, with no code to copy;
430
- 3. gives the Mac its tailnet address with `tailscale serve`;
431
- 4. installs the listener and the relay as launchd agents;
432
- 5. asks macOS for Accessibility and Screen Recording for the Python that runs them;
433
- 6. fetches the Whisper speech model (about 1.6 GB, once) and your display names;
434
- 7. makes this Mac's Shortcut, "ChartRemotely" (talk, or type when it asks),
435
- opens it for import, and removes a "ChartRemotely Ask" an older setup left
436
- in your library.
425
+ and runs `chartremotely setup`. Setup first says what is installing — version,
426
+ where it came from, the Python it runs on — lists its six steps, and asks
427
+ before changing anything:
428
+
429
+ 1. **Identity** — proves who owns the display (your existing npub by answering
430
+ a Nostr DM, a key already saved on this Mac, or a new key it makes and saves
431
+ for you in the Keychain and, through Safari, in iCloud Passwords), then
432
+ pairs this Mac with that identity, with no code to copy;
433
+ 2. **Tailscale** — gives the Mac its tailnet address with `tailscale serve`;
434
+ 3. **Services** — installs the listener and the relay as launchd agents;
435
+ 4. **Permissions** — asks macOS for Accessibility and Screen Recording for the
436
+ Python that runs them;
437
+ 5. **Hearing** — fetches the Whisper speech model (about 1.6 GB, once) and
438
+ your display names;
439
+ 6. **Shortcut** — makes this Mac's Shortcut, "ChartRemotely" (talk, or type
440
+ when it asks), opens it for import, and removes a "ChartRemotely Ask" an
441
+ older setup left in your library.
437
442
 
438
443
  Run it again at any time: finished steps are skipped. `chartremotely doctor`
439
444
  reports anything still missing.
@@ -473,7 +478,11 @@ switch to a grid - discovery follows, and takes about a second.
473
478
 
474
479
  ## Status
475
480
 
476
- Early. The thinkorswim adapter works; the interfaces are still moving.
481
+ In service. Releases are published to PyPI and installed with the one-line
482
+ installer above; a paired Mac takes commands by voice, from the web app at
483
+ <https://chartremotely.tollbooth-dpyc.com>, and from any MCP client through the
484
+ operator ([ChartRemotely-mcp](https://github.com/lonniev/ChartRemotely-mcp)).
485
+ thinkorswim is the one charting application it drives today.
477
486
 
478
487
  ## License
479
488
 
@@ -78,6 +78,7 @@ chartremotely serve run the local listener
78
78
  chartremotely pair adopt this display to an operator
79
79
  chartremotely relay hold a connection open for the operator
80
80
  chartremotely doctor check everything the agent needs
81
+ chartremotely token print the local listener's access token
81
82
  ```
82
83
 
83
84
  `serve` and `relay` are two different transports and most setups want both.
@@ -194,19 +195,23 @@ curl -fsSL https://chartremotely.tollbooth-dpyc.com/install.sh | sh
194
195
 
195
196
  That installs [uv](https://docs.astral.sh/uv/) if it is missing, installs this
196
197
  agent from PyPI (`uv tool install --python 3.12 'chartremotely[macos,setup]'`),
197
- and runs `chartremotely setup`, which:
198
-
199
- 1. proves who owns the display — your existing npub by answering a Nostr DM,
200
- a key already saved on this Mac, or a new key it makes and saves for you
201
- in the Keychain and, through Safari, in iCloud Passwords;
202
- 2. pairs this Mac with that identity, with no code to copy;
203
- 3. gives the Mac its tailnet address with `tailscale serve`;
204
- 4. installs the listener and the relay as launchd agents;
205
- 5. asks macOS for Accessibility and Screen Recording for the Python that runs them;
206
- 6. fetches the Whisper speech model (about 1.6 GB, once) and your display names;
207
- 7. makes this Mac's Shortcut, "ChartRemotely" (talk, or type when it asks),
208
- opens it for import, and removes a "ChartRemotely Ask" an older setup left
209
- in your library.
198
+ and runs `chartremotely setup`. Setup first says what is installing — version,
199
+ where it came from, the Python it runs on — lists its six steps, and asks
200
+ before changing anything:
201
+
202
+ 1. **Identity** — proves who owns the display (your existing npub by answering
203
+ a Nostr DM, a key already saved on this Mac, or a new key it makes and saves
204
+ for you in the Keychain and, through Safari, in iCloud Passwords), then
205
+ pairs this Mac with that identity, with no code to copy;
206
+ 2. **Tailscale** — gives the Mac its tailnet address with `tailscale serve`;
207
+ 3. **Services** — installs the listener and the relay as launchd agents;
208
+ 4. **Permissions** — asks macOS for Accessibility and Screen Recording for the
209
+ Python that runs them;
210
+ 5. **Hearing** — fetches the Whisper speech model (about 1.6 GB, once) and
211
+ your display names;
212
+ 6. **Shortcut** — makes this Mac's Shortcut, "ChartRemotely" (talk, or type
213
+ when it asks), opens it for import, and removes a "ChartRemotely Ask" an
214
+ older setup left in your library.
210
215
 
211
216
  Run it again at any time: finished steps are skipped. `chartremotely doctor`
212
217
  reports anything still missing.
@@ -246,7 +251,11 @@ switch to a grid - discovery follows, and takes about a second.
246
251
 
247
252
  ## Status
248
253
 
249
- Early. The thinkorswim adapter works; the interfaces are still moving.
254
+ In service. Releases are published to PyPI and installed with the one-line
255
+ installer above; a paired Mac takes commands by voice, from the web app at
256
+ <https://chartremotely.tollbooth-dpyc.com>, and from any MCP client through the
257
+ operator ([ChartRemotely-mcp](https://github.com/lonniev/ChartRemotely-mcp)).
258
+ thinkorswim is the one charting application it drives today.
250
259
 
251
260
  ## License
252
261
 
@@ -60,8 +60,11 @@ class Client:
60
60
  self._ids = itertools.count(1)
61
61
 
62
62
  def _send(self, message: dict) -> dict:
63
+ # The operator refuses a request with no bearer. The agent proves
64
+ # itself in its arguments, so at the connection it is anonymous.
63
65
  headers = {"Content-Type": "application/json",
64
- "Accept": "application/json, text/event-stream"}
66
+ "Accept": "application/json, text/event-stream",
67
+ "Authorization": "Bearer anonymous"}
65
68
  if self.session:
66
69
  headers["mcp-session-id"] = self.session
67
70
  request = urllib.request.Request(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartremotely
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Remote control of desktop charting applications on macOS
5
5
  Author: Lonnie VanZandt
6
6
  License:
@@ -305,6 +305,7 @@ chartremotely serve run the local listener
305
305
  chartremotely pair adopt this display to an operator
306
306
  chartremotely relay hold a connection open for the operator
307
307
  chartremotely doctor check everything the agent needs
308
+ chartremotely token print the local listener's access token
308
309
  ```
309
310
 
310
311
  `serve` and `relay` are two different transports and most setups want both.
@@ -421,19 +422,23 @@ curl -fsSL https://chartremotely.tollbooth-dpyc.com/install.sh | sh
421
422
 
422
423
  That installs [uv](https://docs.astral.sh/uv/) if it is missing, installs this
423
424
  agent from PyPI (`uv tool install --python 3.12 'chartremotely[macos,setup]'`),
424
- and runs `chartremotely setup`, which:
425
-
426
- 1. proves who owns the display — your existing npub by answering a Nostr DM,
427
- a key already saved on this Mac, or a new key it makes and saves for you
428
- in the Keychain and, through Safari, in iCloud Passwords;
429
- 2. pairs this Mac with that identity, with no code to copy;
430
- 3. gives the Mac its tailnet address with `tailscale serve`;
431
- 4. installs the listener and the relay as launchd agents;
432
- 5. asks macOS for Accessibility and Screen Recording for the Python that runs them;
433
- 6. fetches the Whisper speech model (about 1.6 GB, once) and your display names;
434
- 7. makes this Mac's Shortcut, "ChartRemotely" (talk, or type when it asks),
435
- opens it for import, and removes a "ChartRemotely Ask" an older setup left
436
- in your library.
425
+ and runs `chartremotely setup`. Setup first says what is installing — version,
426
+ where it came from, the Python it runs on — lists its six steps, and asks
427
+ before changing anything:
428
+
429
+ 1. **Identity** — proves who owns the display (your existing npub by answering
430
+ a Nostr DM, a key already saved on this Mac, or a new key it makes and saves
431
+ for you in the Keychain and, through Safari, in iCloud Passwords), then
432
+ pairs this Mac with that identity, with no code to copy;
433
+ 2. **Tailscale** — gives the Mac its tailnet address with `tailscale serve`;
434
+ 3. **Services** — installs the listener and the relay as launchd agents;
435
+ 4. **Permissions** — asks macOS for Accessibility and Screen Recording for the
436
+ Python that runs them;
437
+ 5. **Hearing** — fetches the Whisper speech model (about 1.6 GB, once) and
438
+ your display names;
439
+ 6. **Shortcut** — makes this Mac's Shortcut, "ChartRemotely" (talk, or type
440
+ when it asks), opens it for import, and removes a "ChartRemotely Ask" an
441
+ older setup left in your library.
437
442
 
438
443
  Run it again at any time: finished steps are skipped. `chartremotely doctor`
439
444
  reports anything still missing.
@@ -473,7 +478,11 @@ switch to a grid - discovery follows, and takes about a second.
473
478
 
474
479
  ## Status
475
480
 
476
- Early. The thinkorswim adapter works; the interfaces are still moving.
481
+ In service. Releases are published to PyPI and installed with the one-line
482
+ installer above; a paired Mac takes commands by voice, from the web app at
483
+ <https://chartremotely.tollbooth-dpyc.com>, and from any MCP client through the
484
+ operator ([ChartRemotely-mcp](https://github.com/lonniev/ChartRemotely-mcp)).
485
+ thinkorswim is the one charting application it drives today.
477
486
 
478
487
  ## License
479
488
 
@@ -44,6 +44,7 @@ chartremotely.egg-info/entry_points.txt
44
44
  chartremotely.egg-info/requires.txt
45
45
  chartremotely.egg-info/top_level.txt
46
46
  chartremotely/assets/shortcut.plist
47
+ tests/test_changelog.py
47
48
  tests/test_hearing.py
48
49
  tests/test_patron.py
49
50
  tests/test_popups.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "chartremotely"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  description = "Remote control of desktop charting applications on macOS"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,85 @@
1
+ """The changelog must not be the reason two independent changes conflict.
2
+
3
+ `CHANGELOG.md` was the single most conflict-prone file in the fleet, and not
4
+ through carelessness: every pull request appended to the same
5
+ `### Added` / `### Fixed` anchors of the same `## [Unreleased]` section, so any
6
+ two concurrent PRs collided there even when their code touched nothing in
7
+ common. Six did in `beesknees-mcp` in one day.
8
+
9
+ If A and B are independent, it must not matter whether they land A-then-B or
10
+ B-then-A. These tests hold that as a property rather than as an intention.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import importlib.util
16
+ import pathlib
17
+ import re
18
+
19
+ import pytest
20
+
21
+ ROOT = pathlib.Path(__file__).resolve().parents[1]
22
+
23
+
24
+ def _module():
25
+ spec = importlib.util.spec_from_file_location("changelog", ROOT / "scripts" / "changelog.py")
26
+ assert spec and spec.loader
27
+ mod = importlib.util.module_from_spec(spec)
28
+ spec.loader.exec_module(mod)
29
+ return mod
30
+
31
+
32
+ cl = _module()
33
+
34
+
35
+ def _write(d: pathlib.Path, name: str, body: str) -> None:
36
+ (d / name).write_text(body)
37
+
38
+
39
+ def test_the_order_two_changes_land_in_does_not_reach_the_output(tmp_path) -> None:
40
+ """The property the whole change exists for.
41
+
42
+ Two independent entries, folded in both possible orders, must produce the
43
+ same section — otherwise the changelog still carries a fact about merge
44
+ order that nobody needs and everybody has to resolve.
45
+ """
46
+ first, second = tmp_path / "a", tmp_path / "b"
47
+ for d in (first, second):
48
+ d.mkdir()
49
+ _write(first, "fixed-alpha.md", "- Alpha was broken and is not.\n")
50
+ _write(first, "fixed-beta.md", "- Beta was broken and is not.\n")
51
+ # The same two, created in the opposite order.
52
+ _write(second, "fixed-beta.md", "- Beta was broken and is not.\n")
53
+ _write(second, "fixed-alpha.md", "- Alpha was broken and is not.\n")
54
+
55
+ a = cl.section("1.0.0", cl.read_fragments(first), "2026-01-01")
56
+ b = cl.section("1.0.0", cl.read_fragments(second), "2026-01-01")
57
+ assert a == b
58
+
59
+
60
+ def test_kinds_are_grouped_in_keep_a_changelog_order(tmp_path) -> None:
61
+ _write(tmp_path, "fixed-one.md", "- A fix.\n")
62
+ _write(tmp_path, "added-two.md", "- An addition.\n")
63
+ _write(tmp_path, "notes-three.md", "- Some reasoning.\n")
64
+
65
+ out = cl.section("2.1.0", cl.read_fragments(tmp_path), "2026-01-01")
66
+ assert out.startswith("## [2.1.0] — 2026-01-01")
67
+ order = [m.group(1) for m in re.finditer(r"^### (\w+)", out, re.MULTILINE)]
68
+ assert order == ["Added", "Fixed", "Notes"], order
69
+ # Nothing empty is left standing.
70
+ assert "### Changed" not in out
71
+
72
+
73
+ def test_a_misnamed_fragment_is_refused_rather_than_dropped(tmp_path) -> None:
74
+ """A changelog that quietly loses an entry is worse than one that refuses
75
+ to build — the entry is somebody's account of what they did to the money."""
76
+ _write(tmp_path, "fxied-typo.md", "- Something real.\n")
77
+ with pytest.raises(SystemExit, match="unknown kind"):
78
+ cl.read_fragments(tmp_path)
79
+
80
+
81
+ def test_the_new_section_goes_above_the_ones_already_there(tmp_path) -> None:
82
+ existing = "# Changelog\n\nPreamble.\n\n## [0.9.0] — 2025-01-01\n\n### Fixed\n\n- Older.\n"
83
+ out = cl.fold(existing, "## [1.0.0] — 2026-01-01\n\n### Added\n\n- Newer.\n")
84
+ assert out.index("## [1.0.0]") < out.index("## [0.9.0]")
85
+ assert "Preamble." in out and "- Older." in out
File without changes
File without changes