ucode-agent 1.2.0 → 1.3.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
@@ -103,15 +103,44 @@ long think are for. When the wait stops being worth it, switch.
103
103
 
104
104
  ## What it does
105
105
 
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.
106
+ **Thirteen tools.** `read_file`, `read_files`, `write_file`, `batch_write`,
107
+ `edit_file`, `multi_edit`, `edit_files`, `list_dir`, `glob`, `grep`,
108
+ `run_command`, `run_commands`, `web_search`. Read-only calls run in parallel,
109
+ and start the moment the model finishes writing them — while the rest of its
110
+ reply is still arriving. Anything that writes runs on its own, in order.
111
+
112
+ **Parallel workers.** When a build splits into parts that touch different files
113
+ — the API route, the upload component, the results view — the model hands them
114
+ to up to three workers that build at the same time, each line in the transcript
115
+ tagged with the worker's name. File writes take turns so two never collide.
116
+
117
+ **Installs that start early.** The moment a `package.json` with dependencies is
118
+ written, its install starts in the background while the rest of the app is
119
+ still being written. An install the model asks for later waits for that one
120
+ instead of running twice, and anything run in that folder waits for it too.
121
+
122
+ **Errors fixed before you see them.** When the model says it is done, ucode
123
+ type-checks every file it changed — `tsc --noEmit` for TypeScript projects,
124
+ a syntax check for JavaScript and Python — and hands any errors back to fix,
125
+ up to three rounds.
126
+
127
+ **A plan you can see.** For longer jobs the model keeps a short checklist, shown
128
+ as one line: `plan 2/5 ✓ Scaffold · ✓ Upload · ▸ Score dial · ○ Findings · ○ Polish`.
129
+
130
+ **It knows the project before it asks.** Each turn starts with a map of every
131
+ file and the names each code file exports, so the model goes straight to the
132
+ right file instead of searching for it.
133
+
134
+ **Project memory.** `UCODE.md` in a project — and `~/.ucode/UCODE.md` for how you
135
+ like to work everywhere — is read at the start of every turn. `/remember <note>`
136
+ adds a line to it.
110
137
 
111
138
  **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.
139
+ when it fails it says *why*. It tolerates what does not matter tabs against
140
+ spaces, a different indent depth, Windows line endings and re-indents the
141
+ replacement to fit the file, but a match found twice is still refused.
142
+ `edit_files` changes several files in one call, and writes none of them if any
143
+ edit fails.
115
144
 
116
145
  **Diffs with real line numbers.** Removed lines are numbered where they were,
117
146
  added lines where they now are. Numbers you can jump to, not decoration.
@@ -179,6 +208,7 @@ Everything after the frontmatter is the instruction.
179
208
  | `/model` | show the models and switch — `/models` does the same |
180
209
  | `/resume` | pick up an earlier conversation — `/session`, `/sessions` too |
181
210
  | `/new` | save this one and start fresh |
211
+ | `/remember <note>` | add a standing note to this project's `UCODE.md` |
182
212
  | `/skills` | what it knows how to do, and what is loaded |
183
213
  | `/search <query>` | look something up on the web |
184
214
  | `/copy` | last reply to the clipboard |
@@ -209,8 +239,11 @@ ucode [options]
209
239
  | `~/.ucode/.env` | `UCODE_API_KEY`, and `TAVILY_API_KEY` for web search |
210
240
  | `~/.ucode/sessions/` | one JSON per conversation |
211
241
  | `.ucode/skills/` | skills belonging to a project |
242
+ | `UCODE.md` | project memory, read every turn |
243
+ | `~/.ucode/UCODE.md` | your own standing instructions, for every project |
212
244
 
213
- Environment overrides: `UCODE_MODEL`, `UCODE_MAX_CONTEXT_TOKENS`,
245
+ Environment overrides: `UCODE_MODEL`, `UCODE_WORKER_MODEL` (a faster model for
246
+ parallel workers), `UCODE_WORKER_STEPS`, `UCODE_MAX_CONTEXT_TOKENS`,
214
247
  `UCODE_MAX_STEPS`, `UCODE_MAX_TOOL_OUTPUT`, `UCODE_REQUEST_TIMEOUT_MS`,
215
248
  `UCODE_BASE_URL`.
216
249
 
@@ -226,6 +259,7 @@ src/core/provider.js the only file that knows which provider answers
226
259
  src/core/history.js sessions on disk
227
260
  src/core/window.js folding a long conversation to fit
228
261
  src/core/skills.js loading skills, and deciding which load themselves
262
+ src/core/context.js the project map and project memory
229
263
  src/core/failure.js one error shape: what, why, what next
230
264
  src/tools/ the eleven tools, plus their shared plumbing
231
265
  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.3.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",
@@ -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
+ }