@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.1",
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"
@@ -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.836",
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",