mira-harness 0.2.5 → 0.2.7
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 +100 -15
- package/dist/commands/report.d.ts +3 -3
- package/dist/env.d.ts +2 -0
- package/dist/index.js +18 -2
- package/dist/mcp.js +18 -2
- 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";
|
|
@@ -432,6 +434,9 @@ function clearTitle() {
|
|
|
432
434
|
// src/commands/report.ts
|
|
433
435
|
import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
434
436
|
import { resolve as resolve3 } from "node:path";
|
|
437
|
+
function isRunRecord(x) {
|
|
438
|
+
return typeof x === "object" && x !== null && Array.isArray(x.messages) && typeof x.sent === "string";
|
|
439
|
+
}
|
|
435
440
|
function loadRunRecords(inFile, category) {
|
|
436
441
|
const path = inFile ? resolve3(process.cwd(), inFile) : RUNS_FILE;
|
|
437
442
|
const raw = readFileSync2(path, "utf8");
|
|
@@ -442,7 +447,9 @@ function loadRunRecords(inFile, category) {
|
|
|
442
447
|
if (!t)
|
|
443
448
|
continue;
|
|
444
449
|
try {
|
|
445
|
-
|
|
450
|
+
const rec = JSON.parse(t);
|
|
451
|
+
if (isRunRecord(rec))
|
|
452
|
+
records.push(rec);
|
|
446
453
|
} catch {}
|
|
447
454
|
}
|
|
448
455
|
return category ? records.filter((r) => r.category === category) : records;
|
|
@@ -613,6 +620,8 @@ function listCatalog(opts = {}) {
|
|
|
613
620
|
let probes = probesFor(opts.category, source);
|
|
614
621
|
if (opts.grep)
|
|
615
622
|
probes = grepProbes(probes, opts.grep);
|
|
623
|
+
if (opts.only)
|
|
624
|
+
probes = onlyProbes(probes, opts.only);
|
|
616
625
|
if (opts.max !== undefined)
|
|
617
626
|
probes = probes.slice(0, opts.max);
|
|
618
627
|
if (opts.json) {
|
|
@@ -863,7 +872,14 @@ function req(name) {
|
|
|
863
872
|
return v;
|
|
864
873
|
}
|
|
865
874
|
var tgEnv = {
|
|
866
|
-
apiId: () =>
|
|
875
|
+
apiId: () => {
|
|
876
|
+
const raw = req("TG_API_ID");
|
|
877
|
+
const n = Number(raw);
|
|
878
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
879
|
+
throw new Error(`TG_API_ID must be a positive integer (got "${raw}") — see .env.example`);
|
|
880
|
+
}
|
|
881
|
+
return n;
|
|
882
|
+
},
|
|
867
883
|
apiHash: () => req("TG_API_HASH"),
|
|
868
884
|
session: () => process.env.TG_SESSION ?? "",
|
|
869
885
|
miraPeer: process.env.MIRA_PEER ?? "mira",
|
|
@@ -1173,7 +1189,7 @@ async function loop(opts) {
|
|
|
1173
1189
|
process.exit(1);
|
|
1174
1190
|
}
|
|
1175
1191
|
if (opts.list) {
|
|
1176
|
-
listCatalog({ category: opts.category, max: opts.max, catalog: opts.catalog, grep: opts.grep });
|
|
1192
|
+
listCatalog({ category: opts.category, max: opts.max, catalog: opts.catalog, grep: opts.grep, only: opts.only });
|
|
1177
1193
|
return;
|
|
1178
1194
|
}
|
|
1179
1195
|
const source = opts.catalog ? loadCatalog(opts.catalog) : CATALOG;
|
|
@@ -1192,10 +1208,15 @@ async function loop(opts) {
|
|
|
1192
1208
|
console.error("TG_SESSION is empty — run `mira-harness login` first, then put it in .env.");
|
|
1193
1209
|
process.exit(1);
|
|
1194
1210
|
}
|
|
1195
|
-
|
|
1211
|
+
let matched = probesFor(opts.category, source);
|
|
1212
|
+
if (opts.grep)
|
|
1213
|
+
matched = grepProbes(matched, opts.grep);
|
|
1214
|
+
if (opts.only)
|
|
1215
|
+
matched = onlyProbes(matched, opts.only);
|
|
1196
1216
|
const probes = matched.slice(0, opts.max);
|
|
1197
1217
|
if (!probes.length) {
|
|
1198
|
-
|
|
1218
|
+
const why = opts.only ? `--only "${opts.only}"` : opts.grep ? `--grep "${opts.grep}"` : undefined;
|
|
1219
|
+
console.error(why ? `no probes match ${why}.` : "no probes selected.");
|
|
1199
1220
|
process.exit(1);
|
|
1200
1221
|
}
|
|
1201
1222
|
const gap = opts.gap ?? DEFAULT_GAP_MS;
|
|
@@ -1313,10 +1334,13 @@ async function send(rawMessage, opts = {}) {
|
|
|
1313
1334
|
collect.firstReplyTimeoutMs = opts.timeout;
|
|
1314
1335
|
const peer = tgEnv.miraPeer;
|
|
1315
1336
|
const client = await connect(session);
|
|
1337
|
+
let verdict;
|
|
1316
1338
|
try {
|
|
1317
1339
|
const result = await withProgress(`@${peer}`, () => sendAndCollect(client, peer, message, collect), opts.quiet);
|
|
1340
|
+
if (opts.expect)
|
|
1341
|
+
verdict = evaluate(opts.expect, result);
|
|
1318
1342
|
if (!opts.noLog)
|
|
1319
|
-
await appendRun(result);
|
|
1343
|
+
await appendRun(result, verdict ? { assert: verdict } : {});
|
|
1320
1344
|
if (!opts.quiet) {
|
|
1321
1345
|
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
1346
|
}
|
|
@@ -1324,6 +1348,16 @@ async function send(rawMessage, opts = {}) {
|
|
|
1324
1348
|
} finally {
|
|
1325
1349
|
await client.disconnect();
|
|
1326
1350
|
}
|
|
1351
|
+
if (verdict) {
|
|
1352
|
+
if (!opts.quiet) {
|
|
1353
|
+
note(verdict.ok ? c.green("✓ assertions passed") : c.red("✗ assertions failed"));
|
|
1354
|
+
if (!verdict.ok)
|
|
1355
|
+
for (const ch of verdict.checks.filter((x) => !x.ok))
|
|
1356
|
+
note(c.dim(` ✗ ${ch.name}: ${ch.detail}`));
|
|
1357
|
+
}
|
|
1358
|
+
if (!verdict.ok)
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1327
1361
|
}
|
|
1328
1362
|
|
|
1329
1363
|
// src/commands/stats.ts
|
|
@@ -1501,6 +1535,41 @@ function posInt(name, v) {
|
|
|
1501
1535
|
}
|
|
1502
1536
|
return n;
|
|
1503
1537
|
}
|
|
1538
|
+
function nonNegInt(name, v) {
|
|
1539
|
+
const n = Number(v);
|
|
1540
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
1541
|
+
console.error(`--${name} must be a non-negative integer (got "${v}")`);
|
|
1542
|
+
process.exit(1);
|
|
1543
|
+
}
|
|
1544
|
+
return n;
|
|
1545
|
+
}
|
|
1546
|
+
function buildSendExpect(o) {
|
|
1547
|
+
const raw = {};
|
|
1548
|
+
if (o.expectReply)
|
|
1549
|
+
raw.replies = true;
|
|
1550
|
+
if (o.expectJson)
|
|
1551
|
+
raw.json = true;
|
|
1552
|
+
if (o.expectText !== undefined)
|
|
1553
|
+
raw.textMatches = o.expectText;
|
|
1554
|
+
if (o.expectMinLinks !== undefined)
|
|
1555
|
+
raw.minLinks = nonNegInt("expect-min-links", o.expectMinLinks);
|
|
1556
|
+
if (o.expectMinButtons !== undefined)
|
|
1557
|
+
raw.minButtons = nonNegInt("expect-min-buttons", o.expectMinButtons);
|
|
1558
|
+
if (o.expectWebapp)
|
|
1559
|
+
raw.hasWebApp = true;
|
|
1560
|
+
if (o.expectMedia !== undefined)
|
|
1561
|
+
raw.media = o.expectMedia;
|
|
1562
|
+
if (o.expectMaxMs !== undefined)
|
|
1563
|
+
raw.maxFirstReplyMs = ms("expect-max-ms", o.expectMaxMs);
|
|
1564
|
+
if (!Object.keys(raw).length)
|
|
1565
|
+
return;
|
|
1566
|
+
const parsed = ExpectSchema.safeParse(raw);
|
|
1567
|
+
if (!parsed.success) {
|
|
1568
|
+
console.error(`invalid --expect-* value: ${parsed.error.issues[0]?.message ?? "schema error"}`);
|
|
1569
|
+
process.exit(1);
|
|
1570
|
+
}
|
|
1571
|
+
return parsed.data;
|
|
1572
|
+
}
|
|
1504
1573
|
var program = new Command;
|
|
1505
1574
|
var version = getVersion();
|
|
1506
1575
|
program.name("mira-harness").description("Automated probe harness for the @mira Telegram bot (GramJS userbot).").version(version, "-V, --version");
|
|
@@ -1509,15 +1578,16 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
1509
1578
|
});
|
|
1510
1579
|
program.command("login").description("One-time interactive login -> prints TG_SESSION for .env").action(login);
|
|
1511
1580
|
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) => {
|
|
1581
|
+
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
1582
|
await send(parts.join(" "), {
|
|
1514
1583
|
quiet: opts.quiet,
|
|
1515
1584
|
settle: ms("settle", opts.settle),
|
|
1516
1585
|
timeout: ms("timeout", opts.timeout),
|
|
1517
|
-
noLog: opts.log === false
|
|
1586
|
+
noLog: opts.log === false,
|
|
1587
|
+
expect: buildSendExpect(opts)
|
|
1518
1588
|
});
|
|
1519
1589
|
});
|
|
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) => {
|
|
1590
|
+
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
1591
|
await loop({
|
|
1522
1592
|
category: opts.category,
|
|
1523
1593
|
max: posInt("max", opts.max),
|
|
@@ -1529,6 +1599,7 @@ program.command("loop").description("Run the experiment catalog against @mira at
|
|
|
1529
1599
|
list: opts.list,
|
|
1530
1600
|
catalog: opts.catalog,
|
|
1531
1601
|
grep: opts.grep,
|
|
1602
|
+
only: opts.only,
|
|
1532
1603
|
noFail: opts.fail === false,
|
|
1533
1604
|
quiet: opts.quiet
|
|
1534
1605
|
});
|
|
@@ -1573,7 +1644,21 @@ Examples:
|
|
|
1573
1644
|
`);
|
|
1574
1645
|
if (process.argv.length <= 2) {
|
|
1575
1646
|
banner(version);
|
|
1576
|
-
|
|
1647
|
+
console.log(` Drive @mira from your terminal — QA, learn, and assert on its behavior.
|
|
1648
|
+
`);
|
|
1649
|
+
console.log(` ${c.bold("Commands")}`);
|
|
1650
|
+
const cmds = program.commands.filter((cmd) => cmd.name() !== "help");
|
|
1651
|
+
const pad = Math.max(...cmds.map((cmd) => cmd.name().length));
|
|
1652
|
+
for (const cmd of cmds) {
|
|
1653
|
+
console.log(` ${c.cyan(cmd.name().padEnd(pad))} ${c.dim(cmd.description())}`);
|
|
1654
|
+
}
|
|
1655
|
+
console.log(`
|
|
1656
|
+
${c.bold("Quick start")}`);
|
|
1657
|
+
for (const ex of ["login", "doctor", 'send "What can you do?"', "loop --category core"]) {
|
|
1658
|
+
console.log(c.dim(` $ mira-harness ${ex}`));
|
|
1659
|
+
}
|
|
1660
|
+
console.log(`
|
|
1661
|
+
Run ${c.bold("mira-harness --help")} for all options + examples, or ${c.bold("mira-harness <command> --help")}.`);
|
|
1577
1662
|
process.exit(0);
|
|
1578
1663
|
}
|
|
1579
1664
|
program.parseAsync(process.argv).then(() => process.exit(0)).catch((e) => {
|
|
@@ -7,9 +7,9 @@ export type RunRecord = ProbeResult & {
|
|
|
7
7
|
assert?: Verdict;
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
* Read the JSONL run log into records (malformed lines skipped). Throws
|
|
11
|
-
* the file is absent — callers decide how to surface that. Shared by
|
|
12
|
-
* `stats` so the two stay in lockstep on the log format.
|
|
10
|
+
* Read the JSONL run log into records (malformed/wrong-shape lines skipped). Throws
|
|
11
|
+
* ENOENT when the file is absent — callers decide how to surface that. Shared by
|
|
12
|
+
* `report` and `stats` so the two stay in lockstep on the log format.
|
|
13
13
|
*/
|
|
14
14
|
export declare function loadRunRecords(inFile?: string, category?: string): RunRecord[];
|
|
15
15
|
export declare function buildReport(records: RunRecord[]): string;
|
package/dist/env.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import "dotenv/config";
|
|
9
9
|
export declare const tgEnv: {
|
|
10
|
+
/** Telegram api_id — a positive integer. Reject non-numeric/zero up front so
|
|
11
|
+
* `doctor` flags a bad value instead of passing NaN into GramJS (cryptic later). */
|
|
10
12
|
apiId: () => number;
|
|
11
13
|
apiHash: () => string;
|
|
12
14
|
/** Empty until `mira-harness login` mints it; required by send/loop. */
|
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";
|
|
@@ -410,7 +414,14 @@ function req(name) {
|
|
|
410
414
|
return v;
|
|
411
415
|
}
|
|
412
416
|
var tgEnv = {
|
|
413
|
-
apiId: () =>
|
|
417
|
+
apiId: () => {
|
|
418
|
+
const raw = req("TG_API_ID");
|
|
419
|
+
const n = Number(raw);
|
|
420
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
421
|
+
throw new Error(`TG_API_ID must be a positive integer (got "${raw}") — see .env.example`);
|
|
422
|
+
}
|
|
423
|
+
return n;
|
|
424
|
+
},
|
|
414
425
|
apiHash: () => req("TG_API_HASH"),
|
|
415
426
|
session: () => process.env.TG_SESSION ?? "",
|
|
416
427
|
miraPeer: process.env.MIRA_PEER ?? "mira",
|
|
@@ -592,6 +603,9 @@ async function appendRun(result, meta = {}) {
|
|
|
592
603
|
}
|
|
593
604
|
|
|
594
605
|
// src/commands/report.ts
|
|
606
|
+
function isRunRecord(x) {
|
|
607
|
+
return typeof x === "object" && x !== null && Array.isArray(x.messages) && typeof x.sent === "string";
|
|
608
|
+
}
|
|
595
609
|
function loadRunRecords(inFile, category) {
|
|
596
610
|
const path = inFile ? resolve3(process.cwd(), inFile) : RUNS_FILE;
|
|
597
611
|
const raw = readFileSync2(path, "utf8");
|
|
@@ -602,7 +616,9 @@ function loadRunRecords(inFile, category) {
|
|
|
602
616
|
if (!t)
|
|
603
617
|
continue;
|
|
604
618
|
try {
|
|
605
|
-
|
|
619
|
+
const rec = JSON.parse(t);
|
|
620
|
+
if (isRunRecord(rec))
|
|
621
|
+
records.push(rec);
|
|
606
622
|
} catch {}
|
|
607
623
|
}
|
|
608
624
|
return category ? records.filter((r) => r.category === category) : records;
|
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";
|
|
@@ -420,7 +424,14 @@ function req(name) {
|
|
|
420
424
|
return v;
|
|
421
425
|
}
|
|
422
426
|
var tgEnv = {
|
|
423
|
-
apiId: () =>
|
|
427
|
+
apiId: () => {
|
|
428
|
+
const raw = req("TG_API_ID");
|
|
429
|
+
const n = Number(raw);
|
|
430
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
431
|
+
throw new Error(`TG_API_ID must be a positive integer (got "${raw}") — see .env.example`);
|
|
432
|
+
}
|
|
433
|
+
return n;
|
|
434
|
+
},
|
|
424
435
|
apiHash: () => req("TG_API_HASH"),
|
|
425
436
|
session: () => process.env.TG_SESSION ?? "",
|
|
426
437
|
miraPeer: process.env.MIRA_PEER ?? "mira",
|
|
@@ -603,6 +614,9 @@ async function appendRun(result, meta = {}) {
|
|
|
603
614
|
}
|
|
604
615
|
|
|
605
616
|
// src/commands/report.ts
|
|
617
|
+
function isRunRecord(x) {
|
|
618
|
+
return typeof x === "object" && x !== null && Array.isArray(x.messages) && typeof x.sent === "string";
|
|
619
|
+
}
|
|
606
620
|
function loadRunRecords(inFile, category) {
|
|
607
621
|
const path = inFile ? resolve3(process.cwd(), inFile) : RUNS_FILE;
|
|
608
622
|
const raw = readFileSync2(path, "utf8");
|
|
@@ -613,7 +627,9 @@ function loadRunRecords(inFile, category) {
|
|
|
613
627
|
if (!t)
|
|
614
628
|
continue;
|
|
615
629
|
try {
|
|
616
|
-
|
|
630
|
+
const rec = JSON.parse(t);
|
|
631
|
+
if (isRunRecord(rec))
|
|
632
|
+
records.push(rec);
|
|
617
633
|
} catch {}
|
|
618
634
|
}
|
|
619
635
|
return category ? records.filter((r) => r.category === category) : records;
|
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.7",
|
|
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": {
|