browse-code 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
browse_code/cli.py ADDED
@@ -0,0 +1,75 @@
1
+ import os
2
+ import sys
3
+ import shutil
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ def get_data_dir():
8
+ home = Path.home()
9
+ data_dir = home / ".browse_code"
10
+ data_dir.mkdir(exist_ok=True)
11
+ return data_dir
12
+
13
+ def install_extension():
14
+ data_dir = get_data_dir()
15
+ installed_marker = data_dir / ".installed"
16
+ ext_dest = data_dir / "extension"
17
+
18
+ # Copy extension files from package to ~/.browse_code/extension
19
+ pkg_ext_dir = Path(__file__).parent / "extension"
20
+
21
+ if not installed_marker.exists():
22
+ ans_allow = input("Do you allow browse_code to install its Chrome extension? (y/n): ")
23
+ if ans_allow.lower() != 'y':
24
+ print("Extension installation skipped.")
25
+ return
26
+
27
+ print("Installing browse_code extension...")
28
+ if ext_dest.exists():
29
+ shutil.rmtree(ext_dest)
30
+ shutil.copytree(pkg_ext_dir, ext_dest)
31
+
32
+ print("\n========================================================")
33
+ print("Extension files copied to: ", ext_dest)
34
+ print("To load the extension in Chrome:")
35
+ print("1. Open Chrome and navigate to chrome://extensions/")
36
+ print("2. Enable 'Developer mode' at the top right")
37
+ print("3. Click 'Load unpacked' and select the folder:")
38
+ print(f" {ext_dest}")
39
+ print("========================================================\n")
40
+
41
+ ans = input("Do you want me to try launching Chrome with this extension now? (y/n): ")
42
+ if ans.lower() == 'y':
43
+ # Attempt to launch chrome
44
+ # On Windows:
45
+ chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
46
+ if not os.path.exists(chrome_path):
47
+ chrome_path = r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
48
+ if os.path.exists(chrome_path):
49
+ subprocess.Popen([chrome_path, f"--load-extension={ext_dest}"])
50
+ else:
51
+ print("Could not find Chrome executable automatically.")
52
+
53
+ installed_marker.touch()
54
+ else:
55
+ # Already installed, check if files are still there, if not recopy
56
+ if not ext_dest.exists():
57
+ shutil.copytree(pkg_ext_dir, ext_dest)
58
+
59
+ def main():
60
+ install_extension()
61
+
62
+ print("\nStarting AI session server...\n")
63
+ # Now we need to start the FastAPI server.
64
+ # The original server.py is in the same package
65
+ try:
66
+ from .server import app
67
+ except ImportError:
68
+ # Fallback if run directly during dev
69
+ from server import app
70
+
71
+ import uvicorn
72
+ uvicorn.run(app, host="127.0.0.1", port=5505, access_log=False)
73
+
74
+ if __name__ == "__main__":
75
+ main()
@@ -0,0 +1,356 @@
1
+ const LOCAL_SERVER = "http://127.0.0.1:5505";
2
+ const hostname = window.location.hostname;
3
+
4
+ let PLATFORM = {};
5
+
6
+ if (hostname.includes('gemini.google.com')) {
7
+ PLATFORM = {
8
+ name: "Gemini",
9
+ inputBox: 'rich-textarea div[contenteditable="true"], div[role="textbox"][contenteditable="true"]',
10
+ sendBtn: 'button[aria-label*="Send"], button[aria-label*="send"], button[mattooltip*="Send"]',
11
+ responseContainer: 'message-content, model-response, .model-response-text'
12
+ };
13
+ } else if (hostname.includes('claude.ai')) {
14
+ PLATFORM = {
15
+ name: "Claude",
16
+ inputBox: '.ProseMirror[contenteditable="true"]',
17
+ sendBtn: 'button[aria-label*="Send"], button[aria-label*="send"]',
18
+ responseContainer: '.font-claude-response, .font-claude-message'
19
+ };
20
+ } else if (hostname.includes('huggingface.co')) {
21
+ PLATFORM = {
22
+ name: "HuggingFace",
23
+ inputBox: 'input[name="prompt"]',
24
+ sendBtn: 'button[type="submit"]',
25
+ responseContainer: '.prose, .markdown, .break-words, [class*="message"]'
26
+ };
27
+ } else {
28
+ PLATFORM = {
29
+ name: "ChatGPT",
30
+ inputBox: '#prompt-textarea',
31
+ sendBtn: '[data-testid="send-button"], button[data-testid*="send"]',
32
+ responseContainer: '.markdown, .prose'
33
+ };
34
+ }
35
+
36
+ const spoofScript = document.createElement('script');
37
+ spoofScript.src = chrome.runtime.getURL('spoof.js');
38
+ (document.head || document.documentElement).appendChild(spoofScript);
39
+ spoofScript.onload = function () { this.remove(); };
40
+
41
+ const SYSTEM_PROMPT = `You are an elite, autonomous AI software engineer (similar to Antigravity or Devin) connected to a local execution bridge.
42
+ You do NOT have direct access to my file system. You interact with it ONLY by emitting the XML tool tags defined below, which are executed locally on my machine.
43
+
44
+ ═══════════════════════════════════════
45
+ GOLDEN RULES (violating any of these breaks the session)
46
+ ═══════════════════════════════════════
47
+ 1. STOP AFTER EVERY TOOL CALL. The moment you emit one or more tool tags, END YOUR TURN. Do not write "Result:", do not guess what the output will be, do not continue coding as if you already have it. A human/script will run the tool and give you the real output in the next message. Generating fake tool output is the single worst failure mode — never do it.
48
+ 2. NEVER claim a file was created, edited, or tested unless you emitted the corresponding tag THIS turn AND already received its result.
49
+ 3. NEVER fabricate file contents, line numbers, search results, or terminal output. If you haven't seen it via a tool result in this conversation, you don't know it.
50
+ 4. Explore before you edit. Default order: view_dir → search_code → read_lines → patch/write. Do not read a full file "just in case" — read only what your search told you to read.
51
+ 5. Prefer patch over write. Only use write for brand-new files or a full intentional rewrite. patch requires the <search> block to match the existing file EXACTLY (same whitespace/indentation) — read_lines first if you're not certain of the exact text.
52
+ 6. Terminals have no memory of directory state between calls. Every terminal_run/terminal_bg must include any necessary cd, chained with &&.
53
+ 7. **terminal_run is FULLY BLOCKING and HARD-CAPPED at 300s.** Under the hood it runs synchronously and only returns once the process exits completely — there is no streaming, no partial progress, nothing, until it's done or the 300s timeout fires. If it times out, you get a forced error containing only whatever STDOUT/STDERR happened to be captured before the kill — this is NOT a reliable signal of success or failure, just a fragment.
54
+ Because of this, terminal_run must NEVER be used for anything that installs dependencies, builds, scaffolds a project, or could plausibly run past a few seconds — e.g. npm/yarn/pnpm install, pip install, create-next-app, docker build, webpack/vite build, test suites with network calls, git clone of large repos, etc.
55
+ For ALL such commands: start with terminal_bg (returns a PID instantly, non-blocking), then poll with cron_monitor (delay 20-30s) — repeat cron_monitor while status is still "running." Only treat the command as done when a poll returns a completed/exited status.
56
+ If you ever see a terminal_run timeout error, do NOT re-run the same command with terminal_run again — restart it via terminal_bg instead.
57
+ terminal_run is only appropriate for short, fast, deterministic commands you're confident finish in a few seconds (e.g. ls, cat, a single quick lint check, git status).
58
+ 8. If a result is [TRUNCATED] or a file is large, narrow down with search_code or read_lines instead of re-requesting the whole file.
59
+
60
+ ═══════════════════════════════════════
61
+ WORKFLOW: "Autonomous Researcher"
62
+ ═══════════════════════════════════════
63
+ 1. **Explore:** view_dir to understand project structure.
64
+ 2. **Search:** search_code to grep for exactly which file/line defines a function or variable.
65
+ 3. **Inspect:** read_lines to read only the relevant chunk.
66
+ 4. **Modify:** patch to surgically replace targeted code. write only for brand-new files.
67
+ 5. **Test:** terminal_bg to run dev servers/tests; monitor with cron_monitor or terminal_logs.
68
+
69
+ ═══════════════════════════════════════
70
+ AVAILABLE TOOLS
71
+ ═══════════════════════════════════════
72
+ CRITICAL FORMAT RULE: Every individual tool call is wrapped in its OWN \`\`\`tool ... \`\`\` fence. If you issue multiple tool calls in one turn, output multiple separate \`\`\`tool\`\`\` blocks back to back — never combine two tags inside one fence.
73
+
74
+ | Tool | Format | Description |
75
+ |---|---|---|
76
+ | **View Dir** | \`\`\`tool\n<tool='view_dir'>src/components</tool>\n\`\`\` | Lists files. Leave empty for root. |
77
+ | **Search Code** | \`\`\`tool\n<tool='search_code' query='functionName'>src</tool>\n\`\`\` | Fast regex/string search. Returns file & line numbers. |
78
+ | **Read Lines** | \`\`\`tool\n<tool='read_lines' path='App.js' start='20' end='45'></tool>\n\`\`\` | Reads a specific chunk of a file. |
79
+ | **Read File** | \`\`\`tool\n<tool='read'>path/to/file.py</tool>\n\`\`\` | Reads a full file (avoid if file is large — prefer search_code + read_lines). |
80
+ | **Write File** | \`\`\`tool\n<tool='write' path='path.js'>\nRAW CODE\n</tool>\n\`\`\` | Creates/overwrites a file completely. |
81
+ | **Patch Code** | \`\`\`tool\n<tool='patch' path='main.py'>\n<search>\nOLD\n</search>\n<replace>\nNEW\n</replace>\n</tool>\n\`\`\` | Surgically replaces a block. <search> must match the file byte-for-byte. |
82
+ | **Run Terminal** | \`\`\`tool\n<tool='terminal_run'>npm test</tool>\n\`\`\` | BLOCKING, synchronous, hard 300s timeout. Only for short/fast commands (see Rule 7). |
83
+ | **Background Term**| \`\`\`tool\n<tool='terminal_bg'>npm run dev</tool>\n\`\`\` | Non-blocking. Returns a PID instantly. Required for installs/builds/servers. |
84
+ | **Cron Monitor** | \`\`\`tool\n<tool='cron_monitor' pid='1234' delay='15'></tool>\n\`\`\` | Sleeps X seconds, then returns the background PID's logs. Poll repeatedly until status is no longer "running." |
85
+ | **Term Logs** | \`\`\`tool\n<tool='terminal_logs' pid='1234'></tool>\n\`\`\` | Reads output of a background PID instantly. |
86
+ | **Kill Term** | \`\`\`tool\n<tool='terminal_kill' pid='1234'></tool>\n\`\`\` | Stops a background PID. |
87
+
88
+ ═══════════════════════════════════════
89
+ WORKED EXAMPLES
90
+ ═══════════════════════════════════════
91
+
92
+ --- Example A: code fix (stop-and-wait pattern) ---
93
+ User: "Fix the off-by-one in the pagination helper."
94
+
95
+ Your turn:
96
+ I'll locate the pagination logic first.
97
+ \`\`\`tool
98
+ <tool='search_code' query='pagination'>src</tool>
99
+ \`\`\`
100
+ [END TURN — wait for result]
101
+
102
+ Next turn (after receiving real results):
103
+ Found it in src/utils/paginate.js:14.
104
+ \`\`\`tool
105
+ <tool='read_lines' path='src/utils/paginate.js' start='1' end='30'></tool>
106
+ \`\`\`
107
+ [END TURN — wait for result]
108
+
109
+ Next turn (after receiving the real file contents):
110
+ \`\`\`tool
111
+ <tool='patch' path='src/utils/paginate.js'>
112
+ <search>
113
+ return start + pageSize;
114
+ </search>
115
+ <replace>
116
+ return start + pageSize - 1;
117
+ </replace>
118
+ </tool>
119
+ \`\`\`
120
+ [END TURN — wait for result. Only say "Fixed" after this result confirms success.]
121
+
122
+ --- Example B: long-running install (bg + poll pattern) ---
123
+ User: "Scaffold a new Next.js app called 'dashboard'."
124
+
125
+ Your turn:
126
+ This will download packages, so I'll run it in the background.
127
+ \`\`\`tool
128
+ <tool='terminal_bg'>npx create-next-app@latest dashboard --yes</tool>
129
+ \`\`\`
130
+ [END TURN — wait for PID]
131
+
132
+ Next turn (after receiving PID, e.g. 4821):
133
+ \`\`\`tool
134
+ <tool='cron_monitor' pid='4821' delay='25'></tool>
135
+ \`\`\`
136
+ [END TURN — wait for result]
137
+
138
+ Next turn: if status is still "running," issue cron_monitor again. Only once the log shows completion do you proceed or report success — never use terminal_run for this command, and never assume it finished just because time has passed.
139
+
140
+ ═══════════════════════════════════════
141
+ COMPLETION
142
+ ═══════════════════════════════════════
143
+ When the task is fully done and you have real tool results confirming every change, reply in plain text summarizing exactly what changed (files touched, what was added/removed/fixed, commands run and their confirmed final status) — no more and no less than what you actually executed and verified this session.
144
+
145
+ To confirm you understand these instructions, reply ONLY with: "Agent Initialized. Awaiting command."`;
146
+
147
+ let messageCount = 0;
148
+ let injectN = 5;
149
+
150
+ chrome.storage.local.get(['injectN'], (res) => {
151
+ if (res.injectN !== undefined) injectN = parseInt(res.injectN, 10);
152
+ });
153
+ chrome.storage.onChanged.addListener((changes, namespace) => {
154
+ if (namespace === 'local' && changes.injectN) {
155
+ injectN = parseInt(changes.injectN.newValue, 10);
156
+ }
157
+ });
158
+
159
+ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
160
+ sendResponse({ status: "received" });
161
+ if (request.action === "INITIALIZE_AGENT") {
162
+ messageCount = 0;
163
+ startNewChat()
164
+ .then(() => injectAndSend(SYSTEM_PROMPT))
165
+ .catch(err => console.error("Initialization sequence failed:", err));
166
+ }
167
+ return true;
168
+ });
169
+
170
+ async function startNewChat() {
171
+ if (PLATFORM.name === "HuggingFace") {
172
+ return Promise.resolve();
173
+ }
174
+
175
+ if (PLATFORM.name === "Gemini") {
176
+ const elements = document.querySelectorAll('span, div, a');
177
+ for (let el of elements) {
178
+ if (el.textContent.trim().toLowerCase() === 'new chat') {
179
+ el.closest('button, a, [role="button"]')?.click();
180
+ break;
181
+ }
182
+ }
183
+ } else if (PLATFORM.name === "Claude") {
184
+ const newChatBtn = document.querySelector('a[href="/new"], a[href="/chat/new"]');
185
+ if (newChatBtn) newChatBtn.click();
186
+ } else {
187
+ const newChatBtn = document.querySelector('a[href="/"], [data-testid="new-chat-button"]');
188
+ if (newChatBtn) newChatBtn.click();
189
+ }
190
+ await new Promise(r => setTimeout(r, 2000));
191
+ }
192
+
193
+ async function injectAndSend(promptText) {
194
+ let inputBox = null;
195
+ for (let i = 0; i < 30; i++) {
196
+ inputBox = document.querySelector(PLATFORM.inputBox);
197
+ if (inputBox) break;
198
+ await new Promise(r => setTimeout(r, 200));
199
+ }
200
+
201
+ if (!inputBox) return;
202
+ inputBox.focus();
203
+
204
+ if (inputBox.tagName === 'INPUT' || inputBox.tagName === 'TEXTAREA') {
205
+ inputBox.value = promptText;
206
+ } else {
207
+ const dataTransfer = new DataTransfer();
208
+ dataTransfer.setData('text/plain', promptText);
209
+ const pasteEvent = new ClipboardEvent('paste', {
210
+ clipboardData: dataTransfer,
211
+ bubbles: true,
212
+ cancelable: true
213
+ });
214
+
215
+ inputBox.dispatchEvent(pasteEvent);
216
+
217
+ if (inputBox.textContent.trim() === "") {
218
+ document.execCommand('insertText', false, promptText);
219
+ }
220
+ }
221
+
222
+ inputBox.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
223
+ inputBox.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
224
+
225
+ await new Promise(r => setTimeout(r, 800));
226
+
227
+ const sendButton = document.querySelector(PLATFORM.sendBtn);
228
+ if (sendButton && (!sendButton.disabled && sendButton.getAttribute('aria-disabled') !== 'true')) {
229
+ sendButton.click();
230
+ } else {
231
+ inputBox.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter' }));
232
+ }
233
+ }
234
+
235
+ // --- NEW: Robust Text Stability Engine ---
236
+ let isWaitingForLLM = false;
237
+ let lastProcessedMessage = "";
238
+ let trackInterval = null;
239
+
240
+ const messageQueue = [];
241
+ let isInjectingQueue = false;
242
+
243
+ function processQueue() {
244
+ if (messageQueue.length === 0 || isInjectingQueue || isWaitingForLLM) return;
245
+
246
+ let inputBox = document.querySelector(PLATFORM.inputBox);
247
+ if (!inputBox) return;
248
+
249
+ // Do not interrupt the user if they are currently typing a message
250
+ const userText = inputBox.tagName === 'INPUT' || inputBox.tagName === 'TEXTAREA' ? inputBox.value : inputBox.textContent;
251
+ if (userText && userText.trim() !== "") return;
252
+
253
+ // Do not inject if the LLM is currently generating (Send button is missing or disabled)
254
+ const sendButton = document.querySelector(PLATFORM.sendBtn);
255
+ if (!sendButton || sendButton.disabled || sendButton.getAttribute('aria-disabled') === 'true') {
256
+ return;
257
+ }
258
+
259
+ const nextMessage = messageQueue.shift();
260
+ isInjectingQueue = true;
261
+
262
+ injectAndSend(nextMessage).then(() => {
263
+ isInjectingQueue = false;
264
+ }).catch((err) => {
265
+ console.error("Queue injection failed", err);
266
+ isInjectingQueue = false;
267
+ });
268
+ }
269
+
270
+ // Primary observer loop: checks every 1 second
271
+ setInterval(() => {
272
+ processQueue();
273
+
274
+ if (isWaitingForLLM) return;
275
+
276
+ const allContainers = document.querySelectorAll(PLATFORM.responseContainer);
277
+ // Filter out hidden carousel messages
278
+ const containers = Array.from(allContainers).filter(el => el.offsetWidth > 0 && el.offsetHeight > 0);
279
+ if (containers.length === 0) return;
280
+
281
+ const lastContainer = containers[containers.length - 1];
282
+ if (lastContainer.hasAttribute('data-agent-processed')) return;
283
+
284
+ // Grab the very last message in the chat using textContent to avoid layout thrashing
285
+ const currentText = lastContainer.textContent || "";
286
+
287
+ // If the LLM has generated a closing tool tag that we haven't processed yet, lock it in and wait for it to finish typing
288
+ if (currentText.includes("</tool>")) {
289
+ isWaitingForLLM = true;
290
+ trackResponse(currentText);
291
+ }
292
+ }, 1000);
293
+
294
+ function trackResponse(initialText) {
295
+ let previousText = initialText;
296
+ let unchangedTicks = 0;
297
+
298
+ if (trackInterval) clearInterval(trackInterval);
299
+
300
+ // Sub-loop: monitors text generation speed
301
+ trackInterval = setInterval(async () => {
302
+ const allContainers = document.querySelectorAll(PLATFORM.responseContainer);
303
+ const containers = Array.from(allContainers).filter(el => el.offsetWidth > 0 && el.offsetHeight > 0);
304
+ const lastContainer = containers.length > 0 ? containers[containers.length - 1] : null;
305
+ if (!lastContainer) return;
306
+
307
+ const currentText = lastContainer.textContent || "";
308
+
309
+ // Check if the LLM is still typing
310
+ if (currentText.length > previousText.length) {
311
+ previousText = currentText;
312
+ unchangedTicks = 0;
313
+ } else {
314
+ unchangedTicks++;
315
+ }
316
+
317
+ // If text has not changed for 2.5 seconds (5 ticks), assume generation is complete!
318
+ if (unchangedTicks > 4) {
319
+ clearInterval(trackInterval);
320
+ lastContainer.setAttribute('data-agent-processed', 'true');
321
+ isWaitingForLLM = false; // UNLOCK IMMEDIATELY to prevent deadlocks from long-running tools
322
+
323
+
324
+ const toolMatches = currentText.match(/<tool=[\s\S]*?<\/tool>/g);
325
+ if (toolMatches && toolMatches.length > 0) {
326
+ try {
327
+ const toolPromises = toolMatches.map(async (toolCall) => {
328
+ const response = await fetch(`${LOCAL_SERVER}/extension/run-tool`, {
329
+ method: 'POST',
330
+ headers: { 'Content-Type': 'application/json' },
331
+ body: JSON.stringify({ tool_call: toolCall })
332
+ });
333
+ const resultData = await response.json();
334
+
335
+ const toolNameMatch = toolCall.match(/<tool='(.*?)'/);
336
+ const toolName = toolNameMatch ? toolNameMatch[1] : 'unknown';
337
+
338
+ return `\n[System - Tool Execution: ${toolName}]\nStatus: Success\n------------------------------------\nResult Output:\n${resultData.output}\n------------------------------------\n`;
339
+ });
340
+
341
+ const results = await Promise.all(toolPromises);
342
+ let combinedFeedback = results.join("") + "Instruction: Review the output above. If the task requires more steps, continue. If finished, summarize what was done.";
343
+
344
+ messageCount++;
345
+ if (injectN > 0 && messageCount % injectN === 0) {
346
+ combinedFeedback += `\n\n[System Reminder (Context Refresh)]:\n${SYSTEM_PROMPT}`;
347
+ }
348
+
349
+ messageQueue.push(combinedFeedback);
350
+ } catch (err) {
351
+ messageQueue.push(`\n[System - Error]: Tool execution failed. Ensure your Python backend is running.`);
352
+ }
353
+ }
354
+ }
355
+ }, 500);
356
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Local Agent Bridge",
4
+ "version": "2.0",
5
+ "description": "Autonomous AI Code Execution Bridge",
6
+ "permissions": [
7
+ "activeTab",
8
+ "storage"
9
+ ],
10
+ "content_scripts": [
11
+ {
12
+ "matches": [
13
+ "*://*.chatgpt.com/*",
14
+ "*://gemini.google.com/*",
15
+ "*://*.claude.ai/*",
16
+ "*://*.huggingface.co/*"
17
+ ],
18
+ "js": ["content.js"]
19
+ }
20
+ ],
21
+ "action": {
22
+ "default_popup": "popup.html"
23
+ },
24
+ "web_accessible_resources": [
25
+ {
26
+ "resources": ["spoof.js"],
27
+ "matches": ["<all_urls>"]
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,109 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <style>
6
+ body {
7
+ font-family: 'Segoe UI', sans-serif;
8
+ width: 350px;
9
+ padding: 15px;
10
+ margin: 0;
11
+ background-color: #0f172a;
12
+ color: #f8fafc;
13
+ }
14
+
15
+ h3 {
16
+ margin-top: 0;
17
+ font-size: 16px;
18
+ border-bottom: 1px solid #334155;
19
+ padding-bottom: 5px;
20
+ }
21
+
22
+ label {
23
+ font-size: 12px;
24
+ font-weight: bold;
25
+ color: #94a3b8;
26
+ }
27
+
28
+ input {
29
+ width: 100%;
30
+ box-sizing: border-box;
31
+ padding: 8px;
32
+ margin: 8px 0;
33
+ background: #1e293b;
34
+ border: 1px solid #475569;
35
+ color: white;
36
+ border-radius: 4px;
37
+ }
38
+
39
+ button {
40
+ width: 100%;
41
+ padding: 10px;
42
+ font-weight: bold;
43
+ border: none;
44
+ border-radius: 5px;
45
+ cursor: pointer;
46
+ transition: 0.2s;
47
+ }
48
+
49
+ #save-btn {
50
+ background-color: #3b82f6;
51
+ color: white;
52
+ margin-bottom: 15px;
53
+ }
54
+
55
+ #init-btn {
56
+ background-color: #10b981;
57
+ color: white;
58
+ margin-bottom: 15px;
59
+ }
60
+
61
+ .dashboard {
62
+ background: #1e293b;
63
+ padding: 10px;
64
+ border-radius: 5px;
65
+ font-size: 12px;
66
+ }
67
+
68
+ .process-card {
69
+ background: #000;
70
+ border: 1px solid #334155;
71
+ padding: 8px;
72
+ margin-top: 8px;
73
+ border-radius: 4px;
74
+ }
75
+
76
+ .pid {
77
+ color: #f59e0b;
78
+ font-weight: bold;
79
+ }
80
+
81
+ .log-preview {
82
+ font-family: monospace;
83
+ color: #a3e635;
84
+ font-size: 10px;
85
+ white-space: pre-wrap;
86
+ overflow-x: hidden;
87
+ margin-top: 5px;
88
+ }
89
+ </style>
90
+ </head>
91
+
92
+ <body>
93
+ <h3>Agent Dashboard</h3>
94
+ <label>Workspace Directory:</label>
95
+ <input type="text" id="dir-input" placeholder="C:\Projects\app">
96
+ <label>Inject Prompt Every N Messages (0 to disable):</label>
97
+ <input type="number" id="inject-n-input" placeholder="5" value="5" min="0">
98
+ <button id="save-btn">Save Directory</button>
99
+ <button id="init-btn">Initialize Agent in Chat</button>
100
+
101
+ <h3>Live Terminals</h3>
102
+ <div class="dashboard" id="process-list">
103
+ <div style="color: #94a3b8;">No active background processes...</div>
104
+ </div>
105
+
106
+ <script src="popup.js"></script>
107
+ </body>
108
+
109
+ </html>
@@ -0,0 +1,90 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const dirInput = document.getElementById('dir-input');
3
+ const injectNInput = document.getElementById('inject-n-input');
4
+ const saveBtn = document.getElementById('save-btn');
5
+ const initBtn = document.getElementById('init-btn');
6
+ const processList = document.getElementById('process-list');
7
+
8
+ chrome.storage.local.get(['workspaceDir', 'injectN'], (result) => {
9
+ if (result.workspaceDir) {
10
+ dirInput.value = result.workspaceDir;
11
+ syncWithServer(result.workspaceDir);
12
+ }
13
+ if (result.injectN !== undefined && injectNInput) {
14
+ injectNInput.value = result.injectN;
15
+ }
16
+ });
17
+
18
+ saveBtn.addEventListener('click', () => {
19
+ const path = dirInput.value.trim();
20
+ let injectN = 5;
21
+ if (injectNInput) {
22
+ injectN = parseInt(injectNInput.value, 10);
23
+ if (isNaN(injectN)) injectN = 5;
24
+ }
25
+ chrome.storage.local.set({ workspaceDir: path, injectN: injectN });
26
+ syncWithServer(path);
27
+ saveBtn.innerText = '✅ Saved!';
28
+ setTimeout(() => saveBtn.innerText = 'Save Directory', 2000);
29
+ });
30
+
31
+ initBtn.addEventListener('click', () => {
32
+ initBtn.innerText = 'Initializing...';
33
+ chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
34
+ const activeTab = tabs[0];
35
+ const validUrls = ['chatgpt.com', 'gemini.google.com', 'claude.ai', 'huggingface.co'];
36
+
37
+ if (activeTab && validUrls.some(url => activeTab.url.includes(url))) {
38
+ chrome.tabs.sendMessage(activeTab.id, { action: "INITIALIZE_AGENT" }, () => {
39
+ if (chrome.runtime.lastError) {
40
+ initBtn.innerText = '❌ Error: Refresh Tab First!';
41
+ setTimeout(() => initBtn.innerText = '✨ Initialize Agent in Chat', 3000);
42
+ } else {
43
+ initBtn.innerText = '✨ Initialize Agent in Chat';
44
+ }
45
+ });
46
+ } else {
47
+ initBtn.innerText = '❌ Not a valid AI chat tab';
48
+ setTimeout(() => initBtn.innerText = '✨ Initialize Agent in Chat', 3000);
49
+ }
50
+ });
51
+ });
52
+
53
+ function saveWorkspace(path) {
54
+ fetch('http://127.0.0.1:5505/set-workspace', {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify({ path: path })
58
+ }).catch(() => {
59
+ // Silently ignore connection errors so Chrome doesn't flag them in the extension dashboard
60
+ });
61
+ }
62
+
63
+ function updateStatus() {
64
+ fetch('http://127.0.0.1:5505/status')
65
+ .then(res => res.json())
66
+ .then(data => {
67
+ if (!data.processes || data.processes.length === 0) {
68
+ processList.innerHTML = '<div style="color: #94a3b8;">No active background processes...</div>';
69
+ return;
70
+ }
71
+
72
+ let html = '';
73
+ data.processes.forEach(proc => {
74
+ const statusColor = proc.status === 'running' ? '#10b981' : '#ef4444';
75
+ html += `
76
+ <div class="process-card">
77
+ <div><span class="pid">[PID: ${proc.pid}]</span> - <span style="color:${statusColor}">${proc.status.toUpperCase()}</span></div>
78
+ <div class="log-preview">${proc.logs || 'Waiting for output...'}</div>
79
+ </div>`;
80
+ });
81
+ processList.innerHTML = html;
82
+ })
83
+ .catch(() => {
84
+ processList.innerHTML = '<div style="color: #ef4444;">Disconnected from local server.</div>';
85
+ });
86
+ }
87
+
88
+ setInterval(pollStatus, 1500);
89
+ pollStatus();
90
+ });
@@ -0,0 +1,3 @@
1
+ Object.defineProperty(document, 'visibilityState', { get: () => 'visible' });
2
+ Object.defineProperty(document, 'hidden', { get: () => false });
3
+ window.addEventListener('visibilitychange', (e) => e.stopImmediatePropagation(), true);
browse_code/server.py ADDED
@@ -0,0 +1,574 @@
1
+ import os
2
+ import re
3
+ import subprocess
4
+ import difflib
5
+ import threading
6
+ import uuid
7
+ import atexit
8
+ import asyncio
9
+ import shutil
10
+ import time
11
+ import platform
12
+ from collections import deque
13
+ from contextlib import asynccontextmanager
14
+ from fastapi import FastAPI, HTTPException, Request
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from pydantic import BaseModel
17
+
18
+ @asynccontextmanager
19
+ async def lifespan(app: FastAPI):
20
+ print_startup_banner(WORKSPACE_DIR, "127.0.0.1", 5505)
21
+ yield
22
+
23
+
24
+ app = FastAPI(title="Agent Bridge Backend", lifespan=lifespan)
25
+
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["*"],
29
+ allow_credentials=True,
30
+ allow_methods=["*"],
31
+ allow_headers=["*"],
32
+ )
33
+
34
+ # ============================================================
35
+ # Shared ANSI palette — used by the diff engine, tool banners,
36
+ # request logger, and startup banner.
37
+ # ============================================================
38
+ C_RESET = "\033[0m"
39
+ C_BOLD = "\033[1m"
40
+ C_DIM = "\033[2;90m"
41
+ C_GRAY = "\033[90m"
42
+ C_MUTED = "\033[38;5;245m"
43
+ C_HEADER = "\033[1;32m"
44
+ C_GREEN = "\033[32m"
45
+
46
+ C_OK = "\033[1;32m" # success / 2xx
47
+ C_ERR = "\033[1;31m" # error / 5xx
48
+ C_WARN = "\033[1;33m" # warn / 4xx
49
+ C_INFO = "\033[1;34m" # info
50
+
51
+ C_ADD_FG = "\033[38;5;150m"
52
+ C_ADD_BG = "\033[48;2;0;40;0m"
53
+ C_ADD_MARK = "\033[1;38;5;114m\033[48;2;0;40;0m"
54
+ C_ADD_NUM = "\033[32m"
55
+ C_DEL_FG = "\033[38;5;210m"
56
+ C_DEL_BG = "\033[48;2;45;0;0m"
57
+ C_DEL_MARK = "\033[1;38;5;203m\033[48;2;45;0;0m"
58
+ C_DEL_NUM = "\033[31m"
59
+
60
+ # icon + friendly label per tool tag, used by the tool-call banner
61
+ TOOL_META = {
62
+ "view_dir": ("📁", "View Directory"),
63
+ "read": ("📄", "Read File"),
64
+ "read_lines": ("📄", "Read Lines"),
65
+ "write": ("✍️ ", "Write File"),
66
+ "patch": ("🩹", "Patch File"),
67
+ "search_code": ("🔍", "Search Code"),
68
+ "terminal_run": ("💻", "Run Command"),
69
+ "terminal_bg": ("⚙️ ", "Background Process"),
70
+ "terminal_logs": ("📜", "Process Logs"),
71
+ "terminal_kill": ("✋", "Kill Process"),
72
+ "cron_monitor": ("⏰", "Monitor Process"),
73
+ }
74
+
75
+
76
+ def print_tool_start(tool_name: str, detail: str = ""):
77
+ """Print a compact, colored banner when a tool call begins."""
78
+ icon, label = TOOL_META.get(tool_name, ("🔧", tool_name or "Unknown Tool"))
79
+ line = f"\n{C_HEADER}{icon} {label}{C_RESET}"
80
+ if detail:
81
+ detail = detail.strip().replace("\n", " ⏎ ")
82
+ if len(detail) > 90:
83
+ detail = detail[:89] + "…"
84
+ line += f" {C_DIM}{detail}{C_RESET}"
85
+ print(line)
86
+
87
+
88
+ def print_tool_end(ok: bool, elapsed_ms: float, note: str = ""):
89
+ """Print a compact status line when a tool call finishes."""
90
+ if ok:
91
+ badge = f"{C_OK}✓ done{C_RESET}"
92
+ else:
93
+ badge = f"{C_ERR}✗ failed{C_RESET}"
94
+ timing = f"{C_DIM}{elapsed_ms:.0f}ms{C_RESET}"
95
+ line = f" {badge} {timing}"
96
+ if note:
97
+ note = note.strip().replace("\n", " ⏎ ")
98
+ if len(note) > 90:
99
+ note = note[:89] + "…"
100
+ line += f" {C_MUTED}{note}{C_RESET}"
101
+ print(line)
102
+
103
+
104
+ def print_startup_banner(workspace: str, host: str, port: int):
105
+ width = 78
106
+ title = " Agent Bridge Backend "
107
+ pad = width - len(title)
108
+ left, right = pad // 2, pad - pad // 2
109
+ print()
110
+ print(f"{C_HEADER}╭{'─' * left}{title}{'─' * right}╮{C_RESET}")
111
+ ws_text = f" Workspace {workspace}"
112
+ ws_pad = max(0, width - len(ws_text))
113
+ print(f"{C_HEADER}│{C_RESET} {C_MUTED}Workspace{C_RESET} {workspace}{' ' * ws_pad}{C_HEADER}│{C_RESET}")
114
+ ep_text = f" Endpoint http://{host}:{port}"
115
+ ep_pad = max(0, width - len(ep_text))
116
+ print(f"{C_HEADER}│{C_RESET} {C_MUTED}Endpoint {C_RESET} http://{host}:{port}{' ' * ep_pad}{C_HEADER}│{C_RESET}")
117
+ print(f"{C_HEADER}╰{'─' * width}╯{C_RESET}")
118
+ print()
119
+
120
+
121
+ WORKSPACE_DIR = r"C:\Users\dedeep vasireddy\Downloads\test"
122
+ BACKGROUND_PROCESSES = {}
123
+
124
+ def cleanup_background_processes():
125
+ print(f"\n{C_WARN}⏻ Shutting down — cleaning up background processes...{C_RESET}")
126
+ for pid, data in BACKGROUND_PROCESSES.items():
127
+ if data['status'] == 'running':
128
+ try:
129
+ data['process'].terminate()
130
+ except:
131
+ pass
132
+
133
+ atexit.register(cleanup_background_processes)
134
+
135
+
136
+ @app.middleware("http")
137
+ async def request_logger(request: Request, call_next):
138
+ """Compact single-line request logger, replacing uvicorn's default access log."""
139
+ start = time.perf_counter()
140
+ response = await call_next(request)
141
+ elapsed_ms = (time.perf_counter() - start) * 1000
142
+
143
+ status = response.status_code
144
+ if status < 300:
145
+ color = C_OK
146
+ elif status < 500:
147
+ color = C_WARN
148
+ else:
149
+ color = C_ERR
150
+
151
+ # The /extension/run-tool endpoint already prints its own detailed
152
+ # banner + result, so skip the generic line there to avoid duplication.
153
+ if request.url.path != "/extension/run-tool":
154
+ print(f"{C_DIM}→{C_RESET} {request.method} {request.url.path} "
155
+ f"{color}{status}{C_RESET} {C_DIM}{elapsed_ms:.0f}ms{C_RESET}")
156
+
157
+ return response
158
+
159
+
160
+ # ============================================================
161
+ # TUI Block Diff Engine (v3) — single-gutter, full-width highlight
162
+ # style, matching editor-style inline diffs (Claude Code / VS Code).
163
+ # ============================================================
164
+
165
+
166
+ def _terminal_width(default: int = 72, cap: int = 88) -> int:
167
+ # NOTE: this process's stdout is usually not a real TTY (it's a FastAPI
168
+ # server, often piped/redirected), so shutil.get_terminal_size() falls
169
+ # back to `default` rather than reflecting the actual viewer width. Keep
170
+ # `default` conservative — if it's too wide, the padded highlight bar
171
+ # wraps in the viewer and bleeds color onto the next visual line.
172
+ try:
173
+ size = shutil.get_terminal_size(fallback=(default, 20))
174
+ cols = size.columns
175
+ # A fallback tuple and a "real" detected size look identical here,
176
+ # so only trust an unusually small detected width; otherwise stay
177
+ # conservative rather than risk wrapping.
178
+ if cols <= 0:
179
+ cols = default
180
+ except Exception:
181
+ cols = default
182
+ return max(40, min(cols, cap, default))
183
+
184
+
185
+ def _clip(text: str, max_len: int) -> str:
186
+ if max_len <= 1:
187
+ return text[:max_len]
188
+ if len(text) > max_len:
189
+ return text[: max_len - 1] + "…"
190
+ return text
191
+
192
+
193
+ def print_terminal_diff(filepath: str, old_content: str, new_content: str, context_lines: int = 2,
194
+ large_line_threshold: int = 300, large_change_threshold: int = 120):
195
+ """Render a single-gutter, full-width-highlight diff (Claude Code TUI style).
196
+
197
+ For large diffs, unchanged/context lines are suppressed entirely (only a
198
+ collapsed "N unchanged lines" marker is shown) to keep the output readable.
199
+ """
200
+ old_lines = old_content.splitlines()
201
+ new_lines = new_content.splitlines()
202
+ matcher = difflib.SequenceMatcher(None, old_lines, new_lines)
203
+ opcodes = matcher.get_opcodes()
204
+
205
+ additions = sum(j2 - j1 for tag, i1, i2, j1, j2 in opcodes if tag in ("insert", "replace"))
206
+ deletions = sum(i2 - i1 for tag, i1, i2, j1, j2 in opcodes if tag in ("delete", "replace"))
207
+ is_new_file = old_content == "" and new_content != ""
208
+
209
+ # Large diffs: drop context lines entirely, show changed lines only.
210
+ total_lines = max(len(old_lines), len(new_lines))
211
+ total_changes = additions + deletions
212
+ if total_lines > large_line_threshold or total_changes > large_change_threshold:
213
+ context_lines = 0
214
+
215
+ width = _terminal_width()
216
+ num_width = max(len(str(max(len(old_lines), len(new_lines), 1))), 2)
217
+ content_width = max(10, width - num_width - 6) # " N m code" + safety margin
218
+
219
+ # ---- Header: "filename +N -N" -----------------------------
220
+ tag_label = " (new file)" if is_new_file else ""
221
+ print()
222
+ header = f"{C_HEADER}{filepath}{C_RESET}{C_DIM}{tag_label}{C_RESET}"
223
+ stats = f" {C_ADD_NUM}+{additions}{C_RESET} {C_DEL_NUM}-{deletions}{C_RESET}"
224
+ print(f"{header}{stats}")
225
+ print(f"{C_GRAY}{'─' * width}{C_RESET}")
226
+
227
+ if not opcodes or (len(opcodes) == 1 and opcodes[0][0] == "equal"):
228
+ print(f"{C_DIM} (no changes){C_RESET}\n")
229
+ return
230
+
231
+ def row(number, marker, text, bg="", fg="", mark_style=""):
232
+ num_s = f"{number:>{num_width}}" if number is not None else " " * num_width
233
+ body = _clip(text, content_width)
234
+ if bg:
235
+ # pad body so the background color fills the full row width
236
+ padded = body.ljust(content_width)
237
+ m = mark_style if mark_style else fg
238
+ print(f"{bg}{fg} {num_s} {m}{marker}{fg} {padded} {C_RESET}")
239
+ else:
240
+ print(f"{C_MUTED} {num_s} {C_GRAY}{marker}{C_RESET} {body}")
241
+
242
+ def gap_marker(hidden):
243
+ print(f"{C_DIM} ⋮ {hidden} unchanged line{'s' if hidden != 1 else ''}{C_RESET}")
244
+
245
+ new_ln = 1 # running "position in final file" counter, drives the single gutter
246
+
247
+ for tag, i1, i2, j1, j2 in opcodes:
248
+ if tag == "equal":
249
+ block_len = i2 - i1
250
+ if block_len == 0:
251
+ continue
252
+
253
+ if context_lines <= 0:
254
+ # Large diff mode: skip unchanged lines entirely, just show the gap.
255
+ new_ln += block_len
256
+ gap_marker(block_len)
257
+ continue
258
+
259
+ if block_len > context_lines * 2:
260
+ for k in range(context_lines):
261
+ row(new_ln, " ", old_lines[i1 + k])
262
+ new_ln += 1
263
+ hidden = block_len - context_lines * 2
264
+ new_ln += hidden # skip numbering for hidden lines
265
+ gap_marker(hidden)
266
+ for k in range(block_len - context_lines, block_len):
267
+ row(new_ln, " ", old_lines[i1 + k])
268
+ new_ln += 1
269
+ else:
270
+ for k in range(block_len):
271
+ row(new_ln, " ", old_lines[i1 + k])
272
+ new_ln += 1
273
+ continue
274
+
275
+ if tag in ("delete", "replace"):
276
+ for i in range(i1, i2):
277
+ row(new_ln, "-", old_lines[i], bg=C_DEL_BG, fg=C_DEL_FG, mark_style=C_DEL_MARK)
278
+
279
+ if tag in ("insert", "replace"):
280
+ for j in range(j1, j2):
281
+ row(new_ln, "+", new_lines[j], bg=C_ADD_BG, fg=C_ADD_FG, mark_style=C_ADD_MARK)
282
+ new_ln += 1
283
+
284
+
285
+ def strip_ansi(text: str) -> str:
286
+ ansi_escape = re.compile(r'\x1b\[([0-9]{1,3}(;[0-9]{1,3})*)?[m|K]')
287
+ return ansi_escape.sub('', text)
288
+
289
+ def truncate_output(text: str, max_chars=12000) -> str:
290
+ if len(text) > max_chars:
291
+ return text[:max_chars] + f"\n\n...[OUTPUT TRUNCATED]"
292
+ return text
293
+
294
+ def stream_process_output(pid: str, process: subprocess.Popen):
295
+ try:
296
+ for line in iter(process.stdout.readline, ''):
297
+ BACKGROUND_PROCESSES[pid]['logs'].append(line)
298
+ except Exception as e:
299
+ BACKGROUND_PROCESSES[pid]['logs'].append(f"\n[Stream Error: {str(e)}]\n")
300
+ finally:
301
+ process.stdout.close()
302
+ BACKGROUND_PROCESSES[pid]['status'] = 'exited'
303
+
304
+ def secure_path(path: str) -> str:
305
+ filepath = os.path.abspath(os.path.join(WORKSPACE_DIR, path))
306
+ if not filepath.startswith(os.path.abspath(WORKSPACE_DIR)):
307
+ raise HTTPException(status_code=400, detail="Security Error: Path traversal detected.")
308
+ return filepath
309
+
310
+ class WorkspaceModel(BaseModel):
311
+ path: str
312
+
313
+ class ToolModel(BaseModel):
314
+ tool_call: str
315
+
316
+ @app.post("/set-workspace")
317
+ async def set_workspace(data: WorkspaceModel):
318
+ global WORKSPACE_DIR
319
+ new_path = data.path.strip()
320
+ if new_path and os.path.exists(new_path):
321
+ WORKSPACE_DIR = new_path
322
+ print(f"\n[Workspace Updated]: {WORKSPACE_DIR}\n")
323
+ return {"status": "success", "workspace": WORKSPACE_DIR}
324
+ raise HTTPException(status_code=400, detail="Invalid workspace path")
325
+
326
+ @app.get("/status")
327
+ async def get_status():
328
+ active_procs = []
329
+ for pid, data in BACKGROUND_PROCESSES.items():
330
+ recent_logs = list(data['logs'])[-5:]
331
+ active_procs.append({
332
+ "pid": pid,
333
+ "status": data['status'],
334
+ "logs": strip_ansi("".join(recent_logs))
335
+ })
336
+ return {
337
+ "workspace": WORKSPACE_DIR,
338
+ "processes": active_procs
339
+ }
340
+
341
+ def _identify_tool(tool_raw: str):
342
+ """Parse which tool tag was used and build a short human-readable detail string."""
343
+ m = re.search(r"<tool='view_dir'>(.*?)</tool>", tool_raw, re.DOTALL)
344
+ if m:
345
+ return "view_dir", (m.group(1).strip() or ".")
346
+
347
+ m = re.search(r"<tool='read'>(.*?)</tool>", tool_raw, re.DOTALL)
348
+ if m:
349
+ return "read", m.group(1).strip()
350
+
351
+ m = re.search(r"<tool='write'\s+path='(.*?)'>(.*?)</tool>", tool_raw, re.DOTALL)
352
+ if m:
353
+ return "write", m.group(1).strip()
354
+
355
+ m = re.search(r"<tool='patch'\s+path='(.*?)'>\s*<search>(.*?)</search>\s*<replace>(.*?)</replace>\s*</tool>", tool_raw, re.DOTALL)
356
+ if m:
357
+ return "patch", m.group(1).strip()
358
+
359
+ m = re.search(r"<tool='search_code'\s+query='(.*?)'>(.*?)</tool>", tool_raw, re.DOTALL)
360
+ if m:
361
+ return "search_code", f"\"{m.group(1).strip()}\""
362
+
363
+ m = re.search(r"<tool='read_lines'\s+path='(.*?)'\s+start='(\d+)'\s+end='(\d+)'></tool>", tool_raw, re.DOTALL)
364
+ if m:
365
+ return "read_lines", f"{m.group(1).strip()} L{m.group(2)}-{m.group(3)}"
366
+
367
+ m = re.search(r"<tool='terminal_run'>(.*?)</tool>", tool_raw, re.DOTALL)
368
+ if m:
369
+ return "terminal_run", m.group(1).strip()
370
+
371
+ m = re.search(r"<tool='terminal_bg'>(.*?)</tool>", tool_raw, re.DOTALL)
372
+ if m:
373
+ return "terminal_bg", m.group(1).strip()
374
+
375
+ m = re.search(r"<tool='terminal_logs'\s+pid='(.*?)'></tool>", tool_raw, re.DOTALL)
376
+ if m:
377
+ return "terminal_logs", f"pid={m.group(1).strip()}"
378
+
379
+ m = re.search(r"<tool='terminal_kill'\s+pid='(.*?)'></tool>", tool_raw, re.DOTALL)
380
+ if m:
381
+ return "terminal_kill", f"pid={m.group(1).strip()}"
382
+
383
+ m = re.search(r"<tool='cron_monitor'\s+pid='(.*?)'\s+delay='(\d+)'></tool>", tool_raw, re.DOTALL)
384
+ if m:
385
+ return "cron_monitor", f"pid={m.group(1).strip()} delay={m.group(2)}s"
386
+
387
+ return None, ""
388
+
389
+
390
+ def _short_preview(output: str, max_len: int = 90) -> str:
391
+ if not output:
392
+ return ""
393
+ first_line = str(output).splitlines()[0] if output else ""
394
+ if len(first_line) > max_len:
395
+ first_line = first_line[: max_len - 1] + "…"
396
+ return first_line
397
+
398
+
399
+ @app.post("/extension/run-tool")
400
+ async def run_tool(data: ToolModel):
401
+ global WORKSPACE_DIR
402
+ tool_raw = data.tool_call
403
+
404
+ tool_name, detail = _identify_tool(tool_raw)
405
+ print_tool_start(tool_name, detail)
406
+ start = time.perf_counter()
407
+
408
+ view_match = re.search(r"<tool='view_dir'>(.*?)</tool>", tool_raw, re.DOTALL)
409
+ read_match = re.search(r"<tool='read'>(.*?)</tool>", tool_raw, re.DOTALL)
410
+ write_match = re.search(r"<tool='write'\s+path='(.*?)'>(.*?)</tool>", tool_raw, re.DOTALL)
411
+ patch_match = re.search(r"<tool='patch'\s+path='(.*?)'>\s*<search>(.*?)</search>\s*<replace>(.*?)</replace>\s*</tool>", tool_raw, re.DOTALL)
412
+ search_code_match = re.search(r"<tool='search_code'\s+query='(.*?)'>(.*?)</tool>", tool_raw, re.DOTALL)
413
+ read_lines_match = re.search(r"<tool='read_lines'\s+path='(.*?)'\s+start='(\d+)'\s+end='(\d+)'></tool>", tool_raw, re.DOTALL)
414
+ term_run_match = re.search(r"<tool='terminal_run'>(.*?)</tool>", tool_raw, re.DOTALL)
415
+ term_bg_match = re.search(r"<tool='terminal_bg'>(.*?)</tool>", tool_raw, re.DOTALL)
416
+ term_logs_match = re.search(r"<tool='terminal_logs'\s+pid='(.*?)'></tool>", tool_raw, re.DOTALL)
417
+ term_kill_match = re.search(r"<tool='terminal_kill'\s+pid='(.*?)'></tool>", tool_raw, re.DOTALL)
418
+ cron_match = re.search(r"<tool='cron_monitor'\s+pid='(.*?)'\s+delay='(\d+)'></tool>", tool_raw, re.DOTALL)
419
+
420
+ try:
421
+ if view_match:
422
+ sub_dir = view_match.group(1).strip()
423
+ target_dir = secure_path(sub_dir) if sub_dir else WORKSPACE_DIR
424
+ tree = []
425
+ for root, dirs, files in os.walk(target_dir):
426
+ for f in files:
427
+ rel_path = os.path.relpath(os.path.join(root, f), WORKSPACE_DIR)
428
+ if not any(part.startswith('.') or part == 'node_modules' or part == '__pycache__' for part in rel_path.split(os.sep)):
429
+ tree.append(rel_path)
430
+ result = {"output": truncate_output("\n".join(tree) if tree else "Directory is empty.")}
431
+
432
+ elif read_match:
433
+ filepath = secure_path(read_match.group(1).strip())
434
+ if not os.path.exists(filepath):
435
+ result = {"output": "Error: File not found."}
436
+ else:
437
+ with open(filepath, 'r', encoding='utf-8') as f:
438
+ result = {"output": truncate_output(f.read())}
439
+
440
+ elif read_lines_match:
441
+ filepath = secure_path(read_lines_match.group(1).strip())
442
+ start_line, end_line = int(read_lines_match.group(2)), int(read_lines_match.group(3))
443
+ if not os.path.exists(filepath):
444
+ result = {"output": "Error: File not found."}
445
+ else:
446
+ with open(filepath, 'r', encoding='utf-8') as f:
447
+ lines = f.readlines()
448
+ chunk = lines[max(0, start_line-1) : end_line]
449
+ formatted_chunk = "".join([f"{i+start_line}: {line}" for i, line in enumerate(chunk)])
450
+ result = {"output": truncate_output(f"--- Lines {start_line} to {end_line} ---\n{formatted_chunk}")}
451
+
452
+ elif search_code_match:
453
+ query, sub_dir = search_code_match.group(1).strip(), search_code_match.group(2).strip()
454
+ target_dir = secure_path(sub_dir) if sub_dir else WORKSPACE_DIR
455
+ results = []
456
+ for root, dirs, files in os.walk(target_dir):
457
+ if any(part.startswith('.') or part == 'node_modules' for part in root.split(os.sep)): continue
458
+ for f in files:
459
+ filepath = os.path.join(root, f)
460
+ rel_path = os.path.relpath(filepath, WORKSPACE_DIR)
461
+ try:
462
+ with open(filepath, 'r', encoding='utf-8') as file:
463
+ for line_num, line in enumerate(file, 1):
464
+ if query in line: results.append(f"{rel_path}:{line_num}: {line.strip()}")
465
+ except: pass
466
+ result = {"output": truncate_output("\n".join(results) if results else "No matches found.")}
467
+
468
+ elif write_match:
469
+ path = write_match.group(1).strip()
470
+ filepath = secure_path(path)
471
+ content = re.sub(r'^```[a-zA-Z]*\n|\n```$', '', write_match.group(2).strip(), flags=re.MULTILINE)
472
+
473
+ old_content = ""
474
+ if os.path.exists(filepath):
475
+ with open(filepath, 'r', encoding='utf-8') as f:
476
+ old_content = f.read()
477
+
478
+ print_terminal_diff(path, old_content, content)
479
+
480
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
481
+ with open(filepath, 'w', encoding='utf-8') as f:
482
+ f.write(content)
483
+ result = {"output": f"Successfully wrote {path}"}
484
+
485
+ elif patch_match:
486
+ path = patch_match.group(1).strip()
487
+ filepath = secure_path(path)
488
+ search = re.sub(r'^```[a-zA-Z]*\n|\n```$', '', patch_match.group(2).strip('\n'), flags=re.MULTILINE)
489
+ replace = re.sub(r'^```[a-zA-Z]*\n|\n```$', '', patch_match.group(3).strip('\n'), flags=re.MULTILINE)
490
+
491
+ if not os.path.exists(filepath):
492
+ result = {"output": "Error: File not found."}
493
+ else:
494
+ with open(filepath, 'r', encoding='utf-8') as f:
495
+ original = f.read()
496
+ if search not in original:
497
+ result = {"output": "Error: Search block not found exactly as written."}
498
+ else:
499
+ new_content = original.replace(search, replace, 1)
500
+ print_terminal_diff(path, original, new_content)
501
+ with open(filepath, 'w', encoding='utf-8') as f:
502
+ f.write(new_content)
503
+ result = {"output": f"Patch applied successfully to {path}."}
504
+
505
+ elif term_run_match:
506
+ cmd = term_run_match.group(1).strip()
507
+ env = os.environ.copy()
508
+ env["FORCE_COLOR"] = "true"
509
+ try:
510
+ res = subprocess.run(cmd, shell=True, cwd=WORKSPACE_DIR, capture_output=True, text=True, timeout=300, env=env)
511
+ output = strip_ansi(f"STDOUT:\n{res.stdout}\nSTDERR:\n{res.stderr}")
512
+ result = {"output": truncate_output(output if output.strip() else "Executed.")}
513
+ except subprocess.TimeoutExpired as e:
514
+ partial = e.stdout or ""
515
+ result = {"output": truncate_output(strip_ansi(f"Timed out after 300s. Output:\n{partial}"))}
516
+
517
+ elif term_bg_match:
518
+ cmd = term_bg_match.group(1).strip()
519
+ pid = str(uuid.uuid4())[:8]
520
+ env = os.environ.copy()
521
+ env["FORCE_COLOR"] = "true"
522
+ process = subprocess.Popen(cmd, shell=True, cwd=WORKSPACE_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, env=env)
523
+ BACKGROUND_PROCESSES[pid] = {'process': process, 'logs': deque(maxlen=500), 'status': 'running'}
524
+ threading.Thread(target=stream_process_output, args=(pid, process), daemon=True).start()
525
+ result = {"output": f"Process started. PID: {pid}. Use terminal_logs to view."}
526
+
527
+ elif term_logs_match:
528
+ pid = term_logs_match.group(1).strip()
529
+ if pid not in BACKGROUND_PROCESSES:
530
+ result = {"output": "Error: PID not found."}
531
+ else:
532
+ logs = "".join(list(BACKGROUND_PROCESSES[pid]['logs'])) or "No output yet."
533
+ result = {"output": truncate_output(strip_ansi(f"STATUS: {BACKGROUND_PROCESSES[pid]['status']}\nLOGS:\n{logs}"))}
534
+
535
+ elif term_kill_match:
536
+ pid = term_kill_match.group(1).strip()
537
+ if pid in BACKGROUND_PROCESSES:
538
+ proc = BACKGROUND_PROCESSES[pid]['process']
539
+ if platform.system() == "Windows":
540
+ subprocess.run(f"taskkill /F /T /PID {proc.pid}", shell=True, capture_output=True)
541
+ else:
542
+ proc.terminate()
543
+ BACKGROUND_PROCESSES[pid]['status'] = 'killed'
544
+ result = {"output": f"Process {pid} terminated."}
545
+ else:
546
+ result = {"output": "Error: PID not found."}
547
+
548
+ elif cron_match:
549
+ pid = cron_match.group(1).strip()
550
+ delay = int(cron_match.group(2).strip())
551
+ if pid not in BACKGROUND_PROCESSES:
552
+ result = {"output": "Error: PID not found."}
553
+ else:
554
+ safe_delay = min(delay, 120)
555
+ await asyncio.sleep(safe_delay)
556
+ logs = "".join(list(BACKGROUND_PROCESSES[pid]['logs'])) or "No output yet."
557
+ status = BACKGROUND_PROCESSES[pid]['status']
558
+ result = {"output": truncate_output(strip_ansi(f"[WOKE UP AFTER {safe_delay}s]\nSTATUS: {status}\nLOGS:\n{logs}"))}
559
+
560
+ else:
561
+ result = {"output": "Error: Unrecognized tool tag."}
562
+
563
+ except Exception as e:
564
+ result = {"output": f"Error: {str(e)}"}
565
+
566
+ elapsed_ms = (time.perf_counter() - start) * 1000
567
+ output_text = str(result.get("output", ""))
568
+ ok = not output_text.startswith("Error")
569
+ print_tool_end(ok, elapsed_ms, note=_short_preview(output_text))
570
+ return result
571
+
572
+ if __name__ == "__main__":
573
+ import uvicorn
574
+ uvicorn.run(app, host="127.0.0.1", port=5505, access_log=False)
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: browse_code
3
+ Version: 0.1.0
4
+ Summary: A local AI bridge package
5
+ Requires-Dist: fastapi
6
+ Requires-Dist: uvicorn
7
+ Requires-Dist: pydantic
8
+ Dynamic: requires-dist
9
+ Dynamic: summary
@@ -0,0 +1,13 @@
1
+ browse_code/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ browse_code/cli.py,sha256=ofLJcremtBArkiYGlO2ZmSGczfcLZ0jyWWKeXP3Nb9o,2700
3
+ browse_code/server.py,sha256=71G-SL-B0tZbi6sUASkL9JsGbWTNwO_qxFK--AUZHlE,24460
4
+ browse_code/extension/content.js,sha256=YNo4X2E-6ECmmPrvgcz2YGaYwWB9m0wgMrYgp4FAksk,18611
5
+ browse_code/extension/manifest.json,sha256=HZGVv6Tz9Vf8lF36qXjdv_ymeIOt_BAE3SsMXd6Zb0o,609
6
+ browse_code/extension/popup.html,sha256=FzDrhvFdVQfJvbPtOP29Qf_zOG74L14_MQHY2NThjHs,2267
7
+ browse_code/extension/popup.js,sha256=p7UnulL6RagF8nEnl0qq6bKlZP5VKwPVrm1fmHkdfJc,3949
8
+ browse_code/extension/spoof.js,sha256=Gw-32i1H6TLOJ1wya6FgEpWwcRw8MBOT2jXqC9ucISI,232
9
+ browse_code-0.1.0.dist-info/METADATA,sha256=0ITT_ssbL79d5zN8IUpHsBqdZhk8cHD8fcLOrkrk97Q,209
10
+ browse_code-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
11
+ browse_code-0.1.0.dist-info/entry_points.txt,sha256=RKZORZlRlErzq7s0lsBoQftIJht3XbEt97LEwSZCKps,44
12
+ browse_code-0.1.0.dist-info/top_level.txt,sha256=Foeuwuu4FrELgUyxG7VVnVXsd8w2lrlGIxO2IJHPJqU,12
13
+ browse_code-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bc = browse_code.cli:main
@@ -0,0 +1 @@
1
+ browse_code