@tiens.nguyen/gu-cli 1.0.840 → 1.0.842
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/browser-command.mjs +0 -80
- package/gonext_agent_chat.py +64 -3
- package/gu-repl.mjs +12 -86
- package/package.json +1 -1
package/browser-command.mjs
CHANGED
|
@@ -324,86 +324,6 @@ export function findDevInstalls({ roots = browserRoots(), extDir = extensionDir(
|
|
|
324
324
|
return found;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
// THE PUBLISHED EXTENSION, FOUND IN CHROME'S OWN RECORDS (#386, GitHub #176). Its ID is fixed by the
|
|
328
|
-
// store (the last part of the store URL), so a profile has it when its preferences list that ID
|
|
329
|
-
// from anywhere but an unpacked folder. Disabled is told apart: it is installed but cannot connect.
|
|
330
|
-
export const STORE_EXTENSION_ID = "ldfcjapmdbgdkdhlbdkbkiankneabagj";
|
|
331
|
-
|
|
332
|
-
export function findStoreInstalls({ roots = browserRoots(), id = STORE_EXTENSION_ID } = {}) {
|
|
333
|
-
const found = [];
|
|
334
|
-
for (const { browser, dir } of roots) {
|
|
335
|
-
let profiles = [];
|
|
336
|
-
try {
|
|
337
|
-
profiles = readdirSync(dir).filter((p) => p === "Default" || /^Profile \d+$/.test(p));
|
|
338
|
-
} catch {
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
for (const profile of profiles) {
|
|
342
|
-
for (const file of ["Secure Preferences", "Preferences"]) {
|
|
343
|
-
let e;
|
|
344
|
-
try {
|
|
345
|
-
e = JSON.parse(readFileSync(join(dir, profile, file), "utf8"))?.extensions?.settings?.[id];
|
|
346
|
-
} catch {
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
if (!e || UNPACKED.has(e.location)) continue;
|
|
350
|
-
const reasons = Array.isArray(e.disable_reasons) ? e.disable_reasons.length : Number(e.disable_reasons) || 0;
|
|
351
|
-
found.push({ browser, profile, id, disabled: e.state === 0 || reasons > 0 });
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return found;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Where the install stands, and what to tell the user — PURE, so every stage can be tested.
|
|
361
|
-
* → { stage, lines }: "remove-dev" (a developer copy would block the store one, #148), "add" (not
|
|
362
|
-
* in Chrome yet), "enable" (added but switched off), "connect" (added, not connected yet), "done".
|
|
363
|
-
*/
|
|
364
|
-
export function installStage({ dev = [], store = [], status = {}, storeUrl = STORE_URL } = {}) {
|
|
365
|
-
if (status.connected && status.installType === "normal") {
|
|
366
|
-
return { stage: "done", lines: [`✓ gu browser is connected (Chrome Web Store, v${status.version ?? "?"}).`] };
|
|
367
|
-
}
|
|
368
|
-
if (dev.length) {
|
|
369
|
-
return {
|
|
370
|
-
stage: "remove-dev",
|
|
371
|
-
lines: [
|
|
372
|
-
"1. First remove the developer copy of gu browser — only one can connect.",
|
|
373
|
-
" In chrome://extensions (gu opened it), click Remove on this one:",
|
|
374
|
-
...dev.map((d) => ` ${d.browser} · ${d.profile} · ID ${d.id}`),
|
|
375
|
-
" Waiting for it to be removed…",
|
|
376
|
-
],
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
if (!store.length) {
|
|
380
|
-
return {
|
|
381
|
-
stage: "add",
|
|
382
|
-
lines: [
|
|
383
|
-
"1. Add gu browser to Chrome: on its store page, click \"Add to Chrome\".",
|
|
384
|
-
` ${storeUrl}`,
|
|
385
|
-
" (gu opened the page for you.) Waiting for it to be added…",
|
|
386
|
-
],
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
if (store.every((s) => s.disabled)) {
|
|
390
|
-
return {
|
|
391
|
-
stage: "enable",
|
|
392
|
-
lines: [
|
|
393
|
-
"2. gu browser is added but switched off. In chrome://extensions, turn it on.",
|
|
394
|
-
" Waiting for it to be turned on…",
|
|
395
|
-
],
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
return {
|
|
399
|
-
stage: "connect",
|
|
400
|
-
lines: [
|
|
401
|
-
"2. gu browser is added. Waiting for it to connect to gu…",
|
|
402
|
-
" Keep Chrome open — it connects within 30 s, or click its icon → Connect now.",
|
|
403
|
-
],
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
327
|
// gu's own leftovers of the developer install, in ~/.gonext.
|
|
408
328
|
export function localLeftovers() {
|
|
409
329
|
return [extensionDir(), statePath(), seenPath(), logPath()].filter((p) => existsSync(p));
|
package/gonext_agent_chat.py
CHANGED
|
@@ -9511,6 +9511,52 @@ def _shell_exit_code(s: dict):
|
|
|
9511
9511
|
return (None, None)
|
|
9512
9512
|
|
|
9513
9513
|
|
|
9514
|
+
def _shells_add_new(rec: dict) -> None:
|
|
9515
|
+
"""Append one record to shells.json under an exclusive lock, RE-READING the file inside the
|
|
9516
|
+
lock so two overlapping turns do not overwrite each other's record (#387). _to_background used
|
|
9517
|
+
to _shells_load() OUTSIDE any lock then _shells_save() the grown list — a concurrent reader's
|
|
9518
|
+
own save in between silently dropped this record, and the repl's "N shells running (click)"
|
|
9519
|
+
link disappeared for a live background task."""
|
|
9520
|
+
import fcntl # macOS/Linux posix lock; the workspace here is the user's Mac
|
|
9521
|
+
import json as _json
|
|
9522
|
+
import os
|
|
9523
|
+
import time as _t
|
|
9524
|
+
path = _shells_path()
|
|
9525
|
+
lock_fd = os.open(path + ".lock", os.O_RDWR | os.O_CREAT, 0o644)
|
|
9526
|
+
try:
|
|
9527
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
|
9528
|
+
# Re-read INSIDE the lock (the dict envelope {"shells": [...]} matches _shells_save).
|
|
9529
|
+
try:
|
|
9530
|
+
with open(path, encoding="utf-8") as fh:
|
|
9531
|
+
shells = _json.load(fh).get("shells", [])
|
|
9532
|
+
except Exception: # noqa: BLE001
|
|
9533
|
+
shells = []
|
|
9534
|
+
shells = [s for s in shells if isinstance(s, dict) and s.get("pid")]
|
|
9535
|
+
shells.append(rec)
|
|
9536
|
+
# Same prune policy as _shells_save so the file stays bounded.
|
|
9537
|
+
now = _t.time()
|
|
9538
|
+
kept = []
|
|
9539
|
+
for s in shells:
|
|
9540
|
+
st = _shell_state(s, now=now)
|
|
9541
|
+
if st["state"] != "running" and now - st["ended"] > _SHELLS_KEEP_DONE_S:
|
|
9542
|
+
continue
|
|
9543
|
+
kept.append(s)
|
|
9544
|
+
kept = kept[-_SHELLS_KEEP:]
|
|
9545
|
+
tmp = f"{path}.{os.getpid()}.tmp"
|
|
9546
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
9547
|
+
_json.dump({"shells": kept}, fh, indent=2)
|
|
9548
|
+
fh.flush()
|
|
9549
|
+
os.fsync(fh.fileno())
|
|
9550
|
+
os.replace(tmp, path) # the terminal polls this file — never let it read half of it
|
|
9551
|
+
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
|
9552
|
+
finally:
|
|
9553
|
+
try:
|
|
9554
|
+
os.close(lock_fd)
|
|
9555
|
+
except OSError:
|
|
9556
|
+
pass
|
|
9557
|
+
_log(f"shells.json +1 → pid {rec.get('pid')} registered (#387)")
|
|
9558
|
+
|
|
9559
|
+
|
|
9514
9560
|
def _shell_state(s: dict, alive=None, now=None) -> dict:
|
|
9515
9561
|
"""{state, code, runtime, ended} for a shell record. state: running | exited (0) | failed |
|
|
9516
9562
|
killed | ended (gone, exit code unknown). `alive` is injectable so tests need no process."""
|
|
@@ -15575,14 +15621,12 @@ def run_agent_chat(cfg):
|
|
|
15575
15621
|
|
|
15576
15622
|
def _to_background(why: str) -> str:
|
|
15577
15623
|
"""Leave the command running as a background shell; say so to the model."""
|
|
15578
|
-
|
|
15579
|
-
shells.append({
|
|
15624
|
+
_shells_add_new({
|
|
15580
15625
|
"id": str(proc.pid), "pid": proc.pid, "pgid": proc.pid,
|
|
15581
15626
|
"command": command, "workdir": wd, "log": log_path,
|
|
15582
15627
|
"exit_file": exit_path if _use_shell else "",
|
|
15583
15628
|
"started": start, "started_at": _t.strftime("%Y-%m-%d %H:%M:%S"),
|
|
15584
15629
|
})
|
|
15585
|
-
_shells_save(shells)
|
|
15586
15630
|
_emit({"type": "step", "text":
|
|
15587
15631
|
f"Running in background → {_trim_words(command)} (shell {proc.pid})"})
|
|
15588
15632
|
tail = _ws_distill_output(_read_log())
|
|
@@ -15673,6 +15717,15 @@ def run_agent_chat(cfg):
|
|
|
15673
15717
|
# waiting for it like any command; it hands off at the deadline if needed.
|
|
15674
15718
|
if ports and all(_ws_is_ephemeral_port(p_) for p_ in ports):
|
|
15675
15719
|
ports = []
|
|
15720
|
+
if ports and background:
|
|
15721
|
+
# AN EXPLICIT background=True STILL LANDS IN shells.json (#387, GitHub
|
|
15722
|
+
# #177). A shell that opens a port in its first 3s used to be filed ONLY
|
|
15723
|
+
# as a server here — so the repl bar read shells.json, found nothing
|
|
15724
|
+
# running, and showed no "N shells running (click)" link for a build the
|
|
15725
|
+
# model had just told the user was in the background.
|
|
15726
|
+
msg = _to_background("started (also listening)")
|
|
15727
|
+
_last_obs["text"] = msg
|
|
15728
|
+
return msg
|
|
15676
15729
|
if ports:
|
|
15677
15730
|
servers = _ws_load_servers()
|
|
15678
15731
|
servers.append({
|
|
@@ -15697,6 +15750,14 @@ def run_agent_chat(cfg):
|
|
|
15697
15750
|
# no port, so it would otherwise hit the timeout kill below and its
|
|
15698
15751
|
# window would be destroyed). Returning fast here also dodges the
|
|
15699
15752
|
# smolagents 60s executor cap that was killing the tool call.
|
|
15753
|
+
if background:
|
|
15754
|
+
# The window check above would classify it as an app and skip
|
|
15755
|
+
# shells.json entirely — the same missing-link bug as the port
|
|
15756
|
+
# branch (#387, GitHub #177). background=True asked for a shell,
|
|
15757
|
+
# so a shell it is.
|
|
15758
|
+
msg = _to_background("started (window open)")
|
|
15759
|
+
_last_obs["text"] = msg
|
|
15760
|
+
return msg
|
|
15700
15761
|
if _ws_gui_pids(proc.pid):
|
|
15701
15762
|
servers = _ws_load_servers()
|
|
15702
15763
|
servers.append({
|
package/gu-repl.mjs
CHANGED
|
@@ -139,7 +139,7 @@ import {
|
|
|
139
139
|
import { completedFlush, splitLiveStream } from "./completed-flush.mjs";
|
|
140
140
|
import { isScaffoldStep, isGroupableStep, stepGroupMark, stepGroupRows, relativizePaths } from "./step-group.mjs";
|
|
141
141
|
import { clearLogsNoticeText } from "./clear-logs.mjs";
|
|
142
|
-
import { allowIn, allowedIn, bridgeCommand, browserReport, bridgeStatus, extensionDir, findDevInstalls, helpInstall, installExtensionFiles, installSteps, localLeftovers,
|
|
142
|
+
import { allowIn, allowedIn, bridgeCommand, browserReport, bridgeStatus, extensionDir, findDevInstalls, helpInstall, installExtensionFiles, installSteps, localLeftovers, openExtensionsPage, removeLocalFiles, STORE_URL, stopBridge } from "./browser-command.mjs";
|
|
143
143
|
import { commandLogFile, killShell, logTail, markAnnounced, readShells, recapQuestion, shellsToRecap, statusText as shellStatusText, renderShellDetails, shellPanelRows, shellPickerLabel, shellState, shellsNotice } from "./shell-details.mjs";
|
|
144
144
|
import { discoverSkills, matchSkillCommand } from "./skills.mjs";
|
|
145
145
|
import { appendNew, archivePathFor, archivedWorkspaceKey, markReset, seedArchive } from "./conversation-archive.mjs";
|
|
@@ -9620,100 +9620,30 @@ function printShellDetails(rec) {
|
|
|
9620
9620
|
// /browser (#363): the extension's connection, else how to install it.
|
|
9621
9621
|
async function showBrowser() {
|
|
9622
9622
|
const st = await bridgeStatus().catch(() => ({ running: false, connected: false }));
|
|
9623
|
-
const lines = browserReport(st, { allowed: allowedIn(resolve(process.cwd()))
|
|
9623
|
+
const lines = browserReport(st, { allowed: allowedIn(resolve(process.cwd())) });
|
|
9624
9624
|
console.log("");
|
|
9625
9625
|
for (const [i, l] of lines.entries()) console.log(GUTTER + (i === 0 && st.connected ? accent(l) : l.startsWith("⚠") ? yellow(l) : dim(l)));
|
|
9626
9626
|
console.log("");
|
|
9627
9627
|
paintBar();
|
|
9628
9628
|
}
|
|
9629
9629
|
|
|
9630
|
-
// /browser install (#150
|
|
9631
|
-
//
|
|
9632
|
-
// a developer copy is removed first (it would block the store one, #148), then the store page is
|
|
9633
|
-
// opened, and the panel in place of the input box (#384) shows each stage live until the store copy
|
|
9634
|
-
// is CONNECTED — or Esc stops waiting. gu's own window still needs none of this.
|
|
9630
|
+
// /browser install (#150): the extension, for the agent to use YOUR Chrome. Optional — gu's own
|
|
9631
|
+
// window needs nothing. The store page once published; until then the developer way.
|
|
9635
9632
|
async function browserInstall() {
|
|
9636
|
-
|
|
9637
|
-
const done = (line) => {
|
|
9638
|
-
console.log("\n" + GUTTER + line + "\n");
|
|
9639
|
-
paintBar();
|
|
9640
|
-
};
|
|
9633
|
+
let folder = extensionDir();
|
|
9641
9634
|
if (!STORE_URL) {
|
|
9642
|
-
// No published listing configured (GU_BROWSER_STORE_URL=""): the developer way, as before.
|
|
9643
|
-
let folder = extensionDir();
|
|
9644
9635
|
try {
|
|
9645
9636
|
folder = installExtensionFiles();
|
|
9646
9637
|
} catch (e) {
|
|
9647
9638
|
console.log(GUTTER + yellow(`could not copy the extension to ${folder}: ${e.message}`));
|
|
9648
9639
|
}
|
|
9649
|
-
await bridgeStatus().catch(() => null);
|
|
9650
|
-
console.log("");
|
|
9651
|
-
for (const l of installSteps(folder, process.platform, "")) console.log(GUTTER + dim(l));
|
|
9652
|
-
if (helpInstall(folder)) console.log(GUTTER + dim("(Opened chrome://extensions and copied the folder to your clipboard.)"));
|
|
9653
|
-
console.log("");
|
|
9654
|
-
paintBar();
|
|
9655
|
-
return;
|
|
9656
9640
|
}
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
const
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
if (st.stage === "add" && !opened.has("store")) { opened.add("store"); openStorePage(); }
|
|
9664
|
-
if (st.stage === "enable" && !opened.has("ext2")) { opened.add("ext2"); openExtensionsPage(); }
|
|
9665
|
-
return st;
|
|
9666
|
-
};
|
|
9667
|
-
let st = state();
|
|
9668
|
-
if (st.stage === "done") return done(accent(st.lines[0]));
|
|
9669
|
-
if (!barPinned()) {
|
|
9670
|
-
// No zone to wait in: say what to do once.
|
|
9671
|
-
console.log("");
|
|
9672
|
-
for (const l of st.lines) console.log(GUTTER + dim(l));
|
|
9673
|
-
console.log(GUTTER + dim("Then type /browser — it should say connected.") + "\n");
|
|
9674
|
-
paintBar();
|
|
9675
|
-
return;
|
|
9676
|
-
}
|
|
9677
|
-
let polling = false;
|
|
9678
|
-
const poll = setInterval(async () => {
|
|
9679
|
-
if (polling) return;
|
|
9680
|
-
polling = true;
|
|
9681
|
-
try {
|
|
9682
|
-
status = await bridgeStatus({ waitMs: 0 }).catch(() => status);
|
|
9683
|
-
st = state();
|
|
9684
|
-
if (st.stage === "done") {
|
|
9685
|
-
clearInterval(poll);
|
|
9686
|
-
closeZonePanel();
|
|
9687
|
-
done(accent(st.lines[0]) + dim(" The agent can use your Chrome now."));
|
|
9688
|
-
}
|
|
9689
|
-
} finally {
|
|
9690
|
-
polling = false;
|
|
9691
|
-
}
|
|
9692
|
-
}, 2000);
|
|
9693
|
-
openZonePanel({
|
|
9694
|
-
refreshMs: 1000,
|
|
9695
|
-
rows: (width) => {
|
|
9696
|
-
// A row the panel would clip is WRAPPED instead: the store link is 84 characters, and a link
|
|
9697
|
-
// cut short at 80 columns cannot be copied.
|
|
9698
|
-
const room = Math.max(20, width - 1 - GUTTER.length);
|
|
9699
|
-
const wrap = (l) => {
|
|
9700
|
-
const out = [];
|
|
9701
|
-
for (let rest = l; rest.length; rest = rest.slice(room)) out.push(rest.slice(0, room));
|
|
9702
|
-
return out.length ? out : [""];
|
|
9703
|
-
};
|
|
9704
|
-
return [
|
|
9705
|
-
GUTTER + bold(accent("Install gu browser")) + dim(" — so the agent can use your own Chrome"),
|
|
9706
|
-
"",
|
|
9707
|
-
...st.lines.flatMap((l) => wrap(/https?:\/\//.test(l) ? l.trim() : l).map((r) => GUTTER + (/^\d\./.test(l) ? r : dim(r)))),
|
|
9708
|
-
"",
|
|
9709
|
-
GUTTER + dim("Esc to stop waiting · /browser install picks up where you left off"),
|
|
9710
|
-
];
|
|
9711
|
-
},
|
|
9712
|
-
onClose: () => {
|
|
9713
|
-
clearInterval(poll);
|
|
9714
|
-
if (st.stage !== "done") done(dim("(stopped waiting — /browser install picks up where you left off)"));
|
|
9715
|
-
},
|
|
9716
|
-
});
|
|
9641
|
+
await bridgeStatus().catch(() => null); // running, so a fresh install has something to connect to
|
|
9642
|
+
console.log("");
|
|
9643
|
+
for (const l of installSteps(folder)) console.log(GUTTER + dim(l));
|
|
9644
|
+
if (STORE_URL ? openExtensionsPage() : helpInstall(folder)) console.log(GUTTER + dim(STORE_URL ? "(Opened Chrome.)" : "(Opened chrome://extensions and copied the folder to your clipboard.)"));
|
|
9645
|
+
console.log("");
|
|
9646
|
+
paintBar();
|
|
9717
9647
|
}
|
|
9718
9648
|
|
|
9719
9649
|
// /browser cleanup (#145, #147): remove the developer (unpacked) install so the Chrome Web Store
|
|
@@ -10830,11 +10760,7 @@ async function approvalPrompt(command) {
|
|
|
10830
10760
|
process.stdout.write(
|
|
10831
10761
|
"\n" + yellow("🌐 Allow gu to use a web browser?") + dim(" (no answer in ~3 min = Yes, this time)") + "\n" +
|
|
10832
10762
|
dim(` It wants to: ${what || "open a page"}`) + "\n" +
|
|
10833
|
-
dim(" In your Chrome if the gu extension is there, else in gu's own Chrome window.") + "\n"
|
|
10834
|
-
// #386: the way to "your Chrome", said where the question is asked.
|
|
10835
|
-
(STORE_URL && !findStoreInstalls().length
|
|
10836
|
-
? dim(" To use your own Chrome (your logins): /browser install adds the gu extension.") + "\n"
|
|
10837
|
-
: "")
|
|
10763
|
+
dim(" In your Chrome if the gu extension is there, else in gu's own Chrome window.") + "\n"
|
|
10838
10764
|
);
|
|
10839
10765
|
// "Yes, this time" is THE default (#380, GitHub #167): highlighted, so Enter allows it once, and
|
|
10840
10766
|
// also what an unanswered question becomes — the countdown used to sit on "No", which read as
|
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.842",
|
|
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",
|