nils-agent-session 1.21.9

CLI crate for nils-agent-session in the nils-cli workspace.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
use std::path::PathBuf;

use clap::{Args, Parser, Subcommand, ValueEnum, ValueHint};
use nils_common::cli_contract::OutputFormat;

/// Default delay before pasting the initial prompt (ms). Shared by `start`'s
/// `--paste-delay-ms` default and the serve create endpoint.
pub const DEFAULT_PASTE_DELAY_MS: u64 = 1200;
/// Default number of pane lines captured by `glance` (CLI and serve).
pub const DEFAULT_GLANCE_TAIL: usize = 40;

#[derive(Debug, Parser)]
#[command(
    name = "agent-session",
    version,
    long_version = nils_build_info::long_version(env!("CARGO_PKG_VERSION")),
    about = "Start and manage tmux-backed Codex, Claude Code, and Hermes sessions.",
    long_about = "Start and manage tmux-backed Codex, Claude Code, and Hermes sessions for mobile handoff workflows.",
    disable_help_subcommand = true,
    after_help = "EXAMPLES:\n  agent-session start --agent codex --cwd ~/Project/app --prompt-file prompt.md\n  agent-session start --agent hermes --cwd ~\n  agent-session list\n  agent-session glance <id> --tail 40\n  agent-session send <id> --text yes --key enter\n  agent-session send <id> --key c-c\n  agent-session resume <id>\n  agent-session command <id>\n  agent-session attach <id>\n  agent-session delete <id>\n\nENVIRONMENT:\n  AGENT_SESSION_HOST       Hostname used in generated ssh attach commands.\n  AGENT_SESSION_STATE_DIR  Default state directory override.\n  AGENT_SESSION_TMUX_BIN   tmux binary override.\n  AGENT_SESSION_CODEX_BIN  codex binary override.\n  AGENT_SESSION_CLAUDE_BIN claude binary override.\n  AGENT_SESSION_HERMES_BIN hermes binary override.\n\nEXIT CODES:\n  0   success\n  1   runtime error\n  64  command-line usage error"
)]
pub struct Cli {
    /// State directory. Defaults to AGENT_SESSION_STATE_DIR, XDG_STATE_HOME/agent-session, or ~/.local/state/agent-session.
    #[arg(long = "state-dir", global = true, value_name = "PATH", value_hint = ValueHint::DirPath)]
    pub state_dir: Option<PathBuf>,

    /// Hostname used in generated ssh attach commands.
    #[arg(long, global = true, value_name = "HOST")]
    pub host: Option<String>,

    #[command(subcommand)]
    pub command: Command,
}

#[derive(Debug, Subcommand)]
pub enum Command {
    /// Start an interactive tmux-backed agent session.
    Start(StartArgs),
    /// Run a one-shot agent task in a tmux session and write output to a log file.
    Run(RunArgs),
    /// List recorded agent sessions.
    List(ListArgs),
    /// Print the attach command for a session.
    #[command(name = "command")]
    Show(SessionRefArgs),
    /// Attach to a tmux session from the current terminal.
    Attach(AttachArgs),
    /// Print captured tmux pane output or a one-shot run log.
    Logs(LogsArgs),
    /// Send input (literal text and/or special keys) to a live session.
    Send(SendArgs),
    /// Capture the recent pane tail plus live status as a dashboard glance.
    Glance(GlanceArgs),
    /// Recreate a missing tmux runtime from exact provider resume metadata.
    Resume(ResumeArgs),
    /// Inspect or ingest metadata-only agent turn lifecycle events.
    Activity(ActivityArgs),
    /// Serve the control plane (HTTP) and PTY attach (WebSocket) over loopback.
    Serve(ServeArgs),
    /// Delete session state and kill the tmux session if it is still alive.
    Delete(DeleteArgs),
    /// Print shell completion script.
    Completion(CompletionArgs),
}

#[derive(Debug, Args)]
pub struct StartArgs {
    /// Agent to run.
    #[arg(long, value_enum)]
    pub agent: AgentKind,

    /// Working directory for the agent session. Defaults to the current directory.
    #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
    pub cwd: Option<PathBuf>,

    /// Human-readable session title.
    #[arg(long, value_name = "TITLE")]
    pub title: Option<String>,

    /// Short explicit session id. Usually auto-generated.
    #[arg(long, value_name = "ID")]
    pub id: Option<String>,

    /// Prompt text. Prefer --prompt-file or --prompt-stdin for long prompts.
    #[arg(long, value_name = "TEXT")]
    pub prompt: Option<String>,

    /// Prompt file path, or '-' to read stdin.
    #[arg(long = "prompt-file", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub prompt_file: Option<PathBuf>,

    /// Read prompt from stdin.
    #[arg(long)]
    pub prompt_stdin: bool,

    /// tmux binary override.
    #[arg(long = "tmux-bin", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub tmux_bin: Option<PathBuf>,

    /// Agent binary override.
    #[arg(long = "agent-bin", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub agent_bin: Option<PathBuf>,

    /// Extra argument passed to the underlying agent command.
    #[arg(long = "agent-arg", value_name = "ARG")]
    pub agent_args: Vec<String>,

    /// Delay before pasting the initial prompt into the tmux pane.
    #[arg(long = "paste-delay-ms", default_value_t = DEFAULT_PASTE_DELAY_MS)]
    pub paste_delay_ms: u64,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct RunArgs {
    /// Agent to run.
    #[arg(long, value_enum)]
    pub agent: AgentKind,

    /// Working directory for the agent session. Defaults to the current directory.
    #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
    pub cwd: Option<PathBuf>,

    /// Human-readable session title.
    #[arg(long, value_name = "TITLE")]
    pub title: Option<String>,

    /// Short explicit session id. Usually auto-generated.
    #[arg(long, value_name = "ID")]
    pub id: Option<String>,

    /// Prompt text. Prefer --prompt-file or --prompt-stdin for long prompts.
    #[arg(long, value_name = "TEXT")]
    pub prompt: Option<String>,

    /// Prompt file path, or '-' to read stdin.
    #[arg(long = "prompt-file", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub prompt_file: Option<PathBuf>,

    /// Read prompt from stdin.
    #[arg(long)]
    pub prompt_stdin: bool,

    /// tmux binary override.
    #[arg(long = "tmux-bin", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub tmux_bin: Option<PathBuf>,

    /// Agent binary override.
    #[arg(long = "agent-bin", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub agent_bin: Option<PathBuf>,

    /// Extra argument passed to the underlying agent command.
    #[arg(long = "agent-arg", value_name = "ARG")]
    pub agent_args: Vec<String>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct ListArgs {
    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct SessionRefArgs {
    /// Session id.
    #[arg(value_name = "ID")]
    pub id: String,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct AttachArgs {
    /// Session id.
    #[arg(value_name = "ID")]
    pub id: String,

    /// tmux binary override.
    #[arg(long = "tmux-bin", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub tmux_bin: Option<PathBuf>,
}

#[derive(Debug, Args)]
pub struct LogsArgs {
    /// Session id.
    #[arg(value_name = "ID")]
    pub id: String,

    /// Number of lines to capture from the tmux pane.
    #[arg(long, default_value_t = 120)]
    pub tail: usize,

    /// tmux binary override.
    #[arg(long = "tmux-bin", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub tmux_bin: Option<PathBuf>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct SendArgs {
    /// Session id.
    #[arg(value_name = "ID")]
    pub id: String,

    /// Literal text to type into the session. Applied before any --key. Prefer
    /// --text-stdin for secrets (--text is visible in this process's arguments).
    #[arg(long, value_name = "TEXT")]
    pub text: Option<String>,

    /// Read the literal text to type from stdin (secret-safe; never echoed).
    #[arg(long = "text-stdin")]
    pub text_stdin: bool,

    /// Special key to press (repeatable), applied in order after any text.
    #[arg(long = "key", value_enum, value_name = "KEY")]
    pub keys: Vec<SpecialKey>,

    /// tmux binary override.
    #[arg(long = "tmux-bin", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub tmux_bin: Option<PathBuf>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct GlanceArgs {
    /// Session id.
    #[arg(value_name = "ID")]
    pub id: String,

    /// Number of pane lines to capture for the glance tail.
    #[arg(long, default_value_t = DEFAULT_GLANCE_TAIL)]
    pub tail: usize,

    /// tmux binary override.
    #[arg(long = "tmux-bin", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub tmux_bin: Option<PathBuf>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct ResumeArgs {
    /// Session id.
    #[arg(value_name = "ID")]
    pub id: String,

    /// tmux binary override.
    #[arg(long = "tmux-bin", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub tmux_bin: Option<PathBuf>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct ActivityArgs {
    #[command(subcommand)]
    pub command: ActivityCommand,
}

#[derive(Debug, Subcommand)]
pub enum ActivityCommand {
    /// Ingest one normalized metadata-only lifecycle event from stdin.
    Event(ActivityEventArgs),
    /// Inspect the durable turn-state snapshot for one session.
    Status(ActivityStatusArgs),
    /// Translate one provider hook payload into a safe normalized event.
    #[command(hide = true)]
    Hook(ActivityHookArgs),
    /// Report provider support, version, configuration, and repair guidance.
    Doctor(ActivityDoctorArgs),
    /// Preview or apply additive provider hook configuration.
    Setup(ActivitySetupArgs),
}

#[derive(Debug, Args)]
pub struct ActivityEventArgs {
    /// Session id.
    #[arg(value_name = "ID")]
    pub id: String,

    /// Read the JSON event from stdin.
    #[arg(long, required = true)]
    pub stdin: bool,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct ActivityStatusArgs {
    /// Session id.
    #[arg(value_name = "ID")]
    pub id: String,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct ActivityHookArgs {
    /// Provider whose hook payload is on stdin.
    #[arg(long, value_enum)]
    pub agent: AgentKind,

    /// Provider event name when the raw payload does not carry one.
    #[arg(long, hide = true)]
    pub event: Option<String>,
}

#[derive(Debug, Args)]
pub struct ActivityDoctorArgs {
    /// Limit diagnostics to one provider.
    #[arg(long, value_enum)]
    pub agent: Option<AgentKind>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct ActivitySetupArgs {
    /// Provider to configure.
    #[arg(long, value_enum)]
    pub agent: AgentKind,

    /// Preview the exact additive change without writing it.
    #[arg(
        long,
        required_unless_present_any = ["apply", "remove", "repair"],
        conflicts_with_all = ["apply", "remove", "repair"]
    )]
    pub dry_run: bool,

    /// Apply the additive provider integration.
    #[arg(long, conflicts_with_all = ["dry_run", "remove", "repair"])]
    pub apply: bool,

    /// Remove only agent-session-owned provider hook entries.
    #[arg(long, conflicts_with_all = ["dry_run", "apply", "repair"])]
    pub remove: bool,

    /// Restore missing agent-session-owned entries without replacing others.
    #[arg(long, conflicts_with_all = ["dry_run", "apply", "remove"])]
    pub repair: bool,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct ServeArgs {
    /// Address to bind. Defaults to loopback; a non-loopback address is refused
    /// unless --allow-non-loopback is passed (it exposes a remote shell).
    #[arg(long, value_name = "ADDR", default_value = "127.0.0.1:8781")]
    pub bind: String,

    /// Bearer token required on write and attach endpoints. Falls back to
    /// AGENT_SESSION_TOKEN. When unset, writes and attach are disabled (reads
    /// still work on loopback).
    #[arg(long, value_name = "TOKEN")]
    pub token: Option<String>,

    /// Read the bearer token once from stdin instead of process arguments.
    #[arg(long = "token-stdin", conflicts_with = "token")]
    pub token_stdin: bool,

    /// Machine identity reported in responses. Falls back to
    /// AGENT_SESSION_MACHINE, then --host, then the short hostname.
    #[arg(long, value_name = "NAME")]
    pub machine: Option<String>,

    /// Deliberately allow binding a non-loopback address. Without this, a
    /// non-loopback --bind is refused because it exposes a remote shell.
    #[arg(long = "allow-non-loopback")]
    pub allow_non_loopback: bool,

    /// tmux binary override.
    #[arg(long = "tmux-bin", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub tmux_bin: Option<PathBuf>,
}

#[derive(Debug, Args)]
pub struct DeleteArgs {
    /// Session id.
    #[arg(value_name = "ID")]
    pub id: String,

    /// tmux binary override.
    #[arg(long = "tmux-bin", value_name = "PATH", value_hint = ValueHint::FilePath)]
    pub tmux_bin: Option<PathBuf>,

    /// Output format.
    #[arg(long, value_enum, default_value_t = OutputFormat::Text)]
    pub format: OutputFormat,
}

#[derive(Debug, Args)]
pub struct CompletionArgs {
    /// Shell to generate completion script for.
    #[arg(value_enum)]
    pub shell: crate::completion::CompletionShell,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum AgentKind {
    Codex,
    Claude,
    Hermes,
}

impl AgentKind {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Codex => "codex",
            Self::Claude => "claude",
            Self::Hermes => "hermes",
        }
    }

    /// Parse an agent name (as accepted by `--agent` / emitted by `as_str`).
    /// Used by the serve create endpoint to map a JSON `agent` field.
    pub fn from_name(name: &str) -> Option<Self> {
        match name {
            "codex" => Some(Self::Codex),
            "claude" => Some(Self::Claude),
            "hermes" => Some(Self::Hermes),
            _ => None,
        }
    }
}

/// Named special keys accepted by `send`, mapped to tmux `send-keys` names.
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum SpecialKey {
    Enter,
    Escape,
    #[value(name = "c-c")]
    CtrlC,
    Up,
    Down,
    Left,
    Right,
    Tab,
}

impl SpecialKey {
    /// Canonical CLI name, used in the JSON contract (never echoes user input).
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Enter => "enter",
            Self::Escape => "escape",
            Self::CtrlC => "c-c",
            Self::Up => "up",
            Self::Down => "down",
            Self::Left => "left",
            Self::Right => "right",
            Self::Tab => "tab",
        }
    }

    /// Parse a canonical key name (as accepted by `--key` / emitted by `as_str`).
    /// Used by the serve WebSocket protocol to map client key names to keys.
    pub fn from_name(name: &str) -> Option<Self> {
        match name {
            "enter" => Some(Self::Enter),
            "escape" => Some(Self::Escape),
            "c-c" => Some(Self::CtrlC),
            "up" => Some(Self::Up),
            "down" => Some(Self::Down),
            "left" => Some(Self::Left),
            "right" => Some(Self::Right),
            "tab" => Some(Self::Tab),
            _ => None,
        }
    }

    /// tmux `send-keys` key name.
    pub fn tmux_key(self) -> &'static str {
        match self {
            Self::Enter => "Enter",
            Self::Escape => "Escape",
            Self::CtrlC => "C-c",
            Self::Up => "Up",
            Self::Down => "Down",
            Self::Left => "Left",
            Self::Right => "Right",
            Self::Tab => "Tab",
        }
    }
}