live-cmd 0.1.0__py3-none-any.whl
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.
- live/__init__.py +1 -0
- live/cli.py +250 -0
- live/completion.py +312 -0
- live/config.py +80 -0
- live/follow.py +199 -0
- live/format.py +206 -0
- live/lock.py +82 -0
- live/paths.py +38 -0
- live/reader.py +529 -0
- live/recorder.py +441 -0
- live/select_session.py +48 -0
- live/sweep.py +221 -0
- live/verbose.py +48 -0
- live/verbs.py +495 -0
- live/watcher.py +270 -0
- live_cmd-0.1.0.dist-info/METADATA +53 -0
- live_cmd-0.1.0.dist-info/RECORD +19 -0
- live_cmd-0.1.0.dist-info/WHEEL +4 -0
- live_cmd-0.1.0.dist-info/entry_points.txt +2 -0
live/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
live/cli.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Top-level CLI dispatch: `live <verb> ...`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from . import verbs
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _count_or_cursor(prefix: str):
|
|
13
|
+
"""Build a parser for `N` (count) or `<prefix>N` (cursor) on `-n` / `-c`.
|
|
14
|
+
|
|
15
|
+
`tail` uses `+N` (lines `n >= N`, Unix); `head` uses `-N` (drop last N,
|
|
16
|
+
GNU). The opposite sign is accepted but treated as a plain count.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def parse(value: str) -> tuple[str, int]:
|
|
20
|
+
if value.startswith(prefix):
|
|
21
|
+
rest = value[1:]
|
|
22
|
+
if rest.isdigit():
|
|
23
|
+
return ("cursor", int(rest))
|
|
24
|
+
elif value.startswith(("+", "-")) and value[1:].isdigit():
|
|
25
|
+
return ("count", int(value[1:]))
|
|
26
|
+
elif value.isdigit():
|
|
27
|
+
return ("count", int(value))
|
|
28
|
+
raise argparse.ArgumentTypeError(f"expected N or {prefix}N (got {value!r})")
|
|
29
|
+
|
|
30
|
+
return parse
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _make_parser() -> argparse.ArgumentParser:
|
|
34
|
+
p = argparse.ArgumentParser(
|
|
35
|
+
prog="live",
|
|
36
|
+
description="Stream long-lived command output to coding agents.",
|
|
37
|
+
add_help=True,
|
|
38
|
+
)
|
|
39
|
+
p.add_argument("--version", action="version", version=f"live {__version__}")
|
|
40
|
+
sub = p.add_subparsers(dest="verb", metavar="<verb>")
|
|
41
|
+
|
|
42
|
+
# run
|
|
43
|
+
run_p = sub.add_parser("run", help="Run <cmd> under a PTY; record.")
|
|
44
|
+
run_p.add_argument(
|
|
45
|
+
"-n", "--name", default=None, help="Session name."
|
|
46
|
+
)
|
|
47
|
+
run_p.add_argument(
|
|
48
|
+
"cmd",
|
|
49
|
+
nargs=argparse.REMAINDER,
|
|
50
|
+
help="Command to run; `--` for flag-starting commands.",
|
|
51
|
+
)
|
|
52
|
+
run_p.set_defaults(func=verbs.cmd_run)
|
|
53
|
+
|
|
54
|
+
# ls
|
|
55
|
+
ls_p = sub.add_parser("ls", help="List sessions in scope.")
|
|
56
|
+
ls_p.add_argument("-a", "--all", action="store_true", help="Include exited.")
|
|
57
|
+
ls_p.add_argument(
|
|
58
|
+
"-g",
|
|
59
|
+
"--global",
|
|
60
|
+
action="store_true",
|
|
61
|
+
dest="global_",
|
|
62
|
+
help="Global scope.",
|
|
63
|
+
)
|
|
64
|
+
ls_p.add_argument("--json", action="store_true", help="Emit NDJSON.")
|
|
65
|
+
ls_p.add_argument(
|
|
66
|
+
"selector", nargs="?", default=None, help="NAME or UUID-prefix filter."
|
|
67
|
+
)
|
|
68
|
+
ls_p.set_defaults(func=verbs.cmd_ls)
|
|
69
|
+
|
|
70
|
+
# cat
|
|
71
|
+
cat_p = sub.add_parser("cat", help="Concatenate session.")
|
|
72
|
+
cat_p.add_argument("-v", "--verbose", action="store_true", help="Verbose output.")
|
|
73
|
+
cat_p.add_argument(
|
|
74
|
+
"-g",
|
|
75
|
+
"--global",
|
|
76
|
+
action="store_true",
|
|
77
|
+
dest="global_",
|
|
78
|
+
help="Global scope.",
|
|
79
|
+
)
|
|
80
|
+
ag = cat_p.add_mutually_exclusive_group()
|
|
81
|
+
ag.add_argument(
|
|
82
|
+
"--strip-ansi",
|
|
83
|
+
action="store_true",
|
|
84
|
+
dest="strip_ansi",
|
|
85
|
+
help="Strip ANSI.",
|
|
86
|
+
)
|
|
87
|
+
ag.add_argument("--raw", action="store_true", dest="raw", help="Keep ANSI.")
|
|
88
|
+
cat_p.add_argument("selector", help="NAME or UUID-prefix.")
|
|
89
|
+
cat_p.set_defaults(func=verbs.cmd_cat)
|
|
90
|
+
|
|
91
|
+
# head
|
|
92
|
+
head_p = sub.add_parser("head", help="Head session.")
|
|
93
|
+
head_p.add_argument("-v", "--verbose", action="store_true", help="Verbose output.")
|
|
94
|
+
head_p.add_argument(
|
|
95
|
+
"-g",
|
|
96
|
+
"--global",
|
|
97
|
+
action="store_true",
|
|
98
|
+
dest="global_",
|
|
99
|
+
help="Global scope.",
|
|
100
|
+
)
|
|
101
|
+
ag = head_p.add_mutually_exclusive_group()
|
|
102
|
+
ag.add_argument(
|
|
103
|
+
"--strip-ansi",
|
|
104
|
+
action="store_true",
|
|
105
|
+
dest="strip_ansi",
|
|
106
|
+
help="Strip ANSI.",
|
|
107
|
+
)
|
|
108
|
+
ag.add_argument("--raw", action="store_true", dest="raw", help="Keep ANSI.")
|
|
109
|
+
mode = head_p.add_mutually_exclusive_group()
|
|
110
|
+
mode.add_argument(
|
|
111
|
+
"-n",
|
|
112
|
+
"--lines",
|
|
113
|
+
type=_count_or_cursor("-"),
|
|
114
|
+
default=None,
|
|
115
|
+
help="First N lines (default 10); -N drops last N.",
|
|
116
|
+
)
|
|
117
|
+
mode.add_argument(
|
|
118
|
+
"-c",
|
|
119
|
+
"--bytes",
|
|
120
|
+
dest="bytes_",
|
|
121
|
+
metavar="BYTES",
|
|
122
|
+
type=_count_or_cursor("-"),
|
|
123
|
+
default=None,
|
|
124
|
+
help="First K bytes; -K drops last K.",
|
|
125
|
+
)
|
|
126
|
+
mode.add_argument(
|
|
127
|
+
"-t",
|
|
128
|
+
"--time",
|
|
129
|
+
type=float,
|
|
130
|
+
default=None,
|
|
131
|
+
help="Lines with idx t <= T (epoch).",
|
|
132
|
+
)
|
|
133
|
+
head_p.add_argument("selector", help="NAME or UUID-prefix.")
|
|
134
|
+
head_p.set_defaults(func=verbs.cmd_head)
|
|
135
|
+
|
|
136
|
+
# tail
|
|
137
|
+
tail_p = sub.add_parser("tail", help="Tail session.")
|
|
138
|
+
tail_p.add_argument(
|
|
139
|
+
"-f", "--follow", action="store_true", help="Follow until exit."
|
|
140
|
+
)
|
|
141
|
+
tail_p.add_argument("-v", "--verbose", action="store_true", help="Verbose output.")
|
|
142
|
+
tail_p.add_argument(
|
|
143
|
+
"-g",
|
|
144
|
+
"--global",
|
|
145
|
+
action="store_true",
|
|
146
|
+
dest="global_",
|
|
147
|
+
help="Global scope.",
|
|
148
|
+
)
|
|
149
|
+
ag = tail_p.add_mutually_exclusive_group()
|
|
150
|
+
ag.add_argument(
|
|
151
|
+
"--strip-ansi",
|
|
152
|
+
action="store_true",
|
|
153
|
+
dest="strip_ansi",
|
|
154
|
+
help="Strip ANSI.",
|
|
155
|
+
)
|
|
156
|
+
ag.add_argument("--raw", action="store_true", dest="raw", help="Keep ANSI.")
|
|
157
|
+
mode = tail_p.add_mutually_exclusive_group()
|
|
158
|
+
mode.add_argument(
|
|
159
|
+
"-n",
|
|
160
|
+
"--lines",
|
|
161
|
+
type=_count_or_cursor("+"),
|
|
162
|
+
default=None,
|
|
163
|
+
help="Last N lines (default 10); +N for lines n >= N.",
|
|
164
|
+
)
|
|
165
|
+
mode.add_argument(
|
|
166
|
+
"-c",
|
|
167
|
+
"--bytes",
|
|
168
|
+
dest="bytes_",
|
|
169
|
+
metavar="BYTES",
|
|
170
|
+
type=_count_or_cursor("+"),
|
|
171
|
+
default=None,
|
|
172
|
+
help="Last K bytes; +K for bytes after offset K.",
|
|
173
|
+
)
|
|
174
|
+
mode.add_argument(
|
|
175
|
+
"-t",
|
|
176
|
+
"--time",
|
|
177
|
+
type=float,
|
|
178
|
+
default=None,
|
|
179
|
+
help="Lines with idx t > T (epoch).",
|
|
180
|
+
)
|
|
181
|
+
tail_p.add_argument("selector", help="NAME or UUID-prefix.")
|
|
182
|
+
tail_p.set_defaults(func=verbs.cmd_tail)
|
|
183
|
+
|
|
184
|
+
# rm
|
|
185
|
+
rm_p = sub.add_parser("rm", help="Delete sessions.")
|
|
186
|
+
rm_p.add_argument(
|
|
187
|
+
"-f",
|
|
188
|
+
"--force",
|
|
189
|
+
action="store_true",
|
|
190
|
+
help="SIGTERM live runs; ignore missing.",
|
|
191
|
+
)
|
|
192
|
+
rm_p.add_argument(
|
|
193
|
+
"-g",
|
|
194
|
+
"--global",
|
|
195
|
+
action="store_true",
|
|
196
|
+
dest="global_",
|
|
197
|
+
help="Global scope.",
|
|
198
|
+
)
|
|
199
|
+
rm_p.add_argument(
|
|
200
|
+
"--all-exited",
|
|
201
|
+
action="store_true",
|
|
202
|
+
dest="all_exited",
|
|
203
|
+
help="Remove all dead sessions.",
|
|
204
|
+
)
|
|
205
|
+
rm_p.add_argument(
|
|
206
|
+
"selectors", nargs="*", help="NAME(s) or UUID-prefix(es)."
|
|
207
|
+
)
|
|
208
|
+
rm_p.set_defaults(func=verbs.cmd_rm)
|
|
209
|
+
|
|
210
|
+
# llms.txt
|
|
211
|
+
llms_p = sub.add_parser("llms.txt", help="Print agent guide.")
|
|
212
|
+
llms_p.set_defaults(func=verbs.cmd_llms_txt)
|
|
213
|
+
|
|
214
|
+
# completion
|
|
215
|
+
comp_p = sub.add_parser("completion", help="Print shell completion script.")
|
|
216
|
+
comp_p.add_argument("shell", choices=["bash", "zsh", "fish"])
|
|
217
|
+
comp_p.set_defaults(func=verbs.cmd_completion)
|
|
218
|
+
|
|
219
|
+
# update-shell
|
|
220
|
+
up_p = sub.add_parser(
|
|
221
|
+
"update-shell", help="Install completion for the current shell."
|
|
222
|
+
)
|
|
223
|
+
up_p.add_argument(
|
|
224
|
+
"shell",
|
|
225
|
+
nargs="?",
|
|
226
|
+
choices=["bash", "zsh", "fish"],
|
|
227
|
+
default=None,
|
|
228
|
+
help="Target shell (default: $SHELL).",
|
|
229
|
+
)
|
|
230
|
+
up_p.set_defaults(func=verbs.cmd_update_shell)
|
|
231
|
+
|
|
232
|
+
return p
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main(argv: list[str] | None = None) -> int:
|
|
236
|
+
args = argv if argv is not None else sys.argv[1:]
|
|
237
|
+
parser = _make_parser()
|
|
238
|
+
|
|
239
|
+
parsed = parser.parse_args(args)
|
|
240
|
+
if not getattr(parsed, "verb", None):
|
|
241
|
+
parser.print_help()
|
|
242
|
+
return 0
|
|
243
|
+
try:
|
|
244
|
+
return parsed.func(parsed)
|
|
245
|
+
except KeyboardInterrupt:
|
|
246
|
+
return 130
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
sys.exit(main())
|
live/completion.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Shell completion script payloads, returned by `live completion <shell>`.
|
|
2
|
+
|
|
3
|
+
Each script offers verb completion, per-verb flag completion, selector
|
|
4
|
+
completion (NAME or UUID via `live ls --json`), and `live run <TAB>` handoff
|
|
5
|
+
to the wrapped command's completion. The selector helper mirrors the verb's
|
|
6
|
+
scope flags: `ls` only suggests active sessions unless `-a` was typed;
|
|
7
|
+
`cat`/`tail`/`rm` always pass `-a` since exited sessions remain valid
|
|
8
|
+
targets. `-g` is honored when present in the command line.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
BASH = r"""# bash completion for live
|
|
15
|
+
_live_complete() {
|
|
16
|
+
local cur prev words cword
|
|
17
|
+
_init_completion -n : 2>/dev/null || {
|
|
18
|
+
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
19
|
+
words=("${COMP_WORDS[@]}")
|
|
20
|
+
cword=$COMP_CWORD
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Find the verb (first non-flag token after `live`).
|
|
24
|
+
local verb verb_idx
|
|
25
|
+
for ((i=1; i<cword; i++)); do
|
|
26
|
+
case "${words[i]}" in
|
|
27
|
+
-*) continue ;;
|
|
28
|
+
*) verb="${words[i]}"; verb_idx=$i; break ;;
|
|
29
|
+
esac
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
if [ -z "$verb" ]; then
|
|
33
|
+
COMPREPLY=( $(compgen -W "run ls cat head tail rm llms.txt completion update-shell" -- "$cur") )
|
|
34
|
+
return
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
case "$verb" in
|
|
38
|
+
run)
|
|
39
|
+
# If the user has typed a non-flag arg after `run`, hand off to the wrapped
|
|
40
|
+
# command's completion via _command_offset.
|
|
41
|
+
local seen_cmd=0
|
|
42
|
+
for ((i=verb_idx+1; i<cword; i++)); do
|
|
43
|
+
case "${words[i]}" in
|
|
44
|
+
--) seen_cmd=$((i+1)); break ;;
|
|
45
|
+
-n) i=$((i+1)) ;;
|
|
46
|
+
-*) ;;
|
|
47
|
+
*) seen_cmd=$i; break ;;
|
|
48
|
+
esac
|
|
49
|
+
done
|
|
50
|
+
if [ "$seen_cmd" -gt 0 ] && type _command_offset >/dev/null 2>&1; then
|
|
51
|
+
_command_offset $seen_cmd
|
|
52
|
+
return
|
|
53
|
+
fi
|
|
54
|
+
COMPREPLY=( $(compgen -W "-n --" -- "$cur") )
|
|
55
|
+
;;
|
|
56
|
+
ls)
|
|
57
|
+
if [[ "$cur" == -* ]]; then
|
|
58
|
+
COMPREPLY=( $(compgen -W "-a --all -g --global --json" -- "$cur") )
|
|
59
|
+
else
|
|
60
|
+
COMPREPLY=( $(compgen -W "$(_live_selectors $(_live_all_flag) $(_live_global_flag))" -- "$cur") )
|
|
61
|
+
fi
|
|
62
|
+
;;
|
|
63
|
+
cat)
|
|
64
|
+
if [[ "$cur" == -* ]]; then
|
|
65
|
+
COMPREPLY=( $(compgen -W "-v --verbose -g --global --strip-ansi --raw" -- "$cur") )
|
|
66
|
+
else
|
|
67
|
+
COMPREPLY=( $(compgen -W "$(_live_selectors -a $(_live_global_flag))" -- "$cur") )
|
|
68
|
+
fi
|
|
69
|
+
;;
|
|
70
|
+
head)
|
|
71
|
+
if [[ "$cur" == -* ]]; then
|
|
72
|
+
COMPREPLY=( $(compgen -W "-v --verbose -g --global --strip-ansi --raw -n --lines -c --bytes -t --time" -- "$cur") )
|
|
73
|
+
else
|
|
74
|
+
COMPREPLY=( $(compgen -W "$(_live_selectors -a $(_live_global_flag))" -- "$cur") )
|
|
75
|
+
fi
|
|
76
|
+
;;
|
|
77
|
+
tail)
|
|
78
|
+
if [[ "$cur" == -* ]]; then
|
|
79
|
+
COMPREPLY=( $(compgen -W "-v --verbose -f --follow -g --global --strip-ansi --raw -n --lines -c --bytes -t --time" -- "$cur") )
|
|
80
|
+
else
|
|
81
|
+
COMPREPLY=( $(compgen -W "$(_live_selectors -a $(_live_global_flag))" -- "$cur") )
|
|
82
|
+
fi
|
|
83
|
+
;;
|
|
84
|
+
rm)
|
|
85
|
+
if [[ "$cur" == -* ]]; then
|
|
86
|
+
COMPREPLY=( $(compgen -W "-f --force -g --global --all-exited" -- "$cur") )
|
|
87
|
+
else
|
|
88
|
+
COMPREPLY=( $(compgen -W "$(_live_selectors -a $(_live_global_flag))" -- "$cur") )
|
|
89
|
+
fi
|
|
90
|
+
;;
|
|
91
|
+
completion|update-shell)
|
|
92
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
|
93
|
+
;;
|
|
94
|
+
esac
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_live_all_flag() {
|
|
98
|
+
local w
|
|
99
|
+
for w in "${words[@]:1}"; do
|
|
100
|
+
case "$w" in -a|--all) echo -a; return ;; esac
|
|
101
|
+
done
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_live_global_flag() {
|
|
105
|
+
local w
|
|
106
|
+
for w in "${words[@]:1}"; do
|
|
107
|
+
case "$w" in -g|--global) echo -g; return ;; esac
|
|
108
|
+
done
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_live_selectors() {
|
|
112
|
+
live ls "$@" --json 2>/dev/null \
|
|
113
|
+
| awk -F'"' '{ for (i=1;i<=NF;i++) if ($i=="id"||$i=="name") print $(i+2) }' \
|
|
114
|
+
| sort -u
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
complete -F _live_complete live
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
ZSH = r"""#compdef live
|
|
122
|
+
|
|
123
|
+
_live() {
|
|
124
|
+
local context state line
|
|
125
|
+
typeset -A opt_args
|
|
126
|
+
|
|
127
|
+
_arguments -C \
|
|
128
|
+
'1: :_live_verbs' \
|
|
129
|
+
'*::arg:->args' \
|
|
130
|
+
&& return 0
|
|
131
|
+
|
|
132
|
+
case $state in
|
|
133
|
+
args)
|
|
134
|
+
case $words[1] in
|
|
135
|
+
run)
|
|
136
|
+
# Complete our flag before the wrapped command; any non-flag
|
|
137
|
+
# word triggers `_normal` against the wrapped command.
|
|
138
|
+
_arguments -S \
|
|
139
|
+
'-n+[session name]:name:' \
|
|
140
|
+
'*::command:_normal'
|
|
141
|
+
;;
|
|
142
|
+
ls)
|
|
143
|
+
_arguments \
|
|
144
|
+
'(-a --all)'{-a,--all} \
|
|
145
|
+
'(-g --global)'{-g,--global} \
|
|
146
|
+
'--json' \
|
|
147
|
+
'1:selector:_live_selectors'
|
|
148
|
+
;;
|
|
149
|
+
cat)
|
|
150
|
+
_arguments \
|
|
151
|
+
'(-v --verbose)'{-v,--verbose} \
|
|
152
|
+
'(-g --global)'{-g,--global} \
|
|
153
|
+
'(--strip-ansi --raw)--strip-ansi' \
|
|
154
|
+
'(--strip-ansi --raw)--raw' \
|
|
155
|
+
'1:selector:_live_selectors'
|
|
156
|
+
;;
|
|
157
|
+
head)
|
|
158
|
+
_arguments \
|
|
159
|
+
'(-v --verbose)'{-v,--verbose} \
|
|
160
|
+
'(-g --global)'{-g,--global} \
|
|
161
|
+
'(--strip-ansi --raw)--strip-ansi' \
|
|
162
|
+
'(--strip-ansi --raw)--raw' \
|
|
163
|
+
'(-n --lines)'{-n+,--lines=}':lines:' \
|
|
164
|
+
'(-c --bytes)'{-c+,--bytes=}':bytes:' \
|
|
165
|
+
'(-t --time)'{-t+,--time=}':epoch-seconds:' \
|
|
166
|
+
'1:selector:_live_selectors'
|
|
167
|
+
;;
|
|
168
|
+
tail)
|
|
169
|
+
_arguments \
|
|
170
|
+
'(-v --verbose)'{-v,--verbose} \
|
|
171
|
+
'(-f --follow)'{-f,--follow} \
|
|
172
|
+
'(-g --global)'{-g,--global} \
|
|
173
|
+
'(--strip-ansi --raw)--strip-ansi' \
|
|
174
|
+
'(--strip-ansi --raw)--raw' \
|
|
175
|
+
'(-n --lines)'{-n+,--lines=}':lines:' \
|
|
176
|
+
'(-c --bytes)'{-c+,--bytes=}':bytes:' \
|
|
177
|
+
'(-t --time)'{-t+,--time=}':epoch-seconds:' \
|
|
178
|
+
'1:selector:_live_selectors'
|
|
179
|
+
;;
|
|
180
|
+
rm)
|
|
181
|
+
_arguments \
|
|
182
|
+
'(-f --force)'{-f,--force} \
|
|
183
|
+
'(-g --global)'{-g,--global} \
|
|
184
|
+
'--all-exited' \
|
|
185
|
+
'*:selector:_live_selectors'
|
|
186
|
+
;;
|
|
187
|
+
completion|update-shell)
|
|
188
|
+
_arguments '1:shell:(bash zsh fish)'
|
|
189
|
+
;;
|
|
190
|
+
esac
|
|
191
|
+
;;
|
|
192
|
+
esac
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_live_verbs() {
|
|
196
|
+
local -a verbs
|
|
197
|
+
verbs=(
|
|
198
|
+
'run:Wrap <cmd> under a PTY and record'
|
|
199
|
+
'ls:List sessions in scope'
|
|
200
|
+
'cat:Concatenate stream.*.log for a session'
|
|
201
|
+
'head:Head first lines of a session'
|
|
202
|
+
'tail:Tail a session'
|
|
203
|
+
'rm:Delete sessions'
|
|
204
|
+
'llms.txt:Print a token-minimal agent guide'
|
|
205
|
+
'completion:Print shell completion script'
|
|
206
|
+
'update-shell:Install completion for the current shell'
|
|
207
|
+
)
|
|
208
|
+
_describe -t verbs 'verb' verbs
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Selector completion. For `live ls`, only suggest active sessions unless -a
|
|
212
|
+
# was typed; for other verbs, always include exited (still valid targets).
|
|
213
|
+
# Honors -g/--global when present in the command line.
|
|
214
|
+
_live_selectors() {
|
|
215
|
+
local -a sel_args names
|
|
216
|
+
local w want_all=0 want_global=0
|
|
217
|
+
for w in $words[@]; do
|
|
218
|
+
case $w in
|
|
219
|
+
-a|--all) want_all=1 ;;
|
|
220
|
+
-g|--global) want_global=1 ;;
|
|
221
|
+
esac
|
|
222
|
+
done
|
|
223
|
+
if [[ $words[1] != ls ]] || (( want_all )); then
|
|
224
|
+
sel_args+=(-a)
|
|
225
|
+
fi
|
|
226
|
+
(( want_global )) && sel_args+=(-g)
|
|
227
|
+
names=( ${(f)"$(live ls $sel_args --json 2>/dev/null | awk -F'"' '{ for (i=1;i<=NF;i++) if ($i=="id"||$i=="name") print $(i+2) }' | sort -u)"} )
|
|
228
|
+
(( $#names )) && _values 'selector' "${names[@]}"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_live "$@"
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
FISH = r"""# fish completion for live
|
|
236
|
+
|
|
237
|
+
function __live_selectors
|
|
238
|
+
set -l toks (commandline -opc)
|
|
239
|
+
set -l verb $toks[2]
|
|
240
|
+
set -l args
|
|
241
|
+
if test "$verb" != "ls"; or contains -- -a $toks; or contains -- --all $toks
|
|
242
|
+
set -a args -a
|
|
243
|
+
end
|
|
244
|
+
if contains -- -g $toks; or contains -- --global $toks
|
|
245
|
+
set -a args -g
|
|
246
|
+
end
|
|
247
|
+
live ls $args --json 2>/dev/null | string match -rga '"(?:id|name)":"([^"]+)"' | sort -u
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
set -l verbs run ls cat head tail rm llms.txt completion update-shell
|
|
251
|
+
|
|
252
|
+
complete -c live -f
|
|
253
|
+
complete -c live -n "not __fish_seen_subcommand_from $verbs" -a run -d 'Wrap <cmd> under a PTY'
|
|
254
|
+
complete -c live -n "not __fish_seen_subcommand_from $verbs" -a ls -d 'List sessions'
|
|
255
|
+
complete -c live -n "not __fish_seen_subcommand_from $verbs" -a cat -d 'Concatenate stream.*.log'
|
|
256
|
+
complete -c live -n "not __fish_seen_subcommand_from $verbs" -a head -d 'Head first lines of a session'
|
|
257
|
+
complete -c live -n "not __fish_seen_subcommand_from $verbs" -a tail -d 'Tail a session'
|
|
258
|
+
complete -c live -n "not __fish_seen_subcommand_from $verbs" -a rm -d 'Delete sessions'
|
|
259
|
+
complete -c live -n "not __fish_seen_subcommand_from $verbs" -a llms.txt -d 'Print agent guide'
|
|
260
|
+
complete -c live -n "not __fish_seen_subcommand_from $verbs" -a completion -d 'Print completion script'
|
|
261
|
+
complete -c live -n "not __fish_seen_subcommand_from $verbs" -a update-shell -d 'Install completion for current shell'
|
|
262
|
+
|
|
263
|
+
# Selector completion for ls / cat / head / tail / rm.
|
|
264
|
+
complete -c live -n "__fish_seen_subcommand_from ls cat head tail rm" -a "(__live_selectors)"
|
|
265
|
+
|
|
266
|
+
# ls
|
|
267
|
+
complete -c live -n "__fish_seen_subcommand_from ls" -s a -l all -d 'Include exited sessions'
|
|
268
|
+
complete -c live -n "__fish_seen_subcommand_from ls" -s g -l global -d 'Show sessions from all directories'
|
|
269
|
+
complete -c live -n "__fish_seen_subcommand_from ls" -l json -d 'Emit NDJSON'
|
|
270
|
+
|
|
271
|
+
# cat
|
|
272
|
+
complete -c live -n "__fish_seen_subcommand_from cat" -s v -l verbose -d 'Add stderr metadata'
|
|
273
|
+
complete -c live -n "__fish_seen_subcommand_from cat" -s g -l global -d 'Resolve selector globally'
|
|
274
|
+
complete -c live -n "__fish_seen_subcommand_from cat" -l strip-ansi -d 'Remove ANSI escapes'
|
|
275
|
+
complete -c live -n "__fish_seen_subcommand_from cat" -l raw -d 'Keep ANSI escapes'
|
|
276
|
+
|
|
277
|
+
# head
|
|
278
|
+
complete -c live -n "__fish_seen_subcommand_from head" -s v -l verbose
|
|
279
|
+
complete -c live -n "__fish_seen_subcommand_from head" -s g -l global -d 'Resolve selector globally'
|
|
280
|
+
complete -c live -n "__fish_seen_subcommand_from head" -l strip-ansi
|
|
281
|
+
complete -c live -n "__fish_seen_subcommand_from head" -l raw
|
|
282
|
+
complete -c live -n "__fish_seen_subcommand_from head" -s n -l lines -r -d 'First N lines (default 10)'
|
|
283
|
+
complete -c live -n "__fish_seen_subcommand_from head" -s c -l bytes -r -d 'First K bytes'
|
|
284
|
+
complete -c live -n "__fish_seen_subcommand_from head" -s t -l time -r -d 'Lines with t <= T (epoch seconds)'
|
|
285
|
+
|
|
286
|
+
# tail
|
|
287
|
+
complete -c live -n "__fish_seen_subcommand_from tail" -s v -l verbose
|
|
288
|
+
complete -c live -n "__fish_seen_subcommand_from tail" -s f -l follow -d 'Follow new lines'
|
|
289
|
+
complete -c live -n "__fish_seen_subcommand_from tail" -s g -l global -d 'Resolve selector globally'
|
|
290
|
+
complete -c live -n "__fish_seen_subcommand_from tail" -l strip-ansi
|
|
291
|
+
complete -c live -n "__fish_seen_subcommand_from tail" -l raw
|
|
292
|
+
complete -c live -n "__fish_seen_subcommand_from tail" -s n -l lines -r -d 'Last N lines'
|
|
293
|
+
complete -c live -n "__fish_seen_subcommand_from tail" -s c -l bytes -r -d 'Last K bytes'
|
|
294
|
+
complete -c live -n "__fish_seen_subcommand_from tail" -s t -l time -r -d 'Time cursor (epoch seconds)'
|
|
295
|
+
|
|
296
|
+
# rm
|
|
297
|
+
complete -c live -n "__fish_seen_subcommand_from rm" -s f -l force -d 'Kill running recorders'
|
|
298
|
+
complete -c live -n "__fish_seen_subcommand_from rm" -s g -l global -d 'Resolve selectors globally'
|
|
299
|
+
complete -c live -n "__fish_seen_subcommand_from rm" -l all-exited -d 'Remove every dead session'
|
|
300
|
+
|
|
301
|
+
# run -- hand off after first non-flag token.
|
|
302
|
+
complete -c live -n "__fish_seen_subcommand_from run" -s n -r -d 'Session name'
|
|
303
|
+
complete -c live -n "__fish_seen_subcommand_from run; and __fish_complete_subcommand --skip 2" \
|
|
304
|
+
-a "(__fish_complete_subcommand --skip 2)"
|
|
305
|
+
|
|
306
|
+
# completion / update-shell
|
|
307
|
+
complete -c live -n "__fish_seen_subcommand_from completion update-shell" -a "bash zsh fish"
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def script_for(shell: str) -> str | None:
|
|
312
|
+
return {"bash": BASH, "zsh": ZSH, "fish": FISH}.get(shell)
|
live/config.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .paths import config_path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULTS = {
|
|
12
|
+
"ttlDays": 7,
|
|
13
|
+
"maxKb": 512,
|
|
14
|
+
"segmentKb": 64,
|
|
15
|
+
"heartbeatSec": 30,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_VALIDATORS = {
|
|
20
|
+
"ttlDays": lambda v: isinstance(v, int) and not isinstance(v, bool) and v >= 0,
|
|
21
|
+
"maxKb": lambda v: isinstance(v, int) and not isinstance(v, bool) and v > 0,
|
|
22
|
+
"segmentKb": lambda v: isinstance(v, int) and not isinstance(v, bool) and v > 0,
|
|
23
|
+
"heartbeatSec": lambda v: isinstance(v, int) and not isinstance(v, bool) and v > 0,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Config:
|
|
29
|
+
ttl_days: int
|
|
30
|
+
max_kb: int
|
|
31
|
+
segment_kb: int
|
|
32
|
+
heartbeat_sec: int
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def max_bytes(self) -> int:
|
|
36
|
+
return self.max_kb * 1024
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def segment_bytes(self) -> int:
|
|
40
|
+
return self.segment_kb * 1024
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_file(path: Path) -> dict[str, int]:
|
|
44
|
+
"""Read config; return {} for missing/malformed (warns on malformed)."""
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return {}
|
|
47
|
+
try:
|
|
48
|
+
with path.open("r", encoding="utf-8") as f:
|
|
49
|
+
raw = json.load(f)
|
|
50
|
+
except (OSError, ValueError) as e:
|
|
51
|
+
print(f"live: malformed config at {path}: {e} — falling back to defaults",
|
|
52
|
+
file=sys.stderr)
|
|
53
|
+
return {}
|
|
54
|
+
if not isinstance(raw, dict):
|
|
55
|
+
return {}
|
|
56
|
+
out: dict[str, int] = {}
|
|
57
|
+
for key, val in raw.items():
|
|
58
|
+
validator = _VALIDATORS.get(key)
|
|
59
|
+
if validator is None:
|
|
60
|
+
continue
|
|
61
|
+
if validator(val):
|
|
62
|
+
out[key] = val
|
|
63
|
+
return out
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_config() -> Config:
|
|
67
|
+
"""Read `~/.live/config.json`, falling back per-field to compiled defaults."""
|
|
68
|
+
cfg_path = config_path()
|
|
69
|
+
if not cfg_path.exists():
|
|
70
|
+
try:
|
|
71
|
+
cfg_path.write_text(json.dumps(DEFAULTS) + "\n")
|
|
72
|
+
except OSError:
|
|
73
|
+
pass
|
|
74
|
+
merged = {**DEFAULTS, **_load_file(cfg_path)}
|
|
75
|
+
return Config(
|
|
76
|
+
ttl_days=int(merged["ttlDays"]),
|
|
77
|
+
max_kb=int(merged["maxKb"]),
|
|
78
|
+
segment_kb=int(merged["segmentKb"]),
|
|
79
|
+
heartbeat_sec=int(merged["heartbeatSec"]),
|
|
80
|
+
)
|