mira-harness 0.2.4 → 0.2.6
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 +6 -4
- package/dist/catalog.d.ts +3 -0
- package/dist/cli.js +77 -13
- package/dist/index.js +4 -0
- package/dist/mcp.js +4 -0
- package/llms.txt +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,8 +100,8 @@ a "typing…" fallback for a slow bot — replies run 5–62s) and capture, per
|
|
|
100
100
|
|---|---|
|
|
101
101
|
| `login` | One-time interactive login → prints `TG_SESSION` |
|
|
102
102
|
| `doctor` | Check `.env` / session / connectivity / @mira resolution (read-only) |
|
|
103
|
-
| `send [message...]` | One probe → full reply as JSON (message via arg or stdin). `--quiet --settle --timeout --no-log
|
|
104
|
-
| `loop` | Run the catalog paced; grades `expect` probes (exit 1 on failure). `--category --max --confirm --peer --gap --settle --timeout --list --catalog --grep --no-fail --quiet` |
|
|
103
|
+
| `send [message...]` | One probe → full reply as JSON (message via arg or stdin). `--quiet --settle --timeout --no-log --expect-*` |
|
|
104
|
+
| `loop` | Run the catalog paced; grades `expect` probes (exit 1 on failure). `--category --max --confirm --peer --gap --settle --timeout --list --catalog --grep --only --no-fail --quiet` |
|
|
105
105
|
| `catalog` | List the catalog (no sends). `--category --catalog --json` |
|
|
106
106
|
| `watch` | Live-tail @mira's messages (observe-only). `--peer` |
|
|
107
107
|
| `report` | Distill the run log into Markdown. `--in --out --category` |
|
|
@@ -164,8 +164,10 @@ graded probe fails — so it drops straight into CI. Add `--no-fail` to report w
|
|
|
164
164
|
|
|
165
165
|
`assert` re-grades a **saved** run log against the catalog's `expect` (offline, no @mira) — the
|
|
166
166
|
fast loop for *developing* assertions (capture once, then tune `expect` and re-grade instantly),
|
|
167
|
-
and a way to gate a committed run-log fixture in CI **without a Telegram session**.
|
|
168
|
-
|
|
167
|
+
and a way to gate a committed run-log fixture in CI **without a Telegram session**. For a one-off
|
|
168
|
+
check, `send` takes inline assertions: `send "What can you do?" --expect-min-links 1 --expect-max-ms 60000`
|
|
169
|
+
(any `--expect-*` exits 1 on failure). Run a subset of probes by id with `loop --grep <regex>` or
|
|
170
|
+
`loop --only <id1,id2>`; emit a catalog JSON Schema for your editor with `schema`.
|
|
169
171
|
|
|
170
172
|
### Drift detection
|
|
171
173
|
|
package/dist/catalog.d.ts
CHANGED
|
@@ -85,3 +85,6 @@ export declare function probesFor(category?: string, source?: Probe[]): Probe[];
|
|
|
85
85
|
/** Keep probes whose `id` matches the pattern (regex, case-insensitive; falls
|
|
86
86
|
* back to substring on an invalid regex). Used by `loop --grep` to run a subset. */
|
|
87
87
|
export declare function grepProbes(probes: Probe[], pattern: string): Probe[];
|
|
88
|
+
/** Keep only probes whose `id` is in the comma-separated list (exact match).
|
|
89
|
+
* Used by `loop --only` for precise selection. */
|
|
90
|
+
export declare function onlyProbes(probes: Probe[], idsCsv: string): Probe[];
|
package/dist/cli.js
CHANGED
|
@@ -3,11 +3,6 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
|
-
// src/catalog.ts
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
|
-
import { resolve } from "node:path";
|
|
9
|
-
import { z as z2 } from "zod";
|
|
10
|
-
|
|
11
6
|
// src/assert.ts
|
|
12
7
|
import { z } from "zod";
|
|
13
8
|
var ExpectSchema = z.object({
|
|
@@ -77,6 +72,9 @@ function evaluate(expect, result) {
|
|
|
77
72
|
}
|
|
78
73
|
|
|
79
74
|
// src/catalog.ts
|
|
75
|
+
import { readFileSync } from "node:fs";
|
|
76
|
+
import { resolve } from "node:path";
|
|
77
|
+
import { z as z2 } from "zod";
|
|
80
78
|
var CATEGORIES = ["core", "skills", "generation", "wallet"];
|
|
81
79
|
var ProbeSchema = z2.object({
|
|
82
80
|
id: z2.string().min(1),
|
|
@@ -307,6 +305,10 @@ function grepProbes(probes, pattern) {
|
|
|
307
305
|
const needle = pattern.toLowerCase();
|
|
308
306
|
return probes.filter((p) => p.id.toLowerCase().includes(needle));
|
|
309
307
|
}
|
|
308
|
+
function onlyProbes(probes, idsCsv) {
|
|
309
|
+
const ids = new Set(idsCsv.split(",").map((s) => s.trim()).filter(Boolean));
|
|
310
|
+
return probes.filter((p) => ids.has(p.id));
|
|
311
|
+
}
|
|
310
312
|
|
|
311
313
|
// src/commands/assert.ts
|
|
312
314
|
import { resolve as resolve4 } from "node:path";
|
|
@@ -362,7 +364,7 @@ var TIPS = [
|
|
|
362
364
|
"MIRA_NO_BANNER=1 hides this banner · --quiet hides all decoration"
|
|
363
365
|
];
|
|
364
366
|
function banner(version, opts = {}) {
|
|
365
|
-
if (opts.quiet || process.env.MIRA_NO_BANNER || !process.stderr.isTTY)
|
|
367
|
+
if (opts.quiet || process.env.MIRA_NO_BANNER || !process.stderr.isTTY || !process.stdout.isTTY)
|
|
366
368
|
return;
|
|
367
369
|
const tip = TIPS[new Date().getDate() % TIPS.length];
|
|
368
370
|
const lines = [
|
|
@@ -613,6 +615,8 @@ function listCatalog(opts = {}) {
|
|
|
613
615
|
let probes = probesFor(opts.category, source);
|
|
614
616
|
if (opts.grep)
|
|
615
617
|
probes = grepProbes(probes, opts.grep);
|
|
618
|
+
if (opts.only)
|
|
619
|
+
probes = onlyProbes(probes, opts.only);
|
|
616
620
|
if (opts.max !== undefined)
|
|
617
621
|
probes = probes.slice(0, opts.max);
|
|
618
622
|
if (opts.json) {
|
|
@@ -1173,7 +1177,7 @@ async function loop(opts) {
|
|
|
1173
1177
|
process.exit(1);
|
|
1174
1178
|
}
|
|
1175
1179
|
if (opts.list) {
|
|
1176
|
-
listCatalog({ category: opts.category, max: opts.max, catalog: opts.catalog, grep: opts.grep });
|
|
1180
|
+
listCatalog({ category: opts.category, max: opts.max, catalog: opts.catalog, grep: opts.grep, only: opts.only });
|
|
1177
1181
|
return;
|
|
1178
1182
|
}
|
|
1179
1183
|
const source = opts.catalog ? loadCatalog(opts.catalog) : CATALOG;
|
|
@@ -1192,10 +1196,15 @@ async function loop(opts) {
|
|
|
1192
1196
|
console.error("TG_SESSION is empty — run `mira-harness login` first, then put it in .env.");
|
|
1193
1197
|
process.exit(1);
|
|
1194
1198
|
}
|
|
1195
|
-
|
|
1199
|
+
let matched = probesFor(opts.category, source);
|
|
1200
|
+
if (opts.grep)
|
|
1201
|
+
matched = grepProbes(matched, opts.grep);
|
|
1202
|
+
if (opts.only)
|
|
1203
|
+
matched = onlyProbes(matched, opts.only);
|
|
1196
1204
|
const probes = matched.slice(0, opts.max);
|
|
1197
1205
|
if (!probes.length) {
|
|
1198
|
-
|
|
1206
|
+
const why = opts.only ? `--only "${opts.only}"` : opts.grep ? `--grep "${opts.grep}"` : undefined;
|
|
1207
|
+
console.error(why ? `no probes match ${why}.` : "no probes selected.");
|
|
1199
1208
|
process.exit(1);
|
|
1200
1209
|
}
|
|
1201
1210
|
const gap = opts.gap ?? DEFAULT_GAP_MS;
|
|
@@ -1313,6 +1322,7 @@ async function send(rawMessage, opts = {}) {
|
|
|
1313
1322
|
collect.firstReplyTimeoutMs = opts.timeout;
|
|
1314
1323
|
const peer = tgEnv.miraPeer;
|
|
1315
1324
|
const client = await connect(session);
|
|
1325
|
+
let verdict;
|
|
1316
1326
|
try {
|
|
1317
1327
|
const result = await withProgress(`@${peer}`, () => sendAndCollect(client, peer, message, collect), opts.quiet);
|
|
1318
1328
|
if (!opts.noLog)
|
|
@@ -1321,9 +1331,21 @@ async function send(rawMessage, opts = {}) {
|
|
|
1321
1331
|
note(result.timedOut ? c.yellow("no reply (timed out)") : c.green(`${result.messages.length} message(s) · first reply ${((result.firstReplyMs ?? 0) / 1000).toFixed(1)}s`));
|
|
1322
1332
|
}
|
|
1323
1333
|
console.log(JSON.stringify(result, null, 2));
|
|
1334
|
+
if (opts.expect)
|
|
1335
|
+
verdict = evaluate(opts.expect, result);
|
|
1324
1336
|
} finally {
|
|
1325
1337
|
await client.disconnect();
|
|
1326
1338
|
}
|
|
1339
|
+
if (verdict) {
|
|
1340
|
+
if (!opts.quiet) {
|
|
1341
|
+
note(verdict.ok ? c.green("✓ assertions passed") : c.red("✗ assertions failed"));
|
|
1342
|
+
if (!verdict.ok)
|
|
1343
|
+
for (const ch of verdict.checks.filter((x) => !x.ok))
|
|
1344
|
+
note(c.dim(` ✗ ${ch.name}: ${ch.detail}`));
|
|
1345
|
+
}
|
|
1346
|
+
if (!verdict.ok)
|
|
1347
|
+
process.exit(1);
|
|
1348
|
+
}
|
|
1327
1349
|
}
|
|
1328
1350
|
|
|
1329
1351
|
// src/commands/stats.ts
|
|
@@ -1501,6 +1523,41 @@ function posInt(name, v) {
|
|
|
1501
1523
|
}
|
|
1502
1524
|
return n;
|
|
1503
1525
|
}
|
|
1526
|
+
function nonNegInt(name, v) {
|
|
1527
|
+
const n = Number(v);
|
|
1528
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
1529
|
+
console.error(`--${name} must be a non-negative integer (got "${v}")`);
|
|
1530
|
+
process.exit(1);
|
|
1531
|
+
}
|
|
1532
|
+
return n;
|
|
1533
|
+
}
|
|
1534
|
+
function buildSendExpect(o) {
|
|
1535
|
+
const raw = {};
|
|
1536
|
+
if (o.expectReply)
|
|
1537
|
+
raw.replies = true;
|
|
1538
|
+
if (o.expectJson)
|
|
1539
|
+
raw.json = true;
|
|
1540
|
+
if (o.expectText !== undefined)
|
|
1541
|
+
raw.textMatches = o.expectText;
|
|
1542
|
+
if (o.expectMinLinks !== undefined)
|
|
1543
|
+
raw.minLinks = nonNegInt("expect-min-links", o.expectMinLinks);
|
|
1544
|
+
if (o.expectMinButtons !== undefined)
|
|
1545
|
+
raw.minButtons = nonNegInt("expect-min-buttons", o.expectMinButtons);
|
|
1546
|
+
if (o.expectWebapp)
|
|
1547
|
+
raw.hasWebApp = true;
|
|
1548
|
+
if (o.expectMedia !== undefined)
|
|
1549
|
+
raw.media = o.expectMedia;
|
|
1550
|
+
if (o.expectMaxMs !== undefined)
|
|
1551
|
+
raw.maxFirstReplyMs = ms("expect-max-ms", o.expectMaxMs);
|
|
1552
|
+
if (!Object.keys(raw).length)
|
|
1553
|
+
return;
|
|
1554
|
+
const parsed = ExpectSchema.safeParse(raw);
|
|
1555
|
+
if (!parsed.success) {
|
|
1556
|
+
console.error(`invalid --expect-* value: ${parsed.error.issues[0]?.message ?? "schema error"}`);
|
|
1557
|
+
process.exit(1);
|
|
1558
|
+
}
|
|
1559
|
+
return parsed.data;
|
|
1560
|
+
}
|
|
1504
1561
|
var program = new Command;
|
|
1505
1562
|
var version = getVersion();
|
|
1506
1563
|
program.name("mira-harness").description("Automated probe harness for the @mira Telegram bot (GramJS userbot).").version(version, "-V, --version");
|
|
@@ -1509,15 +1566,16 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
1509
1566
|
});
|
|
1510
1567
|
program.command("login").description("One-time interactive login -> prints TG_SESSION for .env").action(login);
|
|
1511
1568
|
program.command("doctor").description("Check .env, session, connectivity and @mira resolution (read-only)").action(doctor);
|
|
1512
|
-
program.command("send").description("Send one probe to @mira and print the full settled reply as JSON").argument("[message...]", "the message (omit to read from stdin)").option("-q, --quiet", "suppress the progress spinner / status line", false).option("--settle <ms>", "quiet window before concluding a reply").option("--timeout <ms>", "give up if no reply by this many ms").option("--no-log", "do not append the result to the run log").action(async (parts, opts) => {
|
|
1569
|
+
program.command("send").description("Send one probe to @mira and print the full settled reply as JSON").argument("[message...]", "the message (omit to read from stdin)").option("-q, --quiet", "suppress the progress spinner / status line", false).option("--settle <ms>", "quiet window before concluding a reply").option("--timeout <ms>", "give up if no reply by this many ms").option("--no-log", "do not append the result to the run log").option("--expect-reply", "assert a reply arrives (any --expect-* exits 1 on failure)").option("--expect-json", "assert the first reply text is valid JSON").option("--expect-text <regex>", "assert some reply text matches this regex (case-insensitive)").option("--expect-min-links <n>", "assert at least N links across the reply").option("--expect-min-buttons <n>", "assert at least N inline buttons").option("--expect-webapp", "assert a web_app/startapp Launch button is present").option("--expect-media <kind>", "assert media of this kind (photo|video|audio|document|webpage|other)").option("--expect-max-ms <ms>", "assert the first reply arrives within this many ms").action(async (parts, opts) => {
|
|
1513
1570
|
await send(parts.join(" "), {
|
|
1514
1571
|
quiet: opts.quiet,
|
|
1515
1572
|
settle: ms("settle", opts.settle),
|
|
1516
1573
|
timeout: ms("timeout", opts.timeout),
|
|
1517
|
-
noLog: opts.log === false
|
|
1574
|
+
noLog: opts.log === false,
|
|
1575
|
+
expect: buildSendExpect(opts)
|
|
1518
1576
|
});
|
|
1519
1577
|
});
|
|
1520
|
-
program.command("loop").description("Run the experiment catalog against @mira at a human pace").option("-c, --category <category>", `only this category (${CATEGORIES.join(" | ")})`).option("-m, --max <n>", "max probes this run", "6").option("--confirm", "press a safe ✅ Confirm on generation probes (spends credits)", false).option("--peer <peer>", "'experiment'|'group' for TG_EXPERIMENT_CHAT, or a literal allowlisted peer").option("--gap <ms>", "delay between sends").option("--settle <ms>", "quiet window before concluding a reply").option("--timeout <ms>", "give up if no reply by this many ms").option("--list", "list the probes that would run, then exit (no sends)", false).option("--catalog <file>", "custom catalog JSON file (instead of the built-in catalog)").option("--grep <pattern>", "run only probes whose id matches this regex (case-insensitive)").option("--no-fail", "report failed assertions but still exit 0 (default: exit 1 on failure)").option("-q, --quiet", "suppress progress spinners", false).action(async (opts) => {
|
|
1578
|
+
program.command("loop").description("Run the experiment catalog against @mira at a human pace").option("-c, --category <category>", `only this category (${CATEGORIES.join(" | ")})`).option("-m, --max <n>", "max probes this run", "6").option("--confirm", "press a safe ✅ Confirm on generation probes (spends credits)", false).option("--peer <peer>", "'experiment'|'group' for TG_EXPERIMENT_CHAT, or a literal allowlisted peer").option("--gap <ms>", "delay between sends").option("--settle <ms>", "quiet window before concluding a reply").option("--timeout <ms>", "give up if no reply by this many ms").option("--list", "list the probes that would run, then exit (no sends)", false).option("--catalog <file>", "custom catalog JSON file (instead of the built-in catalog)").option("--grep <pattern>", "run only probes whose id matches this regex (case-insensitive)").option("--only <ids>", "run only probes with these comma-separated ids (exact match)").option("--no-fail", "report failed assertions but still exit 0 (default: exit 1 on failure)").option("-q, --quiet", "suppress progress spinners", false).action(async (opts) => {
|
|
1521
1579
|
await loop({
|
|
1522
1580
|
category: opts.category,
|
|
1523
1581
|
max: posInt("max", opts.max),
|
|
@@ -1529,6 +1587,7 @@ program.command("loop").description("Run the experiment catalog against @mira at
|
|
|
1529
1587
|
list: opts.list,
|
|
1530
1588
|
catalog: opts.catalog,
|
|
1531
1589
|
grep: opts.grep,
|
|
1590
|
+
only: opts.only,
|
|
1532
1591
|
noFail: opts.fail === false,
|
|
1533
1592
|
quiet: opts.quiet
|
|
1534
1593
|
});
|
|
@@ -1573,7 +1632,12 @@ Examples:
|
|
|
1573
1632
|
`);
|
|
1574
1633
|
if (process.argv.length <= 2) {
|
|
1575
1634
|
banner(version);
|
|
1576
|
-
|
|
1635
|
+
console.log(` Drive @mira from your terminal — QA, learn, and assert on its behavior.
|
|
1636
|
+
`);
|
|
1637
|
+
console.log(` Commands ${c.cyan("login · doctor · send · loop · catalog · watch")}`);
|
|
1638
|
+
console.log(` ${c.cyan("report · stats · diff · assert · schema")}
|
|
1639
|
+
`);
|
|
1640
|
+
console.log(` Run ${c.bold("mira-harness --help")} for options + examples, or ${c.bold("mira-harness <command> --help")}.`);
|
|
1577
1641
|
process.exit(0);
|
|
1578
1642
|
}
|
|
1579
1643
|
program.parseAsync(process.argv).then(() => process.exit(0)).catch((e) => {
|
package/dist/index.js
CHANGED
|
@@ -393,6 +393,10 @@ function grepProbes(probes, pattern) {
|
|
|
393
393
|
const needle = pattern.toLowerCase();
|
|
394
394
|
return probes.filter((p) => p.id.toLowerCase().includes(needle));
|
|
395
395
|
}
|
|
396
|
+
function onlyProbes(probes, idsCsv) {
|
|
397
|
+
const ids = new Set(idsCsv.split(",").map((s) => s.trim()).filter(Boolean));
|
|
398
|
+
return probes.filter((p) => ids.has(p.id));
|
|
399
|
+
}
|
|
396
400
|
// src/client.ts
|
|
397
401
|
import { Api, TelegramClient } from "telegram";
|
|
398
402
|
import { EditedMessage } from "telegram/events/EditedMessage.js";
|
package/dist/mcp.js
CHANGED
|
@@ -310,6 +310,10 @@ function grepProbes(probes, pattern) {
|
|
|
310
310
|
const needle = pattern.toLowerCase();
|
|
311
311
|
return probes.filter((p) => p.id.toLowerCase().includes(needle));
|
|
312
312
|
}
|
|
313
|
+
function onlyProbes(probes, idsCsv) {
|
|
314
|
+
const ids = new Set(idsCsv.split(",").map((s) => s.trim()).filter(Boolean));
|
|
315
|
+
return probes.filter((p) => ids.has(p.id));
|
|
316
|
+
}
|
|
313
317
|
|
|
314
318
|
// src/client.ts
|
|
315
319
|
import { Api, TelegramClient } from "telegram";
|
package/llms.txt
CHANGED
|
@@ -18,8 +18,8 @@ so the only programmatic path is a userbot (a real user account over MTProto / G
|
|
|
18
18
|
|
|
19
19
|
## CLI commands
|
|
20
20
|
- `doctor` — env / session / connectivity / @mira resolution check (read-only). Run this first.
|
|
21
|
-
- `send [message...]` — one probe → full reply as JSON. Message via arg or stdin. Flags: `--settle <ms> --timeout <ms> --quiet --no-log`.
|
|
22
|
-
- `loop` — run the catalog, paced. Flags: `--category <core|skills|generation|wallet> --max <n> --peer <experiment> --gap <ms> --settle <ms> --timeout <ms> --list --catalog <file.json> --grep <regex> --no-fail --quiet`. Observe-only unless `--confirm`. Grades probes that declare `expect` (PASS/FAIL) and **exits non-zero** if any graded probe fails (`--no-fail` to report without failing) — drops straight into CI. `--grep <regex>` runs only probes whose id matches.
|
|
21
|
+
- `send [message...]` — one probe → full reply as JSON. Message via arg or stdin. Flags: `--settle <ms> --timeout <ms> --quiet --no-log`. Inline assertions (any sets exit 1 on failure): `--expect-reply --expect-json --expect-text <regex> --expect-min-links <n> --expect-min-buttons <n> --expect-webapp --expect-media <kind> --expect-max-ms <ms>`.
|
|
22
|
+
- `loop` — run the catalog, paced. Flags: `--category <core|skills|generation|wallet> --max <n> --peer <experiment> --gap <ms> --settle <ms> --timeout <ms> --list --catalog <file.json> --grep <regex> --no-fail --quiet`. Observe-only unless `--confirm`. Grades probes that declare `expect` (PASS/FAIL) and **exits non-zero** if any graded probe fails (`--no-fail` to report without failing) — drops straight into CI. `--grep <regex>` runs only probes whose id matches; `--only <id1,id2>` selects exact ids.
|
|
23
23
|
- `catalog` — list the catalog, no network. Flags: `--category --catalog <file.json> --json`.
|
|
24
24
|
- `watch` — live-tail @mira's messages, observe-only. Flag: `--peer`.
|
|
25
25
|
- `report` — distill the JSONL run log into Markdown. Flags: `--in <file> --out <file> --category`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mira-harness",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "A CLI + MCP dev-tool for communicating with the @mira Telegram bot — capture its full reply (buttons/links/media) and run a self-driving experiment catalog.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|