chels-code 0.0.0-stage → 0.5.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,370 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import test from "node:test";
7
+ import { render } from "ink-testing-library";
8
+ import { ActionConfirmation } from "../src/ui/ConfirmPanel.js";
9
+ import { App } from "../src/ui/App.js";
10
+ import { TextArea } from "../src/ui/TextArea.js";
11
+ import { sanitize } from "../src/ui/theme.js";
12
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+ function output(app) {
14
+ return app.frames.map((frame) => sanitize(frame)).join("\n");
15
+ }
16
+ async function waitFor(app, predicate, timeout = 4_000) {
17
+ const startedAt = Date.now();
18
+ while (Date.now() - startedAt < timeout) {
19
+ if (predicate(output(app)))
20
+ return;
21
+ await delay(20);
22
+ }
23
+ throw new Error(`Timed out waiting for UI output.\n${output(app).slice(-4_000)}`);
24
+ }
25
+ async function waitForFrame(app, predicate, timeout = 4_000) {
26
+ const startedAt = Date.now();
27
+ while (Date.now() - startedAt < timeout) {
28
+ if (predicate(sanitize(app.lastFrame() ?? "")))
29
+ return;
30
+ await delay(20);
31
+ }
32
+ throw new Error(`Timed out waiting for the current frame.\n${sanitize(app.lastFrame() ?? "")}`);
33
+ }
34
+ async function waitForReady(app, timeout = 4_000) {
35
+ const startedAt = Date.now();
36
+ while (Date.now() - startedAt < timeout) {
37
+ const frame = sanitize(app.lastFrame() ?? "");
38
+ if (frame.includes("Setup failed"))
39
+ return;
40
+ if (frame.includes("Ask anything") && !frame.includes("Thinking"))
41
+ return;
42
+ await delay(20);
43
+ }
44
+ throw new Error(`Timed out waiting for the input to become ready.\n${output(app).slice(-4_000)}`);
45
+ }
46
+ test("text area supports multiline input, cursor movement, and enter to send", async () => {
47
+ let submitted = null;
48
+ const app = render(_jsx(TextArea, { onSubmit: (text) => { submitted = text; }, columns: 60, rows: 24 }));
49
+ try {
50
+ await delay(20);
51
+ app.stdin.write("hello");
52
+ await delay(20);
53
+ app.stdin.write("\u001b[13;2u");
54
+ await delay(20);
55
+ app.stdin.write("world");
56
+ await delay(20);
57
+ app.stdin.write("\u001b[A");
58
+ await delay(20);
59
+ app.stdin.write("!");
60
+ await delay(20);
61
+ app.stdin.write("\r");
62
+ await delay(30);
63
+ assert.equal(submitted, "hello!\nworld");
64
+ assert.match(sanitize(app.lastFrame() ?? ""), /Ask anything/);
65
+ }
66
+ finally {
67
+ app.unmount();
68
+ }
69
+ });
70
+ test("command confirmation resolves approval with y", async () => {
71
+ let resolved = null;
72
+ const app = render(_jsx(ActionConfirmation, { action: { kind: "command", command: "npm test" }, onResolve: (approved) => { resolved = approved; } }));
73
+ try {
74
+ await delay(20);
75
+ assert.match(sanitize(app.lastFrame() ?? ""), /Run this PowerShell command/);
76
+ app.stdin.write("y");
77
+ await delay(20);
78
+ assert.equal(resolved, true);
79
+ }
80
+ finally {
81
+ app.unmount();
82
+ }
83
+ });
84
+ test("runs a mocked minimal session with plan approval and write approval", async (t) => {
85
+ const originalFetch = globalThis.fetch;
86
+ const workspace = await mkdtemp(path.join(os.tmpdir(), "chels-ui-"));
87
+ let writeIssued = false;
88
+ const jsonResponse = (payload) => new Response(JSON.stringify(payload), {
89
+ status: 200,
90
+ headers: { "Content-Type": "application/json" },
91
+ });
92
+ globalThis.fetch = (async (input, init) => {
93
+ const url = new URL(String(input));
94
+ const host = url.hostname;
95
+ if (url.pathname.endsWith("/models")) {
96
+ const model = host === "openrouter.ai"
97
+ ? {
98
+ id: "qwen/qwen3-coder:free",
99
+ pricing: { prompt: 0, completion: 0 },
100
+ supported_parameters: ["tools"],
101
+ context_length: 128_000,
102
+ }
103
+ : host === "integrate.api.nvidia.com"
104
+ ? {
105
+ id: "meta/llama-3.3-70b-instruct",
106
+ supported_parameters: ["tools"],
107
+ context_length: 128_000,
108
+ }
109
+ : {
110
+ id: "llama-3.3-70b-versatile",
111
+ supported_parameters: ["tools"],
112
+ context_length: 128_000,
113
+ };
114
+ return jsonResponse({ data: [model] });
115
+ }
116
+ if (url.pathname.endsWith("/key")) {
117
+ return jsonResponse({ data: { limit: 5, limit_remaining: 5, usage: 0 } });
118
+ }
119
+ const body = JSON.parse(String(init?.body));
120
+ const system = body.messages.find((message) => message.role === "system")?.content ?? "";
121
+ await delay(60);
122
+ if (system.includes("Combine three AI work breakdowns")) {
123
+ return jsonResponse({
124
+ choices: [{
125
+ message: {
126
+ role: "assistant",
127
+ content: JSON.stringify({
128
+ tasks: [{
129
+ title: "Write greeting",
130
+ instructions: "Create hello.txt with a short greeting.",
131
+ provider: "openrouter",
132
+ }],
133
+ }),
134
+ },
135
+ }],
136
+ });
137
+ }
138
+ if (system.includes("final reporting agent")) {
139
+ return jsonResponse({
140
+ choices: [{
141
+ message: {
142
+ role: "assistant",
143
+ content: "Final: created hello.txt.",
144
+ },
145
+ }],
146
+ });
147
+ }
148
+ if (system.includes("working on an approved subtask")) {
149
+ if (!writeIssued) {
150
+ writeIssued = true;
151
+ return jsonResponse({
152
+ choices: [{
153
+ message: {
154
+ role: "assistant",
155
+ content: null,
156
+ tool_calls: [{
157
+ id: "call_write",
158
+ type: "function",
159
+ function: {
160
+ name: "write_file",
161
+ arguments: JSON.stringify({
162
+ path: "hello.txt",
163
+ content: "hello from ChelsCode\n",
164
+ }),
165
+ },
166
+ }],
167
+ },
168
+ }],
169
+ });
170
+ }
171
+ return jsonResponse({
172
+ choices: [{
173
+ message: {
174
+ role: "assistant",
175
+ content: "All done. hello.txt was written.",
176
+ },
177
+ }],
178
+ });
179
+ }
180
+ return jsonResponse({
181
+ choices: [{
182
+ message: {
183
+ role: "assistant",
184
+ content: `Proposal from ${host}.`,
185
+ },
186
+ }],
187
+ });
188
+ });
189
+ const app = render(_jsx(App, { apiKeys: { openrouter: "test", nvidia: "test", groq: "test" }, workspace: workspace, version: "0.5.0" }));
190
+ t.after(async () => {
191
+ globalThis.fetch = originalFetch;
192
+ app.unmount();
193
+ await rm(workspace, { recursive: true, force: true });
194
+ });
195
+ await waitForReady(app);
196
+ const initial = output(app);
197
+ assert.ok(initial.includes("I like cats"), "the remaining tagline should be visible");
198
+ assert.ok(initial.includes("██████╗"), "the start screen should show the big ChelsCode wordmark");
199
+ assert.ok(initial.includes("Build · MystBloom V1.0 Free ChelsCode · max"), "the chat should show the build, model, plan, and effort status line");
200
+ assert.ok(initial.includes("MystBloom V1.0"), "the assistant name should be visible");
201
+ assert.match(initial, /0 \(0%\) shift\+tab mode ctrl\+p commands/, "the footer should start at zero usage");
202
+ assert.ok(!initial.includes("enter send"), "the old hint row should be gone");
203
+ assert.ok(!initial.includes("Three AIs. One coding team."), "the longer tagline should be gone");
204
+ assert.ok(!initial.includes("Connected · 3 model providers ready."), "the connected notice should be gone");
205
+ assert.doesNotMatch(initial, /●\s*OpenRouter\s+●\s*NVIDIA/, "the provider chip row should be gone");
206
+ assert.match(initial, /Ask anything/);
207
+ app.stdin.write("Create a greeting file");
208
+ await delay(30);
209
+ app.stdin.write("\r");
210
+ await waitFor(app, (text) => text.includes("Proceed with this plan?"));
211
+ assert.match(output(app), /MystBloom V1\.0/, "the plan should show the assistant name");
212
+ await delay(30);
213
+ app.stdin.write("y");
214
+ await waitFor(app, (text) => text.includes("Write this file?"));
215
+ await delay(30);
216
+ app.stdin.write("y");
217
+ await waitFor(app, (text) => text.includes("◆ Done")
218
+ && text.includes("Final: created hello.txt.")
219
+ && text.includes("Made"));
220
+ const finalOutput = output(app);
221
+ assert.match(finalOutput, /> Create a greeting file/, "the user message should use the > prefix");
222
+ assert.match(finalOutput, /[\u2800-\u28FF] Thinking/, "the live status should animate while working");
223
+ assert.match(finalOutput, /◆ Done/, "the response should finish with Done");
224
+ assert.match(finalOutput, /\+\s*hello\.txt/, "Done should list the file that was made");
225
+ assert.ok(finalOutput.includes("Final: created hello.txt."), "one AI should write the final answer");
226
+ assert.ok(!finalOutput.includes("All done. hello.txt was written."), "intermediate subtask chatter should stay hidden");
227
+ assert.ok(!finalOutput.includes("Subtask"), "subtask headers should stay hidden");
228
+ assert.ok(!finalOutput.includes("◆ You"), "the old You heading should be gone");
229
+ assert.doesNotMatch(finalOutput, /●\s*OpenRouter\s+·/, "provider/model answer headers should be gone");
230
+ assert.doesNotMatch(finalOutput, /●\s*NVIDIA/, "provider attribution should stay hidden");
231
+ assert.equal(await readFile(path.join(workspace, "hello.txt"), "utf8"), "hello from ChelsCode\n");
232
+ });
233
+ test("answers a simple question with one AI instead of the three-AI plan", async (t) => {
234
+ const originalFetch = globalThis.fetch;
235
+ const workspace = await mkdtemp(path.join(os.tmpdir(), "chels-question-"));
236
+ let completionCalls = 0;
237
+ let visibleTools = [];
238
+ const jsonResponse = (payload) => new Response(JSON.stringify(payload), {
239
+ status: 200,
240
+ headers: { "Content-Type": "application/json" },
241
+ });
242
+ globalThis.fetch = (async (input, init) => {
243
+ const url = new URL(String(input));
244
+ if (url.pathname.endsWith("/models")) {
245
+ const model = url.hostname === "openrouter.ai"
246
+ ? {
247
+ id: "qwen/qwen3-coder:free",
248
+ pricing: { prompt: 0, completion: 0 },
249
+ supported_parameters: ["tools"],
250
+ context_length: 128_000,
251
+ }
252
+ : url.hostname === "integrate.api.nvidia.com"
253
+ ? {
254
+ id: "meta/llama-3.3-70b-instruct",
255
+ supported_parameters: ["tools"],
256
+ context_length: 128_000,
257
+ }
258
+ : {
259
+ id: "llama-3.3-70b-versatile",
260
+ supported_parameters: ["tools"],
261
+ context_length: 128_000,
262
+ };
263
+ return jsonResponse({ data: [model] });
264
+ }
265
+ if (url.pathname.endsWith("/key")) {
266
+ return jsonResponse({ data: { limit: 5, limit_remaining: 5, usage: 0 } });
267
+ }
268
+ completionCalls += 1;
269
+ const body = JSON.parse(String(init?.body));
270
+ visibleTools = body.tools?.map((tool) => tool.function.name) ?? [];
271
+ await delay(60);
272
+ return jsonResponse({
273
+ choices: [{
274
+ message: {
275
+ role: "assistant",
276
+ content: "Hi! Ask me anything about the workspace.",
277
+ },
278
+ }],
279
+ usage: { prompt_tokens: 1_000, completion_tokens: 200, total_tokens: 1_200, cost: 0.5 },
280
+ });
281
+ });
282
+ const app = render(_jsx(App, { apiKeys: { openrouter: "test", nvidia: "test", groq: "test" }, workspace: workspace, version: "0.5.0" }));
283
+ t.after(async () => {
284
+ globalThis.fetch = originalFetch;
285
+ app.unmount();
286
+ await rm(workspace, { recursive: true, force: true });
287
+ });
288
+ await waitForReady(app);
289
+ const initial = output(app);
290
+ assert.ok(initial.includes("Build · MystBloom V1.0 Free ChelsCode · max"), "the chat should show the build, model, plan, and effort status line");
291
+ assert.ok(initial.includes("MystBloom V1.0"), "the assistant name should be visible");
292
+ assert.match(initial, /0 \(0%\) shift\+tab mode ctrl\+p commands/, "the footer should start at zero usage");
293
+ assert.ok(!initial.includes("enter send"), "the old hint row should be gone");
294
+ app.stdin.write("hi");
295
+ await delay(30);
296
+ app.stdin.write("\r");
297
+ await waitFor(app, (text) => text.includes("Hi! Ask me anything about the workspace."));
298
+ await waitFor(app, (text) => text.includes("1.2K (10%)"));
299
+ const text = output(app);
300
+ assert.match(text, /1\.2K \(10%\)/, "the footer should show session tokens and credit usage");
301
+ assert.match(text, /1\.2K \(10%\) shift\+tab mode ctrl\+p commands/, "the token footer should show both shortcuts and not include the model name");
302
+ assert.match(text, /> hi/, "the user message should use the > prefix");
303
+ assert.match(text, /[\u2800-\u28FF] Thinking/, "the live status should animate while working");
304
+ assert.match(text, /◆ Done/, "the answer should finish with Done");
305
+ assert.ok(text.includes("Hi! Ask me anything about the workspace."), "the one AI answer should still be shown");
306
+ assert.ok(!text.includes("Made"), "a question should not claim anything was made");
307
+ assert.ok(!text.includes("● OpenRouter"), "provider/model answer headers should be gone");
308
+ assert.ok(!text.includes("Analysis"), "a question should not trigger the three-AI analysis");
309
+ assert.ok(!text.includes("Proceed with this plan?"), "a question should not ask for plan approval");
310
+ assert.ok(!text.includes("perspectives received"), "a question should not show three-way progress");
311
+ assert.equal(completionCalls, 1, "only one AI should answer a question");
312
+ assert.deepEqual(visibleTools, ["list_files", "read_file", "get_current_datetime", "get_weather", "web_search", "get_news"], "a question should get read-only tools plus current-info lookups");
313
+ app.stdin.write("\u0010");
314
+ await waitForFrame(app, (text) => text.includes("Commands"));
315
+ const menu = sanitize(app.lastFrame() ?? "");
316
+ assert.match(menu, /Mode/);
317
+ assert.match(menu, /Effort/);
318
+ assert.match(menu, /Exit/);
319
+ assert.match(menu, /Help/);
320
+ await delay(30);
321
+ app.stdin.write("\u001b[B");
322
+ await delay(30);
323
+ app.stdin.write("\u001b[B");
324
+ await delay(30);
325
+ app.stdin.write("\r");
326
+ await waitFor(app, (text) => text.includes("enter send · shift+enter newline"));
327
+ app.stdin.write("\u0010");
328
+ await waitForFrame(app, (text) => text.includes("Commands"));
329
+ await delay(30);
330
+ app.stdin.write("\u001b[B");
331
+ await delay(30);
332
+ app.stdin.write("\r");
333
+ await waitForFrame(app, (text) => text.includes("enter apply"));
334
+ const effortMenu = sanitize(app.lastFrame() ?? "");
335
+ assert.match(effortMenu, /low/);
336
+ assert.match(effortMenu, /medium/);
337
+ assert.match(effortMenu, /high/);
338
+ assert.match(effortMenu, /max/);
339
+ assert.match(effortMenu, /ultra/);
340
+ assert.match(effortMenu, /max current/, "max should be the default effort");
341
+ await delay(30);
342
+ app.stdin.write("\u001b[B");
343
+ await delay(30);
344
+ app.stdin.write("\r");
345
+ await waitFor(app, (text) => text.includes("Build · MystBloom V1.0 Free ChelsCode · ultra"));
346
+ app.stdin.write("\u0010");
347
+ await waitForFrame(app, (text) => text.includes("Commands"));
348
+ await delay(30);
349
+ app.stdin.write("\r");
350
+ await waitForFrame(app, (text) => text.includes("enter switch"));
351
+ const modeMenu = sanitize(app.lastFrame() ?? "");
352
+ assert.match(modeMenu, /Build/);
353
+ assert.match(modeMenu, /Plan/);
354
+ assert.match(modeMenu, /Build current/, "Build should be the default mode");
355
+ await delay(30);
356
+ app.stdin.write("\u001b[B");
357
+ await delay(30);
358
+ app.stdin.write("\r");
359
+ await waitFor(app, (text) => text.includes("Plan · MystBloom V1.0 Free ChelsCode · ultra"));
360
+ assert.ok(sanitize(app.lastFrame() ?? "").includes("Describe what you want to plan…"), "the mode menu should update the input prompt");
361
+ app.stdin.write("\u001b[Z");
362
+ await waitForFrame(app, (text) => text.includes("Build · MystBloom V1.0 Free ChelsCode · ultra"));
363
+ assert.ok(sanitize(app.lastFrame() ?? "").includes("Describe what you want to build…"), "Shift+Tab should switch back to Build without inserting spaces");
364
+ app.stdin.write("\u0014");
365
+ await waitForFrame(app, (text) => text.includes("Plan · MystBloom V1.0 Free ChelsCode · ultra"));
366
+ assert.ok(sanitize(app.lastFrame() ?? "").includes("Describe what you want to plan…"), "Ctrl+T should switch modes when the terminal intercepts Shift+Tab");
367
+ app.stdin.write("\t");
368
+ await waitForFrame(app, (text) => text.includes("Build · MystBloom V1.0 Free ChelsCode · ultra"));
369
+ assert.ok(sanitize(app.lastFrame() ?? "").includes("Describe what you want to build…"), "a plain Tab should toggle the mode instead of inserting whitespace");
370
+ });
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "chels-code",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.5.0",
4
+ "description": "A TypeScript + Ink terminal coding assistant for OpenRouter, NVIDIA, and Groq",
5
+ "type": "module",
6
+ "bin": {
7
+ "chelscode": "dist/src/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json",
14
+ "dev": "tsx src/cli.tsx",
15
+ "start": "npm run build && node dist/src/cli.js",
16
+ "test": "node --import tsx --test \"test/*.test.ts\" \"test/*.test.tsx\""
17
+ },
18
+ "engines": {
19
+ "node": ">=22"
20
+ },
21
+ "dependencies": {
22
+ "ink": "^8.0.0",
23
+ "react": "^19.3.1"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^26.6.6",
27
+ "@types/react": "^19.3.0",
28
+ "ink-testing-library": "^4.0.0",
29
+ "tsx": "^4.23.15",
30
+ "typescript": "^7.0.2"
31
+ }
32
+ }