Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit aab0e07

Browse files
committed
feat(hooks): add daemon auto-start/stop via SessionStart hook
- Default the daemon feature in flowctl-cli Cargo.toml so serve command is always compiled in - Add DaemonStart and DaemonStop hook subcommands that manage a background flowctl serve process via PID file - Register SessionStart hook (daemon-start) and append daemon-stop to the Stop hook array in hooks.json - Fix pre-existing mcp_initialize trycmd version mismatch Task: fn-1-web-dashboard-daemon-sessionstart-hook.1
1 parent 830dda3 commit aab0e07

4 files changed

Lines changed: 116 additions & 2 deletions

File tree

flowctl/crates/flowctl-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ name = "flowctl"
1111
path = "src/main.rs"
1212

1313
[features]
14-
default = []
14+
default = ["daemon"]
1515
daemon = ["dep:flowctl-daemon", "dep:flowctl-web", "dep:tokio", "dep:leptos", "dep:leptos_axum", "dep:any_spawner", "dep:axum"]
1616

1717
[dependencies]

flowctl/crates/flowctl-cli/src/commands/hook.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ pub enum HookCmd {
3131
TaskCompleted,
3232
/// Rewrite Bash commands via rtk token optimizer (PreToolUse hook).
3333
RtkRewrite,
34+
/// Start flowctl daemon in background (SessionStart hook).
35+
DaemonStart,
36+
/// Stop flowctl daemon (Stop hook).
37+
DaemonStop,
3438
}
3539

3640
pub fn dispatch(cmd: &HookCmd) {
@@ -42,6 +46,8 @@ pub fn dispatch(cmd: &HookCmd) {
4246
HookCmd::SubagentContext => cmd_subagent_context(),
4347
HookCmd::TaskCompleted => cmd_task_completed(),
4448
HookCmd::RtkRewrite => cmd_rtk_rewrite(),
49+
HookCmd::DaemonStart => cmd_daemon_start(),
50+
HookCmd::DaemonStop => cmd_daemon_stop(),
4551
}
4652
}
4753

@@ -1797,6 +1803,94 @@ fn cmd_rtk_rewrite() {
17971803
}
17981804
}
17991805

1806+
// ═══════════════════════════════════════════════════════════════════════
1807+
// Daemon Start / Stop
1808+
// ═══════════════════════════════════════════════════════════════════════
1809+
1810+
fn daemon_pid_file() -> PathBuf {
1811+
let flow_dir = get_flow_dir();
1812+
let canonical = fs::canonicalize(&flow_dir)
1813+
.unwrap_or_else(|_| flow_dir.to_path_buf());
1814+
let hash = md5_hex(canonical.to_string_lossy().as_bytes());
1815+
PathBuf::from(format!(
1816+
"{}/flowctl-daemon-{hash}.pid",
1817+
env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into())
1818+
))
1819+
}
1820+
1821+
fn cmd_daemon_start() {
1822+
let _input = read_stdin_json();
1823+
1824+
let flow_dir = get_flow_dir();
1825+
if !flow_dir.exists() {
1826+
std::process::exit(0);
1827+
}
1828+
1829+
let pid_file = daemon_pid_file();
1830+
1831+
// If PID file exists and process is alive, exit (already running)
1832+
if pid_file.exists() {
1833+
if let Ok(contents) = fs::read_to_string(&pid_file) {
1834+
if let Ok(pid) = contents.trim().parse::<u32>() {
1835+
let alive = Command::new("kill")
1836+
.args(["-0", &pid.to_string()])
1837+
.stdout(std::process::Stdio::null())
1838+
.stderr(std::process::Stdio::null())
1839+
.status()
1840+
.map(|s| s.success())
1841+
.unwrap_or(false);
1842+
if alive {
1843+
std::process::exit(0);
1844+
}
1845+
}
1846+
}
1847+
}
1848+
1849+
let self_exe = match std::env::current_exe() {
1850+
Ok(p) if p.exists() => p,
1851+
_ => std::process::exit(0),
1852+
};
1853+
1854+
let child = Command::new(&self_exe)
1855+
.args(["serve", "--port", "17319"])
1856+
.stdout(std::process::Stdio::null())
1857+
.stderr(std::process::Stdio::null())
1858+
.spawn();
1859+
1860+
match child {
1861+
Ok(c) => {
1862+
let _ = fs::write(&pid_file, c.id().to_string());
1863+
println!("flowctl dashboard: http://127.0.0.1:17319");
1864+
}
1865+
Err(_) => {
1866+
// Silently fail — daemon is optional
1867+
}
1868+
}
1869+
1870+
std::process::exit(0);
1871+
}
1872+
1873+
fn cmd_daemon_stop() {
1874+
let _input = read_stdin_json();
1875+
1876+
let pid_file = daemon_pid_file();
1877+
1878+
if pid_file.exists() {
1879+
if let Ok(contents) = fs::read_to_string(&pid_file) {
1880+
if let Ok(pid) = contents.trim().parse::<u32>() {
1881+
let _ = Command::new("kill")
1882+
.arg(pid.to_string())
1883+
.stdout(std::process::Stdio::null())
1884+
.stderr(std::process::Stdio::null())
1885+
.status();
1886+
}
1887+
}
1888+
let _ = fs::remove_file(&pid_file);
1889+
}
1890+
1891+
std::process::exit(0);
1892+
}
1893+
18001894
// ── Shared helpers ────────────────────────────────────────────────────
18011895

18021896
fn read_stdin_json() -> Value {

flowctl/tests/cmd/mcp_initialize.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ bin.name = "flowctl"
22
args = ["mcp"]
33
stdin = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
44
stdout = """
5-
{"id":1,"jsonrpc":"2.0","result":{"capabilities":{"tools":{}},"protocolVersion":"2024-11-05","serverInfo":{"name":"flowctl","version":"0.1.25"}}}
5+
{"id":1,"jsonrpc":"2.0","result":{"capabilities":{"tools":{}},"protocolVersion":"2024-11-05","serverInfo":{"name":"flowctl","version":"0.1.26"}}}
66
"""
77
status.code = 0

hooks/hooks.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
{
22
"description": "Ralph workflow guards - only active when FLOW_RALPH=1 and ralph-init has been run",
33
"hooks": {
4+
"SessionStart": [
5+
{
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "\"${DROID_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/bin/flowctl\" hook daemon-start",
10+
"timeout": 10
11+
}
12+
]
13+
}
14+
],
415
"PreToolUse": [
516
{
617
"matcher": "Bash|Execute",
@@ -86,6 +97,15 @@
8697
}
8798
],
8899
"description": "Auto-capture session memories to .flow/memory/ (requires memory.enabled or memory.auto in config)"
100+
},
101+
{
102+
"hooks": [
103+
{
104+
"type": "command",
105+
"command": "\"${DROID_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/bin/flowctl\" hook daemon-stop 2>/dev/null || true",
106+
"timeout": 5
107+
}
108+
]
89109
}
90110
],
91111
"SubagentStop": [

0 commit comments

Comments
 (0)