@tiens.nguyen/gu-cli 1.0.882 → 1.0.883

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
@@ -2,7 +2,8 @@
2
2
 
3
3
  <p align="center">
4
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.
5
+ Your models — Ollama, MLX or any OpenAI-compatible provider. Your files stay 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">
@@ -35,8 +36,9 @@ 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
 
@@ -56,12 +58,12 @@ gu token (a free trial is offered). After that, type what you want done.
56
58
 
57
59
  ## Quick start
58
60
 
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.
61
+ 1. **Start gu.** Run `gu` in a project folder. Your free 30-day token is created on the spot:
62
+ no sign-up, no browser, no card.
61
63
  2. **Check your models.** `gu doctor` sets up and tests the models on this machine.
62
64
  (`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.
65
+ 3. **Ask.** Type what you want done. Conversations are saved per folder and resumed next time;
66
+ `/new` starts a fresh one.
65
67
 
66
68
  ## Models
67
69
 
@@ -119,6 +121,7 @@ Ctrl-C stops the running turn. Type `/` at the prompt to see every command; Tab
119
121
  | `/save-settings`, `/load-settings` | move providers, models and key *names* (no secrets) to another machine |
120
122
  | `/save-preset`, `/open-presets`, `/use-presets` | your whole setup, keys included, to copy, edit and apply (backs up first) |
121
123
  | `/clear-logs` | show what logs, RAG indexes and old API versions take on disk, then ask what to clear |
124
+ | `/voucher CODE` | use a voucher code: adds free days to this computer's trial or your free account |
122
125
  | `/support` | send the support team a request with gu's logs attached (secrets masked; shows what goes first) |
123
126
  | `/help`, `/exit` | list commands / quit |
124
127
 
@@ -161,8 +164,17 @@ gu fix-path | fix-powershell repair PATH / remove PowerShell's `gu` alias
161
164
 
162
165
  ## Plans
163
166
 
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).
167
+ | | How long | What you need |
168
+ | --- | --- | --- |
169
+ | **Free trial** | 30 days, starts on the first run | nothing: the token is created for you |
170
+ | **Free plan** | 3 more months on that computer | a free account at [gucli.com](https://gucli.com) |
171
+ | **Pro** | while subscribed | more computers, plus provider budgets |
172
+
173
+ Got a **voucher code**? Type `/voucher CODE` in gu (or enter it on your
174
+ [account page](https://gucli.com/account)) for extra free days on top.
175
+
176
+ Free time belongs to the computer: reinstalling doesn't restart it. Already have a token? Paste
177
+ it at the first-run question instead, or later with `gu-cli token <token>`. Current prices: [gucli.com/pricing](https://gucli.com/pricing).
166
178
 
167
179
  ## Troubleshooting
168
180
 
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("");
@@ -15080,6 +15129,10 @@ async function main() {
15080
15129
  await runPricesCommand(line);
15081
15130
  continue;
15082
15131
  }
15132
+ if (line === "/voucher" || line.startsWith("/voucher ")) {
15133
+ await runVoucherCommand(line);
15134
+ continue;
15135
+ }
15083
15136
  if (line === "/clear-logs") {
15084
15137
  await runClearLogs();
15085
15138
  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.883",
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",