@qatonic_innovations/qaios 0.2.0 → 0.3.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/README.md +47 -17
- package/dist/index.js +141 -14
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -35,18 +35,20 @@ The published package is `@qatonic_innovations/qaios`; the installed **command i
|
|
|
35
35
|
|
|
36
36
|
### Requirements
|
|
37
37
|
|
|
38
|
-
- **Node.js
|
|
39
|
-
(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- An **
|
|
43
|
-
[console.anthropic.com](https://console.anthropic.com/settings/keys)
|
|
44
|
-
|
|
38
|
+
- **Node.js 22 LTS or newer.** QAIOS uses Node's **built-in** SQLite
|
|
39
|
+
(`node:sqlite`, stable in Node 22) for its local audit log — **no native
|
|
40
|
+
module to compile**, so `npm install` is just a download. (On Node < 22 the
|
|
41
|
+
built-in isn't available; upgrade Node.)
|
|
42
|
+
- An **LLM provider API key.** QAIOS uses **Anthropic** by default — get a key
|
|
43
|
+
at [console.anthropic.com](https://console.anthropic.com/settings/keys) and
|
|
44
|
+
export it:
|
|
45
45
|
```bash
|
|
46
46
|
export ANTHROPIC_API_KEY=sk-ant-... # macOS/Linux
|
|
47
47
|
setx ANTHROPIC_API_KEY "sk-ant-..." # Windows (open a NEW shell after)
|
|
48
48
|
```
|
|
49
|
-
|
|
49
|
+
Prefer **OpenAI**? Set `llm.provider: openai` and export `OPENAI_API_KEY`
|
|
50
|
+
instead — see [Choose your LLM provider](#choose-your-llm-provider) below.
|
|
51
|
+
Keys are read from the environment and **never written to disk** by QAIOS.
|
|
50
52
|
- **Playwright** in your project, for `qaios run` / `snapshot` / `explore` / `a11y`:
|
|
51
53
|
```bash
|
|
52
54
|
npm i -D @playwright/test && npx playwright install
|
|
@@ -57,14 +59,9 @@ The published package is `@qatonic_innovations/qaios`; the installed **command i
|
|
|
57
59
|
|
|
58
60
|
### Install troubleshooting
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- Confirm your Node version: `node -v` (prefer 20 LTS).
|
|
64
|
-
- Rebuild the binary: `npm rebuild better-sqlite3` — or reinstall qaios.
|
|
65
|
-
- Behind a proxy/firewall? The prebuilt binary is fetched from GitHub
|
|
66
|
-
release assets; allow that host, or install C/C++ build tools so it can
|
|
67
|
-
compile locally.
|
|
62
|
+
QAIOS has **no native dependencies** — it uses Node's built-in SQLite — so
|
|
63
|
+
install is friction-free. If `qaios init` reports a SQLite/`node:sqlite` error,
|
|
64
|
+
your Node is too old: run `node -v` and upgrade to **Node 22 LTS or newer**.
|
|
68
65
|
|
|
69
66
|
`qaios doctor` will tell you exactly which check failed and what to run.
|
|
70
67
|
|
|
@@ -126,7 +123,7 @@ Run `qaios <command> --help` for the full option list of any command.
|
|
|
126
123
|
|
|
127
124
|
```bash
|
|
128
125
|
# Generate API tests from an OpenAPI spec
|
|
129
|
-
qaios test --type api --spec ./openapi.yaml "exercise the /orders endpoints"
|
|
126
|
+
qaios test --type api --api-spec ./openapi.yaml "exercise the /orders endpoints"
|
|
130
127
|
|
|
131
128
|
# Run a suite; QAIOS classifies failures and self-heals locator drift
|
|
132
129
|
qaios run
|
|
@@ -204,6 +201,39 @@ provider's native guaranteed-schema mode (Anthropic forced tool-use / OpenAI
|
|
|
204
201
|
strict function calling), so generated artifacts stay schema-valid. You can
|
|
205
202
|
override the key's env-var name with `llm.apiKeyEnv` if needed.
|
|
206
203
|
|
|
204
|
+
#### Picking a model
|
|
205
|
+
|
|
206
|
+
`llm.model` is a free-form string passed straight to the provider — **any model
|
|
207
|
+
that provider's API accepts works**, not just the defaults. When `llm.model` is
|
|
208
|
+
omitted, QAIOS uses a sensible default per provider:
|
|
209
|
+
|
|
210
|
+
| Provider | Default | Other examples you can set |
|
|
211
|
+
| ----------- | ------------------- | ------------------------------------------------- |
|
|
212
|
+
| `anthropic` | `claude-sonnet-4-6` | `claude-opus-4-7`, `claude-haiku-4-5-20251001`, … |
|
|
213
|
+
| `openai` | `gpt-4o` | `gpt-4o-mini` (cheaper), `gpt-4.1`, … |
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
llm:
|
|
217
|
+
provider: openai
|
|
218
|
+
model: gpt-4o-mini # any OpenAI model id
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Two things to know about non-default models:
|
|
222
|
+
|
|
223
|
+
- **Cost tracking.** QAIOS knows exact pricing for a built-in set of models
|
|
224
|
+
(Sonnet/Opus/Haiku and gpt-4o/4o-mini/4.1/4.1-mini). A model **outside** that
|
|
225
|
+
set still runs fine, but the USD figure in the audit log is approximate — it
|
|
226
|
+
bills at a default rate and prints a one-time `no pricing for model …`
|
|
227
|
+
warning. The per-workflow call/cost **cap still applies** regardless.
|
|
228
|
+
- **Structured output.** QAIOS relies on the provider's strict tool/function
|
|
229
|
+
calling. The defaults are chosen for strong schema adherence; a much older or
|
|
230
|
+
unusual model may fail validation more often (the built-in retry loop
|
|
231
|
+
recovers from occasional misses).
|
|
232
|
+
|
|
233
|
+
The defaults are the **live-validated** starting points — every skill is
|
|
234
|
+
exercised end-to-end against them. Other models share the same code path but
|
|
235
|
+
aren't individually certified.
|
|
236
|
+
|
|
207
237
|
### Operating modes
|
|
208
238
|
|
|
209
239
|
- **LITE** (default) — HIGH/CRITICAL risk pauses for review; routine work flows through.
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,8 @@ import { realpathSync, readFileSync, existsSync, rmSync, mkdirSync, writeFileSyn
|
|
|
6
6
|
import path12 from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { Command, InvalidArgumentError } from 'commander';
|
|
9
|
-
import Database from 'better-sqlite3';
|
|
10
9
|
import { createHash } from 'crypto';
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
11
|
import { z, ZodError } from 'zod';
|
|
12
12
|
import { monotonicFactory } from 'ulid';
|
|
13
13
|
import Anthropic from '@anthropic-ai/sdk';
|
|
@@ -843,6 +843,14 @@ var McpServerConfig = z.object({
|
|
|
843
843
|
var QaiosConfig = z.object({
|
|
844
844
|
version: z.literal(1),
|
|
845
845
|
mode: Mode.default("LITE"),
|
|
846
|
+
// The application under test. `baseUrl` is read by run / snapshot / fix /
|
|
847
|
+
// test (CLI `--base-url` overrides it per run). OPTIONAL, not defaulted — a
|
|
848
|
+
// `.default({})` would make `qaios init` serialize an empty `app: {}` stub
|
|
849
|
+
// into every config, and a user later hand-adding an `app:` block would then
|
|
850
|
+
// hit a duplicate-key YAML error. Callers use `config?.app?.baseUrl`.
|
|
851
|
+
app: z.object({
|
|
852
|
+
baseUrl: z.string().url().optional()
|
|
853
|
+
}).optional(),
|
|
846
854
|
llm: z.object({
|
|
847
855
|
// Which LLM provider backs every skill. Default stays anthropic so
|
|
848
856
|
// existing projects are unchanged. Set `openai` to use OpenAI instead;
|
|
@@ -1134,6 +1142,92 @@ function ensureDirExists(dir) {
|
|
|
1134
1142
|
throw err;
|
|
1135
1143
|
}
|
|
1136
1144
|
}
|
|
1145
|
+
var nodeRequire = createRequire(import.meta.url);
|
|
1146
|
+
var { DatabaseSync } = nodeRequire("node:sqlite");
|
|
1147
|
+
var StatementAdapter = class {
|
|
1148
|
+
constructor(stmt) {
|
|
1149
|
+
this.stmt = stmt;
|
|
1150
|
+
}
|
|
1151
|
+
stmt;
|
|
1152
|
+
run(...params) {
|
|
1153
|
+
return this.stmt.run(...params);
|
|
1154
|
+
}
|
|
1155
|
+
get(...params) {
|
|
1156
|
+
return this.stmt.get(...params);
|
|
1157
|
+
}
|
|
1158
|
+
all(...params) {
|
|
1159
|
+
return this.stmt.all(...params);
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
var SqliteDb = class {
|
|
1163
|
+
db;
|
|
1164
|
+
/** Current nesting depth of `transaction()` — drives BEGIN vs SAVEPOINT. */
|
|
1165
|
+
txDepth = 0;
|
|
1166
|
+
constructor(location) {
|
|
1167
|
+
this.db = new DatabaseSync(location);
|
|
1168
|
+
}
|
|
1169
|
+
prepare(sql) {
|
|
1170
|
+
return new StatementAdapter(this.db.prepare(sql));
|
|
1171
|
+
}
|
|
1172
|
+
exec(sql) {
|
|
1173
|
+
this.db.exec(sql);
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* better-sqlite3-compatible `pragma()`. Two call shapes are used in the repo:
|
|
1177
|
+
* - a SET: `pragma('journal_mode = WAL')` / `pragma('foreign_keys = ON')`
|
|
1178
|
+
* - a READ: `pragma('journal_mode', { simple: true })` → the scalar value
|
|
1179
|
+
* node:sqlite has no pragma helper, so route through exec/prepare. A statement
|
|
1180
|
+
* containing `=` is a set (no useful result); otherwise it's a read.
|
|
1181
|
+
*/
|
|
1182
|
+
pragma(source, opts = {}) {
|
|
1183
|
+
const isSet = source.includes("=");
|
|
1184
|
+
if (isSet) {
|
|
1185
|
+
this.db.exec(`PRAGMA ${source}`);
|
|
1186
|
+
return void 0;
|
|
1187
|
+
}
|
|
1188
|
+
const rows = this.db.prepare(`PRAGMA ${source}`).all();
|
|
1189
|
+
if (opts.simple === true) {
|
|
1190
|
+
const first = rows[0];
|
|
1191
|
+
if (first === void 0) return void 0;
|
|
1192
|
+
const keys = Object.keys(first);
|
|
1193
|
+
return keys.length > 0 ? first[keys[0]] : void 0;
|
|
1194
|
+
}
|
|
1195
|
+
return rows;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* better-sqlite3-compatible `transaction(fn)`: returns a callable that runs
|
|
1199
|
+
* `fn` atomically, rolling back on throw. node:sqlite has no transaction
|
|
1200
|
+
* helper, so wrap explicitly — and like better-sqlite3, be **savepoint-aware**
|
|
1201
|
+
* so NESTED `transaction()` calls don't error with "cannot start a transaction
|
|
1202
|
+
* within a transaction". The outermost call uses BEGIN/COMMIT/ROLLBACK; a
|
|
1203
|
+
* nested call uses a uniquely-named SAVEPOINT / RELEASE / ROLLBACK TO.
|
|
1204
|
+
*/
|
|
1205
|
+
transaction(fn) {
|
|
1206
|
+
return (...args) => {
|
|
1207
|
+
const nested = this.txDepth > 0;
|
|
1208
|
+
const savepoint = `qaios_sp_${this.txDepth}`;
|
|
1209
|
+
this.db.exec(nested ? `SAVEPOINT ${savepoint}` : "BEGIN");
|
|
1210
|
+
this.txDepth += 1;
|
|
1211
|
+
try {
|
|
1212
|
+
const result = fn(...args);
|
|
1213
|
+
this.db.exec(nested ? `RELEASE ${savepoint}` : "COMMIT");
|
|
1214
|
+
this.txDepth -= 1;
|
|
1215
|
+
return result;
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
this.txDepth -= 1;
|
|
1218
|
+
try {
|
|
1219
|
+
this.db.exec(nested ? `ROLLBACK TO ${savepoint}` : "ROLLBACK");
|
|
1220
|
+
if (nested) this.db.exec(`RELEASE ${savepoint}`);
|
|
1221
|
+
} catch {
|
|
1222
|
+
}
|
|
1223
|
+
throw err;
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
close() {
|
|
1228
|
+
this.db.close();
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1137
1231
|
var __dirname$1 = path12.dirname(fileURLToPath(import.meta.url));
|
|
1138
1232
|
var DEFAULT_MIGRATIONS_DIR = path12.resolve(__dirname$1, "migrations");
|
|
1139
1233
|
var Storage = class _Storage {
|
|
@@ -1150,7 +1244,7 @@ var Storage = class _Storage {
|
|
|
1150
1244
|
* (recommended for tests).
|
|
1151
1245
|
*/
|
|
1152
1246
|
static open(dbPath, opts = {}) {
|
|
1153
|
-
const db = new
|
|
1247
|
+
const db = new SqliteDb(dbPath);
|
|
1154
1248
|
try {
|
|
1155
1249
|
if (!opts.disableWal && dbPath !== ":memory:") {
|
|
1156
1250
|
db.pragma("journal_mode = WAL");
|
|
@@ -2083,6 +2177,12 @@ var PRICING = {
|
|
|
2083
2177
|
cacheReadPerMTok: 0.3,
|
|
2084
2178
|
cacheWritePerMTok: 3.75
|
|
2085
2179
|
},
|
|
2180
|
+
"claude-opus-4-8": {
|
|
2181
|
+
inputPerMTok: 15,
|
|
2182
|
+
outputPerMTok: 75,
|
|
2183
|
+
cacheReadPerMTok: 1.5,
|
|
2184
|
+
cacheWritePerMTok: 18.75
|
|
2185
|
+
},
|
|
2086
2186
|
"claude-opus-4-7": {
|
|
2087
2187
|
inputPerMTok: 15,
|
|
2088
2188
|
outputPerMTok: 75,
|
|
@@ -2581,6 +2681,20 @@ function tempForTier(tier) {
|
|
|
2581
2681
|
return 0.1;
|
|
2582
2682
|
}
|
|
2583
2683
|
}
|
|
2684
|
+
var DEFAULT_LLM_TIMEOUT_MS = 12e4;
|
|
2685
|
+
function llmTimeoutMs() {
|
|
2686
|
+
const raw = process.env["QAIOS_LLM_TIMEOUT_MS"];
|
|
2687
|
+
if (raw === void 0) return DEFAULT_LLM_TIMEOUT_MS;
|
|
2688
|
+
const n = Number.parseInt(raw, 10);
|
|
2689
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_LLM_TIMEOUT_MS;
|
|
2690
|
+
}
|
|
2691
|
+
function buildCallSignal(cancelSignal) {
|
|
2692
|
+
const ms = llmTimeoutMs();
|
|
2693
|
+
if (ms <= 0) return cancelSignal;
|
|
2694
|
+
const timeout = AbortSignal.timeout(ms);
|
|
2695
|
+
if (cancelSignal === void 0) return timeout;
|
|
2696
|
+
return AbortSignal.any([timeout, cancelSignal]);
|
|
2697
|
+
}
|
|
2584
2698
|
function schemaToJsonSchema(schema) {
|
|
2585
2699
|
const probe = schema;
|
|
2586
2700
|
if (typeof probe.toJSONSchema === "function") {
|
|
@@ -2650,7 +2764,8 @@ var SkillRunner = class {
|
|
|
2650
2764
|
maxTokens: 16384,
|
|
2651
2765
|
temperature: tempForTier(skill.modelTier)
|
|
2652
2766
|
};
|
|
2653
|
-
|
|
2767
|
+
const signal = buildCallSignal(ctx.cancelSignal);
|
|
2768
|
+
if (signal !== void 0) callOpts.signal = signal;
|
|
2654
2769
|
response = await ctx.llm.call(callOpts);
|
|
2655
2770
|
} catch (err) {
|
|
2656
2771
|
ctx.auditLogger.append({
|
|
@@ -6802,7 +6917,7 @@ function directoryIsEmpty(projectRoot) {
|
|
|
6802
6917
|
}
|
|
6803
6918
|
|
|
6804
6919
|
// src/commands/doctor.ts
|
|
6805
|
-
var MIN_NODE_MAJOR =
|
|
6920
|
+
var MIN_NODE_MAJOR = 22;
|
|
6806
6921
|
function runDoctorEnv() {
|
|
6807
6922
|
return { exitCode: ExitCode.SUCCESS, envSnapshot: snapshotEnv() };
|
|
6808
6923
|
}
|
|
@@ -9992,18 +10107,14 @@ function runInit(opts = {}) {
|
|
|
9992
10107
|
} catch {
|
|
9993
10108
|
}
|
|
9994
10109
|
const raw = err.message ?? String(err);
|
|
9995
|
-
const
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
const message = isBindingError ? `Could not load the native SQLite module (better-sqlite3). This usually means the prebuilt binary didn't download or doesn't match your Node version.
|
|
9999
|
-
\u2022 Ensure you're on Node 20 LTS (run \`node -v\`).
|
|
10000
|
-
\u2022 Reinstall to fetch/rebuild the binary: \`npm rebuild better-sqlite3\` (or reinstall qaios).
|
|
10001
|
-
\u2022 Behind a proxy/firewall? The binary is fetched from GitHub releases \u2014 allow that, or install build tools so it can compile.
|
|
10110
|
+
const isNodeTooOld = /node:sqlite|Cannot find module 'node:sqlite'|DatabaseSync/i.test(raw);
|
|
10111
|
+
const message = isNodeTooOld ? `QAIOS needs Node's built-in SQLite (Node 22 LTS or newer).
|
|
10112
|
+
\u2022 Check your version: \`node -v\` \u2014 upgrade to Node 22+ if it's older.
|
|
10002
10113
|
Original error: ${raw}` : `Failed to initialise workflows.db: ${raw}`;
|
|
10003
10114
|
return {
|
|
10004
10115
|
exitCode: err instanceof StorageError ? ExitCode.INTERNAL : ExitCode.TOOL_ERROR,
|
|
10005
10116
|
error: {
|
|
10006
|
-
code:
|
|
10117
|
+
code: isNodeTooOld ? "qaios.init.node_too_old" : err instanceof StorageError ? err.code : "qaios.init.db_failed",
|
|
10007
10118
|
message
|
|
10008
10119
|
},
|
|
10009
10120
|
detection
|
|
@@ -10426,6 +10537,13 @@ async function runMcp(opts) {
|
|
|
10426
10537
|
if (ownsStorage) storage.close();
|
|
10427
10538
|
}
|
|
10428
10539
|
}
|
|
10540
|
+
function withTimeout(promise, ms, message) {
|
|
10541
|
+
let timer;
|
|
10542
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
10543
|
+
timer = setTimeout(() => reject(new McpError("qaios.mcp.test_timeout", message)), ms);
|
|
10544
|
+
});
|
|
10545
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
10546
|
+
}
|
|
10429
10547
|
function listServers(repo, opts, writeOut) {
|
|
10430
10548
|
const servers = repo.list();
|
|
10431
10549
|
if (opts.json === true) {
|
|
@@ -10565,8 +10683,13 @@ async function testServer(repo, opts, writeOut) {
|
|
|
10565
10683
|
servers: [{ ...config, enabled: true }]
|
|
10566
10684
|
});
|
|
10567
10685
|
const ownsClient = opts.mcpClient === void 0;
|
|
10686
|
+
const MCP_TEST_TIMEOUT_MS = 15e3;
|
|
10568
10687
|
try {
|
|
10569
|
-
const tools = await
|
|
10688
|
+
const tools = await withTimeout(
|
|
10689
|
+
client.listTools(opts.name),
|
|
10690
|
+
MCP_TEST_TIMEOUT_MS,
|
|
10691
|
+
`MCP server "${opts.name}" did not respond within ${MCP_TEST_TIMEOUT_MS / 1e3}s \u2014 is it a valid MCP server?`
|
|
10692
|
+
);
|
|
10570
10693
|
writeOut(`\u2713 Connected to "${opts.name}". Tools (${tools.length}):`);
|
|
10571
10694
|
for (const t of tools) {
|
|
10572
10695
|
writeOut(` - ${t.name}${t.description !== void 0 ? ` \u2014 ${t.description}` : ""}`);
|
|
@@ -12190,7 +12313,11 @@ function buildProgram() {
|
|
|
12190
12313
|
}
|
|
12191
12314
|
process.exit(result.exitCode);
|
|
12192
12315
|
});
|
|
12193
|
-
program.command("explore <url>").description("Run an exploratory testing session against a URL").option(
|
|
12316
|
+
program.command("explore <url>").description("Run an exploratory testing session against a URL").option(
|
|
12317
|
+
"--duration <seconds>",
|
|
12318
|
+
"time budget in seconds (min 60, default 600)",
|
|
12319
|
+
(v) => parseInt(v, 10)
|
|
12320
|
+
).option("--focus <text>", "optional natural-language focus hint").option("--charter-only", "generate charter and stop; no findings, no defects").option("--no-defects", "skip defect.create + filing for findings").option("--defect-target <target>", "where to file defects: stdout | github | jira").option("--defect-repo <repo>", "github repo (owner/repo) when --defect-target=github").option("--defect-project <project>", "jira project key when --defect-target=jira").action(async (url, cmdOpts, command) => {
|
|
12194
12321
|
const globalOpts = command.parent?.opts() ?? {};
|
|
12195
12322
|
const opts = {
|
|
12196
12323
|
url,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qatonic_innovations/qaios",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI QA engineer in your terminal — designs, writes, runs, heals, and explores tests for web UI and APIs with audit-grade traceability.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"self-healing"
|
|
30
30
|
],
|
|
31
31
|
"engines": {
|
|
32
|
-
"node": ">=
|
|
32
|
+
"node": ">=22.0.0"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
|
35
35
|
"access": "public"
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@anthropic-ai/sdk": "^0.40.0",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
-
"better-sqlite3": "^11.7.0",
|
|
50
49
|
"commander": "^12.1.0",
|
|
51
50
|
"openai": "^4.77.0",
|
|
52
51
|
"ink": "^5.2.1",
|
|
@@ -62,7 +61,6 @@
|
|
|
62
61
|
"zod-to-json-schema": "^3.24.0"
|
|
63
62
|
},
|
|
64
63
|
"devDependencies": {
|
|
65
|
-
"@types/better-sqlite3": "^7.6.12",
|
|
66
64
|
"@types/pixelmatch": "^5.2.6",
|
|
67
65
|
"@types/pngjs": "^6.0.5",
|
|
68
66
|
"@types/react": "^18.3.28",
|