ucode-agent 1.7.0 → 1.8.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/src/ui/theme.js CHANGED
@@ -276,3 +276,47 @@ export function planLine(items) {
276
276
  });
277
277
  return ` ${sky(`plan ${done}/${list.length}`)} ${parts.join(dim(' · '))}`;
278
278
  }
279
+
280
+ /**
281
+ * The background ucode paints behind itself.
282
+ *
283
+ * A terminal's own background is whatever the person set it to years ago:
284
+ * white, solarized, a photograph. The interface was drawn for a dark one, and
285
+ * on a light terminal the dim greys it relies on turn to near-invisible smoke.
286
+ * So ucode paints its own ground for as long as it is running, and the
287
+ * alternate screen gives it back untouched on exit.
288
+ *
289
+ * Near-black rather than black: a true #000 against a bright room is a hole,
290
+ * and the box edges lose their softness. This is the shade a code editor
291
+ * settles on for the same reason.
292
+ */
293
+ export const BACKGROUND = process.env.UCODE_BG || '#131316';
294
+
295
+ const rgb = (hex) => {
296
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex.trim());
297
+ return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)] : [19, 19, 22];
298
+ };
299
+
300
+ /** Turn the background on. Everything painted after this sits on it. */
301
+ export const BG_ON = (() => {
302
+ if (process.env.NO_COLOR || process.env.UCODE_BG === 'off') return '';
303
+ const [r, g, b] = rgb(BACKGROUND);
304
+ return `\x1b[48;2;${r};${g};${b}m`;
305
+ })();
306
+
307
+ /** Hand the terminal its own colours back. */
308
+ export const BG_OFF = BG_ON ? '\x1b[0m' : '';
309
+
310
+ /**
311
+ * Keep the background on across a line that resets it.
312
+ *
313
+ * chalk closes a foreground with 39 and a background with 49, and 49 means
314
+ * "the terminal's default" — which is exactly the colour being painted over.
315
+ * A diff line, which sets its own background, would therefore punch a hole
316
+ * through to the terminal's ground for the rest of the line. Re-asserting the
317
+ * background after every reset closes those holes.
318
+ */
319
+ export function onBackground(text) {
320
+ if (!BG_ON) return text;
321
+ return BG_ON + String(text).replace(/\x1b\[(?:0|49)m/g, (m) => m + BG_ON);
322
+ }
package/ucode.js CHANGED
@@ -1,124 +1,132 @@
1
- #!/usr/bin/env node
2
- /**
3
- * ucode.js — the command.
4
- *
5
- * Parses the arguments, builds an Agent, and gets out of the way. Everything
6
- * of substance is under src/.
7
- */
8
-
9
- import path from 'node:path';
10
- import process from 'node:process';
11
- import { readFile } from 'node:fs/promises';
12
- import { realpathSync } from 'node:fs';
13
- import { pathToFileURL, fileURLToPath } from 'node:url';
14
-
15
- import { Agent } from './src/core/loop.js';
16
- import { setModel, modelName, MODELS, DEFAULT_MODEL, ENV_FILE } from './src/core/provider.js';
17
- import { VERSION } from './src/core/version.js';
18
- import { Plain } from './src/ui/plain.js';
19
- import { blue, dim, sky } from './src/ui/theme.js';
20
-
21
- function parseArgs(argv) {
22
- const args = { debug: false, model: null, cwd: process.cwd(), help: false, plan: false, version: false };
23
-
24
- for (let i = 0; i < argv.length; i++) {
25
- const a = argv[i];
26
- if (a === '--debug') args.debug = true;
27
- else if (a === '--plan') args.plan = true;
28
- else if (a === '--model' || a === '-m') args.model = argv[++i];
29
- else if (a === '--cwd' || a === '-C') args.cwd = path.resolve(argv[++i]);
30
- else if (a === '--help' || a === '-h') args.help = true;
31
- else if (a === '--version' || a === '-v') args.version = true;
32
- }
33
-
34
- return args;
35
- }
36
-
37
- function usage() {
38
- const entries = Object.entries(MODELS);
39
- const width = Math.max(...entries.map(([, m]) => m.name.length));
40
- const models = entries
41
- .map(([id, m]) => ` ${m.name.padEnd(width)} ${dim(id)}`)
42
- .join('\n');
43
-
44
- process.stdout.write(
45
- `\n ${blue('ucode')} — a terminal coding agent\n\n` +
46
- ' ucode [options]\n\n' +
47
- ` -m, --model <id> which model to use (default: ${modelName(DEFAULT_MODEL)})\n` +
48
- ' -C, --cwd <dir> work in another directory\n' +
49
- ' --plan start in plan mode: read and research, change nothing\n' +
50
- ' --debug print stack traces when something breaks\n' +
51
- ' -v, --version print the version\n' +
52
- ' -h, --help this message\n' +
53
- ' doctor check that everything ucode needs is working\n\n' +
54
- ` ${sky('Models')}\n${models}\n\n` +
55
- ` Needs UCODE_API_KEY in the environment or in ${ENV_FILE}\n` +
56
- ' Free keys: https://openrouter.ai/keys\n\n'
57
- );
58
- }
59
-
60
- async function main() {
61
- const args = parseArgs(process.argv.slice(2));
62
-
63
- if (args.help) return usage();
64
-
65
- if (process.argv[2] === 'doctor') {
66
- const { runDoctor } = await import('./src/core/doctor.js');
67
- process.stdout.write(`${(await runDoctor()).join('\n')}\n`);
68
- return;
69
- }
70
-
71
- if (args.version) {
72
- process.stdout.write(`${VERSION}\n`);
73
- return;
74
- }
75
-
76
- if (args.model) {
77
- try {
78
- setModel(args.model);
79
- } catch (err) {
80
- new Plain({ cwd: args.cwd }).error(err);
81
- process.exitCode = 1;
82
- return;
83
- }
84
- }
85
-
86
- const agent = new Agent({ cwd: args.cwd, debug: args.debug });
87
- if (args.plan) agent.ui.mode = 'plan';
88
-
89
- try {
90
- await agent.start();
91
- } catch (err) {
92
- agent.ui.error(err, { debug: args.debug });
93
- await agent.persist().catch(() => {});
94
- agent.ui.close();
95
- process.exitCode = 1;
96
- }
97
- }
98
-
99
- /**
100
- * Was this file launched, or imported?
101
- *
102
- * Importing it — which another front end would do to reuse Agent — must not
103
- * open a terminal session. The obvious check is comparing `import.meta.url`
104
- * with argv[1], and it is wrong: after `npm link`, argv[1] arrives as the path
105
- * through the symlink in the global node_modules while `import.meta.url` has
106
- * already been resolved to the real file. The two never match, so the command
107
- * starts, matches nothing, and exits successfully having done absolutely
108
- * nothing which is a great deal harder to diagnose than a crash.
109
- *
110
- * Comparing real paths is what actually answers the question.
111
- */
112
- function launchedDirectly() {
113
- const entry = process.argv[1];
114
- if (!entry) return false;
115
-
116
- const self = fileURLToPath(import.meta.url);
117
- try {
118
- return realpathSync(entry) === realpathSync(self);
119
- } catch {
120
- return pathToFileURL(entry).href === import.meta.url;
121
- }
122
- }
123
-
124
- if (launchedDirectly()) main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ucode.js — the command.
4
+ *
5
+ * Parses the arguments, builds an Agent, and gets out of the way. Everything
6
+ * of substance is under src/.
7
+ */
8
+
9
+ import path from 'node:path';
10
+ import process from 'node:process';
11
+ import { readFile } from 'node:fs/promises';
12
+ import { realpathSync } from 'node:fs';
13
+ import { pathToFileURL, fileURLToPath } from 'node:url';
14
+
15
+ import { Agent } from './src/core/loop.js';
16
+ import { setModel, modelName, MODELS, DEFAULT_MODEL, ENV_FILE } from './src/core/provider.js';
17
+ import { VERSION } from './src/core/version.js';
18
+ import { Plain } from './src/ui/plain.js';
19
+ import { blue, dim, sky } from './src/ui/theme.js';
20
+
21
+ function parseArgs(argv) {
22
+ const args = { debug: false, model: null, cwd: process.cwd(), help: false, plan: false, version: false };
23
+
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a === '--debug') args.debug = true;
27
+ else if (a === '--plan') args.plan = true;
28
+ else if (a === '--model' || a === '-m') args.model = argv[++i];
29
+ else if (a === '--cwd' || a === '-C') args.cwd = path.resolve(argv[++i]);
30
+ else if (a === '--help' || a === '-h') args.help = true;
31
+ else if (a === '--version' || a === '-v') args.version = true;
32
+ }
33
+
34
+ return args;
35
+ }
36
+
37
+ function usage() {
38
+ const entries = Object.entries(MODELS);
39
+ const width = Math.max(...entries.map(([, m]) => m.name.length));
40
+ const models = entries
41
+ .map(([id, m]) => ` ${m.name.padEnd(width)} ${dim(id)}`)
42
+ .join('\n');
43
+
44
+ process.stdout.write(
45
+ `\n ${blue('ucode')} — a terminal coding agent\n\n` +
46
+ ' ucode [options]\n\n' +
47
+ ` -m, --model <id> which model to use (default: ${modelName(DEFAULT_MODEL)})\n` +
48
+ ' -C, --cwd <dir> work in another directory\n' +
49
+ ' --plan start in plan mode: read and research, change nothing\n' +
50
+ ' --debug print stack traces when something breaks\n' +
51
+ ' -v, --version print the version\n' +
52
+ ' -h, --help this message\n' +
53
+ ' doctor check that everything ucode needs is working\n' +
54
+ ' login <key> save your key for every folder on this machine\n\n' +
55
+ ` ${sky('Models')}\n${models}\n\n` +
56
+ ` Needs UCODE_API_KEY in the environment or in ${ENV_FILE}\n` +
57
+ ' Free keys: https://openrouter.ai/keys\n\n'
58
+ );
59
+ }
60
+
61
+ async function main() {
62
+ const args = parseArgs(process.argv.slice(2));
63
+
64
+ if (args.help) return usage();
65
+
66
+ if (process.argv[2] === 'login') {
67
+ const { saveKey } = await import('./src/core/login.js');
68
+ process.stdout.write(`${await saveKey(process.argv[3])}
69
+ `);
70
+ return;
71
+ }
72
+
73
+ if (process.argv[2] === 'doctor') {
74
+ const { runDoctor } = await import('./src/core/doctor.js');
75
+ process.stdout.write(`${(await runDoctor()).join('\n')}\n`);
76
+ return;
77
+ }
78
+
79
+ if (args.version) {
80
+ process.stdout.write(`${VERSION}\n`);
81
+ return;
82
+ }
83
+
84
+ if (args.model) {
85
+ try {
86
+ setModel(args.model);
87
+ } catch (err) {
88
+ new Plain({ cwd: args.cwd }).error(err);
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+ }
93
+
94
+ const agent = new Agent({ cwd: args.cwd, debug: args.debug });
95
+ if (args.plan) agent.ui.mode = 'plan';
96
+
97
+ try {
98
+ await agent.start();
99
+ } catch (err) {
100
+ agent.ui.error(err, { debug: args.debug });
101
+ await agent.persist().catch(() => {});
102
+ agent.ui.close();
103
+ process.exitCode = 1;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Was this file launched, or imported?
109
+ *
110
+ * Importing it which another front end would do to reuse Agent — must not
111
+ * open a terminal session. The obvious check is comparing `import.meta.url`
112
+ * with argv[1], and it is wrong: after `npm link`, argv[1] arrives as the path
113
+ * through the symlink in the global node_modules while `import.meta.url` has
114
+ * already been resolved to the real file. The two never match, so the command
115
+ * starts, matches nothing, and exits successfully having done absolutely
116
+ * nothing — which is a great deal harder to diagnose than a crash.
117
+ *
118
+ * Comparing real paths is what actually answers the question.
119
+ */
120
+ function launchedDirectly() {
121
+ const entry = process.argv[1];
122
+ if (!entry) return false;
123
+
124
+ const self = fileURLToPath(import.meta.url);
125
+ try {
126
+ return realpathSync(entry) === realpathSync(self);
127
+ } catch {
128
+ return pathToFileURL(entry).href === import.meta.url;
129
+ }
130
+ }
131
+
132
+ if (launchedDirectly()) main();