swarph-cli 0.2.0__tar.gz → 0.4.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.2.0/src/swarph_cli.egg-info → swarph_cli-0.4.0}/PKG-INFO +86 -13
- swarph_cli-0.4.0/README.md +184 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/pyproject.toml +5 -3
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli/__init__.py +1 -1
- swarph_cli-0.4.0/src/swarph_cli/commands/chat.py +348 -0
- swarph_cli-0.4.0/src/swarph_cli/commands/onboard.py +377 -0
- swarph_cli-0.4.0/src/swarph_cli/commands/ratify.py +283 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli/main.py +13 -4
- {swarph_cli-0.2.0 → swarph_cli-0.4.0/src/swarph_cli.egg-info}/PKG-INFO +86 -13
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/SOURCES.txt +9 -1
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/requires.txt +1 -1
- swarph_cli-0.4.0/tests/test_chat_command.py +444 -0
- swarph_cli-0.4.0/tests/test_onboard_command.py +279 -0
- swarph_cli-0.4.0/tests/test_ratify_command.py +224 -0
- swarph_cli-0.4.0/tests/test_smoke_chat.py +61 -0
- swarph_cli-0.4.0/tests/test_smoke_phase_5_5.py +144 -0
- swarph_cli-0.2.0/README.md +0 -111
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/LICENSE +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/setup.cfg +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/tests/test_import_command.py +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/tests/test_main.py +0 -0
- {swarph_cli-0.2.0 → swarph_cli-0.4.0}/tests/test_smoke_one_shot.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. Phase 2 one-shot
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.4.0 ships Phase 5.5 `swarph onboard` + `swarph ratify` on top of Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL (PLAN.md §13 / §15).
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/darw007d/swarph-cli
|
|
@@ -24,7 +24,7 @@ Classifier: Topic :: Utilities
|
|
|
24
24
|
Requires-Python: >=3.10
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
License-File: LICENSE
|
|
27
|
-
Requires-Dist: swarph-mesh>=0.
|
|
27
|
+
Requires-Dist: swarph-mesh>=0.5.0
|
|
28
28
|
Requires-Dist: swarph-shared>=0.2.0
|
|
29
29
|
Provides-Extra: dev
|
|
30
30
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
@@ -49,12 +49,84 @@ This is one of three repos in the v0.3.x architecture:
|
|
|
49
49
|
|
|
50
50
|
## Status
|
|
51
51
|
|
|
52
|
-
**v0.
|
|
52
|
+
**v0.4.0 — Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL + Phase 5.5 onboard/ratify.** Five verbs ship:
|
|
53
53
|
|
|
54
|
-
1. `swarph "prompt"` — Phase 2 one-shot mode (
|
|
55
|
-
2. `swarph
|
|
54
|
+
1. `swarph "prompt"` — Phase 2 one-shot mode (any of five providers)
|
|
55
|
+
2. `swarph chat` — Phase 5 interactive REPL with multi-turn history + slash commands
|
|
56
|
+
3. `swarph import <path>` — Phase 2.5 session import (Claude JSONL → swarph-native, with `--report-only` for honest pre-commit inspection)
|
|
57
|
+
4. `swarph onboard <peer-name>` — **NEW** Phase 5.5 mechanics-phase onboarding (PLAN.md §15.4)
|
|
58
|
+
5. `swarph ratify <peer-name>` — **NEW** Phase 5.5 witness ratification (PLAN.md §15.4a)
|
|
56
59
|
|
|
57
|
-
Subsequent phases extend the CLI surface (
|
|
60
|
+
Subsequent phases extend the CLI surface (`--ask <peer>`, daemon).
|
|
61
|
+
|
|
62
|
+
### `swarph onboard` + `swarph ratify` (Phase 5.5)
|
|
63
|
+
|
|
64
|
+
Per PLAN.md §15, onboarding splits into a **mechanics phase** (`swarph onboard`) that automates the boring parts (registry POST, scaffolding, token resolution) and a **manual contract phase** (the new peer composes the handshake DM in their own words). A witness peer judges the handshake and runs `swarph ratify <peer>` to flip `ratified=true`, gating `task_claim` server-side.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# New peer self-onboards
|
|
68
|
+
$ swarph onboard razorpeter
|
|
69
|
+
[1/6] validate_node_name('razorpeter') ok
|
|
70
|
+
[2/6] prepare peer-registry row ok
|
|
71
|
+
[3/6] resolve MESH_GATEWAY_TOKEN ok
|
|
72
|
+
[4/6] POST .../peers/register ok (registered_unratified=true)
|
|
73
|
+
[5/6] verify_subscription_setup() ok
|
|
74
|
+
[6/6] scaffold ~/swarph_state/razorpeter/ ok
|
|
75
|
+
|
|
76
|
+
[manual] handshake template at /tmp/razorpeter-handshake.md
|
|
77
|
+
Edit each section in your own words, then send to your witness peer.
|
|
78
|
+
|
|
79
|
+
# After peer composes + sends handshake, witness ratifies
|
|
80
|
+
$ SWARPH_WITNESS=lab-ovh swarph ratify razorpeter \
|
|
81
|
+
--reason "handshake covers all four invariants in own words"
|
|
82
|
+
[1/6] validate_node_name('razorpeter') ok
|
|
83
|
+
[2/6] verify witness 'lab-ovh' is ratified ok
|
|
84
|
+
[3/6] verify 'razorpeter' is registered_unratified ok
|
|
85
|
+
[4/6] PATCH .../peers/razorpeter ok
|
|
86
|
+
[5/6] verify peer_ratifications audit row ok (id=N reason='...')
|
|
87
|
+
[6/6] invalidate local TTL cache ok
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Server-side gating (mesh-gateway PR A): unratified peers can read inbox + send DMs (so the handshake itself works) but `task_claim` returns 403. Witness must itself be ratified — no self-ratification, no unratified-witnesses-ratifying-others. Audit log (`peer_ratifications`) is append-only.
|
|
91
|
+
|
|
92
|
+
### `swarph chat`
|
|
93
|
+
|
|
94
|
+
Interactive REPL against any of the five swarph-mesh adapters (`gemini` / `deepseek` / `claude` / `openai` / `grok`). Multi-turn conversation history accumulates in-memory; cumulative session cost + token totals tracked.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
$ swarph chat --provider claude
|
|
98
|
+
swarph chat — Phase 5 REPL
|
|
99
|
+
provider=claude model=(adapter default) caller=cli.repl.ubuntu
|
|
100
|
+
|
|
101
|
+
Type a message and press Enter to send. Slash commands:
|
|
102
|
+
/help /clear /system /provider /model /history /cost /quit
|
|
103
|
+
Ctrl-D to exit.
|
|
104
|
+
|
|
105
|
+
> hello
|
|
106
|
+
Hi! How can I help...
|
|
107
|
+
# 8+12t $0 0.34s
|
|
108
|
+
|
|
109
|
+
> /provider gemini
|
|
110
|
+
[switched to provider=gemini; model reset to adapter default; history cleared]
|
|
111
|
+
|
|
112
|
+
> /cost
|
|
113
|
+
[turns=1 in=8 out=12 cost=$0]
|
|
114
|
+
|
|
115
|
+
> /quit
|
|
116
|
+
[swarph-chat] bye.
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Slash commands:**
|
|
120
|
+
- `/help` — print available commands
|
|
121
|
+
- `/quit`, `/exit` (or Ctrl-D) — exit
|
|
122
|
+
- `/clear`, `/reset` — clear history (keeps system prompt)
|
|
123
|
+
- `/system [prompt]` — set or clear system prompt
|
|
124
|
+
- `/provider <name>` — switch provider (resets history)
|
|
125
|
+
- `/model <name>` — switch model
|
|
126
|
+
- `/history` — print running message list
|
|
127
|
+
- `/cost` — cumulative session cost + tokens
|
|
128
|
+
|
|
129
|
+
**Out of scope until Phase 5.6** (`swarph daemon`): inbox drain coroutine, `/inbox` and `/reply` slash commands. Streaming output ships alongside the cross-adapter `stream()` work in v0.5+ of swarph-mesh.
|
|
58
130
|
|
|
59
131
|
### `swarph import`
|
|
60
132
|
|
|
@@ -115,13 +187,14 @@ Pong!
|
|
|
115
187
|
|
|
116
188
|
| Phase | What lands |
|
|
117
189
|
|---|---|
|
|
118
|
-
| **0**
|
|
119
|
-
| **2** | One-shot mode: `swarph "hello" --provider gemini` |
|
|
190
|
+
| **0** | Scaffold — entry-point + status banner |
|
|
191
|
+
| **2** (v0.1.0) | One-shot mode: `swarph "hello" --provider gemini` |
|
|
192
|
+
| **2.5** (v0.2.0) | `swarph import` — Claude JSONL → swarph-native session format |
|
|
193
|
+
| **5** (v0.3.0) | `swarph chat` interactive REPL — multi-turn against any of five adapters + slash commands |
|
|
194
|
+
| **5.5** (v0.4.0 — this release) | **`swarph onboard <peer-name>` + `swarph ratify <peer-name>`** — six mechanics steps + handshake template + witness flip (PLAN.md §15) |
|
|
120
195
|
| **3** | `--ask <peer>` mesh-aware one-shot via MeshClient |
|
|
121
|
-
| **5** |
|
|
122
|
-
| **
|
|
123
|
-
| **5.7** | `swarph daemon` foreground drain loop + `swarph chat` REPL with drain coroutine (PLAN.md §16) |
|
|
124
|
-
| **6** | PyPI publish |
|
|
196
|
+
| **5.6** | `swarph daemon` foreground drain loop + REPL drain coroutine + `/inbox`, `/reply` (PLAN.md §16) |
|
|
197
|
+
| **6** | (already done) PyPI publish |
|
|
125
198
|
|
|
126
199
|
## Why split CLI from substrate
|
|
127
200
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# swarph-cli
|
|
2
|
+
|
|
3
|
+
The `swarph` binary — multi-LLM CLI with mesh-gateway integration. Thin client over the [`swarph-mesh`](https://github.com/darw007d/swarph-mesh) substrate.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install swarph-cli
|
|
7
|
+
swarph --version
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
This is one of three repos in the v0.3.x architecture:
|
|
11
|
+
|
|
12
|
+
| Repo | Role |
|
|
13
|
+
|---|---|
|
|
14
|
+
| [`swarph-mesh`](https://github.com/darw007d/swarph-mesh) | Substrate Python package — Protocol + adapters + SwarphCall + MeshClient. Pure library, no CLI |
|
|
15
|
+
| [`swarph-cli`](https://github.com/darw007d/swarph-cli) | This repo — the `swarph` binary |
|
|
16
|
+
| [`swarph-meshlm`](https://github.com/darw007d/swarph-meshlm) | Simon Willison `llm` plugin |
|
|
17
|
+
|
|
18
|
+
## Status
|
|
19
|
+
|
|
20
|
+
**v0.4.0 — Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL + Phase 5.5 onboard/ratify.** Five verbs ship:
|
|
21
|
+
|
|
22
|
+
1. `swarph "prompt"` — Phase 2 one-shot mode (any of five providers)
|
|
23
|
+
2. `swarph chat` — Phase 5 interactive REPL with multi-turn history + slash commands
|
|
24
|
+
3. `swarph import <path>` — Phase 2.5 session import (Claude JSONL → swarph-native, with `--report-only` for honest pre-commit inspection)
|
|
25
|
+
4. `swarph onboard <peer-name>` — **NEW** Phase 5.5 mechanics-phase onboarding (PLAN.md §15.4)
|
|
26
|
+
5. `swarph ratify <peer-name>` — **NEW** Phase 5.5 witness ratification (PLAN.md §15.4a)
|
|
27
|
+
|
|
28
|
+
Subsequent phases extend the CLI surface (`--ask <peer>`, daemon).
|
|
29
|
+
|
|
30
|
+
### `swarph onboard` + `swarph ratify` (Phase 5.5)
|
|
31
|
+
|
|
32
|
+
Per PLAN.md §15, onboarding splits into a **mechanics phase** (`swarph onboard`) that automates the boring parts (registry POST, scaffolding, token resolution) and a **manual contract phase** (the new peer composes the handshake DM in their own words). A witness peer judges the handshake and runs `swarph ratify <peer>` to flip `ratified=true`, gating `task_claim` server-side.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# New peer self-onboards
|
|
36
|
+
$ swarph onboard razorpeter
|
|
37
|
+
[1/6] validate_node_name('razorpeter') ok
|
|
38
|
+
[2/6] prepare peer-registry row ok
|
|
39
|
+
[3/6] resolve MESH_GATEWAY_TOKEN ok
|
|
40
|
+
[4/6] POST .../peers/register ok (registered_unratified=true)
|
|
41
|
+
[5/6] verify_subscription_setup() ok
|
|
42
|
+
[6/6] scaffold ~/swarph_state/razorpeter/ ok
|
|
43
|
+
|
|
44
|
+
[manual] handshake template at /tmp/razorpeter-handshake.md
|
|
45
|
+
Edit each section in your own words, then send to your witness peer.
|
|
46
|
+
|
|
47
|
+
# After peer composes + sends handshake, witness ratifies
|
|
48
|
+
$ SWARPH_WITNESS=lab-ovh swarph ratify razorpeter \
|
|
49
|
+
--reason "handshake covers all four invariants in own words"
|
|
50
|
+
[1/6] validate_node_name('razorpeter') ok
|
|
51
|
+
[2/6] verify witness 'lab-ovh' is ratified ok
|
|
52
|
+
[3/6] verify 'razorpeter' is registered_unratified ok
|
|
53
|
+
[4/6] PATCH .../peers/razorpeter ok
|
|
54
|
+
[5/6] verify peer_ratifications audit row ok (id=N reason='...')
|
|
55
|
+
[6/6] invalidate local TTL cache ok
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Server-side gating (mesh-gateway PR A): unratified peers can read inbox + send DMs (so the handshake itself works) but `task_claim` returns 403. Witness must itself be ratified — no self-ratification, no unratified-witnesses-ratifying-others. Audit log (`peer_ratifications`) is append-only.
|
|
59
|
+
|
|
60
|
+
### `swarph chat`
|
|
61
|
+
|
|
62
|
+
Interactive REPL against any of the five swarph-mesh adapters (`gemini` / `deepseek` / `claude` / `openai` / `grok`). Multi-turn conversation history accumulates in-memory; cumulative session cost + token totals tracked.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
$ swarph chat --provider claude
|
|
66
|
+
swarph chat — Phase 5 REPL
|
|
67
|
+
provider=claude model=(adapter default) caller=cli.repl.ubuntu
|
|
68
|
+
|
|
69
|
+
Type a message and press Enter to send. Slash commands:
|
|
70
|
+
/help /clear /system /provider /model /history /cost /quit
|
|
71
|
+
Ctrl-D to exit.
|
|
72
|
+
|
|
73
|
+
> hello
|
|
74
|
+
Hi! How can I help...
|
|
75
|
+
# 8+12t $0 0.34s
|
|
76
|
+
|
|
77
|
+
> /provider gemini
|
|
78
|
+
[switched to provider=gemini; model reset to adapter default; history cleared]
|
|
79
|
+
|
|
80
|
+
> /cost
|
|
81
|
+
[turns=1 in=8 out=12 cost=$0]
|
|
82
|
+
|
|
83
|
+
> /quit
|
|
84
|
+
[swarph-chat] bye.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Slash commands:**
|
|
88
|
+
- `/help` — print available commands
|
|
89
|
+
- `/quit`, `/exit` (or Ctrl-D) — exit
|
|
90
|
+
- `/clear`, `/reset` — clear history (keeps system prompt)
|
|
91
|
+
- `/system [prompt]` — set or clear system prompt
|
|
92
|
+
- `/provider <name>` — switch provider (resets history)
|
|
93
|
+
- `/model <name>` — switch model
|
|
94
|
+
- `/history` — print running message list
|
|
95
|
+
- `/cost` — cumulative session cost + tokens
|
|
96
|
+
|
|
97
|
+
**Out of scope until Phase 5.6** (`swarph daemon`): inbox drain coroutine, `/inbox` and `/reply` slash commands. Streaming output ships alongside the cross-adapter `stream()` work in v0.5+ of swarph-mesh.
|
|
98
|
+
|
|
99
|
+
### `swarph import`
|
|
100
|
+
|
|
101
|
+
Per PLAN.md §17, session import is the **knowledge half of onboarding** — gives a memory-carrying peer (or human migrating CLIs) the substantive context they're bringing into the swarph, paired with §15's contract half (handshake DM acknowledging the four invariants).
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Inspect what would be imported (lossy → honest framing)
|
|
105
|
+
$ swarph import ~/.claude/projects/.../X.jsonl --report-only
|
|
106
|
+
|
|
107
|
+
# Commit — writes ~/.swarph/sessions/<session-id>.jsonl
|
|
108
|
+
$ swarph import ~/.claude/projects/.../X.jsonl
|
|
109
|
+
|
|
110
|
+
# Refuse-with-error if target exists (protects continuation turns)
|
|
111
|
+
$ swarph import same-source.jsonl
|
|
112
|
+
swarph import: target /home/.../X.jsonl already exists (...)
|
|
113
|
+
To proceed:
|
|
114
|
+
--force overwrite (destroys continuation turns)
|
|
115
|
+
--target-session NAME write to a different file
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**What ports cleanly:** plain user/assistant/system text, role tags, conversation order.
|
|
119
|
+
|
|
120
|
+
**What's lossy** (counted in report, kept as visible text where possible):
|
|
121
|
+
- `thinking` blocks (Anthropic-specific reasoning trace)
|
|
122
|
+
- `tool_use` blocks (call shape doesn't port across providers)
|
|
123
|
+
- `tool_result` blocks (companion drop with `tool_use`)
|
|
124
|
+
|
|
125
|
+
**What's dropped:** attachments (would need re-upload), provider-side KV cache, conversation IDs, `cache_control` annotations.
|
|
126
|
+
|
|
127
|
+
Honest framing per PLAN.md §17.3: **teleport is "import + continue", not "freeze and resume"** — the first turn after import on a new provider pays cold-cache cost. Phase 5+ adds `--continue` for live REPL integration.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
$ swarph "say pong" --provider gemini
|
|
131
|
+
Pong!
|
|
132
|
+
# 3+26t $0.0000 0.73s caller=cli.oneshot.ubuntu provider=gemini
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `--json` mode semantics
|
|
136
|
+
|
|
137
|
+
`--json` is a **harness trigger**, not a strict-validation gate. When set, swarph routes the response through the swarph-mesh JSON harness:
|
|
138
|
+
|
|
139
|
+
- A permissive `{"type": "object"}` schema is synthesised when `--schema` is absent (Phase 5+ adds Pydantic validation).
|
|
140
|
+
- The harness retries once with `[USER]`-turn feedback on parse failure.
|
|
141
|
+
- **Malformed-JSON exits with code 1** + raw text on stdout for caller recovery. Useful for shell scripts:
|
|
142
|
+
```bash
|
|
143
|
+
if swarph "give me a trade" --json; then
|
|
144
|
+
# parsed dict was on stdout
|
|
145
|
+
...
|
|
146
|
+
fi
|
|
147
|
+
```
|
|
148
|
+
- Pretty-printed parsed dict on stdout when parse succeeds; `error_class=malformed_json` shows up in the stderr attribution footer when it doesn't.
|
|
149
|
+
|
|
150
|
+
## Spec
|
|
151
|
+
|
|
152
|
+
→ [hedge-fund-mcp / research/swarph_cli/PLAN.md](https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md)
|
|
153
|
+
|
|
154
|
+
## Phase rollout
|
|
155
|
+
|
|
156
|
+
| Phase | What lands |
|
|
157
|
+
|---|---|
|
|
158
|
+
| **0** | Scaffold — entry-point + status banner |
|
|
159
|
+
| **2** (v0.1.0) | One-shot mode: `swarph "hello" --provider gemini` |
|
|
160
|
+
| **2.5** (v0.2.0) | `swarph import` — Claude JSONL → swarph-native session format |
|
|
161
|
+
| **5** (v0.3.0) | `swarph chat` interactive REPL — multi-turn against any of five adapters + slash commands |
|
|
162
|
+
| **5.5** (v0.4.0 — this release) | **`swarph onboard <peer-name>` + `swarph ratify <peer-name>`** — six mechanics steps + handshake template + witness flip (PLAN.md §15) |
|
|
163
|
+
| **3** | `--ask <peer>` mesh-aware one-shot via MeshClient |
|
|
164
|
+
| **5.6** | `swarph daemon` foreground drain loop + REPL drain coroutine + `/inbox`, `/reply` (PLAN.md §16) |
|
|
165
|
+
| **6** | (already done) PyPI publish |
|
|
166
|
+
|
|
167
|
+
## Why split CLI from substrate
|
|
168
|
+
|
|
169
|
+
`swarph-mesh` (the library) is imported by `omega-boss`, Council judges, `lab-orchestrator`, and any future swarph peer that wants to write programs against the Protocol. Those callers don't need the CLI surface or the console-script entry point. Keeping the CLI in a separate repo means library users `pip install swarph-mesh` without pulling argparse + REPL plumbing they'll never run.
|
|
170
|
+
|
|
171
|
+
## Install (dev)
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
git clone https://github.com/darw007d/swarph-cli
|
|
175
|
+
cd swarph-cli
|
|
176
|
+
python -m venv venv && source venv/bin/activate
|
|
177
|
+
pip install -e ".[dev]"
|
|
178
|
+
pytest
|
|
179
|
+
swarph --version
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT. Pierre Samson + Claude Opus, 2026.
|
|
@@ -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. Phase 2 one-shot
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.4.0 ships Phase 5.5 `swarph onboard` + `swarph ratify` on top of Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL (PLAN.md §13 / §15)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -29,7 +29,9 @@ classifiers = [
|
|
|
29
29
|
"Topic :: Utilities",
|
|
30
30
|
]
|
|
31
31
|
dependencies = [
|
|
32
|
-
|
|
32
|
+
# Phase 5 REPL exercises all five adapters; bumped to 0.5.0 for
|
|
33
|
+
# OpenAI + Grok availability in /provider switch.
|
|
34
|
+
"swarph-mesh>=0.5.0",
|
|
33
35
|
"swarph-shared>=0.2.0",
|
|
34
36
|
]
|
|
35
37
|
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""``swarph chat`` — interactive REPL.
|
|
2
|
+
|
|
3
|
+
Phase 5 per PLAN.md §13. Multi-turn conversation against any of the
|
|
4
|
+
five swarph-mesh adapters (gemini / deepseek / claude / openai / grok).
|
|
5
|
+
Stdlib-only — uses ``readline`` for line editing + in-process history,
|
|
6
|
+
no third-party REPL framework.
|
|
7
|
+
|
|
8
|
+
Slash commands (typed as the entire input):
|
|
9
|
+
|
|
10
|
+
/help — print this list
|
|
11
|
+
/quit | /exit — exit (Ctrl-D also works)
|
|
12
|
+
/clear | /reset — clear conversation history (keeps system prompt)
|
|
13
|
+
/system [prompt] — show or set the system prompt; ``/system`` alone clears it
|
|
14
|
+
/provider <name> — switch provider (resets the conversation)
|
|
15
|
+
/model <name> — switch model
|
|
16
|
+
/history — print the running message list
|
|
17
|
+
/cost — print cumulative session cost + token totals
|
|
18
|
+
|
|
19
|
+
Out of scope per PLAN.md §13 (lands in 5.6):
|
|
20
|
+
/inbox /reply — require the inbox-drain coroutine
|
|
21
|
+
background drain — Phase 5.6 ``swarph daemon``
|
|
22
|
+
|
|
23
|
+
Streaming output is not wired here — every swarph-mesh adapter raises
|
|
24
|
+
``NotImplementedError`` on ``stream()`` as of v0.5.0; the REPL awaits
|
|
25
|
+
the full response and prints in one block. Token-by-token streaming
|
|
26
|
+
lands alongside the cross-adapter ``stream()`` work in v0.5+.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import argparse
|
|
32
|
+
import asyncio
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from typing import Optional
|
|
37
|
+
|
|
38
|
+
from swarph_cli.caller import default_caller
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_BANNER = """\
|
|
42
|
+
swarph chat — Phase 5 REPL
|
|
43
|
+
provider={provider} model={model} caller={caller}{system}
|
|
44
|
+
|
|
45
|
+
Type a message and press Enter to send. Slash commands:
|
|
46
|
+
/help /clear /system /provider /model /history /cost /quit
|
|
47
|
+
Ctrl-D to exit.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
_HELP = """\
|
|
51
|
+
Slash commands:
|
|
52
|
+
/help — show this list
|
|
53
|
+
/quit, /exit — exit (Ctrl-D works too)
|
|
54
|
+
/clear, /reset — clear conversation history (keeps system prompt)
|
|
55
|
+
/system [prompt] — show or set system prompt; bare /system clears it
|
|
56
|
+
/provider <name> — switch provider (clears history; rebinds adapter)
|
|
57
|
+
/model <name> — switch model
|
|
58
|
+
/history — print the running message list
|
|
59
|
+
/cost — print cumulative cost + token totals
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class ReplState:
|
|
65
|
+
"""In-memory REPL state. Lives for the duration of a single
|
|
66
|
+
``swarph chat`` process."""
|
|
67
|
+
|
|
68
|
+
provider: str
|
|
69
|
+
model: Optional[str]
|
|
70
|
+
caller: str
|
|
71
|
+
system_prompt: Optional[str]
|
|
72
|
+
temperature: float = 0.7
|
|
73
|
+
max_tokens: Optional[int] = None
|
|
74
|
+
messages: list = field(default_factory=list)
|
|
75
|
+
total_input_tokens: int = 0
|
|
76
|
+
total_output_tokens: int = 0
|
|
77
|
+
total_cost_usd: float = 0.0
|
|
78
|
+
turn_count: int = 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
82
|
+
p = argparse.ArgumentParser(
|
|
83
|
+
prog="swarph chat",
|
|
84
|
+
description=(
|
|
85
|
+
"Interactive REPL against any swarph-mesh adapter. "
|
|
86
|
+
"Phase 5 per PLAN.md §13."
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
p.add_argument(
|
|
90
|
+
"--provider",
|
|
91
|
+
default="gemini",
|
|
92
|
+
help='LLM provider (gemini / deepseek / claude / openai / grok).',
|
|
93
|
+
)
|
|
94
|
+
p.add_argument(
|
|
95
|
+
"--model",
|
|
96
|
+
default=None,
|
|
97
|
+
help="Provider-specific model id. Defaults to the adapter's default_model.",
|
|
98
|
+
)
|
|
99
|
+
p.add_argument(
|
|
100
|
+
"--caller",
|
|
101
|
+
default=None,
|
|
102
|
+
help='Caller-convention slug. Defaults to "cli.repl.<user>".',
|
|
103
|
+
)
|
|
104
|
+
p.add_argument(
|
|
105
|
+
"--system",
|
|
106
|
+
default=None,
|
|
107
|
+
help="System prompt prepended to every turn.",
|
|
108
|
+
)
|
|
109
|
+
p.add_argument(
|
|
110
|
+
"--temperature",
|
|
111
|
+
type=float,
|
|
112
|
+
default=0.7,
|
|
113
|
+
)
|
|
114
|
+
p.add_argument(
|
|
115
|
+
"--max-tokens",
|
|
116
|
+
type=int,
|
|
117
|
+
default=None,
|
|
118
|
+
)
|
|
119
|
+
return p
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _default_repl_caller() -> str:
|
|
123
|
+
"""``cli.repl.<user>`` — distinct from ``cli.oneshot.<user>`` so
|
|
124
|
+
attribution rows distinguish REPL turns from one-shot calls.
|
|
125
|
+
|
|
126
|
+
Falls back to the caller-convention default if env-derived user
|
|
127
|
+
extraction fails."""
|
|
128
|
+
user = os.environ.get("USER") or os.environ.get("LOGNAME")
|
|
129
|
+
if not user:
|
|
130
|
+
return default_caller()
|
|
131
|
+
user_slug = "".join(c if c.isalnum() else "_" for c in user.lower())
|
|
132
|
+
return f"cli.repl.{user_slug}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _print(msg: str = "", *, file=None) -> None:
|
|
136
|
+
"""Centralized print helper — tests monkeypatch this so they can
|
|
137
|
+
capture REPL output without poking sys.stdout."""
|
|
138
|
+
print(msg, file=file or sys.stdout, flush=True)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _read_line(prompt: str) -> str:
|
|
142
|
+
"""Single-line input. Tests monkeypatch this to inject scripted
|
|
143
|
+
input. Production uses stdlib ``input()`` — readline is auto-loaded
|
|
144
|
+
by import-time on POSIX, giving line editing + history for free."""
|
|
145
|
+
try:
|
|
146
|
+
import readline # noqa: F401 — side-effect-only on POSIX
|
|
147
|
+
except ImportError:
|
|
148
|
+
pass # Windows / minimal builds; raw input still works
|
|
149
|
+
return input(prompt)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _format_attribution(
|
|
153
|
+
*, input_tokens: int, output_tokens: int, cost_usd: float, duration_s: float, cached: bool
|
|
154
|
+
) -> str:
|
|
155
|
+
cost_str = f"${cost_usd:.4f}" if cost_usd > 0 else "$0"
|
|
156
|
+
line = (
|
|
157
|
+
f"# {input_tokens}+{output_tokens}t "
|
|
158
|
+
f"{cost_str} {duration_s:.2f}s"
|
|
159
|
+
)
|
|
160
|
+
if cached:
|
|
161
|
+
line += " (cached)"
|
|
162
|
+
return line
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _handle_slash(state: ReplState, line: str) -> str:
|
|
166
|
+
"""Apply a slash command to ``state``. Return one of:
|
|
167
|
+
|
|
168
|
+
- ``"continue"`` — keep the REPL running
|
|
169
|
+
- ``"quit"`` — exit the REPL with code 0
|
|
170
|
+
- ``"unknown"`` — unrecognized command (caller prints + continues)
|
|
171
|
+
"""
|
|
172
|
+
parts = line.split(maxsplit=1)
|
|
173
|
+
cmd = parts[0].lower()
|
|
174
|
+
arg = parts[1] if len(parts) > 1 else ""
|
|
175
|
+
|
|
176
|
+
if cmd in ("/quit", "/exit"):
|
|
177
|
+
return "quit"
|
|
178
|
+
|
|
179
|
+
if cmd == "/help":
|
|
180
|
+
_print(_HELP)
|
|
181
|
+
return "continue"
|
|
182
|
+
|
|
183
|
+
if cmd in ("/clear", "/reset"):
|
|
184
|
+
state.messages = []
|
|
185
|
+
_print("[cleared conversation history]")
|
|
186
|
+
return "continue"
|
|
187
|
+
|
|
188
|
+
if cmd == "/system":
|
|
189
|
+
if not arg:
|
|
190
|
+
if state.system_prompt:
|
|
191
|
+
_print(f"[system prompt cleared (was: {state.system_prompt!r})]")
|
|
192
|
+
state.system_prompt = None
|
|
193
|
+
else:
|
|
194
|
+
_print("[no system prompt set]")
|
|
195
|
+
else:
|
|
196
|
+
state.system_prompt = arg
|
|
197
|
+
_print(f"[system prompt set: {arg!r}]")
|
|
198
|
+
return "continue"
|
|
199
|
+
|
|
200
|
+
if cmd == "/provider":
|
|
201
|
+
if not arg:
|
|
202
|
+
_print(f"[current provider: {state.provider}]")
|
|
203
|
+
return "continue"
|
|
204
|
+
state.provider = arg
|
|
205
|
+
state.model = None # adapter's default_model picks up
|
|
206
|
+
state.messages = []
|
|
207
|
+
_print(
|
|
208
|
+
f"[switched to provider={arg}; model reset to adapter default; "
|
|
209
|
+
f"history cleared]"
|
|
210
|
+
)
|
|
211
|
+
return "continue"
|
|
212
|
+
|
|
213
|
+
if cmd == "/model":
|
|
214
|
+
if not arg:
|
|
215
|
+
_print(f"[current model: {state.model or '(adapter default)'}]")
|
|
216
|
+
return "continue"
|
|
217
|
+
state.model = arg
|
|
218
|
+
_print(f"[switched to model={arg}]")
|
|
219
|
+
return "continue"
|
|
220
|
+
|
|
221
|
+
if cmd == "/history":
|
|
222
|
+
if not state.messages:
|
|
223
|
+
_print("[no messages yet]")
|
|
224
|
+
return "continue"
|
|
225
|
+
for i, m in enumerate(state.messages):
|
|
226
|
+
_print(f" [{i}] {m.role}: {m.content[:200]}")
|
|
227
|
+
return "continue"
|
|
228
|
+
|
|
229
|
+
if cmd == "/cost":
|
|
230
|
+
cost_str = f"${state.total_cost_usd:.6f}" if state.total_cost_usd > 0 else "$0"
|
|
231
|
+
_print(
|
|
232
|
+
f"[turns={state.turn_count} "
|
|
233
|
+
f"in={state.total_input_tokens} out={state.total_output_tokens} "
|
|
234
|
+
f"cost={cost_str}]"
|
|
235
|
+
)
|
|
236
|
+
return "continue"
|
|
237
|
+
|
|
238
|
+
return "unknown"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def _send_turn(state: ReplState, user_text: str) -> int:
|
|
242
|
+
"""Send one user turn, append assistant reply on success. Returns
|
|
243
|
+
0 on success, non-zero on adapter error (REPL keeps running either
|
|
244
|
+
way; the return is for tests).
|
|
245
|
+
|
|
246
|
+
On adapter error the user turn is *not* appended to state.messages
|
|
247
|
+
so the user can retry the same input without doubling it up."""
|
|
248
|
+
# Local import — keeps the chat module importable in test contexts
|
|
249
|
+
# that don't have all five adapters installed.
|
|
250
|
+
from swarph_mesh import ChatMessage, SwarphCall
|
|
251
|
+
|
|
252
|
+
state.messages.append(ChatMessage(role="user", content=user_text))
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
sc = SwarphCall(
|
|
256
|
+
provider=state.provider,
|
|
257
|
+
caller=state.caller,
|
|
258
|
+
model=state.model,
|
|
259
|
+
)
|
|
260
|
+
resp = await sc.chat(
|
|
261
|
+
messages=state.messages,
|
|
262
|
+
system_prompt=state.system_prompt,
|
|
263
|
+
temperature=state.temperature,
|
|
264
|
+
max_tokens=state.max_tokens,
|
|
265
|
+
)
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
# Pop the failed user turn so retry doesn't compound it.
|
|
268
|
+
state.messages.pop()
|
|
269
|
+
_print(f"[error] {exc}", file=sys.stderr)
|
|
270
|
+
return 1
|
|
271
|
+
|
|
272
|
+
state.messages.append(ChatMessage(role="assistant", content=resp.text))
|
|
273
|
+
state.total_input_tokens += resp.input_tokens
|
|
274
|
+
state.total_output_tokens += resp.output_tokens
|
|
275
|
+
state.total_cost_usd += resp.cost_usd
|
|
276
|
+
state.turn_count += 1
|
|
277
|
+
|
|
278
|
+
_print(resp.text)
|
|
279
|
+
_print(
|
|
280
|
+
_format_attribution(
|
|
281
|
+
input_tokens=resp.input_tokens,
|
|
282
|
+
output_tokens=resp.output_tokens,
|
|
283
|
+
cost_usd=resp.cost_usd,
|
|
284
|
+
duration_s=resp.duration_s,
|
|
285
|
+
cached=resp.cached,
|
|
286
|
+
),
|
|
287
|
+
file=sys.stderr,
|
|
288
|
+
)
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def _repl_loop(state: ReplState) -> int:
|
|
293
|
+
"""Main REPL loop. Returns process exit code."""
|
|
294
|
+
sys_line = f" system={state.system_prompt!r}" if state.system_prompt else ""
|
|
295
|
+
_print(
|
|
296
|
+
_BANNER.format(
|
|
297
|
+
provider=state.provider,
|
|
298
|
+
model=state.model or "(adapter default)",
|
|
299
|
+
caller=state.caller,
|
|
300
|
+
system=sys_line,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
while True:
|
|
305
|
+
try:
|
|
306
|
+
line = _read_line("> ")
|
|
307
|
+
except EOFError:
|
|
308
|
+
_print()
|
|
309
|
+
_print("[swarph-chat] bye.")
|
|
310
|
+
return 0
|
|
311
|
+
except KeyboardInterrupt:
|
|
312
|
+
_print()
|
|
313
|
+
_print("[interrupted — type /quit to exit]")
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
line = line.rstrip()
|
|
317
|
+
if not line:
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
if line.startswith("/"):
|
|
321
|
+
result = _handle_slash(state, line)
|
|
322
|
+
if result == "quit":
|
|
323
|
+
_print("[swarph-chat] bye.")
|
|
324
|
+
return 0
|
|
325
|
+
if result == "unknown":
|
|
326
|
+
_print(f"[unknown command: {line.split()[0]!r} — try /help]")
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
await _send_turn(state, line)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def run_chat(argv: list[str]) -> int:
|
|
333
|
+
"""Entry point invoked by ``swarph_cli.main`` verb dispatch.
|
|
334
|
+
|
|
335
|
+
Returns process exit code."""
|
|
336
|
+
args = _build_parser().parse_args(argv)
|
|
337
|
+
caller = args.caller or _default_repl_caller()
|
|
338
|
+
|
|
339
|
+
state = ReplState(
|
|
340
|
+
provider=args.provider,
|
|
341
|
+
model=args.model,
|
|
342
|
+
caller=caller,
|
|
343
|
+
system_prompt=args.system,
|
|
344
|
+
temperature=args.temperature,
|
|
345
|
+
max_tokens=args.max_tokens,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return asyncio.run(_repl_loop(state))
|