@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.
Files changed (3) hide show
  1. package/README.md +47 -17
  2. package/dist/index.js +141 -14
  3. 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 20 LTS recommended.** QAIOS bundles a native SQLite module
39
- (better-sqlite3) for its local audit log; Node 20 LTS has the widest
40
- prebuilt-binary coverage. Newer Node usually works but may need to compile
41
- the binary (build tools + network access).
42
- - An **Anthropic API key** — get one at
43
- [console.anthropic.com](https://console.anthropic.com/settings/keys), then
44
- put it in your environment:
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
- The key is read from the environment and **never written to disk** by QAIOS.
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
- If `qaios init` fails with a SQLite/native-binding error
61
- (`Could not load the native SQLite module`):
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 Database(dbPath);
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
- if (ctx.cancelSignal !== void 0) callOpts.signal = ctx.cancelSignal;
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 = 20;
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 isBindingError = /bindings file|was compiled against|NODE_MODULE_VERSION|\.node/i.test(
9996
- raw
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: isBindingError ? "qaios.init.sqlite_binding_missing" : err instanceof StorageError ? err.code : "qaios.init.db_failed",
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 client.listTools(opts.name);
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("--duration <seconds>", "time budget in seconds (default 600)", (v) => parseInt(v, 10)).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) => {
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.2.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": ">=20.0.0"
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",