@tiens.nguyen/gu-cli 1.0.882 → 1.0.884

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/README.md CHANGED
@@ -1,8 +1,9 @@
1
- <h1 align="center">gu — the coding agent in your terminal</h1>
1
+ <h1 align="center">gu — the coding agent for your own models</h1>
2
2
 
3
3
  <p align="center">
4
- <strong>It reads your repo, makes the change, and proves it works.</strong><br>
5
- Your models — Ollama, MLX or any OpenAI-compatible provider. Your files stay in your folder.
4
+ <strong>A coding agent in your terminal that runs on Ollama, MLX or any OpenAI-compatible API.</strong><br>
5
+ It reads your repo, makes the change, runs the tests and shows you what it did. Your code stays in your folder.<br>
6
+ <strong>Free for 30 days. No account, no sign-in, no card.</strong>
6
7
  </p>
7
8
 
8
9
  <p align="center">
@@ -15,7 +16,7 @@
15
16
 
16
17
  <p align="center">
17
18
  <a href="https://gucli.com">Website</a> ·
18
- <a href="https://gucli.com/docs">Docs</a> ·
19
+ <a href="https://gucli.com/start">Docs</a> ·
19
20
  <a href="https://gucli.com/pricing">Pricing</a> ·
20
21
  <a href="https://gucli.com/support">Support</a>
21
22
  </p>
@@ -35,33 +36,47 @@ cd your-project
35
36
  gu
36
37
  ```
37
38
 
38
- The first run asks a few questions: how this machine runs, which model provider to use, and your
39
- gu token (a free trial is offered). After that, type what you want done.
39
+ **Free to start, no account needed.** The first run creates a free 30-day token for this
40
+ computer automatically. Press Enter to use it. It also asks how this machine runs and which model
41
+ provider to use. Then type what you want done.
40
42
 
41
43
  > **`gu: command not found`?** Run `npx @tiens.nguyen/gu-cli fix-path`. See [Troubleshooting](#troubleshooting).
42
44
 
43
- ## What it does
44
-
45
- - **Writes the code, in your terminal.** Ask in plain words; gu reads the repo, edits the files,
46
- runs the build and the tests, and tells you what it changed.
47
- - **Your models.** Run [Ollama](https://ollama.com) or MLX on your own machine, or bring any
48
- OpenAI-compatible provider. Switch at any time with `/model`.
49
- - **It stays in the repo.** Every edit is a file you can read, diff and revert, and `/undo` puts
50
- back what a stopped turn changed. No hidden workspace.
51
- - **Knows your codebase.** Local RAG indexes the folder so the model finds the right files.
52
- - **Extensible.** [MCP](https://modelcontextprotocol.io) servers, skills (`SKILL.md`), and hooks.
53
- Already set up for Claude Code? `/switch-skill claude` reuses its `.claude` folder.
54
- - **Tests in a real browser.** The `browser` tool drives Chrome to check the web pages it builds.
55
- - **Private by default.** Logs hold no prompts, code or keys unless you run `gu --log`.
45
+ ## Why gu
46
+
47
+ - **Your models, your choice.** Run [Ollama](https://ollama.com) or MLX on your own machine or a GPU
48
+ box on your network, or bring any OpenAI-compatible provider (OpenAI, DeepSeek, OpenRouter…). Use
49
+ one model to plan and another to code; switch any time with `/model`.
50
+ - **Private by default.** Logs hold no prompts, code or keys unless you run `gu --log`. Need
51
+ everything in-house? Private mode runs the API and database on your own servers.
52
+ - **It proves it works.** gu runs the build and the tests and fixes what fails. For web pages, the
53
+ `browser` tool drives a real Chrome and checks the result before gu says it is done.
54
+ - **It stays in the repo.** Every edit is a file you can read, diff and revert; `/undo` puts back
55
+ what a stopped turn changed. No hidden workspace.
56
+ - **Made for servers and SSH.** macOS, Linux and Windows, and it works over SSH in an 80-column
57
+ terminal, so you can run it where the code actually lives.
58
+ - **Fits the setup you have.** [MCP](https://modelcontextprotocol.io) servers, skills (`SKILL.md`)
59
+ and hooks. Already use Claude Code? `/switch-skill claude` reuses its `.claude` folder. Local RAG
60
+ indexes the folder so the model finds the right files.
61
+
62
+ ## Is gu for you?
63
+
64
+ **A good fit if** you run (or want to run) your own models, can't send code to someone else's
65
+ cloud, work on remote Linux boxes over SSH, or want a coding agent that costs $4 a month instead
66
+ of a model subscription.
67
+
68
+ **Probably not if** you want the strongest frontier model with zero setup and don't mind where your
69
+ code goes: the model vendors' own agents are simpler for that. gu can still use those models
70
+ through an OpenAI-compatible API, but its strength is everything else.
56
71
 
57
72
  ## Quick start
58
73
 
59
- 1. **Sign in.** `gu login` opens the browser. On a private machine, paste a token from your
60
- [account page](https://gucli.com/account) with `gu-cli token <token>` instead.
74
+ 1. **Start gu.** Run `gu` in a project folder. Your free 30-day token is created on the spot:
75
+ no sign-up, no browser, no card.
61
76
  2. **Check your models.** `gu doctor` sets up and tests the models on this machine.
62
77
  (`gu doctor --client` installs only the agent's dependencies, when the models run elsewhere.)
63
- 3. **Ask.** Run `gu` in a project folder. Conversations are saved per folder and resumed next
64
- time; `/new` starts a fresh one.
78
+ 3. **Ask.** Type what you want done. Conversations are saved per folder and resumed next time;
79
+ `/new` starts a fresh one.
65
80
 
66
81
  ## Models
67
82
 
@@ -119,6 +134,7 @@ Ctrl-C stops the running turn. Type `/` at the prompt to see every command; Tab
119
134
  | `/save-settings`, `/load-settings` | move providers, models and key *names* (no secrets) to another machine |
120
135
  | `/save-preset`, `/open-presets`, `/use-presets` | your whole setup, keys included, to copy, edit and apply (backs up first) |
121
136
  | `/clear-logs` | show what logs, RAG indexes and old API versions take on disk, then ask what to clear |
137
+ | `/voucher CODE` | use a voucher code: adds free days to this computer's trial or your free account |
122
138
  | `/support` | send the support team a request with gu's logs attached (secrets masked; shows what goes first) |
123
139
  | `/help`, `/exit` | list commands / quit |
124
140
 
@@ -161,8 +177,17 @@ gu fix-path | fix-powershell repair PATH / remove PowerShell's `gu` alias
161
177
 
162
178
  ## Plans
163
179
 
164
- Every new computer gets a **free trial**. After it, the **Free** plan covers 1 device and **Pro**
165
- covers more, plus provider budgets. Current prices: [gucli.com/pricing](https://gucli.com/pricing).
180
+ | | How long | What you need |
181
+ | --- | --- | --- |
182
+ | **Free trial** | 30 days, starts on the first run | nothing: the token is created for you |
183
+ | **Free plan** | 3 more months on that computer | a free account at [gucli.com](https://gucli.com) |
184
+ | **Pro** | while subscribed: **$4 a month** (110.000 ₫) + VAT, or $40 a year | the hosted agent and coder (no model server to run), up to 5 computers, provider budgets |
185
+
186
+ Got a **voucher code**? Type `/voucher CODE` in gu (or enter it on your
187
+ [account page](https://gucli.com/account)) for extra free days on top.
188
+
189
+ Free time belongs to the computer: reinstalling doesn't restart it. Already have a token? Paste
190
+ it at the first-run question instead, or later with `gu-cli token <token>`. Current prices: [gucli.com/pricing](https://gucli.com/pricing).
166
191
 
167
192
  ## Troubleshooting
168
193
 
package/cli-token.mjs CHANGED
@@ -533,6 +533,35 @@ export async function renewToken({ mode, apiBase, hostedBase, token, workerKey,
533
533
  }
534
534
  }
535
535
 
536
+ /**
537
+ * /voucher CODE (#425): redeem a marketing code with this computer's token as the credential — a
538
+ * trial (no account) or an account. Returns { ok: true, days, token } — the token already carries the
539
+ * extra days — or { ok: false, why, error } where error is the service's own sentence. Never throws.
540
+ */
541
+ export async function redeemVoucher({ mode, apiBase, hostedBase, token, device, code, fetchImpl = fetch, timeoutMs = 8000 }) {
542
+ const base = String(mode === "expert" ? hostedBase : apiBase || hostedBase).replace(/\/+$/, "");
543
+ if (!token) return { ok: false, why: "no-token", error: "This computer has no gu token yet — restart gu to get the free trial, then try again." };
544
+ if (!base) return { ok: false, why: "no-api", error: "No gu service address is set on this computer." };
545
+ try {
546
+ const res = await fetchImpl(`${base}/api/billing/cli-token/voucher`, {
547
+ method: "POST",
548
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
549
+ body: JSON.stringify({ code: String(code ?? ""), ...(device ? { device } : {}) }),
550
+ signal: AbortSignal.timeout(timeoutMs),
551
+ });
552
+ const data = await res.json().catch(() => null);
553
+ if (!res.ok) {
554
+ const error = typeof data?.error === "string" && data.error
555
+ ? data.error
556
+ : res.status === 404 ? "This gu service does not take vouchers yet." : `The gu service answered HTTP ${res.status}.`;
557
+ return { ok: false, why: typeof data?.why === "string" ? data.why : `http-${res.status}`, error };
558
+ }
559
+ return { ok: true, days: Number(data?.days ?? 0), token: typeof data?.token === "string" ? data.token : "" };
560
+ } catch (e) {
561
+ return { ok: false, why: "network", error: e?.name === "TimeoutError" ? "The gu service did not answer in time. Try again." : "Can't reach the gu service right now." };
562
+ }
563
+ }
564
+
536
565
  // ---------------------------------------------------------------------------
537
566
  // WHAT THE BAR SAYS
538
567
  // ---------------------------------------------------------------------------
package/gu-repl.mjs CHANGED
@@ -1041,6 +1041,7 @@ const COMMANDS = [
1041
1041
  { name: "/shells", desc: "long commands the agent left running in the background — status, runtime, command, output; stop one" },
1042
1042
  { name: "/budget", desc: "short view of your API provider budgets and balances (Pro) — the same items as the web /providers page" },
1043
1043
  { name: "/tokens", desc: "break down the last prompt sent to the coding model — how much of it was RAG" },
1044
+ { name: "/voucher", usage: "/voucher CODE", desc: "use a voucher code — it adds free days to this computer's trial or your free account" },
1044
1045
  { name: "/support", desc: "send the gu support team a request with gu's logs attached (secrets masked) — shows what goes first, then asks" },
1045
1046
  { name: "/clear-logs", desc: "show what gu's logs, RAG indexes and old API versions take on disk, then ask what to clear — the logs first" },
1046
1047
  { name: "/prices", usage: "/prices [update]", desc: "the price table and the cache table, and what the last turn and this session cost — \"update\" applies gu's newest published tables" },
@@ -8397,6 +8398,54 @@ async function noteTurnCost(inputTokens, breakdown, coder = null) {
8397
8398
  * use marked; what the last turn and this session cost; and, when gu has published newer tables,
8398
8399
  * that they are waiting. `/prices update` applies them — the same as clicking the notice.
8399
8400
  */
8401
+ /**
8402
+ * /voucher CODE (#425) — a marketing code that adds free days. Works on the free trial (no account)
8403
+ * and on a free account; the service answers with a fresh token that already carries the days, so
8404
+ * the bar and the licence see them at once — no restart.
8405
+ */
8406
+ async function runVoucherCommand(line = "/voucher") {
8407
+ const code = line.trim().slice("/voucher".length).trim();
8408
+ console.log("");
8409
+ if (!code) {
8410
+ console.log(GUTTER + "Type the code after the command: " + accent("/voucher CODE") + "\n");
8411
+ return;
8412
+ }
8413
+ const ct = await import("./cli-token.mjs");
8414
+ const pk = ct.publicKey();
8415
+ const dir = join(homedir(), ".gonext");
8416
+ const state = await ct.readState(dir);
8417
+ const { DEFAULT_API_BASE } = await import("./device-login.mjs");
8418
+ const device = await ct.thisDevice({ instanceId: myInstanceId || "" }).catch(() => null);
8419
+ const r = await withPendingStatus("checking the voucher", () =>
8420
+ ct.redeemVoucher({
8421
+ mode: isClientExpert() ? "expert" : "client",
8422
+ apiBase,
8423
+ hostedBase: String(process.env.GU_TOKEN_API_BASE ?? "").trim() || DEFAULT_API_BASE,
8424
+ token: state.token,
8425
+ device,
8426
+ code,
8427
+ })
8428
+ );
8429
+ if (!r.ok) {
8430
+ console.log(GUTTER + yellow(r.error) + "\n");
8431
+ return;
8432
+ }
8433
+ // Kept only if it checks out here, like every renewal.
8434
+ const check = r.token && pk ? ct.verifyCliToken(r.token, pk) : { ok: false };
8435
+ if (check.ok) {
8436
+ await ct.writeState(dir, { token: r.token, lastAttemptAt: new Date().toISOString() }).catch(() => {});
8437
+ const d = ct.enforcementDecision({ token: r.token, publicKeyB64: pk, site: ct.siteUrl(), deviceId: await thisDeviceId(ct) });
8438
+ licenceBlock = d.enforced && !d.allowed ? d.why : null;
8439
+ tokenNotice = ct.barNotice({ token: r.token, publicKeyB64: pk, mode: isClientExpert() ? "expert" : "client", now: new Date(), attempted: true, site: ct.siteUrl() });
8440
+ }
8441
+ const p = check.ok ? check.payload : null;
8442
+ const until = p?.plan === "trial" ? p.paidUntil : p?.plan === "free" && typeof p.freeUntil === "string" ? p.freeUntil : null;
8443
+ console.log(GUTTER + accent(`✓ +${r.days} free days added.`) + (until ? ` Free on this computer until ${ct.shortDate(until)}.` : ""));
8444
+ if (!check.ok) console.log(GUTTER + dim("gu picks the days up at its next token refresh (within the hour)."));
8445
+ console.log("");
8446
+ if (barPinned()) paintBar();
8447
+ }
8448
+
8400
8449
  async function runPricesCommand(line = "/prices") {
8401
8450
  const mp = await import("./model-prices.mjs");
8402
8451
  console.log("");
@@ -14061,7 +14110,10 @@ async function main() {
14061
14110
  if (isClientExpert()) {
14062
14111
  try {
14063
14112
  const ceApi = await import("./client-expert-api.mjs");
14064
- apiBannerVersion = String((await ceApi.probeLocalApi()).body?.version ?? "").trim();
14113
+ // The API this gu is configured for, on ITS port — not the default port, which another
14114
+ // API (or none) may hold (#254: the banner differed between two machines).
14115
+ const port = Number(new URL(apiBase).port) || undefined;
14116
+ apiBannerVersion = String((await ceApi.probeLocalApi(port)).body?.version ?? "").trim();
14065
14117
  } catch { /* absent beats guessed — no suffix */ }
14066
14118
  }
14067
14119
  const statusRows = [
@@ -15080,6 +15132,10 @@ async function main() {
15080
15132
  await runPricesCommand(line);
15081
15133
  continue;
15082
15134
  }
15135
+ if (line === "/voucher" || line.startsWith("/voucher ")) {
15136
+ await runVoucherCommand(line);
15137
+ continue;
15138
+ }
15083
15139
  if (line === "/clear-logs") {
15084
15140
  await runClearLogs();
15085
15141
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.882",
3
+ "version": "1.0.884",
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",