@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.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/agent/research-agent.js +253 -0
- package/dist/agent/skills.js +265 -0
- package/dist/agent/tools.js +429 -0
- package/dist/agent/tools.test.js +27 -0
- package/dist/cli/commands/init.js +370 -0
- package/dist/cli/index.js +445 -0
- package/dist/cli/shell/shell.js +76 -0
- package/dist/cli/shell/tui.js +11 -0
- package/dist/config/env-store.js +47 -0
- package/dist/config/load-config.js +58 -0
- package/dist/export/formatters.js +37 -0
- package/dist/providers/llm/gateway.js +64 -0
- package/dist/providers/llm/huggingface.js +33 -0
- package/dist/providers/llm/models.js +97 -0
- package/dist/providers/llm/readiness.js +50 -0
- package/dist/providers/llm/registry.js +56 -0
- package/dist/storage/jsonl.js +29 -0
- package/dist/storage/jsonl.test.js +38 -0
- package/dist/storage/run-store.js +134 -0
- package/dist/storage/run-store.test.js +65 -0
- package/dist/tools/chrome-devtools-mcp.js +61 -0
- package/dist/tools/file-tools.js +128 -0
- package/dist/tools/mcp-bridge.js +118 -0
- package/dist/tools/mcp-oauth.js +276 -0
- package/dist/tools/open-url.js +99 -0
- package/dist/tools/search-web.js +153 -0
- package/dist/types/index.js +91 -0
- package/dist/types/schema.test.js +60 -0
- package/dist/ui/ink/SciraApp.js +274 -0
- package/dist/ui/ink/components/effects.js +44 -0
- package/dist/ui/ink/components/home-screen.js +69 -0
- package/dist/ui/ink/components/overlays.js +111 -0
- package/dist/ui/ink/constants.js +56 -0
- package/dist/ui/ink/hooks/use-agent-turn.js +186 -0
- package/dist/ui/ink/hooks/use-feed-lines.js +186 -0
- package/dist/ui/ink/hooks/use-feed.js +69 -0
- package/dist/ui/ink/hooks/use-keyboard.js +315 -0
- package/dist/ui/ink/hooks/use-mouse.js +31 -0
- package/dist/ui/ink/hooks/use-session.js +103 -0
- package/dist/ui/ink/hooks/use-settings.js +155 -0
- package/dist/ui/ink/hooks/use-submit.js +366 -0
- package/dist/ui/ink/hooks/use-suggestions.js +91 -0
- package/dist/ui/ink/lib/file-mentions.js +71 -0
- package/dist/ui/ink/lib/markdown.js +245 -0
- package/dist/ui/ink/lib/utils.js +224 -0
- package/dist/ui/ink/session-manager.js +160 -0
- package/dist/ui/ink/types.js +1 -0
- package/dist/utils/ids.js +15 -0
- package/dist/utils/markdown-joiner.js +249 -0
- package/dist/watch/runner.js +65 -0
- 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
|
+
}
|