@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,249 @@
|
|
|
1
|
+
// Regex patterns for markdown matching
|
|
2
|
+
const LINK_PATTERN = /^\[.*?\]\(.*?\)$/;
|
|
3
|
+
const BOLD_PATTERN = /^\*\*.*?\*\*$/;
|
|
4
|
+
// Matches *text* but NOT **text** (negative lookahead ensures second char isn't *)
|
|
5
|
+
const ITALIC_PATTERN = /^\*(?!\*).+\*$/;
|
|
6
|
+
const TABLE_ROW_PATTERN = /^\|.+\|$/;
|
|
7
|
+
// Matches markdown table delimiter rows like: | --- | ---: | :-: |
|
|
8
|
+
const TABLE_DELIMITER_PATTERN = /^\|\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)*\|\s*$/;
|
|
9
|
+
const WHITESPACE_PATTERN = /\s/;
|
|
10
|
+
// Rich XML tags that must be passed through intact
|
|
11
|
+
const RICH_TAGS = ['app_preview', 'download'];
|
|
12
|
+
// Matches an opening rich tag e.g. <app_preview> or <download>
|
|
13
|
+
const RICH_TAG_OPEN_RE = new RegExp(`<(${RICH_TAGS.join('|')})>`, 'i');
|
|
14
|
+
// Inline buffer cap: flush as raw text if a markdown element doesn't close within this many chars
|
|
15
|
+
const MAX_INLINE_BUFFER = 512;
|
|
16
|
+
// Rich-tag buffer cap: safety valve for malformed/missing closing tags
|
|
17
|
+
const MAX_RICH_TAG_BUFFER = 65536;
|
|
18
|
+
class MarkdownJoiner {
|
|
19
|
+
buffer = '';
|
|
20
|
+
bufferMode = null;
|
|
21
|
+
richTagName = null;
|
|
22
|
+
tableLineBuffer = '';
|
|
23
|
+
tableLineMode = null;
|
|
24
|
+
isAtLineStart = true;
|
|
25
|
+
isInTable = false;
|
|
26
|
+
pendingTableHeaderLine = null;
|
|
27
|
+
processText(text) {
|
|
28
|
+
let output = '';
|
|
29
|
+
for (const char of text) {
|
|
30
|
+
// Rich-tag passthrough mode: buffer everything until closing tag
|
|
31
|
+
if (this.bufferMode === 'rich-tag') {
|
|
32
|
+
this.buffer += char;
|
|
33
|
+
// Safety cap: if the rich tag never closes, flush as raw text
|
|
34
|
+
if (this.buffer.length > MAX_RICH_TAG_BUFFER) {
|
|
35
|
+
output += this.buffer;
|
|
36
|
+
this.richTagName = null;
|
|
37
|
+
this.clearBuffer();
|
|
38
|
+
this.isAtLineStart = char === '\n';
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const closeTag = `</${this.richTagName}>`;
|
|
42
|
+
if (this.buffer.endsWith(closeTag)) {
|
|
43
|
+
output += this.buffer;
|
|
44
|
+
this.richTagName = null;
|
|
45
|
+
this.clearBuffer();
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (this.tableLineMode) {
|
|
50
|
+
this.tableLineBuffer += char;
|
|
51
|
+
if (char === '\n') {
|
|
52
|
+
output += this.flushTableLine();
|
|
53
|
+
this.isAtLineStart = true;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
this.isAtLineStart = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (this.bufferMode === 'inline') {
|
|
60
|
+
this.buffer += char;
|
|
61
|
+
// Cap inline buffer to prevent quadratic regex cost on unbounded input
|
|
62
|
+
if (this.buffer.length > MAX_INLINE_BUFFER) {
|
|
63
|
+
output += this.buffer;
|
|
64
|
+
this.clearBuffer();
|
|
65
|
+
this.isAtLineStart = char === '\n';
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Check if buffer has grown into a rich tag opener
|
|
69
|
+
if (this.buffer.startsWith('<')) {
|
|
70
|
+
const match = RICH_TAG_OPEN_RE.exec(this.buffer);
|
|
71
|
+
if (match && this.buffer.endsWith('>')) {
|
|
72
|
+
// Confirmed rich tag open — switch to rich-tag mode
|
|
73
|
+
this.richTagName = match[1];
|
|
74
|
+
this.bufferMode = 'rich-tag';
|
|
75
|
+
this.isAtLineStart = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Still potentially building a rich tag — keep buffering until > or mismatch
|
|
79
|
+
if (!this.isFalsePositiveTag(char)) {
|
|
80
|
+
this.isAtLineStart = char === '\n';
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Not a rich tag — flush as raw text
|
|
84
|
+
output += this.buffer;
|
|
85
|
+
this.clearBuffer();
|
|
86
|
+
this.isAtLineStart = char === '\n';
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Check for complete markdown elements or false positives
|
|
90
|
+
if (this.isCompleteLink() || this.isCompleteBold() || this.isCompleteItalic()) {
|
|
91
|
+
// Complete markdown element - flush buffer as is
|
|
92
|
+
output += this.buffer;
|
|
93
|
+
this.clearBuffer();
|
|
94
|
+
}
|
|
95
|
+
else if (this.isFalsePositive(char)) {
|
|
96
|
+
// False positive - flush buffer as raw text
|
|
97
|
+
output += this.buffer;
|
|
98
|
+
this.clearBuffer();
|
|
99
|
+
}
|
|
100
|
+
this.isAtLineStart = char === '\n';
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
if (this.isAtLineStart) {
|
|
104
|
+
if (this.pendingTableHeaderLine) {
|
|
105
|
+
if (char !== '|') {
|
|
106
|
+
output += this.pendingTableHeaderLine;
|
|
107
|
+
this.pendingTableHeaderLine = null;
|
|
108
|
+
// fall through to handle this char normally
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
this.tableLineMode = 'delimiter';
|
|
112
|
+
this.tableLineBuffer = char;
|
|
113
|
+
this.isAtLineStart = false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (this.isInTable && char !== '|')
|
|
118
|
+
this.isInTable = false;
|
|
119
|
+
if (!this.isInTable && !this.pendingTableHeaderLine && char === '|') {
|
|
120
|
+
this.tableLineMode = 'header';
|
|
121
|
+
this.tableLineBuffer = char;
|
|
122
|
+
this.isAtLineStart = false;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (char === '<') {
|
|
127
|
+
this.buffer = char;
|
|
128
|
+
this.bufferMode = 'inline';
|
|
129
|
+
this.isAtLineStart = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (char === '[' || char === '*') {
|
|
133
|
+
this.buffer = char;
|
|
134
|
+
this.bufferMode = 'inline';
|
|
135
|
+
this.isAtLineStart = false;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Pass through character directly
|
|
139
|
+
output += char;
|
|
140
|
+
this.isAtLineStart = char === '\n';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return output;
|
|
144
|
+
}
|
|
145
|
+
flushTableLine() {
|
|
146
|
+
const lineWithNewline = this.tableLineBuffer;
|
|
147
|
+
const line = lineWithNewline.endsWith('\n') ? lineWithNewline.slice(0, -1) : lineWithNewline;
|
|
148
|
+
this.tableLineBuffer = '';
|
|
149
|
+
const mode = this.tableLineMode;
|
|
150
|
+
this.tableLineMode = null;
|
|
151
|
+
if (mode === 'header') {
|
|
152
|
+
if (this.isTableHeaderCandidate(line)) {
|
|
153
|
+
// Hold header line until we see whether next line is a delimiter row
|
|
154
|
+
this.pendingTableHeaderLine = lineWithNewline;
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
return lineWithNewline;
|
|
158
|
+
}
|
|
159
|
+
if (mode === 'delimiter') {
|
|
160
|
+
const headerLine = this.pendingTableHeaderLine ?? '';
|
|
161
|
+
this.pendingTableHeaderLine = null;
|
|
162
|
+
if (TABLE_DELIMITER_PATTERN.test(line))
|
|
163
|
+
this.isInTable = true;
|
|
164
|
+
return headerLine + lineWithNewline;
|
|
165
|
+
}
|
|
166
|
+
return lineWithNewline;
|
|
167
|
+
}
|
|
168
|
+
isTableHeaderCandidate(line) {
|
|
169
|
+
return TABLE_ROW_PATTERN.test(line) && !TABLE_DELIMITER_PATTERN.test(line);
|
|
170
|
+
}
|
|
171
|
+
isCompleteLink() {
|
|
172
|
+
// Match [text](url) pattern
|
|
173
|
+
return LINK_PATTERN.test(this.buffer);
|
|
174
|
+
}
|
|
175
|
+
isCompleteBold() {
|
|
176
|
+
// Match **text** pattern
|
|
177
|
+
return BOLD_PATTERN.test(this.buffer);
|
|
178
|
+
}
|
|
179
|
+
isCompleteItalic() {
|
|
180
|
+
// Match *text* pattern (but not **text**)
|
|
181
|
+
return ITALIC_PATTERN.test(this.buffer);
|
|
182
|
+
}
|
|
183
|
+
isFalsePositiveTag(char) {
|
|
184
|
+
// A < buffer is a false positive if we hit newline, another <, or > without matching a rich tag
|
|
185
|
+
if (char === '\n' || (char === '<' && this.buffer.length > 1))
|
|
186
|
+
return true;
|
|
187
|
+
if (char === '>' && !RICH_TAG_OPEN_RE.test(this.buffer))
|
|
188
|
+
return true;
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
isFalsePositive(char) {
|
|
192
|
+
// For links: if we see [ followed by something other than valid link syntax
|
|
193
|
+
if (this.buffer.startsWith('[')) {
|
|
194
|
+
// If we hit a newline or another [ without completing the link, it's false positive
|
|
195
|
+
return char === '\n' || (char === '[' && this.buffer.length > 1);
|
|
196
|
+
}
|
|
197
|
+
// For emphasis: if we see * or ** followed by whitespace or newline
|
|
198
|
+
if (this.buffer.startsWith('*')) {
|
|
199
|
+
// Single * followed by whitespace is likely a list item or not emphasis
|
|
200
|
+
// (buffer already includes char, so length 2 means just "*" + the whitespace char)
|
|
201
|
+
if (this.buffer.length === 2 && WHITESPACE_PATTERN.test(char)) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
// If we hit newline without completing emphasis, it's false positive
|
|
205
|
+
return char === '\n';
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
clearBuffer() {
|
|
210
|
+
this.buffer = '';
|
|
211
|
+
this.bufferMode = null;
|
|
212
|
+
}
|
|
213
|
+
flush() {
|
|
214
|
+
const remaining = (this.pendingTableHeaderLine ?? '') + this.tableLineBuffer + this.buffer;
|
|
215
|
+
this.pendingTableHeaderLine = null;
|
|
216
|
+
this.tableLineBuffer = '';
|
|
217
|
+
this.tableLineMode = null;
|
|
218
|
+
this.clearBuffer();
|
|
219
|
+
return remaining;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export const markdownJoinerTransform = () => () => {
|
|
223
|
+
const joiner = new MarkdownJoiner();
|
|
224
|
+
return new TransformStream({
|
|
225
|
+
transform(chunk, controller) {
|
|
226
|
+
if (chunk.type === 'text-delta') {
|
|
227
|
+
const processedText = joiner.processText(chunk.text);
|
|
228
|
+
if (processedText) {
|
|
229
|
+
controller.enqueue({
|
|
230
|
+
...chunk,
|
|
231
|
+
text: processedText,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
controller.enqueue(chunk);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
flush(controller) {
|
|
240
|
+
const remaining = joiner.flush();
|
|
241
|
+
if (remaining) {
|
|
242
|
+
controller.enqueue({
|
|
243
|
+
type: 'text-delta',
|
|
244
|
+
text: remaining,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { diffLines } from "diff";
|
|
3
|
+
import { createRun, listRuns, getRunPaths } from "../storage/run-store.js";
|
|
4
|
+
import { runResearchAgent } from "../agent/research-agent.js";
|
|
5
|
+
/** Compare two report.md texts and return a human-readable diff summary. */
|
|
6
|
+
export function diffReports(prev, next) {
|
|
7
|
+
const changes = diffLines(prev, next);
|
|
8
|
+
const added = changes.filter((c) => c.added).map((c) => c.value.trim()).filter(Boolean);
|
|
9
|
+
const removed = changes.filter((c) => c.removed).map((c) => c.value.trim()).filter(Boolean);
|
|
10
|
+
if (added.length === 0 && removed.length === 0)
|
|
11
|
+
return "No changes detected.";
|
|
12
|
+
const lines = [];
|
|
13
|
+
if (added.length > 0) {
|
|
14
|
+
lines.push(`+++ ${added.length} added section(s):\n${added.map((a) => ` + ${a.slice(0, 120)}`).join("\n")}`);
|
|
15
|
+
}
|
|
16
|
+
if (removed.length > 0) {
|
|
17
|
+
lines.push(`--- ${removed.length} removed section(s):\n${removed.map((r) => ` - ${r.slice(0, 120)}`).join("\n")}`);
|
|
18
|
+
}
|
|
19
|
+
return lines.join("\n");
|
|
20
|
+
}
|
|
21
|
+
async function getLastReport(goal, config, projectRoot) {
|
|
22
|
+
const runs = await listRuns(config, projectRoot);
|
|
23
|
+
const last = runs.find((r) => r.goal === goal || r.goal.includes(goal));
|
|
24
|
+
if (!last)
|
|
25
|
+
return "";
|
|
26
|
+
return readFile(getRunPaths(last.path).report, "utf8").catch(() => "");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Run the watch loop. Resolves when maxRuns is reached or signal is aborted.
|
|
30
|
+
*/
|
|
31
|
+
export async function watchLoop(opts, signal) {
|
|
32
|
+
const { goal, intervalMs, maxRuns, config, projectRoot = process.cwd() } = opts;
|
|
33
|
+
let tick = 0;
|
|
34
|
+
while (!signal?.aborted) {
|
|
35
|
+
if (maxRuns !== undefined && tick >= maxRuns)
|
|
36
|
+
break;
|
|
37
|
+
const prevReport = await getLastReport(goal, config, projectRoot);
|
|
38
|
+
let runPath;
|
|
39
|
+
try {
|
|
40
|
+
const state = await createRun(goal, config, projectRoot);
|
|
41
|
+
runPath = state.path;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
opts.onError?.(err instanceof Error ? err : new Error(String(err)), tick);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
opts.onRunStart?.(runPath, tick);
|
|
48
|
+
try {
|
|
49
|
+
await runResearchAgent(runPath, goal, config);
|
|
50
|
+
const nextReport = await readFile(getRunPaths(runPath).report, "utf8").catch(() => "");
|
|
51
|
+
const diffText = diffReports(prevReport, nextReport);
|
|
52
|
+
opts.onRunComplete?.(runPath, diffText, tick);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
opts.onError?.(err instanceof Error ? err : new Error(String(err)), tick);
|
|
56
|
+
}
|
|
57
|
+
tick++;
|
|
58
|
+
if (maxRuns !== undefined && tick >= maxRuns)
|
|
59
|
+
break;
|
|
60
|
+
await new Promise((resolve) => {
|
|
61
|
+
const id = setTimeout(resolve, intervalMs);
|
|
62
|
+
signal?.addEventListener("abort", () => { clearTimeout(id); resolve(); }, { once: true });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scira/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scira — terminal-native AI research agent with grounded sources, verified claims, and local run storage.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"scira": "dist/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"research",
|
|
17
|
+
"ai",
|
|
18
|
+
"cli",
|
|
19
|
+
"agent",
|
|
20
|
+
"llm",
|
|
21
|
+
"rag"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/zaidmukaddam/scira-cli.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/zaidmukaddam/scira-cli/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/zaidmukaddam/scira-cli#readme",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -p tsconfig.json",
|
|
33
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
34
|
+
"prepublishOnly": "bun run build",
|
|
35
|
+
"dev": "tsx src/cli/index.ts",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@ai-sdk/mcp": "^1.0.47",
|
|
41
|
+
"@ai-sdk/openai-compatible": "^2.0.48",
|
|
42
|
+
"@ai-sdk/xai": "^3.0.93",
|
|
43
|
+
"@clack/prompts": "^1.5.1",
|
|
44
|
+
"@mendable/firecrawl-js": "^4.25.3",
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
+
"@mozilla/readability": "^0.6.0",
|
|
47
|
+
"ai": "^6.0.201",
|
|
48
|
+
"bash-tool": "^1.3.17",
|
|
49
|
+
"commander": "^15.0.0",
|
|
50
|
+
"diff": "^9.0.0",
|
|
51
|
+
"exa-js": "^2.13.0",
|
|
52
|
+
"files-sdk": "^1.8.0",
|
|
53
|
+
"ink": "^7.0.5",
|
|
54
|
+
"jsdom": "^29.1.1",
|
|
55
|
+
"ora": "^9.4.0",
|
|
56
|
+
"parallel-web": "^1.1.0",
|
|
57
|
+
"react": "^19.2.7",
|
|
58
|
+
"string-width": "8.2.1",
|
|
59
|
+
"workers-ai-provider": "^3.1.14",
|
|
60
|
+
"zod": "^4.4.3"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/jsdom": "^28.0.3",
|
|
64
|
+
"@types/node": "^25.9.3",
|
|
65
|
+
"@types/react": "^19.2.17",
|
|
66
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
67
|
+
"tsx": "^4.22.4",
|
|
68
|
+
"typescript": "^6.0.3",
|
|
69
|
+
"vitest": "^4.1.8"
|
|
70
|
+
},
|
|
71
|
+
"engines": {
|
|
72
|
+
"node": ">=20"
|
|
73
|
+
}
|
|
74
|
+
}
|