@tiens.nguyen/gu-cli 1.0.789 → 1.0.791
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/client-expert-run.mjs +274 -47
- package/client-expert-setup.mjs +56 -7
- package/gonext_agent_chat.py +14 -252
- package/gu-cli.mjs +16 -3
- package/gu-repl.mjs +73 -311
- package/mongo-native.mjs +27 -2
- package/package.json +1 -2
- package/terminal-layout.mjs +2 -171
- package/clear-logs.mjs +0 -352
package/gonext_agent_chat.py
CHANGED
|
@@ -5183,90 +5183,6 @@ def _same_embed_model(a: str, b: str) -> bool:
|
|
|
5183
5183
|
_RUNTOOL_PREFIX = "__RUNTOOL__::"
|
|
5184
5184
|
|
|
5185
5185
|
|
|
5186
|
-
# ---- the command that is running, WHOLE (task #331) ------------------------------------------
|
|
5187
|
-
# "Running → <cmd>" carries the first 70 characters, which is right for a history line and wrong
|
|
5188
|
-
# for the one moment someone is watching: a heredoc or a long pipeline was cut at the source, so
|
|
5189
|
-
# the terminal could not show the rest whatever its layout. Sent beside that line (which stays,
|
|
5190
|
-
# for the web app and for older terminals) as ONE line of JSON — a command can span lines.
|
|
5191
|
-
_RUNCMD_PREFIX = "__RUNCMD__::"
|
|
5192
|
-
# How much it has printed so far: "__RUNOUT__::<lines>". From the wait loop, only when it changed.
|
|
5193
|
-
_RUNOUT_PREFIX = "__RUNOUT__::"
|
|
5194
|
-
_RUNCMD_MAX_CHARS = 4000
|
|
5195
|
-
|
|
5196
|
-
# SECRETS. The 70-character cut hid most credentials by accident; showing the whole command must
|
|
5197
|
-
# not undo that. Conservative and by SHAPE — a value after a name that says it is a secret, a
|
|
5198
|
-
# bearer header, user:password@ in a URL, sshpass -p, and the key formats gu itself hands out.
|
|
5199
|
-
# This is what goes to the SCREEN and the job stream; the command that runs is untouched.
|
|
5200
|
-
# "pass" and "token" only as whole words: --passes=3, bypass=true and --max-tokens 100 are not secrets.
|
|
5201
|
-
_SECRET_NAME = (r"(?:password|passwd|passphrase|pwd|(?<![a-z])pass(?![a-z])|secret|token(?![a-z])"
|
|
5202
|
-
r"|api[_-]?key|access[_-]?key)")
|
|
5203
|
-
_CMD_SECRET_RES = (
|
|
5204
|
-
(re.compile(r"(?i)(\b[\w.-]*" + _SECRET_NAME + r"[\w.-]*\s*[=:]\s*)(\"[^\"]*\"|'[^']*'|[^\s;&|]+)"), r"\1•••"),
|
|
5205
|
-
(re.compile(r"(?i)(--?[\w-]*" + _SECRET_NAME + r"[\w-]*[ =])(\"[^\"]*\"|'[^']*'|[^\s;&|]+)"), r"\1•••"),
|
|
5206
|
-
(re.compile(r"(?i)(\bsshpass\s+-p\s*)(\"[^\"]*\"|'[^']*'|[^\s;&|]+)"), r"\1•••"),
|
|
5207
|
-
(re.compile(r"(?i)(authorization:\s*(?:bearer|basic)\s+)[^\s\"']+"), r"\1•••"),
|
|
5208
|
-
(re.compile(r"(?i)(\b[a-z][a-z0-9+.-]*://[^\s/:@]+:)[^\s/@]+(@)"), r"\1•••\2"),
|
|
5209
|
-
(re.compile(r"\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|wk_[A-Za-z0-9_-]{16,}|gu1\.[A-Za-z0-9_.-]{20,})"), "•••"),
|
|
5210
|
-
)
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
def _mask_command_secrets(command: str) -> str:
|
|
5214
|
-
"""The command as it may be SHOWN: credentials replaced by •••, everything else as written."""
|
|
5215
|
-
text = str(command or "")
|
|
5216
|
-
for rx, sub in _CMD_SECRET_RES:
|
|
5217
|
-
text = rx.sub(sub, text)
|
|
5218
|
-
return text
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
def _command_label(command: str, limit: int = 70) -> str:
|
|
5222
|
-
"""A command as ONE LINE of a step label: masked, its first line only, cut to `limit`.
|
|
5223
|
-
|
|
5224
|
-
A step is a line of the stream. A heredoc's newlines made "Command finished → python3 - <<'EOF'"
|
|
5225
|
-
followed by every line of the script as a bullet of its own — and, on the way in, ended the
|
|
5226
|
-
live status row the moment the "Running →" line was read (#332). " …" says there is more.
|
|
5227
|
-
"""
|
|
5228
|
-
lines = [ln.strip() for ln in _mask_command_secrets(command).replace("\r", "").split("\n") if ln.strip()]
|
|
5229
|
-
if not lines:
|
|
5230
|
-
return ""
|
|
5231
|
-
return lines[0][:limit] + (" …" if len(lines) > 1 else "")
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
def _running_command_line(command: str) -> str:
|
|
5235
|
-
"""The stream line that carries the whole command to the terminal (masked, capped)."""
|
|
5236
|
-
shown = _mask_command_secrets(command)
|
|
5237
|
-
if len(shown) > _RUNCMD_MAX_CHARS:
|
|
5238
|
-
shown = shown[:_RUNCMD_MAX_CHARS] + "…"
|
|
5239
|
-
return _RUNCMD_PREFIX + json.dumps({"cmd": shown}, ensure_ascii=False)
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
def _count_output_lines(path: str, state: dict) -> int:
|
|
5243
|
-
"""Lines a running command has written to its log so far. Reads only what is NEW since the
|
|
5244
|
-
last call (`state` keeps the offset), so a chatty ten-minute build is not re-read every tick."""
|
|
5245
|
-
try:
|
|
5246
|
-
with open(path, "rb") as fh:
|
|
5247
|
-
fh.seek(state.get("off", 0))
|
|
5248
|
-
chunk = fh.read()
|
|
5249
|
-
except OSError:
|
|
5250
|
-
return state.get("lines", 0)
|
|
5251
|
-
if chunk:
|
|
5252
|
-
state["off"] = state.get("off", 0) + len(chunk)
|
|
5253
|
-
state["lines"] = state.get("lines", 0) + chunk.count(b"\n")
|
|
5254
|
-
state["partial"] = not chunk.endswith(b"\n")
|
|
5255
|
-
# a last line with no newline yet is still a line someone could read
|
|
5256
|
-
return state.get("lines", 0) + (1 if state.get("partial") else 0)
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
def _emit_command_progress(path: str, state: dict) -> None:
|
|
5260
|
-
"""Tell the terminal how much the running command has printed — only when that changed."""
|
|
5261
|
-
try:
|
|
5262
|
-
n = _count_output_lines(path, state)
|
|
5263
|
-
if n != state.get("sent", 0):
|
|
5264
|
-
state["sent"] = n
|
|
5265
|
-
_emit({"type": "step", "text": f"{_RUNOUT_PREFIX}{n}"})
|
|
5266
|
-
except Exception: # noqa: BLE001 — a progress figure must never fail a command
|
|
5267
|
-
pass
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
5186
|
def _fmt_call(name: str, **kw) -> str:
|
|
5271
5187
|
"""A tool call rendered the way the model writes it. Skips empty/default arguments."""
|
|
5272
5188
|
parts = []
|
|
@@ -6475,114 +6391,6 @@ def _and_chain_parts(command):
|
|
|
6475
6391
|
return parts
|
|
6476
6392
|
|
|
6477
6393
|
|
|
6478
|
-
def _parse_heredoc(command):
|
|
6479
|
-
"""A heredoc or here-string in `command` → what to run and what to feed it. (task #332)
|
|
6480
|
-
|
|
6481
|
-
THE BUG THIS EXISTS FOR. run_command has no shell, so
|
|
6482
|
-
|
|
6483
|
-
python3 - <<'EOF'
|
|
6484
|
-
print("hello")
|
|
6485
|
-
EOF
|
|
6486
|
-
|
|
6487
|
-
became argv ["python3", "-", "<<EOF", "print(hello)", "EOF"]: python read its script from an
|
|
6488
|
-
EMPTY stdin and exited 0 with no output. Nothing ran and the step looked like a success —
|
|
6489
|
-
the #204 failure again, in the one shape #204's check did not cover (its redirect pattern
|
|
6490
|
-
matches "<" and ">", not "<<"). Seen live: the model lost two steps rewriting the work as
|
|
6491
|
-
`python3 -c`. Heredocs are how a strong model runs a multi-line script without a file, so
|
|
6492
|
-
this is the common case, not an edge.
|
|
6493
|
-
|
|
6494
|
-
SUPPORTED, still with no shell: ONE program fed a body.
|
|
6495
|
-
PROG ARGS <<'TAG' … TAG the body becomes the program's stdin
|
|
6496
|
-
PROG ARGS <<-TAG … TAG same, leading tabs stripped
|
|
6497
|
-
PROG ARGS <<< 'text' a here-string: text + newline on stdin
|
|
6498
|
-
REFUSED, each with the one change that fixes it — a guess costs a model step:
|
|
6499
|
-
an unquoted TAG (or a double-quoted here-string) whose text has $ or ` — a shell would
|
|
6500
|
-
expand those and we cannot; anything after the closing TAG; no closing TAG; two heredocs.
|
|
6501
|
-
|
|
6502
|
-
Returns None when there is no heredoc (`echo "a << b"` and `python3 -c "print(1 << 3)"` are
|
|
6503
|
-
not: the operator only counts OUTSIDE quotes), else {"command", "stdin"} or {"error"}.
|
|
6504
|
-
Pure, so every shape is testable without spawning anything.
|
|
6505
|
-
"""
|
|
6506
|
-
s = command or ""
|
|
6507
|
-
q = None
|
|
6508
|
-
i = 0
|
|
6509
|
-
at = -1
|
|
6510
|
-
while i < len(s):
|
|
6511
|
-
c = s[i]
|
|
6512
|
-
if q:
|
|
6513
|
-
if c == q:
|
|
6514
|
-
q = None
|
|
6515
|
-
elif c == "\\" and q == '"':
|
|
6516
|
-
i += 1
|
|
6517
|
-
elif c in "'\"":
|
|
6518
|
-
q = c
|
|
6519
|
-
elif c == "\\":
|
|
6520
|
-
i += 1
|
|
6521
|
-
elif s.startswith("<<", i):
|
|
6522
|
-
at = i
|
|
6523
|
-
break
|
|
6524
|
-
i += 1
|
|
6525
|
-
if at < 0:
|
|
6526
|
-
return None
|
|
6527
|
-
_why = "Error: that command did NOT run — nothing was executed. "
|
|
6528
|
-
before = s[:at]
|
|
6529
|
-
if s.startswith("<<<", at):
|
|
6530
|
-
# HERE-STRING: the next word is the input; anything after it stays an argument.
|
|
6531
|
-
rest = s[at + 3:]
|
|
6532
|
-
import shlex as _shlex
|
|
6533
|
-
try:
|
|
6534
|
-
words = _shlex.split(rest)
|
|
6535
|
-
except ValueError as e:
|
|
6536
|
-
return {"error": _why + f"The here-string is not closed: {e}."}
|
|
6537
|
-
if not words:
|
|
6538
|
-
return {"error": _why + "'<<<' needs the text to feed the program: PROG <<< 'text'."}
|
|
6539
|
-
lead = rest.lstrip()
|
|
6540
|
-
if not lead.startswith("'") and re.search(r"[$`]", words[0]):
|
|
6541
|
-
return {"error": _why + "run_command has NO shell, so $variables and `commands` in a "
|
|
6542
|
-
"here-string are not expanded. Put the text in SINGLE quotes to pass it "
|
|
6543
|
-
"exactly as written, or write the final text yourself."}
|
|
6544
|
-
tail = " ".join(_shlex.quote(w) for w in words[1:])
|
|
6545
|
-
return {"command": (before.rstrip() + (" " + tail if tail else "")).strip(),
|
|
6546
|
-
"stdin": words[0] + "\n"}
|
|
6547
|
-
m = re.match(r"<<(-?)[ \t]*(?:'([^'\n]+)'|\"([^\"\n]+)\"|\\?([A-Za-z_][\w.-]*))", s[at:])
|
|
6548
|
-
if not m:
|
|
6549
|
-
return {"error": _why + "'<<' must be followed by a tag: PROG <<'EOF', the text, then "
|
|
6550
|
-
"EOF on a line of its own."}
|
|
6551
|
-
strip_tabs = bool(m.group(1))
|
|
6552
|
-
tag = m.group(2) or m.group(3) or m.group(4)
|
|
6553
|
-
quoted = bool(m.group(2) or m.group(3)) or s[at + m.end() - len(tag) - 1] == "\\"
|
|
6554
|
-
after = s[at + m.end():]
|
|
6555
|
-
line_rest, nl, body_and_more = after.partition("\n")
|
|
6556
|
-
if "<<" in line_rest and _parse_heredoc(line_rest) is not None:
|
|
6557
|
-
return {"error": _why + "Only one heredoc per command is supported. Write the second "
|
|
6558
|
-
"text with create_file, or run the two commands in separate calls."}
|
|
6559
|
-
lines = body_and_more.split("\n") if nl else []
|
|
6560
|
-
end = -1
|
|
6561
|
-
for k, ln in enumerate(lines):
|
|
6562
|
-
if (ln.lstrip("\t") if strip_tabs else ln).rstrip("\r") == tag:
|
|
6563
|
-
end = k
|
|
6564
|
-
break
|
|
6565
|
-
if end < 0:
|
|
6566
|
-
return {"error": _why + f"The heredoc is never closed: put {tag} on a line of its own "
|
|
6567
|
-
"after the text."}
|
|
6568
|
-
body_lines = [(ln.lstrip("\t") if strip_tabs else ln).rstrip("\r") for ln in lines[:end]]
|
|
6569
|
-
trailing = "\n".join(lines[end + 1:]).strip()
|
|
6570
|
-
if trailing:
|
|
6571
|
-
return {"error": _why + f"There is more after the closing {tag}, and run_command runs "
|
|
6572
|
-
"ONE command per call (only && chains continue). Run what follows in a "
|
|
6573
|
-
"separate run_command call."}
|
|
6574
|
-
cmdline = (before.rstrip() + " " + line_rest.strip()).strip()
|
|
6575
|
-
body = "\n".join(body_lines) + ("\n" if body_lines else "")
|
|
6576
|
-
if not quoted and re.search(r"[$`]", body):
|
|
6577
|
-
return {"error": _why + "run_command has NO shell, so the $variables and `commands` in "
|
|
6578
|
-
f"this heredoc would not be expanded. Quote the tag — <<'{tag}' — to pass the "
|
|
6579
|
-
"text exactly as written (that is what a script wants), or write the final "
|
|
6580
|
-
"text yourself."}
|
|
6581
|
-
if not cmdline:
|
|
6582
|
-
return {"error": _why + "The heredoc has no program to feed: PROG <<'EOF' … EOF."}
|
|
6583
|
-
return {"command": cmdline, "stdin": body}
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
6394
|
def _shell_syntax_error(command, argv):
|
|
6587
6395
|
"""Why this command cannot do what it says, or None. (task #204)
|
|
6588
6396
|
|
|
@@ -7155,7 +6963,7 @@ def _ws_request_approval(api_base, worker_key, job_id, command, reason, deadline
|
|
|
7155
6963
|
_srv, _, _rest = command[len("__MCPTOOL__::"):].partition("\t")
|
|
7156
6964
|
_emit({"type": "step", "text": f"Awaiting your approval: MCP tool {_srv}/{_rest.split(chr(9))[0]}"})
|
|
7157
6965
|
else:
|
|
7158
|
-
_emit({"type": "step", "text": f"Awaiting your approval to run: {
|
|
6966
|
+
_emit({"type": "step", "text": f"Awaiting your approval to run: {command[:70]}"})
|
|
7159
6967
|
deadline = _t.time() + max(30, int(deadline_s)) # per-call; risky=180s, __MAXSTEP__ long
|
|
7160
6968
|
while _t.time() < deadline:
|
|
7161
6969
|
_t.sleep(1.2)
|
|
@@ -7533,12 +7341,6 @@ def _ws_scrubbed_env() -> dict:
|
|
|
7533
7341
|
# CI=1 makes watch-mode test runners (CRA's `npm test`, jest --watch) run ONCE and
|
|
7534
7342
|
# exit instead of sitting in interactive watch mode until the timeout kills them.
|
|
7535
7343
|
env["CI"] = "1"
|
|
7536
|
-
# WINDOWS: a python child reads and writes in the ANSI code page unless told otherwise, so a
|
|
7537
|
-
# script fed on stdin (a heredoc, #332) with "é" or "→" in it is mis-decoded, and printing
|
|
7538
|
-
# one raises UnicodeEncodeError. The worker's own python is started with PYTHONUTF8=1 for
|
|
7539
|
-
# exactly this reason; the whitelist above dropped it for the commands it runs.
|
|
7540
|
-
if _IS_WINDOWS:
|
|
7541
|
-
env["PYTHONUTF8"] = "1"
|
|
7542
7344
|
return env
|
|
7543
7345
|
|
|
7544
7346
|
|
|
@@ -12699,7 +12501,7 @@ def run_agent_chat(cfg):
|
|
|
12699
12501
|
"""Run a command inside a workspace that has run permission (e.g. 'npm test', 'npm run build', 'npm start', 'mvn test', 'python3 app.py'). One-shot commands return their output when they exit. If the command turns out to be a SERVER (keeps running and listens on a port), it is left RUNNING and its URL is reported. If it is a DESKTOP/GUI app (opens a window — tkinter, pygame, PyQt…), it is LAUNCHED and left running: its window appears on the user's Mac; tell them it's open. Both are stoppable via stop_server. Also allows DEPLOY commands (ssh/scp/rsync) — use non-interactive KEY auth (-o BatchMode=yes); never put a password in the command (there is no stdin). Note: an interactive CLI that reads stdin (input()) cannot be driven here — it will fail with EOFError; make it non-interactive or give the user the command to run themselves.
|
|
12700
12502
|
|
|
12701
12503
|
Args:
|
|
12702
|
-
command: the command line. Chain dependent steps with && in ONE call ("cd app && npm ci && npm test"): they run in order, cd sets the folder, the first failure stops the rest.
|
|
12504
|
+
command: the command line. Chain dependent steps with && in ONE call ("cd app && npm ci && npm test"): they run in order, cd sets the folder, the first failure stops the rest. No other shell syntax (no pipes/redirects/;). sudo/su are blocked.
|
|
12703
12505
|
workdir: directory to run in. Empty = first registered workspace.
|
|
12704
12506
|
timeout_seconds: give up after this many seconds if the command neither exits nor starts a server (max 600).
|
|
12705
12507
|
"""
|
|
@@ -12744,24 +12546,7 @@ def run_agent_chat(cfg):
|
|
|
12744
12546
|
"gu-local-worker workspace add <path> --allow-run)")
|
|
12745
12547
|
_last_obs["text"] = msg
|
|
12746
12548
|
return msg
|
|
12747
|
-
|
|
12748
|
-
# command line is what is left. `command` stays the whole text for the risk gate,
|
|
12749
|
-
# the status row and the log; `_cmdline` is what is split and judged as argv.
|
|
12750
|
-
_here = _parse_heredoc(command)
|
|
12751
|
-
if _here and _here.get("error"):
|
|
12752
|
-
_log(f"run_command refused heredoc: {_c(_clip(command, 120))}")
|
|
12753
|
-
_last_obs["text"] = _here["error"]
|
|
12754
|
-
return _here["error"]
|
|
12755
|
-
_cmdline = _here["command"] if _here else (command or "")
|
|
12756
|
-
_stdin_text = _here["stdin"] if _here else None
|
|
12757
|
-
if _here and _and_chain_parts(_cmdline):
|
|
12758
|
-
msg = ("Error: that command did NOT run — nothing was executed. A heredoc "
|
|
12759
|
-
"cannot be part of an && chain here. Run the program that reads it in "
|
|
12760
|
-
"its own run_command call, and pass the folder as the cwd argument "
|
|
12761
|
-
"instead of `cd X &&`.")
|
|
12762
|
-
_last_obs["text"] = msg
|
|
12763
|
-
return msg
|
|
12764
|
-
argv = shlex.split(_cmdline)
|
|
12549
|
+
argv = shlex.split(command or "")
|
|
12765
12550
|
if not argv:
|
|
12766
12551
|
msg = "Error: empty command."
|
|
12767
12552
|
_last_obs["text"] = msg
|
|
@@ -12769,7 +12554,7 @@ def run_agent_chat(cfg):
|
|
|
12769
12554
|
# AN `&&` CHAIN IS ONE STEP (task #274): each part runs through THIS function —
|
|
12770
12555
|
# allowlist, risk gate, approval, server detection, all of it — in order, and
|
|
12771
12556
|
# the first failure stops the rest. `cd DIR` sets the folder for what follows.
|
|
12772
|
-
_chain =
|
|
12557
|
+
_chain = _and_chain_parts(command)
|
|
12773
12558
|
if _chain:
|
|
12774
12559
|
_chain_start = _t.time()
|
|
12775
12560
|
_cur_wd = wd
|
|
@@ -12819,7 +12604,7 @@ def run_agent_chat(cfg):
|
|
|
12819
12604
|
# executes the `cd` PROGRAM — which exits 0 with no output — and the step is
|
|
12820
12605
|
# reported as a success that did nothing. Refusing costs one step; the observed
|
|
12821
12606
|
# alternative cost two hour-long turns and a false report to the user.
|
|
12822
|
-
_shell_err = _shell_syntax_error(
|
|
12607
|
+
_shell_err = _shell_syntax_error(command, argv)
|
|
12823
12608
|
if _shell_err:
|
|
12824
12609
|
_log(f"run_command refused shell syntax: {_c(_clip(command, 120))}")
|
|
12825
12610
|
_last_obs["text"] = _shell_err
|
|
@@ -12837,10 +12622,7 @@ def run_agent_chat(cfg):
|
|
|
12837
12622
|
# Risk gate: privilege escalation / denylisted / destructive. When the
|
|
12838
12623
|
# client can approve interactively (terminal), PAUSE and ask the user;
|
|
12839
12624
|
# otherwise (web) keep hard-blocking the privilege/denylist cases.
|
|
12840
|
-
|
|
12841
|
-
# program (a script that prints "sudo …" is not a sudo), and what a script does
|
|
12842
|
-
# is no more visible here than it is for `python3 -c`.
|
|
12843
|
-
_hard_reason, _ask_reason = _ws_command_risk(_cmdline, argv, _WS_RUN_DENYLIST, wd)
|
|
12625
|
+
_hard_reason, _ask_reason = _ws_command_risk(command, argv, _WS_RUN_DENYLIST, wd)
|
|
12844
12626
|
# Shift+Tab may have switched it since the turn started (#47).
|
|
12845
12627
|
global _accept_edits
|
|
12846
12628
|
if _interactive_approval and _job_id:
|
|
@@ -12856,7 +12638,7 @@ def run_agent_chat(cfg):
|
|
|
12856
12638
|
# states it. "this needs root and sudo needs a password we cannot type" is a
|
|
12857
12639
|
# different question from "sudo is passwordless here", and the user can only
|
|
12858
12640
|
# answer the one they are shown.
|
|
12859
|
-
_priv = _ws_priv_reason(
|
|
12641
|
+
_priv = _ws_priv_reason(command)
|
|
12860
12642
|
_facts = _ws_access_facts() if _priv else None
|
|
12861
12643
|
if _priv and _facts and not _facts["can_elevate"]:
|
|
12862
12644
|
# REFUSE BEFORE SPAWNING (requirement 5). With no tty, `sudo x` dies with
|
|
@@ -12907,8 +12689,7 @@ def run_agent_chat(cfg):
|
|
|
12907
12689
|
_emit({"type": "step", "text": f"Server already up → {_urls} (reused)"})
|
|
12908
12690
|
_last_obs["text"] = msg
|
|
12909
12691
|
return msg
|
|
12910
|
-
_emit({"type": "step", "text": f"Running → {
|
|
12911
|
-
_emit({"type": "step", "text": _running_command_line(command)}) # whole (#331)
|
|
12692
|
+
_emit({"type": "step", "text": f"Running → {command[:70]}"})
|
|
12912
12693
|
t = max(5, min(int(timeout_seconds or 180), 600))
|
|
12913
12694
|
# Own process group (start_new_session) + output to a log file. This is
|
|
12914
12695
|
# what lets a server OUTLIVE this job process, and what lets a timeout
|
|
@@ -12924,30 +12705,13 @@ def run_agent_chat(cfg):
|
|
|
12924
12705
|
if _attrib_note:
|
|
12925
12706
|
_log(f"run_command: {_attrib_note}")
|
|
12926
12707
|
# Scrubbed env: repo scripts must never see worker secrets.
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
import tempfile
|
|
12933
|
-
_stdin_fh = tempfile.TemporaryFile()
|
|
12934
|
-
_stdin_fh.write(_stdin_text.encode("utf-8"))
|
|
12935
|
-
_stdin_fh.seek(0)
|
|
12936
|
-
try:
|
|
12937
|
-
# stdin is passed ONLY for a heredoc — every other command keeps exactly
|
|
12938
|
-
# the stdin it has always had.
|
|
12939
|
-
proc = subprocess.Popen(
|
|
12940
|
-
argv, cwd=wd, env=_ws_scrubbed_env(),
|
|
12941
|
-
stdout=log_fh, stderr=subprocess.STDOUT,
|
|
12942
|
-
**({"stdin": _stdin_fh} if _stdin_fh is not None else {}),
|
|
12943
|
-
**_ws_spawn_kwargs(),
|
|
12944
|
-
)
|
|
12945
|
-
finally:
|
|
12946
|
-
if _stdin_fh is not None:
|
|
12947
|
-
_stdin_fh.close() # the child holds its own copy
|
|
12708
|
+
proc = subprocess.Popen(
|
|
12709
|
+
argv, cwd=wd, env=_ws_scrubbed_env(),
|
|
12710
|
+
stdout=log_fh, stderr=subprocess.STDOUT,
|
|
12711
|
+
**_ws_spawn_kwargs(),
|
|
12712
|
+
)
|
|
12948
12713
|
_INFLIGHT_CMDS.add(proc.pid) # killed with the agent until it is classified
|
|
12949
12714
|
start = _t.time()
|
|
12950
|
-
_out_seen: dict = {} # how much of its output the terminal has been told about
|
|
12951
12715
|
|
|
12952
12716
|
def _read_log() -> str:
|
|
12953
12717
|
log_fh.flush()
|
|
@@ -12980,7 +12744,7 @@ def run_agent_chat(cfg):
|
|
|
12980
12744
|
if rc == 0 else f"FAILED (exit {rc})"
|
|
12981
12745
|
)
|
|
12982
12746
|
_emit({"type": "step", "text":
|
|
12983
|
-
f"Command {'finished' if rc == 0 else 'failed'} → {
|
|
12747
|
+
f"Command {'finished' if rc == 0 else 'failed'} → {command[:50]}"})
|
|
12984
12748
|
# SAY "no output" RATHER THAN SHOW NOTHING (task #204). An empty result
|
|
12985
12749
|
# was indistinguishable from a command that never ran — which is exactly
|
|
12986
12750
|
# what `cd X && Y` used to do — and the model read the silence as a
|
|
@@ -13054,8 +12818,6 @@ def run_agent_chat(cfg):
|
|
|
13054
12818
|
f"{t}s — killed. Output so far:\n{tail}")
|
|
13055
12819
|
_last_obs["text"] = msg
|
|
13056
12820
|
return msg
|
|
13057
|
-
log_fh.flush()
|
|
13058
|
-
_emit_command_progress(log_path, _out_seen)
|
|
13059
12821
|
_t.sleep(2)
|
|
13060
12822
|
except Exception as ex: # noqa: BLE001
|
|
13061
12823
|
msg = f"Error: run_command failed: {type(ex).__name__}: {ex}"
|
package/gu-cli.mjs
CHANGED
|
@@ -716,8 +716,21 @@ const currentApiVersion = async () => {
|
|
|
716
716
|
// that gap. Naming both, and the fix when they differ, is the whole point of the command.
|
|
717
717
|
const installed = await currentVersion();
|
|
718
718
|
const running = String(probe.body?.version ?? "").trim();
|
|
719
|
+
// "(mongo, persistence on)" IS THE API'S CONFIGURATION, NOT A LIVE DATABASE (task #329):
|
|
720
|
+
// /api/health answers without touching MongoDB, so this line said "up" on a machine whose
|
|
721
|
+
// mongod was gone while every real request timed out. The handshake is the live check.
|
|
722
|
+
let mongoDown = "";
|
|
723
|
+
if (probe.ok) {
|
|
724
|
+
try {
|
|
725
|
+
const { localMongoDown } = await import("./client-expert-run.mjs");
|
|
726
|
+
mongoDown = await localMongoDown(ce.localMongoUri());
|
|
727
|
+
} catch { /* unknown — report what the API said */ }
|
|
728
|
+
}
|
|
719
729
|
console.log(
|
|
720
|
-
|
|
730
|
+
mongoDown
|
|
731
|
+
? `\n API is running on ${ce.LOCAL_API_BASE}` + (running ? ` (${running})` : "") +
|
|
732
|
+
`, but MongoDB is NOT answering at ${mongoDown}\n — every request will hang until it does. Run \`gu\`: it says how to start it.`
|
|
733
|
+
: probe.ok
|
|
721
734
|
? `\n API is up on ${ce.LOCAL_API_BASE} (mongo, persistence on)` +
|
|
722
735
|
(running ? ` — running ${running}` : "")
|
|
723
736
|
: `\n API is not usable: ${probe.why}\n Log: ${ce.API_LOG_FILE}`
|
|
@@ -745,7 +758,7 @@ const currentApiVersion = async () => {
|
|
|
745
758
|
}
|
|
746
759
|
} catch { /* no network, no manifest, no line — never an error here */ }
|
|
747
760
|
console.log("");
|
|
748
|
-
process.exit(probe.ok ? 0 : 1);
|
|
761
|
+
process.exit(probe.ok && !mongoDown ? 0 : 1);
|
|
749
762
|
}
|
|
750
763
|
|
|
751
764
|
if (sub === "upgrade") {
|
|
@@ -1528,7 +1541,7 @@ if (args[0] === "mongo") {
|
|
|
1528
1541
|
if (!brew) fail("install", "Homebrew is not installed (https://brew.sh)");
|
|
1529
1542
|
const hasMongod = (await mn.captureOutput("sh", ["-c", "command -v mongod || true"])).trim();
|
|
1530
1543
|
if (!hasMongod) {
|
|
1531
|
-
const ok = await mn.runBrewSteps(brew, mn.
|
|
1544
|
+
const ok = await mn.runBrewSteps(brew, mn.brewInstallSteps({ trust: await mn.brewHasTrust(brew) }).slice(0, -1), { onLine: (l) => console.log(` ${l}`) });
|
|
1532
1545
|
if (!ok) fail("install", "brew install failed (see above)");
|
|
1533
1546
|
}
|
|
1534
1547
|
say("install", true);
|