dev-bubble 0.7.8__tar.gz → 0.7.9__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.
- {dev_bubble-0.7.8/dev_bubble.egg-info → dev_bubble-0.7.9}/PKG-INFO +7 -9
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/README.md +6 -8
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/SPEC.md +4 -5
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/__init__.py +1 -1
- dev_bubble-0.7.8/bubble/claude.py → dev_bubble-0.7.9/bubble/ai.py +136 -43
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/auth_proxy.py +52 -52
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/cli.py +99 -64
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/settings.py +67 -47
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/config.py +24 -19
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/data/skill.md +11 -1
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/finalization.py +10 -5
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/tools/vscode.sh +65 -135
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/provisioning.py +2 -2
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/remote.py +9 -9
- {dev_bubble-0.7.8 → dev_bubble-0.7.9/dev_bubble.egg-info}/PKG-INFO +7 -9
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/dev_bubble.egg-info/SOURCES.txt +2 -2
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/conftest.py +2 -2
- dev_bubble-0.7.8/tests/test_claude.py → dev_bubble-0.7.9/tests/test_ai.py +239 -85
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_auth_proxy.py +17 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_branch_no_target.py +1 -1
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_claude_projects_symlink.py +28 -28
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_config.py +64 -137
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_github_token.py +156 -14
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_mounts.py +8 -8
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_remote.py +6 -6
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_tools.py +0 -12
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/.claude/CLAUDE.md +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/.github/workflows/ci.yml +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/.github/workflows/publish.yml +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/.gitignore +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/CHANGELOG.md +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/LICENSE +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/__main__.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/automation.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/clean.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/clone.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/cloud.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/cloud_types.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/__init__.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/cloud_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/completion.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/doctor.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/images.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/infrastructure.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/lifecycle.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/list_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/relay_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/remote_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/security_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/commands/status_cmd.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/container_helpers.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/default_repos.json +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/git_store.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/github_token.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/graphql_validator.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/hooks/__init__.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/hooks/lean.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/hooks/python.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/image_management.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/__init__.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/builder.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/base.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/cloud-init.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/lean-toolchain.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/lean.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/python.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/tools/claude.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/tools/codex.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/tools/elan.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/tools/emacs.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/tools/gh.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/tools/neovim.sh +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/images/scripts/tools/pins.json +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/lean.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/lifecycle.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/naming.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/native.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/network.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/notices.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/output.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/relay.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/repo_registry.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/runtime/__init__.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/runtime/base.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/runtime/colima.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/runtime/incus.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/security.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/setup.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/skill.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/spinner.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/target.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/token_store.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/tools.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/tunnel.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/bubble/vscode.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/config/com.bubble.git-update.plist +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/config/com.bubble.image-refresh.plist +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/config/com.bubble.relay-daemon.plist +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/conftest.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/dev_bubble.egg-info/dependency_links.txt +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/dev_bubble.egg-info/entry_points.txt +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/dev_bubble.egg-info/requires.txt +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/dev_bubble.egg-info/top_level.txt +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/pyproject.toml +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/setup.cfg +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_build_lock.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_cloud.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_completion.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_customize.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_editor.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_git_store.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_graphql_validator.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_hooks.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_integration.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_lifecycle.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_list_columns.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_list_remote.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_multi_target.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_naming.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_native.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_network.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_notices.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_relay.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_repo_registry.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_security.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_skill.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_spinner.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_status.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_target.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_tunnel.py +0 -0
- {dev_bubble-0.7.8 → dev_bubble-0.7.9}/tests/test_vscode.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dev-bubble
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.9
|
|
4
4
|
Summary: Containerized development environments powered by Incus
|
|
5
5
|
Author-email: Kim Morrison <kim@tqft.net>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -43,7 +43,7 @@ We assume that you work using VSCode, emacs, or neovim, and that you collaborate
|
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
# Install
|
|
46
|
-
uv tool install
|
|
46
|
+
uv tool install git+https://github.com/kim-em/bubble.git
|
|
47
47
|
|
|
48
48
|
# Open a bubble for a GitHub PR — just paste the URL, and you get a containerized VSCode window!
|
|
49
49
|
bubble https://github.com/leanprover-community/mathlib4/pull/35219
|
|
@@ -124,11 +124,9 @@ bubble pop mathlib4-pr-35219
|
|
|
124
124
|
## Development Install
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
# Install from
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
# For development
|
|
131
|
-
uv pip install -e '.[dev]'
|
|
127
|
+
# Install from a local clone (editable — always uses latest code)
|
|
128
|
+
git clone https://github.com/kim-em/bubble.git
|
|
129
|
+
uv tool install --force --editable bubble
|
|
132
130
|
```
|
|
133
131
|
|
|
134
132
|
## How It Works
|
|
@@ -263,10 +261,10 @@ bubble mathlib4/issues/42
|
|
|
263
261
|
|
|
264
262
|
Claude is instructed to read the issue, implement a fix on the `issue-<number>` branch, and open a PR. This turns `bubble 42` (for an issue) into an autonomous coding agent workflow.
|
|
265
263
|
|
|
266
|
-
You can also provide a custom prompt for any bubble via the `
|
|
264
|
+
You can also provide a custom prompt for any bubble via the `BUBBLE_AI_PROMPT` environment variable:
|
|
267
265
|
|
|
268
266
|
```bash
|
|
269
|
-
|
|
267
|
+
BUBBLE_AI_PROMPT="Refactor the parser module" bubble leanprover/lean4
|
|
270
268
|
```
|
|
271
269
|
|
|
272
270
|
Requirements: Claude Code must be installed in the container (see tool settings above), the `gh` CLI must be available on the host (for fetching issue/PR metadata), and the default VS Code editor must be used. With `--shell` or `--no-interactive`, the prompt is not injected. If `gh` is unavailable or the API call fails, bubble proceeds without injecting a prompt.
|
|
@@ -9,7 +9,7 @@ We assume that you work using VSCode, emacs, or neovim, and that you collaborate
|
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
# Install
|
|
12
|
-
uv tool install
|
|
12
|
+
uv tool install git+https://github.com/kim-em/bubble.git
|
|
13
13
|
|
|
14
14
|
# Open a bubble for a GitHub PR — just paste the URL, and you get a containerized VSCode window!
|
|
15
15
|
bubble https://github.com/leanprover-community/mathlib4/pull/35219
|
|
@@ -90,11 +90,9 @@ bubble pop mathlib4-pr-35219
|
|
|
90
90
|
## Development Install
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
# Install from
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# For development
|
|
97
|
-
uv pip install -e '.[dev]'
|
|
93
|
+
# Install from a local clone (editable — always uses latest code)
|
|
94
|
+
git clone https://github.com/kim-em/bubble.git
|
|
95
|
+
uv tool install --force --editable bubble
|
|
98
96
|
```
|
|
99
97
|
|
|
100
98
|
## How It Works
|
|
@@ -229,10 +227,10 @@ bubble mathlib4/issues/42
|
|
|
229
227
|
|
|
230
228
|
Claude is instructed to read the issue, implement a fix on the `issue-<number>` branch, and open a PR. This turns `bubble 42` (for an issue) into an autonomous coding agent workflow.
|
|
231
229
|
|
|
232
|
-
You can also provide a custom prompt for any bubble via the `
|
|
230
|
+
You can also provide a custom prompt for any bubble via the `BUBBLE_AI_PROMPT` environment variable:
|
|
233
231
|
|
|
234
232
|
```bash
|
|
235
|
-
|
|
233
|
+
BUBBLE_AI_PROMPT="Refactor the parser module" bubble leanprover/lean4
|
|
236
234
|
```
|
|
237
235
|
|
|
238
236
|
Requirements: Claude Code must be installed in the container (see tool settings above), the `gh` CLI must be available on the host (for fetching issue/PR metadata), and the default VS Code editor must be used. With `--shell` or `--no-interactive`, the prompt is not injected. If `gh` is unavailable or the API call fails, bubble proceeds without injecting a prompt.
|
|
@@ -56,7 +56,7 @@ equivalent to `bubble open <url>`.
|
|
|
56
56
|
| `-b`, `--new-branch` | string | | Create a new branch |
|
|
57
57
|
| `--base` | string | | Base branch for `-b` |
|
|
58
58
|
| `--mount` | string (repeatable) | | Mount host dir into container |
|
|
59
|
-
| `--
|
|
59
|
+
| `--ai-config/--no-ai-config` | flag | enabled | Mount AI provider configs read-only |
|
|
60
60
|
| `--claude-credentials/--no-claude-credentials` | flag | enabled | Mount Claude credentials |
|
|
61
61
|
| `--codex-credentials/--no-codex-credentials` | flag | enabled | Mount Codex credentials |
|
|
62
62
|
| `--ssh HOST` | string | | Run on remote host |
|
|
@@ -912,7 +912,7 @@ validation already confirmed the repo exists).
|
|
|
912
912
|
|
|
913
913
|
### 10.1 Claude Code config mounting
|
|
914
914
|
|
|
915
|
-
When `--
|
|
915
|
+
When `--ai-config` is enabled (default), mount specific items from
|
|
916
916
|
`~/.claude/` into `/home/user/.claude/` read-only:
|
|
917
917
|
- `CLAUDE.md`, `settings.json`, `skills/`, `keybindings.json`, `commands/`
|
|
918
918
|
|
|
@@ -1087,11 +1087,10 @@ Configuration is managed through dedicated subcommands rather than a generic
|
|
|
1087
1087
|
get/set interface:
|
|
1088
1088
|
|
|
1089
1089
|
- `bubble tools set TOOL yes|no|auto` — configure tool installation
|
|
1090
|
-
- `bubble
|
|
1091
|
-
- `bubble codex credentials on|off` — toggle Codex credential mounting
|
|
1090
|
+
- `bubble ai credentials on|off [--provider claude|codex]` — toggle AI credential mounting
|
|
1092
1091
|
- `bubble security set NAME on|off|auto` — configure security settings
|
|
1093
1092
|
- `bubble config set KEY VALUE` — set security settings (alias)
|
|
1094
|
-
- `bubble config symlink-
|
|
1093
|
+
- `bubble config symlink-ai-projects` — symlink AI projects directory
|
|
1095
1094
|
|
|
1096
1095
|
---
|
|
1097
1096
|
|
|
@@ -1,20 +1,52 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""AI provider integration for bubble containers.
|
|
2
|
+
|
|
3
|
+
Handles prompt generation and task injection for the configured preferred
|
|
4
|
+
AI provider (default: Claude Code). Provider-neutral where possible;
|
|
5
|
+
provider-specific details (binary names, env vars, prompt file paths)
|
|
6
|
+
are dispatched via the ``[ai] preferred`` config key.
|
|
7
|
+
"""
|
|
2
8
|
|
|
3
9
|
import json
|
|
4
10
|
import re
|
|
5
11
|
import shlex
|
|
6
12
|
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
7
14
|
|
|
8
15
|
from .config import DATA_DIR
|
|
9
16
|
from .runtime.base import ContainerRuntime
|
|
10
17
|
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
)
|
|
18
|
+
# Provider-specific task commands.
|
|
19
|
+
# Each reads the prompt from .vscode/ai-prompt.txt, runs the AI tool
|
|
20
|
+
# with autonomous permissions, then deletes the prompt file.
|
|
21
|
+
_TASK_COMMANDS = {
|
|
22
|
+
"claude": (
|
|
23
|
+
"test -f .vscode/ai-prompt.txt && ANTHROPIC_API_KEY= CLAUDECODE="
|
|
24
|
+
' claude --dangerously-skip-permissions "$(cat .vscode/ai-prompt.txt)"'
|
|
25
|
+
" && rm -f .vscode/ai-prompt.txt"
|
|
26
|
+
),
|
|
27
|
+
"codex": (
|
|
28
|
+
"test -f .vscode/ai-prompt.txt &&"
|
|
29
|
+
' codex --approval-mode full-auto "$(cat .vscode/ai-prompt.txt)"'
|
|
30
|
+
" && rm -f .vscode/ai-prompt.txt"
|
|
31
|
+
),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
SUPPORTED_PROVIDERS = frozenset(_TASK_COMMANDS)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _task_command_for(provider: str) -> str:
|
|
38
|
+
"""Return the VS Code task shell command for the given AI provider.
|
|
39
|
+
|
|
40
|
+
Raises ``ValueError`` for unknown providers so typos are caught early.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
return _TASK_COMMANDS[provider]
|
|
44
|
+
except KeyError:
|
|
45
|
+
supported = ", ".join(sorted(SUPPORTED_PROVIDERS))
|
|
46
|
+
raise ValueError(f"Unknown AI provider {provider!r} (supported: {supported})") from None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
AI_TASK_COMMAND = _TASK_COMMANDS["claude"]
|
|
18
50
|
|
|
19
51
|
TEMPLATES_DIR = DATA_DIR / "templates"
|
|
20
52
|
|
|
@@ -173,7 +205,7 @@ def generate_issue_prompt(
|
|
|
173
205
|
second_opinion: str = "auto",
|
|
174
206
|
config: dict | None = None,
|
|
175
207
|
) -> str | None:
|
|
176
|
-
"""Fetch GitHub issue details and generate
|
|
208
|
+
"""Fetch GitHub issue details and generate an AI prompt.
|
|
177
209
|
|
|
178
210
|
Returns the prompt string, or None if the issue can't be fetched.
|
|
179
211
|
Uses a custom template from ~/.bubble/templates/issue.txt if present,
|
|
@@ -235,7 +267,7 @@ def generate_issue_prompt(
|
|
|
235
267
|
|
|
236
268
|
|
|
237
269
|
def generate_pr_prompt(owner: str, repo: str, pr_num: str, branch: str) -> str | None:
|
|
238
|
-
"""Fetch GitHub PR details and generate
|
|
270
|
+
"""Fetch GitHub PR details and generate an AI prompt.
|
|
239
271
|
|
|
240
272
|
Returns the prompt string, or None if the PR can't be fetched.
|
|
241
273
|
Uses a custom template from ~/.bubble/templates/pr.txt if present,
|
|
@@ -260,17 +292,92 @@ def generate_pr_prompt(owner: str, repo: str, pr_num: str, branch: str) -> str |
|
|
|
260
292
|
)
|
|
261
293
|
|
|
262
294
|
|
|
263
|
-
|
|
264
|
-
|
|
295
|
+
# Known-safe keys to copy from the host's ~/.claude.json.
|
|
296
|
+
# Only cosmetic/UX settings — no credentials, MCP config, or host-specific paths.
|
|
297
|
+
_CLAUDE_JSON_SAFE_KEYS = frozenset(
|
|
298
|
+
{
|
|
299
|
+
"theme",
|
|
300
|
+
"hasCompletedOnboarding",
|
|
301
|
+
"numStartups",
|
|
302
|
+
"preferredNotifChannel",
|
|
303
|
+
"autoUpdaterStatus",
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def setup_claude_settings(
|
|
309
|
+
runtime: ContainerRuntime,
|
|
310
|
+
container: str,
|
|
311
|
+
project_dir: str,
|
|
312
|
+
):
|
|
313
|
+
"""Pre-populate ~/.claude.json in the container to skip the first-run wizard.
|
|
314
|
+
|
|
315
|
+
Copies allowlisted settings (theme, onboarding state, etc.) from the host's
|
|
316
|
+
~/.claude.json if it exists. Always ensures hasCompletedOnboarding=True
|
|
317
|
+
and pre-trusts the project directory. This runs for ALL bubbles, not just
|
|
318
|
+
those with AI task injection.
|
|
319
|
+
|
|
320
|
+
Best-effort: failures are logged but do not abort bubble creation.
|
|
321
|
+
"""
|
|
322
|
+
host_claude_json = Path.home() / ".claude.json"
|
|
323
|
+
|
|
324
|
+
# Extract only allowlisted keys from host settings
|
|
325
|
+
settings: dict = {}
|
|
326
|
+
if host_claude_json.is_file():
|
|
327
|
+
try:
|
|
328
|
+
host_data = json.loads(host_claude_json.read_text())
|
|
329
|
+
if isinstance(host_data, dict):
|
|
330
|
+
settings = {k: v for k, v in host_data.items() if k in _CLAUDE_JSON_SAFE_KEYS}
|
|
331
|
+
except (OSError, json.JSONDecodeError):
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
# Ensure onboarding is marked complete
|
|
335
|
+
settings["hasCompletedOnboarding"] = True
|
|
336
|
+
n = settings.get("numStartups", 0)
|
|
337
|
+
settings["numStartups"] = (n if isinstance(n, int) else 0) + 1
|
|
338
|
+
|
|
339
|
+
# Pre-trust the project directory
|
|
340
|
+
settings["projects"] = {
|
|
341
|
+
project_dir: {"hasTrustDialogAccepted": True, "allowedTools": []},
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# Write to container (best-effort — don't abort bubble creation on failure)
|
|
345
|
+
settings_json = shlex.quote(json.dumps(settings, indent=2))
|
|
346
|
+
try:
|
|
347
|
+
runtime.exec(
|
|
348
|
+
container,
|
|
349
|
+
["su", "-", "user", "-c", f"printf '%s' {settings_json} > ~/.claude.json"],
|
|
350
|
+
)
|
|
351
|
+
except Exception:
|
|
352
|
+
from .output import detail
|
|
353
|
+
|
|
354
|
+
detail("Warning: could not pre-populate Claude Code settings.", err=True)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def inject_ai_task(
|
|
358
|
+
runtime: ContainerRuntime,
|
|
359
|
+
container: str,
|
|
360
|
+
project_dir: str,
|
|
361
|
+
prompt: str,
|
|
362
|
+
config: dict | None = None,
|
|
363
|
+
quiet: bool = False,
|
|
265
364
|
):
|
|
266
|
-
"""Inject
|
|
365
|
+
"""Inject AI auto-start task into a container's VS Code configuration.
|
|
366
|
+
|
|
367
|
+
Dispatches to the configured preferred AI provider (default: Claude).
|
|
267
368
|
|
|
268
|
-
- Writes prompt to .vscode/
|
|
269
|
-
- Creates/updates .vscode/tasks.json with
|
|
369
|
+
- Writes prompt to .vscode/ai-prompt.txt
|
|
370
|
+
- Creates/updates .vscode/tasks.json with AI task (runOn: folderOpen)
|
|
270
371
|
- Configures .vscode/settings.json for automatic tasks
|
|
271
372
|
- Adds generated files to git exclude
|
|
272
|
-
- Pre-trusts the project directory in
|
|
373
|
+
- Pre-trusts the project directory in the preferred provider's config
|
|
273
374
|
"""
|
|
375
|
+
provider = "claude"
|
|
376
|
+
if config:
|
|
377
|
+
provider = config.get("ai", {}).get("preferred", "claude")
|
|
378
|
+
|
|
379
|
+
task_command = _task_command_for(provider)
|
|
380
|
+
|
|
274
381
|
q_dir = shlex.quote(project_dir)
|
|
275
382
|
q_prompt = shlex.quote(prompt)
|
|
276
383
|
|
|
@@ -280,20 +387,21 @@ def inject_claude_task(
|
|
|
280
387
|
# Write prompt to file
|
|
281
388
|
runtime.exec(
|
|
282
389
|
container,
|
|
283
|
-
["su", "-", "user", "-c", f"printf '%s' {q_prompt} > {q_dir}/.vscode/
|
|
390
|
+
["su", "-", "user", "-c", f"printf '%s' {q_prompt} > {q_dir}/.vscode/ai-prompt.txt"],
|
|
284
391
|
)
|
|
285
392
|
|
|
286
|
-
# Create or update tasks.json with
|
|
287
|
-
|
|
288
|
-
"label": "
|
|
393
|
+
# Create or update tasks.json with AI task
|
|
394
|
+
ai_task = {
|
|
395
|
+
"label": "AI",
|
|
289
396
|
"type": "shell",
|
|
290
|
-
"command":
|
|
397
|
+
"command": task_command,
|
|
291
398
|
"runOptions": {"runOn": "folderOpen"},
|
|
292
399
|
"presentation": {"reveal": "always", "panel": "dedicated"},
|
|
293
400
|
}
|
|
294
|
-
tasks_json_str = shlex.quote(json.dumps(
|
|
401
|
+
tasks_json_str = shlex.quote(json.dumps(ai_task))
|
|
295
402
|
|
|
296
|
-
# Script: if tasks.json exists, add
|
|
403
|
+
# Script: if tasks.json exists, add AI task (removing old Claude and AI labels);
|
|
404
|
+
# otherwise create new file
|
|
297
405
|
script = (
|
|
298
406
|
f"cd {q_dir} && "
|
|
299
407
|
f"if [ -f .vscode/tasks.json ]; then "
|
|
@@ -301,7 +409,7 @@ def inject_claude_task(
|
|
|
301
409
|
f"import json,sys; "
|
|
302
410
|
f"t=json.load(open('.vscode/tasks.json')); "
|
|
303
411
|
f"t['tasks']=[x for x in t.get('tasks',[])"
|
|
304
|
-
f" if x.get('label')
|
|
412
|
+
f" if x.get('label') not in ('Claude','AI')]+[json.loads(sys.argv[1])]; "
|
|
305
413
|
f"json.dump(t,open('.vscode/tasks.json','w'),indent=2)"
|
|
306
414
|
f'" {tasks_json_str}; '
|
|
307
415
|
f"else "
|
|
@@ -332,31 +440,16 @@ def inject_claude_task(
|
|
|
332
440
|
f"cd {q_dir} && "
|
|
333
441
|
f"GIT_DIR=$(git rev-parse --git-dir) && "
|
|
334
442
|
f"mkdir -p $GIT_DIR/info && "
|
|
335
|
-
f"for f in .vscode/
|
|
443
|
+
f"for f in .vscode/ai-prompt.txt .vscode/claude-prompt.txt"
|
|
444
|
+
f" .vscode/settings.json .vscode/tasks.json; do "
|
|
336
445
|
f' grep -qxF "$f" $GIT_DIR/info/exclude 2>/dev/null'
|
|
337
446
|
f' || echo "$f" >> $GIT_DIR/info/exclude; '
|
|
338
447
|
f"done"
|
|
339
448
|
)
|
|
340
449
|
runtime.exec(container, ["su", "-", "user", "-c", exclude_script])
|
|
341
450
|
|
|
342
|
-
# Pre-trust the project directory and skip onboarding in .claude.json
|
|
343
|
-
trust_script = (
|
|
344
|
-
f'python3 -c "'
|
|
345
|
-
f"import json,os; "
|
|
346
|
-
f"p=os.path.expanduser('~/.claude.json'); "
|
|
347
|
-
f"d=json.load(open(p)) if os.path.exists(p) else {{}}; "
|
|
348
|
-
f"d['hasCompletedOnboarding']=True; "
|
|
349
|
-
f"n=d.get('numStartups',0); d['numStartups']=(n if isinstance(n,int) else 0)+1; "
|
|
350
|
-
f"d.setdefault('projects',{{}}); "
|
|
351
|
-
f"proj=d['projects'].setdefault({shlex.quote(project_dir)!r},{{}}); " # noqa: E501
|
|
352
|
-
f"proj['hasTrustDialogAccepted']=True; "
|
|
353
|
-
f"proj.setdefault('allowedTools',[]); "
|
|
354
|
-
f"json.dump(d,open(p,'w'),indent=2)"
|
|
355
|
-
f'"'
|
|
356
|
-
)
|
|
357
|
-
runtime.exec(container, ["su", "-", "user", "-c", trust_script])
|
|
358
|
-
|
|
359
451
|
if not quiet:
|
|
360
452
|
from .output import detail
|
|
361
453
|
|
|
362
|
-
|
|
454
|
+
label = provider.capitalize()
|
|
455
|
+
detail(f"{label} task injected (will start on VS Code folder open).")
|
|
@@ -326,19 +326,21 @@ def validate_api_path(
|
|
|
326
326
|
# ---------------------------------------------------------------------------
|
|
327
327
|
|
|
328
328
|
|
|
329
|
-
def
|
|
330
|
-
"""Skip a balanced { ... } block starting at
|
|
329
|
+
def _skip_braced_tokens(tokens: list, start: int) -> int:
|
|
330
|
+
"""Skip a balanced { ... } block in a token list starting at *start*.
|
|
331
331
|
|
|
332
|
-
|
|
332
|
+
tokens[start] must be a '{' punct token.
|
|
333
|
+
Returns the index after the closing '}', or -1 on error.
|
|
333
334
|
"""
|
|
334
|
-
if start >= len(
|
|
335
|
+
if start >= len(tokens) or tokens[start].value != "{":
|
|
335
336
|
return -1
|
|
336
337
|
depth = 1
|
|
337
338
|
i = start + 1
|
|
338
|
-
while i < len(
|
|
339
|
-
|
|
339
|
+
while i < len(tokens) and depth > 0:
|
|
340
|
+
v = tokens[i].value
|
|
341
|
+
if v == "{":
|
|
340
342
|
depth += 1
|
|
341
|
-
elif
|
|
343
|
+
elif v == "}":
|
|
342
344
|
depth -= 1
|
|
343
345
|
i += 1
|
|
344
346
|
return i if depth == 0 else -1
|
|
@@ -350,64 +352,62 @@ def _collect_graphql_op_types(query: str) -> list[str]:
|
|
|
350
352
|
Returns a list of operation types found (e.g. ['query', 'mutation']).
|
|
351
353
|
Handles line comments, fragment definitions, and anonymous queries.
|
|
352
354
|
Multiple operations in a single document are all reported.
|
|
355
|
+
|
|
356
|
+
Uses the string-aware tokenizer from graphql_validator so that braces
|
|
357
|
+
inside string literals are not miscounted.
|
|
353
358
|
"""
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if idx >= 0:
|
|
359
|
-
line = line[:idx]
|
|
360
|
-
lines.append(line)
|
|
361
|
-
cleaned = " ".join(lines).strip()
|
|
362
|
-
|
|
363
|
-
if not cleaned:
|
|
359
|
+
from .graphql_validator import _tokenize
|
|
360
|
+
|
|
361
|
+
tokens = _tokenize(query)
|
|
362
|
+
if not tokens:
|
|
364
363
|
return []
|
|
365
364
|
|
|
366
|
-
ops = []
|
|
367
|
-
|
|
365
|
+
ops: list[str] = []
|
|
366
|
+
i = 0
|
|
368
367
|
|
|
369
|
-
while
|
|
370
|
-
|
|
371
|
-
while pos < len(cleaned) and cleaned[pos] in " \t\r\n":
|
|
372
|
-
pos += 1
|
|
373
|
-
if pos >= len(cleaned):
|
|
374
|
-
break
|
|
368
|
+
while i < len(tokens):
|
|
369
|
+
tok = tokens[i]
|
|
375
370
|
|
|
376
371
|
# Anonymous query starts with {
|
|
377
|
-
if
|
|
372
|
+
if tok.kind == "punct" and tok.value == "{":
|
|
378
373
|
ops.append("query")
|
|
379
|
-
end =
|
|
374
|
+
end = _skip_braced_tokens(tokens, i)
|
|
380
375
|
if end == -1:
|
|
381
376
|
break
|
|
382
|
-
|
|
377
|
+
i = end
|
|
383
378
|
continue
|
|
384
379
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
380
|
+
if tok.kind == "ident":
|
|
381
|
+
# Skip fragment definitions: fragment Name on Type { ... }
|
|
382
|
+
if tok.value == "fragment":
|
|
383
|
+
# Scan forward to the opening brace
|
|
384
|
+
j = i + 1
|
|
385
|
+
while j < len(tokens) and tokens[j].value != "{":
|
|
386
|
+
j += 1
|
|
387
|
+
if j >= len(tokens):
|
|
388
|
+
break
|
|
389
|
+
end = _skip_braced_tokens(tokens, j)
|
|
390
|
+
if end == -1:
|
|
391
|
+
break
|
|
392
|
+
i = end
|
|
393
|
+
continue
|
|
395
394
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
395
|
+
# Check for operation keyword
|
|
396
|
+
if tok.value.lower() in ("query", "mutation", "subscription"):
|
|
397
|
+
ops.append(tok.value.lower())
|
|
398
|
+
# Scan forward to the opening brace
|
|
399
|
+
j = i + 1
|
|
400
|
+
while j < len(tokens) and tokens[j].value != "{":
|
|
401
|
+
j += 1
|
|
402
|
+
if j >= len(tokens):
|
|
403
|
+
break
|
|
404
|
+
end = _skip_braced_tokens(tokens, j)
|
|
405
|
+
if end == -1:
|
|
406
|
+
break
|
|
407
|
+
i = end
|
|
408
|
+
continue
|
|
409
409
|
|
|
410
|
-
# Unrecognized
|
|
410
|
+
# Unrecognized token — stop parsing
|
|
411
411
|
break
|
|
412
412
|
|
|
413
413
|
return ops
|