@tiens.nguyen/gu-cli 1.0.828 → 1.0.830
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.
- package/gonext_agent_chat.py +171 -17
- package/gu-repl.mjs +13 -7
- package/package.json +1 -1
package/gonext_agent_chat.py
CHANGED
|
@@ -5109,6 +5109,46 @@ def _repair_unterminated_string(code: str) -> str:
|
|
|
5109
5109
|
return code
|
|
5110
5110
|
|
|
5111
5111
|
|
|
5112
|
+
# A COMMIT MESSAGE IN A SHELL LINE IN A PYTHON STRING (#360 follow-up). The model passes run_command
|
|
5113
|
+
# a whole `git commit -F - <<'EOF' … EOF` (or -m "…") as ONE normal "…" string. The message has
|
|
5114
|
+
# line breaks AND, often, the string's own quote ('"node /opt/…/gu"', "turn's") — so the literal
|
|
5115
|
+
# ends early and escaping newlines cannot help. Measured 2026-10-14: 2 of the last 2 commit turns
|
|
5116
|
+
# lost a step to exactly this. The command argument runs from the call's opening quote to the
|
|
5117
|
+
# LAST matching quote that is followed by the next argument or the closing paren; re-quoted as a
|
|
5118
|
+
# triple-quoted string it is the text the model meant. Kept only when the block then compiles.
|
|
5119
|
+
_RUN_CMD_OPEN_RE = re.compile(r"\brun_command\(\s*(?:command\s*=\s*)?(['\"])(?!\1\1)")
|
|
5120
|
+
|
|
5121
|
+
|
|
5122
|
+
def _repair_run_command_string(code: str) -> str:
|
|
5123
|
+
try:
|
|
5124
|
+
compile(code, "<gu-code>", "exec")
|
|
5125
|
+
return code
|
|
5126
|
+
except SyntaxError:
|
|
5127
|
+
pass
|
|
5128
|
+
except Exception: # noqa: BLE001
|
|
5129
|
+
return code
|
|
5130
|
+
m = _RUN_CMD_OPEN_RE.search(code)
|
|
5131
|
+
if not m:
|
|
5132
|
+
return code
|
|
5133
|
+
q, start = m.group(1), m.end()
|
|
5134
|
+
closer = re.compile(re.escape(q) + r"(?=\s*(?:,\s*(?:workdir|timeout_seconds|background)\s*=|,\s*['\"]|\)))")
|
|
5135
|
+
next_arg = re.compile(re.escape(q) + r"\s*,\s*(?:workdir|timeout_seconds|background)\s*=")
|
|
5136
|
+
for end in reversed([x.start() for x in closer.finditer(code, start)]):
|
|
5137
|
+
body = code[start:end]
|
|
5138
|
+
if next_arg.search(body):
|
|
5139
|
+
continue # it swallowed the next argument — the command ends earlier
|
|
5140
|
+
for wrap in ('"""', "'''"):
|
|
5141
|
+
if wrap in body or body.endswith(wrap[0]):
|
|
5142
|
+
continue
|
|
5143
|
+
fixed = code[:m.start(1)] + wrap + body + wrap + code[end + 1:]
|
|
5144
|
+
try:
|
|
5145
|
+
compile(fixed, "<gu-code>", "exec")
|
|
5146
|
+
return fixed
|
|
5147
|
+
except SyntaxError:
|
|
5148
|
+
continue
|
|
5149
|
+
return code
|
|
5150
|
+
|
|
5151
|
+
|
|
5112
5152
|
def _repair_code_block_strings(text: str) -> str:
|
|
5113
5153
|
"""Apply _repair_unterminated_string to the python inside a <code>…</code> block
|
|
5114
5154
|
(leaving the surrounding Thought prose untouched). No-op when there's no block or the
|
|
@@ -5134,6 +5174,8 @@ def _repair_code_block_strings(text: str) -> str:
|
|
|
5134
5174
|
close_start = open_end + cm.start()
|
|
5135
5175
|
inner = text[open_end:close_start]
|
|
5136
5176
|
fixed = _repair_unterminated_string(inner)
|
|
5177
|
+
if fixed == inner:
|
|
5178
|
+
fixed = _repair_run_command_string(inner)
|
|
5137
5179
|
if fixed != inner:
|
|
5138
5180
|
return text[:open_end] + fixed + text[close_start:]
|
|
5139
5181
|
# Stage 2 — only worth trying when there's MORE than one </code> (extra tags in content).
|
|
@@ -7950,6 +7992,18 @@ _BG_LINE = ("A LONG job (build, deploy, big test run) → run_command(…, backg
|
|
|
7950
7992
|
"timeout moves to the background by itself instead of being killed.\n")
|
|
7951
7993
|
|
|
7952
7994
|
|
|
7995
|
+
# What to do instead, said where quoting broke — in gu's own pre-check (an unbalanced quote never
|
|
7996
|
+
# reaches bash) and in bash's error. In a commit turn the next step was otherwise a guess.
|
|
7997
|
+
_HEREDOC_HINT = ("\n\nSHELL QUOTING broke this line. For text with quotes or line breaks (a commit "
|
|
7998
|
+
"message, an issue body) use a heredoc inside a triple-quoted string: "
|
|
7999
|
+
"run_command(\"\"\"git commit -F - <<'EOF'\n<message>\nEOF\"\"\") — nothing in the "
|
|
8000
|
+
"message needs escaping.")
|
|
8001
|
+
|
|
8002
|
+
# bash's words for a line whose quotes do not pair up.
|
|
8003
|
+
_SHELL_QUOTE_ERR_RE = re.compile(
|
|
8004
|
+
r"unexpected EOF while looking for matching|syntax error near unexpected token|"
|
|
8005
|
+
r"syntax error: unexpected end of file|unterminated quoted string")
|
|
8006
|
+
|
|
7953
8007
|
_RUN_DOC_ARGV = ("No other shell syntax (no pipes/redirects/;). sudo/su are blocked.")
|
|
7954
8008
|
_RUN_DOC_SHELL = ("It runs in bash, so pipes, redirects, ;, ||, $( ), globs and heredocs work too "
|
|
7955
8009
|
"(e.g. 'npm test 2>&1 | tail -40'). sudo/su are blocked.")
|
|
@@ -8941,6 +8995,70 @@ def _shell_find(shells: list, key: str):
|
|
|
8941
8995
|
return None
|
|
8942
8996
|
|
|
8943
8997
|
|
|
8998
|
+
_EPHEMERAL_FLOOR = None
|
|
8999
|
+
|
|
9000
|
+
|
|
9001
|
+
def _ws_ephemeral_floor() -> int:
|
|
9002
|
+
"""First port of the OS's ephemeral range — where a bind to port 0 lands. Linux reads
|
|
9003
|
+
ip_local_port_range (32768 by default), macOS its sysctl (49152); Windows and anything
|
|
9004
|
+
unreadable use the IANA 49152."""
|
|
9005
|
+
global _EPHEMERAL_FLOOR
|
|
9006
|
+
if _EPHEMERAL_FLOOR is None:
|
|
9007
|
+
floor = 49152
|
|
9008
|
+
try:
|
|
9009
|
+
if sys.platform.startswith("linux"):
|
|
9010
|
+
with open("/proc/sys/net/ipv4/ip_local_port_range", encoding="utf-8") as fh:
|
|
9011
|
+
floor = int(fh.read().split()[0])
|
|
9012
|
+
elif sys.platform == "darwin":
|
|
9013
|
+
import subprocess
|
|
9014
|
+
out = subprocess.run(["sysctl", "-n", "net.inet.ip.portrange.first"],
|
|
9015
|
+
capture_output=True, text=True, timeout=3).stdout.strip()
|
|
9016
|
+
floor = int(out) if out.isdigit() else floor
|
|
9017
|
+
except Exception: # noqa: BLE001
|
|
9018
|
+
pass
|
|
9019
|
+
_EPHEMERAL_FLOOR = max(1024, floor)
|
|
9020
|
+
return _EPHEMERAL_FLOOR
|
|
9021
|
+
|
|
9022
|
+
|
|
9023
|
+
def _ws_is_ephemeral_port(port) -> bool:
|
|
9024
|
+
try:
|
|
9025
|
+
return int(port) >= _ws_ephemeral_floor()
|
|
9026
|
+
except (TypeError, ValueError):
|
|
9027
|
+
return False
|
|
9028
|
+
|
|
9029
|
+
|
|
9030
|
+
def _server_as_shell(srv: dict) -> dict:
|
|
9031
|
+
"""A left-running server/app in the shape shell_output reads, so a command that was filed as
|
|
9032
|
+
a server (wrongly, or not) can still be waited on and read (#360)."""
|
|
9033
|
+
import time as _t
|
|
9034
|
+
started = srv.get("started")
|
|
9035
|
+
if started is None:
|
|
9036
|
+
try:
|
|
9037
|
+
started = _t.mktime(_t.strptime(str(srv.get("started_at")), "%Y-%m-%d %H:%M:%S"))
|
|
9038
|
+
except (ValueError, OverflowError):
|
|
9039
|
+
started = _t.time()
|
|
9040
|
+
return {"id": str(srv.get("pid")), "pid": srv.get("pid"), "pgid": srv.get("pgid") or srv.get("pid"),
|
|
9041
|
+
"command": srv.get("command"), "workdir": srv.get("workdir"), "log": srv.get("log"),
|
|
9042
|
+
"exit_file": "", "started": started, "read_offset": srv.get("read_offset", 0),
|
|
9043
|
+
"server": True}
|
|
9044
|
+
|
|
9045
|
+
|
|
9046
|
+
def _shell_or_server(shells: list, servers: list, key: str):
|
|
9047
|
+
"""shell_output's target: a shell by id, else a server by pid or port; with no key, the newest
|
|
9048
|
+
of both — the model asking about "the" background job means the one it just started."""
|
|
9049
|
+
key = (key or "").strip().lstrip("#")
|
|
9050
|
+
if key:
|
|
9051
|
+
sh = _shell_find(shells, key)
|
|
9052
|
+
if sh is not None:
|
|
9053
|
+
return sh
|
|
9054
|
+
for srv in reversed(servers):
|
|
9055
|
+
if key == str(srv.get("pid")) or any(key == str(p) for p in srv.get("ports") or []):
|
|
9056
|
+
return _server_as_shell(srv)
|
|
9057
|
+
return None
|
|
9058
|
+
cands = list(shells) + [_server_as_shell(x) for x in servers]
|
|
9059
|
+
return max(cands, key=lambda x: float(x.get("started") or 0)) if cands else None
|
|
9060
|
+
|
|
9061
|
+
|
|
8944
9062
|
def _shell_kill(s: dict) -> None:
|
|
8945
9063
|
"""Stop a shell's whole process group and mark it killed (unless it already finished)."""
|
|
8946
9064
|
import time as _t
|
|
@@ -9020,8 +9138,19 @@ def _range_is_covered(served, start: int, end: int):
|
|
|
9020
9138
|
return None
|
|
9021
9139
|
|
|
9022
9140
|
|
|
9023
|
-
|
|
9024
|
-
|
|
9141
|
+
_DISTILL_FLOOR = 5200 # 1200 head + 4000 tail — what every window got before #360
|
|
9142
|
+
|
|
9143
|
+
|
|
9144
|
+
def _ws_distill_output(out: str, cap_head: int = None, cap_tail: int = None) -> str:
|
|
9145
|
+
"""Cap huge build/test output: failures live at the tail, context at the head.
|
|
9146
|
+
|
|
9147
|
+
THE CAP SCALES WITH THE WINDOW, like a file read (#360 follow-up, issue #49). A fixed 5,200
|
|
9148
|
+
chars cut a `git diff` in a commit turn and the model spent a step re-reading it with sed;
|
|
9149
|
+
a command's output is a read too. Never below the old size, so a small window is unchanged."""
|
|
9150
|
+
if cap_head is None or cap_tail is None:
|
|
9151
|
+
total = max(_DISTILL_FLOOR, int(_OBS_CAPS.get("read") or 0))
|
|
9152
|
+
cap_head = cap_head if cap_head is not None else int(total * 0.23)
|
|
9153
|
+
cap_tail = cap_tail if cap_tail is not None else total - cap_head
|
|
9025
9154
|
if len(out) <= cap_head + cap_tail + 200:
|
|
9026
9155
|
return out
|
|
9027
9156
|
return (out[:cap_head] + f"\n…[{len(out) - cap_head - cap_tail} chars omitted]…\n"
|
|
@@ -14356,7 +14485,8 @@ def run_agent_chat(cfg):
|
|
|
14356
14485
|
_scan = _shell_scan(command) if _shell_bin else None
|
|
14357
14486
|
if _scan and _scan["error"]:
|
|
14358
14487
|
msg = (f"Error: that command did NOT run — {_scan['error']}. Nothing was "
|
|
14359
|
-
"executed; fix the quoting and send it again."
|
|
14488
|
+
"executed; fix the quoting and send it again."
|
|
14489
|
+
+ (_HEREDOC_HINT if "quote" in str(_scan["error"]) else ""))
|
|
14360
14490
|
_last_obs["text"] = msg
|
|
14361
14491
|
return msg
|
|
14362
14492
|
_use_shell = bool(_scan) and _shell_needed(_scan)
|
|
@@ -14620,6 +14750,12 @@ def run_agent_chat(cfg):
|
|
|
14620
14750
|
# denied" is the part the reader already has; who we were and what we
|
|
14621
14751
|
# were allowed to be is the part they need, and without it both the
|
|
14622
14752
|
# model and the user are guessing at the next step.
|
|
14753
|
+
# A QUOTING ERROR NAMES THE FORM THAT WORKS (#360 follow-up). The model's
|
|
14754
|
+
# `git commit -m '…turn'\''s…'` lost its backslash to Python and bash
|
|
14755
|
+
# stopped on the quote; the next step was spent guessing. A heredoc in a
|
|
14756
|
+
# triple-quoted string has no quoting to get wrong.
|
|
14757
|
+
if rc != 0 and _use_shell and _SHELL_QUOTE_ERR_RE.search(out or ""):
|
|
14758
|
+
result += _HEREDOC_HINT
|
|
14623
14759
|
if rc != 0 and _WS_PERM_ERR_RE.search(out or ""):
|
|
14624
14760
|
_f = _ws_access_facts()
|
|
14625
14761
|
result = f"{result}\n\n{_ws_access_report(_f, _ws_paths_in(command))}"
|
|
@@ -14629,10 +14765,16 @@ def run_agent_chat(cfg):
|
|
|
14629
14765
|
return result
|
|
14630
14766
|
if _t.time() - start >= 3:
|
|
14631
14767
|
ports = _ws_listening_ports(proc.pid)
|
|
14768
|
+
# A port the OS PICKED (bind to 0) is a test run's own helper — the UI
|
|
14769
|
+
# suite's fake API on :52192 was filed as a "server", so the model could
|
|
14770
|
+
# not wait for the run and spent 3 steps polling it by hand (#360). Keep
|
|
14771
|
+
# waiting for it like any command; it hands off at the deadline if needed.
|
|
14772
|
+
if ports and all(_ws_is_ephemeral_port(p_) for p_ in ports):
|
|
14773
|
+
ports = []
|
|
14632
14774
|
if ports:
|
|
14633
14775
|
servers = _ws_load_servers()
|
|
14634
14776
|
servers.append({
|
|
14635
|
-
"pgid": proc.pid, "pid": proc.pid, "command": command,
|
|
14777
|
+
"pgid": proc.pid, "pid": proc.pid, "command": command, "started": start,
|
|
14636
14778
|
"workdir": wd, "ports": ports, "log": log_path,
|
|
14637
14779
|
"started_at": _t.strftime("%Y-%m-%d %H:%M:%S"),
|
|
14638
14780
|
})
|
|
@@ -14642,7 +14784,9 @@ def run_agent_chat(cfg):
|
|
|
14642
14784
|
tail = _ws_distill_output(_read_log())
|
|
14643
14785
|
result = (f"RUNNING: '{command}' is up and listening — {urls} "
|
|
14644
14786
|
f"(pid {proc.pid}, log {log_path}). It stays running in "
|
|
14645
|
-
"the background; use stop_server to stop it. "
|
|
14787
|
+
"the background; use stop_server to stop it. If it is "
|
|
14788
|
+
"NOT meant to stay up (a test run that opened a port), "
|
|
14789
|
+
f"shell_output('{proc.pid}', wait_seconds=…) waits for it. "
|
|
14646
14790
|
f"Startup output:\n{tail}")
|
|
14647
14791
|
_last_obs["text"] = result
|
|
14648
14792
|
return result
|
|
@@ -14654,7 +14798,7 @@ def run_agent_chat(cfg):
|
|
|
14654
14798
|
if _ws_gui_pids(proc.pid):
|
|
14655
14799
|
servers = _ws_load_servers()
|
|
14656
14800
|
servers.append({
|
|
14657
|
-
"pgid": proc.pid, "pid": proc.pid, "command": command,
|
|
14801
|
+
"pgid": proc.pid, "pid": proc.pid, "command": command, "started": start,
|
|
14658
14802
|
"workdir": wd, "ports": [], "kind": "gui", "log": log_path,
|
|
14659
14803
|
"started_at": _t.strftime("%Y-%m-%d %H:%M:%S"),
|
|
14660
14804
|
})
|
|
@@ -14785,11 +14929,13 @@ def run_agent_chat(cfg):
|
|
|
14785
14929
|
try:
|
|
14786
14930
|
import time as _t
|
|
14787
14931
|
shells = _shells_load()
|
|
14788
|
-
|
|
14932
|
+
servers = _ws_load_servers()
|
|
14933
|
+
sh = _shell_or_server(shells, servers, shell_id)
|
|
14789
14934
|
if sh is None:
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14935
|
+
ids = [str(x.get("id")) for x in shells] + [str(x.get("pid")) for x in servers]
|
|
14936
|
+
msg = ("No background shell or server matches '" + (shell_id or "") + "'. "
|
|
14937
|
+
+ ("Running: " + ", ".join(ids) if ids
|
|
14938
|
+
else "Nothing was left running in the background."))
|
|
14793
14939
|
_last_obs["text"] = msg
|
|
14794
14940
|
return msg
|
|
14795
14941
|
deadline = _t.time() + max(0, min(int(wait_seconds or 0), 540))
|
|
@@ -14804,14 +14950,22 @@ def run_agent_chat(cfg):
|
|
|
14804
14950
|
except OSError:
|
|
14805
14951
|
chunk = b""
|
|
14806
14952
|
# Re-read the file before writing: the terminal may have killed it meanwhile.
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14953
|
+
if sh.get("server"):
|
|
14954
|
+
servers = _ws_load_servers(prune=False)
|
|
14955
|
+
for x in servers:
|
|
14956
|
+
if str(x.get("pid")) == str(sh.get("id")):
|
|
14957
|
+
x["read_offset"] = int(sh.get("read_offset") or 0) + len(chunk)
|
|
14958
|
+
_ws_save_servers(servers)
|
|
14959
|
+
else:
|
|
14960
|
+
shells = _shells_load()
|
|
14961
|
+
cur = _shell_find(shells, str(sh.get("id"))) or sh
|
|
14962
|
+
cur["read_offset"] = int(sh.get("read_offset") or 0) + len(chunk)
|
|
14963
|
+
if st["state"] != "running":
|
|
14964
|
+
cur["seen"] = True
|
|
14965
|
+
_shells_save(shells)
|
|
14813
14966
|
new = _ws_distill_output(chunk.decode("utf-8", errors="replace"))
|
|
14814
|
-
head = (f"
|
|
14967
|
+
head = (f"{'server' if sh.get('server') else 'shell'} {sh.get('id')}: "
|
|
14968
|
+
f"{_shell_status_text(st).upper()} after "
|
|
14815
14969
|
f"{_fmt_runtime(st['runtime'])} — '{_clip(str(sh.get('command')), 200)}' "
|
|
14816
14970
|
f"(log {sh.get('log')})")
|
|
14817
14971
|
if st["state"] == "exited":
|
package/gu-repl.mjs
CHANGED
|
@@ -369,7 +369,12 @@ const vwidth = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
|
369
369
|
// any theme without a font that has to be installed. Rows are plain text; padded + coloured at
|
|
370
370
|
// render time. LOGO_W = the visible column the status text starts after, and it MUST match the
|
|
371
371
|
// widest row or the status facts sit ragged beside it.
|
|
372
|
-
|
|
372
|
+
//
|
|
373
|
+
// LOWERCASE, THREE ROWS (#361). The capitals "G U" took four rows and the U, drawn as a closed
|
|
374
|
+
// square, read as a box; with an all-caps "GU AGENT" beside it the banner shouted its name twice.
|
|
375
|
+
// The product is written "gu", so the mark is too: a g with a real descender and a u — a wordmark,
|
|
376
|
+
// one row shorter, and the only coloured thing on the banner.
|
|
377
|
+
const LOGO = ["█▀▀█ █ █", "█▄▄█ █▄▄█", "▄▄▄█"];
|
|
373
378
|
const LOGO_W = 9;
|
|
374
379
|
// Lay the LOGO and the status rows SIDE BY SIDE (logo left, facts right), or STACK them (logo
|
|
375
380
|
// then facts) when the terminal is too narrow to fit both without wrapping — so the banner is
|
|
@@ -13326,7 +13331,7 @@ async function main() {
|
|
|
13326
13331
|
// right (label dim, value bright) — one fact per short line instead of the old wrapped
|
|
13327
13332
|
// "· … · …" wall. renderBanner lays them side by side, or stacks on a narrow terminal.
|
|
13328
13333
|
{
|
|
13329
|
-
const lbl = (s) => dim(s.padEnd(
|
|
13334
|
+
const lbl = (s) => dim(s.padEnd(9)); // aligned dim labels, values start at col 9 (#361: every row has one)
|
|
13330
13335
|
const ws = folderLabel(); // ONE definition, shared with the bottom bar's folder segment
|
|
13331
13336
|
// Task #170: `p.agentModelId` is "default_model" whenever a direct agent URL is set with
|
|
13332
13337
|
// no agentModelName beside it — mlx_lm's "whatever I loaded" sentinel, which Host mode
|
|
@@ -13358,7 +13363,8 @@ async function main() {
|
|
|
13358
13363
|
} catch { /* absent beats guessed — no suffix */ }
|
|
13359
13364
|
}
|
|
13360
13365
|
const statusRows = [
|
|
13361
|
-
|
|
13366
|
+
// #361: the name in the default colour — the mark beside it is the one accent.
|
|
13367
|
+
bold("gu") + dim(` agent · v${REPL_VERSION}${apiVersionSuffix(apiBannerVersion)}`),
|
|
13362
13368
|
// Task #134: a client machine OVERRIDES the account's agent model locally (the
|
|
13363
13369
|
// account value points at the model host's own server). Show what will actually be
|
|
13364
13370
|
// used, not a value this machine can't reach.
|
|
@@ -13403,7 +13409,7 @@ async function main() {
|
|
|
13403
13409
|
const name = m.label === "client expert" ? accent(m.label + " ") : m.label + " ";
|
|
13404
13410
|
return [lbl("mode") + name + dim(m.detail)];
|
|
13405
13411
|
})(),
|
|
13406
|
-
sessionTestAuto ? accent("auto
|
|
13412
|
+
lbl("test") + (sessionTestAuto ? accent("auto") : dim("off")),
|
|
13407
13413
|
// Same reason as workspaceSyncLabel: on Client Expert there is no cloud to sync TO, so
|
|
13408
13414
|
// the banner must not advertise one.
|
|
13409
13415
|
lbl("folder") +
|
|
@@ -13420,7 +13426,7 @@ async function main() {
|
|
|
13420
13426
|
// model that appears offline, and nobody thinks to check a setting they cannot see. Only
|
|
13421
13427
|
// shown when it is actually routing, and never the credentials — host and user only.
|
|
13422
13428
|
if (bannerProxy) statusRows.push(lbl("proxy") + bannerProxy);
|
|
13423
|
-
statusRows.push(
|
|
13429
|
+
statusRows.push(lbl("key") + dim(`${workerKey.slice(0, 8)}…`));
|
|
13424
13430
|
// Under the key: the account this machine signs in as, and its PLAN (#267) — so a machine
|
|
13425
13431
|
// paired to the wrong account, or a lapsed Pro, is obvious at a glance. Both from the VERIFIED
|
|
13426
13432
|
// gu token (no network call), which is the only source in private mode; client mode falls back
|
|
@@ -13429,7 +13435,7 @@ async function main() {
|
|
|
13429
13435
|
const acct = await bannerAccount(accountEmail);
|
|
13430
13436
|
if (acct.email || acct.plan) {
|
|
13431
13437
|
const plan = acct.plan ? (acct.pro ? accent(acct.plan) : dim(acct.plan)) : "";
|
|
13432
|
-
statusRows.push(acct.email ?
|
|
13438
|
+
statusRows.push(acct.email ? lbl("account") + dim(acct.email) + (plan ? dim(" · ") + plan : "") : lbl("plan") + plan);
|
|
13433
13439
|
}
|
|
13434
13440
|
// Skills and MCP servers (task #269) — one row, only when there are any.
|
|
13435
13441
|
try {
|
|
@@ -13439,7 +13445,7 @@ async function main() {
|
|
|
13439
13445
|
// #325: say whose they are when this folder uses Claude Code's — even when there are none.
|
|
13440
13446
|
const claudes = readSkillSource(process.cwd()) === "claude";
|
|
13441
13447
|
if (bits.length || claudes) {
|
|
13442
|
-
statusRows.push(dim((claudes ? "Claude's: " : "") + (bits.join(" · ") || "no skills or MCP servers") + (claudes ? " (/switch-skill · /mcp)" : " (/help · /mcp)")));
|
|
13448
|
+
statusRows.push(lbl("skills") + dim((claudes ? "Claude's: " : "") + (bits.join(" · ") || "no skills or MCP servers") + (claudes ? " (/switch-skill · /mcp)" : " (/help · /mcp)")));
|
|
13443
13449
|
}
|
|
13444
13450
|
} catch {
|
|
13445
13451
|
/* a broken file is reported below; the banner is not the place to fail */
|
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.830",
|
|
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",
|