@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 +50 -25
- package/cli-token.mjs +29 -0
- package/gu-repl.mjs +57 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
<h1 align="center">gu — the coding agent
|
|
1
|
+
<h1 align="center">gu — the coding agent for your own models</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<strong>
|
|
5
|
-
|
|
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/
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
##
|
|
44
|
-
|
|
45
|
-
- **
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- **
|
|
53
|
-
|
|
54
|
-
- **
|
|
55
|
-
|
|
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. **
|
|
60
|
-
|
|
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.**
|
|
64
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|