zwarm 1.3.11__py3-none-any.whl → 2.0.1__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.
- zwarm/adapters/codex_mcp.py +475 -227
- zwarm/cli/main.py +485 -143
- zwarm/core/config.py +2 -0
- zwarm/orchestrator.py +83 -28
- zwarm/prompts/orchestrator.py +29 -13
- zwarm/sessions/__init__.py +2 -0
- zwarm/sessions/manager.py +87 -8
- zwarm/tools/delegation.py +358 -323
- zwarm-2.0.1.dist-info/METADATA +309 -0
- {zwarm-1.3.11.dist-info → zwarm-2.0.1.dist-info}/RECORD +12 -12
- zwarm-1.3.11.dist-info/METADATA +0 -525
- {zwarm-1.3.11.dist-info → zwarm-2.0.1.dist-info}/WHEEL +0 -0
- {zwarm-1.3.11.dist-info → zwarm-2.0.1.dist-info}/entry_points.txt +0 -0
zwarm/cli/main.py
CHANGED
|
@@ -304,8 +304,13 @@ def exec(
|
|
|
304
304
|
console.print(f" Mode: {mode.value}")
|
|
305
305
|
console.print(f" Task: {task}")
|
|
306
306
|
|
|
307
|
+
# Use isolated codex config if available
|
|
308
|
+
config_path = working_dir / ".zwarm" / "codex.toml"
|
|
309
|
+
if not config_path.exists():
|
|
310
|
+
config_path = None
|
|
311
|
+
|
|
307
312
|
try:
|
|
308
|
-
executor = get_adapter(adapter.value, model=model)
|
|
313
|
+
executor = get_adapter(adapter.value, model=model, config_path=config_path)
|
|
309
314
|
except ValueError as e:
|
|
310
315
|
console.print(f"[red]Error:[/] {e}")
|
|
311
316
|
sys.exit(1)
|
|
@@ -743,6 +748,13 @@ def init(
|
|
|
743
748
|
config_toml_path.write_text(toml_content)
|
|
744
749
|
console.print(f" [green]✓[/] Created .zwarm/config.toml")
|
|
745
750
|
|
|
751
|
+
# Create codex.toml for isolated codex configuration
|
|
752
|
+
codex_toml_path = state_dir / "codex.toml"
|
|
753
|
+
if not codex_toml_path.exists():
|
|
754
|
+
codex_content = _generate_codex_toml()
|
|
755
|
+
codex_toml_path.write_text(codex_content)
|
|
756
|
+
console.print(f" [green]✓[/] Created .zwarm/codex.toml (isolated codex config)")
|
|
757
|
+
|
|
746
758
|
# Create zwarm.yaml
|
|
747
759
|
if create_project_config:
|
|
748
760
|
if zwarm_yaml_path.exists() and not non_interactive:
|
|
@@ -813,6 +825,35 @@ def _generate_config_toml(
|
|
|
813
825
|
return "\n".join(lines)
|
|
814
826
|
|
|
815
827
|
|
|
828
|
+
def _generate_codex_toml(
|
|
829
|
+
model: str = "gpt-5.1-codex-mini",
|
|
830
|
+
reasoning_effort: str = "high",
|
|
831
|
+
) -> str:
|
|
832
|
+
"""
|
|
833
|
+
Generate codex.toml for isolated codex configuration.
|
|
834
|
+
|
|
835
|
+
This file is used by zwarm instead of ~/.codex/config.toml to ensure
|
|
836
|
+
consistent behavior across different environments.
|
|
837
|
+
"""
|
|
838
|
+
lines = [
|
|
839
|
+
"# Codex configuration for zwarm",
|
|
840
|
+
"# This file isolates zwarm's codex settings from your global ~/.codex/config.toml",
|
|
841
|
+
"# Generated by 'zwarm init'",
|
|
842
|
+
"",
|
|
843
|
+
"# Model settings",
|
|
844
|
+
f'model = "{model}"',
|
|
845
|
+
f'model_reasoning_effort = "{reasoning_effort}" # low | medium | high',
|
|
846
|
+
"",
|
|
847
|
+
"# Approval settings - zwarm manages these automatically",
|
|
848
|
+
"# disable_response_storage = false",
|
|
849
|
+
"",
|
|
850
|
+
"# You can override any codex setting here",
|
|
851
|
+
"# See: https://github.com/openai/codex#configuration",
|
|
852
|
+
"",
|
|
853
|
+
]
|
|
854
|
+
return "\n".join(lines)
|
|
855
|
+
|
|
856
|
+
|
|
816
857
|
def _generate_zwarm_yaml(
|
|
817
858
|
description: str = "",
|
|
818
859
|
context: str = "",
|
|
@@ -1092,6 +1133,7 @@ def clean(
|
|
|
1092
1133
|
def interactive(
|
|
1093
1134
|
default_dir: Annotated[Path, typer.Option("--dir", "-d", help="Default working directory")] = Path("."),
|
|
1094
1135
|
model: Annotated[Optional[str], typer.Option("--model", help="Default model override")] = None,
|
|
1136
|
+
adapter: Annotated[str, typer.Option("--adapter", "-a", help="Executor adapter")] = "codex_mcp",
|
|
1095
1137
|
state_dir: Annotated[Path, typer.Option("--state-dir", help="State directory for persistence")] = Path(".zwarm"),
|
|
1096
1138
|
):
|
|
1097
1139
|
"""
|
|
@@ -1100,79 +1142,121 @@ def interactive(
|
|
|
1100
1142
|
Spawn multiple agents across different directories, manage them interactively,
|
|
1101
1143
|
and view their outputs. You are the orchestrator.
|
|
1102
1144
|
|
|
1145
|
+
This uses the SAME code path as `zwarm orchestrate` - the adapter layer.
|
|
1146
|
+
Interactive is the human-in-the-loop version, orchestrate is the LLM version.
|
|
1147
|
+
|
|
1103
1148
|
[bold]Commands:[/]
|
|
1104
|
-
[cyan]spawn[/] "task" [opts] Start a coding agent session
|
|
1105
|
-
[cyan]
|
|
1149
|
+
[cyan]spawn[/] "task" [opts] Start a coding agent session (sync mode)
|
|
1150
|
+
[cyan]async[/] "task" [opts] Start async session (fire-and-forget)
|
|
1106
1151
|
[cyan]ls[/] / [cyan]list[/] Dashboard of all sessions
|
|
1107
|
-
[cyan]?[/]
|
|
1108
|
-
[cyan]
|
|
1109
|
-
[cyan]
|
|
1110
|
-
[cyan]kill[/] ID Stop a session
|
|
1152
|
+
[cyan]?[/] ID Quick peek (status + latest message)
|
|
1153
|
+
[cyan]show[/] ID Full session details & history
|
|
1154
|
+
[cyan]c[/] / [cyan]continue[/] ID "msg" Continue a sync conversation
|
|
1155
|
+
[cyan]kill[/] ID Stop a session (keeps in history)
|
|
1156
|
+
[cyan]rm[/] ID Delete session entirely
|
|
1111
1157
|
[cyan]killall[/] Stop all running sessions
|
|
1158
|
+
[cyan]clean[/] Remove old sessions (>7 days)
|
|
1112
1159
|
[cyan]q[/] / [cyan]quit[/] Exit
|
|
1113
1160
|
|
|
1114
1161
|
[bold]Spawn Options:[/]
|
|
1115
|
-
spawn "task" --dir ~/project --model gpt-5.1-codex-max
|
|
1162
|
+
spawn "task" --dir ~/project --model gpt-5.1-codex-max --async
|
|
1116
1163
|
|
|
1117
1164
|
[bold]Examples:[/]
|
|
1118
1165
|
$ zwarm interactive
|
|
1119
1166
|
> spawn "Build auth module" --dir ~/api
|
|
1120
1167
|
> spawn "Fix tests" --dir ~/api
|
|
1121
|
-
>
|
|
1168
|
+
> c abc123 "Now add error handling"
|
|
1122
1169
|
> ls
|
|
1123
1170
|
> ? abc123
|
|
1124
1171
|
"""
|
|
1125
|
-
from zwarm.
|
|
1172
|
+
from zwarm.adapters import get_adapter, list_adapters
|
|
1173
|
+
from zwarm.core.models import ConversationSession, SessionStatus, SessionMode
|
|
1126
1174
|
import argparse
|
|
1127
|
-
import subprocess as sp
|
|
1128
1175
|
|
|
1129
|
-
# Initialize
|
|
1130
|
-
manager = CodexSessionManager(state_dir)
|
|
1176
|
+
# Initialize adapter (same as orchestrator uses)
|
|
1131
1177
|
default_model = model or "gpt-5.1-codex-mini"
|
|
1178
|
+
default_adapter = adapter
|
|
1179
|
+
|
|
1180
|
+
# Config path for isolated codex configuration
|
|
1181
|
+
codex_config_path = state_dir / "codex.toml"
|
|
1182
|
+
if not codex_config_path.exists():
|
|
1183
|
+
# Try relative to working dir
|
|
1184
|
+
codex_config_path = default_dir / ".zwarm" / "codex.toml"
|
|
1185
|
+
if not codex_config_path.exists():
|
|
1186
|
+
codex_config_path = None # Fall back to overrides
|
|
1187
|
+
|
|
1188
|
+
# Session tracking - same pattern as orchestrator
|
|
1189
|
+
sessions: dict[str, ConversationSession] = {}
|
|
1190
|
+
adapters_cache: dict[str, Any] = {}
|
|
1191
|
+
|
|
1192
|
+
def get_or_create_adapter(adapter_name: str, session_model: str | None = None) -> Any:
|
|
1193
|
+
"""Get or create an adapter instance with isolated config."""
|
|
1194
|
+
cache_key = f"{adapter_name}:{session_model or default_model}"
|
|
1195
|
+
if cache_key not in adapters_cache:
|
|
1196
|
+
adapters_cache[cache_key] = get_adapter(
|
|
1197
|
+
adapter_name,
|
|
1198
|
+
model=session_model or default_model,
|
|
1199
|
+
config_path=codex_config_path,
|
|
1200
|
+
)
|
|
1201
|
+
return adapters_cache[cache_key]
|
|
1202
|
+
|
|
1203
|
+
def find_session(query: str) -> tuple[ConversationSession | None, str | None]:
|
|
1204
|
+
"""Find session by full or partial ID."""
|
|
1205
|
+
if query in sessions:
|
|
1206
|
+
return sessions[query], query
|
|
1207
|
+
for sid, session in sessions.items():
|
|
1208
|
+
if sid.startswith(query):
|
|
1209
|
+
return session, sid
|
|
1210
|
+
return None, None
|
|
1132
1211
|
|
|
1133
1212
|
console.print("\n[bold cyan]zwarm interactive[/] - Multi-Agent Command Center\n")
|
|
1134
1213
|
console.print(f" Working dir: {default_dir.absolute()}")
|
|
1135
1214
|
console.print(f" Model: [cyan]{default_model}[/]")
|
|
1136
|
-
console.print(f"
|
|
1215
|
+
console.print(f" Adapter: [cyan]{default_adapter}[/]")
|
|
1216
|
+
if codex_config_path:
|
|
1217
|
+
console.print(f" Config: [green]{codex_config_path}[/] (isolated)")
|
|
1218
|
+
else:
|
|
1219
|
+
console.print(f" Config: [yellow]using fallback overrides[/] (run 'zwarm init' for isolation)")
|
|
1220
|
+
console.print(f" Available: {', '.join(list_adapters())}")
|
|
1137
1221
|
console.print("\n Type [cyan]help[/] for commands, [cyan]quit[/] to exit.\n")
|
|
1138
1222
|
|
|
1139
1223
|
def show_help():
|
|
1140
1224
|
help_table = Table(show_header=False, box=None, padding=(0, 2))
|
|
1141
1225
|
help_table.add_column("Command", style="cyan", width=35)
|
|
1142
1226
|
help_table.add_column("Description")
|
|
1143
|
-
help_table.add_row('spawn "task" [options]', "Start
|
|
1227
|
+
help_table.add_row('spawn "task" [options]', "Start session (waits for completion)")
|
|
1144
1228
|
help_table.add_row(" --dir PATH", "Working directory")
|
|
1145
1229
|
help_table.add_row(" --model NAME", "Model override")
|
|
1230
|
+
help_table.add_row(" --async", "Background mode (don't wait)")
|
|
1146
1231
|
help_table.add_row("", "")
|
|
1147
|
-
help_table.add_row(
|
|
1148
|
-
help_table.add_row("
|
|
1149
|
-
help_table.add_row("", "")
|
|
1150
|
-
help_table.add_row(
|
|
1151
|
-
help_table.add_row("
|
|
1152
|
-
help_table.add_row("c / continue ID \"msg\"", "Continue a completed session")
|
|
1153
|
-
help_table.add_row("logs ID [-f]", "Show raw JSONL output")
|
|
1232
|
+
help_table.add_row("ls / list", "Dashboard of all sessions")
|
|
1233
|
+
help_table.add_row("? / show ID", "Show session details & messages")
|
|
1234
|
+
help_table.add_row('c ID "msg"', "Continue conversation (wait for response)")
|
|
1235
|
+
help_table.add_row('ca ID "msg"', "Continue async (fire-and-forget)")
|
|
1236
|
+
help_table.add_row("check ID", "Check session status")
|
|
1154
1237
|
help_table.add_row("kill ID", "Stop a running session")
|
|
1155
1238
|
help_table.add_row("killall", "Stop all running sessions")
|
|
1156
|
-
help_table.add_row("clean", "Remove completed sessions")
|
|
1239
|
+
help_table.add_row("clean", "Remove old completed sessions")
|
|
1157
1240
|
help_table.add_row("q / quit", "Exit")
|
|
1158
1241
|
console.print(help_table)
|
|
1159
1242
|
|
|
1160
|
-
def show_sessions(
|
|
1161
|
-
sessions
|
|
1243
|
+
def show_sessions():
|
|
1244
|
+
"""List all sessions from CodexSessionManager (same as orchestrator)."""
|
|
1245
|
+
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1246
|
+
from datetime import datetime
|
|
1162
1247
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
sessions = [s for s in sessions if s.status == SessionStatus.RUNNING or s.status == SessionStatus.COMPLETED][:20]
|
|
1248
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1249
|
+
all_sessions = manager.list_sessions()
|
|
1166
1250
|
|
|
1167
|
-
if not
|
|
1251
|
+
if not all_sessions:
|
|
1168
1252
|
console.print(" [dim]No sessions. Use 'spawn \"task\"' to start one.[/]")
|
|
1169
1253
|
return
|
|
1170
1254
|
|
|
1171
1255
|
# Status summary
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1256
|
+
running = sum(1 for s in all_sessions if s.status == SessStatus.RUNNING)
|
|
1257
|
+
completed = sum(1 for s in all_sessions if s.status == SessStatus.COMPLETED)
|
|
1258
|
+
failed = sum(1 for s in all_sessions if s.status == SessStatus.FAILED)
|
|
1259
|
+
killed = sum(1 for s in all_sessions if s.status == SessStatus.KILLED)
|
|
1176
1260
|
|
|
1177
1261
|
summary_parts = []
|
|
1178
1262
|
if running:
|
|
@@ -1181,48 +1265,91 @@ def interactive(
|
|
|
1181
1265
|
summary_parts.append(f"[green]{completed} done[/]")
|
|
1182
1266
|
if failed:
|
|
1183
1267
|
summary_parts.append(f"[red]{failed} failed[/]")
|
|
1268
|
+
if killed:
|
|
1269
|
+
summary_parts.append(f"[dim]{killed} killed[/]")
|
|
1184
1270
|
if summary_parts:
|
|
1185
1271
|
console.print(" | ".join(summary_parts))
|
|
1186
1272
|
console.print()
|
|
1187
1273
|
|
|
1274
|
+
def time_ago(iso_str: str) -> str:
|
|
1275
|
+
"""Convert ISO timestamp to human-readable 'X ago' format."""
|
|
1276
|
+
try:
|
|
1277
|
+
dt = datetime.fromisoformat(iso_str)
|
|
1278
|
+
delta = datetime.now() - dt
|
|
1279
|
+
secs = delta.total_seconds()
|
|
1280
|
+
if secs < 60:
|
|
1281
|
+
return f"{int(secs)}s"
|
|
1282
|
+
elif secs < 3600:
|
|
1283
|
+
return f"{int(secs/60)}m"
|
|
1284
|
+
elif secs < 86400:
|
|
1285
|
+
return f"{secs/3600:.1f}h"
|
|
1286
|
+
else:
|
|
1287
|
+
return f"{secs/86400:.1f}d"
|
|
1288
|
+
except:
|
|
1289
|
+
return "?"
|
|
1290
|
+
|
|
1188
1291
|
table = Table(box=None, show_header=True, header_style="bold dim")
|
|
1189
1292
|
table.add_column("ID", style="cyan", width=10)
|
|
1190
1293
|
table.add_column("", width=2) # Status icon
|
|
1191
|
-
table.add_column("
|
|
1192
|
-
table.add_column("Task", max_width=
|
|
1193
|
-
table.add_column("
|
|
1194
|
-
table.add_column("
|
|
1294
|
+
table.add_column("T", width=2) # Turn
|
|
1295
|
+
table.add_column("Task", max_width=30)
|
|
1296
|
+
table.add_column("Updated", justify="right", width=8)
|
|
1297
|
+
table.add_column("Last Message", max_width=40)
|
|
1195
1298
|
|
|
1196
1299
|
status_icons = {
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1300
|
+
"running": "[yellow]●[/]",
|
|
1301
|
+
"completed": "[green]✓[/]",
|
|
1302
|
+
"failed": "[red]✗[/]",
|
|
1303
|
+
"killed": "[dim]○[/]",
|
|
1304
|
+
"pending": "[dim]◌[/]",
|
|
1202
1305
|
}
|
|
1203
1306
|
|
|
1204
|
-
for s in
|
|
1205
|
-
icon = status_icons.get(s.status, "?")
|
|
1206
|
-
task_preview = s.task[:
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1307
|
+
for s in all_sessions:
|
|
1308
|
+
icon = status_icons.get(s.status.value, "?")
|
|
1309
|
+
task_preview = s.task[:27] + "..." if len(s.task) > 30 else s.task
|
|
1310
|
+
updated = time_ago(s.updated_at)
|
|
1311
|
+
|
|
1312
|
+
# Get last assistant message preview
|
|
1313
|
+
messages = manager.get_messages(s.id)
|
|
1314
|
+
last_msg = ""
|
|
1315
|
+
for msg in reversed(messages):
|
|
1316
|
+
if msg.role == "assistant":
|
|
1317
|
+
last_msg = msg.content.replace("\n", " ")[:37]
|
|
1318
|
+
if len(msg.content) > 37:
|
|
1319
|
+
last_msg += "..."
|
|
1320
|
+
break
|
|
1321
|
+
|
|
1322
|
+
# Style the last message based on recency
|
|
1323
|
+
if s.status == SessStatus.RUNNING:
|
|
1324
|
+
last_msg_styled = f"[yellow]{last_msg or '(working...)'}[/]"
|
|
1325
|
+
updated_styled = f"[yellow]{updated}[/]"
|
|
1326
|
+
elif s.status == SessStatus.COMPLETED:
|
|
1327
|
+
# Highlight if recently completed (< 60s)
|
|
1328
|
+
try:
|
|
1329
|
+
dt = datetime.fromisoformat(s.updated_at)
|
|
1330
|
+
is_recent = (datetime.now() - dt).total_seconds() < 60
|
|
1331
|
+
except:
|
|
1332
|
+
is_recent = False
|
|
1333
|
+
if is_recent:
|
|
1334
|
+
last_msg_styled = f"[green bold]{last_msg or '(done)'}[/]"
|
|
1335
|
+
updated_styled = f"[green bold]{updated} ★[/]"
|
|
1336
|
+
else:
|
|
1337
|
+
last_msg_styled = f"[green]{last_msg or '(done)'}[/]"
|
|
1338
|
+
updated_styled = f"[dim]{updated}[/]"
|
|
1339
|
+
elif s.status == SessStatus.FAILED:
|
|
1340
|
+
last_msg_styled = f"[red]{s.error[:37] if s.error else '(failed)'}...[/]"
|
|
1341
|
+
updated_styled = f"[red]{updated}[/]"
|
|
1216
1342
|
else:
|
|
1217
|
-
|
|
1343
|
+
last_msg_styled = f"[dim]{last_msg or '-'}[/]"
|
|
1344
|
+
updated_styled = f"[dim]{updated}[/]"
|
|
1218
1345
|
|
|
1219
1346
|
table.add_row(
|
|
1220
1347
|
s.short_id,
|
|
1221
1348
|
icon,
|
|
1222
|
-
|
|
1349
|
+
str(s.turn),
|
|
1223
1350
|
task_preview,
|
|
1224
|
-
|
|
1225
|
-
|
|
1351
|
+
updated_styled,
|
|
1352
|
+
last_msg_styled,
|
|
1226
1353
|
)
|
|
1227
1354
|
|
|
1228
1355
|
console.print(table)
|
|
@@ -1233,6 +1360,7 @@ def interactive(
|
|
|
1233
1360
|
parser.add_argument("task", nargs="*")
|
|
1234
1361
|
parser.add_argument("--dir", "-d", type=Path, default=None)
|
|
1235
1362
|
parser.add_argument("--model", "-m", default=None)
|
|
1363
|
+
parser.add_argument("--async", dest="async_mode", action="store_true", default=False)
|
|
1236
1364
|
|
|
1237
1365
|
try:
|
|
1238
1366
|
parsed, _ = parser.parse_known_args(args)
|
|
@@ -1240,48 +1368,81 @@ def interactive(
|
|
|
1240
1368
|
"task": " ".join(parsed.task) if parsed.task else "",
|
|
1241
1369
|
"dir": parsed.dir,
|
|
1242
1370
|
"model": parsed.model,
|
|
1371
|
+
"async_mode": parsed.async_mode,
|
|
1243
1372
|
}
|
|
1244
1373
|
except SystemExit:
|
|
1245
1374
|
return {"error": "Invalid spawn arguments"}
|
|
1246
1375
|
|
|
1247
1376
|
def do_spawn(args: list[str]):
|
|
1248
|
-
"""Spawn a new coding agent session."""
|
|
1377
|
+
"""Spawn a new coding agent session using CodexSessionManager (same as orchestrator)."""
|
|
1378
|
+
from zwarm.sessions import CodexSessionManager
|
|
1379
|
+
import time
|
|
1380
|
+
|
|
1249
1381
|
parsed = parse_spawn_args(args)
|
|
1250
1382
|
|
|
1251
1383
|
if "error" in parsed:
|
|
1252
1384
|
console.print(f" [red]{parsed['error']}[/]")
|
|
1253
|
-
console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME][/]")
|
|
1385
|
+
console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME] [--async][/]")
|
|
1254
1386
|
return
|
|
1255
1387
|
|
|
1256
1388
|
if not parsed["task"]:
|
|
1257
1389
|
console.print(" [red]Task required[/]")
|
|
1258
|
-
console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME][/]")
|
|
1390
|
+
console.print(" [dim]Usage: spawn \"task\" [--dir PATH] [--model NAME] [--async][/]")
|
|
1259
1391
|
return
|
|
1260
1392
|
|
|
1261
1393
|
task = parsed["task"]
|
|
1262
1394
|
work_dir = (parsed["dir"] or default_dir).absolute()
|
|
1263
1395
|
session_model = parsed["model"] or default_model
|
|
1396
|
+
is_async = parsed.get("async_mode", False)
|
|
1264
1397
|
|
|
1265
1398
|
if not work_dir.exists():
|
|
1266
1399
|
console.print(f" [red]Directory not found:[/] {work_dir}")
|
|
1267
1400
|
return
|
|
1268
1401
|
|
|
1269
|
-
|
|
1402
|
+
mode_str = "async" if is_async else "sync"
|
|
1403
|
+
console.print(f"\n[dim]Spawning {mode_str} session...[/]")
|
|
1270
1404
|
console.print(f" [dim]Dir: {work_dir}[/]")
|
|
1271
1405
|
console.print(f" [dim]Model: {session_model}[/]")
|
|
1272
1406
|
|
|
1273
1407
|
try:
|
|
1408
|
+
# Use CodexSessionManager - SAME as orchestrator's delegate()
|
|
1409
|
+
manager = CodexSessionManager(work_dir / ".zwarm")
|
|
1274
1410
|
session = manager.start_session(
|
|
1275
1411
|
task=task,
|
|
1276
1412
|
working_dir=work_dir,
|
|
1277
1413
|
model=session_model,
|
|
1414
|
+
sandbox="workspace-write",
|
|
1278
1415
|
source="user",
|
|
1279
1416
|
adapter="codex",
|
|
1280
1417
|
)
|
|
1281
1418
|
|
|
1282
1419
|
console.print(f"\n[green]✓[/] Session: [cyan]{session.short_id}[/]")
|
|
1283
|
-
console.print(f" PID: {session.pid}")
|
|
1284
|
-
|
|
1420
|
+
console.print(f" [dim]PID: {session.pid}[/]")
|
|
1421
|
+
|
|
1422
|
+
# For sync mode, wait for completion and show response
|
|
1423
|
+
if not is_async:
|
|
1424
|
+
console.print(f"\n[dim]Waiting for completion...[/]")
|
|
1425
|
+
timeout = 300.0
|
|
1426
|
+
start = time.time()
|
|
1427
|
+
while time.time() - start < timeout:
|
|
1428
|
+
# get_session() auto-updates status based on output completion
|
|
1429
|
+
session = manager.get_session(session.id)
|
|
1430
|
+
if session.status != SessStatus.RUNNING:
|
|
1431
|
+
break
|
|
1432
|
+
time.sleep(1.0)
|
|
1433
|
+
|
|
1434
|
+
# Get the response
|
|
1435
|
+
messages = manager.get_messages(session.id)
|
|
1436
|
+
for msg in messages:
|
|
1437
|
+
if msg.role == "assistant":
|
|
1438
|
+
response_preview = msg.content[:300]
|
|
1439
|
+
if len(msg.content) > 300:
|
|
1440
|
+
response_preview += "..."
|
|
1441
|
+
console.print(f"\n[bold]Response:[/]\n{response_preview}")
|
|
1442
|
+
break
|
|
1443
|
+
|
|
1444
|
+
console.print(f"\n[dim]Use 'show {session.short_id}' to see full details[/]")
|
|
1445
|
+
console.print(f"[dim]Use 'c {session.short_id} \"message\"' to continue[/]")
|
|
1285
1446
|
|
|
1286
1447
|
except Exception as e:
|
|
1287
1448
|
console.print(f" [red]Error:[/] {e}")
|
|
@@ -1351,34 +1512,81 @@ def interactive(
|
|
|
1351
1512
|
except Exception as e:
|
|
1352
1513
|
console.print(f" [red]Error starting orchestrator:[/] {e}")
|
|
1353
1514
|
|
|
1354
|
-
def
|
|
1355
|
-
"""
|
|
1515
|
+
def do_peek(session_id: str):
|
|
1516
|
+
"""Quick peek at session - just status + latest message (for fast polling)."""
|
|
1517
|
+
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1518
|
+
|
|
1519
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1356
1520
|
session = manager.get_session(session_id)
|
|
1521
|
+
|
|
1357
1522
|
if not session:
|
|
1358
|
-
console.print(f" [red]
|
|
1523
|
+
console.print(f" [red]Not found:[/] {session_id}")
|
|
1359
1524
|
return
|
|
1360
1525
|
|
|
1361
|
-
#
|
|
1526
|
+
# Status icon
|
|
1527
|
+
icon = {
|
|
1528
|
+
"running": "[yellow]●[/]",
|
|
1529
|
+
"completed": "[green]✓[/]",
|
|
1530
|
+
"failed": "[red]✗[/]",
|
|
1531
|
+
"killed": "[dim]○[/]",
|
|
1532
|
+
}.get(session.status.value, "?")
|
|
1533
|
+
|
|
1534
|
+
# Get latest assistant message
|
|
1362
1535
|
messages = manager.get_messages(session.id)
|
|
1536
|
+
latest = None
|
|
1537
|
+
for msg in reversed(messages):
|
|
1538
|
+
if msg.role == "assistant":
|
|
1539
|
+
latest = msg.content.replace("\n", " ")
|
|
1540
|
+
break
|
|
1541
|
+
|
|
1542
|
+
if session.status == SessStatus.RUNNING:
|
|
1543
|
+
console.print(f"{icon} [cyan]{session.short_id}[/] [yellow](running...)[/]")
|
|
1544
|
+
elif latest:
|
|
1545
|
+
# Truncate for one-liner
|
|
1546
|
+
preview = latest[:120] + "..." if len(latest) > 120 else latest
|
|
1547
|
+
console.print(f"{icon} [cyan]{session.short_id}[/] {preview}")
|
|
1548
|
+
elif session.status == SessStatus.FAILED:
|
|
1549
|
+
error = (session.error or "unknown")[:80]
|
|
1550
|
+
console.print(f"{icon} [cyan]{session.short_id}[/] [red]{error}[/]")
|
|
1551
|
+
else:
|
|
1552
|
+
console.print(f"{icon} [cyan]{session.short_id}[/] [dim](no response)[/]")
|
|
1553
|
+
|
|
1554
|
+
def do_show(session_id: str):
|
|
1555
|
+
"""Show full session details and messages using CodexSessionManager."""
|
|
1556
|
+
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1557
|
+
|
|
1558
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1559
|
+
session = manager.get_session(session_id)
|
|
1560
|
+
|
|
1561
|
+
if not session:
|
|
1562
|
+
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1563
|
+
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1564
|
+
return
|
|
1363
1565
|
|
|
1364
1566
|
# Status styling
|
|
1365
1567
|
status_display = {
|
|
1366
|
-
"running": "[yellow]
|
|
1568
|
+
"running": "[yellow]● running[/]",
|
|
1367
1569
|
"completed": "[green]✓ completed[/]",
|
|
1368
1570
|
"failed": "[red]✗ failed[/]",
|
|
1369
|
-
"killed": "[dim]
|
|
1370
|
-
"pending": "[dim]
|
|
1371
|
-
}.get(session.status.value, session.status.value)
|
|
1571
|
+
"killed": "[dim]○ killed[/]",
|
|
1572
|
+
"pending": "[dim]◌ pending[/]",
|
|
1573
|
+
}.get(session.status.value, str(session.status.value))
|
|
1372
1574
|
|
|
1373
1575
|
console.print()
|
|
1374
1576
|
console.print(f"[bold cyan]Session {session.short_id}[/] {status_display}")
|
|
1375
|
-
console.print(f"[dim]
|
|
1577
|
+
console.print(f"[dim]Adapter:[/] {session.adapter} [dim]│[/] [dim]Model:[/] {session.model}")
|
|
1376
1578
|
console.print(f"[dim]Task:[/] {session.task}")
|
|
1377
|
-
console.print(f"[dim]Dir:[/] {session.working_dir} [dim]│[/] [dim]
|
|
1579
|
+
console.print(f"[dim]Dir:[/] {session.working_dir} [dim]│[/] [dim]Turn:[/] {session.turn}")
|
|
1580
|
+
console.print(f"[dim]Source:[/] {session.source_display} [dim]│[/] [dim]Runtime:[/] {session.runtime}")
|
|
1581
|
+
if session.pid:
|
|
1582
|
+
console.print(f"[dim]PID:[/] {session.pid}")
|
|
1378
1583
|
console.print()
|
|
1379
1584
|
|
|
1585
|
+
# Get messages from manager
|
|
1586
|
+
messages = manager.get_messages(session.id)
|
|
1587
|
+
|
|
1380
1588
|
if not messages:
|
|
1381
|
-
if session.
|
|
1589
|
+
if session.is_running:
|
|
1382
1590
|
console.print("[yellow]Session is still running...[/]")
|
|
1383
1591
|
else:
|
|
1384
1592
|
console.print("[dim]No messages captured.[/]")
|
|
@@ -1394,10 +1602,7 @@ def interactive(
|
|
|
1394
1602
|
content = content[:2000] + "\n\n[dim]... (truncated)[/]"
|
|
1395
1603
|
console.print(Panel(content, title="[bold green]Assistant[/]", border_style="green"))
|
|
1396
1604
|
elif msg.role == "tool":
|
|
1397
|
-
|
|
1398
|
-
console.print(f" [dim]⚙[/] {msg.content}")
|
|
1399
|
-
else:
|
|
1400
|
-
console.print(f" [dim]└─ {msg.content[:100]}[/]")
|
|
1605
|
+
console.print(f" [dim]Tool: {msg.content[:100]}[/]")
|
|
1401
1606
|
|
|
1402
1607
|
console.print()
|
|
1403
1608
|
if session.token_usage:
|
|
@@ -1407,89 +1612,196 @@ def interactive(
|
|
|
1407
1612
|
if session.error:
|
|
1408
1613
|
console.print(f"[red]Error:[/] {session.error}")
|
|
1409
1614
|
|
|
1410
|
-
def do_continue(session_id: str, message: str):
|
|
1411
|
-
"""
|
|
1615
|
+
def do_continue(session_id: str, message: str, wait: bool = True):
|
|
1616
|
+
"""
|
|
1617
|
+
Continue a conversation using CodexSessionManager.inject_message().
|
|
1618
|
+
|
|
1619
|
+
Works for both sync and async sessions:
|
|
1620
|
+
- If session was sync (or wait=True): wait for response
|
|
1621
|
+
- If session was async (or wait=False): fire-and-forget, check later with '?'
|
|
1622
|
+
"""
|
|
1623
|
+
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1624
|
+
import time
|
|
1625
|
+
|
|
1626
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1412
1627
|
session = manager.get_session(session_id)
|
|
1628
|
+
|
|
1413
1629
|
if not session:
|
|
1414
1630
|
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1631
|
+
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1415
1632
|
return
|
|
1416
1633
|
|
|
1417
|
-
if session.status ==
|
|
1634
|
+
if session.status == SessStatus.RUNNING:
|
|
1418
1635
|
console.print("[yellow]Session is still running.[/]")
|
|
1419
|
-
console.print("[dim]Wait for it to complete,
|
|
1636
|
+
console.print("[dim]Wait for it to complete, or use '?' to check progress.[/]")
|
|
1420
1637
|
return
|
|
1421
1638
|
|
|
1422
|
-
|
|
1639
|
+
if session.status == SessStatus.KILLED:
|
|
1640
|
+
console.print("[yellow]Session was killed.[/]")
|
|
1641
|
+
console.print("[dim]Start a new session with 'spawn'.[/]")
|
|
1642
|
+
return
|
|
1423
1643
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1644
|
+
# Determine if we should wait based on session source
|
|
1645
|
+
# Sessions from 'user' with --async flag have source='user'
|
|
1646
|
+
# We'll use wait parameter to control this
|
|
1647
|
+
should_wait = wait
|
|
1648
|
+
|
|
1649
|
+
console.print(f"\n[dim]Sending message to {session.short_id}...[/]")
|
|
1650
|
+
|
|
1651
|
+
try:
|
|
1652
|
+
# Inject message - spawns new background process
|
|
1653
|
+
updated_session = manager.inject_message(session.id, message)
|
|
1654
|
+
|
|
1655
|
+
if not updated_session:
|
|
1656
|
+
console.print(f" [red]Failed to inject message[/]")
|
|
1657
|
+
return
|
|
1658
|
+
|
|
1659
|
+
console.print(f"[green]✓[/] Message sent (turn {updated_session.turn})")
|
|
1660
|
+
|
|
1661
|
+
if should_wait:
|
|
1662
|
+
# Sync mode: wait for completion
|
|
1663
|
+
console.print(f"[dim]Waiting for response...[/]")
|
|
1664
|
+
timeout = 300.0
|
|
1665
|
+
start = time.time()
|
|
1666
|
+
while time.time() - start < timeout:
|
|
1667
|
+
# get_session() auto-updates status based on output completion
|
|
1668
|
+
session = manager.get_session(session.id)
|
|
1669
|
+
if session.status != SessStatus.RUNNING:
|
|
1670
|
+
break
|
|
1671
|
+
time.sleep(1.0)
|
|
1672
|
+
|
|
1673
|
+
# Get the response (last assistant message)
|
|
1674
|
+
messages = manager.get_messages(session.id)
|
|
1675
|
+
response = ""
|
|
1676
|
+
for msg in reversed(messages):
|
|
1677
|
+
if msg.role == "assistant":
|
|
1678
|
+
response = msg.content
|
|
1679
|
+
break
|
|
1680
|
+
|
|
1681
|
+
console.print(f"\n[bold]Response:[/]")
|
|
1682
|
+
if len(response) > 500:
|
|
1683
|
+
console.print(response[:500] + "...")
|
|
1684
|
+
console.print(f"\n[dim]Use 'show {session.short_id}' to see full response[/]")
|
|
1685
|
+
else:
|
|
1686
|
+
console.print(response or "(no response captured)")
|
|
1687
|
+
else:
|
|
1688
|
+
# Async mode: return immediately
|
|
1689
|
+
console.print(f"[dim]Running in background (PID: {updated_session.pid})[/]")
|
|
1690
|
+
console.print(f"[dim]Use '? {session.short_id}' to check progress[/]")
|
|
1431
1691
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1692
|
+
except Exception as e:
|
|
1693
|
+
console.print(f" [red]Error:[/] {e}")
|
|
1694
|
+
import traceback
|
|
1695
|
+
console.print(f" [dim]{traceback.format_exc()}[/]")
|
|
1696
|
+
|
|
1697
|
+
def do_check(session_id: str):
|
|
1698
|
+
"""Check status of a session using CodexSessionManager (same as orchestrator)."""
|
|
1699
|
+
from zwarm.sessions import CodexSessionManager
|
|
1700
|
+
|
|
1701
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1434
1702
|
session = manager.get_session(session_id)
|
|
1703
|
+
|
|
1435
1704
|
if not session:
|
|
1436
1705
|
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1706
|
+
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1437
1707
|
return
|
|
1438
1708
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1709
|
+
status_icon = {
|
|
1710
|
+
"running": "●",
|
|
1711
|
+
"completed": "✓",
|
|
1712
|
+
"failed": "✗",
|
|
1713
|
+
"killed": "○",
|
|
1714
|
+
"pending": "◌",
|
|
1715
|
+
}.get(session.status.value, "?")
|
|
1716
|
+
|
|
1717
|
+
console.print(f"\n[bold]Session {session.short_id}[/]: {status_icon} {session.status.value}")
|
|
1718
|
+
console.print(f" [dim]Task: {session.task[:60]}{'...' if len(session.task) > 60 else ''}[/]")
|
|
1719
|
+
console.print(f" [dim]Turn: {session.turn} | Runtime: {session.runtime}[/]")
|
|
1720
|
+
|
|
1721
|
+
if session.status.value == "completed":
|
|
1722
|
+
messages = manager.get_messages(session.id)
|
|
1723
|
+
for msg in reversed(messages):
|
|
1724
|
+
if msg.role == "assistant":
|
|
1725
|
+
console.print(f"\n[bold]Response:[/]")
|
|
1726
|
+
if len(msg.content) > 500:
|
|
1727
|
+
console.print(msg.content[:500] + "...")
|
|
1728
|
+
else:
|
|
1729
|
+
console.print(msg.content)
|
|
1730
|
+
break
|
|
1731
|
+
elif session.status.value == "failed":
|
|
1732
|
+
console.print(f"[red]Error:[/] {session.error or 'Unknown error'}")
|
|
1733
|
+
elif session.status.value == "running":
|
|
1734
|
+
console.print("[dim]Session is still running...[/]")
|
|
1735
|
+
console.print(f" [dim]PID: {session.pid}[/]")
|
|
1463
1736
|
|
|
1464
1737
|
def do_kill(session_id: str):
|
|
1465
|
-
"""Kill a running session."""
|
|
1738
|
+
"""Kill a running session using CodexSessionManager (same as orchestrator)."""
|
|
1739
|
+
from zwarm.sessions import CodexSessionManager
|
|
1740
|
+
|
|
1741
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1466
1742
|
session = manager.get_session(session_id)
|
|
1743
|
+
|
|
1467
1744
|
if not session:
|
|
1468
1745
|
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1469
1746
|
return
|
|
1470
1747
|
|
|
1471
1748
|
if not session.is_running:
|
|
1472
|
-
console.print(f"[yellow]Session {session.short_id} is not running.[/]")
|
|
1749
|
+
console.print(f"[yellow]Session {session.short_id} is not running ({session.status.value}).[/]")
|
|
1473
1750
|
return
|
|
1474
1751
|
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1752
|
+
try:
|
|
1753
|
+
killed = manager.kill_session(session.id)
|
|
1754
|
+
if killed:
|
|
1755
|
+
console.print(f"[green]✓[/] Stopped session {session.short_id}")
|
|
1756
|
+
else:
|
|
1757
|
+
console.print(f"[red]Failed to stop session[/]")
|
|
1758
|
+
except Exception as e:
|
|
1759
|
+
console.print(f"[red]Failed to stop session:[/] {e}")
|
|
1479
1760
|
|
|
1480
1761
|
def do_killall():
|
|
1481
|
-
"""Kill all running sessions."""
|
|
1482
|
-
sessions
|
|
1762
|
+
"""Kill all running sessions using CodexSessionManager."""
|
|
1763
|
+
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1764
|
+
|
|
1765
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1766
|
+
running_sessions = manager.list_sessions(status=SessStatus.RUNNING)
|
|
1767
|
+
|
|
1768
|
+
if not running_sessions:
|
|
1769
|
+
console.print("[dim]No running sessions to kill.[/]")
|
|
1770
|
+
return
|
|
1771
|
+
|
|
1483
1772
|
killed = 0
|
|
1484
|
-
for session in
|
|
1485
|
-
|
|
1486
|
-
|
|
1773
|
+
for session in running_sessions:
|
|
1774
|
+
try:
|
|
1775
|
+
if manager.kill_session(session.id):
|
|
1776
|
+
killed += 1
|
|
1777
|
+
except Exception as e:
|
|
1778
|
+
console.print(f"[red]Failed to stop {session.short_id}:[/] {e}")
|
|
1779
|
+
|
|
1487
1780
|
console.print(f"[green]✓[/] Killed {killed} sessions")
|
|
1488
1781
|
|
|
1489
1782
|
def do_clean():
|
|
1490
|
-
"""Remove completed sessions."""
|
|
1491
|
-
|
|
1492
|
-
|
|
1783
|
+
"""Remove old completed/failed sessions from disk."""
|
|
1784
|
+
from zwarm.sessions import CodexSessionManager
|
|
1785
|
+
|
|
1786
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1787
|
+
cleaned = manager.cleanup_completed(keep_days=7)
|
|
1788
|
+
console.print(f"[green]✓[/] Cleaned {cleaned} old sessions")
|
|
1789
|
+
|
|
1790
|
+
def do_delete(session_id: str):
|
|
1791
|
+
"""Delete a session entirely (removes from disk and ls)."""
|
|
1792
|
+
from zwarm.sessions import CodexSessionManager
|
|
1793
|
+
|
|
1794
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1795
|
+
session = manager.get_session(session_id)
|
|
1796
|
+
|
|
1797
|
+
if not session:
|
|
1798
|
+
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1799
|
+
return
|
|
1800
|
+
|
|
1801
|
+
if manager.delete_session(session.id):
|
|
1802
|
+
console.print(f"[green]✓[/] Deleted session {session.short_id}")
|
|
1803
|
+
else:
|
|
1804
|
+
console.print(f"[red]Failed to delete session[/]")
|
|
1493
1805
|
|
|
1494
1806
|
# REPL loop
|
|
1495
1807
|
import shlex
|
|
@@ -1510,10 +1822,18 @@ def interactive(
|
|
|
1510
1822
|
args = parts[1:]
|
|
1511
1823
|
|
|
1512
1824
|
if cmd in ("q", "quit", "exit"):
|
|
1513
|
-
|
|
1514
|
-
if
|
|
1515
|
-
console.print(f" [yellow]Warning:[/] {len(
|
|
1825
|
+
active = [s for s in sessions.values() if s.status == SessionStatus.ACTIVE]
|
|
1826
|
+
if active:
|
|
1827
|
+
console.print(f" [yellow]Warning:[/] {len(active)} sessions still active")
|
|
1516
1828
|
console.print(" [dim]Use 'killall' first, or they'll continue in background[/]")
|
|
1829
|
+
|
|
1830
|
+
# Cleanup adapters
|
|
1831
|
+
for adapter_instance in adapters_cache.values():
|
|
1832
|
+
try:
|
|
1833
|
+
asyncio.run(adapter_instance.cleanup())
|
|
1834
|
+
except Exception:
|
|
1835
|
+
pass
|
|
1836
|
+
|
|
1517
1837
|
console.print("\n[dim]Goodbye![/]\n")
|
|
1518
1838
|
break
|
|
1519
1839
|
|
|
@@ -1521,34 +1841,50 @@ def interactive(
|
|
|
1521
1841
|
show_help()
|
|
1522
1842
|
|
|
1523
1843
|
elif cmd in ("ls", "list"):
|
|
1524
|
-
|
|
1525
|
-
show_sessions(show_all)
|
|
1844
|
+
show_sessions()
|
|
1526
1845
|
|
|
1527
1846
|
elif cmd == "spawn":
|
|
1528
1847
|
do_spawn(args)
|
|
1529
1848
|
|
|
1849
|
+
elif cmd == "async":
|
|
1850
|
+
# Async spawn shorthand
|
|
1851
|
+
do_spawn(args + ["--async"])
|
|
1852
|
+
|
|
1530
1853
|
elif cmd == "orchestrate":
|
|
1531
1854
|
do_orchestrate(args)
|
|
1532
1855
|
|
|
1533
|
-
elif cmd in ("?", "
|
|
1856
|
+
elif cmd in ("?", "peek"):
|
|
1534
1857
|
if not args:
|
|
1535
1858
|
console.print(" [red]Usage:[/] ? SESSION_ID")
|
|
1859
|
+
else:
|
|
1860
|
+
do_peek(args[0])
|
|
1861
|
+
|
|
1862
|
+
elif cmd in ("show", "check"):
|
|
1863
|
+
if not args:
|
|
1864
|
+
console.print(" [red]Usage:[/] show SESSION_ID")
|
|
1536
1865
|
else:
|
|
1537
1866
|
do_show(args[0])
|
|
1538
1867
|
|
|
1539
1868
|
elif cmd in ("c", "continue"):
|
|
1869
|
+
# Sync continue - waits for response
|
|
1540
1870
|
if len(args) < 2:
|
|
1541
1871
|
console.print(" [red]Usage:[/] c SESSION_ID \"message\"")
|
|
1542
1872
|
else:
|
|
1543
|
-
do_continue(args[0], " ".join(args[1:]))
|
|
1873
|
+
do_continue(args[0], " ".join(args[1:]), wait=True)
|
|
1874
|
+
|
|
1875
|
+
elif cmd in ("ca", "async"):
|
|
1876
|
+
# Async continue - fire and forget
|
|
1877
|
+
if len(args) < 2:
|
|
1878
|
+
console.print(" [red]Usage:[/] ca SESSION_ID \"message\"")
|
|
1879
|
+
console.print(" [dim]Sends message and returns immediately (check with '?')[/]")
|
|
1880
|
+
else:
|
|
1881
|
+
do_continue(args[0], " ".join(args[1:]), wait=False)
|
|
1544
1882
|
|
|
1545
|
-
elif cmd == "
|
|
1883
|
+
elif cmd == "check":
|
|
1546
1884
|
if not args:
|
|
1547
|
-
console.print(" [red]Usage:[/]
|
|
1885
|
+
console.print(" [red]Usage:[/] check SESSION_ID")
|
|
1548
1886
|
else:
|
|
1549
|
-
|
|
1550
|
-
session_id = [a for a in args if not a.startswith("-")][0]
|
|
1551
|
-
do_logs(session_id, follow)
|
|
1887
|
+
do_check(args[0])
|
|
1552
1888
|
|
|
1553
1889
|
elif cmd == "kill":
|
|
1554
1890
|
if not args:
|
|
@@ -1562,6 +1898,12 @@ def interactive(
|
|
|
1562
1898
|
elif cmd == "clean":
|
|
1563
1899
|
do_clean()
|
|
1564
1900
|
|
|
1901
|
+
elif cmd in ("rm", "delete", "remove"):
|
|
1902
|
+
if not args:
|
|
1903
|
+
console.print(" [red]Usage:[/] rm SESSION_ID")
|
|
1904
|
+
else:
|
|
1905
|
+
do_delete(args[0])
|
|
1906
|
+
|
|
1565
1907
|
else:
|
|
1566
1908
|
console.print(f" [yellow]Unknown command:[/] {cmd}")
|
|
1567
1909
|
console.print(" [dim]Type 'help' for available commands[/]")
|