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 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**. Run a subset
168
- of probes by id with `loop --grep <regex>`; emit a catalog JSON Schema for your editor with `schema`.
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
- const matched = opts.grep ? grepProbes(probesFor(opts.category, source), opts.grep) : probesFor(opts.category, source);
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
- console.error(opts.grep ? `no probes match --grep "${opts.grep}".` : "no probes selected.");
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
- program.outputHelp();
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.4",
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": {