Skip to content

Commit 5d7ee9e

Browse files
lzwjavaclaude
andcommitted
feat(main): integrate /proxy and /ca_bundle commands into REPL
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c68fe40 commit 5d7ee9e

4 files changed

Lines changed: 110 additions & 21 deletions

File tree

iclaw/completer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"/model_provider",
88
"/model",
99
"/search_provider",
10+
"/proxy",
11+
"/ca_bundle",
1012
"/copy",
1113
"/status",
1214
"/help",

iclaw/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def load_session_settings() -> dict:
3737

3838

3939
def save_session_settings(
40-
model_provider, current_model, search_provider, proxy=None, ca_bundle=None
40+
*, model_provider, current_model, search_provider, proxy=None, ca_bundle=None
4141
) -> None:
4242
config = _load_config()
4343
config["model_provider"] = model_provider

iclaw/main.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
from prompt_toolkit import PromptSession
88

9+
from iclaw import http
910
from iclaw.at_mention import resolve_at_mentions
1011
from iclaw.commands.model import handle_model_command, handle_model_provider_command
12+
from iclaw.commands.proxy import handle_ca_bundle_command, handle_proxy_command
1113
from iclaw.commands.search_provider import handle_search_provider_command
1214
from iclaw.commands.utils import handle_copy_command
1315
from iclaw.completer import IclawCompleter
@@ -28,6 +30,8 @@
2830
("/model_provider", "Select and authenticate with the model provider"),
2931
("/model", "Select specific model from your provider"),
3032
("/search_provider", "Select the web search provider"),
33+
("/proxy", "Set HTTP/HTTPS proxy (usage: /proxy [url|off])"),
34+
("/ca_bundle", "Set CA bundle for HTTPS (usage: /ca_bundle [path|off])"),
3135
("/copy", "Copy last Copilot response to clipboard"),
3236
("/status", "Show current settings"),
3337
("/help", "Show available commands"),
@@ -44,6 +48,9 @@ def main():
4448
model_provider = settings["model_provider"]
4549
current_model = settings["current_model"]
4650
search_provider = settings["search_provider"]
51+
proxy = settings["proxy"]
52+
ca_bundle = settings["ca_bundle"]
53+
http.reconfigure(proxy=proxy, ca_bundle=ca_bundle)
4754

4855
if github_token:
4956
print("Connecting to GitHub Copilot...")
@@ -91,20 +98,66 @@ def main():
9198
copilot_token = t
9299
github_token = load_github_token()
93100
token_expiry = time.monotonic() + TOKEN_REFRESH_INTERVAL
94-
save_session_settings(model_provider, current_model, search_provider)
101+
save_session_settings(
102+
model_provider=model_provider,
103+
current_model=current_model,
104+
search_provider=search_provider,
105+
proxy=proxy,
106+
ca_bundle=ca_bundle,
107+
)
95108
continue
96109
if user_input == "/model":
97110
current_model = handle_model_command(copilot_token, current_model)
98-
save_session_settings(model_provider, current_model, search_provider)
111+
save_session_settings(
112+
model_provider=model_provider,
113+
current_model=current_model,
114+
search_provider=search_provider,
115+
proxy=proxy,
116+
ca_bundle=ca_bundle,
117+
)
99118
continue
100119
if user_input == "/search_provider":
101120
search_provider = handle_search_provider_command(search_provider)
102-
save_session_settings(model_provider, current_model, search_provider)
121+
save_session_settings(
122+
model_provider=model_provider,
123+
current_model=current_model,
124+
search_provider=search_provider,
125+
proxy=proxy,
126+
ca_bundle=ca_bundle,
127+
)
128+
continue
129+
if user_input == "/proxy" or user_input.startswith("/proxy "):
130+
parts = user_input.split(maxsplit=1)
131+
arg = parts[1] if len(parts) > 1 else None
132+
proxy = handle_proxy_command(proxy, arg)
133+
http.reconfigure(proxy=proxy, ca_bundle=ca_bundle)
134+
save_session_settings(
135+
model_provider=model_provider,
136+
current_model=current_model,
137+
search_provider=search_provider,
138+
proxy=proxy,
139+
ca_bundle=ca_bundle,
140+
)
141+
continue
142+
if user_input == "/ca_bundle" or user_input.startswith("/ca_bundle "):
143+
parts = user_input.split(maxsplit=1)
144+
arg = parts[1] if len(parts) > 1 else None
145+
ca_bundle = handle_ca_bundle_command(ca_bundle, arg)
146+
http.reconfigure(proxy=proxy, ca_bundle=ca_bundle)
147+
save_session_settings(
148+
model_provider=model_provider,
149+
current_model=current_model,
150+
search_provider=search_provider,
151+
proxy=proxy,
152+
ca_bundle=ca_bundle,
153+
)
103154
continue
104155
if user_input == "/status":
105156
print(f" model_provider: {model_provider}")
106157
print(f" model: {current_model}")
107158
print(f" search_provider: {search_provider}")
159+
print(f" proxy: {proxy or '(not set)'}")
160+
print(f" ca_bundle: {ca_bundle or '(system default)'}")
108161
print(f" cwd: {os.getcwd()}")
109162
print()
110163
continue

tests/test_main.py

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ def _ps(*inputs):
2020

2121

2222
class TestMain(unittest.TestCase):
23+
@patch("iclaw.main.http")
2324
@patch("iclaw.main.chat")
2425
@patch("iclaw.main.load_github_token")
2526
@patch("iclaw.main.PromptSession")
26-
def test_main_cli(self, mock_ps, mock_load, mock_chat):
27+
def test_main_cli(self, mock_ps, mock_load, mock_chat, mock_http):
2728
mock_load.return_value = "gt"
2829
mock_ps.return_value = _mock_session(".exit")
2930
with (
@@ -50,95 +51,108 @@ def test_load_github_token_invalid_json(self):
5051
mp.read_text.return_value = "not json"
5152
self.assertIsNone(main.load_github_token())
5253

54+
@patch("iclaw.main.http")
5355
@patch("iclaw.main.load_github_token", return_value=None)
5456
@patch("iclaw.main.PromptSession")
55-
def test_main_no_token(self, mock_ps, mock_load):
57+
def test_main_no_token(self, mock_ps, mock_load, mock_http):
5658
mock_ps.return_value = _mock_session(".exit")
5759
with patch("sys.stdout"), patch("sys.stderr"):
5860
main.main()
5961

62+
@patch("iclaw.main.http")
6063
@patch("iclaw.main.load_github_token", return_value="gt")
6164
@patch("iclaw.main.get_copilot_token", side_effect=Exception("fail"))
6265
@patch("iclaw.main.PromptSession")
63-
def test_main_copilot_token_error(self, mock_ps, mock_cp, mock_load):
66+
def test_main_copilot_token_error(self, mock_ps, mock_cp, mock_load, mock_http):
6467
mock_ps.return_value = _mock_session(".exit")
6568
with patch("sys.stdout"), patch("sys.stderr"):
6669
main.main()
6770

71+
@patch("iclaw.main.http")
6872
@patch("iclaw.main.load_github_token", return_value="gt")
6973
@patch("iclaw.main.get_copilot_token", return_value="ct")
7074
@patch("iclaw.main.PromptSession")
71-
def test_main_empty_and_help(self, mock_ps, mock_cp, mock_load):
75+
def test_main_empty_and_help(self, mock_ps, mock_cp, mock_load, mock_http):
7276
mock_ps.return_value = _mock_session("", "/help", "/", ".exit")
7377
with patch("sys.stdout"), patch("iclaw.main.time.monotonic", return_value=0):
7478
main.main()
7579

80+
@patch("iclaw.main.http")
7681
@patch("iclaw.main.load_github_token", return_value="gt")
7782
@patch("iclaw.main.get_copilot_token", return_value="ct")
7883
@patch("iclaw.main.PromptSession")
79-
def test_main_eof(self, mock_ps, mock_cp, mock_load):
84+
def test_main_eof(self, mock_ps, mock_cp, mock_load, mock_http):
8085
mock_ps.return_value = _mock_session(EOFError())
8186
with patch("sys.stdout"), patch("iclaw.main.time.monotonic", return_value=0):
8287
main.main()
8388

89+
@patch("iclaw.main.http")
8490
@patch("iclaw.main.load_github_token", return_value=None)
8591
@patch("iclaw.main.PromptSession")
86-
def test_main_not_authenticated(self, mock_ps, mock_load):
92+
def test_main_not_authenticated(self, mock_ps, mock_load, mock_http):
8793
mock_ps.return_value = _mock_session("hello", ".exit")
8894
with patch("sys.stdout"), patch("sys.stderr"):
8995
main.main()
9096

97+
@patch("iclaw.main.http")
9198
@patch("iclaw.main.load_github_token", return_value="gt")
9299
@patch("iclaw.main.get_copilot_token", return_value="ct")
93100
@patch("iclaw.main.handle_copy_command")
94101
@patch("iclaw.main.PromptSession")
95-
def test_main_copy(self, mock_ps, mock_copy, mock_cp, mock_load):
102+
def test_main_copy(self, mock_ps, mock_copy, mock_cp, mock_load, mock_http):
96103
mock_ps.return_value = _mock_session("/copy", ".exit")
97104
with patch("sys.stdout"), patch("iclaw.main.time.monotonic", return_value=0):
98105
main.main()
99106
mock_copy.assert_called_once()
100107

108+
@patch("iclaw.main.http")
101109
@patch("iclaw.main.load_github_token", return_value="gt")
102110
@patch("iclaw.main.get_copilot_token", return_value="ct")
103111
@patch("iclaw.main.handle_model_provider_command", return_value=("copilot", None))
104112
@patch("iclaw.main.PromptSession")
105-
def test_main_model_provider(self, mock_ps, mock_mp, mock_cp, mock_load):
113+
def test_main_model_provider(self, mock_ps, mock_mp, mock_cp, mock_load, mock_http):
106114
mock_ps.return_value = _mock_session("/model_provider", ".exit")
107115
with patch("sys.stdout"), patch("iclaw.main.time.monotonic", return_value=0):
108116
main.main()
109117

118+
@patch("iclaw.main.http")
110119
@patch("iclaw.main.load_github_token", return_value="gt")
111120
@patch("iclaw.main.get_copilot_token", return_value="ct")
112121
@patch("iclaw.main.handle_model_command", return_value="gpt-4o")
113122
@patch("iclaw.main.PromptSession")
114-
def test_main_model(self, mock_ps, mock_mc, mock_cp, mock_load):
123+
def test_main_model(self, mock_ps, mock_mc, mock_cp, mock_load, mock_http):
115124
mock_ps.return_value = _mock_session("/model", ".exit")
116125
with patch("sys.stdout"), patch("iclaw.main.time.monotonic", return_value=0):
117126
main.main()
118127

128+
@patch("iclaw.main.http")
119129
@patch("iclaw.main.load_github_token", return_value="gt")
120130
@patch("iclaw.main.get_copilot_token", return_value="ct")
121131
@patch("iclaw.main.handle_search_provider_command", return_value="bing")
122132
@patch("iclaw.main.PromptSession")
123-
def test_main_search_provider(self, mock_ps, mock_sp, mock_cp, mock_load):
133+
def test_main_search_provider(
134+
self, mock_ps, mock_sp, mock_cp, mock_load, mock_http
135+
):
124136
mock_ps.return_value = _mock_session("/search_provider", ".exit")
125137
with patch("sys.stdout"), patch("iclaw.main.time.monotonic", return_value=0):
126138
main.main()
127139

140+
@patch("iclaw.main.http")
128141
@patch("iclaw.main.load_github_token", return_value="gt")
129142
@patch("iclaw.main.get_copilot_token", return_value="ct")
130143
@patch("iclaw.main.chat", return_value={"content": "hi"})
131144
@patch("iclaw.main.PromptSession")
132-
def test_main_chat(self, mock_ps, mock_chat, mock_cp, mock_load):
145+
def test_main_chat(self, mock_ps, mock_chat, mock_cp, mock_load, mock_http):
133146
mock_ps.return_value = _mock_session("hello", ".exit")
134147
with patch("sys.stdout"), patch("iclaw.main.time.monotonic", return_value=0):
135148
main.main()
136149

150+
@patch("iclaw.main.http")
137151
@patch("iclaw.main.load_github_token", return_value="gt")
138152
@patch("iclaw.main.get_copilot_token", return_value="ct")
139153
@patch("iclaw.main.chat", side_effect=Exception("API error"))
140154
@patch("iclaw.main.PromptSession")
141-
def test_main_chat_error(self, mock_ps, mock_chat, mock_cp, mock_load):
155+
def test_main_chat_error(self, mock_ps, mock_chat, mock_cp, mock_load, mock_http):
142156
mock_ps.return_value = _mock_session("hello", ".exit")
143157
with (
144158
patch("sys.stdout"),
@@ -147,13 +161,14 @@ def test_main_chat_error(self, mock_ps, mock_chat, mock_cp, mock_load):
147161
):
148162
main.main()
149163

164+
@patch("iclaw.main.http")
150165
@patch("iclaw.main.load_github_token", return_value="gt")
151166
@patch("iclaw.main.get_copilot_token", return_value="ct")
152167
@patch("iclaw.main.chat")
153168
@patch("iclaw.main.web_search", return_value="search results")
154169
@patch("iclaw.main.PromptSession")
155170
def test_main_tool_call_web_search(
156-
self, mock_ps, mock_ws, mock_chat, mock_cp, mock_load
171+
self, mock_ps, mock_ws, mock_chat, mock_cp, mock_load, mock_http
157172
):
158173
mock_ps.return_value = _mock_session("hello", ".exit")
159174
mock_chat.side_effect = [
@@ -175,13 +190,14 @@ def test_main_tool_call_web_search(
175190
main.main()
176191
mock_ws.assert_called_once()
177192

193+
@patch("iclaw.main.http")
178194
@patch("iclaw.main.load_github_token", return_value="gt")
179195
@patch("iclaw.main.get_copilot_token", return_value="ct")
180196
@patch("iclaw.main.chat")
181197
@patch("iclaw.main.exec", return_value="output")
182198
@patch("iclaw.main.PromptSession")
183199
def test_main_tool_call_exec(
184-
self, mock_ps, mock_exec, mock_chat, mock_cp, mock_load
200+
self, mock_ps, mock_exec, mock_chat, mock_cp, mock_load, mock_http
185201
):
186202
mock_ps.return_value = _mock_session("hello", ".exit")
187203
mock_chat.side_effect = [
@@ -202,11 +218,14 @@ def test_main_tool_call_exec(
202218
with patch("sys.stdout"), patch("iclaw.main.time.monotonic", return_value=0):
203219
main.main()
204220

221+
@patch("iclaw.main.http")
205222
@patch("iclaw.main.load_github_token", return_value="gt")
206223
@patch("iclaw.main.get_copilot_token", return_value="ct")
207224
@patch("iclaw.main.chat")
208225
@patch("iclaw.main.PromptSession")
209-
def test_main_tool_call_edit(self, mock_ps, mock_chat, mock_cp, mock_load):
226+
def test_main_tool_call_edit(
227+
self, mock_ps, mock_chat, mock_cp, mock_load, mock_http
228+
):
210229
mock_ps.return_value = _mock_session("hello", ".exit")
211230
mock_chat.side_effect = [
212231
{
@@ -236,11 +255,14 @@ def test_main_tool_call_edit(self, mock_ps, mock_chat, mock_cp, mock_load):
236255
if os.path.exists(tmp):
237256
os.remove(tmp)
238257

258+
@patch("iclaw.main.http")
239259
@patch("iclaw.main.load_github_token", return_value="gt")
240260
@patch("iclaw.main.get_copilot_token", return_value="ct")
241261
@patch("iclaw.main.chat", return_value={"content": "hi"})
242262
@patch("iclaw.main.PromptSession")
243-
def test_main_token_refresh(self, mock_ps, mock_chat, mock_cp, mock_load):
263+
def test_main_token_refresh(
264+
self, mock_ps, mock_chat, mock_cp, mock_load, mock_http
265+
):
244266
mock_ps.return_value = _mock_session("hello", ".exit")
245267
with (
246268
patch("sys.stdout"),
@@ -250,18 +272,30 @@ def test_main_token_refresh(self, mock_ps, mock_chat, mock_cp, mock_load):
250272
# get_copilot_token called twice: once at startup, once on refresh
251273
self.assertEqual(mock_cp.call_count, 2)
252274

275+
@patch("iclaw.main.http")
253276
@patch("iclaw.main.load_github_token", return_value="gt")
254277
@patch("iclaw.main.get_copilot_token", return_value="ct")
255278
@patch(
256279
"iclaw.main.handle_model_provider_command",
257280
return_value=("copilot", "new_token"),
258281
)
259282
@patch("iclaw.main.PromptSession")
260-
def test_main_model_provider_with_token(self, mock_ps, mock_mp, mock_cp, mock_load):
283+
def test_main_model_provider_with_token(
284+
self, mock_ps, mock_mp, mock_cp, mock_load, mock_http
285+
):
261286
mock_ps.return_value = _mock_session("/model_provider", ".exit")
262287
with patch("sys.stdout"), patch("iclaw.main.time.monotonic", return_value=0):
263288
main.main()
264289

290+
@patch("iclaw.main.http")
291+
@patch("iclaw.main.load_github_token", return_value="gt")
292+
@patch("iclaw.main.get_copilot_token", return_value="ct")
293+
@patch("iclaw.main.PromptSession")
294+
def test_main_status(self, mock_ps, mock_cp, mock_load, mock_http):
295+
mock_ps.return_value = _mock_session("/status", ".exit")
296+
with patch("sys.stdout"), patch("iclaw.main.time.monotonic", return_value=0):
297+
main.main()
298+
265299

266300
if __name__ == "__main__":
267301
unittest.main()

0 commit comments

Comments
 (0)