Skip to content

Commit dfa7bf7

Browse files
committed
feat: migrate config from JSON to YAML
- Replace json with pyyaml (pyyaml>=6.0) in config.py and auth.py - Default config path: ~/.config/iclaw/config.json → config.yaml - Update all tests (test_config, test_main, integration_tests/test_repl) - Update docs (README, CLAUDE.md, specs, plans)
1 parent 477add7 commit dfa7bf7

12 files changed

Lines changed: 53 additions & 32 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ GITHUB_TOKEN_INTEGRATION=<token> python3 -m unittest discover integration_tests
4242
## Architecture
4343

4444
**Authentication flow:**
45-
1. `iclaw-login` runs GitHub OAuth Device Flow → saves `github_token` to `~/.config/iclaw/config.json`
45+
1. `iclaw-login` runs GitHub OAuth Device Flow → saves `github_token` to `~/.config/iclaw/config.yaml`
4646
2. `iclaw` startup reads the token via `iclaw/config.py:load_github_token()`, then exchanges it for a short-lived Copilot token (`iclaw/github_api.py:get_copilot_token()`)
4747
3. The Copilot token is refreshed every `TOKEN_REFRESH_INTERVAL` seconds (24 min) during the session
4848

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ pip install -e .
4646
```
4747
/provider_model
4848
```
49-
- **Copilot**: select option 1, follow the GitHub device authorization flow. Your token is saved to `~/.config/iclaw/config.json`.
50-
- **OpenRouter**: select option 2. iclaw reads `OPENROUTER_API_KEY` from the environment, or prompts for a key and saves it to `~/.config/iclaw/config.json`.
49+
- **Copilot**: select option 1, follow the GitHub device authorization flow. Your token is saved to `~/.config/iclaw/config.yaml`.
50+
- **OpenRouter**: select option 2. iclaw reads `OPENROUTER_API_KEY` from the environment, or prompts for a key and saves it to `~/.config/iclaw/config.yaml`.
5151

5252
### CLI Commands
5353
- `/provider_model`: Select and authenticate with the model provider.

README_CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pip install -e .
3434
```
3535
/provider_model
3636
```
37-
选择 `copilot`,然后按照 GitHub 设备授权流程操作。令牌将保存到 `~/.config/iclaw/config.json`
37+
选择 `copilot`,然后按照 GitHub 设备授权流程操作。令牌将保存到 `~/.config/iclaw/config.yaml`
3838

3939
### 终端命令
4040
- `/provider_model`: 选择并认证模型提供商。

docs/superpowers/plans/2026-03-29-log-command.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
**Goal:** Add a `/log` command with `verbose`/`info` levels to control REPL output verbosity.
66

7-
**Architecture:** Central `log.py` module with `log_info()`/`log_verbose()` functions. All `print()` calls in tool modules replaced with these. Level persisted to config.json.
7+
**Architecture:** Central `log.py` module with `log_info()`/`log_verbose()` functions. All `print()` calls in tool modules replaced with these. Level persisted to config.yaml.
88

99
**Tech Stack:** Python 3, unittest, prompt_toolkit (existing)
1010

docs/superpowers/specs/2026-03-29-log-command-design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ New module providing centralized log output control.
2222
- `/log verbose` — sets level to VERBOSE
2323
- `/log info` — sets level to INFO
2424
- `/log <invalid>` — prints "Unknown log level: <invalid>. Use 'verbose' or 'info'."
25-
- Persisted to `config.json` as `"log_level": "verbose"` or `"log_level": "info"`
25+
- Persisted to `config.yaml` as `log_level: verbose` or `log_level: info`
2626
- Added to `COMMANDS` list in `completer.py` and `COMMANDS_HELP` in `main.py`
2727

2828
## Call Site Changes

docs/superpowers/specs/2026-03-29-proxy-ca-bundle-design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Add `/proxy` and `/ca_bundle` REPL commands that persist settings to config and
1212

1313
### Configuration (`config.py`)
1414

15-
Two new keys in `~/.config/iclaw/config.json`:
15+
Two new keys in `~/.config/iclaw/config.yaml`:
1616

1717
- `"proxy"` — string URL (e.g., `"http://127.0.0.1:7890"`) or absent/null when disabled
1818
- `"ca_bundle"` — string file path (e.g., `"/path/to/ca-cert.pem"`) or absent/null for system default

iclaw/commands/auth.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import json
21
import sys
32
from datetime import datetime, timezone
3+
4+
import yaml
5+
46
from iclaw.login import get_device_code, poll_for_access_token
57

68

@@ -36,7 +38,9 @@ def handle_login_command(config_path):
3638
"created_at": datetime.now(timezone.utc).isoformat(),
3739
}
3840
config_path.parent.mkdir(parents=True, exist_ok=True)
39-
config_path.write_text(json.dumps(config, indent=2))
41+
config_path.write_text(
42+
yaml.dump(config, default_flow_style=False, sort_keys=False)
43+
)
4044
print(f"\nSaved GitHub token to {config_path}")
4145
return github_token
4246

iclaw/config.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import json
21
import os
32
from pathlib import Path
43

5-
_default_config = Path.home() / ".config" / "iclaw" / "config.json"
4+
import yaml
5+
6+
_default_config = Path.home() / ".config" / "iclaw" / "config.yaml"
67
CONFIG_PATH = Path(os.environ.get("ICLAW_CONFIG_PATH", str(_default_config)))
78
TOKEN_REFRESH_INTERVAL = 24 * 60 # seconds
89

@@ -11,14 +12,15 @@ def _load_config() -> dict:
1112
if not CONFIG_PATH.exists():
1213
return {}
1314
try:
14-
return json.loads(CONFIG_PATH.read_text())
15-
except json.JSONDecodeError:
15+
data = yaml.safe_load(CONFIG_PATH.read_text())
16+
return data if isinstance(data, dict) else {}
17+
except yaml.YAMLError:
1618
return {}
1719

1820

1921
def _save_config(config: dict) -> None:
2022
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
21-
CONFIG_PATH.write_text(json.dumps(config, indent=2))
23+
CONFIG_PATH.write_text(yaml.dump(config, default_flow_style=False, sort_keys=False))
2224

2325

2426
def load_github_token():

integration_tests/test_repl.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import tempfile
1919
import threading
2020
import time
21+
22+
import yaml
2123
import unittest
2224
from http.server import BaseHTTPRequestHandler, HTTPServer
2325

@@ -96,11 +98,11 @@ def stop(self):
9698

9799

98100
def _make_config_with_token(token="fake-github-token"):
99-
"""Write a config.json to a temp file and return its path."""
101+
"""Write a config.yaml to a temp file and return its path."""
100102
f = tempfile.NamedTemporaryFile(
101-
mode="w", suffix=".json", delete=False, prefix="iclaw_config_"
103+
mode="w", suffix=".yaml", delete=False, prefix="iclaw_config_"
102104
)
103-
json.dump({"github_token": token}, f)
105+
yaml.dump({"github_token": token}, f, default_flow_style=False, sort_keys=False)
104106
f.close()
105107
return f.name
106108

@@ -116,7 +118,7 @@ def _make_env(config_path=None, port=None):
116118
env = os.environ.copy()
117119
env.pop("PYTHONPATH", None)
118120
# Point iclaw at the temp config (or a nonexistent path for no-token tests).
119-
env["ICLAW_CONFIG_PATH"] = config_path or "/tmp/iclaw_no_such_config.json"
121+
env["ICLAW_CONFIG_PATH"] = config_path or "/tmp/iclaw_no_such_config.yaml"
120122
if port:
121123
env["ICLAW_GITHUB_API_BASE"] = f"http://127.0.0.1:{port}"
122124
env["ICLAW_COPILOT_API_BASE"] = f"http://127.0.0.1:{port}"
@@ -186,7 +188,7 @@ def _send(process, text):
186188

187189

188190
class TestReplNoToken(unittest.TestCase):
189-
"""REPL behaviour when no config.json exists."""
191+
"""REPL behaviour when no config.yaml exists."""
190192

191193
def setUp(self):
192194
self.process = _start_repl(_make_env()) # no config_path → no token

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ version = "0.1.0"
88
description = "Interactive CLI REPL for chatting with GitHub Copilot"
99
readme = "README.md"
1010
requires-python = ">=3.8"
11-
dependencies = ["requests>=2.32.0", "pyperclip>=1.8.0", "beautifulsoup4>=4.12.0", "readability-lxml>=0.8.1", "lxml>=5.0.0", "tavily-python>=0.3.0", "prompt-toolkit>=3.0.0", "playwright>=1.40.0", "rich>=13.0.0"]
11+
dependencies = ["requests>=2.32.0", "pyperclip>=1.8.0", "beautifulsoup4>=4.12.0", "readability-lxml>=0.8.1", "lxml>=5.0.0", "tavily-python>=0.3.0", "prompt-toolkit>=3.0.0", "playwright>=1.40.0", "rich>=13.0.0", "pyyaml>=6.0"]
1212

1313
[project.scripts]
1414
iclaw = "iclaw.main:main"

0 commit comments

Comments
 (0)