@scira/cli 0.1.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/dist/agent/research-agent.js +253 -0
  4. package/dist/agent/skills.js +265 -0
  5. package/dist/agent/tools.js +429 -0
  6. package/dist/agent/tools.test.js +27 -0
  7. package/dist/cli/commands/init.js +370 -0
  8. package/dist/cli/index.js +445 -0
  9. package/dist/cli/shell/shell.js +76 -0
  10. package/dist/cli/shell/tui.js +11 -0
  11. package/dist/config/env-store.js +47 -0
  12. package/dist/config/load-config.js +58 -0
  13. package/dist/export/formatters.js +37 -0
  14. package/dist/providers/llm/gateway.js +64 -0
  15. package/dist/providers/llm/huggingface.js +33 -0
  16. package/dist/providers/llm/models.js +97 -0
  17. package/dist/providers/llm/readiness.js +50 -0
  18. package/dist/providers/llm/registry.js +56 -0
  19. package/dist/storage/jsonl.js +29 -0
  20. package/dist/storage/jsonl.test.js +38 -0
  21. package/dist/storage/run-store.js +134 -0
  22. package/dist/storage/run-store.test.js +65 -0
  23. package/dist/tools/chrome-devtools-mcp.js +61 -0
  24. package/dist/tools/file-tools.js +128 -0
  25. package/dist/tools/mcp-bridge.js +118 -0
  26. package/dist/tools/mcp-oauth.js +276 -0
  27. package/dist/tools/open-url.js +99 -0
  28. package/dist/tools/search-web.js +153 -0
  29. package/dist/types/index.js +91 -0
  30. package/dist/types/schema.test.js +60 -0
  31. package/dist/ui/ink/SciraApp.js +274 -0
  32. package/dist/ui/ink/components/effects.js +44 -0
  33. package/dist/ui/ink/components/home-screen.js +69 -0
  34. package/dist/ui/ink/components/overlays.js +111 -0
  35. package/dist/ui/ink/constants.js +56 -0
  36. package/dist/ui/ink/hooks/use-agent-turn.js +186 -0
  37. package/dist/ui/ink/hooks/use-feed-lines.js +186 -0
  38. package/dist/ui/ink/hooks/use-feed.js +69 -0
  39. package/dist/ui/ink/hooks/use-keyboard.js +315 -0
  40. package/dist/ui/ink/hooks/use-mouse.js +31 -0
  41. package/dist/ui/ink/hooks/use-session.js +103 -0
  42. package/dist/ui/ink/hooks/use-settings.js +155 -0
  43. package/dist/ui/ink/hooks/use-submit.js +366 -0
  44. package/dist/ui/ink/hooks/use-suggestions.js +91 -0
  45. package/dist/ui/ink/lib/file-mentions.js +71 -0
  46. package/dist/ui/ink/lib/markdown.js +245 -0
  47. package/dist/ui/ink/lib/utils.js +224 -0
  48. package/dist/ui/ink/session-manager.js +160 -0
  49. package/dist/ui/ink/types.js +1 -0
  50. package/dist/utils/ids.js +15 -0
  51. package/dist/utils/markdown-joiner.js +249 -0
  52. package/dist/watch/runner.js +65 -0
  53. package/package.json +74 -0
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, writeFile, readFile } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import * as p from "@clack/prompts";
7
+ import { detectEnv } from "../../providers/llm/readiness.js";
8
+ import { listModels } from "../../providers/llm/models.js";
9
+ import { LLM_PROVIDERS, LLM_PROVIDER_LABELS, defaultModelFor } from "../../providers/llm/registry.js";
10
+ const SCIRA_DIR = join(homedir(), ".scira");
11
+ const ENV_FILE = join(SCIRA_DIR, ".env");
12
+ const CONFIG_FILE = join(SCIRA_DIR, "config.json");
13
+ const SEARCH_PROVIDERS = ["parallel", "exa", "firecrawl"];
14
+ function parseEnvFile(content) {
15
+ const env = {};
16
+ for (const line of content.split("\n")) {
17
+ const trimmed = line.trim();
18
+ if (!trimmed || trimmed.startsWith("#"))
19
+ continue;
20
+ const eq = trimmed.indexOf("=");
21
+ if (eq === -1)
22
+ continue;
23
+ const key = trimmed.slice(0, eq).trim();
24
+ const value = trimmed.slice(eq + 1).trim();
25
+ if (key)
26
+ env[key] = value;
27
+ }
28
+ return env;
29
+ }
30
+ export async function initCommand() {
31
+ p.intro("Welcome to Scira!");
32
+ // Create .scira directory if it doesn't exist
33
+ await mkdir(SCIRA_DIR, { recursive: true });
34
+ // Read existing .env and config
35
+ const existingEnv = existsSync(ENV_FILE) ? parseEnvFile(await readFile(ENV_FILE, "utf8")) : {};
36
+ const existingConfig = existsSync(CONFIG_FILE) ? JSON.parse(await readFile(CONFIG_FILE, "utf8")) : null;
37
+ // Ask if user wants to reconfigure
38
+ const shouldReconfigure = existingConfig ? await p.confirm({
39
+ message: "Configuration already exists. Reconfigure?",
40
+ initialValue: false,
41
+ }) : true;
42
+ if (p.isCancel(shouldReconfigure)) {
43
+ p.cancel("Setup cancelled.");
44
+ process.exit(0);
45
+ }
46
+ // If not reconfiguring and config exists, just verify and exit
47
+ if (!shouldReconfigure && existingConfig) {
48
+ p.note("Using existing configuration", "Configuration");
49
+ p.log.success(`LLM Provider: ${LLM_PROVIDER_LABELS[existingConfig.llmProvider || "gateway"]}`);
50
+ p.log.success(`Model: ${existingConfig.model || "default"}`);
51
+ p.log.success(`Search Provider: ${existingConfig.search?.provider || "exa"}`);
52
+ // Verify credentials
53
+ p.note("Verifying your setup...", "Verification");
54
+ const s = p.spinner();
55
+ s.start("Checking credentials...");
56
+ const checks = detectEnv(existingConfig.search?.provider || "exa", existingConfig.llmProvider || "gateway");
57
+ const missingRequired = checks.filter((c) => c.required && !c.present);
58
+ if (missingRequired.length === 0) {
59
+ s.stop("All credentials present!");
60
+ }
61
+ else {
62
+ s.stop(`Missing: ${missingRequired.map((c) => c.name).join(", ")}`);
63
+ p.note("Some required credentials are missing. Run `scira init` again to reconfigure.", "Warning");
64
+ }
65
+ p.outro("Configuration is up to date!");
66
+ return;
67
+ }
68
+ // Step 1: LLM Provider
69
+ p.note("Choose your LLM provider", "LLM Provider");
70
+ const llmProvider = await p.select({
71
+ message: "Select LLM provider",
72
+ options: LLM_PROVIDERS.map((provider) => ({
73
+ value: provider,
74
+ label: LLM_PROVIDER_LABELS[provider],
75
+ })),
76
+ initialValue: existingConfig?.llmProvider || "gateway",
77
+ });
78
+ if (p.isCancel(llmProvider)) {
79
+ p.cancel("Setup cancelled.");
80
+ process.exit(0);
81
+ }
82
+ // Step 2: API Keys based on provider
83
+ p.note("Scira requires API keys to function. Let's set them up.", "API Keys");
84
+ const envKeys = { ...existingEnv };
85
+ if (llmProvider === "gateway") {
86
+ if (!envKeys.AI_GATEWAY_API_KEY || shouldReconfigure) {
87
+ const aiGatewayKey = await p.text({
88
+ message: envKeys.AI_GATEWAY_API_KEY ? "Enter your Vercel AI Gateway API key (current: *****)" : "Enter your Vercel AI Gateway API key",
89
+ placeholder: "sk-...",
90
+ defaultValue: envKeys.AI_GATEWAY_API_KEY,
91
+ validate: (value) => {
92
+ if (!value)
93
+ return "AI Gateway API key is required";
94
+ if (!value.startsWith("sk-"))
95
+ return "Invalid API key format";
96
+ },
97
+ });
98
+ if (p.isCancel(aiGatewayKey)) {
99
+ p.cancel("Setup cancelled.");
100
+ process.exit(0);
101
+ }
102
+ envKeys.AI_GATEWAY_API_KEY = aiGatewayKey;
103
+ }
104
+ else {
105
+ p.log.success("AI Gateway API key already set");
106
+ }
107
+ }
108
+ else if (llmProvider === "xai") {
109
+ if (!envKeys.XAI_API_KEY || shouldReconfigure) {
110
+ const xaiKey = await p.text({
111
+ message: envKeys.XAI_API_KEY ? "Enter your xAI API key (current: *****)" : "Enter your xAI API key",
112
+ placeholder: "sk-...",
113
+ defaultValue: envKeys.XAI_API_KEY,
114
+ validate: (value) => {
115
+ if (!value)
116
+ return "xAI API key is required";
117
+ },
118
+ });
119
+ if (p.isCancel(xaiKey)) {
120
+ p.cancel("Setup cancelled.");
121
+ process.exit(0);
122
+ }
123
+ envKeys.XAI_API_KEY = xaiKey;
124
+ }
125
+ else {
126
+ p.log.success("xAI API key already set");
127
+ }
128
+ }
129
+ else if (llmProvider === "workers-ai") {
130
+ if (!envKeys.CLOUDFLARE_ACCOUNT_ID || !envKeys.CLOUDFLARE_API_TOKEN || shouldReconfigure) {
131
+ const accountId = await p.text({
132
+ message: envKeys.CLOUDFLARE_ACCOUNT_ID ? "Enter your Cloudflare Account ID (current: *****)" : "Enter your Cloudflare Account ID",
133
+ placeholder: "your-account-id",
134
+ defaultValue: envKeys.CLOUDFLARE_ACCOUNT_ID,
135
+ validate: (value) => {
136
+ if (!value)
137
+ return "Cloudflare Account ID is required";
138
+ },
139
+ });
140
+ if (p.isCancel(accountId)) {
141
+ p.cancel("Setup cancelled.");
142
+ process.exit(0);
143
+ }
144
+ const apiToken = await p.text({
145
+ message: envKeys.CLOUDFLARE_API_TOKEN ? "Enter your Cloudflare API Token (current: *****)" : "Enter your Cloudflare API Token",
146
+ placeholder: "your-api-token",
147
+ defaultValue: envKeys.CLOUDFLARE_API_TOKEN,
148
+ validate: (value) => {
149
+ if (!value)
150
+ return "Cloudflare API Token is required";
151
+ },
152
+ });
153
+ if (p.isCancel(apiToken)) {
154
+ p.cancel("Setup cancelled.");
155
+ process.exit(0);
156
+ }
157
+ envKeys.CLOUDFLARE_ACCOUNT_ID = accountId;
158
+ envKeys.CLOUDFLARE_API_TOKEN = apiToken;
159
+ }
160
+ else {
161
+ p.log.success("Cloudflare credentials already set");
162
+ }
163
+ }
164
+ else if (llmProvider === "huggingface") {
165
+ if (!envKeys.HF_API_KEY || shouldReconfigure) {
166
+ const hfKey = await p.text({
167
+ message: envKeys.HF_API_KEY ? "Enter your HuggingFace API key (current: *****)" : "Enter your HuggingFace API key",
168
+ placeholder: "hf_...",
169
+ defaultValue: envKeys.HF_API_KEY,
170
+ validate: (value) => {
171
+ if (!value)
172
+ return "HuggingFace API key is required";
173
+ },
174
+ });
175
+ if (p.isCancel(hfKey)) {
176
+ p.cancel("Setup cancelled.");
177
+ process.exit(0);
178
+ }
179
+ envKeys.HF_API_KEY = hfKey;
180
+ }
181
+ else {
182
+ p.log.success("HuggingFace API key already set");
183
+ }
184
+ }
185
+ // Optional search provider keys
186
+ const searchProvider = await p.select({
187
+ message: "Select search provider",
188
+ options: SEARCH_PROVIDERS.map((provider) => ({
189
+ value: provider,
190
+ label: provider.charAt(0).toUpperCase() + provider.slice(1),
191
+ })),
192
+ initialValue: existingConfig?.search?.provider || "exa",
193
+ });
194
+ if (p.isCancel(searchProvider)) {
195
+ p.cancel("Setup cancelled.");
196
+ process.exit(0);
197
+ }
198
+ if (searchProvider === "exa") {
199
+ if (!envKeys.EXA_API_KEY || shouldReconfigure) {
200
+ const exaKey = await p.text({
201
+ message: envKeys.EXA_API_KEY ? "Enter your Exa API key (optional, current: *****)" : "Enter your Exa API key (optional, press Enter to skip)",
202
+ placeholder: "exa_...",
203
+ defaultValue: envKeys.EXA_API_KEY,
204
+ });
205
+ if (p.isCancel(exaKey)) {
206
+ p.cancel("Setup cancelled.");
207
+ process.exit(0);
208
+ }
209
+ if (exaKey)
210
+ envKeys.EXA_API_KEY = exaKey;
211
+ }
212
+ else {
213
+ p.log.success("Exa API key already set");
214
+ }
215
+ }
216
+ else if (searchProvider === "firecrawl") {
217
+ if (!envKeys.FIRECRAWL_API_KEY || shouldReconfigure) {
218
+ const firecrawlKey = await p.text({
219
+ message: envKeys.FIRECRAWL_API_KEY ? "Enter your Firecrawl API key (optional, current: *****)" : "Enter your Firecrawl API key (optional, press Enter to skip)",
220
+ placeholder: "fc-...",
221
+ defaultValue: envKeys.FIRECRAWL_API_KEY,
222
+ });
223
+ if (p.isCancel(firecrawlKey)) {
224
+ p.cancel("Setup cancelled.");
225
+ process.exit(0);
226
+ }
227
+ if (firecrawlKey)
228
+ envKeys.FIRECRAWL_API_KEY = firecrawlKey;
229
+ }
230
+ else {
231
+ p.log.success("Firecrawl API key already set");
232
+ }
233
+ }
234
+ // Write .env file
235
+ const envContent = Object.entries(envKeys)
236
+ .map(([key, value]) => `${key}=${value}`)
237
+ .join("\n");
238
+ await writeFile(ENV_FILE, envContent, "utf8");
239
+ p.log.success("API keys saved to ~/.scira/.env");
240
+ // Step 3: Model Selection
241
+ p.note("Select your AI model", "Model");
242
+ const s = p.spinner();
243
+ s.start("Fetching available models...");
244
+ let models = [];
245
+ try {
246
+ const tempConfig = {
247
+ llmProvider: llmProvider,
248
+ model: defaultModelFor(llmProvider),
249
+ lastModels: {},
250
+ approvalMode: "suggest",
251
+ runDirectory: ".scira/runs",
252
+ maxSources: 20,
253
+ citationPolicy: "strict",
254
+ search: {
255
+ provider: searchProvider,
256
+ maxResults: 8,
257
+ includeDomains: [],
258
+ excludeDomains: [],
259
+ },
260
+ mcp: {
261
+ chromeDevtools: {
262
+ enabled: false,
263
+ command: "npx",
264
+ args: ["-y", "chrome-devtools-mcp@latest"],
265
+ toolPrefix: "devtools_",
266
+ },
267
+ servers: [],
268
+ },
269
+ };
270
+ const modelList = await listModels(tempConfig);
271
+ models = modelList.map((m) => m.id);
272
+ s.stop(`Found ${models.length} models`);
273
+ }
274
+ catch (error) {
275
+ s.stop("Failed to fetch models, using default");
276
+ models = [defaultModelFor(llmProvider)];
277
+ }
278
+ const model = await p.select({
279
+ message: "Select AI model",
280
+ options: models.map((m) => ({ value: m, label: m })),
281
+ initialValue: existingConfig?.model || defaultModelFor(llmProvider),
282
+ });
283
+ if (p.isCancel(model)) {
284
+ p.cancel("Setup cancelled.");
285
+ process.exit(0);
286
+ }
287
+ // Step 4: Config
288
+ p.note("Configure your research preferences.", "Configuration");
289
+ const approvalMode = await p.select({
290
+ message: "Select tool approval mode",
291
+ options: [
292
+ { value: "suggest", label: "Suggest (ask before expensive actions)" },
293
+ { value: "manual", label: "Manual (ask before every action)" },
294
+ { value: "auto", label: "Auto (run without asking)" },
295
+ ],
296
+ initialValue: existingConfig?.approvalMode || "suggest",
297
+ });
298
+ if (p.isCancel(approvalMode)) {
299
+ p.cancel("Setup cancelled.");
300
+ process.exit(0);
301
+ }
302
+ const maxSources = await p.text({
303
+ message: "Maximum sources per run",
304
+ defaultValue: String(existingConfig?.maxSources || 20),
305
+ placeholder: "20",
306
+ validate: (value) => {
307
+ const num = Number.parseInt(value || "", 10);
308
+ if (Number.isNaN(num) || num < 1)
309
+ return "Must be a positive number";
310
+ },
311
+ });
312
+ if (p.isCancel(maxSources)) {
313
+ p.cancel("Setup cancelled.");
314
+ process.exit(0);
315
+ }
316
+ const citationPolicy = await p.select({
317
+ message: "Select citation policy",
318
+ options: [
319
+ { value: "strict", label: "Strict (all claims must be cited)" },
320
+ { value: "balanced", label: "Balanced (citations for major claims)" },
321
+ ],
322
+ initialValue: existingConfig?.citationPolicy || "strict",
323
+ });
324
+ if (p.isCancel(citationPolicy)) {
325
+ p.cancel("Setup cancelled.");
326
+ process.exit(0);
327
+ }
328
+ // Write config file
329
+ const config = {
330
+ llmProvider: llmProvider,
331
+ model,
332
+ lastModels: { [llmProvider]: model, ...(existingConfig?.lastModels || {}) },
333
+ approvalMode: approvalMode,
334
+ search: {
335
+ provider: searchProvider,
336
+ maxResults: existingConfig?.search?.maxResults || 8,
337
+ includeDomains: existingConfig?.search?.includeDomains || [],
338
+ excludeDomains: existingConfig?.search?.excludeDomains || [],
339
+ },
340
+ maxSources: Number.parseInt(maxSources, 10),
341
+ citationPolicy: citationPolicy,
342
+ runDirectory: existingConfig?.runDirectory || ".scira/runs",
343
+ mcp: existingConfig?.mcp || {
344
+ chromeDevtools: {
345
+ enabled: false,
346
+ command: "npx",
347
+ args: ["-y", "chrome-devtools-mcp@latest"],
348
+ toolPrefix: "devtools_",
349
+ },
350
+ servers: [],
351
+ },
352
+ };
353
+ await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
354
+ p.log.success("Configuration saved to ~/.scira/config.json");
355
+ // Step 5: Verify
356
+ p.note("Verifying your setup...", "Verification");
357
+ const s2 = p.spinner();
358
+ s2.start("Checking credentials...");
359
+ // Load the config we just created
360
+ const checks = detectEnv(config.search.provider, config.llmProvider);
361
+ const missingRequired = checks.filter((c) => c.required && !c.present);
362
+ if (missingRequired.length === 0) {
363
+ s2.stop("All credentials present!");
364
+ }
365
+ else {
366
+ s2.stop(`Missing: ${missingRequired.map((c) => c.name).join(", ")}`);
367
+ p.note("Some required credentials are missing. Please run `scira init` again.", "Warning");
368
+ }
369
+ p.outro("Setup complete! Run `scira doctor` to verify your configuration.");
370
+ }