@scira/cli 0.1.0 → 0.1.1

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,53 @@
1
+ const OSC_BG_QUERY = "\x1b]11;?\x07";
2
+ export function parseOscBackgroundColor(raw) {
3
+ const trimmed = raw.trim();
4
+ const rgb = /rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)/.exec(trimmed);
5
+ if (rgb) {
6
+ const scale = (h) => {
7
+ const expanded = h.length < 4 ? h.repeat(4 / h.length).slice(0, 4) : h.slice(0, 4);
8
+ return Math.round(parseInt(expanded, 16) / 65535 * 255);
9
+ };
10
+ return { r: scale(rgb[1]), g: scale(rgb[2]), b: scale(rgb[3]) };
11
+ }
12
+ const hex = /#([0-9a-fA-F]{6})/.exec(trimmed);
13
+ if (hex) {
14
+ return {
15
+ r: parseInt(hex[1].slice(0, 2), 16),
16
+ g: parseInt(hex[1].slice(2, 4), 16),
17
+ b: parseInt(hex[1].slice(4, 6), 16),
18
+ };
19
+ }
20
+ return undefined;
21
+ }
22
+ export function luminance({ r, g, b }) {
23
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
24
+ }
25
+ export function themeFromLuminance(lum) {
26
+ return lum > 0.55 ? "light" : "dark";
27
+ }
28
+ export function probeTerminalTheme(stdin, stdout, timeoutMs = 400) {
29
+ return new Promise((resolve) => {
30
+ let settled = false;
31
+ const finish = (value) => {
32
+ if (settled)
33
+ return;
34
+ settled = true;
35
+ clearTimeout(timer);
36
+ stdin.off("data", onData);
37
+ resolve(value);
38
+ };
39
+ const onData = (chunk) => {
40
+ const str = typeof chunk === "string" ? chunk : chunk.toString();
41
+ const match = /\x1b\]11;([^\x07\x1b]+)/.exec(str);
42
+ if (!match)
43
+ return;
44
+ const color = parseOscBackgroundColor(match[1]);
45
+ if (!color)
46
+ return;
47
+ finish(themeFromLuminance(luminance(color)));
48
+ };
49
+ const timer = setTimeout(() => finish(undefined), timeoutMs);
50
+ stdin.on("data", onData);
51
+ stdout.write(OSC_BG_QUERY);
52
+ });
53
+ }
@@ -0,0 +1,12 @@
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
+ });
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { createContext, useContext, useMemo, useState } from "react";
3
+ import { probeTerminalTheme } from "./terminal-probe.js";
4
+ import { detectTerminalTheme, getTheme, getThemeFromResolved, watchAutoThemeChanges, } from "./theme.js";
5
+ const ThemeCtx = createContext(getTheme("auto"));
6
+ export function ThemeProvider({ config, stdin, stdout, children }) {
7
+ const [autoResolved, setAutoResolved] = useState(detectTerminalTheme);
8
+ const [probed, setProbed] = useState(undefined);
9
+ React.useEffect(() => {
10
+ if (config.theme !== "auto") {
11
+ setProbed(undefined);
12
+ return;
13
+ }
14
+ const sync = () => {
15
+ void (async () => {
16
+ const live = stdin && stdout ? await probeTerminalTheme(stdin, stdout) : undefined;
17
+ setProbed((prev) => (prev === live ? prev : live));
18
+ const next = live ?? detectTerminalTheme();
19
+ setAutoResolved((prev) => (prev === next ? prev : next));
20
+ })();
21
+ };
22
+ return watchAutoThemeChanges(sync);
23
+ }, [config.theme, stdin, stdout]);
24
+ const colors = useMemo(() => {
25
+ if (config.theme !== "auto")
26
+ return getTheme(config.theme);
27
+ return getThemeFromResolved(probed ?? autoResolved);
28
+ }, [config.theme, autoResolved, probed]);
29
+ return _jsx(ThemeCtx.Provider, { value: colors, children: children });
30
+ }
31
+ export function useTheme() {
32
+ return useContext(ThemeCtx);
33
+ }
@@ -0,0 +1,183 @@
1
+ import { readFileSync, unwatchFile, watchFile } from "node:fs";
2
+ import { execSync } from "node:child_process";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ export const DARK_THEME = {
6
+ accent: "#FFE0C2",
7
+ accentDim: "#CFB59D",
8
+ background: "",
9
+ border: "#FFE0C2",
10
+ text: "white",
11
+ textDim: "ansi256(245)",
12
+ textInverse: "black",
13
+ inputText: "#ffffff",
14
+ cursorBackground: "#FFE0C2",
15
+ cursorForeground: "#000000",
16
+ success: "green",
17
+ warning: "yellow",
18
+ error: "red",
19
+ modalBackground: "",
20
+ userBandBackground: "ansi256(238)",
21
+ };
22
+ export const LIGHT_THEME = {
23
+ accent: "#FFE0C2",
24
+ accentDim: "#CFB59D",
25
+ background: "",
26
+ border: "#FFE0C2",
27
+ text: "black",
28
+ textDim: "gray",
29
+ textInverse: "white",
30
+ inputText: "#000000",
31
+ cursorBackground: "#CFB59D",
32
+ cursorForeground: "#000000",
33
+ success: "green",
34
+ warning: "yellow",
35
+ error: "red",
36
+ modalBackground: "",
37
+ userBandBackground: "#f0f0f0",
38
+ };
39
+ function inferThemeFromName(name) {
40
+ const n = name.toLowerCase();
41
+ if (/\blight\b|day\b|solarized light|github light/i.test(n))
42
+ return "light";
43
+ if (/\bdark\b|night\b|dim\b|solarized dark|github dark|monokai|dracula|one dark/i.test(n))
44
+ return "dark";
45
+ return undefined;
46
+ }
47
+ function readColorFgbg() {
48
+ const colorfgbg = process.env.COLORFGBG;
49
+ if (!colorfgbg)
50
+ return undefined;
51
+ const parts = colorfgbg.split(";");
52
+ const bg = parts[1];
53
+ if (!bg)
54
+ return undefined;
55
+ const bgNum = parseInt(bg, 10);
56
+ if (Number.isNaN(bgNum))
57
+ return undefined;
58
+ if (bgNum === 0 || bgNum === 8)
59
+ return "dark";
60
+ if (bgNum === 7 || bgNum === 15)
61
+ return "light";
62
+ return undefined;
63
+ }
64
+ function readTerminalProfile() {
65
+ const profile = process.env.TERM_PROFILE || process.env.ITERM_PROFILE || "";
66
+ if (!profile)
67
+ return undefined;
68
+ if (/light|day|solar/i.test(profile))
69
+ return "light";
70
+ if (/dark|night|dim/i.test(profile))
71
+ return "dark";
72
+ return undefined;
73
+ }
74
+ function editorSettingsPaths() {
75
+ const home = homedir();
76
+ if (process.platform === "darwin") {
77
+ return [
78
+ join(home, "Library/Application Support/Cursor/User/settings.json"),
79
+ join(home, "Library/Application Support/Code/User/settings.json"),
80
+ ];
81
+ }
82
+ if (process.platform === "win32") {
83
+ const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
84
+ return [
85
+ join(appData, "Cursor/User/settings.json"),
86
+ join(appData, "Code/User/settings.json"),
87
+ ];
88
+ }
89
+ return [
90
+ join(home, ".config/Cursor/User/settings.json"),
91
+ join(home, ".config/Code/User/settings.json"),
92
+ ];
93
+ }
94
+ function readEditorColorTheme() {
95
+ if (process.env.TERM_PROGRAM !== "vscode")
96
+ return undefined;
97
+ for (const path of editorSettingsPaths()) {
98
+ try {
99
+ const settings = JSON.parse(readFileSync(path, "utf8"));
100
+ const theme = settings["workbench.colorTheme"];
101
+ if (typeof theme !== "string")
102
+ continue;
103
+ const inferred = inferThemeFromName(theme);
104
+ if (inferred)
105
+ return inferred;
106
+ }
107
+ catch {
108
+ // try next settings file
109
+ }
110
+ }
111
+ return undefined;
112
+ }
113
+ function readSystemAppearance() {
114
+ if (process.platform === "darwin") {
115
+ try {
116
+ const style = execSync("defaults read -g AppleInterfaceStyle 2>/dev/null", {
117
+ encoding: "utf8",
118
+ stdio: ["ignore", "pipe", "ignore"],
119
+ }).trim();
120
+ return style === "Dark" ? "dark" : "light";
121
+ }
122
+ catch {
123
+ return "light";
124
+ }
125
+ }
126
+ if (process.platform === "linux") {
127
+ try {
128
+ const scheme = execSync("gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null", {
129
+ encoding: "utf8",
130
+ stdio: ["ignore", "pipe", "ignore"],
131
+ }).trim();
132
+ if (/dark/i.test(scheme))
133
+ return "dark";
134
+ if (/light/i.test(scheme))
135
+ return "light";
136
+ }
137
+ catch {
138
+ // fall through
139
+ }
140
+ const gtk = process.env.GTK_THEME;
141
+ if (gtk && /dark/i.test(gtk))
142
+ return "dark";
143
+ }
144
+ return undefined;
145
+ }
146
+ export function detectTerminalTheme() {
147
+ return readColorFgbg()
148
+ ?? readTerminalProfile()
149
+ ?? readEditorColorTheme()
150
+ ?? readSystemAppearance()
151
+ ?? "light";
152
+ }
153
+ export function getThemeFromResolved(resolved) {
154
+ return resolved === "light" ? LIGHT_THEME : DARK_THEME;
155
+ }
156
+ export function getTheme(theme) {
157
+ const resolved = theme === "auto" ? detectTerminalTheme() : theme;
158
+ return getThemeFromResolved(resolved);
159
+ }
160
+ /** Poll editor settings + system appearance while theme mode is auto. */
161
+ export function watchAutoThemeChanges(onChange) {
162
+ const cleanups = [];
163
+ onChange();
164
+ if (process.env.TERM_PROGRAM === "vscode") {
165
+ for (const path of editorSettingsPaths()) {
166
+ try {
167
+ watchFile(path, { interval: 400 }, onChange);
168
+ cleanups.push(() => {
169
+ try {
170
+ unwatchFile(path);
171
+ }
172
+ catch { /* file may be gone */ }
173
+ });
174
+ }
175
+ catch {
176
+ // settings file not present yet
177
+ }
178
+ }
179
+ }
180
+ const id = setInterval(onChange, 1500);
181
+ cleanups.push(() => clearInterval(id));
182
+ return () => { cleanups.forEach((fn) => fn()); };
183
+ }
@@ -0,0 +1,41 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { detectTerminalTheme, getTheme, 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("black");
22
+ expect(getTheme("auto").inputText).toBe("#000000");
23
+ expect(getTheme("auto").userBandBackground).toBe("#f0f0f0");
24
+ process.env.COLORFGBG = "15;0";
25
+ expect(getTheme("auto").text).toBe("white");
26
+ expect(getTheme("auto").inputText).toBe("#ffffff");
27
+ expect(getTheme("auto").userBandBackground).toBe("ansi256(238)");
28
+ });
29
+ it("watchAutoThemeChanges fires immediately and on interval", () => {
30
+ vi.useFakeTimers();
31
+ const onChange = vi.fn();
32
+ const stop = watchAutoThemeChanges(onChange);
33
+ expect(onChange).toHaveBeenCalledTimes(1);
34
+ vi.advanceTimersByTime(1500);
35
+ expect(onChange).toHaveBeenCalledTimes(2);
36
+ stop();
37
+ vi.advanceTimersByTime(1500);
38
+ expect(onChange).toHaveBeenCalledTimes(2);
39
+ vi.useRealTimers();
40
+ });
41
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scira/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Scira — terminal-native AI research agent with grounded sources, verified claims, and local run storage.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,15 +37,14 @@
37
37
  "test:watch": "vitest"
38
38
  },
39
39
  "dependencies": {
40
- "@ai-sdk/mcp": "^1.0.47",
41
- "@ai-sdk/openai-compatible": "^2.0.48",
42
- "@ai-sdk/xai": "^3.0.93",
40
+ "@ai-sdk/mcp": "^1.0.48",
41
+ "@ai-sdk/openai-compatible": "^2.0.49",
42
+ "@ai-sdk/xai": "^3.0.94",
43
43
  "@clack/prompts": "^1.5.1",
44
44
  "@mendable/firecrawl-js": "^4.25.3",
45
45
  "@modelcontextprotocol/sdk": "^1.29.0",
46
46
  "@mozilla/readability": "^0.6.0",
47
- "ai": "^6.0.201",
48
- "bash-tool": "^1.3.17",
47
+ "ai": "^6.0.202",
49
48
  "commander": "^15.0.0",
50
49
  "diff": "^9.0.0",
51
50
  "exa-js": "^2.13.0",