@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.
- package/dist/agent/research-agent.js +4 -3
- package/dist/cli/commands/init.js +3 -1
- package/dist/cli/index.js +96 -103
- package/dist/types/index.js +2 -0
- package/dist/ui/ink/SciraApp.js +102 -13
- package/dist/ui/ink/components/home-screen.js +74 -17
- package/dist/ui/ink/components/overlays.js +85 -31
- package/dist/ui/ink/constants.js +5 -2
- package/dist/ui/ink/hooks/use-agent-turn.js +25 -8
- package/dist/ui/ink/hooks/use-feed-lines.js +65 -39
- package/dist/ui/ink/hooks/use-feed-lines.test.js +16 -0
- package/dist/ui/ink/hooks/use-feed.js +18 -18
- package/dist/ui/ink/hooks/use-keyboard.js +36 -4
- package/dist/ui/ink/hooks/use-mcp-actions.js +44 -0
- package/dist/ui/ink/hooks/use-mouse.js +1 -1
- package/dist/ui/ink/hooks/use-settings.js +16 -0
- package/dist/ui/ink/hooks/use-submit.js +2 -2
- package/dist/ui/ink/hooks/use-theme.js +1 -0
- package/dist/ui/ink/lib/markdown.js +14 -14
- package/dist/ui/ink/lib/tool-result.js +319 -0
- package/dist/ui/ink/lib/tool-result.test.js +60 -0
- package/dist/ui/ink/lib/utils.js +88 -6
- package/dist/ui/ink/lib/utils.test.js +31 -0
- package/dist/ui/ink/session-manager.js +41 -4
- package/dist/ui/ink/session-manager.test.js +31 -0
- package/dist/ui/ink/terminal-probe.js +53 -0
- package/dist/ui/ink/terminal-probe.test.js +12 -0
- package/dist/ui/ink/theme-context.js +33 -0
- package/dist/ui/ink/theme.js +183 -0
- package/dist/ui/ink/theme.test.js +41 -0
- package/package.json +5 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
3
|
import { ToolLoopAgent, isLoopFinished } from "ai";
|
|
4
|
-
import
|
|
4
|
+
import { Spinner } from "picospinner";
|
|
5
5
|
import { getLanguageModel, requireLlmKeys } from "../providers/llm/registry.js";
|
|
6
6
|
import { createResearchTools, createOneShotTools, createCodingTools } from "./tools.js";
|
|
7
7
|
import { SKILL_CATALOG } from "./skills.js";
|
|
@@ -173,7 +173,7 @@ export async function createOneShotAgent(runPath, goal, config, onApprovalRequir
|
|
|
173
173
|
* Run the research agent headlessly, streaming a compact timeline to stdout.
|
|
174
174
|
*/
|
|
175
175
|
export async function runResearchAgent(runPath, goal, config, workspacePath) {
|
|
176
|
-
const spinner =
|
|
176
|
+
const spinner = new Spinner();
|
|
177
177
|
const onApprovalRequired = async (toolName, description) => {
|
|
178
178
|
spinner.stop();
|
|
179
179
|
console.error(`\n⚠ ${toolName} needs approval`);
|
|
@@ -193,7 +193,8 @@ export async function runResearchAgent(runPath, goal, config, workspacePath) {
|
|
|
193
193
|
const result = await bundle.agent.stream({ prompt: goal });
|
|
194
194
|
for await (const part of result.fullStream) {
|
|
195
195
|
if (part.type === "tool-call") {
|
|
196
|
-
spinner.
|
|
196
|
+
spinner.setText(`${CODING_ICONS[part.toolName] ?? TOOL_ICONS[part.toolName] ?? "•"} ${part.toolName} ${summarize(part.input)}`);
|
|
197
|
+
spinner.start();
|
|
197
198
|
}
|
|
198
199
|
else if (part.type === "tool-result") {
|
|
199
200
|
spinner.succeed(`${part.toolName}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
@@ -244,6 +244,7 @@ export async function initCommand() {
|
|
|
244
244
|
let models = [];
|
|
245
245
|
try {
|
|
246
246
|
const tempConfig = {
|
|
247
|
+
theme: "auto",
|
|
247
248
|
llmProvider: llmProvider,
|
|
248
249
|
model: defaultModelFor(llmProvider),
|
|
249
250
|
lastModels: {},
|
|
@@ -327,6 +328,7 @@ export async function initCommand() {
|
|
|
327
328
|
}
|
|
328
329
|
// Write config file
|
|
329
330
|
const config = {
|
|
331
|
+
theme: existingConfig?.theme ?? "auto",
|
|
330
332
|
llmProvider: llmProvider,
|
|
331
333
|
model,
|
|
332
334
|
lastModels: { [llmProvider]: model, ...(existingConfig?.lastModels || {}) },
|
package/dist/cli/index.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
import process from "node:process";
|
|
3
|
+
if (typeof Bun === "undefined") {
|
|
4
|
+
console.error("scira requires Bun. Install it from https://bun.sh and run: bun run dist/cli/index.js");
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
3
7
|
import { readFileSync } from "node:fs";
|
|
4
8
|
import { readFile } from "node:fs/promises";
|
|
5
9
|
import { dirname, join } from "node:path";
|
|
6
10
|
import { homedir } from "node:os";
|
|
7
11
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import
|
|
12
|
+
import sade from "sade";
|
|
9
13
|
const { version: pkgVersion } = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../../package.json"), "utf8"));
|
|
10
14
|
// Load keys from the global config dir (~/.scira/.env) so they work regardless
|
|
11
15
|
// of where the CLI is invoked from (e.g. after pnpm link / global install).
|
|
@@ -29,15 +33,15 @@ import { createMcpBridge } from "../tools/mcp-bridge.js";
|
|
|
29
33
|
import { saveGlobalMcpConfig } from "../config/load-config.js";
|
|
30
34
|
import { runOAuthFlow } from "../tools/mcp-oauth.js";
|
|
31
35
|
import { initCommand } from "./commands/init.js";
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.
|
|
39
|
-
.
|
|
40
|
-
.
|
|
36
|
+
const prog = sade("scira");
|
|
37
|
+
prog
|
|
38
|
+
.version(pkgVersion)
|
|
39
|
+
.describe("Terminal-native AI research agent.");
|
|
40
|
+
prog
|
|
41
|
+
.command("*", "research question or coding task", { default: true })
|
|
42
|
+
.option("--workspace", "enable coding tools for this workspace directory")
|
|
43
|
+
.action(async (opts) => {
|
|
44
|
+
const question = opts._.length > 0 ? opts._.join(" ") : undefined;
|
|
41
45
|
const config = await loadConfig();
|
|
42
46
|
if (!question) {
|
|
43
47
|
await openTuiHome(config);
|
|
@@ -46,31 +50,30 @@ program
|
|
|
46
50
|
requireLlmKeys(config);
|
|
47
51
|
const run = await createRun(question, config);
|
|
48
52
|
console.log(`Run: ${run.path}`);
|
|
49
|
-
if (
|
|
50
|
-
console.log(`Workspace: ${
|
|
53
|
+
if (opts.workspace) {
|
|
54
|
+
console.log(`Workspace: ${opts.workspace}`);
|
|
51
55
|
}
|
|
52
56
|
console.log("");
|
|
53
|
-
await runResearchAgent(run.path, question, config,
|
|
57
|
+
await runResearchAgent(run.path, question, config, opts.workspace);
|
|
54
58
|
console.log(`\nRun complete: ${run.path}`);
|
|
55
59
|
});
|
|
56
|
-
|
|
57
|
-
.
|
|
60
|
+
prog
|
|
61
|
+
.command("init", "initialize Scira with API keys and configuration")
|
|
58
62
|
.action(async () => {
|
|
59
63
|
await initCommand();
|
|
60
64
|
});
|
|
61
|
-
|
|
62
|
-
.
|
|
63
|
-
.description("create a new interactive research run")
|
|
65
|
+
prog
|
|
66
|
+
.command("new <question>", "create a new interactive research run")
|
|
64
67
|
.option("--no-shell", "create the run without opening the interactive shell")
|
|
65
68
|
.option("--tui", "open the Ink TUI after creating the run")
|
|
66
69
|
.option("--shell", "open the classic readline shell after creating the run")
|
|
67
|
-
.action(async (question,
|
|
70
|
+
.action(async (question, opts) => {
|
|
68
71
|
const config = await loadConfig();
|
|
69
72
|
const run = await createRun(question, config);
|
|
70
|
-
if (
|
|
73
|
+
if (opts.tui) {
|
|
71
74
|
await openTui(run.path, config);
|
|
72
75
|
}
|
|
73
|
-
else if (
|
|
76
|
+
else if (opts.shell) {
|
|
74
77
|
await openShell(run.path, config);
|
|
75
78
|
}
|
|
76
79
|
else {
|
|
@@ -78,37 +81,34 @@ program.command("new")
|
|
|
78
81
|
console.log(`Open TUI: scira resume --tui ${run.id}`);
|
|
79
82
|
}
|
|
80
83
|
});
|
|
81
|
-
|
|
82
|
-
.
|
|
83
|
-
.description("resume an existing run")
|
|
84
|
+
prog
|
|
85
|
+
.command("resume <run-id>", "resume an existing run")
|
|
84
86
|
.option("--shell", "resume in the classic readline shell")
|
|
85
87
|
.option("--tui", "resume in the Ink TUI")
|
|
86
|
-
.action(async (runId,
|
|
88
|
+
.action(async (runId, opts) => {
|
|
87
89
|
const config = await loadConfig();
|
|
88
90
|
const runPath = await findRun(runId, config);
|
|
89
|
-
if (
|
|
91
|
+
if (opts.shell) {
|
|
90
92
|
await openShell(runPath, config);
|
|
91
93
|
}
|
|
92
94
|
else {
|
|
93
95
|
await openTui(runPath, config);
|
|
94
96
|
}
|
|
95
97
|
});
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
+
prog
|
|
99
|
+
.command("list", "list runs")
|
|
98
100
|
.action(async () => {
|
|
99
101
|
const config = await loadConfig();
|
|
100
102
|
console.table(await listRuns(config));
|
|
101
103
|
});
|
|
102
|
-
|
|
103
|
-
.
|
|
104
|
-
.description("show run status")
|
|
104
|
+
prog
|
|
105
|
+
.command("show <run-id>", "show run status")
|
|
105
106
|
.action(async (runId) => {
|
|
106
107
|
const config = await loadConfig();
|
|
107
108
|
console.log(await summarizeRun(await findRun(runId, config)));
|
|
108
109
|
});
|
|
109
|
-
|
|
110
|
-
.
|
|
111
|
-
.description("run (or re-run) the research agent on an existing run")
|
|
110
|
+
prog
|
|
111
|
+
.command("run <run-id>", "run (or re-run) the research agent on an existing run")
|
|
112
112
|
.action(async (runId) => {
|
|
113
113
|
const config = await loadConfig();
|
|
114
114
|
requireLlmKeys(config);
|
|
@@ -117,23 +117,21 @@ program.command("run")
|
|
|
117
117
|
await runResearchAgent(runPath, goal, config);
|
|
118
118
|
console.log(`\nRun complete: ${runPath}`);
|
|
119
119
|
});
|
|
120
|
-
|
|
121
|
-
.
|
|
122
|
-
.description("show the verification report for a run's claims")
|
|
120
|
+
prog
|
|
121
|
+
.command("verify <run-id>", "show the verification report for a run's claims")
|
|
123
122
|
.action(async (runId) => {
|
|
124
123
|
const config = await loadConfig();
|
|
125
124
|
const runPath = await findRun(runId, config);
|
|
126
125
|
console.log(await verificationReport(runPath));
|
|
127
126
|
});
|
|
128
|
-
|
|
129
|
-
.
|
|
130
|
-
.option("--format
|
|
131
|
-
.option("--output
|
|
132
|
-
.
|
|
133
|
-
.
|
|
134
|
-
const fmt = options.format.toLowerCase();
|
|
127
|
+
prog
|
|
128
|
+
.command("export <run-id>", "export run report (md, json, or csv)")
|
|
129
|
+
.option("--format", "export format: md | json | csv", "md")
|
|
130
|
+
.option("--output", "write to file instead of stdout")
|
|
131
|
+
.action(async (runId, opts) => {
|
|
132
|
+
const fmt = opts.format.toLowerCase();
|
|
135
133
|
if (!["md", "json", "csv"].includes(fmt)) {
|
|
136
|
-
throw new Error(`Unknown format "${
|
|
134
|
+
throw new Error(`Unknown format "${opts.format}". Supported: md, json, csv.`);
|
|
137
135
|
}
|
|
138
136
|
const config = await loadConfig();
|
|
139
137
|
const runPath = await findRun(runId, config);
|
|
@@ -152,20 +150,19 @@ program.command("export")
|
|
|
152
150
|
const bundle = { runId, goal, sources, claims };
|
|
153
151
|
output = fmt === "json" ? toJson(bundle) : toCsv(bundle);
|
|
154
152
|
}
|
|
155
|
-
if (
|
|
153
|
+
if (opts.output) {
|
|
156
154
|
const { writeFile, mkdir } = await import("node:fs/promises");
|
|
157
155
|
const { dirname } = await import("node:path");
|
|
158
|
-
await mkdir(dirname(
|
|
159
|
-
await writeFile(
|
|
160
|
-
console.log(`Exported to ${
|
|
156
|
+
await mkdir(dirname(opts.output), { recursive: true });
|
|
157
|
+
await writeFile(opts.output, output, "utf8");
|
|
158
|
+
console.log(`Exported to ${opts.output}`);
|
|
161
159
|
}
|
|
162
160
|
else {
|
|
163
161
|
console.log(output);
|
|
164
162
|
}
|
|
165
163
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.description("list configured MCP servers")
|
|
164
|
+
prog
|
|
165
|
+
.command("mcp list", "list configured MCP servers")
|
|
169
166
|
.action(async () => {
|
|
170
167
|
const config = await loadConfig();
|
|
171
168
|
const dt = config.mcp.chromeDevtools;
|
|
@@ -177,22 +174,19 @@ mcp.command("list")
|
|
|
177
174
|
console.log(`${s.name} [${s.transport}]${authLabel}${oauthStatus} ${s.enabled ? "enabled" : "disabled"} ${loc}`);
|
|
178
175
|
}
|
|
179
176
|
});
|
|
180
|
-
|
|
181
|
-
.
|
|
182
|
-
.
|
|
183
|
-
.
|
|
184
|
-
.argument("[args...]")
|
|
185
|
-
.option("--bearer <token>", "bearer token for Authorization header")
|
|
186
|
-
.option("--header <name:value>", "custom header in name:value format")
|
|
177
|
+
prog
|
|
178
|
+
.command("mcp add <transport> <name> <target> [args...]", "add an MCP server")
|
|
179
|
+
.option("--bearer", "bearer token for Authorization header")
|
|
180
|
+
.option("--header", "custom header in name:value format")
|
|
187
181
|
.option("--oauth", "use OAuth PKCE flow (requires --oauth-client-id)")
|
|
188
|
-
.option("--oauth-client-id
|
|
189
|
-
.option("--oauth-client-secret
|
|
190
|
-
.option("--oauth-issuer
|
|
191
|
-
.option("--oauth-auth-url
|
|
192
|
-
.option("--oauth-token-url
|
|
193
|
-
.option("--oauth-scopes
|
|
194
|
-
.description("add an MCP server")
|
|
182
|
+
.option("--oauth-client-id", "OAuth client ID")
|
|
183
|
+
.option("--oauth-client-secret", "OAuth client secret (optional for PKCE)")
|
|
184
|
+
.option("--oauth-issuer", "OAuth issuer URL for auto-discovery")
|
|
185
|
+
.option("--oauth-auth-url", "OAuth authorization endpoint URL")
|
|
186
|
+
.option("--oauth-token-url", "OAuth token endpoint URL")
|
|
187
|
+
.option("--oauth-scopes", "OAuth scopes (space-separated)")
|
|
195
188
|
.action(async (transport, name, target, args, opts) => {
|
|
189
|
+
const restArgs = Array.isArray(args) ? args : args ? [args] : [];
|
|
196
190
|
if (!["stdio", "sse", "http"].includes(transport)) {
|
|
197
191
|
throw new Error("transport must be one of: stdio, sse, http");
|
|
198
192
|
}
|
|
@@ -230,7 +224,7 @@ mcp.command("add")
|
|
|
230
224
|
oauthScopes: opts.oauthScopes,
|
|
231
225
|
};
|
|
232
226
|
const entry = transport === "stdio"
|
|
233
|
-
? { ...base, transport: "stdio", command: target, args }
|
|
227
|
+
? { ...base, transport: "stdio", command: target, args: restArgs }
|
|
234
228
|
: { ...base, transport: transport, url: target, args: [] };
|
|
235
229
|
const nextMcp = { ...config.mcp, servers: [...config.mcp.servers, entry] };
|
|
236
230
|
await saveGlobalMcpConfig(nextMcp);
|
|
@@ -239,9 +233,8 @@ mcp.command("add")
|
|
|
239
233
|
console.log(`Run: scira mcp oauth ${name} to authenticate`);
|
|
240
234
|
}
|
|
241
235
|
});
|
|
242
|
-
|
|
243
|
-
.
|
|
244
|
-
.description("run OAuth PKCE flow for an MCP server")
|
|
236
|
+
prog
|
|
237
|
+
.command("mcp oauth <name>", "run OAuth PKCE flow for an MCP server")
|
|
245
238
|
.action(async (name) => {
|
|
246
239
|
const config = await loadConfig();
|
|
247
240
|
const srv = config.mcp.servers.find((s) => s.name === name);
|
|
@@ -251,9 +244,8 @@ mcp.command("oauth")
|
|
|
251
244
|
throw new Error(`"${name}" is not an OAuth server (authType: ${srv.authType})`);
|
|
252
245
|
await runOAuthFlow(srv, config);
|
|
253
246
|
});
|
|
254
|
-
|
|
255
|
-
.
|
|
256
|
-
.description("enable an MCP server")
|
|
247
|
+
prog
|
|
248
|
+
.command("mcp enable <name>", "enable an MCP server")
|
|
257
249
|
.action(async (name) => {
|
|
258
250
|
const config = await loadConfig();
|
|
259
251
|
const nextMcp = name === "chromeDevtools" || name === "devtools"
|
|
@@ -262,9 +254,8 @@ mcp.command("enable")
|
|
|
262
254
|
await saveGlobalMcpConfig(nextMcp);
|
|
263
255
|
console.log(`Enabled MCP server "${name}"`);
|
|
264
256
|
});
|
|
265
|
-
|
|
266
|
-
.
|
|
267
|
-
.description("disable an MCP server")
|
|
257
|
+
prog
|
|
258
|
+
.command("mcp disable <name>", "disable an MCP server")
|
|
268
259
|
.action(async (name) => {
|
|
269
260
|
const config = await loadConfig();
|
|
270
261
|
const nextMcp = name === "chromeDevtools" || name === "devtools"
|
|
@@ -273,9 +264,8 @@ mcp.command("disable")
|
|
|
273
264
|
await saveGlobalMcpConfig(nextMcp);
|
|
274
265
|
console.log(`Disabled MCP server "${name}"`);
|
|
275
266
|
});
|
|
276
|
-
|
|
277
|
-
.
|
|
278
|
-
.description("remove an MCP server from config")
|
|
267
|
+
prog
|
|
268
|
+
.command("mcp remove <name>", "remove an MCP server from config")
|
|
279
269
|
.action(async (name) => {
|
|
280
270
|
const config = await loadConfig();
|
|
281
271
|
if (!config.mcp.servers.some((s) => s.name === name)) {
|
|
@@ -285,63 +275,63 @@ mcp.command("remove")
|
|
|
285
275
|
await saveGlobalMcpConfig(nextMcp);
|
|
286
276
|
console.log(`Removed MCP server "${name}" from ~/.scira/config.json`);
|
|
287
277
|
});
|
|
288
|
-
|
|
289
|
-
.
|
|
278
|
+
prog
|
|
279
|
+
.command("watch <goal>", "monitor a topic by running research on a schedule and diffing reports")
|
|
290
280
|
.option("--daily", "run once per day (default)")
|
|
291
281
|
.option("--hourly", "run once per hour")
|
|
292
282
|
.option("--weekly", "run once per week")
|
|
293
|
-
.option("--interval
|
|
294
|
-
.option("--runs
|
|
295
|
-
.
|
|
296
|
-
.action(async (goal, options) => {
|
|
283
|
+
.option("--interval", "custom interval in milliseconds")
|
|
284
|
+
.option("--runs", "stop after N runs (default: run forever)")
|
|
285
|
+
.action(async (goal, opts) => {
|
|
297
286
|
const config = await loadConfig();
|
|
298
287
|
const INTERVALS = {
|
|
299
288
|
hourly: 60 * 60 * 1000,
|
|
300
289
|
daily: 24 * 60 * 60 * 1000,
|
|
301
290
|
weekly: 7 * 24 * 60 * 60 * 1000,
|
|
302
291
|
};
|
|
303
|
-
const intervalMs =
|
|
304
|
-
? parseInt(
|
|
305
|
-
:
|
|
306
|
-
:
|
|
292
|
+
const intervalMs = opts.interval
|
|
293
|
+
? parseInt(String(opts.interval), 10)
|
|
294
|
+
: opts.hourly ? INTERVALS.hourly
|
|
295
|
+
: opts.weekly ? INTERVALS.weekly
|
|
307
296
|
: INTERVALS.daily;
|
|
308
297
|
if (Number.isNaN(intervalMs) || intervalMs < 1000) {
|
|
309
298
|
throw new Error("Interval must be at least 1000 ms.");
|
|
310
299
|
}
|
|
300
|
+
const maxRuns = opts.runs != null ? parseInt(String(opts.runs), 10) : undefined;
|
|
311
301
|
const { watchLoop } = await import("../watch/runner.js");
|
|
312
302
|
const controller = new AbortController();
|
|
313
303
|
process.on("SIGINT", () => { console.log("\nStopping watch…"); controller.abort(); });
|
|
314
304
|
process.on("SIGTERM", () => { controller.abort(); });
|
|
315
305
|
console.log(`Watching: "${goal}"`);
|
|
316
|
-
console.log(`Interval: ${intervalMs / 1000}s${
|
|
306
|
+
console.log(`Interval: ${intervalMs / 1000}s${maxRuns ? ` · max ${maxRuns} runs` : ""}`);
|
|
317
307
|
console.log("Press Ctrl-C to stop.\n");
|
|
318
308
|
await watchLoop({
|
|
319
|
-
goal, intervalMs, maxRuns
|
|
309
|
+
goal, intervalMs, maxRuns, config,
|
|
320
310
|
onRunStart: (runPath, tick) => { console.log(`\n[tick ${tick + 1}] Starting run → ${runPath}`); },
|
|
321
311
|
onRunComplete: (runPath, diffText, tick) => { console.log(`[tick ${tick + 1}] Done. Diff:\n${diffText}`); },
|
|
322
312
|
onError: (err, tick) => { console.error(`[tick ${tick + 1}] Error: ${err.message}`); },
|
|
323
313
|
}, controller.signal);
|
|
324
314
|
console.log("Watch finished.");
|
|
325
315
|
});
|
|
326
|
-
|
|
327
|
-
.
|
|
328
|
-
.
|
|
329
|
-
.action(async (
|
|
316
|
+
prog
|
|
317
|
+
.command("models", "list models for the configured LLM provider")
|
|
318
|
+
.option("--provider", "gateway only: filter by model prefix such as anthropic, openai, or google")
|
|
319
|
+
.action(async (opts) => {
|
|
330
320
|
const config = await loadConfig();
|
|
331
|
-
const models = config.llmProvider === "gateway" &&
|
|
332
|
-
? await listGatewayModels(
|
|
321
|
+
const models = config.llmProvider === "gateway" && opts.provider
|
|
322
|
+
? await listGatewayModels(opts.provider)
|
|
333
323
|
: await listModels(config);
|
|
334
324
|
for (const model of models) {
|
|
335
325
|
console.log(model.id);
|
|
336
326
|
}
|
|
337
327
|
});
|
|
338
|
-
|
|
339
|
-
.
|
|
328
|
+
prog
|
|
329
|
+
.command("config", "print resolved config")
|
|
340
330
|
.action(async () => {
|
|
341
331
|
console.log(JSON.stringify(await loadConfig(), null, 2));
|
|
342
332
|
});
|
|
343
|
-
|
|
344
|
-
.
|
|
333
|
+
prog
|
|
334
|
+
.command("doctor", "check local setup")
|
|
345
335
|
.action(async () => {
|
|
346
336
|
const config = await loadConfig();
|
|
347
337
|
const nodeCheck = checkNodeVersion(20);
|
|
@@ -437,7 +427,10 @@ async function commandResolves(command) {
|
|
|
437
427
|
}
|
|
438
428
|
}
|
|
439
429
|
try {
|
|
440
|
-
|
|
430
|
+
const parsed = prog.parse(process.argv, { lazy: true });
|
|
431
|
+
if (parsed?.handler) {
|
|
432
|
+
await parsed.handler(...parsed.args);
|
|
433
|
+
}
|
|
441
434
|
}
|
|
442
435
|
catch (error) {
|
|
443
436
|
console.error(error instanceof Error ? error.message : String(error));
|
package/dist/types/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const ApprovalModeSchema = z.enum(["manual", "suggest", "auto"]);
|
|
3
|
+
export const ThemeSchema = z.enum(["dark", "light", "auto"]).default("auto");
|
|
3
4
|
export const SciraConfigSchema = z.object({
|
|
5
|
+
theme: ThemeSchema,
|
|
4
6
|
llmProvider: z.enum(["gateway", "xai", "workers-ai", "huggingface"]).default("gateway"),
|
|
5
7
|
model: z.string().default("deepseek/deepseek-v4-flash"),
|
|
6
8
|
// last selected model per LLM provider, restored when switching back
|
package/dist/ui/ink/SciraApp.js
CHANGED
|
@@ -6,8 +6,10 @@ import { CWD_DISPLAY, wrapText, wrapInputWithCursor, loadInputHistory, saveInput
|
|
|
6
6
|
import { deleteRun } from "../../storage/run-store.js";
|
|
7
7
|
import { useMountEffect, TipCycler, AnimationTick, MouseTracker } from "./components/effects.js";
|
|
8
8
|
import { useFeedLines, computeGroups } from "./hooks/use-feed-lines.js";
|
|
9
|
+
import { feedToolItemId, isToolItemCollapsed } from "./lib/tool-result.js";
|
|
9
10
|
import { useAgentTurn } from "./hooks/use-agent-turn.js";
|
|
10
|
-
import { TopBar, InputBar, HintLine, CommandMenuBox, HelpBox, ApprovalBox, MenuDialog, McpDialog } from "./components/overlays.js";
|
|
11
|
+
import { TopBar, InputBar, HintLine, CommandMenuBox, HelpBox, ApprovalBox, MenuDialog, McpDialog, buildMcpDialogRows } from "./components/overlays.js";
|
|
12
|
+
import { useMcpActions } from "./hooks/use-mcp-actions.js";
|
|
11
13
|
import { useKeyboard } from "./hooks/use-keyboard.js";
|
|
12
14
|
import { HomeScreen } from "./components/home-screen.js";
|
|
13
15
|
import { useFeed } from "./hooks/use-feed.js";
|
|
@@ -16,6 +18,7 @@ import { useSuggestions } from "./hooks/use-suggestions.js";
|
|
|
16
18
|
import { useSubmit } from "./hooks/use-submit.js";
|
|
17
19
|
import { useSession } from "./hooks/use-session.js";
|
|
18
20
|
import { useMouse } from "./hooks/use-mouse.js";
|
|
21
|
+
import { ThemeProvider, useTheme } from "./hooks/use-theme.js";
|
|
19
22
|
export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
20
23
|
const { exit } = useApp();
|
|
21
24
|
const { stdout } = useStdout();
|
|
@@ -36,6 +39,7 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
36
39
|
const [notice, setNotice] = useState("");
|
|
37
40
|
const [pendingRerun, setPendingRerun] = useState(false);
|
|
38
41
|
const [mcpOpen, setMcpOpen] = useState(false);
|
|
42
|
+
const [mcpRowIdx, setMcpRowIdx] = useState(0);
|
|
39
43
|
const [sessions, setSessions] = useState([]);
|
|
40
44
|
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
41
45
|
const [sessionsModalOpen, setSessionsModalOpen] = useState(false);
|
|
@@ -68,7 +72,7 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
68
72
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
69
73
|
const [heroHidden, setHeroHidden] = useState(false);
|
|
70
74
|
const [busy, setBusy] = useState(false);
|
|
71
|
-
const [
|
|
75
|
+
const [, setBlink] = useState(true);
|
|
72
76
|
const [frame, setFrame] = useState(0);
|
|
73
77
|
const [reasoningTick, setReasoningTick] = useState(0);
|
|
74
78
|
const [commandMenuIndex, setCommandMenuIndex] = useState(0);
|
|
@@ -77,6 +81,21 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
77
81
|
const [collapsedGroups, setCollapsedGroups] = useState(new Set());
|
|
78
82
|
const [focusedGroupKey, setFocusedGroupKey] = useState(null);
|
|
79
83
|
const [pendingCollapse, setPendingCollapse] = useState(new Set());
|
|
84
|
+
const [itemExpandState, setItemExpandState] = useState(new Map());
|
|
85
|
+
React.useEffect(() => {
|
|
86
|
+
setItemExpandState(new Map());
|
|
87
|
+
}, [currentRunPath]);
|
|
88
|
+
const toggleToolItem = useCallback((id) => {
|
|
89
|
+
setItemExpandState((prev) => {
|
|
90
|
+
const tool = feed.find((it, i) => it.kind === "tool" && feedToolItemId(i, it.toolCallId) === id);
|
|
91
|
+
if (tool?.kind !== "tool")
|
|
92
|
+
return prev;
|
|
93
|
+
const next = new Map(prev);
|
|
94
|
+
const collapsed = isToolItemCollapsed(id, tool.name, tool.status, prev);
|
|
95
|
+
next.set(id, collapsed);
|
|
96
|
+
return next;
|
|
97
|
+
});
|
|
98
|
+
}, [feed]);
|
|
80
99
|
const doneGroupKeys = useMemo(() => {
|
|
81
100
|
const { groups } = computeGroups(feed);
|
|
82
101
|
return [...groups.entries()].filter(([, g]) => !g.active).map(([k]) => k).sort((a, b) => a - b);
|
|
@@ -96,18 +115,21 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
96
115
|
}));
|
|
97
116
|
});
|
|
98
117
|
}, [doneGroupKeys, feed]);
|
|
99
|
-
const
|
|
100
|
-
if (focusedGroupKey === null)
|
|
101
|
-
return;
|
|
118
|
+
const toggleGroup = useCallback((groupKey) => {
|
|
102
119
|
setCollapsedGroups((prev) => {
|
|
103
120
|
const next = new Set(prev);
|
|
104
|
-
if (next.has(
|
|
105
|
-
next.delete(
|
|
121
|
+
if (next.has(groupKey))
|
|
122
|
+
next.delete(groupKey);
|
|
106
123
|
else
|
|
107
|
-
next.add(
|
|
124
|
+
next.add(groupKey);
|
|
108
125
|
return next;
|
|
109
126
|
});
|
|
110
|
-
}, [
|
|
127
|
+
}, []);
|
|
128
|
+
const toggleFocusedGroup = useCallback(() => {
|
|
129
|
+
if (focusedGroupKey === null)
|
|
130
|
+
return;
|
|
131
|
+
toggleGroup(focusedGroupKey);
|
|
132
|
+
}, [focusedGroupKey, toggleGroup]);
|
|
111
133
|
// Auto-collapse groups when they become inactive if they're in pendingCollapse
|
|
112
134
|
React.useEffect(() => {
|
|
113
135
|
if (pendingCollapse.size === 0)
|
|
@@ -212,6 +234,48 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
212
234
|
const { menu, setMenu, modelName, openMenu, applyMenuSelection, handleSettings } = useSettings({
|
|
213
235
|
config, setConfig, screen, pushFeed, setNotice,
|
|
214
236
|
});
|
|
237
|
+
const notifyMcp = useCallback((message) => {
|
|
238
|
+
if (screen === "chat")
|
|
239
|
+
pushFeed({ kind: "status", text: message });
|
|
240
|
+
else
|
|
241
|
+
setNotice(message);
|
|
242
|
+
}, [screen, pushFeed]);
|
|
243
|
+
const { toggleMcp, removeMcp } = useMcpActions(config, setConfig, notifyMcp);
|
|
244
|
+
const mcpRows = useMemo(() => buildMcpDialogRows(config), [config]);
|
|
245
|
+
const mcpRowCount = mcpRows.length;
|
|
246
|
+
const toggleMcpRow = useCallback((idx) => {
|
|
247
|
+
const row = mcpRows[idx];
|
|
248
|
+
if (!row)
|
|
249
|
+
return;
|
|
250
|
+
void toggleMcp(row.key === "chromeDevtools"
|
|
251
|
+
? { kind: "devtools" }
|
|
252
|
+
: { kind: "server", name: row.name });
|
|
253
|
+
}, [mcpRows, toggleMcp]);
|
|
254
|
+
const removeMcpRow = useCallback((idx) => {
|
|
255
|
+
const row = mcpRows[idx];
|
|
256
|
+
if (!row?.removable)
|
|
257
|
+
return;
|
|
258
|
+
void removeMcp(row.name);
|
|
259
|
+
setMcpRowIdx((i) => Math.max(0, Math.min(i, mcpRowCount - 2)));
|
|
260
|
+
}, [mcpRows, mcpRowCount, removeMcp]);
|
|
261
|
+
const handleMcpToggle = useCallback((row) => {
|
|
262
|
+
void toggleMcp(row.key === "chromeDevtools"
|
|
263
|
+
? { kind: "devtools" }
|
|
264
|
+
: { kind: "server", name: row.name });
|
|
265
|
+
}, [toggleMcp]);
|
|
266
|
+
const handleMcpRemove = useCallback((row) => {
|
|
267
|
+
if (!row.removable)
|
|
268
|
+
return;
|
|
269
|
+
void removeMcp(row.name);
|
|
270
|
+
setMcpRowIdx((i) => Math.max(0, Math.min(i, mcpRowCount - 2)));
|
|
271
|
+
}, [mcpRowCount, removeMcp]);
|
|
272
|
+
React.useEffect(() => {
|
|
273
|
+
if (mcpOpen)
|
|
274
|
+
setMcpRowIdx(0);
|
|
275
|
+
}, [mcpOpen]);
|
|
276
|
+
React.useEffect(() => {
|
|
277
|
+
setMcpRowIdx((i) => Math.min(i, Math.max(0, mcpRowCount - 1)));
|
|
278
|
+
}, [mcpRowCount]);
|
|
215
279
|
const { runTurn } = useAgentTurn({
|
|
216
280
|
config, currentRunPath, queuedPromptRef, fullModeRef, conversationRef, turnsRef, feedRef,
|
|
217
281
|
setBusy, setScrollOffset, refreshRun, recordUsage, setMode, getSubscriber,
|
|
@@ -246,12 +310,29 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
246
310
|
const menuHeight = commandMenuHeight + helpHeight + approvalHeight;
|
|
247
311
|
const feedRows = Math.max(3, rows - 6 - inputLines.length - menuHeight);
|
|
248
312
|
const hasRunningTool = feed.some((it) => it.kind === "tool" && it.status === "running");
|
|
249
|
-
const feedLines = useFeedLines(feed, innerWidth, reasoningTick, hasRunningTool ? frame : 0, collapsedGroups, focusedGroupKey);
|
|
313
|
+
const { lines: feedLines, toggleAtLine, groupToggleAtLine } = useFeedLines(feed, innerWidth, reasoningTick, hasRunningTool ? frame : 0, collapsedGroups, focusedGroupKey, itemExpandState, hoveredIdx, config);
|
|
250
314
|
const contentRows = Math.max(1, feedRows);
|
|
251
315
|
const maxScrollOffset = Math.max(0, feedLines.length - contentRows);
|
|
252
316
|
wheelStateRef.current = { screen, maxScrollOffset };
|
|
253
317
|
const clampedOffset = Math.min(scrollOffset, maxScrollOffset);
|
|
254
318
|
const startIdx = Math.max(0, feedLines.length - contentRows - clampedOffset);
|
|
319
|
+
const feedStartRow = 3;
|
|
320
|
+
if (screen === "chat") {
|
|
321
|
+
const clickMap = new Map();
|
|
322
|
+
const hoverMap = new Map();
|
|
323
|
+
const registerLine = (lineIdx, onClick) => {
|
|
324
|
+
const vis = lineIdx - startIdx;
|
|
325
|
+
if (vis < 0 || vis >= contentRows)
|
|
326
|
+
return;
|
|
327
|
+
const row = feedStartRow + vis;
|
|
328
|
+
clickMap.set(row, onClick);
|
|
329
|
+
hoverMap.set(row, lineIdx);
|
|
330
|
+
};
|
|
331
|
+
toggleAtLine.forEach((id, lineIdx) => registerLine(lineIdx, () => toggleToolItem(id)));
|
|
332
|
+
groupToggleAtLine.forEach((groupKey, lineIdx) => registerLine(lineIdx, () => toggleGroup(groupKey)));
|
|
333
|
+
clickMapRef.current = clickMap;
|
|
334
|
+
hoverMapRef.current = hoverMap;
|
|
335
|
+
}
|
|
255
336
|
const visibleLines = feedLines.slice(startIdx, startIdx + contentRows);
|
|
256
337
|
const scrollLabel = clampedOffset > 0
|
|
257
338
|
? (startIdx > 0 ? `↑ ${startIdx} · ↓ ${clampedOffset} · wheel/⇞⇟` : `top · ↓ ${clampedOffset} · wheel/⇞⇟`)
|
|
@@ -261,14 +342,22 @@ export function SciraApp({ runPath: initialRunPath, config: initialConfig }) {
|
|
|
261
342
|
setNotice,
|
|
262
343
|
exit,
|
|
263
344
|
input: { text: inputText, setText: setInputText, cursorPos, setCursorPos, history: inputHistory, historyIndex, setHistoryIndex },
|
|
264
|
-
dialogs: {
|
|
345
|
+
dialogs: {
|
|
346
|
+
approvalPending, setApprovalPending, menu, setMenu, applyMenuSelection, helpOpen, setHelpOpen,
|
|
347
|
+
mcpOpen, setMcpOpen, mcpRowIdx, setMcpRowIdx, mcpRowCount, toggleMcpRow, removeMcpRow,
|
|
348
|
+
},
|
|
265
349
|
suggestions: { activeSuggestions, activeSuggestionKind, commandMenuIndex, setCommandMenuIndex, acceptActiveSuggestion },
|
|
266
350
|
chat: { setScrollOffset, contentRows, maxScrollOffset, pendingRerun, setPendingRerun, busy, stopTurn, submitChat, toggleAllGroups, toggleFocusedGroup, focusPrevGroup, focusNextGroup, unfocusGroup, hasFocusedGroup: focusedGroupKey !== null },
|
|
267
351
|
home: { sessionsModalOpen, setSessionsModalOpen, sessionsModalIdx, setSessionsModalIdx, sessions, deleteSession, selectedIdx, setSelectedIdx, setHeroHidden, openRun, submitHome },
|
|
268
352
|
});
|
|
269
353
|
const activeUsage = usage[config.model];
|
|
354
|
+
const themed = (node) => (_jsx(ThemeProvider, { config: config, stdin: stdin, stdout: stdout, children: node }));
|
|
270
355
|
if (screen === "home") {
|
|
271
|
-
return (_jsxs(Box, { flexDirection: "column", width: cols, height: rows, paddingX: 2, children: [_jsx(TipCycler, { setTipIndex: setTipIndex }), !sessionsModalOpen && stdout !== undefined && stdin !== undefined && (_jsx(MouseTracker, { stdout: stdout, stdin: stdin, onData: handleMouseData, onUnmount: () => setHoveredIdx(null) })), busy && _jsx(AnimationTick, { setBlink: setBlink, setFrame: setFrame, setReasoningTick: setReasoningTick }), _jsx(TopBar, { screen: screen, runState: runState, fullMode: fullMode, activeUsage: activeUsage, busy: busy, frame: frame, cwdDisplay: CWD_DISPLAY }), _jsx(HomeScreen, { cols: cols, rows: rows, sessions: sessions, selectedIdx: selectedIdx, hoveredIdx: hoveredIdx, heroHidden: heroHidden, notice: notice, tipIndex: tipIndex, commandMenuHeight: commandMenuHeight, sessionsModalOpen: sessionsModalOpen, sessionsModalIdx: sessionsModalIdx, inputText: inputText, clickMapRef: clickMapRef, hoverMapRef: hoverMapRef, setSelectedIdx: setSelectedIdx, setSessionsModalOpen: setSessionsModalOpen, setSessionsModalIdx: setSessionsModalIdx, setNotice: setNotice, openRun: openRun, submitHome: submitHome, exit: exit }), _jsxs(Box, { flexDirection: "column",
|
|
356
|
+
return themed(_jsxs(Box, { flexDirection: "column", width: cols, height: rows, paddingX: 2, children: [_jsx(TipCycler, { setTipIndex: setTipIndex }), (!sessionsModalOpen || mcpOpen) && stdout !== undefined && stdin !== undefined && (_jsx(MouseTracker, { stdout: stdout, stdin: stdin, onData: handleMouseData, onUnmount: () => setHoveredIdx(null) })), busy && _jsx(AnimationTick, { setBlink: setBlink, setFrame: setFrame, setReasoningTick: setReasoningTick }), _jsx(TopBar, { screen: screen, runState: runState, fullMode: fullMode, activeUsage: activeUsage, busy: busy, frame: frame, cwdDisplay: CWD_DISPLAY, config: config }), _jsx(HomeScreen, { cols: cols, rows: rows, sessions: sessions, selectedIdx: selectedIdx, hoveredIdx: hoveredIdx, heroHidden: heroHidden, notice: notice, tipIndex: tipIndex, commandMenuHeight: commandMenuHeight, mcpOpen: mcpOpen, sessionsModalOpen: sessionsModalOpen, sessionsModalIdx: sessionsModalIdx, inputText: inputText, config: config, modelName: modelName, clickMapRef: clickMapRef, hoverMapRef: hoverMapRef, setSelectedIdx: setSelectedIdx, setSessionsModalOpen: setSessionsModalOpen, setSessionsModalIdx: setSessionsModalIdx, setNotice: setNotice, openRun: openRun, submitHome: submitHome, exit: exit }), _jsxs(Box, { flexDirection: "column", paddingBottom: 1, children: [_jsx(CommandMenuBox, { activeSuggestions: activeSuggestions, activeSuggestionKind: activeSuggestionKind, commandMenuIndex: commandMenuIndex, innerWidth: innerWidth, sessions: sessions, config: config }), _jsx(InputBar, { inputLines: inputLines, cursorLine: cursorLine, cursorCol: cursorCol, showCursor: showCursor, approvalPending: !!approvalPending, busy: busy, frame: frame, boxWidth: boxWidth, modelName: modelName, config: config }), _jsx(HintLine, { screen: screen, busy: busy, config: config })] }), _jsx(MenuDialog, { menu: menu, cols: cols, rows: rows, config: config }), _jsx(McpDialog, { open: mcpOpen, config: config, cols: cols, rows: rows, selectedIdx: mcpRowIdx, hoveredIdx: hoveredIdx, onToggle: handleMcpToggle, onRemove: handleMcpRemove, clickMapRef: clickMapRef, hoverMapRef: hoverMapRef })] }));
|
|
272
357
|
}
|
|
273
|
-
return (_jsxs(Box, { flexDirection: "column", width: cols, height: rows, paddingX: 2, children: [stdout !== undefined && stdin !== undefined && (_jsx(MouseTracker, { stdout: stdout, stdin: stdin, onData: handleMouseData, onUnmount: () => setHoveredIdx(null) })), busy && _jsx(AnimationTick, { setBlink: setBlink, setFrame: setFrame, setReasoningTick: setReasoningTick }), _jsx(TopBar, { screen: screen, runState: runState, fullMode: fullMode, activeUsage: activeUsage, busy: busy, frame: frame, cwdDisplay: CWD_DISPLAY }), _jsx(Box, { flexDirection: "column", flexGrow: 1, paddingTop: 1, overflow: "hidden", children: visibleLines }), _jsxs(
|
|
358
|
+
return themed(_jsxs(Box, { flexDirection: "column", width: cols, height: rows, paddingX: 2, children: [stdout !== undefined && stdin !== undefined && (_jsx(MouseTracker, { stdout: stdout, stdin: stdin, onData: handleMouseData, onUnmount: () => setHoveredIdx(null) })), busy && _jsx(AnimationTick, { setBlink: setBlink, setFrame: setFrame, setReasoningTick: setReasoningTick }), _jsx(TopBar, { screen: screen, runState: runState, fullMode: fullMode, activeUsage: activeUsage, busy: busy, frame: frame, cwdDisplay: CWD_DISPLAY, config: config }), _jsx(Box, { flexDirection: "column", flexGrow: 1, paddingTop: 1, overflow: "hidden", children: visibleLines }), _jsxs(ChatInputChrome, { children: [_jsx(CommandMenuBox, { activeSuggestions: activeSuggestions, activeSuggestionKind: activeSuggestionKind, commandMenuIndex: commandMenuIndex, innerWidth: innerWidth, sessions: sessions, config: config }), _jsx(HelpBox, { open: helpOpen, innerWidth: innerWidth, config: config }), approvalPending && _jsx(ApprovalBox, { toolName: approvalPending.toolName, description: approvalPending.description, innerWidth: innerWidth, config: config }), _jsx(InputBar, { inputLines: inputLines, cursorLine: cursorLine, cursorCol: cursorCol, showCursor: showCursor, approvalPending: !!approvalPending, busy: busy, frame: frame, boxWidth: boxWidth, modelName: modelName, config: config }), _jsx(HintLine, { screen: screen, busy: busy, scrollLabel: scrollLabel, hasDoneGroups: doneGroupKeys.length > 0, hasFocusedGroup: focusedGroupKey !== null, config: config })] }), _jsx(MenuDialog, { menu: menu, cols: cols, rows: rows, config: config }), _jsx(McpDialog, { open: mcpOpen, config: config, cols: cols, rows: rows, selectedIdx: mcpRowIdx, hoveredIdx: hoveredIdx, onToggle: handleMcpToggle, onRemove: handleMcpRemove, clickMapRef: clickMapRef, hoverMapRef: hoverMapRef })] }));
|
|
359
|
+
}
|
|
360
|
+
function ChatInputChrome({ children }) {
|
|
361
|
+
const theme = useTheme();
|
|
362
|
+
return (_jsx(Box, { flexDirection: "column", ...(theme.background ? { backgroundColor: theme.background } : {}), paddingBottom: 1, children: children }));
|
|
274
363
|
}
|