@syrin/iris 0.4.0 → 0.6.10
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/dist/cli.js +3488 -1050
- package/dist/index.js +419 -114
- package/dist/next.js +5 -1
- package/dist/server.d.ts +19 -1
- package/dist/server.js +2406 -1005
- package/dist/test.js +2248 -1084
- package/dist/vite.d.ts +43 -0
- package/dist/vite.js +144 -0
- package/package.json +15 -10
package/dist/test.js
CHANGED
|
@@ -191,16 +191,40 @@ ${body}
|
|
|
191
191
|
</${JUnit.SUITE}>
|
|
192
192
|
`;
|
|
193
193
|
}
|
|
194
|
-
async function writeJUnit(
|
|
195
|
-
await
|
|
194
|
+
async function writeJUnit(fs2, path, results, opts) {
|
|
195
|
+
await fs2.writeFile(path, toJUnitXml(results, opts));
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
// ../test/dist/boot.js
|
|
199
|
-
import { join as
|
|
199
|
+
import { join as join6 } from "path";
|
|
200
200
|
|
|
201
201
|
// ../protocol/dist/constants.js
|
|
202
202
|
var IRIS_DEFAULT_PORT = 4400;
|
|
203
203
|
var IRIS_WS_PATH = "/iris";
|
|
204
|
+
var IRIS_PROTOCOL_VERSION = 1;
|
|
205
|
+
var TRANSPORT_LIMITS = {
|
|
206
|
+
MAX_MESSAGE_BYTES: 1024 * 1024,
|
|
207
|
+
MAX_MESSAGES_PER_SECOND: 1e3,
|
|
208
|
+
MAX_SESSIONS: 32,
|
|
209
|
+
MAX_PENDING_CONNECTIONS: 16,
|
|
210
|
+
HELLO_TIMEOUT_MS: 5e3,
|
|
211
|
+
MAX_BUFFER_BYTES: 8 * 1024 * 1024,
|
|
212
|
+
MAX_SESSION_ID_LENGTH: 128,
|
|
213
|
+
MAX_URL_LENGTH: 4096,
|
|
214
|
+
MAX_TITLE_LENGTH: 512,
|
|
215
|
+
MAX_ADAPTERS: 32,
|
|
216
|
+
MAX_ADAPTER_NAME_LENGTH: 128,
|
|
217
|
+
MAX_TOKEN_LENGTH: 512,
|
|
218
|
+
MAX_COMMAND_ID_LENGTH: 128,
|
|
219
|
+
MAX_COMMAND_NAME_LENGTH: 128,
|
|
220
|
+
MAX_REF_LENGTH: 128,
|
|
221
|
+
MAX_ERROR_LENGTH: 4096,
|
|
222
|
+
MAX_SERIALIZE_DEPTH: 8,
|
|
223
|
+
MAX_COLLECTION_ITEMS: 200,
|
|
224
|
+
MAX_OBJECT_KEYS: 200,
|
|
225
|
+
MAX_STRING_LENGTH: 64 * 1024
|
|
226
|
+
};
|
|
227
|
+
var DANGEROUS_ACTION_CONFIRM_ARG = "confirmDangerous";
|
|
204
228
|
var REPLAY_PROGRAM_VERSION = 1;
|
|
205
229
|
var IrisDir = {
|
|
206
230
|
ROOT: ".iris",
|
|
@@ -243,6 +267,7 @@ var CRAWL_DEFAULTS = {
|
|
|
243
267
|
/** HTTP status at/above which a response counts as a failed request. */
|
|
244
268
|
FAILED_STATUS: 400
|
|
245
269
|
};
|
|
270
|
+
var UpdateCheckIntervalMs = 24 * 60 * 60 * 1e3;
|
|
246
271
|
var CONTRACT_FILE_VERSION = 1;
|
|
247
272
|
var FROM_DISK_ARG = "fromDisk";
|
|
248
273
|
var ContractReadError = {
|
|
@@ -301,7 +326,7 @@ var ReplayStatus = {
|
|
|
301
326
|
DRIFT: "drift",
|
|
302
327
|
// an anchor missed (testid renamed / signal not observed) — legible drift returned
|
|
303
328
|
ERROR: "error"
|
|
304
|
-
// the flow
|
|
329
|
+
// the flow could not load or a resolved action failed
|
|
305
330
|
};
|
|
306
331
|
var DriftReason = {
|
|
307
332
|
TESTID_NOT_FOUND: "testid_not_found",
|
|
@@ -335,8 +360,10 @@ var HealStatus = {
|
|
|
335
360
|
// drift exists but no proposal cleared the confidence floor
|
|
336
361
|
NOTHING_TO_HEAL: "nothing_to_heal",
|
|
337
362
|
// replay was green
|
|
363
|
+
CONSEQUENCE_BROKEN: "consequence_broken",
|
|
364
|
+
// rebind resolves a locator but the flow's success consequence no longer fires — REFUSED (file untouched)
|
|
338
365
|
ERROR: "error"
|
|
339
|
-
// flow missing/malformed/invalid-name
|
|
366
|
+
// flow missing/malformed/invalid-name, or a resolved action failed
|
|
340
367
|
};
|
|
341
368
|
var HEAL_CONFIDENCE_MIN = 0.5;
|
|
342
369
|
var AnnotationTarget = {
|
|
@@ -352,7 +379,8 @@ var AnnotationErrorCode = {
|
|
|
352
379
|
var COMPILED_PREDICATE_PREFIX = "will";
|
|
353
380
|
var RING_BUFFER_DEFAULTS = {
|
|
354
381
|
MAX_EVENTS: 2e3,
|
|
355
|
-
MAX_AGE_MS: 6e4
|
|
382
|
+
MAX_AGE_MS: 6e4,
|
|
383
|
+
MAX_BYTES: TRANSPORT_LIMITS.MAX_BUFFER_BYTES
|
|
356
384
|
};
|
|
357
385
|
var EventType = {
|
|
358
386
|
DOM_ADDED: "dom.added",
|
|
@@ -537,6 +565,8 @@ var MessageKind = {
|
|
|
537
565
|
|
|
538
566
|
// ../protocol/dist/messages.js
|
|
539
567
|
import { z } from "zod";
|
|
568
|
+
var sessionIdSchema = z.string().min(1).max(TRANSPORT_LIMITS.MAX_SESSION_ID_LENGTH);
|
|
569
|
+
var refSchema = z.string().max(TRANSPORT_LIMITS.MAX_REF_LENGTH);
|
|
540
570
|
var HumanControlDataSchema = z.object({
|
|
541
571
|
kind: z.nativeEnum(HumanControlKind),
|
|
542
572
|
text: z.string().optional()
|
|
@@ -544,35 +574,37 @@ var HumanControlDataSchema = z.object({
|
|
|
544
574
|
var IrisEventSchema = z.object({
|
|
545
575
|
t: z.number(),
|
|
546
576
|
type: z.nativeEnum(EventType),
|
|
547
|
-
sessionId:
|
|
577
|
+
sessionId: sessionIdSchema,
|
|
548
578
|
/** Stable element reference this event concerns, when applicable (e.g. "e7"). */
|
|
549
|
-
ref:
|
|
579
|
+
ref: refSchema.optional(),
|
|
550
580
|
/** Event-type-specific payload. Kept open here; refined per observer at the edges. */
|
|
551
581
|
data: z.record(z.unknown()).default({})
|
|
552
582
|
});
|
|
553
583
|
var HelloMessageSchema = z.object({
|
|
554
584
|
kind: z.literal(MessageKind.HELLO),
|
|
555
|
-
protocolVersion: z.
|
|
556
|
-
sessionId:
|
|
557
|
-
url: z.string(),
|
|
558
|
-
title: z.string(),
|
|
559
|
-
adapters: z.array(z.string()),
|
|
585
|
+
protocolVersion: z.literal(IRIS_PROTOCOL_VERSION),
|
|
586
|
+
sessionId: sessionIdSchema,
|
|
587
|
+
url: z.string().max(TRANSPORT_LIMITS.MAX_URL_LENGTH),
|
|
588
|
+
title: z.string().max(TRANSPORT_LIMITS.MAX_TITLE_LENGTH),
|
|
589
|
+
adapters: z.array(z.string().max(TRANSPORT_LIMITS.MAX_ADAPTER_NAME_LENGTH)).max(TRANSPORT_LIMITS.MAX_ADAPTERS),
|
|
590
|
+
/** Optional browser/bridge pairing token. Required when the bridge configures one. */
|
|
591
|
+
token: z.string().max(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH).optional(),
|
|
560
592
|
/** Whether the app has advertised a capability registry (iris.describe). */
|
|
561
593
|
hasCapabilities: z.boolean().optional()
|
|
562
594
|
});
|
|
563
595
|
var CommandMessageSchema = z.object({
|
|
564
596
|
kind: z.literal(MessageKind.COMMAND),
|
|
565
|
-
id: z.string(),
|
|
566
|
-
sessionId:
|
|
567
|
-
name: z.string(),
|
|
597
|
+
id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
|
|
598
|
+
sessionId: sessionIdSchema.optional(),
|
|
599
|
+
name: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_NAME_LENGTH),
|
|
568
600
|
args: z.record(z.unknown()).default({})
|
|
569
601
|
});
|
|
570
602
|
var CommandResultSchema = z.object({
|
|
571
603
|
kind: z.literal(MessageKind.COMMAND_RESULT),
|
|
572
|
-
id: z.string(),
|
|
604
|
+
id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
|
|
573
605
|
ok: z.boolean(),
|
|
574
606
|
result: z.unknown().optional(),
|
|
575
|
-
error: z.string().optional()
|
|
607
|
+
error: z.string().max(TRANSPORT_LIMITS.MAX_ERROR_LENGTH).optional()
|
|
576
608
|
});
|
|
577
609
|
var EventMessageSchema = z.object({
|
|
578
610
|
kind: z.literal(MessageKind.EVENT),
|
|
@@ -585,6 +617,25 @@ var IrisMessageSchema = z.discriminatedUnion("kind", [
|
|
|
585
617
|
EventMessageSchema
|
|
586
618
|
]);
|
|
587
619
|
|
|
620
|
+
// ../protocol/dist/security.js
|
|
621
|
+
var DANGEROUS_ACTION = /\b(delete|remove|destroy|erase|drop|terminate|revoke|reset|logout|log out|sign out|close account|cancel subscription|purchase|buy|pay|place order|confirm order|deploy|publish|send|transfer|withdraw|refund)\b/i;
|
|
622
|
+
function isLoopbackHostname(hostname) {
|
|
623
|
+
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
624
|
+
if (normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") {
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
const octets = normalized.split(".");
|
|
628
|
+
return octets.length === 4 && octets[0] === "127" && octets.every((octet) => {
|
|
629
|
+
if (!/^\d{1,3}$/.test(octet))
|
|
630
|
+
return false;
|
|
631
|
+
const value = Number(octet);
|
|
632
|
+
return value >= 0 && value <= 255;
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
function isDangerousActionText(text) {
|
|
636
|
+
return DANGEROUS_ACTION.test(text.replace(/[_-]+/g, " "));
|
|
637
|
+
}
|
|
638
|
+
|
|
588
639
|
// ../protocol/dist/toon.js
|
|
589
640
|
var ROLE_MAP = {
|
|
590
641
|
button: "btn",
|
|
@@ -846,24 +897,44 @@ var AnnotationSchema = z2.discriminatedUnion("kind", [
|
|
|
846
897
|
]);
|
|
847
898
|
|
|
848
899
|
// ../server/dist/index.js
|
|
849
|
-
import { join as
|
|
900
|
+
import { join as join5 } from "path";
|
|
850
901
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
851
902
|
|
|
903
|
+
// ../server/dist/http-server.js
|
|
904
|
+
import * as http from "http";
|
|
905
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
906
|
+
|
|
907
|
+
// ../server/dist/log.js
|
|
908
|
+
function log(event, fields = {}) {
|
|
909
|
+
const line = JSON.stringify({ event, ...fields });
|
|
910
|
+
process.stderr.write(`${line}
|
|
911
|
+
`);
|
|
912
|
+
}
|
|
913
|
+
|
|
852
914
|
// ../server/dist/bridge.js
|
|
915
|
+
import { timingSafeEqual } from "crypto";
|
|
916
|
+
import * as http2 from "http";
|
|
853
917
|
import { WebSocketServer } from "ws";
|
|
854
918
|
|
|
855
919
|
// ../server/dist/events/ring-buffer.js
|
|
856
920
|
var RingBuffer = class {
|
|
857
921
|
#maxEvents;
|
|
858
922
|
#maxAgeMs;
|
|
923
|
+
#maxBytes;
|
|
859
924
|
#events = [];
|
|
925
|
+
#eventBytes = [];
|
|
926
|
+
#totalBytes = 0;
|
|
860
927
|
#droppedCount = 0;
|
|
861
928
|
constructor(options = {}) {
|
|
862
929
|
this.#maxEvents = options.maxEvents ?? RING_BUFFER_DEFAULTS.MAX_EVENTS;
|
|
863
930
|
this.#maxAgeMs = options.maxAgeMs ?? RING_BUFFER_DEFAULTS.MAX_AGE_MS;
|
|
931
|
+
this.#maxBytes = options.maxBytes ?? RING_BUFFER_DEFAULTS.MAX_BYTES;
|
|
864
932
|
}
|
|
865
933
|
push(event, now) {
|
|
866
934
|
this.#events.push(event);
|
|
935
|
+
const bytes = Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
936
|
+
this.#eventBytes.push(bytes);
|
|
937
|
+
this.#totalBytes += bytes;
|
|
867
938
|
this.#evict(now);
|
|
868
939
|
}
|
|
869
940
|
/** Events at or after a given timestamp cursor. */
|
|
@@ -889,10 +960,14 @@ var RingBuffer = class {
|
|
|
889
960
|
#evict(now) {
|
|
890
961
|
const before = this.#events.length;
|
|
891
962
|
const cutoff = now - this.#maxAgeMs;
|
|
892
|
-
|
|
893
|
-
this.#events
|
|
963
|
+
while (this.#events.length > this.#maxEvents || this.#totalBytes > this.#maxBytes && this.#events.length > 0) {
|
|
964
|
+
this.#events.shift();
|
|
965
|
+
this.#totalBytes -= this.#eventBytes.shift() ?? 0;
|
|
966
|
+
}
|
|
967
|
+
while ((this.#events[0]?.t ?? cutoff) < cutoff) {
|
|
968
|
+
this.#events.shift();
|
|
969
|
+
this.#totalBytes -= this.#eventBytes.shift() ?? 0;
|
|
894
970
|
}
|
|
895
|
-
this.#events = this.#events.filter((e) => e.t >= cutoff);
|
|
896
971
|
this.#droppedCount += before - this.#events.length;
|
|
897
972
|
}
|
|
898
973
|
/** Snapshot of buffer health for the agent — total events held and cumulative drops since connect. */
|
|
@@ -1129,6 +1204,14 @@ var Session = class {
|
|
|
1129
1204
|
this.#pending.delete(id);
|
|
1130
1205
|
}
|
|
1131
1206
|
}
|
|
1207
|
+
/** End this transport without letting a stale socket remove its replacement session. */
|
|
1208
|
+
disconnect(reason) {
|
|
1209
|
+
this.rejectAll(reason);
|
|
1210
|
+
try {
|
|
1211
|
+
this.#socket.close(1008, reason);
|
|
1212
|
+
} catch {
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1132
1215
|
// ── Live-control: state machine + human→agent inbox (server-owned) ───────────────
|
|
1133
1216
|
getState() {
|
|
1134
1217
|
return this.#state;
|
|
@@ -1250,11 +1333,15 @@ var Session = class {
|
|
|
1250
1333
|
var SessionManager = class {
|
|
1251
1334
|
#sessions = /* @__PURE__ */ new Map();
|
|
1252
1335
|
add(session) {
|
|
1336
|
+
const previous = this.#sessions.get(session.id);
|
|
1253
1337
|
this.#sessions.set(session.id, session);
|
|
1338
|
+
return previous;
|
|
1254
1339
|
}
|
|
1255
|
-
remove(
|
|
1256
|
-
this.#sessions.get(
|
|
1257
|
-
|
|
1340
|
+
remove(session) {
|
|
1341
|
+
if (this.#sessions.get(session.id) !== session)
|
|
1342
|
+
return false;
|
|
1343
|
+
session.rejectAll("session disconnected");
|
|
1344
|
+
return this.#sessions.delete(session.id);
|
|
1258
1345
|
}
|
|
1259
1346
|
get(sessionId) {
|
|
1260
1347
|
return this.#sessions.get(sessionId);
|
|
@@ -1271,7 +1358,16 @@ var SessionManager = class {
|
|
|
1271
1358
|
}
|
|
1272
1359
|
/**
|
|
1273
1360
|
* Resolve the target session. With an explicit id, returns it. With none and exactly
|
|
1274
|
-
* one connected, returns that.
|
|
1361
|
+
* one connected, returns that.
|
|
1362
|
+
*
|
|
1363
|
+
* With none and multiple connected, applies smart auto-selection:
|
|
1364
|
+
* 1. Prefer non-throttled sessions (not hidden + recently heard from).
|
|
1365
|
+
* 2. Within each tier, prefer lowest lastSeenMs (most recently active SDK heartbeat).
|
|
1366
|
+
* 3. If two or more non-throttled sessions are within 1 s of each other, throw —
|
|
1367
|
+
* genuinely ambiguous, agent must specify sessionId.
|
|
1368
|
+
* 4. If ALL sessions are throttled (e.g. user is working in their editor on another
|
|
1369
|
+
* desktop), skip the gap check and pick the freshest heartbeat. This lets the agent
|
|
1370
|
+
* keep working in the background without requiring sessionId every time.
|
|
1275
1371
|
*/
|
|
1276
1372
|
resolve(sessionId) {
|
|
1277
1373
|
if (sessionId !== void 0) {
|
|
@@ -1285,26 +1381,48 @@ var SessionManager = class {
|
|
|
1285
1381
|
if (this.#sessions.size === 0) {
|
|
1286
1382
|
throw new Error("no browser session connected \u2014 is your app running with @syrin/iris-browser enabled?");
|
|
1287
1383
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1384
|
+
const all = [...this.#sessions.values()];
|
|
1385
|
+
if (all.length === 1) {
|
|
1386
|
+
const [only] = all;
|
|
1387
|
+
if (only === void 0)
|
|
1388
|
+
throw new Error("session lookup failed");
|
|
1389
|
+
only.markAgentActivity();
|
|
1390
|
+
return only;
|
|
1291
1391
|
}
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1392
|
+
const scored = all.map((s) => ({ s, score: s.throttled() ? 1 : 0, ms: s.lastSeenMs() }));
|
|
1393
|
+
const bestScore = Math.min(...scored.map((x) => x.score));
|
|
1394
|
+
const candidates = scored.filter((x) => x.score === bestScore);
|
|
1395
|
+
candidates.sort((a, b) => a.ms - b.ms);
|
|
1396
|
+
const [best, runnerUp] = candidates;
|
|
1397
|
+
if (best === void 0)
|
|
1294
1398
|
throw new Error("session lookup failed");
|
|
1295
|
-
|
|
1296
|
-
|
|
1399
|
+
const allThrottled = bestScore === 1;
|
|
1400
|
+
const RECENCY_GAP_MS = allThrottled ? 0 : 1e3;
|
|
1401
|
+
const clearWinner = runnerUp === void 0 || best.ms + RECENCY_GAP_MS < runnerUp.ms;
|
|
1402
|
+
if (!clearWinner) {
|
|
1403
|
+
const detail = all.map((s) => `${s.id} (${s.throttled() ? "throttled" : "active"}, lastSeenMs=${s.lastSeenMs()})`).join(", ");
|
|
1404
|
+
throw new Error(`multiple sessions connected \u2014 pass sessionId to target one: ${detail}`);
|
|
1405
|
+
}
|
|
1406
|
+
best.s.markAgentActivity();
|
|
1407
|
+
return best.s;
|
|
1297
1408
|
}
|
|
1298
1409
|
};
|
|
1299
1410
|
|
|
1300
|
-
// ../server/dist/log.js
|
|
1301
|
-
function log(event, fields = {}) {
|
|
1302
|
-
const line = JSON.stringify({ event, ...fields });
|
|
1303
|
-
process.stderr.write(`${line}
|
|
1304
|
-
`);
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
1411
|
// ../server/dist/bridge.js
|
|
1412
|
+
function normalizeOrigin(origin) {
|
|
1413
|
+
try {
|
|
1414
|
+
return new URL(origin).origin;
|
|
1415
|
+
} catch {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function tokensMatch(expected, received) {
|
|
1420
|
+
if (received === void 0)
|
|
1421
|
+
return false;
|
|
1422
|
+
const expectedBytes = Buffer.from(expected);
|
|
1423
|
+
const receivedBytes = Buffer.from(received);
|
|
1424
|
+
return expectedBytes.length === receivedBytes.length && timingSafeEqual(expectedBytes, receivedBytes);
|
|
1425
|
+
}
|
|
1308
1426
|
function rawToString(raw) {
|
|
1309
1427
|
if (typeof raw === "string")
|
|
1310
1428
|
return raw;
|
|
@@ -1320,31 +1438,132 @@ var Bridge = class {
|
|
|
1320
1438
|
ready;
|
|
1321
1439
|
#wss;
|
|
1322
1440
|
#clock;
|
|
1441
|
+
#token;
|
|
1442
|
+
#allowedOrigins;
|
|
1443
|
+
#maxMessagesPerSecond;
|
|
1444
|
+
#maxSessions;
|
|
1445
|
+
#maxPendingConnections;
|
|
1446
|
+
#helloTimeoutMs;
|
|
1447
|
+
#pendingConnections = 0;
|
|
1323
1448
|
constructor(options) {
|
|
1449
|
+
const host = options.host ?? "127.0.0.1";
|
|
1450
|
+
if ((options.token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
|
|
1451
|
+
throw new Error(`Iris pairing token exceeds ${String(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH)} characters`);
|
|
1452
|
+
}
|
|
1453
|
+
if (!isLoopbackHostname(host) && (options.token === void 0 || options.token.length === 0)) {
|
|
1454
|
+
throw new Error("a pairing token is required when the Iris bridge binds beyond localhost");
|
|
1455
|
+
}
|
|
1324
1456
|
this.#clock = options.clock ?? (() => Date.now());
|
|
1325
|
-
this.#
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
this
|
|
1331
|
-
|
|
1332
|
-
|
|
1457
|
+
this.#token = options.token !== void 0 && options.token.length > 0 ? options.token : void 0;
|
|
1458
|
+
this.#allowedOrigins = new Set((options.allowedOrigins ?? []).map(normalizeOrigin).filter((origin) => origin !== null));
|
|
1459
|
+
this.#maxMessagesPerSecond = options.maxMessagesPerSecond ?? TRANSPORT_LIMITS.MAX_MESSAGES_PER_SECOND;
|
|
1460
|
+
this.#maxSessions = options.maxSessions ?? TRANSPORT_LIMITS.MAX_SESSIONS;
|
|
1461
|
+
this.#maxPendingConnections = options.maxPendingConnections ?? TRANSPORT_LIMITS.MAX_PENDING_CONNECTIONS;
|
|
1462
|
+
this.#helloTimeoutMs = options.helloTimeoutMs ?? TRANSPORT_LIMITS.HELLO_TIMEOUT_MS;
|
|
1463
|
+
if (options.server !== void 0) {
|
|
1464
|
+
const srv = options.server;
|
|
1465
|
+
this.#wss = new WebSocketServer({
|
|
1466
|
+
server: srv,
|
|
1467
|
+
path: IRIS_WS_PATH,
|
|
1468
|
+
maxPayload: TRANSPORT_LIMITS.MAX_MESSAGE_BYTES,
|
|
1469
|
+
verifyClient: ({ origin }, done) => {
|
|
1470
|
+
const allowed = this.#originAllowed(origin);
|
|
1471
|
+
if (!allowed)
|
|
1472
|
+
log("origin_rejected", { origin: origin ?? "missing" });
|
|
1473
|
+
done(allowed, 403, "Forbidden");
|
|
1474
|
+
}
|
|
1333
1475
|
});
|
|
1334
|
-
|
|
1476
|
+
this.ready = new Promise((resolve) => {
|
|
1477
|
+
if (srv.listening) {
|
|
1478
|
+
resolve(srv.address().port);
|
|
1479
|
+
} else {
|
|
1480
|
+
srv.once("listening", () => {
|
|
1481
|
+
resolve(srv.address().port);
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
} else {
|
|
1486
|
+
this.#wss = new WebSocketServer({
|
|
1487
|
+
port: options.port,
|
|
1488
|
+
host,
|
|
1489
|
+
path: IRIS_WS_PATH,
|
|
1490
|
+
maxPayload: TRANSPORT_LIMITS.MAX_MESSAGE_BYTES,
|
|
1491
|
+
verifyClient: ({ origin }, done) => {
|
|
1492
|
+
const allowed = this.#originAllowed(origin);
|
|
1493
|
+
if (!allowed)
|
|
1494
|
+
log("origin_rejected", { origin: origin ?? "missing" });
|
|
1495
|
+
done(allowed, 403, "Forbidden");
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
this.ready = new Promise((resolve) => {
|
|
1499
|
+
this.#wss.on("listening", () => {
|
|
1500
|
+
resolve(this.#wss.address().port);
|
|
1501
|
+
});
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1335
1504
|
this.#wss.on("connection", (socket) => {
|
|
1336
1505
|
this.#onConnection(socket);
|
|
1337
1506
|
});
|
|
1338
1507
|
}
|
|
1339
1508
|
#onConnection(socket) {
|
|
1509
|
+
if (this.#pendingConnections >= this.#maxPendingConnections) {
|
|
1510
|
+
socket.close(1013, "too many pending handshakes");
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
this.#pendingConnections += 1;
|
|
1514
|
+
let awaitingHello = true;
|
|
1340
1515
|
let session;
|
|
1516
|
+
let messageWindowStartedAt = this.#clock();
|
|
1517
|
+
let messagesInWindow = 0;
|
|
1518
|
+
const releasePending = () => {
|
|
1519
|
+
if (!awaitingHello)
|
|
1520
|
+
return;
|
|
1521
|
+
awaitingHello = false;
|
|
1522
|
+
this.#pendingConnections -= 1;
|
|
1523
|
+
};
|
|
1524
|
+
const helloTimer = setTimeout(() => {
|
|
1525
|
+
if (!awaitingHello)
|
|
1526
|
+
return;
|
|
1527
|
+
releasePending();
|
|
1528
|
+
socket.close(1008, "hello timeout");
|
|
1529
|
+
}, this.#helloTimeoutMs);
|
|
1341
1530
|
socket.on("message", (raw) => {
|
|
1531
|
+
const now = this.#clock();
|
|
1532
|
+
if (now - messageWindowStartedAt >= 1e3) {
|
|
1533
|
+
messageWindowStartedAt = now;
|
|
1534
|
+
messagesInWindow = 0;
|
|
1535
|
+
}
|
|
1536
|
+
messagesInWindow += 1;
|
|
1537
|
+
if (messagesInWindow > this.#maxMessagesPerSecond) {
|
|
1538
|
+
log("message_rate_exceeded", {});
|
|
1539
|
+
socket.close(1008, "message rate exceeded");
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1342
1542
|
const parsed = this.#parse(rawToString(raw));
|
|
1343
|
-
if (parsed === null)
|
|
1543
|
+
if (parsed === null) {
|
|
1544
|
+
socket.close(1008, "invalid message");
|
|
1344
1545
|
return;
|
|
1546
|
+
}
|
|
1345
1547
|
if (parsed.kind === MessageKind.HELLO) {
|
|
1548
|
+
if (session !== void 0) {
|
|
1549
|
+
socket.close(1008, "hello already received");
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (this.#token !== void 0 && !tokensMatch(this.#token, parsed.token)) {
|
|
1553
|
+
log("authentication_failed", {});
|
|
1554
|
+
socket.close(1008, "authentication failed");
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
const existing = this.sessions.get(parsed.sessionId);
|
|
1558
|
+
if (existing === void 0 && this.sessions.count() >= this.#maxSessions) {
|
|
1559
|
+
socket.close(1013, "session limit reached");
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
clearTimeout(helloTimer);
|
|
1563
|
+
releasePending();
|
|
1346
1564
|
session = new Session(parsed, socket, this.#clock);
|
|
1347
|
-
this.sessions.add(session);
|
|
1565
|
+
const replaced = this.sessions.add(session);
|
|
1566
|
+
replaced?.disconnect("session replaced by a newer connection");
|
|
1348
1567
|
log("session_connected", { sessionId: session.id, url: session.url });
|
|
1349
1568
|
return;
|
|
1350
1569
|
}
|
|
@@ -1358,15 +1577,28 @@ var Bridge = class {
|
|
|
1358
1577
|
}
|
|
1359
1578
|
});
|
|
1360
1579
|
socket.on("close", () => {
|
|
1580
|
+
clearTimeout(helloTimer);
|
|
1581
|
+
releasePending();
|
|
1361
1582
|
if (session !== void 0) {
|
|
1362
|
-
this.sessions.remove(session
|
|
1363
|
-
|
|
1583
|
+
if (this.sessions.remove(session)) {
|
|
1584
|
+
log("session_disconnected", { sessionId: session.id });
|
|
1585
|
+
}
|
|
1364
1586
|
}
|
|
1365
1587
|
});
|
|
1366
1588
|
socket.on("error", (err) => {
|
|
1367
1589
|
log("socket_error", { error: err.message });
|
|
1368
1590
|
});
|
|
1369
1591
|
}
|
|
1592
|
+
#originAllowed(origin) {
|
|
1593
|
+
if (origin === void 0)
|
|
1594
|
+
return true;
|
|
1595
|
+
const normalized = normalizeOrigin(origin);
|
|
1596
|
+
if (normalized === null)
|
|
1597
|
+
return false;
|
|
1598
|
+
if (this.#allowedOrigins.has(normalized))
|
|
1599
|
+
return true;
|
|
1600
|
+
return isLoopbackHostname(new URL(normalized).hostname);
|
|
1601
|
+
}
|
|
1370
1602
|
#parse(text) {
|
|
1371
1603
|
let json;
|
|
1372
1604
|
try {
|
|
@@ -1383,6 +1615,8 @@ var Bridge = class {
|
|
|
1383
1615
|
}
|
|
1384
1616
|
close() {
|
|
1385
1617
|
return new Promise((resolve) => {
|
|
1618
|
+
for (const client of this.#wss.clients)
|
|
1619
|
+
client.terminate();
|
|
1386
1620
|
this.#wss.close(() => {
|
|
1387
1621
|
resolve();
|
|
1388
1622
|
});
|
|
@@ -1497,6 +1731,7 @@ var IrisTool = {
|
|
|
1497
1731
|
STATE: "iris_state",
|
|
1498
1732
|
CAPABILITIES: "iris_capabilities",
|
|
1499
1733
|
CONTRACT_SAVE: "iris_contract_save",
|
|
1734
|
+
DOMAIN: "iris_domain",
|
|
1500
1735
|
FLOW_SAVE: "iris_flow_save",
|
|
1501
1736
|
FLOW_LIST: "iris_flow_list",
|
|
1502
1737
|
FLOW_LOAD: "iris_flow_load",
|
|
@@ -1530,180 +1765,555 @@ var IrisTool = {
|
|
|
1530
1765
|
/** Navigate the connected browser tab to a URL. */
|
|
1531
1766
|
NAVIGATE: "iris_navigate",
|
|
1532
1767
|
/** Reload the connected browser tab (soft or hard). */
|
|
1533
|
-
REFRESH: "iris_refresh"
|
|
1768
|
+
REFRESH: "iris_refresh",
|
|
1769
|
+
/** Report running version, latest available, changelog, and breaking changes. */
|
|
1770
|
+
VERSION_INFO: "iris_version_info",
|
|
1771
|
+
/** Install the latest server version and restart (Claude Code reconnects automatically). */
|
|
1772
|
+
APPLY_UPDATE: "iris_apply_update",
|
|
1773
|
+
/** Restore the previous server version and restart. */
|
|
1774
|
+
ROLLBACK: "iris_rollback"
|
|
1534
1775
|
};
|
|
1535
1776
|
|
|
1536
|
-
// ../server/dist/
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
baselines: join(root, IrisDir.BASELINES_SUBDIR),
|
|
1544
|
-
project: join(root, IrisDir.PROJECT_FILE),
|
|
1545
|
-
visual: join(root, IrisDir.VISUAL_SUBDIR)
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
function visualPath(root, name) {
|
|
1549
|
-
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.png`);
|
|
1550
|
-
}
|
|
1551
|
-
function visualDiffPath(root, name) {
|
|
1552
|
-
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.diff.png`);
|
|
1553
|
-
}
|
|
1554
|
-
function flowPath(root, name) {
|
|
1555
|
-
return join(root, IrisDir.FLOWS_SUBDIR, `${name}.json`);
|
|
1556
|
-
}
|
|
1557
|
-
function isValidFlowName(name) {
|
|
1558
|
-
return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
|
|
1559
|
-
}
|
|
1560
|
-
async function ensureIrisDir(fs, root) {
|
|
1561
|
-
const p = irisDirPaths(root);
|
|
1562
|
-
await fs.mkdir(p.root);
|
|
1563
|
-
await fs.mkdir(p.flows);
|
|
1564
|
-
await fs.mkdir(p.baselines);
|
|
1565
|
-
}
|
|
1566
|
-
var JSON_INDENT = 2;
|
|
1567
|
-
function stableSerialize(capabilities, generatedAt) {
|
|
1568
|
-
const envelope = {
|
|
1569
|
-
version: CONTRACT_FILE_VERSION,
|
|
1570
|
-
generatedAt,
|
|
1571
|
-
capabilities: {
|
|
1572
|
-
testids: [...capabilities.testids].sort(),
|
|
1573
|
-
signals: [...capabilities.signals].sort(),
|
|
1574
|
-
stores: [...capabilities.stores].sort(),
|
|
1575
|
-
flows: [...capabilities.flows].map((f) => ({ name: f.name, steps: [...f.steps] })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
1777
|
+
// ../server/dist/tools/tools-helpers.js
|
|
1778
|
+
function parseInteractive(tree) {
|
|
1779
|
+
const items = [];
|
|
1780
|
+
for (const line of tree.split("\n")) {
|
|
1781
|
+
const match = /\(ref=(e\d+)\)/.exec(line);
|
|
1782
|
+
if (match !== null) {
|
|
1783
|
+
items.push({ ref: match[1] ?? "", desc: line.replace(/\s*\(ref=e\d+\)/, "").trim() });
|
|
1576
1784
|
}
|
|
1577
|
-
};
|
|
1578
|
-
return `${JSON.stringify(envelope, null, JSON_INDENT)}
|
|
1579
|
-
`;
|
|
1580
|
-
}
|
|
1581
|
-
async function writeContract(fs, root, capabilities, now) {
|
|
1582
|
-
await ensureIrisDir(fs, root);
|
|
1583
|
-
await fs.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
|
|
1584
|
-
}
|
|
1585
|
-
async function readContract(fs, root) {
|
|
1586
|
-
const path = irisDirPaths(root).contract;
|
|
1587
|
-
if (!await fs.exists(path))
|
|
1588
|
-
return { ok: false, reason: ContractReadError.MISSING };
|
|
1589
|
-
let text;
|
|
1590
|
-
try {
|
|
1591
|
-
text = await fs.readFile(path);
|
|
1592
|
-
} catch (error) {
|
|
1593
|
-
return {
|
|
1594
|
-
ok: false,
|
|
1595
|
-
reason: fs.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
|
|
1596
|
-
};
|
|
1597
|
-
}
|
|
1598
|
-
let parsed;
|
|
1599
|
-
try {
|
|
1600
|
-
parsed = JSON.parse(text);
|
|
1601
|
-
} catch {
|
|
1602
|
-
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
1603
1785
|
}
|
|
1604
|
-
|
|
1605
|
-
if (!result.success)
|
|
1606
|
-
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
1607
|
-
return { ok: true, capabilities: result.data.capabilities, generatedAt: result.data.generatedAt };
|
|
1786
|
+
return items;
|
|
1608
1787
|
}
|
|
1609
|
-
|
|
1610
|
-
// ../server/dist/flows/flows.js
|
|
1611
1788
|
function asString(value) {
|
|
1612
1789
|
return typeof value === "string" ? value : void 0;
|
|
1613
1790
|
}
|
|
1791
|
+
function asNumber(value) {
|
|
1792
|
+
return typeof value === "number" ? value : void 0;
|
|
1793
|
+
}
|
|
1614
1794
|
function asRecord(value) {
|
|
1615
1795
|
return typeof value === "object" && value !== null ? value : {};
|
|
1616
1796
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1797
|
+
|
|
1798
|
+
// ../server/dist/flows/replay.js
|
|
1799
|
+
function asString2(value) {
|
|
1800
|
+
return typeof value === "string" ? value : void 0;
|
|
1619
1801
|
}
|
|
1620
|
-
function
|
|
1621
|
-
|
|
1622
|
-
const by = asString(sub["by"]);
|
|
1623
|
-
const value = asString(sub["value"]);
|
|
1624
|
-
const action = asString(sub["action"]);
|
|
1625
|
-
const args = asRecord(sub["args"]);
|
|
1626
|
-
if (by === QueryBy.TESTID && value !== void 0) {
|
|
1627
|
-
return buildStep(IrisTool.ACT, { kind: AnchorKind.TESTID, value }, action, args, false);
|
|
1628
|
-
}
|
|
1629
|
-
return buildStep(IrisTool.ACT, degradedAnchor(), action, args, true);
|
|
1802
|
+
function asRecord2(value) {
|
|
1803
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
1630
1804
|
}
|
|
1631
|
-
function
|
|
1632
|
-
const
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
return step;
|
|
1805
|
+
function replayActionArgs(value, confirmDangerous = false) {
|
|
1806
|
+
const args = { ...asRecord2(value) };
|
|
1807
|
+
delete args[DANGEROUS_ACTION_CONFIRM_ARG];
|
|
1808
|
+
if (confirmDangerous)
|
|
1809
|
+
args[DANGEROUS_ACTION_CONFIRM_ARG] = true;
|
|
1810
|
+
return args;
|
|
1638
1811
|
}
|
|
1639
|
-
function
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
out2.expect = step.expect;
|
|
1650
|
-
return out2;
|
|
1812
|
+
function compileActStep(args, res) {
|
|
1813
|
+
const testid = asString2(asRecord2(res)["testid"]);
|
|
1814
|
+
const action = asString2(args["action"]) ?? "";
|
|
1815
|
+
const actArgs = replayActionArgs(args["args"]);
|
|
1816
|
+
if (testid !== void 0) {
|
|
1817
|
+
return {
|
|
1818
|
+
tool: IrisTool.ACT,
|
|
1819
|
+
stable: true,
|
|
1820
|
+
args: { by: QueryBy.TESTID, value: testid, action, args: actArgs }
|
|
1821
|
+
};
|
|
1651
1822
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
if (step.expect !== void 0)
|
|
1658
|
-
out.expect = step.expect;
|
|
1659
|
-
return out;
|
|
1823
|
+
return {
|
|
1824
|
+
tool: IrisTool.ACT,
|
|
1825
|
+
stable: false,
|
|
1826
|
+
args: { ref: asString2(args["ref"]) ?? "", action, args: actArgs }
|
|
1827
|
+
};
|
|
1660
1828
|
}
|
|
1661
|
-
function
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1829
|
+
function compileSequenceStep(args, res) {
|
|
1830
|
+
const inputSteps = Array.isArray(args["steps"]) ? args["steps"] : [];
|
|
1831
|
+
const resolved = Array.isArray(asRecord2(res)["steps"]) ? asRecord2(res)["steps"] : [];
|
|
1832
|
+
let stable = inputSteps.length > 0;
|
|
1833
|
+
const subSteps = inputSteps.map((raw, i) => {
|
|
1834
|
+
const step = asRecord2(raw);
|
|
1835
|
+
const action = asString2(step["action"]) ?? "";
|
|
1836
|
+
const stepArgs = replayActionArgs(step["args"]);
|
|
1837
|
+
const testid = asString2(asRecord2(resolved[i])["testid"]);
|
|
1838
|
+
if (testid !== void 0) {
|
|
1839
|
+
return { by: QueryBy.TESTID, value: testid, action, args: stepArgs };
|
|
1840
|
+
}
|
|
1841
|
+
stable = false;
|
|
1842
|
+
return { ref: asString2(step["ref"]) ?? "", action, args: stepArgs };
|
|
1667
1843
|
});
|
|
1668
|
-
|
|
1669
|
-
if (ann.dynamic.length > 0) {
|
|
1670
|
-
out.dynamic = ann.dynamic.map((value) => ({ kind: AnchorKind.TESTID, value }));
|
|
1671
|
-
}
|
|
1672
|
-
if (ann.success !== void 0)
|
|
1673
|
-
out.success = ann.success;
|
|
1674
|
-
return out;
|
|
1844
|
+
return { tool: IrisTool.ACT_SEQUENCE, stable, args: { steps: subSteps } };
|
|
1675
1845
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
* The single byte-stable flow serializer: 2-space indent + one trailing newline. save(),
|
|
1689
|
-
* saveFlow() and heal() all route through it so an unchanged flow that round-trips through any
|
|
1690
|
-
* of them produces byte-identical on-disk content (locked by the byte-stability tests).
|
|
1691
|
-
*/
|
|
1692
|
-
#serialize(flow) {
|
|
1693
|
-
return `${JSON.stringify(flow, null, JSON_INDENT2)}
|
|
1694
|
-
`;
|
|
1846
|
+
async function resolveRef(session, step) {
|
|
1847
|
+
const by = asString2(step.by);
|
|
1848
|
+
const value = asString2(step.value);
|
|
1849
|
+
if (by === QueryBy.TESTID && value !== void 0) {
|
|
1850
|
+
const result = await session.command(IrisCommand.QUERY, { by, value });
|
|
1851
|
+
if (!result.ok)
|
|
1852
|
+
throw new Error(result.error ?? "query failed");
|
|
1853
|
+
const elements = Array.isArray(asRecord2(result.result)["elements"]) ? asRecord2(result.result)["elements"] : [];
|
|
1854
|
+
const ref2 = asString2(asRecord2(elements[0])["ref"]);
|
|
1855
|
+
if (ref2 === void 0)
|
|
1856
|
+
throw new Error(`testid '${value}' did not resolve in current page`);
|
|
1857
|
+
return elements.length > 1 ? { ref: ref2, note: `ambiguous testid '${value}', used first match` } : { ref: ref2 };
|
|
1695
1858
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1859
|
+
const ref = asString2(step.ref);
|
|
1860
|
+
if (ref === void 0 || ref.length === 0)
|
|
1861
|
+
throw new Error("step has no testid or ref to resolve");
|
|
1862
|
+
return { ref, note: "replayed by stale ref (not portable across sessions)" };
|
|
1863
|
+
}
|
|
1864
|
+
async function replayProgram(session, program, confirmDangerous = false) {
|
|
1865
|
+
const results = [];
|
|
1866
|
+
for (const step of program.steps) {
|
|
1867
|
+
try {
|
|
1868
|
+
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
1869
|
+
const subs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
1870
|
+
const notes = [];
|
|
1871
|
+
const liveSteps = [];
|
|
1872
|
+
for (const raw of subs) {
|
|
1873
|
+
const sub = asRecord2(raw);
|
|
1874
|
+
const { ref, note } = await resolveRef(session, sub);
|
|
1875
|
+
if (note !== void 0)
|
|
1876
|
+
notes.push(note);
|
|
1877
|
+
liveSteps.push({
|
|
1878
|
+
ref,
|
|
1879
|
+
action: asString2(sub["action"]) ?? "",
|
|
1880
|
+
args: replayActionArgs(sub["args"], confirmDangerous)
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
const r = await session.command(IrisCommand.ACT_SEQUENCE, { steps: liveSteps });
|
|
1884
|
+
results.push(buildResult(step.tool, r.ok, r.error, notes));
|
|
1885
|
+
if (!r.ok)
|
|
1886
|
+
break;
|
|
1887
|
+
} else {
|
|
1888
|
+
const { ref, note } = await resolveRef(session, step.args);
|
|
1889
|
+
const r = await session.command(IrisCommand.ACT, {
|
|
1890
|
+
ref,
|
|
1891
|
+
action: asString2(step.args["action"]) ?? "",
|
|
1892
|
+
args: replayActionArgs(step.args["args"], confirmDangerous)
|
|
1893
|
+
});
|
|
1894
|
+
results.push(buildResult(step.tool, r.ok, r.error, note !== void 0 ? [note] : []));
|
|
1895
|
+
if (!r.ok)
|
|
1896
|
+
break;
|
|
1897
|
+
}
|
|
1898
|
+
} catch (e) {
|
|
1899
|
+
results.push({
|
|
1900
|
+
tool: step.tool,
|
|
1901
|
+
ok: false,
|
|
1902
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1903
|
+
});
|
|
1904
|
+
break;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return results;
|
|
1908
|
+
}
|
|
1909
|
+
function buildResult(tool, ok, error, notes) {
|
|
1910
|
+
const base = { tool, ok };
|
|
1911
|
+
if (!ok)
|
|
1912
|
+
base.error = error ?? "command failed";
|
|
1913
|
+
if (notes.length > 0)
|
|
1914
|
+
base.note = notes.join("; ");
|
|
1915
|
+
return base;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// ../server/dist/flows/flow-replay.js
|
|
1919
|
+
function editDistance(a, b) {
|
|
1920
|
+
const s = a.toLowerCase();
|
|
1921
|
+
const t = b.toLowerCase();
|
|
1922
|
+
const rows = s.length + 1;
|
|
1923
|
+
const cols = t.length + 1;
|
|
1924
|
+
const prev = new Array(cols);
|
|
1925
|
+
const curr = new Array(cols);
|
|
1926
|
+
for (let j = 0; j < cols; j++)
|
|
1927
|
+
prev[j] = j;
|
|
1928
|
+
for (let i = 1; i < rows; i++) {
|
|
1929
|
+
curr[0] = i;
|
|
1930
|
+
for (let j = 1; j < cols; j++) {
|
|
1931
|
+
const cost = s[i - 1] === t[j - 1] ? 0 : 1;
|
|
1932
|
+
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
1933
|
+
}
|
|
1934
|
+
for (let j = 0; j < cols; j++)
|
|
1935
|
+
prev[j] = curr[j] ?? 0;
|
|
1936
|
+
}
|
|
1937
|
+
return prev[cols - 1] ?? 0;
|
|
1938
|
+
}
|
|
1939
|
+
function nearestTestid(missing, present) {
|
|
1940
|
+
let best = null;
|
|
1941
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1942
|
+
for (const candidate of present) {
|
|
1943
|
+
const distance = editDistance(missing, candidate);
|
|
1944
|
+
if (distance < bestDistance || distance === bestDistance && best !== null && candidate.length < best.length || distance === bestDistance && best !== null && candidate.length === best.length && candidate < best) {
|
|
1945
|
+
best = candidate;
|
|
1946
|
+
bestDistance = distance;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
return best;
|
|
1950
|
+
}
|
|
1951
|
+
function readQuery(result) {
|
|
1952
|
+
if (!result.ok)
|
|
1953
|
+
return { refs: [] };
|
|
1954
|
+
const payload = asRecord(result.result);
|
|
1955
|
+
const elements = Array.isArray(payload["elements"]) ? payload["elements"] : [];
|
|
1956
|
+
const refs = elements.map((e) => asString(asRecord(e)["ref"]) ?? "").filter((r) => r.length > 0);
|
|
1957
|
+
const rawHint = payload["hint"];
|
|
1958
|
+
if (typeof rawHint === "object" && rawHint !== null) {
|
|
1959
|
+
const hint = asRecord(rawHint);
|
|
1960
|
+
const present = Array.isArray(hint["presentTestids"]) ? hint["presentTestids"].filter((t) => typeof t === "string") : [];
|
|
1961
|
+
return {
|
|
1962
|
+
refs,
|
|
1963
|
+
hint: {
|
|
1964
|
+
route: asString(hint["route"]) ?? "",
|
|
1965
|
+
presentTestids: present,
|
|
1966
|
+
presentRegions: [],
|
|
1967
|
+
knownEmptyState: hint["knownEmptyState"] === true
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
return { refs };
|
|
1972
|
+
}
|
|
1973
|
+
function nearestIsAmbiguous(missing, present) {
|
|
1974
|
+
if (present.length < 2)
|
|
1975
|
+
return false;
|
|
1976
|
+
let min = Number.POSITIVE_INFINITY;
|
|
1977
|
+
let count = 0;
|
|
1978
|
+
for (const candidate of present) {
|
|
1979
|
+
const distance = editDistance(missing, candidate);
|
|
1980
|
+
if (distance < min) {
|
|
1981
|
+
min = distance;
|
|
1982
|
+
count = 1;
|
|
1983
|
+
} else if (distance === min) {
|
|
1984
|
+
count += 1;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return count >= 2;
|
|
1988
|
+
}
|
|
1989
|
+
function testidDrift(value, hint) {
|
|
1990
|
+
const present = hint?.presentTestids ?? [];
|
|
1991
|
+
const drift = {
|
|
1992
|
+
reasonKind: DriftReason.TESTID_NOT_FOUND,
|
|
1993
|
+
reason: `testid "${value}" not found`,
|
|
1994
|
+
anchor: value,
|
|
1995
|
+
nearest: nearestTestid(value, present)
|
|
1996
|
+
};
|
|
1997
|
+
if (nearestIsAmbiguous(value, present))
|
|
1998
|
+
drift.ambiguous = true;
|
|
1999
|
+
return drift;
|
|
2000
|
+
}
|
|
2001
|
+
function anchorLabel(anchor) {
|
|
2002
|
+
if (anchor.kind === AnchorKind.TESTID)
|
|
2003
|
+
return anchor.value;
|
|
2004
|
+
if (anchor.kind === AnchorKind.SIGNAL)
|
|
2005
|
+
return anchor.name;
|
|
2006
|
+
return anchor.name ?? anchor.role;
|
|
2007
|
+
}
|
|
2008
|
+
async function runTestidStep(session, step, index, value, dynamic, confirmDangerous) {
|
|
2009
|
+
const queryResult = await session.command(IrisCommand.QUERY, { by: QueryBy.TESTID, value });
|
|
2010
|
+
const { refs, hint } = readQuery(queryResult);
|
|
2011
|
+
if (refs.length === 0) {
|
|
2012
|
+
return {
|
|
2013
|
+
step: index,
|
|
2014
|
+
tool: step.tool,
|
|
2015
|
+
anchor: value,
|
|
2016
|
+
ok: false,
|
|
2017
|
+
drift: testidDrift(value, hint)
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
const ref = refs[0] ?? "";
|
|
2021
|
+
const note = refs.length > 1 ? `ambiguous testid '${value}', used first match` : void 0;
|
|
2022
|
+
const act = await session.command(IrisCommand.ACT, {
|
|
2023
|
+
ref,
|
|
2024
|
+
action: step.action ?? "",
|
|
2025
|
+
args: replayActionArgs(step.args, confirmDangerous)
|
|
2026
|
+
});
|
|
2027
|
+
const result = { step: index, tool: step.tool, anchor: value, ok: act.ok };
|
|
2028
|
+
if (!act.ok) {
|
|
2029
|
+
result.error = act.error ?? "command failed";
|
|
2030
|
+
if (note !== void 0)
|
|
2031
|
+
result.note = note;
|
|
2032
|
+
return result;
|
|
2033
|
+
}
|
|
2034
|
+
const expectTestid = step.expect?.element?.testid;
|
|
2035
|
+
if (expectTestid !== void 0 && !dynamic.has(expectTestid)) {
|
|
2036
|
+
const expectQuery = await session.command(IrisCommand.QUERY, {
|
|
2037
|
+
by: QueryBy.TESTID,
|
|
2038
|
+
value: expectTestid
|
|
2039
|
+
});
|
|
2040
|
+
const expectRefs = readQuery(expectQuery);
|
|
2041
|
+
if (expectRefs.refs.length === 0) {
|
|
2042
|
+
return {
|
|
2043
|
+
step: index,
|
|
2044
|
+
tool: step.tool,
|
|
2045
|
+
anchor: expectTestid,
|
|
2046
|
+
ok: false,
|
|
2047
|
+
drift: testidDrift(expectTestid, expectRefs.hint)
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (note !== void 0)
|
|
2052
|
+
result.note = note;
|
|
2053
|
+
return result;
|
|
2054
|
+
}
|
|
2055
|
+
async function runSignalStep(session, step, index, name, waitForSignal, signalTimeoutMs) {
|
|
2056
|
+
const verdict = await waitForSignal(session, { kind: "signal", name }, signalTimeoutMs);
|
|
2057
|
+
if (verdict.pass)
|
|
2058
|
+
return { step: index, tool: step.tool, anchor: name, ok: true };
|
|
2059
|
+
return {
|
|
2060
|
+
step: index,
|
|
2061
|
+
tool: step.tool,
|
|
2062
|
+
anchor: name,
|
|
2063
|
+
ok: false,
|
|
2064
|
+
drift: {
|
|
2065
|
+
reasonKind: DriftReason.SIGNAL_NOT_OBSERVED,
|
|
2066
|
+
reason: `signal "${name}" not observed`,
|
|
2067
|
+
anchor: name,
|
|
2068
|
+
nearest: null
|
|
2069
|
+
}
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
async function replayFlow(session, flow, waitForSignal, signalTimeoutMs, confirmDangerous = false) {
|
|
2073
|
+
const results = [];
|
|
2074
|
+
const dynamic = new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
2075
|
+
let index = 0;
|
|
2076
|
+
for (const step of flow.steps) {
|
|
2077
|
+
const label = anchorLabel(step.anchor);
|
|
2078
|
+
let result;
|
|
2079
|
+
if (step.anchor.kind === AnchorKind.SIGNAL) {
|
|
2080
|
+
result = await runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs);
|
|
2081
|
+
} else {
|
|
2082
|
+
result = await runTestidStep(session, step, index, label, dynamic, confirmDangerous);
|
|
2083
|
+
}
|
|
2084
|
+
results.push(result);
|
|
2085
|
+
if (result.drift !== void 0 || !result.ok)
|
|
2086
|
+
break;
|
|
2087
|
+
index += 1;
|
|
2088
|
+
}
|
|
2089
|
+
return results;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// ../server/dist/flows/heal.js
|
|
2093
|
+
function confidenceFor(from, to) {
|
|
2094
|
+
if (from === to)
|
|
2095
|
+
return 1;
|
|
2096
|
+
const span = Math.max(from.length, to.length);
|
|
2097
|
+
if (span === 0)
|
|
2098
|
+
return 1;
|
|
2099
|
+
const raw = 1 - editDistance(from, to) / span;
|
|
2100
|
+
if (raw >= 1)
|
|
2101
|
+
return 1;
|
|
2102
|
+
if (raw <= 0)
|
|
2103
|
+
return Number.EPSILON;
|
|
2104
|
+
return raw;
|
|
2105
|
+
}
|
|
2106
|
+
function applyHealChanges(flow, changes) {
|
|
2107
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
2108
|
+
for (const change of changes)
|
|
2109
|
+
byStep.set(change.step, change);
|
|
2110
|
+
const applied = [];
|
|
2111
|
+
const steps = flow.steps.map((step, index) => {
|
|
2112
|
+
const change = byStep.get(index);
|
|
2113
|
+
if (change === void 0 || step.anchor.kind !== AnchorKind.TESTID || step.anchor.value !== change.from) {
|
|
2114
|
+
return step;
|
|
2115
|
+
}
|
|
2116
|
+
applied.push(change);
|
|
2117
|
+
return { ...step, anchor: { kind: AnchorKind.TESTID, value: change.to } };
|
|
2118
|
+
});
|
|
2119
|
+
return { flow: { ...flow, steps }, applied };
|
|
2120
|
+
}
|
|
2121
|
+
function proposeRebindWith(drift, step, minConfidence) {
|
|
2122
|
+
if (drift.reasonKind !== DriftReason.TESTID_NOT_FOUND)
|
|
2123
|
+
return void 0;
|
|
2124
|
+
if (drift.ambiguous === true)
|
|
2125
|
+
return void 0;
|
|
2126
|
+
const to = drift.nearest;
|
|
2127
|
+
if (to === null)
|
|
2128
|
+
return void 0;
|
|
2129
|
+
const confidence = confidenceFor(drift.anchor, to);
|
|
2130
|
+
if (confidence < minConfidence)
|
|
2131
|
+
return void 0;
|
|
2132
|
+
return { step, from: drift.anchor, to, confidence };
|
|
2133
|
+
}
|
|
2134
|
+
function collectProposals(steps, minConfidence = HEAL_CONFIDENCE_MIN) {
|
|
2135
|
+
const proposals = [];
|
|
2136
|
+
for (const step of steps) {
|
|
2137
|
+
if (step.drift === void 0)
|
|
2138
|
+
continue;
|
|
2139
|
+
const proposal = proposeRebindWith(step.drift, step.step, minConfidence);
|
|
2140
|
+
if (proposal !== void 0)
|
|
2141
|
+
proposals.push(proposal);
|
|
2142
|
+
}
|
|
2143
|
+
return proposals;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// ../server/dist/project/iris-dir.js
|
|
2147
|
+
import { join } from "path";
|
|
2148
|
+
function irisDirPaths(root) {
|
|
2149
|
+
return {
|
|
2150
|
+
root,
|
|
2151
|
+
contract: join(root, IrisDir.CONTRACT_FILE),
|
|
2152
|
+
flows: join(root, IrisDir.FLOWS_SUBDIR),
|
|
2153
|
+
baselines: join(root, IrisDir.BASELINES_SUBDIR),
|
|
2154
|
+
project: join(root, IrisDir.PROJECT_FILE),
|
|
2155
|
+
visual: join(root, IrisDir.VISUAL_SUBDIR)
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
function visualPath(root, name) {
|
|
2159
|
+
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.png`);
|
|
2160
|
+
}
|
|
2161
|
+
function visualDiffPath(root, name) {
|
|
2162
|
+
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.diff.png`);
|
|
2163
|
+
}
|
|
2164
|
+
function flowPath(root, name) {
|
|
2165
|
+
return join(root, IrisDir.FLOWS_SUBDIR, `${name}.json`);
|
|
2166
|
+
}
|
|
2167
|
+
function isValidFlowName(name) {
|
|
2168
|
+
return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
|
|
2169
|
+
}
|
|
2170
|
+
async function ensureIrisDir(fs2, root) {
|
|
2171
|
+
const p = irisDirPaths(root);
|
|
2172
|
+
await fs2.mkdir(p.root);
|
|
2173
|
+
await fs2.mkdir(p.flows);
|
|
2174
|
+
await fs2.mkdir(p.baselines);
|
|
2175
|
+
}
|
|
2176
|
+
var JSON_INDENT = 2;
|
|
2177
|
+
function stableSerialize(capabilities, generatedAt) {
|
|
2178
|
+
const envelope = {
|
|
2179
|
+
version: CONTRACT_FILE_VERSION,
|
|
2180
|
+
generatedAt,
|
|
2181
|
+
capabilities: {
|
|
2182
|
+
testids: [...capabilities.testids].sort(),
|
|
2183
|
+
signals: [...capabilities.signals].sort(),
|
|
2184
|
+
stores: [...capabilities.stores].sort(),
|
|
2185
|
+
flows: [...capabilities.flows].map((f) => ({ name: f.name, steps: [...f.steps] })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
return `${JSON.stringify(envelope, null, JSON_INDENT)}
|
|
2189
|
+
`;
|
|
2190
|
+
}
|
|
2191
|
+
async function writeContract(fs2, root, capabilities, now) {
|
|
2192
|
+
await ensureIrisDir(fs2, root);
|
|
2193
|
+
await fs2.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
|
|
2194
|
+
}
|
|
2195
|
+
async function readContract(fs2, root) {
|
|
2196
|
+
const path = irisDirPaths(root).contract;
|
|
2197
|
+
if (!await fs2.exists(path))
|
|
2198
|
+
return { ok: false, reason: ContractReadError.MISSING };
|
|
2199
|
+
let text;
|
|
2200
|
+
try {
|
|
2201
|
+
text = await fs2.readFile(path);
|
|
2202
|
+
} catch (error) {
|
|
2203
|
+
return {
|
|
2204
|
+
ok: false,
|
|
2205
|
+
reason: fs2.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
let parsed;
|
|
2209
|
+
try {
|
|
2210
|
+
parsed = JSON.parse(text);
|
|
2211
|
+
} catch {
|
|
2212
|
+
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
2213
|
+
}
|
|
2214
|
+
const result = ContractFileSchema.safeParse(parsed);
|
|
2215
|
+
if (!result.success)
|
|
2216
|
+
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
2217
|
+
return { ok: true, capabilities: result.data.capabilities, generatedAt: result.data.generatedAt };
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// ../server/dist/flows/flows.js
|
|
2221
|
+
function asString3(value) {
|
|
2222
|
+
return typeof value === "string" ? value : void 0;
|
|
2223
|
+
}
|
|
2224
|
+
function asRecord3(value) {
|
|
2225
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
2226
|
+
}
|
|
2227
|
+
function degradedAnchor() {
|
|
2228
|
+
return { kind: AnchorKind.ROLE, role: DEGRADED_ANCHOR_ROLE };
|
|
2229
|
+
}
|
|
2230
|
+
function subStepToFlowStep(raw) {
|
|
2231
|
+
const sub = asRecord3(raw);
|
|
2232
|
+
const by = asString3(sub["by"]);
|
|
2233
|
+
const value = asString3(sub["value"]);
|
|
2234
|
+
const action = asString3(sub["action"]);
|
|
2235
|
+
const args = asRecord3(sub["args"]);
|
|
2236
|
+
if (by === QueryBy.TESTID && value !== void 0) {
|
|
2237
|
+
return buildStep(IrisTool.ACT, { kind: AnchorKind.TESTID, value }, action, args, false);
|
|
2238
|
+
}
|
|
2239
|
+
return buildStep(IrisTool.ACT, degradedAnchor(), action, args, true);
|
|
2240
|
+
}
|
|
2241
|
+
function buildStep(tool, anchor, action, args, degraded) {
|
|
2242
|
+
const step = { tool, anchor, args };
|
|
2243
|
+
if (action !== void 0)
|
|
2244
|
+
step.action = action;
|
|
2245
|
+
if (degraded)
|
|
2246
|
+
step.degraded = true;
|
|
2247
|
+
return step;
|
|
2248
|
+
}
|
|
2249
|
+
function recordedStepToFlowStep(step) {
|
|
2250
|
+
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
2251
|
+
const rawSubs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
2252
|
+
const subs = rawSubs.map(subStepToFlowStep);
|
|
2253
|
+
const degraded = subs.some((s) => s.degraded === true);
|
|
2254
|
+
const anchor = subs[0]?.anchor ?? degradedAnchor();
|
|
2255
|
+
const out2 = { tool: IrisTool.ACT_SEQUENCE, anchor, steps: subs };
|
|
2256
|
+
if (degraded)
|
|
2257
|
+
out2.degraded = true;
|
|
2258
|
+
if (step.expect !== void 0)
|
|
2259
|
+
out2.expect = step.expect;
|
|
2260
|
+
return out2;
|
|
2261
|
+
}
|
|
2262
|
+
const by = asString3(step.args["by"]);
|
|
2263
|
+
const value = asString3(step.args["value"]);
|
|
2264
|
+
const action = asString3(step.args["action"]);
|
|
2265
|
+
const args = asRecord3(step.args["args"]);
|
|
2266
|
+
const out = by === QueryBy.TESTID && value !== void 0 ? buildStep(step.tool, { kind: AnchorKind.TESTID, value }, action, args, false) : buildStep(step.tool, degradedAnchor(), action, args, true);
|
|
2267
|
+
if (step.expect !== void 0)
|
|
2268
|
+
out.expect = step.expect;
|
|
2269
|
+
return out;
|
|
2270
|
+
}
|
|
2271
|
+
function withAnnotations(flow, ann) {
|
|
2272
|
+
if (ann === void 0)
|
|
2273
|
+
return flow;
|
|
2274
|
+
const steps = flow.steps.map((step, i) => {
|
|
2275
|
+
const expect = ann.stepExpect.get(i);
|
|
2276
|
+
return expect === void 0 ? step : { ...step, expect };
|
|
2277
|
+
});
|
|
2278
|
+
const out = { ...flow, steps };
|
|
2279
|
+
if (ann.dynamic.length > 0) {
|
|
2280
|
+
out.dynamic = ann.dynamic.map((value) => ({ kind: AnchorKind.TESTID, value }));
|
|
2281
|
+
}
|
|
2282
|
+
if (ann.success !== void 0)
|
|
2283
|
+
out.success = ann.success;
|
|
2284
|
+
return out;
|
|
2285
|
+
}
|
|
2286
|
+
var JSON_INDENT2 = 2;
|
|
2287
|
+
var FLOW_SUFFIX = ".json";
|
|
2288
|
+
var FlowStore = class {
|
|
2289
|
+
#fs;
|
|
2290
|
+
#root;
|
|
2291
|
+
#clock;
|
|
2292
|
+
constructor(fs2, root, clock) {
|
|
2293
|
+
this.#fs = fs2;
|
|
2294
|
+
this.#root = root;
|
|
2295
|
+
this.#clock = clock;
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* The single byte-stable flow serializer: 2-space indent + one trailing newline. save(),
|
|
2299
|
+
* saveFlow() and heal() all route through it so an unchanged flow that round-trips through any
|
|
2300
|
+
* of them produces byte-identical on-disk content (locked by the byte-stability tests).
|
|
2301
|
+
*/
|
|
2302
|
+
#serialize(flow) {
|
|
2303
|
+
return `${JSON.stringify(flow, null, JSON_INDENT2)}
|
|
2304
|
+
`;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Convert a CompiledProgram (testid-normalized) into an anchored, on-disk flow + write it.
|
|
2308
|
+
* Optionally fold structured annotations (per-step expect, dynamic[], success) onto
|
|
2309
|
+
* the flow before writing. Omitting `annotations` reproduces the same bytes.
|
|
2310
|
+
*/
|
|
2311
|
+
async save(program, annotations) {
|
|
2312
|
+
if (!isValidFlowName(program.name)) {
|
|
2313
|
+
return { ok: false, code: FlowErrorCode.INVALID_NAME };
|
|
2314
|
+
}
|
|
2315
|
+
const steps = program.steps.map(recordedStepToFlowStep);
|
|
2316
|
+
const base = {
|
|
1707
2317
|
version: FLOW_FILE_VERSION,
|
|
1708
2318
|
name: program.name,
|
|
1709
2319
|
createdAt: this.#clock.now(),
|
|
@@ -1766,19 +2376,7 @@ var FlowStore = class {
|
|
|
1766
2376
|
if (!loaded.ok)
|
|
1767
2377
|
return { ok: false, code: loaded.code };
|
|
1768
2378
|
const flow = loaded.value;
|
|
1769
|
-
const
|
|
1770
|
-
for (const change of changes)
|
|
1771
|
-
byStep.set(change.step, change);
|
|
1772
|
-
const applied = [];
|
|
1773
|
-
const steps = flow.steps.map((step, index) => {
|
|
1774
|
-
const change = byStep.get(index);
|
|
1775
|
-
if (change === void 0 || step.anchor.kind !== AnchorKind.TESTID || step.anchor.value !== change.from) {
|
|
1776
|
-
return step;
|
|
1777
|
-
}
|
|
1778
|
-
applied.push(change);
|
|
1779
|
-
return { ...step, anchor: { kind: AnchorKind.TESTID, value: change.to } };
|
|
1780
|
-
});
|
|
1781
|
-
const next = { ...flow, steps };
|
|
2379
|
+
const { flow: next, applied } = applyHealChanges(flow, changes);
|
|
1782
2380
|
await this.#fs.writeFile(flowPath(this.#root, name), this.#serialize(next));
|
|
1783
2381
|
return { ok: true, value: { name, changed: applied } };
|
|
1784
2382
|
}
|
|
@@ -1826,8 +2424,8 @@ var ProjectStore = class {
|
|
|
1826
2424
|
#fs;
|
|
1827
2425
|
#root;
|
|
1828
2426
|
#clock;
|
|
1829
|
-
constructor(
|
|
1830
|
-
this.#fs =
|
|
2427
|
+
constructor(fs2, root, clock) {
|
|
2428
|
+
this.#fs = fs2;
|
|
1831
2429
|
this.#root = root;
|
|
1832
2430
|
this.#clock = clock;
|
|
1833
2431
|
}
|
|
@@ -2006,10 +2604,10 @@ function createNodeFileSystem() {
|
|
|
2006
2604
|
|
|
2007
2605
|
// ../server/dist/mcp.js
|
|
2008
2606
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2009
|
-
import { z as
|
|
2607
|
+
import { z as z17 } from "zod";
|
|
2010
2608
|
|
|
2011
2609
|
// ../server/dist/tools/tools.js
|
|
2012
|
-
import { z as
|
|
2610
|
+
import { z as z16 } from "zod";
|
|
2013
2611
|
|
|
2014
2612
|
// ../server/dist/input/real-input.js
|
|
2015
2613
|
var DriveError = class extends Error {
|
|
@@ -2070,8 +2668,10 @@ async function performGesture(page, action, box, args, sleep) {
|
|
|
2070
2668
|
}
|
|
2071
2669
|
return { performed: false, center };
|
|
2072
2670
|
}
|
|
2671
|
+
var HIDE_IRIS_CHROME_CSS = "[data-iris-overlay]{display:none !important}";
|
|
2672
|
+
var SCREENSHOT_DETERMINISM = { style: HIDE_IRIS_CHROME_CSS, animations: "disabled" };
|
|
2073
2673
|
async function capturePage(page, opts) {
|
|
2074
|
-
const buf = await page.screenshot(opts.clip !== void 0 ? { clip: opts.clip } : opts.fullPage === true ? { fullPage: true } : {});
|
|
2674
|
+
const buf = await page.screenshot(opts.clip !== void 0 ? { ...SCREENSHOT_DETERMINISM, clip: opts.clip } : opts.fullPage === true ? { ...SCREENSHOT_DETERMINISM, fullPage: true } : { ...SCREENSHOT_DETERMINISM });
|
|
2075
2675
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
2076
2676
|
}
|
|
2077
2677
|
var nodeSleep = (ms) => new Promise((resolve) => {
|
|
@@ -2306,6 +2906,7 @@ var PredicateSchema = z3.lazy(() => z3.discriminatedUnion("kind", [
|
|
|
2306
2906
|
name: z3.string().optional(),
|
|
2307
2907
|
dataMatches: z3.record(z3.unknown()).optional()
|
|
2308
2908
|
}),
|
|
2909
|
+
z3.object({ kind: z3.literal("settled"), quietMs: z3.number().positive().optional() }),
|
|
2309
2910
|
z3.object({ kind: z3.literal("allOf"), predicates: z3.array(PredicateSchema) }),
|
|
2310
2911
|
z3.object({ kind: z3.literal("anyOf"), predicates: z3.array(PredicateSchema) }),
|
|
2311
2912
|
z3.object({ kind: z3.literal("not"), predicate: PredicateSchema })
|
|
@@ -2502,6 +3103,39 @@ function evalSignal(events, p) {
|
|
|
2502
3103
|
evidence: sameName.length > 0 ? { nearMiss: sameName } : void 0
|
|
2503
3104
|
};
|
|
2504
3105
|
}
|
|
3106
|
+
var SETTLE_ACTIVITY = /* @__PURE__ */ new Set([
|
|
3107
|
+
EventType.NET_REQUEST,
|
|
3108
|
+
EventType.DOM_ADDED,
|
|
3109
|
+
EventType.DOM_REMOVED,
|
|
3110
|
+
EventType.DOM_ATTR
|
|
3111
|
+
]);
|
|
3112
|
+
var DEFAULT_QUIET_MS = 500;
|
|
3113
|
+
function evalSettled(events, p, now) {
|
|
3114
|
+
const quietMs = p.quietMs ?? DEFAULT_QUIET_MS;
|
|
3115
|
+
let lastT = -1;
|
|
3116
|
+
let lastType;
|
|
3117
|
+
for (const e of events) {
|
|
3118
|
+
if (SETTLE_ACTIVITY.has(e.type) && e.t > lastT) {
|
|
3119
|
+
lastT = e.t;
|
|
3120
|
+
lastType = e.type;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
if (lastT < 0) {
|
|
3124
|
+
return {
|
|
3125
|
+
pass: true,
|
|
3126
|
+
evidence: { settled: true, quietForMs: null, note: "no activity to settle" }
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
const quietForMs = now - lastT;
|
|
3130
|
+
if (quietForMs >= quietMs) {
|
|
3131
|
+
return { pass: true, evidence: { settled: true, quietForMs, lastActivity: lastType } };
|
|
3132
|
+
}
|
|
3133
|
+
return {
|
|
3134
|
+
pass: false,
|
|
3135
|
+
failureReason: `not settled: last activity (${String(lastType)}) ${String(quietForMs)}ms ago, need ${String(quietMs)}ms quiet`,
|
|
3136
|
+
evidence: { quietForMs, lastActivity: lastType }
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
2505
3139
|
async function evaluatePredicate(session, predicate, since = 0) {
|
|
2506
3140
|
const events = session.eventsSince(since);
|
|
2507
3141
|
switch (predicate.kind) {
|
|
@@ -2519,6 +3153,8 @@ async function evaluatePredicate(session, predicate, since = 0) {
|
|
|
2519
3153
|
return evalAnimation(events, predicate);
|
|
2520
3154
|
case "signal":
|
|
2521
3155
|
return evalSignal(events, predicate);
|
|
3156
|
+
case "settled":
|
|
3157
|
+
return evalSettled(events, predicate, session.elapsed());
|
|
2522
3158
|
case "allOf": {
|
|
2523
3159
|
const results = await Promise.all(predicate.predicates.map((p) => evaluatePredicate(session, p, since)));
|
|
2524
3160
|
const failed = results.find((r) => !r.pass);
|
|
@@ -2544,6 +3180,10 @@ async function evaluatePredicate(session, predicate, since = 0) {
|
|
|
2544
3180
|
function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
2545
3181
|
return new Promise((resolve) => {
|
|
2546
3182
|
let done = false;
|
|
3183
|
+
const failed = (error) => ({
|
|
3184
|
+
pass: false,
|
|
3185
|
+
failureReason: error instanceof Error ? error.message : String(error)
|
|
3186
|
+
});
|
|
2547
3187
|
const finish = (result) => {
|
|
2548
3188
|
if (done)
|
|
2549
3189
|
return;
|
|
@@ -2557,6 +3197,8 @@ function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
2557
3197
|
void evaluatePredicate(session, predicate, since).then((r) => {
|
|
2558
3198
|
if (r.pass)
|
|
2559
3199
|
finish(r);
|
|
3200
|
+
}).catch((error) => {
|
|
3201
|
+
finish(failed(error));
|
|
2560
3202
|
});
|
|
2561
3203
|
};
|
|
2562
3204
|
const unsub = session.onEvent(() => {
|
|
@@ -2570,141 +3212,30 @@ function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
2570
3212
|
evidence: r.evidence,
|
|
2571
3213
|
failureReason: r.failureReason ?? "timed out waiting for predicate"
|
|
2572
3214
|
});
|
|
3215
|
+
}).catch((error) => {
|
|
3216
|
+
finish(failed(error));
|
|
2573
3217
|
});
|
|
2574
3218
|
}, timeoutMs);
|
|
2575
3219
|
check();
|
|
2576
3220
|
});
|
|
2577
3221
|
}
|
|
2578
3222
|
|
|
2579
|
-
// ../server/dist/flows/replay.js
|
|
2580
|
-
function asString2(value) {
|
|
2581
|
-
return typeof value === "string" ? value : void 0;
|
|
2582
|
-
}
|
|
2583
|
-
function asRecord2(value) {
|
|
2584
|
-
return typeof value === "object" && value !== null ? value : {};
|
|
2585
|
-
}
|
|
2586
|
-
function compileActStep(args, res) {
|
|
2587
|
-
const testid = asString2(asRecord2(res)["testid"]);
|
|
2588
|
-
const action = asString2(args["action"]) ?? "";
|
|
2589
|
-
const actArgs = asRecord2(args["args"]);
|
|
2590
|
-
if (testid !== void 0) {
|
|
2591
|
-
return {
|
|
2592
|
-
tool: IrisTool.ACT,
|
|
2593
|
-
stable: true,
|
|
2594
|
-
args: { by: QueryBy.TESTID, value: testid, action, args: actArgs }
|
|
2595
|
-
};
|
|
2596
|
-
}
|
|
2597
|
-
return {
|
|
2598
|
-
tool: IrisTool.ACT,
|
|
2599
|
-
stable: false,
|
|
2600
|
-
args: { ref: asString2(args["ref"]) ?? "", action, args: actArgs }
|
|
2601
|
-
};
|
|
2602
|
-
}
|
|
2603
|
-
function compileSequenceStep(args, res) {
|
|
2604
|
-
const inputSteps = Array.isArray(args["steps"]) ? args["steps"] : [];
|
|
2605
|
-
const resolved = Array.isArray(asRecord2(res)["steps"]) ? asRecord2(res)["steps"] : [];
|
|
2606
|
-
let stable = inputSteps.length > 0;
|
|
2607
|
-
const subSteps = inputSteps.map((raw, i) => {
|
|
2608
|
-
const step = asRecord2(raw);
|
|
2609
|
-
const action = asString2(step["action"]) ?? "";
|
|
2610
|
-
const stepArgs = asRecord2(step["args"]);
|
|
2611
|
-
const testid = asString2(asRecord2(resolved[i])["testid"]);
|
|
2612
|
-
if (testid !== void 0) {
|
|
2613
|
-
return { by: QueryBy.TESTID, value: testid, action, args: stepArgs };
|
|
2614
|
-
}
|
|
2615
|
-
stable = false;
|
|
2616
|
-
return { ref: asString2(step["ref"]) ?? "", action, args: stepArgs };
|
|
2617
|
-
});
|
|
2618
|
-
return { tool: IrisTool.ACT_SEQUENCE, stable, args: { steps: subSteps } };
|
|
2619
|
-
}
|
|
2620
|
-
async function resolveRef(session, step) {
|
|
2621
|
-
const by = asString2(step.by);
|
|
2622
|
-
const value = asString2(step.value);
|
|
2623
|
-
if (by === QueryBy.TESTID && value !== void 0) {
|
|
2624
|
-
const result = await session.command(IrisCommand.QUERY, { by, value });
|
|
2625
|
-
if (!result.ok)
|
|
2626
|
-
throw new Error(result.error ?? "query failed");
|
|
2627
|
-
const elements = Array.isArray(asRecord2(result.result)["elements"]) ? asRecord2(result.result)["elements"] : [];
|
|
2628
|
-
const ref2 = asString2(asRecord2(elements[0])["ref"]);
|
|
2629
|
-
if (ref2 === void 0)
|
|
2630
|
-
throw new Error(`testid '${value}' did not resolve in current page`);
|
|
2631
|
-
return elements.length > 1 ? { ref: ref2, note: `ambiguous testid '${value}', used first match` } : { ref: ref2 };
|
|
2632
|
-
}
|
|
2633
|
-
const ref = asString2(step.ref);
|
|
2634
|
-
if (ref === void 0 || ref.length === 0)
|
|
2635
|
-
throw new Error("step has no testid or ref to resolve");
|
|
2636
|
-
return { ref, note: "replayed by stale ref (not portable across sessions)" };
|
|
2637
|
-
}
|
|
2638
|
-
async function replayProgram(session, program) {
|
|
2639
|
-
const results = [];
|
|
2640
|
-
for (const step of program.steps) {
|
|
2641
|
-
try {
|
|
2642
|
-
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
2643
|
-
const subs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
2644
|
-
const notes = [];
|
|
2645
|
-
const liveSteps = [];
|
|
2646
|
-
for (const raw of subs) {
|
|
2647
|
-
const sub = asRecord2(raw);
|
|
2648
|
-
const { ref, note } = await resolveRef(session, sub);
|
|
2649
|
-
if (note !== void 0)
|
|
2650
|
-
notes.push(note);
|
|
2651
|
-
liveSteps.push({
|
|
2652
|
-
ref,
|
|
2653
|
-
action: asString2(sub["action"]) ?? "",
|
|
2654
|
-
args: asRecord2(sub["args"])
|
|
2655
|
-
});
|
|
2656
|
-
}
|
|
2657
|
-
const r = await session.command(IrisCommand.ACT_SEQUENCE, { steps: liveSteps });
|
|
2658
|
-
results.push(buildResult(step.tool, r.ok, r.error, notes));
|
|
2659
|
-
if (!r.ok)
|
|
2660
|
-
break;
|
|
2661
|
-
} else {
|
|
2662
|
-
const { ref, note } = await resolveRef(session, step.args);
|
|
2663
|
-
const r = await session.command(IrisCommand.ACT, {
|
|
2664
|
-
ref,
|
|
2665
|
-
action: asString2(step.args["action"]) ?? "",
|
|
2666
|
-
args: asRecord2(step.args["args"])
|
|
2667
|
-
});
|
|
2668
|
-
results.push(buildResult(step.tool, r.ok, r.error, note !== void 0 ? [note] : []));
|
|
2669
|
-
if (!r.ok)
|
|
2670
|
-
break;
|
|
2671
|
-
}
|
|
2672
|
-
} catch (e) {
|
|
2673
|
-
results.push({
|
|
2674
|
-
tool: step.tool,
|
|
2675
|
-
ok: false,
|
|
2676
|
-
error: e instanceof Error ? e.message : String(e)
|
|
2677
|
-
});
|
|
2678
|
-
break;
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
return results;
|
|
2682
|
-
}
|
|
2683
|
-
function buildResult(tool, ok, error, notes) {
|
|
2684
|
-
const base = { tool, ok };
|
|
2685
|
-
if (!ok)
|
|
2686
|
-
base.error = error ?? "command failed";
|
|
2687
|
-
if (notes.length > 0)
|
|
2688
|
-
base.note = notes.join("; ");
|
|
2689
|
-
return base;
|
|
2690
|
-
}
|
|
2691
|
-
|
|
2692
3223
|
// ../server/dist/events/event-filters.js
|
|
2693
|
-
function
|
|
3224
|
+
function asString4(value) {
|
|
2694
3225
|
return typeof value === "string" ? value : void 0;
|
|
2695
3226
|
}
|
|
2696
|
-
function
|
|
3227
|
+
function asNumber2(value) {
|
|
2697
3228
|
return typeof value === "number" ? value : void 0;
|
|
2698
3229
|
}
|
|
2699
3230
|
function matchNet(e, method, urlContains, status) {
|
|
2700
3231
|
const d = e.data;
|
|
2701
|
-
if (method !== void 0 &&
|
|
3232
|
+
if (method !== void 0 && asString4(d["method"])?.toUpperCase() !== method.toUpperCase()) {
|
|
2702
3233
|
return false;
|
|
2703
3234
|
}
|
|
2704
|
-
if (urlContains !== void 0 && !(
|
|
3235
|
+
if (urlContains !== void 0 && !(asString4(d["url"]) ?? "").includes(urlContains)) {
|
|
2705
3236
|
return false;
|
|
2706
3237
|
}
|
|
2707
|
-
if (status !== void 0 &&
|
|
3238
|
+
if (status !== void 0 && asNumber2(d["status"]) !== status)
|
|
2708
3239
|
return false;
|
|
2709
3240
|
return true;
|
|
2710
3241
|
}
|
|
@@ -2721,8 +3252,8 @@ function matchConsole(e, level) {
|
|
|
2721
3252
|
var HINT_SAMPLE_MAX = 5;
|
|
2722
3253
|
function netEmptyHint(allNet) {
|
|
2723
3254
|
const present = allNet.slice(-HINT_SAMPLE_MAX).reverse().map((e) => {
|
|
2724
|
-
const status =
|
|
2725
|
-
const base = { method:
|
|
3255
|
+
const status = asNumber2(e.data["status"]);
|
|
3256
|
+
const base = { method: asString4(e.data["method"]) ?? "", url: asString4(e.data["url"]) ?? "" };
|
|
2726
3257
|
return status === void 0 ? base : { ...base, status };
|
|
2727
3258
|
});
|
|
2728
3259
|
return { totalInWindow: allNet.length, present };
|
|
@@ -2753,6 +3284,8 @@ function refuseIfThrottled(session, refuse) {
|
|
|
2753
3284
|
}
|
|
2754
3285
|
|
|
2755
3286
|
// ../server/dist/session/output-budget.js
|
|
3287
|
+
var LARGE_TIMELINE_EVENTS = 80;
|
|
3288
|
+
var LARGE_TIMELINE_BYTES = 8e3;
|
|
2756
3289
|
function applyEventBudget(events, maxEvents) {
|
|
2757
3290
|
if (maxEvents === void 0 || maxEvents < 0 || events.length <= maxEvents) {
|
|
2758
3291
|
return { events, droppedOldest: 0 };
|
|
@@ -2763,8 +3296,92 @@ function applyEventBudget(events, maxEvents) {
|
|
|
2763
3296
|
};
|
|
2764
3297
|
}
|
|
2765
3298
|
function costHint(payload, events, droppedOldest = 0) {
|
|
2766
|
-
const
|
|
2767
|
-
|
|
3299
|
+
const json = JSON.stringify(payload) ?? "";
|
|
3300
|
+
const bytes = json.length;
|
|
3301
|
+
const base = droppedOldest > 0 ? { events, bytes, droppedOldest } : { events, bytes };
|
|
3302
|
+
if (events >= LARGE_TIMELINE_EVENTS || bytes >= LARGE_TIMELINE_BYTES) {
|
|
3303
|
+
base.recommendation = `large timeline (${String(events)} events, ~${String(estimateTokens(json))} tokens) \u2014 pass filters:[...] (e.g. ["signal","net"]) or max_events to scope your next call and cut tokens`;
|
|
3304
|
+
}
|
|
3305
|
+
return base;
|
|
3306
|
+
}
|
|
3307
|
+
var CHARS_PER_TOKEN = 4;
|
|
3308
|
+
function estimateTokens(text) {
|
|
3309
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
3310
|
+
}
|
|
3311
|
+
function sizeCost(payload) {
|
|
3312
|
+
const json = JSON.stringify(payload) ?? "";
|
|
3313
|
+
return { bytes: json.length, tokens: estimateTokens(json) };
|
|
3314
|
+
}
|
|
3315
|
+
function withSizeCost(result) {
|
|
3316
|
+
if (typeof result !== "object" || result === null)
|
|
3317
|
+
return result;
|
|
3318
|
+
return { ...result, cost: sizeCost(result) };
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
// ../server/dist/tools/snapshot-delta.js
|
|
3322
|
+
var SnapshotDeltaMode = {
|
|
3323
|
+
FULL: "full",
|
|
3324
|
+
DELTA: "delta",
|
|
3325
|
+
UNCHANGED: "unchanged"
|
|
3326
|
+
};
|
|
3327
|
+
function snapshotDelta(prevTree, nextTree) {
|
|
3328
|
+
if (prevTree === void 0)
|
|
3329
|
+
return { mode: SnapshotDeltaMode.FULL };
|
|
3330
|
+
const { added, removed } = diffLines(normalizeLines(prevTree), normalizeLines(nextTree));
|
|
3331
|
+
if (added.length === 0 && removed.length === 0)
|
|
3332
|
+
return { mode: SnapshotDeltaMode.UNCHANGED };
|
|
3333
|
+
return {
|
|
3334
|
+
mode: SnapshotDeltaMode.DELTA,
|
|
3335
|
+
delta: { added, removed, addedCount: added.length, removedCount: removed.length }
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
var DEFAULT_MAX_ENTRIES = 50;
|
|
3339
|
+
var SnapshotCache = class {
|
|
3340
|
+
#map = /* @__PURE__ */ new Map();
|
|
3341
|
+
#max;
|
|
3342
|
+
constructor(max = DEFAULT_MAX_ENTRIES) {
|
|
3343
|
+
this.#max = max;
|
|
3344
|
+
}
|
|
3345
|
+
/** Last tree for this key IF the route still matches; undefined when absent or route changed. */
|
|
3346
|
+
recall(key, route) {
|
|
3347
|
+
const entry = this.#map.get(key);
|
|
3348
|
+
return entry !== void 0 && entry.route === route ? entry.tree : void 0;
|
|
3349
|
+
}
|
|
3350
|
+
remember(key, route, tree) {
|
|
3351
|
+
if (this.#map.size >= this.#max && !this.#map.has(key)) {
|
|
3352
|
+
const oldest = this.#map.keys().next().value;
|
|
3353
|
+
if (oldest !== void 0)
|
|
3354
|
+
this.#map.delete(oldest);
|
|
3355
|
+
}
|
|
3356
|
+
this.#map.set(key, { route, tree });
|
|
3357
|
+
}
|
|
3358
|
+
};
|
|
3359
|
+
function snapshotCacheKey(sessionId, scope, mode) {
|
|
3360
|
+
return `${sessionId}\0${scope}\0${mode}`;
|
|
3361
|
+
}
|
|
3362
|
+
function applySnapshotDelta(raw, opts, cache) {
|
|
3363
|
+
if (typeof raw !== "object" || raw === null)
|
|
3364
|
+
return raw;
|
|
3365
|
+
const r = raw;
|
|
3366
|
+
if (typeof r["tree"] !== "string")
|
|
3367
|
+
return raw;
|
|
3368
|
+
const tree = r["tree"];
|
|
3369
|
+
const status = typeof r["status"] === "object" && r["status"] !== null ? r["status"] : {};
|
|
3370
|
+
const route = typeof status["route"] === "string" ? status["route"] : "";
|
|
3371
|
+
const key = snapshotCacheKey(opts.sessionId, opts.scope, opts.mode);
|
|
3372
|
+
if (!opts.diff) {
|
|
3373
|
+
cache.remember(key, route, tree);
|
|
3374
|
+
return raw;
|
|
3375
|
+
}
|
|
3376
|
+
const prev = cache.recall(key, route);
|
|
3377
|
+
cache.remember(key, route, tree);
|
|
3378
|
+
const decision = snapshotDelta(prev, tree);
|
|
3379
|
+
if (decision.mode === SnapshotDeltaMode.FULL)
|
|
3380
|
+
return raw;
|
|
3381
|
+
if (decision.mode === SnapshotDeltaMode.UNCHANGED) {
|
|
3382
|
+
return { mode: SnapshotDeltaMode.UNCHANGED, status: r["status"] };
|
|
3383
|
+
}
|
|
3384
|
+
return { mode: SnapshotDeltaMode.DELTA, delta: decision.delta, status: r["status"] };
|
|
2768
3385
|
}
|
|
2769
3386
|
|
|
2770
3387
|
// ../server/dist/session/state-select.js
|
|
@@ -2815,25 +3432,55 @@ function capDepth(value, maxDepth) {
|
|
|
2815
3432
|
return value;
|
|
2816
3433
|
}
|
|
2817
3434
|
|
|
2818
|
-
// ../server/dist/tools/
|
|
2819
|
-
function
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
3435
|
+
// ../server/dist/tools/query-paginate.js
|
|
3436
|
+
function paginateQueryResult(result, limit, countOnly) {
|
|
3437
|
+
if (typeof result !== "object" || result === null)
|
|
3438
|
+
return result;
|
|
3439
|
+
const record = result;
|
|
3440
|
+
const elements = record["elements"];
|
|
3441
|
+
if (!Array.isArray(elements))
|
|
3442
|
+
return result;
|
|
3443
|
+
const total = elements.length;
|
|
3444
|
+
if (countOnly) {
|
|
3445
|
+
const { elements: _dropped, ...rest } = record;
|
|
3446
|
+
return { ...rest, count: total };
|
|
3447
|
+
}
|
|
3448
|
+
if (limit !== void 0 && limit >= 0 && total > limit) {
|
|
3449
|
+
return { ...record, elements: elements.slice(0, limit), total, truncated: true };
|
|
3450
|
+
}
|
|
3451
|
+
return result;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
// ../server/dist/tools/assert-grade.js
|
|
3455
|
+
var PRESENCE_ONLY_ADVICE = "This predicate only checks element/text presence, not an observable consequence. A locator healed to the wrong element (or a stale render) can satisfy it while the feature is broken. Prefer a { signal } or { net } assertion \u2014 or allOf it with one \u2014 so green means the feature actually worked.";
|
|
3456
|
+
function walk(predicate) {
|
|
3457
|
+
switch (predicate.kind) {
|
|
3458
|
+
case "signal":
|
|
3459
|
+
case "net":
|
|
3460
|
+
return { consequence: true, presence: false };
|
|
3461
|
+
case "element":
|
|
3462
|
+
case "text":
|
|
3463
|
+
return { consequence: false, presence: true };
|
|
3464
|
+
case "route":
|
|
3465
|
+
case "console":
|
|
3466
|
+
case "animation":
|
|
3467
|
+
case "settled":
|
|
3468
|
+
return { consequence: false, presence: false };
|
|
3469
|
+
case "allOf":
|
|
3470
|
+
case "anyOf": {
|
|
3471
|
+
const subs = predicate.predicates.map(walk);
|
|
3472
|
+
return {
|
|
3473
|
+
consequence: subs.some((s) => s.consequence),
|
|
3474
|
+
presence: subs.some((s) => s.presence)
|
|
3475
|
+
};
|
|
2825
3476
|
}
|
|
3477
|
+
case "not":
|
|
3478
|
+
return walk(predicate.predicate);
|
|
2826
3479
|
}
|
|
2827
|
-
return items;
|
|
2828
|
-
}
|
|
2829
|
-
function asString4(value) {
|
|
2830
|
-
return typeof value === "string" ? value : void 0;
|
|
2831
|
-
}
|
|
2832
|
-
function asNumber2(value) {
|
|
2833
|
-
return typeof value === "number" ? value : void 0;
|
|
2834
3480
|
}
|
|
2835
|
-
function
|
|
2836
|
-
|
|
3481
|
+
function isPresenceOnlyAssertion(predicate) {
|
|
3482
|
+
const kinds = walk(predicate);
|
|
3483
|
+
return kinds.presence && !kinds.consequence;
|
|
2837
3484
|
}
|
|
2838
3485
|
|
|
2839
3486
|
// ../server/dist/tools/contract-tools.js
|
|
@@ -2867,7 +3514,7 @@ var CONTRACT_TOOLS = [
|
|
|
2867
3514
|
throw new Error(r.reason === ContractReadError.MISSING ? "no .iris/contract.json on disk \u2014 run iris_contract_save first (or omit fromDisk to read the live session)" : ".iris/contract.json is malformed \u2014 fix or regenerate it with iris_contract_save");
|
|
2868
3515
|
return { ...r.capabilities, source: "disk", generatedAt: r.generatedAt };
|
|
2869
3516
|
}
|
|
2870
|
-
const caps = await commandOrThrow(deps,
|
|
3517
|
+
const caps = await commandOrThrow(deps, asString(args["sessionId"]), IrisCommand.CAPABILITIES, {});
|
|
2871
3518
|
return { ...caps, source: "live" };
|
|
2872
3519
|
}
|
|
2873
3520
|
},
|
|
@@ -2882,7 +3529,7 @@ var CONTRACT_TOOLS = [
|
|
|
2882
3529
|
signalCount: z4.number()
|
|
2883
3530
|
},
|
|
2884
3531
|
handler: async (deps, args) => {
|
|
2885
|
-
const res = await commandOrThrow(deps,
|
|
3532
|
+
const res = await commandOrThrow(deps, asString(args["sessionId"]), IrisCommand.CAPABILITIES, {});
|
|
2886
3533
|
const caps = CapabilitiesSchema.parse(res);
|
|
2887
3534
|
await writeContract(deps.fs, deps.irisRoot, caps, deps.now);
|
|
2888
3535
|
return {
|
|
@@ -2895,253 +3542,399 @@ var CONTRACT_TOOLS = [
|
|
|
2895
3542
|
}
|
|
2896
3543
|
];
|
|
2897
3544
|
|
|
2898
|
-
// ../server/dist/
|
|
3545
|
+
// ../server/dist/domain/domain-tools.js
|
|
2899
3546
|
import { z as z5 } from "zod";
|
|
2900
|
-
|
|
2901
|
-
|
|
3547
|
+
|
|
3548
|
+
// ../server/dist/flows/flow-classify.js
|
|
3549
|
+
var FlowAssertionGrade = {
|
|
3550
|
+
/** At least one step (or the success end-condition) asserts a signal/network consequence. */
|
|
3551
|
+
ASSERTED: "asserted",
|
|
3552
|
+
/** Only element-presence checks — a healed-but-wrong locator could still pass. */
|
|
3553
|
+
PRESENCE_ONLY: "presence-only",
|
|
3554
|
+
/** Performs actions but asserts nothing observable — passes even if the feature is broken. */
|
|
3555
|
+
ASSERTION_FREE: "assertion-free"
|
|
2902
3556
|
};
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
throw new Error(result.error ?? `command '${name}' failed`);
|
|
2908
|
-
return result.result;
|
|
3557
|
+
var ASSERTION_FREE_WARNING = "This flow performs actions but asserts no observable consequence \u2014 it will pass even if the feature is broken. Add a consequence assertion with iris_annotate (assert-signal / assert-net) or a success-state.";
|
|
3558
|
+
var PRESENCE_ONLY_WARNING = "This flow only checks element presence, not an observable consequence (signal/network). A locator healed to the wrong element can still pass it. Add a consequence assertion (assert-signal / assert-net / success-state).";
|
|
3559
|
+
function expectIsConsequence(e) {
|
|
3560
|
+
return e !== void 0 && (e.signal !== void 0 || e.net !== void 0);
|
|
2909
3561
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
ok: z5.boolean(),
|
|
2920
|
-
url: z5.string().optional(),
|
|
2921
|
-
reason: z5.string().optional()
|
|
2922
|
-
},
|
|
2923
|
-
handler: async (deps, args) => {
|
|
2924
|
-
const url = asString4(args["url"]);
|
|
2925
|
-
if (url === void 0 || url.length === 0)
|
|
2926
|
-
return { ok: false, reason: "url required" };
|
|
2927
|
-
await commandOrThrow2(deps, asString4(args["sessionId"]), IrisCommand.NAVIGATE, { url });
|
|
2928
|
-
return { ok: true, url };
|
|
2929
|
-
}
|
|
2930
|
-
},
|
|
2931
|
-
{
|
|
2932
|
-
name: IrisTool.REFRESH,
|
|
2933
|
-
description: "Reload the connected browser tab. Pass { hard: true } to bypass the browser cache (equivalent to Cmd+Shift+R). The SDK reconnects automatically after the reload.",
|
|
2934
|
-
inputSchema: {
|
|
2935
|
-
hard: z5.boolean().optional().describe("Set true to bypass the browser cache. Default: false (normal reload)."),
|
|
2936
|
-
...sessionIdShape2
|
|
2937
|
-
},
|
|
2938
|
-
outputSchema: {
|
|
2939
|
-
ok: z5.boolean()
|
|
2940
|
-
},
|
|
2941
|
-
handler: async (deps, args) => {
|
|
2942
|
-
await commandOrThrow2(deps, asString4(args["sessionId"]), IrisCommand.REFRESH, {
|
|
2943
|
-
hard: args["hard"] === true
|
|
2944
|
-
});
|
|
2945
|
-
return { ok: true };
|
|
2946
|
-
}
|
|
3562
|
+
function expectIsWeak(e) {
|
|
3563
|
+
return e !== void 0 && e.element !== void 0 && e.signal === void 0 && e.net === void 0;
|
|
3564
|
+
}
|
|
3565
|
+
function flattenSteps(steps) {
|
|
3566
|
+
const out = [];
|
|
3567
|
+
for (const s of steps) {
|
|
3568
|
+
out.push(s);
|
|
3569
|
+
if (s.steps !== void 0)
|
|
3570
|
+
out.push(...flattenSteps(s.steps));
|
|
2947
3571
|
}
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
3572
|
+
return out;
|
|
3573
|
+
}
|
|
3574
|
+
function classifyFlowAssertions(flow) {
|
|
3575
|
+
const all = flattenSteps(flow.steps);
|
|
3576
|
+
let consequenceSteps = 0;
|
|
3577
|
+
let weakSteps = 0;
|
|
3578
|
+
for (const s of all) {
|
|
3579
|
+
if (expectIsConsequence(s.expect))
|
|
3580
|
+
consequenceSteps++;
|
|
3581
|
+
else if (expectIsWeak(s.expect))
|
|
3582
|
+
weakSteps++;
|
|
3583
|
+
}
|
|
3584
|
+
const successIsConsequence = expectIsConsequence(flow.success);
|
|
3585
|
+
const successIsWeak = expectIsWeak(flow.success);
|
|
3586
|
+
const hasConsequenceAssertion = consequenceSteps > 0 || successIsConsequence;
|
|
3587
|
+
const hasAnyAssertion = hasConsequenceAssertion || weakSteps > 0 || successIsWeak;
|
|
3588
|
+
let grade;
|
|
3589
|
+
let warning;
|
|
3590
|
+
if (hasConsequenceAssertion) {
|
|
3591
|
+
grade = FlowAssertionGrade.ASSERTED;
|
|
3592
|
+
} else if (hasAnyAssertion) {
|
|
3593
|
+
grade = FlowAssertionGrade.PRESENCE_ONLY;
|
|
3594
|
+
warning = PRESENCE_ONLY_WARNING;
|
|
3595
|
+
} else {
|
|
3596
|
+
grade = FlowAssertionGrade.ASSERTION_FREE;
|
|
3597
|
+
warning = ASSERTION_FREE_WARNING;
|
|
3598
|
+
}
|
|
3599
|
+
return {
|
|
3600
|
+
grade,
|
|
3601
|
+
hasConsequenceAssertion,
|
|
3602
|
+
totalSteps: all.length,
|
|
3603
|
+
consequenceSteps,
|
|
3604
|
+
weakSteps,
|
|
3605
|
+
successIsConsequence,
|
|
3606
|
+
...warning !== void 0 ? { warning } : {}
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
2952
3609
|
|
|
2953
|
-
// ../server/dist/flows/flow-
|
|
2954
|
-
function
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
3610
|
+
// ../server/dist/flows/flow-success.js
|
|
3611
|
+
function dynamicTestids(flow) {
|
|
3612
|
+
return new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
3613
|
+
}
|
|
3614
|
+
function successLabel(success) {
|
|
3615
|
+
if (success.signal !== void 0)
|
|
3616
|
+
return success.signal;
|
|
3617
|
+
if (success.net !== void 0)
|
|
3618
|
+
return success.net.urlContains ?? success.net.method ?? "net";
|
|
3619
|
+
return success.element?.testid ?? success.element?.name ?? success.element?.role ?? "success";
|
|
3620
|
+
}
|
|
3621
|
+
function successToPredicate(success, dynamic) {
|
|
3622
|
+
const parts = [];
|
|
3623
|
+
if (success.signal !== void 0) {
|
|
3624
|
+
parts.push(success.signalData !== void 0 ? { kind: "signal", name: success.signal, dataMatches: success.signalData } : { kind: "signal", name: success.signal });
|
|
3625
|
+
}
|
|
3626
|
+
if (success.net !== void 0) {
|
|
3627
|
+
const net = { kind: "net" };
|
|
3628
|
+
if (success.net.method !== void 0)
|
|
3629
|
+
net.method = success.net.method;
|
|
3630
|
+
if (success.net.urlContains !== void 0)
|
|
3631
|
+
net.urlContains = success.net.urlContains;
|
|
3632
|
+
if (success.net.status !== void 0)
|
|
3633
|
+
net.status = success.net.status;
|
|
3634
|
+
parts.push(net);
|
|
3635
|
+
}
|
|
3636
|
+
const element = success.element;
|
|
3637
|
+
if (element !== void 0) {
|
|
3638
|
+
const testid = element.testid;
|
|
3639
|
+
if (testid === void 0 || !dynamic.has(testid)) {
|
|
3640
|
+
const query = {};
|
|
3641
|
+
if (testid !== void 0)
|
|
3642
|
+
query["testid"] = testid;
|
|
3643
|
+
if (element.role !== void 0)
|
|
3644
|
+
query["role"] = element.role;
|
|
3645
|
+
if (element.name !== void 0)
|
|
3646
|
+
query["name"] = element.name;
|
|
3647
|
+
if (Object.keys(query).length > 0)
|
|
3648
|
+
parts.push({ kind: "element", query });
|
|
2968
3649
|
}
|
|
2969
|
-
for (let j = 0; j < cols; j++)
|
|
2970
|
-
prev[j] = curr[j] ?? 0;
|
|
2971
3650
|
}
|
|
2972
|
-
|
|
3651
|
+
const [first] = parts;
|
|
3652
|
+
if (parts.length === 0)
|
|
3653
|
+
return void 0;
|
|
3654
|
+
if (parts.length === 1 && first !== void 0)
|
|
3655
|
+
return first;
|
|
3656
|
+
return { kind: "allOf", predicates: parts };
|
|
2973
3657
|
}
|
|
2974
|
-
function
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3658
|
+
async function assertSuccess(session, success, dynamic, waitForSignal, timeoutMs, since = 0) {
|
|
3659
|
+
if (success === void 0)
|
|
3660
|
+
return { pass: true };
|
|
3661
|
+
const predicate = successToPredicate(success, dynamic);
|
|
3662
|
+
if (predicate === void 0)
|
|
3663
|
+
return { pass: true };
|
|
3664
|
+
return waitForSignal(session, predicate, timeoutMs, since);
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
// ../server/dist/domain/flow-risk.js
|
|
3668
|
+
var RiskLevel = {
|
|
3669
|
+
HIGH: "high",
|
|
3670
|
+
MEDIUM: "medium",
|
|
3671
|
+
LOW: "low",
|
|
3672
|
+
UNKNOWN: "unknown"
|
|
3673
|
+
};
|
|
3674
|
+
var RANK = {
|
|
3675
|
+
[RiskLevel.HIGH]: 3,
|
|
3676
|
+
[RiskLevel.MEDIUM]: 2,
|
|
3677
|
+
[RiskLevel.UNKNOWN]: 1,
|
|
3678
|
+
[RiskLevel.LOW]: 0
|
|
3679
|
+
};
|
|
3680
|
+
function latestRun(name, runs) {
|
|
3681
|
+
let best;
|
|
3682
|
+
for (const run of runs) {
|
|
3683
|
+
if (run.name === name && (best === void 0 || run.at > best.at))
|
|
3684
|
+
best = run;
|
|
2983
3685
|
}
|
|
2984
3686
|
return best;
|
|
2985
3687
|
}
|
|
2986
|
-
function
|
|
2987
|
-
if (
|
|
2988
|
-
return {
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
3688
|
+
function runRisk(run) {
|
|
3689
|
+
if (run === void 0)
|
|
3690
|
+
return { level: RiskLevel.UNKNOWN, reason: "never run" };
|
|
3691
|
+
if (run.status === RunStatus.ERROR || run.status === RunStatus.FAIL) {
|
|
3692
|
+
return { level: RiskLevel.HIGH, reason: "last run failed" };
|
|
3693
|
+
}
|
|
3694
|
+
if (run.status === RunStatus.DRIFT)
|
|
3695
|
+
return { level: RiskLevel.HIGH, reason: "last run drifted" };
|
|
3696
|
+
const errors = (run.evidence?.consoleErrors ?? 0) + (run.evidence?.networkErrors ?? 0);
|
|
3697
|
+
if (errors > 0) {
|
|
2996
3698
|
return {
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
route: asString4(hint["route"]) ?? "",
|
|
3000
|
-
presentTestids: present,
|
|
3001
|
-
presentRegions: [],
|
|
3002
|
-
knownEmptyState: hint["knownEmptyState"] === true
|
|
3003
|
-
}
|
|
3699
|
+
level: RiskLevel.MEDIUM,
|
|
3700
|
+
reason: `last run passed but logged ${String(errors)} error(s)`
|
|
3004
3701
|
};
|
|
3005
3702
|
}
|
|
3006
|
-
return {
|
|
3007
|
-
}
|
|
3008
|
-
function testidDrift(value, hint) {
|
|
3009
|
-
return {
|
|
3010
|
-
reasonKind: DriftReason.TESTID_NOT_FOUND,
|
|
3011
|
-
reason: `testid "${value}" not found`,
|
|
3012
|
-
anchor: value,
|
|
3013
|
-
nearest: nearestTestid(value, hint?.presentTestids ?? [])
|
|
3014
|
-
};
|
|
3015
|
-
}
|
|
3016
|
-
function anchorLabel(anchor) {
|
|
3017
|
-
if (anchor.kind === AnchorKind.TESTID)
|
|
3018
|
-
return anchor.value;
|
|
3019
|
-
if (anchor.kind === AnchorKind.SIGNAL)
|
|
3020
|
-
return anchor.name;
|
|
3021
|
-
return anchor.name ?? anchor.role;
|
|
3703
|
+
return { level: RiskLevel.LOW, reason: "last run passed clean" };
|
|
3022
3704
|
}
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
const { refs, hint } = readQuery(queryResult);
|
|
3026
|
-
if (refs.length === 0) {
|
|
3705
|
+
function gradeRisk(grade) {
|
|
3706
|
+
if (grade === FlowAssertionGrade.ASSERTION_FREE) {
|
|
3027
3707
|
return {
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
anchor: value,
|
|
3031
|
-
ok: false,
|
|
3032
|
-
drift: testidDrift(value, hint)
|
|
3708
|
+
level: RiskLevel.MEDIUM,
|
|
3709
|
+
reason: "asserts no consequence \u2014 a green run proves little"
|
|
3033
3710
|
};
|
|
3034
3711
|
}
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
const act = await session.command(IrisCommand.ACT, {
|
|
3038
|
-
ref,
|
|
3039
|
-
action: step.action ?? "",
|
|
3040
|
-
args: step.args ?? {}
|
|
3041
|
-
});
|
|
3042
|
-
const result = { step: index, tool: step.tool, anchor: value, ok: act.ok };
|
|
3043
|
-
if (!act.ok) {
|
|
3044
|
-
result.error = act.error ?? "command failed";
|
|
3045
|
-
if (note !== void 0)
|
|
3046
|
-
result.note = note;
|
|
3047
|
-
return result;
|
|
3048
|
-
}
|
|
3049
|
-
const expectTestid = step.expect?.element?.testid;
|
|
3050
|
-
if (expectTestid !== void 0 && !dynamic.has(expectTestid)) {
|
|
3051
|
-
const expectQuery = await session.command(IrisCommand.QUERY, {
|
|
3052
|
-
by: QueryBy.TESTID,
|
|
3053
|
-
value: expectTestid
|
|
3054
|
-
});
|
|
3055
|
-
const expectRefs = readQuery(expectQuery);
|
|
3056
|
-
if (expectRefs.refs.length === 0) {
|
|
3057
|
-
return {
|
|
3058
|
-
step: index,
|
|
3059
|
-
tool: step.tool,
|
|
3060
|
-
anchor: expectTestid,
|
|
3061
|
-
ok: false,
|
|
3062
|
-
drift: testidDrift(expectTestid, expectRefs.hint)
|
|
3063
|
-
};
|
|
3064
|
-
}
|
|
3712
|
+
if (grade === FlowAssertionGrade.PRESENCE_ONLY) {
|
|
3713
|
+
return { level: RiskLevel.LOW, reason: "presence-only assertion" };
|
|
3065
3714
|
}
|
|
3066
|
-
|
|
3067
|
-
result.note = note;
|
|
3068
|
-
return result;
|
|
3715
|
+
return { level: RiskLevel.LOW, reason: "asserts a consequence" };
|
|
3069
3716
|
}
|
|
3070
|
-
|
|
3071
|
-
const
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
return {
|
|
3075
|
-
step: index,
|
|
3076
|
-
tool: step.tool,
|
|
3077
|
-
anchor: name,
|
|
3078
|
-
ok: false,
|
|
3079
|
-
drift: {
|
|
3080
|
-
reasonKind: DriftReason.SIGNAL_NOT_OBSERVED,
|
|
3081
|
-
reason: `signal "${name}" not observed`,
|
|
3082
|
-
anchor: name,
|
|
3083
|
-
nearest: null
|
|
3084
|
-
}
|
|
3085
|
-
};
|
|
3717
|
+
function flowRisk(grade, run) {
|
|
3718
|
+
const r = runRisk(run);
|
|
3719
|
+
const g = gradeRisk(grade);
|
|
3720
|
+
const top = RANK[r.level] >= RANK[g.level] ? r : g;
|
|
3721
|
+
return run === void 0 ? { level: top.level, reason: top.reason } : { level: top.level, reason: top.reason, lastStatus: run.status };
|
|
3086
3722
|
}
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
const dynamic = new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
3090
|
-
let index = 0;
|
|
3091
|
-
for (const step of flow.steps) {
|
|
3092
|
-
const label = anchorLabel(step.anchor);
|
|
3093
|
-
let result;
|
|
3094
|
-
if (step.anchor.kind === AnchorKind.SIGNAL) {
|
|
3095
|
-
result = await runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs);
|
|
3096
|
-
} else {
|
|
3097
|
-
result = await runTestidStep(session, step, index, label, dynamic);
|
|
3098
|
-
}
|
|
3099
|
-
results.push(result);
|
|
3100
|
-
if (result.drift !== void 0 || !result.ok)
|
|
3101
|
-
break;
|
|
3102
|
-
index += 1;
|
|
3103
|
-
}
|
|
3104
|
-
return results;
|
|
3723
|
+
function rankByRisk(entries) {
|
|
3724
|
+
return [...entries].sort((a, b) => RANK[b.risk.level] - RANK[a.risk.level] || a.name.localeCompare(b.name)).map((e) => e.name);
|
|
3105
3725
|
}
|
|
3106
3726
|
|
|
3107
|
-
// ../server/dist/
|
|
3108
|
-
function
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3727
|
+
// ../server/dist/domain/domain-model.js
|
|
3728
|
+
function flatten(steps) {
|
|
3729
|
+
const out = [];
|
|
3730
|
+
for (const s of steps) {
|
|
3731
|
+
out.push(s);
|
|
3732
|
+
if (s.steps !== void 0)
|
|
3733
|
+
out.push(...flatten(s.steps));
|
|
3734
|
+
}
|
|
3735
|
+
return out;
|
|
3736
|
+
}
|
|
3737
|
+
function flowSignals(flow) {
|
|
3738
|
+
const set = /* @__PURE__ */ new Set();
|
|
3739
|
+
for (const step of flatten(flow.steps)) {
|
|
3740
|
+
if (step.anchor.kind === AnchorKind.SIGNAL)
|
|
3741
|
+
set.add(step.anchor.name);
|
|
3742
|
+
if (step.expect?.signal !== void 0)
|
|
3743
|
+
set.add(step.expect.signal);
|
|
3744
|
+
}
|
|
3745
|
+
if (flow.success?.signal !== void 0)
|
|
3746
|
+
set.add(flow.success.signal);
|
|
3747
|
+
return [...set];
|
|
3748
|
+
}
|
|
3749
|
+
function flowTestids(flow) {
|
|
3750
|
+
const set = /* @__PURE__ */ new Set();
|
|
3751
|
+
for (const step of flatten(flow.steps)) {
|
|
3752
|
+
if (step.anchor.kind === AnchorKind.TESTID)
|
|
3753
|
+
set.add(step.anchor.value);
|
|
3754
|
+
if (step.expect?.element?.testid !== void 0)
|
|
3755
|
+
set.add(step.expect.element.testid);
|
|
3756
|
+
}
|
|
3757
|
+
if (flow.success?.element?.testid !== void 0)
|
|
3758
|
+
set.add(flow.success.element.testid);
|
|
3759
|
+
return [...set];
|
|
3760
|
+
}
|
|
3761
|
+
var EMPTY_CONTRACT = { testids: [], signals: [], stores: [], flows: [] };
|
|
3762
|
+
function buildDomainModel(flows, contract, runs = []) {
|
|
3763
|
+
const caps = contract ?? EMPTY_CONTRACT;
|
|
3764
|
+
const hasHistory = runs.length > 0;
|
|
3765
|
+
const flowSummaries = flows.map((flow) => {
|
|
3766
|
+
const c = classifyFlowAssertions(flow);
|
|
3767
|
+
const summary = {
|
|
3768
|
+
name: flow.name,
|
|
3769
|
+
steps: c.totalSteps,
|
|
3770
|
+
grade: c.grade,
|
|
3771
|
+
asserts: c.hasConsequenceAssertion,
|
|
3772
|
+
signals: flowSignals(flow),
|
|
3773
|
+
testids: flowTestids(flow)
|
|
3774
|
+
};
|
|
3775
|
+
if (flow.success !== void 0)
|
|
3776
|
+
summary.mustHold = successLabel(flow.success);
|
|
3777
|
+
if (c.warning !== void 0)
|
|
3778
|
+
summary.warning = c.warning;
|
|
3779
|
+
if (hasHistory)
|
|
3780
|
+
summary.risk = flowRisk(c.grade, latestRun(flow.name, runs));
|
|
3781
|
+
return summary;
|
|
3782
|
+
});
|
|
3783
|
+
const testedSignals = new Set(flowSummaries.flatMap((f) => f.signals));
|
|
3784
|
+
const testedTestids = new Set(flowSummaries.flatMap((f) => f.testids));
|
|
3785
|
+
const coverage = {
|
|
3786
|
+
asserted: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.ASSERTED).length,
|
|
3787
|
+
presenceOnly: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.PRESENCE_ONLY).length,
|
|
3788
|
+
assertionFree: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.ASSERTION_FREE).length
|
|
3789
|
+
};
|
|
3790
|
+
const gaps = {
|
|
3791
|
+
unassertedFlows: flowSummaries.filter((f) => !f.asserts).map((f) => f.name),
|
|
3792
|
+
declaredUntestedSignals: caps.signals.filter((s) => !testedSignals.has(s)),
|
|
3793
|
+
declaredUntestedTestids: caps.testids.filter((t) => !testedTestids.has(t))
|
|
3794
|
+
};
|
|
3795
|
+
const riskRanked = hasHistory ? rankByRisk(flowSummaries.filter((f) => f.risk !== void 0).map((f) => ({ name: f.name, risk: f.risk }))) : [];
|
|
3796
|
+
const top = riskRanked[0];
|
|
3797
|
+
const topFlow = top === void 0 ? void 0 : flowSummaries.find((f) => f.name === top);
|
|
3798
|
+
const topRisk = topFlow?.risk !== void 0 && (topFlow.risk.level === RiskLevel.HIGH || topFlow.risk.level === RiskLevel.MEDIUM) ? { name: topFlow.name, reason: topFlow.risk.reason } : void 0;
|
|
3799
|
+
return {
|
|
3800
|
+
flowCount: flows.length,
|
|
3801
|
+
flows: flowSummaries,
|
|
3802
|
+
declared: { testids: caps.testids.length, signals: caps.signals, stores: caps.stores },
|
|
3803
|
+
coverage,
|
|
3804
|
+
gaps,
|
|
3805
|
+
riskRanked,
|
|
3806
|
+
summary: buildSummary(flows.length, coverage, gaps, topRisk)
|
|
3807
|
+
};
|
|
3120
3808
|
}
|
|
3121
|
-
function
|
|
3122
|
-
if (
|
|
3123
|
-
return
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
if (
|
|
3129
|
-
|
|
3130
|
-
|
|
3809
|
+
function buildSummary(flowCount, coverage, gaps, topRisk) {
|
|
3810
|
+
if (flowCount === 0) {
|
|
3811
|
+
return "No saved flows yet \u2014 record the critical journeys (iris_record_start) so the agent learns the app.";
|
|
3812
|
+
}
|
|
3813
|
+
const parts = [
|
|
3814
|
+
`${String(flowCount)} flow${flowCount === 1 ? "" : "s"}: ${String(coverage.asserted)} asserted, ${String(coverage.presenceOnly)} presence-only, ${String(coverage.assertionFree)} assertion-free`
|
|
3815
|
+
];
|
|
3816
|
+
if (topRisk !== void 0) {
|
|
3817
|
+
parts.push(`test first: ${topRisk.name} (${topRisk.reason})`);
|
|
3818
|
+
}
|
|
3819
|
+
if (gaps.declaredUntestedSignals.length > 0) {
|
|
3820
|
+
parts.push(`${String(gaps.declaredUntestedSignals.length)} declared signal(s) no flow asserts (${gaps.declaredUntestedSignals.join(", ")})`);
|
|
3821
|
+
}
|
|
3822
|
+
if (gaps.unassertedFlows.length > 0) {
|
|
3823
|
+
parts.push(`${String(gaps.unassertedFlows.length)} flow(s) assert no consequence`);
|
|
3824
|
+
}
|
|
3825
|
+
return parts.join(". ") + ".";
|
|
3131
3826
|
}
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3827
|
+
|
|
3828
|
+
// ../server/dist/domain/domain-tools.js
|
|
3829
|
+
var DOMAIN_TOOLS = [
|
|
3830
|
+
{
|
|
3831
|
+
name: IrisTool.DOMAIN,
|
|
3832
|
+
description: "Read the app domain model BEFORE testing: every saved flow with its assertion grade, the consequence that MUST hold for it (mustHold = what it actually tests), the anchors/signals it exercises, plus GAPS \u2014 declared signals/testids that NO flow asserts (untested intent), and flows that assert no observable consequence. Use this to decide what to test and where the real risk is, instead of crawling the whole app. Reads .iris/flows/ + .iris/contract.json (no browser needed).",
|
|
3833
|
+
inputSchema: {},
|
|
3834
|
+
outputSchema: {
|
|
3835
|
+
flowCount: z5.number(),
|
|
3836
|
+
flows: z5.array(z5.object({
|
|
3837
|
+
name: z5.string(),
|
|
3838
|
+
steps: z5.number(),
|
|
3839
|
+
grade: z5.string(),
|
|
3840
|
+
asserts: z5.boolean(),
|
|
3841
|
+
mustHold: z5.string().optional().describe("The success consequence that must hold for this flow (what it actually tests)."),
|
|
3842
|
+
warning: z5.string().optional(),
|
|
3843
|
+
signals: z5.array(z5.string()),
|
|
3844
|
+
testids: z5.array(z5.string())
|
|
3845
|
+
})),
|
|
3846
|
+
declared: z5.object({
|
|
3847
|
+
testids: z5.number(),
|
|
3848
|
+
signals: z5.array(z5.string()),
|
|
3849
|
+
stores: z5.array(z5.string())
|
|
3850
|
+
}),
|
|
3851
|
+
coverage: z5.object({
|
|
3852
|
+
asserted: z5.number(),
|
|
3853
|
+
presenceOnly: z5.number(),
|
|
3854
|
+
assertionFree: z5.number()
|
|
3855
|
+
}),
|
|
3856
|
+
gaps: z5.object({
|
|
3857
|
+
unassertedFlows: z5.array(z5.string()),
|
|
3858
|
+
declaredUntestedSignals: z5.array(z5.string()),
|
|
3859
|
+
declaredUntestedTestids: z5.array(z5.string())
|
|
3860
|
+
}),
|
|
3861
|
+
riskRanked: z5.array(z5.string()).describe("Flow names worst-risk first (run history + assertion quality). Test these first."),
|
|
3862
|
+
summary: z5.string()
|
|
3863
|
+
},
|
|
3864
|
+
handler: async (deps) => {
|
|
3865
|
+
const names = await deps.flows.list();
|
|
3866
|
+
const flows = [];
|
|
3867
|
+
for (const name of names) {
|
|
3868
|
+
const loaded = await deps.flows.load(name);
|
|
3869
|
+
if (loaded.ok)
|
|
3870
|
+
flows.push(loaded.value);
|
|
3871
|
+
}
|
|
3872
|
+
const contract = await readContract(deps.fs, deps.irisRoot);
|
|
3873
|
+
const project = await deps.project.read();
|
|
3874
|
+
const runs = project.ok ? project.file.runs : [];
|
|
3875
|
+
return buildDomainModel(flows, contract.ok ? contract.capabilities : null, runs);
|
|
3876
|
+
}
|
|
3140
3877
|
}
|
|
3141
|
-
|
|
3878
|
+
];
|
|
3879
|
+
|
|
3880
|
+
// ../server/dist/tools/browser-tools.js
|
|
3881
|
+
import { z as z6 } from "zod";
|
|
3882
|
+
var sessionIdShape2 = {
|
|
3883
|
+
sessionId: z6.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3884
|
+
};
|
|
3885
|
+
async function commandOrThrow2(deps, sessionId, name, args) {
|
|
3886
|
+
const session = deps.sessions.resolve(sessionId);
|
|
3887
|
+
const result = await session.command(name, args);
|
|
3888
|
+
if (!result.ok)
|
|
3889
|
+
throw new Error(result.error ?? `command '${name}' failed`);
|
|
3890
|
+
return result.result;
|
|
3142
3891
|
}
|
|
3892
|
+
var BROWSER_TOOLS = [
|
|
3893
|
+
{
|
|
3894
|
+
name: IrisTool.NAVIGATE,
|
|
3895
|
+
description: "Navigate the connected browser tab to a URL. The SDK reconnects automatically after the page loads. Use iris_sessions to confirm the new tab is connected before acting.",
|
|
3896
|
+
inputSchema: {
|
|
3897
|
+
url: z6.string().describe("The URL to navigate to."),
|
|
3898
|
+
...sessionIdShape2
|
|
3899
|
+
},
|
|
3900
|
+
outputSchema: {
|
|
3901
|
+
ok: z6.boolean(),
|
|
3902
|
+
url: z6.string().optional(),
|
|
3903
|
+
reason: z6.string().optional()
|
|
3904
|
+
},
|
|
3905
|
+
handler: async (deps, args) => {
|
|
3906
|
+
const url = asString(args["url"]);
|
|
3907
|
+
if (url === void 0 || url.length === 0)
|
|
3908
|
+
return { ok: false, reason: "url required" };
|
|
3909
|
+
const result = await commandOrThrow2(deps, asString(args["sessionId"]), IrisCommand.NAVIGATE, { url });
|
|
3910
|
+
return {
|
|
3911
|
+
ok: result.ok === true,
|
|
3912
|
+
...typeof result.url === "string" ? { url: result.url } : {},
|
|
3913
|
+
...typeof result.reason === "string" ? { reason: result.reason } : {}
|
|
3914
|
+
};
|
|
3915
|
+
}
|
|
3916
|
+
},
|
|
3917
|
+
{
|
|
3918
|
+
name: IrisTool.REFRESH,
|
|
3919
|
+
description: "Reload the connected browser tab. Pass { hard: true } to bypass the browser cache (equivalent to Cmd+Shift+R). The SDK reconnects automatically after the reload.",
|
|
3920
|
+
inputSchema: {
|
|
3921
|
+
hard: z6.boolean().optional().describe("Set true to bypass the browser cache. Default: false (normal reload)."),
|
|
3922
|
+
...sessionIdShape2
|
|
3923
|
+
},
|
|
3924
|
+
outputSchema: {
|
|
3925
|
+
ok: z6.boolean()
|
|
3926
|
+
},
|
|
3927
|
+
handler: async (deps, args) => {
|
|
3928
|
+
await commandOrThrow2(deps, asString(args["sessionId"]), IrisCommand.REFRESH, {
|
|
3929
|
+
hard: args["hard"] === true
|
|
3930
|
+
});
|
|
3931
|
+
return { ok: true };
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
];
|
|
3143
3935
|
|
|
3144
3936
|
// ../server/dist/flows/flow-tools.js
|
|
3937
|
+
import { z as z7 } from "zod";
|
|
3145
3938
|
function latestRecordedFlow(events) {
|
|
3146
3939
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
3147
3940
|
const event = events[i];
|
|
@@ -3187,18 +3980,26 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs) {
|
|
|
3187
3980
|
var FLOW_TOOLS = [
|
|
3188
3981
|
{
|
|
3189
3982
|
name: IrisTool.FLOW_SAVE,
|
|
3190
|
-
description: 'Persist the last/active recording (by name) as a git-checked, anchor-resolved flow at .iris/flows/<name>.json. Each step is bound to a SEMANTIC anchor (testid/role/signal), never a volatile ref; steps without a resolvable testid are kept with degraded:true (a "add a data-testid here" marker) rather than dropped. Returns { name, stepCount, degraded, empty } or
|
|
3983
|
+
description: 'Persist the last/active recording (by name) as a git-checked, anchor-resolved flow at .iris/flows/<name>.json. Each step is bound to a SEMANTIC anchor (testid/role/signal), never a volatile ref; steps without a resolvable testid are kept with degraded:true (a "add a data-testid here" marker) rather than dropped. Returns { name, stepCount, degraded, empty, assertions } \u2014 `assertions.grade` is asserted | presence-only | assertion-free: a flow that only acts (or only checks element presence) will pass even if the feature breaks, so when grade is not "asserted" follow assertions.warning and add a consequence assertion via iris_annotate (assert-signal / assert-net / success-state).',
|
|
3191
3984
|
inputSchema: {
|
|
3192
|
-
flowName:
|
|
3985
|
+
flowName: z7.string().describe("Name for the flow file (saved to .iris/flows/<flowName>.json). Use again in iris_flow_load/iris_flow_replay.")
|
|
3193
3986
|
},
|
|
3194
3987
|
outputSchema: {
|
|
3195
|
-
saved:
|
|
3196
|
-
path:
|
|
3197
|
-
stepCount:
|
|
3198
|
-
degraded:
|
|
3988
|
+
saved: z7.boolean(),
|
|
3989
|
+
path: z7.string(),
|
|
3990
|
+
stepCount: z7.number().optional(),
|
|
3991
|
+
degraded: z7.number().optional(),
|
|
3992
|
+
assertions: z7.object({
|
|
3993
|
+
grade: z7.string().describe("asserted | presence-only | assertion-free"),
|
|
3994
|
+
hasConsequenceAssertion: z7.boolean(),
|
|
3995
|
+
totalSteps: z7.number(),
|
|
3996
|
+
consequenceSteps: z7.number(),
|
|
3997
|
+
weakSteps: z7.number(),
|
|
3998
|
+
warning: z7.string().optional()
|
|
3999
|
+
}).optional()
|
|
3199
4000
|
},
|
|
3200
4001
|
handler: (deps, args) => {
|
|
3201
|
-
const name =
|
|
4002
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3202
4003
|
const program = deps.recordings.getCompiled(name);
|
|
3203
4004
|
if (program === void 0) {
|
|
3204
4005
|
return Promise.resolve({
|
|
@@ -3212,10 +4013,12 @@ var FLOW_TOOLS = [
|
|
|
3212
4013
|
dynamic: deps.annotations.dynamic(name),
|
|
3213
4014
|
...success !== void 0 ? { success } : {}
|
|
3214
4015
|
};
|
|
3215
|
-
return deps.flows.save(program, annotations).then((res) => {
|
|
3216
|
-
if (res.ok)
|
|
3217
|
-
|
|
3218
|
-
|
|
4016
|
+
return deps.flows.save(program, annotations).then(async (res) => {
|
|
4017
|
+
if (!res.ok)
|
|
4018
|
+
return { error: flowErrorMessage(res.code), code: res.code };
|
|
4019
|
+
deps.annotations.clear(name);
|
|
4020
|
+
const loaded = await deps.flows.load(res.value.name);
|
|
4021
|
+
return loaded.ok ? { ...res.value, assertions: classifyFlowAssertions(loaded.value) } : res.value;
|
|
3219
4022
|
});
|
|
3220
4023
|
}
|
|
3221
4024
|
},
|
|
@@ -3224,22 +4027,27 @@ var FLOW_TOOLS = [
|
|
|
3224
4027
|
description: "List saved flow names under .iris/flows (a fresh agent learns the demonstrated journeys without a browser).",
|
|
3225
4028
|
inputSchema: {},
|
|
3226
4029
|
outputSchema: {
|
|
3227
|
-
flows:
|
|
4030
|
+
flows: z7.array(z7.object({ name: z7.string(), path: z7.string(), createdAt: z7.number().optional() }))
|
|
3228
4031
|
},
|
|
3229
|
-
|
|
4032
|
+
// Return {name, path} objects to MATCH the declared outputSchema. Returning bare name strings
|
|
4033
|
+
// (the prior bug) made schema-validating MCP clients reject the result ("expected object,
|
|
4034
|
+
// received string") — caught driving the live demo.
|
|
4035
|
+
handler: (deps) => deps.flows.list().then((names) => ({
|
|
4036
|
+
flows: names.map((name) => ({ name, path: flowPath(deps.irisRoot, name) }))
|
|
4037
|
+
}))
|
|
3230
4038
|
},
|
|
3231
4039
|
{
|
|
3232
4040
|
name: IrisTool.FLOW_LOAD,
|
|
3233
4041
|
description: "Read + validate a saved flow by flowName from .iris/flows/<flowName>.json. Returns the FlowFile (version, flowName, createdAt, anchored steps) or a structured { error, code }.",
|
|
3234
4042
|
inputSchema: {
|
|
3235
|
-
flowName:
|
|
4043
|
+
flowName: z7.string().describe("Flow file name (without .json extension) from iris_flow_list.")
|
|
3236
4044
|
},
|
|
3237
4045
|
outputSchema: {
|
|
3238
|
-
flowName:
|
|
3239
|
-
steps:
|
|
3240
|
-
createdAt:
|
|
4046
|
+
flowName: z7.string(),
|
|
4047
|
+
steps: z7.array(z7.unknown()),
|
|
4048
|
+
createdAt: z7.number().optional()
|
|
3241
4049
|
},
|
|
3242
|
-
handler: (deps, args) => deps.flows.load(
|
|
4050
|
+
handler: (deps, args) => deps.flows.load(asString(args["flowName"]) ?? "").then((res) => {
|
|
3243
4051
|
if (!res.ok)
|
|
3244
4052
|
return { error: flowErrorMessage(res.code), code: res.code };
|
|
3245
4053
|
const { name, ...rest } = res.value;
|
|
@@ -3248,20 +4056,21 @@ var FLOW_TOOLS = [
|
|
|
3248
4056
|
},
|
|
3249
4057
|
{
|
|
3250
4058
|
name: IrisTool.FLOW_REPLAY,
|
|
3251
|
-
description: `Replay a git-checked flow from .iris/flows/<name>.json. RE-RESOLVES each step's semantic anchor (testid via iris_query; signal via predicate) against the LIVE DOM \u2014 never reuses a stale ref. On an anchor MISS returns legible DRIFT { step, anchor, drift:{ reasonKind, reason, nearest } } (the closest surviving testid) and stops \u2014 the "whose fault is it" contract. Returns { name, status: ok|drift|error, steps:[...] };
|
|
4059
|
+
description: `Replay a git-checked flow from .iris/flows/<name>.json. RE-RESOLVES each step's semantic anchor (testid via iris_query; signal via predicate) against the LIVE DOM \u2014 never reuses a stale ref. On an anchor MISS returns legible DRIFT { step, anchor, drift:{ reasonKind, reason, nearest } } (the closest surviving testid) and stops \u2014 the "whose fault is it" contract. Returns { name, status: ok|drift|error, steps:[...] }; missing/malformed files and action failures are status:error with a structured code (distinct from contract-changed drift).`,
|
|
3252
4060
|
inputSchema: {
|
|
3253
|
-
flowName:
|
|
3254
|
-
|
|
4061
|
+
flowName: z7.string().describe("Flow file name (without .json extension) from iris_flow_list."),
|
|
4062
|
+
confirmDangerous: z7.boolean().optional().describe("Set true to allow destructive controls during this replay only."),
|
|
4063
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3255
4064
|
},
|
|
3256
4065
|
outputSchema: {
|
|
3257
|
-
status:
|
|
3258
|
-
steps:
|
|
3259
|
-
proposals:
|
|
3260
|
-
error:
|
|
4066
|
+
status: z7.string().describe("ok | drift | error"),
|
|
4067
|
+
steps: z7.array(z7.unknown()),
|
|
4068
|
+
proposals: z7.array(z7.unknown()).optional(),
|
|
4069
|
+
error: z7.object({ code: z7.string(), message: z7.string() }).optional()
|
|
3261
4070
|
},
|
|
3262
4071
|
handler: async (deps, args) => {
|
|
3263
4072
|
const startedAt = deps.now();
|
|
3264
|
-
const name =
|
|
4073
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3265
4074
|
const loaded = await deps.flows.load(name);
|
|
3266
4075
|
if (!loaded.ok) {
|
|
3267
4076
|
await recordReplayRun(deps, name, ReplayStatus.ERROR, 0, deps.now() - startedAt);
|
|
@@ -3272,12 +4081,35 @@ var FLOW_TOOLS = [
|
|
|
3272
4081
|
error: { code: loaded.code, message: flowErrorMessage(loaded.code) }
|
|
3273
4082
|
};
|
|
3274
4083
|
}
|
|
3275
|
-
const session = deps.sessions.resolve(
|
|
3276
|
-
const
|
|
4084
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4085
|
+
const replayFloor = session.elapsed();
|
|
4086
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
4087
|
+
const stepsClean = steps.length > 0 && steps.every((s) => s.ok && s.drift === void 0);
|
|
4088
|
+
if (stepsClean && loaded.value.success !== void 0) {
|
|
4089
|
+
const verdict = await assertSuccess(session, loaded.value.success, dynamicTestids(loaded.value), waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, replayFloor);
|
|
4090
|
+
const row = {
|
|
4091
|
+
step: steps.length,
|
|
4092
|
+
tool: "success",
|
|
4093
|
+
anchor: successLabel(loaded.value.success),
|
|
4094
|
+
ok: verdict.pass,
|
|
4095
|
+
...verdict.pass ? {} : { error: verdict.failureReason ?? "flow.success not satisfied" }
|
|
4096
|
+
};
|
|
4097
|
+
steps.push(row);
|
|
4098
|
+
}
|
|
3277
4099
|
const driftSteps = steps.filter((s) => s.drift !== void 0).length;
|
|
3278
4100
|
const allOk = steps.every((s) => s.ok);
|
|
3279
|
-
const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.
|
|
4101
|
+
const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.ERROR;
|
|
3280
4102
|
await recordReplayRun(deps, name, status, driftSteps, deps.now() - startedAt);
|
|
4103
|
+
const failed = steps.find((step) => !step.ok && step.drift === void 0);
|
|
4104
|
+
if (failed !== void 0) {
|
|
4105
|
+
const message = failed.error ?? "flow action failed";
|
|
4106
|
+
return {
|
|
4107
|
+
name,
|
|
4108
|
+
status,
|
|
4109
|
+
steps,
|
|
4110
|
+
error: { code: ReplayStatus.ERROR, message }
|
|
4111
|
+
};
|
|
4112
|
+
}
|
|
3281
4113
|
return { name, status, steps };
|
|
3282
4114
|
}
|
|
3283
4115
|
},
|
|
@@ -3285,20 +4117,20 @@ var FLOW_TOOLS = [
|
|
|
3285
4117
|
name: IrisTool.FLOW_SAVE_RECORDED,
|
|
3286
4118
|
description: "Persist the HUMAN-recorded flow from the live tab. The recorder toolbar compiles the human's real clicks/inputs into a semantically anchored FlowFile in-page and emits it; this tool reads the LATEST recorded-flow from the session and writes it to .iris/flows/<name>.json (no recompilation \u2014 the browser already resolved every anchor). Pass `name` to override the recorded name. Returns { name, stepCount, degraded, empty } or { error, code } (code flow_no_recorded when no recording is present).",
|
|
3287
4119
|
inputSchema: {
|
|
3288
|
-
flowName:
|
|
4120
|
+
flowName: z7.string().optional().describe("Override the flow name embedded in the recorded flow. Omit to use the recorder-assigned name."),
|
|
3289
4121
|
...{
|
|
3290
|
-
sessionId:
|
|
4122
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3291
4123
|
}
|
|
3292
4124
|
},
|
|
3293
4125
|
outputSchema: {
|
|
3294
|
-
flowName:
|
|
3295
|
-
stepCount:
|
|
3296
|
-
degraded:
|
|
3297
|
-
error:
|
|
3298
|
-
code:
|
|
4126
|
+
flowName: z7.string().optional(),
|
|
4127
|
+
stepCount: z7.number().optional(),
|
|
4128
|
+
degraded: z7.number().optional(),
|
|
4129
|
+
error: z7.string().optional(),
|
|
4130
|
+
code: z7.string().optional()
|
|
3299
4131
|
},
|
|
3300
4132
|
handler: async (deps, args) => {
|
|
3301
|
-
const session = deps.sessions.resolve(
|
|
4133
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
3302
4134
|
const recorded = latestRecordedFlow(session.eventsSince(0));
|
|
3303
4135
|
if (recorded === void 0) {
|
|
3304
4136
|
return {
|
|
@@ -3306,7 +4138,7 @@ var FLOW_TOOLS = [
|
|
|
3306
4138
|
code: RecordedSaveError.NO_RECORDED_FLOW
|
|
3307
4139
|
};
|
|
3308
4140
|
}
|
|
3309
|
-
const override =
|
|
4141
|
+
const override = asString(args["flowName"]);
|
|
3310
4142
|
const flow = override !== void 0 ? { ...recorded.flow, name: override } : recorded.flow;
|
|
3311
4143
|
const res = await deps.flows.saveFlow(flow);
|
|
3312
4144
|
if (!res.ok)
|
|
@@ -3317,35 +4149,38 @@ var FLOW_TOOLS = [
|
|
|
3317
4149
|
},
|
|
3318
4150
|
{
|
|
3319
4151
|
name: IrisTool.FLOW_HEAL,
|
|
3320
|
-
description: "Self-healing replay. Re-runs iris_flow_replay; on testid DRIFT computes confidence-scored nearest-match rebind PROPOSALS. With apply:false (default) returns the proposed diff WITHOUT writing. With apply:true, writes the confident rebind(s) back into .iris/flows/<name>.json and returns what changed \u2014 never silently. A drift with no proposal above the confidence floor is status:unhealable (file untouched). Returns { name, status: healed|drift|unhealable|nothing_to_heal|error, applied, proposals[], changed[], message }.",
|
|
4152
|
+
description: "Self-healing replay. Re-runs iris_flow_replay; on testid DRIFT computes confidence-scored nearest-match rebind PROPOSALS. With apply:false (default) returns the proposed diff WITHOUT writing. With apply:true, writes the confident rebind(s) back into .iris/flows/<name>.json and returns what changed \u2014 never silently. Before writing, apply re-replays the healed flow and re-asserts its success consequence: if the rebound locator resolves but the consequence no longer fires, the write is REFUSED (status:consequence_broken) \u2014 it heals the locator, never the intent. A drift with no proposal above the confidence floor is status:unhealable (file untouched). Returns { name, status: healed|drift|unhealable|consequence_broken|nothing_to_heal|error, applied, proposals[], changed[], message }.",
|
|
3321
4153
|
inputSchema: {
|
|
3322
|
-
flowName:
|
|
3323
|
-
apply:
|
|
3324
|
-
|
|
4154
|
+
flowName: z7.string().describe("Flow file name to heal (from iris_flow_list)."),
|
|
4155
|
+
apply: z7.boolean().optional(),
|
|
4156
|
+
confirmDangerous: z7.boolean().optional().describe("Set true to allow destructive controls during this heal replay only."),
|
|
4157
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3325
4158
|
},
|
|
3326
4159
|
outputSchema: {
|
|
3327
|
-
flowName:
|
|
3328
|
-
status:
|
|
3329
|
-
applied:
|
|
3330
|
-
proposals:
|
|
3331
|
-
changed:
|
|
3332
|
-
message:
|
|
3333
|
-
error:
|
|
4160
|
+
flowName: z7.string(),
|
|
4161
|
+
status: z7.string(),
|
|
4162
|
+
applied: z7.boolean(),
|
|
4163
|
+
proposals: z7.array(z7.unknown()),
|
|
4164
|
+
changed: z7.array(z7.unknown()),
|
|
4165
|
+
message: z7.string(),
|
|
4166
|
+
error: z7.object({ code: z7.string(), message: z7.string() }).optional()
|
|
3334
4167
|
},
|
|
3335
4168
|
handler: (deps, args) => healFlow(deps, args).then(({ name, ...rest }) => ({ flowName: name, ...rest }))
|
|
3336
4169
|
}
|
|
3337
4170
|
];
|
|
3338
4171
|
var HEAL_MESSAGES = {
|
|
3339
4172
|
NOTHING: "nothing to heal \u2014 every anchor resolved on replay",
|
|
3340
|
-
HEALED: "rewrote drifted testid anchors to their nearest surviving match",
|
|
4173
|
+
HEALED: "rewrote drifted testid anchors to their nearest surviving match and re-verified the flow's success consequence still fires",
|
|
3341
4174
|
DRIFT_DRY: "confident rebind(s) proposed \u2014 re-run with apply:true to write them to disk",
|
|
3342
|
-
UNHEALABLE: `drift found, but no nearest match cleared the confidence floor (HEAL_CONFIDENCE_MIN=${HEAL_CONFIDENCE_MIN}); file left untouched \u2014 add a data-testid or fix the flow by hand
|
|
4175
|
+
UNHEALABLE: `drift found, but no nearest match cleared the confidence floor (HEAL_CONFIDENCE_MIN=${HEAL_CONFIDENCE_MIN}); file left untouched \u2014 add a data-testid or fix the flow by hand`,
|
|
4176
|
+
HEALED_UNVERIFIED: "rewrote drifted testid anchors \u2014 but this flow declares no success consequence, so the rebind resolves a locator without proving the intent still holds. Add a success-state assertion (iris_annotate) so future heals can be verified.",
|
|
4177
|
+
CONSEQUENCE_BROKEN: "rebind resolves the drifted locator to a surviving element, but the healed flow no longer satisfies its success consequence \u2014 refusing to write (a heal that loses the intent would ship a green-but-dead test). Fix by hand and verify"
|
|
3343
4178
|
};
|
|
3344
4179
|
function toChange(proposal) {
|
|
3345
4180
|
return { step: proposal.step, from: proposal.from, to: proposal.to };
|
|
3346
4181
|
}
|
|
3347
4182
|
async function healFlow(deps, args) {
|
|
3348
|
-
const name =
|
|
4183
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3349
4184
|
const apply = args["apply"] === true;
|
|
3350
4185
|
const loaded = await deps.flows.load(name);
|
|
3351
4186
|
if (!loaded.ok) {
|
|
@@ -3359,9 +4194,22 @@ async function healFlow(deps, args) {
|
|
|
3359
4194
|
error: { code: loaded.code, message: flowErrorMessage(loaded.code) }
|
|
3360
4195
|
};
|
|
3361
4196
|
}
|
|
3362
|
-
const session = deps.sessions.resolve(
|
|
3363
|
-
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS);
|
|
4197
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4198
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
3364
4199
|
const drifted = steps.some((s) => s.drift !== void 0);
|
|
4200
|
+
const failed = steps.find((s) => !s.ok && s.drift === void 0);
|
|
4201
|
+
if (failed !== void 0) {
|
|
4202
|
+
const message = failed.error ?? "flow replay failed before an anchor could be healed";
|
|
4203
|
+
return {
|
|
4204
|
+
name,
|
|
4205
|
+
status: HealStatus.ERROR,
|
|
4206
|
+
applied: false,
|
|
4207
|
+
proposals: [],
|
|
4208
|
+
changed: [],
|
|
4209
|
+
message,
|
|
4210
|
+
error: { code: ReplayStatus.ERROR, message }
|
|
4211
|
+
};
|
|
4212
|
+
}
|
|
3365
4213
|
if (!drifted) {
|
|
3366
4214
|
return {
|
|
3367
4215
|
name,
|
|
@@ -3393,6 +4241,23 @@ async function healFlow(deps, args) {
|
|
|
3393
4241
|
message: HEAL_MESSAGES.DRIFT_DRY
|
|
3394
4242
|
};
|
|
3395
4243
|
}
|
|
4244
|
+
const { flow: healed } = applyHealChanges(loaded.value, proposals.map(toChange));
|
|
4245
|
+
if (healed.success !== void 0) {
|
|
4246
|
+
const verifyFloor = session.elapsed();
|
|
4247
|
+
const verifySteps = await replayFlow(session, healed, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
4248
|
+
const verifyClean = verifySteps.length > 0 && verifySteps.every((s) => s.ok && s.drift === void 0);
|
|
4249
|
+
const verdict = verifyClean ? await assertSuccess(session, healed.success, dynamicTestids(healed), waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, verifyFloor) : { pass: false, failureReason: "healed flow did not replay cleanly" };
|
|
4250
|
+
if (!verdict.pass) {
|
|
4251
|
+
return {
|
|
4252
|
+
name,
|
|
4253
|
+
status: HealStatus.CONSEQUENCE_BROKEN,
|
|
4254
|
+
applied: false,
|
|
4255
|
+
proposals,
|
|
4256
|
+
changed: [],
|
|
4257
|
+
message: `${HEAL_MESSAGES.CONSEQUENCE_BROKEN} (${successLabel(healed.success)}: ${verdict.failureReason ?? "not satisfied"})`
|
|
4258
|
+
};
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
3396
4261
|
const written = await deps.flows.heal(name, proposals.map(toChange));
|
|
3397
4262
|
if (!written.ok) {
|
|
3398
4263
|
return {
|
|
@@ -3411,14 +4276,14 @@ async function healFlow(deps, args) {
|
|
|
3411
4276
|
applied: written.value.changed.length > 0,
|
|
3412
4277
|
proposals,
|
|
3413
4278
|
changed: written.value.changed,
|
|
3414
|
-
message: HEAL_MESSAGES.HEALED
|
|
4279
|
+
message: loaded.value.success !== void 0 ? HEAL_MESSAGES.HEALED : HEAL_MESSAGES.HEALED_UNVERIFIED
|
|
3415
4280
|
};
|
|
3416
4281
|
}
|
|
3417
4282
|
|
|
3418
4283
|
// ../server/dist/project/project-tools.js
|
|
3419
|
-
import { z as
|
|
4284
|
+
import { z as z8 } from "zod";
|
|
3420
4285
|
var sessionIdShape3 = {
|
|
3421
|
-
sessionId:
|
|
4286
|
+
sessionId: z8.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3422
4287
|
};
|
|
3423
4288
|
var REGRESSION_STATUSES = /* @__PURE__ */ new Set([
|
|
3424
4289
|
RunStatus.FAIL,
|
|
@@ -3459,12 +4324,12 @@ var PROJECT_TOOLS = [
|
|
|
3459
4324
|
name: IrisTool.PROJECT,
|
|
3460
4325
|
description: 'Read cross-run history from .iris/project.json \u2014 the memory of how past runs behaved. With { name } it also returns the last run for that flow plus a diff-vs-last summary (status change, regressed flag, consoleErrors/driftSteps deltas) so you can answer "did it behave like last time?". Returns { runs, learned?, lastRun?, diff? } or { error, reason } when no/invalid history exists.',
|
|
3461
4326
|
inputSchema: {
|
|
3462
|
-
name:
|
|
4327
|
+
name: z8.string().optional().describe("Filter runs by this name. Omit to return all runs."),
|
|
3463
4328
|
...sessionIdShape3
|
|
3464
4329
|
},
|
|
3465
4330
|
outputSchema: {
|
|
3466
|
-
runs:
|
|
3467
|
-
diff:
|
|
4331
|
+
runs: z8.array(z8.unknown()),
|
|
4332
|
+
diff: z8.unknown().optional()
|
|
3468
4333
|
},
|
|
3469
4334
|
handler: async (deps, args) => {
|
|
3470
4335
|
const read = await deps.project.read();
|
|
@@ -3474,7 +4339,7 @@ var PROJECT_TOOLS = [
|
|
|
3474
4339
|
reason: read.reason
|
|
3475
4340
|
};
|
|
3476
4341
|
}
|
|
3477
|
-
const name =
|
|
4342
|
+
const name = asString(args["name"]);
|
|
3478
4343
|
if (name === void 0) {
|
|
3479
4344
|
return { runs: read.file.runs, learned: read.file.learned };
|
|
3480
4345
|
}
|
|
@@ -3492,22 +4357,22 @@ var PROJECT_TOOLS = [
|
|
|
3492
4357
|
name: IrisTool.RUN_RECORD,
|
|
3493
4358
|
description: "Explicitly record a run outcome into .iris/project.json (the manual companion to the auto-record on iris_flow_replay). Use it to log the result of an assertion sequence or a manual journey so future runs can diff against it. Returns { recorded:true, name, status }.",
|
|
3494
4359
|
inputSchema: {
|
|
3495
|
-
name:
|
|
3496
|
-
status:
|
|
3497
|
-
kind:
|
|
3498
|
-
summary:
|
|
4360
|
+
name: z8.string().describe("Run name for grouping in iris_project history."),
|
|
4361
|
+
status: z8.nativeEnum(RunStatus).describe("Outcome: pass | fail | drift | error"),
|
|
4362
|
+
kind: z8.nativeEnum(RunKind).optional(),
|
|
4363
|
+
summary: z8.string().optional().describe("One-line human summary of what this run covered."),
|
|
3499
4364
|
...sessionIdShape3
|
|
3500
4365
|
},
|
|
3501
4366
|
outputSchema: {
|
|
3502
|
-
recorded:
|
|
3503
|
-
runName:
|
|
3504
|
-
status:
|
|
4367
|
+
recorded: z8.boolean(),
|
|
4368
|
+
runName: z8.string(),
|
|
4369
|
+
status: z8.string()
|
|
3505
4370
|
},
|
|
3506
4371
|
handler: async (deps, args) => {
|
|
3507
|
-
const name =
|
|
4372
|
+
const name = asString(args["name"]) ?? "";
|
|
3508
4373
|
const status = args["status"];
|
|
3509
4374
|
const kindArg = args["kind"];
|
|
3510
|
-
const summary =
|
|
4375
|
+
const summary = asString(args["summary"]);
|
|
3511
4376
|
await deps.project.recordRun({
|
|
3512
4377
|
kind: typeof kindArg === "string" ? kindArg : RunKind.MANUAL,
|
|
3513
4378
|
name,
|
|
@@ -3520,7 +4385,7 @@ var PROJECT_TOOLS = [
|
|
|
3520
4385
|
];
|
|
3521
4386
|
|
|
3522
4387
|
// ../server/dist/visual/visual-tools.js
|
|
3523
|
-
import { z as
|
|
4388
|
+
import { z as z9 } from "zod";
|
|
3524
4389
|
|
|
3525
4390
|
// ../server/dist/visual/visual-diff.js
|
|
3526
4391
|
async function loadDeps() {
|
|
@@ -3618,8 +4483,8 @@ async function diffPng(baselineBytes, currentBytes, opts = {}) {
|
|
|
3618
4483
|
var VisualStore = class {
|
|
3619
4484
|
#fs;
|
|
3620
4485
|
#root;
|
|
3621
|
-
constructor(
|
|
3622
|
-
this.#fs =
|
|
4486
|
+
constructor(fs2, root) {
|
|
4487
|
+
this.#fs = fs2;
|
|
3623
4488
|
this.#root = root;
|
|
3624
4489
|
}
|
|
3625
4490
|
/** The absolute baseline path for `name` (for echoing back to the agent). */
|
|
@@ -3671,13 +4536,13 @@ var VisualStore = class {
|
|
|
3671
4536
|
|
|
3672
4537
|
// ../server/dist/visual/visual-tools.js
|
|
3673
4538
|
var sessionIdShape4 = {
|
|
3674
|
-
sessionId:
|
|
4539
|
+
sessionId: z9.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3675
4540
|
};
|
|
3676
|
-
var rectShape =
|
|
3677
|
-
x:
|
|
3678
|
-
y:
|
|
3679
|
-
width:
|
|
3680
|
-
height:
|
|
4541
|
+
var rectShape = z9.object({
|
|
4542
|
+
x: z9.number(),
|
|
4543
|
+
y: z9.number(),
|
|
4544
|
+
width: z9.number(),
|
|
4545
|
+
height: z9.number()
|
|
3681
4546
|
});
|
|
3682
4547
|
function screenshotProvider(deps) {
|
|
3683
4548
|
const p = deps.realInput;
|
|
@@ -3689,11 +4554,11 @@ var noProvider = {
|
|
|
3689
4554
|
recommendation: VISUAL_NO_PROVIDER_RECOMMENDATION
|
|
3690
4555
|
};
|
|
3691
4556
|
function asBox(value) {
|
|
3692
|
-
const b =
|
|
3693
|
-
const x =
|
|
3694
|
-
const y =
|
|
3695
|
-
const w =
|
|
3696
|
-
const h =
|
|
4557
|
+
const b = asRecord(asRecord(value)["box"]);
|
|
4558
|
+
const x = asNumber(b["x"]);
|
|
4559
|
+
const y = asNumber(b["y"]);
|
|
4560
|
+
const w = asNumber(b["width"]);
|
|
4561
|
+
const h = asNumber(b["height"]);
|
|
3697
4562
|
if (x === void 0 || y === void 0 || w === void 0 || h === void 0)
|
|
3698
4563
|
return void 0;
|
|
3699
4564
|
if (w <= 0 || h <= 0)
|
|
@@ -3703,12 +4568,12 @@ function asBox(value) {
|
|
|
3703
4568
|
async function buildOpts(deps, sessionId, args) {
|
|
3704
4569
|
const clipArg = args["clip"];
|
|
3705
4570
|
if (clipArg !== void 0) {
|
|
3706
|
-
const c =
|
|
4571
|
+
const c = asRecord(clipArg);
|
|
3707
4572
|
const box = asBox({ box: c });
|
|
3708
4573
|
if (box !== void 0)
|
|
3709
4574
|
return { clip: box };
|
|
3710
4575
|
}
|
|
3711
|
-
const ref =
|
|
4576
|
+
const ref = asString(args["ref"]);
|
|
3712
4577
|
if (ref !== void 0) {
|
|
3713
4578
|
const session = deps.sessions.resolve(sessionId);
|
|
3714
4579
|
const res = await session.command(IrisCommand.INSPECT, { ref });
|
|
@@ -3734,31 +4599,31 @@ var VISUAL_TOOLS = [
|
|
|
3734
4599
|
name: IrisTool.SCREENSHOT,
|
|
3735
4600
|
description: "Capture a pixel screenshot of the DRIVEN page (needs `iris drive`/IRIS_CDP_URL \u2014 the SDK has no screenshotter) and save it as a visual baseline at .iris/visual/<name>.png. { fullPage } for the whole scroll height, { ref } or { clip:{x,y,width,height} } for one element/region. Returns { saved:true, name, path, bytes } or { ok:false, reason } when no driven browser is attached.",
|
|
3736
4601
|
inputSchema: {
|
|
3737
|
-
name:
|
|
3738
|
-
fullPage:
|
|
3739
|
-
ref:
|
|
4602
|
+
name: z9.string().describe("Baseline name \u2014 saved as .iris/visual/<name>.png. Use the same name in iris_visual_diff to compare."),
|
|
4603
|
+
fullPage: z9.boolean().optional().describe("Capture the full scroll height. Default: viewport only."),
|
|
4604
|
+
ref: z9.string().optional().describe("Element ref to screenshot (scopes to element bounding box). Omit for full page."),
|
|
3740
4605
|
clip: rectShape.optional().describe("Explicit { x, y, width, height } clip rectangle in page coordinates."),
|
|
3741
4606
|
...sessionIdShape4
|
|
3742
4607
|
},
|
|
3743
4608
|
outputSchema: {
|
|
3744
|
-
ok:
|
|
3745
|
-
saved:
|
|
3746
|
-
name:
|
|
3747
|
-
path:
|
|
3748
|
-
bytes:
|
|
3749
|
-
reason:
|
|
3750
|
-
recommendation:
|
|
4609
|
+
ok: z9.boolean(),
|
|
4610
|
+
saved: z9.boolean().optional(),
|
|
4611
|
+
name: z9.string().optional(),
|
|
4612
|
+
path: z9.string().optional(),
|
|
4613
|
+
bytes: z9.number().optional(),
|
|
4614
|
+
reason: z9.string().optional(),
|
|
4615
|
+
recommendation: z9.string().optional()
|
|
3751
4616
|
},
|
|
3752
4617
|
handler: async (deps, args) => {
|
|
3753
4618
|
const provider = screenshotProvider(deps);
|
|
3754
4619
|
if (provider === void 0)
|
|
3755
4620
|
return noProvider;
|
|
3756
|
-
const sessionId =
|
|
4621
|
+
const sessionId = asString(args["sessionId"]);
|
|
3757
4622
|
const session = deps.sessions.resolve(sessionId);
|
|
3758
4623
|
const png = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
|
|
3759
4624
|
if (png === void 0)
|
|
3760
4625
|
return { ok: false, reason: VisualReason.CAPTURE_FAILED };
|
|
3761
|
-
const name =
|
|
4626
|
+
const name = asString(args["name"]) ?? "default";
|
|
3762
4627
|
const store = new VisualStore(deps.fs, deps.irisRoot);
|
|
3763
4628
|
const path = await store.saveBaseline(name, png);
|
|
3764
4629
|
return { ok: true, saved: true, name, path, bytes: png.length };
|
|
@@ -3768,39 +4633,39 @@ var VISUAL_TOOLS = [
|
|
|
3768
4633
|
name: IrisTool.VISUAL_DIFF,
|
|
3769
4634
|
description: "Perceptually diff the DRIVEN page against a saved visual baseline (see iris_screenshot). { masks:[{x,y,width,height}] } neutralizes volatile regions; { maxRatio } sets the pass tolerance (default 0). Returns { matched, changedPixels, totalPixels, ratio, region?, diffPath, dimensionMismatch } \u2014 the overlay diff is written to .iris/visual/<baseline>.diff.png \u2014 or { ok:false, reason } (no-provider / baseline-missing).",
|
|
3770
4635
|
inputSchema: {
|
|
3771
|
-
baseline:
|
|
3772
|
-
fullPage:
|
|
3773
|
-
ref:
|
|
4636
|
+
baseline: z9.string().describe("Baseline screenshot name (from iris_screenshot). Used to compare with the current screenshot."),
|
|
4637
|
+
fullPage: z9.boolean().optional(),
|
|
4638
|
+
ref: z9.string().optional(),
|
|
3774
4639
|
clip: rectShape.optional(),
|
|
3775
|
-
masks:
|
|
3776
|
-
maxRatio:
|
|
3777
|
-
threshold:
|
|
4640
|
+
masks: z9.array(rectShape).optional(),
|
|
4641
|
+
maxRatio: z9.number().optional(),
|
|
4642
|
+
threshold: z9.number().optional().describe("Pixel difference threshold (0\u20131). Default: 0.01."),
|
|
3778
4643
|
...sessionIdShape4
|
|
3779
4644
|
},
|
|
3780
4645
|
outputSchema: {
|
|
3781
|
-
ok:
|
|
3782
|
-
match:
|
|
3783
|
-
diffPct:
|
|
3784
|
-
diffPath:
|
|
3785
|
-
reason:
|
|
4646
|
+
ok: z9.boolean(),
|
|
4647
|
+
match: z9.boolean().optional(),
|
|
4648
|
+
diffPct: z9.number().optional(),
|
|
4649
|
+
diffPath: z9.string().optional(),
|
|
4650
|
+
reason: z9.string().optional()
|
|
3786
4651
|
},
|
|
3787
4652
|
handler: async (deps, args) => {
|
|
3788
4653
|
const provider = screenshotProvider(deps);
|
|
3789
4654
|
if (provider === void 0)
|
|
3790
4655
|
return noProvider;
|
|
3791
|
-
const baseline =
|
|
4656
|
+
const baseline = asString(args["baseline"]) ?? "";
|
|
3792
4657
|
const store = new VisualStore(deps.fs, deps.irisRoot);
|
|
3793
4658
|
const baselineBytes = await store.readBaseline(baseline);
|
|
3794
4659
|
if (baselineBytes === void 0)
|
|
3795
4660
|
return { ok: false, reason: VisualReason.BASELINE_MISSING };
|
|
3796
|
-
const sessionId =
|
|
4661
|
+
const sessionId = asString(args["sessionId"]);
|
|
3797
4662
|
const session = deps.sessions.resolve(sessionId);
|
|
3798
4663
|
const current = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
|
|
3799
4664
|
if (current === void 0)
|
|
3800
4665
|
return { ok: false, reason: VisualReason.CAPTURE_FAILED };
|
|
3801
4666
|
const masks = rectsFrom(args["masks"]);
|
|
3802
|
-
const threshold =
|
|
3803
|
-
const maxRatio =
|
|
4667
|
+
const threshold = asNumber(args["threshold"]);
|
|
4668
|
+
const maxRatio = asNumber(args["maxRatio"]);
|
|
3804
4669
|
const result = await diffPng(baselineBytes, current, {
|
|
3805
4670
|
...threshold !== void 0 ? { threshold } : {},
|
|
3806
4671
|
...maxRatio !== void 0 ? { maxRatio } : {},
|
|
@@ -3817,7 +4682,7 @@ var VISUAL_TOOLS = [
|
|
|
3817
4682
|
];
|
|
3818
4683
|
|
|
3819
4684
|
// ../server/dist/crawl/crawl-tools.js
|
|
3820
|
-
import { z as
|
|
4685
|
+
import { z as z10 } from "zod";
|
|
3821
4686
|
|
|
3822
4687
|
// ../server/dist/crawl/crawl.js
|
|
3823
4688
|
function isActivity(e) {
|
|
@@ -3830,7 +4695,7 @@ function failedRequests(events, floor) {
|
|
|
3830
4695
|
return events.filter((e) => {
|
|
3831
4696
|
if (e.type !== EventType.NET_REQUEST)
|
|
3832
4697
|
return false;
|
|
3833
|
-
const status =
|
|
4698
|
+
const status = asNumber(e.data["status"]);
|
|
3834
4699
|
return status !== void 0 && status >= floor;
|
|
3835
4700
|
});
|
|
3836
4701
|
}
|
|
@@ -3860,7 +4725,7 @@ async function crawl(session, opts, sleep) {
|
|
|
3860
4725
|
const act = await session.command(IrisCommand.ACT, {
|
|
3861
4726
|
ref: item.ref,
|
|
3862
4727
|
action: ActionType.CLICK,
|
|
3863
|
-
args: {}
|
|
4728
|
+
args: opts.confirmDangerous === true ? { [DANGEROUS_ACTION_CONFIRM_ARG]: true } : {}
|
|
3864
4729
|
});
|
|
3865
4730
|
await sleep(settleMs);
|
|
3866
4731
|
const events = session.eventsSince(since);
|
|
@@ -3871,14 +4736,14 @@ async function crawl(session, opts, sleep) {
|
|
|
3871
4736
|
kind: CrawlAnomalyKind.CONSOLE_ERROR,
|
|
3872
4737
|
ref: item.ref,
|
|
3873
4738
|
desc: item.desc,
|
|
3874
|
-
detail:
|
|
4739
|
+
detail: asString(e.data["message"]) ?? e.type
|
|
3875
4740
|
});
|
|
3876
4741
|
}
|
|
3877
4742
|
for (const e of failedRequests(events, CRAWL_DEFAULTS.FAILED_STATUS)) {
|
|
3878
4743
|
counts.failedRequests += 1;
|
|
3879
|
-
const method =
|
|
3880
|
-
const url =
|
|
3881
|
-
const status =
|
|
4744
|
+
const method = asString(e.data["method"]) ?? "";
|
|
4745
|
+
const url = asString(e.data["url"]) ?? "";
|
|
4746
|
+
const status = asNumber(e.data["status"]);
|
|
3882
4747
|
anomalies.push({
|
|
3883
4748
|
kind: CrawlAnomalyKind.FAILED_REQUEST,
|
|
3884
4749
|
ref: item.ref,
|
|
@@ -3886,7 +4751,7 @@ async function crawl(session, opts, sleep) {
|
|
|
3886
4751
|
detail: `${method} ${url} \u2192 ${status ?? ""}`.trim()
|
|
3887
4752
|
});
|
|
3888
4753
|
}
|
|
3889
|
-
const dispatched =
|
|
4754
|
+
const dispatched = asRecord(act.result)["dispatched"] !== false && act.ok;
|
|
3890
4755
|
if (dispatched && errs.length === 0 && !events.some(isActivity)) {
|
|
3891
4756
|
counts.deadControls += 1;
|
|
3892
4757
|
anomalies.push({
|
|
@@ -3914,32 +4779,34 @@ var CRAWL_TOOLS = [
|
|
|
3914
4779
|
name: IrisTool.CRAWL,
|
|
3915
4780
|
description: "Autonomously click every reachable interactive control (bounded by maxSteps, default 25) and report anomalies WITHOUT a script: console errors, failed requests (status \u2265 400), and DEAD controls (dispatched but the app did nothing). DESTRUCTIVE \u2014 it really clicks (may navigate/mutate state); use iris_explore first for a non-destructive list. Returns { interactiveFound, stepsRun, anomalies[{kind,ref,desc,detail}], counts, visited, truncated }.",
|
|
3916
4781
|
inputSchema: {
|
|
3917
|
-
maxSteps:
|
|
3918
|
-
settleMs:
|
|
3919
|
-
scope:
|
|
3920
|
-
|
|
4782
|
+
maxSteps: z10.number().optional().describe("Maximum number of controls to click. Default: 25."),
|
|
4783
|
+
settleMs: z10.number().optional().describe("Milliseconds to wait after each click for the app to react. Default: 500."),
|
|
4784
|
+
scope: z10.string().optional().describe("CSS selector or element ref to restrict crawling to a subtree."),
|
|
4785
|
+
confirmDangerous: z10.boolean().optional().describe("Set true to allow controls classified as destructive. Default false; those controls are blocked by the browser."),
|
|
4786
|
+
sessionId: z10.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3921
4787
|
},
|
|
3922
4788
|
outputSchema: {
|
|
3923
|
-
interactiveFound:
|
|
3924
|
-
stepsRun:
|
|
3925
|
-
anomalies:
|
|
3926
|
-
kind:
|
|
3927
|
-
ref:
|
|
3928
|
-
desc:
|
|
3929
|
-
detail:
|
|
4789
|
+
interactiveFound: z10.number(),
|
|
4790
|
+
stepsRun: z10.number(),
|
|
4791
|
+
anomalies: z10.array(z10.object({
|
|
4792
|
+
kind: z10.string(),
|
|
4793
|
+
ref: z10.string(),
|
|
4794
|
+
desc: z10.string(),
|
|
4795
|
+
detail: z10.string().optional()
|
|
3930
4796
|
})),
|
|
3931
|
-
counts:
|
|
3932
|
-
truncated:
|
|
4797
|
+
counts: z10.record(z10.number()),
|
|
4798
|
+
truncated: z10.boolean()
|
|
3933
4799
|
},
|
|
3934
4800
|
handler: (deps, args) => {
|
|
3935
|
-
const session = deps.sessions.resolve(
|
|
3936
|
-
const maxSteps =
|
|
3937
|
-
const settleMs =
|
|
3938
|
-
const scope =
|
|
4801
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4802
|
+
const maxSteps = asNumber(args["maxSteps"]);
|
|
4803
|
+
const settleMs = asNumber(args["settleMs"]);
|
|
4804
|
+
const scope = asString(args["scope"]);
|
|
3939
4805
|
const opts = {
|
|
3940
4806
|
...maxSteps !== void 0 ? { maxSteps } : {},
|
|
3941
4807
|
...settleMs !== void 0 ? { settleMs } : {},
|
|
3942
|
-
...scope !== void 0 ? { scope } : {}
|
|
4808
|
+
...scope !== void 0 ? { scope } : {},
|
|
4809
|
+
...args["confirmDangerous"] === true ? { confirmDangerous: true } : {}
|
|
3943
4810
|
};
|
|
3944
4811
|
return crawl(session, opts, nodeSleep2);
|
|
3945
4812
|
}
|
|
@@ -3947,7 +4814,7 @@ var CRAWL_TOOLS = [
|
|
|
3947
4814
|
];
|
|
3948
4815
|
|
|
3949
4816
|
// ../server/dist/input/scroll-tools.js
|
|
3950
|
-
import { z as
|
|
4817
|
+
import { z as z11 } from "zod";
|
|
3951
4818
|
|
|
3952
4819
|
// ../server/dist/input/scroll-find.js
|
|
3953
4820
|
async function queryFirst(session, q) {
|
|
@@ -3956,9 +4823,9 @@ async function queryFirst(session, q) {
|
|
|
3956
4823
|
value: q.value,
|
|
3957
4824
|
...q.name !== void 0 ? { name: q.name } : {}
|
|
3958
4825
|
});
|
|
3959
|
-
const elements =
|
|
4826
|
+
const elements = asRecord(res.result)["elements"];
|
|
3960
4827
|
if (Array.isArray(elements) && elements.length > 0)
|
|
3961
|
-
return
|
|
4828
|
+
return asRecord(elements[0]);
|
|
3962
4829
|
return void 0;
|
|
3963
4830
|
}
|
|
3964
4831
|
async function scrollToFind(session, q, opts = {}) {
|
|
@@ -3977,7 +4844,7 @@ async function scrollToFind(session, q, opts = {}) {
|
|
|
3977
4844
|
const hit = await queryFirst(session, q);
|
|
3978
4845
|
if (hit !== void 0)
|
|
3979
4846
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
3980
|
-
const data =
|
|
4847
|
+
const data = asRecord(sr.result);
|
|
3981
4848
|
if (data["atEnd"] === true || data["scrolled"] === false) {
|
|
3982
4849
|
return { found: false, scrolls, exhausted: true };
|
|
3983
4850
|
}
|
|
@@ -3985,7 +4852,7 @@ async function scrollToFind(session, q, opts = {}) {
|
|
|
3985
4852
|
for (let i = 0; i < max; i += 1) {
|
|
3986
4853
|
const sr = await session.command(IrisCommand.SCROLL, q.container !== void 0 ? { ref: q.container } : {});
|
|
3987
4854
|
scrolls += 1;
|
|
3988
|
-
const data =
|
|
4855
|
+
const data = asRecord(sr.result);
|
|
3989
4856
|
const hit = await queryFirst(session, q);
|
|
3990
4857
|
if (hit !== void 0)
|
|
3991
4858
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
@@ -4002,58 +4869,58 @@ var SCROLL_TOOLS = [
|
|
|
4002
4869
|
name: IrisTool.SCROLL_TO,
|
|
4003
4870
|
description: "Find an element in a VIRTUALIZED list that has not rendered yet. Pass `by` (role|text|testid|label|placeholder|alt) and `value` (query string) to identify the target row. Scrolls the container until the row mounts, the list ends, or maxScrolls (default 20) is spent. Pass targetIndex + totalCount for bisection \u2014 jumps directly to the estimated offset in one scroll (e.g. targetIndex:800 totalCount:1000 jumps to 80% of scrollHeight). Returns { found, element?, scrolls, exhausted }.",
|
|
4004
4871
|
inputSchema: {
|
|
4005
|
-
by:
|
|
4006
|
-
value:
|
|
4007
|
-
name:
|
|
4008
|
-
container:
|
|
4009
|
-
maxScrolls:
|
|
4010
|
-
targetIndex:
|
|
4011
|
-
totalCount:
|
|
4012
|
-
sessionId:
|
|
4872
|
+
by: z11.string().describe("Query strategy for finding the target: role | text | testid | label | placeholder | alt"),
|
|
4873
|
+
value: z11.string().describe("Query value for the selected strategy (the element to scroll into view)."),
|
|
4874
|
+
name: z11.string().optional().describe("Optional accessible name filter when using by=role."),
|
|
4875
|
+
container: z11.string().optional().describe("Element ref for the scrollable container. Omit to scroll the document."),
|
|
4876
|
+
maxScrolls: z11.number().optional().describe("Maximum number of scroll steps before giving up. Default: 20."),
|
|
4877
|
+
targetIndex: z11.number().optional().describe("Known row index of the target in the list. Combine with totalCount for bisection \u2014 jumps directly to the estimated offset."),
|
|
4878
|
+
totalCount: z11.number().optional().describe("Total item count in the virtualized list. Required for bisection with targetIndex."),
|
|
4879
|
+
sessionId: z11.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
4013
4880
|
},
|
|
4014
4881
|
outputSchema: {
|
|
4015
|
-
found:
|
|
4016
|
-
element:
|
|
4017
|
-
scrolls:
|
|
4018
|
-
exhausted:
|
|
4882
|
+
found: z11.boolean(),
|
|
4883
|
+
element: z11.object({ ref: z11.string(), role: z11.string(), name: z11.string() }).optional(),
|
|
4884
|
+
scrolls: z11.number(),
|
|
4885
|
+
exhausted: z11.boolean()
|
|
4019
4886
|
},
|
|
4020
4887
|
handler: (deps, args) => {
|
|
4021
|
-
const session = deps.sessions.resolve(
|
|
4022
|
-
const name =
|
|
4023
|
-
const container =
|
|
4024
|
-
const targetIndex =
|
|
4025
|
-
const totalCount =
|
|
4888
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4889
|
+
const name = asString(args["name"]);
|
|
4890
|
+
const container = asString(args["container"]);
|
|
4891
|
+
const targetIndex = asNumber(args["targetIndex"]);
|
|
4892
|
+
const totalCount = asNumber(args["totalCount"]);
|
|
4026
4893
|
const q = {
|
|
4027
|
-
by:
|
|
4028
|
-
value:
|
|
4894
|
+
by: asString(args["by"]) ?? "",
|
|
4895
|
+
value: asString(args["value"]) ?? "",
|
|
4029
4896
|
...name !== void 0 ? { name } : {},
|
|
4030
4897
|
...container !== void 0 ? { container } : {},
|
|
4031
4898
|
...targetIndex !== void 0 ? { targetIndex } : {},
|
|
4032
4899
|
...totalCount !== void 0 ? { totalCount } : {}
|
|
4033
4900
|
};
|
|
4034
|
-
const maxScrolls =
|
|
4901
|
+
const maxScrolls = asNumber(args["maxScrolls"]);
|
|
4035
4902
|
return scrollToFind(session, q, maxScrolls !== void 0 ? { maxScrolls } : {});
|
|
4036
4903
|
}
|
|
4037
4904
|
}
|
|
4038
4905
|
];
|
|
4039
4906
|
|
|
4040
4907
|
// ../server/dist/session/session-tools.js
|
|
4041
|
-
import { z as
|
|
4908
|
+
import { z as z12 } from "zod";
|
|
4042
4909
|
var SESSION_TOOLS = [
|
|
4043
4910
|
{
|
|
4044
4911
|
name: IrisTool.SESSION,
|
|
4045
4912
|
description: "Tune the presenter session for this app. { idleEndMs } sets how long the session stays open after you go quiet before it AUTO-ENDS (page glow off, the floating panel is kept so the human can read + Copy/Export the run). Default 5min. Raise it for slow apps, lower it for quick checks. The auto-end is enforced SERVER-SIDE (immune to background-tab throttling) and also fires if you (the MCP client) disconnect \u2014 so a forgotten or crashed session never leaves the HUD running forever. If you go quiet and then act again, the session revives automatically. Returns { applied, idleEndMs }.",
|
|
4046
4913
|
inputSchema: {
|
|
4047
|
-
idleEndMs:
|
|
4048
|
-
sessionId:
|
|
4914
|
+
idleEndMs: z12.number().optional().describe("Idle window in milliseconds after which the presenter session auto-ends. Default: 300000 (5 min). Raise for slow apps."),
|
|
4915
|
+
sessionId: z12.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
4049
4916
|
},
|
|
4050
4917
|
outputSchema: {
|
|
4051
|
-
applied:
|
|
4052
|
-
idleEndMs:
|
|
4918
|
+
applied: z12.boolean(),
|
|
4919
|
+
idleEndMs: z12.number().optional()
|
|
4053
4920
|
},
|
|
4054
4921
|
handler: async (deps, args) => {
|
|
4055
|
-
const session = deps.sessions.resolve(
|
|
4056
|
-
const idleEndMs =
|
|
4922
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4923
|
+
const idleEndMs = asNumber(args["idleEndMs"]);
|
|
4057
4924
|
if (idleEndMs !== void 0)
|
|
4058
4925
|
session.setIdleEndMs(idleEndMs);
|
|
4059
4926
|
const res = await session.command(IrisCommand.SESSION_CONFIG, idleEndMs !== void 0 ? { idleEndMs } : {});
|
|
@@ -4065,7 +4932,7 @@ var SESSION_TOOLS = [
|
|
|
4065
4932
|
];
|
|
4066
4933
|
|
|
4067
4934
|
// ../server/dist/flows/annotate-tools.js
|
|
4068
|
-
import { z as
|
|
4935
|
+
import { z as z13 } from "zod";
|
|
4069
4936
|
|
|
4070
4937
|
// ../server/dist/flows/annotate.js
|
|
4071
4938
|
function compileAnnotation(a, stepCount) {
|
|
@@ -4142,23 +5009,23 @@ var ANNOTATE_TOOLS = [
|
|
|
4142
5009
|
description: "Attach a STRUCTURED annotation to the active recording, compiling it into the flow. kind: assert-signal { name, dataMatches? } \u2192 the last step asserts that signal; assert-visible { testid } \u2192 the last step asserts that element is present; mark-dynamic { testid } \u2192 the flow records that region as LLM-dynamic (replay won't assert its content); success-state { signal | testid } \u2192 the flow golden end-condition. Folded onto disk by iris_flow_save. Returns { ok:true, target:step|flow, compiled } (e.g. \"will assert signal diff:shown\") or { ok:false, code } (annotate_no_recording | annotate_no_step | annotate_unknown_kind | annotate_missing_field). FIRST CUT: structured only \u2014 a free natural-language string is rejected (annotate_unknown_kind), never guessed into a predicate. Pass `flow` to target a named recording (defaults to 'default'); `name` is the assert-signal's SIGNAL name, not the recording.",
|
|
4143
5010
|
inputSchema: {
|
|
4144
5011
|
// `flow` selects the recording; `name`/`signal`/`testid`/`dataMatches` are the annotation fields.
|
|
4145
|
-
flow:
|
|
4146
|
-
kind:
|
|
4147
|
-
name:
|
|
4148
|
-
testid:
|
|
4149
|
-
signal:
|
|
4150
|
-
dataMatches:
|
|
4151
|
-
sessionId:
|
|
4152
|
-
annotation:
|
|
5012
|
+
flow: z13.string().optional().describe("Named recording to annotate. Defaults to 'default'."),
|
|
5013
|
+
kind: z13.string().describe("Annotation kind: assert-signal | assert-visible | mark-dynamic | success-state."),
|
|
5014
|
+
name: z13.string().optional().describe("Signal name for assert-signal annotations."),
|
|
5015
|
+
testid: z13.string().optional().describe("data-testid value for assert-visible / mark-dynamic / success-state annotations."),
|
|
5016
|
+
signal: z13.string().optional().describe("Signal name for success-state annotations."),
|
|
5017
|
+
dataMatches: z13.record(z13.unknown()).optional().describe("Key/value pairs the signal payload must match (assert-signal only)."),
|
|
5018
|
+
sessionId: z13.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open."),
|
|
5019
|
+
annotation: z13.unknown().optional().describe("Structured annotation: { kind, name, dataMatches? } for assert-signal; { kind, testid } for assert-visible / mark-dynamic; { kind, signal?, testid? } for success-state.")
|
|
4153
5020
|
},
|
|
4154
5021
|
outputSchema: {
|
|
4155
|
-
ok:
|
|
4156
|
-
target:
|
|
4157
|
-
compiled:
|
|
4158
|
-
code:
|
|
5022
|
+
ok: z13.boolean(),
|
|
5023
|
+
target: z13.string().optional(),
|
|
5024
|
+
compiled: z13.string().optional(),
|
|
5025
|
+
code: z13.string().optional()
|
|
4159
5026
|
},
|
|
4160
5027
|
handler: (deps, args) => {
|
|
4161
|
-
const name =
|
|
5028
|
+
const name = asString(args["flow"]) ?? DEFAULT_RECORDING;
|
|
4162
5029
|
const parsed = AnnotationSchema.safeParse(args);
|
|
4163
5030
|
if (!parsed.success) {
|
|
4164
5031
|
return Promise.resolve({ ok: false, code: AnnotationErrorCode.UNKNOWN_KIND });
|
|
@@ -4187,19 +5054,19 @@ var ANNOTATE_TOOLS = [
|
|
|
4187
5054
|
];
|
|
4188
5055
|
|
|
4189
5056
|
// ../server/dist/session/live-control-tools.js
|
|
4190
|
-
import { z as
|
|
5057
|
+
import { z as z14 } from "zod";
|
|
4191
5058
|
var sessionIdShape5 = {
|
|
4192
|
-
sessionId:
|
|
5059
|
+
sessionId: z14.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
4193
5060
|
};
|
|
4194
5061
|
var LIVE_CONTROL_TOOLS = [
|
|
4195
5062
|
{
|
|
4196
5063
|
name: IrisTool.END_SESSION,
|
|
4197
5064
|
description: 'End this live testing session. Sets the server state to "ended", tells the human panel (PRESENTER), and stops driving. Optional `summary` is shown in the panel. Idempotent.',
|
|
4198
|
-
inputSchema: { summary:
|
|
4199
|
-
outputSchema: { ended:
|
|
5065
|
+
inputSchema: { summary: z14.string().optional(), ...sessionIdShape5 },
|
|
5066
|
+
outputSchema: { ended: z14.boolean(), sessionId: z14.string() },
|
|
4200
5067
|
handler: (deps, args) => {
|
|
4201
|
-
const session = deps.sessions.resolve(
|
|
4202
|
-
session.setState(SessionState.ENDED,
|
|
5068
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5069
|
+
session.setState(SessionState.ENDED, asString(args["summary"]));
|
|
4203
5070
|
return Promise.resolve({ ended: true, sessionId: session.id });
|
|
4204
5071
|
}
|
|
4205
5072
|
},
|
|
@@ -4207,9 +5074,9 @@ var LIVE_CONTROL_TOOLS = [
|
|
|
4207
5074
|
name: IrisTool.RESUME,
|
|
4208
5075
|
description: 'Clear a human pause and resume driving the page. Sets state "active" and syncs the panel (PRESENTER). Call after you have addressed the human guidance returned by a paused iris_act.',
|
|
4209
5076
|
inputSchema: { ...sessionIdShape5 },
|
|
4210
|
-
outputSchema: { ok:
|
|
5077
|
+
outputSchema: { ok: z14.boolean() },
|
|
4211
5078
|
handler: (deps, args) => {
|
|
4212
|
-
const session = deps.sessions.resolve(
|
|
5079
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4213
5080
|
session.setState(SessionState.ACTIVE);
|
|
4214
5081
|
return Promise.resolve({ ok: true });
|
|
4215
5082
|
}
|
|
@@ -4218,9 +5085,9 @@ var LIVE_CONTROL_TOOLS = [
|
|
|
4218
5085
|
name: IrisTool.MESSAGES,
|
|
4219
5086
|
description: "Drain and return any messages the human queued from the panel since the last poll. Use to explicitly check for human guidance without acting.",
|
|
4220
5087
|
inputSchema: { ...sessionIdShape5 },
|
|
4221
|
-
outputSchema: { messages:
|
|
5088
|
+
outputSchema: { messages: z14.array(z14.unknown()) },
|
|
4222
5089
|
handler: (deps, args) => {
|
|
4223
|
-
const session = deps.sessions.resolve(
|
|
5090
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4224
5091
|
return Promise.resolve({ messages: session.drainInbox() });
|
|
4225
5092
|
}
|
|
4226
5093
|
}
|
|
@@ -4245,6 +5112,266 @@ function withControl(session, result) {
|
|
|
4245
5112
|
return control === void 0 ? result : { ...result, control };
|
|
4246
5113
|
}
|
|
4247
5114
|
|
|
5115
|
+
// ../server/dist/update/update-tools.js
|
|
5116
|
+
import { z as z15 } from "zod";
|
|
5117
|
+
|
|
5118
|
+
// ../server/dist/update/update-checker.js
|
|
5119
|
+
import * as fs from "fs";
|
|
5120
|
+
import * as https from "https";
|
|
5121
|
+
import { join as join2 } from "path";
|
|
5122
|
+
import { homedir } from "os";
|
|
5123
|
+
var IRIS_HOME = join2(homedir(), ".iris");
|
|
5124
|
+
var MANIFEST_PATH = join2(IRIS_HOME, "update-manifest.json");
|
|
5125
|
+
var NPM_REGISTRY = "https://registry.npmjs.org/@syrin/iris/latest";
|
|
5126
|
+
function loadManifest() {
|
|
5127
|
+
if (!fs.existsSync(MANIFEST_PATH))
|
|
5128
|
+
return null;
|
|
5129
|
+
try {
|
|
5130
|
+
const raw = fs.readFileSync(MANIFEST_PATH, "utf8");
|
|
5131
|
+
return JSON.parse(raw);
|
|
5132
|
+
} catch {
|
|
5133
|
+
return null;
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
function saveManifest(manifest) {
|
|
5137
|
+
fs.mkdirSync(IRIS_HOME, { recursive: true });
|
|
5138
|
+
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2), "utf8");
|
|
5139
|
+
}
|
|
5140
|
+
function isCacheFresh(manifest) {
|
|
5141
|
+
const checked = new Date(manifest.lastChecked).getTime();
|
|
5142
|
+
return Date.now() - checked < UpdateCheckIntervalMs;
|
|
5143
|
+
}
|
|
5144
|
+
function fetchNpmInfo() {
|
|
5145
|
+
return new Promise((resolve, reject) => {
|
|
5146
|
+
const req = https.get(NPM_REGISTRY, (res) => {
|
|
5147
|
+
let body = "";
|
|
5148
|
+
res.setEncoding("utf8");
|
|
5149
|
+
res.on("data", (chunk) => {
|
|
5150
|
+
body += chunk;
|
|
5151
|
+
});
|
|
5152
|
+
res.on("end", () => {
|
|
5153
|
+
try {
|
|
5154
|
+
resolve(JSON.parse(body));
|
|
5155
|
+
} catch (err) {
|
|
5156
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
5157
|
+
}
|
|
5158
|
+
});
|
|
5159
|
+
res.on("error", reject);
|
|
5160
|
+
});
|
|
5161
|
+
req.setTimeout(5e3, () => {
|
|
5162
|
+
req.destroy();
|
|
5163
|
+
reject(new Error("npm registry request timed out"));
|
|
5164
|
+
});
|
|
5165
|
+
req.on("error", reject);
|
|
5166
|
+
});
|
|
5167
|
+
}
|
|
5168
|
+
async function checkForUpdate(currentVersion) {
|
|
5169
|
+
const cached = loadManifest();
|
|
5170
|
+
if (cached !== null && cached.currentVersion === currentVersion && isCacheFresh(cached)) {
|
|
5171
|
+
return cached;
|
|
5172
|
+
}
|
|
5173
|
+
try {
|
|
5174
|
+
const info = await fetchNpmInfo();
|
|
5175
|
+
const updateAvailable = info.version !== currentVersion;
|
|
5176
|
+
const manifest = {
|
|
5177
|
+
currentVersion,
|
|
5178
|
+
latestVersion: info.version,
|
|
5179
|
+
updateAvailable,
|
|
5180
|
+
lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5181
|
+
...info.iris?.changelog !== void 0 ? { changelog: info.iris.changelog } : {},
|
|
5182
|
+
...info.iris?.breakingChanges !== void 0 ? { breakingChanges: info.iris.breakingChanges } : {},
|
|
5183
|
+
...cached?.previousVersion !== void 0 ? { previousVersion: cached.previousVersion } : {}
|
|
5184
|
+
};
|
|
5185
|
+
saveManifest(manifest);
|
|
5186
|
+
return manifest;
|
|
5187
|
+
} catch (err) {
|
|
5188
|
+
log("iris_update_check_failed", {
|
|
5189
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5190
|
+
});
|
|
5191
|
+
if (cached !== null)
|
|
5192
|
+
return { ...cached, currentVersion };
|
|
5193
|
+
return {
|
|
5194
|
+
currentVersion,
|
|
5195
|
+
updateAvailable: false,
|
|
5196
|
+
lastChecked: (/* @__PURE__ */ new Date()).toISOString()
|
|
5197
|
+
};
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
|
|
5201
|
+
// ../server/dist/update/updater.js
|
|
5202
|
+
import { execFile } from "child_process";
|
|
5203
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5204
|
+
import { platform } from "os";
|
|
5205
|
+
import { dirname, join as join3 } from "path";
|
|
5206
|
+
var NPM_BIN = platform() === "win32" ? "npm.cmd" : "npm";
|
|
5207
|
+
var NPM_TIMEOUT_MS = 12e4;
|
|
5208
|
+
var ExecutionKind = {
|
|
5209
|
+
/** Launched via `npx @syrin/iris` — npm re-resolves the package on restart. */
|
|
5210
|
+
NPX: "npx",
|
|
5211
|
+
/** Installed globally via `npm install -g`. */
|
|
5212
|
+
GLOBAL: "global",
|
|
5213
|
+
/** Installed as a local project dependency. */
|
|
5214
|
+
LOCAL: "local"
|
|
5215
|
+
};
|
|
5216
|
+
function detectExecutionKind() {
|
|
5217
|
+
const script = process.argv[1] ?? "";
|
|
5218
|
+
if (script.includes("/_npx/") || script.includes("\\_npx\\"))
|
|
5219
|
+
return ExecutionKind.NPX;
|
|
5220
|
+
if (script.includes("/node_modules/") || script.includes("\\node_modules\\")) {
|
|
5221
|
+
return ExecutionKind.LOCAL;
|
|
5222
|
+
}
|
|
5223
|
+
return ExecutionKind.GLOBAL;
|
|
5224
|
+
}
|
|
5225
|
+
function findLocalProjectRoot() {
|
|
5226
|
+
let dir = process.cwd();
|
|
5227
|
+
for (; ; ) {
|
|
5228
|
+
if (existsSync2(join3(dir, "package.json")))
|
|
5229
|
+
return dir;
|
|
5230
|
+
const parent = dirname(dir);
|
|
5231
|
+
if (parent === dir)
|
|
5232
|
+
return null;
|
|
5233
|
+
dir = parent;
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
function runNpm(args, opts = {}) {
|
|
5237
|
+
return new Promise((resolve, reject) => {
|
|
5238
|
+
execFile(NPM_BIN, args, { timeout: NPM_TIMEOUT_MS, ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {} }, (err, _stdout, stderr) => {
|
|
5239
|
+
if (err !== null) {
|
|
5240
|
+
reject(new Error(`npm ${args.join(" ")} failed: ${stderr !== "" ? stderr : err.message}`));
|
|
5241
|
+
} else {
|
|
5242
|
+
resolve();
|
|
5243
|
+
}
|
|
5244
|
+
});
|
|
5245
|
+
});
|
|
5246
|
+
}
|
|
5247
|
+
async function installVersion(version, kind) {
|
|
5248
|
+
const pkg = `@syrin/iris@${version}`;
|
|
5249
|
+
if (kind === ExecutionKind.NPX) {
|
|
5250
|
+
log("iris_update_npx_strategy", {
|
|
5251
|
+
note: "Running via npx \u2014 exiting so Claude Code restarts and npx fetches the new version"
|
|
5252
|
+
});
|
|
5253
|
+
return;
|
|
5254
|
+
}
|
|
5255
|
+
if (kind === ExecutionKind.LOCAL) {
|
|
5256
|
+
const root = findLocalProjectRoot();
|
|
5257
|
+
if (root !== null) {
|
|
5258
|
+
await runNpm(["install", pkg], { cwd: root });
|
|
5259
|
+
return;
|
|
5260
|
+
}
|
|
5261
|
+
log("iris_update_local_no_root", { fallback: "global" });
|
|
5262
|
+
}
|
|
5263
|
+
await runNpm(["install", "-g", pkg]);
|
|
5264
|
+
}
|
|
5265
|
+
async function installVersionRollback(version, kind) {
|
|
5266
|
+
if (kind === ExecutionKind.NPX) {
|
|
5267
|
+
log("iris_rollback_npx_strategy", {
|
|
5268
|
+
note: "Running via npx \u2014 update your .mcp.json args to pin the version you want to restore"
|
|
5269
|
+
});
|
|
5270
|
+
return;
|
|
5271
|
+
}
|
|
5272
|
+
await installVersion(version, kind);
|
|
5273
|
+
}
|
|
5274
|
+
async function applyUpdate(targetVersion) {
|
|
5275
|
+
const manifest = loadManifest();
|
|
5276
|
+
if (manifest !== null) {
|
|
5277
|
+
saveManifest({ ...manifest, previousVersion: manifest.currentVersion });
|
|
5278
|
+
}
|
|
5279
|
+
const kind = detectExecutionKind();
|
|
5280
|
+
log("iris_update_applying", { version: targetVersion, executionKind: kind });
|
|
5281
|
+
await installVersion(targetVersion, kind);
|
|
5282
|
+
log("iris_update_applied", { version: targetVersion, executionKind: kind });
|
|
5283
|
+
process.exit(0);
|
|
5284
|
+
}
|
|
5285
|
+
async function rollback() {
|
|
5286
|
+
const manifest = loadManifest();
|
|
5287
|
+
if (manifest === null || manifest.previousVersion === void 0) {
|
|
5288
|
+
throw new Error("No previous version available for rollback");
|
|
5289
|
+
}
|
|
5290
|
+
const prev = manifest.previousVersion;
|
|
5291
|
+
const kind = detectExecutionKind();
|
|
5292
|
+
log("iris_rollback_applying", { version: prev, executionKind: kind });
|
|
5293
|
+
await installVersionRollback(prev, kind);
|
|
5294
|
+
log("iris_rollback_applied", { version: prev, executionKind: kind });
|
|
5295
|
+
process.exit(0);
|
|
5296
|
+
}
|
|
5297
|
+
|
|
5298
|
+
// ../server/dist/server-version.js
|
|
5299
|
+
import { createRequire } from "module";
|
|
5300
|
+
var _pkg = createRequire(import.meta.url)("../package.json");
|
|
5301
|
+
var SERVER_VERSION = _pkg.version;
|
|
5302
|
+
|
|
5303
|
+
// ../server/dist/update/update-tools.js
|
|
5304
|
+
var UPDATE_TOOLS = [
|
|
5305
|
+
{
|
|
5306
|
+
name: IrisTool.VERSION_INFO,
|
|
5307
|
+
description: "Returns the running Iris version, latest available version, release changelog, and any breaking changes. Call this at the start of a session or when unexpected tool behavior suggests a version mismatch.",
|
|
5308
|
+
inputSchema: {},
|
|
5309
|
+
outputSchema: {
|
|
5310
|
+
currentVersion: z15.string().describe("The Iris server version currently running."),
|
|
5311
|
+
latestVersion: z15.string().optional().describe("Latest published version on npm."),
|
|
5312
|
+
updateAvailable: z15.boolean().describe("True when a newer version is available to install."),
|
|
5313
|
+
executionKind: z15.string().describe('How iris was launched: "npx" (no install needed \u2014 restart applies update), "global" (npm install -g), or "local" (project node_modules).'),
|
|
5314
|
+
changelog: z15.string().optional().describe("Release notes for the latest version."),
|
|
5315
|
+
breakingChanges: z15.array(z15.string()).optional().describe("Breaking changes in the latest version that may affect your scripts."),
|
|
5316
|
+
rollbackAvailable: z15.boolean().describe("True when a previous version is stored and can be restored."),
|
|
5317
|
+
previousVersion: z15.string().optional().describe("The version that would be restored on rollback.")
|
|
5318
|
+
},
|
|
5319
|
+
handler: async (_deps) => {
|
|
5320
|
+
const manifest = await checkForUpdate(SERVER_VERSION);
|
|
5321
|
+
return {
|
|
5322
|
+
currentVersion: manifest.currentVersion,
|
|
5323
|
+
...manifest.latestVersion !== void 0 ? { latestVersion: manifest.latestVersion } : {},
|
|
5324
|
+
updateAvailable: manifest.updateAvailable,
|
|
5325
|
+
executionKind: detectExecutionKind(),
|
|
5326
|
+
...manifest.changelog !== void 0 ? { changelog: manifest.changelog } : {},
|
|
5327
|
+
...manifest.breakingChanges !== void 0 ? { breakingChanges: manifest.breakingChanges } : {},
|
|
5328
|
+
rollbackAvailable: manifest.previousVersion !== void 0,
|
|
5329
|
+
...manifest.previousVersion !== void 0 ? { previousVersion: manifest.previousVersion } : {}
|
|
5330
|
+
};
|
|
5331
|
+
}
|
|
5332
|
+
},
|
|
5333
|
+
{
|
|
5334
|
+
name: IrisTool.APPLY_UPDATE,
|
|
5335
|
+
description: 'Install the latest Iris server version and restart. Strategy depends on how iris was launched (check executionKind from iris_version_info): "global" and "local" installs run npm install then exit; "npx" just exits \u2014 Claude Code restarts and npx re-resolves the latest version from npm automatically. The MCP connection briefly drops during restart.',
|
|
5336
|
+
inputSchema: {
|
|
5337
|
+
confirm: z15.boolean().describe("Set to true to confirm the update should be applied. Required to prevent accidental upgrades.")
|
|
5338
|
+
},
|
|
5339
|
+
outputSchema: {
|
|
5340
|
+
ok: z15.boolean(),
|
|
5341
|
+
message: z15.string().optional()
|
|
5342
|
+
},
|
|
5343
|
+
handler: async (_deps, args) => {
|
|
5344
|
+
if (args["confirm"] !== true) {
|
|
5345
|
+
return { ok: false, message: "Set confirm:true to apply the update" };
|
|
5346
|
+
}
|
|
5347
|
+
const manifest = await checkForUpdate(SERVER_VERSION);
|
|
5348
|
+
if (!manifest.updateAvailable || manifest.latestVersion === void 0) {
|
|
5349
|
+
return { ok: false, message: "No update available \u2014 already on the latest version" };
|
|
5350
|
+
}
|
|
5351
|
+
await applyUpdate(manifest.latestVersion);
|
|
5352
|
+
return { ok: true };
|
|
5353
|
+
}
|
|
5354
|
+
},
|
|
5355
|
+
{
|
|
5356
|
+
name: IrisTool.ROLLBACK,
|
|
5357
|
+
description: "Restore the previous Iris server version and restart. Use when an update introduced a regression. The MCP connection will briefly drop \u2014 Claude Code restarts the process automatically with the restored binary.",
|
|
5358
|
+
inputSchema: {
|
|
5359
|
+
confirm: z15.boolean().describe("Set to true to confirm the rollback. Required to prevent accidental downgrades.")
|
|
5360
|
+
},
|
|
5361
|
+
outputSchema: {
|
|
5362
|
+
ok: z15.boolean(),
|
|
5363
|
+
message: z15.string().optional()
|
|
5364
|
+
},
|
|
5365
|
+
handler: async (_deps, args) => {
|
|
5366
|
+
if (args["confirm"] !== true) {
|
|
5367
|
+
return { ok: false, message: "Set confirm:true to apply the rollback" };
|
|
5368
|
+
}
|
|
5369
|
+
await rollback();
|
|
5370
|
+
return { ok: true };
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
];
|
|
5374
|
+
|
|
4248
5375
|
// ../server/dist/tools/tools.js
|
|
4249
5376
|
async function snapshotTree(deps, sessionId) {
|
|
4250
5377
|
const session = deps.sessions.resolve(sessionId);
|
|
@@ -4255,7 +5382,7 @@ async function snapshotTree(deps, sessionId) {
|
|
|
4255
5382
|
return { lines: normalizeLines(snap.tree ?? ""), route: snap.status?.route ?? "" };
|
|
4256
5383
|
}
|
|
4257
5384
|
var sessionIdShape6 = {
|
|
4258
|
-
sessionId:
|
|
5385
|
+
sessionId: z16.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open \u2014 Iris resolves it automatically.")
|
|
4259
5386
|
};
|
|
4260
5387
|
async function commandOrThrow3(deps, sessionId, name, args) {
|
|
4261
5388
|
const session = deps.sessions.resolve(sessionId);
|
|
@@ -4265,11 +5392,11 @@ async function commandOrThrow3(deps, sessionId, name, args) {
|
|
|
4265
5392
|
return result.result;
|
|
4266
5393
|
}
|
|
4267
5394
|
function asBox2(value) {
|
|
4268
|
-
const b =
|
|
4269
|
-
const x =
|
|
4270
|
-
const y =
|
|
4271
|
-
const w =
|
|
4272
|
-
const h =
|
|
5395
|
+
const b = asRecord(asRecord(value)["box"]);
|
|
5396
|
+
const x = asNumber(b["x"]);
|
|
5397
|
+
const y = asNumber(b["y"]);
|
|
5398
|
+
const w = asNumber(b["width"]);
|
|
5399
|
+
const h = asNumber(b["height"]);
|
|
4273
5400
|
if (x === void 0 || y === void 0 || w === void 0 || h === void 0)
|
|
4274
5401
|
return void 0;
|
|
4275
5402
|
if (w <= 0 || h <= 0)
|
|
@@ -4285,29 +5412,51 @@ async function tryRealInput(deps, session, ref, action, args) {
|
|
|
4285
5412
|
return synthetic();
|
|
4286
5413
|
if (!isPointerAction(action))
|
|
4287
5414
|
return synthetic(InputModeReason.NOT_POINTER);
|
|
4288
|
-
const inner =
|
|
5415
|
+
const inner = asRecord(args["args"]);
|
|
4289
5416
|
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && inner["native"] !== true) {
|
|
4290
5417
|
return synthetic(InputModeReason.SYNTHETIC_CLICK_PREFERRED);
|
|
4291
5418
|
}
|
|
4292
5419
|
if (!await provider.isAvailableFor(session.url))
|
|
4293
5420
|
return synthetic(InputModeReason.PAGE_NOT_CORRELATED);
|
|
4294
|
-
const
|
|
5421
|
+
const inspected = await commandOrThrow3(deps, session.id, IrisCommand.INSPECT, { ref });
|
|
5422
|
+
const confirmed = inner[DANGEROUS_ACTION_CONFIRM_ARG] === true;
|
|
5423
|
+
const dangerousDescriptorText = (value2) => {
|
|
5424
|
+
const descriptor = asRecord(value2);
|
|
5425
|
+
return [
|
|
5426
|
+
asString(descriptor["name"]) ?? "",
|
|
5427
|
+
asString(descriptor["text"]) ?? "",
|
|
5428
|
+
asString(descriptor["value"]) ?? "",
|
|
5429
|
+
asString(descriptor["href"]) ?? "",
|
|
5430
|
+
asString(descriptor["formAction"]) ?? "",
|
|
5431
|
+
asString(descriptor["formText"]) ?? ""
|
|
5432
|
+
].join(" ");
|
|
5433
|
+
};
|
|
5434
|
+
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && !confirmed && isDangerousActionText(dangerousDescriptorText(inspected))) {
|
|
5435
|
+
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
5436
|
+
}
|
|
5437
|
+
const box = asBox2(inspected);
|
|
4295
5438
|
if (box === void 0)
|
|
4296
5439
|
return synthetic(InputModeReason.ELEMENT_NOT_LOCATABLE);
|
|
4297
5440
|
let toBox;
|
|
4298
5441
|
if (action === ActionType.DRAG) {
|
|
4299
|
-
const toRef =
|
|
5442
|
+
const toRef = asString(inner["toRef"]);
|
|
4300
5443
|
if (toRef === void 0)
|
|
4301
5444
|
return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
|
|
4302
|
-
|
|
5445
|
+
const targetInspected = await commandOrThrow3(deps, session.id, IrisCommand.INSPECT, {
|
|
5446
|
+
ref: toRef
|
|
5447
|
+
});
|
|
5448
|
+
if (!confirmed && isDangerousActionText(`${dangerousDescriptorText(inspected)} ${dangerousDescriptorText(targetInspected)}`)) {
|
|
5449
|
+
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
5450
|
+
}
|
|
5451
|
+
toBox = asBox2(targetInspected);
|
|
4303
5452
|
if (toBox === void 0)
|
|
4304
5453
|
return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
|
|
4305
5454
|
}
|
|
4306
5455
|
const performArgs = {};
|
|
4307
|
-
const value =
|
|
5456
|
+
const value = asString(inner["value"]);
|
|
4308
5457
|
if (value !== void 0)
|
|
4309
5458
|
performArgs.value = value;
|
|
4310
|
-
const text =
|
|
5459
|
+
const text = asString(inner["text"]);
|
|
4311
5460
|
if (text !== void 0)
|
|
4312
5461
|
performArgs.text = text;
|
|
4313
5462
|
if (toBox !== void 0)
|
|
@@ -4326,23 +5475,24 @@ async function tryRealInput(deps, session, ref, action, args) {
|
|
|
4326
5475
|
};
|
|
4327
5476
|
}
|
|
4328
5477
|
}
|
|
5478
|
+
var SNAPSHOT_CACHE = new SnapshotCache();
|
|
4329
5479
|
var TOOLS = [
|
|
4330
5480
|
{
|
|
4331
5481
|
name: IrisTool.SESSIONS,
|
|
4332
5482
|
description: "List connected browser sessions (tab url/title, sessionId, last-seen, health: hidden/focused/throttled, and `realInputAvailable` \u2014 true when native CDP/launched real input is driving this tab), plus a `recommendation` pointing to `iris drive` when a tab is hidden/throttled and may be un-scriptable from here.",
|
|
4333
5483
|
inputSchema: {},
|
|
4334
5484
|
outputSchema: {
|
|
4335
|
-
sessions:
|
|
4336
|
-
sessionId:
|
|
4337
|
-
url:
|
|
4338
|
-
title:
|
|
4339
|
-
lastSeenMs:
|
|
4340
|
-
throttled:
|
|
4341
|
-
focused:
|
|
4342
|
-
hidden:
|
|
4343
|
-
realInputAvailable:
|
|
4344
|
-
stale:
|
|
4345
|
-
recommendation:
|
|
5485
|
+
sessions: z16.array(z16.object({
|
|
5486
|
+
sessionId: z16.string(),
|
|
5487
|
+
url: z16.string(),
|
|
5488
|
+
title: z16.string().optional(),
|
|
5489
|
+
lastSeenMs: z16.number(),
|
|
5490
|
+
throttled: z16.boolean(),
|
|
5491
|
+
focused: z16.boolean(),
|
|
5492
|
+
hidden: z16.boolean(),
|
|
5493
|
+
realInputAvailable: z16.boolean().optional(),
|
|
5494
|
+
stale: z16.boolean().optional(),
|
|
5495
|
+
recommendation: z16.string().optional()
|
|
4346
5496
|
})).describe("Connected browser sessions with health state.")
|
|
4347
5497
|
},
|
|
4348
5498
|
handler: async (deps) => {
|
|
@@ -4356,71 +5506,95 @@ var TOOLS = [
|
|
|
4356
5506
|
},
|
|
4357
5507
|
{
|
|
4358
5508
|
name: IrisTool.SNAPSHOT,
|
|
4359
|
-
description: "Semantic accessibility snapshot of the page or a subtree. mode: full|interactive|status. Use to see what is on screen right now.",
|
|
5509
|
+
description: "Semantic accessibility snapshot of the page or a subtree. mode: full|interactive|status. Use to see what is on screen right now. The result carries cost:{ bytes, tokens } (estimated) \u2014 if it is large, re-scope (pass `scope`) or use mode:interactive/status instead of reading the whole tree. Pass diff:true after your first snapshot to get back ONLY what changed since your last look (mode:delta with added/removed, or mode:unchanged) \u2014 far fewer tokens and no stale tree to mis-read; a route change resets it to a full snapshot automatically.",
|
|
4360
5510
|
inputSchema: {
|
|
4361
|
-
scope:
|
|
4362
|
-
mode:
|
|
5511
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the snapshot to a subtree. Omit to snapshot the whole page."),
|
|
5512
|
+
mode: z16.nativeEnum(SnapshotMode).optional().describe("full = all elements; interactive = only clickable/focusable elements; status = only route + title. Default: full."),
|
|
5513
|
+
diff: z16.boolean().optional().describe("Return only what changed since your last snapshot of the same scope/mode (mode:delta|unchanged). First call (or after a route change) still returns the full tree."),
|
|
4363
5514
|
...sessionIdShape6
|
|
4364
5515
|
},
|
|
4365
5516
|
outputSchema: {
|
|
4366
|
-
tree:
|
|
4367
|
-
status:
|
|
5517
|
+
tree: z16.string().optional().describe("Indented ARIA tree of every element on the page (or the scoped subtree)."),
|
|
5518
|
+
status: z16.object({ route: z16.string(), title: z16.string().optional() }).optional(),
|
|
5519
|
+
mode: z16.string().optional().describe("delta | unchanged when diff:true returned a change set."),
|
|
5520
|
+
delta: z16.object({
|
|
5521
|
+
added: z16.array(z16.string()),
|
|
5522
|
+
removed: z16.array(z16.string()),
|
|
5523
|
+
addedCount: z16.number(),
|
|
5524
|
+
removedCount: z16.number()
|
|
5525
|
+
}).optional().describe("Only present on a diff:true call that found changes."),
|
|
5526
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional().describe("Estimated size of this result \u2014 re-scope if large.")
|
|
4368
5527
|
},
|
|
4369
|
-
handler: (deps, args) =>
|
|
4370
|
-
|
|
4371
|
-
mode
|
|
4372
|
-
|
|
5528
|
+
handler: (deps, args) => {
|
|
5529
|
+
const sessionId = asString(args["sessionId"]);
|
|
5530
|
+
const mode = asString(args["mode"]) ?? SnapshotMode.FULL;
|
|
5531
|
+
return commandOrThrow3(deps, sessionId, IrisCommand.SNAPSHOT, {
|
|
5532
|
+
scope: args["scope"],
|
|
5533
|
+
mode
|
|
5534
|
+
}).then((raw) => withSizeCost(applySnapshotDelta(raw, {
|
|
5535
|
+
sessionId: sessionId ?? "default",
|
|
5536
|
+
scope: asString(args["scope"]) ?? "",
|
|
5537
|
+
mode,
|
|
5538
|
+
diff: args["diff"] === true
|
|
5539
|
+
}, SNAPSHOT_CACHE)));
|
|
5540
|
+
}
|
|
4373
5541
|
},
|
|
4374
5542
|
{
|
|
4375
5543
|
name: IrisTool.QUERY,
|
|
4376
|
-
description: "Find elements by Testing-Library semantics. Pass `by` (role|text|label|placeholder|testid|alt) and `value` (the query string). Returns matching refs + descriptors + visibility. On zero matches, also returns hint:{ route, presentTestids[], knownEmptyState } so you can distinguish an empty state from a missing element WITHOUT taking a snapshot.",
|
|
5544
|
+
description: "Find elements by Testing-Library semantics. Pass `by` (role|text|label|placeholder|testid|alt) and `value` (the query string). Returns matching refs + descriptors + visibility. Pass `limit` to cap descriptors (broad role queries can be large) or `count_only:true` for just the match count \u2014 both cut tokens. On zero matches, also returns hint:{ route, presentTestids[], knownEmptyState } so you can distinguish an empty state from a missing element WITHOUT taking a snapshot.",
|
|
4377
5545
|
inputSchema: {
|
|
4378
|
-
by:
|
|
4379
|
-
value:
|
|
4380
|
-
name:
|
|
4381
|
-
scope:
|
|
5546
|
+
by: z16.string().describe("Query strategy: role | text | label | placeholder | testid | alt"),
|
|
5547
|
+
value: z16.string().describe("Query value for the selected strategy (e.g. by=role value=button, or by=testid value=submit-btn)."),
|
|
5548
|
+
name: z16.string().optional().describe("Accessible name filter \u2014 narrows results when `by` is role and the page has many elements of that role."),
|
|
5549
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the search to a subtree."),
|
|
5550
|
+
limit: z16.number().optional().describe("Cap the returned descriptors to the first N (cuts tokens on broad queries). If more matched, the result carries total + truncated:true so the trim is never silent \u2014 narrow with name/scope."),
|
|
5551
|
+
count_only: z16.boolean().optional().describe('Return just { count } (no element descriptors) \u2014 use when you only need "how many match?" and not their refs.'),
|
|
4382
5552
|
...sessionIdShape6
|
|
4383
5553
|
},
|
|
4384
5554
|
outputSchema: {
|
|
4385
|
-
elements:
|
|
4386
|
-
ref:
|
|
4387
|
-
role:
|
|
4388
|
-
name:
|
|
4389
|
-
value:
|
|
4390
|
-
states:
|
|
4391
|
-
visible:
|
|
4392
|
-
})),
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
5555
|
+
elements: z16.array(z16.object({
|
|
5556
|
+
ref: z16.string(),
|
|
5557
|
+
role: z16.string(),
|
|
5558
|
+
name: z16.string(),
|
|
5559
|
+
value: z16.string().optional(),
|
|
5560
|
+
states: z16.array(z16.string()),
|
|
5561
|
+
visible: z16.boolean()
|
|
5562
|
+
})).optional(),
|
|
5563
|
+
count: z16.number().optional().describe("Match count \u2014 present when count_only is set."),
|
|
5564
|
+
total: z16.number().optional().describe("Total matches before `limit` truncation \u2014 present only when truncated."),
|
|
5565
|
+
truncated: z16.boolean().optional().describe("True when `limit` dropped some matches."),
|
|
5566
|
+
hint: z16.object({
|
|
5567
|
+
route: z16.string(),
|
|
5568
|
+
presentTestids: z16.array(z16.string()),
|
|
5569
|
+
knownEmptyState: z16.boolean()
|
|
5570
|
+
}).optional().describe("Present only on zero matches \u2014 tells you what IS on the page so you can diagnose the miss."),
|
|
5571
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional().describe("Estimated size of this result \u2014 narrow with `name`/`scope`/`limit` if large.")
|
|
4398
5572
|
},
|
|
4399
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5573
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.QUERY, {
|
|
4400
5574
|
by: args["by"],
|
|
4401
5575
|
value: args["value"],
|
|
4402
5576
|
name: args["name"],
|
|
4403
5577
|
scope: args["scope"]
|
|
4404
|
-
})
|
|
5578
|
+
}).then((result) => withSizeCost(paginateQueryResult(result, asNumber(args["limit"]), args["count_only"] === true)))
|
|
4405
5579
|
},
|
|
4406
5580
|
{
|
|
4407
5581
|
name: IrisTool.INSPECT,
|
|
4408
5582
|
description: "Deep info on one element by ref: full a11y props, visibility, box, and (with @syrin/iris-react) component stack + source file.",
|
|
4409
5583
|
inputSchema: {
|
|
4410
|
-
ref:
|
|
5584
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
|
|
4411
5585
|
...sessionIdShape6
|
|
4412
5586
|
},
|
|
4413
5587
|
outputSchema: {
|
|
4414
|
-
ref:
|
|
4415
|
-
role:
|
|
4416
|
-
name:
|
|
4417
|
-
value:
|
|
4418
|
-
states:
|
|
4419
|
-
visible:
|
|
4420
|
-
box:
|
|
4421
|
-
component:
|
|
5588
|
+
ref: z16.string(),
|
|
5589
|
+
role: z16.string(),
|
|
5590
|
+
name: z16.string(),
|
|
5591
|
+
value: z16.string().optional(),
|
|
5592
|
+
states: z16.array(z16.string()),
|
|
5593
|
+
visible: z16.boolean(),
|
|
5594
|
+
box: z16.object({ x: z16.number(), y: z16.number(), width: z16.number(), height: z16.number() }).optional(),
|
|
5595
|
+
component: z16.object({ name: z16.string().optional(), sourceFile: z16.string().optional() }).optional()
|
|
4422
5596
|
},
|
|
4423
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5597
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.INSPECT, {
|
|
4424
5598
|
ref: args["ref"]
|
|
4425
5599
|
})
|
|
4426
5600
|
},
|
|
@@ -4428,30 +5602,30 @@ var TOOLS = [
|
|
|
4428
5602
|
name: IrisTool.ACT,
|
|
4429
5603
|
description: 'Execute one action against a ref: click|dblclick|hover|focus|fill|type|clear|select|check|uncheck|submit|press|scrollIntoView. Returns immediately with a `since` cursor \u2014 observe the reaction with iris_observe. Carries effect:{dispatched,targetMatched,visible,enabled,focusMoved,valueChanged,domMutatedWithin,occluded,occludedBy,scrolledIntoView} to tell "action missed" from "app didn\'t react"; dispatched=landed, settled=a real frame flushed, and a settle timeout never fails the tool. occluded=true means the click point is covered by another element (a real user could not click it) \u2014 synthetic dispatch still delivered the event; scrolledIntoView=true means an off-viewport target was scrolled in first. inputMode is "real" (native CDP, no synthetic effect block) or "synthetic"; clicks default to the occlusion-honest synthetic path even when CDP is configured \u2014 pass args.native:true to force a trusted native click (file pickers, clipboard). inputModeReason explains any real\u2192synthetic choice so it is never silent. Full model (real-input, throttled tabs, `iris drive`): docs/usage.md \xA718.',
|
|
4430
5604
|
inputSchema: {
|
|
4431
|
-
ref:
|
|
4432
|
-
action:
|
|
4433
|
-
args:
|
|
4434
|
-
refuseWhenThrottled:
|
|
5605
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
|
|
5606
|
+
action: z16.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
|
|
5607
|
+
args: z16.record(z16.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press, { native: true } to force a trusted native click, { confirmDangerous: true } to allow a potentially destructive control."),
|
|
5608
|
+
refuseWhenThrottled: z16.boolean().optional().describe("Throw instead of silently sending synthetic events when the tab is throttled/backgrounded. Default: false (synthetic events are still sent)."),
|
|
4435
5609
|
...sessionIdShape6
|
|
4436
5610
|
},
|
|
4437
5611
|
outputSchema: {
|
|
4438
|
-
since:
|
|
4439
|
-
dispatched:
|
|
4440
|
-
settled:
|
|
4441
|
-
inputMode:
|
|
4442
|
-
result:
|
|
4443
|
-
session:
|
|
5612
|
+
since: z16.number().describe("Cursor \u2014 pass to iris_observe/iris_wait_for/iris_assert to scope reaction queries to this act."),
|
|
5613
|
+
dispatched: z16.boolean(),
|
|
5614
|
+
settled: z16.boolean().nullable(),
|
|
5615
|
+
inputMode: z16.string(),
|
|
5616
|
+
result: z16.unknown().optional(),
|
|
5617
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4444
5618
|
},
|
|
4445
5619
|
handler: async (deps, args) => {
|
|
4446
|
-
const session = deps.sessions.resolve(
|
|
5620
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4447
5621
|
const paused = pausedShortCircuit(session);
|
|
4448
5622
|
if (paused !== void 0)
|
|
4449
5623
|
return paused;
|
|
4450
5624
|
refuseIfThrottled(session, args["refuseWhenThrottled"]);
|
|
4451
5625
|
const since = session.elapsed();
|
|
4452
5626
|
session.markActCursor(since);
|
|
4453
|
-
const ref =
|
|
4454
|
-
const action =
|
|
5627
|
+
const ref = asString(args["ref"]) ?? "";
|
|
5628
|
+
const action = asString(args["action"]) ?? "";
|
|
4455
5629
|
const real = await tryRealInput(deps, session, ref, action, args);
|
|
4456
5630
|
if (real.result !== void 0) {
|
|
4457
5631
|
if (deps.recordings.active().length > 0) {
|
|
@@ -4477,7 +5651,7 @@ var TOOLS = [
|
|
|
4477
5651
|
if (deps.recordings.active().length > 0) {
|
|
4478
5652
|
deps.recordings.capture(compileActStep(args, result.result));
|
|
4479
5653
|
}
|
|
4480
|
-
const r =
|
|
5654
|
+
const r = asRecord(result.result);
|
|
4481
5655
|
return withControl(session, {
|
|
4482
5656
|
since,
|
|
4483
5657
|
inputMode: InputMode.SYNTHETIC,
|
|
@@ -4496,17 +5670,17 @@ var TOOLS = [
|
|
|
4496
5670
|
name: IrisTool.ACT_SEQUENCE,
|
|
4497
5671
|
description: "Run multiple actions in order (fill -> fill -> submit) in one round-trip. Returns per-step effects[] (see iris_act).",
|
|
4498
5672
|
inputSchema: {
|
|
4499
|
-
steps:
|
|
5673
|
+
steps: z16.array(z16.record(z16.unknown())).describe("Ordered list of { ref, action, args? } objects. Each step is equivalent to one iris_act call; put confirmDangerous:true in a destructive step args object."),
|
|
4500
5674
|
...sessionIdShape6
|
|
4501
5675
|
},
|
|
4502
5676
|
outputSchema: {
|
|
4503
|
-
since:
|
|
4504
|
-
dispatched:
|
|
4505
|
-
result:
|
|
4506
|
-
session:
|
|
5677
|
+
since: z16.number(),
|
|
5678
|
+
dispatched: z16.boolean(),
|
|
5679
|
+
result: z16.unknown().optional(),
|
|
5680
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4507
5681
|
},
|
|
4508
5682
|
handler: async (deps, args) => {
|
|
4509
|
-
const session = deps.sessions.resolve(
|
|
5683
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4510
5684
|
const paused = pausedShortCircuit(session);
|
|
4511
5685
|
if (paused !== void 0)
|
|
4512
5686
|
return paused;
|
|
@@ -4518,7 +5692,7 @@ var TOOLS = [
|
|
|
4518
5692
|
if (deps.recordings.active().length > 0) {
|
|
4519
5693
|
deps.recordings.capture(compileSequenceStep(args, result.result));
|
|
4520
5694
|
}
|
|
4521
|
-
const r =
|
|
5695
|
+
const r = asRecord(result.result);
|
|
4522
5696
|
return withControl(session, {
|
|
4523
5697
|
since,
|
|
4524
5698
|
dispatched: r["count"] !== void 0,
|
|
@@ -4529,34 +5703,34 @@ var TOOLS = [
|
|
|
4529
5703
|
},
|
|
4530
5704
|
{
|
|
4531
5705
|
name: IrisTool.ACT_AND_WAIT,
|
|
4532
|
-
description: "Act on a ref, then wait for a predicate to hold \u2014 one hop for the act->observe->assert loop. Returns { effect } (the action result), { verdict } (predicate pass/evidence/near-miss), and { trace } (the reaction report of everything the app did after the action). timeout_ms 0 evaluates the predicate once without waiting.",
|
|
5706
|
+
description: "Act on a ref, then wait for a predicate to hold \u2014 one hop for the act->observe->assert loop. Omit `until` to wait for the page to settle (network + DOM idle) \u2014 use this instead of a fixed sleep. Returns { effect } (the action result), { verdict } (predicate pass/evidence/near-miss), and { trace } (the reaction report of everything the app did after the action). timeout_ms 0 evaluates the predicate once without waiting.",
|
|
4533
5707
|
inputSchema: {
|
|
4534
|
-
ref:
|
|
4535
|
-
action:
|
|
4536
|
-
args:
|
|
4537
|
-
until: PredicateSchema.describe(
|
|
4538
|
-
timeout_ms:
|
|
4539
|
-
refuseWhenThrottled:
|
|
5708
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query."),
|
|
5709
|
+
action: z16.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
|
|
5710
|
+
args: z16.record(z16.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press, { confirmDangerous: true } for a potentially destructive control."),
|
|
5711
|
+
until: PredicateSchema.optional().describe('Predicate to wait for after the action completes (same shape as iris_assert). OMIT to wait for the page to SETTLE \u2014 network + DOM idle \u2014 the deterministic default instead of a sleep. To assert a consequence AND settle, allOf them: { kind: "allOf", predicates: [<your predicate>, { kind: "settled" }] }.'),
|
|
5712
|
+
timeout_ms: z16.number().optional().describe("Maximum wait time in milliseconds. 0 = evaluate once without waiting. Default: 4000."),
|
|
5713
|
+
refuseWhenThrottled: z16.boolean().optional().describe("Throw if the tab is throttled. Default: false."),
|
|
4540
5714
|
...sessionIdShape6
|
|
4541
5715
|
},
|
|
4542
5716
|
outputSchema: {
|
|
4543
|
-
effect:
|
|
4544
|
-
verdict:
|
|
4545
|
-
pass:
|
|
4546
|
-
evidence:
|
|
4547
|
-
failureReason:
|
|
5717
|
+
effect: z16.unknown().describe("The iris_act result (dispatched, settled, inputMode, etc.)."),
|
|
5718
|
+
verdict: z16.object({
|
|
5719
|
+
pass: z16.boolean(),
|
|
5720
|
+
evidence: z16.unknown().optional(),
|
|
5721
|
+
failureReason: z16.string().optional()
|
|
4548
5722
|
}),
|
|
4549
|
-
trace:
|
|
4550
|
-
session:
|
|
5723
|
+
trace: z16.unknown().describe("Reaction report (same shape as iris_observe summary)."),
|
|
5724
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4551
5725
|
},
|
|
4552
5726
|
handler: async (deps, args) => {
|
|
4553
|
-
const session = deps.sessions.resolve(
|
|
5727
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4554
5728
|
const paused = pausedShortCircuit(session);
|
|
4555
5729
|
if (paused !== void 0)
|
|
4556
5730
|
return paused;
|
|
4557
5731
|
refuseIfThrottled(session, args["refuseWhenThrottled"]);
|
|
4558
|
-
const until = PredicateSchema.parse(args["until"]);
|
|
4559
|
-
const timeout2 =
|
|
5732
|
+
const until = args["until"] !== void 0 ? PredicateSchema.parse(args["until"]) : { kind: "settled" };
|
|
5733
|
+
const timeout2 = asNumber(args["timeout_ms"]) ?? 4e3;
|
|
4560
5734
|
const since = session.elapsed();
|
|
4561
5735
|
session.markActCursor(since);
|
|
4562
5736
|
const actResult = await session.command(IrisCommand.ACT, {
|
|
@@ -4580,40 +5754,41 @@ var TOOLS = [
|
|
|
4580
5754
|
name: IrisTool.OBSERVE,
|
|
4581
5755
|
description: "Return the timeline of everything the app did in a window (DOM/network/route/console/animation/signal), with a summary. Use after an action. Pass `max_events` to cap the timeline to the most recent N (older events are dropped and counted in cost.droppedOldest). Every result carries a `cost:{events,bytes}` hint so you can self-budget your next call.",
|
|
4582
5756
|
inputSchema: {
|
|
4583
|
-
window_ms:
|
|
4584
|
-
since:
|
|
4585
|
-
filters:
|
|
4586
|
-
max_events:
|
|
5757
|
+
window_ms: z16.number().optional().describe("Time window to look back. Default: 2000ms. Ignored when `since` is provided."),
|
|
5758
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act or iris_observe call. Scopes the event window to exactly that span."),
|
|
5759
|
+
filters: z16.array(z16.string()).optional().describe("Event type allowlist: dom | net | route | console | animation | signal. Omit to return all types."),
|
|
5760
|
+
max_events: z16.number().optional().describe("Cap the timeline to the most recent N events. Older events are counted in cost.droppedOldest."),
|
|
4587
5761
|
...sessionIdShape6
|
|
4588
5762
|
},
|
|
4589
5763
|
outputSchema: {
|
|
4590
|
-
events:
|
|
4591
|
-
summary:
|
|
4592
|
-
total:
|
|
4593
|
-
network:
|
|
4594
|
-
domAdded:
|
|
4595
|
-
domRemoved:
|
|
4596
|
-
domChanged:
|
|
4597
|
-
routeChanges:
|
|
4598
|
-
consoleErrors:
|
|
4599
|
-
animations:
|
|
4600
|
-
signals:
|
|
5764
|
+
events: z16.array(z16.unknown()),
|
|
5765
|
+
summary: z16.object({
|
|
5766
|
+
total: z16.number(),
|
|
5767
|
+
network: z16.number(),
|
|
5768
|
+
domAdded: z16.number(),
|
|
5769
|
+
domRemoved: z16.number(),
|
|
5770
|
+
domChanged: z16.number(),
|
|
5771
|
+
routeChanges: z16.number(),
|
|
5772
|
+
consoleErrors: z16.number(),
|
|
5773
|
+
animations: z16.number(),
|
|
5774
|
+
signals: z16.number()
|
|
4601
5775
|
}),
|
|
4602
|
-
cost:
|
|
4603
|
-
events:
|
|
4604
|
-
bytes:
|
|
4605
|
-
droppedOldest:
|
|
5776
|
+
cost: z16.object({
|
|
5777
|
+
events: z16.number(),
|
|
5778
|
+
bytes: z16.number(),
|
|
5779
|
+
droppedOldest: z16.number().optional(),
|
|
5780
|
+
recommendation: z16.string().optional().describe("Present when the timeline is large \u2014 scope your next call (filters/max_events).")
|
|
4606
5781
|
}),
|
|
4607
|
-
session:
|
|
5782
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4608
5783
|
},
|
|
4609
5784
|
handler: (deps, args) => {
|
|
4610
|
-
const session = deps.sessions.resolve(
|
|
4611
|
-
const since =
|
|
4612
|
-
const windowMs =
|
|
5785
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5786
|
+
const since = asNumber(args["since"]);
|
|
5787
|
+
const windowMs = asNumber(args["window_ms"]) ?? 2e3;
|
|
4613
5788
|
const events = since !== void 0 ? session.eventsSince(since) : session.eventsInWindow(windowMs);
|
|
4614
5789
|
const filters = Array.isArray(args["filters"]) ? args["filters"] : void 0;
|
|
4615
5790
|
const filtered = filters === void 0 ? events : events.filter((e) => filters.includes(e.type));
|
|
4616
|
-
const { events: budgeted, droppedOldest } = applyEventBudget(filtered,
|
|
5791
|
+
const { events: budgeted, droppedOldest } = applyEventBudget(filtered, asNumber(args["max_events"]));
|
|
4617
5792
|
const report = buildReactionReport(budgeted, windowMs);
|
|
4618
5793
|
return Promise.resolve(withControl(session, {
|
|
4619
5794
|
...report,
|
|
@@ -4626,99 +5801,113 @@ var TOOLS = [
|
|
|
4626
5801
|
name: IrisTool.WAIT_FOR,
|
|
4627
5802
|
description: "Block until a predicate is satisfied (or already true in the recent buffer), else time out. Returns matching evidence or a near-miss diagnosis. By default it only counts events since your last act, so a signal buffered BEFORE the action can never fake a pass; pass `since` (an observe/act cursor) to widen or narrow that window explicitly.",
|
|
4628
5803
|
inputSchema: {
|
|
4629
|
-
predicate: PredicateSchema.describe(
|
|
4630
|
-
timeout_ms:
|
|
4631
|
-
since:
|
|
5804
|
+
predicate: PredicateSchema.describe('Predicate to wait for: { signal }, { net }, { element }, { kind: "settled", quietMs } (deterministic network + DOM idle \u2014 prefer this over a fixed sleep), or a combination via allOf/anyOf.'),
|
|
5805
|
+
timeout_ms: z16.number().optional().describe("Maximum wait in milliseconds. Default: 4000."),
|
|
5806
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the wait to events after that act."),
|
|
4632
5807
|
...sessionIdShape6
|
|
4633
5808
|
},
|
|
4634
5809
|
outputSchema: {
|
|
4635
|
-
pass:
|
|
4636
|
-
evidence:
|
|
4637
|
-
failureReason:
|
|
4638
|
-
session:
|
|
5810
|
+
pass: z16.boolean(),
|
|
5811
|
+
evidence: z16.unknown().optional(),
|
|
5812
|
+
failureReason: z16.string().optional(),
|
|
5813
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4639
5814
|
},
|
|
4640
5815
|
handler: async (deps, args) => {
|
|
4641
|
-
const session = deps.sessions.resolve(
|
|
5816
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4642
5817
|
const predicate = PredicateSchema.parse(args["predicate"]);
|
|
4643
|
-
const since =
|
|
4644
|
-
const verdict = await waitForPredicate(session, predicate,
|
|
5818
|
+
const since = asNumber(args["since"]) ?? session.lastActCursor() ?? 0;
|
|
5819
|
+
const verdict = await waitForPredicate(session, predicate, asNumber(args["timeout_ms"]) ?? 4e3, since);
|
|
4645
5820
|
return withControl(session, { ...verdict, ...healthEnvelope(session) });
|
|
4646
5821
|
}
|
|
4647
5822
|
},
|
|
4648
5823
|
{
|
|
4649
5824
|
name: IrisTool.ASSERT,
|
|
4650
|
-
description: "Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.",
|
|
5825
|
+
description: "Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. Prefer a { signal } or { net } consequence over { element }/{ text } presence \u2014 a passing presence-only assertion returns `advice` because a wrong/healed element can fake it. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.",
|
|
4651
5826
|
inputSchema: {
|
|
4652
5827
|
predicate: PredicateSchema.describe("Predicate to evaluate: { signal }, { net }, { element } or a combination."),
|
|
4653
|
-
timeout_ms:
|
|
4654
|
-
since:
|
|
5828
|
+
timeout_ms: z16.number().optional().describe("If > 0, wait up to this many milliseconds before failing. Default: 0 (evaluate once)."),
|
|
5829
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the assertion to events after that act."),
|
|
4655
5830
|
...sessionIdShape6
|
|
4656
5831
|
},
|
|
4657
5832
|
outputSchema: {
|
|
4658
|
-
pass:
|
|
4659
|
-
evidence:
|
|
4660
|
-
failureReason:
|
|
4661
|
-
|
|
5833
|
+
pass: z16.boolean(),
|
|
5834
|
+
evidence: z16.unknown().optional(),
|
|
5835
|
+
failureReason: z16.string().optional(),
|
|
5836
|
+
advice: z16.string().optional().describe("Present on a PASSING presence-only assertion \u2014 nudges toward a consequence."),
|
|
5837
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4662
5838
|
},
|
|
4663
5839
|
handler: async (deps, args) => {
|
|
4664
|
-
const session = deps.sessions.resolve(
|
|
5840
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4665
5841
|
const predicate = PredicateSchema.parse(args["predicate"]);
|
|
4666
|
-
const timeout2 =
|
|
4667
|
-
const since =
|
|
5842
|
+
const timeout2 = asNumber(args["timeout_ms"]) ?? 0;
|
|
5843
|
+
const since = asNumber(args["since"]) ?? session.lastActCursor() ?? 0;
|
|
4668
5844
|
const verdict = timeout2 > 0 ? await waitForPredicate(session, predicate, timeout2, since) : await evaluatePredicate(session, predicate, since);
|
|
4669
|
-
|
|
5845
|
+
const advice = verdict.pass && isPresenceOnlyAssertion(predicate) ? { advice: PRESENCE_ONLY_ADVICE } : {};
|
|
5846
|
+
return withControl(session, { ...verdict, ...advice, ...healthEnvelope(session) });
|
|
4670
5847
|
}
|
|
4671
5848
|
},
|
|
4672
5849
|
{
|
|
4673
5850
|
name: IrisTool.NETWORK,
|
|
4674
5851
|
description: 'Filtered list of network calls. Fast path for "did POST /x return 200?". A zero-match filter returns a `hint` { totalInWindow, present[] } of the calls that DID fire, so a miss is diagnosable.',
|
|
4675
5852
|
inputSchema: {
|
|
4676
|
-
since:
|
|
4677
|
-
method:
|
|
4678
|
-
urlContains:
|
|
4679
|
-
status:
|
|
5853
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to requests fired after that act."),
|
|
5854
|
+
method: z16.string().optional().describe("HTTP method filter: GET | POST | PUT | DELETE | PATCH etc."),
|
|
5855
|
+
urlContains: z16.string().optional().describe("Substring that the request URL must contain."),
|
|
5856
|
+
status: z16.number().optional().describe("HTTP status code filter (e.g. 200, 404, 500)."),
|
|
5857
|
+
limit: z16.number().optional().describe("Keep only the most recent N matching calls (older are dropped and counted in droppedOldest) \u2014 cuts tokens on a wide window."),
|
|
4680
5858
|
...sessionIdShape6
|
|
4681
5859
|
},
|
|
4682
5860
|
outputSchema: {
|
|
4683
|
-
calls:
|
|
4684
|
-
|
|
5861
|
+
calls: z16.array(z16.unknown()),
|
|
5862
|
+
total: z16.number().optional().describe("Total matches before `limit` \u2014 present only when capped."),
|
|
5863
|
+
droppedOldest: z16.number().optional().describe("How many older matches `limit` dropped."),
|
|
5864
|
+
hint: z16.object({ totalInWindow: z16.number(), present: z16.array(z16.string()) }).optional(),
|
|
5865
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional()
|
|
4685
5866
|
},
|
|
4686
5867
|
handler: (deps, args) => {
|
|
4687
|
-
const session = deps.sessions.resolve(
|
|
4688
|
-
const since =
|
|
4689
|
-
const method =
|
|
4690
|
-
const urlContains =
|
|
4691
|
-
const status =
|
|
5868
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5869
|
+
const since = asNumber(args["since"]) ?? 0;
|
|
5870
|
+
const method = asString(args["method"]);
|
|
5871
|
+
const urlContains = asString(args["urlContains"]);
|
|
5872
|
+
const status = asNumber(args["status"]);
|
|
5873
|
+
const limit = asNumber(args["limit"]);
|
|
4692
5874
|
const allNet = session.eventsSince(since).filter((e) => e.type === EventType.NET_REQUEST);
|
|
4693
|
-
const
|
|
4694
|
-
if (
|
|
4695
|
-
return Promise.resolve({ calls, hint: netEmptyHint(allNet) });
|
|
5875
|
+
const matched = allNet.filter((e) => matchNet(e, method, urlContains, status));
|
|
5876
|
+
if (matched.length === 0 && allNet.length > 0) {
|
|
5877
|
+
return Promise.resolve(withSizeCost({ calls: matched, hint: netEmptyHint(allNet) }));
|
|
4696
5878
|
}
|
|
4697
|
-
|
|
5879
|
+
const { events: calls, droppedOldest } = applyEventBudget(matched, limit);
|
|
5880
|
+
return Promise.resolve(withSizeCost(droppedOldest > 0 ? { calls, total: matched.length, droppedOldest } : { calls }));
|
|
4698
5881
|
}
|
|
4699
5882
|
},
|
|
4700
5883
|
{
|
|
4701
5884
|
name: IrisTool.CONSOLE,
|
|
4702
5885
|
description: 'Console/error log. Fast path for "were there any errors during this flow?". When a level filter matches nothing, returns a `hint` { totalInWindow, byLevel } so 0 errors is distinguishable from a silent page.',
|
|
4703
5886
|
inputSchema: {
|
|
4704
|
-
level:
|
|
4705
|
-
since:
|
|
5887
|
+
level: z16.string().optional().describe("Log level filter: error | warn | info | log. Omit to return all levels."),
|
|
5888
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to log entries after that act."),
|
|
5889
|
+
limit: z16.number().optional().describe("Keep only the most recent N matching entries (older are dropped and counted in droppedOldest) \u2014 cuts tokens when a page spams the console."),
|
|
4706
5890
|
...sessionIdShape6
|
|
4707
5891
|
},
|
|
4708
5892
|
outputSchema: {
|
|
4709
|
-
logs:
|
|
4710
|
-
|
|
5893
|
+
logs: z16.array(z16.unknown()),
|
|
5894
|
+
total: z16.number().optional().describe("Total matches before `limit` \u2014 present only when capped."),
|
|
5895
|
+
droppedOldest: z16.number().optional().describe("How many older matches `limit` dropped."),
|
|
5896
|
+
hint: z16.object({ totalInWindow: z16.number(), byLevel: z16.record(z16.number()) }).optional(),
|
|
5897
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional()
|
|
4711
5898
|
},
|
|
4712
5899
|
handler: (deps, args) => {
|
|
4713
|
-
const session = deps.sessions.resolve(
|
|
4714
|
-
const since =
|
|
4715
|
-
const level =
|
|
5900
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5901
|
+
const since = asNumber(args["since"]) ?? 0;
|
|
5902
|
+
const level = asString(args["level"]);
|
|
5903
|
+
const limit = asNumber(args["limit"]);
|
|
4716
5904
|
const allConsole = session.eventsSince(since).filter(isConsoleEvent);
|
|
4717
|
-
const
|
|
4718
|
-
if (
|
|
4719
|
-
return Promise.resolve({ logs, hint: consoleEmptyHint(allConsole) });
|
|
5905
|
+
const matched = allConsole.filter((e) => matchConsole(e, level));
|
|
5906
|
+
if (matched.length === 0 && allConsole.length > 0) {
|
|
5907
|
+
return Promise.resolve(withSizeCost({ logs: matched, hint: consoleEmptyHint(allConsole) }));
|
|
4720
5908
|
}
|
|
4721
|
-
|
|
5909
|
+
const { events: logs, droppedOldest } = applyEventBudget(matched, limit);
|
|
5910
|
+
return Promise.resolve(withSizeCost(droppedOldest > 0 ? { logs, total: matched.length, droppedOldest } : { logs }));
|
|
4722
5911
|
}
|
|
4723
5912
|
},
|
|
4724
5913
|
{
|
|
@@ -4726,24 +5915,24 @@ var TOOLS = [
|
|
|
4726
5915
|
description: "Currently running + recently completed animations with targets/timing.",
|
|
4727
5916
|
inputSchema: { ...sessionIdShape6 },
|
|
4728
5917
|
outputSchema: {
|
|
4729
|
-
animations:
|
|
5918
|
+
animations: z16.array(z16.unknown())
|
|
4730
5919
|
},
|
|
4731
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5920
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.ANIMATIONS, {})
|
|
4732
5921
|
},
|
|
4733
5922
|
{
|
|
4734
5923
|
name: IrisTool.BASELINE_SAVE,
|
|
4735
5924
|
description: "Snapshot the current semantic state under a name, to diff against later (regression detection).",
|
|
4736
5925
|
inputSchema: {
|
|
4737
|
-
name:
|
|
5926
|
+
name: z16.string().describe('Label for this baseline snapshot (e.g. "dashboard-initial"). Use the same name in iris_diff to compare.'),
|
|
4738
5927
|
...sessionIdShape6
|
|
4739
5928
|
},
|
|
4740
5929
|
outputSchema: {
|
|
4741
|
-
baseline:
|
|
4742
|
-
lineCount:
|
|
5930
|
+
baseline: z16.string().describe("Saved baseline name \u2014 pass to iris_diff to compare."),
|
|
5931
|
+
lineCount: z16.number()
|
|
4743
5932
|
},
|
|
4744
5933
|
handler: async (deps, args) => {
|
|
4745
|
-
const name =
|
|
4746
|
-
const { lines, route } = await snapshotTree(deps,
|
|
5934
|
+
const name = asString(args["name"]) ?? "default";
|
|
5935
|
+
const { lines, route } = await snapshotTree(deps, asString(args["sessionId"]));
|
|
4747
5936
|
deps.baselines.save({ name, lines, route });
|
|
4748
5937
|
return { baseline: name, lineCount: lines.length };
|
|
4749
5938
|
}
|
|
@@ -4753,7 +5942,7 @@ var TOOLS = [
|
|
|
4753
5942
|
description: "List saved baseline names.",
|
|
4754
5943
|
inputSchema: {},
|
|
4755
5944
|
outputSchema: {
|
|
4756
|
-
baselines:
|
|
5945
|
+
baselines: z16.array(z16.string())
|
|
4757
5946
|
},
|
|
4758
5947
|
handler: (deps) => Promise.resolve({ baselines: deps.baselines.list() })
|
|
4759
5948
|
},
|
|
@@ -4761,23 +5950,23 @@ var TOOLS = [
|
|
|
4761
5950
|
name: IrisTool.DIFF,
|
|
4762
5951
|
description: 'Diff current semantic state vs a saved baseline: REMOVED/ADDED elements + console-error count. Call iris_baseline_list to list saved baselines, iris_baseline_save to create one. Pass `baseline` (name from iris_baseline_list). Answers "did anything silently go missing/break?".',
|
|
4763
5952
|
inputSchema: {
|
|
4764
|
-
baseline:
|
|
5953
|
+
baseline: z16.string().describe("Baseline name to compare against. Call iris_baseline_list to get available names; names are created by iris_baseline_save."),
|
|
4765
5954
|
...sessionIdShape6
|
|
4766
5955
|
},
|
|
4767
5956
|
outputSchema: {
|
|
4768
|
-
baseline:
|
|
4769
|
-
removed:
|
|
4770
|
-
added:
|
|
4771
|
-
consoleErrors:
|
|
4772
|
-
routeChanged:
|
|
5957
|
+
baseline: z16.string(),
|
|
5958
|
+
removed: z16.array(z16.string()),
|
|
5959
|
+
added: z16.array(z16.string()),
|
|
5960
|
+
consoleErrors: z16.number(),
|
|
5961
|
+
routeChanged: z16.boolean()
|
|
4773
5962
|
},
|
|
4774
5963
|
handler: async (deps, args) => {
|
|
4775
|
-
const name =
|
|
5964
|
+
const name = asString(args["baseline"]) ?? "default";
|
|
4776
5965
|
const base = deps.baselines.get(name);
|
|
4777
5966
|
if (base === void 0)
|
|
4778
5967
|
throw new Error(`no baseline named '${name}'`);
|
|
4779
|
-
const session = deps.sessions.resolve(
|
|
4780
|
-
const { lines, route } = await snapshotTree(deps,
|
|
5968
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5969
|
+
const { lines, route } = await snapshotTree(deps, asString(args["sessionId"]));
|
|
4781
5970
|
const { removed, added } = diffLines(base.lines, lines);
|
|
4782
5971
|
const consoleErrors = session.eventsSince(0).filter((e) => e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT).length;
|
|
4783
5972
|
return { baseline: name, removed, added, consoleErrors, routeChanged: base.route !== route };
|
|
@@ -4787,16 +5976,16 @@ var TOOLS = [
|
|
|
4787
5976
|
name: IrisTool.RECORD_START,
|
|
4788
5977
|
description: "Start recording the event timeline under a name (for replay / a flow report).",
|
|
4789
5978
|
inputSchema: {
|
|
4790
|
-
recordingName:
|
|
5979
|
+
recordingName: z16.string().describe("Identifier for this recording. Pass the same name to iris_record_stop and iris_replay."),
|
|
4791
5980
|
...sessionIdShape6
|
|
4792
5981
|
},
|
|
4793
5982
|
outputSchema: {
|
|
4794
|
-
recordingName:
|
|
4795
|
-
since:
|
|
5983
|
+
recordingName: z16.string(),
|
|
5984
|
+
since: z16.number()
|
|
4796
5985
|
},
|
|
4797
5986
|
handler: (deps, args) => {
|
|
4798
|
-
const session = deps.sessions.resolve(
|
|
4799
|
-
const name =
|
|
5987
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5988
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
4800
5989
|
const cursor = session.elapsed();
|
|
4801
5990
|
deps.recordings.start(name, cursor);
|
|
4802
5991
|
return Promise.resolve({ recordingName: name, since: cursor });
|
|
@@ -4806,17 +5995,17 @@ var TOOLS = [
|
|
|
4806
5995
|
name: IrisTool.RECORD_STOP,
|
|
4807
5996
|
description: "Stop the recording identified by `recordingName` and return both the reaction report for the span and a compiled, replayable { program: { version, steps:[{tool,args,stable}] } } of the agent acts captured during it.",
|
|
4808
5997
|
inputSchema: {
|
|
4809
|
-
recordingName:
|
|
5998
|
+
recordingName: z16.string().describe("Identifier of an active recording started with iris_record_start."),
|
|
4810
5999
|
...sessionIdShape6
|
|
4811
6000
|
},
|
|
4812
6001
|
outputSchema: {
|
|
4813
|
-
recordingName:
|
|
4814
|
-
program:
|
|
4815
|
-
warning:
|
|
6002
|
+
recordingName: z16.string(),
|
|
6003
|
+
program: z16.unknown(),
|
|
6004
|
+
warning: z16.string().optional()
|
|
4816
6005
|
},
|
|
4817
6006
|
handler: (deps, args) => {
|
|
4818
|
-
const session = deps.sessions.resolve(
|
|
4819
|
-
const name =
|
|
6007
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
6008
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
4820
6009
|
const rec = deps.recordings.stop(name);
|
|
4821
6010
|
if (rec === void 0)
|
|
4822
6011
|
throw new Error(`no active recording named '${name}'`);
|
|
@@ -4842,29 +6031,30 @@ var TOOLS = [
|
|
|
4842
6031
|
},
|
|
4843
6032
|
{
|
|
4844
6033
|
name: IrisTool.REPLAY,
|
|
4845
|
-
description: "Re-execute a previously recorded program by recordingName. Re-resolves each step to its element by testid (falling back to the stored ref for unstable steps) and runs the actions in order against the live session. Stops at the first failure. Returns { ok, steps:[{tool,ok,error?,note?}] }.",
|
|
6034
|
+
description: "Re-execute a previously recorded program by recordingName. Re-resolves each step to its element by testid (falling back to the stored ref for unstable steps) and runs the actions in order against the live session. Stops at the first failure. Destructive controls require confirmDangerous:true on every replay; confirmation is never persisted. Returns { ok, steps:[{tool,ok,error?,note?}] }.",
|
|
4846
6035
|
inputSchema: {
|
|
4847
|
-
recordingName:
|
|
6036
|
+
recordingName: z16.string().describe("Name of a compiled recording (from iris_record_stop) to re-execute."),
|
|
6037
|
+
confirmDangerous: z16.boolean().optional().describe("Set true to allow destructive controls during this replay only."),
|
|
4848
6038
|
...sessionIdShape6
|
|
4849
6039
|
},
|
|
4850
6040
|
outputSchema: {
|
|
4851
|
-
recordingName:
|
|
4852
|
-
ok:
|
|
4853
|
-
steps:
|
|
4854
|
-
tool:
|
|
4855
|
-
ok:
|
|
4856
|
-
error:
|
|
4857
|
-
note:
|
|
6041
|
+
recordingName: z16.string(),
|
|
6042
|
+
ok: z16.boolean(),
|
|
6043
|
+
steps: z16.array(z16.object({
|
|
6044
|
+
tool: z16.string(),
|
|
6045
|
+
ok: z16.boolean(),
|
|
6046
|
+
error: z16.string().optional(),
|
|
6047
|
+
note: z16.string().optional()
|
|
4858
6048
|
}))
|
|
4859
6049
|
},
|
|
4860
6050
|
handler: async (deps, args) => {
|
|
4861
|
-
const name =
|
|
6051
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
4862
6052
|
const program = deps.recordings.getCompiled(name);
|
|
4863
6053
|
if (program === void 0)
|
|
4864
6054
|
throw new Error(`no compiled recording named '${name}'`);
|
|
4865
|
-
const session = deps.sessions.resolve(
|
|
6055
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4866
6056
|
const since = session.elapsed();
|
|
4867
|
-
const steps = await replayProgram(session, program);
|
|
6057
|
+
const steps = await replayProgram(session, program, args["confirmDangerous"] === true);
|
|
4868
6058
|
return { recordingName: name, since, steps, ok: steps.every((s) => s.ok) };
|
|
4869
6059
|
}
|
|
4870
6060
|
},
|
|
@@ -4872,32 +6062,33 @@ var TOOLS = [
|
|
|
4872
6062
|
name: IrisTool.NARRATE,
|
|
4873
6063
|
description: "Narrate your intent on the page (presenter HUD) so the human watching sees what you are about to do and why. Use a short sentence before a meaningful action.",
|
|
4874
6064
|
inputSchema: {
|
|
4875
|
-
text:
|
|
4876
|
-
level:
|
|
6065
|
+
text: z16.string().describe("Short sentence describing your next action, shown on the presenter HUD for the developer watching."),
|
|
6066
|
+
level: z16.string().optional().describe("Display severity: info | warn | error. Default: info."),
|
|
4877
6067
|
...sessionIdShape6
|
|
4878
6068
|
},
|
|
4879
|
-
outputSchema: {
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
6069
|
+
outputSchema: { ok: z16.boolean() },
|
|
6070
|
+
handler: async (deps, args) => {
|
|
6071
|
+
const result = await commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.NARRATE, {
|
|
6072
|
+
text: args["text"],
|
|
6073
|
+
level: args["level"]
|
|
6074
|
+
});
|
|
6075
|
+
return { ok: true, ...result };
|
|
6076
|
+
}
|
|
4886
6077
|
},
|
|
4887
6078
|
{
|
|
4888
6079
|
name: IrisTool.CLOCK,
|
|
4889
6080
|
description: "Control a fake clock: { freeze:true } to freeze time, { advanceMs:N } to fast-forward timers (toasts, debounces, auto-dismiss), { reset:true } to restore. Lets you test time-gated UI deterministically.",
|
|
4890
6081
|
inputSchema: {
|
|
4891
|
-
freeze:
|
|
4892
|
-
advanceMs:
|
|
4893
|
-
reset:
|
|
6082
|
+
freeze: z16.boolean().optional().describe("Freeze the fake clock. Time stops advancing until advanceMs or reset."),
|
|
6083
|
+
advanceMs: z16.number().optional().describe("Fast-forward time by this many milliseconds \u2014 triggers debounces, toasts, auto-dismiss timers."),
|
|
6084
|
+
reset: z16.boolean().optional().describe("Restore the real clock."),
|
|
4894
6085
|
...sessionIdShape6
|
|
4895
6086
|
},
|
|
4896
6087
|
outputSchema: {
|
|
4897
|
-
ok:
|
|
4898
|
-
elapsed:
|
|
6088
|
+
ok: z16.boolean().optional(),
|
|
6089
|
+
elapsed: z16.number().optional()
|
|
4899
6090
|
},
|
|
4900
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
6091
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.CLOCK, {
|
|
4901
6092
|
freeze: args["freeze"],
|
|
4902
6093
|
advanceMs: args["advanceMs"],
|
|
4903
6094
|
reset: args["reset"]
|
|
@@ -4907,27 +6098,27 @@ var TOOLS = [
|
|
|
4907
6098
|
name: IrisTool.STATE,
|
|
4908
6099
|
description: "Read live framework state without the app pre-broadcasting it. PREFERRED/RELIABLE: `store` reads a registered store (e.g. 'workspace'); omit `store` to read all stores. To avoid paying for a huge store, scope the read: `path` extracts a dot-path sub-tree (e.g. 'captionCache.v3', with numeric array indices), and `depth` collapses anything deeper than N levels to a size marker. A wrong `path` returns { found:false, availableKeys } so it is diagnosable. `ref` attempts a best-effort read of the nearest React component's hook state and is BOUNDED \u2014 on failure it returns component: { ok: false, reason: 'component-state-unavailable' }. Without path/depth: returns { stores, storeNames, component? }.",
|
|
4909
6100
|
inputSchema: {
|
|
4910
|
-
ref:
|
|
4911
|
-
store:
|
|
4912
|
-
path:
|
|
4913
|
-
depth:
|
|
6101
|
+
ref: z16.string().optional().describe("Element ref \u2014 attempts a best-effort read of the nearest React component's hook state."),
|
|
6102
|
+
store: z16.string().optional().describe("Registered store name (e.g. 'workspace'). Omit to read all stores."),
|
|
6103
|
+
path: z16.string().optional().describe("Dot-path into the store (e.g. 'captionCache.v3'). Numeric array indices are supported."),
|
|
6104
|
+
depth: z16.number().optional().describe("Collapse anything deeper than N levels to a size marker \u2014 avoids huge outputs for large stores."),
|
|
4914
6105
|
...sessionIdShape6
|
|
4915
6106
|
},
|
|
4916
6107
|
outputSchema: {
|
|
4917
|
-
stores:
|
|
4918
|
-
storeNames:
|
|
4919
|
-
found:
|
|
4920
|
-
value:
|
|
4921
|
-
component:
|
|
6108
|
+
stores: z16.record(z16.unknown()).optional(),
|
|
6109
|
+
storeNames: z16.array(z16.string()).optional(),
|
|
6110
|
+
found: z16.boolean().optional(),
|
|
6111
|
+
value: z16.unknown().optional(),
|
|
6112
|
+
component: z16.object({ ok: z16.boolean(), reason: z16.string().optional(), state: z16.unknown().optional() }).optional()
|
|
4922
6113
|
},
|
|
4923
6114
|
handler: async (deps, args) => {
|
|
4924
|
-
const store =
|
|
4925
|
-
const result = await commandOrThrow3(deps,
|
|
6115
|
+
const store = asString(args["store"]);
|
|
6116
|
+
const result = await commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.STATE_READ, {
|
|
4926
6117
|
ref: args["ref"],
|
|
4927
6118
|
store
|
|
4928
6119
|
});
|
|
4929
|
-
const path =
|
|
4930
|
-
const depth =
|
|
6120
|
+
const path = asString(args["path"]);
|
|
6121
|
+
const depth = asNumber(args["depth"]);
|
|
4931
6122
|
if (path === void 0 && depth === void 0)
|
|
4932
6123
|
return result;
|
|
4933
6124
|
const root = result;
|
|
@@ -4947,16 +6138,16 @@ var TOOLS = [
|
|
|
4947
6138
|
name: IrisTool.EXPLORE,
|
|
4948
6139
|
description: "Autonomous-exploration helper: list interactive elements (with refs) + current console-error count, so the agent can drive the app and report anomalies.",
|
|
4949
6140
|
inputSchema: {
|
|
4950
|
-
scope:
|
|
6141
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the interactive element list to a subtree."),
|
|
4951
6142
|
...sessionIdShape6
|
|
4952
6143
|
},
|
|
4953
6144
|
outputSchema: {
|
|
4954
|
-
interactive:
|
|
4955
|
-
consoleErrors:
|
|
4956
|
-
hint:
|
|
6145
|
+
interactive: z16.array(z16.unknown()),
|
|
6146
|
+
consoleErrors: z16.number(),
|
|
6147
|
+
hint: z16.string()
|
|
4957
6148
|
},
|
|
4958
6149
|
handler: async (deps, args) => {
|
|
4959
|
-
const session = deps.sessions.resolve(
|
|
6150
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4960
6151
|
const result = await session.command(IrisCommand.SNAPSHOT, {
|
|
4961
6152
|
mode: SnapshotMode.INTERACTIVE,
|
|
4962
6153
|
scope: args["scope"]
|
|
@@ -4974,6 +6165,7 @@ var TOOLS = [
|
|
|
4974
6165
|
},
|
|
4975
6166
|
// iris_capabilities (live | fromDisk) + iris_contract_save. See contract-tools.ts.
|
|
4976
6167
|
...CONTRACT_TOOLS,
|
|
6168
|
+
...DOMAIN_TOOLS,
|
|
4977
6169
|
// iris_flow_save / iris_flow_list / iris_flow_load. See flow-tools.ts.
|
|
4978
6170
|
...FLOW_TOOLS,
|
|
4979
6171
|
// iris_project (read history + diff-vs-last) / iris_run_record. See project-tools.ts.
|
|
@@ -4991,7 +6183,9 @@ var TOOLS = [
|
|
|
4991
6183
|
// Live-control: iris_end_session / iris_resume / iris_messages. See live-control-tools.ts.
|
|
4992
6184
|
...LIVE_CONTROL_TOOLS,
|
|
4993
6185
|
// iris_navigate / iris_refresh — browser navigation tools. See browser-tools.ts.
|
|
4994
|
-
...BROWSER_TOOLS
|
|
6186
|
+
...BROWSER_TOOLS,
|
|
6187
|
+
// iris_version_info / iris_apply_update / iris_rollback — update lifecycle tools.
|
|
6188
|
+
...UPDATE_TOOLS
|
|
4995
6189
|
];
|
|
4996
6190
|
|
|
4997
6191
|
// ../server/dist/tools/profiles.js
|
|
@@ -5125,7 +6319,7 @@ async function runTool(tool, deps, args) {
|
|
|
5125
6319
|
return result;
|
|
5126
6320
|
if (!isPlainObject(result) || "session" in result)
|
|
5127
6321
|
return result;
|
|
5128
|
-
const session = deps.sessions.resolve(
|
|
6322
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5129
6323
|
const envelope = { ...healthEnvelope(session) };
|
|
5130
6324
|
const lease = session.takeSessionLease();
|
|
5131
6325
|
if (lease !== void 0)
|
|
@@ -5137,7 +6331,7 @@ async function runTool(tool, deps, args) {
|
|
|
5137
6331
|
}
|
|
5138
6332
|
|
|
5139
6333
|
// ../server/dist/mcp.js
|
|
5140
|
-
var SERVER_INFO = { name: "iris", version:
|
|
6334
|
+
var SERVER_INFO = { name: "iris", version: SERVER_VERSION };
|
|
5141
6335
|
var ENCODING_ENV = "IRIS_ENCODING";
|
|
5142
6336
|
var TOON_VALUE = "toon";
|
|
5143
6337
|
function encodeResult(result, useToon) {
|
|
@@ -5251,10 +6445,27 @@ function createToolInvoker(deps) {
|
|
|
5251
6445
|
};
|
|
5252
6446
|
}
|
|
5253
6447
|
|
|
6448
|
+
// ../server/dist/daemon.js
|
|
6449
|
+
import { join as join4 } from "path";
|
|
6450
|
+
import { homedir as homedir2 } from "os";
|
|
6451
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, unlinkSync, openSync } from "fs";
|
|
6452
|
+
import { spawn } from "child_process";
|
|
6453
|
+
var IRIS_HOME2 = join4(homedir2(), ".iris");
|
|
6454
|
+
|
|
5254
6455
|
// ../server/dist/index.js
|
|
5255
6456
|
async function start(options = {}) {
|
|
5256
6457
|
const port = options.port ?? IRIS_DEFAULT_PORT;
|
|
5257
|
-
const
|
|
6458
|
+
const envToken = process.env["IRIS_TOKEN"];
|
|
6459
|
+
const envOrigins = process.env["IRIS_ALLOWED_ORIGINS"];
|
|
6460
|
+
const host = options.host ?? process.env["IRIS_HOST"];
|
|
6461
|
+
const token = options.token ?? (envToken !== void 0 && envToken.length > 0 ? envToken : void 0);
|
|
6462
|
+
const allowedOrigins = options.allowedOrigins ?? envOrigins?.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
|
|
6463
|
+
const bridge = new Bridge({
|
|
6464
|
+
port,
|
|
6465
|
+
...host === void 0 ? {} : { host },
|
|
6466
|
+
...token === void 0 ? {} : { token },
|
|
6467
|
+
...allowedOrigins === void 0 ? {} : { allowedOrigins }
|
|
6468
|
+
});
|
|
5258
6469
|
const reaper = new SessionReaper(bridge.sessions);
|
|
5259
6470
|
reaper.start();
|
|
5260
6471
|
const baselines = new BaselineStore();
|
|
@@ -5283,11 +6494,11 @@ async function start(options = {}) {
|
|
|
5283
6494
|
}
|
|
5284
6495
|
}
|
|
5285
6496
|
if (options.mcp !== false) {
|
|
5286
|
-
const
|
|
5287
|
-
const irisRoot = options.irisRoot ??
|
|
6497
|
+
const fs2 = createNodeFileSystem();
|
|
6498
|
+
const irisRoot = options.irisRoot ?? join5(process.cwd(), IrisDir.ROOT);
|
|
5288
6499
|
const now = options.now ?? (() => Date.now());
|
|
5289
|
-
const flows = new FlowStore(
|
|
5290
|
-
const project = new ProjectStore(
|
|
6500
|
+
const flows = new FlowStore(fs2, irisRoot, { now });
|
|
6501
|
+
const project = new ProjectStore(fs2, irisRoot, { now });
|
|
5291
6502
|
const annotations = new AnnotationStore();
|
|
5292
6503
|
const deps = {
|
|
5293
6504
|
sessions: bridge.sessions,
|
|
@@ -5296,7 +6507,7 @@ async function start(options = {}) {
|
|
|
5296
6507
|
annotations,
|
|
5297
6508
|
flows,
|
|
5298
6509
|
project,
|
|
5299
|
-
fs,
|
|
6510
|
+
fs: fs2,
|
|
5300
6511
|
irisRoot,
|
|
5301
6512
|
now
|
|
5302
6513
|
};
|
|
@@ -5321,17 +6532,17 @@ async function start(options = {}) {
|
|
|
5321
6532
|
|
|
5322
6533
|
// ../test/dist/boot.js
|
|
5323
6534
|
function defaultBuildDeps(server, opts) {
|
|
5324
|
-
const
|
|
5325
|
-
const irisRoot = opts.irisRoot ??
|
|
6535
|
+
const fs2 = createNodeFileSystem();
|
|
6536
|
+
const irisRoot = opts.irisRoot ?? join6(process.cwd(), IrisDir.ROOT);
|
|
5326
6537
|
const now = opts.now ?? (() => Date.now());
|
|
5327
6538
|
const base = {
|
|
5328
6539
|
sessions: server.bridge.sessions,
|
|
5329
6540
|
baselines: new BaselineStore(),
|
|
5330
6541
|
recordings: new RecordingStore(),
|
|
5331
|
-
flows: new FlowStore(
|
|
5332
|
-
project: new ProjectStore(
|
|
6542
|
+
flows: new FlowStore(fs2, irisRoot, { now }),
|
|
6543
|
+
project: new ProjectStore(fs2, irisRoot, { now }),
|
|
5333
6544
|
annotations: new AnnotationStore(),
|
|
5334
|
-
fs,
|
|
6545
|
+
fs: fs2,
|
|
5335
6546
|
irisRoot,
|
|
5336
6547
|
now
|
|
5337
6548
|
};
|
|
@@ -5596,53 +6807,6 @@ function createTestContext(invoke, options = {}) {
|
|
|
5596
6807
|
};
|
|
5597
6808
|
}
|
|
5598
6809
|
|
|
5599
|
-
// ../test/dist/success-assert.js
|
|
5600
|
-
function successToPredicate(success, dynamic) {
|
|
5601
|
-
const parts = [];
|
|
5602
|
-
if (success.signal !== void 0) {
|
|
5603
|
-
parts.push(success.signalData !== void 0 ? { kind: PredicateKind.SIGNAL, name: success.signal, dataMatches: success.signalData } : { kind: PredicateKind.SIGNAL, name: success.signal });
|
|
5604
|
-
}
|
|
5605
|
-
if (success.net !== void 0) {
|
|
5606
|
-
const net = { kind: PredicateKind.NET };
|
|
5607
|
-
if (success.net.method !== void 0)
|
|
5608
|
-
net.method = success.net.method;
|
|
5609
|
-
if (success.net.urlContains !== void 0)
|
|
5610
|
-
net.urlContains = success.net.urlContains;
|
|
5611
|
-
if (success.net.status !== void 0)
|
|
5612
|
-
net.status = success.net.status;
|
|
5613
|
-
parts.push(net);
|
|
5614
|
-
}
|
|
5615
|
-
const element = success.element;
|
|
5616
|
-
if (element !== void 0) {
|
|
5617
|
-
const testid = element.testid;
|
|
5618
|
-
if (testid === void 0 || !dynamic.has(testid)) {
|
|
5619
|
-
const query = {};
|
|
5620
|
-
if (testid !== void 0)
|
|
5621
|
-
query["testid"] = testid;
|
|
5622
|
-
if (element.role !== void 0)
|
|
5623
|
-
query["role"] = element.role;
|
|
5624
|
-
if (element.name !== void 0)
|
|
5625
|
-
query["name"] = element.name;
|
|
5626
|
-
if (Object.keys(query).length > 0) {
|
|
5627
|
-
parts.push({ kind: PredicateKind.ELEMENT, query });
|
|
5628
|
-
}
|
|
5629
|
-
}
|
|
5630
|
-
}
|
|
5631
|
-
if (parts.length === 0)
|
|
5632
|
-
return void 0;
|
|
5633
|
-
if (parts.length === 1)
|
|
5634
|
-
return parts[0];
|
|
5635
|
-
return { kind: "allOf", predicates: parts };
|
|
5636
|
-
}
|
|
5637
|
-
async function assertSuccess(session, success, dynamic, waitForSignal, timeoutMs) {
|
|
5638
|
-
if (success === void 0)
|
|
5639
|
-
return { pass: true };
|
|
5640
|
-
const predicate = successToPredicate(success, dynamic);
|
|
5641
|
-
if (predicate === void 0)
|
|
5642
|
-
return { pass: true };
|
|
5643
|
-
return waitForSignal(session, predicate, timeoutMs);
|
|
5644
|
-
}
|
|
5645
|
-
|
|
5646
6810
|
// ../test/dist/flow-spec.js
|
|
5647
6811
|
function toDynamicSet(flow) {
|
|
5648
6812
|
return new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|