@scira/cli 0.1.4 → 0.1.6
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/dist/agent/harness-agent.js +206 -0
- package/dist/agent/{research-agent.js → main-agent.js} +20 -1
- package/dist/cli/commands/init.js +7 -5
- package/dist/cli/index.js +52 -11
- package/dist/cli/shell/shell.js +4 -5
- package/dist/cli/shell/tui.js +5 -2
- package/dist/config/env-guide.js +24 -0
- package/dist/config/env-store.js +5 -3
- package/dist/config/load-config.js +9 -14
- package/dist/providers/harness/local-sandbox.js +143 -0
- package/dist/providers/llm/gateway.js +5 -2
- package/dist/providers/llm/models.js +13 -0
- package/dist/providers/llm/readiness.js +5 -1
- package/dist/providers/llm/registry.js +24 -3
- package/dist/storage/jsonl.js +2 -2
- package/dist/storage/run-store.js +15 -15
- package/dist/tools/agent-tools.js +7 -7
- package/dist/tools/background-tasks.js +4 -5
- package/dist/tools/mcp-oauth.js +29 -25
- package/dist/tools/open-url.js +1 -2
- package/dist/tools/todos.js +3 -3
- package/dist/types/index.js +13 -1
- package/dist/ui/ink/SciraApp.js +53 -12
- package/dist/ui/ink/components/home-screen.js +2 -2
- package/dist/ui/ink/components/overlays.js +73 -15
- package/dist/ui/ink/constants.js +37 -7
- package/dist/ui/ink/hooks/use-agent-turn.js +17 -6
- package/dist/ui/ink/hooks/use-feed-lines.js +34 -7
- package/dist/ui/ink/hooks/use-keyboard.js +28 -5
- package/dist/ui/ink/hooks/use-session.js +7 -5
- package/dist/ui/ink/hooks/use-settings.js +20 -0
- package/dist/ui/ink/hooks/use-submit.js +15 -8
- package/dist/ui/ink/lib/file-mentions.js +1 -2
- package/dist/ui/ink/lib/tool-result.js +205 -2
- package/dist/ui/ink/lib/utils.js +52 -28
- package/dist/ui/ink/theme.js +5 -10
- package/dist/watch/runner.js +2 -2
- package/package.json +15 -13
- package/dist/agent/background-tasks.js +0 -173
- package/dist/agent/todos.js +0 -140
- package/dist/agent/tools.js +0 -432
- package/dist/agent/tools.test.js +0 -60
- package/dist/agent/workspace.js +0 -85
- package/dist/config/env-guide.test.js +0 -18
- package/dist/config/env-store.test.js +0 -60
- package/dist/storage/jsonl.test.js +0 -38
- package/dist/storage/run-store.test.js +0 -65
- package/dist/tools/bash-policy.test.js +0 -38
- package/dist/tools/search-web.test.js +0 -24
- package/dist/tools/workspace.test.js +0 -75
- package/dist/types/schema.test.js +0 -61
- package/dist/ui/ink/hooks/use-feed-lines.test.js +0 -16
- package/dist/ui/ink/lib/tool-result.test.js +0 -60
- package/dist/ui/ink/lib/utils.test.js +0 -48
- package/dist/ui/ink/session-manager.test.js +0 -31
- package/dist/ui/ink/terminal-probe.test.js +0 -12
- package/dist/ui/ink/theme.test.js +0 -68
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { appendJsonl, readJsonl } from "./jsonl.js";
|
|
6
|
-
describe("readJsonl / appendJsonl", () => {
|
|
7
|
-
let dir;
|
|
8
|
-
let file;
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
dir = await mkdtemp(join(tmpdir(), "scira-test-"));
|
|
11
|
-
file = join(dir, "test.jsonl");
|
|
12
|
-
});
|
|
13
|
-
afterEach(async () => {
|
|
14
|
-
await rm(dir, { recursive: true, force: true });
|
|
15
|
-
});
|
|
16
|
-
it("returns [] for a missing file", async () => {
|
|
17
|
-
expect(await readJsonl(join(dir, "missing.jsonl"))).toEqual([]);
|
|
18
|
-
});
|
|
19
|
-
it("round-trips a single object", async () => {
|
|
20
|
-
await appendJsonl(file, { id: "c1", text: "hello" });
|
|
21
|
-
expect(await readJsonl(file)).toEqual([{ id: "c1", text: "hello" }]);
|
|
22
|
-
});
|
|
23
|
-
it("appends multiple objects sequentially", async () => {
|
|
24
|
-
await appendJsonl(file, { n: 1 });
|
|
25
|
-
await appendJsonl(file, { n: 2 });
|
|
26
|
-
await appendJsonl(file, { n: 3 });
|
|
27
|
-
expect(await readJsonl(file)).toEqual([{ n: 1 }, { n: 2 }, { n: 3 }]);
|
|
28
|
-
});
|
|
29
|
-
it("skips malformed lines without throwing", async () => {
|
|
30
|
-
await writeFile(file, '{"ok":true}\nNOT_JSON\n{"ok":false}\n');
|
|
31
|
-
expect(await readJsonl(file)).toEqual([{ ok: true }, { ok: false }]);
|
|
32
|
-
});
|
|
33
|
-
it("creates parent directories that do not exist", async () => {
|
|
34
|
-
const nested = join(dir, "a", "b", "c.jsonl");
|
|
35
|
-
await appendJsonl(nested, { x: 1 });
|
|
36
|
-
expect(await readJsonl(nested)).toEqual([{ x: 1 }]);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { createRun, summarizeRun, setRunTitle, getRunPaths } from "./run-store.js";
|
|
6
|
-
import { SciraConfigSchema } from "../types/index.js";
|
|
7
|
-
import { appendJsonl } from "./jsonl.js";
|
|
8
|
-
const BASE_CONFIG = SciraConfigSchema.parse({});
|
|
9
|
-
describe("createRun / summarizeRun", () => {
|
|
10
|
-
let tmpRoot;
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
tmpRoot = await mkdtemp(join(tmpdir(), "scira-runs-"));
|
|
13
|
-
});
|
|
14
|
-
afterEach(async () => {
|
|
15
|
-
await rm(tmpRoot, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
it("creates all expected files and subdirectories", async () => {
|
|
18
|
-
const config = { ...BASE_CONFIG, runDirectory: tmpRoot };
|
|
19
|
-
const state = await createRun("What is the speed of light?", config, "");
|
|
20
|
-
const paths = getRunPaths(state.path);
|
|
21
|
-
const { readFile, stat } = await import("node:fs/promises");
|
|
22
|
-
await expect(readFile(paths.goal, "utf8")).resolves.toContain("speed of light");
|
|
23
|
-
await expect(stat(paths.artifacts)).resolves.toBeDefined();
|
|
24
|
-
await expect(stat(paths.snapshots)).resolves.toBeDefined();
|
|
25
|
-
});
|
|
26
|
-
it("summarizeRun reflects zero sources and claims on a fresh run", async () => {
|
|
27
|
-
const config = { ...BASE_CONFIG, runDirectory: tmpRoot };
|
|
28
|
-
const state = await createRun("Test question", config, "");
|
|
29
|
-
expect(state.sourceCount).toBe(0);
|
|
30
|
-
expect(state.claimCount).toBe(0);
|
|
31
|
-
expect(state.weakCount).toBe(0);
|
|
32
|
-
expect(state.isFull).toBe(false);
|
|
33
|
-
expect(state.reportDirty).toBe(true);
|
|
34
|
-
expect(state.goal).toContain("Test question");
|
|
35
|
-
});
|
|
36
|
-
it("setRunTitle persists the title and summarizeRun reads it back", async () => {
|
|
37
|
-
const config = { ...BASE_CONFIG, runDirectory: tmpRoot };
|
|
38
|
-
const state = await createRun("Title test", config, "");
|
|
39
|
-
await setRunTitle(state.path, "My Custom Title");
|
|
40
|
-
const updated = await summarizeRun(state.path);
|
|
41
|
-
expect(updated.title).toBe("My Custom Title");
|
|
42
|
-
});
|
|
43
|
-
it("isFull becomes true once a source is appended", async () => {
|
|
44
|
-
const config = { ...BASE_CONFIG, runDirectory: tmpRoot };
|
|
45
|
-
const state = await createRun("Full test", config, "");
|
|
46
|
-
const paths = getRunPaths(state.path);
|
|
47
|
-
await appendJsonl(paths.sources, {
|
|
48
|
-
id: "s1", title: "Test", url: "https://test.com",
|
|
49
|
-
kind: "primary", summary: "", createdAt: new Date().toISOString()
|
|
50
|
-
});
|
|
51
|
-
const updated = await summarizeRun(state.path);
|
|
52
|
-
expect(updated.sourceCount).toBe(1);
|
|
53
|
-
expect(updated.isFull).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
it("weakCount reflects weak claims", async () => {
|
|
56
|
-
const config = { ...BASE_CONFIG, runDirectory: tmpRoot };
|
|
57
|
-
const state = await createRun("Weak test", config, "");
|
|
58
|
-
const paths = getRunPaths(state.path);
|
|
59
|
-
await appendJsonl(paths.claims, { id: "c1", text: "x", confidence: "low", status: "weak", sourceIds: [], reason: "", createdAt: new Date().toISOString() });
|
|
60
|
-
await appendJsonl(paths.claims, { id: "c2", text: "y", confidence: "high", status: "verified", sourceIds: [], reason: "", createdAt: new Date().toISOString() });
|
|
61
|
-
const updated = await summarizeRun(state.path);
|
|
62
|
-
expect(updated.claimCount).toBe(2);
|
|
63
|
-
expect(updated.weakCount).toBe(1);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { isReadOnlyBashCommand } from "./agent-tools.js";
|
|
3
|
-
describe("isReadOnlyBashCommand", () => {
|
|
4
|
-
it("allows common read-only commands", () => {
|
|
5
|
-
expect(isReadOnlyBashCommand("ls -la")).toBe(true);
|
|
6
|
-
expect(isReadOnlyBashCommand("cat package.json")).toBe(true);
|
|
7
|
-
expect(isReadOnlyBashCommand("git status")).toBe(true);
|
|
8
|
-
expect(isReadOnlyBashCommand("git log --oneline -5")).toBe(true);
|
|
9
|
-
expect(isReadOnlyBashCommand("git branch")).toBe(false);
|
|
10
|
-
expect(isReadOnlyBashCommand("git remote -v")).toBe(false);
|
|
11
|
-
});
|
|
12
|
-
it("rejects chained or mutating commands", () => {
|
|
13
|
-
expect(isReadOnlyBashCommand("ls; rm -rf /")).toBe(false);
|
|
14
|
-
expect(isReadOnlyBashCommand("git commit -m x")).toBe(false);
|
|
15
|
-
expect(isReadOnlyBashCommand("npm install")).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
it("rejects multiline chaining and destructive find", () => {
|
|
18
|
-
expect(isReadOnlyBashCommand("git status\nrm -rf .")).toBe(false);
|
|
19
|
-
expect(isReadOnlyBashCommand("git status\nnpm install")).toBe(false);
|
|
20
|
-
expect(isReadOnlyBashCommand("find . -delete")).toBe(false);
|
|
21
|
-
expect(isReadOnlyBashCommand("find . -exec rm {} +")).toBe(false);
|
|
22
|
-
expect(isReadOnlyBashCommand("find . -type f")).toBe(true);
|
|
23
|
-
});
|
|
24
|
-
it("rejects path traversal and absolute paths", () => {
|
|
25
|
-
expect(isReadOnlyBashCommand("cat ../secret")).toBe(false);
|
|
26
|
-
expect(isReadOnlyBashCommand("grep -r token ..")).toBe(false);
|
|
27
|
-
expect(isReadOnlyBashCommand("ls /")).toBe(false);
|
|
28
|
-
expect(isReadOnlyBashCommand("cat .//etc/passwd")).toBe(false);
|
|
29
|
-
expect(isReadOnlyBashCommand("grep -r secret .//etc")).toBe(false);
|
|
30
|
-
expect(isReadOnlyBashCommand("cat package.json")).toBe(true);
|
|
31
|
-
expect(isReadOnlyBashCommand("grep -rn foo .")).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
it("rejects privileged flags on allowlisted binaries", () => {
|
|
34
|
-
expect(isReadOnlyBashCommand("git diff --extcmd=sh")).toBe(false);
|
|
35
|
-
expect(isReadOnlyBashCommand("rg --pre=bash -- foo .")).toBe(false);
|
|
36
|
-
expect(isReadOnlyBashCommand("git -c alias.status=!rm status")).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { SciraConfigSchema } from "../types/index.js";
|
|
3
|
-
import { multiSearchWeb } from "./search-web.js";
|
|
4
|
-
const BASE_CONFIG = SciraConfigSchema.parse({});
|
|
5
|
-
describe("multiSearchWeb", () => {
|
|
6
|
-
it("reports provider errors instead of silently returning empty results", async () => {
|
|
7
|
-
const origExa = process.env.EXA_API_KEY;
|
|
8
|
-
const origFc = process.env.FIRECRAWL_API_KEY;
|
|
9
|
-
process.env.EXA_API_KEY = "invalid";
|
|
10
|
-
process.env.FIRECRAWL_API_KEY = "invalid";
|
|
11
|
-
const config = { ...BASE_CONFIG, search: { ...BASE_CONFIG.search, provider: "exa" } };
|
|
12
|
-
const results = await multiSearchWeb(["test query"], [{}], config);
|
|
13
|
-
expect(results[0]?.results).toEqual([]);
|
|
14
|
-
expect(results[0]?.error).toMatch(/invalid|unauthorized|api key/i);
|
|
15
|
-
if (origExa)
|
|
16
|
-
process.env.EXA_API_KEY = origExa;
|
|
17
|
-
else
|
|
18
|
-
delete process.env.EXA_API_KEY;
|
|
19
|
-
if (origFc)
|
|
20
|
-
process.env.FIRECRAWL_API_KEY = origFc;
|
|
21
|
-
else
|
|
22
|
-
delete process.env.FIRECRAWL_API_KEY;
|
|
23
|
-
});
|
|
24
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { harnessBasename, isRunArtifactPath, resolveInsideRun, resolveProjectRoot, resolveToolPath } from "./workspace.js";
|
|
3
|
-
const RUN = "/tmp/scira-test-run";
|
|
4
|
-
const PROJECT = "/Users/me/my-app";
|
|
5
|
-
const RUN_UNDER_SCIRA = `${PROJECT}/.scira/runs/2024-test-abc`;
|
|
6
|
-
describe("resolveInsideRun", () => {
|
|
7
|
-
it("resolves a relative path inside the run dir", () => {
|
|
8
|
-
expect(resolveInsideRun(RUN, "notes.md")).toBe(`${RUN}/notes.md`);
|
|
9
|
-
});
|
|
10
|
-
it("resolves a nested relative path inside the run dir", () => {
|
|
11
|
-
expect(resolveInsideRun(RUN, "artifacts/output.txt")).toBe(`${RUN}/artifacts/output.txt`);
|
|
12
|
-
});
|
|
13
|
-
it("resolves an absolute path that is inside the run dir", () => {
|
|
14
|
-
expect(resolveInsideRun(RUN, `${RUN}/plan.md`)).toBe(`${RUN}/plan.md`);
|
|
15
|
-
});
|
|
16
|
-
it("throws for a path that escapes with ../", () => {
|
|
17
|
-
expect(() => resolveInsideRun(RUN, "../outside.txt")).toThrow("outside the run directory");
|
|
18
|
-
});
|
|
19
|
-
it("throws for a deep escape path", () => {
|
|
20
|
-
expect(() => resolveInsideRun(RUN, "a/../../outside.txt")).toThrow("outside the run directory");
|
|
21
|
-
});
|
|
22
|
-
it("throws for an absolute path outside the run dir", () => {
|
|
23
|
-
expect(() => resolveInsideRun(RUN, "/etc/passwd")).toThrow("outside the run directory");
|
|
24
|
-
});
|
|
25
|
-
it("throws for a home-dir escape", () => {
|
|
26
|
-
const home = `${process.env.HOME ?? "/root"}/evil.sh`;
|
|
27
|
-
expect(() => resolveInsideRun(RUN, home)).toThrow("outside the run directory");
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
describe("resolveProjectRoot", () => {
|
|
31
|
-
it("returns parent of .scira when run is under .scira/runs", () => {
|
|
32
|
-
expect(resolveProjectRoot(RUN_UNDER_SCIRA)).toBe(PROJECT);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
describe("harnessBasename", () => {
|
|
36
|
-
it("strips run: and ./ prefixes", () => {
|
|
37
|
-
expect(harnessBasename("run:report.md")).toBe("report.md");
|
|
38
|
-
expect(harnessBasename("./plan.md")).toBe("plan.md");
|
|
39
|
-
expect(harnessBasename("notes.md")).toBe("notes.md");
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
describe("isRunArtifactPath", () => {
|
|
43
|
-
it("treats bare harness filenames as run artifacts", () => {
|
|
44
|
-
expect(isRunArtifactPath("plan.md")).toBe(true);
|
|
45
|
-
expect(isRunArtifactPath("notes.md")).toBe(true);
|
|
46
|
-
expect(isRunArtifactPath("src/foo.ts")).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
it("does not treat nested paths as run artifacts by basename", () => {
|
|
49
|
-
expect(isRunArtifactPath("docs/notes.md")).toBe(false);
|
|
50
|
-
expect(isRunArtifactPath("src/plan.md")).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
it("treats run: prefix as run artifact", () => {
|
|
53
|
-
expect(isRunArtifactPath("run:custom.md")).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
describe("resolveToolPath", () => {
|
|
57
|
-
it("routes source paths to workspace", () => {
|
|
58
|
-
const resolved = resolveToolPath(RUN_UNDER_SCIRA, PROJECT, "src/index.ts");
|
|
59
|
-
expect(resolved.scope).toBe("workspace");
|
|
60
|
-
expect(resolved.abs).toBe(`${PROJECT}/src/index.ts`);
|
|
61
|
-
});
|
|
62
|
-
it("routes plan.md to run directory", () => {
|
|
63
|
-
const resolved = resolveToolPath(RUN_UNDER_SCIRA, PROJECT, "plan.md");
|
|
64
|
-
expect(resolved.scope).toBe("run");
|
|
65
|
-
expect(resolved.abs).toBe(`${RUN_UNDER_SCIRA}/plan.md`);
|
|
66
|
-
});
|
|
67
|
-
it("routes nested notes.md to workspace not run", () => {
|
|
68
|
-
const resolved = resolveToolPath(RUN_UNDER_SCIRA, PROJECT, "docs/notes.md");
|
|
69
|
-
expect(resolved.scope).toBe("workspace");
|
|
70
|
-
expect(resolved.abs).toBe(`${PROJECT}/docs/notes.md`);
|
|
71
|
-
});
|
|
72
|
-
it("blocks writes into .scira from workspace paths", () => {
|
|
73
|
-
expect(() => resolveToolPath(RUN_UNDER_SCIRA, PROJECT, ".scira/config.json")).toThrow("inside .scira");
|
|
74
|
-
});
|
|
75
|
-
});
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { ClaimSchema, SourceSchema, SciraConfigSchema } from "./index.js";
|
|
3
|
-
describe("ClaimSchema", () => {
|
|
4
|
-
it("parses a valid claim with defaults", () => {
|
|
5
|
-
const result = ClaimSchema.parse({
|
|
6
|
-
id: "c1",
|
|
7
|
-
text: "The sky is blue.",
|
|
8
|
-
createdAt: new Date().toISOString(),
|
|
9
|
-
});
|
|
10
|
-
expect(result.confidence).toBe("medium");
|
|
11
|
-
expect(result.status).toBe("draft");
|
|
12
|
-
expect(result.sourceIds).toEqual([]);
|
|
13
|
-
expect(result.reason).toBe("");
|
|
14
|
-
});
|
|
15
|
-
it("rejects an invalid confidence value", () => {
|
|
16
|
-
expect(() => ClaimSchema.parse({ id: "c1", text: "x", confidence: "very_high", createdAt: "" })).toThrow();
|
|
17
|
-
});
|
|
18
|
-
it("rejects an invalid status value", () => {
|
|
19
|
-
expect(() => ClaimSchema.parse({ id: "c1", text: "x", status: "maybe", createdAt: "" })).toThrow();
|
|
20
|
-
});
|
|
21
|
-
it("preserves all fields when fully specified", () => {
|
|
22
|
-
const input = {
|
|
23
|
-
id: "c2",
|
|
24
|
-
text: "Claim text.",
|
|
25
|
-
confidence: "high",
|
|
26
|
-
status: "verified",
|
|
27
|
-
sourceIds: ["s1", "s2"],
|
|
28
|
-
reason: "Verified by primary source.",
|
|
29
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
30
|
-
};
|
|
31
|
-
expect(ClaimSchema.parse(input)).toEqual(input);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe("SourceSchema", () => {
|
|
35
|
-
it("parses a valid source with defaults", () => {
|
|
36
|
-
const result = SourceSchema.parse({
|
|
37
|
-
id: "s1",
|
|
38
|
-
title: "Example",
|
|
39
|
-
url: "https://example.com",
|
|
40
|
-
createdAt: new Date().toISOString(),
|
|
41
|
-
});
|
|
42
|
-
expect(result.kind).toBe("unknown");
|
|
43
|
-
expect(result.summary).toBe("");
|
|
44
|
-
});
|
|
45
|
-
it("rejects an invalid kind value", () => {
|
|
46
|
-
expect(() => SourceSchema.parse({ id: "s1", title: "t", url: "u", kind: "blog", createdAt: "" })).toThrow();
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
describe("SciraConfigSchema", () => {
|
|
50
|
-
it("parses an empty object using all defaults", () => {
|
|
51
|
-
const config = SciraConfigSchema.parse({});
|
|
52
|
-
expect(config.llmProvider).toBe("gateway");
|
|
53
|
-
expect(config.approvalMode).toBe("suggest");
|
|
54
|
-
expect(config.alwaysAllowLinks).toBe(false);
|
|
55
|
-
expect(config.runDirectory).toBe(".scira/runs");
|
|
56
|
-
expect(config.maxSources).toBe(20);
|
|
57
|
-
});
|
|
58
|
-
it("rejects an invalid approvalMode", () => {
|
|
59
|
-
expect(() => SciraConfigSchema.parse({ approvalMode: "yolo" })).toThrow();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { computeGroups } from "./use-feed-lines.js";
|
|
3
|
-
describe("computeGroups", () => {
|
|
4
|
-
it("lists thinking and tools in step order", () => {
|
|
5
|
-
const feed = [
|
|
6
|
-
{ kind: "reasoning", text: "plan", durationMs: 100 },
|
|
7
|
-
{ kind: "tool", name: "webSearch", summary: "q", status: "done" },
|
|
8
|
-
{ kind: "reasoning", text: "read", durationMs: 50 },
|
|
9
|
-
{ kind: "tool", name: "readUrl", summary: "url", status: "done" },
|
|
10
|
-
];
|
|
11
|
-
const { groups } = computeGroups(feed);
|
|
12
|
-
const g = groups.get(0);
|
|
13
|
-
expect(g?.stepLabels).toEqual(["thinking", "webSearch", "thinking", "readUrl"]);
|
|
14
|
-
expect(g?.itemCount).toBe(4);
|
|
15
|
-
});
|
|
16
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { DARK_THEME } from "../theme.js";
|
|
3
|
-
import { formatToolResultLines, formatToolResultPreview, isToolItemCollapsed, defaultCollapsedToolName, } from "./tool-result.js";
|
|
4
|
-
function textOf(lines) {
|
|
5
|
-
return lines.map((row) => row.map((s) => s.text).join("")).join("\n");
|
|
6
|
-
}
|
|
7
|
-
describe("formatToolResultLines", () => {
|
|
8
|
-
it("shows queries then a flat sources list", () => {
|
|
9
|
-
const result = JSON.stringify([
|
|
10
|
-
{
|
|
11
|
-
query: "ai news",
|
|
12
|
-
results: [
|
|
13
|
-
{ title: "Alpha", url: "https://a.com", snippet: "First hit snippet." },
|
|
14
|
-
{ title: "Beta", url: "https://b.com", snippet: "Second hit snippet." },
|
|
15
|
-
],
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
query: "ai frameworks",
|
|
19
|
-
results: [
|
|
20
|
-
{ title: "Alpha", url: "https://a.com", snippet: "Duplicate should drop." },
|
|
21
|
-
{ title: "Gamma", url: "https://c.com", snippet: "Third hit snippet." },
|
|
22
|
-
],
|
|
23
|
-
},
|
|
24
|
-
]);
|
|
25
|
-
const out = textOf(formatToolResultLines("webSearch", "ai news · ai frameworks", result, "done", 80, DARK_THEME));
|
|
26
|
-
expect(out.indexOf("Queries")).toBeLessThan(out.indexOf("Sources"));
|
|
27
|
-
expect(out).toContain("ai news");
|
|
28
|
-
expect(out).toContain("ai frameworks");
|
|
29
|
-
expect(out).toContain("Sources (3)");
|
|
30
|
-
expect(out).toContain("Alpha");
|
|
31
|
-
expect(out).toContain("First hit snippet.");
|
|
32
|
-
expect(out).toContain("Gamma");
|
|
33
|
-
expect(out).not.toContain("Duplicate should drop");
|
|
34
|
-
});
|
|
35
|
-
it("shows shell command and full output", () => {
|
|
36
|
-
const out = textOf(formatToolResultLines("bash", "ls -la", "total 0\nfile.txt", "done", 80, DARK_THEME));
|
|
37
|
-
expect(out).toContain("$ ls -la");
|
|
38
|
-
expect(out).toContain("total 0");
|
|
39
|
-
expect(out).toContain("file.txt");
|
|
40
|
-
});
|
|
41
|
-
it("shows readUrl title and body when expanded", () => {
|
|
42
|
-
const result = "# Example\n(snapshot saved to snapshots/example.md)\n\nBody paragraph here.";
|
|
43
|
-
const out = textOf(formatToolResultLines("readUrl", "https://example.com", result, "done", 80, DARK_THEME, true));
|
|
44
|
-
expect(out).toContain("Example");
|
|
45
|
-
expect(out).toContain("snapshots/example.md");
|
|
46
|
-
expect(out).toContain("Body paragraph here.");
|
|
47
|
-
});
|
|
48
|
-
it("hides readUrl body when collapsed", () => {
|
|
49
|
-
const result = "# Example\n(snapshot saved to snapshots/example.md)\n\nBody paragraph here.";
|
|
50
|
-
expect(formatToolResultLines("readUrl", "https://example.com", result, "done", 80, DARK_THEME, false)).toEqual([]);
|
|
51
|
-
expect(formatToolResultPreview("readUrl", "https://example.com", result, "done")).toContain("Example");
|
|
52
|
-
});
|
|
53
|
-
it("defaults readUrl and webSearch collapsed", () => {
|
|
54
|
-
expect(defaultCollapsedToolName("readUrl")).toBe(true);
|
|
55
|
-
expect(defaultCollapsedToolName("webSearch")).toBe(true);
|
|
56
|
-
expect(isToolItemCollapsed("id", "readUrl", "done", new Map())).toBe(true);
|
|
57
|
-
expect(isToolItemCollapsed("id", "webSearch", "done", new Map())).toBe(true);
|
|
58
|
-
expect(isToolItemCollapsed("id", "readUrl", "done", new Map([["id", true]]))).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { summarizeToolInput, summarizeToolOutput, ansiHyperlink, computeLineLinks, linkAtMouseColumn } from "./utils.js";
|
|
3
|
-
describe("hyperlink helpers", () => {
|
|
4
|
-
it("wraps OSC 8 around styled link text", () => {
|
|
5
|
-
const out = ansiHyperlink("docs", "https://example.com", { color: "#FFE0C2", underline: true });
|
|
6
|
-
expect(out).toContain("\x1b]8;;https://example.com\x1b\\");
|
|
7
|
-
expect(out).toContain("docs");
|
|
8
|
-
expect(out).toContain("\x1b]8;;\x1b\\");
|
|
9
|
-
});
|
|
10
|
-
it("maps mouse column to link url", () => {
|
|
11
|
-
const links = computeLineLinks([
|
|
12
|
-
{ text: "see " },
|
|
13
|
-
{ text: "docs", url: "https://example.com" },
|
|
14
|
-
], 2);
|
|
15
|
-
expect(links).toEqual([{ start: 6, end: 9, url: "https://example.com" }]);
|
|
16
|
-
expect(linkAtMouseColumn(links, 7)).toBe("https://example.com");
|
|
17
|
-
expect(linkAtMouseColumn(links, 3)).toBeUndefined();
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
describe("summarizeToolInput", () => {
|
|
21
|
-
it("formats webSearch queries", () => {
|
|
22
|
-
expect(summarizeToolInput("webSearch", { queries: ["a", "b", "c"] })).toBe("a · b +1");
|
|
23
|
-
});
|
|
24
|
-
it("formats readUrl url", () => {
|
|
25
|
-
expect(summarizeToolInput("readUrl", { url: "https://example.com" })).toBe("https://example.com");
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
describe("summarizeToolOutput", () => {
|
|
29
|
-
it("summarizes webSearch json results", () => {
|
|
30
|
-
const output = JSON.stringify([
|
|
31
|
-
{ query: "q1", results: [{ title: "Alpha" }, { title: "Beta" }] },
|
|
32
|
-
{ query: "q2", results: [{ title: "Gamma" }] },
|
|
33
|
-
]);
|
|
34
|
-
expect(summarizeToolOutput("webSearch", output)).toBe("3 results · Alpha · Beta · Gamma");
|
|
35
|
-
});
|
|
36
|
-
it("extracts readUrl page title", () => {
|
|
37
|
-
const output = "# Example Page\n(snapshot saved to snapshots/example.md)\n\nBody text";
|
|
38
|
-
expect(summarizeToolOutput("readUrl", output)).toBe("Example Page");
|
|
39
|
-
});
|
|
40
|
-
it("summarizes listSkills output", () => {
|
|
41
|
-
const output = "plan: Write a research plan\nsources: Gather sources";
|
|
42
|
-
expect(summarizeToolOutput("listSkills", output)).toBe("2 skills · plan, sources");
|
|
43
|
-
});
|
|
44
|
-
it("passes through predetermined string results", () => {
|
|
45
|
-
expect(summarizeToolOutput("requestFullResearch", "Approved. Stop now and do not call more tools."))
|
|
46
|
-
.toBe("Approved. Stop now and do not call more tools.");
|
|
47
|
-
});
|
|
48
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { mergeFeedToolResults } from "./session-manager.js";
|
|
3
|
-
describe("mergeFeedToolResults", () => {
|
|
4
|
-
it("replaces truncated tool results with full buffer copies", () => {
|
|
5
|
-
const feed = [
|
|
6
|
-
{
|
|
7
|
-
kind: "tool",
|
|
8
|
-
name: "webSearch",
|
|
9
|
-
toolCallId: "call-1",
|
|
10
|
-
summary: "query a",
|
|
11
|
-
status: "done",
|
|
12
|
-
result: "truncated…",
|
|
13
|
-
},
|
|
14
|
-
];
|
|
15
|
-
const buffer = [
|
|
16
|
-
{
|
|
17
|
-
kind: "tool",
|
|
18
|
-
name: "webSearch",
|
|
19
|
-
toolCallId: "call-1",
|
|
20
|
-
summary: "query a",
|
|
21
|
-
status: "done",
|
|
22
|
-
result: "x".repeat(5000),
|
|
23
|
-
},
|
|
24
|
-
];
|
|
25
|
-
const merged = mergeFeedToolResults(feed, buffer);
|
|
26
|
-
expect(merged[0].kind).toBe("tool");
|
|
27
|
-
if (merged[0].kind === "tool") {
|
|
28
|
-
expect(merged[0].result).toHaveLength(5000);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { luminance, parseOscBackgroundColor, themeFromLuminance } from "./terminal-probe.js";
|
|
3
|
-
describe("terminal-probe", () => {
|
|
4
|
-
it("parses rgb OSC background colors", () => {
|
|
5
|
-
expect(parseOscBackgroundColor("rgb:1e1e/1e1e/1e1e")).toEqual({ r: 30, g: 30, b: 30 });
|
|
6
|
-
expect(parseOscBackgroundColor("#f0f0f0")).toEqual({ r: 240, g: 240, b: 240 });
|
|
7
|
-
});
|
|
8
|
-
it("classifies luminance into light and dark", () => {
|
|
9
|
-
expect(themeFromLuminance(luminance({ r: 30, g: 30, b: 30 }))).toBe("dark");
|
|
10
|
-
expect(themeFromLuminance(luminance({ r: 240, g: 240, b: 240 }))).toBe("light");
|
|
11
|
-
});
|
|
12
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { detectTerminalTheme, getTheme, inputForegroundForAppearance, resolveRenderingAppearance, watchAutoThemeChanges, } from "./theme.js";
|
|
3
|
-
const env = process.env;
|
|
4
|
-
afterEach(() => {
|
|
5
|
-
process.env = { ...env };
|
|
6
|
-
});
|
|
7
|
-
describe("detectTerminalTheme", () => {
|
|
8
|
-
it("uses COLORFGBG when present", () => {
|
|
9
|
-
process.env.COLORFGBG = "15;0";
|
|
10
|
-
expect(detectTerminalTheme()).toBe("dark");
|
|
11
|
-
process.env.COLORFGBG = "0;15";
|
|
12
|
-
expect(detectTerminalTheme()).toBe("light");
|
|
13
|
-
});
|
|
14
|
-
it("prefers COLORFGBG over terminal profile hints", () => {
|
|
15
|
-
process.env.COLORFGBG = "0;15";
|
|
16
|
-
process.env.TERM_PROFILE = "Dark";
|
|
17
|
-
expect(detectTerminalTheme()).toBe("light");
|
|
18
|
-
});
|
|
19
|
-
it("resolves auto theme colors from detected appearance", () => {
|
|
20
|
-
process.env.COLORFGBG = "0;15";
|
|
21
|
-
expect(getTheme("auto").text).toBe("ansi256(0)");
|
|
22
|
-
expect(getTheme("auto").inputText).toBe("ansi256(0)");
|
|
23
|
-
expect(getTheme("auto").userBandBackground).toBe("#f0f0f0");
|
|
24
|
-
process.env.COLORFGBG = "15;0";
|
|
25
|
-
expect(getTheme("auto").text).toBe("ansi256(15)");
|
|
26
|
-
expect(getTheme("auto").inputText).toBe("ansi256(15)");
|
|
27
|
-
expect(getTheme("auto").userBandBackground).toBe("ansi256(238)");
|
|
28
|
-
});
|
|
29
|
-
it("detects Warp and Apple Terminal as dark when unset", () => {
|
|
30
|
-
delete process.env.COLORFGBG;
|
|
31
|
-
delete process.env.TERM_PROFILE;
|
|
32
|
-
delete process.env.ITERM_PROFILE;
|
|
33
|
-
process.env.TERM_PROGRAM = "WarpTerminal";
|
|
34
|
-
expect(detectTerminalTheme()).toBe("dark");
|
|
35
|
-
process.env.TERM_PROGRAM = "Apple_Terminal";
|
|
36
|
-
expect(detectTerminalTheme()).toBe("dark");
|
|
37
|
-
});
|
|
38
|
-
it("defaults to dark when no signals are present", () => {
|
|
39
|
-
delete process.env.COLORFGBG;
|
|
40
|
-
delete process.env.TERM_PROFILE;
|
|
41
|
-
delete process.env.ITERM_PROFILE;
|
|
42
|
-
delete process.env.TERM_PROGRAM;
|
|
43
|
-
delete process.env.TERM_PROGRAM;
|
|
44
|
-
expect(detectTerminalTheme()).toBe("dark");
|
|
45
|
-
});
|
|
46
|
-
it("maps terminal appearance to ansi256 input foreground", () => {
|
|
47
|
-
expect(inputForegroundForAppearance("dark")).toBe("ansi256(15)");
|
|
48
|
-
expect(inputForegroundForAppearance("light")).toBe("ansi256(0)");
|
|
49
|
-
});
|
|
50
|
-
it("overrides a mismatched locked theme to match the terminal", () => {
|
|
51
|
-
expect(resolveRenderingAppearance("light", "dark")).toBe("dark");
|
|
52
|
-
expect(resolveRenderingAppearance("dark", "light")).toBe("light");
|
|
53
|
-
expect(resolveRenderingAppearance("dark", "dark")).toBe("dark");
|
|
54
|
-
expect(resolveRenderingAppearance("auto", "dark")).toBe("dark");
|
|
55
|
-
});
|
|
56
|
-
it("watchAutoThemeChanges fires immediately and on interval", () => {
|
|
57
|
-
vi.useFakeTimers();
|
|
58
|
-
const onChange = vi.fn();
|
|
59
|
-
const stop = watchAutoThemeChanges(onChange);
|
|
60
|
-
expect(onChange).toHaveBeenCalledTimes(1);
|
|
61
|
-
vi.advanceTimersByTime(1500);
|
|
62
|
-
expect(onChange).toHaveBeenCalledTimes(2);
|
|
63
|
-
stop();
|
|
64
|
-
vi.advanceTimersByTime(1500);
|
|
65
|
-
expect(onChange).toHaveBeenCalledTimes(2);
|
|
66
|
-
vi.useRealTimers();
|
|
67
|
-
});
|
|
68
|
-
});
|