zwarm 1.3.10__py3-none-any.whl → 2.0.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.
- zwarm/adapters/codex_mcp.py +475 -181
- zwarm/cli/main.py +483 -143
- zwarm/core/config.py +2 -0
- zwarm/orchestrator.py +41 -2
- zwarm/prompts/orchestrator.py +29 -13
- zwarm/sessions/__init__.py +2 -0
- zwarm/sessions/manager.py +87 -8
- zwarm/tools/delegation.py +356 -324
- zwarm/watchers/builtin.py +100 -6
- zwarm-2.0.0.dist-info/METADATA +309 -0
- {zwarm-1.3.10.dist-info → zwarm-2.0.0.dist-info}/RECORD +13 -13
- zwarm-1.3.10.dist-info/METADATA +0 -525
- {zwarm-1.3.10.dist-info → zwarm-2.0.0.dist-info}/WHEEL +0 -0
- {zwarm-1.3.10.dist-info → zwarm-2.0.0.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,80 @@ 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
|
+
session = manager.get_session(session.id)
|
|
1429
|
+
if not session.is_running:
|
|
1430
|
+
break
|
|
1431
|
+
time.sleep(1.0)
|
|
1432
|
+
|
|
1433
|
+
# Get the response
|
|
1434
|
+
messages = manager.get_messages(session.id)
|
|
1435
|
+
for msg in messages:
|
|
1436
|
+
if msg.role == "assistant":
|
|
1437
|
+
response_preview = msg.content[:300]
|
|
1438
|
+
if len(msg.content) > 300:
|
|
1439
|
+
response_preview += "..."
|
|
1440
|
+
console.print(f"\n[bold]Response:[/]\n{response_preview}")
|
|
1441
|
+
break
|
|
1442
|
+
|
|
1443
|
+
console.print(f"\n[dim]Use 'show {session.short_id}' to see full details[/]")
|
|
1444
|
+
console.print(f"[dim]Use 'c {session.short_id} \"message\"' to continue[/]")
|
|
1285
1445
|
|
|
1286
1446
|
except Exception as e:
|
|
1287
1447
|
console.print(f" [red]Error:[/] {e}")
|
|
@@ -1351,34 +1511,81 @@ def interactive(
|
|
|
1351
1511
|
except Exception as e:
|
|
1352
1512
|
console.print(f" [red]Error starting orchestrator:[/] {e}")
|
|
1353
1513
|
|
|
1354
|
-
def
|
|
1355
|
-
"""
|
|
1514
|
+
def do_peek(session_id: str):
|
|
1515
|
+
"""Quick peek at session - just status + latest message (for fast polling)."""
|
|
1516
|
+
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1517
|
+
|
|
1518
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1356
1519
|
session = manager.get_session(session_id)
|
|
1520
|
+
|
|
1357
1521
|
if not session:
|
|
1358
|
-
console.print(f" [red]
|
|
1522
|
+
console.print(f" [red]Not found:[/] {session_id}")
|
|
1359
1523
|
return
|
|
1360
1524
|
|
|
1361
|
-
#
|
|
1525
|
+
# Status icon
|
|
1526
|
+
icon = {
|
|
1527
|
+
"running": "[yellow]●[/]",
|
|
1528
|
+
"completed": "[green]✓[/]",
|
|
1529
|
+
"failed": "[red]✗[/]",
|
|
1530
|
+
"killed": "[dim]○[/]",
|
|
1531
|
+
}.get(session.status.value, "?")
|
|
1532
|
+
|
|
1533
|
+
# Get latest assistant message
|
|
1362
1534
|
messages = manager.get_messages(session.id)
|
|
1535
|
+
latest = None
|
|
1536
|
+
for msg in reversed(messages):
|
|
1537
|
+
if msg.role == "assistant":
|
|
1538
|
+
latest = msg.content.replace("\n", " ")
|
|
1539
|
+
break
|
|
1540
|
+
|
|
1541
|
+
if session.status == SessStatus.RUNNING:
|
|
1542
|
+
console.print(f"{icon} [cyan]{session.short_id}[/] [yellow](running...)[/]")
|
|
1543
|
+
elif latest:
|
|
1544
|
+
# Truncate for one-liner
|
|
1545
|
+
preview = latest[:120] + "..." if len(latest) > 120 else latest
|
|
1546
|
+
console.print(f"{icon} [cyan]{session.short_id}[/] {preview}")
|
|
1547
|
+
elif session.status == SessStatus.FAILED:
|
|
1548
|
+
error = (session.error or "unknown")[:80]
|
|
1549
|
+
console.print(f"{icon} [cyan]{session.short_id}[/] [red]{error}[/]")
|
|
1550
|
+
else:
|
|
1551
|
+
console.print(f"{icon} [cyan]{session.short_id}[/] [dim](no response)[/]")
|
|
1552
|
+
|
|
1553
|
+
def do_show(session_id: str):
|
|
1554
|
+
"""Show full session details and messages using CodexSessionManager."""
|
|
1555
|
+
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1556
|
+
|
|
1557
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1558
|
+
session = manager.get_session(session_id)
|
|
1559
|
+
|
|
1560
|
+
if not session:
|
|
1561
|
+
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1562
|
+
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1563
|
+
return
|
|
1363
1564
|
|
|
1364
1565
|
# Status styling
|
|
1365
1566
|
status_display = {
|
|
1366
|
-
"running": "[yellow]
|
|
1567
|
+
"running": "[yellow]● running[/]",
|
|
1367
1568
|
"completed": "[green]✓ completed[/]",
|
|
1368
1569
|
"failed": "[red]✗ failed[/]",
|
|
1369
|
-
"killed": "[dim]
|
|
1370
|
-
"pending": "[dim]
|
|
1371
|
-
}.get(session.status.value, session.status.value)
|
|
1570
|
+
"killed": "[dim]○ killed[/]",
|
|
1571
|
+
"pending": "[dim]◌ pending[/]",
|
|
1572
|
+
}.get(session.status.value, str(session.status.value))
|
|
1372
1573
|
|
|
1373
1574
|
console.print()
|
|
1374
1575
|
console.print(f"[bold cyan]Session {session.short_id}[/] {status_display}")
|
|
1375
|
-
console.print(f"[dim]
|
|
1576
|
+
console.print(f"[dim]Adapter:[/] {session.adapter} [dim]│[/] [dim]Model:[/] {session.model}")
|
|
1376
1577
|
console.print(f"[dim]Task:[/] {session.task}")
|
|
1377
|
-
console.print(f"[dim]Dir:[/] {session.working_dir} [dim]│[/] [dim]
|
|
1578
|
+
console.print(f"[dim]Dir:[/] {session.working_dir} [dim]│[/] [dim]Turn:[/] {session.turn}")
|
|
1579
|
+
console.print(f"[dim]Source:[/] {session.source_display} [dim]│[/] [dim]Runtime:[/] {session.runtime}")
|
|
1580
|
+
if session.pid:
|
|
1581
|
+
console.print(f"[dim]PID:[/] {session.pid}")
|
|
1378
1582
|
console.print()
|
|
1379
1583
|
|
|
1584
|
+
# Get messages from manager
|
|
1585
|
+
messages = manager.get_messages(session.id)
|
|
1586
|
+
|
|
1380
1587
|
if not messages:
|
|
1381
|
-
if session.
|
|
1588
|
+
if session.is_running:
|
|
1382
1589
|
console.print("[yellow]Session is still running...[/]")
|
|
1383
1590
|
else:
|
|
1384
1591
|
console.print("[dim]No messages captured.[/]")
|
|
@@ -1394,10 +1601,7 @@ def interactive(
|
|
|
1394
1601
|
content = content[:2000] + "\n\n[dim]... (truncated)[/]"
|
|
1395
1602
|
console.print(Panel(content, title="[bold green]Assistant[/]", border_style="green"))
|
|
1396
1603
|
elif msg.role == "tool":
|
|
1397
|
-
|
|
1398
|
-
console.print(f" [dim]⚙[/] {msg.content}")
|
|
1399
|
-
else:
|
|
1400
|
-
console.print(f" [dim]└─ {msg.content[:100]}[/]")
|
|
1604
|
+
console.print(f" [dim]Tool: {msg.content[:100]}[/]")
|
|
1401
1605
|
|
|
1402
1606
|
console.print()
|
|
1403
1607
|
if session.token_usage:
|
|
@@ -1407,89 +1611,195 @@ def interactive(
|
|
|
1407
1611
|
if session.error:
|
|
1408
1612
|
console.print(f"[red]Error:[/] {session.error}")
|
|
1409
1613
|
|
|
1410
|
-
def do_continue(session_id: str, message: str):
|
|
1411
|
-
"""
|
|
1614
|
+
def do_continue(session_id: str, message: str, wait: bool = True):
|
|
1615
|
+
"""
|
|
1616
|
+
Continue a conversation using CodexSessionManager.inject_message().
|
|
1617
|
+
|
|
1618
|
+
Works for both sync and async sessions:
|
|
1619
|
+
- If session was sync (or wait=True): wait for response
|
|
1620
|
+
- If session was async (or wait=False): fire-and-forget, check later with '?'
|
|
1621
|
+
"""
|
|
1622
|
+
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1623
|
+
import time
|
|
1624
|
+
|
|
1625
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1412
1626
|
session = manager.get_session(session_id)
|
|
1627
|
+
|
|
1413
1628
|
if not session:
|
|
1414
1629
|
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1630
|
+
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1415
1631
|
return
|
|
1416
1632
|
|
|
1417
|
-
if session.status ==
|
|
1633
|
+
if session.status == SessStatus.RUNNING:
|
|
1418
1634
|
console.print("[yellow]Session is still running.[/]")
|
|
1419
|
-
console.print("[dim]Wait for it to complete,
|
|
1635
|
+
console.print("[dim]Wait for it to complete, or use '?' to check progress.[/]")
|
|
1420
1636
|
return
|
|
1421
1637
|
|
|
1422
|
-
|
|
1638
|
+
if session.status == SessStatus.KILLED:
|
|
1639
|
+
console.print("[yellow]Session was killed.[/]")
|
|
1640
|
+
console.print("[dim]Start a new session with 'spawn'.[/]")
|
|
1641
|
+
return
|
|
1423
1642
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1643
|
+
# Determine if we should wait based on session source
|
|
1644
|
+
# Sessions from 'user' with --async flag have source='user'
|
|
1645
|
+
# We'll use wait parameter to control this
|
|
1646
|
+
should_wait = wait
|
|
1647
|
+
|
|
1648
|
+
console.print(f"\n[dim]Sending message to {session.short_id}...[/]")
|
|
1649
|
+
|
|
1650
|
+
try:
|
|
1651
|
+
# Inject message - spawns new background process
|
|
1652
|
+
updated_session = manager.inject_message(session.id, message)
|
|
1653
|
+
|
|
1654
|
+
if not updated_session:
|
|
1655
|
+
console.print(f" [red]Failed to inject message[/]")
|
|
1656
|
+
return
|
|
1657
|
+
|
|
1658
|
+
console.print(f"[green]✓[/] Message sent (turn {updated_session.turn})")
|
|
1659
|
+
|
|
1660
|
+
if should_wait:
|
|
1661
|
+
# Sync mode: wait for completion
|
|
1662
|
+
console.print(f"[dim]Waiting for response...[/]")
|
|
1663
|
+
timeout = 300.0
|
|
1664
|
+
start = time.time()
|
|
1665
|
+
while time.time() - start < timeout:
|
|
1666
|
+
session = manager.get_session(session.id)
|
|
1667
|
+
if not session.is_running:
|
|
1668
|
+
break
|
|
1669
|
+
time.sleep(1.0)
|
|
1670
|
+
|
|
1671
|
+
# Get the response (last assistant message)
|
|
1672
|
+
messages = manager.get_messages(session.id)
|
|
1673
|
+
response = ""
|
|
1674
|
+
for msg in reversed(messages):
|
|
1675
|
+
if msg.role == "assistant":
|
|
1676
|
+
response = msg.content
|
|
1677
|
+
break
|
|
1678
|
+
|
|
1679
|
+
console.print(f"\n[bold]Response:[/]")
|
|
1680
|
+
if len(response) > 500:
|
|
1681
|
+
console.print(response[:500] + "...")
|
|
1682
|
+
console.print(f"\n[dim]Use 'show {session.short_id}' to see full response[/]")
|
|
1683
|
+
else:
|
|
1684
|
+
console.print(response or "(no response captured)")
|
|
1685
|
+
else:
|
|
1686
|
+
# Async mode: return immediately
|
|
1687
|
+
console.print(f"[dim]Running in background (PID: {updated_session.pid})[/]")
|
|
1688
|
+
console.print(f"[dim]Use '? {session.short_id}' to check progress[/]")
|
|
1431
1689
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1690
|
+
except Exception as e:
|
|
1691
|
+
console.print(f" [red]Error:[/] {e}")
|
|
1692
|
+
import traceback
|
|
1693
|
+
console.print(f" [dim]{traceback.format_exc()}[/]")
|
|
1694
|
+
|
|
1695
|
+
def do_check(session_id: str):
|
|
1696
|
+
"""Check status of a session using CodexSessionManager (same as orchestrator)."""
|
|
1697
|
+
from zwarm.sessions import CodexSessionManager
|
|
1698
|
+
|
|
1699
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1434
1700
|
session = manager.get_session(session_id)
|
|
1701
|
+
|
|
1435
1702
|
if not session:
|
|
1436
1703
|
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1704
|
+
console.print(f" [dim]Use 'ls' to see available sessions[/]")
|
|
1437
1705
|
return
|
|
1438
1706
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1707
|
+
status_icon = {
|
|
1708
|
+
"running": "●",
|
|
1709
|
+
"completed": "✓",
|
|
1710
|
+
"failed": "✗",
|
|
1711
|
+
"killed": "○",
|
|
1712
|
+
"pending": "◌",
|
|
1713
|
+
}.get(session.status.value, "?")
|
|
1714
|
+
|
|
1715
|
+
console.print(f"\n[bold]Session {session.short_id}[/]: {status_icon} {session.status.value}")
|
|
1716
|
+
console.print(f" [dim]Task: {session.task[:60]}{'...' if len(session.task) > 60 else ''}[/]")
|
|
1717
|
+
console.print(f" [dim]Turn: {session.turn} | Runtime: {session.runtime}[/]")
|
|
1718
|
+
|
|
1719
|
+
if session.status.value == "completed":
|
|
1720
|
+
messages = manager.get_messages(session.id)
|
|
1721
|
+
for msg in reversed(messages):
|
|
1722
|
+
if msg.role == "assistant":
|
|
1723
|
+
console.print(f"\n[bold]Response:[/]")
|
|
1724
|
+
if len(msg.content) > 500:
|
|
1725
|
+
console.print(msg.content[:500] + "...")
|
|
1726
|
+
else:
|
|
1727
|
+
console.print(msg.content)
|
|
1728
|
+
break
|
|
1729
|
+
elif session.status.value == "failed":
|
|
1730
|
+
console.print(f"[red]Error:[/] {session.error or 'Unknown error'}")
|
|
1731
|
+
elif session.status.value == "running":
|
|
1732
|
+
console.print("[dim]Session is still running...[/]")
|
|
1733
|
+
console.print(f" [dim]PID: {session.pid}[/]")
|
|
1463
1734
|
|
|
1464
1735
|
def do_kill(session_id: str):
|
|
1465
|
-
"""Kill a running session."""
|
|
1736
|
+
"""Kill a running session using CodexSessionManager (same as orchestrator)."""
|
|
1737
|
+
from zwarm.sessions import CodexSessionManager
|
|
1738
|
+
|
|
1739
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1466
1740
|
session = manager.get_session(session_id)
|
|
1741
|
+
|
|
1467
1742
|
if not session:
|
|
1468
1743
|
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1469
1744
|
return
|
|
1470
1745
|
|
|
1471
1746
|
if not session.is_running:
|
|
1472
|
-
console.print(f"[yellow]Session {session.short_id} is not running.[/]")
|
|
1747
|
+
console.print(f"[yellow]Session {session.short_id} is not running ({session.status.value}).[/]")
|
|
1473
1748
|
return
|
|
1474
1749
|
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1750
|
+
try:
|
|
1751
|
+
killed = manager.kill_session(session.id)
|
|
1752
|
+
if killed:
|
|
1753
|
+
console.print(f"[green]✓[/] Stopped session {session.short_id}")
|
|
1754
|
+
else:
|
|
1755
|
+
console.print(f"[red]Failed to stop session[/]")
|
|
1756
|
+
except Exception as e:
|
|
1757
|
+
console.print(f"[red]Failed to stop session:[/] {e}")
|
|
1479
1758
|
|
|
1480
1759
|
def do_killall():
|
|
1481
|
-
"""Kill all running sessions."""
|
|
1482
|
-
sessions
|
|
1760
|
+
"""Kill all running sessions using CodexSessionManager."""
|
|
1761
|
+
from zwarm.sessions import CodexSessionManager, SessionStatus as SessStatus
|
|
1762
|
+
|
|
1763
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1764
|
+
running_sessions = manager.list_sessions(status=SessStatus.RUNNING)
|
|
1765
|
+
|
|
1766
|
+
if not running_sessions:
|
|
1767
|
+
console.print("[dim]No running sessions to kill.[/]")
|
|
1768
|
+
return
|
|
1769
|
+
|
|
1483
1770
|
killed = 0
|
|
1484
|
-
for session in
|
|
1485
|
-
|
|
1486
|
-
|
|
1771
|
+
for session in running_sessions:
|
|
1772
|
+
try:
|
|
1773
|
+
if manager.kill_session(session.id):
|
|
1774
|
+
killed += 1
|
|
1775
|
+
except Exception as e:
|
|
1776
|
+
console.print(f"[red]Failed to stop {session.short_id}:[/] {e}")
|
|
1777
|
+
|
|
1487
1778
|
console.print(f"[green]✓[/] Killed {killed} sessions")
|
|
1488
1779
|
|
|
1489
1780
|
def do_clean():
|
|
1490
|
-
"""Remove completed sessions."""
|
|
1491
|
-
|
|
1492
|
-
|
|
1781
|
+
"""Remove old completed/failed sessions from disk."""
|
|
1782
|
+
from zwarm.sessions import CodexSessionManager
|
|
1783
|
+
|
|
1784
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1785
|
+
cleaned = manager.cleanup_completed(keep_days=7)
|
|
1786
|
+
console.print(f"[green]✓[/] Cleaned {cleaned} old sessions")
|
|
1787
|
+
|
|
1788
|
+
def do_delete(session_id: str):
|
|
1789
|
+
"""Delete a session entirely (removes from disk and ls)."""
|
|
1790
|
+
from zwarm.sessions import CodexSessionManager
|
|
1791
|
+
|
|
1792
|
+
manager = CodexSessionManager(default_dir / ".zwarm")
|
|
1793
|
+
session = manager.get_session(session_id)
|
|
1794
|
+
|
|
1795
|
+
if not session:
|
|
1796
|
+
console.print(f" [red]Session not found:[/] {session_id}")
|
|
1797
|
+
return
|
|
1798
|
+
|
|
1799
|
+
if manager.delete_session(session.id):
|
|
1800
|
+
console.print(f"[green]✓[/] Deleted session {session.short_id}")
|
|
1801
|
+
else:
|
|
1802
|
+
console.print(f"[red]Failed to delete session[/]")
|
|
1493
1803
|
|
|
1494
1804
|
# REPL loop
|
|
1495
1805
|
import shlex
|
|
@@ -1510,10 +1820,18 @@ def interactive(
|
|
|
1510
1820
|
args = parts[1:]
|
|
1511
1821
|
|
|
1512
1822
|
if cmd in ("q", "quit", "exit"):
|
|
1513
|
-
|
|
1514
|
-
if
|
|
1515
|
-
console.print(f" [yellow]Warning:[/] {len(
|
|
1823
|
+
active = [s for s in sessions.values() if s.status == SessionStatus.ACTIVE]
|
|
1824
|
+
if active:
|
|
1825
|
+
console.print(f" [yellow]Warning:[/] {len(active)} sessions still active")
|
|
1516
1826
|
console.print(" [dim]Use 'killall' first, or they'll continue in background[/]")
|
|
1827
|
+
|
|
1828
|
+
# Cleanup adapters
|
|
1829
|
+
for adapter_instance in adapters_cache.values():
|
|
1830
|
+
try:
|
|
1831
|
+
asyncio.run(adapter_instance.cleanup())
|
|
1832
|
+
except Exception:
|
|
1833
|
+
pass
|
|
1834
|
+
|
|
1517
1835
|
console.print("\n[dim]Goodbye![/]\n")
|
|
1518
1836
|
break
|
|
1519
1837
|
|
|
@@ -1521,34 +1839,50 @@ def interactive(
|
|
|
1521
1839
|
show_help()
|
|
1522
1840
|
|
|
1523
1841
|
elif cmd in ("ls", "list"):
|
|
1524
|
-
|
|
1525
|
-
show_sessions(show_all)
|
|
1842
|
+
show_sessions()
|
|
1526
1843
|
|
|
1527
1844
|
elif cmd == "spawn":
|
|
1528
1845
|
do_spawn(args)
|
|
1529
1846
|
|
|
1847
|
+
elif cmd == "async":
|
|
1848
|
+
# Async spawn shorthand
|
|
1849
|
+
do_spawn(args + ["--async"])
|
|
1850
|
+
|
|
1530
1851
|
elif cmd == "orchestrate":
|
|
1531
1852
|
do_orchestrate(args)
|
|
1532
1853
|
|
|
1533
|
-
elif cmd in ("?", "
|
|
1854
|
+
elif cmd in ("?", "peek"):
|
|
1534
1855
|
if not args:
|
|
1535
1856
|
console.print(" [red]Usage:[/] ? SESSION_ID")
|
|
1857
|
+
else:
|
|
1858
|
+
do_peek(args[0])
|
|
1859
|
+
|
|
1860
|
+
elif cmd in ("show", "check"):
|
|
1861
|
+
if not args:
|
|
1862
|
+
console.print(" [red]Usage:[/] show SESSION_ID")
|
|
1536
1863
|
else:
|
|
1537
1864
|
do_show(args[0])
|
|
1538
1865
|
|
|
1539
1866
|
elif cmd in ("c", "continue"):
|
|
1867
|
+
# Sync continue - waits for response
|
|
1540
1868
|
if len(args) < 2:
|
|
1541
1869
|
console.print(" [red]Usage:[/] c SESSION_ID \"message\"")
|
|
1542
1870
|
else:
|
|
1543
|
-
do_continue(args[0], " ".join(args[1:]))
|
|
1871
|
+
do_continue(args[0], " ".join(args[1:]), wait=True)
|
|
1872
|
+
|
|
1873
|
+
elif cmd in ("ca", "async"):
|
|
1874
|
+
# Async continue - fire and forget
|
|
1875
|
+
if len(args) < 2:
|
|
1876
|
+
console.print(" [red]Usage:[/] ca SESSION_ID \"message\"")
|
|
1877
|
+
console.print(" [dim]Sends message and returns immediately (check with '?')[/]")
|
|
1878
|
+
else:
|
|
1879
|
+
do_continue(args[0], " ".join(args[1:]), wait=False)
|
|
1544
1880
|
|
|
1545
|
-
elif cmd == "
|
|
1881
|
+
elif cmd == "check":
|
|
1546
1882
|
if not args:
|
|
1547
|
-
console.print(" [red]Usage:[/]
|
|
1883
|
+
console.print(" [red]Usage:[/] check SESSION_ID")
|
|
1548
1884
|
else:
|
|
1549
|
-
|
|
1550
|
-
session_id = [a for a in args if not a.startswith("-")][0]
|
|
1551
|
-
do_logs(session_id, follow)
|
|
1885
|
+
do_check(args[0])
|
|
1552
1886
|
|
|
1553
1887
|
elif cmd == "kill":
|
|
1554
1888
|
if not args:
|
|
@@ -1562,6 +1896,12 @@ def interactive(
|
|
|
1562
1896
|
elif cmd == "clean":
|
|
1563
1897
|
do_clean()
|
|
1564
1898
|
|
|
1899
|
+
elif cmd in ("rm", "delete", "remove"):
|
|
1900
|
+
if not args:
|
|
1901
|
+
console.print(" [red]Usage:[/] rm SESSION_ID")
|
|
1902
|
+
else:
|
|
1903
|
+
do_delete(args[0])
|
|
1904
|
+
|
|
1565
1905
|
else:
|
|
1566
1906
|
console.print(f" [yellow]Unknown command:[/] {cmd}")
|
|
1567
1907
|
console.print(" [dim]Type 'help' for available commands[/]")
|