augint-shell 0.71.2__tar.gz → 0.72.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.
- {augint_shell-0.71.2 → augint_shell-0.72.0}/PKG-INFO +10 -8
- {augint_shell-0.71.2 → augint_shell-0.72.0}/README.md +9 -7
- {augint_shell-0.71.2 → augint_shell-0.72.0}/pyproject.toml +1 -1
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/__init__.py +1 -1
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/cli/commands/tools.py +253 -168
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/config.py +1 -1
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/interactive.py +43 -16
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/local_chrome.py +130 -54
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/tmux.py +1 -1
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/cli/__init__.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/cli/__main__.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/cli/commands/__init__.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/cli/commands/llm.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/cli/commands/manage.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/container.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/defaults.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/exceptions.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/gpu.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/scaffold.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/selector.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/__init__.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/ai-shell.toml +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/ai-shell.yaml +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/aider/__init__.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/aider/aider.conf.yml +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/aider/aiderignore +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/claude/__init__.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/claude/settings.json +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/codex/__init__.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/codex/config.toml +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/opencode/__init__.py +0 -0
- {augint_shell-0.71.2 → augint_shell-0.72.0}/src/ai_shell/templates/opencode/opencode.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: augint-shell
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.72.0
|
|
4
4
|
Summary: Launch AI coding tools and local LLMs in Docker containers
|
|
5
5
|
Author: svange
|
|
6
6
|
Requires-Dist: docker>=7.0.0
|
|
@@ -132,7 +132,7 @@ The dev container cannot open a browser on the Windows host, which blocks OAuth
|
|
|
132
132
|
What you get:
|
|
133
133
|
|
|
134
134
|
- Claude drives Chrome tabs on your Windows desktop, visible in real time.
|
|
135
|
-
- Uses a **separate Chrome profile**
|
|
135
|
+
- Uses a **separate Chrome profile per project** -- your normal browsing is untouched, and each repo keeps its own logged-in state.
|
|
136
136
|
- No Chrome extension. No third-party service. All traffic stays on `localhost` between the container and the host.
|
|
137
137
|
|
|
138
138
|
### How it works
|
|
@@ -142,9 +142,10 @@ ai-shell claude --local-chrome
|
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
`ai-shell` automatically:
|
|
145
|
-
1.
|
|
146
|
-
2.
|
|
147
|
-
3.
|
|
145
|
+
1. Computes a stable debug port and Chrome profile for the current project.
|
|
146
|
+
2. Reuses that project's Chrome if it is already running.
|
|
147
|
+
3. If not, **launches Chrome** for the project on its assigned port with its own profile under `%LOCALAPPDATA%\Google\Chrome\ai-shell\...`.
|
|
148
|
+
4. Starts a TCP proxy inside the container and injects `chrome-devtools-mcp` as an MCP server for Claude.
|
|
148
149
|
|
|
149
150
|
No manual setup required. Chrome stays open after Claude exits so your login sessions persist. Sign in to whatever accounts you need in that Chrome window (first time only; cookies persist in the profile).
|
|
150
151
|
|
|
@@ -161,16 +162,17 @@ Or set the environment variable: `AI_SHELL_LOCAL_CHROME=1`.
|
|
|
161
162
|
|
|
162
163
|
### Manual Chrome launch (fallback)
|
|
163
164
|
|
|
164
|
-
If `ai-shell` can't find `chrome.exe` automatically, launch Chrome yourself:
|
|
165
|
+
If `ai-shell` can't find `chrome.exe` automatically, launch Chrome yourself using the project-specific port and profile path that `ai-shell` prints in the error message. The shape of the command is:
|
|
165
166
|
|
|
166
167
|
```
|
|
167
|
-
chrome.exe --remote-debugging-port
|
|
168
|
+
chrome.exe --remote-debugging-port=<project-port> --remote-debugging-address=127.0.0.1 --remote-allow-origins=* --user-data-dir="%LOCALAPPDATA%\Google\Chrome\ai-shell\<project-slug>"
|
|
168
169
|
```
|
|
169
170
|
|
|
170
171
|
### Troubleshooting
|
|
171
172
|
|
|
172
173
|
- **"Chrome could not be found or launched"** -- Chrome is not installed at a standard location. Use the manual launch command above, or set the path in your system PATH.
|
|
173
|
-
- **Tabs appear empty / not logged in** -- Sign in to the accounts you need inside the auto-launched Chrome
|
|
174
|
+
- **Tabs appear empty / not logged in** -- Sign in to the accounts you need inside the auto-launched Chrome for that project. Cookies persist in that project's profile across sessions.
|
|
175
|
+
- **A different repo opened the wrong Chrome window** -- Each project now gets its own Chrome profile and debug port. Re-run from the correct repo so `ai-shell` attaches to that repo's browser instance.
|
|
174
176
|
- **Firefox / Safari** -- not supported. `chrome-devtools-mcp` requires a Chromium-based browser. Edge works with the same flags but has not been tested here.
|
|
175
177
|
|
|
176
178
|
## Standardization architecture
|
|
@@ -119,7 +119,7 @@ The dev container cannot open a browser on the Windows host, which blocks OAuth
|
|
|
119
119
|
What you get:
|
|
120
120
|
|
|
121
121
|
- Claude drives Chrome tabs on your Windows desktop, visible in real time.
|
|
122
|
-
- Uses a **separate Chrome profile**
|
|
122
|
+
- Uses a **separate Chrome profile per project** -- your normal browsing is untouched, and each repo keeps its own logged-in state.
|
|
123
123
|
- No Chrome extension. No third-party service. All traffic stays on `localhost` between the container and the host.
|
|
124
124
|
|
|
125
125
|
### How it works
|
|
@@ -129,9 +129,10 @@ ai-shell claude --local-chrome
|
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
`ai-shell` automatically:
|
|
132
|
-
1.
|
|
133
|
-
2.
|
|
134
|
-
3.
|
|
132
|
+
1. Computes a stable debug port and Chrome profile for the current project.
|
|
133
|
+
2. Reuses that project's Chrome if it is already running.
|
|
134
|
+
3. If not, **launches Chrome** for the project on its assigned port with its own profile under `%LOCALAPPDATA%\Google\Chrome\ai-shell\...`.
|
|
135
|
+
4. Starts a TCP proxy inside the container and injects `chrome-devtools-mcp` as an MCP server for Claude.
|
|
135
136
|
|
|
136
137
|
No manual setup required. Chrome stays open after Claude exits so your login sessions persist. Sign in to whatever accounts you need in that Chrome window (first time only; cookies persist in the profile).
|
|
137
138
|
|
|
@@ -148,16 +149,17 @@ Or set the environment variable: `AI_SHELL_LOCAL_CHROME=1`.
|
|
|
148
149
|
|
|
149
150
|
### Manual Chrome launch (fallback)
|
|
150
151
|
|
|
151
|
-
If `ai-shell` can't find `chrome.exe` automatically, launch Chrome yourself:
|
|
152
|
+
If `ai-shell` can't find `chrome.exe` automatically, launch Chrome yourself using the project-specific port and profile path that `ai-shell` prints in the error message. The shape of the command is:
|
|
152
153
|
|
|
153
154
|
```
|
|
154
|
-
chrome.exe --remote-debugging-port
|
|
155
|
+
chrome.exe --remote-debugging-port=<project-port> --remote-debugging-address=127.0.0.1 --remote-allow-origins=* --user-data-dir="%LOCALAPPDATA%\Google\Chrome\ai-shell\<project-slug>"
|
|
155
156
|
```
|
|
156
157
|
|
|
157
158
|
### Troubleshooting
|
|
158
159
|
|
|
159
160
|
- **"Chrome could not be found or launched"** -- Chrome is not installed at a standard location. Use the manual launch command above, or set the path in your system PATH.
|
|
160
|
-
- **Tabs appear empty / not logged in** -- Sign in to the accounts you need inside the auto-launched Chrome
|
|
161
|
+
- **Tabs appear empty / not logged in** -- Sign in to the accounts you need inside the auto-launched Chrome for that project. Cookies persist in that project's profile across sessions.
|
|
162
|
+
- **A different repo opened the wrong Chrome window** -- Each project now gets its own Chrome profile and debug port. Re-run from the correct repo so `ai-shell` attaches to that repo's browser instance.
|
|
161
163
|
- **Firefox / Safari** -- not supported. `chrome-devtools-mcp` requires a Chromium-based browser. Edge works with the same flags but has not been tested here.
|
|
162
164
|
|
|
163
165
|
## Standardization architecture
|
|
@@ -34,6 +34,14 @@ def _generate_worktree_name() -> str:
|
|
|
34
34
|
return uuid.uuid4().hex[:8]
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _print_tmux_quick_start() -> None:
|
|
38
|
+
"""Print a short tmux quick-start before attaching."""
|
|
39
|
+
console.print("[dim]tmux: mouse click=focus drag=resize wheel=scroll[/dim]")
|
|
40
|
+
console.print(
|
|
41
|
+
"[dim] Ctrl-b o=pane c=tab Space=layout p/n=tab z=zoom d=detach &=kill-tab[/dim]"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
37
45
|
def _setup_worktree(container_name: str, container_project_dir: str, name: str) -> str:
|
|
38
46
|
"""Create a git worktree inside the container and return its absolute container path.
|
|
39
47
|
|
|
@@ -255,6 +263,130 @@ def _get_manager(
|
|
|
255
263
|
return manager, container_name, exec_env, config
|
|
256
264
|
|
|
257
265
|
|
|
266
|
+
def _bedrock_label(exec_env: dict[str, str]) -> str:
|
|
267
|
+
"""Return the user-facing Bedrock suffix for launch messages."""
|
|
268
|
+
profile_label = exec_env.get("AWS_PROFILE", "default")
|
|
269
|
+
region_label = exec_env.get("AWS_REGION", "us-east-1")
|
|
270
|
+
return f" via Bedrock (profile={profile_label}, region={region_label})"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _configure_local_chrome(
|
|
274
|
+
container_name: str,
|
|
275
|
+
*,
|
|
276
|
+
project_name: str,
|
|
277
|
+
project_dir: Path | str | None,
|
|
278
|
+
) -> tuple[list[str], str]:
|
|
279
|
+
"""Attach chrome-devtools-mcp to a host Chrome instance."""
|
|
280
|
+
from ai_shell.local_chrome import (
|
|
281
|
+
LocalChromeUnavailable,
|
|
282
|
+
ensure_host_chrome,
|
|
283
|
+
start_chrome_proxy,
|
|
284
|
+
write_mcp_config,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
console.print("[dim]Connecting to host Chrome...[/dim]")
|
|
288
|
+
try:
|
|
289
|
+
chrome_port = ensure_host_chrome(
|
|
290
|
+
container_name,
|
|
291
|
+
project_name=project_name,
|
|
292
|
+
project_dir=project_dir,
|
|
293
|
+
)
|
|
294
|
+
except LocalChromeUnavailable as exc:
|
|
295
|
+
raise click.ClickException(str(exc)) from exc
|
|
296
|
+
|
|
297
|
+
console.print(f"[dim]Chrome debug port {chrome_port} reachable.[/dim]")
|
|
298
|
+
|
|
299
|
+
# Start TCP proxy: localhost:<port> -> host.docker.internal:<port>
|
|
300
|
+
# Chrome rejects non-localhost Host headers, so the MCP server
|
|
301
|
+
# must connect via localhost.
|
|
302
|
+
start_chrome_proxy(container_name, chrome_port)
|
|
303
|
+
|
|
304
|
+
host_mcp_path = write_mcp_config(chrome_port)
|
|
305
|
+
container_mcp_path = "/etc/ai-shell/chrome-mcp.json"
|
|
306
|
+
_inject_mcp_config(container_name, str(host_mcp_path), container_mcp_path)
|
|
307
|
+
console.print("[dim]Chrome DevTools MCP attached.[/dim]")
|
|
308
|
+
return ["--mcp-config", container_mcp_path], container_mcp_path
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _launch_loaded_config_claude(
|
|
312
|
+
config: AiShellConfig,
|
|
313
|
+
*,
|
|
314
|
+
safe: bool,
|
|
315
|
+
use_aws: bool,
|
|
316
|
+
cli_profile: str | None,
|
|
317
|
+
extra_args: tuple[str, ...],
|
|
318
|
+
local_chrome: bool = False,
|
|
319
|
+
team_mode: bool = False,
|
|
320
|
+
worktree_name: str | None = None,
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Launch Claude for an already loaded project config."""
|
|
323
|
+
use_bedrock = use_aws or config.claude_provider == "aws"
|
|
324
|
+
manager = ContainerManager(config)
|
|
325
|
+
container_name = manager.ensure_dev_container()
|
|
326
|
+
exec_env = build_dev_environment(
|
|
327
|
+
config.extra_env,
|
|
328
|
+
config.project_dir,
|
|
329
|
+
project_name=config.project_name,
|
|
330
|
+
bedrock=use_bedrock,
|
|
331
|
+
aws_profile=config.ai_profile,
|
|
332
|
+
aws_region=config.aws_region,
|
|
333
|
+
bedrock_profile=cli_profile or config.bedrock_profile,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if team_mode:
|
|
337
|
+
exec_env = dict(exec_env)
|
|
338
|
+
exec_env["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] = "1"
|
|
339
|
+
|
|
340
|
+
bedrock_label = ""
|
|
341
|
+
if use_bedrock:
|
|
342
|
+
bedrock_label = _bedrock_label(exec_env)
|
|
343
|
+
console.print(
|
|
344
|
+
"Checking Bedrock access "
|
|
345
|
+
f"(profile={exec_env.get('AWS_PROFILE', 'default')}, "
|
|
346
|
+
f"region={exec_env.get('AWS_REGION', 'us-east-1')})..."
|
|
347
|
+
)
|
|
348
|
+
_check_bedrock_access(container_name, exec_env)
|
|
349
|
+
|
|
350
|
+
workdir: str | None = None
|
|
351
|
+
resolved_worktree_name = worktree_name
|
|
352
|
+
if resolved_worktree_name is not None:
|
|
353
|
+
if resolved_worktree_name == "":
|
|
354
|
+
resolved_worktree_name = _generate_worktree_name()
|
|
355
|
+
container_project_dir = f"/root/projects/{config.project_name}"
|
|
356
|
+
workdir = _setup_worktree(container_name, container_project_dir, resolved_worktree_name)
|
|
357
|
+
console.print(f"[dim]Worktree: {workdir} (branch: worktree-{resolved_worktree_name})[/dim]")
|
|
358
|
+
|
|
359
|
+
mcp_args: list[str] = []
|
|
360
|
+
if local_chrome:
|
|
361
|
+
mcp_args, _ = _configure_local_chrome(
|
|
362
|
+
container_name,
|
|
363
|
+
project_name=config.project_name,
|
|
364
|
+
project_dir=config.project_dir,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if safe:
|
|
368
|
+
cmd = ["claude", *mcp_args, *extra_args]
|
|
369
|
+
console.print(
|
|
370
|
+
f"[bold]Launching Claude Code (safe mode){bedrock_label} in {container_name}...[/bold]"
|
|
371
|
+
)
|
|
372
|
+
manager.exec_interactive(container_name, cmd, extra_env=exec_env, workdir=workdir)
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
cmd_continue = ["claude", "--dangerously-skip-permissions", "-c", *mcp_args, *extra_args]
|
|
376
|
+
console.print(f"[bold]Launching Claude Code{bedrock_label} in {container_name}...[/bold]")
|
|
377
|
+
exit_code, elapsed = manager.run_interactive(
|
|
378
|
+
container_name, cmd_continue, extra_env=exec_env, workdir=workdir
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if exit_code != 0 and elapsed < FAST_FAILURE_THRESHOLD:
|
|
382
|
+
console.print("[yellow]No prior conversation found, starting fresh...[/yellow]")
|
|
383
|
+
cmd_fresh = ["claude", "--dangerously-skip-permissions", *mcp_args, *extra_args]
|
|
384
|
+
manager.exec_interactive(container_name, cmd_fresh, extra_env=exec_env, workdir=workdir)
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
sys.exit(exit_code)
|
|
388
|
+
|
|
389
|
+
|
|
258
390
|
def _load_workspace_repos(workspace_yaml: Path) -> tuple[str, list[dict[str, Any]]]:
|
|
259
391
|
"""Parse workspace.yaml and return (workspace_name, repos_list).
|
|
260
392
|
|
|
@@ -273,20 +405,21 @@ def _load_workspace_repos(workspace_yaml: Path) -> tuple[str, list[dict[str, Any
|
|
|
273
405
|
return workspace_name, repos
|
|
274
406
|
|
|
275
407
|
|
|
276
|
-
def
|
|
408
|
+
def _launch_interactive(
|
|
277
409
|
ctx: click.Context,
|
|
278
410
|
*,
|
|
279
411
|
safe: bool,
|
|
280
412
|
use_aws: bool,
|
|
281
413
|
cli_profile: str | None,
|
|
282
414
|
extra_args: tuple[str, ...],
|
|
415
|
+
worktree_name: str | None = None,
|
|
283
416
|
) -> None:
|
|
284
|
-
"""Interactive
|
|
417
|
+
"""Interactive Claude launcher.
|
|
285
418
|
|
|
286
|
-
Walks the user through a guided wizard to configure
|
|
287
|
-
|
|
419
|
+
Walks the user through a guided wizard to configure panes, then launches
|
|
420
|
+
either a normal single session or a customised tmux session.
|
|
288
421
|
"""
|
|
289
|
-
from ai_shell.interactive import build_interactive_panes, run_interactive_wizard
|
|
422
|
+
from ai_shell.interactive import PaneType, build_interactive_panes, run_interactive_wizard
|
|
290
423
|
from ai_shell.tmux import (
|
|
291
424
|
TMUX_SESSION_PREFIX,
|
|
292
425
|
build_attach_command,
|
|
@@ -294,13 +427,83 @@ def _launch_interactive_multi(
|
|
|
294
427
|
build_tmux_commands,
|
|
295
428
|
)
|
|
296
429
|
|
|
297
|
-
# Load config early -- needed for
|
|
430
|
+
# Load config early -- needed for prompt defaults and any single-pane launch.
|
|
298
431
|
project = ctx.obj.get("project") if ctx.obj else None
|
|
299
432
|
config = load_config(project_override=project, project_dir=Path.cwd())
|
|
433
|
+
|
|
434
|
+
# Gather workspace repos if available.
|
|
435
|
+
workspace_yaml = Path.cwd() / "workspace.yaml"
|
|
436
|
+
workspace_repos = None
|
|
437
|
+
if workspace_yaml.exists():
|
|
438
|
+
_, workspace_repos = _load_workspace_repos(workspace_yaml)
|
|
439
|
+
|
|
440
|
+
# Run the wizard.
|
|
441
|
+
interactive_config = run_interactive_wizard(
|
|
442
|
+
project_name=config.project_name,
|
|
443
|
+
workspace_repos=workspace_repos,
|
|
444
|
+
default_windows=2,
|
|
445
|
+
default_shared_chrome=config.local_chrome is True,
|
|
446
|
+
)
|
|
447
|
+
if interactive_config is None:
|
|
448
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
if interactive_config.pane_count == 1:
|
|
452
|
+
choice = interactive_config.pane_choices[0]
|
|
453
|
+
|
|
454
|
+
if choice.pane_type == PaneType.BASH:
|
|
455
|
+
manager = ContainerManager(config)
|
|
456
|
+
container_name = manager.ensure_dev_container()
|
|
457
|
+
exec_env = build_dev_environment(
|
|
458
|
+
config.extra_env,
|
|
459
|
+
config.project_dir,
|
|
460
|
+
project_name=config.project_name,
|
|
461
|
+
aws_profile=config.ai_profile,
|
|
462
|
+
aws_region=config.aws_region,
|
|
463
|
+
bedrock_profile=cli_profile or config.bedrock_profile,
|
|
464
|
+
)
|
|
465
|
+
console.print(f"[bold]Launching Bash in {container_name}...[/bold]")
|
|
466
|
+
manager.exec_interactive(
|
|
467
|
+
container_name,
|
|
468
|
+
["/bin/bash"],
|
|
469
|
+
extra_env=exec_env,
|
|
470
|
+
workdir=f"/root/projects/{config.project_name}",
|
|
471
|
+
)
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
selected_dir = config.project_dir
|
|
475
|
+
target_label = config.project_name
|
|
476
|
+
if choice.pane_type == PaneType.WORKSPACE_REPO:
|
|
477
|
+
selected_dir = Path.cwd() / choice.repo_path
|
|
478
|
+
if not selected_dir.exists():
|
|
479
|
+
raise click.ClickException(
|
|
480
|
+
f"Repo directory not found: {selected_dir}\n"
|
|
481
|
+
" Run /ai-workspace-sync to clone workspace repos first."
|
|
482
|
+
)
|
|
483
|
+
target_label = choice.repo_name
|
|
484
|
+
|
|
485
|
+
console.print(
|
|
486
|
+
f"[dim]Single Claude pane selected -- launching standard session in "
|
|
487
|
+
f"{target_label}[/dim]"
|
|
488
|
+
)
|
|
489
|
+
selected_config = load_config(project_override=project, project_dir=selected_dir)
|
|
490
|
+
_launch_loaded_config_claude(
|
|
491
|
+
selected_config,
|
|
492
|
+
safe=safe,
|
|
493
|
+
use_aws=use_aws,
|
|
494
|
+
cli_profile=cli_profile,
|
|
495
|
+
extra_args=extra_args,
|
|
496
|
+
local_chrome=interactive_config.shared_chrome,
|
|
497
|
+
team_mode=interactive_config.team_mode,
|
|
498
|
+
worktree_name=worktree_name,
|
|
499
|
+
)
|
|
500
|
+
return
|
|
501
|
+
|
|
300
502
|
session_name = f"{TMUX_SESSION_PREFIX}-{config.project_name}"
|
|
301
503
|
container_name = dev_container_name(config.project_name, config.project_dir)
|
|
302
504
|
|
|
303
|
-
# Check for existing tmux session
|
|
505
|
+
# Check for existing tmux session after the user has actually chosen a
|
|
506
|
+
# multi-pane launch.
|
|
304
507
|
check_cmd = build_check_session_command(container_name, session_name)
|
|
305
508
|
has_session = subprocess.run(check_cmd, capture_output=True).returncode == 0
|
|
306
509
|
|
|
@@ -314,28 +517,13 @@ def _launch_interactive_multi(
|
|
|
314
517
|
if choice == "reconnect":
|
|
315
518
|
attach_cmd = build_attach_command(container_name, session_name)
|
|
316
519
|
logger.debug("tmux reattach: %s", " ".join(attach_cmd))
|
|
520
|
+
_print_tmux_quick_start()
|
|
317
521
|
sys.stdout.flush()
|
|
318
522
|
sys.stderr.flush()
|
|
319
523
|
attach = subprocess.run(attach_cmd)
|
|
320
524
|
sys.exit(attach.returncode)
|
|
321
|
-
|
|
525
|
+
if choice == "cancel":
|
|
322
526
|
return
|
|
323
|
-
# choice == "fresh": fall through to wizard
|
|
324
|
-
|
|
325
|
-
# Gather workspace repos if available.
|
|
326
|
-
workspace_yaml = Path.cwd() / "workspace.yaml"
|
|
327
|
-
workspace_repos = None
|
|
328
|
-
if workspace_yaml.exists():
|
|
329
|
-
_, workspace_repos = _load_workspace_repos(workspace_yaml)
|
|
330
|
-
|
|
331
|
-
# Run the wizard.
|
|
332
|
-
interactive_config = run_interactive_wizard(
|
|
333
|
-
project_name=config.project_name,
|
|
334
|
-
workspace_repos=workspace_repos,
|
|
335
|
-
)
|
|
336
|
-
if interactive_config is None:
|
|
337
|
-
console.print("[dim]Cancelled.[/dim]")
|
|
338
|
-
return
|
|
339
527
|
|
|
340
528
|
# Ensure container is running.
|
|
341
529
|
use_bedrock = use_aws or config.claude_provider == "aws"
|
|
@@ -357,25 +545,12 @@ def _launch_interactive_multi(
|
|
|
357
545
|
# Handle shared Chrome if requested.
|
|
358
546
|
mcp_config_path: str | None = None
|
|
359
547
|
if interactive_config.shared_chrome:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
write_mcp_config,
|
|
548
|
+
_, mcp_config_path = _configure_local_chrome(
|
|
549
|
+
container_name,
|
|
550
|
+
project_name=config.project_name,
|
|
551
|
+
project_dir=config.project_dir,
|
|
365
552
|
)
|
|
366
553
|
|
|
367
|
-
console.print("[dim]Connecting to host Chrome...[/dim]")
|
|
368
|
-
try:
|
|
369
|
-
chrome_port = ensure_host_chrome(container_name)
|
|
370
|
-
except LocalChromeUnavailable as exc:
|
|
371
|
-
raise click.ClickException(str(exc)) from exc
|
|
372
|
-
|
|
373
|
-
start_chrome_proxy(container_name, chrome_port)
|
|
374
|
-
host_mcp_path = write_mcp_config(chrome_port)
|
|
375
|
-
mcp_config_path = "/etc/ai-shell/chrome-mcp.json"
|
|
376
|
-
_inject_mcp_config(container_name, str(host_mcp_path), mcp_config_path)
|
|
377
|
-
console.print("[dim]Chrome DevTools MCP attached.[/dim]")
|
|
378
|
-
|
|
379
554
|
# Build pane specs.
|
|
380
555
|
container_project_root = f"/root/projects/{config.project_name}"
|
|
381
556
|
panes = build_interactive_panes(
|
|
@@ -401,6 +576,7 @@ def _launch_interactive_multi(
|
|
|
401
576
|
|
|
402
577
|
final_cmd = cmds[-1]
|
|
403
578
|
logger.debug("tmux attach: %s", " ".join(final_cmd))
|
|
579
|
+
_print_tmux_quick_start()
|
|
404
580
|
sys.stdout.flush()
|
|
405
581
|
sys.stderr.flush()
|
|
406
582
|
attach = subprocess.run(final_cmd)
|
|
@@ -571,6 +747,7 @@ def _launch_single_repo_multi(
|
|
|
571
747
|
|
|
572
748
|
final_cmd = cmds[-1]
|
|
573
749
|
logger.debug("tmux attach: %s", " ".join(final_cmd))
|
|
750
|
+
_print_tmux_quick_start()
|
|
574
751
|
sys.stdout.flush()
|
|
575
752
|
sys.stderr.flush()
|
|
576
753
|
attach = subprocess.run(final_cmd)
|
|
@@ -628,6 +805,7 @@ def _launch_multi(
|
|
|
628
805
|
if choice == "reconnect":
|
|
629
806
|
attach_cmd = build_attach_command(container_name, session_name)
|
|
630
807
|
logger.debug("tmux reattach: %s", " ".join(attach_cmd))
|
|
808
|
+
_print_tmux_quick_start()
|
|
631
809
|
sys.stdout.flush()
|
|
632
810
|
sys.stderr.flush()
|
|
633
811
|
attach = subprocess.run(attach_cmd)
|
|
@@ -686,41 +864,16 @@ def _launch_multi(
|
|
|
686
864
|
" Run /ai-workspace-sync to clone workspace repos first."
|
|
687
865
|
)
|
|
688
866
|
console.print(f"[dim]Single selection -- launching Claude in {sel.label}[/dim]")
|
|
689
|
-
# Re-derive config from the selected repo's directory
|
|
690
867
|
project = ctx.obj.get("project") if ctx.obj else None
|
|
691
868
|
config = load_config(project_override=project, project_dir=sel_path)
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
bedrock=use_bedrock,
|
|
700
|
-
aws_profile=config.ai_profile,
|
|
701
|
-
aws_region=config.aws_region,
|
|
702
|
-
bedrock_profile=cli_profile or config.bedrock_profile,
|
|
869
|
+
_launch_loaded_config_claude(
|
|
870
|
+
config,
|
|
871
|
+
safe=safe,
|
|
872
|
+
use_aws=use_aws,
|
|
873
|
+
cli_profile=cli_profile,
|
|
874
|
+
extra_args=extra_args,
|
|
875
|
+
worktree_name=worktree_name,
|
|
703
876
|
)
|
|
704
|
-
|
|
705
|
-
if use_bedrock:
|
|
706
|
-
_check_bedrock_access(container_name, exec_env)
|
|
707
|
-
|
|
708
|
-
workdir = f"/root/projects/{config.project_name}"
|
|
709
|
-
if safe:
|
|
710
|
-
cmd = ["claude", *extra_args]
|
|
711
|
-
manager.exec_interactive(container_name, cmd, extra_env=exec_env, workdir=workdir)
|
|
712
|
-
else:
|
|
713
|
-
cmd_c = ["claude", "--dangerously-skip-permissions", "-c", *extra_args]
|
|
714
|
-
exit_code, elapsed = manager.run_interactive(
|
|
715
|
-
container_name, cmd_c, extra_env=exec_env, workdir=workdir
|
|
716
|
-
)
|
|
717
|
-
if exit_code != 0 and elapsed < FAST_FAILURE_THRESHOLD:
|
|
718
|
-
cmd_fresh = ["claude", "--dangerously-skip-permissions", *extra_args]
|
|
719
|
-
manager.exec_interactive(
|
|
720
|
-
container_name, cmd_fresh, extra_env=exec_env, workdir=workdir
|
|
721
|
-
)
|
|
722
|
-
else:
|
|
723
|
-
sys.exit(exit_code)
|
|
724
877
|
return
|
|
725
878
|
|
|
726
879
|
# Multi selection (2-4 repos): validate dirs, build tmux session
|
|
@@ -801,6 +954,7 @@ def _launch_multi(
|
|
|
801
954
|
# Final command: interactive attach (replaces process)
|
|
802
955
|
final_cmd = cmds[-1]
|
|
803
956
|
logger.debug("tmux attach: %s", " ".join(final_cmd))
|
|
957
|
+
_print_tmux_quick_start()
|
|
804
958
|
sys.stdout.flush()
|
|
805
959
|
sys.stderr.flush()
|
|
806
960
|
attach = subprocess.run(final_cmd)
|
|
@@ -852,9 +1006,9 @@ def _launch_multi(
|
|
|
852
1006
|
is_flag=True,
|
|
853
1007
|
default=False,
|
|
854
1008
|
help=(
|
|
855
|
-
"Attach Chrome DevTools MCP to host Chrome
|
|
856
|
-
"
|
|
857
|
-
"
|
|
1009
|
+
"Attach Chrome DevTools MCP to a project-scoped host Chrome session. "
|
|
1010
|
+
"ai-shell launches or reuses a separate Windows Chrome profile for "
|
|
1011
|
+
"this repo and gives Claude browser control over those tabs."
|
|
858
1012
|
),
|
|
859
1013
|
)
|
|
860
1014
|
@click.option(
|
|
@@ -865,8 +1019,8 @@ def _launch_multi(
|
|
|
865
1019
|
default=False,
|
|
866
1020
|
help=(
|
|
867
1021
|
"Interactive multi-pane setup: walk through a guided menu to "
|
|
868
|
-
"configure
|
|
869
|
-
"
|
|
1022
|
+
"configure windows, teams mode, and shared Chrome before launch. "
|
|
1023
|
+
"A single Claude pane falls back to a normal session."
|
|
870
1024
|
),
|
|
871
1025
|
)
|
|
872
1026
|
@click.argument("extra_args", nargs=-1, type=click.UNPROCESSED)
|
|
@@ -887,8 +1041,6 @@ def claude(
|
|
|
887
1041
|
# Incompatibility checks
|
|
888
1042
|
if do_team and do_multi:
|
|
889
1043
|
raise click.ClickException("--team and --multi are incompatible (both manage tmux).")
|
|
890
|
-
if do_interactive and not do_multi:
|
|
891
|
-
raise click.ClickException("--interactive requires --multi.")
|
|
892
1044
|
if do_interactive and do_team:
|
|
893
1045
|
raise click.ClickException(
|
|
894
1046
|
"--interactive and --team are incompatible (interactive handles teams mode itself)."
|
|
@@ -899,16 +1051,18 @@ def claude(
|
|
|
899
1051
|
"(interactive handles Chrome setup itself)."
|
|
900
1052
|
)
|
|
901
1053
|
|
|
1054
|
+
if do_interactive:
|
|
1055
|
+
_launch_interactive(
|
|
1056
|
+
ctx,
|
|
1057
|
+
safe=safe,
|
|
1058
|
+
use_aws=use_aws,
|
|
1059
|
+
cli_profile=cli_profile,
|
|
1060
|
+
extra_args=extra_args,
|
|
1061
|
+
worktree_name=worktree_name,
|
|
1062
|
+
)
|
|
1063
|
+
return
|
|
1064
|
+
|
|
902
1065
|
if do_multi:
|
|
903
|
-
if do_interactive:
|
|
904
|
-
_launch_interactive_multi(
|
|
905
|
-
ctx,
|
|
906
|
-
safe=safe,
|
|
907
|
-
use_aws=use_aws,
|
|
908
|
-
cli_profile=cli_profile,
|
|
909
|
-
extra_args=extra_args,
|
|
910
|
-
)
|
|
911
|
-
return
|
|
912
1066
|
_launch_multi(
|
|
913
1067
|
ctx,
|
|
914
1068
|
safe=safe,
|
|
@@ -929,88 +1083,19 @@ def claude(
|
|
|
929
1083
|
)
|
|
930
1084
|
return
|
|
931
1085
|
|
|
932
|
-
#
|
|
933
|
-
# Load config first to check provider setting
|
|
1086
|
+
# Load config for the current project and launch a normal Claude session.
|
|
934
1087
|
project = ctx.obj.get("project") if ctx.obj else None
|
|
935
1088
|
config = load_config(project_override=project, project_dir=Path.cwd())
|
|
936
|
-
use_bedrock = use_aws or config.claude_provider == "aws"
|
|
937
|
-
|
|
938
|
-
manager, name, exec_env, config = _get_manager(
|
|
939
|
-
ctx,
|
|
940
|
-
bedrock=use_bedrock,
|
|
941
|
-
bedrock_profile=cli_profile or "",
|
|
942
|
-
)
|
|
943
|
-
|
|
944
|
-
if use_bedrock:
|
|
945
|
-
profile_label = exec_env.get("AWS_PROFILE", "default")
|
|
946
|
-
region_label = exec_env.get("AWS_REGION", "us-east-1")
|
|
947
|
-
bedrock_label = f" via Bedrock (profile={profile_label}, region={region_label})"
|
|
948
|
-
console.print(
|
|
949
|
-
f"Checking Bedrock access (profile={profile_label}, region={region_label})..."
|
|
950
|
-
)
|
|
951
|
-
_check_bedrock_access(name, exec_env)
|
|
952
|
-
else:
|
|
953
|
-
bedrock_label = ""
|
|
954
|
-
|
|
955
|
-
# Resolve worktree working directory (if --worktree/-w was given)
|
|
956
|
-
worktree_dir: str | None = None
|
|
957
|
-
if worktree_name is not None:
|
|
958
|
-
if worktree_name == "":
|
|
959
|
-
worktree_name = _generate_worktree_name()
|
|
960
|
-
container_project_dir = f"/root/projects/{config.project_name}"
|
|
961
|
-
worktree_dir = _setup_worktree(name, container_project_dir, worktree_name)
|
|
962
|
-
console.print(f"[dim]Worktree: {worktree_dir} (branch: worktree-{worktree_name})[/dim]")
|
|
963
|
-
|
|
964
|
-
# Local Chrome bridge: probe debug port, write MCP config, inject into args
|
|
965
1089
|
local_chrome = local_chrome or config.local_chrome is True
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
console.print("[dim]Connecting to host Chrome...[/dim]")
|
|
976
|
-
try:
|
|
977
|
-
chrome_port = ensure_host_chrome(name)
|
|
978
|
-
except LocalChromeUnavailable as exc:
|
|
979
|
-
raise click.ClickException(str(exc)) from exc
|
|
980
|
-
|
|
981
|
-
console.print(f"[dim]Chrome debug port {chrome_port} reachable.[/dim]")
|
|
982
|
-
|
|
983
|
-
# Start TCP proxy: localhost:<port> -> host.docker.internal:<port>
|
|
984
|
-
# Chrome rejects non-localhost Host headers, so the MCP server
|
|
985
|
-
# must connect via localhost.
|
|
986
|
-
start_chrome_proxy(name, chrome_port)
|
|
987
|
-
|
|
988
|
-
mcp_path = write_mcp_config(chrome_port)
|
|
989
|
-
container_mcp_path = "/etc/ai-shell/chrome-mcp.json"
|
|
990
|
-
# Inject the MCP config file into the running container
|
|
991
|
-
_inject_mcp_config(name, str(mcp_path), container_mcp_path)
|
|
992
|
-
mcp_args = ["--mcp-config", container_mcp_path]
|
|
993
|
-
console.print("[dim]Chrome DevTools MCP attached.[/dim]")
|
|
994
|
-
|
|
995
|
-
if safe:
|
|
996
|
-
cmd = ["claude", *mcp_args, *extra_args]
|
|
997
|
-
console.print(f"[bold]Launching Claude Code (safe mode){bedrock_label} in {name}...[/bold]")
|
|
998
|
-
manager.exec_interactive(name, cmd, extra_env=exec_env, workdir=worktree_dir)
|
|
999
|
-
else:
|
|
1000
|
-
# Try with -c first (continue previous conversation)
|
|
1001
|
-
cmd_continue = ["claude", "--dangerously-skip-permissions", "-c", *mcp_args, *extra_args]
|
|
1002
|
-
console.print(f"[bold]Launching Claude Code{bedrock_label} in {name}...[/bold]")
|
|
1003
|
-
exit_code, elapsed = manager.run_interactive(
|
|
1004
|
-
name, cmd_continue, extra_env=exec_env, workdir=worktree_dir
|
|
1005
|
-
)
|
|
1006
|
-
|
|
1007
|
-
if exit_code != 0 and elapsed < FAST_FAILURE_THRESHOLD:
|
|
1008
|
-
# -c failed quickly (likely no prior conversation), retry without it
|
|
1009
|
-
console.print("[yellow]No prior conversation found, starting fresh...[/yellow]")
|
|
1010
|
-
cmd_fresh = ["claude", "--dangerously-skip-permissions", *mcp_args, *extra_args]
|
|
1011
|
-
manager.exec_interactive(name, cmd_fresh, extra_env=exec_env, workdir=worktree_dir)
|
|
1012
|
-
else:
|
|
1013
|
-
sys.exit(exit_code)
|
|
1090
|
+
_launch_loaded_config_claude(
|
|
1091
|
+
config,
|
|
1092
|
+
safe=safe,
|
|
1093
|
+
use_aws=use_aws,
|
|
1094
|
+
cli_profile=cli_profile,
|
|
1095
|
+
extra_args=extra_args,
|
|
1096
|
+
local_chrome=local_chrome,
|
|
1097
|
+
worktree_name=worktree_name,
|
|
1098
|
+
)
|
|
1014
1099
|
|
|
1015
1100
|
|
|
1016
1101
|
@click.command(context_settings=CONTEXT_SETTINGS)
|
|
@@ -65,7 +65,7 @@ class AiShellConfig:
|
|
|
65
65
|
bedrock_profile: str = "" # AWS profile for Bedrock LLM API calls
|
|
66
66
|
|
|
67
67
|
# Claude options
|
|
68
|
-
local_chrome: bool = False # Attach Chrome DevTools MCP to host Chrome
|
|
68
|
+
local_chrome: bool = False # Attach Chrome DevTools MCP to project-scoped host Chrome
|
|
69
69
|
|
|
70
70
|
# Per-tool provider
|
|
71
71
|
claude_provider: str = "" # "anthropic" (default) or "aws"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
"""Interactive
|
|
1
|
+
"""Interactive launcher wizard for ``ai-shell claude -i``.
|
|
2
2
|
|
|
3
|
-
Walks the user through a guided menu to configure each
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Walks the user through a guided menu to configure each pane, then converts the
|
|
4
|
+
collected choices into :class:`~ai_shell.tmux.PaneSpec` objects when a tmux
|
|
5
|
+
session is needed.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -48,6 +48,19 @@ class InteractiveConfig:
|
|
|
48
48
|
team_mode: bool = False
|
|
49
49
|
shared_chrome: bool = False
|
|
50
50
|
|
|
51
|
+
@property
|
|
52
|
+
def pane_count(self) -> int:
|
|
53
|
+
"""Return the number of panes requested by the user."""
|
|
54
|
+
return len(self.pane_choices)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def has_claude_panes(self) -> bool:
|
|
58
|
+
"""Return ``True`` when at least one pane launches Claude."""
|
|
59
|
+
return any(
|
|
60
|
+
choice.pane_type in (PaneType.THIS_PROJECT, PaneType.WORKSPACE_REPO)
|
|
61
|
+
for choice in self.pane_choices
|
|
62
|
+
)
|
|
63
|
+
|
|
51
64
|
|
|
52
65
|
# ── Option builder ───────────────────────────────────────────────────
|
|
53
66
|
|
|
@@ -69,7 +82,7 @@ def _build_pane_options(
|
|
|
69
82
|
"""Build the numbered option list for per-window type selection."""
|
|
70
83
|
options: list[_PaneOption] = [
|
|
71
84
|
_PaneOption(
|
|
72
|
-
label=f"This project ({project_name}) - Claude
|
|
85
|
+
label=f"This project ({project_name}) - Claude session",
|
|
73
86
|
pane_type=PaneType.THIS_PROJECT,
|
|
74
87
|
),
|
|
75
88
|
_PaneOption(
|
|
@@ -102,8 +115,12 @@ def run_interactive_wizard(
|
|
|
102
115
|
project_name: str,
|
|
103
116
|
workspace_repos: list[dict[str, Any]] | None = None,
|
|
104
117
|
console: Console | None = None,
|
|
118
|
+
min_windows: int = 1,
|
|
119
|
+
max_windows: int = 4,
|
|
120
|
+
default_windows: int | None = None,
|
|
121
|
+
default_shared_chrome: bool = False,
|
|
105
122
|
) -> InteractiveConfig | None:
|
|
106
|
-
"""Walk the user through the interactive
|
|
123
|
+
"""Walk the user through the interactive launcher flow.
|
|
107
124
|
|
|
108
125
|
Returns :class:`InteractiveConfig` with all choices, or ``None`` if the
|
|
109
126
|
user cancels (Ctrl-C / EOFError).
|
|
@@ -112,13 +129,15 @@ def run_interactive_wizard(
|
|
|
112
129
|
console = Console(stderr=True)
|
|
113
130
|
|
|
114
131
|
options = _build_pane_options(project_name, workspace_repos)
|
|
132
|
+
if default_windows is None:
|
|
133
|
+
default_windows = min_windows
|
|
115
134
|
|
|
116
135
|
try:
|
|
117
136
|
# Step 1: number of windows
|
|
118
137
|
num_windows: int = click.prompt(
|
|
119
138
|
"How many windows?",
|
|
120
|
-
type=click.IntRange(
|
|
121
|
-
default=
|
|
139
|
+
type=click.IntRange(min_windows, max_windows),
|
|
140
|
+
default=default_windows,
|
|
122
141
|
)
|
|
123
142
|
|
|
124
143
|
# Step 2: per-window type
|
|
@@ -144,15 +163,23 @@ def run_interactive_wizard(
|
|
|
144
163
|
)
|
|
145
164
|
|
|
146
165
|
# Step 3: pre-launch options
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
shared_chrome = click.confirm(
|
|
153
|
-
"Enable shared Chrome browser for all Claude panes?",
|
|
154
|
-
default=False,
|
|
166
|
+
team_mode = False
|
|
167
|
+
shared_chrome = False
|
|
168
|
+
has_claude_panes = any(
|
|
169
|
+
choice.pane_type in (PaneType.THIS_PROJECT, PaneType.WORKSPACE_REPO)
|
|
170
|
+
for choice in choices
|
|
155
171
|
)
|
|
172
|
+
if has_claude_panes:
|
|
173
|
+
console.print()
|
|
174
|
+
if num_windows == 1:
|
|
175
|
+
team_prompt = "Enable teams mode for this Claude pane?"
|
|
176
|
+
chrome_prompt = "Enable Chrome browser for this Claude pane?"
|
|
177
|
+
else:
|
|
178
|
+
team_prompt = "Enable teams mode on the primary Claude pane?"
|
|
179
|
+
chrome_prompt = "Enable shared Chrome browser for all Claude panes?"
|
|
180
|
+
|
|
181
|
+
team_mode = click.confirm(team_prompt, default=False)
|
|
182
|
+
shared_chrome = click.confirm(chrome_prompt, default=default_shared_chrome)
|
|
156
183
|
|
|
157
184
|
except (EOFError, KeyboardInterrupt, click.Abort):
|
|
158
185
|
return None
|
|
@@ -18,21 +18,29 @@ import json
|
|
|
18
18
|
import logging
|
|
19
19
|
import os
|
|
20
20
|
import platform
|
|
21
|
-
import socket
|
|
22
21
|
import subprocess
|
|
23
22
|
import time
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from hashlib import sha1
|
|
25
|
+
from http.client import HTTPConnection, HTTPException
|
|
24
26
|
from pathlib import Path
|
|
25
27
|
|
|
28
|
+
from ai_shell.defaults import unique_project_name
|
|
29
|
+
|
|
26
30
|
logger = logging.getLogger(__name__)
|
|
27
31
|
|
|
28
32
|
CHROME_DEBUG_HOST = "host.docker.internal"
|
|
29
|
-
|
|
33
|
+
CHROME_HOST_PROBE_TIMEOUT_SECONDS = 20.0
|
|
34
|
+
CHROME_CONTAINER_PROBE_TIMEOUT_SECONDS = 10.0
|
|
35
|
+
CHROME_PROBE_INTERVAL_SECONDS = 0.5
|
|
36
|
+
CHROME_DEBUG_PORT_RANGE_START = 40000
|
|
37
|
+
CHROME_DEBUG_PORT_RANGE_SIZE = 20000
|
|
30
38
|
|
|
31
39
|
MCP_CONFIG_FILENAME = "chrome-mcp.json"
|
|
32
40
|
|
|
33
|
-
# User-data-dir for
|
|
34
|
-
# the user's normal browsing).
|
|
35
|
-
|
|
41
|
+
# User-data-dir for ai-shell project-specific Chrome profiles (keeps them
|
|
42
|
+
# separate from the user's normal browsing and from other repos).
|
|
43
|
+
_CHROME_PROFILE_ROOT_DIR_NAME = "ai-shell"
|
|
36
44
|
|
|
37
45
|
# Well-known Chrome install paths on Windows
|
|
38
46
|
_CHROME_CANDIDATES = [
|
|
@@ -50,20 +58,6 @@ _NODE_PROXY_TEMPLATE = (
|
|
|
50
58
|
"}}).listen({port},'127.0.0.1')"
|
|
51
59
|
)
|
|
52
60
|
|
|
53
|
-
SETUP_INSTRUCTIONS = """\
|
|
54
|
-
Chrome could not be found or launched automatically, and the debug port \
|
|
55
|
-
is not reachable.
|
|
56
|
-
|
|
57
|
-
To fix, launch Chrome manually with these flags:
|
|
58
|
-
|
|
59
|
-
chrome.exe --remote-debugging-port=9222 \\
|
|
60
|
-
--remote-debugging-address=127.0.0.1 \\
|
|
61
|
-
--remote-allow-origins=* \\
|
|
62
|
-
--user-data-dir="%LOCALAPPDATA%\\Google\\Chrome\\ai-debug-profile"
|
|
63
|
-
|
|
64
|
-
Then re-run this command.
|
|
65
|
-
See README.md "Attaching to your Windows Chrome" for details."""
|
|
66
|
-
|
|
67
61
|
|
|
68
62
|
class LocalChromeUnavailable(Exception):
|
|
69
63
|
"""Raised when the host Chrome debug port is not reachable."""
|
|
@@ -89,23 +83,57 @@ def find_chrome() -> str | None:
|
|
|
89
83
|
return None
|
|
90
84
|
|
|
91
85
|
|
|
92
|
-
def
|
|
93
|
-
"""Return
|
|
86
|
+
def _project_slug(project_name: str, project_dir: str | Path | None = None) -> str:
|
|
87
|
+
"""Return a stable slug for project-scoped Chrome state."""
|
|
88
|
+
if project_dir is not None:
|
|
89
|
+
try:
|
|
90
|
+
return unique_project_name(Path(project_dir), project_name)
|
|
91
|
+
except (TypeError, ValueError):
|
|
92
|
+
logger.debug("Falling back to project-name-only slug for %s", project_name)
|
|
93
|
+
slug = "".join(ch if ch.isalnum() or ch == "-" else "-" for ch in project_name.lower())
|
|
94
|
+
slug = "-".join(part for part in slug.split("-") if part)
|
|
95
|
+
return slug or "project"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _chrome_profile_dir(project_name: str, project_dir: str | Path | None = None) -> str:
|
|
99
|
+
"""Return the user-data-dir path for a project's ai-shell debug Chrome."""
|
|
100
|
+
slug = _project_slug(project_name, project_dir)
|
|
94
101
|
local_app = os.environ.get("LOCALAPPDATA", "")
|
|
95
102
|
if local_app:
|
|
96
|
-
return str(Path(local_app) / "Google" / "Chrome" /
|
|
97
|
-
return str(Path.home() / ".config" / "google-chrome" /
|
|
103
|
+
return str(Path(local_app) / "Google" / "Chrome" / _CHROME_PROFILE_ROOT_DIR_NAME / slug)
|
|
104
|
+
return str(Path.home() / ".config" / "google-chrome" / _CHROME_PROFILE_ROOT_DIR_NAME / slug)
|
|
98
105
|
|
|
99
106
|
|
|
100
|
-
def
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
def _project_debug_port(project_name: str, project_dir: str | Path | None = None) -> int:
|
|
108
|
+
"""Return a stable per-project Chrome remote debugging port."""
|
|
109
|
+
slug = _project_slug(project_name, project_dir)
|
|
110
|
+
digest = sha1(slug.encode("utf-8"), usedforsecurity=False).hexdigest()
|
|
111
|
+
return CHROME_DEBUG_PORT_RANGE_START + (int(digest[:8], 16) % CHROME_DEBUG_PORT_RANGE_SIZE)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _build_setup_instructions(project_name: str, profile_dir: str, port: int) -> str:
|
|
115
|
+
"""Return manual setup instructions for the project's Chrome profile."""
|
|
116
|
+
return f"""\
|
|
117
|
+
Chrome could not be found or launched automatically, and the debug port \
|
|
118
|
+
is not reachable.
|
|
119
|
+
|
|
120
|
+
To fix, launch Chrome manually with these flags:
|
|
121
|
+
|
|
122
|
+
chrome.exe --remote-debugging-port={port} \\
|
|
123
|
+
--remote-debugging-address=127.0.0.1 \\
|
|
124
|
+
--remote-allow-origins=* \\
|
|
125
|
+
--user-data-dir="{profile_dir}"
|
|
126
|
+
|
|
127
|
+
Then re-run this command for project '{project_name}'.
|
|
128
|
+
See README.md "Attaching to your Windows Chrome" for details."""
|
|
106
129
|
|
|
107
130
|
|
|
108
|
-
def launch_chrome(
|
|
131
|
+
def launch_chrome(
|
|
132
|
+
port: int,
|
|
133
|
+
*,
|
|
134
|
+
project_name: str,
|
|
135
|
+
project_dir: str | Path | None = None,
|
|
136
|
+
) -> bool:
|
|
109
137
|
"""Launch Chrome on the host with the debug port enabled.
|
|
110
138
|
|
|
111
139
|
Returns ``True`` if Chrome was launched, ``False`` if Chrome could
|
|
@@ -115,7 +143,7 @@ def launch_chrome(port: int) -> bool:
|
|
|
115
143
|
if chrome_path is None:
|
|
116
144
|
return False
|
|
117
145
|
|
|
118
|
-
profile_dir = _chrome_profile_dir()
|
|
146
|
+
profile_dir = _chrome_profile_dir(project_name, project_dir)
|
|
119
147
|
args = [
|
|
120
148
|
chrome_path,
|
|
121
149
|
f"--remote-debugging-port={port}",
|
|
@@ -167,38 +195,86 @@ def probe_chrome_port(container_name: str, port: int) -> bool:
|
|
|
167
195
|
return True
|
|
168
196
|
|
|
169
197
|
|
|
170
|
-
def
|
|
198
|
+
def probe_host_chrome_port(port: int) -> bool:
|
|
199
|
+
"""Check whether a Chrome debug port is reachable on the host."""
|
|
200
|
+
connection = HTTPConnection("127.0.0.1", port, timeout=2)
|
|
201
|
+
try:
|
|
202
|
+
connection.request("GET", "/json/version")
|
|
203
|
+
response = connection.getresponse()
|
|
204
|
+
return response.status == 200 and bool(response.read().strip())
|
|
205
|
+
except (OSError, HTTPException):
|
|
206
|
+
return False
|
|
207
|
+
finally:
|
|
208
|
+
connection.close()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _wait_until_ready(
|
|
212
|
+
probe_fn: Callable[..., bool],
|
|
213
|
+
*args: object,
|
|
214
|
+
timeout_seconds: float,
|
|
215
|
+
interval_seconds: float = CHROME_PROBE_INTERVAL_SECONDS,
|
|
216
|
+
) -> bool:
|
|
217
|
+
"""Poll until a probe succeeds or the timeout expires."""
|
|
218
|
+
deadline = time.monotonic() + timeout_seconds
|
|
219
|
+
while True:
|
|
220
|
+
if probe_fn(*args):
|
|
221
|
+
return True
|
|
222
|
+
remaining = deadline - time.monotonic()
|
|
223
|
+
if remaining <= 0:
|
|
224
|
+
return False
|
|
225
|
+
time.sleep(min(interval_seconds, remaining))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def ensure_host_chrome(
|
|
229
|
+
container_name: str,
|
|
230
|
+
*,
|
|
231
|
+
project_name: str,
|
|
232
|
+
project_dir: str | Path | None = None,
|
|
233
|
+
) -> int:
|
|
171
234
|
"""Ensure Chrome is running with a debug port reachable from the container.
|
|
172
235
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
3. Raise :class:`LocalChromeUnavailable` if Chrome can't be found or started.
|
|
236
|
+
Each project gets its own debug profile directory and a stable debug port,
|
|
237
|
+
so different repos can keep separate logged-in Chrome instances alive.
|
|
176
238
|
|
|
177
239
|
Returns the port number Chrome is listening on.
|
|
178
240
|
"""
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
port = _find_free_port()
|
|
185
|
-
logger.info(
|
|
186
|
-
"Chrome not found on port %d, launching on port %d", DEFAULT_CHROME_DEBUG_PORT, port
|
|
187
|
-
)
|
|
241
|
+
port = _project_debug_port(project_name, project_dir)
|
|
242
|
+
profile_dir = _chrome_profile_dir(project_name, project_dir)
|
|
243
|
+
|
|
244
|
+
if probe_chrome_port(container_name, port):
|
|
245
|
+
return port
|
|
188
246
|
|
|
189
|
-
|
|
190
|
-
|
|
247
|
+
logger.info("Chrome for project %s not found on port %d, launching it", project_name, port)
|
|
248
|
+
|
|
249
|
+
if not launch_chrome(port, project_name=project_name, project_dir=project_dir):
|
|
250
|
+
raise LocalChromeUnavailable(_build_setup_instructions(project_name, profile_dir, port))
|
|
251
|
+
|
|
252
|
+
if not _wait_until_ready(
|
|
253
|
+
probe_host_chrome_port,
|
|
254
|
+
port,
|
|
255
|
+
timeout_seconds=CHROME_HOST_PROBE_TIMEOUT_SECONDS,
|
|
256
|
+
):
|
|
257
|
+
raise LocalChromeUnavailable(
|
|
258
|
+
f"Chrome was launched for project '{project_name}' on port {port}, but the "
|
|
259
|
+
f"debug port did not open on localhost within "
|
|
260
|
+
f"{int(CHROME_HOST_PROBE_TIMEOUT_SECONDS)} seconds.\n\n"
|
|
261
|
+
"If another ai-shell Chrome window for this project is already open, "
|
|
262
|
+
"close it and retry."
|
|
263
|
+
)
|
|
191
264
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
265
|
+
if _wait_until_ready(
|
|
266
|
+
probe_chrome_port,
|
|
267
|
+
container_name,
|
|
268
|
+
port,
|
|
269
|
+
timeout_seconds=CHROME_CONTAINER_PROBE_TIMEOUT_SECONDS,
|
|
270
|
+
):
|
|
271
|
+
logger.info("Chrome ready on port %d for project %s", port, project_name)
|
|
272
|
+
return port
|
|
198
273
|
|
|
199
274
|
raise LocalChromeUnavailable(
|
|
200
|
-
f"Chrome
|
|
201
|
-
"reachable within
|
|
275
|
+
f"Chrome is listening on localhost:{port} for project '{project_name}', but the "
|
|
276
|
+
"debug port did not become reachable from the dev container within "
|
|
277
|
+
f"{int(CHROME_CONTAINER_PROBE_TIMEOUT_SECONDS)} seconds.\n\n"
|
|
202
278
|
"Check that Docker Desktop can reach the host via host.docker.internal."
|
|
203
279
|
)
|
|
204
280
|
|
|
@@ -250,7 +250,7 @@ def build_tmux_commands(
|
|
|
250
250
|
# Status bar
|
|
251
251
|
("status-style", "bg=colour235 fg=colour248"),
|
|
252
252
|
("status-left", "#[fg=colour172,bold] #S #[fg=colour248]| "),
|
|
253
|
-
("status-right", "#[fg=colour95] C-b z
|
|
253
|
+
("status-right", "#[fg=colour95] C-b: ◫o +c ▦␣ ◀p ▶n ⛶z ⏏d ⌦& "),
|
|
254
254
|
("status-left-length", "40"),
|
|
255
255
|
("status-right-length", "40"),
|
|
256
256
|
]
|
|
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
|