ucode-agent 1.2.0 → 1.4.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 +73 -17
- package/package.json +3 -2
- package/skills/build-app/SKILL.md +4 -2
- package/skills/ui-ux/SKILL.md +5 -1
- package/src/core/context.js +151 -0
- package/src/core/loop.js +550 -40
- package/src/core/provider.js +58 -15
- package/src/core/updater.js +95 -0
- package/src/tools/browser.js +258 -0
- package/src/tools/files.js +173 -16
- package/src/tools/index.js +472 -394
- package/src/tools/shell.js +149 -1
- package/src/ui/plain.js +330 -325
- package/src/ui/screen.js +27 -7
- package/src/ui/theme.js +18 -0
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ corner:
|
|
|
19
19
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
20
20
|
│ › Ask anything… │
|
|
21
21
|
│ │
|
|
22
|
-
│ ◆ Build ·
|
|
22
|
+
│ ◆ Build · North Mini Code 0% │
|
|
23
23
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
24
24
|
|
|
25
25
|
|
|
@@ -44,7 +44,7 @@ the input, so your own words are easy to find in a long session:
|
|
|
44
44
|
╭──────────────────────────────────────────────────────────────────────────────────╮
|
|
45
45
|
│ › now add a dark mode toggle │
|
|
46
46
|
│ │
|
|
47
|
-
│ ◆ Build ·
|
|
47
|
+
│ ◆ Build · North Mini Code 4% │
|
|
48
48
|
╰──────────────────────────────────────────────────────────────────────────────────╯
|
|
49
49
|
```
|
|
50
50
|
|
|
@@ -88,30 +88,75 @@ properly, which is the thing an agent actually depends on.
|
|
|
88
88
|
|
|
89
89
|
| Model | Context | For |
|
|
90
90
|
| --- | --- | --- |
|
|
91
|
-
|
|
|
91
|
+
| Nemotron 3 Ultra | 1M | deepest reasoning, slowest to first token |
|
|
92
92
|
| Nemotron 3.5 Lightning | 1M | the same enormous window, answers much sooner |
|
|
93
93
|
| Nemotron 3 Super | 262k | strong all-rounder, quick to start |
|
|
94
94
|
| Nemotron 3 Nano Omni | 256k | small, fast, reasoning tuned |
|
|
95
|
-
| **North Mini Code** ★ | 256k |
|
|
95
|
+
| **North Mini Code** ★ | 256k | the default — built for code and interface work, quick to answer |
|
|
96
96
|
|
|
97
97
|
`/model` shows them and switches. `ucode -m cohere/north-mini-code:free`
|
|
98
98
|
starts on one.
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
North Mini Code is the default: it is built for code and interfaces, which is most
|
|
101
|
+
of what ucode is asked to do, and it answers far sooner than the big reasoning
|
|
102
|
+
models. Switch to Ultra when a problem needs the million-token window more than
|
|
103
|
+
the speed.
|
|
104
|
+
|
|
105
|
+
**A busy model never stops a build.** Free endpoints are shared, and "too many
|
|
106
|
+
requests" is routine. ucode waits it out with growing pauses, and if the model
|
|
107
|
+
stays busy it carries on with the next one — North Mini Code, then Nemotron 3.5
|
|
108
|
+
Lightning, Super, Ultra — from exactly where it was, and tells you it switched.
|
|
109
|
+
If every model is busy at once it waits a minute and goes round again. Your
|
|
110
|
+
chosen model gets another go a few minutes later.
|
|
103
111
|
|
|
104
112
|
## What it does
|
|
105
113
|
|
|
106
|
-
**
|
|
107
|
-
`
|
|
108
|
-
`web_search`. Read-only calls run in parallel
|
|
109
|
-
|
|
114
|
+
**Fourteen tools.** `read_file`, `read_files`, `write_file`, `batch_write`,
|
|
115
|
+
`edit_file`, `multi_edit`, `edit_files`, `list_dir`, `glob`, `grep`,
|
|
116
|
+
`run_command`, `run_commands`, `look_at_app`, `web_search`. Read-only calls run in parallel,
|
|
117
|
+
and start the moment the model finishes writing them — while the rest of its
|
|
118
|
+
reply is still arriving. Anything that writes runs on its own, in order.
|
|
119
|
+
|
|
120
|
+
**Parallel workers.** When a build splits into parts that touch different files
|
|
121
|
+
— the API route, the upload component, the results view — the model hands them
|
|
122
|
+
to up to three workers that build at the same time, each line in the transcript
|
|
123
|
+
tagged with the worker's name. File writes take turns so two never collide.
|
|
124
|
+
|
|
125
|
+
**Installs that start early.** The moment a `package.json` with dependencies is
|
|
126
|
+
written, its install starts in the background while the rest of the app is
|
|
127
|
+
still being written. An install the model asks for later waits for that one
|
|
128
|
+
instead of running twice, and anything run in that folder waits for it too.
|
|
129
|
+
|
|
130
|
+
**It looks at what it built.** `look_at_app` opens the running app in a real
|
|
131
|
+
browser — the Edge or Chrome already on your machine, so there is nothing extra
|
|
132
|
+
to download — at 375px and 1440px. It reports console errors, failed requests,
|
|
133
|
+
content that spills off a phone screen, broken images and unlabeled controls,
|
|
134
|
+
saves screenshots to `.ucode/screenshots`, and has Nemotron Nano Omni review them
|
|
135
|
+
the way a designer would. The model fixes what it finds before calling the app
|
|
136
|
+
done.
|
|
137
|
+
|
|
138
|
+
**Errors fixed before you see them.** When the model says it is done, ucode
|
|
139
|
+
type-checks every file it changed — `tsc --noEmit` for TypeScript projects,
|
|
140
|
+
a syntax check for JavaScript and Python — and hands any errors back to fix,
|
|
141
|
+
up to three rounds.
|
|
142
|
+
|
|
143
|
+
**A plan you can see.** For longer jobs the model keeps a short checklist, shown
|
|
144
|
+
as one line: `plan 2/5 ✓ Scaffold · ✓ Upload · ▸ Score dial · ○ Findings · ○ Polish`.
|
|
145
|
+
|
|
146
|
+
**It knows the project before it asks.** Each turn starts with a map of every
|
|
147
|
+
file and the names each code file exports, so the model goes straight to the
|
|
148
|
+
right file instead of searching for it.
|
|
149
|
+
|
|
150
|
+
**Project memory.** `UCODE.md` in a project — and `~/.ucode/UCODE.md` for how you
|
|
151
|
+
like to work everywhere — is read at the start of every turn. `/remember <note>`
|
|
152
|
+
adds a line to it.
|
|
110
153
|
|
|
111
154
|
**Edits that never guess.** `edit_file` matches exactly once or it fails, and
|
|
112
|
-
when it fails it says *why
|
|
113
|
-
|
|
114
|
-
|
|
155
|
+
when it fails it says *why*. It tolerates what does not matter — tabs against
|
|
156
|
+
spaces, a different indent depth, Windows line endings — and re-indents the
|
|
157
|
+
replacement to fit the file, but a match found twice is still refused.
|
|
158
|
+
`edit_files` changes several files in one call, and writes none of them if any
|
|
159
|
+
edit fails.
|
|
115
160
|
|
|
116
161
|
**Diffs with real line numbers.** Removed lines are numbered where they were,
|
|
117
162
|
added lines where they now are. Numbers you can jump to, not decoration.
|
|
@@ -125,7 +170,12 @@ them; the chip inside the input box says which is live.
|
|
|
125
170
|
|
|
126
171
|
**Sessions.** Everything is on disk under `~/.ucode/sessions`, saved after every
|
|
127
172
|
step. `/resume` lists them with what each one was actually about, the ones from
|
|
128
|
-
this folder first.
|
|
173
|
+
this folder first. Press `d` twice on one to delete it — the list stays open, so
|
|
174
|
+
clearing out several is quick — or `/session delete 2,5`.
|
|
175
|
+
|
|
176
|
+
**It updates itself.** Each launch checks npm in the background and, if there is
|
|
177
|
+
a newer version, installs it while you work. The next launch is the new one.
|
|
178
|
+
Set `UCODE_NO_UPDATE=1` to turn that off.
|
|
129
179
|
|
|
130
180
|
**A context window that folds rather than forgets.** Past 75% the oldest turns
|
|
131
181
|
are summarised instead of dropped, never cutting between a tool call and its
|
|
@@ -178,7 +228,9 @@ Everything after the frontmatter is the instruction.
|
|
|
178
228
|
| `/help` | the list |
|
|
179
229
|
| `/model` | show the models and switch — `/models` does the same |
|
|
180
230
|
| `/resume` | pick up an earlier conversation — `/session`, `/sessions` too |
|
|
231
|
+
| `/session delete 2,5` | delete saved conversations by number (or `d d` in the list) |
|
|
181
232
|
| `/new` | save this one and start fresh |
|
|
233
|
+
| `/remember <note>` | add a standing note to this project's `UCODE.md` |
|
|
182
234
|
| `/skills` | what it knows how to do, and what is loaded |
|
|
183
235
|
| `/search <query>` | look something up on the web |
|
|
184
236
|
| `/copy` | last reply to the clipboard |
|
|
@@ -209,10 +261,13 @@ ucode [options]
|
|
|
209
261
|
| `~/.ucode/.env` | `UCODE_API_KEY`, and `TAVILY_API_KEY` for web search |
|
|
210
262
|
| `~/.ucode/sessions/` | one JSON per conversation |
|
|
211
263
|
| `.ucode/skills/` | skills belonging to a project |
|
|
264
|
+
| `UCODE.md` | project memory, read every turn |
|
|
265
|
+
| `~/.ucode/UCODE.md` | your own standing instructions, for every project |
|
|
212
266
|
|
|
213
|
-
Environment overrides: `UCODE_MODEL`, `
|
|
267
|
+
Environment overrides: `UCODE_MODEL`, `UCODE_WORKER_MODEL` (a faster model for
|
|
268
|
+
parallel workers), `UCODE_WORKER_STEPS`, `UCODE_MAX_CONTEXT_TOKENS`,
|
|
214
269
|
`UCODE_MAX_STEPS`, `UCODE_MAX_TOOL_OUTPUT`, `UCODE_REQUEST_TIMEOUT_MS`,
|
|
215
|
-
`UCODE_BASE_URL`.
|
|
270
|
+
`UCODE_BASE_URL`, `UCODE_NO_UPDATE`.
|
|
216
271
|
|
|
217
272
|
Web search needs a Tavily key — free, 1000 searches a month, no card. Without
|
|
218
273
|
one, ucode answers from what it knows and says that it could not check.
|
|
@@ -226,6 +281,7 @@ src/core/provider.js the only file that knows which provider answers
|
|
|
226
281
|
src/core/history.js sessions on disk
|
|
227
282
|
src/core/window.js folding a long conversation to fit
|
|
228
283
|
src/core/skills.js loading skills, and deciding which load themselves
|
|
284
|
+
src/core/context.js the project map and project memory
|
|
229
285
|
src/core/failure.js one error shape: what, why, what next
|
|
230
286
|
src/tools/ the eleven tools, plus their shared plumbing
|
|
231
287
|
src/ui/screen.js the full-screen interface
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucode-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "ucode - a terminal coding agent that reads, edits and runs your code, on NVIDIA and Cohere models.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "ucode.js",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"dotenv": "^17.4.2",
|
|
49
49
|
"marked": "^15.0.12",
|
|
50
50
|
"marked-terminal": "^7.3.0",
|
|
51
|
-
"openai": "^7.4.0"
|
|
51
|
+
"openai": "^7.4.0",
|
|
52
|
+
"playwright-core": "^1.63.0"
|
|
52
53
|
}
|
|
53
54
|
}
|
|
@@ -141,8 +141,10 @@ Use `batch_write` for the skeleton — one call, every file.
|
|
|
141
141
|
- `npm run build` — it type-checks and lints; a build that fails is not done.
|
|
142
142
|
- Start it: `npm run dev` goes to the background on its own and comes back with
|
|
143
143
|
the URL once ready. Do not start it twice.
|
|
144
|
-
- Exercise it: `curl` the API route with real input,
|
|
145
|
-
|
|
144
|
+
- Exercise it: `curl` the API route with real input, then `look_at_app` on every
|
|
145
|
+
page — it loads them in a real browser at phone and desktop width and reports
|
|
146
|
+
errors, overflow and a visual review. A clean build proves it compiles, not
|
|
147
|
+
that it works.
|
|
146
148
|
- Fix what you find and check again.
|
|
147
149
|
|
|
148
150
|
## 7. Definition of done
|
package/skills/ui-ux/SKILL.md
CHANGED
|
@@ -283,7 +283,11 @@ because it was first to hand means you were not deciding.
|
|
|
283
283
|
|
|
284
284
|
## 9. Look at it, then report
|
|
285
285
|
|
|
286
|
-
Run it (`npm run dev` starts in the background and returns the URL)
|
|
286
|
+
Run it (`npm run dev` starts in the background and returns the URL), then call
|
|
287
|
+
`look_at_app` with that URL. It opens the app in a real browser at 375px and
|
|
288
|
+
1440px, reports console errors, failed requests, overflow, broken images and
|
|
289
|
+
unlabeled controls, and returns a designer's review of the screenshots. Fix what
|
|
290
|
+
it finds and look again. Then check:
|
|
287
291
|
|
|
288
292
|
1. **375px wide** — no horizontal scroll, nothing overlapping or clipped. If you
|
|
289
293
|
wrote no responsive rules at all, you have not done this.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context.js — what the model knows about the project before it asks.
|
|
3
|
+
*
|
|
4
|
+
* Two things go into the system prompt at the start of every turn:
|
|
5
|
+
*
|
|
6
|
+
* the project map every file, and the names each code file exports, so the
|
|
7
|
+
* model can go straight to the right file instead of
|
|
8
|
+
* spending round trips on list_dir and grep to find it
|
|
9
|
+
*
|
|
10
|
+
* project memory UCODE.md — the stack, the commands, the conventions, the
|
|
11
|
+
* preferences — written once, remembered every session
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { promises as fs } from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { walk } from '../tools/shared.js';
|
|
18
|
+
|
|
19
|
+
export const MEMORY_FILE = 'UCODE.md';
|
|
20
|
+
export const GLOBAL_MEMORY = path.join(os.homedir(), '.ucode', MEMORY_FILE);
|
|
21
|
+
|
|
22
|
+
const MAP_FILES = 400;
|
|
23
|
+
const MAP_CHARS = 8_000;
|
|
24
|
+
const MEMORY_CHARS = 6_000;
|
|
25
|
+
const SYMBOL_BYTES = 120_000;
|
|
26
|
+
const SYMBOLS_PER_FILE = 8;
|
|
27
|
+
|
|
28
|
+
const CODE = /\.(?:[cm]?[jt]sx?|py|go|rs|vue|svelte)$/i;
|
|
29
|
+
|
|
30
|
+
// Files the map never needs to name.
|
|
31
|
+
const NOISE = /(^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|.*\.map|.*\.min\.[jc]ss?|\.DS_Store|next-env\.d\.ts)$/i;
|
|
32
|
+
|
|
33
|
+
/** Parsed symbols, kept per file until the file's mtime changes. */
|
|
34
|
+
const symbolCache = new Map();
|
|
35
|
+
|
|
36
|
+
function symbolsIn(file, text) {
|
|
37
|
+
const found = [];
|
|
38
|
+
const add = (name) => { if (name && !found.includes(name)) found.push(name); };
|
|
39
|
+
|
|
40
|
+
if (/\.py$/i.test(file)) {
|
|
41
|
+
for (const m of text.matchAll(/^(?:async\s+)?(?:def|class)\s+([A-Za-z_]\w*)/gm)) add(m[1]);
|
|
42
|
+
} else if (/\.go$/i.test(file)) {
|
|
43
|
+
for (const m of text.matchAll(/^func\s+(?:\([^)]*\)\s*)?([A-Z]\w*)/gm)) add(m[1]);
|
|
44
|
+
for (const m of text.matchAll(/^type\s+([A-Z]\w*)/gm)) add(m[1]);
|
|
45
|
+
} else if (/\.rs$/i.test(file)) {
|
|
46
|
+
for (const m of text.matchAll(/^pub\s+(?:async\s+)?(?:fn|struct|enum|trait)\s+(\w+)/gm)) add(m[1]);
|
|
47
|
+
} else {
|
|
48
|
+
for (const m of text.matchAll(/export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g)) add(m[1]);
|
|
49
|
+
if (/export\s+default\s+(?:async\s+)?function\s*\(/.test(text)) add('default');
|
|
50
|
+
for (const m of text.matchAll(/export\s*\{([^}]+)\}/g)) {
|
|
51
|
+
for (const part of m[1].split(',')) add(part.trim().split(/\s+as\s+/).pop());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return found.slice(0, SYMBOLS_PER_FILE);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function symbolsFor(root, rel) {
|
|
59
|
+
const abs = path.join(root, rel);
|
|
60
|
+
try {
|
|
61
|
+
const stat = await fs.stat(abs);
|
|
62
|
+
if (stat.size > SYMBOL_BYTES) return [];
|
|
63
|
+
const cached = symbolCache.get(abs);
|
|
64
|
+
if (cached && cached.mtime === stat.mtimeMs) return cached.symbols;
|
|
65
|
+
const symbols = symbolsIn(rel, await fs.readFile(abs, 'utf8'));
|
|
66
|
+
symbolCache.set(abs, { mtime: stat.mtimeMs, symbols });
|
|
67
|
+
return symbols;
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A compact outline of the project: directories, their files, and what each
|
|
75
|
+
* code file exports. Bounded, so a large repository costs a fixed amount of
|
|
76
|
+
* context rather than all of it.
|
|
77
|
+
*/
|
|
78
|
+
export async function projectMap(root) {
|
|
79
|
+
const all = (await walk(root, { limit: MAP_FILES * 3 })).filter((f) => !NOISE.test(f));
|
|
80
|
+
if (all.length === 0) return '(the folder is empty — this is a new project)';
|
|
81
|
+
|
|
82
|
+
const files = all.slice(0, MAP_FILES).sort();
|
|
83
|
+
const symbols = await Promise.all(
|
|
84
|
+
files.map((f) => (CODE.test(f) ? symbolsFor(root, f) : Promise.resolve([])))
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const byDir = new Map();
|
|
88
|
+
files.forEach((f, i) => {
|
|
89
|
+
const dir = path.posix.dirname(f);
|
|
90
|
+
const name = path.posix.basename(f);
|
|
91
|
+
const line = symbols[i].length ? `${name} · ${symbols[i].join(', ')}` : name;
|
|
92
|
+
if (!byDir.has(dir)) byDir.set(dir, []);
|
|
93
|
+
byDir.get(dir).push(line);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const out = [];
|
|
97
|
+
let size = 0;
|
|
98
|
+
let shown = 0;
|
|
99
|
+
for (const [dir, entries] of byDir) {
|
|
100
|
+
const block = [dir === '.' ? './' : `${dir}/`, ...entries.map((e) => ` ${e}`)].join('\n');
|
|
101
|
+
if (size + block.length > MAP_CHARS) {
|
|
102
|
+
out.push(`… ${files.length - shown} more files not shown`);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
out.push(block);
|
|
106
|
+
size += block.length;
|
|
107
|
+
shown += entries.length;
|
|
108
|
+
}
|
|
109
|
+
if (all.length > MAP_FILES) out.push(`… the project has ${all.length}+ files; the rest are not listed`);
|
|
110
|
+
|
|
111
|
+
return out.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function readCapped(file) {
|
|
115
|
+
try {
|
|
116
|
+
const text = (await fs.readFile(file, 'utf8')).trim();
|
|
117
|
+
if (!text) return '';
|
|
118
|
+
return text.length > MEMORY_CHARS ? `${text.slice(0, MEMORY_CHARS)}\n… (cut)` : text;
|
|
119
|
+
} catch {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Standing instructions: ~/.ucode/UCODE.md for how you like to work anywhere,
|
|
126
|
+
* then <project>/UCODE.md for this project. The project file comes second so
|
|
127
|
+
* it wins where the two disagree.
|
|
128
|
+
*/
|
|
129
|
+
export async function loadMemory(root) {
|
|
130
|
+
const personal = await readCapped(GLOBAL_MEMORY);
|
|
131
|
+
const project = await readCapped(path.join(root, MEMORY_FILE));
|
|
132
|
+
const parts = [];
|
|
133
|
+
if (personal) parts.push(`From ~/.ucode/${MEMORY_FILE} (applies everywhere):\n${personal}`);
|
|
134
|
+
if (project) parts.push(`From ./${MEMORY_FILE} (this project):\n${project}`);
|
|
135
|
+
return parts.join('\n\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Append one note to this project's UCODE.md, creating it if needed. */
|
|
139
|
+
export async function remember(root, note) {
|
|
140
|
+
const file = path.join(root, MEMORY_FILE);
|
|
141
|
+
let existing = '';
|
|
142
|
+
try {
|
|
143
|
+
existing = await fs.readFile(file, 'utf8');
|
|
144
|
+
} catch {
|
|
145
|
+
existing = `# Project memory\n\nucode reads this at the start of every session in this folder.\n\n`;
|
|
146
|
+
}
|
|
147
|
+
const line = `- ${String(note).trim().replace(/\s+/g, ' ')}\n`;
|
|
148
|
+
const sep = existing.endsWith('\n') ? '' : '\n';
|
|
149
|
+
await fs.writeFile(file, existing + sep + line, 'utf8');
|
|
150
|
+
return file;
|
|
151
|
+
}
|