ucode-agent 1.1.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.
@@ -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
+ }