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.
- package/README.md +123 -2
- package/dist/src/cli-options.js +30 -0
- package/dist/src/cli.js +142 -0
- package/dist/src/engine.js +453 -0
- package/dist/src/providers.js +250 -0
- package/dist/src/safety.js +48 -0
- package/dist/src/session.js +188 -0
- package/dist/src/terminal-title.js +41 -0
- package/dist/src/types.js +4 -0
- package/dist/src/ui/App.js +252 -0
- package/dist/src/ui/CommandPalette.js +76 -0
- package/dist/src/ui/ConfirmPanel.js +36 -0
- package/dist/src/ui/Spinner.js +21 -0
- package/dist/src/ui/TextArea.js +243 -0
- package/dist/src/ui/Transcript.js +67 -0
- package/dist/src/ui/theme.js +130 -0
- package/dist/src/ui/useTerminalSize.js +22 -0
- package/dist/src/version.js +1 -0
- package/dist/src/workspace-tools.js +405 -0
- package/dist/test/cli-options.test.js +22 -0
- package/dist/test/intent.test.js +48 -0
- package/dist/test/providers.test.js +104 -0
- package/dist/test/resume.test.js +96 -0
- package/dist/test/safety.test.js +44 -0
- package/dist/test/session.test.js +83 -0
- package/dist/test/title.test.js +73 -0
- package/dist/test/tools.test.js +168 -0
- package/dist/test/ui.test.js +370 -0
- package/package.json +30 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { classifyRequest } from "../src/engine.js";
|
|
4
|
+
test("routes questions and read-only requests to one AI", () => {
|
|
5
|
+
for (const request of [
|
|
6
|
+
"hi",
|
|
7
|
+
"thanks",
|
|
8
|
+
"what files are in this project?",
|
|
9
|
+
"how does the text area work?",
|
|
10
|
+
"can you explain the build errors?",
|
|
11
|
+
"list the files",
|
|
12
|
+
"read package.json",
|
|
13
|
+
"review this code",
|
|
14
|
+
"show me the main entry point",
|
|
15
|
+
"the build failed, why?",
|
|
16
|
+
]) {
|
|
17
|
+
assert.equal(classifyRequest(request), "question", request);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
test("routes workspace changes and commands to the three-AI team", () => {
|
|
21
|
+
for (const request of [
|
|
22
|
+
"create hello.txt",
|
|
23
|
+
"fix the failing test",
|
|
24
|
+
"can you build a todo app?",
|
|
25
|
+
"can you help me build a CLI?",
|
|
26
|
+
"please update the README",
|
|
27
|
+
"hey, fix this",
|
|
28
|
+
"run the tests",
|
|
29
|
+
"refactor the session store",
|
|
30
|
+
"add a theme toggle",
|
|
31
|
+
"commit the changes",
|
|
32
|
+
]) {
|
|
33
|
+
assert.equal(classifyRequest(request), "task", request);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
test("routes current-info questions to the single AI", () => {
|
|
37
|
+
for (const request of [
|
|
38
|
+
"what's the date today?",
|
|
39
|
+
"what time is it now?",
|
|
40
|
+
"what's the weather like in Chicago?",
|
|
41
|
+
"what's the weather in Tokyo?",
|
|
42
|
+
"what's the news today?",
|
|
43
|
+
"do you have the latest weather for Paris?",
|
|
44
|
+
"search the web for the latest AI news",
|
|
45
|
+
]) {
|
|
46
|
+
assert.equal(classifyRequest(request), "question", request);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createChatCompletion, discoverModels, fetchAvailableCredits, selectReliableModel, } from "../src/providers.js";
|
|
4
|
+
test("discovers models and marks only explicitly zero-priced models free", async (t) => {
|
|
5
|
+
const originalFetch = globalThis.fetch;
|
|
6
|
+
t.after(() => { globalThis.fetch = originalFetch; });
|
|
7
|
+
let requestedUrl;
|
|
8
|
+
globalThis.fetch = (async (url) => {
|
|
9
|
+
requestedUrl = String(url);
|
|
10
|
+
return new Response(JSON.stringify({
|
|
11
|
+
data: [
|
|
12
|
+
{ id: "paid/model", pricing: { prompt: "0.01", completion: "0.02" } },
|
|
13
|
+
{ id: "free/model:free" },
|
|
14
|
+
{ id: "zero/model", pricing: { prompt: "0", completion: "0" } },
|
|
15
|
+
{ id: "unknown/model", pricing: { prompt: null, completion: "0" } },
|
|
16
|
+
{ name: "missing-id" },
|
|
17
|
+
],
|
|
18
|
+
}));
|
|
19
|
+
});
|
|
20
|
+
const models = await discoverModels("openrouter", "test-key");
|
|
21
|
+
assert.equal(requestedUrl, "https://openrouter.ai/api/v1/models");
|
|
22
|
+
assert.deepEqual(models.map((model) => [model.id, model.free]), [
|
|
23
|
+
["free/model:free", true],
|
|
24
|
+
["zero/model", true],
|
|
25
|
+
["paid/model", false],
|
|
26
|
+
["unknown/model", false],
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
test("creates chat completions with tools only when provided", async (t) => {
|
|
30
|
+
const originalFetch = globalThis.fetch;
|
|
31
|
+
t.after(() => { globalThis.fetch = originalFetch; });
|
|
32
|
+
let requestOptions;
|
|
33
|
+
globalThis.fetch = (async (_url, options) => {
|
|
34
|
+
requestOptions = options;
|
|
35
|
+
return new Response(JSON.stringify({
|
|
36
|
+
choices: [{ message: { content: "ok" } }],
|
|
37
|
+
usage: { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120, cost: 0.0012 },
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
40
|
+
const result = await createChatCompletion({
|
|
41
|
+
provider: "groq",
|
|
42
|
+
apiKey: "test-key",
|
|
43
|
+
model: "model-id",
|
|
44
|
+
messages: [{ role: "user", content: "hello" }],
|
|
45
|
+
});
|
|
46
|
+
assert.equal(result.message.content, "ok");
|
|
47
|
+
assert.deepEqual(result.usage, {
|
|
48
|
+
promptTokens: 100,
|
|
49
|
+
completionTokens: 20,
|
|
50
|
+
totalTokens: 120,
|
|
51
|
+
cost: 0.0012,
|
|
52
|
+
});
|
|
53
|
+
const body = JSON.parse(String(requestOptions?.body));
|
|
54
|
+
assert.equal(body.model, "model-id");
|
|
55
|
+
assert.equal("tools" in body, false);
|
|
56
|
+
assert.equal(new Headers(requestOptions?.headers).get("Authorization"), "Bearer test-key");
|
|
57
|
+
});
|
|
58
|
+
test("automatically prefers free models that support tools", () => {
|
|
59
|
+
const selected = selectReliableModel("openrouter", [
|
|
60
|
+
{ id: "paid/llama-3.3-70b-instruct", name: "paid", free: false, supportsTools: true, contextLength: 128_000 },
|
|
61
|
+
{ id: "free/small-chat:free", name: "small", free: true, supportsTools: false, contextLength: 8_000 },
|
|
62
|
+
{ id: "free/qwen-coder:free", name: "qwen", free: true, supportsTools: true, contextLength: 32_000 },
|
|
63
|
+
]);
|
|
64
|
+
assert.equal(selected.id, "free/qwen-coder:free");
|
|
65
|
+
});
|
|
66
|
+
test("automatically falls back to the strongest stable available chat model", () => {
|
|
67
|
+
const selected = selectReliableModel("groq", [
|
|
68
|
+
{ id: "audio/whisper-large", name: "whisper", free: false, contextLength: 0 },
|
|
69
|
+
{ id: "llama-3.1-8b-instant", name: "instant", free: false, supportsTools: true, contextLength: 128_000 },
|
|
70
|
+
{ id: "llama-3.3-70b-versatile", name: "versatile", free: false, supportsTools: true, contextLength: 128_000 },
|
|
71
|
+
]);
|
|
72
|
+
assert.equal(selected.id, "llama-3.3-70b-versatile");
|
|
73
|
+
});
|
|
74
|
+
test("reads the available OpenRouter credit balance", async (t) => {
|
|
75
|
+
const originalFetch = globalThis.fetch;
|
|
76
|
+
t.after(() => { globalThis.fetch = originalFetch; });
|
|
77
|
+
let requestedUrl;
|
|
78
|
+
globalThis.fetch = (async (url) => {
|
|
79
|
+
requestedUrl = String(url);
|
|
80
|
+
return new Response(JSON.stringify({
|
|
81
|
+
data: { limit: 10, limit_remaining: 6.5, usage: 3.5 },
|
|
82
|
+
}));
|
|
83
|
+
});
|
|
84
|
+
const credits = await fetchAvailableCredits("test-key");
|
|
85
|
+
assert.equal(requestedUrl, "https://openrouter.ai/api/v1/key");
|
|
86
|
+
assert.deepEqual(credits, { remaining: 6.5, source: "key" });
|
|
87
|
+
});
|
|
88
|
+
test("falls back to account credits when the key has no credit limit", async (t) => {
|
|
89
|
+
const originalFetch = globalThis.fetch;
|
|
90
|
+
t.after(() => { globalThis.fetch = originalFetch; });
|
|
91
|
+
globalThis.fetch = (async (input) => {
|
|
92
|
+
const url = String(input);
|
|
93
|
+
if (url.endsWith("/key")) {
|
|
94
|
+
return new Response(JSON.stringify({
|
|
95
|
+
data: { limit: null, limit_remaining: null, usage: 2 },
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
return new Response(JSON.stringify({
|
|
99
|
+
data: { total_credits: 10, total_usage: 2 },
|
|
100
|
+
}));
|
|
101
|
+
});
|
|
102
|
+
const credits = await fetchAvailableCredits("test-key");
|
|
103
|
+
assert.deepEqual(credits, { remaining: 8, source: "account" });
|
|
104
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
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 { App } from "../src/ui/App.js";
|
|
9
|
+
import { sanitize } from "../src/ui/theme.js";
|
|
10
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
const json = (payload) => new Response(JSON.stringify(payload), {
|
|
12
|
+
headers: { "Content-Type": "application/json" },
|
|
13
|
+
});
|
|
14
|
+
async function waitFor(app, predicate, timeout = 4_000) {
|
|
15
|
+
const startedAt = Date.now();
|
|
16
|
+
while (Date.now() - startedAt < timeout) {
|
|
17
|
+
const text = sanitize(app.lastFrame() ?? "");
|
|
18
|
+
if (predicate(text))
|
|
19
|
+
return;
|
|
20
|
+
await delay(20);
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Timed out waiting for the resumed session.\n${sanitize(app.lastFrame() ?? "")}`);
|
|
23
|
+
}
|
|
24
|
+
async function waitForSavedSession(file) {
|
|
25
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
await delay(20);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`The session was not saved to ${file}.`);
|
|
34
|
+
}
|
|
35
|
+
test("resumes a saved session with its transcript, mode, effort, and usage", async (t) => {
|
|
36
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "chels-resume-"));
|
|
37
|
+
const sessionsDir = await mkdtemp(path.join(os.tmpdir(), "chels-resume-store-"));
|
|
38
|
+
const originalFetch = globalThis.fetch;
|
|
39
|
+
globalThis.fetch = (async (input) => {
|
|
40
|
+
const url = new URL(String(input));
|
|
41
|
+
if (url.pathname.endsWith("/models")) {
|
|
42
|
+
return json({
|
|
43
|
+
data: [{
|
|
44
|
+
id: "qwen/qwen3-coder:free",
|
|
45
|
+
pricing: { prompt: 0, completion: 0 },
|
|
46
|
+
supported_parameters: ["tools"],
|
|
47
|
+
context_length: 200_000,
|
|
48
|
+
}],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (url.pathname.endsWith("/key")) {
|
|
52
|
+
return json({ data: { limit: 5, limit_remaining: 5, usage: 0 } });
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Unexpected request: ${url}`);
|
|
55
|
+
});
|
|
56
|
+
const resume = {
|
|
57
|
+
schema: 1,
|
|
58
|
+
id: "abc12345",
|
|
59
|
+
workspace,
|
|
60
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
61
|
+
updatedAt: "2026-01-02T00:00:00.000Z",
|
|
62
|
+
mode: "plan",
|
|
63
|
+
effort: "ultra",
|
|
64
|
+
tokensUsed: 1_234,
|
|
65
|
+
creditsUsed: 0.5,
|
|
66
|
+
creditsAvailable: 5,
|
|
67
|
+
entries: [
|
|
68
|
+
{ id: 1, kind: "user", text: "previous request" },
|
|
69
|
+
{
|
|
70
|
+
id: 2,
|
|
71
|
+
kind: "done",
|
|
72
|
+
text: "previous answer",
|
|
73
|
+
made: [{ kind: "file", path: "page.html" }],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
const app = render(_jsx(App, { apiKeys: { openrouter: "test", nvidia: "test", groq: "test" }, workspace: workspace, version: "0.5.0", sessionId: resume.id, resume: resume, sessionsDir: sessionsDir }));
|
|
78
|
+
t.after(async () => {
|
|
79
|
+
globalThis.fetch = originalFetch;
|
|
80
|
+
app.unmount();
|
|
81
|
+
await rm(workspace, { recursive: true, force: true });
|
|
82
|
+
await rm(sessionsDir, { recursive: true, force: true });
|
|
83
|
+
});
|
|
84
|
+
await waitFor(app, (text) => text.includes("Ask anything") && !text.includes("Thinking"));
|
|
85
|
+
const frame = sanitize(app.lastFrame() ?? "");
|
|
86
|
+
assert.match(frame, /Resumed session abc12345/);
|
|
87
|
+
assert.match(frame, /> previous request/);
|
|
88
|
+
assert.match(frame, /previous answer/);
|
|
89
|
+
assert.match(frame, /Plan · MystBloom V1\.0 Free ChelsCode · ultra/);
|
|
90
|
+
assert.match(frame, /1\.2K \(10%\)/);
|
|
91
|
+
const saved = await waitForSavedSession(path.join(sessionsDir, `${resume.id}.json`));
|
|
92
|
+
assert.equal(saved.mode, "plan");
|
|
93
|
+
assert.equal(saved.effort, "ultra");
|
|
94
|
+
assert.equal(saved.tokensUsed, 1_234);
|
|
95
|
+
assert.deepEqual(saved.entries.map((entry) => entry.kind), ["user", "done"], "the saved transcript should keep prior messages without a duplicate header");
|
|
96
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { isSensitivePath, resolveWorkspacePath } from "../src/safety.js";
|
|
7
|
+
test("blocks paths that escape the workspace", async () => {
|
|
8
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "chels-code-"));
|
|
9
|
+
try {
|
|
10
|
+
await assert.rejects(resolveWorkspacePath(workspace, "..\\outside.txt"), /inside the workspace/);
|
|
11
|
+
await assert.rejects(resolveWorkspacePath(workspace, "C:\\outside.txt"), /Absolute paths/);
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
await rm(workspace, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
test("blocks secret-like paths but allows the example env file", () => {
|
|
18
|
+
assert.equal(isSensitivePath(".env"), true);
|
|
19
|
+
assert.equal(isSensitivePath("config/.env.local"), true);
|
|
20
|
+
assert.equal(isSensitivePath("keys/private-key.pem"), true);
|
|
21
|
+
assert.equal(isSensitivePath(".env.example"), false);
|
|
22
|
+
});
|
|
23
|
+
test("blocks symlinks that resolve outside the workspace", async (t) => {
|
|
24
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "chels-code-"));
|
|
25
|
+
const outside = await mkdtemp(path.join(os.tmpdir(), "chels-outside-"));
|
|
26
|
+
try {
|
|
27
|
+
await writeFile(path.join(outside, "data.txt"), "outside the workspace");
|
|
28
|
+
try {
|
|
29
|
+
await symlink(outside, path.join(workspace, "linked"), "junction");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code === "EPERM") {
|
|
33
|
+
t.skip("Creating symlinks is not permitted in this environment.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
await assert.rejects(resolveWorkspacePath(workspace, "linked\\data.txt", { mustExist: true }), /outside the workspace/);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await rm(workspace, { recursive: true, force: true });
|
|
42
|
+
await rm(outside, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { createSessionId, findLatestSession, loadSession, resumeHint, sameWorkspace, saveSession, } from "../src/session.js";
|
|
7
|
+
function snapshot(overrides = {}) {
|
|
8
|
+
return {
|
|
9
|
+
schema: 1,
|
|
10
|
+
id: "abc12345",
|
|
11
|
+
workspace: process.cwd(),
|
|
12
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
13
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
14
|
+
mode: "plan",
|
|
15
|
+
effort: "ultra",
|
|
16
|
+
tokensUsed: 1_234,
|
|
17
|
+
creditsUsed: 0.5,
|
|
18
|
+
creditsAvailable: 5,
|
|
19
|
+
entries: [
|
|
20
|
+
{ id: 1, kind: "user", text: "make a page" },
|
|
21
|
+
{ id: 2, kind: "done", text: "Made the page.", made: [{ kind: "file", path: "page.html" }] },
|
|
22
|
+
],
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
test("saves, replaces, and loads a session", async (t) => {
|
|
27
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "chels-session-"));
|
|
28
|
+
t.after(async () => { await rm(directory, { recursive: true, force: true }); });
|
|
29
|
+
const first = snapshot();
|
|
30
|
+
saveSession(first, directory);
|
|
31
|
+
assert.deepEqual(loadSession(first.id, directory), first);
|
|
32
|
+
const second = snapshot({ updatedAt: "2026-01-02T00:00:00.000Z" });
|
|
33
|
+
second.entries = [{ id: 1, kind: "note", text: "updated" }];
|
|
34
|
+
saveSession(second, directory);
|
|
35
|
+
assert.deepEqual(loadSession(first.id, directory), second);
|
|
36
|
+
});
|
|
37
|
+
test("finds the latest session for a workspace and skips bad files", async (t) => {
|
|
38
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "chels-session-"));
|
|
39
|
+
t.after(async () => { await rm(directory, { recursive: true, force: true }); });
|
|
40
|
+
const workspace = process.cwd();
|
|
41
|
+
saveSession(snapshot({
|
|
42
|
+
id: "older001",
|
|
43
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
44
|
+
}), directory);
|
|
45
|
+
saveSession(snapshot({
|
|
46
|
+
id: "newer002",
|
|
47
|
+
updatedAt: "2026-02-01T00:00:00.000Z",
|
|
48
|
+
}), directory);
|
|
49
|
+
saveSession(snapshot({
|
|
50
|
+
id: "other003",
|
|
51
|
+
workspace: path.join(workspace, "elsewhere"),
|
|
52
|
+
updatedAt: "2026-03-01T00:00:00.000Z",
|
|
53
|
+
}), directory);
|
|
54
|
+
await writeFile(path.join(directory, "broken04.json"), "{ nope", "utf8");
|
|
55
|
+
const latest = findLatestSession(workspace, directory);
|
|
56
|
+
assert.equal(latest?.id, "newer002");
|
|
57
|
+
assert.equal(findLatestSession(workspace, path.join(directory, "missing")), null);
|
|
58
|
+
});
|
|
59
|
+
test("rejects missing, malformed, and unsafe sessions", async (t) => {
|
|
60
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "chels-session-"));
|
|
61
|
+
t.after(async () => { await rm(directory, { recursive: true, force: true }); });
|
|
62
|
+
assert.throws(() => loadSession("../secret", directory), /Invalid session id/);
|
|
63
|
+
assert.throws(() => loadSession("missing1", directory), /was not found/);
|
|
64
|
+
await writeFile(path.join(directory, "broken01.json"), "{ nope", "utf8");
|
|
65
|
+
assert.throws(() => loadSession("broken01", directory), /not valid JSON/);
|
|
66
|
+
await writeFile(path.join(directory, "future01.json"), JSON.stringify({
|
|
67
|
+
...snapshot(),
|
|
68
|
+
id: "future01",
|
|
69
|
+
schema: 2,
|
|
70
|
+
}), "utf8");
|
|
71
|
+
assert.throws(() => loadSession("future01", directory), /unsupported format/);
|
|
72
|
+
});
|
|
73
|
+
test("creates readable session ids and the resume command", () => {
|
|
74
|
+
const id = createSessionId();
|
|
75
|
+
assert.match(id, /^[a-z0-9]{8}$/);
|
|
76
|
+
assert.notEqual(createSessionId(), id);
|
|
77
|
+
assert.equal(resumeHint(id), `Resume this session with:\n chelscode --id ${id}`);
|
|
78
|
+
});
|
|
79
|
+
test("compares workspaces case-insensitively on Windows", () => {
|
|
80
|
+
const workspace = path.join(process.cwd(), "Example");
|
|
81
|
+
assert.equal(sameWorkspace(workspace, workspace), true);
|
|
82
|
+
assert.equal(sameWorkspace(workspace, workspace.toUpperCase()), process.platform === "win32");
|
|
83
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { cleanTitle, installTerminalTitle, titleSequence, } from "../src/terminal-title.js";
|
|
4
|
+
function recordingOutput(isTTY = true) {
|
|
5
|
+
const writes = [];
|
|
6
|
+
return {
|
|
7
|
+
writes,
|
|
8
|
+
output: {
|
|
9
|
+
isTTY,
|
|
10
|
+
write(chunk) {
|
|
11
|
+
writes.push(chunk);
|
|
12
|
+
return true;
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
test("builds an OSC title sequence and strips control characters", () => {
|
|
18
|
+
assert.equal(titleSequence("ChelsCode"), "\u001b]0;ChelsCode\u0007");
|
|
19
|
+
assert.equal(cleanTitle("Clean\u0007Title\u001b]2;evil"), "CleanTitle]2;evil");
|
|
20
|
+
assert.equal(titleSequence("Chels\u0007Code"), "\u001b]0;ChelsCode\u0007");
|
|
21
|
+
});
|
|
22
|
+
test("sets ChelsCode as the Windows title and restores the previous one", () => {
|
|
23
|
+
const { writes, output } = recordingOutput();
|
|
24
|
+
let current = "Windows PowerShell";
|
|
25
|
+
const restore = installTerminalTitle("ChelsCode", {
|
|
26
|
+
output,
|
|
27
|
+
platform: "win32",
|
|
28
|
+
readTitle: () => current,
|
|
29
|
+
writeTitle: (title) => {
|
|
30
|
+
current = title;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
assert.equal(current, "ChelsCode");
|
|
34
|
+
assert.deepEqual(writes, ["\u001b]0;ChelsCode\u0007"]);
|
|
35
|
+
restore();
|
|
36
|
+
assert.equal(current, "Windows PowerShell");
|
|
37
|
+
assert.deepEqual(writes, [
|
|
38
|
+
"\u001b]0;ChelsCode\u0007",
|
|
39
|
+
"\u001b]0;Windows PowerShell\u0007",
|
|
40
|
+
]);
|
|
41
|
+
restore();
|
|
42
|
+
assert.equal(writes.length, 2, "restoring twice should not write again");
|
|
43
|
+
});
|
|
44
|
+
test("clears the title on non-Windows terminals", () => {
|
|
45
|
+
const { writes, output } = recordingOutput();
|
|
46
|
+
let read = false;
|
|
47
|
+
const restore = installTerminalTitle("ChelsCode", {
|
|
48
|
+
output,
|
|
49
|
+
platform: "linux",
|
|
50
|
+
readTitle: () => {
|
|
51
|
+
read = true;
|
|
52
|
+
return "unused";
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
assert.equal(read, false, "non-Windows titles cannot be read back");
|
|
56
|
+
restore();
|
|
57
|
+
assert.deepEqual(writes, ["\u001b]0;ChelsCode\u0007", "\u001b]0;\u0007"]);
|
|
58
|
+
});
|
|
59
|
+
test("leaves non-interactive output alone", () => {
|
|
60
|
+
const { writes, output } = recordingOutput(false);
|
|
61
|
+
let processTitle = "untouched";
|
|
62
|
+
const restore = installTerminalTitle("ChelsCode", {
|
|
63
|
+
output,
|
|
64
|
+
platform: "win32",
|
|
65
|
+
readTitle: () => "Windows PowerShell",
|
|
66
|
+
writeTitle: (title) => {
|
|
67
|
+
processTitle = title;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
restore();
|
|
71
|
+
assert.deepEqual(writes, []);
|
|
72
|
+
assert.equal(processTitle, "untouched");
|
|
73
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { executeTool, READ_ONLY_TOOL_DEFINITIONS, TOOL_DEFINITIONS } from "../src/workspace-tools.js";
|
|
7
|
+
const makeContext = (workspace, made = []) => ({
|
|
8
|
+
workspace,
|
|
9
|
+
confirm: async () => true,
|
|
10
|
+
onMade: (item) => made.push(item),
|
|
11
|
+
});
|
|
12
|
+
function jsonResponse(payload) {
|
|
13
|
+
return new Response(JSON.stringify(payload), {
|
|
14
|
+
status: 200,
|
|
15
|
+
headers: { "Content-Type": "application/json" },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function textResponse(body) {
|
|
19
|
+
return new Response(body, {
|
|
20
|
+
status: 200,
|
|
21
|
+
headers: { "Content-Type": "text/html" },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
test("tracks files that were actually written and ignores declined writes", async () => {
|
|
25
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "chels-tools-"));
|
|
26
|
+
const made = [];
|
|
27
|
+
const context = {
|
|
28
|
+
workspace,
|
|
29
|
+
confirm: async () => true,
|
|
30
|
+
onMade: (item) => made.push(item),
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
await executeTool("write_file", JSON.stringify({ path: "notes.txt", content: "hello\n" }), context);
|
|
34
|
+
assert.equal(await readFile(path.join(workspace, "notes.txt"), "utf8"), "hello\n");
|
|
35
|
+
assert.deepEqual(made, [{ kind: "file", path: "notes.txt" }]);
|
|
36
|
+
await executeTool("list_files", JSON.stringify({ path: "." }), context);
|
|
37
|
+
assert.deepEqual(made, [{ kind: "file", path: "notes.txt" }], "read-only tools should not count");
|
|
38
|
+
const declined = [];
|
|
39
|
+
await executeTool("write_file", JSON.stringify({ path: "skipped.txt", content: "nope" }), {
|
|
40
|
+
...context,
|
|
41
|
+
confirm: async () => false,
|
|
42
|
+
onMade: (item) => declined.push(item),
|
|
43
|
+
});
|
|
44
|
+
assert.deepEqual(declined, [], "a declined write should not count as made");
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await rm(workspace, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
test("answers the current date and time with the datetime tool", async () => {
|
|
51
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "chels-tools-"));
|
|
52
|
+
try {
|
|
53
|
+
const raw = await executeTool("get_current_datetime", JSON.stringify({}), {
|
|
54
|
+
workspace,
|
|
55
|
+
confirm: async () => true,
|
|
56
|
+
onMade: () => { },
|
|
57
|
+
});
|
|
58
|
+
const result = JSON.parse(raw);
|
|
59
|
+
assert.equal(typeof result.iso, "string");
|
|
60
|
+
assert.equal(typeof result.epochMs, "number");
|
|
61
|
+
assert.ok(!Number.isNaN(Date.parse(result.iso)));
|
|
62
|
+
assert.ok(result.timezone.length > 0);
|
|
63
|
+
assert.ok(result.dayOfWeek.length > 0);
|
|
64
|
+
assert.ok(result.date.length > 0);
|
|
65
|
+
assert.ok(result.time.length > 0);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await rm(workspace, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
test("answers weather questions with the weather tool", async () => {
|
|
72
|
+
const originalFetch = globalThis.fetch;
|
|
73
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "chels-tools-"));
|
|
74
|
+
const urls = [];
|
|
75
|
+
globalThis.fetch = (async (input) => {
|
|
76
|
+
const url = String(input);
|
|
77
|
+
urls.push(url);
|
|
78
|
+
if (url.includes("geocoding-api.open-meteo.com")) {
|
|
79
|
+
return jsonResponse({
|
|
80
|
+
results: [{
|
|
81
|
+
name: "Testville",
|
|
82
|
+
latitude: 12.34,
|
|
83
|
+
longitude: 56.78,
|
|
84
|
+
country: "Testland",
|
|
85
|
+
admin1: "Testshire",
|
|
86
|
+
}],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return jsonResponse({
|
|
90
|
+
current: {
|
|
91
|
+
temperature_2m: 21.5,
|
|
92
|
+
apparent_temperature: 20,
|
|
93
|
+
relative_humidity_2m: 60,
|
|
94
|
+
wind_speed_50m: 5,
|
|
95
|
+
precipitation: 0,
|
|
96
|
+
is_day: 1,
|
|
97
|
+
weather_code: 2,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
const raw = await executeTool("get_weather", JSON.stringify({ location: "Testville" }), makeContext(workspace));
|
|
103
|
+
const result = JSON.parse(raw);
|
|
104
|
+
assert.equal(result.location, "Testville");
|
|
105
|
+
assert.equal(result.temperatureC, 21.5);
|
|
106
|
+
assert.equal(result.condition, "Partly cloudy");
|
|
107
|
+
assert.equal(urls[0]?.includes("geocoding-api.open-meteo.com"), true);
|
|
108
|
+
assert.equal(urls[1]?.includes("api.open-meteo.com"), true);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
globalThis.fetch = originalFetch;
|
|
112
|
+
await rm(workspace, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
test("answers news questions with the news tool", async () => {
|
|
116
|
+
const originalFetch = globalThis.fetch;
|
|
117
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "chels-tools-"));
|
|
118
|
+
globalThis.fetch = (async () => textResponse(`<?xml version="1.0"?>
|
|
119
|
+
<rss version="2.0"><channel><title>Top stories</title>
|
|
120
|
+
<item><title>Headline one</title><link>https://example.com/one</link><pubDate>2026-01-01</pubDate><source>Example News</source></item>
|
|
121
|
+
<item><title>Headline two</title><link>https://example.com/two</link><pubDate>2026-01-02</pubDate><source>Example News</source></item>
|
|
122
|
+
</channel></rss>`));
|
|
123
|
+
try {
|
|
124
|
+
const raw = await executeTool("get_news", JSON.stringify({}), makeContext(workspace));
|
|
125
|
+
const result = JSON.parse(raw);
|
|
126
|
+
assert.equal(result.query, "top news");
|
|
127
|
+
assert.equal(result.items.length, 2);
|
|
128
|
+
assert.equal(result.items[0]?.title, "Headline one");
|
|
129
|
+
assert.equal(result.items[0]?.source, "Example News");
|
|
130
|
+
assert.equal(result.items[1]?.url, "https://example.com/two");
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
globalThis.fetch = originalFetch;
|
|
134
|
+
await rm(workspace, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
test("answers web search questions with the web search tool", async () => {
|
|
138
|
+
const originalFetch = globalThis.fetch;
|
|
139
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "chels-tools-"));
|
|
140
|
+
const searchHtml = `<html><body>
|
|
141
|
+
<div class="result results_links"><div class="result"><a class="result__a" href="https://example.com/one">First result</a><div class="result__snippet">Snippet one text.</div></div></div>
|
|
142
|
+
<div class="result results_links"><div class="result"><a class="result__a" href="https://example.com/two">Second result</a><div class="result__snippet">Snippet two text.</div></div></div>
|
|
143
|
+
</body></html>`;
|
|
144
|
+
globalThis.fetch = (async () => textResponse(searchHtml));
|
|
145
|
+
try {
|
|
146
|
+
const raw = await executeTool("web_search", JSON.stringify({ query: "latest ai news" }), makeContext(workspace));
|
|
147
|
+
const result = JSON.parse(raw);
|
|
148
|
+
assert.equal(result.query, "latest ai news");
|
|
149
|
+
assert.equal(result.results.length, 2);
|
|
150
|
+
assert.equal(result.results[0]?.title, "First result");
|
|
151
|
+
assert.equal(result.results[0]?.url, "https://example.com/one");
|
|
152
|
+
assert.equal(result.results[0]?.snippet, "Snippet one text.");
|
|
153
|
+
assert.equal(result.results[1]?.snippet, "Snippet two text.");
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
globalThis.fetch = originalFetch;
|
|
157
|
+
await rm(workspace, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
test("exposes the current-info tools to question mode", () => {
|
|
161
|
+
assert.deepEqual(READ_ONLY_TOOL_DEFINITIONS.map((tool) => tool.function.name), ["list_files", "read_file", "get_current_datetime", "get_weather", "web_search", "get_news"]);
|
|
162
|
+
assert.ok(TOOL_DEFINITIONS.some((tool) => tool.function.name === "run_command"));
|
|
163
|
+
});
|
|
164
|
+
test("rejects unknown tools", async () => {
|
|
165
|
+
const workspace = await mkdtemp(path.join(os.tmpdir(), "chels-tools-"));
|
|
166
|
+
await assert.rejects(executeTool("not_a_tool", JSON.stringify({}), makeContext(workspace)), /Unknown tool/);
|
|
167
|
+
await rm(workspace, { recursive: true, force: true });
|
|
168
|
+
});
|