@tiens.nguyen/gu-cli 1.0.726 → 1.0.728
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/cli-token.mjs +13 -4
- package/gonext_agent_chat.py +7 -8
- package/gu-cli.mjs +27 -10
- package/gu-repl.mjs +5 -5
- package/package.json +1 -1
- package/prompt_scaffold_check.py +12 -9
- package/terminal-layout.mjs +7 -5
- package/version-check.mjs +34 -0
package/cli-token.mjs
CHANGED
|
@@ -291,7 +291,11 @@ export function barNotice({ token, publicKeyB64, mode, now = new Date(), attempt
|
|
|
291
291
|
if (Number.isInteger(p.activeDevices) && p.activeDevices > p.deviceLimit) {
|
|
292
292
|
return {
|
|
293
293
|
kind: "over-limit",
|
|
294
|
-
|
|
294
|
+
// PLAIN WORDS (asked: "make it simple to understand"). It read "gu is active on 4 devices —
|
|
295
|
+
// Free covers 1", which leaves the reader to work out that "active" means "used gu in the
|
|
296
|
+
// last 30 days" and "covers" means "the plan allows". Kept short: the bar drops it entirely
|
|
297
|
+
// when it does not fit the terminal's width.
|
|
298
|
+
text: `${planName(p.plan)} allows ${computers(p.deviceLimit)} — you've used gu on ${p.activeDevices} this month`,
|
|
295
299
|
};
|
|
296
300
|
}
|
|
297
301
|
const age = now.getTime() - Date.parse(p.issuedAt);
|
|
@@ -305,6 +309,11 @@ export function barNotice({ token, publicKeyB64, mode, now = new Date(), attempt
|
|
|
305
309
|
: { kind: "stale", text: "Sign in to keep getting gu updates — run gu login" };
|
|
306
310
|
}
|
|
307
311
|
|
|
312
|
+
/** "Free plan" / "Pro plan" — the name a person uses. */
|
|
313
|
+
const planName = (plan) => (plan === "pro" ? "Pro plan" : "Free plan");
|
|
314
|
+
/** "1 computer" / "4 computers" — "computer", not "device": a device is also a phone. */
|
|
315
|
+
const computers = (n) => `${n} ${n === 1 ? "computer" : "computers"}`;
|
|
316
|
+
|
|
308
317
|
/** What `gu token` prints about a valid token — plain lines, no colour, for the command to style. */
|
|
309
318
|
export function tokenSummary(payload, now = new Date()) {
|
|
310
319
|
const lines = [];
|
|
@@ -317,7 +326,7 @@ export function tokenSummary(payload, now = new Date()) {
|
|
|
317
326
|
}
|
|
318
327
|
if (Number.isInteger(payload.activeDevices)) {
|
|
319
328
|
const n = payload.activeDevices;
|
|
320
|
-
lines.push(`
|
|
329
|
+
lines.push(`Used on ${computers(n)} in the last 30 days — the ${planName(payload.plan)} allows ${payload.deviceLimit}.`);
|
|
321
330
|
}
|
|
322
331
|
lines.push(`Issued ${shortDate(payload.issuedAt)}.`);
|
|
323
332
|
return lines;
|
|
@@ -370,8 +379,8 @@ export function enforcementDecision({ token, publicKeyB64, now = new Date(), sit
|
|
|
370
379
|
enforced: true,
|
|
371
380
|
allowed: false,
|
|
372
381
|
why:
|
|
373
|
-
`This account
|
|
374
|
-
|
|
382
|
+
`This account has used gu on ${computers(p.activeDevices)} in the last 30 days, and the ` +
|
|
383
|
+
`${planName(p.plan)} allows ${p.deviceLimit}. Sign out of one, or remove it at ${accountUrl}.`,
|
|
375
384
|
};
|
|
376
385
|
}
|
|
377
386
|
return { enforced: true, allowed: true, why: "" };
|
package/gonext_agent_chat.py
CHANGED
|
@@ -1757,15 +1757,15 @@ def _load_prompt_scaffold(api_base: str, worker_key: str):
|
|
|
1757
1757
|
file is NOT quietly replaced by the database version, or an edit that broke it would look like
|
|
1758
1758
|
it had no effect."""
|
|
1759
1759
|
if os.path.isfile(_LOCAL_SCAFFOLD_PATH):
|
|
1760
|
-
where =
|
|
1760
|
+
where = _psc.LOCAL_SCAFFOLD_LABEL # never the path — see prompt_scaffold_check.py
|
|
1761
1761
|
try:
|
|
1762
1762
|
with open(_LOCAL_SCAFFOLD_PATH, encoding="utf-8") as f:
|
|
1763
1763
|
text = f.read()
|
|
1764
1764
|
except Exception as e: # noqa: BLE001
|
|
1765
1765
|
raise _AgentConfigError(
|
|
1766
|
-
f"The agent couldn't start:
|
|
1766
|
+
f"The agent couldn't start: {where} could not be read "
|
|
1767
1767
|
f"({type(e).__name__}). Fix it, or delete it to use the gu service's.") from None
|
|
1768
|
-
problem = _scaffold_problem(text,
|
|
1768
|
+
problem = _scaffold_problem(text, where)
|
|
1769
1769
|
if problem:
|
|
1770
1770
|
raise _AgentConfigError(
|
|
1771
1771
|
f"The agent couldn't start: {problem}. Fix it, or delete it to use the gu service's.")
|
|
@@ -2356,7 +2356,8 @@ def _render_prompt(messages, max_chars: int = 400_000) -> str:
|
|
|
2356
2356
|
# the API and shown on the token-usage page — so it would hand the hidden text to
|
|
2357
2357
|
# anyone who opens a step. Its size stays visible, the text does not.
|
|
2358
2358
|
v = _PROMPT_SCAFFOLD_VERSION["v"]
|
|
2359
|
-
|
|
2359
|
+
# The same words whatever the source — a local file is never announced (the user's call).
|
|
2360
|
+
label = f" v{v}" if isinstance(v, int) else ""
|
|
2360
2361
|
text = (f"[prompt scaffold{label} with the tool signatures — "
|
|
2361
2362
|
f"{len(text)} characters, not shown]")
|
|
2362
2363
|
out.append(f"───── {role or 'message'} ─────\n{text}")
|
|
@@ -12177,10 +12178,8 @@ def run_agent_chat(cfg):
|
|
|
12177
12178
|
# TURN_FAILED) instead of quietly running on a different prompt. The version is
|
|
12178
12179
|
# logged; the text never is.
|
|
12179
12180
|
_scaffold, _scaffold_version, _scaffold_source = _load_prompt_scaffold(worker_api_base, worker_api_key)
|
|
12180
|
-
if
|
|
12181
|
-
|
|
12182
|
-
# change the agent. One line in the thinking area, not a banner.
|
|
12183
|
-
_emit({"type": "step", "text": f"using your local prompt scaffold ({_scaffold_source})"})
|
|
12181
|
+
# A LOCAL FILE IS USED SILENTLY — no line, no location (the user's call: "if it is on
|
|
12182
|
+
# .gonext location, not say anything"). Only a BROKEN one speaks, and without the path.
|
|
12184
12183
|
try:
|
|
12185
12184
|
import yaml as _yaml
|
|
12186
12185
|
import os as _os
|
package/gu-cli.mjs
CHANGED
|
@@ -679,7 +679,6 @@ const currentApiVersion = async () => {
|
|
|
679
679
|
// machine that was already paired, i.e. every machine that could need an upgrade, had no
|
|
680
680
|
// path to one. A published API fix reached nobody.
|
|
681
681
|
const run = await import("./client-expert-run.mjs");
|
|
682
|
-
const { isNewer } = await import("./version-check.mjs");
|
|
683
682
|
const running = await currentVersion();
|
|
684
683
|
|
|
685
684
|
// TOP UP THE SEEDED SETTINGS FIRST, AND UNCONDITIONALLY.
|
|
@@ -719,8 +718,19 @@ const currentApiVersion = async () => {
|
|
|
719
718
|
const dir = run.apiInstallDir(manifest.version);
|
|
720
719
|
const depsIn = await ce.apiDepsInstalled(dir);
|
|
721
720
|
const present = (await run.probeApiInstall(manifest.version)).installed;
|
|
722
|
-
|
|
723
|
-
|
|
721
|
+
// DECIDED FROM WHAT IS RUNNING, not only what is on disk (see upgradePlan). A build already
|
|
722
|
+
// downloaded but never started used to answer "Already on 1.0.161 — nothing newer published"
|
|
723
|
+
// and leave yesterday's 1.0.153 process serving. Reported from the startup scaffold line.
|
|
724
|
+
const { upgradePlan } = await import("./version-check.mjs");
|
|
725
|
+
const liveVersion = String((await ce.probeLocalApi()).body?.version ?? "").trim();
|
|
726
|
+
const plan = upgradePlan({
|
|
727
|
+
published: manifest.version,
|
|
728
|
+
installed: running,
|
|
729
|
+
running: liveVersion,
|
|
730
|
+
ready: present && depsIn,
|
|
731
|
+
});
|
|
732
|
+
if (plan.action === "none") {
|
|
733
|
+
console.log(`\n ${plan.say}\n`);
|
|
724
734
|
process.exit(0);
|
|
725
735
|
}
|
|
726
736
|
// A version already on disk but unusable is a RESUME, not an upgrade — someone whose
|
|
@@ -730,12 +740,12 @@ const currentApiVersion = async () => {
|
|
|
730
740
|
// Caught by running it: "Finishing an interrupted install of 1.0.30" on a box going
|
|
731
741
|
// cleanly from 1.0.29.
|
|
732
742
|
console.log(
|
|
733
|
-
present && !depsIn
|
|
743
|
+
plan.action === "install" && present && !depsIn
|
|
734
744
|
? `\n Finishing an interrupted install of ${manifest.version}`
|
|
735
|
-
: `\n
|
|
745
|
+
: `\n ${plan.say}`
|
|
736
746
|
);
|
|
737
747
|
try {
|
|
738
|
-
if (!present || !depsIn) {
|
|
748
|
+
if (plan.action === "install" && (!present || !depsIn)) {
|
|
739
749
|
await run.downloadApiPackage(manifest, { onProgress: (l) => console.log(` ${l}`) });
|
|
740
750
|
await ce.installApiDeps(dir, {
|
|
741
751
|
onLine: (l) => l && l.length < 160 && console.log(` ${l}`),
|
|
@@ -760,12 +770,19 @@ const currentApiVersion = async () => {
|
|
|
760
770
|
console.log(` Starting ${version}…`);
|
|
761
771
|
await ce.startLocalApi({ apiDir: run.apiInstallDir(version) });
|
|
762
772
|
const health = await ce.waitForApi({});
|
|
773
|
+
// "An API answers" is not "the NEW API answers": an old process that never stopped answers
|
|
774
|
+
// too, and this line once reported the new version while the old one kept serving.
|
|
775
|
+
const serving = String((await ce.probeLocalApi()).body?.version ?? "").trim();
|
|
776
|
+
const wrong = health.ok && Boolean(serving) && serving !== version;
|
|
763
777
|
console.log(
|
|
764
|
-
health.ok
|
|
765
|
-
? ` API
|
|
766
|
-
:
|
|
778
|
+
!health.ok
|
|
779
|
+
? ` API did not come up: ${health.why}\n Log: ${ce.API_LOG_FILE}\n`
|
|
780
|
+
: wrong
|
|
781
|
+
? ` ${serving} is still the one answering on ${ce.LOCAL_API_BASE}, not ${version}.\n` +
|
|
782
|
+
` Stop it (gu-cli api stop), then: gu-cli api start\n`
|
|
783
|
+
: ` API is up on ${ce.LOCAL_API_BASE} (${serving || version})\n`
|
|
767
784
|
);
|
|
768
|
-
process.exit(health.ok ? 0 : 1);
|
|
785
|
+
process.exit(health.ok && !wrong ? 0 : 1);
|
|
769
786
|
}
|
|
770
787
|
|
|
771
788
|
if (sub === "stop") {
|
package/gu-repl.mjs
CHANGED
|
@@ -8744,14 +8744,14 @@ const wrapThought = (s, width, maxLines) => {
|
|
|
8744
8744
|
* own conversation.
|
|
8745
8745
|
*/
|
|
8746
8746
|
/**
|
|
8747
|
-
* WHERE THIS SESSION'S SCAFFOLD COMES FROM, said at `gu` start (task #246) — "scaffold
|
|
8748
|
-
* the user's own
|
|
8749
|
-
*
|
|
8747
|
+
* WHERE THIS SESSION'S SCAFFOLD COMES FROM, said at `gu` start (task #246) — "scaffold fetched" from
|
|
8748
|
+
* the gu service; NOTHING at all when it is the user's own local file (by request — its location is
|
|
8749
|
+
* never printed), unless that file is broken. Background: startup never waits on it (an offline private-mode machine would otherwise
|
|
8750
8750
|
* sit for 10s); the line prints when the answer arrives, or at the next prompt if a turn is running.
|
|
8751
8751
|
*
|
|
8752
8752
|
* · A LOCAL FILE is checked with the worker's OWN checker (prompt_scaffold_check.py — size,
|
|
8753
|
-
* placeholders, a trial render)
|
|
8754
|
-
*
|
|
8753
|
+
* placeholders, a trial render); a good one is silent, a broken one is reported. The API is not
|
|
8754
|
+
* asked: turns use the file.
|
|
8755
8755
|
* · Otherwise the API is asked to REFRESH — the fetch that the rest of the session then reuses
|
|
8756
8756
|
* ("keep in cache until the user type gu").
|
|
8757
8757
|
*/
|
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.728",
|
|
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",
|
package/prompt_scaffold_check.py
CHANGED
|
@@ -6,9 +6,9 @@ at startup (`python3 prompt_scaffold_check.py`) so the "scaffold loaded" line it
|
|
|
6
6
|
file really passed — not just that a file exists. Needs only jinja2 (a smolagents dependency), so
|
|
7
7
|
the startup check is quick.
|
|
8
8
|
|
|
9
|
-
python3 prompt_scaffold_check.py [path] → one JSON line: {"exists", "
|
|
9
|
+
python3 prompt_scaffold_check.py [path] → one JSON line: {"exists", "chars", "problem"}
|
|
10
10
|
|
|
11
|
-
It never prints the scaffold text.
|
|
11
|
+
It never prints the scaffold text, nor where the file is.
|
|
12
12
|
"""
|
|
13
13
|
import json
|
|
14
14
|
import os
|
|
@@ -19,7 +19,10 @@ import sys
|
|
|
19
19
|
# file — the scaffold stays off users' disks unless they put it there themselves — so it is an
|
|
20
20
|
# override for trying a wording before inserting it into production, not a cache.
|
|
21
21
|
LOCAL_SCAFFOLD_PATH = os.path.join(os.path.expanduser("~"), ".gonext", "prompt-scaffolds", "code-agent.txt")
|
|
22
|
-
|
|
22
|
+
# How the file is NAMED in anything a person reads: never its location. Asked for: "dont say out loud
|
|
23
|
+
# where is the location of the scaffold file … not say anything". The deploy app's own Commands
|
|
24
|
+
# window is the one place the path is written down, for its owner.
|
|
25
|
+
LOCAL_SCAFFOLD_LABEL = "your local scaffold file"
|
|
23
26
|
|
|
24
27
|
# What a scaffold must keep — the same list the API and the deploy app use.
|
|
25
28
|
SCAFFOLD_REQUIRED = (
|
|
@@ -97,18 +100,18 @@ def scaffold_problem(template, source: str) -> str:
|
|
|
97
100
|
def check_file(path: str = LOCAL_SCAFFOLD_PATH, label: str = LOCAL_SCAFFOLD_LABEL) -> dict:
|
|
98
101
|
"""The local override's state, for `gu` at startup. Never includes the text."""
|
|
99
102
|
if not os.path.isfile(path):
|
|
100
|
-
return {"exists": False
|
|
103
|
+
return {"exists": False}
|
|
101
104
|
try:
|
|
102
105
|
with open(path, encoding="utf-8") as f:
|
|
103
106
|
text = f.read()
|
|
104
107
|
except Exception as e: # noqa: BLE001
|
|
105
|
-
return {"exists": True, "
|
|
106
|
-
"problem": f"
|
|
107
|
-
return {"exists": True, "
|
|
108
|
-
"problem": scaffold_problem(text,
|
|
108
|
+
return {"exists": True, "chars": 0,
|
|
109
|
+
"problem": f"{label} could not be read ({type(e).__name__})"}
|
|
110
|
+
return {"exists": True, "chars": len(text),
|
|
111
|
+
"problem": scaffold_problem(text, label)}
|
|
109
112
|
|
|
110
113
|
|
|
111
114
|
if __name__ == "__main__":
|
|
112
115
|
target = sys.argv[1] if len(sys.argv) > 1 else LOCAL_SCAFFOLD_PATH
|
|
113
|
-
label = LOCAL_SCAFFOLD_LABEL
|
|
116
|
+
label = LOCAL_SCAFFOLD_LABEL
|
|
114
117
|
print(json.dumps(check_file(target, label)))
|
package/terminal-layout.mjs
CHANGED
|
@@ -1770,20 +1770,22 @@ export function lazyModuleNames(text) {
|
|
|
1770
1770
|
|
|
1771
1771
|
/**
|
|
1772
1772
|
* The one line `gu` prints at startup about the agent's prompt scaffold (task #246) — asked for so
|
|
1773
|
-
* the user
|
|
1774
|
-
*
|
|
1773
|
+
* the user knows where this session's scaffold came from — the gu service ("fetched") — and hears
|
|
1774
|
+
* NOTHING when it is their own local file (by request). Pure; gu-repl gathers the facts.
|
|
1775
1775
|
*
|
|
1776
1776
|
* local — prompt_scaffold_check.py's answer: { exists, path, chars, problem } (null = not run)
|
|
1777
1777
|
* refresh — POST /api/worker/prompt-scaffold/refresh: { status, ok, version, error, kept }
|
|
1778
1778
|
* private — private mode (the local API), which decides what "update" advice means
|
|
1779
|
-
* Returns { kind: "ok" | "warn" | "err", text }.
|
|
1779
|
+
* Returns { kind: "ok" | "warn" | "err", text }, or null when there is nothing to say.
|
|
1780
1780
|
*/
|
|
1781
1781
|
export function scaffoldStartupNotice({ local = null, refresh = null, private: privateMode = false } = {}) {
|
|
1782
|
-
|
|
1782
|
+
// YOUR LOCAL FILE, WORKING: nothing is said — no line, no location (the user's call: "dont say
|
|
1783
|
+
// out loud where is the location of the scaffold file … not say anything"). null = print nothing.
|
|
1784
|
+
// A BROKEN one still speaks, without the path: otherwise every turn fails with no explanation.
|
|
1783
1785
|
if (local?.exists) {
|
|
1784
1786
|
return local.problem
|
|
1785
1787
|
? { kind: "err", text: `scaffold: ${local.problem} — fix it, or delete it to use the gu service's` }
|
|
1786
|
-
:
|
|
1788
|
+
: null;
|
|
1787
1789
|
}
|
|
1788
1790
|
if (!refresh) return { kind: "warn", text: "scaffold: couldn't ask the API — the first turn will try again" };
|
|
1789
1791
|
if (refresh.status === 404) {
|
package/version-check.mjs
CHANGED
|
@@ -70,3 +70,37 @@ export function updatesAvailable({ cliRunning, cliLatest, apiRunning, apiLatest
|
|
|
70
70
|
const api = isNewer(apiLatest, apiRunning) ? String(apiLatest) : "";
|
|
71
71
|
return { cli, api, any: Boolean(cli || api) };
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* WHAT `gu api upgrade` SHOULD DO — decided from the version that is RUNNING, not only the one on
|
|
76
|
+
* disk. Pure, so every case is tested.
|
|
77
|
+
*
|
|
78
|
+
* REPORTED: the startup line said "your local gu API is older than this gu — run gu api upgrade",
|
|
79
|
+
* and upgrading did nothing. 1.0.161 was already downloaded (by the in-app updater), so the old
|
|
80
|
+
* check compared "published 1.0.161" with "installed 1.0.161", answered "Already on 1.0.161 —
|
|
81
|
+
* nothing newer published" and exited — while a process started the day before from 1.0.153 kept
|
|
82
|
+
* serving. `gu api status` had learnt this difference long ago; `upgrade` had not.
|
|
83
|
+
*
|
|
84
|
+
* published — the manifest's version installed — the newest usable build on disk
|
|
85
|
+
* running — what /api/health reports ("" when the API is down or too old to say)
|
|
86
|
+
* ready — `installed` has its dependencies (a runnable install)
|
|
87
|
+
* Returns { action: "none" | "install" | "restart", say }:
|
|
88
|
+
* install → download/finish the published build, then restart onto it;
|
|
89
|
+
* restart → nothing to download, but the running process is not the installed build.
|
|
90
|
+
*/
|
|
91
|
+
export function upgradePlan({ published = "", installed = "", running = "", ready = false } = {}) {
|
|
92
|
+
const target = published && (!installed || isNewer(published, installed)) ? published : installed;
|
|
93
|
+
if (target === published && published && (!ready || installed !== published)) {
|
|
94
|
+
return { action: "install", say: `Upgrading the API: ${running || installed || "none"} → ${published}` };
|
|
95
|
+
}
|
|
96
|
+
if (running && (running === target || isNewer(running, target))) {
|
|
97
|
+
// Running something at least as new as anything here — never "restart" onto an older build.
|
|
98
|
+
return { action: "none", say: `Already on ${running} — nothing newer published.` };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
action: "restart",
|
|
102
|
+
say: running
|
|
103
|
+
? `${target} is installed but ${running} is still running — restarting onto ${target}`
|
|
104
|
+
: `Starting ${target}`,
|
|
105
|
+
};
|
|
106
|
+
}
|