handoff-cli 0.3.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.
- cli/__init__.py +3 -0
- cli/backend.py +224 -0
- cli/backend_types.yaml +91 -0
- cli/commands/__init__.py +0 -0
- cli/commands/env.py +30 -0
- cli/commands/init.py +129 -0
- cli/commands/list.py +81 -0
- cli/commands/resume.py +179 -0
- cli/commands/run.py +211 -0
- cli/commands/tail.py +48 -0
- cli/config.py +351 -0
- cli/core.py +302 -0
- cli/jsonl_parser.py +182 -0
- cli/jsonl_viewer.py +440 -0
- cli/main.py +98 -0
- cli/skills/handoff-codex/SKILL.md +77 -0
- cli/skills/handoff-ds/SKILL.md +77 -0
- cli/skills/handoff-ds.toml +52 -0
- cli/skills/handoff-opus/SKILL.md +77 -0
- cli/stream.py +286 -0
- cli/tui.py +317 -0
- cli/user_config_template.yaml +31 -0
- handoff_cli-0.3.0.dist-info/METADATA +7 -0
- handoff_cli-0.3.0.dist-info/RECORD +26 -0
- handoff_cli-0.3.0.dist-info/WHEEL +4 -0
- handoff_cli-0.3.0.dist-info/entry_points.txt +2 -0
cli/commands/resume.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""handoff resume command.
|
|
2
|
+
|
|
3
|
+
Unifies "reopen a past conversation" into one verb, keyed by seq (or run-id):
|
|
4
|
+
|
|
5
|
+
handoff resume <seq> — interactive: drop into `claude --resume`
|
|
6
|
+
handoff resume <seq> - <<'EOF' ... — non-interactive: dispatch a new task to
|
|
7
|
+
handoff resume <seq> --text "..." that same conversation (claude -p --resume),
|
|
8
|
+
running through the normal run pipeline.
|
|
9
|
+
|
|
10
|
+
The seq → session mapping comes from the runs table: the selected row's
|
|
11
|
+
`session_id` is the underlying claude conversation. `--resume` does not fork, so
|
|
12
|
+
the original seq stays a stable handle — keep using it to add more turns.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import shlex
|
|
18
|
+
|
|
19
|
+
from ..core import get_db, find_run, short_path, row_value
|
|
20
|
+
from ..backend import set_backend_env, build_resume_args, resolve_backend_model
|
|
21
|
+
from ..config import Config
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def cmd_resume(argv: list[str], config: Config):
|
|
25
|
+
"""handoff resume [<run-id|seq>] [--backend <name>] [--pro] [--cwd <dir>]
|
|
26
|
+
[(<input-file|-> | --text <prompt...>)]."""
|
|
27
|
+
pro = False
|
|
28
|
+
cwd = ""
|
|
29
|
+
backend_arg = ""
|
|
30
|
+
selector = ""
|
|
31
|
+
input_src = ""
|
|
32
|
+
text_mode = False
|
|
33
|
+
text_parts = []
|
|
34
|
+
have_selector = False
|
|
35
|
+
|
|
36
|
+
i = 0
|
|
37
|
+
while i < len(argv):
|
|
38
|
+
a = argv[i]
|
|
39
|
+
if a == "-":
|
|
40
|
+
input_src = "-"
|
|
41
|
+
elif a == "--cwd":
|
|
42
|
+
i += 1
|
|
43
|
+
if i >= len(argv):
|
|
44
|
+
print("handoff resume: --cwd requires a value", file=sys.stderr)
|
|
45
|
+
sys.exit(2)
|
|
46
|
+
cwd = argv[i]
|
|
47
|
+
elif a == "--backend":
|
|
48
|
+
i += 1
|
|
49
|
+
if i >= len(argv):
|
|
50
|
+
print("handoff resume: --backend requires a value", file=sys.stderr)
|
|
51
|
+
sys.exit(2)
|
|
52
|
+
backend_arg = argv[i]
|
|
53
|
+
elif a.startswith("--backend="):
|
|
54
|
+
backend_arg = a.split("=", 1)[1]
|
|
55
|
+
elif a == "--text":
|
|
56
|
+
text_mode = True
|
|
57
|
+
if input_src:
|
|
58
|
+
print("handoff resume: --text cannot be combined with an input file", file=sys.stderr)
|
|
59
|
+
sys.exit(2)
|
|
60
|
+
if i + 1 >= len(argv):
|
|
61
|
+
print("handoff resume: --text requires a value", file=sys.stderr)
|
|
62
|
+
sys.exit(2)
|
|
63
|
+
if argv[i + 1] == "--":
|
|
64
|
+
text_parts.extend(argv[i + 2:])
|
|
65
|
+
else:
|
|
66
|
+
text_parts.extend(argv[i + 1:])
|
|
67
|
+
break
|
|
68
|
+
elif a.startswith("--text="):
|
|
69
|
+
text_mode = True
|
|
70
|
+
if input_src:
|
|
71
|
+
print("handoff resume: --text cannot be combined with an input file", file=sys.stderr)
|
|
72
|
+
sys.exit(2)
|
|
73
|
+
text_parts.append(a.split("=", 1)[1])
|
|
74
|
+
text_parts.extend(argv[i + 1:])
|
|
75
|
+
break
|
|
76
|
+
elif a == "--pro":
|
|
77
|
+
pro = True
|
|
78
|
+
elif a in ("-h", "--help"):
|
|
79
|
+
from ..main import usage
|
|
80
|
+
usage()
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
elif a.startswith("-") and a != "-":
|
|
83
|
+
print(f"handoff resume: unknown option {a}", file=sys.stderr)
|
|
84
|
+
sys.exit(2)
|
|
85
|
+
else:
|
|
86
|
+
# First bare positional is the selector (seq/run-id); a second one is
|
|
87
|
+
# an input file (prompt source).
|
|
88
|
+
if not have_selector:
|
|
89
|
+
selector = a
|
|
90
|
+
have_selector = True
|
|
91
|
+
elif text_mode:
|
|
92
|
+
print("handoff resume: --text cannot be combined with an input file", file=sys.stderr)
|
|
93
|
+
sys.exit(2)
|
|
94
|
+
else:
|
|
95
|
+
input_src = a
|
|
96
|
+
i += 1
|
|
97
|
+
|
|
98
|
+
# Resolve the target conversation.
|
|
99
|
+
conn = get_db()
|
|
100
|
+
row = find_run(conn, selector or None)
|
|
101
|
+
|
|
102
|
+
if not row:
|
|
103
|
+
conn.close()
|
|
104
|
+
print("handoff resume: no run found", file=sys.stderr)
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
|
|
107
|
+
session_id = row_value(row, "session_id", "") or row["uuid"]
|
|
108
|
+
row_cwd = row["cwd"]
|
|
109
|
+
saved_backend = row_value(row, "backend", "") or ""
|
|
110
|
+
|
|
111
|
+
# Decide prompt source → interactive vs continuation.
|
|
112
|
+
prompt_text = None
|
|
113
|
+
if text_mode:
|
|
114
|
+
prompt_text = " ".join(text_parts)
|
|
115
|
+
if not prompt_text:
|
|
116
|
+
print("handoff resume: --text requires a non-empty value", file=sys.stderr)
|
|
117
|
+
sys.exit(2)
|
|
118
|
+
elif input_src == "-" or (not input_src and not sys.stdin.isatty()):
|
|
119
|
+
prompt_text = sys.stdin.read()
|
|
120
|
+
elif input_src:
|
|
121
|
+
if not os.path.isfile(input_src):
|
|
122
|
+
print(f"handoff resume: input file not found: {input_src}", file=sys.stderr)
|
|
123
|
+
sys.exit(2)
|
|
124
|
+
with open(input_src) as f:
|
|
125
|
+
prompt_text = f.read()
|
|
126
|
+
|
|
127
|
+
if not cwd:
|
|
128
|
+
cwd = row_cwd
|
|
129
|
+
if not os.path.isdir(cwd):
|
|
130
|
+
print(f"handoff resume: cwd not found: {cwd}", file=sys.stderr)
|
|
131
|
+
sys.exit(2)
|
|
132
|
+
|
|
133
|
+
# A continuation must stay on the conversation's original backend — the
|
|
134
|
+
# session id only means something to the CLI that created it.
|
|
135
|
+
if backend_arg and saved_backend and backend_arg != saved_backend:
|
|
136
|
+
print(
|
|
137
|
+
f"handoff resume: this conversation belongs to backend '{saved_backend}'; "
|
|
138
|
+
f"it cannot be resumed with --backend {backend_arg}. "
|
|
139
|
+
f"Use `handoff run --backend {backend_arg}` to start a new conversation.",
|
|
140
|
+
file=sys.stderr,
|
|
141
|
+
)
|
|
142
|
+
sys.exit(2)
|
|
143
|
+
backend_name = saved_backend or backend_arg or config.default_backend
|
|
144
|
+
|
|
145
|
+
if prompt_text is None:
|
|
146
|
+
# Interactive: reopen the conversation in claude (replaces this process).
|
|
147
|
+
conn.close()
|
|
148
|
+
_resume_interactive(config, backend_name, session_id, cwd, pro)
|
|
149
|
+
else:
|
|
150
|
+
# Non-interactive: dispatch a new turn through the run pipeline.
|
|
151
|
+
conn.close()
|
|
152
|
+
from .run import _execute
|
|
153
|
+
_execute(cwd, prompt_text, backend_name, pro, config, resume_session_id=session_id)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resume_interactive(config: Config, backend_name: str, session_id: str, cwd: str, pro: bool):
|
|
157
|
+
backend_cfg = config.get_backend(backend_name)
|
|
158
|
+
if not backend_cfg:
|
|
159
|
+
print(
|
|
160
|
+
f"handoff: unknown backend '{backend_name}'. "
|
|
161
|
+
f"Available: {', '.join(sorted(config.backends.keys()))}",
|
|
162
|
+
file=sys.stderr,
|
|
163
|
+
)
|
|
164
|
+
sys.exit(2)
|
|
165
|
+
|
|
166
|
+
model = resolve_backend_model(backend_cfg, pro)
|
|
167
|
+
backend_cfg["_resolved_model"] = model
|
|
168
|
+
backend_cfg["_system_prompt"] = config.system_prompt
|
|
169
|
+
|
|
170
|
+
set_backend_env(backend_cfg, model, backend_cfg.get("pro_model", ""))
|
|
171
|
+
|
|
172
|
+
args = build_resume_args(
|
|
173
|
+
backend_cfg, session_id,
|
|
174
|
+
pro_model=backend_cfg.get("pro_model", ""),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
print(f"cd {short_path(cwd)}; {' '.join(shlex.quote(p) for p in args)}", file=sys.stderr)
|
|
178
|
+
os.chdir(cwd)
|
|
179
|
+
os.execvp(args[0], args)
|
cli/commands/run.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""handoff run command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import datetime
|
|
8
|
+
|
|
9
|
+
from ..core import get_db, create_run, task_paths, UUID_RE
|
|
10
|
+
from ..backend import (
|
|
11
|
+
set_backend_env,
|
|
12
|
+
build_args,
|
|
13
|
+
backend_type,
|
|
14
|
+
ensure_backend_token_ready,
|
|
15
|
+
resolve_backend_model,
|
|
16
|
+
wrap_with_pty,
|
|
17
|
+
)
|
|
18
|
+
from ..stream import execute_run
|
|
19
|
+
from ..config import Config
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cmd_run(argv: list[str], config: Config):
|
|
23
|
+
"""handoff run [--backend <name>] [--cwd <dir>] [--pro] (<input-file|-> | --text <prompt...>)."""
|
|
24
|
+
pro = False
|
|
25
|
+
cwd = ""
|
|
26
|
+
backend_arg = ""
|
|
27
|
+
input_src = ""
|
|
28
|
+
text_mode = False
|
|
29
|
+
text_parts = []
|
|
30
|
+
|
|
31
|
+
i = 0
|
|
32
|
+
while i < len(argv):
|
|
33
|
+
a = argv[i]
|
|
34
|
+
if a == "-":
|
|
35
|
+
input_src = "-"
|
|
36
|
+
elif a == "--cwd":
|
|
37
|
+
i += 1
|
|
38
|
+
if i >= len(argv):
|
|
39
|
+
print("handoff run: --cwd requires a value", file=sys.stderr)
|
|
40
|
+
sys.exit(2)
|
|
41
|
+
cwd = argv[i]
|
|
42
|
+
elif a == "--backend":
|
|
43
|
+
i += 1
|
|
44
|
+
if i >= len(argv):
|
|
45
|
+
print("handoff run: --backend requires a value", file=sys.stderr)
|
|
46
|
+
sys.exit(2)
|
|
47
|
+
backend_arg = argv[i]
|
|
48
|
+
elif a.startswith("--backend="):
|
|
49
|
+
backend_arg = a.split("=", 1)[1]
|
|
50
|
+
elif a == "--text":
|
|
51
|
+
text_mode = True
|
|
52
|
+
if input_src:
|
|
53
|
+
print("handoff run: --text cannot be combined with an input file", file=sys.stderr)
|
|
54
|
+
sys.exit(2)
|
|
55
|
+
if i + 1 >= len(argv):
|
|
56
|
+
print("handoff run: --text requires a value", file=sys.stderr)
|
|
57
|
+
sys.exit(2)
|
|
58
|
+
if argv[i + 1] == "--":
|
|
59
|
+
text_parts.extend(argv[i + 2:])
|
|
60
|
+
else:
|
|
61
|
+
text_parts.extend(argv[i + 1:])
|
|
62
|
+
break
|
|
63
|
+
elif a.startswith("--text="):
|
|
64
|
+
text_mode = True
|
|
65
|
+
if input_src:
|
|
66
|
+
print("handoff run: --text cannot be combined with an input file", file=sys.stderr)
|
|
67
|
+
sys.exit(2)
|
|
68
|
+
text_parts.append(a.split("=", 1)[1])
|
|
69
|
+
text_parts.extend(argv[i + 1:])
|
|
70
|
+
break
|
|
71
|
+
elif a == "--pro":
|
|
72
|
+
pro = True
|
|
73
|
+
elif a in ("-h", "--help"):
|
|
74
|
+
from ..main import usage
|
|
75
|
+
usage()
|
|
76
|
+
sys.exit(0)
|
|
77
|
+
elif a == "--":
|
|
78
|
+
i += 1
|
|
79
|
+
if i < len(argv):
|
|
80
|
+
input_src = argv[i]
|
|
81
|
+
break
|
|
82
|
+
elif a.startswith("-"):
|
|
83
|
+
print(f"handoff run: unknown option {a}", file=sys.stderr)
|
|
84
|
+
sys.exit(2)
|
|
85
|
+
else:
|
|
86
|
+
if text_mode:
|
|
87
|
+
print("handoff run: --text cannot be combined with an input file", file=sys.stderr)
|
|
88
|
+
sys.exit(2)
|
|
89
|
+
input_src = a
|
|
90
|
+
i += 1
|
|
91
|
+
|
|
92
|
+
if not cwd:
|
|
93
|
+
cwd = os.getcwd()
|
|
94
|
+
if not os.path.isdir(cwd):
|
|
95
|
+
print(f"handoff run: cwd not found: {cwd}", file=sys.stderr)
|
|
96
|
+
sys.exit(2)
|
|
97
|
+
|
|
98
|
+
if text_mode:
|
|
99
|
+
if not text_parts:
|
|
100
|
+
print("handoff run: --text requires a value", file=sys.stderr)
|
|
101
|
+
sys.exit(2)
|
|
102
|
+
prompt_text = " ".join(text_parts)
|
|
103
|
+
if not prompt_text:
|
|
104
|
+
print("handoff run: --text requires a non-empty value", file=sys.stderr)
|
|
105
|
+
sys.exit(2)
|
|
106
|
+
elif input_src == "-" or (not input_src and not sys.stdin.isatty()):
|
|
107
|
+
prompt_text = sys.stdin.read()
|
|
108
|
+
elif input_src:
|
|
109
|
+
if not os.path.isfile(input_src):
|
|
110
|
+
print(f"handoff run: input file not found: {input_src}", file=sys.stderr)
|
|
111
|
+
sys.exit(2)
|
|
112
|
+
with open(input_src) as f:
|
|
113
|
+
prompt_text = f.read()
|
|
114
|
+
else:
|
|
115
|
+
print("handoff run: input file required, or use --text <prompt...> / pipe via '-'", file=sys.stderr)
|
|
116
|
+
sys.exit(2)
|
|
117
|
+
|
|
118
|
+
backend_name = backend_arg or config.default_backend
|
|
119
|
+
|
|
120
|
+
_execute(cwd, prompt_text, backend_name, pro, config)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _execute(
|
|
124
|
+
cwd: str,
|
|
125
|
+
prompt_text: str,
|
|
126
|
+
backend_name: str,
|
|
127
|
+
pro: bool,
|
|
128
|
+
config: Config,
|
|
129
|
+
resume_session_id: str | None = None,
|
|
130
|
+
):
|
|
131
|
+
"""Shared execution path for file, stdin, and --text run modes.
|
|
132
|
+
|
|
133
|
+
When `resume_session_id` is given, the new run is appended to that existing
|
|
134
|
+
claude conversation (`claude -p ... --resume <id>`) rather than starting a
|
|
135
|
+
fresh session; the new row still gets its own run_id/seq/files but shares the
|
|
136
|
+
session_id. Used by `handoff resume <seq> <prompt>`.
|
|
137
|
+
"""
|
|
138
|
+
backend_cfg = config.get_backend(backend_name)
|
|
139
|
+
if not backend_cfg:
|
|
140
|
+
print(
|
|
141
|
+
f"handoff: unknown backend '{backend_name}'. "
|
|
142
|
+
f"Available: {', '.join(sorted(config.backends.keys()))}",
|
|
143
|
+
file=sys.stderr,
|
|
144
|
+
)
|
|
145
|
+
sys.exit(2)
|
|
146
|
+
|
|
147
|
+
ensure_backend_token_ready(backend_name, backend_cfg, config.user_config_path)
|
|
148
|
+
|
|
149
|
+
conn = get_db()
|
|
150
|
+
run_id, uid, jsonl_path = create_run(
|
|
151
|
+
conn, cwd, prompt_text, backend_name, session_id=resume_session_id
|
|
152
|
+
)
|
|
153
|
+
conn.commit()
|
|
154
|
+
|
|
155
|
+
# tasks dir files
|
|
156
|
+
prompt_path, out_path, result_path = task_paths(run_id)
|
|
157
|
+
|
|
158
|
+
with open(prompt_path, "w") as pf:
|
|
159
|
+
pf.write(prompt_text)
|
|
160
|
+
|
|
161
|
+
# Resolve model
|
|
162
|
+
model = resolve_backend_model(backend_cfg, pro)
|
|
163
|
+
if not model:
|
|
164
|
+
print(
|
|
165
|
+
f"handoff: backend '{backend_name}' resolves no model. "
|
|
166
|
+
f"Set backends.{backend_name}.model in {config.user_config_path} "
|
|
167
|
+
f"(pre-0.3 configs carried this in the now-removed top-level default_model).",
|
|
168
|
+
file=sys.stderr,
|
|
169
|
+
)
|
|
170
|
+
sys.exit(2)
|
|
171
|
+
backend_cfg["_resolved_model"] = model
|
|
172
|
+
backend_cfg["_system_prompt"] = config.system_prompt
|
|
173
|
+
|
|
174
|
+
btype = backend_type(backend_cfg)
|
|
175
|
+
set_backend_env(backend_cfg, model, backend_cfg.get("pro_model", ""))
|
|
176
|
+
if resume_session_id:
|
|
177
|
+
session_id = resume_session_id
|
|
178
|
+
elif btype == "claude":
|
|
179
|
+
session_id = uid if UUID_RE.match(uid) else None
|
|
180
|
+
else:
|
|
181
|
+
# codex assigns the thread id itself; it arrives via the
|
|
182
|
+
# thread.started event and is persisted by execute_run
|
|
183
|
+
session_id = None
|
|
184
|
+
|
|
185
|
+
print(f"RESULT={result_path}")
|
|
186
|
+
print(f"RESULT={result_path}", file=sys.stderr)
|
|
187
|
+
|
|
188
|
+
ts = datetime.datetime.now().strftime("%H:%M:%S")
|
|
189
|
+
label = "resume" if resume_session_id else "start"
|
|
190
|
+
print(f"{ts} {label}\tSESSION={session_id or 'pending'}", file=sys.stderr)
|
|
191
|
+
|
|
192
|
+
# build backend command (wrapped in script for pty when the type needs it)
|
|
193
|
+
backend_cmd = build_args(
|
|
194
|
+
backend_cfg, prompt_text, session_id,
|
|
195
|
+
model=model,
|
|
196
|
+
pro_model=backend_cfg.get("pro_model", ""),
|
|
197
|
+
resume=bool(resume_session_id),
|
|
198
|
+
cwd=cwd,
|
|
199
|
+
)
|
|
200
|
+
cmd = wrap_with_pty(backend_cfg, backend_cmd)
|
|
201
|
+
|
|
202
|
+
execute_run(
|
|
203
|
+
cwd,
|
|
204
|
+
prompt_text,
|
|
205
|
+
cmd,
|
|
206
|
+
conn,
|
|
207
|
+
uid,
|
|
208
|
+
jsonl_path,
|
|
209
|
+
(prompt_path, out_path, result_path),
|
|
210
|
+
backend_type=btype,
|
|
211
|
+
)
|
cli/commands/tail.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""handoff tail command."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from ..core import get_db, find_run, short_path, prompt_prefix, task_paths
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cmd_tail(argv: list[str], config=None):
|
|
10
|
+
"""handoff tail [<run-id|seq>]"""
|
|
11
|
+
selector = ""
|
|
12
|
+
for a in argv:
|
|
13
|
+
if a in ("-h", "--help"):
|
|
14
|
+
from ..main import usage
|
|
15
|
+
usage()
|
|
16
|
+
sys.exit(0)
|
|
17
|
+
elif a.startswith("-"):
|
|
18
|
+
print(f"handoff tail: unknown option {a}", file=sys.stderr)
|
|
19
|
+
sys.exit(2)
|
|
20
|
+
else:
|
|
21
|
+
selector = a
|
|
22
|
+
|
|
23
|
+
conn = get_db()
|
|
24
|
+
row = find_run(conn, selector or None)
|
|
25
|
+
conn.close()
|
|
26
|
+
|
|
27
|
+
if not row:
|
|
28
|
+
print("handoff tail: no run found", file=sys.stderr)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
jsonl_path = row["jsonl_path"]
|
|
32
|
+
if not os.path.exists(jsonl_path):
|
|
33
|
+
print(f"handoff tail: jsonl not found: {jsonl_path}", file=sys.stderr)
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
run_id = row["run_id"]
|
|
37
|
+
prompt_path, out_path, result_path = task_paths(run_id)
|
|
38
|
+
|
|
39
|
+
run_info = {
|
|
40
|
+
"run_id": run_id,
|
|
41
|
+
"date": row["created_at"],
|
|
42
|
+
"cwd": short_path(row["cwd"]),
|
|
43
|
+
"uuid": row["uuid"],
|
|
44
|
+
"out_path": out_path,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
from ..jsonl_viewer import run_tail
|
|
48
|
+
run_tail(jsonl_path, prompt_path, result_path, run_info)
|