@syrin/iris 0.5.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 +2587 -1027
- package/dist/index.js +414 -114
- package/dist/next.js +5 -1
- package/dist/server.d.ts +9 -0
- package/dist/server.js +1903 -1005
- package/dist/test.js +1918 -1074
- package/dist/vite.d.ts +43 -0
- package/dist/vite.js +144 -0
- package/package.json +15 -10
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
// ../server/dist/cli.js
|
|
4
4
|
import { pathToFileURL } from "url";
|
|
5
|
+
import { realpathSync } from "fs";
|
|
5
6
|
|
|
6
7
|
// ../protocol/dist/constants.js
|
|
7
8
|
var IRIS_DEFAULT_PORT = 4400;
|
|
8
9
|
var IRIS_WS_PATH = "/iris";
|
|
10
|
+
var IRIS_PROTOCOL_VERSION = 1;
|
|
11
|
+
var TRANSPORT_LIMITS = {
|
|
12
|
+
MAX_MESSAGE_BYTES: 1024 * 1024,
|
|
13
|
+
MAX_MESSAGES_PER_SECOND: 1e3,
|
|
14
|
+
MAX_SESSIONS: 32,
|
|
15
|
+
MAX_PENDING_CONNECTIONS: 16,
|
|
16
|
+
HELLO_TIMEOUT_MS: 5e3,
|
|
17
|
+
MAX_BUFFER_BYTES: 8 * 1024 * 1024,
|
|
18
|
+
MAX_SESSION_ID_LENGTH: 128,
|
|
19
|
+
MAX_URL_LENGTH: 4096,
|
|
20
|
+
MAX_TITLE_LENGTH: 512,
|
|
21
|
+
MAX_ADAPTERS: 32,
|
|
22
|
+
MAX_ADAPTER_NAME_LENGTH: 128,
|
|
23
|
+
MAX_TOKEN_LENGTH: 512,
|
|
24
|
+
MAX_COMMAND_ID_LENGTH: 128,
|
|
25
|
+
MAX_COMMAND_NAME_LENGTH: 128,
|
|
26
|
+
MAX_REF_LENGTH: 128,
|
|
27
|
+
MAX_ERROR_LENGTH: 4096,
|
|
28
|
+
MAX_SERIALIZE_DEPTH: 8,
|
|
29
|
+
MAX_COLLECTION_ITEMS: 200,
|
|
30
|
+
MAX_OBJECT_KEYS: 200,
|
|
31
|
+
MAX_STRING_LENGTH: 64 * 1024
|
|
32
|
+
};
|
|
33
|
+
var DANGEROUS_ACTION_CONFIRM_ARG = "confirmDangerous";
|
|
9
34
|
var REPLAY_PROGRAM_VERSION = 1;
|
|
10
35
|
var IrisDir = {
|
|
11
36
|
ROOT: ".iris",
|
|
@@ -107,7 +132,7 @@ var ReplayStatus = {
|
|
|
107
132
|
DRIFT: "drift",
|
|
108
133
|
// an anchor missed (testid renamed / signal not observed) — legible drift returned
|
|
109
134
|
ERROR: "error"
|
|
110
|
-
// the flow
|
|
135
|
+
// the flow could not load or a resolved action failed
|
|
111
136
|
};
|
|
112
137
|
var DriftReason = {
|
|
113
138
|
TESTID_NOT_FOUND: "testid_not_found",
|
|
@@ -141,8 +166,10 @@ var HealStatus = {
|
|
|
141
166
|
// drift exists but no proposal cleared the confidence floor
|
|
142
167
|
NOTHING_TO_HEAL: "nothing_to_heal",
|
|
143
168
|
// replay was green
|
|
169
|
+
CONSEQUENCE_BROKEN: "consequence_broken",
|
|
170
|
+
// rebind resolves a locator but the flow's success consequence no longer fires — REFUSED (file untouched)
|
|
144
171
|
ERROR: "error"
|
|
145
|
-
// flow missing/malformed/invalid-name
|
|
172
|
+
// flow missing/malformed/invalid-name, or a resolved action failed
|
|
146
173
|
};
|
|
147
174
|
var HEAL_CONFIDENCE_MIN = 0.5;
|
|
148
175
|
var AnnotationTarget = {
|
|
@@ -158,7 +185,8 @@ var AnnotationErrorCode = {
|
|
|
158
185
|
var COMPILED_PREDICATE_PREFIX = "will";
|
|
159
186
|
var RING_BUFFER_DEFAULTS = {
|
|
160
187
|
MAX_EVENTS: 2e3,
|
|
161
|
-
MAX_AGE_MS: 6e4
|
|
188
|
+
MAX_AGE_MS: 6e4,
|
|
189
|
+
MAX_BYTES: TRANSPORT_LIMITS.MAX_BUFFER_BYTES
|
|
162
190
|
};
|
|
163
191
|
var EventType = {
|
|
164
192
|
DOM_ADDED: "dom.added",
|
|
@@ -343,6 +371,8 @@ var MessageKind = {
|
|
|
343
371
|
|
|
344
372
|
// ../protocol/dist/messages.js
|
|
345
373
|
import { z } from "zod";
|
|
374
|
+
var sessionIdSchema = z.string().min(1).max(TRANSPORT_LIMITS.MAX_SESSION_ID_LENGTH);
|
|
375
|
+
var refSchema = z.string().max(TRANSPORT_LIMITS.MAX_REF_LENGTH);
|
|
346
376
|
var HumanControlDataSchema = z.object({
|
|
347
377
|
kind: z.nativeEnum(HumanControlKind),
|
|
348
378
|
text: z.string().optional()
|
|
@@ -350,35 +380,37 @@ var HumanControlDataSchema = z.object({
|
|
|
350
380
|
var IrisEventSchema = z.object({
|
|
351
381
|
t: z.number(),
|
|
352
382
|
type: z.nativeEnum(EventType),
|
|
353
|
-
sessionId:
|
|
383
|
+
sessionId: sessionIdSchema,
|
|
354
384
|
/** Stable element reference this event concerns, when applicable (e.g. "e7"). */
|
|
355
|
-
ref:
|
|
385
|
+
ref: refSchema.optional(),
|
|
356
386
|
/** Event-type-specific payload. Kept open here; refined per observer at the edges. */
|
|
357
387
|
data: z.record(z.unknown()).default({})
|
|
358
388
|
});
|
|
359
389
|
var HelloMessageSchema = z.object({
|
|
360
390
|
kind: z.literal(MessageKind.HELLO),
|
|
361
|
-
protocolVersion: z.
|
|
362
|
-
sessionId:
|
|
363
|
-
url: z.string(),
|
|
364
|
-
title: z.string(),
|
|
365
|
-
adapters: z.array(z.string()),
|
|
391
|
+
protocolVersion: z.literal(IRIS_PROTOCOL_VERSION),
|
|
392
|
+
sessionId: sessionIdSchema,
|
|
393
|
+
url: z.string().max(TRANSPORT_LIMITS.MAX_URL_LENGTH),
|
|
394
|
+
title: z.string().max(TRANSPORT_LIMITS.MAX_TITLE_LENGTH),
|
|
395
|
+
adapters: z.array(z.string().max(TRANSPORT_LIMITS.MAX_ADAPTER_NAME_LENGTH)).max(TRANSPORT_LIMITS.MAX_ADAPTERS),
|
|
396
|
+
/** Optional browser/bridge pairing token. Required when the bridge configures one. */
|
|
397
|
+
token: z.string().max(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH).optional(),
|
|
366
398
|
/** Whether the app has advertised a capability registry (iris.describe). */
|
|
367
399
|
hasCapabilities: z.boolean().optional()
|
|
368
400
|
});
|
|
369
401
|
var CommandMessageSchema = z.object({
|
|
370
402
|
kind: z.literal(MessageKind.COMMAND),
|
|
371
|
-
id: z.string(),
|
|
372
|
-
sessionId:
|
|
373
|
-
name: z.string(),
|
|
403
|
+
id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
|
|
404
|
+
sessionId: sessionIdSchema.optional(),
|
|
405
|
+
name: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_NAME_LENGTH),
|
|
374
406
|
args: z.record(z.unknown()).default({})
|
|
375
407
|
});
|
|
376
408
|
var CommandResultSchema = z.object({
|
|
377
409
|
kind: z.literal(MessageKind.COMMAND_RESULT),
|
|
378
|
-
id: z.string(),
|
|
410
|
+
id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
|
|
379
411
|
ok: z.boolean(),
|
|
380
412
|
result: z.unknown().optional(),
|
|
381
|
-
error: z.string().optional()
|
|
413
|
+
error: z.string().max(TRANSPORT_LIMITS.MAX_ERROR_LENGTH).optional()
|
|
382
414
|
});
|
|
383
415
|
var EventMessageSchema = z.object({
|
|
384
416
|
kind: z.literal(MessageKind.EVENT),
|
|
@@ -391,6 +423,25 @@ var IrisMessageSchema = z.discriminatedUnion("kind", [
|
|
|
391
423
|
EventMessageSchema
|
|
392
424
|
]);
|
|
393
425
|
|
|
426
|
+
// ../protocol/dist/security.js
|
|
427
|
+
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;
|
|
428
|
+
function isLoopbackHostname(hostname) {
|
|
429
|
+
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
430
|
+
if (normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") {
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
const octets = normalized.split(".");
|
|
434
|
+
return octets.length === 4 && octets[0] === "127" && octets.every((octet) => {
|
|
435
|
+
if (!/^\d{1,3}$/.test(octet))
|
|
436
|
+
return false;
|
|
437
|
+
const value = Number(octet);
|
|
438
|
+
return value >= 0 && value <= 255;
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
function isDangerousActionText(text) {
|
|
442
|
+
return DANGEROUS_ACTION.test(text.replace(/[_-]+/g, " "));
|
|
443
|
+
}
|
|
444
|
+
|
|
394
445
|
// ../protocol/dist/toon.js
|
|
395
446
|
var ROLE_MAP = {
|
|
396
447
|
button: "btn",
|
|
@@ -742,6 +793,7 @@ function createSharedServer() {
|
|
|
742
793
|
}
|
|
743
794
|
|
|
744
795
|
// ../server/dist/bridge.js
|
|
796
|
+
import { timingSafeEqual } from "crypto";
|
|
745
797
|
import * as http2 from "http";
|
|
746
798
|
import { WebSocketServer } from "ws";
|
|
747
799
|
|
|
@@ -749,14 +801,21 @@ import { WebSocketServer } from "ws";
|
|
|
749
801
|
var RingBuffer = class {
|
|
750
802
|
#maxEvents;
|
|
751
803
|
#maxAgeMs;
|
|
804
|
+
#maxBytes;
|
|
752
805
|
#events = [];
|
|
806
|
+
#eventBytes = [];
|
|
807
|
+
#totalBytes = 0;
|
|
753
808
|
#droppedCount = 0;
|
|
754
809
|
constructor(options = {}) {
|
|
755
810
|
this.#maxEvents = options.maxEvents ?? RING_BUFFER_DEFAULTS.MAX_EVENTS;
|
|
756
811
|
this.#maxAgeMs = options.maxAgeMs ?? RING_BUFFER_DEFAULTS.MAX_AGE_MS;
|
|
812
|
+
this.#maxBytes = options.maxBytes ?? RING_BUFFER_DEFAULTS.MAX_BYTES;
|
|
757
813
|
}
|
|
758
814
|
push(event, now) {
|
|
759
815
|
this.#events.push(event);
|
|
816
|
+
const bytes = Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
817
|
+
this.#eventBytes.push(bytes);
|
|
818
|
+
this.#totalBytes += bytes;
|
|
760
819
|
this.#evict(now);
|
|
761
820
|
}
|
|
762
821
|
/** Events at or after a given timestamp cursor. */
|
|
@@ -782,10 +841,14 @@ var RingBuffer = class {
|
|
|
782
841
|
#evict(now) {
|
|
783
842
|
const before = this.#events.length;
|
|
784
843
|
const cutoff = now - this.#maxAgeMs;
|
|
785
|
-
|
|
786
|
-
this.#events
|
|
844
|
+
while (this.#events.length > this.#maxEvents || this.#totalBytes > this.#maxBytes && this.#events.length > 0) {
|
|
845
|
+
this.#events.shift();
|
|
846
|
+
this.#totalBytes -= this.#eventBytes.shift() ?? 0;
|
|
847
|
+
}
|
|
848
|
+
while ((this.#events[0]?.t ?? cutoff) < cutoff) {
|
|
849
|
+
this.#events.shift();
|
|
850
|
+
this.#totalBytes -= this.#eventBytes.shift() ?? 0;
|
|
787
851
|
}
|
|
788
|
-
this.#events = this.#events.filter((e) => e.t >= cutoff);
|
|
789
852
|
this.#droppedCount += before - this.#events.length;
|
|
790
853
|
}
|
|
791
854
|
/** Snapshot of buffer health for the agent — total events held and cumulative drops since connect. */
|
|
@@ -1022,6 +1085,14 @@ var Session = class {
|
|
|
1022
1085
|
this.#pending.delete(id);
|
|
1023
1086
|
}
|
|
1024
1087
|
}
|
|
1088
|
+
/** End this transport without letting a stale socket remove its replacement session. */
|
|
1089
|
+
disconnect(reason) {
|
|
1090
|
+
this.rejectAll(reason);
|
|
1091
|
+
try {
|
|
1092
|
+
this.#socket.close(1008, reason);
|
|
1093
|
+
} catch {
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1025
1096
|
// ── Live-control: state machine + human→agent inbox (server-owned) ───────────────
|
|
1026
1097
|
getState() {
|
|
1027
1098
|
return this.#state;
|
|
@@ -1143,11 +1214,15 @@ var Session = class {
|
|
|
1143
1214
|
var SessionManager = class {
|
|
1144
1215
|
#sessions = /* @__PURE__ */ new Map();
|
|
1145
1216
|
add(session) {
|
|
1217
|
+
const previous = this.#sessions.get(session.id);
|
|
1146
1218
|
this.#sessions.set(session.id, session);
|
|
1219
|
+
return previous;
|
|
1147
1220
|
}
|
|
1148
|
-
remove(
|
|
1149
|
-
this.#sessions.get(
|
|
1150
|
-
|
|
1221
|
+
remove(session) {
|
|
1222
|
+
if (this.#sessions.get(session.id) !== session)
|
|
1223
|
+
return false;
|
|
1224
|
+
session.rejectAll("session disconnected");
|
|
1225
|
+
return this.#sessions.delete(session.id);
|
|
1151
1226
|
}
|
|
1152
1227
|
get(sessionId) {
|
|
1153
1228
|
return this.#sessions.get(sessionId);
|
|
@@ -1215,6 +1290,20 @@ var SessionManager = class {
|
|
|
1215
1290
|
};
|
|
1216
1291
|
|
|
1217
1292
|
// ../server/dist/bridge.js
|
|
1293
|
+
function normalizeOrigin(origin) {
|
|
1294
|
+
try {
|
|
1295
|
+
return new URL(origin).origin;
|
|
1296
|
+
} catch {
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function tokensMatch(expected, received) {
|
|
1301
|
+
if (received === void 0)
|
|
1302
|
+
return false;
|
|
1303
|
+
const expectedBytes = Buffer.from(expected);
|
|
1304
|
+
const receivedBytes = Buffer.from(received);
|
|
1305
|
+
return expectedBytes.length === receivedBytes.length && timingSafeEqual(expectedBytes, receivedBytes);
|
|
1306
|
+
}
|
|
1218
1307
|
function rawToString(raw) {
|
|
1219
1308
|
if (typeof raw === "string")
|
|
1220
1309
|
return raw;
|
|
@@ -1230,11 +1319,41 @@ var Bridge = class {
|
|
|
1230
1319
|
ready;
|
|
1231
1320
|
#wss;
|
|
1232
1321
|
#clock;
|
|
1322
|
+
#token;
|
|
1323
|
+
#allowedOrigins;
|
|
1324
|
+
#maxMessagesPerSecond;
|
|
1325
|
+
#maxSessions;
|
|
1326
|
+
#maxPendingConnections;
|
|
1327
|
+
#helloTimeoutMs;
|
|
1328
|
+
#pendingConnections = 0;
|
|
1233
1329
|
constructor(options) {
|
|
1330
|
+
const host = options.host ?? "127.0.0.1";
|
|
1331
|
+
if ((options.token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
|
|
1332
|
+
throw new Error(`Iris pairing token exceeds ${String(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH)} characters`);
|
|
1333
|
+
}
|
|
1334
|
+
if (!isLoopbackHostname(host) && (options.token === void 0 || options.token.length === 0)) {
|
|
1335
|
+
throw new Error("a pairing token is required when the Iris bridge binds beyond localhost");
|
|
1336
|
+
}
|
|
1234
1337
|
this.#clock = options.clock ?? (() => Date.now());
|
|
1338
|
+
this.#token = options.token !== void 0 && options.token.length > 0 ? options.token : void 0;
|
|
1339
|
+
this.#allowedOrigins = new Set((options.allowedOrigins ?? []).map(normalizeOrigin).filter((origin) => origin !== null));
|
|
1340
|
+
this.#maxMessagesPerSecond = options.maxMessagesPerSecond ?? TRANSPORT_LIMITS.MAX_MESSAGES_PER_SECOND;
|
|
1341
|
+
this.#maxSessions = options.maxSessions ?? TRANSPORT_LIMITS.MAX_SESSIONS;
|
|
1342
|
+
this.#maxPendingConnections = options.maxPendingConnections ?? TRANSPORT_LIMITS.MAX_PENDING_CONNECTIONS;
|
|
1343
|
+
this.#helloTimeoutMs = options.helloTimeoutMs ?? TRANSPORT_LIMITS.HELLO_TIMEOUT_MS;
|
|
1235
1344
|
if (options.server !== void 0) {
|
|
1236
1345
|
const srv = options.server;
|
|
1237
|
-
this.#wss = new WebSocketServer({
|
|
1346
|
+
this.#wss = new WebSocketServer({
|
|
1347
|
+
server: srv,
|
|
1348
|
+
path: IRIS_WS_PATH,
|
|
1349
|
+
maxPayload: TRANSPORT_LIMITS.MAX_MESSAGE_BYTES,
|
|
1350
|
+
verifyClient: ({ origin }, done) => {
|
|
1351
|
+
const allowed = this.#originAllowed(origin);
|
|
1352
|
+
if (!allowed)
|
|
1353
|
+
log("origin_rejected", { origin: origin ?? "missing" });
|
|
1354
|
+
done(allowed, 403, "Forbidden");
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1238
1357
|
this.ready = new Promise((resolve) => {
|
|
1239
1358
|
if (srv.listening) {
|
|
1240
1359
|
resolve(srv.address().port);
|
|
@@ -1247,8 +1366,15 @@ var Bridge = class {
|
|
|
1247
1366
|
} else {
|
|
1248
1367
|
this.#wss = new WebSocketServer({
|
|
1249
1368
|
port: options.port,
|
|
1250
|
-
host
|
|
1251
|
-
path: IRIS_WS_PATH
|
|
1369
|
+
host,
|
|
1370
|
+
path: IRIS_WS_PATH,
|
|
1371
|
+
maxPayload: TRANSPORT_LIMITS.MAX_MESSAGE_BYTES,
|
|
1372
|
+
verifyClient: ({ origin }, done) => {
|
|
1373
|
+
const allowed = this.#originAllowed(origin);
|
|
1374
|
+
if (!allowed)
|
|
1375
|
+
log("origin_rejected", { origin: origin ?? "missing" });
|
|
1376
|
+
done(allowed, 403, "Forbidden");
|
|
1377
|
+
}
|
|
1252
1378
|
});
|
|
1253
1379
|
this.ready = new Promise((resolve) => {
|
|
1254
1380
|
this.#wss.on("listening", () => {
|
|
@@ -1261,14 +1387,64 @@ var Bridge = class {
|
|
|
1261
1387
|
});
|
|
1262
1388
|
}
|
|
1263
1389
|
#onConnection(socket) {
|
|
1390
|
+
if (this.#pendingConnections >= this.#maxPendingConnections) {
|
|
1391
|
+
socket.close(1013, "too many pending handshakes");
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
this.#pendingConnections += 1;
|
|
1395
|
+
let awaitingHello = true;
|
|
1264
1396
|
let session;
|
|
1397
|
+
let messageWindowStartedAt = this.#clock();
|
|
1398
|
+
let messagesInWindow = 0;
|
|
1399
|
+
const releasePending = () => {
|
|
1400
|
+
if (!awaitingHello)
|
|
1401
|
+
return;
|
|
1402
|
+
awaitingHello = false;
|
|
1403
|
+
this.#pendingConnections -= 1;
|
|
1404
|
+
};
|
|
1405
|
+
const helloTimer = setTimeout(() => {
|
|
1406
|
+
if (!awaitingHello)
|
|
1407
|
+
return;
|
|
1408
|
+
releasePending();
|
|
1409
|
+
socket.close(1008, "hello timeout");
|
|
1410
|
+
}, this.#helloTimeoutMs);
|
|
1265
1411
|
socket.on("message", (raw) => {
|
|
1412
|
+
const now = this.#clock();
|
|
1413
|
+
if (now - messageWindowStartedAt >= 1e3) {
|
|
1414
|
+
messageWindowStartedAt = now;
|
|
1415
|
+
messagesInWindow = 0;
|
|
1416
|
+
}
|
|
1417
|
+
messagesInWindow += 1;
|
|
1418
|
+
if (messagesInWindow > this.#maxMessagesPerSecond) {
|
|
1419
|
+
log("message_rate_exceeded", {});
|
|
1420
|
+
socket.close(1008, "message rate exceeded");
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1266
1423
|
const parsed = this.#parse(rawToString(raw));
|
|
1267
|
-
if (parsed === null)
|
|
1424
|
+
if (parsed === null) {
|
|
1425
|
+
socket.close(1008, "invalid message");
|
|
1268
1426
|
return;
|
|
1427
|
+
}
|
|
1269
1428
|
if (parsed.kind === MessageKind.HELLO) {
|
|
1429
|
+
if (session !== void 0) {
|
|
1430
|
+
socket.close(1008, "hello already received");
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
if (this.#token !== void 0 && !tokensMatch(this.#token, parsed.token)) {
|
|
1434
|
+
log("authentication_failed", {});
|
|
1435
|
+
socket.close(1008, "authentication failed");
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
const existing = this.sessions.get(parsed.sessionId);
|
|
1439
|
+
if (existing === void 0 && this.sessions.count() >= this.#maxSessions) {
|
|
1440
|
+
socket.close(1013, "session limit reached");
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
clearTimeout(helloTimer);
|
|
1444
|
+
releasePending();
|
|
1270
1445
|
session = new Session(parsed, socket, this.#clock);
|
|
1271
|
-
this.sessions.add(session);
|
|
1446
|
+
const replaced = this.sessions.add(session);
|
|
1447
|
+
replaced?.disconnect("session replaced by a newer connection");
|
|
1272
1448
|
log("session_connected", { sessionId: session.id, url: session.url });
|
|
1273
1449
|
return;
|
|
1274
1450
|
}
|
|
@@ -1282,15 +1458,28 @@ var Bridge = class {
|
|
|
1282
1458
|
}
|
|
1283
1459
|
});
|
|
1284
1460
|
socket.on("close", () => {
|
|
1461
|
+
clearTimeout(helloTimer);
|
|
1462
|
+
releasePending();
|
|
1285
1463
|
if (session !== void 0) {
|
|
1286
|
-
this.sessions.remove(session
|
|
1287
|
-
|
|
1464
|
+
if (this.sessions.remove(session)) {
|
|
1465
|
+
log("session_disconnected", { sessionId: session.id });
|
|
1466
|
+
}
|
|
1288
1467
|
}
|
|
1289
1468
|
});
|
|
1290
1469
|
socket.on("error", (err) => {
|
|
1291
1470
|
log("socket_error", { error: err.message });
|
|
1292
1471
|
});
|
|
1293
1472
|
}
|
|
1473
|
+
#originAllowed(origin) {
|
|
1474
|
+
if (origin === void 0)
|
|
1475
|
+
return true;
|
|
1476
|
+
const normalized = normalizeOrigin(origin);
|
|
1477
|
+
if (normalized === null)
|
|
1478
|
+
return false;
|
|
1479
|
+
if (this.#allowedOrigins.has(normalized))
|
|
1480
|
+
return true;
|
|
1481
|
+
return isLoopbackHostname(new URL(normalized).hostname);
|
|
1482
|
+
}
|
|
1294
1483
|
#parse(text) {
|
|
1295
1484
|
let json;
|
|
1296
1485
|
try {
|
|
@@ -1307,6 +1496,8 @@ var Bridge = class {
|
|
|
1307
1496
|
}
|
|
1308
1497
|
close() {
|
|
1309
1498
|
return new Promise((resolve) => {
|
|
1499
|
+
for (const client of this.#wss.clients)
|
|
1500
|
+
client.terminate();
|
|
1310
1501
|
this.#wss.close(() => {
|
|
1311
1502
|
resolve();
|
|
1312
1503
|
});
|
|
@@ -1421,6 +1612,7 @@ var IrisTool = {
|
|
|
1421
1612
|
STATE: "iris_state",
|
|
1422
1613
|
CAPABILITIES: "iris_capabilities",
|
|
1423
1614
|
CONTRACT_SAVE: "iris_contract_save",
|
|
1615
|
+
DOMAIN: "iris_domain",
|
|
1424
1616
|
FLOW_SAVE: "iris_flow_save",
|
|
1425
1617
|
FLOW_LIST: "iris_flow_list",
|
|
1426
1618
|
FLOW_LOAD: "iris_flow_load",
|
|
@@ -1463,189 +1655,558 @@ var IrisTool = {
|
|
|
1463
1655
|
ROLLBACK: "iris_rollback"
|
|
1464
1656
|
};
|
|
1465
1657
|
|
|
1466
|
-
// ../server/dist/
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
baselines: join(root, IrisDir.BASELINES_SUBDIR),
|
|
1474
|
-
project: join(root, IrisDir.PROJECT_FILE),
|
|
1475
|
-
visual: join(root, IrisDir.VISUAL_SUBDIR)
|
|
1476
|
-
};
|
|
1477
|
-
}
|
|
1478
|
-
function visualPath(root, name) {
|
|
1479
|
-
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.png`);
|
|
1480
|
-
}
|
|
1481
|
-
function visualDiffPath(root, name) {
|
|
1482
|
-
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.diff.png`);
|
|
1483
|
-
}
|
|
1484
|
-
function flowPath(root, name) {
|
|
1485
|
-
return join(root, IrisDir.FLOWS_SUBDIR, `${name}.json`);
|
|
1486
|
-
}
|
|
1487
|
-
function isValidFlowName(name) {
|
|
1488
|
-
return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
|
|
1489
|
-
}
|
|
1490
|
-
async function ensureIrisDir(fs2, root) {
|
|
1491
|
-
const p = irisDirPaths(root);
|
|
1492
|
-
await fs2.mkdir(p.root);
|
|
1493
|
-
await fs2.mkdir(p.flows);
|
|
1494
|
-
await fs2.mkdir(p.baselines);
|
|
1495
|
-
}
|
|
1496
|
-
var JSON_INDENT = 2;
|
|
1497
|
-
function stableSerialize(capabilities, generatedAt) {
|
|
1498
|
-
const envelope = {
|
|
1499
|
-
version: CONTRACT_FILE_VERSION,
|
|
1500
|
-
generatedAt,
|
|
1501
|
-
capabilities: {
|
|
1502
|
-
testids: [...capabilities.testids].sort(),
|
|
1503
|
-
signals: [...capabilities.signals].sort(),
|
|
1504
|
-
stores: [...capabilities.stores].sort(),
|
|
1505
|
-
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)
|
|
1658
|
+
// ../server/dist/tools/tools-helpers.js
|
|
1659
|
+
function parseInteractive(tree) {
|
|
1660
|
+
const items = [];
|
|
1661
|
+
for (const line of tree.split("\n")) {
|
|
1662
|
+
const match = /\(ref=(e\d+)\)/.exec(line);
|
|
1663
|
+
if (match !== null) {
|
|
1664
|
+
items.push({ ref: match[1] ?? "", desc: line.replace(/\s*\(ref=e\d+\)/, "").trim() });
|
|
1506
1665
|
}
|
|
1507
|
-
};
|
|
1508
|
-
return `${JSON.stringify(envelope, null, JSON_INDENT)}
|
|
1509
|
-
`;
|
|
1510
|
-
}
|
|
1511
|
-
async function writeContract(fs2, root, capabilities, now) {
|
|
1512
|
-
await ensureIrisDir(fs2, root);
|
|
1513
|
-
await fs2.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
|
|
1514
|
-
}
|
|
1515
|
-
async function readContract(fs2, root) {
|
|
1516
|
-
const path = irisDirPaths(root).contract;
|
|
1517
|
-
if (!await fs2.exists(path))
|
|
1518
|
-
return { ok: false, reason: ContractReadError.MISSING };
|
|
1519
|
-
let text;
|
|
1520
|
-
try {
|
|
1521
|
-
text = await fs2.readFile(path);
|
|
1522
|
-
} catch (error) {
|
|
1523
|
-
return {
|
|
1524
|
-
ok: false,
|
|
1525
|
-
reason: fs2.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
|
|
1526
|
-
};
|
|
1527
|
-
}
|
|
1528
|
-
let parsed;
|
|
1529
|
-
try {
|
|
1530
|
-
parsed = JSON.parse(text);
|
|
1531
|
-
} catch {
|
|
1532
|
-
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
1533
1666
|
}
|
|
1534
|
-
|
|
1535
|
-
if (!result.success)
|
|
1536
|
-
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
1537
|
-
return { ok: true, capabilities: result.data.capabilities, generatedAt: result.data.generatedAt };
|
|
1667
|
+
return items;
|
|
1538
1668
|
}
|
|
1539
|
-
|
|
1540
|
-
// ../server/dist/flows/flows.js
|
|
1541
1669
|
function asString(value) {
|
|
1542
1670
|
return typeof value === "string" ? value : void 0;
|
|
1543
1671
|
}
|
|
1672
|
+
function asNumber(value) {
|
|
1673
|
+
return typeof value === "number" ? value : void 0;
|
|
1674
|
+
}
|
|
1544
1675
|
function asRecord(value) {
|
|
1545
1676
|
return typeof value === "object" && value !== null ? value : {};
|
|
1546
1677
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1678
|
+
|
|
1679
|
+
// ../server/dist/flows/replay.js
|
|
1680
|
+
function asString2(value) {
|
|
1681
|
+
return typeof value === "string" ? value : void 0;
|
|
1549
1682
|
}
|
|
1550
|
-
function
|
|
1551
|
-
|
|
1552
|
-
const by = asString(sub["by"]);
|
|
1553
|
-
const value = asString(sub["value"]);
|
|
1554
|
-
const action = asString(sub["action"]);
|
|
1555
|
-
const args = asRecord(sub["args"]);
|
|
1556
|
-
if (by === QueryBy.TESTID && value !== void 0) {
|
|
1557
|
-
return buildStep(IrisTool.ACT, { kind: AnchorKind.TESTID, value }, action, args, false);
|
|
1558
|
-
}
|
|
1559
|
-
return buildStep(IrisTool.ACT, degradedAnchor(), action, args, true);
|
|
1683
|
+
function asRecord2(value) {
|
|
1684
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
1560
1685
|
}
|
|
1561
|
-
function
|
|
1562
|
-
const
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
return step;
|
|
1686
|
+
function replayActionArgs(value, confirmDangerous = false) {
|
|
1687
|
+
const args = { ...asRecord2(value) };
|
|
1688
|
+
delete args[DANGEROUS_ACTION_CONFIRM_ARG];
|
|
1689
|
+
if (confirmDangerous)
|
|
1690
|
+
args[DANGEROUS_ACTION_CONFIRM_ARG] = true;
|
|
1691
|
+
return args;
|
|
1568
1692
|
}
|
|
1569
|
-
function
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
out2.expect = step.expect;
|
|
1580
|
-
return out2;
|
|
1693
|
+
function compileActStep(args, res) {
|
|
1694
|
+
const testid = asString2(asRecord2(res)["testid"]);
|
|
1695
|
+
const action = asString2(args["action"]) ?? "";
|
|
1696
|
+
const actArgs = replayActionArgs(args["args"]);
|
|
1697
|
+
if (testid !== void 0) {
|
|
1698
|
+
return {
|
|
1699
|
+
tool: IrisTool.ACT,
|
|
1700
|
+
stable: true,
|
|
1701
|
+
args: { by: QueryBy.TESTID, value: testid, action, args: actArgs }
|
|
1702
|
+
};
|
|
1581
1703
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
if (step.expect !== void 0)
|
|
1588
|
-
out.expect = step.expect;
|
|
1589
|
-
return out;
|
|
1704
|
+
return {
|
|
1705
|
+
tool: IrisTool.ACT,
|
|
1706
|
+
stable: false,
|
|
1707
|
+
args: { ref: asString2(args["ref"]) ?? "", action, args: actArgs }
|
|
1708
|
+
};
|
|
1590
1709
|
}
|
|
1591
|
-
function
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1710
|
+
function compileSequenceStep(args, res) {
|
|
1711
|
+
const inputSteps = Array.isArray(args["steps"]) ? args["steps"] : [];
|
|
1712
|
+
const resolved = Array.isArray(asRecord2(res)["steps"]) ? asRecord2(res)["steps"] : [];
|
|
1713
|
+
let stable = inputSteps.length > 0;
|
|
1714
|
+
const subSteps = inputSteps.map((raw, i) => {
|
|
1715
|
+
const step = asRecord2(raw);
|
|
1716
|
+
const action = asString2(step["action"]) ?? "";
|
|
1717
|
+
const stepArgs = replayActionArgs(step["args"]);
|
|
1718
|
+
const testid = asString2(asRecord2(resolved[i])["testid"]);
|
|
1719
|
+
if (testid !== void 0) {
|
|
1720
|
+
return { by: QueryBy.TESTID, value: testid, action, args: stepArgs };
|
|
1721
|
+
}
|
|
1722
|
+
stable = false;
|
|
1723
|
+
return { ref: asString2(step["ref"]) ?? "", action, args: stepArgs };
|
|
1597
1724
|
});
|
|
1598
|
-
|
|
1599
|
-
if (ann.dynamic.length > 0) {
|
|
1600
|
-
out.dynamic = ann.dynamic.map((value) => ({ kind: AnchorKind.TESTID, value }));
|
|
1601
|
-
}
|
|
1602
|
-
if (ann.success !== void 0)
|
|
1603
|
-
out.success = ann.success;
|
|
1604
|
-
return out;
|
|
1725
|
+
return { tool: IrisTool.ACT_SEQUENCE, stable, args: { steps: subSteps } };
|
|
1605
1726
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
* The single byte-stable flow serializer: 2-space indent + one trailing newline. save(),
|
|
1619
|
-
* saveFlow() and heal() all route through it so an unchanged flow that round-trips through any
|
|
1620
|
-
* of them produces byte-identical on-disk content (locked by the byte-stability tests).
|
|
1621
|
-
*/
|
|
1622
|
-
#serialize(flow) {
|
|
1623
|
-
return `${JSON.stringify(flow, null, JSON_INDENT2)}
|
|
1624
|
-
`;
|
|
1727
|
+
async function resolveRef(session, step) {
|
|
1728
|
+
const by = asString2(step.by);
|
|
1729
|
+
const value = asString2(step.value);
|
|
1730
|
+
if (by === QueryBy.TESTID && value !== void 0) {
|
|
1731
|
+
const result = await session.command(IrisCommand.QUERY, { by, value });
|
|
1732
|
+
if (!result.ok)
|
|
1733
|
+
throw new Error(result.error ?? "query failed");
|
|
1734
|
+
const elements = Array.isArray(asRecord2(result.result)["elements"]) ? asRecord2(result.result)["elements"] : [];
|
|
1735
|
+
const ref2 = asString2(asRecord2(elements[0])["ref"]);
|
|
1736
|
+
if (ref2 === void 0)
|
|
1737
|
+
throw new Error(`testid '${value}' did not resolve in current page`);
|
|
1738
|
+
return elements.length > 1 ? { ref: ref2, note: `ambiguous testid '${value}', used first match` } : { ref: ref2 };
|
|
1625
1739
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1740
|
+
const ref = asString2(step.ref);
|
|
1741
|
+
if (ref === void 0 || ref.length === 0)
|
|
1742
|
+
throw new Error("step has no testid or ref to resolve");
|
|
1743
|
+
return { ref, note: "replayed by stale ref (not portable across sessions)" };
|
|
1744
|
+
}
|
|
1745
|
+
async function replayProgram(session, program, confirmDangerous = false) {
|
|
1746
|
+
const results = [];
|
|
1747
|
+
for (const step of program.steps) {
|
|
1748
|
+
try {
|
|
1749
|
+
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
1750
|
+
const subs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
1751
|
+
const notes = [];
|
|
1752
|
+
const liveSteps = [];
|
|
1753
|
+
for (const raw of subs) {
|
|
1754
|
+
const sub = asRecord2(raw);
|
|
1755
|
+
const { ref, note } = await resolveRef(session, sub);
|
|
1756
|
+
if (note !== void 0)
|
|
1757
|
+
notes.push(note);
|
|
1758
|
+
liveSteps.push({
|
|
1759
|
+
ref,
|
|
1760
|
+
action: asString2(sub["action"]) ?? "",
|
|
1761
|
+
args: replayActionArgs(sub["args"], confirmDangerous)
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
const r = await session.command(IrisCommand.ACT_SEQUENCE, { steps: liveSteps });
|
|
1765
|
+
results.push(buildResult(step.tool, r.ok, r.error, notes));
|
|
1766
|
+
if (!r.ok)
|
|
1767
|
+
break;
|
|
1768
|
+
} else {
|
|
1769
|
+
const { ref, note } = await resolveRef(session, step.args);
|
|
1770
|
+
const r = await session.command(IrisCommand.ACT, {
|
|
1771
|
+
ref,
|
|
1772
|
+
action: asString2(step.args["action"]) ?? "",
|
|
1773
|
+
args: replayActionArgs(step.args["args"], confirmDangerous)
|
|
1774
|
+
});
|
|
1775
|
+
results.push(buildResult(step.tool, r.ok, r.error, note !== void 0 ? [note] : []));
|
|
1776
|
+
if (!r.ok)
|
|
1777
|
+
break;
|
|
1778
|
+
}
|
|
1779
|
+
} catch (e) {
|
|
1780
|
+
results.push({
|
|
1781
|
+
tool: step.tool,
|
|
1782
|
+
ok: false,
|
|
1783
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1784
|
+
});
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return results;
|
|
1789
|
+
}
|
|
1790
|
+
function buildResult(tool, ok, error, notes) {
|
|
1791
|
+
const base = { tool, ok };
|
|
1792
|
+
if (!ok)
|
|
1793
|
+
base.error = error ?? "command failed";
|
|
1794
|
+
if (notes.length > 0)
|
|
1795
|
+
base.note = notes.join("; ");
|
|
1796
|
+
return base;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// ../server/dist/flows/flow-replay.js
|
|
1800
|
+
function editDistance(a, b) {
|
|
1801
|
+
const s = a.toLowerCase();
|
|
1802
|
+
const t = b.toLowerCase();
|
|
1803
|
+
const rows = s.length + 1;
|
|
1804
|
+
const cols = t.length + 1;
|
|
1805
|
+
const prev = new Array(cols);
|
|
1806
|
+
const curr = new Array(cols);
|
|
1807
|
+
for (let j = 0; j < cols; j++)
|
|
1808
|
+
prev[j] = j;
|
|
1809
|
+
for (let i = 1; i < rows; i++) {
|
|
1810
|
+
curr[0] = i;
|
|
1811
|
+
for (let j = 1; j < cols; j++) {
|
|
1812
|
+
const cost = s[i - 1] === t[j - 1] ? 0 : 1;
|
|
1813
|
+
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
1814
|
+
}
|
|
1815
|
+
for (let j = 0; j < cols; j++)
|
|
1816
|
+
prev[j] = curr[j] ?? 0;
|
|
1817
|
+
}
|
|
1818
|
+
return prev[cols - 1] ?? 0;
|
|
1819
|
+
}
|
|
1820
|
+
function nearestTestid(missing, present) {
|
|
1821
|
+
let best = null;
|
|
1822
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1823
|
+
for (const candidate of present) {
|
|
1824
|
+
const distance = editDistance(missing, candidate);
|
|
1825
|
+
if (distance < bestDistance || distance === bestDistance && best !== null && candidate.length < best.length || distance === bestDistance && best !== null && candidate.length === best.length && candidate < best) {
|
|
1826
|
+
best = candidate;
|
|
1827
|
+
bestDistance = distance;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return best;
|
|
1831
|
+
}
|
|
1832
|
+
function readQuery(result) {
|
|
1833
|
+
if (!result.ok)
|
|
1834
|
+
return { refs: [] };
|
|
1835
|
+
const payload = asRecord(result.result);
|
|
1836
|
+
const elements = Array.isArray(payload["elements"]) ? payload["elements"] : [];
|
|
1837
|
+
const refs = elements.map((e) => asString(asRecord(e)["ref"]) ?? "").filter((r) => r.length > 0);
|
|
1838
|
+
const rawHint = payload["hint"];
|
|
1839
|
+
if (typeof rawHint === "object" && rawHint !== null) {
|
|
1840
|
+
const hint = asRecord(rawHint);
|
|
1841
|
+
const present = Array.isArray(hint["presentTestids"]) ? hint["presentTestids"].filter((t) => typeof t === "string") : [];
|
|
1842
|
+
return {
|
|
1843
|
+
refs,
|
|
1844
|
+
hint: {
|
|
1845
|
+
route: asString(hint["route"]) ?? "",
|
|
1846
|
+
presentTestids: present,
|
|
1847
|
+
presentRegions: [],
|
|
1848
|
+
knownEmptyState: hint["knownEmptyState"] === true
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
return { refs };
|
|
1853
|
+
}
|
|
1854
|
+
function nearestIsAmbiguous(missing, present) {
|
|
1855
|
+
if (present.length < 2)
|
|
1856
|
+
return false;
|
|
1857
|
+
let min = Number.POSITIVE_INFINITY;
|
|
1858
|
+
let count = 0;
|
|
1859
|
+
for (const candidate of present) {
|
|
1860
|
+
const distance = editDistance(missing, candidate);
|
|
1861
|
+
if (distance < min) {
|
|
1862
|
+
min = distance;
|
|
1863
|
+
count = 1;
|
|
1864
|
+
} else if (distance === min) {
|
|
1865
|
+
count += 1;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
return count >= 2;
|
|
1869
|
+
}
|
|
1870
|
+
function testidDrift(value, hint) {
|
|
1871
|
+
const present = hint?.presentTestids ?? [];
|
|
1872
|
+
const drift = {
|
|
1873
|
+
reasonKind: DriftReason.TESTID_NOT_FOUND,
|
|
1874
|
+
reason: `testid "${value}" not found`,
|
|
1875
|
+
anchor: value,
|
|
1876
|
+
nearest: nearestTestid(value, present)
|
|
1877
|
+
};
|
|
1878
|
+
if (nearestIsAmbiguous(value, present))
|
|
1879
|
+
drift.ambiguous = true;
|
|
1880
|
+
return drift;
|
|
1881
|
+
}
|
|
1882
|
+
function anchorLabel(anchor) {
|
|
1883
|
+
if (anchor.kind === AnchorKind.TESTID)
|
|
1884
|
+
return anchor.value;
|
|
1885
|
+
if (anchor.kind === AnchorKind.SIGNAL)
|
|
1886
|
+
return anchor.name;
|
|
1887
|
+
return anchor.name ?? anchor.role;
|
|
1888
|
+
}
|
|
1889
|
+
async function runTestidStep(session, step, index, value, dynamic, confirmDangerous) {
|
|
1890
|
+
const queryResult = await session.command(IrisCommand.QUERY, { by: QueryBy.TESTID, value });
|
|
1891
|
+
const { refs, hint } = readQuery(queryResult);
|
|
1892
|
+
if (refs.length === 0) {
|
|
1893
|
+
return {
|
|
1894
|
+
step: index,
|
|
1895
|
+
tool: step.tool,
|
|
1896
|
+
anchor: value,
|
|
1897
|
+
ok: false,
|
|
1898
|
+
drift: testidDrift(value, hint)
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
const ref = refs[0] ?? "";
|
|
1902
|
+
const note = refs.length > 1 ? `ambiguous testid '${value}', used first match` : void 0;
|
|
1903
|
+
const act = await session.command(IrisCommand.ACT, {
|
|
1904
|
+
ref,
|
|
1905
|
+
action: step.action ?? "",
|
|
1906
|
+
args: replayActionArgs(step.args, confirmDangerous)
|
|
1907
|
+
});
|
|
1908
|
+
const result = { step: index, tool: step.tool, anchor: value, ok: act.ok };
|
|
1909
|
+
if (!act.ok) {
|
|
1910
|
+
result.error = act.error ?? "command failed";
|
|
1911
|
+
if (note !== void 0)
|
|
1912
|
+
result.note = note;
|
|
1913
|
+
return result;
|
|
1914
|
+
}
|
|
1915
|
+
const expectTestid = step.expect?.element?.testid;
|
|
1916
|
+
if (expectTestid !== void 0 && !dynamic.has(expectTestid)) {
|
|
1917
|
+
const expectQuery = await session.command(IrisCommand.QUERY, {
|
|
1918
|
+
by: QueryBy.TESTID,
|
|
1919
|
+
value: expectTestid
|
|
1920
|
+
});
|
|
1921
|
+
const expectRefs = readQuery(expectQuery);
|
|
1922
|
+
if (expectRefs.refs.length === 0) {
|
|
1923
|
+
return {
|
|
1924
|
+
step: index,
|
|
1925
|
+
tool: step.tool,
|
|
1926
|
+
anchor: expectTestid,
|
|
1927
|
+
ok: false,
|
|
1928
|
+
drift: testidDrift(expectTestid, expectRefs.hint)
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
if (note !== void 0)
|
|
1933
|
+
result.note = note;
|
|
1934
|
+
return result;
|
|
1935
|
+
}
|
|
1936
|
+
async function runSignalStep(session, step, index, name, waitForSignal, signalTimeoutMs) {
|
|
1937
|
+
const verdict = await waitForSignal(session, { kind: "signal", name }, signalTimeoutMs);
|
|
1938
|
+
if (verdict.pass)
|
|
1939
|
+
return { step: index, tool: step.tool, anchor: name, ok: true };
|
|
1940
|
+
return {
|
|
1941
|
+
step: index,
|
|
1942
|
+
tool: step.tool,
|
|
1943
|
+
anchor: name,
|
|
1944
|
+
ok: false,
|
|
1945
|
+
drift: {
|
|
1946
|
+
reasonKind: DriftReason.SIGNAL_NOT_OBSERVED,
|
|
1947
|
+
reason: `signal "${name}" not observed`,
|
|
1948
|
+
anchor: name,
|
|
1949
|
+
nearest: null
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
async function replayFlow(session, flow, waitForSignal, signalTimeoutMs, confirmDangerous = false) {
|
|
1954
|
+
const results = [];
|
|
1955
|
+
const dynamic = new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
1956
|
+
let index = 0;
|
|
1957
|
+
for (const step of flow.steps) {
|
|
1958
|
+
const label = anchorLabel(step.anchor);
|
|
1959
|
+
let result;
|
|
1960
|
+
if (step.anchor.kind === AnchorKind.SIGNAL) {
|
|
1961
|
+
result = await runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs);
|
|
1962
|
+
} else {
|
|
1963
|
+
result = await runTestidStep(session, step, index, label, dynamic, confirmDangerous);
|
|
1964
|
+
}
|
|
1965
|
+
results.push(result);
|
|
1966
|
+
if (result.drift !== void 0 || !result.ok)
|
|
1967
|
+
break;
|
|
1968
|
+
index += 1;
|
|
1969
|
+
}
|
|
1970
|
+
return results;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// ../server/dist/flows/heal.js
|
|
1974
|
+
function confidenceFor(from, to) {
|
|
1975
|
+
if (from === to)
|
|
1976
|
+
return 1;
|
|
1977
|
+
const span = Math.max(from.length, to.length);
|
|
1978
|
+
if (span === 0)
|
|
1979
|
+
return 1;
|
|
1980
|
+
const raw = 1 - editDistance(from, to) / span;
|
|
1981
|
+
if (raw >= 1)
|
|
1982
|
+
return 1;
|
|
1983
|
+
if (raw <= 0)
|
|
1984
|
+
return Number.EPSILON;
|
|
1985
|
+
return raw;
|
|
1986
|
+
}
|
|
1987
|
+
function applyHealChanges(flow, changes) {
|
|
1988
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
1989
|
+
for (const change of changes)
|
|
1990
|
+
byStep.set(change.step, change);
|
|
1991
|
+
const applied = [];
|
|
1992
|
+
const steps = flow.steps.map((step, index) => {
|
|
1993
|
+
const change = byStep.get(index);
|
|
1994
|
+
if (change === void 0 || step.anchor.kind !== AnchorKind.TESTID || step.anchor.value !== change.from) {
|
|
1995
|
+
return step;
|
|
1996
|
+
}
|
|
1997
|
+
applied.push(change);
|
|
1998
|
+
return { ...step, anchor: { kind: AnchorKind.TESTID, value: change.to } };
|
|
1999
|
+
});
|
|
2000
|
+
return { flow: { ...flow, steps }, applied };
|
|
2001
|
+
}
|
|
2002
|
+
function proposeRebindWith(drift, step, minConfidence) {
|
|
2003
|
+
if (drift.reasonKind !== DriftReason.TESTID_NOT_FOUND)
|
|
2004
|
+
return void 0;
|
|
2005
|
+
if (drift.ambiguous === true)
|
|
2006
|
+
return void 0;
|
|
2007
|
+
const to = drift.nearest;
|
|
2008
|
+
if (to === null)
|
|
2009
|
+
return void 0;
|
|
2010
|
+
const confidence = confidenceFor(drift.anchor, to);
|
|
2011
|
+
if (confidence < minConfidence)
|
|
2012
|
+
return void 0;
|
|
2013
|
+
return { step, from: drift.anchor, to, confidence };
|
|
2014
|
+
}
|
|
2015
|
+
function collectProposals(steps, minConfidence = HEAL_CONFIDENCE_MIN) {
|
|
2016
|
+
const proposals = [];
|
|
2017
|
+
for (const step of steps) {
|
|
2018
|
+
if (step.drift === void 0)
|
|
2019
|
+
continue;
|
|
2020
|
+
const proposal = proposeRebindWith(step.drift, step.step, minConfidence);
|
|
2021
|
+
if (proposal !== void 0)
|
|
2022
|
+
proposals.push(proposal);
|
|
2023
|
+
}
|
|
2024
|
+
return proposals;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// ../server/dist/project/iris-dir.js
|
|
2028
|
+
import { join } from "path";
|
|
2029
|
+
function irisDirPaths(root) {
|
|
2030
|
+
return {
|
|
2031
|
+
root,
|
|
2032
|
+
contract: join(root, IrisDir.CONTRACT_FILE),
|
|
2033
|
+
flows: join(root, IrisDir.FLOWS_SUBDIR),
|
|
2034
|
+
baselines: join(root, IrisDir.BASELINES_SUBDIR),
|
|
2035
|
+
project: join(root, IrisDir.PROJECT_FILE),
|
|
2036
|
+
visual: join(root, IrisDir.VISUAL_SUBDIR)
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
function visualPath(root, name) {
|
|
2040
|
+
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.png`);
|
|
2041
|
+
}
|
|
2042
|
+
function visualDiffPath(root, name) {
|
|
2043
|
+
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.diff.png`);
|
|
2044
|
+
}
|
|
2045
|
+
function flowPath(root, name) {
|
|
2046
|
+
return join(root, IrisDir.FLOWS_SUBDIR, `${name}.json`);
|
|
2047
|
+
}
|
|
2048
|
+
function isValidFlowName(name) {
|
|
2049
|
+
return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
|
|
2050
|
+
}
|
|
2051
|
+
async function ensureIrisDir(fs2, root) {
|
|
2052
|
+
const p = irisDirPaths(root);
|
|
2053
|
+
await fs2.mkdir(p.root);
|
|
2054
|
+
await fs2.mkdir(p.flows);
|
|
2055
|
+
await fs2.mkdir(p.baselines);
|
|
2056
|
+
}
|
|
2057
|
+
var JSON_INDENT = 2;
|
|
2058
|
+
function stableSerialize(capabilities, generatedAt) {
|
|
2059
|
+
const envelope = {
|
|
2060
|
+
version: CONTRACT_FILE_VERSION,
|
|
2061
|
+
generatedAt,
|
|
2062
|
+
capabilities: {
|
|
2063
|
+
testids: [...capabilities.testids].sort(),
|
|
2064
|
+
signals: [...capabilities.signals].sort(),
|
|
2065
|
+
stores: [...capabilities.stores].sort(),
|
|
2066
|
+
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)
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
return `${JSON.stringify(envelope, null, JSON_INDENT)}
|
|
2070
|
+
`;
|
|
2071
|
+
}
|
|
2072
|
+
async function writeContract(fs2, root, capabilities, now) {
|
|
2073
|
+
await ensureIrisDir(fs2, root);
|
|
2074
|
+
await fs2.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
|
|
2075
|
+
}
|
|
2076
|
+
async function readContract(fs2, root) {
|
|
2077
|
+
const path = irisDirPaths(root).contract;
|
|
2078
|
+
if (!await fs2.exists(path))
|
|
2079
|
+
return { ok: false, reason: ContractReadError.MISSING };
|
|
2080
|
+
let text;
|
|
2081
|
+
try {
|
|
2082
|
+
text = await fs2.readFile(path);
|
|
2083
|
+
} catch (error) {
|
|
2084
|
+
return {
|
|
2085
|
+
ok: false,
|
|
2086
|
+
reason: fs2.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
let parsed;
|
|
2090
|
+
try {
|
|
2091
|
+
parsed = JSON.parse(text);
|
|
2092
|
+
} catch {
|
|
2093
|
+
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
2094
|
+
}
|
|
2095
|
+
const result = ContractFileSchema.safeParse(parsed);
|
|
2096
|
+
if (!result.success)
|
|
2097
|
+
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
2098
|
+
return { ok: true, capabilities: result.data.capabilities, generatedAt: result.data.generatedAt };
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
// ../server/dist/flows/flows.js
|
|
2102
|
+
function asString3(value) {
|
|
2103
|
+
return typeof value === "string" ? value : void 0;
|
|
2104
|
+
}
|
|
2105
|
+
function asRecord3(value) {
|
|
2106
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
2107
|
+
}
|
|
2108
|
+
function degradedAnchor() {
|
|
2109
|
+
return { kind: AnchorKind.ROLE, role: DEGRADED_ANCHOR_ROLE };
|
|
2110
|
+
}
|
|
2111
|
+
function subStepToFlowStep(raw) {
|
|
2112
|
+
const sub = asRecord3(raw);
|
|
2113
|
+
const by = asString3(sub["by"]);
|
|
2114
|
+
const value = asString3(sub["value"]);
|
|
2115
|
+
const action = asString3(sub["action"]);
|
|
2116
|
+
const args = asRecord3(sub["args"]);
|
|
2117
|
+
if (by === QueryBy.TESTID && value !== void 0) {
|
|
2118
|
+
return buildStep(IrisTool.ACT, { kind: AnchorKind.TESTID, value }, action, args, false);
|
|
2119
|
+
}
|
|
2120
|
+
return buildStep(IrisTool.ACT, degradedAnchor(), action, args, true);
|
|
2121
|
+
}
|
|
2122
|
+
function buildStep(tool, anchor, action, args, degraded) {
|
|
2123
|
+
const step = { tool, anchor, args };
|
|
2124
|
+
if (action !== void 0)
|
|
2125
|
+
step.action = action;
|
|
2126
|
+
if (degraded)
|
|
2127
|
+
step.degraded = true;
|
|
2128
|
+
return step;
|
|
2129
|
+
}
|
|
2130
|
+
function recordedStepToFlowStep(step) {
|
|
2131
|
+
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
2132
|
+
const rawSubs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
2133
|
+
const subs = rawSubs.map(subStepToFlowStep);
|
|
2134
|
+
const degraded = subs.some((s) => s.degraded === true);
|
|
2135
|
+
const anchor = subs[0]?.anchor ?? degradedAnchor();
|
|
2136
|
+
const out2 = { tool: IrisTool.ACT_SEQUENCE, anchor, steps: subs };
|
|
2137
|
+
if (degraded)
|
|
2138
|
+
out2.degraded = true;
|
|
2139
|
+
if (step.expect !== void 0)
|
|
2140
|
+
out2.expect = step.expect;
|
|
2141
|
+
return out2;
|
|
2142
|
+
}
|
|
2143
|
+
const by = asString3(step.args["by"]);
|
|
2144
|
+
const value = asString3(step.args["value"]);
|
|
2145
|
+
const action = asString3(step.args["action"]);
|
|
2146
|
+
const args = asRecord3(step.args["args"]);
|
|
2147
|
+
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);
|
|
2148
|
+
if (step.expect !== void 0)
|
|
2149
|
+
out.expect = step.expect;
|
|
2150
|
+
return out;
|
|
2151
|
+
}
|
|
2152
|
+
function withAnnotations(flow, ann) {
|
|
2153
|
+
if (ann === void 0)
|
|
2154
|
+
return flow;
|
|
2155
|
+
const steps = flow.steps.map((step, i) => {
|
|
2156
|
+
const expect = ann.stepExpect.get(i);
|
|
2157
|
+
return expect === void 0 ? step : { ...step, expect };
|
|
2158
|
+
});
|
|
2159
|
+
const out = { ...flow, steps };
|
|
2160
|
+
if (ann.dynamic.length > 0) {
|
|
2161
|
+
out.dynamic = ann.dynamic.map((value) => ({ kind: AnchorKind.TESTID, value }));
|
|
2162
|
+
}
|
|
2163
|
+
if (ann.success !== void 0)
|
|
2164
|
+
out.success = ann.success;
|
|
2165
|
+
return out;
|
|
2166
|
+
}
|
|
2167
|
+
var JSON_INDENT2 = 2;
|
|
2168
|
+
var FLOW_SUFFIX = ".json";
|
|
2169
|
+
var FlowStore = class {
|
|
2170
|
+
#fs;
|
|
2171
|
+
#root;
|
|
2172
|
+
#clock;
|
|
2173
|
+
constructor(fs2, root, clock) {
|
|
2174
|
+
this.#fs = fs2;
|
|
2175
|
+
this.#root = root;
|
|
2176
|
+
this.#clock = clock;
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* The single byte-stable flow serializer: 2-space indent + one trailing newline. save(),
|
|
2180
|
+
* saveFlow() and heal() all route through it so an unchanged flow that round-trips through any
|
|
2181
|
+
* of them produces byte-identical on-disk content (locked by the byte-stability tests).
|
|
2182
|
+
*/
|
|
2183
|
+
#serialize(flow) {
|
|
2184
|
+
return `${JSON.stringify(flow, null, JSON_INDENT2)}
|
|
2185
|
+
`;
|
|
2186
|
+
}
|
|
2187
|
+
/**
|
|
2188
|
+
* Convert a CompiledProgram (testid-normalized) into an anchored, on-disk flow + write it.
|
|
2189
|
+
* Optionally fold structured annotations (per-step expect, dynamic[], success) onto
|
|
2190
|
+
* the flow before writing. Omitting `annotations` reproduces the same bytes.
|
|
2191
|
+
*/
|
|
2192
|
+
async save(program, annotations) {
|
|
2193
|
+
if (!isValidFlowName(program.name)) {
|
|
2194
|
+
return { ok: false, code: FlowErrorCode.INVALID_NAME };
|
|
2195
|
+
}
|
|
2196
|
+
const steps = program.steps.map(recordedStepToFlowStep);
|
|
2197
|
+
const base = {
|
|
2198
|
+
version: FLOW_FILE_VERSION,
|
|
2199
|
+
name: program.name,
|
|
2200
|
+
createdAt: this.#clock.now(),
|
|
2201
|
+
steps
|
|
2202
|
+
};
|
|
2203
|
+
const flow = withAnnotations(base, annotations);
|
|
2204
|
+
await this.#fs.mkdir(irisDirPaths(this.#root).flows);
|
|
2205
|
+
await this.#fs.writeFile(flowPath(this.#root, program.name), this.#serialize(flow));
|
|
2206
|
+
const degraded = flow.steps.filter((s) => s.degraded === true).length;
|
|
2207
|
+
return {
|
|
2208
|
+
ok: true,
|
|
2209
|
+
value: {
|
|
1649
2210
|
name: program.name,
|
|
1650
2211
|
stepCount: flow.steps.length,
|
|
1651
2212
|
degraded,
|
|
@@ -1696,19 +2257,7 @@ var FlowStore = class {
|
|
|
1696
2257
|
if (!loaded.ok)
|
|
1697
2258
|
return { ok: false, code: loaded.code };
|
|
1698
2259
|
const flow = loaded.value;
|
|
1699
|
-
const
|
|
1700
|
-
for (const change of changes)
|
|
1701
|
-
byStep.set(change.step, change);
|
|
1702
|
-
const applied = [];
|
|
1703
|
-
const steps = flow.steps.map((step, index) => {
|
|
1704
|
-
const change = byStep.get(index);
|
|
1705
|
-
if (change === void 0 || step.anchor.kind !== AnchorKind.TESTID || step.anchor.value !== change.from) {
|
|
1706
|
-
return step;
|
|
1707
|
-
}
|
|
1708
|
-
applied.push(change);
|
|
1709
|
-
return { ...step, anchor: { kind: AnchorKind.TESTID, value: change.to } };
|
|
1710
|
-
});
|
|
1711
|
-
const next = { ...flow, steps };
|
|
2260
|
+
const { flow: next, applied } = applyHealChanges(flow, changes);
|
|
1712
2261
|
await this.#fs.writeFile(flowPath(this.#root, name), this.#serialize(next));
|
|
1713
2262
|
return { ok: true, value: { name, changed: applied } };
|
|
1714
2263
|
}
|
|
@@ -1936,10 +2485,10 @@ function createNodeFileSystem() {
|
|
|
1936
2485
|
|
|
1937
2486
|
// ../server/dist/mcp.js
|
|
1938
2487
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1939
|
-
import { z as
|
|
2488
|
+
import { z as z17 } from "zod";
|
|
1940
2489
|
|
|
1941
2490
|
// ../server/dist/tools/tools.js
|
|
1942
|
-
import { z as
|
|
2491
|
+
import { z as z16 } from "zod";
|
|
1943
2492
|
|
|
1944
2493
|
// ../server/dist/input/real-input.js
|
|
1945
2494
|
var DriveError = class extends Error {
|
|
@@ -2000,8 +2549,10 @@ async function performGesture(page, action, box, args, sleep) {
|
|
|
2000
2549
|
}
|
|
2001
2550
|
return { performed: false, center };
|
|
2002
2551
|
}
|
|
2552
|
+
var HIDE_IRIS_CHROME_CSS = "[data-iris-overlay]{display:none !important}";
|
|
2553
|
+
var SCREENSHOT_DETERMINISM = { style: HIDE_IRIS_CHROME_CSS, animations: "disabled" };
|
|
2003
2554
|
async function capturePage(page, opts) {
|
|
2004
|
-
const buf = await page.screenshot(opts.clip !== void 0 ? { clip: opts.clip } : opts.fullPage === true ? { fullPage: true } : {});
|
|
2555
|
+
const buf = await page.screenshot(opts.clip !== void 0 ? { ...SCREENSHOT_DETERMINISM, clip: opts.clip } : opts.fullPage === true ? { ...SCREENSHOT_DETERMINISM, fullPage: true } : { ...SCREENSHOT_DETERMINISM });
|
|
2005
2556
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
2006
2557
|
}
|
|
2007
2558
|
var nodeSleep = (ms) => new Promise((resolve) => {
|
|
@@ -2236,6 +2787,7 @@ var PredicateSchema = z3.lazy(() => z3.discriminatedUnion("kind", [
|
|
|
2236
2787
|
name: z3.string().optional(),
|
|
2237
2788
|
dataMatches: z3.record(z3.unknown()).optional()
|
|
2238
2789
|
}),
|
|
2790
|
+
z3.object({ kind: z3.literal("settled"), quietMs: z3.number().positive().optional() }),
|
|
2239
2791
|
z3.object({ kind: z3.literal("allOf"), predicates: z3.array(PredicateSchema) }),
|
|
2240
2792
|
z3.object({ kind: z3.literal("anyOf"), predicates: z3.array(PredicateSchema) }),
|
|
2241
2793
|
z3.object({ kind: z3.literal("not"), predicate: PredicateSchema })
|
|
@@ -2432,6 +2984,39 @@ function evalSignal(events, p) {
|
|
|
2432
2984
|
evidence: sameName.length > 0 ? { nearMiss: sameName } : void 0
|
|
2433
2985
|
};
|
|
2434
2986
|
}
|
|
2987
|
+
var SETTLE_ACTIVITY = /* @__PURE__ */ new Set([
|
|
2988
|
+
EventType.NET_REQUEST,
|
|
2989
|
+
EventType.DOM_ADDED,
|
|
2990
|
+
EventType.DOM_REMOVED,
|
|
2991
|
+
EventType.DOM_ATTR
|
|
2992
|
+
]);
|
|
2993
|
+
var DEFAULT_QUIET_MS = 500;
|
|
2994
|
+
function evalSettled(events, p, now) {
|
|
2995
|
+
const quietMs = p.quietMs ?? DEFAULT_QUIET_MS;
|
|
2996
|
+
let lastT = -1;
|
|
2997
|
+
let lastType;
|
|
2998
|
+
for (const e of events) {
|
|
2999
|
+
if (SETTLE_ACTIVITY.has(e.type) && e.t > lastT) {
|
|
3000
|
+
lastT = e.t;
|
|
3001
|
+
lastType = e.type;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
if (lastT < 0) {
|
|
3005
|
+
return {
|
|
3006
|
+
pass: true,
|
|
3007
|
+
evidence: { settled: true, quietForMs: null, note: "no activity to settle" }
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
const quietForMs = now - lastT;
|
|
3011
|
+
if (quietForMs >= quietMs) {
|
|
3012
|
+
return { pass: true, evidence: { settled: true, quietForMs, lastActivity: lastType } };
|
|
3013
|
+
}
|
|
3014
|
+
return {
|
|
3015
|
+
pass: false,
|
|
3016
|
+
failureReason: `not settled: last activity (${String(lastType)}) ${String(quietForMs)}ms ago, need ${String(quietMs)}ms quiet`,
|
|
3017
|
+
evidence: { quietForMs, lastActivity: lastType }
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
2435
3020
|
async function evaluatePredicate(session, predicate, since = 0) {
|
|
2436
3021
|
const events = session.eventsSince(since);
|
|
2437
3022
|
switch (predicate.kind) {
|
|
@@ -2449,6 +3034,8 @@ async function evaluatePredicate(session, predicate, since = 0) {
|
|
|
2449
3034
|
return evalAnimation(events, predicate);
|
|
2450
3035
|
case "signal":
|
|
2451
3036
|
return evalSignal(events, predicate);
|
|
3037
|
+
case "settled":
|
|
3038
|
+
return evalSettled(events, predicate, session.elapsed());
|
|
2452
3039
|
case "allOf": {
|
|
2453
3040
|
const results = await Promise.all(predicate.predicates.map((p) => evaluatePredicate(session, p, since)));
|
|
2454
3041
|
const failed = results.find((r) => !r.pass);
|
|
@@ -2474,6 +3061,10 @@ async function evaluatePredicate(session, predicate, since = 0) {
|
|
|
2474
3061
|
function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
2475
3062
|
return new Promise((resolve) => {
|
|
2476
3063
|
let done = false;
|
|
3064
|
+
const failed = (error) => ({
|
|
3065
|
+
pass: false,
|
|
3066
|
+
failureReason: error instanceof Error ? error.message : String(error)
|
|
3067
|
+
});
|
|
2477
3068
|
const finish = (result) => {
|
|
2478
3069
|
if (done)
|
|
2479
3070
|
return;
|
|
@@ -2487,6 +3078,8 @@ function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
2487
3078
|
void evaluatePredicate(session, predicate, since).then((r) => {
|
|
2488
3079
|
if (r.pass)
|
|
2489
3080
|
finish(r);
|
|
3081
|
+
}).catch((error) => {
|
|
3082
|
+
finish(failed(error));
|
|
2490
3083
|
});
|
|
2491
3084
|
};
|
|
2492
3085
|
const unsub = session.onEvent(() => {
|
|
@@ -2500,141 +3093,30 @@ function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
2500
3093
|
evidence: r.evidence,
|
|
2501
3094
|
failureReason: r.failureReason ?? "timed out waiting for predicate"
|
|
2502
3095
|
});
|
|
3096
|
+
}).catch((error) => {
|
|
3097
|
+
finish(failed(error));
|
|
2503
3098
|
});
|
|
2504
3099
|
}, timeoutMs);
|
|
2505
3100
|
check();
|
|
2506
3101
|
});
|
|
2507
3102
|
}
|
|
2508
3103
|
|
|
2509
|
-
// ../server/dist/flows/replay.js
|
|
2510
|
-
function asString2(value) {
|
|
2511
|
-
return typeof value === "string" ? value : void 0;
|
|
2512
|
-
}
|
|
2513
|
-
function asRecord2(value) {
|
|
2514
|
-
return typeof value === "object" && value !== null ? value : {};
|
|
2515
|
-
}
|
|
2516
|
-
function compileActStep(args, res) {
|
|
2517
|
-
const testid = asString2(asRecord2(res)["testid"]);
|
|
2518
|
-
const action = asString2(args["action"]) ?? "";
|
|
2519
|
-
const actArgs = asRecord2(args["args"]);
|
|
2520
|
-
if (testid !== void 0) {
|
|
2521
|
-
return {
|
|
2522
|
-
tool: IrisTool.ACT,
|
|
2523
|
-
stable: true,
|
|
2524
|
-
args: { by: QueryBy.TESTID, value: testid, action, args: actArgs }
|
|
2525
|
-
};
|
|
2526
|
-
}
|
|
2527
|
-
return {
|
|
2528
|
-
tool: IrisTool.ACT,
|
|
2529
|
-
stable: false,
|
|
2530
|
-
args: { ref: asString2(args["ref"]) ?? "", action, args: actArgs }
|
|
2531
|
-
};
|
|
2532
|
-
}
|
|
2533
|
-
function compileSequenceStep(args, res) {
|
|
2534
|
-
const inputSteps = Array.isArray(args["steps"]) ? args["steps"] : [];
|
|
2535
|
-
const resolved = Array.isArray(asRecord2(res)["steps"]) ? asRecord2(res)["steps"] : [];
|
|
2536
|
-
let stable = inputSteps.length > 0;
|
|
2537
|
-
const subSteps = inputSteps.map((raw, i) => {
|
|
2538
|
-
const step = asRecord2(raw);
|
|
2539
|
-
const action = asString2(step["action"]) ?? "";
|
|
2540
|
-
const stepArgs = asRecord2(step["args"]);
|
|
2541
|
-
const testid = asString2(asRecord2(resolved[i])["testid"]);
|
|
2542
|
-
if (testid !== void 0) {
|
|
2543
|
-
return { by: QueryBy.TESTID, value: testid, action, args: stepArgs };
|
|
2544
|
-
}
|
|
2545
|
-
stable = false;
|
|
2546
|
-
return { ref: asString2(step["ref"]) ?? "", action, args: stepArgs };
|
|
2547
|
-
});
|
|
2548
|
-
return { tool: IrisTool.ACT_SEQUENCE, stable, args: { steps: subSteps } };
|
|
2549
|
-
}
|
|
2550
|
-
async function resolveRef(session, step) {
|
|
2551
|
-
const by = asString2(step.by);
|
|
2552
|
-
const value = asString2(step.value);
|
|
2553
|
-
if (by === QueryBy.TESTID && value !== void 0) {
|
|
2554
|
-
const result = await session.command(IrisCommand.QUERY, { by, value });
|
|
2555
|
-
if (!result.ok)
|
|
2556
|
-
throw new Error(result.error ?? "query failed");
|
|
2557
|
-
const elements = Array.isArray(asRecord2(result.result)["elements"]) ? asRecord2(result.result)["elements"] : [];
|
|
2558
|
-
const ref2 = asString2(asRecord2(elements[0])["ref"]);
|
|
2559
|
-
if (ref2 === void 0)
|
|
2560
|
-
throw new Error(`testid '${value}' did not resolve in current page`);
|
|
2561
|
-
return elements.length > 1 ? { ref: ref2, note: `ambiguous testid '${value}', used first match` } : { ref: ref2 };
|
|
2562
|
-
}
|
|
2563
|
-
const ref = asString2(step.ref);
|
|
2564
|
-
if (ref === void 0 || ref.length === 0)
|
|
2565
|
-
throw new Error("step has no testid or ref to resolve");
|
|
2566
|
-
return { ref, note: "replayed by stale ref (not portable across sessions)" };
|
|
2567
|
-
}
|
|
2568
|
-
async function replayProgram(session, program) {
|
|
2569
|
-
const results = [];
|
|
2570
|
-
for (const step of program.steps) {
|
|
2571
|
-
try {
|
|
2572
|
-
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
2573
|
-
const subs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
2574
|
-
const notes = [];
|
|
2575
|
-
const liveSteps = [];
|
|
2576
|
-
for (const raw of subs) {
|
|
2577
|
-
const sub = asRecord2(raw);
|
|
2578
|
-
const { ref, note } = await resolveRef(session, sub);
|
|
2579
|
-
if (note !== void 0)
|
|
2580
|
-
notes.push(note);
|
|
2581
|
-
liveSteps.push({
|
|
2582
|
-
ref,
|
|
2583
|
-
action: asString2(sub["action"]) ?? "",
|
|
2584
|
-
args: asRecord2(sub["args"])
|
|
2585
|
-
});
|
|
2586
|
-
}
|
|
2587
|
-
const r = await session.command(IrisCommand.ACT_SEQUENCE, { steps: liveSteps });
|
|
2588
|
-
results.push(buildResult(step.tool, r.ok, r.error, notes));
|
|
2589
|
-
if (!r.ok)
|
|
2590
|
-
break;
|
|
2591
|
-
} else {
|
|
2592
|
-
const { ref, note } = await resolveRef(session, step.args);
|
|
2593
|
-
const r = await session.command(IrisCommand.ACT, {
|
|
2594
|
-
ref,
|
|
2595
|
-
action: asString2(step.args["action"]) ?? "",
|
|
2596
|
-
args: asRecord2(step.args["args"])
|
|
2597
|
-
});
|
|
2598
|
-
results.push(buildResult(step.tool, r.ok, r.error, note !== void 0 ? [note] : []));
|
|
2599
|
-
if (!r.ok)
|
|
2600
|
-
break;
|
|
2601
|
-
}
|
|
2602
|
-
} catch (e) {
|
|
2603
|
-
results.push({
|
|
2604
|
-
tool: step.tool,
|
|
2605
|
-
ok: false,
|
|
2606
|
-
error: e instanceof Error ? e.message : String(e)
|
|
2607
|
-
});
|
|
2608
|
-
break;
|
|
2609
|
-
}
|
|
2610
|
-
}
|
|
2611
|
-
return results;
|
|
2612
|
-
}
|
|
2613
|
-
function buildResult(tool, ok, error, notes) {
|
|
2614
|
-
const base = { tool, ok };
|
|
2615
|
-
if (!ok)
|
|
2616
|
-
base.error = error ?? "command failed";
|
|
2617
|
-
if (notes.length > 0)
|
|
2618
|
-
base.note = notes.join("; ");
|
|
2619
|
-
return base;
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
3104
|
// ../server/dist/events/event-filters.js
|
|
2623
|
-
function
|
|
3105
|
+
function asString4(value) {
|
|
2624
3106
|
return typeof value === "string" ? value : void 0;
|
|
2625
3107
|
}
|
|
2626
|
-
function
|
|
3108
|
+
function asNumber2(value) {
|
|
2627
3109
|
return typeof value === "number" ? value : void 0;
|
|
2628
3110
|
}
|
|
2629
3111
|
function matchNet(e, method, urlContains, status) {
|
|
2630
3112
|
const d = e.data;
|
|
2631
|
-
if (method !== void 0 &&
|
|
3113
|
+
if (method !== void 0 && asString4(d["method"])?.toUpperCase() !== method.toUpperCase()) {
|
|
2632
3114
|
return false;
|
|
2633
3115
|
}
|
|
2634
|
-
if (urlContains !== void 0 && !(
|
|
3116
|
+
if (urlContains !== void 0 && !(asString4(d["url"]) ?? "").includes(urlContains)) {
|
|
2635
3117
|
return false;
|
|
2636
3118
|
}
|
|
2637
|
-
if (status !== void 0 &&
|
|
3119
|
+
if (status !== void 0 && asNumber2(d["status"]) !== status)
|
|
2638
3120
|
return false;
|
|
2639
3121
|
return true;
|
|
2640
3122
|
}
|
|
@@ -2651,8 +3133,8 @@ function matchConsole(e, level) {
|
|
|
2651
3133
|
var HINT_SAMPLE_MAX = 5;
|
|
2652
3134
|
function netEmptyHint(allNet) {
|
|
2653
3135
|
const present = allNet.slice(-HINT_SAMPLE_MAX).reverse().map((e) => {
|
|
2654
|
-
const status =
|
|
2655
|
-
const base = { method:
|
|
3136
|
+
const status = asNumber2(e.data["status"]);
|
|
3137
|
+
const base = { method: asString4(e.data["method"]) ?? "", url: asString4(e.data["url"]) ?? "" };
|
|
2656
3138
|
return status === void 0 ? base : { ...base, status };
|
|
2657
3139
|
});
|
|
2658
3140
|
return { totalInWindow: allNet.length, present };
|
|
@@ -2683,6 +3165,8 @@ function refuseIfThrottled(session, refuse) {
|
|
|
2683
3165
|
}
|
|
2684
3166
|
|
|
2685
3167
|
// ../server/dist/session/output-budget.js
|
|
3168
|
+
var LARGE_TIMELINE_EVENTS = 80;
|
|
3169
|
+
var LARGE_TIMELINE_BYTES = 8e3;
|
|
2686
3170
|
function applyEventBudget(events, maxEvents) {
|
|
2687
3171
|
if (maxEvents === void 0 || maxEvents < 0 || events.length <= maxEvents) {
|
|
2688
3172
|
return { events, droppedOldest: 0 };
|
|
@@ -2693,8 +3177,92 @@ function applyEventBudget(events, maxEvents) {
|
|
|
2693
3177
|
};
|
|
2694
3178
|
}
|
|
2695
3179
|
function costHint(payload, events, droppedOldest = 0) {
|
|
2696
|
-
const
|
|
2697
|
-
|
|
3180
|
+
const json = JSON.stringify(payload) ?? "";
|
|
3181
|
+
const bytes = json.length;
|
|
3182
|
+
const base = droppedOldest > 0 ? { events, bytes, droppedOldest } : { events, bytes };
|
|
3183
|
+
if (events >= LARGE_TIMELINE_EVENTS || bytes >= LARGE_TIMELINE_BYTES) {
|
|
3184
|
+
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`;
|
|
3185
|
+
}
|
|
3186
|
+
return base;
|
|
3187
|
+
}
|
|
3188
|
+
var CHARS_PER_TOKEN = 4;
|
|
3189
|
+
function estimateTokens(text) {
|
|
3190
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
3191
|
+
}
|
|
3192
|
+
function sizeCost(payload) {
|
|
3193
|
+
const json = JSON.stringify(payload) ?? "";
|
|
3194
|
+
return { bytes: json.length, tokens: estimateTokens(json) };
|
|
3195
|
+
}
|
|
3196
|
+
function withSizeCost(result) {
|
|
3197
|
+
if (typeof result !== "object" || result === null)
|
|
3198
|
+
return result;
|
|
3199
|
+
return { ...result, cost: sizeCost(result) };
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
// ../server/dist/tools/snapshot-delta.js
|
|
3203
|
+
var SnapshotDeltaMode = {
|
|
3204
|
+
FULL: "full",
|
|
3205
|
+
DELTA: "delta",
|
|
3206
|
+
UNCHANGED: "unchanged"
|
|
3207
|
+
};
|
|
3208
|
+
function snapshotDelta(prevTree, nextTree) {
|
|
3209
|
+
if (prevTree === void 0)
|
|
3210
|
+
return { mode: SnapshotDeltaMode.FULL };
|
|
3211
|
+
const { added, removed } = diffLines(normalizeLines(prevTree), normalizeLines(nextTree));
|
|
3212
|
+
if (added.length === 0 && removed.length === 0)
|
|
3213
|
+
return { mode: SnapshotDeltaMode.UNCHANGED };
|
|
3214
|
+
return {
|
|
3215
|
+
mode: SnapshotDeltaMode.DELTA,
|
|
3216
|
+
delta: { added, removed, addedCount: added.length, removedCount: removed.length }
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3219
|
+
var DEFAULT_MAX_ENTRIES = 50;
|
|
3220
|
+
var SnapshotCache = class {
|
|
3221
|
+
#map = /* @__PURE__ */ new Map();
|
|
3222
|
+
#max;
|
|
3223
|
+
constructor(max = DEFAULT_MAX_ENTRIES) {
|
|
3224
|
+
this.#max = max;
|
|
3225
|
+
}
|
|
3226
|
+
/** Last tree for this key IF the route still matches; undefined when absent or route changed. */
|
|
3227
|
+
recall(key, route) {
|
|
3228
|
+
const entry = this.#map.get(key);
|
|
3229
|
+
return entry !== void 0 && entry.route === route ? entry.tree : void 0;
|
|
3230
|
+
}
|
|
3231
|
+
remember(key, route, tree) {
|
|
3232
|
+
if (this.#map.size >= this.#max && !this.#map.has(key)) {
|
|
3233
|
+
const oldest = this.#map.keys().next().value;
|
|
3234
|
+
if (oldest !== void 0)
|
|
3235
|
+
this.#map.delete(oldest);
|
|
3236
|
+
}
|
|
3237
|
+
this.#map.set(key, { route, tree });
|
|
3238
|
+
}
|
|
3239
|
+
};
|
|
3240
|
+
function snapshotCacheKey(sessionId, scope, mode) {
|
|
3241
|
+
return `${sessionId}\0${scope}\0${mode}`;
|
|
3242
|
+
}
|
|
3243
|
+
function applySnapshotDelta(raw, opts, cache) {
|
|
3244
|
+
if (typeof raw !== "object" || raw === null)
|
|
3245
|
+
return raw;
|
|
3246
|
+
const r = raw;
|
|
3247
|
+
if (typeof r["tree"] !== "string")
|
|
3248
|
+
return raw;
|
|
3249
|
+
const tree = r["tree"];
|
|
3250
|
+
const status = typeof r["status"] === "object" && r["status"] !== null ? r["status"] : {};
|
|
3251
|
+
const route = typeof status["route"] === "string" ? status["route"] : "";
|
|
3252
|
+
const key = snapshotCacheKey(opts.sessionId, opts.scope, opts.mode);
|
|
3253
|
+
if (!opts.diff) {
|
|
3254
|
+
cache.remember(key, route, tree);
|
|
3255
|
+
return raw;
|
|
3256
|
+
}
|
|
3257
|
+
const prev = cache.recall(key, route);
|
|
3258
|
+
cache.remember(key, route, tree);
|
|
3259
|
+
const decision = snapshotDelta(prev, tree);
|
|
3260
|
+
if (decision.mode === SnapshotDeltaMode.FULL)
|
|
3261
|
+
return raw;
|
|
3262
|
+
if (decision.mode === SnapshotDeltaMode.UNCHANGED) {
|
|
3263
|
+
return { mode: SnapshotDeltaMode.UNCHANGED, status: r["status"] };
|
|
3264
|
+
}
|
|
3265
|
+
return { mode: SnapshotDeltaMode.DELTA, delta: decision.delta, status: r["status"] };
|
|
2698
3266
|
}
|
|
2699
3267
|
|
|
2700
3268
|
// ../server/dist/session/state-select.js
|
|
@@ -2745,25 +3313,55 @@ function capDepth(value, maxDepth) {
|
|
|
2745
3313
|
return value;
|
|
2746
3314
|
}
|
|
2747
3315
|
|
|
2748
|
-
// ../server/dist/tools/
|
|
2749
|
-
function
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
3316
|
+
// ../server/dist/tools/query-paginate.js
|
|
3317
|
+
function paginateQueryResult(result, limit, countOnly) {
|
|
3318
|
+
if (typeof result !== "object" || result === null)
|
|
3319
|
+
return result;
|
|
3320
|
+
const record = result;
|
|
3321
|
+
const elements = record["elements"];
|
|
3322
|
+
if (!Array.isArray(elements))
|
|
3323
|
+
return result;
|
|
3324
|
+
const total = elements.length;
|
|
3325
|
+
if (countOnly) {
|
|
3326
|
+
const { elements: _dropped, ...rest } = record;
|
|
3327
|
+
return { ...rest, count: total };
|
|
3328
|
+
}
|
|
3329
|
+
if (limit !== void 0 && limit >= 0 && total > limit) {
|
|
3330
|
+
return { ...record, elements: elements.slice(0, limit), total, truncated: true };
|
|
3331
|
+
}
|
|
3332
|
+
return result;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
// ../server/dist/tools/assert-grade.js
|
|
3336
|
+
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.";
|
|
3337
|
+
function walk(predicate) {
|
|
3338
|
+
switch (predicate.kind) {
|
|
3339
|
+
case "signal":
|
|
3340
|
+
case "net":
|
|
3341
|
+
return { consequence: true, presence: false };
|
|
3342
|
+
case "element":
|
|
3343
|
+
case "text":
|
|
3344
|
+
return { consequence: false, presence: true };
|
|
3345
|
+
case "route":
|
|
3346
|
+
case "console":
|
|
3347
|
+
case "animation":
|
|
3348
|
+
case "settled":
|
|
3349
|
+
return { consequence: false, presence: false };
|
|
3350
|
+
case "allOf":
|
|
3351
|
+
case "anyOf": {
|
|
3352
|
+
const subs = predicate.predicates.map(walk);
|
|
3353
|
+
return {
|
|
3354
|
+
consequence: subs.some((s) => s.consequence),
|
|
3355
|
+
presence: subs.some((s) => s.presence)
|
|
3356
|
+
};
|
|
2755
3357
|
}
|
|
3358
|
+
case "not":
|
|
3359
|
+
return walk(predicate.predicate);
|
|
2756
3360
|
}
|
|
2757
|
-
return items;
|
|
2758
|
-
}
|
|
2759
|
-
function asString4(value) {
|
|
2760
|
-
return typeof value === "string" ? value : void 0;
|
|
2761
|
-
}
|
|
2762
|
-
function asNumber2(value) {
|
|
2763
|
-
return typeof value === "number" ? value : void 0;
|
|
2764
3361
|
}
|
|
2765
|
-
function
|
|
2766
|
-
|
|
3362
|
+
function isPresenceOnlyAssertion(predicate) {
|
|
3363
|
+
const kinds = walk(predicate);
|
|
3364
|
+
return kinds.presence && !kinds.consequence;
|
|
2767
3365
|
}
|
|
2768
3366
|
|
|
2769
3367
|
// ../server/dist/tools/contract-tools.js
|
|
@@ -2797,7 +3395,7 @@ var CONTRACT_TOOLS = [
|
|
|
2797
3395
|
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");
|
|
2798
3396
|
return { ...r.capabilities, source: "disk", generatedAt: r.generatedAt };
|
|
2799
3397
|
}
|
|
2800
|
-
const caps = await commandOrThrow(deps,
|
|
3398
|
+
const caps = await commandOrThrow(deps, asString(args["sessionId"]), IrisCommand.CAPABILITIES, {});
|
|
2801
3399
|
return { ...caps, source: "live" };
|
|
2802
3400
|
}
|
|
2803
3401
|
},
|
|
@@ -2812,7 +3410,7 @@ var CONTRACT_TOOLS = [
|
|
|
2812
3410
|
signalCount: z4.number()
|
|
2813
3411
|
},
|
|
2814
3412
|
handler: async (deps, args) => {
|
|
2815
|
-
const res = await commandOrThrow(deps,
|
|
3413
|
+
const res = await commandOrThrow(deps, asString(args["sessionId"]), IrisCommand.CAPABILITIES, {});
|
|
2816
3414
|
const caps = CapabilitiesSchema.parse(res);
|
|
2817
3415
|
await writeContract(deps.fs, deps.irisRoot, caps, deps.now);
|
|
2818
3416
|
return {
|
|
@@ -2825,253 +3423,399 @@ var CONTRACT_TOOLS = [
|
|
|
2825
3423
|
}
|
|
2826
3424
|
];
|
|
2827
3425
|
|
|
2828
|
-
// ../server/dist/
|
|
3426
|
+
// ../server/dist/domain/domain-tools.js
|
|
2829
3427
|
import { z as z5 } from "zod";
|
|
2830
|
-
|
|
2831
|
-
|
|
3428
|
+
|
|
3429
|
+
// ../server/dist/flows/flow-classify.js
|
|
3430
|
+
var FlowAssertionGrade = {
|
|
3431
|
+
/** At least one step (or the success end-condition) asserts a signal/network consequence. */
|
|
3432
|
+
ASSERTED: "asserted",
|
|
3433
|
+
/** Only element-presence checks — a healed-but-wrong locator could still pass. */
|
|
3434
|
+
PRESENCE_ONLY: "presence-only",
|
|
3435
|
+
/** Performs actions but asserts nothing observable — passes even if the feature is broken. */
|
|
3436
|
+
ASSERTION_FREE: "assertion-free"
|
|
2832
3437
|
};
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
throw new Error(result.error ?? `command '${name}' failed`);
|
|
2838
|
-
return result.result;
|
|
3438
|
+
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.";
|
|
3439
|
+
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).";
|
|
3440
|
+
function expectIsConsequence(e) {
|
|
3441
|
+
return e !== void 0 && (e.signal !== void 0 || e.net !== void 0);
|
|
2839
3442
|
}
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
ok: z5.boolean(),
|
|
2850
|
-
url: z5.string().optional(),
|
|
2851
|
-
reason: z5.string().optional()
|
|
2852
|
-
},
|
|
2853
|
-
handler: async (deps, args) => {
|
|
2854
|
-
const url = asString4(args["url"]);
|
|
2855
|
-
if (url === void 0 || url.length === 0)
|
|
2856
|
-
return { ok: false, reason: "url required" };
|
|
2857
|
-
await commandOrThrow2(deps, asString4(args["sessionId"]), IrisCommand.NAVIGATE, { url });
|
|
2858
|
-
return { ok: true, url };
|
|
2859
|
-
}
|
|
2860
|
-
},
|
|
2861
|
-
{
|
|
2862
|
-
name: IrisTool.REFRESH,
|
|
2863
|
-
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.",
|
|
2864
|
-
inputSchema: {
|
|
2865
|
-
hard: z5.boolean().optional().describe("Set true to bypass the browser cache. Default: false (normal reload)."),
|
|
2866
|
-
...sessionIdShape2
|
|
2867
|
-
},
|
|
2868
|
-
outputSchema: {
|
|
2869
|
-
ok: z5.boolean()
|
|
2870
|
-
},
|
|
2871
|
-
handler: async (deps, args) => {
|
|
2872
|
-
await commandOrThrow2(deps, asString4(args["sessionId"]), IrisCommand.REFRESH, {
|
|
2873
|
-
hard: args["hard"] === true
|
|
2874
|
-
});
|
|
2875
|
-
return { ok: true };
|
|
2876
|
-
}
|
|
3443
|
+
function expectIsWeak(e) {
|
|
3444
|
+
return e !== void 0 && e.element !== void 0 && e.signal === void 0 && e.net === void 0;
|
|
3445
|
+
}
|
|
3446
|
+
function flattenSteps(steps) {
|
|
3447
|
+
const out = [];
|
|
3448
|
+
for (const s of steps) {
|
|
3449
|
+
out.push(s);
|
|
3450
|
+
if (s.steps !== void 0)
|
|
3451
|
+
out.push(...flattenSteps(s.steps));
|
|
2877
3452
|
}
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
3453
|
+
return out;
|
|
3454
|
+
}
|
|
3455
|
+
function classifyFlowAssertions(flow) {
|
|
3456
|
+
const all = flattenSteps(flow.steps);
|
|
3457
|
+
let consequenceSteps = 0;
|
|
3458
|
+
let weakSteps = 0;
|
|
3459
|
+
for (const s of all) {
|
|
3460
|
+
if (expectIsConsequence(s.expect))
|
|
3461
|
+
consequenceSteps++;
|
|
3462
|
+
else if (expectIsWeak(s.expect))
|
|
3463
|
+
weakSteps++;
|
|
3464
|
+
}
|
|
3465
|
+
const successIsConsequence = expectIsConsequence(flow.success);
|
|
3466
|
+
const successIsWeak = expectIsWeak(flow.success);
|
|
3467
|
+
const hasConsequenceAssertion = consequenceSteps > 0 || successIsConsequence;
|
|
3468
|
+
const hasAnyAssertion = hasConsequenceAssertion || weakSteps > 0 || successIsWeak;
|
|
3469
|
+
let grade;
|
|
3470
|
+
let warning;
|
|
3471
|
+
if (hasConsequenceAssertion) {
|
|
3472
|
+
grade = FlowAssertionGrade.ASSERTED;
|
|
3473
|
+
} else if (hasAnyAssertion) {
|
|
3474
|
+
grade = FlowAssertionGrade.PRESENCE_ONLY;
|
|
3475
|
+
warning = PRESENCE_ONLY_WARNING;
|
|
3476
|
+
} else {
|
|
3477
|
+
grade = FlowAssertionGrade.ASSERTION_FREE;
|
|
3478
|
+
warning = ASSERTION_FREE_WARNING;
|
|
3479
|
+
}
|
|
3480
|
+
return {
|
|
3481
|
+
grade,
|
|
3482
|
+
hasConsequenceAssertion,
|
|
3483
|
+
totalSteps: all.length,
|
|
3484
|
+
consequenceSteps,
|
|
3485
|
+
weakSteps,
|
|
3486
|
+
successIsConsequence,
|
|
3487
|
+
...warning !== void 0 ? { warning } : {}
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
2882
3490
|
|
|
2883
|
-
// ../server/dist/flows/flow-
|
|
2884
|
-
function
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
3491
|
+
// ../server/dist/flows/flow-success.js
|
|
3492
|
+
function dynamicTestids(flow) {
|
|
3493
|
+
return new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
3494
|
+
}
|
|
3495
|
+
function successLabel(success) {
|
|
3496
|
+
if (success.signal !== void 0)
|
|
3497
|
+
return success.signal;
|
|
3498
|
+
if (success.net !== void 0)
|
|
3499
|
+
return success.net.urlContains ?? success.net.method ?? "net";
|
|
3500
|
+
return success.element?.testid ?? success.element?.name ?? success.element?.role ?? "success";
|
|
3501
|
+
}
|
|
3502
|
+
function successToPredicate(success, dynamic) {
|
|
3503
|
+
const parts = [];
|
|
3504
|
+
if (success.signal !== void 0) {
|
|
3505
|
+
parts.push(success.signalData !== void 0 ? { kind: "signal", name: success.signal, dataMatches: success.signalData } : { kind: "signal", name: success.signal });
|
|
3506
|
+
}
|
|
3507
|
+
if (success.net !== void 0) {
|
|
3508
|
+
const net2 = { kind: "net" };
|
|
3509
|
+
if (success.net.method !== void 0)
|
|
3510
|
+
net2.method = success.net.method;
|
|
3511
|
+
if (success.net.urlContains !== void 0)
|
|
3512
|
+
net2.urlContains = success.net.urlContains;
|
|
3513
|
+
if (success.net.status !== void 0)
|
|
3514
|
+
net2.status = success.net.status;
|
|
3515
|
+
parts.push(net2);
|
|
3516
|
+
}
|
|
3517
|
+
const element = success.element;
|
|
3518
|
+
if (element !== void 0) {
|
|
3519
|
+
const testid = element.testid;
|
|
3520
|
+
if (testid === void 0 || !dynamic.has(testid)) {
|
|
3521
|
+
const query = {};
|
|
3522
|
+
if (testid !== void 0)
|
|
3523
|
+
query["testid"] = testid;
|
|
3524
|
+
if (element.role !== void 0)
|
|
3525
|
+
query["role"] = element.role;
|
|
3526
|
+
if (element.name !== void 0)
|
|
3527
|
+
query["name"] = element.name;
|
|
3528
|
+
if (Object.keys(query).length > 0)
|
|
3529
|
+
parts.push({ kind: "element", query });
|
|
2898
3530
|
}
|
|
2899
|
-
for (let j = 0; j < cols; j++)
|
|
2900
|
-
prev[j] = curr[j] ?? 0;
|
|
2901
3531
|
}
|
|
2902
|
-
|
|
3532
|
+
const [first] = parts;
|
|
3533
|
+
if (parts.length === 0)
|
|
3534
|
+
return void 0;
|
|
3535
|
+
if (parts.length === 1 && first !== void 0)
|
|
3536
|
+
return first;
|
|
3537
|
+
return { kind: "allOf", predicates: parts };
|
|
3538
|
+
}
|
|
3539
|
+
async function assertSuccess(session, success, dynamic, waitForSignal, timeoutMs, since = 0) {
|
|
3540
|
+
if (success === void 0)
|
|
3541
|
+
return { pass: true };
|
|
3542
|
+
const predicate = successToPredicate(success, dynamic);
|
|
3543
|
+
if (predicate === void 0)
|
|
3544
|
+
return { pass: true };
|
|
3545
|
+
return waitForSignal(session, predicate, timeoutMs, since);
|
|
2903
3546
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
3547
|
+
|
|
3548
|
+
// ../server/dist/domain/flow-risk.js
|
|
3549
|
+
var RiskLevel = {
|
|
3550
|
+
HIGH: "high",
|
|
3551
|
+
MEDIUM: "medium",
|
|
3552
|
+
LOW: "low",
|
|
3553
|
+
UNKNOWN: "unknown"
|
|
3554
|
+
};
|
|
3555
|
+
var RANK = {
|
|
3556
|
+
[RiskLevel.HIGH]: 3,
|
|
3557
|
+
[RiskLevel.MEDIUM]: 2,
|
|
3558
|
+
[RiskLevel.UNKNOWN]: 1,
|
|
3559
|
+
[RiskLevel.LOW]: 0
|
|
3560
|
+
};
|
|
3561
|
+
function latestRun(name, runs) {
|
|
3562
|
+
let best;
|
|
3563
|
+
for (const run of runs) {
|
|
3564
|
+
if (run.name === name && (best === void 0 || run.at > best.at))
|
|
3565
|
+
best = run;
|
|
2913
3566
|
}
|
|
2914
3567
|
return best;
|
|
2915
3568
|
}
|
|
2916
|
-
function
|
|
2917
|
-
if (
|
|
2918
|
-
return {
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3569
|
+
function runRisk(run) {
|
|
3570
|
+
if (run === void 0)
|
|
3571
|
+
return { level: RiskLevel.UNKNOWN, reason: "never run" };
|
|
3572
|
+
if (run.status === RunStatus.ERROR || run.status === RunStatus.FAIL) {
|
|
3573
|
+
return { level: RiskLevel.HIGH, reason: "last run failed" };
|
|
3574
|
+
}
|
|
3575
|
+
if (run.status === RunStatus.DRIFT)
|
|
3576
|
+
return { level: RiskLevel.HIGH, reason: "last run drifted" };
|
|
3577
|
+
const errors = (run.evidence?.consoleErrors ?? 0) + (run.evidence?.networkErrors ?? 0);
|
|
3578
|
+
if (errors > 0) {
|
|
2926
3579
|
return {
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
route: asString4(hint["route"]) ?? "",
|
|
2930
|
-
presentTestids: present,
|
|
2931
|
-
presentRegions: [],
|
|
2932
|
-
knownEmptyState: hint["knownEmptyState"] === true
|
|
2933
|
-
}
|
|
3580
|
+
level: RiskLevel.MEDIUM,
|
|
3581
|
+
reason: `last run passed but logged ${String(errors)} error(s)`
|
|
2934
3582
|
};
|
|
2935
3583
|
}
|
|
2936
|
-
return {
|
|
2937
|
-
}
|
|
2938
|
-
function testidDrift(value, hint) {
|
|
2939
|
-
return {
|
|
2940
|
-
reasonKind: DriftReason.TESTID_NOT_FOUND,
|
|
2941
|
-
reason: `testid "${value}" not found`,
|
|
2942
|
-
anchor: value,
|
|
2943
|
-
nearest: nearestTestid(value, hint?.presentTestids ?? [])
|
|
2944
|
-
};
|
|
2945
|
-
}
|
|
2946
|
-
function anchorLabel(anchor) {
|
|
2947
|
-
if (anchor.kind === AnchorKind.TESTID)
|
|
2948
|
-
return anchor.value;
|
|
2949
|
-
if (anchor.kind === AnchorKind.SIGNAL)
|
|
2950
|
-
return anchor.name;
|
|
2951
|
-
return anchor.name ?? anchor.role;
|
|
3584
|
+
return { level: RiskLevel.LOW, reason: "last run passed clean" };
|
|
2952
3585
|
}
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
const { refs, hint } = readQuery(queryResult);
|
|
2956
|
-
if (refs.length === 0) {
|
|
3586
|
+
function gradeRisk(grade) {
|
|
3587
|
+
if (grade === FlowAssertionGrade.ASSERTION_FREE) {
|
|
2957
3588
|
return {
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
anchor: value,
|
|
2961
|
-
ok: false,
|
|
2962
|
-
drift: testidDrift(value, hint)
|
|
3589
|
+
level: RiskLevel.MEDIUM,
|
|
3590
|
+
reason: "asserts no consequence \u2014 a green run proves little"
|
|
2963
3591
|
};
|
|
2964
3592
|
}
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
const act = await session.command(IrisCommand.ACT, {
|
|
2968
|
-
ref,
|
|
2969
|
-
action: step.action ?? "",
|
|
2970
|
-
args: step.args ?? {}
|
|
2971
|
-
});
|
|
2972
|
-
const result = { step: index, tool: step.tool, anchor: value, ok: act.ok };
|
|
2973
|
-
if (!act.ok) {
|
|
2974
|
-
result.error = act.error ?? "command failed";
|
|
2975
|
-
if (note !== void 0)
|
|
2976
|
-
result.note = note;
|
|
2977
|
-
return result;
|
|
3593
|
+
if (grade === FlowAssertionGrade.PRESENCE_ONLY) {
|
|
3594
|
+
return { level: RiskLevel.LOW, reason: "presence-only assertion" };
|
|
2978
3595
|
}
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
3596
|
+
return { level: RiskLevel.LOW, reason: "asserts a consequence" };
|
|
3597
|
+
}
|
|
3598
|
+
function flowRisk(grade, run) {
|
|
3599
|
+
const r = runRisk(run);
|
|
3600
|
+
const g = gradeRisk(grade);
|
|
3601
|
+
const top = RANK[r.level] >= RANK[g.level] ? r : g;
|
|
3602
|
+
return run === void 0 ? { level: top.level, reason: top.reason } : { level: top.level, reason: top.reason, lastStatus: run.status };
|
|
3603
|
+
}
|
|
3604
|
+
function rankByRisk(entries) {
|
|
3605
|
+
return [...entries].sort((a, b) => RANK[b.risk.level] - RANK[a.risk.level] || a.name.localeCompare(b.name)).map((e) => e.name);
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
// ../server/dist/domain/domain-model.js
|
|
3609
|
+
function flatten(steps) {
|
|
3610
|
+
const out = [];
|
|
3611
|
+
for (const s of steps) {
|
|
3612
|
+
out.push(s);
|
|
3613
|
+
if (s.steps !== void 0)
|
|
3614
|
+
out.push(...flatten(s.steps));
|
|
2995
3615
|
}
|
|
2996
|
-
|
|
2997
|
-
result.note = note;
|
|
2998
|
-
return result;
|
|
3616
|
+
return out;
|
|
2999
3617
|
}
|
|
3000
|
-
|
|
3001
|
-
const
|
|
3002
|
-
|
|
3003
|
-
|
|
3618
|
+
function flowSignals(flow) {
|
|
3619
|
+
const set = /* @__PURE__ */ new Set();
|
|
3620
|
+
for (const step of flatten(flow.steps)) {
|
|
3621
|
+
if (step.anchor.kind === AnchorKind.SIGNAL)
|
|
3622
|
+
set.add(step.anchor.name);
|
|
3623
|
+
if (step.expect?.signal !== void 0)
|
|
3624
|
+
set.add(step.expect.signal);
|
|
3625
|
+
}
|
|
3626
|
+
if (flow.success?.signal !== void 0)
|
|
3627
|
+
set.add(flow.success.signal);
|
|
3628
|
+
return [...set];
|
|
3629
|
+
}
|
|
3630
|
+
function flowTestids(flow) {
|
|
3631
|
+
const set = /* @__PURE__ */ new Set();
|
|
3632
|
+
for (const step of flatten(flow.steps)) {
|
|
3633
|
+
if (step.anchor.kind === AnchorKind.TESTID)
|
|
3634
|
+
set.add(step.anchor.value);
|
|
3635
|
+
if (step.expect?.element?.testid !== void 0)
|
|
3636
|
+
set.add(step.expect.element.testid);
|
|
3637
|
+
}
|
|
3638
|
+
if (flow.success?.element?.testid !== void 0)
|
|
3639
|
+
set.add(flow.success.element.testid);
|
|
3640
|
+
return [...set];
|
|
3641
|
+
}
|
|
3642
|
+
var EMPTY_CONTRACT = { testids: [], signals: [], stores: [], flows: [] };
|
|
3643
|
+
function buildDomainModel(flows, contract, runs = []) {
|
|
3644
|
+
const caps = contract ?? EMPTY_CONTRACT;
|
|
3645
|
+
const hasHistory = runs.length > 0;
|
|
3646
|
+
const flowSummaries = flows.map((flow) => {
|
|
3647
|
+
const c = classifyFlowAssertions(flow);
|
|
3648
|
+
const summary = {
|
|
3649
|
+
name: flow.name,
|
|
3650
|
+
steps: c.totalSteps,
|
|
3651
|
+
grade: c.grade,
|
|
3652
|
+
asserts: c.hasConsequenceAssertion,
|
|
3653
|
+
signals: flowSignals(flow),
|
|
3654
|
+
testids: flowTestids(flow)
|
|
3655
|
+
};
|
|
3656
|
+
if (flow.success !== void 0)
|
|
3657
|
+
summary.mustHold = successLabel(flow.success);
|
|
3658
|
+
if (c.warning !== void 0)
|
|
3659
|
+
summary.warning = c.warning;
|
|
3660
|
+
if (hasHistory)
|
|
3661
|
+
summary.risk = flowRisk(c.grade, latestRun(flow.name, runs));
|
|
3662
|
+
return summary;
|
|
3663
|
+
});
|
|
3664
|
+
const testedSignals = new Set(flowSummaries.flatMap((f) => f.signals));
|
|
3665
|
+
const testedTestids = new Set(flowSummaries.flatMap((f) => f.testids));
|
|
3666
|
+
const coverage = {
|
|
3667
|
+
asserted: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.ASSERTED).length,
|
|
3668
|
+
presenceOnly: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.PRESENCE_ONLY).length,
|
|
3669
|
+
assertionFree: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.ASSERTION_FREE).length
|
|
3670
|
+
};
|
|
3671
|
+
const gaps = {
|
|
3672
|
+
unassertedFlows: flowSummaries.filter((f) => !f.asserts).map((f) => f.name),
|
|
3673
|
+
declaredUntestedSignals: caps.signals.filter((s) => !testedSignals.has(s)),
|
|
3674
|
+
declaredUntestedTestids: caps.testids.filter((t) => !testedTestids.has(t))
|
|
3675
|
+
};
|
|
3676
|
+
const riskRanked = hasHistory ? rankByRisk(flowSummaries.filter((f) => f.risk !== void 0).map((f) => ({ name: f.name, risk: f.risk }))) : [];
|
|
3677
|
+
const top = riskRanked[0];
|
|
3678
|
+
const topFlow = top === void 0 ? void 0 : flowSummaries.find((f) => f.name === top);
|
|
3679
|
+
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;
|
|
3004
3680
|
return {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
anchor: name,
|
|
3013
|
-
nearest: null
|
|
3014
|
-
}
|
|
3681
|
+
flowCount: flows.length,
|
|
3682
|
+
flows: flowSummaries,
|
|
3683
|
+
declared: { testids: caps.testids.length, signals: caps.signals, stores: caps.stores },
|
|
3684
|
+
coverage,
|
|
3685
|
+
gaps,
|
|
3686
|
+
riskRanked,
|
|
3687
|
+
summary: buildSummary(flows.length, coverage, gaps, topRisk)
|
|
3015
3688
|
};
|
|
3016
3689
|
}
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
let index = 0;
|
|
3021
|
-
for (const step of flow.steps) {
|
|
3022
|
-
const label = anchorLabel(step.anchor);
|
|
3023
|
-
let result;
|
|
3024
|
-
if (step.anchor.kind === AnchorKind.SIGNAL) {
|
|
3025
|
-
result = await runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs);
|
|
3026
|
-
} else {
|
|
3027
|
-
result = await runTestidStep(session, step, index, label, dynamic);
|
|
3028
|
-
}
|
|
3029
|
-
results.push(result);
|
|
3030
|
-
if (result.drift !== void 0 || !result.ok)
|
|
3031
|
-
break;
|
|
3032
|
-
index += 1;
|
|
3690
|
+
function buildSummary(flowCount, coverage, gaps, topRisk) {
|
|
3691
|
+
if (flowCount === 0) {
|
|
3692
|
+
return "No saved flows yet \u2014 record the critical journeys (iris_record_start) so the agent learns the app.";
|
|
3033
3693
|
}
|
|
3034
|
-
|
|
3694
|
+
const parts = [
|
|
3695
|
+
`${String(flowCount)} flow${flowCount === 1 ? "" : "s"}: ${String(coverage.asserted)} asserted, ${String(coverage.presenceOnly)} presence-only, ${String(coverage.assertionFree)} assertion-free`
|
|
3696
|
+
];
|
|
3697
|
+
if (topRisk !== void 0) {
|
|
3698
|
+
parts.push(`test first: ${topRisk.name} (${topRisk.reason})`);
|
|
3699
|
+
}
|
|
3700
|
+
if (gaps.declaredUntestedSignals.length > 0) {
|
|
3701
|
+
parts.push(`${String(gaps.declaredUntestedSignals.length)} declared signal(s) no flow asserts (${gaps.declaredUntestedSignals.join(", ")})`);
|
|
3702
|
+
}
|
|
3703
|
+
if (gaps.unassertedFlows.length > 0) {
|
|
3704
|
+
parts.push(`${String(gaps.unassertedFlows.length)} flow(s) assert no consequence`);
|
|
3705
|
+
}
|
|
3706
|
+
return parts.join(". ") + ".";
|
|
3035
3707
|
}
|
|
3036
3708
|
|
|
3037
|
-
// ../server/dist/
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3709
|
+
// ../server/dist/domain/domain-tools.js
|
|
3710
|
+
var DOMAIN_TOOLS = [
|
|
3711
|
+
{
|
|
3712
|
+
name: IrisTool.DOMAIN,
|
|
3713
|
+
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).",
|
|
3714
|
+
inputSchema: {},
|
|
3715
|
+
outputSchema: {
|
|
3716
|
+
flowCount: z5.number(),
|
|
3717
|
+
flows: z5.array(z5.object({
|
|
3718
|
+
name: z5.string(),
|
|
3719
|
+
steps: z5.number(),
|
|
3720
|
+
grade: z5.string(),
|
|
3721
|
+
asserts: z5.boolean(),
|
|
3722
|
+
mustHold: z5.string().optional().describe("The success consequence that must hold for this flow (what it actually tests)."),
|
|
3723
|
+
warning: z5.string().optional(),
|
|
3724
|
+
signals: z5.array(z5.string()),
|
|
3725
|
+
testids: z5.array(z5.string())
|
|
3726
|
+
})),
|
|
3727
|
+
declared: z5.object({
|
|
3728
|
+
testids: z5.number(),
|
|
3729
|
+
signals: z5.array(z5.string()),
|
|
3730
|
+
stores: z5.array(z5.string())
|
|
3731
|
+
}),
|
|
3732
|
+
coverage: z5.object({
|
|
3733
|
+
asserted: z5.number(),
|
|
3734
|
+
presenceOnly: z5.number(),
|
|
3735
|
+
assertionFree: z5.number()
|
|
3736
|
+
}),
|
|
3737
|
+
gaps: z5.object({
|
|
3738
|
+
unassertedFlows: z5.array(z5.string()),
|
|
3739
|
+
declaredUntestedSignals: z5.array(z5.string()),
|
|
3740
|
+
declaredUntestedTestids: z5.array(z5.string())
|
|
3741
|
+
}),
|
|
3742
|
+
riskRanked: z5.array(z5.string()).describe("Flow names worst-risk first (run history + assertion quality). Test these first."),
|
|
3743
|
+
summary: z5.string()
|
|
3744
|
+
},
|
|
3745
|
+
handler: async (deps) => {
|
|
3746
|
+
const names = await deps.flows.list();
|
|
3747
|
+
const flows = [];
|
|
3748
|
+
for (const name of names) {
|
|
3749
|
+
const loaded = await deps.flows.load(name);
|
|
3750
|
+
if (loaded.ok)
|
|
3751
|
+
flows.push(loaded.value);
|
|
3752
|
+
}
|
|
3753
|
+
const contract = await readContract(deps.fs, deps.irisRoot);
|
|
3754
|
+
const project = await deps.project.read();
|
|
3755
|
+
const runs = project.ok ? project.file.runs : [];
|
|
3756
|
+
return buildDomainModel(flows, contract.ok ? contract.capabilities : null, runs);
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
];
|
|
3760
|
+
|
|
3761
|
+
// ../server/dist/tools/browser-tools.js
|
|
3762
|
+
import { z as z6 } from "zod";
|
|
3763
|
+
var sessionIdShape2 = {
|
|
3764
|
+
sessionId: z6.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3765
|
+
};
|
|
3766
|
+
async function commandOrThrow2(deps, sessionId, name, args) {
|
|
3767
|
+
const session = deps.sessions.resolve(sessionId);
|
|
3768
|
+
const result = await session.command(name, args);
|
|
3769
|
+
if (!result.ok)
|
|
3770
|
+
throw new Error(result.error ?? `command '${name}' failed`);
|
|
3771
|
+
return result.result;
|
|
3061
3772
|
}
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3773
|
+
var BROWSER_TOOLS = [
|
|
3774
|
+
{
|
|
3775
|
+
name: IrisTool.NAVIGATE,
|
|
3776
|
+
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.",
|
|
3777
|
+
inputSchema: {
|
|
3778
|
+
url: z6.string().describe("The URL to navigate to."),
|
|
3779
|
+
...sessionIdShape2
|
|
3780
|
+
},
|
|
3781
|
+
outputSchema: {
|
|
3782
|
+
ok: z6.boolean(),
|
|
3783
|
+
url: z6.string().optional(),
|
|
3784
|
+
reason: z6.string().optional()
|
|
3785
|
+
},
|
|
3786
|
+
handler: async (deps, args) => {
|
|
3787
|
+
const url = asString(args["url"]);
|
|
3788
|
+
if (url === void 0 || url.length === 0)
|
|
3789
|
+
return { ok: false, reason: "url required" };
|
|
3790
|
+
const result = await commandOrThrow2(deps, asString(args["sessionId"]), IrisCommand.NAVIGATE, { url });
|
|
3791
|
+
return {
|
|
3792
|
+
ok: result.ok === true,
|
|
3793
|
+
...typeof result.url === "string" ? { url: result.url } : {},
|
|
3794
|
+
...typeof result.reason === "string" ? { reason: result.reason } : {}
|
|
3795
|
+
};
|
|
3796
|
+
}
|
|
3797
|
+
},
|
|
3798
|
+
{
|
|
3799
|
+
name: IrisTool.REFRESH,
|
|
3800
|
+
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.",
|
|
3801
|
+
inputSchema: {
|
|
3802
|
+
hard: z6.boolean().optional().describe("Set true to bypass the browser cache. Default: false (normal reload)."),
|
|
3803
|
+
...sessionIdShape2
|
|
3804
|
+
},
|
|
3805
|
+
outputSchema: {
|
|
3806
|
+
ok: z6.boolean()
|
|
3807
|
+
},
|
|
3808
|
+
handler: async (deps, args) => {
|
|
3809
|
+
await commandOrThrow2(deps, asString(args["sessionId"]), IrisCommand.REFRESH, {
|
|
3810
|
+
hard: args["hard"] === true
|
|
3811
|
+
});
|
|
3812
|
+
return { ok: true };
|
|
3813
|
+
}
|
|
3070
3814
|
}
|
|
3071
|
-
|
|
3072
|
-
}
|
|
3815
|
+
];
|
|
3073
3816
|
|
|
3074
3817
|
// ../server/dist/flows/flow-tools.js
|
|
3818
|
+
import { z as z7 } from "zod";
|
|
3075
3819
|
function latestRecordedFlow(events) {
|
|
3076
3820
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
3077
3821
|
const event = events[i];
|
|
@@ -3117,18 +3861,26 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs) {
|
|
|
3117
3861
|
var FLOW_TOOLS = [
|
|
3118
3862
|
{
|
|
3119
3863
|
name: IrisTool.FLOW_SAVE,
|
|
3120
|
-
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
|
|
3864
|
+
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).',
|
|
3121
3865
|
inputSchema: {
|
|
3122
|
-
flowName:
|
|
3866
|
+
flowName: z7.string().describe("Name for the flow file (saved to .iris/flows/<flowName>.json). Use again in iris_flow_load/iris_flow_replay.")
|
|
3123
3867
|
},
|
|
3124
3868
|
outputSchema: {
|
|
3125
|
-
saved:
|
|
3126
|
-
path:
|
|
3127
|
-
stepCount:
|
|
3128
|
-
degraded:
|
|
3869
|
+
saved: z7.boolean(),
|
|
3870
|
+
path: z7.string(),
|
|
3871
|
+
stepCount: z7.number().optional(),
|
|
3872
|
+
degraded: z7.number().optional(),
|
|
3873
|
+
assertions: z7.object({
|
|
3874
|
+
grade: z7.string().describe("asserted | presence-only | assertion-free"),
|
|
3875
|
+
hasConsequenceAssertion: z7.boolean(),
|
|
3876
|
+
totalSteps: z7.number(),
|
|
3877
|
+
consequenceSteps: z7.number(),
|
|
3878
|
+
weakSteps: z7.number(),
|
|
3879
|
+
warning: z7.string().optional()
|
|
3880
|
+
}).optional()
|
|
3129
3881
|
},
|
|
3130
3882
|
handler: (deps, args) => {
|
|
3131
|
-
const name =
|
|
3883
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3132
3884
|
const program = deps.recordings.getCompiled(name);
|
|
3133
3885
|
if (program === void 0) {
|
|
3134
3886
|
return Promise.resolve({
|
|
@@ -3142,10 +3894,12 @@ var FLOW_TOOLS = [
|
|
|
3142
3894
|
dynamic: deps.annotations.dynamic(name),
|
|
3143
3895
|
...success !== void 0 ? { success } : {}
|
|
3144
3896
|
};
|
|
3145
|
-
return deps.flows.save(program, annotations).then((res) => {
|
|
3146
|
-
if (res.ok)
|
|
3147
|
-
|
|
3148
|
-
|
|
3897
|
+
return deps.flows.save(program, annotations).then(async (res) => {
|
|
3898
|
+
if (!res.ok)
|
|
3899
|
+
return { error: flowErrorMessage(res.code), code: res.code };
|
|
3900
|
+
deps.annotations.clear(name);
|
|
3901
|
+
const loaded = await deps.flows.load(res.value.name);
|
|
3902
|
+
return loaded.ok ? { ...res.value, assertions: classifyFlowAssertions(loaded.value) } : res.value;
|
|
3149
3903
|
});
|
|
3150
3904
|
}
|
|
3151
3905
|
},
|
|
@@ -3154,22 +3908,27 @@ var FLOW_TOOLS = [
|
|
|
3154
3908
|
description: "List saved flow names under .iris/flows (a fresh agent learns the demonstrated journeys without a browser).",
|
|
3155
3909
|
inputSchema: {},
|
|
3156
3910
|
outputSchema: {
|
|
3157
|
-
flows:
|
|
3911
|
+
flows: z7.array(z7.object({ name: z7.string(), path: z7.string(), createdAt: z7.number().optional() }))
|
|
3158
3912
|
},
|
|
3159
|
-
|
|
3913
|
+
// Return {name, path} objects to MATCH the declared outputSchema. Returning bare name strings
|
|
3914
|
+
// (the prior bug) made schema-validating MCP clients reject the result ("expected object,
|
|
3915
|
+
// received string") — caught driving the live demo.
|
|
3916
|
+
handler: (deps) => deps.flows.list().then((names) => ({
|
|
3917
|
+
flows: names.map((name) => ({ name, path: flowPath(deps.irisRoot, name) }))
|
|
3918
|
+
}))
|
|
3160
3919
|
},
|
|
3161
3920
|
{
|
|
3162
3921
|
name: IrisTool.FLOW_LOAD,
|
|
3163
3922
|
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 }.",
|
|
3164
3923
|
inputSchema: {
|
|
3165
|
-
flowName:
|
|
3924
|
+
flowName: z7.string().describe("Flow file name (without .json extension) from iris_flow_list.")
|
|
3166
3925
|
},
|
|
3167
3926
|
outputSchema: {
|
|
3168
|
-
flowName:
|
|
3169
|
-
steps:
|
|
3170
|
-
createdAt:
|
|
3927
|
+
flowName: z7.string(),
|
|
3928
|
+
steps: z7.array(z7.unknown()),
|
|
3929
|
+
createdAt: z7.number().optional()
|
|
3171
3930
|
},
|
|
3172
|
-
handler: (deps, args) => deps.flows.load(
|
|
3931
|
+
handler: (deps, args) => deps.flows.load(asString(args["flowName"]) ?? "").then((res) => {
|
|
3173
3932
|
if (!res.ok)
|
|
3174
3933
|
return { error: flowErrorMessage(res.code), code: res.code };
|
|
3175
3934
|
const { name, ...rest } = res.value;
|
|
@@ -3178,20 +3937,21 @@ var FLOW_TOOLS = [
|
|
|
3178
3937
|
},
|
|
3179
3938
|
{
|
|
3180
3939
|
name: IrisTool.FLOW_REPLAY,
|
|
3181
|
-
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:[...] };
|
|
3940
|
+
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).`,
|
|
3182
3941
|
inputSchema: {
|
|
3183
|
-
flowName:
|
|
3184
|
-
|
|
3942
|
+
flowName: z7.string().describe("Flow file name (without .json extension) from iris_flow_list."),
|
|
3943
|
+
confirmDangerous: z7.boolean().optional().describe("Set true to allow destructive controls during this replay only."),
|
|
3944
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3185
3945
|
},
|
|
3186
3946
|
outputSchema: {
|
|
3187
|
-
status:
|
|
3188
|
-
steps:
|
|
3189
|
-
proposals:
|
|
3190
|
-
error:
|
|
3947
|
+
status: z7.string().describe("ok | drift | error"),
|
|
3948
|
+
steps: z7.array(z7.unknown()),
|
|
3949
|
+
proposals: z7.array(z7.unknown()).optional(),
|
|
3950
|
+
error: z7.object({ code: z7.string(), message: z7.string() }).optional()
|
|
3191
3951
|
},
|
|
3192
3952
|
handler: async (deps, args) => {
|
|
3193
3953
|
const startedAt = deps.now();
|
|
3194
|
-
const name =
|
|
3954
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3195
3955
|
const loaded = await deps.flows.load(name);
|
|
3196
3956
|
if (!loaded.ok) {
|
|
3197
3957
|
await recordReplayRun(deps, name, ReplayStatus.ERROR, 0, deps.now() - startedAt);
|
|
@@ -3202,12 +3962,35 @@ var FLOW_TOOLS = [
|
|
|
3202
3962
|
error: { code: loaded.code, message: flowErrorMessage(loaded.code) }
|
|
3203
3963
|
};
|
|
3204
3964
|
}
|
|
3205
|
-
const session = deps.sessions.resolve(
|
|
3206
|
-
const
|
|
3965
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
3966
|
+
const replayFloor = session.elapsed();
|
|
3967
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
3968
|
+
const stepsClean = steps.length > 0 && steps.every((s) => s.ok && s.drift === void 0);
|
|
3969
|
+
if (stepsClean && loaded.value.success !== void 0) {
|
|
3970
|
+
const verdict = await assertSuccess(session, loaded.value.success, dynamicTestids(loaded.value), waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, replayFloor);
|
|
3971
|
+
const row = {
|
|
3972
|
+
step: steps.length,
|
|
3973
|
+
tool: "success",
|
|
3974
|
+
anchor: successLabel(loaded.value.success),
|
|
3975
|
+
ok: verdict.pass,
|
|
3976
|
+
...verdict.pass ? {} : { error: verdict.failureReason ?? "flow.success not satisfied" }
|
|
3977
|
+
};
|
|
3978
|
+
steps.push(row);
|
|
3979
|
+
}
|
|
3207
3980
|
const driftSteps = steps.filter((s) => s.drift !== void 0).length;
|
|
3208
3981
|
const allOk = steps.every((s) => s.ok);
|
|
3209
|
-
const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.
|
|
3982
|
+
const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.ERROR;
|
|
3210
3983
|
await recordReplayRun(deps, name, status, driftSteps, deps.now() - startedAt);
|
|
3984
|
+
const failed = steps.find((step) => !step.ok && step.drift === void 0);
|
|
3985
|
+
if (failed !== void 0) {
|
|
3986
|
+
const message = failed.error ?? "flow action failed";
|
|
3987
|
+
return {
|
|
3988
|
+
name,
|
|
3989
|
+
status,
|
|
3990
|
+
steps,
|
|
3991
|
+
error: { code: ReplayStatus.ERROR, message }
|
|
3992
|
+
};
|
|
3993
|
+
}
|
|
3211
3994
|
return { name, status, steps };
|
|
3212
3995
|
}
|
|
3213
3996
|
},
|
|
@@ -3215,20 +3998,20 @@ var FLOW_TOOLS = [
|
|
|
3215
3998
|
name: IrisTool.FLOW_SAVE_RECORDED,
|
|
3216
3999
|
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).",
|
|
3217
4000
|
inputSchema: {
|
|
3218
|
-
flowName:
|
|
4001
|
+
flowName: z7.string().optional().describe("Override the flow name embedded in the recorded flow. Omit to use the recorder-assigned name."),
|
|
3219
4002
|
...{
|
|
3220
|
-
sessionId:
|
|
4003
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3221
4004
|
}
|
|
3222
4005
|
},
|
|
3223
4006
|
outputSchema: {
|
|
3224
|
-
flowName:
|
|
3225
|
-
stepCount:
|
|
3226
|
-
degraded:
|
|
3227
|
-
error:
|
|
3228
|
-
code:
|
|
4007
|
+
flowName: z7.string().optional(),
|
|
4008
|
+
stepCount: z7.number().optional(),
|
|
4009
|
+
degraded: z7.number().optional(),
|
|
4010
|
+
error: z7.string().optional(),
|
|
4011
|
+
code: z7.string().optional()
|
|
3229
4012
|
},
|
|
3230
4013
|
handler: async (deps, args) => {
|
|
3231
|
-
const session = deps.sessions.resolve(
|
|
4014
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
3232
4015
|
const recorded = latestRecordedFlow(session.eventsSince(0));
|
|
3233
4016
|
if (recorded === void 0) {
|
|
3234
4017
|
return {
|
|
@@ -3236,7 +4019,7 @@ var FLOW_TOOLS = [
|
|
|
3236
4019
|
code: RecordedSaveError.NO_RECORDED_FLOW
|
|
3237
4020
|
};
|
|
3238
4021
|
}
|
|
3239
|
-
const override =
|
|
4022
|
+
const override = asString(args["flowName"]);
|
|
3240
4023
|
const flow = override !== void 0 ? { ...recorded.flow, name: override } : recorded.flow;
|
|
3241
4024
|
const res = await deps.flows.saveFlow(flow);
|
|
3242
4025
|
if (!res.ok)
|
|
@@ -3247,35 +4030,38 @@ var FLOW_TOOLS = [
|
|
|
3247
4030
|
},
|
|
3248
4031
|
{
|
|
3249
4032
|
name: IrisTool.FLOW_HEAL,
|
|
3250
|
-
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 }.",
|
|
4033
|
+
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 }.",
|
|
3251
4034
|
inputSchema: {
|
|
3252
|
-
flowName:
|
|
3253
|
-
apply:
|
|
3254
|
-
|
|
4035
|
+
flowName: z7.string().describe("Flow file name to heal (from iris_flow_list)."),
|
|
4036
|
+
apply: z7.boolean().optional(),
|
|
4037
|
+
confirmDangerous: z7.boolean().optional().describe("Set true to allow destructive controls during this heal replay only."),
|
|
4038
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3255
4039
|
},
|
|
3256
4040
|
outputSchema: {
|
|
3257
|
-
flowName:
|
|
3258
|
-
status:
|
|
3259
|
-
applied:
|
|
3260
|
-
proposals:
|
|
3261
|
-
changed:
|
|
3262
|
-
message:
|
|
3263
|
-
error:
|
|
4041
|
+
flowName: z7.string(),
|
|
4042
|
+
status: z7.string(),
|
|
4043
|
+
applied: z7.boolean(),
|
|
4044
|
+
proposals: z7.array(z7.unknown()),
|
|
4045
|
+
changed: z7.array(z7.unknown()),
|
|
4046
|
+
message: z7.string(),
|
|
4047
|
+
error: z7.object({ code: z7.string(), message: z7.string() }).optional()
|
|
3264
4048
|
},
|
|
3265
4049
|
handler: (deps, args) => healFlow(deps, args).then(({ name, ...rest }) => ({ flowName: name, ...rest }))
|
|
3266
4050
|
}
|
|
3267
4051
|
];
|
|
3268
4052
|
var HEAL_MESSAGES = {
|
|
3269
4053
|
NOTHING: "nothing to heal \u2014 every anchor resolved on replay",
|
|
3270
|
-
HEALED: "rewrote drifted testid anchors to their nearest surviving match",
|
|
4054
|
+
HEALED: "rewrote drifted testid anchors to their nearest surviving match and re-verified the flow's success consequence still fires",
|
|
3271
4055
|
DRIFT_DRY: "confident rebind(s) proposed \u2014 re-run with apply:true to write them to disk",
|
|
3272
|
-
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
|
|
4056
|
+
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`,
|
|
4057
|
+
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.",
|
|
4058
|
+
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"
|
|
3273
4059
|
};
|
|
3274
4060
|
function toChange(proposal) {
|
|
3275
4061
|
return { step: proposal.step, from: proposal.from, to: proposal.to };
|
|
3276
4062
|
}
|
|
3277
4063
|
async function healFlow(deps, args) {
|
|
3278
|
-
const name =
|
|
4064
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3279
4065
|
const apply = args["apply"] === true;
|
|
3280
4066
|
const loaded = await deps.flows.load(name);
|
|
3281
4067
|
if (!loaded.ok) {
|
|
@@ -3289,9 +4075,22 @@ async function healFlow(deps, args) {
|
|
|
3289
4075
|
error: { code: loaded.code, message: flowErrorMessage(loaded.code) }
|
|
3290
4076
|
};
|
|
3291
4077
|
}
|
|
3292
|
-
const session = deps.sessions.resolve(
|
|
3293
|
-
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS);
|
|
4078
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4079
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
3294
4080
|
const drifted = steps.some((s) => s.drift !== void 0);
|
|
4081
|
+
const failed = steps.find((s) => !s.ok && s.drift === void 0);
|
|
4082
|
+
if (failed !== void 0) {
|
|
4083
|
+
const message = failed.error ?? "flow replay failed before an anchor could be healed";
|
|
4084
|
+
return {
|
|
4085
|
+
name,
|
|
4086
|
+
status: HealStatus.ERROR,
|
|
4087
|
+
applied: false,
|
|
4088
|
+
proposals: [],
|
|
4089
|
+
changed: [],
|
|
4090
|
+
message,
|
|
4091
|
+
error: { code: ReplayStatus.ERROR, message }
|
|
4092
|
+
};
|
|
4093
|
+
}
|
|
3295
4094
|
if (!drifted) {
|
|
3296
4095
|
return {
|
|
3297
4096
|
name,
|
|
@@ -3323,6 +4122,23 @@ async function healFlow(deps, args) {
|
|
|
3323
4122
|
message: HEAL_MESSAGES.DRIFT_DRY
|
|
3324
4123
|
};
|
|
3325
4124
|
}
|
|
4125
|
+
const { flow: healed } = applyHealChanges(loaded.value, proposals.map(toChange));
|
|
4126
|
+
if (healed.success !== void 0) {
|
|
4127
|
+
const verifyFloor = session.elapsed();
|
|
4128
|
+
const verifySteps = await replayFlow(session, healed, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
4129
|
+
const verifyClean = verifySteps.length > 0 && verifySteps.every((s) => s.ok && s.drift === void 0);
|
|
4130
|
+
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" };
|
|
4131
|
+
if (!verdict.pass) {
|
|
4132
|
+
return {
|
|
4133
|
+
name,
|
|
4134
|
+
status: HealStatus.CONSEQUENCE_BROKEN,
|
|
4135
|
+
applied: false,
|
|
4136
|
+
proposals,
|
|
4137
|
+
changed: [],
|
|
4138
|
+
message: `${HEAL_MESSAGES.CONSEQUENCE_BROKEN} (${successLabel(healed.success)}: ${verdict.failureReason ?? "not satisfied"})`
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
3326
4142
|
const written = await deps.flows.heal(name, proposals.map(toChange));
|
|
3327
4143
|
if (!written.ok) {
|
|
3328
4144
|
return {
|
|
@@ -3341,14 +4157,14 @@ async function healFlow(deps, args) {
|
|
|
3341
4157
|
applied: written.value.changed.length > 0,
|
|
3342
4158
|
proposals,
|
|
3343
4159
|
changed: written.value.changed,
|
|
3344
|
-
message: HEAL_MESSAGES.HEALED
|
|
4160
|
+
message: loaded.value.success !== void 0 ? HEAL_MESSAGES.HEALED : HEAL_MESSAGES.HEALED_UNVERIFIED
|
|
3345
4161
|
};
|
|
3346
4162
|
}
|
|
3347
4163
|
|
|
3348
4164
|
// ../server/dist/project/project-tools.js
|
|
3349
|
-
import { z as
|
|
4165
|
+
import { z as z8 } from "zod";
|
|
3350
4166
|
var sessionIdShape3 = {
|
|
3351
|
-
sessionId:
|
|
4167
|
+
sessionId: z8.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3352
4168
|
};
|
|
3353
4169
|
var REGRESSION_STATUSES = /* @__PURE__ */ new Set([
|
|
3354
4170
|
RunStatus.FAIL,
|
|
@@ -3389,12 +4205,12 @@ var PROJECT_TOOLS = [
|
|
|
3389
4205
|
name: IrisTool.PROJECT,
|
|
3390
4206
|
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.',
|
|
3391
4207
|
inputSchema: {
|
|
3392
|
-
name:
|
|
4208
|
+
name: z8.string().optional().describe("Filter runs by this name. Omit to return all runs."),
|
|
3393
4209
|
...sessionIdShape3
|
|
3394
4210
|
},
|
|
3395
4211
|
outputSchema: {
|
|
3396
|
-
runs:
|
|
3397
|
-
diff:
|
|
4212
|
+
runs: z8.array(z8.unknown()),
|
|
4213
|
+
diff: z8.unknown().optional()
|
|
3398
4214
|
},
|
|
3399
4215
|
handler: async (deps, args) => {
|
|
3400
4216
|
const read = await deps.project.read();
|
|
@@ -3404,7 +4220,7 @@ var PROJECT_TOOLS = [
|
|
|
3404
4220
|
reason: read.reason
|
|
3405
4221
|
};
|
|
3406
4222
|
}
|
|
3407
|
-
const name =
|
|
4223
|
+
const name = asString(args["name"]);
|
|
3408
4224
|
if (name === void 0) {
|
|
3409
4225
|
return { runs: read.file.runs, learned: read.file.learned };
|
|
3410
4226
|
}
|
|
@@ -3422,22 +4238,22 @@ var PROJECT_TOOLS = [
|
|
|
3422
4238
|
name: IrisTool.RUN_RECORD,
|
|
3423
4239
|
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 }.",
|
|
3424
4240
|
inputSchema: {
|
|
3425
|
-
name:
|
|
3426
|
-
status:
|
|
3427
|
-
kind:
|
|
3428
|
-
summary:
|
|
4241
|
+
name: z8.string().describe("Run name for grouping in iris_project history."),
|
|
4242
|
+
status: z8.nativeEnum(RunStatus).describe("Outcome: pass | fail | drift | error"),
|
|
4243
|
+
kind: z8.nativeEnum(RunKind).optional(),
|
|
4244
|
+
summary: z8.string().optional().describe("One-line human summary of what this run covered."),
|
|
3429
4245
|
...sessionIdShape3
|
|
3430
4246
|
},
|
|
3431
4247
|
outputSchema: {
|
|
3432
|
-
recorded:
|
|
3433
|
-
runName:
|
|
3434
|
-
status:
|
|
4248
|
+
recorded: z8.boolean(),
|
|
4249
|
+
runName: z8.string(),
|
|
4250
|
+
status: z8.string()
|
|
3435
4251
|
},
|
|
3436
4252
|
handler: async (deps, args) => {
|
|
3437
|
-
const name =
|
|
4253
|
+
const name = asString(args["name"]) ?? "";
|
|
3438
4254
|
const status = args["status"];
|
|
3439
4255
|
const kindArg = args["kind"];
|
|
3440
|
-
const summary =
|
|
4256
|
+
const summary = asString(args["summary"]);
|
|
3441
4257
|
await deps.project.recordRun({
|
|
3442
4258
|
kind: typeof kindArg === "string" ? kindArg : RunKind.MANUAL,
|
|
3443
4259
|
name,
|
|
@@ -3450,7 +4266,7 @@ var PROJECT_TOOLS = [
|
|
|
3450
4266
|
];
|
|
3451
4267
|
|
|
3452
4268
|
// ../server/dist/visual/visual-tools.js
|
|
3453
|
-
import { z as
|
|
4269
|
+
import { z as z9 } from "zod";
|
|
3454
4270
|
|
|
3455
4271
|
// ../server/dist/visual/visual-diff.js
|
|
3456
4272
|
async function loadDeps() {
|
|
@@ -3601,13 +4417,13 @@ var VisualStore = class {
|
|
|
3601
4417
|
|
|
3602
4418
|
// ../server/dist/visual/visual-tools.js
|
|
3603
4419
|
var sessionIdShape4 = {
|
|
3604
|
-
sessionId:
|
|
4420
|
+
sessionId: z9.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3605
4421
|
};
|
|
3606
|
-
var rectShape =
|
|
3607
|
-
x:
|
|
3608
|
-
y:
|
|
3609
|
-
width:
|
|
3610
|
-
height:
|
|
4422
|
+
var rectShape = z9.object({
|
|
4423
|
+
x: z9.number(),
|
|
4424
|
+
y: z9.number(),
|
|
4425
|
+
width: z9.number(),
|
|
4426
|
+
height: z9.number()
|
|
3611
4427
|
});
|
|
3612
4428
|
function screenshotProvider(deps) {
|
|
3613
4429
|
const p = deps.realInput;
|
|
@@ -3619,11 +4435,11 @@ var noProvider = {
|
|
|
3619
4435
|
recommendation: VISUAL_NO_PROVIDER_RECOMMENDATION
|
|
3620
4436
|
};
|
|
3621
4437
|
function asBox(value) {
|
|
3622
|
-
const b =
|
|
3623
|
-
const x =
|
|
3624
|
-
const y =
|
|
3625
|
-
const w =
|
|
3626
|
-
const h =
|
|
4438
|
+
const b = asRecord(asRecord(value)["box"]);
|
|
4439
|
+
const x = asNumber(b["x"]);
|
|
4440
|
+
const y = asNumber(b["y"]);
|
|
4441
|
+
const w = asNumber(b["width"]);
|
|
4442
|
+
const h = asNumber(b["height"]);
|
|
3627
4443
|
if (x === void 0 || y === void 0 || w === void 0 || h === void 0)
|
|
3628
4444
|
return void 0;
|
|
3629
4445
|
if (w <= 0 || h <= 0)
|
|
@@ -3633,12 +4449,12 @@ function asBox(value) {
|
|
|
3633
4449
|
async function buildOpts(deps, sessionId, args) {
|
|
3634
4450
|
const clipArg = args["clip"];
|
|
3635
4451
|
if (clipArg !== void 0) {
|
|
3636
|
-
const c =
|
|
4452
|
+
const c = asRecord(clipArg);
|
|
3637
4453
|
const box = asBox({ box: c });
|
|
3638
4454
|
if (box !== void 0)
|
|
3639
4455
|
return { clip: box };
|
|
3640
4456
|
}
|
|
3641
|
-
const ref =
|
|
4457
|
+
const ref = asString(args["ref"]);
|
|
3642
4458
|
if (ref !== void 0) {
|
|
3643
4459
|
const session = deps.sessions.resolve(sessionId);
|
|
3644
4460
|
const res = await session.command(IrisCommand.INSPECT, { ref });
|
|
@@ -3664,31 +4480,31 @@ var VISUAL_TOOLS = [
|
|
|
3664
4480
|
name: IrisTool.SCREENSHOT,
|
|
3665
4481
|
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.",
|
|
3666
4482
|
inputSchema: {
|
|
3667
|
-
name:
|
|
3668
|
-
fullPage:
|
|
3669
|
-
ref:
|
|
4483
|
+
name: z9.string().describe("Baseline name \u2014 saved as .iris/visual/<name>.png. Use the same name in iris_visual_diff to compare."),
|
|
4484
|
+
fullPage: z9.boolean().optional().describe("Capture the full scroll height. Default: viewport only."),
|
|
4485
|
+
ref: z9.string().optional().describe("Element ref to screenshot (scopes to element bounding box). Omit for full page."),
|
|
3670
4486
|
clip: rectShape.optional().describe("Explicit { x, y, width, height } clip rectangle in page coordinates."),
|
|
3671
4487
|
...sessionIdShape4
|
|
3672
4488
|
},
|
|
3673
4489
|
outputSchema: {
|
|
3674
|
-
ok:
|
|
3675
|
-
saved:
|
|
3676
|
-
name:
|
|
3677
|
-
path:
|
|
3678
|
-
bytes:
|
|
3679
|
-
reason:
|
|
3680
|
-
recommendation:
|
|
4490
|
+
ok: z9.boolean(),
|
|
4491
|
+
saved: z9.boolean().optional(),
|
|
4492
|
+
name: z9.string().optional(),
|
|
4493
|
+
path: z9.string().optional(),
|
|
4494
|
+
bytes: z9.number().optional(),
|
|
4495
|
+
reason: z9.string().optional(),
|
|
4496
|
+
recommendation: z9.string().optional()
|
|
3681
4497
|
},
|
|
3682
4498
|
handler: async (deps, args) => {
|
|
3683
4499
|
const provider = screenshotProvider(deps);
|
|
3684
4500
|
if (provider === void 0)
|
|
3685
4501
|
return noProvider;
|
|
3686
|
-
const sessionId =
|
|
4502
|
+
const sessionId = asString(args["sessionId"]);
|
|
3687
4503
|
const session = deps.sessions.resolve(sessionId);
|
|
3688
4504
|
const png = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
|
|
3689
4505
|
if (png === void 0)
|
|
3690
4506
|
return { ok: false, reason: VisualReason.CAPTURE_FAILED };
|
|
3691
|
-
const name =
|
|
4507
|
+
const name = asString(args["name"]) ?? "default";
|
|
3692
4508
|
const store = new VisualStore(deps.fs, deps.irisRoot);
|
|
3693
4509
|
const path = await store.saveBaseline(name, png);
|
|
3694
4510
|
return { ok: true, saved: true, name, path, bytes: png.length };
|
|
@@ -3698,39 +4514,39 @@ var VISUAL_TOOLS = [
|
|
|
3698
4514
|
name: IrisTool.VISUAL_DIFF,
|
|
3699
4515
|
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).",
|
|
3700
4516
|
inputSchema: {
|
|
3701
|
-
baseline:
|
|
3702
|
-
fullPage:
|
|
3703
|
-
ref:
|
|
4517
|
+
baseline: z9.string().describe("Baseline screenshot name (from iris_screenshot). Used to compare with the current screenshot."),
|
|
4518
|
+
fullPage: z9.boolean().optional(),
|
|
4519
|
+
ref: z9.string().optional(),
|
|
3704
4520
|
clip: rectShape.optional(),
|
|
3705
|
-
masks:
|
|
3706
|
-
maxRatio:
|
|
3707
|
-
threshold:
|
|
4521
|
+
masks: z9.array(rectShape).optional(),
|
|
4522
|
+
maxRatio: z9.number().optional(),
|
|
4523
|
+
threshold: z9.number().optional().describe("Pixel difference threshold (0\u20131). Default: 0.01."),
|
|
3708
4524
|
...sessionIdShape4
|
|
3709
4525
|
},
|
|
3710
4526
|
outputSchema: {
|
|
3711
|
-
ok:
|
|
3712
|
-
match:
|
|
3713
|
-
diffPct:
|
|
3714
|
-
diffPath:
|
|
3715
|
-
reason:
|
|
4527
|
+
ok: z9.boolean(),
|
|
4528
|
+
match: z9.boolean().optional(),
|
|
4529
|
+
diffPct: z9.number().optional(),
|
|
4530
|
+
diffPath: z9.string().optional(),
|
|
4531
|
+
reason: z9.string().optional()
|
|
3716
4532
|
},
|
|
3717
4533
|
handler: async (deps, args) => {
|
|
3718
4534
|
const provider = screenshotProvider(deps);
|
|
3719
4535
|
if (provider === void 0)
|
|
3720
4536
|
return noProvider;
|
|
3721
|
-
const baseline =
|
|
4537
|
+
const baseline = asString(args["baseline"]) ?? "";
|
|
3722
4538
|
const store = new VisualStore(deps.fs, deps.irisRoot);
|
|
3723
4539
|
const baselineBytes = await store.readBaseline(baseline);
|
|
3724
4540
|
if (baselineBytes === void 0)
|
|
3725
4541
|
return { ok: false, reason: VisualReason.BASELINE_MISSING };
|
|
3726
|
-
const sessionId =
|
|
4542
|
+
const sessionId = asString(args["sessionId"]);
|
|
3727
4543
|
const session = deps.sessions.resolve(sessionId);
|
|
3728
4544
|
const current = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
|
|
3729
4545
|
if (current === void 0)
|
|
3730
4546
|
return { ok: false, reason: VisualReason.CAPTURE_FAILED };
|
|
3731
4547
|
const masks = rectsFrom(args["masks"]);
|
|
3732
|
-
const threshold =
|
|
3733
|
-
const maxRatio =
|
|
4548
|
+
const threshold = asNumber(args["threshold"]);
|
|
4549
|
+
const maxRatio = asNumber(args["maxRatio"]);
|
|
3734
4550
|
const result = await diffPng(baselineBytes, current, {
|
|
3735
4551
|
...threshold !== void 0 ? { threshold } : {},
|
|
3736
4552
|
...maxRatio !== void 0 ? { maxRatio } : {},
|
|
@@ -3747,7 +4563,7 @@ var VISUAL_TOOLS = [
|
|
|
3747
4563
|
];
|
|
3748
4564
|
|
|
3749
4565
|
// ../server/dist/crawl/crawl-tools.js
|
|
3750
|
-
import { z as
|
|
4566
|
+
import { z as z10 } from "zod";
|
|
3751
4567
|
|
|
3752
4568
|
// ../server/dist/crawl/crawl.js
|
|
3753
4569
|
function isActivity(e) {
|
|
@@ -3760,7 +4576,7 @@ function failedRequests(events, floor) {
|
|
|
3760
4576
|
return events.filter((e) => {
|
|
3761
4577
|
if (e.type !== EventType.NET_REQUEST)
|
|
3762
4578
|
return false;
|
|
3763
|
-
const status =
|
|
4579
|
+
const status = asNumber(e.data["status"]);
|
|
3764
4580
|
return status !== void 0 && status >= floor;
|
|
3765
4581
|
});
|
|
3766
4582
|
}
|
|
@@ -3790,7 +4606,7 @@ async function crawl(session, opts, sleep) {
|
|
|
3790
4606
|
const act = await session.command(IrisCommand.ACT, {
|
|
3791
4607
|
ref: item.ref,
|
|
3792
4608
|
action: ActionType.CLICK,
|
|
3793
|
-
args: {}
|
|
4609
|
+
args: opts.confirmDangerous === true ? { [DANGEROUS_ACTION_CONFIRM_ARG]: true } : {}
|
|
3794
4610
|
});
|
|
3795
4611
|
await sleep(settleMs);
|
|
3796
4612
|
const events = session.eventsSince(since);
|
|
@@ -3801,14 +4617,14 @@ async function crawl(session, opts, sleep) {
|
|
|
3801
4617
|
kind: CrawlAnomalyKind.CONSOLE_ERROR,
|
|
3802
4618
|
ref: item.ref,
|
|
3803
4619
|
desc: item.desc,
|
|
3804
|
-
detail:
|
|
4620
|
+
detail: asString(e.data["message"]) ?? e.type
|
|
3805
4621
|
});
|
|
3806
4622
|
}
|
|
3807
4623
|
for (const e of failedRequests(events, CRAWL_DEFAULTS.FAILED_STATUS)) {
|
|
3808
4624
|
counts.failedRequests += 1;
|
|
3809
|
-
const method =
|
|
3810
|
-
const url =
|
|
3811
|
-
const status =
|
|
4625
|
+
const method = asString(e.data["method"]) ?? "";
|
|
4626
|
+
const url = asString(e.data["url"]) ?? "";
|
|
4627
|
+
const status = asNumber(e.data["status"]);
|
|
3812
4628
|
anomalies.push({
|
|
3813
4629
|
kind: CrawlAnomalyKind.FAILED_REQUEST,
|
|
3814
4630
|
ref: item.ref,
|
|
@@ -3816,7 +4632,7 @@ async function crawl(session, opts, sleep) {
|
|
|
3816
4632
|
detail: `${method} ${url} \u2192 ${status ?? ""}`.trim()
|
|
3817
4633
|
});
|
|
3818
4634
|
}
|
|
3819
|
-
const dispatched =
|
|
4635
|
+
const dispatched = asRecord(act.result)["dispatched"] !== false && act.ok;
|
|
3820
4636
|
if (dispatched && errs.length === 0 && !events.some(isActivity)) {
|
|
3821
4637
|
counts.deadControls += 1;
|
|
3822
4638
|
anomalies.push({
|
|
@@ -3844,32 +4660,34 @@ var CRAWL_TOOLS = [
|
|
|
3844
4660
|
name: IrisTool.CRAWL,
|
|
3845
4661
|
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 }.",
|
|
3846
4662
|
inputSchema: {
|
|
3847
|
-
maxSteps:
|
|
3848
|
-
settleMs:
|
|
3849
|
-
scope:
|
|
3850
|
-
|
|
4663
|
+
maxSteps: z10.number().optional().describe("Maximum number of controls to click. Default: 25."),
|
|
4664
|
+
settleMs: z10.number().optional().describe("Milliseconds to wait after each click for the app to react. Default: 500."),
|
|
4665
|
+
scope: z10.string().optional().describe("CSS selector or element ref to restrict crawling to a subtree."),
|
|
4666
|
+
confirmDangerous: z10.boolean().optional().describe("Set true to allow controls classified as destructive. Default false; those controls are blocked by the browser."),
|
|
4667
|
+
sessionId: z10.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3851
4668
|
},
|
|
3852
4669
|
outputSchema: {
|
|
3853
|
-
interactiveFound:
|
|
3854
|
-
stepsRun:
|
|
3855
|
-
anomalies:
|
|
3856
|
-
kind:
|
|
3857
|
-
ref:
|
|
3858
|
-
desc:
|
|
3859
|
-
detail:
|
|
4670
|
+
interactiveFound: z10.number(),
|
|
4671
|
+
stepsRun: z10.number(),
|
|
4672
|
+
anomalies: z10.array(z10.object({
|
|
4673
|
+
kind: z10.string(),
|
|
4674
|
+
ref: z10.string(),
|
|
4675
|
+
desc: z10.string(),
|
|
4676
|
+
detail: z10.string().optional()
|
|
3860
4677
|
})),
|
|
3861
|
-
counts:
|
|
3862
|
-
truncated:
|
|
4678
|
+
counts: z10.record(z10.number()),
|
|
4679
|
+
truncated: z10.boolean()
|
|
3863
4680
|
},
|
|
3864
4681
|
handler: (deps, args) => {
|
|
3865
|
-
const session = deps.sessions.resolve(
|
|
3866
|
-
const maxSteps =
|
|
3867
|
-
const settleMs =
|
|
3868
|
-
const scope =
|
|
4682
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4683
|
+
const maxSteps = asNumber(args["maxSteps"]);
|
|
4684
|
+
const settleMs = asNumber(args["settleMs"]);
|
|
4685
|
+
const scope = asString(args["scope"]);
|
|
3869
4686
|
const opts = {
|
|
3870
4687
|
...maxSteps !== void 0 ? { maxSteps } : {},
|
|
3871
4688
|
...settleMs !== void 0 ? { settleMs } : {},
|
|
3872
|
-
...scope !== void 0 ? { scope } : {}
|
|
4689
|
+
...scope !== void 0 ? { scope } : {},
|
|
4690
|
+
...args["confirmDangerous"] === true ? { confirmDangerous: true } : {}
|
|
3873
4691
|
};
|
|
3874
4692
|
return crawl(session, opts, nodeSleep2);
|
|
3875
4693
|
}
|
|
@@ -3877,7 +4695,7 @@ var CRAWL_TOOLS = [
|
|
|
3877
4695
|
];
|
|
3878
4696
|
|
|
3879
4697
|
// ../server/dist/input/scroll-tools.js
|
|
3880
|
-
import { z as
|
|
4698
|
+
import { z as z11 } from "zod";
|
|
3881
4699
|
|
|
3882
4700
|
// ../server/dist/input/scroll-find.js
|
|
3883
4701
|
async function queryFirst(session, q) {
|
|
@@ -3886,9 +4704,9 @@ async function queryFirst(session, q) {
|
|
|
3886
4704
|
value: q.value,
|
|
3887
4705
|
...q.name !== void 0 ? { name: q.name } : {}
|
|
3888
4706
|
});
|
|
3889
|
-
const elements =
|
|
4707
|
+
const elements = asRecord(res.result)["elements"];
|
|
3890
4708
|
if (Array.isArray(elements) && elements.length > 0)
|
|
3891
|
-
return
|
|
4709
|
+
return asRecord(elements[0]);
|
|
3892
4710
|
return void 0;
|
|
3893
4711
|
}
|
|
3894
4712
|
async function scrollToFind(session, q, opts = {}) {
|
|
@@ -3907,7 +4725,7 @@ async function scrollToFind(session, q, opts = {}) {
|
|
|
3907
4725
|
const hit = await queryFirst(session, q);
|
|
3908
4726
|
if (hit !== void 0)
|
|
3909
4727
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
3910
|
-
const data =
|
|
4728
|
+
const data = asRecord(sr.result);
|
|
3911
4729
|
if (data["atEnd"] === true || data["scrolled"] === false) {
|
|
3912
4730
|
return { found: false, scrolls, exhausted: true };
|
|
3913
4731
|
}
|
|
@@ -3915,7 +4733,7 @@ async function scrollToFind(session, q, opts = {}) {
|
|
|
3915
4733
|
for (let i = 0; i < max; i += 1) {
|
|
3916
4734
|
const sr = await session.command(IrisCommand.SCROLL, q.container !== void 0 ? { ref: q.container } : {});
|
|
3917
4735
|
scrolls += 1;
|
|
3918
|
-
const data =
|
|
4736
|
+
const data = asRecord(sr.result);
|
|
3919
4737
|
const hit = await queryFirst(session, q);
|
|
3920
4738
|
if (hit !== void 0)
|
|
3921
4739
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
@@ -3932,58 +4750,58 @@ var SCROLL_TOOLS = [
|
|
|
3932
4750
|
name: IrisTool.SCROLL_TO,
|
|
3933
4751
|
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 }.",
|
|
3934
4752
|
inputSchema: {
|
|
3935
|
-
by:
|
|
3936
|
-
value:
|
|
3937
|
-
name:
|
|
3938
|
-
container:
|
|
3939
|
-
maxScrolls:
|
|
3940
|
-
targetIndex:
|
|
3941
|
-
totalCount:
|
|
3942
|
-
sessionId:
|
|
4753
|
+
by: z11.string().describe("Query strategy for finding the target: role | text | testid | label | placeholder | alt"),
|
|
4754
|
+
value: z11.string().describe("Query value for the selected strategy (the element to scroll into view)."),
|
|
4755
|
+
name: z11.string().optional().describe("Optional accessible name filter when using by=role."),
|
|
4756
|
+
container: z11.string().optional().describe("Element ref for the scrollable container. Omit to scroll the document."),
|
|
4757
|
+
maxScrolls: z11.number().optional().describe("Maximum number of scroll steps before giving up. Default: 20."),
|
|
4758
|
+
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."),
|
|
4759
|
+
totalCount: z11.number().optional().describe("Total item count in the virtualized list. Required for bisection with targetIndex."),
|
|
4760
|
+
sessionId: z11.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3943
4761
|
},
|
|
3944
4762
|
outputSchema: {
|
|
3945
|
-
found:
|
|
3946
|
-
element:
|
|
3947
|
-
scrolls:
|
|
3948
|
-
exhausted:
|
|
4763
|
+
found: z11.boolean(),
|
|
4764
|
+
element: z11.object({ ref: z11.string(), role: z11.string(), name: z11.string() }).optional(),
|
|
4765
|
+
scrolls: z11.number(),
|
|
4766
|
+
exhausted: z11.boolean()
|
|
3949
4767
|
},
|
|
3950
4768
|
handler: (deps, args) => {
|
|
3951
|
-
const session = deps.sessions.resolve(
|
|
3952
|
-
const name =
|
|
3953
|
-
const container =
|
|
3954
|
-
const targetIndex =
|
|
3955
|
-
const totalCount =
|
|
4769
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4770
|
+
const name = asString(args["name"]);
|
|
4771
|
+
const container = asString(args["container"]);
|
|
4772
|
+
const targetIndex = asNumber(args["targetIndex"]);
|
|
4773
|
+
const totalCount = asNumber(args["totalCount"]);
|
|
3956
4774
|
const q = {
|
|
3957
|
-
by:
|
|
3958
|
-
value:
|
|
4775
|
+
by: asString(args["by"]) ?? "",
|
|
4776
|
+
value: asString(args["value"]) ?? "",
|
|
3959
4777
|
...name !== void 0 ? { name } : {},
|
|
3960
4778
|
...container !== void 0 ? { container } : {},
|
|
3961
4779
|
...targetIndex !== void 0 ? { targetIndex } : {},
|
|
3962
4780
|
...totalCount !== void 0 ? { totalCount } : {}
|
|
3963
4781
|
};
|
|
3964
|
-
const maxScrolls =
|
|
4782
|
+
const maxScrolls = asNumber(args["maxScrolls"]);
|
|
3965
4783
|
return scrollToFind(session, q, maxScrolls !== void 0 ? { maxScrolls } : {});
|
|
3966
4784
|
}
|
|
3967
4785
|
}
|
|
3968
4786
|
];
|
|
3969
4787
|
|
|
3970
4788
|
// ../server/dist/session/session-tools.js
|
|
3971
|
-
import { z as
|
|
4789
|
+
import { z as z12 } from "zod";
|
|
3972
4790
|
var SESSION_TOOLS = [
|
|
3973
4791
|
{
|
|
3974
4792
|
name: IrisTool.SESSION,
|
|
3975
4793
|
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 }.",
|
|
3976
4794
|
inputSchema: {
|
|
3977
|
-
idleEndMs:
|
|
3978
|
-
sessionId:
|
|
4795
|
+
idleEndMs: z12.number().optional().describe("Idle window in milliseconds after which the presenter session auto-ends. Default: 300000 (5 min). Raise for slow apps."),
|
|
4796
|
+
sessionId: z12.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3979
4797
|
},
|
|
3980
4798
|
outputSchema: {
|
|
3981
|
-
applied:
|
|
3982
|
-
idleEndMs:
|
|
4799
|
+
applied: z12.boolean(),
|
|
4800
|
+
idleEndMs: z12.number().optional()
|
|
3983
4801
|
},
|
|
3984
4802
|
handler: async (deps, args) => {
|
|
3985
|
-
const session = deps.sessions.resolve(
|
|
3986
|
-
const idleEndMs =
|
|
4803
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4804
|
+
const idleEndMs = asNumber(args["idleEndMs"]);
|
|
3987
4805
|
if (idleEndMs !== void 0)
|
|
3988
4806
|
session.setIdleEndMs(idleEndMs);
|
|
3989
4807
|
const res = await session.command(IrisCommand.SESSION_CONFIG, idleEndMs !== void 0 ? { idleEndMs } : {});
|
|
@@ -3995,7 +4813,7 @@ var SESSION_TOOLS = [
|
|
|
3995
4813
|
];
|
|
3996
4814
|
|
|
3997
4815
|
// ../server/dist/flows/annotate-tools.js
|
|
3998
|
-
import { z as
|
|
4816
|
+
import { z as z13 } from "zod";
|
|
3999
4817
|
|
|
4000
4818
|
// ../server/dist/flows/annotate.js
|
|
4001
4819
|
function compileAnnotation(a, stepCount) {
|
|
@@ -4072,23 +4890,23 @@ var ANNOTATE_TOOLS = [
|
|
|
4072
4890
|
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.",
|
|
4073
4891
|
inputSchema: {
|
|
4074
4892
|
// `flow` selects the recording; `name`/`signal`/`testid`/`dataMatches` are the annotation fields.
|
|
4075
|
-
flow:
|
|
4076
|
-
kind:
|
|
4077
|
-
name:
|
|
4078
|
-
testid:
|
|
4079
|
-
signal:
|
|
4080
|
-
dataMatches:
|
|
4081
|
-
sessionId:
|
|
4082
|
-
annotation:
|
|
4893
|
+
flow: z13.string().optional().describe("Named recording to annotate. Defaults to 'default'."),
|
|
4894
|
+
kind: z13.string().describe("Annotation kind: assert-signal | assert-visible | mark-dynamic | success-state."),
|
|
4895
|
+
name: z13.string().optional().describe("Signal name for assert-signal annotations."),
|
|
4896
|
+
testid: z13.string().optional().describe("data-testid value for assert-visible / mark-dynamic / success-state annotations."),
|
|
4897
|
+
signal: z13.string().optional().describe("Signal name for success-state annotations."),
|
|
4898
|
+
dataMatches: z13.record(z13.unknown()).optional().describe("Key/value pairs the signal payload must match (assert-signal only)."),
|
|
4899
|
+
sessionId: z13.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open."),
|
|
4900
|
+
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.")
|
|
4083
4901
|
},
|
|
4084
4902
|
outputSchema: {
|
|
4085
|
-
ok:
|
|
4086
|
-
target:
|
|
4087
|
-
compiled:
|
|
4088
|
-
code:
|
|
4903
|
+
ok: z13.boolean(),
|
|
4904
|
+
target: z13.string().optional(),
|
|
4905
|
+
compiled: z13.string().optional(),
|
|
4906
|
+
code: z13.string().optional()
|
|
4089
4907
|
},
|
|
4090
4908
|
handler: (deps, args) => {
|
|
4091
|
-
const name =
|
|
4909
|
+
const name = asString(args["flow"]) ?? DEFAULT_RECORDING;
|
|
4092
4910
|
const parsed = AnnotationSchema.safeParse(args);
|
|
4093
4911
|
if (!parsed.success) {
|
|
4094
4912
|
return Promise.resolve({ ok: false, code: AnnotationErrorCode.UNKNOWN_KIND });
|
|
@@ -4117,19 +4935,19 @@ var ANNOTATE_TOOLS = [
|
|
|
4117
4935
|
];
|
|
4118
4936
|
|
|
4119
4937
|
// ../server/dist/session/live-control-tools.js
|
|
4120
|
-
import { z as
|
|
4938
|
+
import { z as z14 } from "zod";
|
|
4121
4939
|
var sessionIdShape5 = {
|
|
4122
|
-
sessionId:
|
|
4940
|
+
sessionId: z14.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
4123
4941
|
};
|
|
4124
4942
|
var LIVE_CONTROL_TOOLS = [
|
|
4125
4943
|
{
|
|
4126
4944
|
name: IrisTool.END_SESSION,
|
|
4127
4945
|
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.',
|
|
4128
|
-
inputSchema: { summary:
|
|
4129
|
-
outputSchema: { ended:
|
|
4946
|
+
inputSchema: { summary: z14.string().optional(), ...sessionIdShape5 },
|
|
4947
|
+
outputSchema: { ended: z14.boolean(), sessionId: z14.string() },
|
|
4130
4948
|
handler: (deps, args) => {
|
|
4131
|
-
const session = deps.sessions.resolve(
|
|
4132
|
-
session.setState(SessionState.ENDED,
|
|
4949
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4950
|
+
session.setState(SessionState.ENDED, asString(args["summary"]));
|
|
4133
4951
|
return Promise.resolve({ ended: true, sessionId: session.id });
|
|
4134
4952
|
}
|
|
4135
4953
|
},
|
|
@@ -4137,9 +4955,9 @@ var LIVE_CONTROL_TOOLS = [
|
|
|
4137
4955
|
name: IrisTool.RESUME,
|
|
4138
4956
|
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.',
|
|
4139
4957
|
inputSchema: { ...sessionIdShape5 },
|
|
4140
|
-
outputSchema: { ok:
|
|
4958
|
+
outputSchema: { ok: z14.boolean() },
|
|
4141
4959
|
handler: (deps, args) => {
|
|
4142
|
-
const session = deps.sessions.resolve(
|
|
4960
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4143
4961
|
session.setState(SessionState.ACTIVE);
|
|
4144
4962
|
return Promise.resolve({ ok: true });
|
|
4145
4963
|
}
|
|
@@ -4148,9 +4966,9 @@ var LIVE_CONTROL_TOOLS = [
|
|
|
4148
4966
|
name: IrisTool.MESSAGES,
|
|
4149
4967
|
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.",
|
|
4150
4968
|
inputSchema: { ...sessionIdShape5 },
|
|
4151
|
-
outputSchema: { messages:
|
|
4969
|
+
outputSchema: { messages: z14.array(z14.unknown()) },
|
|
4152
4970
|
handler: (deps, args) => {
|
|
4153
|
-
const session = deps.sessions.resolve(
|
|
4971
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4154
4972
|
return Promise.resolve({ messages: session.drainInbox() });
|
|
4155
4973
|
}
|
|
4156
4974
|
}
|
|
@@ -4176,7 +4994,7 @@ function withControl(session, result) {
|
|
|
4176
4994
|
}
|
|
4177
4995
|
|
|
4178
4996
|
// ../server/dist/update/update-tools.js
|
|
4179
|
-
import { z as
|
|
4997
|
+
import { z as z15 } from "zod";
|
|
4180
4998
|
|
|
4181
4999
|
// ../server/dist/update/update-checker.js
|
|
4182
5000
|
import * as fs from "fs";
|
|
@@ -4370,14 +5188,14 @@ var UPDATE_TOOLS = [
|
|
|
4370
5188
|
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.",
|
|
4371
5189
|
inputSchema: {},
|
|
4372
5190
|
outputSchema: {
|
|
4373
|
-
currentVersion:
|
|
4374
|
-
latestVersion:
|
|
4375
|
-
updateAvailable:
|
|
4376
|
-
executionKind:
|
|
4377
|
-
changelog:
|
|
4378
|
-
breakingChanges:
|
|
4379
|
-
rollbackAvailable:
|
|
4380
|
-
previousVersion:
|
|
5191
|
+
currentVersion: z15.string().describe("The Iris server version currently running."),
|
|
5192
|
+
latestVersion: z15.string().optional().describe("Latest published version on npm."),
|
|
5193
|
+
updateAvailable: z15.boolean().describe("True when a newer version is available to install."),
|
|
5194
|
+
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).'),
|
|
5195
|
+
changelog: z15.string().optional().describe("Release notes for the latest version."),
|
|
5196
|
+
breakingChanges: z15.array(z15.string()).optional().describe("Breaking changes in the latest version that may affect your scripts."),
|
|
5197
|
+
rollbackAvailable: z15.boolean().describe("True when a previous version is stored and can be restored."),
|
|
5198
|
+
previousVersion: z15.string().optional().describe("The version that would be restored on rollback.")
|
|
4381
5199
|
},
|
|
4382
5200
|
handler: async (_deps) => {
|
|
4383
5201
|
const manifest = await checkForUpdate(SERVER_VERSION);
|
|
@@ -4397,11 +5215,11 @@ var UPDATE_TOOLS = [
|
|
|
4397
5215
|
name: IrisTool.APPLY_UPDATE,
|
|
4398
5216
|
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.',
|
|
4399
5217
|
inputSchema: {
|
|
4400
|
-
confirm:
|
|
5218
|
+
confirm: z15.boolean().describe("Set to true to confirm the update should be applied. Required to prevent accidental upgrades.")
|
|
4401
5219
|
},
|
|
4402
5220
|
outputSchema: {
|
|
4403
|
-
ok:
|
|
4404
|
-
message:
|
|
5221
|
+
ok: z15.boolean(),
|
|
5222
|
+
message: z15.string().optional()
|
|
4405
5223
|
},
|
|
4406
5224
|
handler: async (_deps, args) => {
|
|
4407
5225
|
if (args["confirm"] !== true) {
|
|
@@ -4419,11 +5237,11 @@ var UPDATE_TOOLS = [
|
|
|
4419
5237
|
name: IrisTool.ROLLBACK,
|
|
4420
5238
|
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.",
|
|
4421
5239
|
inputSchema: {
|
|
4422
|
-
confirm:
|
|
5240
|
+
confirm: z15.boolean().describe("Set to true to confirm the rollback. Required to prevent accidental downgrades.")
|
|
4423
5241
|
},
|
|
4424
5242
|
outputSchema: {
|
|
4425
|
-
ok:
|
|
4426
|
-
message:
|
|
5243
|
+
ok: z15.boolean(),
|
|
5244
|
+
message: z15.string().optional()
|
|
4427
5245
|
},
|
|
4428
5246
|
handler: async (_deps, args) => {
|
|
4429
5247
|
if (args["confirm"] !== true) {
|
|
@@ -4445,7 +5263,7 @@ async function snapshotTree(deps, sessionId) {
|
|
|
4445
5263
|
return { lines: normalizeLines(snap.tree ?? ""), route: snap.status?.route ?? "" };
|
|
4446
5264
|
}
|
|
4447
5265
|
var sessionIdShape6 = {
|
|
4448
|
-
sessionId:
|
|
5266
|
+
sessionId: z16.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open \u2014 Iris resolves it automatically.")
|
|
4449
5267
|
};
|
|
4450
5268
|
async function commandOrThrow3(deps, sessionId, name, args) {
|
|
4451
5269
|
const session = deps.sessions.resolve(sessionId);
|
|
@@ -4455,11 +5273,11 @@ async function commandOrThrow3(deps, sessionId, name, args) {
|
|
|
4455
5273
|
return result.result;
|
|
4456
5274
|
}
|
|
4457
5275
|
function asBox2(value) {
|
|
4458
|
-
const b =
|
|
4459
|
-
const x =
|
|
4460
|
-
const y =
|
|
4461
|
-
const w =
|
|
4462
|
-
const h =
|
|
5276
|
+
const b = asRecord(asRecord(value)["box"]);
|
|
5277
|
+
const x = asNumber(b["x"]);
|
|
5278
|
+
const y = asNumber(b["y"]);
|
|
5279
|
+
const w = asNumber(b["width"]);
|
|
5280
|
+
const h = asNumber(b["height"]);
|
|
4463
5281
|
if (x === void 0 || y === void 0 || w === void 0 || h === void 0)
|
|
4464
5282
|
return void 0;
|
|
4465
5283
|
if (w <= 0 || h <= 0)
|
|
@@ -4475,29 +5293,51 @@ async function tryRealInput(deps, session, ref, action, args) {
|
|
|
4475
5293
|
return synthetic();
|
|
4476
5294
|
if (!isPointerAction(action))
|
|
4477
5295
|
return synthetic(InputModeReason.NOT_POINTER);
|
|
4478
|
-
const inner =
|
|
5296
|
+
const inner = asRecord(args["args"]);
|
|
4479
5297
|
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && inner["native"] !== true) {
|
|
4480
5298
|
return synthetic(InputModeReason.SYNTHETIC_CLICK_PREFERRED);
|
|
4481
5299
|
}
|
|
4482
5300
|
if (!await provider.isAvailableFor(session.url))
|
|
4483
5301
|
return synthetic(InputModeReason.PAGE_NOT_CORRELATED);
|
|
4484
|
-
const
|
|
5302
|
+
const inspected = await commandOrThrow3(deps, session.id, IrisCommand.INSPECT, { ref });
|
|
5303
|
+
const confirmed = inner[DANGEROUS_ACTION_CONFIRM_ARG] === true;
|
|
5304
|
+
const dangerousDescriptorText = (value2) => {
|
|
5305
|
+
const descriptor = asRecord(value2);
|
|
5306
|
+
return [
|
|
5307
|
+
asString(descriptor["name"]) ?? "",
|
|
5308
|
+
asString(descriptor["text"]) ?? "",
|
|
5309
|
+
asString(descriptor["value"]) ?? "",
|
|
5310
|
+
asString(descriptor["href"]) ?? "",
|
|
5311
|
+
asString(descriptor["formAction"]) ?? "",
|
|
5312
|
+
asString(descriptor["formText"]) ?? ""
|
|
5313
|
+
].join(" ");
|
|
5314
|
+
};
|
|
5315
|
+
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && !confirmed && isDangerousActionText(dangerousDescriptorText(inspected))) {
|
|
5316
|
+
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
5317
|
+
}
|
|
5318
|
+
const box = asBox2(inspected);
|
|
4485
5319
|
if (box === void 0)
|
|
4486
5320
|
return synthetic(InputModeReason.ELEMENT_NOT_LOCATABLE);
|
|
4487
5321
|
let toBox;
|
|
4488
5322
|
if (action === ActionType.DRAG) {
|
|
4489
|
-
const toRef =
|
|
5323
|
+
const toRef = asString(inner["toRef"]);
|
|
4490
5324
|
if (toRef === void 0)
|
|
4491
5325
|
return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
|
|
4492
|
-
|
|
5326
|
+
const targetInspected = await commandOrThrow3(deps, session.id, IrisCommand.INSPECT, {
|
|
5327
|
+
ref: toRef
|
|
5328
|
+
});
|
|
5329
|
+
if (!confirmed && isDangerousActionText(`${dangerousDescriptorText(inspected)} ${dangerousDescriptorText(targetInspected)}`)) {
|
|
5330
|
+
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
5331
|
+
}
|
|
5332
|
+
toBox = asBox2(targetInspected);
|
|
4493
5333
|
if (toBox === void 0)
|
|
4494
5334
|
return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
|
|
4495
5335
|
}
|
|
4496
5336
|
const performArgs = {};
|
|
4497
|
-
const value =
|
|
5337
|
+
const value = asString(inner["value"]);
|
|
4498
5338
|
if (value !== void 0)
|
|
4499
5339
|
performArgs.value = value;
|
|
4500
|
-
const text =
|
|
5340
|
+
const text = asString(inner["text"]);
|
|
4501
5341
|
if (text !== void 0)
|
|
4502
5342
|
performArgs.text = text;
|
|
4503
5343
|
if (toBox !== void 0)
|
|
@@ -4516,23 +5356,24 @@ async function tryRealInput(deps, session, ref, action, args) {
|
|
|
4516
5356
|
};
|
|
4517
5357
|
}
|
|
4518
5358
|
}
|
|
5359
|
+
var SNAPSHOT_CACHE = new SnapshotCache();
|
|
4519
5360
|
var TOOLS = [
|
|
4520
5361
|
{
|
|
4521
5362
|
name: IrisTool.SESSIONS,
|
|
4522
5363
|
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.",
|
|
4523
5364
|
inputSchema: {},
|
|
4524
5365
|
outputSchema: {
|
|
4525
|
-
sessions:
|
|
4526
|
-
sessionId:
|
|
4527
|
-
url:
|
|
4528
|
-
title:
|
|
4529
|
-
lastSeenMs:
|
|
4530
|
-
throttled:
|
|
4531
|
-
focused:
|
|
4532
|
-
hidden:
|
|
4533
|
-
realInputAvailable:
|
|
4534
|
-
stale:
|
|
4535
|
-
recommendation:
|
|
5366
|
+
sessions: z16.array(z16.object({
|
|
5367
|
+
sessionId: z16.string(),
|
|
5368
|
+
url: z16.string(),
|
|
5369
|
+
title: z16.string().optional(),
|
|
5370
|
+
lastSeenMs: z16.number(),
|
|
5371
|
+
throttled: z16.boolean(),
|
|
5372
|
+
focused: z16.boolean(),
|
|
5373
|
+
hidden: z16.boolean(),
|
|
5374
|
+
realInputAvailable: z16.boolean().optional(),
|
|
5375
|
+
stale: z16.boolean().optional(),
|
|
5376
|
+
recommendation: z16.string().optional()
|
|
4536
5377
|
})).describe("Connected browser sessions with health state.")
|
|
4537
5378
|
},
|
|
4538
5379
|
handler: async (deps) => {
|
|
@@ -4546,71 +5387,95 @@ var TOOLS = [
|
|
|
4546
5387
|
},
|
|
4547
5388
|
{
|
|
4548
5389
|
name: IrisTool.SNAPSHOT,
|
|
4549
|
-
description: "Semantic accessibility snapshot of the page or a subtree. mode: full|interactive|status. Use to see what is on screen right now.",
|
|
5390
|
+
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.",
|
|
4550
5391
|
inputSchema: {
|
|
4551
|
-
scope:
|
|
4552
|
-
mode:
|
|
5392
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the snapshot to a subtree. Omit to snapshot the whole page."),
|
|
5393
|
+
mode: z16.nativeEnum(SnapshotMode).optional().describe("full = all elements; interactive = only clickable/focusable elements; status = only route + title. Default: full."),
|
|
5394
|
+
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."),
|
|
4553
5395
|
...sessionIdShape6
|
|
4554
5396
|
},
|
|
4555
5397
|
outputSchema: {
|
|
4556
|
-
tree:
|
|
4557
|
-
status:
|
|
5398
|
+
tree: z16.string().optional().describe("Indented ARIA tree of every element on the page (or the scoped subtree)."),
|
|
5399
|
+
status: z16.object({ route: z16.string(), title: z16.string().optional() }).optional(),
|
|
5400
|
+
mode: z16.string().optional().describe("delta | unchanged when diff:true returned a change set."),
|
|
5401
|
+
delta: z16.object({
|
|
5402
|
+
added: z16.array(z16.string()),
|
|
5403
|
+
removed: z16.array(z16.string()),
|
|
5404
|
+
addedCount: z16.number(),
|
|
5405
|
+
removedCount: z16.number()
|
|
5406
|
+
}).optional().describe("Only present on a diff:true call that found changes."),
|
|
5407
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional().describe("Estimated size of this result \u2014 re-scope if large.")
|
|
4558
5408
|
},
|
|
4559
|
-
handler: (deps, args) =>
|
|
4560
|
-
|
|
4561
|
-
mode
|
|
4562
|
-
|
|
5409
|
+
handler: (deps, args) => {
|
|
5410
|
+
const sessionId = asString(args["sessionId"]);
|
|
5411
|
+
const mode = asString(args["mode"]) ?? SnapshotMode.FULL;
|
|
5412
|
+
return commandOrThrow3(deps, sessionId, IrisCommand.SNAPSHOT, {
|
|
5413
|
+
scope: args["scope"],
|
|
5414
|
+
mode
|
|
5415
|
+
}).then((raw) => withSizeCost(applySnapshotDelta(raw, {
|
|
5416
|
+
sessionId: sessionId ?? "default",
|
|
5417
|
+
scope: asString(args["scope"]) ?? "",
|
|
5418
|
+
mode,
|
|
5419
|
+
diff: args["diff"] === true
|
|
5420
|
+
}, SNAPSHOT_CACHE)));
|
|
5421
|
+
}
|
|
4563
5422
|
},
|
|
4564
5423
|
{
|
|
4565
5424
|
name: IrisTool.QUERY,
|
|
4566
|
-
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.",
|
|
5425
|
+
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.",
|
|
4567
5426
|
inputSchema: {
|
|
4568
|
-
by:
|
|
4569
|
-
value:
|
|
4570
|
-
name:
|
|
4571
|
-
scope:
|
|
5427
|
+
by: z16.string().describe("Query strategy: role | text | label | placeholder | testid | alt"),
|
|
5428
|
+
value: z16.string().describe("Query value for the selected strategy (e.g. by=role value=button, or by=testid value=submit-btn)."),
|
|
5429
|
+
name: z16.string().optional().describe("Accessible name filter \u2014 narrows results when `by` is role and the page has many elements of that role."),
|
|
5430
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the search to a subtree."),
|
|
5431
|
+
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."),
|
|
5432
|
+
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.'),
|
|
4572
5433
|
...sessionIdShape6
|
|
4573
5434
|
},
|
|
4574
5435
|
outputSchema: {
|
|
4575
|
-
elements:
|
|
4576
|
-
ref:
|
|
4577
|
-
role:
|
|
4578
|
-
name:
|
|
4579
|
-
value:
|
|
4580
|
-
states:
|
|
4581
|
-
visible:
|
|
4582
|
-
})),
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
5436
|
+
elements: z16.array(z16.object({
|
|
5437
|
+
ref: z16.string(),
|
|
5438
|
+
role: z16.string(),
|
|
5439
|
+
name: z16.string(),
|
|
5440
|
+
value: z16.string().optional(),
|
|
5441
|
+
states: z16.array(z16.string()),
|
|
5442
|
+
visible: z16.boolean()
|
|
5443
|
+
})).optional(),
|
|
5444
|
+
count: z16.number().optional().describe("Match count \u2014 present when count_only is set."),
|
|
5445
|
+
total: z16.number().optional().describe("Total matches before `limit` truncation \u2014 present only when truncated."),
|
|
5446
|
+
truncated: z16.boolean().optional().describe("True when `limit` dropped some matches."),
|
|
5447
|
+
hint: z16.object({
|
|
5448
|
+
route: z16.string(),
|
|
5449
|
+
presentTestids: z16.array(z16.string()),
|
|
5450
|
+
knownEmptyState: z16.boolean()
|
|
5451
|
+
}).optional().describe("Present only on zero matches \u2014 tells you what IS on the page so you can diagnose the miss."),
|
|
5452
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional().describe("Estimated size of this result \u2014 narrow with `name`/`scope`/`limit` if large.")
|
|
4588
5453
|
},
|
|
4589
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5454
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.QUERY, {
|
|
4590
5455
|
by: args["by"],
|
|
4591
5456
|
value: args["value"],
|
|
4592
5457
|
name: args["name"],
|
|
4593
5458
|
scope: args["scope"]
|
|
4594
|
-
})
|
|
5459
|
+
}).then((result) => withSizeCost(paginateQueryResult(result, asNumber(args["limit"]), args["count_only"] === true)))
|
|
4595
5460
|
},
|
|
4596
5461
|
{
|
|
4597
5462
|
name: IrisTool.INSPECT,
|
|
4598
5463
|
description: "Deep info on one element by ref: full a11y props, visibility, box, and (with @syrin/iris-react) component stack + source file.",
|
|
4599
5464
|
inputSchema: {
|
|
4600
|
-
ref:
|
|
5465
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
|
|
4601
5466
|
...sessionIdShape6
|
|
4602
5467
|
},
|
|
4603
5468
|
outputSchema: {
|
|
4604
|
-
ref:
|
|
4605
|
-
role:
|
|
4606
|
-
name:
|
|
4607
|
-
value:
|
|
4608
|
-
states:
|
|
4609
|
-
visible:
|
|
4610
|
-
box:
|
|
4611
|
-
component:
|
|
5469
|
+
ref: z16.string(),
|
|
5470
|
+
role: z16.string(),
|
|
5471
|
+
name: z16.string(),
|
|
5472
|
+
value: z16.string().optional(),
|
|
5473
|
+
states: z16.array(z16.string()),
|
|
5474
|
+
visible: z16.boolean(),
|
|
5475
|
+
box: z16.object({ x: z16.number(), y: z16.number(), width: z16.number(), height: z16.number() }).optional(),
|
|
5476
|
+
component: z16.object({ name: z16.string().optional(), sourceFile: z16.string().optional() }).optional()
|
|
4612
5477
|
},
|
|
4613
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5478
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.INSPECT, {
|
|
4614
5479
|
ref: args["ref"]
|
|
4615
5480
|
})
|
|
4616
5481
|
},
|
|
@@ -4618,30 +5483,30 @@ var TOOLS = [
|
|
|
4618
5483
|
name: IrisTool.ACT,
|
|
4619
5484
|
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.',
|
|
4620
5485
|
inputSchema: {
|
|
4621
|
-
ref:
|
|
4622
|
-
action:
|
|
4623
|
-
args:
|
|
4624
|
-
refuseWhenThrottled:
|
|
5486
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
|
|
5487
|
+
action: z16.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
|
|
5488
|
+
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."),
|
|
5489
|
+
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)."),
|
|
4625
5490
|
...sessionIdShape6
|
|
4626
5491
|
},
|
|
4627
5492
|
outputSchema: {
|
|
4628
|
-
since:
|
|
4629
|
-
dispatched:
|
|
4630
|
-
settled:
|
|
4631
|
-
inputMode:
|
|
4632
|
-
result:
|
|
4633
|
-
session:
|
|
5493
|
+
since: z16.number().describe("Cursor \u2014 pass to iris_observe/iris_wait_for/iris_assert to scope reaction queries to this act."),
|
|
5494
|
+
dispatched: z16.boolean(),
|
|
5495
|
+
settled: z16.boolean().nullable(),
|
|
5496
|
+
inputMode: z16.string(),
|
|
5497
|
+
result: z16.unknown().optional(),
|
|
5498
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4634
5499
|
},
|
|
4635
5500
|
handler: async (deps, args) => {
|
|
4636
|
-
const session = deps.sessions.resolve(
|
|
5501
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4637
5502
|
const paused = pausedShortCircuit(session);
|
|
4638
5503
|
if (paused !== void 0)
|
|
4639
5504
|
return paused;
|
|
4640
5505
|
refuseIfThrottled(session, args["refuseWhenThrottled"]);
|
|
4641
5506
|
const since = session.elapsed();
|
|
4642
5507
|
session.markActCursor(since);
|
|
4643
|
-
const ref =
|
|
4644
|
-
const action =
|
|
5508
|
+
const ref = asString(args["ref"]) ?? "";
|
|
5509
|
+
const action = asString(args["action"]) ?? "";
|
|
4645
5510
|
const real = await tryRealInput(deps, session, ref, action, args);
|
|
4646
5511
|
if (real.result !== void 0) {
|
|
4647
5512
|
if (deps.recordings.active().length > 0) {
|
|
@@ -4667,7 +5532,7 @@ var TOOLS = [
|
|
|
4667
5532
|
if (deps.recordings.active().length > 0) {
|
|
4668
5533
|
deps.recordings.capture(compileActStep(args, result.result));
|
|
4669
5534
|
}
|
|
4670
|
-
const r =
|
|
5535
|
+
const r = asRecord(result.result);
|
|
4671
5536
|
return withControl(session, {
|
|
4672
5537
|
since,
|
|
4673
5538
|
inputMode: InputMode.SYNTHETIC,
|
|
@@ -4686,17 +5551,17 @@ var TOOLS = [
|
|
|
4686
5551
|
name: IrisTool.ACT_SEQUENCE,
|
|
4687
5552
|
description: "Run multiple actions in order (fill -> fill -> submit) in one round-trip. Returns per-step effects[] (see iris_act).",
|
|
4688
5553
|
inputSchema: {
|
|
4689
|
-
steps:
|
|
5554
|
+
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."),
|
|
4690
5555
|
...sessionIdShape6
|
|
4691
5556
|
},
|
|
4692
5557
|
outputSchema: {
|
|
4693
|
-
since:
|
|
4694
|
-
dispatched:
|
|
4695
|
-
result:
|
|
4696
|
-
session:
|
|
5558
|
+
since: z16.number(),
|
|
5559
|
+
dispatched: z16.boolean(),
|
|
5560
|
+
result: z16.unknown().optional(),
|
|
5561
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4697
5562
|
},
|
|
4698
5563
|
handler: async (deps, args) => {
|
|
4699
|
-
const session = deps.sessions.resolve(
|
|
5564
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4700
5565
|
const paused = pausedShortCircuit(session);
|
|
4701
5566
|
if (paused !== void 0)
|
|
4702
5567
|
return paused;
|
|
@@ -4708,7 +5573,7 @@ var TOOLS = [
|
|
|
4708
5573
|
if (deps.recordings.active().length > 0) {
|
|
4709
5574
|
deps.recordings.capture(compileSequenceStep(args, result.result));
|
|
4710
5575
|
}
|
|
4711
|
-
const r =
|
|
5576
|
+
const r = asRecord(result.result);
|
|
4712
5577
|
return withControl(session, {
|
|
4713
5578
|
since,
|
|
4714
5579
|
dispatched: r["count"] !== void 0,
|
|
@@ -4719,34 +5584,34 @@ var TOOLS = [
|
|
|
4719
5584
|
},
|
|
4720
5585
|
{
|
|
4721
5586
|
name: IrisTool.ACT_AND_WAIT,
|
|
4722
|
-
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.",
|
|
5587
|
+
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.",
|
|
4723
5588
|
inputSchema: {
|
|
4724
|
-
ref:
|
|
4725
|
-
action:
|
|
4726
|
-
args:
|
|
4727
|
-
until: PredicateSchema.describe(
|
|
4728
|
-
timeout_ms:
|
|
4729
|
-
refuseWhenThrottled:
|
|
5589
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query."),
|
|
5590
|
+
action: z16.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
|
|
5591
|
+
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."),
|
|
5592
|
+
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" }] }.'),
|
|
5593
|
+
timeout_ms: z16.number().optional().describe("Maximum wait time in milliseconds. 0 = evaluate once without waiting. Default: 4000."),
|
|
5594
|
+
refuseWhenThrottled: z16.boolean().optional().describe("Throw if the tab is throttled. Default: false."),
|
|
4730
5595
|
...sessionIdShape6
|
|
4731
5596
|
},
|
|
4732
5597
|
outputSchema: {
|
|
4733
|
-
effect:
|
|
4734
|
-
verdict:
|
|
4735
|
-
pass:
|
|
4736
|
-
evidence:
|
|
4737
|
-
failureReason:
|
|
5598
|
+
effect: z16.unknown().describe("The iris_act result (dispatched, settled, inputMode, etc.)."),
|
|
5599
|
+
verdict: z16.object({
|
|
5600
|
+
pass: z16.boolean(),
|
|
5601
|
+
evidence: z16.unknown().optional(),
|
|
5602
|
+
failureReason: z16.string().optional()
|
|
4738
5603
|
}),
|
|
4739
|
-
trace:
|
|
4740
|
-
session:
|
|
5604
|
+
trace: z16.unknown().describe("Reaction report (same shape as iris_observe summary)."),
|
|
5605
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4741
5606
|
},
|
|
4742
5607
|
handler: async (deps, args) => {
|
|
4743
|
-
const session = deps.sessions.resolve(
|
|
5608
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4744
5609
|
const paused = pausedShortCircuit(session);
|
|
4745
5610
|
if (paused !== void 0)
|
|
4746
5611
|
return paused;
|
|
4747
5612
|
refuseIfThrottled(session, args["refuseWhenThrottled"]);
|
|
4748
|
-
const until = PredicateSchema.parse(args["until"]);
|
|
4749
|
-
const timeout =
|
|
5613
|
+
const until = args["until"] !== void 0 ? PredicateSchema.parse(args["until"]) : { kind: "settled" };
|
|
5614
|
+
const timeout = asNumber(args["timeout_ms"]) ?? 4e3;
|
|
4750
5615
|
const since = session.elapsed();
|
|
4751
5616
|
session.markActCursor(since);
|
|
4752
5617
|
const actResult = await session.command(IrisCommand.ACT, {
|
|
@@ -4770,44 +5635,45 @@ var TOOLS = [
|
|
|
4770
5635
|
name: IrisTool.OBSERVE,
|
|
4771
5636
|
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.",
|
|
4772
5637
|
inputSchema: {
|
|
4773
|
-
window_ms:
|
|
4774
|
-
since:
|
|
4775
|
-
filters:
|
|
4776
|
-
max_events:
|
|
5638
|
+
window_ms: z16.number().optional().describe("Time window to look back. Default: 2000ms. Ignored when `since` is provided."),
|
|
5639
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act or iris_observe call. Scopes the event window to exactly that span."),
|
|
5640
|
+
filters: z16.array(z16.string()).optional().describe("Event type allowlist: dom | net | route | console | animation | signal. Omit to return all types."),
|
|
5641
|
+
max_events: z16.number().optional().describe("Cap the timeline to the most recent N events. Older events are counted in cost.droppedOldest."),
|
|
4777
5642
|
...sessionIdShape6
|
|
4778
5643
|
},
|
|
4779
5644
|
outputSchema: {
|
|
4780
|
-
events:
|
|
4781
|
-
summary:
|
|
4782
|
-
total:
|
|
4783
|
-
network:
|
|
4784
|
-
domAdded:
|
|
4785
|
-
domRemoved:
|
|
4786
|
-
domChanged:
|
|
4787
|
-
routeChanges:
|
|
4788
|
-
consoleErrors:
|
|
4789
|
-
animations:
|
|
4790
|
-
signals:
|
|
5645
|
+
events: z16.array(z16.unknown()),
|
|
5646
|
+
summary: z16.object({
|
|
5647
|
+
total: z16.number(),
|
|
5648
|
+
network: z16.number(),
|
|
5649
|
+
domAdded: z16.number(),
|
|
5650
|
+
domRemoved: z16.number(),
|
|
5651
|
+
domChanged: z16.number(),
|
|
5652
|
+
routeChanges: z16.number(),
|
|
5653
|
+
consoleErrors: z16.number(),
|
|
5654
|
+
animations: z16.number(),
|
|
5655
|
+
signals: z16.number()
|
|
4791
5656
|
}),
|
|
4792
|
-
cost:
|
|
4793
|
-
events:
|
|
4794
|
-
bytes:
|
|
4795
|
-
droppedOldest:
|
|
5657
|
+
cost: z16.object({
|
|
5658
|
+
events: z16.number(),
|
|
5659
|
+
bytes: z16.number(),
|
|
5660
|
+
droppedOldest: z16.number().optional(),
|
|
5661
|
+
recommendation: z16.string().optional().describe("Present when the timeline is large \u2014 scope your next call (filters/max_events).")
|
|
4796
5662
|
}),
|
|
4797
|
-
session:
|
|
5663
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4798
5664
|
},
|
|
4799
5665
|
handler: (deps, args) => {
|
|
4800
|
-
const session = deps.sessions.resolve(
|
|
4801
|
-
const since =
|
|
4802
|
-
const windowMs =
|
|
5666
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5667
|
+
const since = asNumber(args["since"]);
|
|
5668
|
+
const windowMs = asNumber(args["window_ms"]) ?? 2e3;
|
|
4803
5669
|
const events = since !== void 0 ? session.eventsSince(since) : session.eventsInWindow(windowMs);
|
|
4804
5670
|
const filters = Array.isArray(args["filters"]) ? args["filters"] : void 0;
|
|
4805
5671
|
const filtered = filters === void 0 ? events : events.filter((e) => filters.includes(e.type));
|
|
4806
|
-
const { events: budgeted, droppedOldest } = applyEventBudget(filtered,
|
|
4807
|
-
const
|
|
5672
|
+
const { events: budgeted, droppedOldest } = applyEventBudget(filtered, asNumber(args["max_events"]));
|
|
5673
|
+
const report2 = buildReactionReport(budgeted, windowMs);
|
|
4808
5674
|
return Promise.resolve(withControl(session, {
|
|
4809
|
-
...
|
|
4810
|
-
cost: costHint(
|
|
5675
|
+
...report2,
|
|
5676
|
+
cost: costHint(report2, budgeted.length, droppedOldest),
|
|
4811
5677
|
...healthEnvelope(session)
|
|
4812
5678
|
}));
|
|
4813
5679
|
}
|
|
@@ -4816,99 +5682,113 @@ var TOOLS = [
|
|
|
4816
5682
|
name: IrisTool.WAIT_FOR,
|
|
4817
5683
|
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.",
|
|
4818
5684
|
inputSchema: {
|
|
4819
|
-
predicate: PredicateSchema.describe(
|
|
4820
|
-
timeout_ms:
|
|
4821
|
-
since:
|
|
5685
|
+
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.'),
|
|
5686
|
+
timeout_ms: z16.number().optional().describe("Maximum wait in milliseconds. Default: 4000."),
|
|
5687
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the wait to events after that act."),
|
|
4822
5688
|
...sessionIdShape6
|
|
4823
5689
|
},
|
|
4824
5690
|
outputSchema: {
|
|
4825
|
-
pass:
|
|
4826
|
-
evidence:
|
|
4827
|
-
failureReason:
|
|
4828
|
-
session:
|
|
5691
|
+
pass: z16.boolean(),
|
|
5692
|
+
evidence: z16.unknown().optional(),
|
|
5693
|
+
failureReason: z16.string().optional(),
|
|
5694
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4829
5695
|
},
|
|
4830
5696
|
handler: async (deps, args) => {
|
|
4831
|
-
const session = deps.sessions.resolve(
|
|
5697
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4832
5698
|
const predicate = PredicateSchema.parse(args["predicate"]);
|
|
4833
|
-
const since =
|
|
4834
|
-
const verdict = await waitForPredicate(session, predicate,
|
|
5699
|
+
const since = asNumber(args["since"]) ?? session.lastActCursor() ?? 0;
|
|
5700
|
+
const verdict = await waitForPredicate(session, predicate, asNumber(args["timeout_ms"]) ?? 4e3, since);
|
|
4835
5701
|
return withControl(session, { ...verdict, ...healthEnvelope(session) });
|
|
4836
5702
|
}
|
|
4837
5703
|
},
|
|
4838
5704
|
{
|
|
4839
5705
|
name: IrisTool.ASSERT,
|
|
4840
|
-
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.",
|
|
5706
|
+
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.",
|
|
4841
5707
|
inputSchema: {
|
|
4842
5708
|
predicate: PredicateSchema.describe("Predicate to evaluate: { signal }, { net }, { element } or a combination."),
|
|
4843
|
-
timeout_ms:
|
|
4844
|
-
since:
|
|
5709
|
+
timeout_ms: z16.number().optional().describe("If > 0, wait up to this many milliseconds before failing. Default: 0 (evaluate once)."),
|
|
5710
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the assertion to events after that act."),
|
|
4845
5711
|
...sessionIdShape6
|
|
4846
5712
|
},
|
|
4847
5713
|
outputSchema: {
|
|
4848
|
-
pass:
|
|
4849
|
-
evidence:
|
|
4850
|
-
failureReason:
|
|
4851
|
-
|
|
5714
|
+
pass: z16.boolean(),
|
|
5715
|
+
evidence: z16.unknown().optional(),
|
|
5716
|
+
failureReason: z16.string().optional(),
|
|
5717
|
+
advice: z16.string().optional().describe("Present on a PASSING presence-only assertion \u2014 nudges toward a consequence."),
|
|
5718
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4852
5719
|
},
|
|
4853
5720
|
handler: async (deps, args) => {
|
|
4854
|
-
const session = deps.sessions.resolve(
|
|
5721
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4855
5722
|
const predicate = PredicateSchema.parse(args["predicate"]);
|
|
4856
|
-
const timeout =
|
|
4857
|
-
const since =
|
|
5723
|
+
const timeout = asNumber(args["timeout_ms"]) ?? 0;
|
|
5724
|
+
const since = asNumber(args["since"]) ?? session.lastActCursor() ?? 0;
|
|
4858
5725
|
const verdict = timeout > 0 ? await waitForPredicate(session, predicate, timeout, since) : await evaluatePredicate(session, predicate, since);
|
|
4859
|
-
|
|
5726
|
+
const advice = verdict.pass && isPresenceOnlyAssertion(predicate) ? { advice: PRESENCE_ONLY_ADVICE } : {};
|
|
5727
|
+
return withControl(session, { ...verdict, ...advice, ...healthEnvelope(session) });
|
|
4860
5728
|
}
|
|
4861
5729
|
},
|
|
4862
5730
|
{
|
|
4863
5731
|
name: IrisTool.NETWORK,
|
|
4864
5732
|
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.',
|
|
4865
5733
|
inputSchema: {
|
|
4866
|
-
since:
|
|
4867
|
-
method:
|
|
4868
|
-
urlContains:
|
|
4869
|
-
status:
|
|
5734
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to requests fired after that act."),
|
|
5735
|
+
method: z16.string().optional().describe("HTTP method filter: GET | POST | PUT | DELETE | PATCH etc."),
|
|
5736
|
+
urlContains: z16.string().optional().describe("Substring that the request URL must contain."),
|
|
5737
|
+
status: z16.number().optional().describe("HTTP status code filter (e.g. 200, 404, 500)."),
|
|
5738
|
+
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."),
|
|
4870
5739
|
...sessionIdShape6
|
|
4871
5740
|
},
|
|
4872
5741
|
outputSchema: {
|
|
4873
|
-
calls:
|
|
4874
|
-
|
|
5742
|
+
calls: z16.array(z16.unknown()),
|
|
5743
|
+
total: z16.number().optional().describe("Total matches before `limit` \u2014 present only when capped."),
|
|
5744
|
+
droppedOldest: z16.number().optional().describe("How many older matches `limit` dropped."),
|
|
5745
|
+
hint: z16.object({ totalInWindow: z16.number(), present: z16.array(z16.string()) }).optional(),
|
|
5746
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional()
|
|
4875
5747
|
},
|
|
4876
5748
|
handler: (deps, args) => {
|
|
4877
|
-
const session = deps.sessions.resolve(
|
|
4878
|
-
const since =
|
|
4879
|
-
const method =
|
|
4880
|
-
const urlContains =
|
|
4881
|
-
const status =
|
|
5749
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5750
|
+
const since = asNumber(args["since"]) ?? 0;
|
|
5751
|
+
const method = asString(args["method"]);
|
|
5752
|
+
const urlContains = asString(args["urlContains"]);
|
|
5753
|
+
const status = asNumber(args["status"]);
|
|
5754
|
+
const limit = asNumber(args["limit"]);
|
|
4882
5755
|
const allNet = session.eventsSince(since).filter((e) => e.type === EventType.NET_REQUEST);
|
|
4883
|
-
const
|
|
4884
|
-
if (
|
|
4885
|
-
return Promise.resolve({ calls, hint: netEmptyHint(allNet) });
|
|
5756
|
+
const matched = allNet.filter((e) => matchNet(e, method, urlContains, status));
|
|
5757
|
+
if (matched.length === 0 && allNet.length > 0) {
|
|
5758
|
+
return Promise.resolve(withSizeCost({ calls: matched, hint: netEmptyHint(allNet) }));
|
|
4886
5759
|
}
|
|
4887
|
-
|
|
5760
|
+
const { events: calls, droppedOldest } = applyEventBudget(matched, limit);
|
|
5761
|
+
return Promise.resolve(withSizeCost(droppedOldest > 0 ? { calls, total: matched.length, droppedOldest } : { calls }));
|
|
4888
5762
|
}
|
|
4889
5763
|
},
|
|
4890
5764
|
{
|
|
4891
5765
|
name: IrisTool.CONSOLE,
|
|
4892
5766
|
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.',
|
|
4893
5767
|
inputSchema: {
|
|
4894
|
-
level:
|
|
4895
|
-
since:
|
|
5768
|
+
level: z16.string().optional().describe("Log level filter: error | warn | info | log. Omit to return all levels."),
|
|
5769
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to log entries after that act."),
|
|
5770
|
+
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."),
|
|
4896
5771
|
...sessionIdShape6
|
|
4897
5772
|
},
|
|
4898
5773
|
outputSchema: {
|
|
4899
|
-
logs:
|
|
4900
|
-
|
|
5774
|
+
logs: z16.array(z16.unknown()),
|
|
5775
|
+
total: z16.number().optional().describe("Total matches before `limit` \u2014 present only when capped."),
|
|
5776
|
+
droppedOldest: z16.number().optional().describe("How many older matches `limit` dropped."),
|
|
5777
|
+
hint: z16.object({ totalInWindow: z16.number(), byLevel: z16.record(z16.number()) }).optional(),
|
|
5778
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional()
|
|
4901
5779
|
},
|
|
4902
5780
|
handler: (deps, args) => {
|
|
4903
|
-
const session = deps.sessions.resolve(
|
|
4904
|
-
const since =
|
|
4905
|
-
const level =
|
|
5781
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5782
|
+
const since = asNumber(args["since"]) ?? 0;
|
|
5783
|
+
const level = asString(args["level"]);
|
|
5784
|
+
const limit = asNumber(args["limit"]);
|
|
4906
5785
|
const allConsole = session.eventsSince(since).filter(isConsoleEvent);
|
|
4907
|
-
const
|
|
4908
|
-
if (
|
|
4909
|
-
return Promise.resolve({ logs, hint: consoleEmptyHint(allConsole) });
|
|
5786
|
+
const matched = allConsole.filter((e) => matchConsole(e, level));
|
|
5787
|
+
if (matched.length === 0 && allConsole.length > 0) {
|
|
5788
|
+
return Promise.resolve(withSizeCost({ logs: matched, hint: consoleEmptyHint(allConsole) }));
|
|
4910
5789
|
}
|
|
4911
|
-
|
|
5790
|
+
const { events: logs, droppedOldest } = applyEventBudget(matched, limit);
|
|
5791
|
+
return Promise.resolve(withSizeCost(droppedOldest > 0 ? { logs, total: matched.length, droppedOldest } : { logs }));
|
|
4912
5792
|
}
|
|
4913
5793
|
},
|
|
4914
5794
|
{
|
|
@@ -4916,24 +5796,24 @@ var TOOLS = [
|
|
|
4916
5796
|
description: "Currently running + recently completed animations with targets/timing.",
|
|
4917
5797
|
inputSchema: { ...sessionIdShape6 },
|
|
4918
5798
|
outputSchema: {
|
|
4919
|
-
animations:
|
|
5799
|
+
animations: z16.array(z16.unknown())
|
|
4920
5800
|
},
|
|
4921
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5801
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.ANIMATIONS, {})
|
|
4922
5802
|
},
|
|
4923
5803
|
{
|
|
4924
5804
|
name: IrisTool.BASELINE_SAVE,
|
|
4925
5805
|
description: "Snapshot the current semantic state under a name, to diff against later (regression detection).",
|
|
4926
5806
|
inputSchema: {
|
|
4927
|
-
name:
|
|
5807
|
+
name: z16.string().describe('Label for this baseline snapshot (e.g. "dashboard-initial"). Use the same name in iris_diff to compare.'),
|
|
4928
5808
|
...sessionIdShape6
|
|
4929
5809
|
},
|
|
4930
5810
|
outputSchema: {
|
|
4931
|
-
baseline:
|
|
4932
|
-
lineCount:
|
|
5811
|
+
baseline: z16.string().describe("Saved baseline name \u2014 pass to iris_diff to compare."),
|
|
5812
|
+
lineCount: z16.number()
|
|
4933
5813
|
},
|
|
4934
5814
|
handler: async (deps, args) => {
|
|
4935
|
-
const name =
|
|
4936
|
-
const { lines, route } = await snapshotTree(deps,
|
|
5815
|
+
const name = asString(args["name"]) ?? "default";
|
|
5816
|
+
const { lines, route } = await snapshotTree(deps, asString(args["sessionId"]));
|
|
4937
5817
|
deps.baselines.save({ name, lines, route });
|
|
4938
5818
|
return { baseline: name, lineCount: lines.length };
|
|
4939
5819
|
}
|
|
@@ -4943,7 +5823,7 @@ var TOOLS = [
|
|
|
4943
5823
|
description: "List saved baseline names.",
|
|
4944
5824
|
inputSchema: {},
|
|
4945
5825
|
outputSchema: {
|
|
4946
|
-
baselines:
|
|
5826
|
+
baselines: z16.array(z16.string())
|
|
4947
5827
|
},
|
|
4948
5828
|
handler: (deps) => Promise.resolve({ baselines: deps.baselines.list() })
|
|
4949
5829
|
},
|
|
@@ -4951,23 +5831,23 @@ var TOOLS = [
|
|
|
4951
5831
|
name: IrisTool.DIFF,
|
|
4952
5832
|
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?".',
|
|
4953
5833
|
inputSchema: {
|
|
4954
|
-
baseline:
|
|
5834
|
+
baseline: z16.string().describe("Baseline name to compare against. Call iris_baseline_list to get available names; names are created by iris_baseline_save."),
|
|
4955
5835
|
...sessionIdShape6
|
|
4956
5836
|
},
|
|
4957
5837
|
outputSchema: {
|
|
4958
|
-
baseline:
|
|
4959
|
-
removed:
|
|
4960
|
-
added:
|
|
4961
|
-
consoleErrors:
|
|
4962
|
-
routeChanged:
|
|
5838
|
+
baseline: z16.string(),
|
|
5839
|
+
removed: z16.array(z16.string()),
|
|
5840
|
+
added: z16.array(z16.string()),
|
|
5841
|
+
consoleErrors: z16.number(),
|
|
5842
|
+
routeChanged: z16.boolean()
|
|
4963
5843
|
},
|
|
4964
5844
|
handler: async (deps, args) => {
|
|
4965
|
-
const name =
|
|
5845
|
+
const name = asString(args["baseline"]) ?? "default";
|
|
4966
5846
|
const base = deps.baselines.get(name);
|
|
4967
5847
|
if (base === void 0)
|
|
4968
5848
|
throw new Error(`no baseline named '${name}'`);
|
|
4969
|
-
const session = deps.sessions.resolve(
|
|
4970
|
-
const { lines, route } = await snapshotTree(deps,
|
|
5849
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5850
|
+
const { lines, route } = await snapshotTree(deps, asString(args["sessionId"]));
|
|
4971
5851
|
const { removed, added } = diffLines(base.lines, lines);
|
|
4972
5852
|
const consoleErrors = session.eventsSince(0).filter((e) => e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT).length;
|
|
4973
5853
|
return { baseline: name, removed, added, consoleErrors, routeChanged: base.route !== route };
|
|
@@ -4977,16 +5857,16 @@ var TOOLS = [
|
|
|
4977
5857
|
name: IrisTool.RECORD_START,
|
|
4978
5858
|
description: "Start recording the event timeline under a name (for replay / a flow report).",
|
|
4979
5859
|
inputSchema: {
|
|
4980
|
-
recordingName:
|
|
5860
|
+
recordingName: z16.string().describe("Identifier for this recording. Pass the same name to iris_record_stop and iris_replay."),
|
|
4981
5861
|
...sessionIdShape6
|
|
4982
5862
|
},
|
|
4983
5863
|
outputSchema: {
|
|
4984
|
-
recordingName:
|
|
4985
|
-
since:
|
|
5864
|
+
recordingName: z16.string(),
|
|
5865
|
+
since: z16.number()
|
|
4986
5866
|
},
|
|
4987
5867
|
handler: (deps, args) => {
|
|
4988
|
-
const session = deps.sessions.resolve(
|
|
4989
|
-
const name =
|
|
5868
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5869
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
4990
5870
|
const cursor = session.elapsed();
|
|
4991
5871
|
deps.recordings.start(name, cursor);
|
|
4992
5872
|
return Promise.resolve({ recordingName: name, since: cursor });
|
|
@@ -4996,17 +5876,17 @@ var TOOLS = [
|
|
|
4996
5876
|
name: IrisTool.RECORD_STOP,
|
|
4997
5877
|
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.",
|
|
4998
5878
|
inputSchema: {
|
|
4999
|
-
recordingName:
|
|
5879
|
+
recordingName: z16.string().describe("Identifier of an active recording started with iris_record_start."),
|
|
5000
5880
|
...sessionIdShape6
|
|
5001
5881
|
},
|
|
5002
5882
|
outputSchema: {
|
|
5003
|
-
recordingName:
|
|
5004
|
-
program:
|
|
5005
|
-
warning:
|
|
5883
|
+
recordingName: z16.string(),
|
|
5884
|
+
program: z16.unknown(),
|
|
5885
|
+
warning: z16.string().optional()
|
|
5006
5886
|
},
|
|
5007
5887
|
handler: (deps, args) => {
|
|
5008
|
-
const session = deps.sessions.resolve(
|
|
5009
|
-
const name =
|
|
5888
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5889
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
5010
5890
|
const rec = deps.recordings.stop(name);
|
|
5011
5891
|
if (rec === void 0)
|
|
5012
5892
|
throw new Error(`no active recording named '${name}'`);
|
|
@@ -5018,43 +5898,44 @@ var TOOLS = [
|
|
|
5018
5898
|
};
|
|
5019
5899
|
deps.recordings.saveCompiled(program);
|
|
5020
5900
|
const unstable = rec.steps.filter((s) => !s.stable).length;
|
|
5021
|
-
const
|
|
5901
|
+
const report2 = buildReactionReport(events, session.elapsed() - rec.cursor);
|
|
5022
5902
|
return Promise.resolve({
|
|
5023
5903
|
recordingName: name,
|
|
5024
5904
|
program,
|
|
5025
5905
|
...unstable > 0 ? {
|
|
5026
5906
|
warning: `${String(unstable)} step(s) not bound to a testid; replay may be brittle (in-session only)`
|
|
5027
5907
|
} : {},
|
|
5028
|
-
...
|
|
5029
|
-
cost: costHint(
|
|
5908
|
+
...report2,
|
|
5909
|
+
cost: costHint(report2, events.length)
|
|
5030
5910
|
});
|
|
5031
5911
|
}
|
|
5032
5912
|
},
|
|
5033
5913
|
{
|
|
5034
5914
|
name: IrisTool.REPLAY,
|
|
5035
|
-
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?}] }.",
|
|
5915
|
+
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?}] }.",
|
|
5036
5916
|
inputSchema: {
|
|
5037
|
-
recordingName:
|
|
5917
|
+
recordingName: z16.string().describe("Name of a compiled recording (from iris_record_stop) to re-execute."),
|
|
5918
|
+
confirmDangerous: z16.boolean().optional().describe("Set true to allow destructive controls during this replay only."),
|
|
5038
5919
|
...sessionIdShape6
|
|
5039
5920
|
},
|
|
5040
5921
|
outputSchema: {
|
|
5041
|
-
recordingName:
|
|
5042
|
-
ok:
|
|
5043
|
-
steps:
|
|
5044
|
-
tool:
|
|
5045
|
-
ok:
|
|
5046
|
-
error:
|
|
5047
|
-
note:
|
|
5922
|
+
recordingName: z16.string(),
|
|
5923
|
+
ok: z16.boolean(),
|
|
5924
|
+
steps: z16.array(z16.object({
|
|
5925
|
+
tool: z16.string(),
|
|
5926
|
+
ok: z16.boolean(),
|
|
5927
|
+
error: z16.string().optional(),
|
|
5928
|
+
note: z16.string().optional()
|
|
5048
5929
|
}))
|
|
5049
5930
|
},
|
|
5050
5931
|
handler: async (deps, args) => {
|
|
5051
|
-
const name =
|
|
5932
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
5052
5933
|
const program = deps.recordings.getCompiled(name);
|
|
5053
5934
|
if (program === void 0)
|
|
5054
5935
|
throw new Error(`no compiled recording named '${name}'`);
|
|
5055
|
-
const session = deps.sessions.resolve(
|
|
5936
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5056
5937
|
const since = session.elapsed();
|
|
5057
|
-
const steps = await replayProgram(session, program);
|
|
5938
|
+
const steps = await replayProgram(session, program, args["confirmDangerous"] === true);
|
|
5058
5939
|
return { recordingName: name, since, steps, ok: steps.every((s) => s.ok) };
|
|
5059
5940
|
}
|
|
5060
5941
|
},
|
|
@@ -5062,13 +5943,13 @@ var TOOLS = [
|
|
|
5062
5943
|
name: IrisTool.NARRATE,
|
|
5063
5944
|
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.",
|
|
5064
5945
|
inputSchema: {
|
|
5065
|
-
text:
|
|
5066
|
-
level:
|
|
5946
|
+
text: z16.string().describe("Short sentence describing your next action, shown on the presenter HUD for the developer watching."),
|
|
5947
|
+
level: z16.string().optional().describe("Display severity: info | warn | error. Default: info."),
|
|
5067
5948
|
...sessionIdShape6
|
|
5068
5949
|
},
|
|
5069
|
-
outputSchema: { ok:
|
|
5950
|
+
outputSchema: { ok: z16.boolean() },
|
|
5070
5951
|
handler: async (deps, args) => {
|
|
5071
|
-
const result = await commandOrThrow3(deps,
|
|
5952
|
+
const result = await commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.NARRATE, {
|
|
5072
5953
|
text: args["text"],
|
|
5073
5954
|
level: args["level"]
|
|
5074
5955
|
});
|
|
@@ -5079,16 +5960,16 @@ var TOOLS = [
|
|
|
5079
5960
|
name: IrisTool.CLOCK,
|
|
5080
5961
|
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.",
|
|
5081
5962
|
inputSchema: {
|
|
5082
|
-
freeze:
|
|
5083
|
-
advanceMs:
|
|
5084
|
-
reset:
|
|
5963
|
+
freeze: z16.boolean().optional().describe("Freeze the fake clock. Time stops advancing until advanceMs or reset."),
|
|
5964
|
+
advanceMs: z16.number().optional().describe("Fast-forward time by this many milliseconds \u2014 triggers debounces, toasts, auto-dismiss timers."),
|
|
5965
|
+
reset: z16.boolean().optional().describe("Restore the real clock."),
|
|
5085
5966
|
...sessionIdShape6
|
|
5086
5967
|
},
|
|
5087
5968
|
outputSchema: {
|
|
5088
|
-
ok:
|
|
5089
|
-
elapsed:
|
|
5969
|
+
ok: z16.boolean().optional(),
|
|
5970
|
+
elapsed: z16.number().optional()
|
|
5090
5971
|
},
|
|
5091
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5972
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.CLOCK, {
|
|
5092
5973
|
freeze: args["freeze"],
|
|
5093
5974
|
advanceMs: args["advanceMs"],
|
|
5094
5975
|
reset: args["reset"]
|
|
@@ -5098,27 +5979,27 @@ var TOOLS = [
|
|
|
5098
5979
|
name: IrisTool.STATE,
|
|
5099
5980
|
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? }.",
|
|
5100
5981
|
inputSchema: {
|
|
5101
|
-
ref:
|
|
5102
|
-
store:
|
|
5103
|
-
path:
|
|
5104
|
-
depth:
|
|
5982
|
+
ref: z16.string().optional().describe("Element ref \u2014 attempts a best-effort read of the nearest React component's hook state."),
|
|
5983
|
+
store: z16.string().optional().describe("Registered store name (e.g. 'workspace'). Omit to read all stores."),
|
|
5984
|
+
path: z16.string().optional().describe("Dot-path into the store (e.g. 'captionCache.v3'). Numeric array indices are supported."),
|
|
5985
|
+
depth: z16.number().optional().describe("Collapse anything deeper than N levels to a size marker \u2014 avoids huge outputs for large stores."),
|
|
5105
5986
|
...sessionIdShape6
|
|
5106
5987
|
},
|
|
5107
5988
|
outputSchema: {
|
|
5108
|
-
stores:
|
|
5109
|
-
storeNames:
|
|
5110
|
-
found:
|
|
5111
|
-
value:
|
|
5112
|
-
component:
|
|
5989
|
+
stores: z16.record(z16.unknown()).optional(),
|
|
5990
|
+
storeNames: z16.array(z16.string()).optional(),
|
|
5991
|
+
found: z16.boolean().optional(),
|
|
5992
|
+
value: z16.unknown().optional(),
|
|
5993
|
+
component: z16.object({ ok: z16.boolean(), reason: z16.string().optional(), state: z16.unknown().optional() }).optional()
|
|
5113
5994
|
},
|
|
5114
5995
|
handler: async (deps, args) => {
|
|
5115
|
-
const store =
|
|
5116
|
-
const result = await commandOrThrow3(deps,
|
|
5996
|
+
const store = asString(args["store"]);
|
|
5997
|
+
const result = await commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.STATE_READ, {
|
|
5117
5998
|
ref: args["ref"],
|
|
5118
5999
|
store
|
|
5119
6000
|
});
|
|
5120
|
-
const path =
|
|
5121
|
-
const depth =
|
|
6001
|
+
const path = asString(args["path"]);
|
|
6002
|
+
const depth = asNumber(args["depth"]);
|
|
5122
6003
|
if (path === void 0 && depth === void 0)
|
|
5123
6004
|
return result;
|
|
5124
6005
|
const root = result;
|
|
@@ -5138,16 +6019,16 @@ var TOOLS = [
|
|
|
5138
6019
|
name: IrisTool.EXPLORE,
|
|
5139
6020
|
description: "Autonomous-exploration helper: list interactive elements (with refs) + current console-error count, so the agent can drive the app and report anomalies.",
|
|
5140
6021
|
inputSchema: {
|
|
5141
|
-
scope:
|
|
6022
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the interactive element list to a subtree."),
|
|
5142
6023
|
...sessionIdShape6
|
|
5143
6024
|
},
|
|
5144
6025
|
outputSchema: {
|
|
5145
|
-
interactive:
|
|
5146
|
-
consoleErrors:
|
|
5147
|
-
hint:
|
|
6026
|
+
interactive: z16.array(z16.unknown()),
|
|
6027
|
+
consoleErrors: z16.number(),
|
|
6028
|
+
hint: z16.string()
|
|
5148
6029
|
},
|
|
5149
6030
|
handler: async (deps, args) => {
|
|
5150
|
-
const session = deps.sessions.resolve(
|
|
6031
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5151
6032
|
const result = await session.command(IrisCommand.SNAPSHOT, {
|
|
5152
6033
|
mode: SnapshotMode.INTERACTIVE,
|
|
5153
6034
|
scope: args["scope"]
|
|
@@ -5165,6 +6046,7 @@ var TOOLS = [
|
|
|
5165
6046
|
},
|
|
5166
6047
|
// iris_capabilities (live | fromDisk) + iris_contract_save. See contract-tools.ts.
|
|
5167
6048
|
...CONTRACT_TOOLS,
|
|
6049
|
+
...DOMAIN_TOOLS,
|
|
5168
6050
|
// iris_flow_save / iris_flow_list / iris_flow_load. See flow-tools.ts.
|
|
5169
6051
|
...FLOW_TOOLS,
|
|
5170
6052
|
// iris_project (read history + diff-vs-last) / iris_run_record. See project-tools.ts.
|
|
@@ -5318,7 +6200,7 @@ async function runTool(tool, deps, args) {
|
|
|
5318
6200
|
return result;
|
|
5319
6201
|
if (!isPlainObject(result) || "session" in result)
|
|
5320
6202
|
return result;
|
|
5321
|
-
const session = deps.sessions.resolve(
|
|
6203
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5322
6204
|
const envelope = { ...healthEnvelope(session) };
|
|
5323
6205
|
const lease = session.takeSessionLease();
|
|
5324
6206
|
if (lease !== void 0)
|
|
@@ -5483,7 +6365,17 @@ function spawnDaemon(nodeExec, scriptPath, args, port) {
|
|
|
5483
6365
|
// ../server/dist/index.js
|
|
5484
6366
|
async function start(options = {}) {
|
|
5485
6367
|
const port = options.port ?? IRIS_DEFAULT_PORT;
|
|
5486
|
-
const
|
|
6368
|
+
const envToken = process.env["IRIS_TOKEN"];
|
|
6369
|
+
const envOrigins = process.env["IRIS_ALLOWED_ORIGINS"];
|
|
6370
|
+
const host = options.host ?? process.env["IRIS_HOST"];
|
|
6371
|
+
const token = options.token ?? (envToken !== void 0 && envToken.length > 0 ? envToken : void 0);
|
|
6372
|
+
const allowedOrigins = options.allowedOrigins ?? envOrigins?.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
|
|
6373
|
+
const bridge = new Bridge({
|
|
6374
|
+
port,
|
|
6375
|
+
...host === void 0 ? {} : { host },
|
|
6376
|
+
...token === void 0 ? {} : { token },
|
|
6377
|
+
...allowedOrigins === void 0 ? {} : { allowedOrigins }
|
|
6378
|
+
});
|
|
5487
6379
|
const reaper = new SessionReaper(bridge.sessions);
|
|
5488
6380
|
reaper.start();
|
|
5489
6381
|
const baselines = new BaselineStore();
|
|
@@ -5751,13 +6643,619 @@ ${val}` : val;
|
|
|
5751
6643
|
});
|
|
5752
6644
|
}
|
|
5753
6645
|
|
|
6646
|
+
// ../server/dist/init/detect.js
|
|
6647
|
+
var Framework = {
|
|
6648
|
+
NEXT: "next",
|
|
6649
|
+
VITE: "vite",
|
|
6650
|
+
HTML: "html"
|
|
6651
|
+
};
|
|
6652
|
+
var PackageManager = {
|
|
6653
|
+
PNPM: "pnpm",
|
|
6654
|
+
YARN: "yarn",
|
|
6655
|
+
BUN: "bun",
|
|
6656
|
+
NPM: "npm"
|
|
6657
|
+
};
|
|
6658
|
+
var NEXT_CONFIGS = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"];
|
|
6659
|
+
var VITE_CONFIGS = ["vite.config.js", "vite.config.ts", "vite.config.mjs", "vite.config.mts"];
|
|
6660
|
+
function depVersion(pkg, name) {
|
|
6661
|
+
return pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? pkg.peerDependencies?.[name];
|
|
6662
|
+
}
|
|
6663
|
+
function hasAnyConfig(files, candidates) {
|
|
6664
|
+
return candidates.some((c) => files.has(c));
|
|
6665
|
+
}
|
|
6666
|
+
function parseMajor(range) {
|
|
6667
|
+
if (range === void 0)
|
|
6668
|
+
return void 0;
|
|
6669
|
+
const match = range.match(/(\d+)/);
|
|
6670
|
+
if (match === null || match[1] === void 0)
|
|
6671
|
+
return void 0;
|
|
6672
|
+
const major = parseInt(match[1], 10);
|
|
6673
|
+
return isNaN(major) ? void 0 : major;
|
|
6674
|
+
}
|
|
6675
|
+
function detectPackageManager(lockfiles) {
|
|
6676
|
+
if (lockfiles.has("pnpm-lock.yaml"))
|
|
6677
|
+
return PackageManager.PNPM;
|
|
6678
|
+
if (lockfiles.has("yarn.lock"))
|
|
6679
|
+
return PackageManager.YARN;
|
|
6680
|
+
if (lockfiles.has("bun.lockb") || lockfiles.has("bun.lock"))
|
|
6681
|
+
return PackageManager.BUN;
|
|
6682
|
+
return PackageManager.NPM;
|
|
6683
|
+
}
|
|
6684
|
+
function detectFramework(input) {
|
|
6685
|
+
const { pkg, configFiles } = input;
|
|
6686
|
+
if (depVersion(pkg, "next") !== void 0 || hasAnyConfig(configFiles, NEXT_CONFIGS)) {
|
|
6687
|
+
return Framework.NEXT;
|
|
6688
|
+
}
|
|
6689
|
+
if (depVersion(pkg, "vite") !== void 0 || hasAnyConfig(configFiles, VITE_CONFIGS)) {
|
|
6690
|
+
return Framework.VITE;
|
|
6691
|
+
}
|
|
6692
|
+
return Framework.HTML;
|
|
6693
|
+
}
|
|
6694
|
+
function detect(input) {
|
|
6695
|
+
const reactMajor = parseMajor(depVersion(input.pkg, "react"));
|
|
6696
|
+
return {
|
|
6697
|
+
framework: detectFramework(input),
|
|
6698
|
+
reactMajor,
|
|
6699
|
+
needsSourceMapping: reactMajor !== void 0 && reactMajor >= 19,
|
|
6700
|
+
packageManager: detectPackageManager(input.lockfiles)
|
|
6701
|
+
};
|
|
6702
|
+
}
|
|
6703
|
+
var INSTALL_ARGS = {
|
|
6704
|
+
[PackageManager.PNPM]: ["add", "-D"],
|
|
6705
|
+
[PackageManager.YARN]: ["add", "-D"],
|
|
6706
|
+
[PackageManager.BUN]: ["add", "-d"],
|
|
6707
|
+
[PackageManager.NPM]: ["i", "-D"]
|
|
6708
|
+
};
|
|
6709
|
+
function installCommandParts(pm, pkg) {
|
|
6710
|
+
return { command: pm, args: [...INSTALL_ARGS[pm], pkg] };
|
|
6711
|
+
}
|
|
6712
|
+
function installCommand(pm, pkg) {
|
|
6713
|
+
const { command, args } = installCommandParts(pm, pkg);
|
|
6714
|
+
return `${command} ${args.join(" ")}`;
|
|
6715
|
+
}
|
|
6716
|
+
|
|
6717
|
+
// ../server/dist/init/mcp.js
|
|
6718
|
+
var MCP_SERVER_NAME = "iris";
|
|
6719
|
+
var NPX = "npx";
|
|
6720
|
+
var IRIS_PACKAGE = "@syrin/iris";
|
|
6721
|
+
var MCP_SUBCOMMAND = "mcp";
|
|
6722
|
+
var PORT_FLAG = "--port";
|
|
6723
|
+
var CLAUDE_CLI = "claude";
|
|
6724
|
+
function npxServerArgs(port) {
|
|
6725
|
+
return port === void 0 ? [IRIS_PACKAGE, MCP_SUBCOMMAND] : [IRIS_PACKAGE, MCP_SUBCOMMAND, PORT_FLAG, String(port)];
|
|
6726
|
+
}
|
|
6727
|
+
function serverInvocation(port) {
|
|
6728
|
+
return [NPX, ...npxServerArgs(port)];
|
|
6729
|
+
}
|
|
6730
|
+
function claudeAddCommand(port) {
|
|
6731
|
+
const tail = serverInvocation(port);
|
|
6732
|
+
const args = [MCP_SUBCOMMAND, "add", MCP_SERVER_NAME, "-s", "user", "--", ...tail];
|
|
6733
|
+
return { command: CLAUDE_CLI, args, display: `${CLAUDE_CLI} ${args.join(" ")}` };
|
|
6734
|
+
}
|
|
6735
|
+
function claudeExistsProbe() {
|
|
6736
|
+
return { command: CLAUDE_CLI, args: [MCP_SUBCOMMAND, "get", MCP_SERVER_NAME] };
|
|
6737
|
+
}
|
|
6738
|
+
function claudeAvailableProbe() {
|
|
6739
|
+
return { command: CLAUDE_CLI, args: ["--version"] };
|
|
6740
|
+
}
|
|
6741
|
+
function mcpManual(port) {
|
|
6742
|
+
const tail = serverInvocation(port).join(" ");
|
|
6743
|
+
return `Register the Iris MCP server ONCE, globally (so every project gets it):
|
|
6744
|
+
|
|
6745
|
+
${CLAUDE_CLI} ${MCP_SUBCOMMAND} add ${MCP_SERVER_NAME} -s user -- ${tail}
|
|
6746
|
+
|
|
6747
|
+
Or, for another agent, add this to its global MCP config (e.g. Cursor's ~/.cursor/mcp.json):
|
|
6748
|
+
|
|
6749
|
+
"${MCP_SERVER_NAME}": { "command": "${NPX}", "args": ${JSON.stringify(serverInvocation(port).slice(1))} }`;
|
|
6750
|
+
}
|
|
6751
|
+
|
|
6752
|
+
// ../server/dist/init/cursor.js
|
|
6753
|
+
var CURSOR_MCP_RELPATH = ".cursor/mcp.json";
|
|
6754
|
+
var CURSOR_DIR_RELPATH = ".cursor";
|
|
6755
|
+
var CursorMergeStatus = {
|
|
6756
|
+
APPLY: "apply",
|
|
6757
|
+
ALREADY: "already",
|
|
6758
|
+
MANUAL: "manual"
|
|
6759
|
+
};
|
|
6760
|
+
function cursorServerEntry(port) {
|
|
6761
|
+
return { command: NPX, args: npxServerArgs(port) };
|
|
6762
|
+
}
|
|
6763
|
+
function parseConfig(existing) {
|
|
6764
|
+
if (existing === null || existing.trim().length === 0)
|
|
6765
|
+
return { ok: true, config: {} };
|
|
6766
|
+
try {
|
|
6767
|
+
const parsed = JSON.parse(existing);
|
|
6768
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
6769
|
+
return { ok: true, config: {} };
|
|
6770
|
+
return { ok: true, config: parsed };
|
|
6771
|
+
} catch {
|
|
6772
|
+
return { ok: false };
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
6775
|
+
function mergeCursorConfig(existing, port) {
|
|
6776
|
+
const parsed = parseConfig(existing);
|
|
6777
|
+
if (!parsed.ok) {
|
|
6778
|
+
return { status: CursorMergeStatus.MANUAL, content: existing ?? "" };
|
|
6779
|
+
}
|
|
6780
|
+
const config = parsed.config;
|
|
6781
|
+
const servers = config.mcpServers ?? {};
|
|
6782
|
+
if (Object.prototype.hasOwnProperty.call(servers, MCP_SERVER_NAME)) {
|
|
6783
|
+
return { status: CursorMergeStatus.ALREADY, content: existing ?? "" };
|
|
6784
|
+
}
|
|
6785
|
+
const merged = {
|
|
6786
|
+
...config,
|
|
6787
|
+
mcpServers: { ...servers, [MCP_SERVER_NAME]: cursorServerEntry(port) }
|
|
6788
|
+
};
|
|
6789
|
+
return { status: CursorMergeStatus.APPLY, content: `${JSON.stringify(merged, null, 2)}
|
|
6790
|
+
` };
|
|
6791
|
+
}
|
|
6792
|
+
|
|
6793
|
+
// ../server/dist/init/vite-config.js
|
|
6794
|
+
var VITE_IMPORT = "import { iris } from '@syrin/iris/vite';";
|
|
6795
|
+
var IRIS_MARKER = "@syrin/iris/vite";
|
|
6796
|
+
function irisPluginCall(port) {
|
|
6797
|
+
return port === void 0 ? "iris()" : `iris({ port: ${String(port)} })`;
|
|
6798
|
+
}
|
|
6799
|
+
var PLUGINS_ARRAY = /plugins\s*:\s*\[/;
|
|
6800
|
+
var IMPORT_LINE = /^import\s.+from\s+['"][^'"]+['"];?\s*$/gm;
|
|
6801
|
+
var VitePatchKind = {
|
|
6802
|
+
APPLY: "apply",
|
|
6803
|
+
ALREADY: "already",
|
|
6804
|
+
MANUAL: "manual"
|
|
6805
|
+
};
|
|
6806
|
+
var NO_PLUGINS_REASON = "couldn't find a `plugins: [...]` array to extend";
|
|
6807
|
+
function insertImport(source) {
|
|
6808
|
+
const matches = [...source.matchAll(IMPORT_LINE)];
|
|
6809
|
+
const last = matches[matches.length - 1];
|
|
6810
|
+
if (last?.index === void 0) {
|
|
6811
|
+
return `${VITE_IMPORT}
|
|
6812
|
+
${source}`;
|
|
6813
|
+
}
|
|
6814
|
+
const end = last.index + last[0].length;
|
|
6815
|
+
return `${source.slice(0, end)}
|
|
6816
|
+
${VITE_IMPORT}${source.slice(end)}`;
|
|
6817
|
+
}
|
|
6818
|
+
function insertPlugin(source, port) {
|
|
6819
|
+
return source.replace(PLUGINS_ARRAY, (match) => `${match}${irisPluginCall(port)}, `);
|
|
6820
|
+
}
|
|
6821
|
+
function patchViteConfig(source, port) {
|
|
6822
|
+
if (source.includes(IRIS_MARKER)) {
|
|
6823
|
+
return { kind: VitePatchKind.ALREADY };
|
|
6824
|
+
}
|
|
6825
|
+
if (!PLUGINS_ARRAY.test(source)) {
|
|
6826
|
+
return { kind: VitePatchKind.MANUAL, reason: NO_PLUGINS_REASON };
|
|
6827
|
+
}
|
|
6828
|
+
return { kind: VitePatchKind.APPLY, code: insertImport(insertPlugin(source, port)) };
|
|
6829
|
+
}
|
|
6830
|
+
|
|
6831
|
+
// ../server/dist/init/snippets.js
|
|
6832
|
+
function connectArg(port) {
|
|
6833
|
+
if (port === void 0 || port === IRIS_DEFAULT_PORT)
|
|
6834
|
+
return "";
|
|
6835
|
+
return `{ url: 'ws://localhost:${String(port)}${IRIS_WS_PATH}' }`;
|
|
6836
|
+
}
|
|
6837
|
+
function viteManual(port) {
|
|
6838
|
+
const call = port === void 0 ? "iris()" : `iris({ port: ${String(port)} })`;
|
|
6839
|
+
return `Add the Iris plugin to your Vite config:
|
|
6840
|
+
|
|
6841
|
+
import { iris } from '@syrin/iris/vite';
|
|
6842
|
+
|
|
6843
|
+
export default defineConfig({
|
|
6844
|
+
plugins: [react(), ${call}],
|
|
6845
|
+
});
|
|
6846
|
+
|
|
6847
|
+
The plugin only applies during \`vite\` (dev) \u2014 it is dropped from \`vite build\`.`;
|
|
6848
|
+
}
|
|
6849
|
+
function nextConfigManual(configFile) {
|
|
6850
|
+
return `Wrap your ${configFile} export with withIris (keeps SWC, dev-only):
|
|
6851
|
+
|
|
6852
|
+
import { withIris } from '@syrin/iris/next';
|
|
6853
|
+
|
|
6854
|
+
export default withIris(nextConfig);`;
|
|
6855
|
+
}
|
|
6856
|
+
function nextIrisDevFile(port) {
|
|
6857
|
+
return `'use client';
|
|
6858
|
+
import { useEffect } from 'react';
|
|
6859
|
+
|
|
6860
|
+
/** Dev-only: connect Iris + install the React adapter, after hydration. */
|
|
6861
|
+
export function IrisDev() {
|
|
6862
|
+
useEffect(() => {
|
|
6863
|
+
if (process.env.NODE_ENV !== 'development') return;
|
|
6864
|
+
void import('@syrin/iris').then(({ iris, install }) => {
|
|
6865
|
+
install();
|
|
6866
|
+
iris.connect(${connectArg(port)});
|
|
6867
|
+
});
|
|
6868
|
+
}, []);
|
|
6869
|
+
return null;
|
|
6870
|
+
}
|
|
6871
|
+
`;
|
|
6872
|
+
}
|
|
6873
|
+
var NEXT_LAYOUT_MANUAL = `Mount <IrisDev /> in your root layout (app/layout.tsx), dev-only:
|
|
6874
|
+
|
|
6875
|
+
import { IrisDev } from './iris-dev';
|
|
6876
|
+
// inside <body>:
|
|
6877
|
+
{process.env.NODE_ENV === 'development' ? <IrisDev /> : null}`;
|
|
6878
|
+
function htmlManual(port) {
|
|
6879
|
+
return `Add a dev-gated module script at app boot:
|
|
6880
|
+
|
|
6881
|
+
<script type="module">
|
|
6882
|
+
if (location.hostname === 'localhost') {
|
|
6883
|
+
const { iris, install } = await import('@syrin/iris');
|
|
6884
|
+
install();
|
|
6885
|
+
iris.connect(${connectArg(port)});
|
|
6886
|
+
}
|
|
6887
|
+
</script>`;
|
|
6888
|
+
}
|
|
6889
|
+
var NEXT_IRIS_DEV_PATH = "app/iris-dev.tsx";
|
|
6890
|
+
|
|
6891
|
+
// ../server/dist/init/plan.js
|
|
6892
|
+
var IRIS_PACKAGE2 = "@syrin/iris";
|
|
6893
|
+
var MCP_TARGET = "global (claude user scope)";
|
|
6894
|
+
var StepStatus = {
|
|
6895
|
+
APPLY: "apply",
|
|
6896
|
+
MANUAL: "manual",
|
|
6897
|
+
ALREADY: "already",
|
|
6898
|
+
SKIP: "skip"
|
|
6899
|
+
};
|
|
6900
|
+
var CLAUDE_MCP_TITLE = "MCP server (Claude, global)";
|
|
6901
|
+
var CURSOR_MCP_TITLE = "MCP server (Cursor, global)";
|
|
6902
|
+
function claudeMcpStep(input) {
|
|
6903
|
+
if (!input.claudeCli)
|
|
6904
|
+
return null;
|
|
6905
|
+
if (input.mcpExists) {
|
|
6906
|
+
return {
|
|
6907
|
+
title: CLAUDE_MCP_TITLE,
|
|
6908
|
+
target: MCP_TARGET,
|
|
6909
|
+
status: StepStatus.ALREADY,
|
|
6910
|
+
detail: "iris already registered (install once, used by every project)"
|
|
6911
|
+
};
|
|
6912
|
+
}
|
|
6913
|
+
const cmd = claudeAddCommand(input.options.port);
|
|
6914
|
+
return {
|
|
6915
|
+
title: CLAUDE_MCP_TITLE,
|
|
6916
|
+
target: MCP_TARGET,
|
|
6917
|
+
status: StepStatus.APPLY,
|
|
6918
|
+
detail: "register iris globally for all projects",
|
|
6919
|
+
exec: { command: cmd.command, args: cmd.args, fallback: cmd.display }
|
|
6920
|
+
};
|
|
6921
|
+
}
|
|
6922
|
+
function cursorMcpStep(input) {
|
|
6923
|
+
if (!input.cursorPresent)
|
|
6924
|
+
return null;
|
|
6925
|
+
const r = mergeCursorConfig(input.cursorConfig, input.options.port);
|
|
6926
|
+
if (r.status === CursorMergeStatus.ALREADY) {
|
|
6927
|
+
return {
|
|
6928
|
+
title: CURSOR_MCP_TITLE,
|
|
6929
|
+
target: input.cursorConfigPath,
|
|
6930
|
+
status: StepStatus.ALREADY,
|
|
6931
|
+
detail: "iris already in Cursor global config"
|
|
6932
|
+
};
|
|
6933
|
+
}
|
|
6934
|
+
if (r.status === CursorMergeStatus.MANUAL) {
|
|
6935
|
+
return {
|
|
6936
|
+
title: CURSOR_MCP_TITLE,
|
|
6937
|
+
target: input.cursorConfigPath,
|
|
6938
|
+
status: StepStatus.MANUAL,
|
|
6939
|
+
detail: `couldn't parse ${input.cursorConfigPath} \u2014 add this server by hand:
|
|
6940
|
+
"iris": ${JSON.stringify(cursorServerEntry(input.options.port))}`
|
|
6941
|
+
};
|
|
6942
|
+
}
|
|
6943
|
+
return {
|
|
6944
|
+
title: CURSOR_MCP_TITLE,
|
|
6945
|
+
target: input.cursorConfigPath,
|
|
6946
|
+
status: StepStatus.APPLY,
|
|
6947
|
+
detail: "register iris in Cursor global config",
|
|
6948
|
+
write: { path: input.cursorConfigPath, content: r.content }
|
|
6949
|
+
};
|
|
6950
|
+
}
|
|
6951
|
+
function mcpSteps(input) {
|
|
6952
|
+
if (!input.options.mcp) {
|
|
6953
|
+
return [
|
|
6954
|
+
{
|
|
6955
|
+
title: "MCP server (global)",
|
|
6956
|
+
target: MCP_TARGET,
|
|
6957
|
+
status: StepStatus.SKIP,
|
|
6958
|
+
detail: "--no-mcp"
|
|
6959
|
+
}
|
|
6960
|
+
];
|
|
6961
|
+
}
|
|
6962
|
+
const steps = [claudeMcpStep(input), cursorMcpStep(input)].filter((s) => s !== null);
|
|
6963
|
+
if (steps.length > 0)
|
|
6964
|
+
return steps;
|
|
6965
|
+
return [
|
|
6966
|
+
{
|
|
6967
|
+
title: "MCP server (global)",
|
|
6968
|
+
target: MCP_TARGET,
|
|
6969
|
+
status: StepStatus.MANUAL,
|
|
6970
|
+
detail: mcpManual(input.options.port)
|
|
6971
|
+
}
|
|
6972
|
+
];
|
|
6973
|
+
}
|
|
6974
|
+
function installStep(input) {
|
|
6975
|
+
const pm = input.detection.packageManager;
|
|
6976
|
+
const command = installCommand(pm, IRIS_PACKAGE2);
|
|
6977
|
+
if (!input.options.install) {
|
|
6978
|
+
return {
|
|
6979
|
+
title: "Install dependency",
|
|
6980
|
+
target: "package.json",
|
|
6981
|
+
status: StepStatus.MANUAL,
|
|
6982
|
+
detail: command
|
|
6983
|
+
};
|
|
6984
|
+
}
|
|
6985
|
+
const parts = installCommandParts(pm, IRIS_PACKAGE2);
|
|
6986
|
+
return {
|
|
6987
|
+
title: "Install dependency",
|
|
6988
|
+
target: "package.json",
|
|
6989
|
+
status: StepStatus.APPLY,
|
|
6990
|
+
detail: command,
|
|
6991
|
+
exec: { command: parts.command, args: parts.args, fallback: command }
|
|
6992
|
+
};
|
|
6993
|
+
}
|
|
6994
|
+
function viteSteps(input) {
|
|
6995
|
+
const cfg = input.viteConfig;
|
|
6996
|
+
const port = input.options.port;
|
|
6997
|
+
if (cfg === null) {
|
|
6998
|
+
return [
|
|
6999
|
+
{
|
|
7000
|
+
title: "Vite plugin",
|
|
7001
|
+
target: "vite.config",
|
|
7002
|
+
status: StepStatus.MANUAL,
|
|
7003
|
+
detail: viteManual(port)
|
|
7004
|
+
}
|
|
7005
|
+
];
|
|
7006
|
+
}
|
|
7007
|
+
const patch = patchViteConfig(cfg.source, port);
|
|
7008
|
+
if (patch.kind === VitePatchKind.ALREADY) {
|
|
7009
|
+
return [
|
|
7010
|
+
{
|
|
7011
|
+
title: "Vite plugin",
|
|
7012
|
+
target: cfg.path,
|
|
7013
|
+
status: StepStatus.ALREADY,
|
|
7014
|
+
detail: "iris() already in plugins"
|
|
7015
|
+
}
|
|
7016
|
+
];
|
|
7017
|
+
}
|
|
7018
|
+
if (patch.kind === VitePatchKind.MANUAL) {
|
|
7019
|
+
return [
|
|
7020
|
+
{
|
|
7021
|
+
title: "Vite plugin",
|
|
7022
|
+
target: cfg.path,
|
|
7023
|
+
status: StepStatus.MANUAL,
|
|
7024
|
+
detail: `${patch.reason}
|
|
7025
|
+
|
|
7026
|
+
${viteManual(port)}`
|
|
7027
|
+
}
|
|
7028
|
+
];
|
|
7029
|
+
}
|
|
7030
|
+
return [
|
|
7031
|
+
{
|
|
7032
|
+
title: "Vite plugin",
|
|
7033
|
+
target: cfg.path,
|
|
7034
|
+
status: StepStatus.APPLY,
|
|
7035
|
+
detail: "add iris() to plugins (also injects connect())",
|
|
7036
|
+
write: { path: cfg.path, content: patch.code }
|
|
7037
|
+
}
|
|
7038
|
+
];
|
|
7039
|
+
}
|
|
7040
|
+
function nextSteps(input) {
|
|
7041
|
+
const configFile = input.nextConfigFile ?? "next.config.mjs";
|
|
7042
|
+
const devFile = input.nextIrisDevExists ? {
|
|
7043
|
+
title: "IrisDev component",
|
|
7044
|
+
target: NEXT_IRIS_DEV_PATH,
|
|
7045
|
+
status: StepStatus.ALREADY,
|
|
7046
|
+
detail: "file exists"
|
|
7047
|
+
} : {
|
|
7048
|
+
title: "IrisDev component",
|
|
7049
|
+
target: NEXT_IRIS_DEV_PATH,
|
|
7050
|
+
status: StepStatus.APPLY,
|
|
7051
|
+
detail: "create dev-only connect component",
|
|
7052
|
+
write: { path: NEXT_IRIS_DEV_PATH, content: nextIrisDevFile(input.options.port) }
|
|
7053
|
+
};
|
|
7054
|
+
return [
|
|
7055
|
+
devFile,
|
|
7056
|
+
{
|
|
7057
|
+
title: "Next config (withIris)",
|
|
7058
|
+
target: configFile,
|
|
7059
|
+
status: StepStatus.MANUAL,
|
|
7060
|
+
detail: nextConfigManual(configFile)
|
|
7061
|
+
},
|
|
7062
|
+
{
|
|
7063
|
+
title: "Mount IrisDev",
|
|
7064
|
+
target: "app/layout.tsx",
|
|
7065
|
+
status: StepStatus.MANUAL,
|
|
7066
|
+
detail: NEXT_LAYOUT_MANUAL
|
|
7067
|
+
}
|
|
7068
|
+
];
|
|
7069
|
+
}
|
|
7070
|
+
function buildPlan(input) {
|
|
7071
|
+
const steps = [...mcpSteps(input), installStep(input)];
|
|
7072
|
+
if (input.detection.framework === Framework.VITE) {
|
|
7073
|
+
steps.push(...viteSteps(input));
|
|
7074
|
+
} else if (input.detection.framework === Framework.NEXT) {
|
|
7075
|
+
steps.push(...nextSteps(input));
|
|
7076
|
+
} else {
|
|
7077
|
+
steps.push({
|
|
7078
|
+
title: "Connect snippet",
|
|
7079
|
+
target: "index.html",
|
|
7080
|
+
status: StepStatus.MANUAL,
|
|
7081
|
+
detail: htmlManual(input.options.port)
|
|
7082
|
+
});
|
|
7083
|
+
}
|
|
7084
|
+
return { framework: input.detection.framework, steps };
|
|
7085
|
+
}
|
|
7086
|
+
|
|
7087
|
+
// ../server/dist/init/run.js
|
|
7088
|
+
var PACKAGE_JSON = "package.json";
|
|
7089
|
+
var NEXT_IRIS_DEV = "app/iris-dev.tsx";
|
|
7090
|
+
var VITE_CONFIG_CANDIDATES = [
|
|
7091
|
+
"vite.config.ts",
|
|
7092
|
+
"vite.config.js",
|
|
7093
|
+
"vite.config.mjs",
|
|
7094
|
+
"vite.config.mts"
|
|
7095
|
+
];
|
|
7096
|
+
var NEXT_CONFIG_CANDIDATES = [
|
|
7097
|
+
"next.config.mjs",
|
|
7098
|
+
"next.config.js",
|
|
7099
|
+
"next.config.ts",
|
|
7100
|
+
"next.config.cjs"
|
|
7101
|
+
];
|
|
7102
|
+
var STATUS_SYMBOL = {
|
|
7103
|
+
[StepStatus.APPLY]: "\u2713",
|
|
7104
|
+
[StepStatus.MANUAL]: "\u26A0",
|
|
7105
|
+
[StepStatus.ALREADY]: "\xB7",
|
|
7106
|
+
[StepStatus.SKIP]: "\u2013"
|
|
7107
|
+
};
|
|
7108
|
+
function firstPresent(files, candidates) {
|
|
7109
|
+
for (const c of candidates)
|
|
7110
|
+
if (files.has(c))
|
|
7111
|
+
return c;
|
|
7112
|
+
return null;
|
|
7113
|
+
}
|
|
7114
|
+
function gatherPlanInput(options, io, pkgRaw) {
|
|
7115
|
+
const pkg = JSON.parse(pkgRaw);
|
|
7116
|
+
const rootFiles = new Set(io.rootFiles());
|
|
7117
|
+
const detectInput = {
|
|
7118
|
+
pkg: typeof pkg === "object" && pkg !== null ? pkg : {},
|
|
7119
|
+
configFiles: rootFiles,
|
|
7120
|
+
lockfiles: rootFiles
|
|
7121
|
+
};
|
|
7122
|
+
const detection = detect(detectInput);
|
|
7123
|
+
const vitePath = firstPresent(rootFiles, VITE_CONFIG_CANDIDATES);
|
|
7124
|
+
const viteSource = vitePath === null ? null : io.readFile(vitePath);
|
|
7125
|
+
const viteConfig = vitePath !== null && viteSource !== null ? { path: vitePath, source: viteSource } : null;
|
|
7126
|
+
const availableProbe = claudeAvailableProbe();
|
|
7127
|
+
const claudeCli = options.mcp ? io.probe(availableProbe.command, availableProbe.args) : false;
|
|
7128
|
+
const existsProbe = claudeExistsProbe();
|
|
7129
|
+
const mcpExists = claudeCli ? io.probe(existsProbe.command, existsProbe.args) : false;
|
|
7130
|
+
const cursorDir = `${io.homeDir()}/${CURSOR_DIR_RELPATH}`;
|
|
7131
|
+
const cursorConfigPath = `${io.homeDir()}/${CURSOR_MCP_RELPATH}`;
|
|
7132
|
+
const cursorPresent = options.mcp && io.exists(cursorDir);
|
|
7133
|
+
const cursorConfig = cursorPresent ? io.readFile(cursorConfigPath) : null;
|
|
7134
|
+
return {
|
|
7135
|
+
detection,
|
|
7136
|
+
claudeCli,
|
|
7137
|
+
mcpExists,
|
|
7138
|
+
cursorPresent,
|
|
7139
|
+
cursorConfig,
|
|
7140
|
+
cursorConfigPath,
|
|
7141
|
+
viteConfig,
|
|
7142
|
+
nextConfigFile: firstPresent(rootFiles, NEXT_CONFIG_CANDIDATES),
|
|
7143
|
+
nextIrisDevExists: io.exists(NEXT_IRIS_DEV),
|
|
7144
|
+
options: { port: options.port, mcp: options.mcp, install: options.install }
|
|
7145
|
+
};
|
|
7146
|
+
}
|
|
7147
|
+
function restartHint(framework) {
|
|
7148
|
+
if (framework === Framework.NEXT)
|
|
7149
|
+
return 'Restart `next dev`, then ask your agent: "List Iris sessions".';
|
|
7150
|
+
if (framework === Framework.VITE)
|
|
7151
|
+
return 'Restart `vite`, then ask your agent: "List Iris sessions".';
|
|
7152
|
+
return 'Reload your app on localhost, then ask your agent: "List Iris sessions".';
|
|
7153
|
+
}
|
|
7154
|
+
function report(plan, dryRun, failed, io) {
|
|
7155
|
+
io.print(dryRun ? "iris init (dry run \u2014 no files written)" : "iris init");
|
|
7156
|
+
io.print("");
|
|
7157
|
+
let applied = 0;
|
|
7158
|
+
let manual = 0;
|
|
7159
|
+
for (const s of plan.steps) {
|
|
7160
|
+
const downgraded = failed.has(s.target);
|
|
7161
|
+
const status = downgraded ? StepStatus.MANUAL : s.status;
|
|
7162
|
+
const detail = downgraded && s.exec !== void 0 ? `step failed \u2014 run manually: ${s.exec.fallback}` : s.detail;
|
|
7163
|
+
io.print(` [${STATUS_SYMBOL[status]}] ${s.title} \u2192 ${s.target}`);
|
|
7164
|
+
if (status === StepStatus.APPLY)
|
|
7165
|
+
applied++;
|
|
7166
|
+
if (status === StepStatus.MANUAL) {
|
|
7167
|
+
manual++;
|
|
7168
|
+
for (const line of detail.split("\n"))
|
|
7169
|
+
io.print(` ${line}`);
|
|
7170
|
+
} else if (detail.length > 0) {
|
|
7171
|
+
io.print(` ${detail}`);
|
|
7172
|
+
}
|
|
7173
|
+
}
|
|
7174
|
+
io.print("");
|
|
7175
|
+
io.print(restartHint(plan.framework));
|
|
7176
|
+
return { ok: true, applied, manual };
|
|
7177
|
+
}
|
|
7178
|
+
function applyEffects(plan, io) {
|
|
7179
|
+
const failed = /* @__PURE__ */ new Set();
|
|
7180
|
+
for (const s of plan.steps) {
|
|
7181
|
+
if (s.status !== StepStatus.APPLY)
|
|
7182
|
+
continue;
|
|
7183
|
+
if (s.write !== void 0)
|
|
7184
|
+
io.writeFile(s.write.path, s.write.content);
|
|
7185
|
+
if (s.exec !== void 0 && !io.exec(s.exec.command, s.exec.args))
|
|
7186
|
+
failed.add(s.target);
|
|
7187
|
+
}
|
|
7188
|
+
return failed;
|
|
7189
|
+
}
|
|
7190
|
+
function runInit(options, io) {
|
|
7191
|
+
const pkgRaw = io.readFile(PACKAGE_JSON);
|
|
7192
|
+
if (pkgRaw === null) {
|
|
7193
|
+
io.print("No package.json found. Run `iris init` from your project root.");
|
|
7194
|
+
return { ok: false, applied: 0, manual: 0 };
|
|
7195
|
+
}
|
|
7196
|
+
const plan = buildPlan(gatherPlanInput(options, io, pkgRaw));
|
|
7197
|
+
const failed = options.dryRun ? /* @__PURE__ */ new Set() : applyEffects(plan, io);
|
|
7198
|
+
return report(plan, options.dryRun, failed, io);
|
|
7199
|
+
}
|
|
7200
|
+
|
|
7201
|
+
// ../server/dist/init/node-io.js
|
|
7202
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync, statSync } from "fs";
|
|
7203
|
+
import { join as join6, dirname as dirname2, isAbsolute } from "path";
|
|
7204
|
+
import { homedir as homedir3 } from "os";
|
|
7205
|
+
import { spawnSync } from "child_process";
|
|
7206
|
+
function buildNodeIo(cwd) {
|
|
7207
|
+
const abs = (rel) => isAbsolute(rel) ? rel : join6(cwd, rel);
|
|
7208
|
+
return {
|
|
7209
|
+
readFile(rel) {
|
|
7210
|
+
const path = abs(rel);
|
|
7211
|
+
if (!existsSync4(path))
|
|
7212
|
+
return null;
|
|
7213
|
+
return readFileSync3(path, "utf8");
|
|
7214
|
+
},
|
|
7215
|
+
writeFile(rel, content) {
|
|
7216
|
+
const path = abs(rel);
|
|
7217
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
7218
|
+
writeFileSync3(path, content, "utf8");
|
|
7219
|
+
},
|
|
7220
|
+
exists(rel) {
|
|
7221
|
+
return existsSync4(abs(rel));
|
|
7222
|
+
},
|
|
7223
|
+
homeDir() {
|
|
7224
|
+
return homedir3();
|
|
7225
|
+
},
|
|
7226
|
+
rootFiles() {
|
|
7227
|
+
return readdirSync(cwd).filter((name) => {
|
|
7228
|
+
try {
|
|
7229
|
+
return statSync(join6(cwd, name)).isFile();
|
|
7230
|
+
} catch {
|
|
7231
|
+
return false;
|
|
7232
|
+
}
|
|
7233
|
+
});
|
|
7234
|
+
},
|
|
7235
|
+
exec(command, args) {
|
|
7236
|
+
const result = spawnSync(command, [...args], { cwd, stdio: "inherit", shell: true });
|
|
7237
|
+
return result.status === 0;
|
|
7238
|
+
},
|
|
7239
|
+
probe(command, args) {
|
|
7240
|
+
const result = spawnSync(command, [...args], { cwd, stdio: "ignore", shell: true });
|
|
7241
|
+
return result.status === 0;
|
|
7242
|
+
},
|
|
7243
|
+
print(line) {
|
|
7244
|
+
process.stdout.write(`${line}
|
|
7245
|
+
`);
|
|
7246
|
+
}
|
|
7247
|
+
};
|
|
7248
|
+
}
|
|
7249
|
+
|
|
5754
7250
|
// ../server/dist/cli.js
|
|
5755
7251
|
var CLI_USAGE = `usage:
|
|
7252
|
+
iris init [--yes] [--dry-run] [--port N] [--no-mcp] [--no-install] (wire Iris into the project in this directory)
|
|
5756
7253
|
iris serve [--port N] [--drive <url>] [--headed]
|
|
5757
7254
|
iris stop [--port N] [--quiet]
|
|
5758
7255
|
iris status [--port N]
|
|
5759
7256
|
iris drive <url> [--headed] (foreground mode \u2014 for debugging)
|
|
5760
7257
|
iris mcp [--port N] [--drive <url>] [--headed] (MCP stdio proxy \u2014 auto-starts daemon if needed)`;
|
|
7258
|
+
var INIT_COMMAND = "init";
|
|
5761
7259
|
var SERVE_COMMAND = "serve";
|
|
5762
7260
|
var STOP_COMMAND = "stop";
|
|
5763
7261
|
var STATUS_COMMAND = "status";
|
|
@@ -5765,9 +7263,13 @@ var DRIVE_COMMAND = "drive";
|
|
|
5765
7263
|
var MCP_COMMAND = "mcp";
|
|
5766
7264
|
var DAEMON_INNER_COMMAND = "_daemon";
|
|
5767
7265
|
var HEADED_FLAG = "--headed";
|
|
5768
|
-
var
|
|
7266
|
+
var PORT_FLAG2 = "--port";
|
|
5769
7267
|
var DRIVE_FLAG = "--drive";
|
|
5770
7268
|
var QUIET_FLAG = "--quiet";
|
|
7269
|
+
var DRY_RUN_FLAG = "--dry-run";
|
|
7270
|
+
var YES_FLAG = "--yes";
|
|
7271
|
+
var NO_MCP_FLAG = "--no-mcp";
|
|
7272
|
+
var NO_INSTALL_FLAG = "--no-install";
|
|
5771
7273
|
function parseServeFlags(args, defaultPort) {
|
|
5772
7274
|
let port = defaultPort;
|
|
5773
7275
|
let driveUrl;
|
|
@@ -5775,7 +7277,7 @@ function parseServeFlags(args, defaultPort) {
|
|
|
5775
7277
|
let i = 0;
|
|
5776
7278
|
while (i < args.length) {
|
|
5777
7279
|
const arg = args[i];
|
|
5778
|
-
if (arg ===
|
|
7280
|
+
if (arg === PORT_FLAG2) {
|
|
5779
7281
|
i++;
|
|
5780
7282
|
const n = args[i];
|
|
5781
7283
|
if (n === void 0)
|
|
@@ -5799,7 +7301,7 @@ function parseServeFlags(args, defaultPort) {
|
|
|
5799
7301
|
return { kind: "ok", port, headless, ...driveUrl !== void 0 ? { driveUrl } : {} };
|
|
5800
7302
|
}
|
|
5801
7303
|
function parsePortFlag(args, defaultPort) {
|
|
5802
|
-
const idx = args.indexOf(
|
|
7304
|
+
const idx = args.indexOf(PORT_FLAG2);
|
|
5803
7305
|
if (idx === -1)
|
|
5804
7306
|
return defaultPort;
|
|
5805
7307
|
const n = args[idx + 1];
|
|
@@ -5826,11 +7328,48 @@ function parseDriveSuffix(args, port) {
|
|
|
5826
7328
|
return { kind: "error", message: CLI_USAGE };
|
|
5827
7329
|
return { kind: "ok", port, driveUrl, headless };
|
|
5828
7330
|
}
|
|
7331
|
+
function parseInitFlags(args) {
|
|
7332
|
+
let port;
|
|
7333
|
+
let mcp = true;
|
|
7334
|
+
let dryRun = false;
|
|
7335
|
+
let install = true;
|
|
7336
|
+
let i = 0;
|
|
7337
|
+
while (i < args.length) {
|
|
7338
|
+
const arg = args[i];
|
|
7339
|
+
if (arg === PORT_FLAG2) {
|
|
7340
|
+
i++;
|
|
7341
|
+
const n = args[i];
|
|
7342
|
+
if (n === void 0)
|
|
7343
|
+
return { kind: "error", message: CLI_USAGE };
|
|
7344
|
+
const parsed = parseInt(n, 10);
|
|
7345
|
+
if (isNaN(parsed))
|
|
7346
|
+
return { kind: "error", message: CLI_USAGE };
|
|
7347
|
+
port = parsed;
|
|
7348
|
+
} else if (arg === NO_MCP_FLAG) {
|
|
7349
|
+
mcp = false;
|
|
7350
|
+
} else if (arg === NO_INSTALL_FLAG) {
|
|
7351
|
+
install = false;
|
|
7352
|
+
} else if (arg === DRY_RUN_FLAG) {
|
|
7353
|
+
dryRun = true;
|
|
7354
|
+
} else if (arg === YES_FLAG) {
|
|
7355
|
+
} else {
|
|
7356
|
+
return { kind: "error", message: CLI_USAGE };
|
|
7357
|
+
}
|
|
7358
|
+
i++;
|
|
7359
|
+
}
|
|
7360
|
+
return { kind: "ok", port, mcp, dryRun, install };
|
|
7361
|
+
}
|
|
5829
7362
|
function parseCliArgs(argv, defaultPort) {
|
|
5830
7363
|
if (argv.length === 0)
|
|
5831
7364
|
return { kind: "serve", port: defaultPort, headless: true };
|
|
5832
7365
|
const [cmd, ...rest] = argv;
|
|
5833
7366
|
switch (cmd) {
|
|
7367
|
+
case INIT_COMMAND: {
|
|
7368
|
+
const r = parseInitFlags(rest);
|
|
7369
|
+
if (r.kind === "error")
|
|
7370
|
+
return r;
|
|
7371
|
+
return { kind: "init", port: r.port, mcp: r.mcp, dryRun: r.dryRun, install: r.install };
|
|
7372
|
+
}
|
|
5834
7373
|
case SERVE_COMMAND: {
|
|
5835
7374
|
const r = parseServeFlags(rest, defaultPort);
|
|
5836
7375
|
if (r.kind === "error")
|
|
@@ -5883,6 +7422,12 @@ function parseCliArgs(argv, defaultPort) {
|
|
|
5883
7422
|
return { kind: "error", message: CLI_USAGE };
|
|
5884
7423
|
}
|
|
5885
7424
|
}
|
|
7425
|
+
function handleInit(parsed) {
|
|
7426
|
+
const cwd = process.cwd();
|
|
7427
|
+
const result = runInit({ cwd, port: parsed.port, mcp: parsed.mcp, dryRun: parsed.dryRun, install: parsed.install }, buildNodeIo(cwd));
|
|
7428
|
+
if (!result.ok)
|
|
7429
|
+
process.exit(1);
|
|
7430
|
+
}
|
|
5886
7431
|
function handleServe(parsed) {
|
|
5887
7432
|
if (isRunning(parsed.port)) {
|
|
5888
7433
|
log("iris_daemon_already_running", { port: parsed.port });
|
|
@@ -5894,7 +7439,7 @@ function handleServe(parsed) {
|
|
|
5894
7439
|
process.exit(1);
|
|
5895
7440
|
return;
|
|
5896
7441
|
}
|
|
5897
|
-
const daemonArgs = [DAEMON_INNER_COMMAND,
|
|
7442
|
+
const daemonArgs = [DAEMON_INNER_COMMAND, PORT_FLAG2, String(parsed.port)];
|
|
5898
7443
|
if (parsed.driveUrl !== void 0) {
|
|
5899
7444
|
daemonArgs.push(DRIVE_FLAG, parsed.driveUrl);
|
|
5900
7445
|
if (!parsed.headless)
|
|
@@ -5974,7 +7519,7 @@ function handleMcp(opts) {
|
|
|
5974
7519
|
process.exit(1);
|
|
5975
7520
|
return;
|
|
5976
7521
|
}
|
|
5977
|
-
const daemonArgs = [DAEMON_INNER_COMMAND,
|
|
7522
|
+
const daemonArgs = [DAEMON_INNER_COMMAND, PORT_FLAG2, String(port)];
|
|
5978
7523
|
if (driveUrl !== void 0) {
|
|
5979
7524
|
daemonArgs.push(DRIVE_FLAG, driveUrl);
|
|
5980
7525
|
if (!headless)
|
|
@@ -6013,6 +7558,9 @@ function main() {
|
|
|
6013
7558
|
log("iris_usage_error", { message: parsed.message });
|
|
6014
7559
|
process.exit(1);
|
|
6015
7560
|
break;
|
|
7561
|
+
case "init":
|
|
7562
|
+
handleInit(parsed);
|
|
7563
|
+
break;
|
|
6016
7564
|
case "serve":
|
|
6017
7565
|
handleServe(parsed);
|
|
6018
7566
|
break;
|
|
@@ -6033,6 +7581,18 @@ function main() {
|
|
|
6033
7581
|
break;
|
|
6034
7582
|
}
|
|
6035
7583
|
}
|
|
6036
|
-
|
|
7584
|
+
function isEntryPoint() {
|
|
7585
|
+
const argv1 = process.argv[1];
|
|
7586
|
+
if (argv1 === void 0)
|
|
7587
|
+
return false;
|
|
7588
|
+
if (import.meta.url === pathToFileURL(argv1).href)
|
|
7589
|
+
return true;
|
|
7590
|
+
try {
|
|
7591
|
+
return import.meta.url === pathToFileURL(realpathSync(argv1)).href;
|
|
7592
|
+
} catch {
|
|
7593
|
+
return false;
|
|
7594
|
+
}
|
|
7595
|
+
}
|
|
7596
|
+
if (isEntryPoint()) {
|
|
6037
7597
|
main();
|
|
6038
7598
|
}
|