@tiens.nguyen/gu-cli 1.0.885 → 1.0.887
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/budget-view.mjs +22 -8
- package/gu-repl.mjs +36 -16
- package/package.json +1 -1
package/budget-view.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gu `/budget` (#205): the SHORT terminal version of web-v2's /providers budgets.
|
|
3
3
|
*
|
|
4
|
-
* Data: GET {apiBase}/api/worker/providers-budget with the
|
|
4
|
+
* Data: GET {hosted}/api/billing/cli-token/providers-budget with gu's token (else {apiBase}/api/worker/providers-budget with the worker key). The server decides
|
|
5
5
|
* Pro / email / "has items" (api/src/providers/terminalBudget.ts); this module only turns its answer
|
|
6
6
|
* into plain lines. Pure (fetch is injected) so every message is testable without a network.
|
|
7
7
|
*/
|
|
@@ -11,15 +11,11 @@ export const BUDGET_TIMEOUT_MS = 15_000;
|
|
|
11
11
|
/** A web URL for a page under `base` — a URL, so never path.join (that eats the "//"; #205). */
|
|
12
12
|
const pageUrl = (base, path) => new URL(path, `${base}/`).href;
|
|
13
13
|
|
|
14
|
-
/** -> { kind: "
|
|
15
|
-
|
|
16
|
-
if (!String(workerKey ?? "").trim() || !String(apiBase ?? "").trim()) return { kind: "not_logged_in" };
|
|
14
|
+
/** One GET -> { kind: "relogin" | "http" | "offline" | "ok", status?, body? } */
|
|
15
|
+
async function getBudget(url, headers, fetchImpl, timeoutMs) {
|
|
17
16
|
let r;
|
|
18
17
|
try {
|
|
19
|
-
r = await fetchImpl(
|
|
20
|
-
headers: { "X-Worker-Key": String(workerKey).trim() },
|
|
21
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
22
|
-
});
|
|
18
|
+
r = await fetchImpl(url, { headers, signal: AbortSignal.timeout(timeoutMs) });
|
|
23
19
|
} catch (e) {
|
|
24
20
|
return { kind: "offline", error: e instanceof Error ? e.message : String(e) };
|
|
25
21
|
}
|
|
@@ -30,6 +26,24 @@ export async function fetchProvidersBudget({ apiBase, workerKey, fetchImpl = fet
|
|
|
30
26
|
return { kind: "ok", status: r.status, body };
|
|
31
27
|
}
|
|
32
28
|
|
|
29
|
+
/**
|
|
30
|
+
* -> { kind: "not_logged_in" | "relogin" | "http" | "offline" | "ok", status?, body? }
|
|
31
|
+
*
|
|
32
|
+
* The account (Pro, email, provider items) lives on the HOSTED API. A computer in Local mode has its
|
|
33
|
+
* own API with none of that, so when gu has a token it asks the hosted API with the token
|
|
34
|
+
* (/api/billing/cli-token/providers-budget). Without a token, or if that server predates the route
|
|
35
|
+
* (404), it falls back to this machine's own API with the worker key.
|
|
36
|
+
*/
|
|
37
|
+
export async function fetchProvidersBudget({ apiBase, workerKey, hostedBase, token, fetchImpl = fetch, timeoutMs = BUDGET_TIMEOUT_MS }) {
|
|
38
|
+
const hosted = String(hostedBase ?? "").trim().replace(/\/+$/, "");
|
|
39
|
+
if (String(token ?? "").trim() && hosted) {
|
|
40
|
+
const r = await getBudget(`${hosted}/api/billing/cli-token/providers-budget`, { authorization: `Bearer ${String(token).trim()}` }, fetchImpl, timeoutMs);
|
|
41
|
+
if (!(r.kind === "http" && r.status === 404)) return r;
|
|
42
|
+
}
|
|
43
|
+
if (!String(workerKey ?? "").trim() || !String(apiBase ?? "").trim()) return { kind: "not_logged_in" };
|
|
44
|
+
return getBudget(`${String(apiBase).replace(/\/+$/, "")}/api/worker/providers-budget`, { "X-Worker-Key": String(workerKey).trim() }, fetchImpl, timeoutMs);
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
const money = (n, cur) => {
|
|
34
48
|
if (typeof n !== "number" || !Number.isFinite(n)) return "—";
|
|
35
49
|
const c = String(cur || "USD").toUpperCase();
|
package/gu-repl.mjs
CHANGED
|
@@ -154,7 +154,7 @@ import { readSkillSource, sourceLabel, SKILL_SOURCE_KEY } from "./skill-source.m
|
|
|
154
154
|
import { codingBannerLabel, isOllamaCloudUrl, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
|
|
155
155
|
import { questionEchoText, thoughtPrintSeq, wrapDiffText } from "./terminal-layout.mjs";
|
|
156
156
|
import { globalPrefixOf } from "./install-path.mjs";
|
|
157
|
-
import { indentBlock, commandBlockRows } from "./terminal-layout.mjs"; // every line of an Allow-prompt detail indented (#189); wrapped + collapsible command (#204)
|
|
157
|
+
import { indentBlock, commandBlockRows, wrapIndented } from "./terminal-layout.mjs"; // every line of an Allow-prompt detail indented (#189); wrapped + collapsible command (#204)
|
|
158
158
|
import { firstGuOnPath, pathGuMismatch, guOnPathIsCurrent, updateAttemptRecord, repeatUpdateNotice } from "./update-verify.mjs";
|
|
159
159
|
import { embedNotice, embedState, setupNoticeRows } from "./embed-model-command.mjs";
|
|
160
160
|
import { HINTS, pickHint, hintRow, hintFits, HINT_AFTER_SECONDS } from "./hints.mjs";
|
|
@@ -7714,6 +7714,14 @@ async function afterStoppedTurn({ taskLine, submittedInput, turnStartMs, stopped
|
|
|
7714
7714
|
if (barPinned()) paintBar();
|
|
7715
7715
|
}
|
|
7716
7716
|
|
|
7717
|
+
/**
|
|
7718
|
+
* A message line under the gutter, cut to the terminal's width with a hanging indent (#293): a
|
|
7719
|
+
* long line used to be left to the terminal, which wraps it back to COLUMN 0 ("…as they do ther" /
|
|
7720
|
+
* "e."). `paint` colours each row after wrapping, so colour codes never count toward the width.
|
|
7721
|
+
*/
|
|
7722
|
+
function sayWrapped(text, paint = (s) => s, hang = " ") {
|
|
7723
|
+
for (const row of wrapIndented(String(text ?? ""), { width: commandWidth(), indent: GUTTER, hang })) console.log(paint(row));
|
|
7724
|
+
}
|
|
7717
7725
|
/**
|
|
7718
7726
|
* /switch-skill — whose skills, MCP servers and hooks THIS FOLDER uses (task #325): Claude Code's,
|
|
7719
7727
|
* read where Claude keeps them (~/.claude, ./.claude, ~/.claude.json), or gu's own (~/.gonext,
|
|
@@ -7758,9 +7766,10 @@ async function runSwitchSkillCommand(line) {
|
|
|
7758
7766
|
return;
|
|
7759
7767
|
}
|
|
7760
7768
|
_skillScan = { at: 0, skills: [] }; // the "/" menu and Tab follow at once
|
|
7761
|
-
|
|
7762
|
-
say
|
|
7763
|
-
say(
|
|
7769
|
+
// #293: wrapped under the line's own indent (never back to column 0); one 2-space indent under "✓".
|
|
7770
|
+
const say = (text, paint) => sayWrapped(text, paint);
|
|
7771
|
+
say(`✓ ${pick === current ? "this folder uses" : "this folder now uses"} ${sourceLabel(pick)}`, accent);
|
|
7772
|
+
say(` skills, MCP servers and hooks are read from ${where(pick)}`, dim);
|
|
7764
7773
|
// What was found there — and what was not, said rather than shown as an empty list.
|
|
7765
7774
|
try {
|
|
7766
7775
|
const sk = discoverSkills({ cwd: process.cwd(), reserved: COMMAND_NAMES });
|
|
@@ -7769,23 +7778,23 @@ async function runSwitchSkillCommand(line) {
|
|
|
7769
7778
|
const nHooks = (h) => Object.values(h).reduce((a, groups) => a + groups.reduce((b, g) => b + g.hooks.length, 0), 0);
|
|
7770
7779
|
const count = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
7771
7780
|
const running = mcp.servers.filter((x) => x.runnable).length;
|
|
7772
|
-
say(
|
|
7781
|
+
say(` ${count(sk.skills.length, "skill")}${sk.skills.length ? ` (${sk.skills.map((x) => "/" + x.name).join(", ")})` : ""} · ${count(running, "MCP server")} · ${count(nHooks(hk.user) + (hk.projectApproved ? nHooks(hk.project) : 0), "hook")}`, dim);
|
|
7773
7782
|
// Nothing there at all is a normal state, not a fault: say so, and that gu needs none of it.
|
|
7774
7783
|
if (!sk.skills.length && !mcp.servers.length && !nHooks(hk.user) && !nHooks(hk.project) && !hk.notes.length) {
|
|
7775
|
-
say(
|
|
7784
|
+
say(" Nothing is set up there yet — gu works the same without any of them.", dim);
|
|
7776
7785
|
}
|
|
7777
|
-
for (const w of [...sk.warnings, ...mcp.warnings, ...hk.warnings]) say(
|
|
7786
|
+
for (const w of [...sk.warnings, ...mcp.warnings, ...hk.warnings]) say(" " + w, yellow);
|
|
7778
7787
|
const unsupported = mcp.servers.filter((x) => x.error);
|
|
7779
|
-
if (unsupported.length) say(
|
|
7788
|
+
if (unsupported.length) say(` ${count(unsupported.length, "MCP server")} cannot run here: ${unsupported.map((x) => `${x.name} (${x.error})`).join("; ")}`, yellow);
|
|
7780
7789
|
const waiting = mcp.servers.filter((x) => x.scope === "project" && !x.approved && !x.error);
|
|
7781
|
-
if (waiting.length) say(
|
|
7782
|
-
if (hk.projectPending) say(
|
|
7783
|
-
if (hk.notes.length) say(
|
|
7784
|
-
if (pick === "claude" && (nHooks(hk.user) || nHooks(hk.project))) say(
|
|
7790
|
+
if (waiting.length) say(` ${count(waiting.length, "project MCP server")} waiting for your approval: ${waiting.map((x) => x.name).join(", ")} — /mcp approve <name>`, yellow);
|
|
7791
|
+
if (hk.projectPending) say(` this folder's ${hk.projectFile.replace(/^\.\//, "")} has hooks that will not run until you approve them — /hooks approve`, yellow);
|
|
7792
|
+
if (hk.notes.length) say(` ${count(hk.notes.length, "hook")} in Claude's settings ${hk.notes.length === 1 ? "is" : "are"} NOT run by gu — /hooks says which and why`, yellow);
|
|
7793
|
+
if (pick === "claude" && (nHooks(hk.user) || nHooks(hk.project))) say(" Claude's hooks get Claude's event and tool names from gu, so they record here as they do there.", dim);
|
|
7785
7794
|
} catch (err) {
|
|
7786
|
-
say(
|
|
7795
|
+
say(` (could not read it back: ${err.message})`, yellow);
|
|
7787
7796
|
}
|
|
7788
|
-
say(
|
|
7797
|
+
say(` It holds from your next question. ${pick === "claude" ? "/switch-skill gu" : "/switch-skill claude"} switches back.`, dim);
|
|
7789
7798
|
console.log("");
|
|
7790
7799
|
if (barPinned()) paintBar();
|
|
7791
7800
|
}
|
|
@@ -7815,7 +7824,11 @@ async function runResumeCommand() {
|
|
|
7815
7824
|
say(dim("Nothing to resume here: every recent session finished its plan.\n"));
|
|
7816
7825
|
return null;
|
|
7817
7826
|
}
|
|
7818
|
-
|
|
7827
|
+
// #293: Goal and Plan are long; wrapped under their own indent, not back to column 0.
|
|
7828
|
+
for (const l of r.block.split("\n").filter((l) => !l.startsWith("If the user wants"))) {
|
|
7829
|
+
if (l.startsWith("## ")) sayWrapped(l.slice(3), bold);
|
|
7830
|
+
else sayWrapped(l, dim);
|
|
7831
|
+
}
|
|
7819
7832
|
console.log("");
|
|
7820
7833
|
if (!(await askYesNo(rm.resumeQuestion(r.resume), true))) {
|
|
7821
7834
|
console.log("");
|
|
@@ -15302,7 +15315,14 @@ async function main() {
|
|
|
15302
15315
|
try {
|
|
15303
15316
|
const bv = await import("./budget-view.mjs");
|
|
15304
15317
|
const ct = await import("./cli-token.mjs");
|
|
15305
|
-
const
|
|
15318
|
+
const { DEFAULT_API_BASE } = await import("./device-login.mjs");
|
|
15319
|
+
const state = await ct.readState(join(homedir(), ".gonext"));
|
|
15320
|
+
const result = await bv.fetchProvidersBudget({
|
|
15321
|
+
apiBase,
|
|
15322
|
+
workerKey,
|
|
15323
|
+
hostedBase: String(process.env.GU_TOKEN_API_BASE ?? "").trim() || DEFAULT_API_BASE,
|
|
15324
|
+
token: state.token,
|
|
15325
|
+
});
|
|
15306
15326
|
const out = bv.budgetLines(result, { site: ct.siteUrl() });
|
|
15307
15327
|
for (const l of out.lines) console.log(GUTTER + (out.tone === "ok" ? l : dim(l)));
|
|
15308
15328
|
console.log("");
|
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.887",
|
|
4
4
|
"description": "gu: the coding agent in your terminal. It reads your repo, makes the change and runs the tests, with your own models (Ollama, MLX or any OpenAI-compatible provider). macOS, Linux and Windows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|