swarph-cli 0.7.9__tar.gz → 0.8.0__tar.gz
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.
- {swarph_cli-0.7.9/src/swarph_cli.egg-info → swarph_cli-0.8.0}/PKG-INFO +2 -2
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/pyproject.toml +2 -2
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/spawn.py +359 -136
- {swarph_cli-0.7.9 → swarph_cli-0.8.0/src/swarph_cli.egg-info}/PKG-INFO +2 -2
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli.egg-info/SOURCES.txt +1 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_cell_loader.py +10 -2
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_spawn_command.py +408 -6
- swarph_cli-0.8.0/tests/test_spawn_windows_relaunch.py +129 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/LICENSE +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/README.md +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/setup.cfg +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_import_command.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_main.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_watchdog.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing, cell.yaml spawn, session import, watchdog. v0.8.0: multi-provider spawn membrane — `swarph spawn` launches claude, codex (GPT), or antigravity/agy (Gemini) per cell.provider, each with billing-scrub + sandbox; plus the Windows conhost auto-relaunch into Windows Terminal (the Enter-inserts-'m' TUI fix).
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/darw007d/swarph-cli
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "swarph-cli"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing
|
|
7
|
+
version = "0.8.0"
|
|
8
|
+
description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing, cell.yaml spawn, session import, watchdog. v0.8.0: multi-provider spawn membrane — `swarph spawn` launches claude, codex (GPT), or antigravity/agy (Gemini) per cell.provider, each with billing-scrub + sandbox; plus the Windows conhost auto-relaunch into Windows Terminal (the Enter-inserts-'m' TUI fix)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|
|
26
26
|
import argparse
|
|
27
27
|
import os
|
|
28
28
|
import shutil
|
|
29
|
+
import subprocess
|
|
29
30
|
import sys
|
|
30
31
|
from pathlib import Path
|
|
31
32
|
from typing import Optional
|
|
@@ -58,7 +59,7 @@ _USAGE = """\
|
|
|
58
59
|
Usage:
|
|
59
60
|
swarph spawn [<role-or-path>] [--onboarding PATH-OR-URL]
|
|
60
61
|
[--dry-run] [--no-starter] [--print-id]
|
|
61
|
-
[--
|
|
62
|
+
[-- provider-extra-args...]
|
|
62
63
|
|
|
63
64
|
Resolution (first match wins):
|
|
64
65
|
--onboarding <path-or-url> explicit override
|
|
@@ -68,16 +69,31 @@ Resolution (first match wins):
|
|
|
68
69
|
mesh-gateway://... v0.7+ — returns NotImplementedError now
|
|
69
70
|
|
|
70
71
|
Flags:
|
|
71
|
-
--dry-run Print the resolved
|
|
72
|
+
--dry-run Print the resolved provider command + cell summary; no exec
|
|
72
73
|
--no-starter Skip starter-prompt injection even if cell.yaml sets one
|
|
73
74
|
--print-id Print resolved session-id to stdout before exec (useful
|
|
74
75
|
for shell scripts capturing the UUID for later resume)
|
|
75
76
|
|
|
76
|
-
Anything after a literal `--` is passed through to
|
|
77
|
-
(e.g. `swarph spawn lab -- --resume` to force the resume picker).
|
|
77
|
+
Anything after a literal `--` is passed through to the provider CLI unchanged
|
|
78
|
+
(e.g. `swarph spawn lab -- --resume` to force the Claude resume picker).
|
|
78
79
|
"""
|
|
79
80
|
|
|
80
81
|
|
|
82
|
+
_CODEX_BILLING_LEAK_KEYS = (
|
|
83
|
+
"OPENAI_API_KEY",
|
|
84
|
+
"OPENAI_API_BASE",
|
|
85
|
+
"OPENAI_BASE_URL",
|
|
86
|
+
"CODEX_API_KEY",
|
|
87
|
+
"OPENAI_ORG_ID",
|
|
88
|
+
"OPENAI_ORGANIZATION",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
_CODEX_SANDBOX_VALUES = frozenset({"workspace-write", "read-only"})
|
|
92
|
+
_CODEX_DEFAULT_SANDBOX = "workspace-write"
|
|
93
|
+
_CODEX_APPROVAL = "on-request"
|
|
94
|
+
_CODEX_PRINT_ID_NOTE = "codex: fresh-session-per-spawn, no pinned id"
|
|
95
|
+
|
|
96
|
+
|
|
81
97
|
def _build_parser() -> argparse.ArgumentParser:
|
|
82
98
|
p = argparse.ArgumentParser(
|
|
83
99
|
prog="swarph spawn",
|
|
@@ -202,36 +218,11 @@ def _resolve_cell(args: argparse.Namespace) -> tuple[Cell, Optional[str]]:
|
|
|
202
218
|
|
|
203
219
|
|
|
204
220
|
def _validate_routing(cell: Cell) -> None:
|
|
205
|
-
"""
|
|
206
|
-
|
|
207
|
-
Phase 1B-primary architecture commits the cell-membrane framing:
|
|
208
|
-
Claude CLI is the membrane for Anthropic-side spawns; non-Anthropic
|
|
209
|
-
cells get a different membrane (currently NOT implemented).
|
|
210
|
-
|
|
211
|
-
cell.yaml ``routing`` field shape (v0):
|
|
212
|
-
::
|
|
213
|
-
routing:
|
|
214
|
-
native: anthropic # only valid value in v0
|
|
215
|
-
|
|
216
|
-
v0 accepts ``routing`` absent (= default anthropic) OR
|
|
217
|
-
``routing.native: anthropic``. Any other value raises CellError
|
|
218
|
-
pointing at Phase 1B v1+ direction.
|
|
219
|
-
|
|
220
|
-
Forward-compat: reads via ``Cell.extra`` (same pattern as
|
|
221
|
-
cursor_path / tmux_session shipped in v0.7.2 before graduating to
|
|
222
|
-
typed fields in swarph-shared 0.4.x). When swarph-shared graduates
|
|
223
|
-
``routing`` to a typed Cell field, this helper swaps to typed
|
|
224
|
-
access with no API surface change for cell.yaml authors.
|
|
225
|
-
|
|
226
|
-
See research/swarph_cli/CELL_MEMBRANE_PHASE_0_RFC.md §5 (Phase 1B)
|
|
227
|
-
for the architectural context. See lab memory
|
|
228
|
-
``project_next_up.md`` for the commander 2026-05-19 decision that
|
|
229
|
-
Phase 1B is primary + path (c) Anthropic-only v0.
|
|
230
|
-
"""
|
|
221
|
+
"""Validate optional ``cell.extra.routing`` against the spawn provider."""
|
|
231
222
|
extra = cell.extra or {}
|
|
232
223
|
routing = extra.get("routing")
|
|
233
224
|
if routing is None:
|
|
234
|
-
return
|
|
225
|
+
return
|
|
235
226
|
if not isinstance(routing, dict):
|
|
236
227
|
raise CellError(
|
|
237
228
|
f"swarph spawn: cell.yaml `routing` must be a mapping, "
|
|
@@ -239,20 +230,33 @@ def _validate_routing(cell: Cell) -> None:
|
|
|
239
230
|
f"research/swarph_cli/CELL_MEMBRANE_PHASE_0_RFC.md for the "
|
|
240
231
|
f"valid v0 schema."
|
|
241
232
|
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
233
|
+
|
|
234
|
+
provider_native = {
|
|
235
|
+
"claude": "anthropic",
|
|
236
|
+
"codex": "codex",
|
|
237
|
+
"antigravity": "antigravity",
|
|
238
|
+
}.get(cell.provider)
|
|
239
|
+
if provider_native is None:
|
|
240
|
+
raise CellError(
|
|
241
|
+
f"swarph spawn: provider {cell.provider!r} is not supported "
|
|
242
|
+
"by this spawn membrane."
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
native = routing.get("native", provider_native)
|
|
246
|
+
|
|
247
|
+
if cell.provider == "antigravity":
|
|
248
|
+
if native in ("antigravity", "gemini"):
|
|
249
|
+
return
|
|
250
|
+
expected = "'antigravity' or 'gemini'"
|
|
251
|
+
else:
|
|
252
|
+
if native == provider_native:
|
|
253
|
+
return
|
|
254
|
+
expected = repr(provider_native)
|
|
255
|
+
|
|
246
256
|
raise CellError(
|
|
247
|
-
f"swarph spawn: cell.yaml `routing.native: {native!r}`
|
|
248
|
-
f"
|
|
249
|
-
f"
|
|
250
|
-
f"scope, deferred per commander 2026-05-19 until a concrete "
|
|
251
|
-
f"non-Anthropic-cell use case emerges. For now, remove the "
|
|
252
|
-
f"`routing.native` field OR set it to 'anthropic' to use the "
|
|
253
|
-
f"existing Claude CLI spawn path. See "
|
|
254
|
-
f"research/swarph_cli/CELL_MEMBRANE_PHASE_0_RFC.md §5 for the "
|
|
255
|
-
f"architectural direction."
|
|
257
|
+
f"swarph spawn: cell.yaml `routing.native: {native!r}` does not "
|
|
258
|
+
f"match provider {cell.provider!r}. Expected routing.native "
|
|
259
|
+
f"{expected}, or omit the routing field."
|
|
256
260
|
)
|
|
257
261
|
|
|
258
262
|
|
|
@@ -309,6 +313,75 @@ def _build_claude_argv(
|
|
|
309
313
|
return argv
|
|
310
314
|
|
|
311
315
|
|
|
316
|
+
def _codex_sandbox(cell: Cell) -> str:
|
|
317
|
+
sandbox = getattr(cell, "sandbox", None) or _CODEX_DEFAULT_SANDBOX
|
|
318
|
+
if sandbox not in _CODEX_SANDBOX_VALUES:
|
|
319
|
+
raise CellError(
|
|
320
|
+
f"cell.yaml: sandbox {sandbox!r} is not valid for provider "
|
|
321
|
+
f"'codex'. Valid values: {sorted(_CODEX_SANDBOX_VALUES)}."
|
|
322
|
+
)
|
|
323
|
+
return sandbox
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _agy_env() -> dict[str, str]:
|
|
327
|
+
"""Return a copy of the environment scrubbed of billing credentials."""
|
|
328
|
+
env = os.environ.copy()
|
|
329
|
+
env.pop("GOOGLE_APPLICATION_CREDENTIALS", None)
|
|
330
|
+
env.pop("GOOGLE_CLOUD_PROJECT", None)
|
|
331
|
+
env.pop("VERTEX_PROJECT", None)
|
|
332
|
+
env.pop("VERTEX_LOCATION", None)
|
|
333
|
+
return env
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _build_agy_argv(
|
|
337
|
+
cell: Cell, no_starter: bool, passthrough: list[str]
|
|
338
|
+
) -> list[str]:
|
|
339
|
+
argv = ["agy"]
|
|
340
|
+
|
|
341
|
+
# codex is adding cell.sandbox; default ON, only off on explicit falsy
|
|
342
|
+
sandbox_attr = getattr(cell, "sandbox", None)
|
|
343
|
+
if sandbox_attr is not None:
|
|
344
|
+
is_sandbox = sandbox_attr
|
|
345
|
+
else:
|
|
346
|
+
is_sandbox = cell.extra.get("sandbox", True)
|
|
347
|
+
|
|
348
|
+
if is_sandbox is not False:
|
|
349
|
+
argv.append("--sandbox")
|
|
350
|
+
|
|
351
|
+
# Pass --add-dir <cwd> for directory setup.
|
|
352
|
+
argv.extend(["--add-dir", str(cell.cwd)])
|
|
353
|
+
|
|
354
|
+
if not no_starter and cell.starter_prompt_path:
|
|
355
|
+
argv.extend(["--prompt-interactive", read_starter_prompt(cell)])
|
|
356
|
+
|
|
357
|
+
argv.extend(passthrough)
|
|
358
|
+
return argv
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _build_codex_argv(cell: Cell, passthrough: list[str]) -> list[str]:
|
|
362
|
+
argv = [
|
|
363
|
+
"codex",
|
|
364
|
+
"-C",
|
|
365
|
+
str(cell.cwd),
|
|
366
|
+
"-s",
|
|
367
|
+
_codex_sandbox(cell),
|
|
368
|
+
"-a",
|
|
369
|
+
_CODEX_APPROVAL,
|
|
370
|
+
]
|
|
371
|
+
argv.extend(passthrough)
|
|
372
|
+
return argv
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _scrubbed_codex_env() -> dict[str, str]:
|
|
376
|
+
env = {
|
|
377
|
+
key: value
|
|
378
|
+
for key, value in os.environ.items()
|
|
379
|
+
if key not in _CODEX_BILLING_LEAK_KEYS
|
|
380
|
+
}
|
|
381
|
+
env["SWARPH_SPAWN"] = "1"
|
|
382
|
+
return env
|
|
383
|
+
|
|
384
|
+
|
|
312
385
|
def _print_banner() -> None:
|
|
313
386
|
sys.stderr.write(_BANNER.format(version=__version__))
|
|
314
387
|
sys.stderr.flush()
|
|
@@ -316,7 +389,7 @@ def _print_banner() -> None:
|
|
|
316
389
|
|
|
317
390
|
def _print_dry_run(
|
|
318
391
|
cell: Cell,
|
|
319
|
-
session_id: str,
|
|
392
|
+
session_id: Optional[str],
|
|
320
393
|
was_generated: bool,
|
|
321
394
|
argv: list[str],
|
|
322
395
|
new_instance: bool = False,
|
|
@@ -339,15 +412,29 @@ def _print_dry_run(
|
|
|
339
412
|
print(f"# name: {cell.name}", file=sys.stderr)
|
|
340
413
|
print(f"# role: {cell.role}", file=sys.stderr)
|
|
341
414
|
print(f"# cwd: {cell.cwd}", file=sys.stderr)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
415
|
+
if cell.provider == "codex":
|
|
416
|
+
print(
|
|
417
|
+
"# session_id: codex: fresh-session-per-spawn, no pinned id "
|
|
418
|
+
"(cell.yaml session_id ignored)",
|
|
419
|
+
file=sys.stderr,
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
print(
|
|
423
|
+
f"# session_id: {session_id} ({sid_label})",
|
|
424
|
+
file=sys.stderr,
|
|
425
|
+
)
|
|
426
|
+
if cell.provider == "codex":
|
|
427
|
+
print(
|
|
428
|
+
"# starter: cwd AGENTS.md auto-read by codex; no "
|
|
429
|
+
"--append-system-prompt injection",
|
|
430
|
+
file=sys.stderr,
|
|
431
|
+
)
|
|
432
|
+
else:
|
|
433
|
+
print(
|
|
434
|
+
f"# starter: "
|
|
435
|
+
f"{cell.starter_prompt_path or '(none)'}",
|
|
436
|
+
file=sys.stderr,
|
|
437
|
+
)
|
|
351
438
|
print(f"# provider: {cell.provider}", file=sys.stderr)
|
|
352
439
|
if cell.lineage is not None:
|
|
353
440
|
print(
|
|
@@ -371,6 +458,69 @@ def _print_dry_run(
|
|
|
371
458
|
print(" ".join(redacted))
|
|
372
459
|
|
|
373
460
|
|
|
461
|
+
def _relaunch_in_windows_terminal(
|
|
462
|
+
claude_bin: str, claude_argv: list[str], cwd: Path,
|
|
463
|
+
) -> bool:
|
|
464
|
+
"""Auto-fix the conhost TUI bug by relaunching the session in Windows Terminal.
|
|
465
|
+
|
|
466
|
+
On legacy Windows console (``conhost.exe``), Claude Code's Ink TUI breaks: the
|
|
467
|
+
SGR terminator ``m`` leaks from the output stream into stdin, so pressing Enter
|
|
468
|
+
inserts a literal ``m`` instead of submitting (see docs/WINDOWS_KNOWN_ISSUES.md).
|
|
469
|
+
Windows Terminal handles VT-input correctly. If we're on conhost AND ``wt.exe``
|
|
470
|
+
is available, relaunch the ``claude`` session inside Windows Terminal and return
|
|
471
|
+
True (the caller should exit this console). Otherwise return False (caller
|
|
472
|
+
proceeds in-place and warns).
|
|
473
|
+
|
|
474
|
+
No-op (returns False) when:
|
|
475
|
+
* not Windows;
|
|
476
|
+
* stdout is not an interactive TTY (CI / piped / redirected) — there is no
|
|
477
|
+
human console to relaunch from, and a detached WT window would be wrong;
|
|
478
|
+
* we are already inside a session WE spawned (``SWARPH_SPAWN`` set) — the
|
|
479
|
+
reliable loop-guard: a relaunched session can never re-relaunch, regardless
|
|
480
|
+
of how ``WT_SESSION`` behaves on this box;
|
|
481
|
+
* operator opted to stay put (``SWARPH_WIN_ACK=1``);
|
|
482
|
+
* already inside Windows Terminal (``WT_SESSION`` set) AND not force-requested
|
|
483
|
+
— the TUI works there. NOTE: some corporate setups INHERIT ``WT_SESSION``
|
|
484
|
+
into child ``conhost`` consoles, so this is a comfort heuristic, not ground
|
|
485
|
+
truth. Set ``SWARPH_FORCE_WT=1`` to relaunch anyway when you know you are on
|
|
486
|
+
a broken conhost that carries an inherited ``WT_SESSION``;
|
|
487
|
+
* ``wt.exe`` is not installed (e.g. locked-down corporate box) — caller warns.
|
|
488
|
+
"""
|
|
489
|
+
if sys.platform != "win32":
|
|
490
|
+
return False
|
|
491
|
+
if not sys.stdout.isatty():
|
|
492
|
+
return False
|
|
493
|
+
if os.environ.get("SWARPH_SPAWN"):
|
|
494
|
+
return False
|
|
495
|
+
if os.environ.get("SWARPH_WIN_ACK"):
|
|
496
|
+
return False
|
|
497
|
+
if os.environ.get("WT_SESSION") and not os.environ.get("SWARPH_FORCE_WT"):
|
|
498
|
+
return False
|
|
499
|
+
wt = shutil.which("wt")
|
|
500
|
+
if not wt:
|
|
501
|
+
return False
|
|
502
|
+
# Relaunch claude inside Windows Terminal, in the cell's cwd, carrying
|
|
503
|
+
# SWARPH_SPAWN=1 so a SessionStart hook doesn't double-inject the starter.
|
|
504
|
+
# claude_argv[0] is the "claude" argv0; the real flags are claude_argv[1:].
|
|
505
|
+
wt_cmd = [wt, "-d", str(cwd), "--", claude_bin, *claude_argv[1:]]
|
|
506
|
+
env = {**os.environ, "SWARPH_SPAWN": "1"}
|
|
507
|
+
try:
|
|
508
|
+
subprocess.Popen(wt_cmd, env=env)
|
|
509
|
+
except OSError as exc:
|
|
510
|
+
print(
|
|
511
|
+
f"swarph spawn: Windows Terminal relaunch failed ({exc}); continuing "
|
|
512
|
+
f"in this console (TUI may misbehave — see docs/WINDOWS_KNOWN_ISSUES.md).",
|
|
513
|
+
file=sys.stderr,
|
|
514
|
+
)
|
|
515
|
+
return False
|
|
516
|
+
print(
|
|
517
|
+
"swarph spawn: relaunched the session in Windows Terminal (avoids the "
|
|
518
|
+
"conhost Enter-inserts-'m' TUI bug). This console can be closed.",
|
|
519
|
+
file=sys.stderr,
|
|
520
|
+
)
|
|
521
|
+
return True
|
|
522
|
+
|
|
523
|
+
|
|
374
524
|
def run_spawn(argv: Optional[list[str]] = None) -> int:
|
|
375
525
|
if argv is None:
|
|
376
526
|
argv = sys.argv[2:] # skip "swarph spawn"
|
|
@@ -410,77 +560,127 @@ def run_spawn(argv: Optional[list[str]] = None) -> int:
|
|
|
410
560
|
print(f"swarph spawn: {exc}", file=sys.stderr)
|
|
411
561
|
return 1
|
|
412
562
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
"swarph spawn:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
563
|
+
session_id: Optional[str]
|
|
564
|
+
was_generated = False
|
|
565
|
+
effective_role: Optional[str] = None
|
|
566
|
+
|
|
567
|
+
if cell.provider == "codex":
|
|
568
|
+
session_id = None
|
|
569
|
+
try:
|
|
570
|
+
spawn_argv = _build_codex_argv(cell, passthrough)
|
|
571
|
+
except CellError as exc:
|
|
572
|
+
print(f"swarph spawn: {exc}", file=sys.stderr)
|
|
573
|
+
return 1
|
|
574
|
+
elif cell.provider == "antigravity":
|
|
575
|
+
session_id = "(fresh-session-per-spawn, no pinned id)"
|
|
576
|
+
was_generated = True
|
|
577
|
+
sidecar_role = requested_role if requested_role else cell.role
|
|
578
|
+
effective_role = sidecar_role
|
|
579
|
+
try:
|
|
580
|
+
spawn_argv = _build_agy_argv(
|
|
581
|
+
cell, args.no_starter, passthrough
|
|
582
|
+
)
|
|
583
|
+
except CellError as exc:
|
|
584
|
+
print(f"swarph spawn: {exc}", file=sys.stderr)
|
|
585
|
+
return 1
|
|
586
|
+
else:
|
|
587
|
+
# When user typed a slot-role (e.g. `swarph spawn drop-on-meta-edge-2`)
|
|
588
|
+
# the cell.yaml resolved to the BASE file (drop-on-meta-edge.yaml) so
|
|
589
|
+
# cell.role = "drop-on-meta-edge". But the operator wants slot 2's
|
|
590
|
+
# sidecar + display name. Use the user's typed role for the sidecar
|
|
591
|
+
# lookup; cell.role stays the base role for cell-context (cwd, starter
|
|
592
|
+
# prompt, lineage, provider).
|
|
593
|
+
sidecar_role = requested_role if requested_role else cell.role
|
|
594
|
+
|
|
595
|
+
try:
|
|
596
|
+
session_id, was_generated, effective_role = load_or_create_session_id(
|
|
597
|
+
sidecar_role, cell, new_instance=args.new_instance
|
|
598
|
+
)
|
|
599
|
+
except CellError as exc:
|
|
600
|
+
print(f"swarph spawn: {exc}", file=sys.stderr)
|
|
601
|
+
return 1
|
|
602
|
+
|
|
603
|
+
if args.new_instance and cell.session_id:
|
|
604
|
+
# Pinned cell.yaml session_id wins over --new-instance; surface
|
|
605
|
+
# the conflict on stderr so the operator knows the flag was a
|
|
606
|
+
# no-op for this cell.
|
|
607
|
+
print(
|
|
608
|
+
"swarph spawn: --new-instance ignored — cell.yaml pins "
|
|
609
|
+
"session_id explicitly. Remove the pinned UUID from cell.yaml "
|
|
610
|
+
"OR pass --session-id <new-uuid> on the claude command line "
|
|
611
|
+
"via `-- --session-id <uuid>` passthrough to override.",
|
|
612
|
+
file=sys.stderr,
|
|
613
|
+
)
|
|
440
614
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
615
|
+
if args.new_instance and effective_role == sidecar_role and not cell.session_id:
|
|
616
|
+
# Degenerate case — --new-instance fired but no base sidecar
|
|
617
|
+
# existed, so we minted into the BASE slot (treating as the
|
|
618
|
+
# original). Surface this as a stderr note so the operator
|
|
619
|
+
# understands the v0.7 PR-B auto-suffix didn't kick in.
|
|
620
|
+
print(
|
|
621
|
+
"swarph spawn: --new-instance fired on a role with no "
|
|
622
|
+
f"existing sidecar — minted as the FIRST instance of "
|
|
623
|
+
f"{cell.role!r} (base slot), not as a sibling. Spawn the "
|
|
624
|
+
"original first via `swarph spawn <role>` (no --new-instance), "
|
|
625
|
+
"then re-run with --new-instance to mint a true sibling.",
|
|
626
|
+
file=sys.stderr,
|
|
627
|
+
)
|
|
454
628
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
629
|
+
try:
|
|
630
|
+
spawn_argv = _build_claude_argv(
|
|
631
|
+
cell, session_id, args.no_starter, passthrough,
|
|
632
|
+
effective_role=effective_role,
|
|
633
|
+
)
|
|
634
|
+
except CellError as exc:
|
|
635
|
+
print(f"swarph spawn: {exc}", file=sys.stderr)
|
|
636
|
+
return 1
|
|
463
637
|
|
|
464
638
|
if args.print_id:
|
|
465
|
-
|
|
639
|
+
if cell.provider == "codex":
|
|
640
|
+
print(_CODEX_PRINT_ID_NOTE)
|
|
641
|
+
else:
|
|
642
|
+
print(session_id)
|
|
466
643
|
|
|
467
644
|
if args.dry_run:
|
|
468
645
|
_print_dry_run(
|
|
469
|
-
cell, session_id, was_generated,
|
|
646
|
+
cell, session_id, was_generated, spawn_argv,
|
|
470
647
|
new_instance=args.new_instance,
|
|
471
648
|
effective_role=effective_role,
|
|
472
649
|
)
|
|
473
650
|
return 0
|
|
474
651
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
652
|
+
if cell.provider == "codex":
|
|
653
|
+
provider_bin = shutil.which("codex")
|
|
654
|
+
if provider_bin is None:
|
|
655
|
+
print(
|
|
656
|
+
"swarph spawn: 'codex' binary not found on PATH. "
|
|
657
|
+
"Install Codex CLI or set PATH explicitly.",
|
|
658
|
+
file=sys.stderr,
|
|
659
|
+
)
|
|
660
|
+
return 127
|
|
661
|
+
elif cell.provider == "antigravity":
|
|
662
|
+
provider_bin = shutil.which("agy")
|
|
663
|
+
if provider_bin is None:
|
|
664
|
+
home_local = Path.home() / ".local" / "bin" / "agy"
|
|
665
|
+
if home_local.exists():
|
|
666
|
+
provider_bin = str(home_local)
|
|
667
|
+
if provider_bin is None:
|
|
668
|
+
print(
|
|
669
|
+
"swarph spawn: 'agy' binary not found on PATH. "
|
|
670
|
+
"Install Antigravity CLI or set PATH explicitly.",
|
|
671
|
+
file=sys.stderr,
|
|
672
|
+
)
|
|
673
|
+
return 127
|
|
674
|
+
else:
|
|
675
|
+
provider_bin = shutil.which("claude")
|
|
676
|
+
if provider_bin is None:
|
|
677
|
+
print(
|
|
678
|
+
"swarph spawn: 'claude' binary not found on PATH. "
|
|
679
|
+
"Install Claude Code (https://docs.anthropic.com/claude/claude-code) "
|
|
680
|
+
"or set PATH explicitly.",
|
|
681
|
+
file=sys.stderr,
|
|
682
|
+
)
|
|
683
|
+
return 127
|
|
484
684
|
|
|
485
685
|
# Windows-platform known-issues banner. Claude Code's TUI (Ink-based)
|
|
486
686
|
# has documented input/rendering bugs on Windows native consoles
|
|
@@ -493,38 +693,61 @@ def run_spawn(argv: Optional[list[str]] = None) -> int:
|
|
|
493
693
|
# Banner is suppressed by --no-banner OR when the operator has
|
|
494
694
|
# already acknowledged via SWARPH_WIN_ACK=1 in env (set once after
|
|
495
695
|
# reading the doc).
|
|
696
|
+
# conhost TUI auto-fix (CLAUDE only — codex/agy don't use the claude TUI):
|
|
697
|
+
# on legacy Windows console (not Windows Terminal), relaunch the claude
|
|
698
|
+
# session in Windows Terminal where the Ink TUI works. Returns True (and we
|
|
699
|
+
# exit this console) only when it actually relaunched.
|
|
700
|
+
if cell.provider == "claude" and _relaunch_in_windows_terminal(
|
|
701
|
+
provider_bin, spawn_argv, cell.cwd
|
|
702
|
+
):
|
|
703
|
+
return 0
|
|
704
|
+
|
|
705
|
+
# Still in a broken console (conhost with no wt.exe, or operator acked).
|
|
706
|
+
# Warn unless suppressed. Inside Windows Terminal (WT_SESSION set) the TUI
|
|
707
|
+
# works, so no warning fires there.
|
|
496
708
|
if (
|
|
497
|
-
|
|
709
|
+
cell.provider == "claude"
|
|
710
|
+
and sys.platform == "win32"
|
|
498
711
|
and not args.no_banner
|
|
499
712
|
and not os.environ.get("SWARPH_WIN_ACK")
|
|
713
|
+
and not os.environ.get("WT_SESSION")
|
|
500
714
|
):
|
|
501
715
|
print(
|
|
502
|
-
"swarph spawn: WARNING — Windows
|
|
503
|
-
"
|
|
504
|
-
"
|
|
505
|
-
"'m'
|
|
506
|
-
"
|
|
507
|
-
"
|
|
716
|
+
"swarph spawn: WARNING — legacy Windows console (conhost) and Windows "
|
|
717
|
+
"Terminal (wt.exe) was not found, so the session couldn't be "
|
|
718
|
+
"auto-relaunched. Claude Code's TUI mis-handles input here (Enter "
|
|
719
|
+
"inserts literal 'm'). Install Windows Terminal (Microsoft Store) and "
|
|
720
|
+
"re-run, or use WSL2. See docs/WINDOWS_KNOWN_ISSUES.md. Set "
|
|
721
|
+
"SWARPH_WIN_ACK=1 to suppress and run here anyway.",
|
|
508
722
|
file=sys.stderr,
|
|
509
723
|
)
|
|
510
724
|
|
|
725
|
+
if cell.provider == "claude":
|
|
726
|
+
try:
|
|
727
|
+
os.chdir(cell.cwd)
|
|
728
|
+
except OSError as exc:
|
|
729
|
+
print(f"swarph spawn: cannot chdir to {cell.cwd}: {exc}", file=sys.stderr)
|
|
730
|
+
return 1
|
|
731
|
+
|
|
732
|
+
# v0.7 PR-C — set SWARPH_SPAWN=1 env so a SessionStart hook
|
|
733
|
+
# installed via `swarph install-hook` knows the prompt was
|
|
734
|
+
# already injected via --append-system-prompt and skips
|
|
735
|
+
# double-injection. The env propagates through execv since we
|
|
736
|
+
# don't use execve with a custom env.
|
|
737
|
+
os.environ["SWARPH_SPAWN"] = "1"
|
|
738
|
+
elif cell.provider == "antigravity":
|
|
739
|
+
env = _agy_env()
|
|
740
|
+
os.environ.clear()
|
|
741
|
+
os.environ.update(env)
|
|
742
|
+
os.environ["SWARPH_SPAWN"] = "1"
|
|
743
|
+
|
|
744
|
+
# exec-replace so the spawned provider session owns stdio +
|
|
745
|
+
# signals cleanly. argv[0] is preserved for ps-grep.
|
|
511
746
|
try:
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
# v0.7 PR-C — set SWARPH_SPAWN=1 env so a SessionStart hook
|
|
518
|
-
# installed via `swarph install-hook` knows the prompt was
|
|
519
|
-
# already injected via --append-system-prompt and skips
|
|
520
|
-
# double-injection. The env propagates through execv since we
|
|
521
|
-
# don't use execve with a custom env.
|
|
522
|
-
os.environ["SWARPH_SPAWN"] = "1"
|
|
523
|
-
|
|
524
|
-
# exec-replace so the spawned claude session owns stdio +
|
|
525
|
-
# signals cleanly. argv[0] is preserved as 'claude' for ps-grep.
|
|
526
|
-
try:
|
|
527
|
-
os.execv(claude_bin, claude_argv)
|
|
747
|
+
if cell.provider == "codex":
|
|
748
|
+
os.execve(provider_bin, spawn_argv, _scrubbed_codex_env())
|
|
749
|
+
else:
|
|
750
|
+
os.execv(provider_bin, spawn_argv)
|
|
528
751
|
except OSError as exc:
|
|
529
752
|
# execv only returns on failure.
|
|
530
753
|
print(f"swarph spawn: exec failed: {exc}", file=sys.stderr)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing, cell.yaml spawn, session import, watchdog. v0.8.0: multi-provider spawn membrane — `swarph spawn` launches claude, codex (GPT), or antigravity/agy (Gemini) per cell.provider, each with billing-scrub + sandbox; plus the Windows conhost auto-relaunch into Windows Terminal (the Enter-inserts-'m' TUI fix).
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/darw007d/swarph-cli
|