scrybe-cli 0.6.2__tar.gz → 0.6.3__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 (84) hide show
  1. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/Cargo.lock +14 -14
  2. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/Cargo.toml +12 -12
  3. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/PKG-INFO +1 -1
  4. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/pyproject.toml +1 -1
  5. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/pyproject.toml +1 -1
  6. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid/pyproject.toml +1 -1
  7. scrybe_cli-0.6.3/scrybe-tools/src/autolaunch.rs +228 -0
  8. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/lib.rs +25 -3
  9. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/README.md +0 -0
  10. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/python/scrybe_cli/__init__.py +0 -0
  11. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/python/scrybe_cli/py.typed +0 -0
  12. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-cli/Cargo.toml +0 -0
  13. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-cli/README.md +0 -0
  14. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-cli/src/lib.rs +0 -0
  15. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-cli/src/main.rs +0 -0
  16. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-cli/src/rpc_client.rs +0 -0
  17. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-cli/src/wrap.rs +0 -0
  18. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-cli/tests/rpc_socket.rs +0 -0
  19. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-core/Cargo.toml +0 -0
  20. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-core/README.md +0 -0
  21. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-core/src/ast.rs +0 -0
  22. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-core/src/change.rs +0 -0
  23. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-core/src/content.rs +0 -0
  24. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-core/src/document.rs +0 -0
  25. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-core/src/error.rs +0 -0
  26. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-core/src/lib.rs +0 -0
  27. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-core/src/plugin.rs +0 -0
  28. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-core/src/workspace.rs +0 -0
  29. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/Cargo.toml +0 -0
  30. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/README.md +0 -0
  31. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/python/scrybe_mcp_server/__init__.py +0 -0
  32. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/python/scrybe_mcp_server/py.typed +0 -0
  33. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/src/lib.rs +0 -0
  34. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/src/main.rs +0 -0
  35. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/src/server.rs +0 -0
  36. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/tests/docx_packaging.rs +0 -0
  37. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/tests/fixtures/call_invalid_args.json +0 -0
  38. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/tests/fixtures/call_lint_success.json +0 -0
  39. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/tests/fixtures/call_no_live_app.json +0 -0
  40. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/tests/fixtures/call_remote_app_error.json +0 -0
  41. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/tests/fixtures/call_unknown_tool.json +0 -0
  42. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/tests/live_app_open.rs +0 -0
  43. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mcp-server/tests/mcp_contract.rs +0 -0
  44. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid/Cargo.toml +0 -0
  45. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid/README.md +0 -0
  46. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid/python/scrybe_mermaid/__init__.py +0 -0
  47. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid/python/scrybe_mermaid/py.typed +0 -0
  48. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid/src/codec.rs +0 -0
  49. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid/src/error.rs +0 -0
  50. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid/src/lib.rs +0 -0
  51. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid/tests/test_python_quickstart.py +0 -0
  52. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid-render/Cargo.toml +0 -0
  53. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid-render/README.md +0 -0
  54. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid-render/examples/provenance_drive.rs +0 -0
  55. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-mermaid-render/src/lib.rs +0 -0
  56. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-render/Cargo.toml +0 -0
  57. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-render/README.md +0 -0
  58. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-render/src/html.rs +0 -0
  59. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-render/src/lib.rs +0 -0
  60. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-render/src/math.rs +0 -0
  61. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-render/src/mermaid.rs +0 -0
  62. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-render/src/theme.rs +0 -0
  63. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-render/src/themes/dark.css +0 -0
  64. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-render/src/themes/default.css +0 -0
  65. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-render/src/themes/solarized.css +0 -0
  66. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-rpc/Cargo.toml +0 -0
  67. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-rpc/README.md +0 -0
  68. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-rpc/src/client.rs +0 -0
  69. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-rpc/src/lib.rs +0 -0
  70. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-rpc/tests/wire_contract.rs +0 -0
  71. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/Cargo.toml +0 -0
  72. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/README.md +0 -0
  73. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/examples/render_drive.rs +0 -0
  74. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/figures.rs +0 -0
  75. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/lint.rs +0 -0
  76. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/schema.rs +0 -0
  77. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/tools/editor.rs +0 -0
  78. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/tools/export.rs +0 -0
  79. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/tools/lint.rs +0 -0
  80. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/tools/mermaid.rs +0 -0
  81. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/tools/mod.rs +0 -0
  82. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/tools/render.rs +0 -0
  83. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/tools/tabs.rs +0 -0
  84. {scrybe_cli-0.6.2 → scrybe_cli-0.6.3}/scrybe-tools/src/tools/ui.rs +0 -0
@@ -3915,7 +3915,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
3915
3915
 
3916
3916
  [[package]]
3917
3917
  name = "scrybe-app"
3918
- version = "0.6.2"
3918
+ version = "0.6.3"
3919
3919
  dependencies = [
3920
3920
  "anyhow",
3921
3921
  "dirs 5.0.1",
@@ -3943,7 +3943,7 @@ dependencies = [
3943
3943
 
3944
3944
  [[package]]
3945
3945
  name = "scrybe-cli"
3946
- version = "0.6.2"
3946
+ version = "0.6.3"
3947
3947
  dependencies = [
3948
3948
  "anyhow",
3949
3949
  "clap",
@@ -3961,7 +3961,7 @@ dependencies = [
3961
3961
 
3962
3962
  [[package]]
3963
3963
  name = "scrybe-core"
3964
- version = "0.6.2"
3964
+ version = "0.6.3"
3965
3965
  dependencies = [
3966
3966
  "anyhow",
3967
3967
  "blake3",
@@ -3978,7 +3978,7 @@ dependencies = [
3978
3978
 
3979
3979
  [[package]]
3980
3980
  name = "scrybe-mcp-client"
3981
- version = "0.6.2"
3981
+ version = "0.6.3"
3982
3982
  dependencies = [
3983
3983
  "anyhow",
3984
3984
  "dirs 5.0.1",
@@ -3993,7 +3993,7 @@ dependencies = [
3993
3993
 
3994
3994
  [[package]]
3995
3995
  name = "scrybe-mcp-server"
3996
- version = "0.6.2"
3996
+ version = "0.6.3"
3997
3997
  dependencies = [
3998
3998
  "anyhow",
3999
3999
  "clap",
@@ -4013,7 +4013,7 @@ dependencies = [
4013
4013
 
4014
4014
  [[package]]
4015
4015
  name = "scrybe-mermaid"
4016
- version = "0.6.2"
4016
+ version = "0.6.3"
4017
4017
  dependencies = [
4018
4018
  "anyhow",
4019
4019
  "base64 0.22.1",
@@ -4030,7 +4030,7 @@ dependencies = [
4030
4030
 
4031
4031
  [[package]]
4032
4032
  name = "scrybe-mermaid-render"
4033
- version = "0.6.2"
4033
+ version = "0.6.3"
4034
4034
  dependencies = [
4035
4035
  "anyhow",
4036
4036
  "hex",
@@ -4040,7 +4040,7 @@ dependencies = [
4040
4040
 
4041
4041
  [[package]]
4042
4042
  name = "scrybe-py"
4043
- version = "0.6.2"
4043
+ version = "0.6.3"
4044
4044
  dependencies = [
4045
4045
  "anyhow",
4046
4046
  "pyo3",
@@ -4054,7 +4054,7 @@ dependencies = [
4054
4054
 
4055
4055
  [[package]]
4056
4056
  name = "scrybe-ratatui"
4057
- version = "0.6.2"
4057
+ version = "0.6.3"
4058
4058
  dependencies = [
4059
4059
  "ratatui",
4060
4060
  "scrybe-core",
@@ -4062,7 +4062,7 @@ dependencies = [
4062
4062
 
4063
4063
  [[package]]
4064
4064
  name = "scrybe-render"
4065
- version = "0.6.2"
4065
+ version = "0.6.3"
4066
4066
  dependencies = [
4067
4067
  "anyhow",
4068
4068
  "pulldown-cmark",
@@ -4075,7 +4075,7 @@ dependencies = [
4075
4075
 
4076
4076
  [[package]]
4077
4077
  name = "scrybe-rpc"
4078
- version = "0.6.2"
4078
+ version = "0.6.3"
4079
4079
  dependencies = [
4080
4080
  "serde",
4081
4081
  "serde_json",
@@ -4084,7 +4084,7 @@ dependencies = [
4084
4084
 
4085
4085
  [[package]]
4086
4086
  name = "scrybe-tools"
4087
- version = "0.6.2"
4087
+ version = "0.6.3"
4088
4088
  dependencies = [
4089
4089
  "anyhow",
4090
4090
  "scrybe-core",
@@ -4102,7 +4102,7 @@ dependencies = [
4102
4102
 
4103
4103
  [[package]]
4104
4104
  name = "scrybe-tui"
4105
- version = "0.6.2"
4105
+ version = "0.6.3"
4106
4106
  dependencies = [
4107
4107
  "anyhow",
4108
4108
  "clap",
@@ -4114,7 +4114,7 @@ dependencies = [
4114
4114
 
4115
4115
  [[package]]
4116
4116
  name = "scrybe-vcs"
4117
- version = "0.6.2"
4117
+ version = "0.6.3"
4118
4118
  dependencies = [
4119
4119
  "anyhow",
4120
4120
  "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.6.2"
9
+ version = "0.6.3"
10
10
 
11
11
  # Workspace-level dependency pins keep all crates in lock-step.
12
12
  [workspace.dependencies]
@@ -60,17 +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.6.2" }
64
- scrybe-render = { path = "scrybe-render", version = "=0.6.2" }
65
- scrybe-mcp-client = { path = "scrybe-mcp-client", version = "=0.6.2" }
66
- scrybe-mcp-server = { path = "scrybe-mcp-server", version = "=0.6.2" }
67
- scrybe-mermaid = { path = "scrybe-mermaid", version = "=0.6.2" }
68
- scrybe-vcs = { path = "scrybe-vcs", version = "=0.6.2" }
69
- scrybe-py = { path = "scrybe-py", version = "=0.6.2" }
70
- scrybe-rpc = { path = "scrybe-rpc", version = "=0.6.2" }
71
- scrybe-tools = { path = "scrybe-tools", version = "=0.6.2" }
72
- scrybe-ratatui = { path = "scrybe-ratatui", version = "=0.6.2" }
73
- scrybe-mermaid-render = { path = "scrybe-mermaid-render", version = "=0.6.2" }
63
+ scrybe-core = { path = "scrybe-core", version = "=0.6.3" }
64
+ scrybe-render = { path = "scrybe-render", version = "=0.6.3" }
65
+ scrybe-mcp-client = { path = "scrybe-mcp-client", version = "=0.6.3" }
66
+ scrybe-mcp-server = { path = "scrybe-mcp-server", version = "=0.6.3" }
67
+ scrybe-mermaid = { path = "scrybe-mermaid", version = "=0.6.3" }
68
+ scrybe-vcs = { path = "scrybe-vcs", version = "=0.6.3" }
69
+ scrybe-py = { path = "scrybe-py", version = "=0.6.3" }
70
+ scrybe-rpc = { path = "scrybe-rpc", version = "=0.6.3" }
71
+ scrybe-tools = { path = "scrybe-tools", version = "=0.6.3" }
72
+ scrybe-ratatui = { path = "scrybe-ratatui", version = "=0.6.3" }
73
+ scrybe-mermaid-render = { path = "scrybe-mermaid-render", version = "=0.6.3" }
74
74
 
75
75
  # Adopted pure-Rust Mermaid → SVG renderer (#132 spike-Pass, 2026-07-17).
76
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.6.2
3
+ Version: 0.6.3
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
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "scrybe-cli"
7
- version = "0.6.2"
7
+ version = "0.6.3"
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 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "scrybe-mcp-server"
7
- version = "0.6.2"
7
+ version = "0.6.3"
8
8
  description = "Scrybe MCP server — standalone inbound MCP server (binary wheel)"
9
9
  license = {text = "Apache-2.0"}
10
10
  readme = "README.md"
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "scrybe-mermaid"
7
- version = "0.6.2"
7
+ version = "0.6.3"
8
8
  description = "Scrybe Mermaid — standalone PNG iTXt codec (Rust-backed)"
9
9
  license = {text = "Apache-2.0"}
10
10
  readme = "README.md"
@@ -0,0 +1,228 @@
1
+ // Copyright 2026 Shawn Hartsock and contributors
2
+ // SPDX-License-Identifier: Apache-2.0 OR MIT
3
+
4
+ //! Opt-in auto-launch of the Scrybe desktop app (issue #225).
5
+ //!
6
+ //! A stateful tool (`open`, `state`, `list_tabs`, …) reaches the running editor
7
+ //! over `~/.scrybe/sock`. When nothing is listening, [`crate::LiveApp::call`]
8
+ //! reports `NoApp` and the tool returns a clean `no_live_app` — correct, but it
9
+ //! means "open this file in Scrybe" fails whenever the app happens to be closed.
10
+ //!
11
+ //! This module restores an *opt-in* launch: when `SCRYBE_MCP_AUTOLAUNCH` is set,
12
+ //! a no-app call first tries to start the installed app, waits for its socket,
13
+ //! and retries once. It is **off by default** on purpose — an MCP server
14
+ //! spawning a GUI window is a deliberate choice, not a surprise. It also
15
+ //! deliberately does *not* repeat the old `which scrybe-app` mistake: a cask- or
16
+ //! DMG-installed app lives in a `.app` bundle that is never on `PATH`, so we
17
+ //! resolve it by **install location**, not by `which`.
18
+
19
+ use std::path::{Path, PathBuf};
20
+ use std::time::Duration;
21
+
22
+ /// Env var that opts INTO auto-launch. Truthy = `1`/`true`/`yes`/`on`.
23
+ pub const AUTOLAUNCH_ENV: &str = "SCRYBE_MCP_AUTOLAUNCH";
24
+
25
+ /// Optional explicit `Scrybe.app` bundle path, checked before the standard
26
+ /// install locations (a non-default install, or CI).
27
+ pub const APP_ENV: &str = "SCRYBE_APP";
28
+
29
+ /// Liveness probes to make after launching before giving up (the app has to
30
+ /// start and bind its socket). 20 × [`POLL_INTERVAL`] ≈ a 5 s budget.
31
+ const DEFAULT_TRIES: u32 = 20;
32
+ /// Delay between liveness probes while waiting for the app to come up.
33
+ const POLL_INTERVAL: Duration = Duration::from_millis(250);
34
+
35
+ /// Is auto-launch opted in? True only when `SCRYBE_MCP_AUTOLAUNCH` is truthy.
36
+ pub fn enabled() -> bool {
37
+ is_truthy(std::env::var(AUTOLAUNCH_ENV).ok().as_deref())
38
+ }
39
+
40
+ fn is_truthy(v: Option<&str>) -> bool {
41
+ matches!(
42
+ v.map(|s| s.trim().to_ascii_lowercase()).as_deref(),
43
+ Some("1" | "true" | "yes" | "on")
44
+ )
45
+ }
46
+
47
+ /// Resolve the installed `Scrybe.app`, in priority order:
48
+ /// `$SCRYBE_APP` → `/Applications/Scrybe.app` → `$HOME/Applications/Scrybe.app`.
49
+ ///
50
+ /// Pure over the injected `env` lookup and `exists` probe, so the resolution
51
+ /// order is unit-testable without touching the real filesystem.
52
+ pub fn resolve_app(
53
+ env: impl Fn(&str) -> Option<String>,
54
+ exists: impl Fn(&Path) -> bool,
55
+ ) -> Option<PathBuf> {
56
+ if let Some(p) = env(APP_ENV) {
57
+ let p = PathBuf::from(p);
58
+ if exists(&p) {
59
+ return Some(p);
60
+ }
61
+ }
62
+ let mut candidates = vec![PathBuf::from("/Applications/Scrybe.app")];
63
+ if let Some(home) = env("HOME") {
64
+ candidates.push(PathBuf::from(home).join("Applications/Scrybe.app"));
65
+ }
66
+ candidates.into_iter().find(|p| exists(p))
67
+ }
68
+
69
+ /// Ensure a reachable app, launching it if it is not already up. Pure
70
+ /// orchestration over the injected `is_live` probe, `launch` action, and
71
+ /// `sleep` — no real subprocess, socket, or wall-clock — so the retry/timeout
72
+ /// logic is unit-testable. Returns `true` if the app is (or became) reachable.
73
+ pub fn ensure_running(
74
+ is_live: impl Fn() -> bool,
75
+ launch: impl Fn() -> bool,
76
+ sleep: impl Fn(),
77
+ tries: u32,
78
+ ) -> bool {
79
+ if is_live() {
80
+ return true;
81
+ }
82
+ if !launch() {
83
+ return false;
84
+ }
85
+ for _ in 0..tries {
86
+ sleep();
87
+ if is_live() {
88
+ return true;
89
+ }
90
+ }
91
+ false
92
+ }
93
+
94
+ /// Launch the resolved `Scrybe.app` detached and in the background. macOS-only;
95
+ /// returns `false` if no app was found or the spawn failed. On every other OS
96
+ /// auto-launch is a no-op — there is no bundle to `open`.
97
+ pub fn launch_installed_app() -> bool {
98
+ launch_app(|k| std::env::var(k).ok(), |p| p.exists())
99
+ }
100
+
101
+ #[cfg(target_os = "macos")]
102
+ fn launch_app(env: impl Fn(&str) -> Option<String>, exists: impl Fn(&Path) -> bool) -> bool {
103
+ let Some(app) = resolve_app(env, exists) else {
104
+ return false;
105
+ };
106
+ // `open -g` launches the bundle in the background without stealing focus.
107
+ std::process::Command::new("open")
108
+ .arg("-g")
109
+ .arg(&app)
110
+ .status()
111
+ .map(|s| s.success())
112
+ .unwrap_or(false)
113
+ }
114
+
115
+ #[cfg(not(target_os = "macos"))]
116
+ fn launch_app(_env: impl Fn(&str) -> Option<String>, _exists: impl Fn(&Path) -> bool) -> bool {
117
+ false
118
+ }
119
+
120
+ /// The production launch-and-wait the live transport calls on a no-app request:
121
+ /// opt-in gated, launches the installed app, and polls the real socket up to the
122
+ /// budget. Returns `true` if the app became reachable.
123
+ pub fn autolaunch_and_wait() -> bool {
124
+ if !enabled() {
125
+ return false;
126
+ }
127
+ ensure_running(
128
+ scrybe_rpc::client::is_live,
129
+ launch_installed_app,
130
+ || std::thread::sleep(POLL_INTERVAL),
131
+ DEFAULT_TRIES,
132
+ )
133
+ }
134
+
135
+ #[cfg(test)]
136
+ mod tests {
137
+ use super::*;
138
+ use std::cell::Cell;
139
+
140
+ #[test]
141
+ fn enabled_is_truthy_only_for_yes_values() {
142
+ for v in ["1", "true", "TRUE", "yes", "on", " on "] {
143
+ assert!(is_truthy(Some(v)), "{v:?} should be truthy");
144
+ }
145
+ for v in ["0", "false", "no", "off", ""] {
146
+ assert!(!is_truthy(Some(v)), "{v:?} should be falsy");
147
+ }
148
+ assert!(!is_truthy(None));
149
+ }
150
+
151
+ #[test]
152
+ fn resolve_prefers_scrybe_app_env_when_it_exists() {
153
+ let app = resolve_app(
154
+ |k| (k == APP_ENV).then(|| "/custom/Scrybe.app".to_string()),
155
+ |p| p == Path::new("/custom/Scrybe.app"),
156
+ );
157
+ assert_eq!(app, Some(PathBuf::from("/custom/Scrybe.app")));
158
+ }
159
+
160
+ #[test]
161
+ fn resolve_skips_a_missing_env_override_and_falls_through() {
162
+ let app = resolve_app(
163
+ |k| (k == APP_ENV).then(|| "/gone/Scrybe.app".to_string()),
164
+ |p| p == Path::new("/Applications/Scrybe.app"),
165
+ );
166
+ assert_eq!(app, Some(PathBuf::from("/Applications/Scrybe.app")));
167
+ }
168
+
169
+ #[test]
170
+ fn resolve_falls_back_to_system_then_user_applications() {
171
+ let sys = resolve_app(|_| None, |p| p == Path::new("/Applications/Scrybe.app"));
172
+ assert_eq!(sys, Some(PathBuf::from("/Applications/Scrybe.app")));
173
+
174
+ let user_app = "/Users/me/Applications/Scrybe.app";
175
+ let user = resolve_app(
176
+ |k| (k == "HOME").then(|| "/Users/me".to_string()),
177
+ |p| p == Path::new(user_app),
178
+ );
179
+ assert_eq!(user, Some(PathBuf::from(user_app)));
180
+ }
181
+
182
+ #[test]
183
+ fn resolve_is_none_when_nothing_is_installed() {
184
+ assert_eq!(resolve_app(|_| None, |_| false), None);
185
+ }
186
+
187
+ #[test]
188
+ fn ensure_running_short_circuits_without_launching_when_already_live() {
189
+ let launched = Cell::new(false);
190
+ let ok = ensure_running(
191
+ || true,
192
+ || {
193
+ launched.set(true);
194
+ true
195
+ },
196
+ || {},
197
+ 20,
198
+ );
199
+ assert!(ok);
200
+ assert!(!launched.get(), "must not launch when already live");
201
+ }
202
+
203
+ #[test]
204
+ fn ensure_running_is_false_when_launch_fails() {
205
+ assert!(!ensure_running(|| false, || false, || {}, 20));
206
+ }
207
+
208
+ #[test]
209
+ fn ensure_running_polls_until_the_app_comes_up() {
210
+ let ticks = Cell::new(0u32);
211
+ let ok = ensure_running(
212
+ || ticks.get() >= 3, // live on the 3rd probe after launch
213
+ || true,
214
+ || ticks.set(ticks.get() + 1),
215
+ 20,
216
+ );
217
+ assert!(ok);
218
+ assert_eq!(ticks.get(), 3);
219
+ }
220
+
221
+ #[test]
222
+ fn ensure_running_gives_up_after_the_budget() {
223
+ let sleeps = Cell::new(0u32);
224
+ let ok = ensure_running(|| false, || true, || sleeps.set(sleeps.get() + 1), 5);
225
+ assert!(!ok);
226
+ assert_eq!(sleeps.get(), 5, "polls exactly `tries` times then gives up");
227
+ }
228
+ }
@@ -13,6 +13,7 @@
13
13
 
14
14
  use serde_json::Value;
15
15
 
16
+ pub mod autolaunch;
16
17
  pub mod figures;
17
18
  pub mod lint;
18
19
  pub mod schema;
@@ -196,12 +197,33 @@ impl Transport for Headless {
196
197
  /// `call` returns `NoApp` and pure tools should fall back to `Headless`.
197
198
  pub struct LiveApp;
198
199
 
200
+ /// Map a `scrybe-rpc` client result onto the transport error taxonomy: no app
201
+ /// running → [`TransportError::NoApp`], an in-band remote error →
202
+ /// [`TransportError::Remote`], anything else (I/O, timeout, frame) →
203
+ /// [`TransportError::Transport`]. Used for the post-auto-launch retry.
204
+ fn map_send(res: Result<Value, scrybe_rpc::ClientError>) -> Result<Value, TransportError> {
205
+ match res {
206
+ Ok(result) => Ok(result),
207
+ Err(e) if e.is_not_running() => Err(TransportError::NoApp),
208
+ Err(scrybe_rpc::ClientError::Remote(err)) => Err(TransportError::Remote(err)),
209
+ Err(e) => Err(TransportError::Transport(e.to_string())),
210
+ }
211
+ }
212
+
199
213
  impl Transport for LiveApp {
200
214
  fn call(&self, method: &str, params: Value) -> Result<Value, TransportError> {
201
- match scrybe_rpc::client::send(method, params) {
215
+ match scrybe_rpc::client::send(method, params.clone()) {
202
216
  Ok(result) => Ok(result),
203
- // The one blessed no-app check — never message-text matching.
204
- Err(e) if e.is_not_running() => Err(TransportError::NoApp),
217
+ // No app running. If auto-launch is opted in (`SCRYBE_MCP_AUTOLAUNCH`),
218
+ // start the installed app, wait for its socket, and retry once;
219
+ // otherwise report NoApp (→ the tool's clean `no_live_app`).
220
+ Err(e) if e.is_not_running() => {
221
+ if crate::autolaunch::autolaunch_and_wait() {
222
+ map_send(scrybe_rpc::client::send(method, params))
223
+ } else {
224
+ Err(TransportError::NoApp)
225
+ }
226
+ }
205
227
  // In-band remote error: the app answered. Business, not engine.
206
228
  Err(scrybe_rpc::ClientError::Remote(err)) => Err(TransportError::Remote(err)),
207
229
  // Everything else (I/O, timeouts, frame/UTF-8/JSON, envelope,
File without changes