scrybe-cli 0.5.0__tar.gz → 0.6.1__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 (92) hide show
  1. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/Cargo.lock +26 -16
  2. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/Cargo.toml +12 -11
  3. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/PKG-INFO +3 -3
  4. {scrybe_cli-0.5.0/scrybe-cli → scrybe_cli-0.6.1}/README.md +2 -2
  5. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/pyproject.toml +1 -1
  6. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/python/scrybe_cli/__init__.py +9 -2
  7. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-cli/Cargo.toml +0 -2
  8. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1/scrybe-cli}/README.md +2 -2
  9. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-cli/src/main.rs +171 -78
  10. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-cli/src/rpc_client.rs +2 -1
  11. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-cli/tests/rpc_socket.rs +50 -43
  12. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-core/README.md +6 -3
  13. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-core/src/ast.rs +220 -10
  14. scrybe_cli-0.6.1/scrybe-core/src/change.rs +830 -0
  15. scrybe_cli-0.6.1/scrybe-core/src/content.rs +236 -0
  16. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-core/src/document.rs +27 -8
  17. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-core/src/error.rs +2 -0
  18. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-core/src/lib.rs +7 -3
  19. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mcp-server/Cargo.toml +0 -1
  20. scrybe_cli-0.6.1/scrybe-mcp-server/README.md +57 -0
  21. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mcp-server/pyproject.toml +1 -1
  22. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mcp-server/python/scrybe_mcp_server/__init__.py +8 -1
  23. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mcp-server/src/lib.rs +3 -3
  24. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mcp-server/src/main.rs +3 -4
  25. scrybe_cli-0.6.1/scrybe-mcp-server/src/server.rs +779 -0
  26. scrybe_cli-0.6.1/scrybe-mcp-server/tests/fixtures/call_invalid_args.json +28 -0
  27. scrybe_cli-0.6.1/scrybe-mcp-server/tests/fixtures/call_lint_success.json +40 -0
  28. scrybe_cli-0.6.1/scrybe-mcp-server/tests/fixtures/call_no_live_app.json +30 -0
  29. scrybe_cli-0.6.1/scrybe-mcp-server/tests/fixtures/call_remote_app_error.json +32 -0
  30. scrybe_cli-0.6.1/scrybe-mcp-server/tests/fixtures/call_unknown_tool.json +19 -0
  31. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mcp-server/tests/live_app_open.rs +44 -16
  32. scrybe_cli-0.6.1/scrybe-mcp-server/tests/mcp_contract.rs +336 -0
  33. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mermaid/README.md +24 -9
  34. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mermaid/pyproject.toml +1 -1
  35. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mermaid/src/codec.rs +200 -4
  36. scrybe_cli-0.6.1/scrybe-mermaid/src/error.rs +31 -0
  37. scrybe_cli-0.6.1/scrybe-mermaid/src/lib.rs +233 -0
  38. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mermaid/tests/test_python_quickstart.py +24 -0
  39. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-render/src/html.rs +60 -0
  40. scrybe_cli-0.6.1/scrybe-rpc/src/client.rs +544 -0
  41. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-rpc/src/lib.rs +123 -16
  42. scrybe_cli-0.6.1/scrybe-rpc/tests/wire_contract.rs +338 -0
  43. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-tools/Cargo.toml +5 -0
  44. scrybe_cli-0.6.1/scrybe-tools/src/figures.rs +562 -0
  45. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-tools/src/lib.rs +82 -22
  46. scrybe_cli-0.6.1/scrybe-tools/src/schema.rs +103 -0
  47. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-tools/src/tools/editor.rs +173 -17
  48. scrybe_cli-0.6.1/scrybe-tools/src/tools/export.rs +404 -0
  49. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-tools/src/tools/lint.rs +27 -19
  50. scrybe_cli-0.6.1/scrybe-tools/src/tools/mermaid.rs +581 -0
  51. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-tools/src/tools/mod.rs +10 -1
  52. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-tools/src/tools/render.rs +11 -12
  53. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-tools/src/tools/tabs.rs +27 -21
  54. scrybe_cli-0.6.1/scrybe-tools/src/tools/ui.rs +443 -0
  55. scrybe_cli-0.5.0/scrybe-core/src/change.rs +0 -304
  56. scrybe_cli-0.5.0/scrybe-core/src/content.rs +0 -73
  57. scrybe_cli-0.5.0/scrybe-mcp-server/README.md +0 -66
  58. scrybe_cli-0.5.0/scrybe-mcp-server/src/server.rs +0 -451
  59. scrybe_cli-0.5.0/scrybe-mcp-server/src/tools.rs +0 -1245
  60. scrybe_cli-0.5.0/scrybe-mermaid/src/error.rs +0 -16
  61. scrybe_cli-0.5.0/scrybe-mermaid/src/lib.rs +0 -119
  62. scrybe_cli-0.5.0/scrybe-rpc/src/client.rs +0 -184
  63. scrybe_cli-0.5.0/scrybe-tools/src/tools/mermaid.rs +0 -201
  64. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/python/scrybe_cli/py.typed +0 -0
  65. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-cli/src/lib.rs +0 -0
  66. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-cli/src/wrap.rs +0 -0
  67. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-core/Cargo.toml +0 -0
  68. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-core/src/plugin.rs +0 -0
  69. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-core/src/workspace.rs +0 -0
  70. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mcp-server/python/scrybe_mcp_server/py.typed +0 -0
  71. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mcp-server/tests/docx_packaging.rs +0 -0
  72. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mermaid/Cargo.toml +0 -0
  73. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mermaid/python/scrybe_mermaid/__init__.py +0 -0
  74. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mermaid/python/scrybe_mermaid/py.typed +0 -0
  75. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mermaid-render/Cargo.toml +0 -0
  76. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mermaid-render/README.md +0 -0
  77. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mermaid-render/examples/provenance_drive.rs +0 -0
  78. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-mermaid-render/src/lib.rs +0 -0
  79. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-render/Cargo.toml +0 -0
  80. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-render/README.md +0 -0
  81. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-render/src/lib.rs +0 -0
  82. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-render/src/math.rs +0 -0
  83. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-render/src/mermaid.rs +0 -0
  84. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-render/src/theme.rs +0 -0
  85. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-render/src/themes/dark.css +0 -0
  86. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-render/src/themes/default.css +0 -0
  87. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-render/src/themes/solarized.css +0 -0
  88. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-rpc/Cargo.toml +0 -0
  89. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-rpc/README.md +0 -0
  90. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-tools/README.md +0 -0
  91. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-tools/examples/render_drive.rs +0 -0
  92. {scrybe_cli-0.5.0 → scrybe_cli-0.6.1}/scrybe-tools/src/lint.rs +0 -0
@@ -3906,7 +3906,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
3906
3906
 
3907
3907
  [[package]]
3908
3908
  name = "scrybe-app"
3909
- version = "0.5.0"
3909
+ version = "0.6.1"
3910
3910
  dependencies = [
3911
3911
  "anyhow",
3912
3912
  "dirs 5.0.1",
@@ -3916,6 +3916,7 @@ dependencies = [
3916
3916
  "scrybe-mcp-server",
3917
3917
  "scrybe-render",
3918
3918
  "scrybe-rpc",
3919
+ "scrybe-tools",
3919
3920
  "scrybe-vcs",
3920
3921
  "serde",
3921
3922
  "serde_json",
@@ -3933,11 +3934,10 @@ dependencies = [
3933
3934
 
3934
3935
  [[package]]
3935
3936
  name = "scrybe-cli"
3936
- version = "0.5.0"
3937
+ version = "0.6.1"
3937
3938
  dependencies = [
3938
3939
  "anyhow",
3939
3940
  "clap",
3940
- "hex",
3941
3941
  "scrybe-core",
3942
3942
  "scrybe-mcp-server",
3943
3943
  "scrybe-mermaid",
@@ -3946,14 +3946,13 @@ dependencies = [
3946
3946
  "scrybe-tools",
3947
3947
  "serde",
3948
3948
  "serde_json",
3949
- "sha2",
3950
3949
  "tracing",
3951
3950
  "tracing-subscriber",
3952
3951
  ]
3953
3952
 
3954
3953
  [[package]]
3955
3954
  name = "scrybe-core"
3956
- version = "0.5.0"
3955
+ version = "0.6.1"
3957
3956
  dependencies = [
3958
3957
  "anyhow",
3959
3958
  "blake3",
@@ -3970,7 +3969,7 @@ dependencies = [
3970
3969
 
3971
3970
  [[package]]
3972
3971
  name = "scrybe-mcp-client"
3973
- version = "0.5.0"
3972
+ version = "0.6.1"
3974
3973
  dependencies = [
3975
3974
  "anyhow",
3976
3975
  "dirs 5.0.1",
@@ -3985,7 +3984,7 @@ dependencies = [
3985
3984
 
3986
3985
  [[package]]
3987
3986
  name = "scrybe-mcp-server"
3988
- version = "0.5.0"
3987
+ version = "0.6.1"
3989
3988
  dependencies = [
3990
3989
  "anyhow",
3991
3990
  "clap",
@@ -4001,12 +4000,11 @@ dependencies = [
4001
4000
  "tokio",
4002
4001
  "tracing",
4003
4002
  "tracing-subscriber",
4004
- "which",
4005
4003
  ]
4006
4004
 
4007
4005
  [[package]]
4008
4006
  name = "scrybe-mermaid"
4009
- version = "0.5.0"
4007
+ version = "0.6.1"
4010
4008
  dependencies = [
4011
4009
  "anyhow",
4012
4010
  "base64 0.22.1",
@@ -4023,7 +4021,7 @@ dependencies = [
4023
4021
 
4024
4022
  [[package]]
4025
4023
  name = "scrybe-mermaid-render"
4026
- version = "0.5.0"
4024
+ version = "0.6.1"
4027
4025
  dependencies = [
4028
4026
  "anyhow",
4029
4027
  "hex",
@@ -4033,7 +4031,7 @@ dependencies = [
4033
4031
 
4034
4032
  [[package]]
4035
4033
  name = "scrybe-py"
4036
- version = "0.5.0"
4034
+ version = "0.6.1"
4037
4035
  dependencies = [
4038
4036
  "anyhow",
4039
4037
  "pyo3",
@@ -4045,9 +4043,17 @@ dependencies = [
4045
4043
  "tracing",
4046
4044
  ]
4047
4045
 
4046
+ [[package]]
4047
+ name = "scrybe-ratatui"
4048
+ version = "0.6.1"
4049
+ dependencies = [
4050
+ "ratatui",
4051
+ "scrybe-core",
4052
+ ]
4053
+
4048
4054
  [[package]]
4049
4055
  name = "scrybe-render"
4050
- version = "0.5.0"
4056
+ version = "0.6.1"
4051
4057
  dependencies = [
4052
4058
  "anyhow",
4053
4059
  "pulldown-cmark",
@@ -4060,7 +4066,7 @@ dependencies = [
4060
4066
 
4061
4067
  [[package]]
4062
4068
  name = "scrybe-rpc"
4063
- version = "0.5.0"
4069
+ version = "0.6.1"
4064
4070
  dependencies = [
4065
4071
  "serde",
4066
4072
  "serde_json",
@@ -4069,8 +4075,9 @@ dependencies = [
4069
4075
 
4070
4076
  [[package]]
4071
4077
  name = "scrybe-tools"
4072
- version = "0.5.0"
4078
+ version = "0.6.1"
4073
4079
  dependencies = [
4080
+ "anyhow",
4074
4081
  "scrybe-core",
4075
4082
  "scrybe-mermaid",
4076
4083
  "scrybe-mermaid-render",
@@ -4078,24 +4085,27 @@ dependencies = [
4078
4085
  "scrybe-rpc",
4079
4086
  "serde",
4080
4087
  "serde_json",
4088
+ "tempfile",
4081
4089
  "thiserror 2.0.18",
4082
4090
  "uuid",
4091
+ "which",
4083
4092
  ]
4084
4093
 
4085
4094
  [[package]]
4086
4095
  name = "scrybe-tui"
4087
- version = "0.5.0"
4096
+ version = "0.6.1"
4088
4097
  dependencies = [
4089
4098
  "anyhow",
4090
4099
  "clap",
4091
4100
  "crossterm",
4092
4101
  "ratatui",
4093
4102
  "scrybe-core",
4103
+ "scrybe-ratatui",
4094
4104
  ]
4095
4105
 
4096
4106
  [[package]]
4097
4107
  name = "scrybe-vcs"
4098
- version = "0.5.0"
4108
+ version = "0.6.1"
4099
4109
  dependencies = [
4100
4110
  "anyhow",
4101
4111
  "chrono",
@@ -6,7 +6,7 @@ members = ["scrybe-core", "scrybe-render", "scrybe-mcp-server", "scrybe-mermaid"
6
6
  # `version.workspace = true`, so a release bumps all crates in lock-step
7
7
  # from one place. (See RELEASE.md; issue #128.)
8
8
  [workspace.package]
9
- version = "0.5.0"
9
+ version = "0.6.1"
10
10
 
11
11
  # Workspace-level dependency pins keep all crates in lock-step.
12
12
  [workspace.dependencies]
@@ -60,16 +60,17 @@ tauri-build = { version = "2", features = [] }
60
60
  # Internal crates — version pinned (=) so cargo publish accepts the
61
61
  # tarballs (path-deps without versions are rejected at publish time).
62
62
  # Workspace lock-step: every release bumps all members together.
63
- scrybe-core = { path = "scrybe-core", version = "=0.5.0" }
64
- scrybe-render = { path = "scrybe-render", version = "=0.5.0" }
65
- scrybe-mcp-client = { path = "scrybe-mcp-client", version = "=0.5.0" }
66
- scrybe-mcp-server = { path = "scrybe-mcp-server", version = "=0.5.0" }
67
- scrybe-mermaid = { path = "scrybe-mermaid", version = "=0.5.0" }
68
- scrybe-vcs = { path = "scrybe-vcs", version = "=0.5.0" }
69
- scrybe-py = { path = "scrybe-py", version = "=0.5.0" }
70
- scrybe-rpc = { path = "scrybe-rpc", version = "=0.5.0" }
71
- scrybe-tools = { path = "scrybe-tools", version = "=0.5.0" }
72
- scrybe-mermaid-render = { path = "scrybe-mermaid-render", version = "=0.5.0" }
63
+ scrybe-core = { path = "scrybe-core", version = "=0.6.1" }
64
+ scrybe-render = { path = "scrybe-render", version = "=0.6.1" }
65
+ scrybe-mcp-client = { path = "scrybe-mcp-client", version = "=0.6.1" }
66
+ scrybe-mcp-server = { path = "scrybe-mcp-server", version = "=0.6.1" }
67
+ scrybe-mermaid = { path = "scrybe-mermaid", version = "=0.6.1" }
68
+ scrybe-vcs = { path = "scrybe-vcs", version = "=0.6.1" }
69
+ scrybe-py = { path = "scrybe-py", version = "=0.6.1" }
70
+ scrybe-rpc = { path = "scrybe-rpc", version = "=0.6.1" }
71
+ scrybe-tools = { path = "scrybe-tools", version = "=0.6.1" }
72
+ scrybe-ratatui = { path = "scrybe-ratatui", version = "=0.6.1" }
73
+ scrybe-mermaid-render = { path = "scrybe-mermaid-render", version = "=0.6.1" }
73
74
 
74
75
  # Adopted pure-Rust Mermaid → SVG renderer (#132 spike-Pass, 2026-07-17).
75
76
  # SVG-only by default here; the `png` feature (resvg) is opted into per-crate.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrybe-cli
3
- Version: 0.5.0
3
+ Version: 0.6.1
4
4
  Summary: Scrybe CLI — headless Markdown render/lint/mermaid tool (binary wheel)
5
5
  Home-Page: https://github.com/hartsock/scrybe
6
6
  License: Apache-2.0
@@ -39,8 +39,8 @@ also the launcher for the Tauri GUI app: `scrybe file.md` locates the
39
39
  | `scrybe render [FILE]` | Render Markdown to HTML (stdin → stdout by default); `--theme`, `--full-html`, `--output` |
40
40
  | `scrybe lint FILE` | Word count, headings, code blocks, broken links; `--json` for machine output; exits 1 if broken links found |
41
41
  | `scrybe mermaid embed PNG SOURCE` | Embed Mermaid source into PNG iTXt chunk |
42
- | `scrybe mermaid extract PNG` | Print embedded Mermaid source |
43
- | `scrybe mermaid verify PNG` | Verify SHA-256 integrity of embedded source; exits 1 if tampered |
42
+ | `scrybe mermaid extract PNG` | Print embedded Mermaid source, verifying its SHA-256 by default; exit 2 if tampered, `--unverified` to skip the check (forensics) |
43
+ | `scrybe mermaid verify PNG` | Verify SHA-256 integrity of embedded source; exits 1 if tampered or digest missing |
44
44
  | `scrybe open [PATH]` | Launch the Scrybe GUI, optionally at a file or directory |
45
45
  | `scrybe version` | Print version and active feature flags |
46
46
  | `scrybe [PATH]` | Bare invocation with a path injects `open` automatically |
@@ -29,8 +29,8 @@ also the launcher for the Tauri GUI app: `scrybe file.md` locates the
29
29
  | `scrybe render [FILE]` | Render Markdown to HTML (stdin → stdout by default); `--theme`, `--full-html`, `--output` |
30
30
  | `scrybe lint FILE` | Word count, headings, code blocks, broken links; `--json` for machine output; exits 1 if broken links found |
31
31
  | `scrybe mermaid embed PNG SOURCE` | Embed Mermaid source into PNG iTXt chunk |
32
- | `scrybe mermaid extract PNG` | Print embedded Mermaid source |
33
- | `scrybe mermaid verify PNG` | Verify SHA-256 integrity of embedded source; exits 1 if tampered |
32
+ | `scrybe mermaid extract PNG` | Print embedded Mermaid source, verifying its SHA-256 by default; exit 2 if tampered, `--unverified` to skip the check (forensics) |
33
+ | `scrybe mermaid verify PNG` | Verify SHA-256 integrity of embedded source; exits 1 if tampered or digest missing |
34
34
  | `scrybe open [PATH]` | Launch the Scrybe GUI, optionally at a file or directory |
35
35
  | `scrybe version` | Print version and active feature flags |
36
36
  | `scrybe [PATH]` | Bare invocation with a path injects `open` automatically |
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "scrybe-cli"
7
- version = "0.5.0"
7
+ version = "0.6.1"
8
8
  description = "Scrybe CLI — headless Markdown render/lint/mermaid tool (binary wheel)"
9
9
  license = {text = "Apache-2.0"}
10
10
  readme = "README.md"
@@ -4,7 +4,7 @@ This package installs the `scrybe` command on your PATH, providing
4
4
  render, lint, mermaid embed/extract, and the GUI integration RPC client.
5
5
 
6
6
  For programmatic Python library access (Document, render_markdown,
7
- ContentId, etc.), install scrybe-py:
7
+ ContentDigest, etc.), install scrybe-py:
8
8
 
9
9
  pip install scrybe-py
10
10
  >>> import scrybe
@@ -30,7 +30,12 @@ except ImportError:
30
30
  # Soft dependency on scrybe-py: re-export when present so users can drive
31
31
  # the Rust library directly without juggling two imports.
32
32
  try:
33
- from scrybe import ContentId, Document, render_markdown # type: ignore[import-not-found]
33
+ from scrybe import ( # type: ignore[import-not-found]
34
+ ContentDigest,
35
+ ContentId, # deprecated alias for ContentDigest
36
+ Document,
37
+ render_markdown,
38
+ )
34
39
 
35
40
  _SCRYBE_LIB_AVAILABLE = True
36
41
  except ImportError:
@@ -52,12 +57,14 @@ except ImportError:
52
57
  def __call__(self, *args: object, **kwargs: object) -> None:
53
58
  raise ImportError(f"{self._name}: {_MISSING_MSG}")
54
59
 
60
+ ContentDigest = _Missing("ContentDigest") # type: ignore[assignment,misc]
55
61
  ContentId = _Missing("ContentId") # type: ignore[assignment,misc]
56
62
  Document = _Missing("Document") # type: ignore[assignment,misc]
57
63
  render_markdown = _Missing("render_markdown") # type: ignore[assignment]
58
64
 
59
65
 
60
66
  __all__ = [
67
+ "ContentDigest",
61
68
  "ContentId",
62
69
  "Document",
63
70
  "render_markdown",
@@ -28,8 +28,6 @@ scrybe-tools.workspace = true
28
28
  anyhow.workspace = true
29
29
  serde.workspace = true
30
30
  serde_json.workspace = true
31
- sha2.workspace = true
32
- hex.workspace = true
33
31
  tracing.workspace = true
34
32
  tracing-subscriber.workspace = true
35
33
  clap.workspace = true
@@ -29,8 +29,8 @@ also the launcher for the Tauri GUI app: `scrybe file.md` locates the
29
29
  | `scrybe render [FILE]` | Render Markdown to HTML (stdin → stdout by default); `--theme`, `--full-html`, `--output` |
30
30
  | `scrybe lint FILE` | Word count, headings, code blocks, broken links; `--json` for machine output; exits 1 if broken links found |
31
31
  | `scrybe mermaid embed PNG SOURCE` | Embed Mermaid source into PNG iTXt chunk |
32
- | `scrybe mermaid extract PNG` | Print embedded Mermaid source |
33
- | `scrybe mermaid verify PNG` | Verify SHA-256 integrity of embedded source; exits 1 if tampered |
32
+ | `scrybe mermaid extract PNG` | Print embedded Mermaid source, verifying its SHA-256 by default; exit 2 if tampered, `--unverified` to skip the check (forensics) |
33
+ | `scrybe mermaid verify PNG` | Verify SHA-256 integrity of embedded source; exits 1 if tampered or digest missing |
34
34
  | `scrybe open [PATH]` | Launch the Scrybe GUI, optionally at a file or directory |
35
35
  | `scrybe version` | Print version and active feature flags |
36
36
  | `scrybe [PATH]` | Bare invocation with a path injects `open` automatically |
@@ -288,10 +288,17 @@ enum Command {
288
288
 
289
289
  /// Extract Mermaid source from a PNG iTXt metadata chunk.
290
290
  ///
291
- /// Top-level shortcut for `scrybe mermaid extract`. Same args.
291
+ /// Top-level shortcut for `scrybe mermaid extract`. Same args and exit
292
+ /// codes: 0 = success, 1 = extraction failed (no payload / unreadable
293
+ /// PNG), 2 = verification failed (stored sha256 does not match the
294
+ /// extracted source).
292
295
  Extract {
293
296
  #[arg(value_name = "PNG")]
294
297
  png: std::path::PathBuf,
298
+ /// Skip digest verification and print the raw stored source
299
+ /// (forensics on tampered payloads).
300
+ #[arg(long)]
301
+ unverified: bool,
295
302
  },
296
303
 
297
304
  /// Print version and active feature flags.
@@ -309,10 +316,21 @@ enum MermaidCmd {
309
316
  #[arg(short, long)]
310
317
  out: Option<std::path::PathBuf>,
311
318
  },
312
- /// Extract Mermaid source from a PNG.
319
+ /// Extract Mermaid source from a PNG, verifying its sha256 by default.
320
+ ///
321
+ /// The stored digest is checked against a freshly-computed hash of the
322
+ /// extracted source. Exit codes: 0 = success (a payload with no stored
323
+ /// digest prints a note to stderr), 1 = extraction failed (no payload /
324
+ /// unreadable PNG), 2 = verification failed (tampered — stored sha256
325
+ /// does not match the extracted source). Use --unverified to skip the
326
+ /// check and print the raw stored source for forensics.
313
327
  Extract {
314
328
  #[arg(value_name = "PNG")]
315
329
  png: std::path::PathBuf,
330
+ /// Skip digest verification and print the raw stored source
331
+ /// (forensics on tampered payloads).
332
+ #[arg(long)]
333
+ unverified: bool,
316
334
  },
317
335
  /// Verify the sha256 integrity of the embedded Mermaid source in a PNG.
318
336
  ///
@@ -336,6 +354,20 @@ enum MermaidCmd {
336
354
  #[arg(short, long, value_name = "OUT")]
337
355
  out: std::path::PathBuf,
338
356
  },
357
+ /// Export every Mermaid diagram in a document to sibling PNG figures.
358
+ ///
359
+ /// For `foo.md`, writes `foo_fig_01.png`, `foo_fig_02.png`, … next to the
360
+ /// document in document order; each PNG embeds its Mermaid source (uuid +
361
+ /// sha256) so it round-trips via `scrybe mermaid extract`. Pure-Rust
362
+ /// rendering (no `mmdc`) via the shared `export_figures` tool.
363
+ Export {
364
+ /// Markdown document whose Mermaid diagrams to export.
365
+ #[arg(value_name = "FILE")]
366
+ input: std::path::PathBuf,
367
+ /// Output JSON (`{count, figures}`) instead of one path per line.
368
+ #[arg(long)]
369
+ json: bool,
370
+ },
339
371
  }
340
372
 
341
373
  /// Known subcommand names. Anything else in argv[1] is treated as a path to open.
@@ -443,12 +475,12 @@ fn main() -> anyhow::Result<()> {
443
475
  for bl in &report.broken_links {
444
476
  eprintln!(" - [{}]({})", bl.text, bl.url);
445
477
  }
446
- // Also print content-id.
478
+ // Also print the content digest.
447
479
  eprintln!(
448
- "{:<30} {} (CID {})",
480
+ "{:<30} {} (digest {})",
449
481
  "File:",
450
482
  input.display(),
451
- doc.content_id()
483
+ doc.content_digest()
452
484
  );
453
485
  }
454
486
 
@@ -465,35 +497,42 @@ fn main() -> anyhow::Result<()> {
465
497
  std::fs::write(&dest, &embedded)?;
466
498
  println!("Embedded into {}", dest.display());
467
499
  }
468
- MermaidCmd::Extract { png } => {
469
- let bytes = std::fs::read(&png)?;
470
- let payload = scrybe_mermaid::extract(&bytes)?;
471
- println!("{}", payload.source);
500
+ MermaidCmd::Extract { png, unverified } => {
501
+ run_mermaid_extract(&png, unverified)?;
472
502
  }
473
503
  MermaidCmd::Verify { png } => {
474
504
  let bytes = std::fs::read(&png)?;
505
+ // `extract` verifies by default — a mismatch is a typed error.
475
506
  match scrybe_mermaid::extract(&bytes) {
476
- Err(e) => {
477
- eprintln!("scrybe mermaid verify: failed to extract payload: {e}");
478
- std::process::exit(1);
479
- }
480
- Ok(payload) => {
481
- // Recompute the sha256 of the extracted source.
482
- use sha2::{Digest, Sha256};
483
- let mut hasher = Sha256::new();
484
- hasher.update(payload.source.as_bytes());
485
- let computed = hex::encode(hasher.finalize());
486
-
487
- if computed == payload.sha256 {
488
- println!("OK — sha256 {} matches embedded source", &computed[..16]);
489
- } else {
507
+ Ok(payload) => match &payload.verification {
508
+ scrybe_mermaid::VerificationStatus::Verified { digest, .. } => {
509
+ println!(
510
+ "OK — sha256 {} matches embedded source",
511
+ digest_short(digest)
512
+ );
513
+ }
514
+ scrybe_mermaid::VerificationStatus::NoDigest => {
490
515
  eprintln!(
491
- "TAMPERED — stored sha256 {} does not match computed {}",
492
- &payload.sha256[..16],
493
- &computed[..16]
516
+ "MISSING — payload carries no sha256 digest; nothing to verify"
494
517
  );
495
518
  std::process::exit(1);
496
519
  }
520
+ },
521
+ Err(scrybe_mermaid::MermaidError::VerificationFailed {
522
+ expected,
523
+ actual,
524
+ ..
525
+ }) => {
526
+ eprintln!(
527
+ "TAMPERED — stored sha256 {} does not match computed {}",
528
+ digest_short(&expected),
529
+ digest_short(&actual)
530
+ );
531
+ std::process::exit(1);
532
+ }
533
+ Err(e) => {
534
+ eprintln!("scrybe mermaid verify: failed to extract payload: {e}");
535
+ std::process::exit(1);
497
536
  }
498
537
  }
499
538
  }
@@ -517,6 +556,28 @@ fn main() -> anyhow::Result<()> {
517
556
  println!(" uuid {}", d["uuid"].as_str().unwrap_or(""));
518
557
  println!(" sha256 {}", d["sha256"].as_str().unwrap_or(""));
519
558
  }
559
+ MermaidCmd::Export { input, json } => {
560
+ let reg = scrybe_tools::Registry::default();
561
+ let outcome = reg.call(
562
+ "export_figures",
563
+ &scrybe_tools::Ctx::headless(),
564
+ &serde_json::json!({ "path": input.to_string_lossy() }),
565
+ )?;
566
+ if let Some(err) = outcome.tool_error {
567
+ eprintln!("scrybe mermaid export: {}", err.message);
568
+ std::process::exit(1);
569
+ }
570
+ let d = &outcome.data;
571
+ if json {
572
+ println!("{}", serde_json::to_string_pretty(d)?);
573
+ } else {
574
+ let figures = d["figures"].as_array().cloned().unwrap_or_default();
575
+ for f in &figures {
576
+ println!("{}", f["path"].as_str().unwrap_or(""));
577
+ }
578
+ println!("{} figure(s)", d["count"].as_u64().unwrap_or(0));
579
+ }
580
+ }
520
581
  },
521
582
 
522
583
  Command::Open { path } => match path {
@@ -526,13 +587,11 @@ fn main() -> anyhow::Result<()> {
526
587
  // the "one tab, one file, refresh on re-open" semantics.
527
588
  match rpc_client::send("open", serde_json::json!({"path": canon.to_string_lossy()}))
528
589
  {
529
- Ok(resp) => match resp.error {
530
- None => println!("Opening {} in Scrybe", canon.display()),
531
- Some(e) => {
532
- anyhow::bail!("scrybe open failed: {} ({})", e.message, e.code)
533
- }
534
- },
535
- Err(e) if e.contains("no Scrybe running") => {
590
+ Ok(_) => println!("Opening {} in Scrybe", canon.display()),
591
+ Err(scrybe_rpc::ClientError::Remote(e)) => {
592
+ anyhow::bail!("scrybe open failed: {} ({})", e.message, e.code)
593
+ }
594
+ Err(e) if e.is_not_running() => {
536
595
  // Fall through to launching the app.
537
596
  launch_scrybe(Some(&canon)).map_err(|e| anyhow::anyhow!("{e}"))?;
538
597
  println!("Opening {} in Scrybe", canon.display());
@@ -552,28 +611,23 @@ fn main() -> anyhow::Result<()> {
552
611
  Command::Save { path } => {
553
612
  let canon = path.canonicalize().unwrap_or_else(|_| path.clone());
554
613
  match rpc_client::send("save", serde_json::json!({"path": canon.to_string_lossy()})) {
555
- Ok(resp) => match resp.error {
556
- // Reply-correlated save reports { path, bytes, was_dirty };
557
- // an older app replies the legacy {applied} ack with no
558
- // byte count — don't fabricate one.
559
- None => {
560
- let bytes = resp
561
- .result
562
- .as_ref()
563
- .and_then(|r| r.get("bytes"))
564
- .and_then(serde_json::Value::as_u64);
565
- match bytes {
566
- Some(b) => println!("Saved {} ({b} bytes)", canon.display()),
567
- None => println!("Saved {}", canon.display()),
568
- }
569
- }
570
- // Not open: silent no-op per the documented CLI contract.
571
- Some(e) if e.code == scrybe_rpc::ERR_TAB_NOT_OPEN => {}
572
- Some(e) => {
573
- anyhow::bail!("scrybe save failed: {} ({})", e.message, e.code)
614
+ // Reply-correlated save reports { path, bytes, was_dirty };
615
+ // an older app replies the legacy {applied} ack with no
616
+ // byte count — don't fabricate one.
617
+ Ok(result) => {
618
+ let bytes = result.get("bytes").and_then(serde_json::Value::as_u64);
619
+ match bytes {
620
+ Some(b) => println!("Saved {} ({b} bytes)", canon.display()),
621
+ None => println!("Saved {}", canon.display()),
574
622
  }
575
- },
576
- Err(e) if e.contains("no Scrybe running") => {
623
+ }
624
+ // Not open: silent no-op per the documented CLI contract.
625
+ Err(scrybe_rpc::ClientError::Remote(e))
626
+ if e.code == scrybe_rpc::ERR_TAB_NOT_OPEN => {}
627
+ Err(scrybe_rpc::ClientError::Remote(e)) => {
628
+ anyhow::bail!("scrybe save failed: {} ({})", e.message, e.code)
629
+ }
630
+ Err(e) if e.is_not_running() => {
577
631
  // Silent no-op per design: nothing open, nothing to save.
578
632
  }
579
633
  Err(e) => anyhow::bail!("scrybe save failed: {e}"),
@@ -586,13 +640,11 @@ fn main() -> anyhow::Result<()> {
586
640
  "close",
587
641
  serde_json::json!({"path": canon.to_string_lossy()}),
588
642
  ) {
589
- Ok(resp) => match resp.error {
590
- None => println!("Closed {}", canon.display()),
591
- Some(e) => {
592
- anyhow::bail!("scrybe close failed: {} ({})", e.message, e.code)
593
- }
594
- },
595
- Err(e) if e.contains("no Scrybe running") => {
643
+ Ok(_) => println!("Closed {}", canon.display()),
644
+ Err(scrybe_rpc::ClientError::Remote(e)) => {
645
+ anyhow::bail!("scrybe close failed: {} ({})", e.message, e.code)
646
+ }
647
+ Err(e) if e.is_not_running() => {
596
648
  // Silent no-op.
597
649
  }
598
650
  Err(e) => anyhow::bail!("scrybe close failed: {e}"),
@@ -646,13 +698,11 @@ fn main() -> anyhow::Result<()> {
646
698
  }
647
699
  Command::Quit { force } => {
648
700
  match rpc_client::send("quit", serde_json::json!({"force": force})) {
649
- Ok(resp) => match resp.error {
650
- None => println!("Quitting Scrybe"),
651
- Some(e) => {
652
- anyhow::bail!("scrybe quit failed: {} ({})", e.message, e.code)
653
- }
654
- },
655
- Err(e) if e.contains("no Scrybe running") => {
701
+ Ok(_) => println!("Quitting Scrybe"),
702
+ Err(scrybe_rpc::ClientError::Remote(e)) => {
703
+ anyhow::bail!("scrybe quit failed: {} ({})", e.message, e.code)
704
+ }
705
+ Err(e) if e.is_not_running() => {
656
706
  // Silent no-op.
657
707
  }
658
708
  Err(e) => anyhow::bail!("scrybe quit failed: {e}"),
@@ -687,11 +737,9 @@ fn main() -> anyhow::Result<()> {
687
737
  std::fs::write(&dest, &embedded)?;
688
738
  println!("Embedded into {}", dest.display());
689
739
  }
690
- Command::Extract { png } => {
740
+ Command::Extract { png, unverified } => {
691
741
  // Top-level alias for `mermaid extract` — same code path.
692
- let bytes = std::fs::read(&png)?;
693
- let payload = scrybe_mermaid::extract(&bytes)?;
694
- println!("{}", payload.source);
742
+ run_mermaid_extract(&png, unverified)?;
695
743
  }
696
744
 
697
745
  Command::Version => {
@@ -702,6 +750,49 @@ fn main() -> anyhow::Result<()> {
702
750
  Ok(())
703
751
  }
704
752
 
753
+ // ---------------------------------------------------------------------------
754
+ // Mermaid extract handler (shared by `mermaid extract` and top-level `extract`)
755
+ // ---------------------------------------------------------------------------
756
+
757
+ /// First 16 hex chars of a digest for display — safe on short or garbage
758
+ /// stored values (attacker-controlled input must never panic the CLI).
759
+ fn digest_short(d: &str) -> &str {
760
+ d.get(..16).unwrap_or(d)
761
+ }
762
+
763
+ /// Extract embedded Mermaid source from `png` and print it.
764
+ ///
765
+ /// Verifies the stored sha256 against the extracted source by default.
766
+ /// Exit codes: 0 = success (no-digest payloads print a stderr note),
767
+ /// 1 = extraction failure (no payload / unreadable PNG, via the returned
768
+ /// error), 2 = verification failure (tampered).
769
+ fn run_mermaid_extract(png: &std::path::Path, unverified: bool) -> anyhow::Result<()> {
770
+ let bytes = std::fs::read(png)?;
771
+
772
+ if unverified {
773
+ // Forensics path: raw stored fields, no digest check.
774
+ let payload = scrybe_mermaid::extract_unverified(&bytes)?;
775
+ println!("{}", payload.source);
776
+ return Ok(());
777
+ }
778
+
779
+ match scrybe_mermaid::extract(&bytes) {
780
+ Ok(payload) => {
781
+ if !payload.is_verified() {
782
+ eprintln!("note: payload carries no sha256 digest — source is unverified");
783
+ }
784
+ println!("{}", payload.source);
785
+ Ok(())
786
+ }
787
+ Err(e @ scrybe_mermaid::MermaidError::VerificationFailed { .. }) => {
788
+ eprintln!("scrybe mermaid extract: {e}");
789
+ eprintln!("(use --unverified to print the stored source anyway)");
790
+ std::process::exit(2);
791
+ }
792
+ Err(e) => Err(e.into()),
793
+ }
794
+ }
795
+
705
796
  // ---------------------------------------------------------------------------
706
797
  // Phase 2 read-side handlers
707
798
  // ---------------------------------------------------------------------------
@@ -711,13 +802,15 @@ fn require_running_gui<F: FnOnce(serde_json::Value) -> anyhow::Result<()>>(
711
802
  params: serde_json::Value,
712
803
  on_ok: F,
713
804
  ) -> anyhow::Result<()> {
805
+ // The client types every failure (A3): in-band app errors are `Remote`,
806
+ // no-app is `is_not_running()`, and malformed replies arrive pre-typed
807
+ // (envelope/frame/JSON violations) — no message-text matching anywhere.
714
808
  match rpc_client::send(method, params) {
715
- Ok(resp) => match (resp.result, resp.error) {
716
- (Some(r), None) => on_ok(r),
717
- (None, Some(e)) => anyhow::bail!("scrybe {method}: {} ({})", e.message, e.code),
718
- _ => anyhow::bail!("scrybe {method}: malformed response"),
719
- },
720
- Err(e) if e.contains("no Scrybe running") => {
809
+ Ok(result) => on_ok(result),
810
+ Err(scrybe_rpc::ClientError::Remote(e)) => {
811
+ anyhow::bail!("scrybe {method}: {} ({})", e.message, e.code)
812
+ }
813
+ Err(e) if e.is_not_running() => {
721
814
  anyhow::bail!(
722
815
  "scrybe {method}: no Scrybe running — start the app first, or open the file with `scrybe <path>`"
723
816
  )