@tiens.nguyen/gu-cli 1.0.766 → 1.0.768
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-api.mjs +29 -5
- package/client-expert-run.mjs +2 -1
- package/gonext_agent_chat.py +6 -0
- package/gu-cli.mjs +14 -2
- package/gu-repl.mjs +22 -6
- package/package.json +2 -1
- package/step-tokens.mjs +55 -0
package/client-expert-api.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { spawnSurviving, pidOnPort } from "./platform-tools.mjs";
|
|
|
23
23
|
import { DEFAULT_API_BASE as HOSTED_API_BASE } from "./device-login.mjs";
|
|
24
24
|
import { spawn } from "node:child_process";
|
|
25
25
|
import { writeFile, mkdir, readFile, access } from "node:fs/promises";
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
26
27
|
import { homedir } from "node:os";
|
|
27
28
|
import { join } from "node:path";
|
|
28
29
|
|
|
@@ -32,6 +33,29 @@ export const LOCAL_API_BASE = `http://127.0.0.1:${LOCAL_API_PORT}`;
|
|
|
32
33
|
export const LOCAL_MONGO_URI = process.env.GONEXT_LOCAL_MONGO_URI || "mongodb://127.0.0.1:27017";
|
|
33
34
|
export const LOCAL_DB_NAME = process.env.GONEXT_LOCAL_DB_NAME || "gu";
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* THE URI THIS MACHINE'S MONGODB ACTUALLY ACCEPTS — credentials included (task #15).
|
|
38
|
+
*
|
|
39
|
+
* Setup asks for a username and password when 27017 already belongs to a MongoDB with its own
|
|
40
|
+
* admin, and saves the working URI as localMongoUri in client-expert.json. Every later caller
|
|
41
|
+
* used LOCAL_MONGO_URI instead — the empty pair — so on such a machine the model seed failed
|
|
42
|
+
* with "Command find requires authentication" (reported on a fresh Ubuntu box: coder "—", "agent
|
|
43
|
+
* model is not set"), and every API restart started a server that could not read its own
|
|
44
|
+
* database. Read here, once, as every default, so no caller can forget it.
|
|
45
|
+
*
|
|
46
|
+
* GONEXT_LOCAL_MONGO_URI still wins: setup treats a forced URI as deliberate and never saves one.
|
|
47
|
+
*/
|
|
48
|
+
export function localMongoUri({ env = process.env, home = homedir() } = {}) {
|
|
49
|
+
if (env.GONEXT_LOCAL_MONGO_URI) return env.GONEXT_LOCAL_MONGO_URI;
|
|
50
|
+
try {
|
|
51
|
+
const st = JSON.parse(readFileSync(join(home, ".gonext", "client-expert.json"), "utf8"));
|
|
52
|
+
if (typeof st?.localMongoUri === "string" && st.localMongoUri) return st.localMongoUri;
|
|
53
|
+
} catch {
|
|
54
|
+
/* no setup yet, or unreadable — the default is the only thing there is to try */
|
|
55
|
+
}
|
|
56
|
+
return LOCAL_MONGO_URI;
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
const GONEXT_DIR = join(homedir(), ".gonext");
|
|
36
60
|
export const API_PID_FILE = join(GONEXT_DIR, "local-api.pid");
|
|
37
61
|
export const API_LOG_FILE = join(GONEXT_DIR, "local-api.log");
|
|
@@ -62,7 +86,7 @@ export function generateLocalUserId() {
|
|
|
62
86
|
*/
|
|
63
87
|
export function apiEnv({
|
|
64
88
|
port = LOCAL_API_PORT,
|
|
65
|
-
mongoUri =
|
|
89
|
+
mongoUri = localMongoUri(),
|
|
66
90
|
dbName = LOCAL_DB_NAME,
|
|
67
91
|
base = {},
|
|
68
92
|
hostedBase = hostedApiBase(),
|
|
@@ -201,7 +225,7 @@ export function installApiDeps(apiDir, { onLine } = {}) {
|
|
|
201
225
|
*/
|
|
202
226
|
export async function seedDatabase({
|
|
203
227
|
apiDir,
|
|
204
|
-
mongoUri =
|
|
228
|
+
mongoUri = localMongoUri(),
|
|
205
229
|
dbName = LOCAL_DB_NAME,
|
|
206
230
|
userId,
|
|
207
231
|
workerKey,
|
|
@@ -245,7 +269,7 @@ export async function seedDatabase({
|
|
|
245
269
|
export async function startLocalApi({
|
|
246
270
|
apiDir,
|
|
247
271
|
port = LOCAL_API_PORT,
|
|
248
|
-
mongoUri =
|
|
272
|
+
mongoUri = localMongoUri(),
|
|
249
273
|
dbName = LOCAL_DB_NAME,
|
|
250
274
|
} = {}) {
|
|
251
275
|
await mkdir(GONEXT_DIR, { recursive: true });
|
|
@@ -700,7 +724,7 @@ export function settingsPatch({ existing = {}, models = CLIENT_EXPERT_MODELS } =
|
|
|
700
724
|
*/
|
|
701
725
|
export async function modelsAlreadyConfigured({
|
|
702
726
|
apiDir,
|
|
703
|
-
mongoUri =
|
|
727
|
+
mongoUri = localMongoUri(),
|
|
704
728
|
dbName = LOCAL_DB_NAME,
|
|
705
729
|
userId,
|
|
706
730
|
} = {}) {
|
|
@@ -721,7 +745,7 @@ export async function modelsAlreadyConfigured({
|
|
|
721
745
|
|
|
722
746
|
export async function seedSettings({
|
|
723
747
|
apiDir,
|
|
724
|
-
mongoUri =
|
|
748
|
+
mongoUri = localMongoUri(),
|
|
725
749
|
dbName = LOCAL_DB_NAME,
|
|
726
750
|
userId,
|
|
727
751
|
// The box the user chose at setup (task #220), or the shared-Ollama defaults when the
|
package/client-expert-run.mjs
CHANGED
|
@@ -894,7 +894,7 @@ export async function runClientExpertSetup({ ask }) {
|
|
|
894
894
|
// machine alone (that rule protects anyone who set their models up by hand), so on a re-run
|
|
895
895
|
// of this step the answer would be collected and silently discarded. Point at `/model`
|
|
896
896
|
// instead — it is the one path that CAN change a machine that is already set up.
|
|
897
|
-
const already = await modelsAlreadyConfigured({ apiDir, userId: creds.userId });
|
|
897
|
+
const already = await modelsAlreadyConfigured({ apiDir, mongoUri, userId: creds.userId });
|
|
898
898
|
if (already) {
|
|
899
899
|
console.log("");
|
|
900
900
|
console.log(dim(" Models are already configured on this machine — leaving them alone."));
|
|
@@ -946,6 +946,7 @@ export async function runClientExpertSetup({ ask }) {
|
|
|
946
946
|
try {
|
|
947
947
|
const written = await seedSettings({
|
|
948
948
|
apiDir,
|
|
949
|
+
mongoUri,
|
|
949
950
|
userId: creds.userId,
|
|
950
951
|
...(chosenModels ? { models: chosenModels } : {}),
|
|
951
952
|
});
|
package/gonext_agent_chat.py
CHANGED
|
@@ -10257,8 +10257,12 @@ def run_agent_chat(cfg):
|
|
|
10257
10257
|
f"(fresh {_bd['ragFresh']}, carried {_bd['ragCarried']}) · "
|
|
10258
10258
|
f"tools {_bd['tools']} · code {_bd['code']}"
|
|
10259
10259
|
+ ("" if _RAG_LEDGER else " [RAG NOT CALLED this turn]"))
|
|
10260
|
+
# `step` (task #298): which model call these figures belong to, so the terminal
|
|
10261
|
+
# can show THIS step's ↑/↓ on the thinking line instead of the turn totals the
|
|
10262
|
+
# bar already shows. Same counter as _RAG_STEP, advanced once per call above.
|
|
10260
10263
|
_emit({"type": "tokens", "codeInput": self._code_tokens_total,
|
|
10261
10264
|
"codeMax": self._code_tokens_max, "stepInput": _step_est,
|
|
10265
|
+
"step": self._call_index,
|
|
10262
10266
|
"exact": False, "breakdown": _bd})
|
|
10263
10267
|
# RECORD WHAT WE ARE ABOUT TO SEND, BEFORE WE SEND IT (task #155).
|
|
10264
10268
|
#
|
|
@@ -10514,6 +10518,7 @@ def run_agent_chat(cfg):
|
|
|
10514
10518
|
"codeInput": self._code_tokens_total,
|
|
10515
10519
|
"codeMax": self._code_tokens_max,
|
|
10516
10520
|
"stepInput": exact,
|
|
10521
|
+
"step": self._call_index,
|
|
10517
10522
|
# Task #142: the SAME split, rescaled to the server's authoritative
|
|
10518
10523
|
# count — the shares are the shares whether or not our ~4 chars/token
|
|
10519
10524
|
# estimate was right, so the breakdown never contradicts the total.
|
|
@@ -10547,6 +10552,7 @@ def run_agent_chat(cfg):
|
|
|
10547
10552
|
"codeMax": self._code_tokens_max,
|
|
10548
10553
|
"codeOutput": self._code_output_tokens_total,
|
|
10549
10554
|
"stepOutput": step_out,
|
|
10555
|
+
"step": self._call_index,
|
|
10550
10556
|
# Absent, not 0, when the server never reported it — the page must be
|
|
10551
10557
|
# able to say "not reported" rather than show a 0% hit rate that
|
|
10552
10558
|
# would read as a cache problem we do not actually have.
|
package/gu-cli.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { mkdir, mkdtemp, readFile, rm, stat, unlink, writeFile } from "node:fs/p
|
|
|
19
19
|
import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
21
|
import { createTaskPool, createFolderQueue } from "./job-pools.mjs";
|
|
22
|
+
import { NO_STEP, foldStepTokens, withStepTokens } from "./step-tokens.mjs";
|
|
22
23
|
import { agentStoppedLine, jobEnded, jobStatusFrom409, stopGracefully } from "./agent-stop.mjs";
|
|
23
24
|
import { ragJobConfig } from "./rag-job-config.mjs";
|
|
24
25
|
import { codingKeyFallbackLine, isOllamaCloudUrl } from "./coding-key-prompt.mjs";
|
|
@@ -1229,7 +1230,7 @@ if (args[0] === "clear" || args[0] === "reset") {
|
|
|
1229
1230
|
try {
|
|
1230
1231
|
const ce = await import("./client-expert-api.mjs");
|
|
1231
1232
|
const { MongoClient } = ce.loadMongoDriver(apiDirForDrop);
|
|
1232
|
-
const client = new MongoClient(ce.
|
|
1233
|
+
const client = new MongoClient(ce.localMongoUri(), { serverSelectionTimeoutMS: 8000 });
|
|
1233
1234
|
await client.connect();
|
|
1234
1235
|
await client.db(ce.LOCAL_DB_NAME).dropDatabase();
|
|
1235
1236
|
await client.close();
|
|
@@ -3960,6 +3961,9 @@ async function runAgentChatJob(job) {
|
|
|
3960
3961
|
// ragFresh/ragCarried). Kept so a later token PATCH that carries no breakdown of its own
|
|
3961
3962
|
// (the output-token event) still re-sends the last one rather than blanking the row.
|
|
3962
3963
|
let latestPromptBreakdown = null;
|
|
3964
|
+
// Task #298: the CURRENT model call's own ↑ in / ↓ out, for the thinking line (the bar
|
|
3965
|
+
// already shows the totals). Rides on the breakdown — see step-tokens.mjs.
|
|
3966
|
+
let stepTokens = NO_STEP;
|
|
3963
3967
|
// Raw model-stream tokens (event.type === "stream") are Python code + `Thought:`
|
|
3964
3968
|
// prose + <code>/<|"|> fragments. The web Thought panel renders reasoning as
|
|
3965
3969
|
// markdown, which turns `#`-prefixed code comments into <h1> headings and
|
|
@@ -4125,11 +4129,19 @@ async function runAgentChatJob(job) {
|
|
|
4125
4129
|
const breakdown =
|
|
4126
4130
|
event.breakdown && typeof event.breakdown === "object" ? event.breakdown : null;
|
|
4127
4131
|
if (breakdown) latestPromptBreakdown = breakdown;
|
|
4132
|
+
const prevStep = stepTokens;
|
|
4133
|
+
stepTokens = foldStepTokens(stepTokens, event);
|
|
4134
|
+
const stepChanged =
|
|
4135
|
+
stepTokens.step !== prevStep.step ||
|
|
4136
|
+
stepTokens.stepIn !== prevStep.stepIn ||
|
|
4137
|
+
stepTokens.stepOut !== prevStep.stepOut;
|
|
4138
|
+
if (latestPromptBreakdown) latestPromptBreakdown = withStepTokens(latestPromptBreakdown, stepTokens);
|
|
4128
4139
|
if (
|
|
4129
4140
|
nextTotal !== latestCodeTokens ||
|
|
4130
4141
|
nextMax !== latestMaxCodeTokens ||
|
|
4131
4142
|
nextOut !== latestCodeOutputTokens ||
|
|
4132
|
-
breakdown
|
|
4143
|
+
breakdown ||
|
|
4144
|
+
(stepChanged && latestPromptBreakdown)
|
|
4133
4145
|
) {
|
|
4134
4146
|
latestCodeTokens = nextTotal;
|
|
4135
4147
|
latestMaxCodeTokens = nextMax;
|
package/gu-repl.mjs
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* Commands: /exit /model /new /help Ctrl-C stops following the current run.
|
|
25
25
|
*/
|
|
26
26
|
import { readFile, mkdir, writeFile, unlink, rename } from "node:fs/promises";
|
|
27
|
+
import { readStepTokens } from "./step-tokens.mjs";
|
|
27
28
|
import { existsSync, statSync, readFileSync, writeFileSync, readdirSync, openSync, readSync, closeSync } from "node:fs";
|
|
28
29
|
import { spawn } from "node:child_process";
|
|
29
30
|
// FIRST, before anything that could itself fail on an old runtime: npm only WARNS about
|
|
@@ -1943,9 +1944,12 @@ let lastPromptBreakdown = null;
|
|
|
1943
1944
|
* never leads it, so the screen never claims output that has not been produced.
|
|
1944
1945
|
*/
|
|
1945
1946
|
let shownOutputTokens = 0;
|
|
1946
|
-
// The same for INPUT (#53)
|
|
1947
|
-
// the "↓ N" output figure, so the thinking line reads both directions the way the bar does.
|
|
1947
|
+
// The same for INPUT (#53), shown as "↑ N" before the "↓ N" output figure.
|
|
1948
1948
|
let shownInputTokens = 0;
|
|
1949
|
+
// Task #298: both figures are THIS STEP's, not the turn's (the bar has those). Which step the
|
|
1950
|
+
// shown values belong to, so a new step starts its count from zero instead of easing on from
|
|
1951
|
+
// the previous step's number.
|
|
1952
|
+
let shownStepNo = 0;
|
|
1949
1953
|
// Seconds from Enter to the first full prompt reaching the coding model (prompt assembly +
|
|
1950
1954
|
// retrieval). Shown by /tokens; 0 until a turn has run.
|
|
1951
1955
|
let lastPromptAssemblySeconds = 0;
|
|
@@ -9634,6 +9638,9 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
9634
9638
|
// Task #104: this turn's OUTPUT-token total, read from the job doc each poll. Folded into
|
|
9635
9639
|
// the lifetime per-workspace + global counters once the turn completes.
|
|
9636
9640
|
let latestOutputTokens = 0;
|
|
9641
|
+
// Task #298: the current model call's own ↑/↓, read from the polled breakdown. null until
|
|
9642
|
+
// this turn reports a step — never the previous turn's, which the kept breakdown still holds.
|
|
9643
|
+
let liveStepTokens = null;
|
|
9637
9644
|
let outputTokensPosted = false; // guard: increment the lifetime counters once per turn
|
|
9638
9645
|
// Task #142: when this turn was submitted, and when its first full prompt reached the coding
|
|
9639
9646
|
// model. The gap between them is prompt assembly + RAG retrieval — the thing the user asked
|
|
@@ -9875,10 +9882,18 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
9875
9882
|
// Advanced once per tick — this branch runs ~8x/second while the agent is thinking, which is
|
|
9876
9883
|
// what makes the climb visible at all. Parked (streaming) the block is not rebuilt, so the
|
|
9877
9884
|
// figure holds rather than jittering against flowing output.
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
//
|
|
9881
|
-
|
|
9885
|
+
// THIS STEP, NOT THE TURN (task #298). The turn totals were exactly the bar's "sent" and
|
|
9886
|
+
// "↓ out" one row below — the same two numbers twice. ↑ is the current model call's prompt,
|
|
9887
|
+
// ↓ its output (known when the call returns); both start from zero at each new step.
|
|
9888
|
+
const st = liveStepTokens;
|
|
9889
|
+
if (!st || st.step !== shownStepNo) {
|
|
9890
|
+
shownStepNo = st ? st.step : 0;
|
|
9891
|
+
shownInputTokens = 0;
|
|
9892
|
+
shownOutputTokens = 0;
|
|
9893
|
+
}
|
|
9894
|
+
shownOutputTokens = easeCount(shownOutputTokens, st ? st.stepOut : 0);
|
|
9895
|
+
shownInputTokens = easeCount(shownInputTokens, st ? st.stepIn : 0);
|
|
9896
|
+
// ↑ input then ↓ output (#53). Each absent until known, never a "0" that claims a measurement.
|
|
9882
9897
|
const outLabel =
|
|
9883
9898
|
(shownInputTokens > 0 ? dim(" · ↑ ") + accent(fmtTokens(shownInputTokens)) : "") +
|
|
9884
9899
|
(shownOutputTokens > 0 ? dim(" · ↓ ") + accent(fmtTokens(shownOutputTokens)) : "");
|
|
@@ -10493,6 +10508,7 @@ async function runAgentTurn(history, { attachJobId = "" } = {}) {
|
|
|
10493
10508
|
// what is in the context right now, so it must be allowed to shrink when a trim lands.
|
|
10494
10509
|
if (job.agentPromptBreakdown && typeof job.agentPromptBreakdown === "object") {
|
|
10495
10510
|
lastPromptBreakdown = job.agentPromptBreakdown;
|
|
10511
|
+
liveStepTokens = readStepTokens(job.agentPromptBreakdown);
|
|
10496
10512
|
// Persisted per folder so the next `gu` here starts from the real picture instead
|
|
10497
10513
|
// of a zero that looks like a failure. Fire-and-forget: a bar value is never worth
|
|
10498
10514
|
// delaying a turn for, and losing one is harmless. Shown while attached (it is what
|
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.768",
|
|
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",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"accent.mjs",
|
|
50
50
|
"gu-cli.mjs",
|
|
51
51
|
"gu-repl.mjs",
|
|
52
|
+
"step-tokens.mjs",
|
|
52
53
|
"terminal-hover.mjs",
|
|
53
54
|
"terminal-layout.mjs",
|
|
54
55
|
"terminal-viewport.mjs",
|
package/step-tokens.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THIS STEP'S TOKENS, for the thinking line (task #298).
|
|
3
|
+
*
|
|
4
|
+
* The thinking line showed "· ↑ 478k · ↓ 4.3k" — the turn's running totals, which are exactly the
|
|
5
|
+
* bar's "sent 478k" and "↓ out 4.3k" one row below. Same numbers twice. What the line can say that
|
|
6
|
+
* the bar cannot is what the CURRENT model call is costing: its own prompt (↑) and its own output
|
|
7
|
+
* (↓), starting from zero at each new step.
|
|
8
|
+
*
|
|
9
|
+
* Python already knows both per call (`stepInput`, `stepOutput`) and tags them with `step`. The API
|
|
10
|
+
* only stores job fields it knows, but it passes agentPromptBreakdown through as-is — so the worker
|
|
11
|
+
* rides these three figures on that object (liveStep / liveStepIn / liveStepOut) and no API release
|
|
12
|
+
* is needed. Everything that reads the breakdown does so by named bucket, so extra keys are inert.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The state before any step of a turn. */
|
|
16
|
+
export const NO_STEP = Object.freeze({ step: 0, stepIn: 0, stepOut: 0 });
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fold one python "tokens" event into the current step's figures.
|
|
20
|
+
*
|
|
21
|
+
* A new `step` number starts from zero: its output is not known until the call returns, so the old
|
|
22
|
+
* step's ↓ must not linger beside the new step's ↑. Input can arrive twice for one step — the
|
|
23
|
+
* estimate at send time, then the server's exact count if higher — and the larger one wins.
|
|
24
|
+
*/
|
|
25
|
+
export function foldStepTokens(prev, event) {
|
|
26
|
+
let { step, stepIn, stepOut } = prev ?? NO_STEP;
|
|
27
|
+
if (!event || typeof event !== "object") return { step, stepIn, stepOut };
|
|
28
|
+
if (Number.isFinite(event.step) && event.step !== step) {
|
|
29
|
+
step = event.step;
|
|
30
|
+
stepIn = 0;
|
|
31
|
+
stepOut = 0;
|
|
32
|
+
}
|
|
33
|
+
if (Number.isFinite(event.stepInput) && event.stepInput > stepIn) stepIn = event.stepInput;
|
|
34
|
+
if (Number.isFinite(event.stepOutput) && event.stepOutput >= 0) stepOut = event.stepOutput;
|
|
35
|
+
return { step, stepIn, stepOut };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The breakdown the worker PATCHes, carrying the step's figures. */
|
|
39
|
+
export function withStepTokens(breakdown, s) {
|
|
40
|
+
if (!breakdown || typeof breakdown !== "object" || !s || !s.step) return breakdown;
|
|
41
|
+
return { ...breakdown, liveStep: s.step, liveStepIn: s.stepIn, liveStepOut: s.stepOut };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What the terminal reads back from a polled breakdown: this step's figures, or null when the
|
|
46
|
+
* breakdown carries none (an older worker, or no step reported yet) — then the line shows nothing
|
|
47
|
+
* rather than a total dressed up as a step.
|
|
48
|
+
*/
|
|
49
|
+
export function readStepTokens(breakdown) {
|
|
50
|
+
if (!breakdown || typeof breakdown !== "object") return null;
|
|
51
|
+
const step = Number(breakdown.liveStep);
|
|
52
|
+
if (!Number.isFinite(step) || step <= 0) return null;
|
|
53
|
+
const n = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : 0);
|
|
54
|
+
return { step, stepIn: n(breakdown.liveStepIn), stepOut: n(breakdown.liveStepOut) };
|
|
55
|
+
}
|