walla-cli 0.1.13__tar.gz → 0.1.14__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.
- {walla_cli-0.1.13 → walla_cli-0.1.14}/AGENTS.md +5 -2
- {walla_cli-0.1.13 → walla_cli-0.1.14}/PKG-INFO +16 -2
- {walla_cli-0.1.13 → walla_cli-0.1.14}/README.md +15 -1
- {walla_cli-0.1.13 → walla_cli-0.1.14}/docs/data_engineering_standards.md +3 -1
- walla_cli-0.1.14/incoming/teach/README.md +3 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/pyproject.toml +1 -1
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_cli_json.py +5 -1
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_core.py +2 -1
- walla_cli-0.1.14/tests/test_teach.py +73 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/__init__.py +1 -1
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/cli/cmd_ops.py +6 -1
- walla_cli-0.1.14/walla/cli/cmd_teach.py +53 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/cli/main.py +10 -1
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/core/instruct.py +13 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/core/path.py +10 -0
- walla_cli-0.1.14/walla/core/teach.py +138 -0
- walla_cli-0.1.14/walla/core/teach_pr.py +199 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/mcp/server.py +22 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/.cursor/rules/quality.mdc +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/.github/workflows/ci.yml +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/.gitignore +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/LICENSE +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/docs/WIRE.md +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/docs/assets/walla-demo-poster.jpg +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/docs/assets/walla-demo-preview.gif +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/docs/assets/walla-demo.mp4 +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/docs/assets/walla-github-banner.jpg +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/docs/code_quality.md +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/docs/mcp.md +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/docs/objective_function.md +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/docs/rate_limit_analysis.md +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/fixtures/categories.json +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/fixtures/edit_item_request.json +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/fixtures/item_detail.json +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/fixtures/offer_buyer_request.json +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/fixtures/search_bicicleta.json +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/fixtures/sell_item_request.json +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/fixtures/sell_suggest_draft.json +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/fixtures/wire_contracts.json +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/scripts/check_code_quality.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/scripts/dogfood_run.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/scripts/probe_rate.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/skills/walla/SKILL.md +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/skills/walla-buy/SKILL.md +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/skills/walla-sell/SKILL.md +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_account.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_account_more.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_catch.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_categories_find.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_chat_open.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_desk.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_edges.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_edit.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_export.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_http_more.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_live.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_negotiate.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_parsers.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_polite.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_regressions.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_search_api.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_sell.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_session_store.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_shortlist.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_suggest_watch.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_verdict.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/tests/test_wire_contracts.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/uv.lock +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/__init__.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/actions.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/chat.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/cookies.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/desk.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/desk_watch.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/inbox.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/login.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/offer_wire.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/prompt.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/sell.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/sell_edit.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/sell_edit_wire.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/sell_photos.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/sell_steps.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/sell_suggest.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/sell_upload.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/sell_wire.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/account/session_store.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/cli/__init__.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/cli/banner.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/cli/catch.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/cli/cmd_account.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/cli/cmd_desk.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/cli/cmd_edit.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/cli/cmd_look.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/cli/cmd_sell.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/cli/console.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/core/__init__.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/core/envelope.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/core/errors.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/core/exceptions.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/core/human_fix.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/http/__init__.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/http/client.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/http/headers.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/http/polite.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/http/sign.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/hunter/__init__.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/hunter/converge.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/hunter/export.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/hunter/export_pdf.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/hunter/negotiate.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/hunter/profile_store.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/hunter/pursuits.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/hunter/shortlist.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/hunter/stack.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/hunter/verdict.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/hunter/watches.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/mcp/__init__.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/mcp/_entry.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/models/__init__.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/models/account.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/models/listing.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/models/profile.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/search/__init__.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/search/api.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/search/category_find.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/search/parse.py +0 -0
- {walla_cli-0.1.13 → walla_cli-0.1.14}/walla/search/pick_category.py +0 -0
|
@@ -67,5 +67,8 @@ Ranks: models 10 → exceptions 15 → http 20 → core 30 → search/account/hu
|
|
|
67
67
|
- **Don't** create listings without `Accept: application/vnd.upload-v2+json`
|
|
68
68
|
(HTTP 405). Use `walla sell` / `fixtures/sell_item_request.json`.
|
|
69
69
|
- **Don't** require `.env` credentials; login is cookie paste.
|
|
70
|
-
- **Don't**
|
|
71
|
-
|
|
70
|
+
- **Don't** nag about transcripts on search/desk/pursue. First-use notice once.
|
|
71
|
+
At 20/40/60 prompts ask yes/no, then A-E (gh/git/browser/hub/manual).
|
|
72
|
+
If they ask to upload a transcript, run `walla teach` immediately.
|
|
73
|
+
`walla teach FILE --method a --yes` opens a PR; on failure use
|
|
74
|
+
https://github.com/berendgort/wallapop-cli/compare and help them finish.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: walla-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.14
|
|
4
4
|
Summary: Wallapop.es CLI - look · offer · talk · sell for humans and agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/berendgort/wallapop-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/berendgort/wallapop-cli
|
|
@@ -198,7 +198,8 @@ walla doctor --json
|
|
|
198
198
|
| `walla negotiate` | Win-win Spanish draft (never sends) |
|
|
199
199
|
| `walla inbox` / `thread` / `say` | Talk (`say` needs `--yes`) |
|
|
200
200
|
| `walla offer` | Formal offer (needs `--yes`) |
|
|
201
|
-
| `walla sell` / `unsell` | Publish or delete
|
|
201
|
+
| `walla sell` / `unsell` / `edit` | Publish, edit, or delete (`--yes`) |
|
|
202
|
+
| `walla teach` | Park a redacted agent transcript locally |
|
|
202
203
|
| `walla fav list\|add\|rm` | Favorites |
|
|
203
204
|
| `walla instruct` / `doctor` | Agent recipe + health |
|
|
204
205
|
|
|
@@ -225,6 +226,19 @@ python scripts/check_code_quality.py
|
|
|
225
226
|
|
|
226
227
|
Read [`AGENTS.md`](AGENTS.md) before extending.
|
|
227
228
|
|
|
229
|
+
## Help improve walla
|
|
230
|
+
|
|
231
|
+
Long agent chats help. After you confirm, pick a PR method:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
walla teach ./cursor-export.md --method a --yes --json # A gh B git C browser D hub E manual
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Cookies/tokens are stripped first. If `gh`/`git`/`hub` is missing, open
|
|
238
|
+
https://github.com/berendgort/wallapop-cli/compare and file the PR by hand
|
|
239
|
+
(parked file is in `~/.config/walla/teach/`). Agents ask once on first use,
|
|
240
|
+
then at 20 / 40 / 60 prompts — never on every search.
|
|
241
|
+
|
|
228
242
|
## Disclaimer
|
|
229
243
|
|
|
230
244
|
Unofficial. Not affiliated with Wallapop. Personal / research use.
|
|
@@ -154,7 +154,8 @@ walla doctor --json
|
|
|
154
154
|
| `walla negotiate` | Win-win Spanish draft (never sends) |
|
|
155
155
|
| `walla inbox` / `thread` / `say` | Talk (`say` needs `--yes`) |
|
|
156
156
|
| `walla offer` | Formal offer (needs `--yes`) |
|
|
157
|
-
| `walla sell` / `unsell` | Publish or delete
|
|
157
|
+
| `walla sell` / `unsell` / `edit` | Publish, edit, or delete (`--yes`) |
|
|
158
|
+
| `walla teach` | Park a redacted agent transcript locally |
|
|
158
159
|
| `walla fav list\|add\|rm` | Favorites |
|
|
159
160
|
| `walla instruct` / `doctor` | Agent recipe + health |
|
|
160
161
|
|
|
@@ -181,6 +182,19 @@ python scripts/check_code_quality.py
|
|
|
181
182
|
|
|
182
183
|
Read [`AGENTS.md`](AGENTS.md) before extending.
|
|
183
184
|
|
|
185
|
+
## Help improve walla
|
|
186
|
+
|
|
187
|
+
Long agent chats help. After you confirm, pick a PR method:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
walla teach ./cursor-export.md --method a --yes --json # A gh B git C browser D hub E manual
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Cookies/tokens are stripped first. If `gh`/`git`/`hub` is missing, open
|
|
194
|
+
https://github.com/berendgort/wallapop-cli/compare and file the PR by hand
|
|
195
|
+
(parked file is in `~/.config/walla/teach/`). Agents ask once on first use,
|
|
196
|
+
then at 20 / 40 / 60 prompts — never on every search.
|
|
197
|
+
|
|
184
198
|
## Disclaimer
|
|
185
199
|
|
|
186
200
|
Unofficial. Not affiliated with Wallapop. Personal / research use.
|
|
@@ -15,8 +15,9 @@ Code shape: [`code_quality.md`](code_quality.md). Wire truth: [`WIRE.md`](WIRE.m
|
|
|
15
15
|
|
|
16
16
|
| Mode | What | Mutability | Clock |
|
|
17
17
|
|---|---|---|---|
|
|
18
|
-
| Working | `profile.json`, `watches.json`, `session.json` | mutable | user lifetime |
|
|
18
|
+
| Working | `profile.json`, `watches.json`, `session.json`, `teach.json` | mutable | user lifetime |
|
|
19
19
|
| Archive | redacted fixtures under `fixtures/` | append on capture | repo life |
|
|
20
|
+
| Teach drop | `~/.config/walla/teach/*.md` | append | user opt-in |
|
|
20
21
|
| Live | `api.wallapop.com` responses | read | request |
|
|
21
22
|
|
|
22
23
|
There is no database in v1. Do not stand up Postgres, Redis, or a second
|
|
@@ -37,6 +38,7 @@ file for the same fields is a defect.
|
|
|
37
38
|
- Profile: one home point, one search radius, one pickup radius.
|
|
38
39
|
- Watch: one keyword query + optional filters; seen listing ids are a set.
|
|
39
40
|
- Session: tokens / cookie fields only. Never the password.
|
|
41
|
+
- Teach: `teach.json` (first-notice clock) and one redacted file per `walla teach`.
|
|
40
42
|
- Fixture: one captured response shape, secrets redacted, `schema_version` in
|
|
41
43
|
the companion parser module docstring.
|
|
42
44
|
|
|
@@ -40,12 +40,16 @@ def test_rate_limit_fields() -> None:
|
|
|
40
40
|
assert p["retry_after_s"] == 2.0
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def test_instruct_json() -> None:
|
|
43
|
+
def test_instruct_json(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
44
|
+
monkeypatch.setenv("WALLA_CONFIG_DIR", str(tmp_path))
|
|
44
45
|
result = runner.invoke(app, ["instruct", "--json"])
|
|
45
46
|
assert result.exit_code == 0
|
|
46
47
|
data = json.loads(result.stdout)
|
|
47
48
|
assert data["ok"] is True
|
|
48
49
|
assert data["data"]["name"] == "walla"
|
|
50
|
+
assert data["data"]["teach"]["first_notice"]
|
|
51
|
+
again = runner.invoke(app, ["instruct", "--json"])
|
|
52
|
+
assert json.loads(again.stdout)["data"]["teach"]["first_notice"] is None
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
def test_doctor_never_echoes_secrets(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -14,7 +14,7 @@ from walla.core.exceptions import (
|
|
|
14
14
|
WallaParseError,
|
|
15
15
|
WallaUnsupportedError,
|
|
16
16
|
)
|
|
17
|
-
from walla.core.path import config_dir, profile_path
|
|
17
|
+
from walla.core.path import config_dir, profile_path, teach_dir
|
|
18
18
|
from walla.http.polite import reset_polite
|
|
19
19
|
from walla.hunter.profile_store import parse_intake
|
|
20
20
|
|
|
@@ -32,6 +32,7 @@ def test_paths(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
|
32
32
|
monkeypatch.setenv("WALLA_CONFIG_DIR", str(tmp_path))
|
|
33
33
|
assert config_dir() == tmp_path
|
|
34
34
|
assert profile_path() == tmp_path / "profile.json"
|
|
35
|
+
assert teach_dir() == tmp_path / "teach"
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def test_parse_intake(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Local transcript parking (no Wallapop wire)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from pytest import MonkeyPatch
|
|
9
|
+
|
|
10
|
+
from walla.core.teach import (
|
|
11
|
+
FIRST_NOTICE,
|
|
12
|
+
park_transcript,
|
|
13
|
+
redact_secrets,
|
|
14
|
+
should_ask_feedback,
|
|
15
|
+
teach_payload,
|
|
16
|
+
)
|
|
17
|
+
from walla.core.teach_pr import MANUAL_PR, plan_pr, ship_pr
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_redact_jwt_and_cookie() -> None:
|
|
21
|
+
blob, n = redact_secrets(
|
|
22
|
+
"cookie __Secure-next-auth.session-token=abc.def\n"
|
|
23
|
+
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.aaa.bbb"
|
|
24
|
+
)
|
|
25
|
+
assert n == 2
|
|
26
|
+
assert "[redacted]" in blob
|
|
27
|
+
assert "session-token=abc" not in blob
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_park_and_first_notice(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
|
|
31
|
+
monkeypatch.setenv("WALLA_CONFIG_DIR", str(tmp_path))
|
|
32
|
+
first = teach_payload()
|
|
33
|
+
assert first["first_notice"] == FIRST_NOTICE
|
|
34
|
+
second = teach_payload()
|
|
35
|
+
assert second["first_notice"] is None
|
|
36
|
+
parked = park_transcript(
|
|
37
|
+
"hello Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.aaa.bbb",
|
|
38
|
+
name="hunt",
|
|
39
|
+
)
|
|
40
|
+
path = Path(str(parked["path"]))
|
|
41
|
+
assert path.is_file()
|
|
42
|
+
assert path.parent == tmp_path / "teach"
|
|
43
|
+
assert parked["redacted"] >= 1
|
|
44
|
+
assert "eyJhbGci" not in path.read_text(encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_should_ask_every_twenty() -> None:
|
|
48
|
+
assert should_ask_feedback(19) is False
|
|
49
|
+
assert should_ask_feedback(20) is True
|
|
50
|
+
assert should_ask_feedback(21, last_asked_band=20) is False
|
|
51
|
+
assert should_ask_feedback(40, last_asked_band=20) is True
|
|
52
|
+
assert should_ask_feedback(40, last_asked_band=40) is False
|
|
53
|
+
assert should_ask_feedback(60, last_asked_band=40) is True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_pr_menu_and_draft(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
|
|
57
|
+
monkeypatch.setenv("WALLA_CONFIG_DIR", str(tmp_path))
|
|
58
|
+
payload = teach_payload()
|
|
59
|
+
ids = [m["id"] for m in payload["methods"]]
|
|
60
|
+
assert ids == ["A", "B", "C", "D", "E"]
|
|
61
|
+
assert payload["manual_pr"] == MANUAL_PR
|
|
62
|
+
draft = plan_pr("a")
|
|
63
|
+
assert draft["method"] == "a"
|
|
64
|
+
assert "github.com" in draft["manual_pr"]
|
|
65
|
+
parked = park_transcript("hello kite hunt", name="x")
|
|
66
|
+
pr = ship_pr(Path(str(parked["path"])), method="e", yes=False)
|
|
67
|
+
assert pr["status"] == "draft"
|
|
68
|
+
assert pr["manual_pr"] == MANUAL_PR
|
|
69
|
+
manual = ship_pr(Path(str(parked["path"])), method="E", yes=True)
|
|
70
|
+
assert manual["status"] == "manual"
|
|
71
|
+
assert manual["open"] == MANUAL_PR
|
|
72
|
+
with pytest.raises(ValueError, match="method"):
|
|
73
|
+
plan_pr("z")
|
|
@@ -10,6 +10,7 @@ from walla import __version__
|
|
|
10
10
|
from walla.account.session_store import load_session
|
|
11
11
|
from walla.cli.catch import run_cmd
|
|
12
12
|
from walla.core.instruct import instruct_recipe
|
|
13
|
+
from walla.core.teach import teach_payload
|
|
13
14
|
from walla.hunter.profile_store import load_profile, parse_intake, save_profile
|
|
14
15
|
|
|
15
16
|
__all__ = ("register",)
|
|
@@ -37,7 +38,7 @@ def register(app: typer.Typer) -> None:
|
|
|
37
38
|
except Exception: # noqa: BLE001
|
|
38
39
|
reachable = False
|
|
39
40
|
has_session = bool(sess and (sess.access_token or sess.session_cookie))
|
|
40
|
-
|
|
41
|
+
out: dict[str, Any] = {
|
|
41
42
|
"version": __version__,
|
|
42
43
|
"network": {"reachable": reachable},
|
|
43
44
|
"auth": {
|
|
@@ -46,6 +47,10 @@ def register(app: typer.Typer) -> None:
|
|
|
46
47
|
},
|
|
47
48
|
"profile": {"ready": profile.ready, "label": profile.label},
|
|
48
49
|
}
|
|
50
|
+
notice = teach_payload()
|
|
51
|
+
if notice.get("first_notice"):
|
|
52
|
+
out["teach"] = notice
|
|
53
|
+
return out
|
|
49
54
|
|
|
50
55
|
run_cmd(_run, as_json=json)
|
|
51
56
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Park a redacted agent transcript locally."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from walla.cli.catch import run_cmd
|
|
11
|
+
from walla.core.teach import park_transcript
|
|
12
|
+
from walla.core.teach_pr import ship_pr
|
|
13
|
+
|
|
14
|
+
__all__ = ("register",)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register(app: typer.Typer) -> None:
|
|
18
|
+
@app.command("teach")
|
|
19
|
+
def teach_cmd(
|
|
20
|
+
source: Path | None = typer.Argument(
|
|
21
|
+
None,
|
|
22
|
+
help="Transcript file, or - for stdin",
|
|
23
|
+
),
|
|
24
|
+
name: str | None = typer.Option(None, "--name", help="Filename stem"),
|
|
25
|
+
method: str | None = typer.Option(
|
|
26
|
+
None,
|
|
27
|
+
"--method",
|
|
28
|
+
help="PR path: a=gh b=git c=browser d=hub e=manual",
|
|
29
|
+
),
|
|
30
|
+
yes: bool = typer.Option(False, "--yes", help="Open/push the PR for real"),
|
|
31
|
+
json: bool = typer.Option(False, "--json"),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Park a redacted transcript; --method a-e opens a GitHub PR."""
|
|
34
|
+
|
|
35
|
+
def _run() -> dict[str, Any]:
|
|
36
|
+
if source is None:
|
|
37
|
+
raise ValueError("Pass a file, or pipe stdin: walla teach - < chat.md")
|
|
38
|
+
if str(source) == "-":
|
|
39
|
+
text = typer.get_text_stream("stdin").read()
|
|
40
|
+
stem = name or "stdin"
|
|
41
|
+
else:
|
|
42
|
+
path = source.expanduser()
|
|
43
|
+
if not path.is_file():
|
|
44
|
+
raise ValueError(f"transcript not found: {path}")
|
|
45
|
+
text = path.read_text(encoding="utf-8")
|
|
46
|
+
stem = name or path.stem
|
|
47
|
+
parked = park_transcript(text, name=stem)
|
|
48
|
+
out: dict[str, Any] = {"status": "parked", **parked}
|
|
49
|
+
if method:
|
|
50
|
+
out["pr"] = ship_pr(Path(str(parked["path"])), method=method, yes=yes)
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
run_cmd(_run, as_json=json)
|
|
@@ -7,7 +7,15 @@ from typing import Any
|
|
|
7
7
|
import typer
|
|
8
8
|
from typer.core import TyperGroup
|
|
9
9
|
|
|
10
|
-
from walla.cli import
|
|
10
|
+
from walla.cli import (
|
|
11
|
+
cmd_account,
|
|
12
|
+
cmd_desk,
|
|
13
|
+
cmd_edit,
|
|
14
|
+
cmd_look,
|
|
15
|
+
cmd_ops,
|
|
16
|
+
cmd_sell,
|
|
17
|
+
cmd_teach,
|
|
18
|
+
)
|
|
11
19
|
from walla.cli.banner import print_banner
|
|
12
20
|
|
|
13
21
|
__all__ = ("app", "cli")
|
|
@@ -37,6 +45,7 @@ cmd_look.register(app)
|
|
|
37
45
|
cmd_account.register(app)
|
|
38
46
|
cmd_sell.register(app)
|
|
39
47
|
cmd_edit.register(app)
|
|
48
|
+
cmd_teach.register(app)
|
|
40
49
|
cmd_desk.register(app)
|
|
41
50
|
|
|
42
51
|
|
|
@@ -6,6 +6,7 @@ import json
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from walla.core.path import profile_path
|
|
9
|
+
from walla.core.teach import teach_payload
|
|
9
10
|
from walla.models.profile import Profile
|
|
10
11
|
|
|
11
12
|
__all__ = ("instruct_recipe",)
|
|
@@ -140,6 +141,10 @@ def instruct_recipe() -> dict[str, Any]:
|
|
|
140
141
|
"walla edit <item_id> --eur N [--desc …] [--ship/--no-ship] --yes",
|
|
141
142
|
"walla unsell <item_id> --yes",
|
|
142
143
|
],
|
|
144
|
+
"teach": [
|
|
145
|
+
"walla teach <export.md> --json",
|
|
146
|
+
"walla teach <export.md> --method a --yes --json",
|
|
147
|
+
],
|
|
143
148
|
},
|
|
144
149
|
"rules": [
|
|
145
150
|
"Always present Wallapop shortlists ranked best-first with markdown item links.",
|
|
@@ -162,7 +167,15 @@ def instruct_recipe() -> dict[str, Any]:
|
|
|
162
167
|
"Negotiate drafts are win-win: respectful Spanish, no lowballs, no pressure.",
|
|
163
168
|
"never_pay: walla stops before payment; human completes in the app.",
|
|
164
169
|
"No .env credentials. Login is cookie paste only (optional --password).",
|
|
170
|
+
"If the human asks to upload/save a transcript via the CLI, do it "
|
|
171
|
+
"now: write the chat to a file, walla teach FILE --json, then "
|
|
172
|
+
"print A-E and wait. Do not wait for the 20-prompt cadence.",
|
|
173
|
+
"Count user prompts. At 20, 40, 60... ask once to upload a redacted "
|
|
174
|
+
"transcript. If they agree, print A-E (gh/git/browser/hub/manual) "
|
|
175
|
+
"and wait for a letter. Assume the tool is on PATH; if it is not, "
|
|
176
|
+
"open data.teach.manual_pr and help them file the PR by hand.",
|
|
165
177
|
],
|
|
178
|
+
"teach": teach_payload(),
|
|
166
179
|
"envelope": ["ok", "api_version", "data | error / error_type / retryable"],
|
|
167
180
|
"standards": [
|
|
168
181
|
"docs/code_quality.md",
|
|
@@ -11,6 +11,8 @@ __all__ = (
|
|
|
11
11
|
"profile_path",
|
|
12
12
|
"pursuits_path",
|
|
13
13
|
"session_path",
|
|
14
|
+
"teach_dir",
|
|
15
|
+
"teach_state_path",
|
|
14
16
|
"watches_path",
|
|
15
17
|
)
|
|
16
18
|
|
|
@@ -40,3 +42,11 @@ def last_search_path() -> Path:
|
|
|
40
42
|
|
|
41
43
|
def pursuits_path() -> Path:
|
|
42
44
|
return config_dir() / "pursuits.json"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def teach_dir() -> Path:
|
|
48
|
+
return config_dir() / "teach"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def teach_state_path() -> Path:
|
|
52
|
+
return config_dir() / "teach.json"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Park agent transcripts locally (no Wallapop I/O)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from walla.core.path import teach_dir, teach_state_path
|
|
11
|
+
from walla.core.teach_pr import MANUAL_PR, method_menu
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
"ASK_EVERY",
|
|
15
|
+
"FIRST_NOTICE",
|
|
16
|
+
"ask_copy",
|
|
17
|
+
"consume_first_notice",
|
|
18
|
+
"park_transcript",
|
|
19
|
+
"redact_secrets",
|
|
20
|
+
"should_ask_feedback",
|
|
21
|
+
"teach_payload",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
ASK_EVERY = 20
|
|
25
|
+
|
|
26
|
+
FIRST_NOTICE = (
|
|
27
|
+
"Long agent chats (especially hunts) help improve walla. After you confirm, "
|
|
28
|
+
"the agent can open a PR with walla teach --method A-E (gh/git/browser/hub/"
|
|
29
|
+
"manual). Tools are assumed on PATH; otherwise open "
|
|
30
|
+
f"{MANUAL_PR} yourself."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_SECRET = re.compile(
|
|
34
|
+
r"(?i)("
|
|
35
|
+
r"(?:__secure-next-auth\.session-token|session-token|bearer|refresh_token)"
|
|
36
|
+
r"['\"=\s:]+[^\s'\"]+"
|
|
37
|
+
r"|eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+"
|
|
38
|
+
r")"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def redact_secrets(text: str) -> tuple[str, int]:
|
|
43
|
+
"""Replace session cookies, bearers, and JWTs. Returns text, hit count."""
|
|
44
|
+
hits = 0
|
|
45
|
+
|
|
46
|
+
def _sub(match: re.Match[str]) -> str:
|
|
47
|
+
nonlocal hits
|
|
48
|
+
hits += 1
|
|
49
|
+
return "[redacted]"
|
|
50
|
+
|
|
51
|
+
return _SECRET.sub(_sub, text), hits
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def park_transcript(text: str, *, name: str | None = None) -> dict[str, Any]:
|
|
55
|
+
"""Write a redacted copy under ~/.config/walla/teach/."""
|
|
56
|
+
body, n_redacted = redact_secrets(text)
|
|
57
|
+
if not body.strip():
|
|
58
|
+
raise ValueError("transcript is empty")
|
|
59
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
60
|
+
safe = re.sub(r"[^a-zA-Z0-9._-]+", "-", (name or "chat").strip())[:40]
|
|
61
|
+
dest = teach_dir() / f"{stamp}-{safe or 'chat'}.md"
|
|
62
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
dest.write_text(body, encoding="utf-8")
|
|
64
|
+
return {
|
|
65
|
+
"path": str(dest),
|
|
66
|
+
"bytes": dest.stat().st_size,
|
|
67
|
+
"redacted": n_redacted,
|
|
68
|
+
"how": f"parked locally; PR via walla teach --method a --yes or {MANUAL_PR}",
|
|
69
|
+
"manual_pr": MANUAL_PR,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def should_ask_feedback(user_prompts: int, *, last_asked_band: int = 0) -> bool:
|
|
74
|
+
"""True at 20, 40, 60... once per band (count >= 20)."""
|
|
75
|
+
if user_prompts < ASK_EVERY:
|
|
76
|
+
return False
|
|
77
|
+
band = (user_prompts // ASK_EVERY) * ASK_EVERY
|
|
78
|
+
return band > last_asked_band
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def ask_copy(user_prompts: int) -> str:
|
|
82
|
+
return (
|
|
83
|
+
f"This chat is {user_prompts} prompts in. Upload a redacted transcript "
|
|
84
|
+
"to improve walla? If yes, reply with A B C D or E (gh / git / browser "
|
|
85
|
+
f"/ hub / I'll do it). Manual PR: {MANUAL_PR}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _load_state() -> dict[str, Any]:
|
|
90
|
+
path = teach_state_path()
|
|
91
|
+
if not path.is_file():
|
|
92
|
+
return {"schema_version": 1, "first_notice_at": None}
|
|
93
|
+
try:
|
|
94
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
95
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
96
|
+
return {"schema_version": 1, "first_notice_at": None}
|
|
97
|
+
return raw if isinstance(raw, dict) else {"schema_version": 1}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _save_state(state: dict[str, Any]) -> None:
|
|
101
|
+
path = teach_state_path()
|
|
102
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
path.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def consume_first_notice() -> str | None:
|
|
107
|
+
"""Return the first-use blurb once, then persist that it was shown."""
|
|
108
|
+
state = _load_state()
|
|
109
|
+
if state.get("first_notice_at"):
|
|
110
|
+
return None
|
|
111
|
+
state["first_notice_at"] = datetime.now(timezone.utc).isoformat()
|
|
112
|
+
_save_state(state)
|
|
113
|
+
return FIRST_NOTICE
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def teach_payload() -> dict[str, Any]:
|
|
117
|
+
"""Instruct/doctor field. Never attach this to search/desk envelopes."""
|
|
118
|
+
notice = consume_first_notice()
|
|
119
|
+
return {
|
|
120
|
+
"dir": str(teach_dir()),
|
|
121
|
+
"how": "walla teach <export.md> --method a --yes --json",
|
|
122
|
+
"ask_every": ASK_EVERY,
|
|
123
|
+
"first_notice": notice,
|
|
124
|
+
"confirm": "Ask yes/no before any upload. Never dump the transcript unasked.",
|
|
125
|
+
"methods": method_menu(),
|
|
126
|
+
"manual_pr": MANUAL_PR,
|
|
127
|
+
"ask_rule": (
|
|
128
|
+
"Count user prompts. At 20, 40, 60... ask once to upload. "
|
|
129
|
+
"If yes, print A-E and wait for a letter. Assume gh/git/hub/"
|
|
130
|
+
"browser is installed; on failure open data.teach.manual_pr "
|
|
131
|
+
"and walk them through a manual PR. Never on search/desk."
|
|
132
|
+
),
|
|
133
|
+
"ask_copy": ask_copy(ASK_EVERY),
|
|
134
|
+
"cli": (
|
|
135
|
+
"Save the chat to a file, then: "
|
|
136
|
+
"walla teach FILE --method a|b|c|d|e --yes --json"
|
|
137
|
+
),
|
|
138
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Open a GitHub PR for a parked transcript (gh / git / hub / browser / manual)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import webbrowser
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"MANUAL_PR",
|
|
13
|
+
"METHODS",
|
|
14
|
+
"REPO",
|
|
15
|
+
"method_menu",
|
|
16
|
+
"plan_pr",
|
|
17
|
+
"ship_pr",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
REPO = "berendgort/wallapop-cli"
|
|
21
|
+
MANUAL_PR = f"https://github.com/{REPO}/compare"
|
|
22
|
+
UPLOAD = f"https://github.com/{REPO}/upload/main/incoming/teach"
|
|
23
|
+
|
|
24
|
+
METHODS: dict[str, dict[str, str]] = {
|
|
25
|
+
"a": {
|
|
26
|
+
"bin": "gh",
|
|
27
|
+
"label": "GitHub CLI (gh fork/push/pr create)",
|
|
28
|
+
},
|
|
29
|
+
"b": {
|
|
30
|
+
"bin": "git",
|
|
31
|
+
"label": "git branch, commit, push, then open the compare URL",
|
|
32
|
+
},
|
|
33
|
+
"c": {
|
|
34
|
+
"bin": "browser",
|
|
35
|
+
"label": "Open GitHub in the browser (upload + compare links)",
|
|
36
|
+
},
|
|
37
|
+
"d": {
|
|
38
|
+
"bin": "hub",
|
|
39
|
+
"label": "hub pull-request (legacy GitHub CLI)",
|
|
40
|
+
},
|
|
41
|
+
"e": {
|
|
42
|
+
"bin": "manual",
|
|
43
|
+
"label": "I will open the PR myself (print the link + file path)",
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def method_menu() -> list[dict[str, str]]:
|
|
49
|
+
return [
|
|
50
|
+
{"id": key.upper(), "key": key, **row} for key, row in METHODS.items()
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _which(name: str) -> str | None:
|
|
55
|
+
if name in {"browser", "manual"}:
|
|
56
|
+
return name
|
|
57
|
+
return shutil.which(name)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def plan_pr(method: str) -> dict[str, Any]:
|
|
61
|
+
key = method.strip().lower()
|
|
62
|
+
if key not in METHODS:
|
|
63
|
+
raise ValueError("method must be A B C D or E")
|
|
64
|
+
spec = METHODS[key]
|
|
65
|
+
binary = spec["bin"]
|
|
66
|
+
found = _which(binary)
|
|
67
|
+
return {
|
|
68
|
+
"method": key,
|
|
69
|
+
"label": spec["label"],
|
|
70
|
+
"assume_on_path": binary,
|
|
71
|
+
"found": bool(found),
|
|
72
|
+
"manual_pr": MANUAL_PR,
|
|
73
|
+
"upload": UPLOAD,
|
|
74
|
+
"hint": (
|
|
75
|
+
f"Assumes `{binary}` is installed. If not, open {MANUAL_PR} "
|
|
76
|
+
"and create the PR by hand (or pick E)."
|
|
77
|
+
),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def ship_pr(parked: Path, *, method: str, yes: bool) -> dict[str, Any]:
|
|
82
|
+
"""Copy into incoming/teach and open a PR. Needs --yes to mutate git/gh."""
|
|
83
|
+
plan = plan_pr(method)
|
|
84
|
+
plan["parked"] = str(parked)
|
|
85
|
+
if not yes:
|
|
86
|
+
plan["status"] = "draft"
|
|
87
|
+
plan["next"] = (
|
|
88
|
+
f"Reply with a letter, then: walla teach {parked} "
|
|
89
|
+
f"--method {plan['method']} --yes --json"
|
|
90
|
+
)
|
|
91
|
+
return plan
|
|
92
|
+
key = str(plan["method"])
|
|
93
|
+
if key == "e" or not plan["found"]:
|
|
94
|
+
plan["status"] = "manual"
|
|
95
|
+
plan["open"] = MANUAL_PR
|
|
96
|
+
return plan
|
|
97
|
+
if key == "c":
|
|
98
|
+
webbrowser.open(UPLOAD)
|
|
99
|
+
webbrowser.open(MANUAL_PR)
|
|
100
|
+
plan["status"] = "opened_browser"
|
|
101
|
+
plan["open"] = MANUAL_PR
|
|
102
|
+
return plan
|
|
103
|
+
if key == "a":
|
|
104
|
+
return _ship_gh(parked, plan)
|
|
105
|
+
if key == "d":
|
|
106
|
+
return _ship_hub(parked, plan)
|
|
107
|
+
return _ship_git(parked, plan)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _run(cmd: list[str], *, cwd: Path | None = None) -> tuple[int, str, str]:
|
|
111
|
+
proc = subprocess.run(
|
|
112
|
+
cmd,
|
|
113
|
+
cwd=str(cwd) if cwd else None,
|
|
114
|
+
capture_output=True,
|
|
115
|
+
text=True,
|
|
116
|
+
timeout=120,
|
|
117
|
+
check=False,
|
|
118
|
+
)
|
|
119
|
+
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _ship_gh(parked: Path, plan: dict[str, Any]) -> dict[str, Any]:
|
|
123
|
+
code, out, err = _run(
|
|
124
|
+
[
|
|
125
|
+
"gh",
|
|
126
|
+
"pr",
|
|
127
|
+
"create",
|
|
128
|
+
"--repo",
|
|
129
|
+
REPO,
|
|
130
|
+
"--title",
|
|
131
|
+
f"teach: {parked.name}",
|
|
132
|
+
"--body",
|
|
133
|
+
f"Redacted agent transcript from `walla teach`.\n\nFile: `{parked}`",
|
|
134
|
+
]
|
|
135
|
+
)
|
|
136
|
+
blob = f"{out}\n{err}"
|
|
137
|
+
url = next((ln for ln in blob.splitlines() if "github.com" in ln), "")
|
|
138
|
+
plan["status"] = "ok" if code == 0 else "failed"
|
|
139
|
+
plan["open"] = url or MANUAL_PR
|
|
140
|
+
if code != 0:
|
|
141
|
+
plan["error"] = (err or out or "gh pr create failed")[:300]
|
|
142
|
+
plan["assist"] = (
|
|
143
|
+
"gh is missing a fork/branch. Agent: help install/auth gh, or "
|
|
144
|
+
f"open {MANUAL_PR} and attach {parked}."
|
|
145
|
+
)
|
|
146
|
+
return plan
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _ship_hub(parked: Path, plan: dict[str, Any]) -> dict[str, Any]:
|
|
150
|
+
code, out, err = _run(["hub", "pull-request", "-m", f"teach: {parked.name}"])
|
|
151
|
+
blob = f"{out}\n{err}"
|
|
152
|
+
url = next((ln for ln in blob.splitlines() if "github.com" in ln), "")
|
|
153
|
+
plan["status"] = "ok" if code == 0 else "failed"
|
|
154
|
+
plan["open"] = url or MANUAL_PR
|
|
155
|
+
if code != 0:
|
|
156
|
+
plan["error"] = (err or out or "hub pull-request failed")[:300]
|
|
157
|
+
plan["assist"] = f"hub failed. Open {MANUAL_PR} or switch to A (gh)."
|
|
158
|
+
return plan
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _ship_git(parked: Path, plan: dict[str, Any]) -> dict[str, Any]:
|
|
162
|
+
root = _git_root()
|
|
163
|
+
if root is None:
|
|
164
|
+
plan["status"] = "manual"
|
|
165
|
+
plan["open"] = MANUAL_PR
|
|
166
|
+
plan["assist"] = (
|
|
167
|
+
"Not inside a wallapop-cli clone. Open the compare link or "
|
|
168
|
+
"clone the repo, copy the parked file into incoming/teach/, "
|
|
169
|
+
"commit, push, then open the PR URL."
|
|
170
|
+
)
|
|
171
|
+
return plan
|
|
172
|
+
dest_dir = root / "incoming" / "teach"
|
|
173
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
dest = dest_dir / parked.name
|
|
175
|
+
dest.write_text(parked.read_text(encoding="utf-8"), encoding="utf-8")
|
|
176
|
+
branch = f"teach/{parked.stem}"[:60]
|
|
177
|
+
for cmd in (
|
|
178
|
+
["git", "checkout", "-b", branch],
|
|
179
|
+
["git", "add", str(dest)],
|
|
180
|
+
["git", "commit", "-m", f"teach: park {parked.name}"],
|
|
181
|
+
["git", "push", "-u", "origin", "HEAD"],
|
|
182
|
+
):
|
|
183
|
+
code, out, err = _run(cmd, cwd=root)
|
|
184
|
+
if code != 0 and "already exists" not in (err + out):
|
|
185
|
+
plan["status"] = "failed"
|
|
186
|
+
plan["open"] = MANUAL_PR
|
|
187
|
+
plan["error"] = (err or out or "git failed")[:300]
|
|
188
|
+
plan["assist"] = f"Finish by hand: {MANUAL_PR}"
|
|
189
|
+
return plan
|
|
190
|
+
plan["status"] = "pushed"
|
|
191
|
+
plan["open"] = f"https://github.com/{REPO}/compare/main...{branch}?expand=1"
|
|
192
|
+
return plan
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _git_root() -> Path | None:
|
|
196
|
+
code, out, _err = _run(["git", "rev-parse", "--show-toplevel"])
|
|
197
|
+
if code != 0 or not out:
|
|
198
|
+
return None
|
|
199
|
+
return Path(out)
|
|
@@ -183,4 +183,26 @@ def build_server() -> Any:
|
|
|
183
183
|
def me() -> dict[str, Any]:
|
|
184
184
|
return _wrap(whoami)
|
|
185
185
|
|
|
186
|
+
@mcp.tool()
|
|
187
|
+
def teach(
|
|
188
|
+
text: str,
|
|
189
|
+
name: str | None = None,
|
|
190
|
+
method: str | None = None,
|
|
191
|
+
confirm: bool = False,
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
"""Park a redacted transcript; with method+confirm, open a GitHub PR."""
|
|
194
|
+
from pathlib import Path
|
|
195
|
+
|
|
196
|
+
from walla.core.teach import park_transcript
|
|
197
|
+
from walla.core.teach_pr import ship_pr
|
|
198
|
+
|
|
199
|
+
def _run() -> dict[str, Any]:
|
|
200
|
+
parked = park_transcript(text, name=name)
|
|
201
|
+
if not method:
|
|
202
|
+
return parked
|
|
203
|
+
pr = ship_pr(Path(str(parked["path"])), method=method, yes=confirm)
|
|
204
|
+
return {**parked, "pr": pr}
|
|
205
|
+
|
|
206
|
+
return _wrap(_run)
|
|
207
|
+
|
|
186
208
|
return mcp
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|