@tiens.nguyen/gu-cli 1.0.833 → 1.0.834

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.833",
3
+ "version": "1.0.834",
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",
@@ -125,6 +125,13 @@
125
125
  "skills.mjs",
126
126
  "skill-source.mjs",
127
127
  "shell-details.mjs",
128
+ "browser-command.mjs",
129
+ "browser-bridge.mjs",
130
+ "browser-extension/manifest.json",
131
+ "browser-extension/background.js",
132
+ "browser-extension/popup.html",
133
+ "browser-extension/popup.js",
134
+ "browser-extension/icons/",
128
135
  "step-group.mjs",
129
136
  "mcp-config.mjs",
130
137
  "mcp-command.mjs",
@@ -136,6 +143,7 @@
136
143
  "gu_mcp.py",
137
144
  "gu_skills.py",
138
145
  "gu_test_digest.py",
146
+ "gu_browser.py",
139
147
  "gu_hooks.py",
140
148
  "prompt_scaffold_check.py",
141
149
  "gonext_transcribe.py",
package/shell-details.mjs CHANGED
@@ -11,9 +11,9 @@
11
11
  // read from the same two facts the agent uses: is the process group alive, and has the shell
12
12
  // written its exit file.
13
13
 
14
- import { existsSync, openSync, readSync, closeSync, fstatSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
14
+ import { existsSync, openSync, readSync, closeSync, fstatSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
15
15
  import { homedir } from "node:os";
16
- import { isAbsolute, join, normalize } from "node:path";
16
+ import { basename, dirname, isAbsolute, join, normalize } from "node:path";
17
17
  import { spawnSync } from "node:child_process";
18
18
 
19
19
  export const shellsPath = (home = homedir()) => join(home, ".gonext", "shells.json");
@@ -63,7 +63,7 @@ export function shellState(rec, { alive, exit, now = Date.now() / 1000 } = {}) {
63
63
  if (live && !ex && !rec?.killed) return { state: "running", code: null, runtime: Math.max(0, now - started), ended: now };
64
64
  const ended = Number(rec?.ended) || ex?.at || now;
65
65
  const state = rec?.killed ? "killed" : !ex ? "ended" : ex.code === 0 ? "exited" : "failed";
66
- return { state, code: ex ? ex.code : null, runtime: Math.max(0, ended - started), ended };
66
+ return { state, code: ex ? ex.code : null, runtime: Math.max(0, ended - started), ended, by: String(rec?.killed_by ?? "") };
67
67
  }
68
68
 
69
69
  /** 7m 2s — Claude Code's spelling. */
@@ -79,7 +79,10 @@ export function fmtRuntime(seconds) {
79
79
  export function statusText(st) {
80
80
  if (st.state === "running") return "running";
81
81
  if (st.state === "exited" || st.state === "failed") return `exited ${st.code}`;
82
- return st.state === "killed" ? "killed" : "ended (exit code unknown)";
82
+ // Who stopped it (#362/#143): a bare "killed" read as a crash when the agent had stopped its own
83
+ // search on purpose.
84
+ if (st.state === "killed") return st.by ? `killed — stopped by ${st.by}` : "killed";
85
+ return "ended (exit code unknown)";
83
86
  }
84
87
 
85
88
  /**
@@ -250,8 +253,59 @@ export async function killShell(rec, { path = shellsPath(), platform = process.p
250
253
  const mine = shells.find((s) => String(s.id) === String(rec.id));
251
254
  if (!mine || readExit(mine)) return; // it finished on its own first: keep the real exit code
252
255
  mine.killed = true;
256
+ mine.killed_by = "you (Stop it in Shell details)";
257
+ mine.announced = true; // you stopped it — no recap turn for it
253
258
  mine.ended = Date.now() / 1000;
254
259
  const tmp = `${path}.${process.pid}.tmp`;
255
260
  writeFileSync(tmp, JSON.stringify({ shells }, null, 2));
256
261
  renameSync(tmp, path);
257
262
  }
263
+
264
+ /**
265
+ * Shells whose ending the agent has not heard about (#362) — Claude Code tells its model when a
266
+ * background command completes; gu's terminal starts a short recap turn for these. Only this
267
+ * folder's shells (shells.json is shared by every project), only ones that ended while this
268
+ * terminal was open, never one the agent already read (`seen`) or was told about (`announced`),
269
+ * and never one you stopped yourself.
270
+ */
271
+ export function shellsToRecap(entries, { cwd = process.cwd(), since = 0 } = {}) {
272
+ // The nearest folder that still exists, resolved (macOS: /var → /private/var), plus the rest —
273
+ // a build may have deleted the folder it ran in.
274
+ const real = (p) => {
275
+ const raw = String(p ?? "");
276
+ if (!raw) return raw;
277
+ try { return realpathSync(raw); } catch { /* gone */ }
278
+ const up = dirname(raw);
279
+ return up === raw ? raw : join(real(up), basename(raw));
280
+ };
281
+ const base = real(cwd).replace(/\/+$/, "");
282
+ const under = (wd) => {
283
+ const w = real(wd);
284
+ return w === base || w.startsWith(base + "/");
285
+ };
286
+ return entries
287
+ .filter(({ rec, st }) => st.state !== "running" && st.state !== "killed" && !rec.seen && !rec.announced
288
+ && st.ended >= since && under(rec.workdir))
289
+ .map((e) => e.rec);
290
+ }
291
+
292
+ /** Mark these shells announced in shells.json, so neither the agent nor another terminal repeats it. */
293
+ export function markAnnounced(ids, { path = shellsPath() } = {}) {
294
+ const shells = readShells(path);
295
+ let n = 0;
296
+ for (const s of shells) if (ids.includes(String(s.id)) && !s.announced) { s.announced = true; n++; }
297
+ if (!n) return 0;
298
+ const tmp = `${path}.${process.pid}.tmp`;
299
+ writeFileSync(tmp, JSON.stringify({ shells }, null, 2));
300
+ renameSync(tmp, path);
301
+ return n;
302
+ }
303
+
304
+ /** The recap question the terminal asks for finished shells. */
305
+ export function recapQuestion(entries) {
306
+ const parts = entries.map(({ rec, st }) =>
307
+ `shell ${rec.id} ${statusText(st)} after ${fmtRuntime(st.runtime)} (${String(rec.command ?? "").replace(/\s+/g, " ").trim().slice(0, 120)})`);
308
+ const ids = entries.map(({ rec }) => `shell_output('${rec.id}')`).join(", ");
309
+ return `[gu: background ${entries.length === 1 ? "shell" : "shells"} finished — ${parts.join("; ")}] `
310
+ + `Read the result with ${ids}, tell me briefly what happened, and continue with the next step if the task needs one.`;
311
+ }