swarph-cli 0.7.9__tar.gz → 0.9.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.9.0}/PKG-INFO +2 -2
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/pyproject.toml +2 -2
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/__init__.py +1 -1
- swarph_cli-0.9.0/src/swarph_cli/commands/memory_sync.py +194 -0
- swarph_cli-0.9.0/src/swarph_cli/commands/mesh.py +514 -0
- swarph_cli-0.9.0/src/swarph_cli/commands/spawn.py +910 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/main.py +4 -1
- {swarph_cli-0.7.9 → swarph_cli-0.9.0/src/swarph_cli.egg-info}/PKG-INFO +2 -2
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli.egg-info/SOURCES.txt +6 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_cell_loader.py +10 -2
- swarph_cli-0.9.0/tests/test_memory_sync.py +57 -0
- swarph_cli-0.9.0/tests/test_mesh_command.py +233 -0
- swarph_cli-0.9.0/tests/test_mesh_sidecar.py +206 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_spawn_command.py +408 -6
- swarph_cli-0.9.0/tests/test_spawn_windows_relaunch.py +129 -0
- swarph_cli-0.7.9/src/swarph_cli/commands/spawn.py +0 -532
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/LICENSE +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/README.md +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/setup.cfg +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_import_command.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_main.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.0}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.7.9 → swarph_cli-0.9.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
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider spawn (claude/codex/antigravity per cell.provider, via a ProviderMembrane), cell.yaml, session import, watchdog. v0.9.0 adds `swarph mesh` (send/inbox/register with per-peer tokens) + a provider-agnostic inbox sidecar, and `assisted_memory` (git-backed durable memory: restore-on-spawn + saver loop + current-task anchor) toggled per cell.
|
|
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
|
|
7
|
+
version = "0.9.0"
|
|
8
|
+
description = "The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider spawn (claude/codex/antigravity per cell.provider, via a ProviderMembrane), cell.yaml, session import, watchdog. v0.9.0 adds `swarph mesh` (send/inbox/register with per-peer tokens) + a provider-agnostic inbox sidecar, and `assisted_memory` (git-backed durable memory: restore-on-spawn + saver loop + current-task anchor) toggled per cell."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Assisted memory saver loop (Stage 2) + Restore helper."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import datetime
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from swarph_cli.cell import Cell, load_cell, CellError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_memory_repo_path(cell: Cell) -> Path:
|
|
16
|
+
# Memory repos are stored locally under ~/.local/share/swarph/memory/<role>
|
|
17
|
+
return Path.home() / ".local" / "share" / "swarph" / "memory" / cell.role
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_files_to_sync(cell: Cell) -> list[tuple[str, Path]]:
|
|
21
|
+
files_to_sync = []
|
|
22
|
+
|
|
23
|
+
# Common
|
|
24
|
+
if (cell.cwd / "CURRENT_TASK.md").exists():
|
|
25
|
+
files_to_sync.append(("CURRENT_TASK.md", cell.cwd / "CURRENT_TASK.md"))
|
|
26
|
+
|
|
27
|
+
if cell.provider == "claude":
|
|
28
|
+
if (cell.cwd / "CLAUDE.md").exists():
|
|
29
|
+
files_to_sync.append(("CLAUDE.md", cell.cwd / "CLAUDE.md"))
|
|
30
|
+
mem_dir = Path.home() / ".claude"
|
|
31
|
+
if (mem_dir / "MEMORY.md").exists():
|
|
32
|
+
files_to_sync.append(("MEMORY.md", mem_dir / "MEMORY.md"))
|
|
33
|
+
for p in mem_dir.glob("memory/*.md"):
|
|
34
|
+
files_to_sync.append((f"memory/{p.name}", p))
|
|
35
|
+
if (mem_dir / "inbox-cursor").exists():
|
|
36
|
+
files_to_sync.append(("inbox-cursor", mem_dir / "inbox-cursor"))
|
|
37
|
+
|
|
38
|
+
elif cell.provider == "codex":
|
|
39
|
+
if (cell.cwd / "AGENTS.md").exists():
|
|
40
|
+
files_to_sync.append(("AGENTS.md", cell.cwd / "AGENTS.md"))
|
|
41
|
+
|
|
42
|
+
elif cell.provider in ("antigravity", "gemini"):
|
|
43
|
+
if (cell.cwd / "GEMINI.md").exists():
|
|
44
|
+
files_to_sync.append(("GEMINI.md", cell.cwd / "GEMINI.md"))
|
|
45
|
+
if (cell.cwd / "inbox-cursor.json").exists():
|
|
46
|
+
files_to_sync.append(("inbox-cursor.json", cell.cwd / "inbox-cursor.json"))
|
|
47
|
+
|
|
48
|
+
gemini_tmp = Path.home() / ".gemini" / "tmp"
|
|
49
|
+
if gemini_tmp.is_dir():
|
|
50
|
+
for proj_dir in gemini_tmp.iterdir():
|
|
51
|
+
if proj_dir.is_dir():
|
|
52
|
+
for p in proj_dir.glob("memory/*.md"):
|
|
53
|
+
rel = f"tmp/{proj_dir.name}/memory/{p.name}"
|
|
54
|
+
files_to_sync.append((rel, p))
|
|
55
|
+
|
|
56
|
+
history_proj = Path.home() / ".gemini" / "history" / ".project_root"
|
|
57
|
+
if history_proj.exists():
|
|
58
|
+
files_to_sync.append(("history/.project_root", history_proj))
|
|
59
|
+
|
|
60
|
+
return files_to_sync
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _clone_if_missing(repo_url: str, repo_dir: Path) -> bool:
|
|
64
|
+
if not (repo_dir / ".git").is_dir():
|
|
65
|
+
repo_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
if "://" not in repo_url and "@" not in repo_url:
|
|
67
|
+
repo_url = f"git@github.com:{repo_url}.git"
|
|
68
|
+
try:
|
|
69
|
+
subprocess.run(["git", "clone", repo_url, str(repo_dir)], check=True)
|
|
70
|
+
except subprocess.CalledProcessError as exc:
|
|
71
|
+
print(f"swarph: git clone failed: {exc}", file=sys.stderr)
|
|
72
|
+
return False
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def perform_restore(cell: Cell) -> Optional[str]:
|
|
77
|
+
"""Stage 3: Restore files from memory repo to the filesystem.
|
|
78
|
+
|
|
79
|
+
Returns the text of CURRENT_TASK.md if it exists and was restored,
|
|
80
|
+
so the caller can surface it to the context window.
|
|
81
|
+
"""
|
|
82
|
+
am = cell.assisted_memory
|
|
83
|
+
if not am or not am.get("enabled"):
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
repo_url = am["repo"]
|
|
87
|
+
repo_dir = get_memory_repo_path(cell)
|
|
88
|
+
|
|
89
|
+
if not _clone_if_missing(repo_url, repo_dir):
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# pull --ff-only
|
|
93
|
+
try:
|
|
94
|
+
subprocess.run(["git", "-C", str(repo_dir), "pull", "--ff-only", "origin", "main"], check=True, capture_output=True)
|
|
95
|
+
except subprocess.CalledProcessError as exc:
|
|
96
|
+
print(f"swarph: memory pull --ff-only failed (diverged/offline?): {exc}", file=sys.stderr)
|
|
97
|
+
|
|
98
|
+
# Walk the repo dir and copy everything back, EXCEPT .git and .gitignore
|
|
99
|
+
for root, dirs, files in os.walk(repo_dir):
|
|
100
|
+
if ".git" in dirs:
|
|
101
|
+
dirs.remove(".git")
|
|
102
|
+
for f in files:
|
|
103
|
+
if f == ".gitignore":
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
src = Path(root) / f
|
|
107
|
+
rel = src.relative_to(repo_dir)
|
|
108
|
+
|
|
109
|
+
dest = None
|
|
110
|
+
if rel.parts[0] in ("CURRENT_TASK.md", "CLAUDE.md", "AGENTS.md", "GEMINI.md", "inbox-cursor.json"):
|
|
111
|
+
dest = cell.cwd / rel
|
|
112
|
+
elif cell.provider == "claude" and rel.parts[0] == "MEMORY.md":
|
|
113
|
+
dest = Path.home() / ".claude" / "MEMORY.md"
|
|
114
|
+
elif cell.provider == "claude" and rel.parts[0] == "memory":
|
|
115
|
+
dest = Path.home() / ".claude" / rel
|
|
116
|
+
elif cell.provider == "claude" and rel.parts[0] == "inbox-cursor":
|
|
117
|
+
dest = Path.home() / ".claude" / "inbox-cursor"
|
|
118
|
+
elif cell.provider in ("antigravity", "gemini") and rel.parts[0] == "tmp":
|
|
119
|
+
dest = Path.home() / ".gemini" / rel
|
|
120
|
+
elif cell.provider in ("antigravity", "gemini") and rel.parts[0] == "history":
|
|
121
|
+
dest = Path.home() / ".gemini" / rel
|
|
122
|
+
|
|
123
|
+
if dest:
|
|
124
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
shutil.copy2(src, dest)
|
|
126
|
+
|
|
127
|
+
current_task = cell.cwd / "CURRENT_TASK.md"
|
|
128
|
+
if current_task.exists():
|
|
129
|
+
return current_task.read_text(encoding="utf-8")
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def run_memory_sync(argv: list[str]) -> int:
|
|
134
|
+
parser = argparse.ArgumentParser(prog="swarph memory-sync")
|
|
135
|
+
parser.add_argument("cell_yaml", help="Path to cell.yaml")
|
|
136
|
+
args = parser.parse_args(argv)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
cell = load_cell(Path(args.cell_yaml))
|
|
140
|
+
except CellError as exc:
|
|
141
|
+
print(f"swarph memory-sync: {exc}", file=sys.stderr)
|
|
142
|
+
return 1
|
|
143
|
+
|
|
144
|
+
am = cell.assisted_memory
|
|
145
|
+
if not am or not am.get("enabled"):
|
|
146
|
+
print("swarph memory-sync: assisted_memory not enabled for this cell. Exiting.", file=sys.stderr)
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
repo_url = am["repo"]
|
|
150
|
+
repo_dir = get_memory_repo_path(cell)
|
|
151
|
+
|
|
152
|
+
if not _clone_if_missing(repo_url, repo_dir):
|
|
153
|
+
return 1
|
|
154
|
+
|
|
155
|
+
gitignore_path = repo_dir / ".gitignore"
|
|
156
|
+
if not gitignore_path.exists():
|
|
157
|
+
gitignore_path.write_text("secrets/\n.*creds*\n*.token\n", encoding="utf-8")
|
|
158
|
+
subprocess.run(["git", "-C", str(repo_dir), "add", ".gitignore"], check=True)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
subprocess.run(["git", "-C", str(repo_dir), "pull", "--ff-only", "origin", "main"], check=True, capture_output=True)
|
|
162
|
+
except subprocess.CalledProcessError:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
# EMPTY-GUARD check
|
|
166
|
+
guard_file = None
|
|
167
|
+
if cell.provider == "claude":
|
|
168
|
+
guard_file = cell.cwd / "CLAUDE.md"
|
|
169
|
+
elif cell.provider == "codex":
|
|
170
|
+
guard_file = cell.cwd / "AGENTS.md"
|
|
171
|
+
elif cell.provider in ("antigravity", "gemini"):
|
|
172
|
+
guard_file = cell.cwd / "GEMINI.md"
|
|
173
|
+
|
|
174
|
+
if guard_file and (not guard_file.exists() or guard_file.stat().st_size == 0):
|
|
175
|
+
print(f"[auto_sync] {datetime.datetime.now(datetime.timezone.utc).strftime('%FT%TZ')} SAFETY: {guard_file} missing or empty — skipping sync", file=sys.stderr)
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
files_to_sync = _get_files_to_sync(cell)
|
|
179
|
+
for rel_path, abs_path in files_to_sync:
|
|
180
|
+
dest = repo_dir / rel_path
|
|
181
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
shutil.copy2(abs_path, dest)
|
|
183
|
+
subprocess.run(["git", "-C", str(repo_dir), "add", str(dest)], check=True)
|
|
184
|
+
|
|
185
|
+
diff_check = subprocess.run(["git", "-C", str(repo_dir), "diff", "--cached", "--quiet"])
|
|
186
|
+
if diff_check.returncode != 0:
|
|
187
|
+
ts = datetime.datetime.now(datetime.timezone.utc).strftime('%FT%TZ')
|
|
188
|
+
subprocess.run(["git", "-C", str(repo_dir), "commit", "-m", f"auto-snapshot {ts}"], check=True, capture_output=True)
|
|
189
|
+
try:
|
|
190
|
+
subprocess.run(["git", "-C", str(repo_dir), "push", "origin", "main"], check=True, capture_output=True)
|
|
191
|
+
except subprocess.CalledProcessError as exc:
|
|
192
|
+
print(f"swarph memory-sync: push failed: {exc}", file=sys.stderr)
|
|
193
|
+
|
|
194
|
+
return 0
|