ucode-agent 1.63.0 → 1.65.0
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 +157 -8
- package/package.json +65 -64
- package/src/core/commands.js +44 -0
- package/src/core/genericcheck.js +147 -0
- package/src/core/headless.js +73 -0
- package/src/core/lessons.js +83 -0
- package/src/core/loop.js +611 -20
- package/src/core/mcp.js +337 -0
- package/src/core/mcpcli.js +52 -0
- package/src/core/provider.js +93 -11
- package/src/core/scope.js +62 -3
- package/src/core/settings.js +148 -0
- package/src/core/snapshot.js +147 -0
- package/src/core/terminal.js +232 -0
- package/src/core/tests.js +14 -1
- package/src/tools/browser.js +2 -1
- package/src/tools/index.js +4 -0
- package/src/tools/search.js +74 -0
- package/src/tools/shared.js +33 -2
- package/src/tools/shell.js +4 -1
- package/src/ui/activity.js +2 -2
- package/src/ui/plain.js +11 -8
- package/src/ui/screen.js +9 -7
- package/src/ui/theme.js +163 -12
- package/ucode.js +37 -1
package/README.md
CHANGED
|
@@ -4,6 +4,23 @@ A coding agent that lives in your terminal. It reads your code, edits it, runs
|
|
|
4
4
|
your commands, and keeps every conversation on disk. It runs on Google's
|
|
5
5
|
Gemini models, free with a key.
|
|
6
6
|
|
|
7
|
+
## At a glance
|
|
8
|
+
|
|
9
|
+
| | |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| **Builds** | a whole app in about a minute, from a starter that already works, opened for you when it does |
|
|
12
|
+
| **Thinks** | a thinking level per step — cheap on easy steps, more on the plan, most when a fix has failed |
|
|
13
|
+
| **Designs** | its own tone, typefaces and accent for every app, and a check that sends the generated look back |
|
|
14
|
+
| **Checks** | types, syntax, related tests, the running server's errors, and the page itself — opened and clicked |
|
|
15
|
+
| **Fixes** | sends problems back to the model, tries a different approach when a fix fails, learns the common ones |
|
|
16
|
+
| **Edits real code** | finds the code first, smallest change in the code's own style; renames by code shape |
|
|
17
|
+
| **Undoes** | `/undo [n]` puts the whole project back, including what commands changed |
|
|
18
|
+
| **Git** | `/diff`, `/commit` with a written message, `/review` for bugs |
|
|
19
|
+
| **Extends** | MCP servers, hooks, skills, your own slash commands |
|
|
20
|
+
| **Restyles** | itself and your terminal, when you ask: "make ucode orange and my terminal navy" |
|
|
21
|
+
| **Automates** | `ucode -p "task" --json` for scripts and CI; `npm run eval` runs ten real jobs |
|
|
22
|
+
| **Stays free** | Gemini's free tier, paced to its per-minute limit — or Ollama, offline |
|
|
23
|
+
|
|
7
24
|
It opens on a quiet screen — the name, the place to type, and the version in the
|
|
8
25
|
corner:
|
|
9
26
|
|
|
@@ -19,7 +36,7 @@ corner:
|
|
|
19
36
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
20
37
|
│ › Ask anything… │
|
|
21
38
|
│ │
|
|
22
|
-
│ BUILD
|
|
39
|
+
│ BUILD Gemini 3.5 Flash-Lite 0% │
|
|
23
40
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
24
41
|
|
|
25
42
|
try build me a landing page for a coffee shop
|
|
@@ -27,7 +44,7 @@ corner:
|
|
|
27
44
|
add a dark mode toggle that remembers the choice
|
|
28
45
|
|
|
29
46
|
|
|
30
|
-
v1.
|
|
47
|
+
v1.65.0
|
|
31
48
|
```
|
|
32
49
|
|
|
33
50
|
A light crosses the wordmark once as it opens, and the three lines under the box
|
|
@@ -49,7 +66,7 @@ The dashboard is at http://localhost:3000, and `npm run dev` brings it back up.
|
|
|
49
66
|
╭──────────────────────────────────────────────────────────────────────────────────╮
|
|
50
67
|
│ › now add a dark mode toggle │
|
|
51
68
|
│ │
|
|
52
|
-
│ BUILD
|
|
69
|
+
│ BUILD Gemini 3.5 Flash-Lite 4% │
|
|
53
70
|
╰──────────────────────────────────────────────────────────────────────────────────╯
|
|
54
71
|
```
|
|
55
72
|
|
|
@@ -101,7 +118,7 @@ When Google is overloaded and Flash-Lite stops answering, ucode carries on with
|
|
|
101
118
|
|
|
102
119
|
## What it does
|
|
103
120
|
|
|
104
|
-
**Twenty-one tools.** `create_app`, `read_file`, `read_files`, `write_file`,
|
|
121
|
+
**Twenty-one tools, and any MCP server you add.** `create_app`, `read_file`, `read_files`, `write_file`,
|
|
105
122
|
`batch_write`, `edit_file`, `multi_edit`, `edit_files`, `rename_symbol`,
|
|
106
123
|
`find_symbol`, `outline`, `type_of`, `add_block`, `list_dir`, `glob`, `grep`,
|
|
107
124
|
`run_command`, `run_commands`, `look_at_app`, `web_search`, `deploy`. Read-only
|
|
@@ -253,7 +270,7 @@ check and a screenshot; the only way to find out is to press something.
|
|
|
253
270
|
|
|
254
271
|
It also reports console errors, failed requests, content that spills off a
|
|
255
272
|
phone screen, broken images and unlabeled controls, saves screenshots to
|
|
256
|
-
`.ucode/screenshots`, and has
|
|
273
|
+
`.ucode/screenshots`, and has Gemini review them the way a designer
|
|
257
274
|
would. The model fixes what it finds before calling the app done. Both widths load at once, and the designer review — the slow part — runs
|
|
258
275
|
on the first look at an app in each request and is skipped, not waited on, when
|
|
259
276
|
the vision model is busy. The look after the fixes re-runs only the fast checks:
|
|
@@ -365,7 +382,14 @@ Everything after the frontmatter is the instruction.
|
|
|
365
382
|
| `/session delete 2,5` | delete saved conversations by number (or `d d` in the list) |
|
|
366
383
|
| `/new` | save this one and start fresh |
|
|
367
384
|
| `/remember <note>` | add a standing note to this project's `UCODE.md` |
|
|
368
|
-
| `/undo` | put back
|
|
385
|
+
| `/undo [n]` | put the project back as it was before the last turn — or `n` turns — including what commands changed |
|
|
386
|
+
| `/diff` | what has changed this session |
|
|
387
|
+
| `/commit [msg]` | commit the changes, with a message written from the diff if you give none |
|
|
388
|
+
| `/review` | read the uncommitted changes for bugs, changing nothing |
|
|
389
|
+
| `/init` | read the project and write its `UCODE.md` |
|
|
390
|
+
| `/mcp` | connected MCP servers and their tools |
|
|
391
|
+
| `/permissions [ask\|auto]` | ask before every command, or run them; what is always allowed |
|
|
392
|
+
| `/theme [what]` | change how ucode or your terminal looks — or just ask in words |
|
|
369
393
|
| `/look [url]` | open the running app and report what is on the page |
|
|
370
394
|
| `/deploy [folder]` | put the app online and get its link |
|
|
371
395
|
| `/mic` | say what you want instead of typing it — same as `ctrl+t` |
|
|
@@ -393,6 +417,112 @@ Recording uses what the computer already has: Windows' built-in recorder, `sox`
|
|
|
393
417
|
or `ffmpeg` on macOS (`brew install sox`), `arecord` or `sox` on Linux. If a
|
|
394
418
|
quiet mic is taken for silence, set `UCODE_MIC_QUIET` lower than 800.
|
|
395
419
|
|
|
420
|
+
### Change how it looks — and your terminal
|
|
421
|
+
|
|
422
|
+
Just ask: "make ucode orange with the arc spinner", "put my name under the
|
|
423
|
+
logo", "make my terminal navy with a bigger font", "make the terminal a bit
|
|
424
|
+
see-through". Or use `/theme`:
|
|
425
|
+
|
|
426
|
+
```
|
|
427
|
+
/theme what it looks like now, and the choices
|
|
428
|
+
/theme orange a new colour at once (a name, #ff8c2b, or rgb(...))
|
|
429
|
+
/theme reset ucode's own blue again
|
|
430
|
+
/theme terminal reset the terminal back the way it was
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
ucode's look is saved in `~/.ucode/theme.json` — accent, spinner (`dots`,
|
|
434
|
+
`line`, `arc`, `circle`, `square`, `bounce`, `pulse`, `star`) and the line
|
|
435
|
+
under the logo — so it survives restarts and updates.
|
|
436
|
+
|
|
437
|
+
The terminal is changed the way each one allows, after you say yes:
|
|
438
|
+
|
|
439
|
+
| Terminal | What changes | How long |
|
|
440
|
+
| --- | --- | --- |
|
|
441
|
+
| Windows Terminal | background, text, cursor, font, size, opacity | kept, every tab (the old settings are backed up) |
|
|
442
|
+
| Terminal.app (macOS) | background, text, cursor, font, size | this window |
|
|
443
|
+
| iTerm2 (macOS) | background, text, cursor | this session |
|
|
444
|
+
| Linux, VS Code and others | background, text, cursor | this session |
|
|
445
|
+
|
|
446
|
+
### Your own commands
|
|
447
|
+
|
|
448
|
+
A file `.ucode/commands/explain.md` (or `~/.ucode/commands/` for every
|
|
449
|
+
project) becomes `/explain`. Its text is the prompt; `$ARGUMENTS` is replaced
|
|
450
|
+
by whatever you type after the command.
|
|
451
|
+
|
|
452
|
+
### MCP servers
|
|
453
|
+
|
|
454
|
+
Connect tools from any MCP server — library docs, GitHub, a database:
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
ucode mcp add context7 npx -y @upstash/context7-mcp
|
|
458
|
+
ucode mcp add github --url https://api.githubcopilot.com/mcp/ --header "Authorization=Bearer ${GITHUB_TOKEN}"
|
|
459
|
+
ucode mcp list
|
|
460
|
+
ucode mcp remove github
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
They are saved in `~/.ucode/mcp.json` (`--project` puts them in this folder's
|
|
464
|
+
`.ucode/mcp.json`). ucode asks before each MCP tool runs; answer `a` to always
|
|
465
|
+
allow that tool. A project's own servers and hooks only run once you approve them.
|
|
466
|
+
|
|
467
|
+
### Permissions and hooks
|
|
468
|
+
|
|
469
|
+
`.ucode/settings.json` (or `~/.ucode/settings.json`):
|
|
470
|
+
|
|
471
|
+
```json
|
|
472
|
+
{
|
|
473
|
+
"commands": "ask",
|
|
474
|
+
"allow": ["npm test", "git status"],
|
|
475
|
+
"hooks": {
|
|
476
|
+
"afterEdit": ["npx prettier --write {files}"],
|
|
477
|
+
"beforeCommand": ["node guard.js"]
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
`"commands": "ask"` puts every command to you first (`/permissions ask`);
|
|
483
|
+
answering `a` adds it to `allow`. A `beforeCommand` hook that exits non-zero
|
|
484
|
+
stops the command; it sees it in `UCODE_COMMAND`.
|
|
485
|
+
|
|
486
|
+
### Run it without a keyboard
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
ucode -p "fix the failing test" prints the answer
|
|
490
|
+
ucode -p "make a quiz app" --json --yes one JSON line: ok, answer, files, steps, requests, time
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Progress goes to stderr. With no `--yes`, anything that would be asked is declined.
|
|
494
|
+
`npm run eval` runs ten real jobs this way and checks each one — use it before
|
|
495
|
+
a release (it spends about 30 free requests, and takes about seven minutes).
|
|
496
|
+
|
|
497
|
+
### Other models
|
|
498
|
+
|
|
499
|
+
Any OpenAI-compatible server works, Ollama on your own computer included —
|
|
500
|
+
free and offline:
|
|
501
|
+
|
|
502
|
+
```
|
|
503
|
+
UCODE_BASE_URL=http://localhost:11434/v1 UCODE_MODEL=qwen2.5-coder ucode
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## How it thinks
|
|
507
|
+
|
|
508
|
+
- **Thinking levels.** Flash-Lite does not think at all unless asked. ucode
|
|
509
|
+
asks for a little on every step (it costs nothing on a straightforward
|
|
510
|
+
write), more on the first step of a build, and the most when a fix has
|
|
511
|
+
already failed. `UCODE_THINK=0` turns it off.
|
|
512
|
+
- **A design direction for every build** — a tone, two typefaces and an accent —
|
|
513
|
+
so two apps never come out the same, and a check for the generated look
|
|
514
|
+
(the starter's colours, Inter, purple gradients, gradient text, emoji icons)
|
|
515
|
+
that sends it back to be fixed. `UCODE_DESIGN_CHECK=0` turns the check off.
|
|
516
|
+
- **Learns from its mistakes.** Problems ucode keeps catching are counted in
|
|
517
|
+
`~/.ucode/lessons.json`, and the common ones are warned about before the next build.
|
|
518
|
+
- **Tries a different approach** when the same problem survives a fix, and
|
|
519
|
+
offers to hand that fix to Gemini 3.5 Flash — only if you say yes, since it
|
|
520
|
+
has about 20 free requests a day.
|
|
521
|
+
- **Changes to existing code** get their own rules: find the code, read only
|
|
522
|
+
what is involved, make the smallest change in the code's own style.
|
|
523
|
+
- **Stays under the free limit.** Requests are spaced to Google's per-minute
|
|
524
|
+
limit instead of being refused and waited out; `/stats` shows how many were sent.
|
|
525
|
+
|
|
396
526
|
## Options
|
|
397
527
|
|
|
398
528
|
```
|
|
@@ -401,6 +531,9 @@ ucode [options]
|
|
|
401
531
|
-m, --model <id> which model to use
|
|
402
532
|
-C, --cwd <dir> work in another directory
|
|
403
533
|
--plan start in plan mode
|
|
534
|
+
-p, --print <task> do one task with no keyboard, print the answer, exit
|
|
535
|
+
--json with -p: one JSON object about the run
|
|
536
|
+
-y, --yes with -p: say yes to anything that would be asked
|
|
404
537
|
--debug print stack traces when something breaks
|
|
405
538
|
-v, --version print the version
|
|
406
539
|
-h, --help the above
|
|
@@ -410,7 +543,12 @@ ucode [options]
|
|
|
410
543
|
|
|
411
544
|
| | |
|
|
412
545
|
| --- | --- |
|
|
413
|
-
| `~/.ucode/.env` | `
|
|
546
|
+
| `~/.ucode/.env` | `GEMINI_API_KEY`, and `TAVILY_API_KEY` for web search |
|
|
547
|
+
| `~/.ucode/settings.json`, `.ucode/settings.json` | permissions, always-allowed commands, hooks |
|
|
548
|
+
| `~/.ucode/mcp.json`, `.ucode/mcp.json` | MCP servers |
|
|
549
|
+
| `.ucode/commands/*.md` | your own slash commands |
|
|
550
|
+
| `~/.ucode/snapshots/` | the project before each turn, for `/undo` |
|
|
551
|
+
| `~/.ucode/lessons.json` | mistakes ucode keeps catching, warned about next time |
|
|
414
552
|
| `~/.ucode/sessions/` | one JSON per conversation |
|
|
415
553
|
| `.ucode/skills/` | skills belonging to a project |
|
|
416
554
|
| `UCODE.md` | project memory, read every turn |
|
|
@@ -420,7 +558,11 @@ Environment overrides: `UCODE_MODEL`, `UCODE_WORKER_MODEL` (a faster model for
|
|
|
420
558
|
parallel workers), `UCODE_WORKER_STEPS`, `UCODE_MAX_CONTEXT_TOKENS`,
|
|
421
559
|
`UCODE_MAX_STEPS`, `UCODE_MAX_TOOL_OUTPUT`, `UCODE_REQUEST_TIMEOUT_MS`,
|
|
422
560
|
`UCODE_STALL_MS` (how long a silent reply is waited on before asking again, 60s),
|
|
423
|
-
`UCODE_BASE_URL`, `UCODE_NO_UPDATE
|
|
561
|
+
`UCODE_BASE_URL`, `UCODE_NO_UPDATE`, `UCODE_THINK=0`, `UCODE_DESIGN_CHECK=0`,
|
|
562
|
+
`UCODE_RPM` (requests a minute before pacing, 0 = off), `UCODE_RIPGREP=0`.
|
|
563
|
+
|
|
564
|
+
Search uses ripgrep (`rg`) when it is installed — much faster on a big
|
|
565
|
+
project — and its own search otherwise.
|
|
424
566
|
|
|
425
567
|
Web search needs a Tavily key — free, 1000 searches a month, no card. Without
|
|
426
568
|
one, ucode answers from what it knows and says that it could not check.
|
|
@@ -436,6 +578,13 @@ src/core/window.js folding a long conversation to fit
|
|
|
436
578
|
src/core/skills.js loading skills, and deciding which load themselves
|
|
437
579
|
src/core/context.js the project map and project memory
|
|
438
580
|
src/core/failure.js one error shape: what, why, what next
|
|
581
|
+
src/core/scope.js what a request carries: build scope, design direction, edit rules
|
|
582
|
+
src/core/genericcheck.js the check for a generated-looking design
|
|
583
|
+
src/core/lessons.js mistakes counted across sessions, warned about up front
|
|
584
|
+
src/core/snapshot.js the project before every turn, for /undo
|
|
585
|
+
src/core/settings.js permissions, always-allow, hooks, project trust
|
|
586
|
+
src/core/mcp.js the MCP client: stdio and HTTP servers, no SDK
|
|
587
|
+
src/core/headless.js ucode -p: one job, no keyboard
|
|
439
588
|
src/tools/ the twenty-one tools, plus their shared plumbing
|
|
440
589
|
src/ui/screen.js the full-screen interface
|
|
441
590
|
src/ui/plain.js the same interface for when there is no terminal
|
package/package.json
CHANGED
|
@@ -1,64 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ucode-agent",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "ucode - a terminal coding agent that reads, edits and runs your code, on Google Gemini models.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "ucode.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"ucode": "ucode.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"ucode.js",
|
|
12
|
-
"src/",
|
|
13
|
-
"skills/",
|
|
14
|
-
"templates/",
|
|
15
|
-
"THIRD_PARTY_NOTICES.md",
|
|
16
|
-
"LICENSE-APACHE"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"start": "node ucode.js",
|
|
20
|
-
"test": "node test/run.js",
|
|
21
|
-
"prepublishOnly": "node scripts/no-bundled-key.js && node test/run.js",
|
|
22
|
-
"hooks": "node scripts/install-hooks.js"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"marked
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "ucode-agent",
|
|
3
|
+
"version": "1.65.0",
|
|
4
|
+
"description": "ucode - a terminal coding agent that reads, edits and runs your code, on Google Gemini models.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "ucode.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ucode": "ucode.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"ucode.js",
|
|
12
|
+
"src/",
|
|
13
|
+
"skills/",
|
|
14
|
+
"templates/",
|
|
15
|
+
"THIRD_PARTY_NOTICES.md",
|
|
16
|
+
"LICENSE-APACHE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node ucode.js",
|
|
20
|
+
"test": "node test/run.js",
|
|
21
|
+
"prepublishOnly": "node scripts/no-bundled-key.js && node test/run.js",
|
|
22
|
+
"hooks": "node scripts/install-hooks.js",
|
|
23
|
+
"eval": "node test/evals/run.js"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/sppideey/ucode-agent.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/sppideey/ucode-agent/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/sppideey/ucode-agent#readme",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"agent",
|
|
38
|
+
"cli",
|
|
39
|
+
"terminal",
|
|
40
|
+
"coding-agent",
|
|
41
|
+
"llm",
|
|
42
|
+
"gemini",
|
|
43
|
+
"google-gemini",
|
|
44
|
+
"ai"
|
|
45
|
+
],
|
|
46
|
+
"author": "om dixit",
|
|
47
|
+
"license": "(MIT OR Apache-2.0)",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@babel/parser": "^7.29.9",
|
|
50
|
+
"chalk": "^6.0.0",
|
|
51
|
+
"dotenv": "^18.0.3",
|
|
52
|
+
"jsonrepair": "^3.15.0",
|
|
53
|
+
"marked": "^15.0.12",
|
|
54
|
+
"marked-terminal": "^7.3.0",
|
|
55
|
+
"openai": "^7.23.0",
|
|
56
|
+
"playwright-core": "^1.63.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^26.6.2",
|
|
60
|
+
"typescript": "^5.9.3"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands.js — slash commands the user writes themselves.
|
|
3
|
+
*
|
|
4
|
+
* ~/.ucode/commands/<name>.md yours, everywhere
|
|
5
|
+
* <project>/.ucode/commands/<name>.md this project's (wins over yours)
|
|
6
|
+
*
|
|
7
|
+
* The file is a prompt. `/name some words` sends it, with $ARGUMENTS replaced
|
|
8
|
+
* by the words — or the words added at the end when the file has no
|
|
9
|
+
* $ARGUMENTS. Its first line is what /help shows.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { promises as fs } from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
export const USER_COMMANDS = path.join(os.homedir(), '.ucode', 'commands');
|
|
17
|
+
export const projectCommands = (cwd) => path.join(cwd, '.ucode', 'commands');
|
|
18
|
+
|
|
19
|
+
async function readDir(dir) {
|
|
20
|
+
const found = new Map();
|
|
21
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
22
|
+
if (!entry.isFile() || !/\.md$/i.test(entry.name)) continue;
|
|
23
|
+
const name = entry.name.replace(/\.md$/i, '').toLowerCase();
|
|
24
|
+
if (!/^[a-z0-9][\w-]*$/.test(name)) continue;
|
|
25
|
+
const body = await fs.readFile(path.join(dir, entry.name), 'utf8').catch(() => null);
|
|
26
|
+
if (!body?.trim()) continue;
|
|
27
|
+
const first = body.trim().split('\n')[0].replace(/^#+\s*/, '').trim();
|
|
28
|
+
found.set(name, { name, body: body.trim(), description: first.slice(0, 70) });
|
|
29
|
+
}
|
|
30
|
+
return found;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Every command, by name without the slash. */
|
|
34
|
+
export async function loadCommands(cwd, { userDir = USER_COMMANDS } = {}) {
|
|
35
|
+
const [mine, project] = await Promise.all([readDir(userDir), readDir(projectCommands(cwd))]);
|
|
36
|
+
return new Map([...mine, ...project]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The prompt a command sends, given what was typed after it. */
|
|
40
|
+
export function expandCommand(body, args = '') {
|
|
41
|
+
const words = String(args).trim();
|
|
42
|
+
if (body.includes('$ARGUMENTS')) return body.split('$ARGUMENTS').join(words);
|
|
43
|
+
return words ? `${body}\n\n${words}` : body;
|
|
44
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* genericcheck.js — the generated look, caught in code.
|
|
3
|
+
*
|
|
4
|
+
* The rules against it were already written down, in the system prompt and
|
|
5
|
+
* the ui-ux skill: not the starter's palette, not Inter at every size, not a
|
|
6
|
+
* purple-to-blue gradient, not emoji standing in for icons. Flash-Lite reads
|
|
7
|
+
* them and ships the starter's teal anyway. A rule the model can skip is a
|
|
8
|
+
* suggestion; a check that hands the problem back is a rule.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is a few regular expressions over files already on disk —
|
|
11
|
+
* milliseconds, no model call. Only a hit costs anything: one fix round.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { promises as fs } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
/** The starter's own accent, which means nobody chose one. */
|
|
18
|
+
const STARTER = {
|
|
19
|
+
'plain-html': { files: ['styles.css'], token: /--accent\s*:\s*#2dd4bf\b/i },
|
|
20
|
+
'next-shadcn': { files: ['src/app/globals.css'], token: /--primary\s*:\s*oklch\(\s*0\.53\s+0\.2\s+264\s*\)/i },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Files that carry a Next.js app's look. */
|
|
24
|
+
const LOOK_FILES = {
|
|
25
|
+
'next-shadcn': ['src/app/globals.css', 'src/app/layout.tsx', 'src/app/page.tsx'],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const NAMED = {
|
|
29
|
+
purple: 300, violet: 300, indigo: 275, blueviolet: 271, mediumpurple: 260, rebeccapurple: 270,
|
|
30
|
+
darkviolet: 282, slateblue: 248, mediumslateblue: 249, darkslateblue: 248, magenta: 300, fuchsia: 300,
|
|
31
|
+
blue: 240, royalblue: 225, mediumblue: 240, dodgerblue: 210, cornflowerblue: 219,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Hue in degrees of one colour, or null for a grey or something unreadable. */
|
|
35
|
+
export function hueOf(colour) {
|
|
36
|
+
const c = String(colour).trim().toLowerCase();
|
|
37
|
+
if (NAMED[c] !== undefined) return NAMED[c];
|
|
38
|
+
|
|
39
|
+
let r; let g; let b;
|
|
40
|
+
const hex = /^#([0-9a-f]{3,8})$/.exec(c);
|
|
41
|
+
if (hex) {
|
|
42
|
+
let h = hex[1];
|
|
43
|
+
if (h.length === 3 || h.length === 4) h = [...h.slice(0, 3)].map((x) => x + x).join('');
|
|
44
|
+
[r, g, b] = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
|
|
45
|
+
}
|
|
46
|
+
const rgb = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/.exec(c);
|
|
47
|
+
if (rgb) [r, g, b] = rgb.slice(1, 4).map((v) => Number(v) / 255);
|
|
48
|
+
const hsl = /^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%/.exec(c);
|
|
49
|
+
if (hsl) return Number(hsl[2]) < 25 ? null : Number(hsl[1]) % 360;
|
|
50
|
+
const lch = /^oklch\(\s*[\d.]+%?\s+([\d.]+)\s+([\d.]+)/.exec(c);
|
|
51
|
+
// oklch puts blue near 264 and purple near 300-310; shift to the same wheel as hsl.
|
|
52
|
+
if (lch) return Number(lch[1]) < 0.06 ? null : (Number(lch[2]) - 25 + 360) % 360;
|
|
53
|
+
if (r === undefined) return null;
|
|
54
|
+
|
|
55
|
+
const max = Math.max(r, g, b);
|
|
56
|
+
const min = Math.min(r, g, b);
|
|
57
|
+
const d = max - min;
|
|
58
|
+
const light = (max + min) / 2;
|
|
59
|
+
const sat = d === 0 ? 0 : d / (1 - Math.abs(2 * light - 1));
|
|
60
|
+
if (sat < 0.25) return null;
|
|
61
|
+
let hue;
|
|
62
|
+
if (max === r) hue = ((g - b) / d) % 6;
|
|
63
|
+
else if (max === g) hue = (b - r) / d + 2;
|
|
64
|
+
else hue = (r - g) / d + 4;
|
|
65
|
+
return Math.round((hue * 60 + 360) % 360);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Every gradient's argument list, brackets balanced. */
|
|
69
|
+
function gradients(css) {
|
|
70
|
+
const found = [];
|
|
71
|
+
const re = /(?:linear|radial|conic)-gradient\(/gi;
|
|
72
|
+
let m;
|
|
73
|
+
while ((m = re.exec(css))) {
|
|
74
|
+
let depth = 1;
|
|
75
|
+
let i = re.lastIndex;
|
|
76
|
+
for (; i < css.length && depth; i++) {
|
|
77
|
+
if (css[i] === '(') depth++;
|
|
78
|
+
else if (css[i] === ')') depth--;
|
|
79
|
+
}
|
|
80
|
+
found.push(css.slice(re.lastIndex, i - 1));
|
|
81
|
+
}
|
|
82
|
+
return found;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const COLOUR = /#[0-9a-f]{3,8}\b|(?:rgba?|hsla?|oklch)\([^)]*\)|\b[a-z]+\b/gi;
|
|
86
|
+
|
|
87
|
+
/** The purple-to-blue gradient: every coloured stop blue-to-purple, at least one of them purple. */
|
|
88
|
+
export function purpleGradient(css) {
|
|
89
|
+
return gradients(css).some((args) => {
|
|
90
|
+
const hues = (args.match(COLOUR) ?? []).map(hueOf).filter((h) => h !== null);
|
|
91
|
+
return hues.length >= 2 && hues.every((h) => h >= 200 && h <= 330) && hues.some((h) => h >= 250);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The files that make up the page. For a plain page that is index.html and
|
|
97
|
+
* whatever it actually links: a starter stylesheet left behind, unlinked,
|
|
98
|
+
* says nothing about how the app looks.
|
|
99
|
+
*/
|
|
100
|
+
async function lookFiles(dir, template) {
|
|
101
|
+
if (template !== 'plain-html') return LOOK_FILES[template] ?? [];
|
|
102
|
+
const html = await fs.readFile(path.join(dir, 'index.html'), 'utf8').catch(() => '');
|
|
103
|
+
const linked = [...html.matchAll(/<(?:link|script)\b[^>]*\b(?:href|src)\s*=\s*["']([^"'?#]+)/gi)]
|
|
104
|
+
.map((m) => m[1])
|
|
105
|
+
.filter((f) => !/^(?:[a-z]+:)?\/\//i.test(f) && /\.(?:css|m?js)$/i.test(f));
|
|
106
|
+
return ['index.html', ...new Set(linked)];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Problems with the look of the app in `dir`, as lines for the model. Never throws. */
|
|
110
|
+
export async function genericLook(dir, template = 'plain-html') {
|
|
111
|
+
const files = await lookFiles(dir, template);
|
|
112
|
+
const texts = await Promise.all(files.map((f) => fs.readFile(path.join(dir, f), 'utf8').catch(() => '')));
|
|
113
|
+
const all = texts.join('\n');
|
|
114
|
+
if (!all.trim()) return [];
|
|
115
|
+
|
|
116
|
+
const problems = [];
|
|
117
|
+
const starter = STARTER[template];
|
|
118
|
+
if (starter) {
|
|
119
|
+
const own = starter.files.filter((f) => files.includes(f) || template !== 'plain-html');
|
|
120
|
+
const texts2 = await Promise.all(own.map((f) => fs.readFile(path.join(dir, f), 'utf8').catch(() => '')));
|
|
121
|
+
if (texts2.some((t) => starter.token.test(t))) {
|
|
122
|
+
problems.push('The accent is still the starter\'s own colour, so the app looks like every other one built from it. Pick an accent for this app and set it in the tokens.');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (/font-family\s*:\s*["']?Inter["']?\s*[,;}]|--font-[\w-]+\s*:\s*["']?Inter["']?\s*[,;]|family=Inter(?![+\w])|import\s*\{[^}]*\bInter\b[^}]*\}\s*from\s*["']next\/font\/google/i.test(all)) {
|
|
126
|
+
problems.push('The type is Inter, the default of generated apps. Choose a typeface with a character that fits this app.');
|
|
127
|
+
}
|
|
128
|
+
if (purpleGradient(all)) {
|
|
129
|
+
problems.push('There is a purple-to-blue gradient, the most recognisable mark of a generated design. Use the app\'s own accent, flat or with a quiet tint.');
|
|
130
|
+
}
|
|
131
|
+
if (/(?:-webkit-)?background-clip\s*:\s*text/i.test(all)) {
|
|
132
|
+
problems.push('There is gradient text (background-clip: text). Set headings in a solid colour and let the type carry them.');
|
|
133
|
+
}
|
|
134
|
+
const emoji = all.match(/<(?:button|h[1-6])\b[^>]*>\s*\p{Extended_Pictographic}/gu) ?? [];
|
|
135
|
+
if (emoji.length >= 3) {
|
|
136
|
+
problems.push(`Emoji stand in for icons in ${emoji.length} buttons or headings. Use small inline SVG icons or plain words.`);
|
|
137
|
+
}
|
|
138
|
+
return problems;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The fix-round text for a list of look problems. */
|
|
142
|
+
export function genericMessage(problems) {
|
|
143
|
+
return 'ucode checked the design for the generated look and found:\n' +
|
|
144
|
+
problems.map((p) => `- ${p}`).join('\n') +
|
|
145
|
+
'\nFix these in the design tokens and styles only - a new accent, a new typeface. Do not ' +
|
|
146
|
+
'restructure the app or change what it does.';
|
|
147
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* headless.js — `ucode -p "task"`: one job, no keyboard, then exit.
|
|
3
|
+
*
|
|
4
|
+
* For scripts, CI and the eval set. Progress goes to stderr, the answer to
|
|
5
|
+
* stdout (or, with --json, one JSON object describing the run), and the exit
|
|
6
|
+
* code says whether it worked. Nobody is there to approve anything, so every
|
|
7
|
+
* question is answered no — unless --yes says to answer yes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Readable } from 'node:stream';
|
|
11
|
+
import { Plain } from '../ui/plain.js';
|
|
12
|
+
import { Agent } from './loop.js';
|
|
13
|
+
import { requestCount } from './provider.js';
|
|
14
|
+
import { stopServers } from '../tools/shell.js';
|
|
15
|
+
import { closeBrowser } from '../tools/browser.js';
|
|
16
|
+
|
|
17
|
+
export class Headless extends Plain {
|
|
18
|
+
constructor({ cwd, yes = false }) {
|
|
19
|
+
super({ cwd, input: Readable.from([]), output: process.stderr });
|
|
20
|
+
this.yes = yes;
|
|
21
|
+
this.answer = '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
assistant(text, opts = {}) {
|
|
25
|
+
super.assistant(text, opts);
|
|
26
|
+
if (!opts.replay && String(text).trim()) this.answer = String(text).trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
confirm({ action }) {
|
|
30
|
+
this.note(`${this.yes ? 'approved' : 'declined'} (no one to ask): ${action}`);
|
|
31
|
+
return Promise.resolve(this.yes);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Run one prompt. Resolves to the exit code. */
|
|
36
|
+
export async function runHeadless({ cwd, prompt, json = false, yes = false, plan = false, write = (s) => process.stdout.write(s) }) {
|
|
37
|
+
const ui = new Headless({ cwd, yes });
|
|
38
|
+
const agent = new Agent({ cwd, ui });
|
|
39
|
+
if (plan) ui.mode = 'plan';
|
|
40
|
+
const started = Date.now();
|
|
41
|
+
const sent = requestCount();
|
|
42
|
+
let error = null;
|
|
43
|
+
try {
|
|
44
|
+
await agent.bootstrap();
|
|
45
|
+
agent.startMcp();
|
|
46
|
+
await agent.mcpStarting;
|
|
47
|
+
await agent.turn(prompt);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
error = err;
|
|
50
|
+
ui.error(err);
|
|
51
|
+
} finally {
|
|
52
|
+
stopServers();
|
|
53
|
+
agent.mcp?.close();
|
|
54
|
+
await closeBrowser().catch(() => {});
|
|
55
|
+
await agent.settled?.().catch(() => {});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ok = !error && !agent.endedSilently;
|
|
59
|
+
if (json) {
|
|
60
|
+
write(`${JSON.stringify({
|
|
61
|
+
ok,
|
|
62
|
+
answer: ui.answer,
|
|
63
|
+
files: [...(agent.touched ?? [])],
|
|
64
|
+
steps: agent.stats.steps,
|
|
65
|
+
requests: requestCount() - sent,
|
|
66
|
+
ms: Date.now() - started,
|
|
67
|
+
error: error ? (error.failed ?? error.message ?? String(error)) : null,
|
|
68
|
+
})}\n`);
|
|
69
|
+
} else if (ui.answer) {
|
|
70
|
+
write(`${ui.answer}\n`);
|
|
71
|
+
}
|
|
72
|
+
return ok ? 0 : 1;
|
|
73
|
+
}
|