@qatonic_innovations/qaios 0.1.2 → 0.3.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/README.md +78 -20
- package/dist/index.js +515 -63
- package/package.json +3 -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
|
|
|
@@ -159,8 +156,7 @@ mode: LITE # LITE | FULL | TRUST
|
|
|
159
156
|
app:
|
|
160
157
|
baseUrl: https://staging.myapp.com
|
|
161
158
|
llm:
|
|
162
|
-
provider: anthropic
|
|
163
|
-
apiKeyEnv: ANTHROPIC_API_KEY # key is read from env, never stored
|
|
159
|
+
provider: anthropic # anthropic | openai
|
|
164
160
|
maxLlmCallsPerWorkflow: 15
|
|
165
161
|
costAlertThresholdUsdCents: 50
|
|
166
162
|
testing:
|
|
@@ -176,6 +172,68 @@ defects:
|
|
|
176
172
|
|
|
177
173
|
`qaios config show` prints the resolved config; `qaios config set <key> <value>` validates against the schema before writing. **API keys are never written to config** — they come from environment variables.
|
|
178
174
|
|
|
175
|
+
### Choose your LLM provider
|
|
176
|
+
|
|
177
|
+
QAIOS works with **Anthropic** (default) or **OpenAI**. Set the provider in
|
|
178
|
+
`.qaios/config.yaml` and export that provider's key:
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
# Anthropic (default)
|
|
182
|
+
llm:
|
|
183
|
+
provider: anthropic # reads ANTHROPIC_API_KEY
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
# OpenAI
|
|
188
|
+
llm:
|
|
189
|
+
provider: openai # reads OPENAI_API_KEY
|
|
190
|
+
model: gpt-4o # optional; default gpt-4o (try gpt-4o-mini for lower cost)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
export OPENAI_API_KEY=sk-...
|
|
195
|
+
qaios doctor # confirms the configured provider's key is reachable
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The rest of QAIOS is provider-blind — every command (`test`, `run`, `fix`,
|
|
199
|
+
`explore`, `a11y`, …) works identically on either. Structured output uses each
|
|
200
|
+
provider's native guaranteed-schema mode (Anthropic forced tool-use / OpenAI
|
|
201
|
+
strict function calling), so generated artifacts stay schema-valid. You can
|
|
202
|
+
override the key's env-var name with `llm.apiKeyEnv` if needed.
|
|
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
|
+
|
|
179
237
|
### Operating modes
|
|
180
238
|
|
|
181
239
|
- **LITE** (default) — HIGH/CRITICAL risk pauses for review; routine work flows through.
|
|
@@ -190,8 +248,8 @@ qaios config set mode TRUST
|
|
|
190
248
|
|
|
191
249
|
## Cost & privacy
|
|
192
250
|
|
|
193
|
-
-
|
|
194
|
-
- **Local-first.** No telemetry, no phone-home. The only outbound traffic is (a) LLM calls to Anthropic with the prompts QAIOS builds, and (b) any MCP servers you configure. You bring your own API key; QAIOS does not proxy your requests. See [SECURITY.md](https://github.com/qatonic/qaios/blob/main/SECURITY.md) for exactly what's sent.
|
|
251
|
+
- Skills run on Claude Sonnet (Anthropic) or gpt-4o (OpenAI) per your `llm.provider`. A typical `qaios test` costs ~**$0.04–0.10**. Each workflow is capped (default `min(15 calls, $0.50)`, configurable) and aborts if exceeded.
|
|
252
|
+
- **Local-first.** No telemetry, no phone-home. The only outbound traffic is (a) LLM calls to your configured provider (Anthropic or OpenAI) with the prompts QAIOS builds, and (b) any MCP servers you configure. You bring your own API key; QAIOS does not proxy your requests. See [SECURITY.md](https://github.com/qatonic/qaios/blob/main/SECURITY.md) for exactly what's sent.
|
|
195
253
|
|
|
196
254
|
---
|
|
197
255
|
|
package/dist/index.js
CHANGED
|
@@ -6,11 +6,12 @@ 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';
|
|
14
|
+
import OpenAI from 'openai';
|
|
14
15
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
15
16
|
import { spawn, spawnSync } from 'child_process';
|
|
16
17
|
import { tmpdir } from 'os';
|
|
@@ -843,14 +844,23 @@ var QaiosConfig = z.object({
|
|
|
843
844
|
version: z.literal(1),
|
|
844
845
|
mode: Mode.default("LITE"),
|
|
845
846
|
llm: z.object({
|
|
846
|
-
provider
|
|
847
|
-
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
|
|
847
|
+
// Which LLM provider backs every skill. Default stays anthropic so
|
|
848
|
+
// existing projects are unchanged. Set `openai` to use OpenAI instead;
|
|
849
|
+
// the provider's own key env var (ANTHROPIC_API_KEY / OPENAI_API_KEY)
|
|
850
|
+
// is read automatically — see runtime/config/env.ts + llm/factory.ts.
|
|
851
|
+
provider: z.enum(["anthropic", "openai"]).default("anthropic"),
|
|
852
|
+
// Optional override of the env var that holds the API key. When unset,
|
|
853
|
+
// the factory picks the provider's conventional var.
|
|
854
|
+
apiKeyEnv: z.string().optional(),
|
|
855
|
+
// Optional explicit model id. When set, it overrides the per-tier
|
|
856
|
+
// models below for ALL tiers (single-model v0.1 behavior). When unset,
|
|
857
|
+
// the factory resolves the provider's default (Sonnet for anthropic,
|
|
858
|
+
// gpt-4o for openai).
|
|
859
|
+
model: z.string().optional(),
|
|
860
|
+
// v0.1: every tier maps to one model (claude-sonnet-4-6 for anthropic).
|
|
861
|
+
// There is no real tier routing yet. The skill-declared tier (1/2/3) is
|
|
862
|
+
// recorded in the audit log so v0.5 can light up real per-tier routing
|
|
863
|
+
// without re-tagging skills. Do not branch on tier in skill code.
|
|
854
864
|
models: z.object({
|
|
855
865
|
tier1: z.string().default("claude-sonnet-4-6"),
|
|
856
866
|
// v0.5 → opus
|
|
@@ -1124,6 +1134,92 @@ function ensureDirExists(dir) {
|
|
|
1124
1134
|
throw err;
|
|
1125
1135
|
}
|
|
1126
1136
|
}
|
|
1137
|
+
var nodeRequire = createRequire(import.meta.url);
|
|
1138
|
+
var { DatabaseSync } = nodeRequire("node:sqlite");
|
|
1139
|
+
var StatementAdapter = class {
|
|
1140
|
+
constructor(stmt) {
|
|
1141
|
+
this.stmt = stmt;
|
|
1142
|
+
}
|
|
1143
|
+
stmt;
|
|
1144
|
+
run(...params) {
|
|
1145
|
+
return this.stmt.run(...params);
|
|
1146
|
+
}
|
|
1147
|
+
get(...params) {
|
|
1148
|
+
return this.stmt.get(...params);
|
|
1149
|
+
}
|
|
1150
|
+
all(...params) {
|
|
1151
|
+
return this.stmt.all(...params);
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
var SqliteDb = class {
|
|
1155
|
+
db;
|
|
1156
|
+
/** Current nesting depth of `transaction()` — drives BEGIN vs SAVEPOINT. */
|
|
1157
|
+
txDepth = 0;
|
|
1158
|
+
constructor(location) {
|
|
1159
|
+
this.db = new DatabaseSync(location);
|
|
1160
|
+
}
|
|
1161
|
+
prepare(sql) {
|
|
1162
|
+
return new StatementAdapter(this.db.prepare(sql));
|
|
1163
|
+
}
|
|
1164
|
+
exec(sql) {
|
|
1165
|
+
this.db.exec(sql);
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* better-sqlite3-compatible `pragma()`. Two call shapes are used in the repo:
|
|
1169
|
+
* - a SET: `pragma('journal_mode = WAL')` / `pragma('foreign_keys = ON')`
|
|
1170
|
+
* - a READ: `pragma('journal_mode', { simple: true })` → the scalar value
|
|
1171
|
+
* node:sqlite has no pragma helper, so route through exec/prepare. A statement
|
|
1172
|
+
* containing `=` is a set (no useful result); otherwise it's a read.
|
|
1173
|
+
*/
|
|
1174
|
+
pragma(source, opts = {}) {
|
|
1175
|
+
const isSet = source.includes("=");
|
|
1176
|
+
if (isSet) {
|
|
1177
|
+
this.db.exec(`PRAGMA ${source}`);
|
|
1178
|
+
return void 0;
|
|
1179
|
+
}
|
|
1180
|
+
const rows = this.db.prepare(`PRAGMA ${source}`).all();
|
|
1181
|
+
if (opts.simple === true) {
|
|
1182
|
+
const first = rows[0];
|
|
1183
|
+
if (first === void 0) return void 0;
|
|
1184
|
+
const keys = Object.keys(first);
|
|
1185
|
+
return keys.length > 0 ? first[keys[0]] : void 0;
|
|
1186
|
+
}
|
|
1187
|
+
return rows;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* better-sqlite3-compatible `transaction(fn)`: returns a callable that runs
|
|
1191
|
+
* `fn` atomically, rolling back on throw. node:sqlite has no transaction
|
|
1192
|
+
* helper, so wrap explicitly — and like better-sqlite3, be **savepoint-aware**
|
|
1193
|
+
* so NESTED `transaction()` calls don't error with "cannot start a transaction
|
|
1194
|
+
* within a transaction". The outermost call uses BEGIN/COMMIT/ROLLBACK; a
|
|
1195
|
+
* nested call uses a uniquely-named SAVEPOINT / RELEASE / ROLLBACK TO.
|
|
1196
|
+
*/
|
|
1197
|
+
transaction(fn) {
|
|
1198
|
+
return (...args) => {
|
|
1199
|
+
const nested = this.txDepth > 0;
|
|
1200
|
+
const savepoint = `qaios_sp_${this.txDepth}`;
|
|
1201
|
+
this.db.exec(nested ? `SAVEPOINT ${savepoint}` : "BEGIN");
|
|
1202
|
+
this.txDepth += 1;
|
|
1203
|
+
try {
|
|
1204
|
+
const result = fn(...args);
|
|
1205
|
+
this.db.exec(nested ? `RELEASE ${savepoint}` : "COMMIT");
|
|
1206
|
+
this.txDepth -= 1;
|
|
1207
|
+
return result;
|
|
1208
|
+
} catch (err) {
|
|
1209
|
+
this.txDepth -= 1;
|
|
1210
|
+
try {
|
|
1211
|
+
this.db.exec(nested ? `ROLLBACK TO ${savepoint}` : "ROLLBACK");
|
|
1212
|
+
if (nested) this.db.exec(`RELEASE ${savepoint}`);
|
|
1213
|
+
} catch {
|
|
1214
|
+
}
|
|
1215
|
+
throw err;
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
close() {
|
|
1220
|
+
this.db.close();
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1127
1223
|
var __dirname$1 = path12.dirname(fileURLToPath(import.meta.url));
|
|
1128
1224
|
var DEFAULT_MIGRATIONS_DIR = path12.resolve(__dirname$1, "migrations");
|
|
1129
1225
|
var Storage = class _Storage {
|
|
@@ -1140,7 +1236,7 @@ var Storage = class _Storage {
|
|
|
1140
1236
|
* (recommended for tests).
|
|
1141
1237
|
*/
|
|
1142
1238
|
static open(dbPath, opts = {}) {
|
|
1143
|
-
const db = new
|
|
1239
|
+
const db = new SqliteDb(dbPath);
|
|
1144
1240
|
try {
|
|
1145
1241
|
if (!opts.disableWal && dbPath !== ":memory:") {
|
|
1146
1242
|
db.pragma("journal_mode = WAL");
|
|
@@ -2033,18 +2129,29 @@ function computeEntryHash(entry) {
|
|
|
2033
2129
|
const { hash: _ignored, ...rest } = entry;
|
|
2034
2130
|
return sha256Hex2(canonicalize(rest));
|
|
2035
2131
|
}
|
|
2036
|
-
function
|
|
2037
|
-
const v = process.env[
|
|
2132
|
+
function readKey(name) {
|
|
2133
|
+
const v = process.env[name];
|
|
2038
2134
|
if (typeof v !== "string") return void 0;
|
|
2039
2135
|
return v.trim().length > 0 ? v : void 0;
|
|
2040
2136
|
}
|
|
2137
|
+
function readAnthropicApiKey() {
|
|
2138
|
+
return readKey("ANTHROPIC_API_KEY");
|
|
2139
|
+
}
|
|
2140
|
+
function readOpenAiApiKey() {
|
|
2141
|
+
return readKey("OPENAI_API_KEY");
|
|
2142
|
+
}
|
|
2143
|
+
function readProviderApiKey(provider, apiKeyEnvOverride) {
|
|
2144
|
+
if (apiKeyEnvOverride !== void 0 && apiKeyEnvOverride.trim().length > 0) {
|
|
2145
|
+
return readKey(apiKeyEnvOverride);
|
|
2146
|
+
}
|
|
2147
|
+
return provider === "openai" ? readOpenAiApiKey() : readAnthropicApiKey();
|
|
2148
|
+
}
|
|
2041
2149
|
function snapshotEnv() {
|
|
2042
|
-
const
|
|
2150
|
+
const a = readAnthropicApiKey();
|
|
2151
|
+
const o = readOpenAiApiKey();
|
|
2043
2152
|
return {
|
|
2044
|
-
anthropicApiKey: {
|
|
2045
|
-
|
|
2046
|
-
length: k?.length ?? 0
|
|
2047
|
-
}
|
|
2153
|
+
anthropicApiKey: { present: a !== void 0, length: a?.length ?? 0 },
|
|
2154
|
+
openaiApiKey: { present: o !== void 0, length: o?.length ?? 0 }
|
|
2048
2155
|
};
|
|
2049
2156
|
}
|
|
2050
2157
|
var PRICING = {
|
|
@@ -2062,6 +2169,12 @@ var PRICING = {
|
|
|
2062
2169
|
cacheReadPerMTok: 0.3,
|
|
2063
2170
|
cacheWritePerMTok: 3.75
|
|
2064
2171
|
},
|
|
2172
|
+
"claude-opus-4-8": {
|
|
2173
|
+
inputPerMTok: 15,
|
|
2174
|
+
outputPerMTok: 75,
|
|
2175
|
+
cacheReadPerMTok: 1.5,
|
|
2176
|
+
cacheWritePerMTok: 18.75
|
|
2177
|
+
},
|
|
2065
2178
|
"claude-opus-4-7": {
|
|
2066
2179
|
inputPerMTok: 15,
|
|
2067
2180
|
outputPerMTok: 75,
|
|
@@ -2079,16 +2192,52 @@ var PRICING = {
|
|
|
2079
2192
|
outputPerMTok: 4,
|
|
2080
2193
|
cacheReadPerMTok: 0.08,
|
|
2081
2194
|
cacheWritePerMTok: 1
|
|
2195
|
+
},
|
|
2196
|
+
// ── OpenAI (provider: openai) ──────────────────────────────────────────
|
|
2197
|
+
// USD per 1M tokens. OpenAI bills cached input at ~0.5× the input rate;
|
|
2198
|
+
// there is no separate cache-write charge, so cacheWritePerMTok is 0.
|
|
2199
|
+
"gpt-4o": {
|
|
2200
|
+
inputPerMTok: 2.5,
|
|
2201
|
+
outputPerMTok: 10,
|
|
2202
|
+
cacheReadPerMTok: 1.25,
|
|
2203
|
+
cacheWritePerMTok: 0
|
|
2204
|
+
},
|
|
2205
|
+
"gpt-4o-mini": {
|
|
2206
|
+
inputPerMTok: 0.15,
|
|
2207
|
+
outputPerMTok: 0.6,
|
|
2208
|
+
cacheReadPerMTok: 0.075,
|
|
2209
|
+
cacheWritePerMTok: 0
|
|
2210
|
+
},
|
|
2211
|
+
"gpt-4.1": {
|
|
2212
|
+
inputPerMTok: 2,
|
|
2213
|
+
outputPerMTok: 8,
|
|
2214
|
+
cacheReadPerMTok: 0.5,
|
|
2215
|
+
cacheWritePerMTok: 0
|
|
2216
|
+
},
|
|
2217
|
+
"gpt-4.1-mini": {
|
|
2218
|
+
inputPerMTok: 0.4,
|
|
2219
|
+
outputPerMTok: 1.6,
|
|
2220
|
+
cacheReadPerMTok: 0.1,
|
|
2221
|
+
cacheWritePerMTok: 0
|
|
2082
2222
|
}
|
|
2083
2223
|
};
|
|
2084
2224
|
var DEFAULT_PRICING = PRICING["claude-sonnet-4-6"];
|
|
2225
|
+
var _unknownModelWarned = /* @__PURE__ */ new Set();
|
|
2085
2226
|
function computeCostUsdCents(model, usage) {
|
|
2086
|
-
const p = PRICING[model]
|
|
2227
|
+
const p = PRICING[model];
|
|
2228
|
+
if (p === void 0 && !_unknownModelWarned.has(model)) {
|
|
2229
|
+
_unknownModelWarned.add(model);
|
|
2230
|
+
process.stderr.write(
|
|
2231
|
+
`(qaios) warning: no pricing for model "${model}" \u2014 billing at the default rate. Costs in the audit log are approximate for this model.
|
|
2232
|
+
`
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
const pricing = p ?? DEFAULT_PRICING;
|
|
2087
2236
|
const tok = (n) => typeof n === "number" && Number.isFinite(n) && n > 0 ? n : 0;
|
|
2088
|
-
const inputUsd = tok(usage.inputTokens) / 1e6 *
|
|
2089
|
-
const outputUsd = tok(usage.outputTokens) / 1e6 *
|
|
2090
|
-
const cacheReadUsd = tok(usage.cacheReadTokens) / 1e6 *
|
|
2091
|
-
const cacheWriteUsd = tok(usage.cacheWriteTokens) / 1e6 *
|
|
2237
|
+
const inputUsd = tok(usage.inputTokens) / 1e6 * pricing.inputPerMTok;
|
|
2238
|
+
const outputUsd = tok(usage.outputTokens) / 1e6 * pricing.outputPerMTok;
|
|
2239
|
+
const cacheReadUsd = tok(usage.cacheReadTokens) / 1e6 * pricing.cacheReadPerMTok;
|
|
2240
|
+
const cacheWriteUsd = tok(usage.cacheWriteTokens) / 1e6 * pricing.cacheWritePerMTok;
|
|
2092
2241
|
const totalCents = (inputUsd + outputUsd + cacheReadUsd + cacheWriteUsd) * 100;
|
|
2093
2242
|
return Math.ceil(totalCents);
|
|
2094
2243
|
}
|
|
@@ -2177,6 +2326,260 @@ function mapResponse(response, latencyMs) {
|
|
|
2177
2326
|
stopReason: response.stop_reason
|
|
2178
2327
|
};
|
|
2179
2328
|
}
|
|
2329
|
+
var UNSUPPORTED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
2330
|
+
"minLength",
|
|
2331
|
+
"maxLength",
|
|
2332
|
+
"pattern",
|
|
2333
|
+
"format",
|
|
2334
|
+
"minimum",
|
|
2335
|
+
"maximum",
|
|
2336
|
+
"exclusiveMinimum",
|
|
2337
|
+
"exclusiveMaximum",
|
|
2338
|
+
"multipleOf",
|
|
2339
|
+
"minItems",
|
|
2340
|
+
"maxItems",
|
|
2341
|
+
"uniqueItems",
|
|
2342
|
+
"minProperties",
|
|
2343
|
+
"maxProperties",
|
|
2344
|
+
"default",
|
|
2345
|
+
"$schema",
|
|
2346
|
+
"patternProperties"
|
|
2347
|
+
]);
|
|
2348
|
+
function isPlainObject(v) {
|
|
2349
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2350
|
+
}
|
|
2351
|
+
function makeNullable(node) {
|
|
2352
|
+
const t = node["type"];
|
|
2353
|
+
if (typeof t === "string") {
|
|
2354
|
+
if (t === "null") return node;
|
|
2355
|
+
return { ...node, type: [t, "null"] };
|
|
2356
|
+
}
|
|
2357
|
+
if (Array.isArray(t)) {
|
|
2358
|
+
return t.includes("null") ? node : { ...node, type: [...t, "null"] };
|
|
2359
|
+
}
|
|
2360
|
+
return { anyOf: [node, { type: "null" }] };
|
|
2361
|
+
}
|
|
2362
|
+
function resolveRef(ref, root) {
|
|
2363
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
2364
|
+
const parts = ref.slice(2).split("/").map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
2365
|
+
let cur = root;
|
|
2366
|
+
for (const part of parts) {
|
|
2367
|
+
if (!isPlainObject(cur) && !Array.isArray(cur)) return void 0;
|
|
2368
|
+
cur = cur[part];
|
|
2369
|
+
}
|
|
2370
|
+
return isPlainObject(cur) ? cur : void 0;
|
|
2371
|
+
}
|
|
2372
|
+
function inlineRefs(node, root, seen) {
|
|
2373
|
+
if (Array.isArray(node)) return node.map((n) => inlineRefs(n, root, seen));
|
|
2374
|
+
if (!isPlainObject(node)) return node;
|
|
2375
|
+
const ref = node["$ref"];
|
|
2376
|
+
if (typeof ref === "string") {
|
|
2377
|
+
if (seen.has(ref)) return { ...node };
|
|
2378
|
+
const target = resolveRef(ref, root);
|
|
2379
|
+
if (target !== void 0) {
|
|
2380
|
+
const next = new Set(seen).add(ref);
|
|
2381
|
+
const { $ref: _drop, ...siblings } = node;
|
|
2382
|
+
const inlined = inlineRefs(target, root, next);
|
|
2383
|
+
return { ...inlined, ...siblings };
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
const out = {};
|
|
2387
|
+
for (const [k, v] of Object.entries(node)) out[k] = inlineRefs(v, root, seen);
|
|
2388
|
+
return out;
|
|
2389
|
+
}
|
|
2390
|
+
function openaiStrictify(schema) {
|
|
2391
|
+
if (!isPlainObject(schema)) return schema;
|
|
2392
|
+
return strictifyNode(inlineRefs(schema, schema, /* @__PURE__ */ new Set()));
|
|
2393
|
+
}
|
|
2394
|
+
function strictifyNode(schema) {
|
|
2395
|
+
if (!isPlainObject(schema)) return schema;
|
|
2396
|
+
const out = {};
|
|
2397
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2398
|
+
if (UNSUPPORTED_KEYWORDS.has(key)) continue;
|
|
2399
|
+
if (key === "properties" && isPlainObject(value)) {
|
|
2400
|
+
const props = {};
|
|
2401
|
+
for (const [propName, propSchema] of Object.entries(value)) {
|
|
2402
|
+
props[propName] = strictifyNode(propSchema);
|
|
2403
|
+
}
|
|
2404
|
+
out["properties"] = props;
|
|
2405
|
+
continue;
|
|
2406
|
+
}
|
|
2407
|
+
if (key === "items") {
|
|
2408
|
+
out["items"] = Array.isArray(value) ? value.map((v) => strictifyNode(v)) : strictifyNode(value);
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
if ((key === "anyOf" || key === "oneOf" || key === "allOf") && Array.isArray(value)) {
|
|
2412
|
+
out[key] = value.map((v) => strictifyNode(v));
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
out[key] = value;
|
|
2416
|
+
}
|
|
2417
|
+
if (out["type"] === "object" && isPlainObject(out["properties"])) {
|
|
2418
|
+
const props = out["properties"];
|
|
2419
|
+
const originalRequired = new Set(
|
|
2420
|
+
Array.isArray(schema["required"]) ? schema["required"] : []
|
|
2421
|
+
);
|
|
2422
|
+
const allKeys = Object.keys(props);
|
|
2423
|
+
for (const k of allKeys) {
|
|
2424
|
+
if (!originalRequired.has(k)) {
|
|
2425
|
+
props[k] = makeNullable(props[k]);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
out["required"] = allKeys;
|
|
2429
|
+
out["additionalProperties"] = false;
|
|
2430
|
+
}
|
|
2431
|
+
return out;
|
|
2432
|
+
}
|
|
2433
|
+
var DEFAULT_OPENAI_MODEL = "gpt-4o";
|
|
2434
|
+
var DEFAULT_MAX_TOKENS2 = 4096;
|
|
2435
|
+
var OpenAiClient = class {
|
|
2436
|
+
client;
|
|
2437
|
+
explicitApiKey;
|
|
2438
|
+
defaultModel;
|
|
2439
|
+
defaultMaxTokens;
|
|
2440
|
+
constructor(opts = {}) {
|
|
2441
|
+
this.client = opts.client ?? null;
|
|
2442
|
+
this.explicitApiKey = opts.apiKey;
|
|
2443
|
+
this.defaultModel = opts.defaultModel ?? DEFAULT_OPENAI_MODEL;
|
|
2444
|
+
this.defaultMaxTokens = opts.defaultMaxTokens ?? DEFAULT_MAX_TOKENS2;
|
|
2445
|
+
}
|
|
2446
|
+
resolveClient() {
|
|
2447
|
+
if (this.client !== null) return this.client;
|
|
2448
|
+
const apiKey = this.explicitApiKey ?? readOpenAiApiKey();
|
|
2449
|
+
if (apiKey === void 0 || apiKey.trim().length === 0) {
|
|
2450
|
+
throw new LlmError({
|
|
2451
|
+
code: "qaios.llm.api_key_missing",
|
|
2452
|
+
message: 'OPENAI_API_KEY is not set (llm.provider is "openai").\nGet one at https://platform.openai.com/api-keys, then:\n export OPENAI_API_KEY=sk-\u2026\n`qaios doctor` will confirm the key is reachable.'
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
const sdk = new OpenAI({ apiKey });
|
|
2456
|
+
this.client = sdk;
|
|
2457
|
+
return this.client;
|
|
2458
|
+
}
|
|
2459
|
+
async call(opts) {
|
|
2460
|
+
const client = this.resolveClient();
|
|
2461
|
+
const model = opts.model ?? this.defaultModel;
|
|
2462
|
+
const params = {
|
|
2463
|
+
model,
|
|
2464
|
+
max_tokens: opts.maxTokens ?? this.defaultMaxTokens,
|
|
2465
|
+
// OpenAI carries the system prompt as a leading system-role message.
|
|
2466
|
+
messages: [
|
|
2467
|
+
{ role: "system", content: opts.systemPrompt },
|
|
2468
|
+
{ role: "user", content: opts.userPrompt }
|
|
2469
|
+
]
|
|
2470
|
+
};
|
|
2471
|
+
if (opts.temperature !== void 0) params["temperature"] = opts.temperature;
|
|
2472
|
+
if (opts.tools && opts.tools.length > 0) {
|
|
2473
|
+
params["tools"] = opts.tools.map((t) => ({
|
|
2474
|
+
type: "function",
|
|
2475
|
+
function: {
|
|
2476
|
+
name: t.name,
|
|
2477
|
+
description: t.description,
|
|
2478
|
+
parameters: openaiStrictify(t.input_schema),
|
|
2479
|
+
strict: true
|
|
2480
|
+
}
|
|
2481
|
+
}));
|
|
2482
|
+
}
|
|
2483
|
+
if (opts.toolChoice !== void 0) {
|
|
2484
|
+
params["tool_choice"] = mapToolChoice(opts.toolChoice);
|
|
2485
|
+
}
|
|
2486
|
+
const reqOpts = {};
|
|
2487
|
+
if (opts.signal !== void 0) reqOpts.signal = opts.signal;
|
|
2488
|
+
const start = Date.now();
|
|
2489
|
+
const response = await client.chat.completions.create(params, reqOpts);
|
|
2490
|
+
const latencyMs = Date.now() - start;
|
|
2491
|
+
return mapResponse2(response, latencyMs);
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
function mapToolChoice(choice) {
|
|
2495
|
+
switch (choice.type) {
|
|
2496
|
+
case "auto":
|
|
2497
|
+
return "auto";
|
|
2498
|
+
case "any":
|
|
2499
|
+
return "required";
|
|
2500
|
+
case "tool":
|
|
2501
|
+
return { type: "function", function: { name: choice.name } };
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
function mapFinishReason(reason) {
|
|
2505
|
+
switch (reason) {
|
|
2506
|
+
case "stop":
|
|
2507
|
+
return "end_turn";
|
|
2508
|
+
case "tool_calls":
|
|
2509
|
+
case "function_call":
|
|
2510
|
+
return "tool_use";
|
|
2511
|
+
case "length":
|
|
2512
|
+
return "max_tokens";
|
|
2513
|
+
default:
|
|
2514
|
+
return reason;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
function mapResponse2(response, latencyMs) {
|
|
2518
|
+
const choice = response.choices[0];
|
|
2519
|
+
const message = choice?.message;
|
|
2520
|
+
const output = message?.content ?? "";
|
|
2521
|
+
const toolCalls = (message?.tool_calls ?? []).map((tc) => ({
|
|
2522
|
+
id: tc.id,
|
|
2523
|
+
name: tc.function.name,
|
|
2524
|
+
// OpenAI returns function arguments as a JSON STRING — parse to match the
|
|
2525
|
+
// parsed-object shape Anthropic's tool_use blocks give us. A malformed
|
|
2526
|
+
// payload surfaces as an LlmError rather than a silent {}.
|
|
2527
|
+
input: parseArguments(tc.function.arguments, tc.function.name)
|
|
2528
|
+
}));
|
|
2529
|
+
const promptTokens = response.usage?.prompt_tokens ?? 0;
|
|
2530
|
+
const cachedTokens = response.usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
2531
|
+
const usage = {
|
|
2532
|
+
// OpenAI's prompt_tokens INCLUDES cached tokens; bill the uncached portion
|
|
2533
|
+
// at the input rate and the cached portion at the cache-read rate.
|
|
2534
|
+
inputTokens: Math.max(0, promptTokens - cachedTokens),
|
|
2535
|
+
outputTokens: response.usage?.completion_tokens ?? 0
|
|
2536
|
+
};
|
|
2537
|
+
if (cachedTokens > 0) usage.cacheReadTokens = cachedTokens;
|
|
2538
|
+
return {
|
|
2539
|
+
output,
|
|
2540
|
+
toolCalls,
|
|
2541
|
+
usage,
|
|
2542
|
+
model: response.model,
|
|
2543
|
+
latencyMs,
|
|
2544
|
+
costUsdCents: computeCostUsdCents(response.model, usage),
|
|
2545
|
+
stopReason: mapFinishReason(choice?.finish_reason ?? null)
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
function parseArguments(raw, toolName) {
|
|
2549
|
+
let parsed;
|
|
2550
|
+
try {
|
|
2551
|
+
parsed = JSON.parse(raw);
|
|
2552
|
+
} catch (err) {
|
|
2553
|
+
throw new LlmError({
|
|
2554
|
+
code: "qaios.llm.malformed_tool_arguments",
|
|
2555
|
+
message: `OpenAI returned non-JSON arguments for tool "${toolName}".`,
|
|
2556
|
+
cause: err
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
return stripNulls(parsed);
|
|
2560
|
+
}
|
|
2561
|
+
function stripNulls(value) {
|
|
2562
|
+
if (Array.isArray(value)) return value.map(stripNulls);
|
|
2563
|
+
if (value !== null && typeof value === "object") {
|
|
2564
|
+
const out = {};
|
|
2565
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2566
|
+
if (v === null) continue;
|
|
2567
|
+
out[k] = stripNulls(v);
|
|
2568
|
+
}
|
|
2569
|
+
return out;
|
|
2570
|
+
}
|
|
2571
|
+
return value;
|
|
2572
|
+
}
|
|
2573
|
+
function defaultModelFor(provider) {
|
|
2574
|
+
return provider === "openai" ? DEFAULT_OPENAI_MODEL : DEFAULT_LLM_MODEL;
|
|
2575
|
+
}
|
|
2576
|
+
function createLlmClient(opts = {}) {
|
|
2577
|
+
if (opts.client !== void 0) return opts.client;
|
|
2578
|
+
const provider = opts.provider ?? "anthropic";
|
|
2579
|
+
const defaultModel = opts.model ?? defaultModelFor(provider);
|
|
2580
|
+
const clientOpts = opts.apiKey !== void 0 ? { apiKey: opts.apiKey, defaultModel } : { defaultModel };
|
|
2581
|
+
return provider === "openai" ? new OpenAiClient(clientOpts) : new LlmClient(clientOpts);
|
|
2582
|
+
}
|
|
2180
2583
|
var SkillError = class extends Error {
|
|
2181
2584
|
code;
|
|
2182
2585
|
skillId;
|
|
@@ -6021,7 +6424,7 @@ function parseOpenApi(specPath) {
|
|
|
6021
6424
|
function isRecord(v) {
|
|
6022
6425
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6023
6426
|
}
|
|
6024
|
-
function
|
|
6427
|
+
function resolveRef2(refValue, doc, depth = 0) {
|
|
6025
6428
|
if (depth > 4) return { $ref: refValue };
|
|
6026
6429
|
if (!refValue.startsWith("#/")) return { $ref: refValue };
|
|
6027
6430
|
const segments = refValue.slice(2).split("/").map((seg) => seg.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
@@ -6032,7 +6435,7 @@ function resolveRef(refValue, doc, depth = 0) {
|
|
|
6032
6435
|
if (cursor === void 0) return { $ref: refValue };
|
|
6033
6436
|
}
|
|
6034
6437
|
if (isRecord(cursor) && typeof cursor["$ref"] === "string") {
|
|
6035
|
-
return
|
|
6438
|
+
return resolveRef2(cursor["$ref"], doc, depth + 1);
|
|
6036
6439
|
}
|
|
6037
6440
|
return cursor;
|
|
6038
6441
|
}
|
|
@@ -6041,7 +6444,7 @@ function resolveRefsDeep(value, doc, depth = 0) {
|
|
|
6041
6444
|
if (Array.isArray(value)) return value.map((v) => resolveRefsDeep(v, doc, depth + 1));
|
|
6042
6445
|
if (!isRecord(value)) return value;
|
|
6043
6446
|
if (typeof value["$ref"] === "string") {
|
|
6044
|
-
const resolved =
|
|
6447
|
+
const resolved = resolveRef2(value["$ref"], doc);
|
|
6045
6448
|
return resolveRefsDeep(resolved, doc, depth + 1);
|
|
6046
6449
|
}
|
|
6047
6450
|
const out = {};
|
|
@@ -6491,7 +6894,7 @@ function directoryIsEmpty(projectRoot) {
|
|
|
6491
6894
|
}
|
|
6492
6895
|
|
|
6493
6896
|
// src/commands/doctor.ts
|
|
6494
|
-
var MIN_NODE_MAJOR =
|
|
6897
|
+
var MIN_NODE_MAJOR = 22;
|
|
6495
6898
|
function runDoctorEnv() {
|
|
6496
6899
|
return { exitCode: ExitCode.SUCCESS, envSnapshot: snapshotEnv() };
|
|
6497
6900
|
}
|
|
@@ -6499,7 +6902,7 @@ function runDoctor(opts = {}) {
|
|
|
6499
6902
|
const cwd = path12.resolve(opts.cwd ?? process.cwd());
|
|
6500
6903
|
const checks = [];
|
|
6501
6904
|
checks.push(checkNode());
|
|
6502
|
-
const apiKeyCheck =
|
|
6905
|
+
const apiKeyCheck = checkProviderApiKey(cwd);
|
|
6503
6906
|
checks.push(apiKeyCheck);
|
|
6504
6907
|
const qaiosDir = path12.join(cwd, ".qaios");
|
|
6505
6908
|
const qaiosExists = existsSync(qaiosDir) && safeIsDir(qaiosDir);
|
|
@@ -6568,23 +6971,42 @@ function checkNode() {
|
|
|
6568
6971
|
detail: `Node ${version} is below the required v${MIN_NODE_MAJOR}.`
|
|
6569
6972
|
};
|
|
6570
6973
|
}
|
|
6571
|
-
function
|
|
6572
|
-
const
|
|
6974
|
+
function resolveProviderFromConfig(cwd) {
|
|
6975
|
+
const candidate = path12.join(cwd, ".qaios", "config.yaml");
|
|
6976
|
+
if (!existsSync(candidate)) return { provider: "anthropic" };
|
|
6977
|
+
try {
|
|
6978
|
+
const raw = parse(readFileSync(candidate, "utf-8"));
|
|
6979
|
+
const parsed = QaiosConfig.safeParse(raw ?? { version: 1 });
|
|
6980
|
+
if (parsed.success) {
|
|
6981
|
+
const out = {
|
|
6982
|
+
provider: parsed.data.llm.provider
|
|
6983
|
+
};
|
|
6984
|
+
if (parsed.data.llm.apiKeyEnv !== void 0) out.apiKeyEnv = parsed.data.llm.apiKeyEnv;
|
|
6985
|
+
return out;
|
|
6986
|
+
}
|
|
6987
|
+
} catch {
|
|
6988
|
+
}
|
|
6989
|
+
return { provider: "anthropic" };
|
|
6990
|
+
}
|
|
6991
|
+
function checkProviderApiKey(cwd) {
|
|
6992
|
+
const { provider, apiKeyEnv } = resolveProviderFromConfig(cwd);
|
|
6993
|
+
const envVar = apiKeyEnv ?? (provider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY");
|
|
6994
|
+
const key = readProviderApiKey(provider, apiKeyEnv);
|
|
6573
6995
|
if (key !== void 0) {
|
|
6574
|
-
return { name:
|
|
6996
|
+
return { name: envVar, status: "ok", detail: `set in environment (provider: ${provider})` };
|
|
6575
6997
|
}
|
|
6576
|
-
const raw = process.env[
|
|
6998
|
+
const raw = process.env[envVar];
|
|
6577
6999
|
if (typeof raw === "string" && raw.length > 0) {
|
|
6578
7000
|
return {
|
|
6579
|
-
name:
|
|
7001
|
+
name: envVar,
|
|
6580
7002
|
status: "warn",
|
|
6581
7003
|
detail: "set but blank/whitespace-only \u2014 LLM commands will fail; export a real key"
|
|
6582
7004
|
};
|
|
6583
7005
|
}
|
|
6584
7006
|
return {
|
|
6585
|
-
name:
|
|
7007
|
+
name: envVar,
|
|
6586
7008
|
status: "warn",
|
|
6587
|
-
detail:
|
|
7009
|
+
detail: `not set; export it before running LLM-backed commands (provider: ${provider})`
|
|
6588
7010
|
};
|
|
6589
7011
|
}
|
|
6590
7012
|
function checkDb(dbPath) {
|
|
@@ -6874,12 +7296,20 @@ Use formal techniques (same enumeration as design.web). Prefer:
|
|
|
6874
7296
|
Rules:
|
|
6875
7297
|
- steps describe HTTP interactions: "POST /api/v1/users with body {email, password}",
|
|
6876
7298
|
not implementation details.
|
|
6877
|
-
- oracles describe response shape + status code + observable side effects.
|
|
7299
|
+
- oracles describe response shape + status code + observable side effects. EVERY oracle
|
|
6878
7300
|
MUST reference an HTTP status code (e.g. "200", "201", "401", "404") AND/OR a
|
|
6879
7301
|
response.<property> path so the writer skill can produce a deterministic assertion.
|
|
7302
|
+
An oracle that names neither is invalid \u2014 do not emit it.
|
|
6880
7303
|
- dataNeeds describe request body categories: "valid signup payload", "payload missing
|
|
6881
7304
|
email", "payload with email > 254 chars".
|
|
6882
7305
|
- For each endpoint, generate at minimum 3 scenarios; more if the endpoint is risk-tagged.
|
|
7306
|
+
- COVERAGE FLOOR (non-negotiable): across the suite you MUST include at least one
|
|
7307
|
+
scenario with testType="negative" (e.g. invalid auth / 4xx) AND at least one with
|
|
7308
|
+
testType="boundary" (e.g. a min/max field length or numeric edge). Suites missing
|
|
7309
|
+
either are incomplete.
|
|
7310
|
+
- Every requirement id provided in the input MUST be referenced by at least one
|
|
7311
|
+
scenario's requirementIds \u2014 do not leave a stated requirement uncovered, and do not
|
|
7312
|
+
cite a requirement id that wasn't given.
|
|
6883
7313
|
- Cross-endpoint dependency tests (e.g., create then read) get their own scenario with
|
|
6884
7314
|
testType=integration.
|
|
6885
7315
|
|
|
@@ -6954,19 +7384,26 @@ function checkAuthScenarios(output, endpoints) {
|
|
|
6954
7384
|
const authNeeded = endpoints.filter((e) => e.authRequired);
|
|
6955
7385
|
if (authNeeded.length === 0) return 1;
|
|
6956
7386
|
const scenarios = output.designSpec.scenarios;
|
|
6957
|
-
const
|
|
7387
|
+
const isAuthNegative = (s) => {
|
|
7388
|
+
if (s.testType !== "negative") return false;
|
|
7389
|
+
const oracleMentionsAuthCode = /\b401\b|\b403\b|unauthor|forbidden/i.test(s.oracle);
|
|
7390
|
+
const dataNeedsMentionsAuth = s.dataNeeds.some(
|
|
7391
|
+
(d) => /\b(missing|invalid|no|expired|wrong)\b.*(token|auth|key|credential|role)/i.test(d)
|
|
7392
|
+
);
|
|
7393
|
+
return oracleMentionsAuthCode || dataNeedsMentionsAuth;
|
|
7394
|
+
};
|
|
7395
|
+
const anyAuthTest = authNeeded.some((ep) => {
|
|
6958
7396
|
const re = endpointStepPattern(ep.path, ep.method);
|
|
6959
|
-
return scenarios.some((s) =>
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
return s.testType === "negative" && matchesEndpoint && (oracleMentions401 || dataNeedsMentionsAuth);
|
|
6967
|
-
});
|
|
7397
|
+
return scenarios.some((s) => isAuthNegative(s) && re.test(s.steps.join("\n")));
|
|
7398
|
+
});
|
|
7399
|
+
const hasGenericAuthTest = scenarios.some(isAuthNegative);
|
|
7400
|
+
if (!anyAuthTest && !hasGenericAuthTest) return 0.5;
|
|
7401
|
+
const explicitlyCovered = authNeeded.filter((ep) => {
|
|
7402
|
+
const re = endpointStepPattern(ep.path, ep.method);
|
|
7403
|
+
return scenarios.some((s) => isAuthNegative(s) && re.test(s.steps.join("\n")));
|
|
6968
7404
|
}).length;
|
|
6969
|
-
|
|
7405
|
+
const fraction = explicitlyCovered / authNeeded.length;
|
|
7406
|
+
return Math.max(0.85, fraction);
|
|
6970
7407
|
}
|
|
6971
7408
|
var designApiSkill = {
|
|
6972
7409
|
id: "design.api",
|
|
@@ -8203,6 +8640,15 @@ var skills = {
|
|
|
8203
8640
|
"audit.a11y": auditA11ySkill
|
|
8204
8641
|
};
|
|
8205
8642
|
|
|
8643
|
+
// src/llm.ts
|
|
8644
|
+
function resolveLlmClient(injected, llmConfig) {
|
|
8645
|
+
if (injected !== void 0) return injected;
|
|
8646
|
+
const opts = {};
|
|
8647
|
+
if (llmConfig?.provider !== void 0) opts.provider = llmConfig.provider;
|
|
8648
|
+
if (llmConfig?.model !== void 0) opts.model = llmConfig.model;
|
|
8649
|
+
return createLlmClient(opts);
|
|
8650
|
+
}
|
|
8651
|
+
|
|
8206
8652
|
// src/commands/a11y.ts
|
|
8207
8653
|
function loadConfig(cwd) {
|
|
8208
8654
|
const candidate = path12.join(cwd, ".qaios", "config.yaml");
|
|
@@ -8243,7 +8689,7 @@ async function runA11y(opts) {
|
|
|
8243
8689
|
const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
|
|
8244
8690
|
const auditLogger = new AuditLogger(storage.db);
|
|
8245
8691
|
const workflowsRepo = new WorkflowsRepository(storage.db);
|
|
8246
|
-
const llm = opts.llm
|
|
8692
|
+
const llm = resolveLlmClient(opts.llm, config?.llm);
|
|
8247
8693
|
const writeOut = (line) => {
|
|
8248
8694
|
if (opts.quiet === true) return;
|
|
8249
8695
|
if (opts.json === true) process.stdout.write(JSON.stringify({ kind: "log", line }) + "\n");
|
|
@@ -8567,7 +9013,7 @@ async function runExplore(opts) {
|
|
|
8567
9013
|
const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
|
|
8568
9014
|
const auditLogger = new AuditLogger(storage.db);
|
|
8569
9015
|
const workflowsRepo = new WorkflowsRepository(storage.db);
|
|
8570
|
-
const llm = opts.llm
|
|
9016
|
+
const llm = resolveLlmClient(opts.llm, config?.llm);
|
|
8571
9017
|
const writeOut = (line) => {
|
|
8572
9018
|
if (opts.quiet === true) return;
|
|
8573
9019
|
if (opts.json === true) {
|
|
@@ -8978,7 +9424,7 @@ async function runFix(opts) {
|
|
|
8978
9424
|
const testResultsRepo = new TestResultsRepository(storage.db);
|
|
8979
9425
|
const workflowsRepo = new WorkflowsRepository(storage.db);
|
|
8980
9426
|
const config = loadConfig3(cwd);
|
|
8981
|
-
const llm = opts.llm
|
|
9427
|
+
const llm = resolveLlmClient(opts.llm, config?.llm);
|
|
8982
9428
|
const mode = opts.mode ?? config?.mode ?? "LITE";
|
|
8983
9429
|
const writeOut = (line) => {
|
|
8984
9430
|
if (opts.quiet === true) return;
|
|
@@ -9638,18 +10084,14 @@ function runInit(opts = {}) {
|
|
|
9638
10084
|
} catch {
|
|
9639
10085
|
}
|
|
9640
10086
|
const raw = err.message ?? String(err);
|
|
9641
|
-
const
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
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.
|
|
9645
|
-
\u2022 Ensure you're on Node 20 LTS (run \`node -v\`).
|
|
9646
|
-
\u2022 Reinstall to fetch/rebuild the binary: \`npm rebuild better-sqlite3\` (or reinstall qaios).
|
|
9647
|
-
\u2022 Behind a proxy/firewall? The binary is fetched from GitHub releases \u2014 allow that, or install build tools so it can compile.
|
|
10087
|
+
const isNodeTooOld = /node:sqlite|Cannot find module 'node:sqlite'|DatabaseSync/i.test(raw);
|
|
10088
|
+
const message = isNodeTooOld ? `QAIOS needs Node's built-in SQLite (Node 22 LTS or newer).
|
|
10089
|
+
\u2022 Check your version: \`node -v\` \u2014 upgrade to Node 22+ if it's older.
|
|
9648
10090
|
Original error: ${raw}` : `Failed to initialise workflows.db: ${raw}`;
|
|
9649
10091
|
return {
|
|
9650
10092
|
exitCode: err instanceof StorageError ? ExitCode.INTERNAL : ExitCode.TOOL_ERROR,
|
|
9651
10093
|
error: {
|
|
9652
|
-
code:
|
|
10094
|
+
code: isNodeTooOld ? "qaios.init.node_too_old" : err instanceof StorageError ? err.code : "qaios.init.db_failed",
|
|
9653
10095
|
message
|
|
9654
10096
|
},
|
|
9655
10097
|
detection
|
|
@@ -9758,7 +10200,7 @@ function readRawConfig(configPath) {
|
|
|
9758
10200
|
}
|
|
9759
10201
|
return parsed;
|
|
9760
10202
|
}
|
|
9761
|
-
function
|
|
10203
|
+
function readKey2(obj, key) {
|
|
9762
10204
|
const segments = key.split(".");
|
|
9763
10205
|
let cursor = obj;
|
|
9764
10206
|
for (const seg of segments) {
|
|
@@ -9849,7 +10291,7 @@ function getValue(configPath, opts, writeOut) {
|
|
|
9849
10291
|
}
|
|
9850
10292
|
return { exitCode: ExitCode.SUCCESS, value: raw };
|
|
9851
10293
|
}
|
|
9852
|
-
const value =
|
|
10294
|
+
const value = readKey2(raw, opts.key);
|
|
9853
10295
|
if (value === void 0) {
|
|
9854
10296
|
return {
|
|
9855
10297
|
exitCode: ExitCode.USER_ERROR,
|
|
@@ -10240,6 +10682,16 @@ async function testServer(repo, opts, writeOut) {
|
|
|
10240
10682
|
if (ownsClient) await client.close();
|
|
10241
10683
|
}
|
|
10242
10684
|
}
|
|
10685
|
+
function loadLlmConfig(cwd) {
|
|
10686
|
+
const candidate = path12.join(cwd, ".qaios", "config.yaml");
|
|
10687
|
+
if (!existsSync(candidate)) return void 0;
|
|
10688
|
+
try {
|
|
10689
|
+
const parsed = parse(readFileSync(candidate, "utf-8"));
|
|
10690
|
+
return parsed?.llm;
|
|
10691
|
+
} catch {
|
|
10692
|
+
return void 0;
|
|
10693
|
+
}
|
|
10694
|
+
}
|
|
10243
10695
|
async function applyDecision(args) {
|
|
10244
10696
|
const { gate, action, gatesRepo, auditLogger, orchestrator, skipResume } = args;
|
|
10245
10697
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -10300,7 +10752,7 @@ async function runReview(opts) {
|
|
|
10300
10752
|
const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
|
|
10301
10753
|
const auditLogger = new AuditLogger(storage.db);
|
|
10302
10754
|
const gatesRepo = new GatesRepository(storage.db);
|
|
10303
|
-
const llm = opts.llm
|
|
10755
|
+
const llm = resolveLlmClient(opts.llm, loadLlmConfig(cwd));
|
|
10304
10756
|
try {
|
|
10305
10757
|
const pending = gatesRepo.listPending(opts.workflowId);
|
|
10306
10758
|
if (pending.length === 0) {
|
|
@@ -10525,8 +10977,8 @@ async function runRun(opts) {
|
|
|
10525
10977
|
const ownsStorage = opts.storage === void 0;
|
|
10526
10978
|
const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
|
|
10527
10979
|
const auditLogger = new AuditLogger(storage.db);
|
|
10528
|
-
const llm = opts.llm ?? new LlmClient();
|
|
10529
10980
|
const config = loadRunConfig(cwd);
|
|
10981
|
+
const llm = resolveLlmClient(opts.llm, config?.llm);
|
|
10530
10982
|
const args = {
|
|
10531
10983
|
cwd,
|
|
10532
10984
|
noClassify: opts.noClassify === true,
|
|
@@ -10825,7 +11277,7 @@ async function runSnapshotCheck(opts) {
|
|
|
10825
11277
|
const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
|
|
10826
11278
|
const auditLogger = new AuditLogger(storage.db);
|
|
10827
11279
|
const baselineRepo = new VisualBaselinesRepository(storage.db);
|
|
10828
|
-
const llm = opts.llm
|
|
11280
|
+
const llm = resolveLlmClient(opts.llm, config?.llm);
|
|
10829
11281
|
let baselines = baselineRepo.list();
|
|
10830
11282
|
if (opts.feature !== void 0) {
|
|
10831
11283
|
const feat = opts.feature;
|
|
@@ -11401,7 +11853,7 @@ ${epSummary}`;
|
|
|
11401
11853
|
const ownsStorage = opts.storage === void 0;
|
|
11402
11854
|
const storage = opts.storage ?? Storage.open(path12.join(qaiosDir, "workflows.db"), { skipMigrations: false });
|
|
11403
11855
|
const auditLogger = new AuditLogger(storage.db);
|
|
11404
|
-
const llm = opts.llm
|
|
11856
|
+
const llm = resolveLlmClient(opts.llm, config?.llm);
|
|
11405
11857
|
const gateConfig = {};
|
|
11406
11858
|
if (opts.nonInteractive === true) gateConfig.nonInteractive = true;
|
|
11407
11859
|
if (config?.gates?.autoExpireOnTimeout !== void 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qatonic_innovations/qaios",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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,8 +46,8 @@
|
|
|
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",
|
|
50
|
+
"openai": "^4.77.0",
|
|
51
51
|
"ink": "^5.2.1",
|
|
52
52
|
"pino": "^9.5.0",
|
|
53
53
|
"pino-pretty": "^11.3.0",
|
|
@@ -61,7 +61,6 @@
|
|
|
61
61
|
"zod-to-json-schema": "^3.24.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@types/better-sqlite3": "^7.6.12",
|
|
65
64
|
"@types/pixelmatch": "^5.2.6",
|
|
66
65
|
"@types/pngjs": "^6.0.5",
|
|
67
66
|
"@types/react": "^18.3.28",
|