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 CHANGED
@@ -19,7 +19,7 @@ corner:
19
19
  ╭──────────────────────────────────────────────────────────────────────────────╮
20
20
  │ › Ask anything… │
21
21
  │ │
22
- │ ◆ Build · Nemotron 3 Ultra 0% │
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 · Nemotron 3 Ultra 4% │
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
- | **Nemotron 3 Ultra** | 1M | the default — deepest reasoning, slowest to first token |
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 | code and UI specialist reach for it on frontend work |
95
+ | **North Mini Code** ★ | 256k | the defaultbuilt 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
- Ultra is the default because the work this is for read a codebase, hold it in
101
- mind, change several files consistently is what a million-token window and a
102
- long think are for. When the wait stops being worth it, switch.
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
- **Twelve tools.** `read_file`, `read_files`, `write_file`, `batch_write`, `edit_file`,
107
- `multi_edit`, `list_dir`, `glob`, `grep`, `run_command`, `run_commands`,
108
- `web_search`. Read-only calls run in parallel; anything that writes runs on its
109
- own, in order.
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* the text is there but the indentation differs, or
113
- its first line appears at line 40 and the rest does not. A wrong edit reported
114
- as a success is the most expensive thing an agent can do.
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`, `UCODE_MAX_CONTEXT_TOKENS`,
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.2.0",
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, load the page, check the
145
- core loop end to end. A clean build proves it compiles, not that it works.
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
@@ -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) and check:
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
+ }