@tiens.nguyen/gu-cli 1.0.836 → 1.0.837
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.
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
// host.tabs() → [{id, title, url, current}] host.switchTo(tabId) → boolean
|
|
11
11
|
// host.info(tabId) → {url, title, status} | null host.activate(tabId)
|
|
12
12
|
// host.close(tabId)
|
|
13
|
+
// host.runsJs — true only where running gu's own JavaScript text is allowed (gu's own window).
|
|
14
|
+
// The Chrome Web Store extension leaves it unset: text sent at run time and executed is
|
|
15
|
+
// "remote code", which a Manifest V3 store extension may not run (#377, GitHub #158).
|
|
13
16
|
// No chrome.* and no Node API in here: it runs in an extension service worker and in Node.
|
|
14
17
|
|
|
15
18
|
const consoleBuf = new Map(); // tabId → [{level, text}] since the last "console" read
|
|
@@ -305,6 +308,11 @@ export async function runAction(cmd, host) {
|
|
|
305
308
|
await sleep(Math.max(0, Math.min(Number(cmd.seconds) || 1, 30)) * 1000);
|
|
306
309
|
return after(host, id);
|
|
307
310
|
case "js": {
|
|
311
|
+
if (!host.runsJs) {
|
|
312
|
+
throw new Error("js is not available through the gu Chrome extension (Chrome Web Store rules: " +
|
|
313
|
+
"it runs only the code it ships with). Use read, console, click and type instead — or gu's own " +
|
|
314
|
+
"Chrome window, which has it.");
|
|
315
|
+
}
|
|
308
316
|
const value = await evaluate(host, id, String(cmd.text || ""));
|
|
309
317
|
return after(host, id, { value: value === undefined ? "undefined" : JSON.stringify(value)?.slice(0, 8000) });
|
|
310
318
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "gu browser",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.4",
|
|
5
5
|
"description": "Lets the gu coding agent open, see and operate web pages in its own tabs (tab group \"gu\") with a real mouse and keyboard.",
|
|
6
6
|
"minimum_chrome_version": "116",
|
|
7
7
|
"icons": {
|
|
@@ -17,9 +17,6 @@
|
|
|
17
17
|
"alarms",
|
|
18
18
|
"storage"
|
|
19
19
|
],
|
|
20
|
-
"host_permissions": [
|
|
21
|
-
"<all_urls>"
|
|
22
|
-
],
|
|
23
20
|
"background": {
|
|
24
21
|
"service_worker": "background.js",
|
|
25
22
|
"type": "module"
|
package/gonext_agent_chat.py
CHANGED
|
@@ -9736,6 +9736,62 @@ def _run_state_path() -> str:
|
|
|
9736
9736
|
return _RUN_STATE["path"]
|
|
9737
9737
|
|
|
9738
9738
|
|
|
9739
|
+
_CD_FIRST_RE = re.compile(
|
|
9740
|
+
r"""^\s*(?:cd|pushd|set-location|sl|chdir)\s+(?:-(?:literalpath|path)\s+)?(['"]?)([^'";&|\s]+)\1(?=\s*(?:$|&&|;|\|\||\n))""",
|
|
9741
|
+
re.IGNORECASE)
|
|
9742
|
+
|
|
9743
|
+
|
|
9744
|
+
def _cd_from_root(command: str, kept: str, root: str) -> str:
|
|
9745
|
+
"""The folder a line should start in: `kept` (where the last shell line ended), unless the line
|
|
9746
|
+
begins by entering a RELATIVE folder that does not exist there but does at `root` — then
|
|
9747
|
+
`root`. PURE but for isdir. Anything else (absolute, ~, $VAR, `..`, already there) → `kept`."""
|
|
9748
|
+
if not kept or not root or os.path.realpath(kept) == os.path.realpath(root):
|
|
9749
|
+
return kept
|
|
9750
|
+
m = _CD_FIRST_RE.match(command or "")
|
|
9751
|
+
if not m:
|
|
9752
|
+
return kept
|
|
9753
|
+
target = m.group(2)
|
|
9754
|
+
if os.path.isabs(target) or target.startswith(("~", "$", ".")) or re.match(r"^[A-Za-z]:", target):
|
|
9755
|
+
return kept
|
|
9756
|
+
if os.path.isdir(os.path.join(kept, target)) or not os.path.isdir(os.path.join(root, target)):
|
|
9757
|
+
return kept
|
|
9758
|
+
return root
|
|
9759
|
+
|
|
9760
|
+
|
|
9761
|
+
def _patch_dotted_imports() -> bool:
|
|
9762
|
+
"""`import urllib.request` in the agent's sandbox binds the name `urllib` (#378, GitHub #162).
|
|
9763
|
+
|
|
9764
|
+
smolagents 1.26's evaluate_import stores `import a.b` under the literal key "a.b" and never binds
|
|
9765
|
+
`a`, so `urllib.request.urlopen(…)` — urllib.request is an allowed import here — fails with
|
|
9766
|
+
"module 'urllib.error' has no attribute 'request'". Measured: three identical failing steps in
|
|
9767
|
+
one deploy turn before the model gave up and used curl. Python binds the TOP package for a
|
|
9768
|
+
dotted import; this does the same, and only when that top package is itself an allowed import
|
|
9769
|
+
(so `import os.path` stays refused). Idempotent. False when smolagents is not there."""
|
|
9770
|
+
try:
|
|
9771
|
+
import ast as _ast
|
|
9772
|
+
from importlib import import_module as _imp
|
|
9773
|
+
from smolagents import local_python_executor as _lpe
|
|
9774
|
+
except Exception: # noqa: BLE001
|
|
9775
|
+
return False
|
|
9776
|
+
if getattr(_lpe.evaluate_import, "_gu_dotted", False):
|
|
9777
|
+
return True
|
|
9778
|
+
orig = _lpe.evaluate_import
|
|
9779
|
+
|
|
9780
|
+
def evaluate_import(expression, state, authorized_imports):
|
|
9781
|
+
r = orig(expression, state, authorized_imports)
|
|
9782
|
+
if isinstance(expression, _ast.Import):
|
|
9783
|
+
for alias in expression.names:
|
|
9784
|
+
if not alias.asname and "." in alias.name:
|
|
9785
|
+
top = alias.name.split(".")[0]
|
|
9786
|
+
if _lpe.check_import_authorized(top, authorized_imports):
|
|
9787
|
+
state[top] = _lpe.get_safe_module(_imp(top), authorized_imports)
|
|
9788
|
+
return r
|
|
9789
|
+
|
|
9790
|
+
evaluate_import._gu_dotted = True
|
|
9791
|
+
_lpe.evaluate_import = evaluate_import
|
|
9792
|
+
return True
|
|
9793
|
+
|
|
9794
|
+
|
|
9739
9795
|
def _run_state_cwd() -> str:
|
|
9740
9796
|
"""The folder the last shell line ended in, this turn — or ""."""
|
|
9741
9797
|
p = _RUN_STATE["path"]
|
|
@@ -10069,6 +10125,7 @@ def run_agent_chat(cfg):
|
|
|
10069
10125
|
f"then: gu stop && gu start"})
|
|
10070
10126
|
_log(f"smolagents import FAILED on {_where}: {e}")
|
|
10071
10127
|
return
|
|
10128
|
+
_patch_dotted_imports()
|
|
10072
10129
|
|
|
10073
10130
|
messages = cfg.get("messages") or []
|
|
10074
10131
|
_turn_image_refs = _take_image_markers(messages)
|
|
@@ -15189,6 +15246,12 @@ def run_agent_chat(cfg):
|
|
|
15189
15246
|
wd = _rag_read_allowed(_kept)
|
|
15190
15247
|
except Exception: # noqa: BLE001 — left the allowed folders: start over
|
|
15191
15248
|
wd = _wd_before
|
|
15249
|
+
# `cd sub` AGAIN FROM INSIDE sub (#378, GitHub #161), measured: after
|
|
15250
|
+
# `cd frontend && npx tsc` the shell stayed in frontend, the next line was
|
|
15251
|
+
# `cd frontend && node test…` → "no such folder: frontend", then two text-only
|
|
15252
|
+
# steps before the tests ran — 4 steps for one test run. A line that starts by
|
|
15253
|
+
# entering a folder that is not here but IS at the project root means the root.
|
|
15254
|
+
wd = _cd_from_root(command, wd, _wd_before)
|
|
15192
15255
|
w = _ws_for_path(wd)
|
|
15193
15256
|
if w is None or not w.get("allowRun"):
|
|
15194
15257
|
# Contextual nudge (task #102): the #1 reason the model shells out here is
|
|
@@ -15939,7 +16002,7 @@ def run_agent_chat(cfg):
|
|
|
15939
16002
|
"""Use a REAL Chrome browser like a person does — open a page, see it, click with the mouse, type with the keyboard — to test a UI flow, check a page renders, fill a form or click through an app. The first call asks the user's permission. It uses the user's Chrome through the gu extension when that is installed, otherwise gu's own Chrome window (a separate profile: not logged in to the user's accounts). Loop: open → read (or screenshot) → click / type → read again to check what happened. Only gu's own tabs, and tabs the user shared, are used. If the answer starts BROWSER NOT AVAILABLE, stop and tell the user (the answer says what to do).
|
|
15940
16003
|
|
|
15941
16004
|
Args:
|
|
15942
|
-
action: one of: open (url) · read (page text + visible interactive elements numbered [n] with x,y) · screenshot (an image of the page; pixels = click x,y) · click / double_click / right_click / hover (ref, or x and y) · type (text into the focused field; with ref it clicks that field first) · key (text: "Enter", "Tab", "Escape", "ctrl+a", "shift+Tab"… several separated by spaces) · scroll (direction, amount) · back · reload · wait (seconds) · js (text = a JavaScript expression run in the page; returns its value) · console (the page's console messages since the last call) · tabs · switch (tab) · close
|
|
16005
|
+
action: one of: open (url) · read (page text + visible interactive elements numbered [n] with x,y) · screenshot (an image of the page; pixels = click x,y) · click / double_click / right_click / hover (ref, or x and y) · type (text into the focused field; with ref it clicks that field first) · key (text: "Enter", "Tab", "Escape", "ctrl+a", "shift+Tab"… several separated by spaces) · scroll (direction, amount) · back · reload · wait (seconds) · js (text = a JavaScript expression run in the page; returns its value — gu's own Chrome window only: through the gu Chrome extension it is refused, so prefer read / console) · console (the page's console messages since the last call) · tabs · switch (tab) · close
|
|
15943
16006
|
url: for open — a full http(s):// or file:// url (localhost:3000 is taken as http://localhost:3000)
|
|
15944
16007
|
ref: an element's [n] from the last read, for click/hover/type
|
|
15945
16008
|
x: page x coordinate for click/hover (from read's @x,y or the screenshot), when no ref
|
|
@@ -16090,6 +16153,16 @@ def run_agent_chat(cfg):
|
|
|
16090
16153
|
"Finish with final_answer(<summary>): the answer in a few sentences, then the "
|
|
16091
16154
|
"relevant files as `path:start-end — what is there`, one per line. At most "
|
|
16092
16155
|
"about 250 words; never paste whole files.")
|
|
16156
|
+
# ITS OWN CONTEXT, ITS OWN "ALREADY SHOWN" (#378, GitHub #160). The sub-agent runs the
|
|
16157
|
+
# same tool functions, so its reads went into the turn's re-read guard — and the main
|
|
16158
|
+
# agent was then refused lines it had never been shown ("already in this
|
|
16159
|
+
# conversation"): 3 refused steps in one measured turn, then a whole-file read
|
|
16160
|
+
# (50k chars carried by every later step) to get round it. The guards are swapped
|
|
16161
|
+
# for empty ones while the sub-agent runs, and the main agent's put back after.
|
|
16162
|
+
_guards = (_reads, _read_ranges, _seen_calls)
|
|
16163
|
+
_saved = tuple(dict(g) for g in _guards)
|
|
16164
|
+
for g in _guards:
|
|
16165
|
+
g.clear()
|
|
16093
16166
|
try:
|
|
16094
16167
|
with contextlib.redirect_stdout(sys.stderr):
|
|
16095
16168
|
out = sub.run(sub_task)
|
|
@@ -16097,6 +16170,10 @@ def run_agent_chat(cfg):
|
|
|
16097
16170
|
msg = f"Error: explore failed: {type(e).__name__}: {_clip(str(e), 200)}"
|
|
16098
16171
|
_last_obs["text"] = msg
|
|
16099
16172
|
return msg
|
|
16173
|
+
finally:
|
|
16174
|
+
for g, old in zip(_guards, _saved):
|
|
16175
|
+
g.clear()
|
|
16176
|
+
g.update(old)
|
|
16100
16177
|
n = sum(1 for s in getattr(sub.memory, "steps", []) if type(s).__name__ == "ActionStep")
|
|
16101
16178
|
_emit({"type": "step", "text": f"Explored → {n} step(s), in its own context"})
|
|
16102
16179
|
_log(f"explore: {n} sub-step(s), {len(str(out))} chars back")
|
package/gu_browser.py
CHANGED
|
@@ -19,7 +19,7 @@ import urllib.request
|
|
|
19
19
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
20
20
|
EXT_SRC = os.path.join(HERE, "browser-extension")
|
|
21
21
|
EXT_FILES = ("manifest.json", "background.js", "actions.js", "popup.html", "popup.js",
|
|
22
|
-
"icons/icon16.png", "icons/icon48.png", "icons/icon128.png")
|
|
22
|
+
"icons/icon16.png", "icons/icon32.png", "icons/icon48.png", "icons/icon128.png")
|
|
23
23
|
BRIDGE = os.path.join(HERE, "browser-bridge.mjs")
|
|
24
24
|
|
|
25
25
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.837",
|
|
4
4
|
"description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|