@syrin/iris 0.4.0 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +3488 -1050
- package/dist/index.js +419 -114
- package/dist/next.js +5 -1
- package/dist/server.d.ts +19 -1
- package/dist/server.js +2406 -1005
- package/dist/test.js +2248 -1084
- package/dist/vite.d.ts +43 -0
- package/dist/vite.js +144 -0
- package/package.json +15 -10
package/dist/server.js
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
// ../server/dist/index.js
|
|
2
|
-
import { join as
|
|
2
|
+
import { join as join5 } from "path";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
|
|
5
5
|
// ../protocol/dist/constants.js
|
|
6
6
|
var IRIS_DEFAULT_PORT = 4400;
|
|
7
7
|
var IRIS_WS_PATH = "/iris";
|
|
8
|
+
var IRIS_PROTOCOL_VERSION = 1;
|
|
9
|
+
var TRANSPORT_LIMITS = {
|
|
10
|
+
MAX_MESSAGE_BYTES: 1024 * 1024,
|
|
11
|
+
MAX_MESSAGES_PER_SECOND: 1e3,
|
|
12
|
+
MAX_SESSIONS: 32,
|
|
13
|
+
MAX_PENDING_CONNECTIONS: 16,
|
|
14
|
+
HELLO_TIMEOUT_MS: 5e3,
|
|
15
|
+
MAX_BUFFER_BYTES: 8 * 1024 * 1024,
|
|
16
|
+
MAX_SESSION_ID_LENGTH: 128,
|
|
17
|
+
MAX_URL_LENGTH: 4096,
|
|
18
|
+
MAX_TITLE_LENGTH: 512,
|
|
19
|
+
MAX_ADAPTERS: 32,
|
|
20
|
+
MAX_ADAPTER_NAME_LENGTH: 128,
|
|
21
|
+
MAX_TOKEN_LENGTH: 512,
|
|
22
|
+
MAX_COMMAND_ID_LENGTH: 128,
|
|
23
|
+
MAX_COMMAND_NAME_LENGTH: 128,
|
|
24
|
+
MAX_REF_LENGTH: 128,
|
|
25
|
+
MAX_ERROR_LENGTH: 4096,
|
|
26
|
+
MAX_SERIALIZE_DEPTH: 8,
|
|
27
|
+
MAX_COLLECTION_ITEMS: 200,
|
|
28
|
+
MAX_OBJECT_KEYS: 200,
|
|
29
|
+
MAX_STRING_LENGTH: 64 * 1024
|
|
30
|
+
};
|
|
31
|
+
var DANGEROUS_ACTION_CONFIRM_ARG = "confirmDangerous";
|
|
8
32
|
var REPLAY_PROGRAM_VERSION = 1;
|
|
9
33
|
var IrisDir = {
|
|
10
34
|
ROOT: ".iris",
|
|
@@ -47,6 +71,7 @@ var CRAWL_DEFAULTS = {
|
|
|
47
71
|
/** HTTP status at/above which a response counts as a failed request. */
|
|
48
72
|
FAILED_STATUS: 400
|
|
49
73
|
};
|
|
74
|
+
var UpdateCheckIntervalMs = 24 * 60 * 60 * 1e3;
|
|
50
75
|
var CONTRACT_FILE_VERSION = 1;
|
|
51
76
|
var FROM_DISK_ARG = "fromDisk";
|
|
52
77
|
var ContractReadError = {
|
|
@@ -105,7 +130,7 @@ var ReplayStatus = {
|
|
|
105
130
|
DRIFT: "drift",
|
|
106
131
|
// an anchor missed (testid renamed / signal not observed) — legible drift returned
|
|
107
132
|
ERROR: "error"
|
|
108
|
-
// the flow
|
|
133
|
+
// the flow could not load or a resolved action failed
|
|
109
134
|
};
|
|
110
135
|
var DriftReason = {
|
|
111
136
|
TESTID_NOT_FOUND: "testid_not_found",
|
|
@@ -139,8 +164,10 @@ var HealStatus = {
|
|
|
139
164
|
// drift exists but no proposal cleared the confidence floor
|
|
140
165
|
NOTHING_TO_HEAL: "nothing_to_heal",
|
|
141
166
|
// replay was green
|
|
167
|
+
CONSEQUENCE_BROKEN: "consequence_broken",
|
|
168
|
+
// rebind resolves a locator but the flow's success consequence no longer fires — REFUSED (file untouched)
|
|
142
169
|
ERROR: "error"
|
|
143
|
-
// flow missing/malformed/invalid-name
|
|
170
|
+
// flow missing/malformed/invalid-name, or a resolved action failed
|
|
144
171
|
};
|
|
145
172
|
var HEAL_CONFIDENCE_MIN = 0.5;
|
|
146
173
|
var AnnotationTarget = {
|
|
@@ -156,7 +183,8 @@ var AnnotationErrorCode = {
|
|
|
156
183
|
var COMPILED_PREDICATE_PREFIX = "will";
|
|
157
184
|
var RING_BUFFER_DEFAULTS = {
|
|
158
185
|
MAX_EVENTS: 2e3,
|
|
159
|
-
MAX_AGE_MS: 6e4
|
|
186
|
+
MAX_AGE_MS: 6e4,
|
|
187
|
+
MAX_BYTES: TRANSPORT_LIMITS.MAX_BUFFER_BYTES
|
|
160
188
|
};
|
|
161
189
|
var EventType = {
|
|
162
190
|
DOM_ADDED: "dom.added",
|
|
@@ -341,6 +369,8 @@ var MessageKind = {
|
|
|
341
369
|
|
|
342
370
|
// ../protocol/dist/messages.js
|
|
343
371
|
import { z } from "zod";
|
|
372
|
+
var sessionIdSchema = z.string().min(1).max(TRANSPORT_LIMITS.MAX_SESSION_ID_LENGTH);
|
|
373
|
+
var refSchema = z.string().max(TRANSPORT_LIMITS.MAX_REF_LENGTH);
|
|
344
374
|
var HumanControlDataSchema = z.object({
|
|
345
375
|
kind: z.nativeEnum(HumanControlKind),
|
|
346
376
|
text: z.string().optional()
|
|
@@ -348,35 +378,37 @@ var HumanControlDataSchema = z.object({
|
|
|
348
378
|
var IrisEventSchema = z.object({
|
|
349
379
|
t: z.number(),
|
|
350
380
|
type: z.nativeEnum(EventType),
|
|
351
|
-
sessionId:
|
|
381
|
+
sessionId: sessionIdSchema,
|
|
352
382
|
/** Stable element reference this event concerns, when applicable (e.g. "e7"). */
|
|
353
|
-
ref:
|
|
383
|
+
ref: refSchema.optional(),
|
|
354
384
|
/** Event-type-specific payload. Kept open here; refined per observer at the edges. */
|
|
355
385
|
data: z.record(z.unknown()).default({})
|
|
356
386
|
});
|
|
357
387
|
var HelloMessageSchema = z.object({
|
|
358
388
|
kind: z.literal(MessageKind.HELLO),
|
|
359
|
-
protocolVersion: z.
|
|
360
|
-
sessionId:
|
|
361
|
-
url: z.string(),
|
|
362
|
-
title: z.string(),
|
|
363
|
-
adapters: z.array(z.string()),
|
|
389
|
+
protocolVersion: z.literal(IRIS_PROTOCOL_VERSION),
|
|
390
|
+
sessionId: sessionIdSchema,
|
|
391
|
+
url: z.string().max(TRANSPORT_LIMITS.MAX_URL_LENGTH),
|
|
392
|
+
title: z.string().max(TRANSPORT_LIMITS.MAX_TITLE_LENGTH),
|
|
393
|
+
adapters: z.array(z.string().max(TRANSPORT_LIMITS.MAX_ADAPTER_NAME_LENGTH)).max(TRANSPORT_LIMITS.MAX_ADAPTERS),
|
|
394
|
+
/** Optional browser/bridge pairing token. Required when the bridge configures one. */
|
|
395
|
+
token: z.string().max(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH).optional(),
|
|
364
396
|
/** Whether the app has advertised a capability registry (iris.describe). */
|
|
365
397
|
hasCapabilities: z.boolean().optional()
|
|
366
398
|
});
|
|
367
399
|
var CommandMessageSchema = z.object({
|
|
368
400
|
kind: z.literal(MessageKind.COMMAND),
|
|
369
|
-
id: z.string(),
|
|
370
|
-
sessionId:
|
|
371
|
-
name: z.string(),
|
|
401
|
+
id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
|
|
402
|
+
sessionId: sessionIdSchema.optional(),
|
|
403
|
+
name: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_NAME_LENGTH),
|
|
372
404
|
args: z.record(z.unknown()).default({})
|
|
373
405
|
});
|
|
374
406
|
var CommandResultSchema = z.object({
|
|
375
407
|
kind: z.literal(MessageKind.COMMAND_RESULT),
|
|
376
|
-
id: z.string(),
|
|
408
|
+
id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
|
|
377
409
|
ok: z.boolean(),
|
|
378
410
|
result: z.unknown().optional(),
|
|
379
|
-
error: z.string().optional()
|
|
411
|
+
error: z.string().max(TRANSPORT_LIMITS.MAX_ERROR_LENGTH).optional()
|
|
380
412
|
});
|
|
381
413
|
var EventMessageSchema = z.object({
|
|
382
414
|
kind: z.literal(MessageKind.EVENT),
|
|
@@ -389,6 +421,25 @@ var IrisMessageSchema = z.discriminatedUnion("kind", [
|
|
|
389
421
|
EventMessageSchema
|
|
390
422
|
]);
|
|
391
423
|
|
|
424
|
+
// ../protocol/dist/security.js
|
|
425
|
+
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;
|
|
426
|
+
function isLoopbackHostname(hostname) {
|
|
427
|
+
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
428
|
+
if (normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
const octets = normalized.split(".");
|
|
432
|
+
return octets.length === 4 && octets[0] === "127" && octets.every((octet) => {
|
|
433
|
+
if (!/^\d{1,3}$/.test(octet))
|
|
434
|
+
return false;
|
|
435
|
+
const value = Number(octet);
|
|
436
|
+
return value >= 0 && value <= 255;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function isDangerousActionText(text) {
|
|
440
|
+
return DANGEROUS_ACTION.test(text.replace(/[_-]+/g, " "));
|
|
441
|
+
}
|
|
442
|
+
|
|
392
443
|
// ../protocol/dist/toon.js
|
|
393
444
|
var ROLE_MAP = {
|
|
394
445
|
button: "btn",
|
|
@@ -649,21 +700,116 @@ var AnnotationSchema = z2.discriminatedUnion("kind", [
|
|
|
649
700
|
})
|
|
650
701
|
]);
|
|
651
702
|
|
|
703
|
+
// ../server/dist/http-server.js
|
|
704
|
+
import * as http from "http";
|
|
705
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
706
|
+
|
|
707
|
+
// ../server/dist/log.js
|
|
708
|
+
function log(event, fields = {}) {
|
|
709
|
+
const line = JSON.stringify({ event, ...fields });
|
|
710
|
+
process.stderr.write(`${line}
|
|
711
|
+
`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ../server/dist/http-server.js
|
|
715
|
+
var MCP_SSE_PATH = "/mcp/sse";
|
|
716
|
+
var MCP_MESSAGE_PATH = "/mcp/message";
|
|
717
|
+
function createSharedServer() {
|
|
718
|
+
let mcpFactory;
|
|
719
|
+
const transports = /* @__PURE__ */ new Map();
|
|
720
|
+
const httpServer = http.createServer((req, res) => {
|
|
721
|
+
const url = req.url ?? "/";
|
|
722
|
+
if (req.method === "GET" && url === MCP_SSE_PATH) {
|
|
723
|
+
if (mcpFactory === void 0) {
|
|
724
|
+
res.writeHead(503, { "Content-Type": "text/plain" });
|
|
725
|
+
res.end("MCP server not ready");
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const mcpServer = mcpFactory();
|
|
729
|
+
const transport = new SSEServerTransport(MCP_MESSAGE_PATH, res);
|
|
730
|
+
const sid = transport.sessionId;
|
|
731
|
+
transports.set(sid, transport);
|
|
732
|
+
res.on("close", () => {
|
|
733
|
+
transports.delete(sid);
|
|
734
|
+
transport.close().catch(() => void 0);
|
|
735
|
+
mcpServer.close().catch(() => void 0);
|
|
736
|
+
log("mcp_client_disconnected", { sessionId: sid });
|
|
737
|
+
});
|
|
738
|
+
mcpServer.connect(transport).then(() => {
|
|
739
|
+
log("mcp_client_connected", { sessionId: sid });
|
|
740
|
+
}).catch((err) => {
|
|
741
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
742
|
+
log("mcp_connect_error", { error: message });
|
|
743
|
+
});
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (req.method === "POST" && url.startsWith(MCP_MESSAGE_PATH)) {
|
|
747
|
+
const parsed = new URL(url, "http://localhost");
|
|
748
|
+
const sessionId = parsed.searchParams.get("sessionId");
|
|
749
|
+
if (sessionId === null) {
|
|
750
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
751
|
+
res.end("missing sessionId");
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const transport = transports.get(sessionId);
|
|
755
|
+
if (transport === void 0) {
|
|
756
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
757
|
+
res.end("session not found");
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
transport.handlePostMessage(req, res).catch((err) => {
|
|
761
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
762
|
+
log("mcp_message_error", { error: message });
|
|
763
|
+
});
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
767
|
+
res.end("not found");
|
|
768
|
+
});
|
|
769
|
+
function attachMcp(factory) {
|
|
770
|
+
mcpFactory = factory;
|
|
771
|
+
}
|
|
772
|
+
async function close() {
|
|
773
|
+
for (const transport of transports.values()) {
|
|
774
|
+
await transport.close();
|
|
775
|
+
}
|
|
776
|
+
transports.clear();
|
|
777
|
+
await new Promise((resolve, reject) => {
|
|
778
|
+
httpServer.close((err) => {
|
|
779
|
+
if (err !== void 0 && err !== null)
|
|
780
|
+
reject(err);
|
|
781
|
+
else
|
|
782
|
+
resolve();
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
return { httpServer, attachMcp, close };
|
|
787
|
+
}
|
|
788
|
+
|
|
652
789
|
// ../server/dist/bridge.js
|
|
790
|
+
import { timingSafeEqual } from "crypto";
|
|
791
|
+
import * as http2 from "http";
|
|
653
792
|
import { WebSocketServer } from "ws";
|
|
654
793
|
|
|
655
794
|
// ../server/dist/events/ring-buffer.js
|
|
656
795
|
var RingBuffer = class {
|
|
657
796
|
#maxEvents;
|
|
658
797
|
#maxAgeMs;
|
|
798
|
+
#maxBytes;
|
|
659
799
|
#events = [];
|
|
800
|
+
#eventBytes = [];
|
|
801
|
+
#totalBytes = 0;
|
|
660
802
|
#droppedCount = 0;
|
|
661
803
|
constructor(options = {}) {
|
|
662
804
|
this.#maxEvents = options.maxEvents ?? RING_BUFFER_DEFAULTS.MAX_EVENTS;
|
|
663
805
|
this.#maxAgeMs = options.maxAgeMs ?? RING_BUFFER_DEFAULTS.MAX_AGE_MS;
|
|
806
|
+
this.#maxBytes = options.maxBytes ?? RING_BUFFER_DEFAULTS.MAX_BYTES;
|
|
664
807
|
}
|
|
665
808
|
push(event, now) {
|
|
666
809
|
this.#events.push(event);
|
|
810
|
+
const bytes = Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
811
|
+
this.#eventBytes.push(bytes);
|
|
812
|
+
this.#totalBytes += bytes;
|
|
667
813
|
this.#evict(now);
|
|
668
814
|
}
|
|
669
815
|
/** Events at or after a given timestamp cursor. */
|
|
@@ -689,10 +835,14 @@ var RingBuffer = class {
|
|
|
689
835
|
#evict(now) {
|
|
690
836
|
const before = this.#events.length;
|
|
691
837
|
const cutoff = now - this.#maxAgeMs;
|
|
692
|
-
|
|
693
|
-
this.#events
|
|
838
|
+
while (this.#events.length > this.#maxEvents || this.#totalBytes > this.#maxBytes && this.#events.length > 0) {
|
|
839
|
+
this.#events.shift();
|
|
840
|
+
this.#totalBytes -= this.#eventBytes.shift() ?? 0;
|
|
841
|
+
}
|
|
842
|
+
while ((this.#events[0]?.t ?? cutoff) < cutoff) {
|
|
843
|
+
this.#events.shift();
|
|
844
|
+
this.#totalBytes -= this.#eventBytes.shift() ?? 0;
|
|
694
845
|
}
|
|
695
|
-
this.#events = this.#events.filter((e) => e.t >= cutoff);
|
|
696
846
|
this.#droppedCount += before - this.#events.length;
|
|
697
847
|
}
|
|
698
848
|
/** Snapshot of buffer health for the agent — total events held and cumulative drops since connect. */
|
|
@@ -929,6 +1079,14 @@ var Session = class {
|
|
|
929
1079
|
this.#pending.delete(id);
|
|
930
1080
|
}
|
|
931
1081
|
}
|
|
1082
|
+
/** End this transport without letting a stale socket remove its replacement session. */
|
|
1083
|
+
disconnect(reason) {
|
|
1084
|
+
this.rejectAll(reason);
|
|
1085
|
+
try {
|
|
1086
|
+
this.#socket.close(1008, reason);
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
932
1090
|
// ── Live-control: state machine + human→agent inbox (server-owned) ───────────────
|
|
933
1091
|
getState() {
|
|
934
1092
|
return this.#state;
|
|
@@ -1050,11 +1208,15 @@ var Session = class {
|
|
|
1050
1208
|
var SessionManager = class {
|
|
1051
1209
|
#sessions = /* @__PURE__ */ new Map();
|
|
1052
1210
|
add(session) {
|
|
1211
|
+
const previous = this.#sessions.get(session.id);
|
|
1053
1212
|
this.#sessions.set(session.id, session);
|
|
1213
|
+
return previous;
|
|
1054
1214
|
}
|
|
1055
|
-
remove(
|
|
1056
|
-
this.#sessions.get(
|
|
1057
|
-
|
|
1215
|
+
remove(session) {
|
|
1216
|
+
if (this.#sessions.get(session.id) !== session)
|
|
1217
|
+
return false;
|
|
1218
|
+
session.rejectAll("session disconnected");
|
|
1219
|
+
return this.#sessions.delete(session.id);
|
|
1058
1220
|
}
|
|
1059
1221
|
get(sessionId) {
|
|
1060
1222
|
return this.#sessions.get(sessionId);
|
|
@@ -1071,7 +1233,16 @@ var SessionManager = class {
|
|
|
1071
1233
|
}
|
|
1072
1234
|
/**
|
|
1073
1235
|
* Resolve the target session. With an explicit id, returns it. With none and exactly
|
|
1074
|
-
* one connected, returns that.
|
|
1236
|
+
* one connected, returns that.
|
|
1237
|
+
*
|
|
1238
|
+
* With none and multiple connected, applies smart auto-selection:
|
|
1239
|
+
* 1. Prefer non-throttled sessions (not hidden + recently heard from).
|
|
1240
|
+
* 2. Within each tier, prefer lowest lastSeenMs (most recently active SDK heartbeat).
|
|
1241
|
+
* 3. If two or more non-throttled sessions are within 1 s of each other, throw —
|
|
1242
|
+
* genuinely ambiguous, agent must specify sessionId.
|
|
1243
|
+
* 4. If ALL sessions are throttled (e.g. user is working in their editor on another
|
|
1244
|
+
* desktop), skip the gap check and pick the freshest heartbeat. This lets the agent
|
|
1245
|
+
* keep working in the background without requiring sessionId every time.
|
|
1075
1246
|
*/
|
|
1076
1247
|
resolve(sessionId) {
|
|
1077
1248
|
if (sessionId !== void 0) {
|
|
@@ -1085,26 +1256,48 @@ var SessionManager = class {
|
|
|
1085
1256
|
if (this.#sessions.size === 0) {
|
|
1086
1257
|
throw new Error("no browser session connected \u2014 is your app running with @syrin/iris-browser enabled?");
|
|
1087
1258
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1259
|
+
const all = [...this.#sessions.values()];
|
|
1260
|
+
if (all.length === 1) {
|
|
1261
|
+
const [only] = all;
|
|
1262
|
+
if (only === void 0)
|
|
1263
|
+
throw new Error("session lookup failed");
|
|
1264
|
+
only.markAgentActivity();
|
|
1265
|
+
return only;
|
|
1091
1266
|
}
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1267
|
+
const scored = all.map((s) => ({ s, score: s.throttled() ? 1 : 0, ms: s.lastSeenMs() }));
|
|
1268
|
+
const bestScore = Math.min(...scored.map((x) => x.score));
|
|
1269
|
+
const candidates = scored.filter((x) => x.score === bestScore);
|
|
1270
|
+
candidates.sort((a, b) => a.ms - b.ms);
|
|
1271
|
+
const [best, runnerUp] = candidates;
|
|
1272
|
+
if (best === void 0)
|
|
1094
1273
|
throw new Error("session lookup failed");
|
|
1095
|
-
|
|
1096
|
-
|
|
1274
|
+
const allThrottled = bestScore === 1;
|
|
1275
|
+
const RECENCY_GAP_MS = allThrottled ? 0 : 1e3;
|
|
1276
|
+
const clearWinner = runnerUp === void 0 || best.ms + RECENCY_GAP_MS < runnerUp.ms;
|
|
1277
|
+
if (!clearWinner) {
|
|
1278
|
+
const detail = all.map((s) => `${s.id} (${s.throttled() ? "throttled" : "active"}, lastSeenMs=${s.lastSeenMs()})`).join(", ");
|
|
1279
|
+
throw new Error(`multiple sessions connected \u2014 pass sessionId to target one: ${detail}`);
|
|
1280
|
+
}
|
|
1281
|
+
best.s.markAgentActivity();
|
|
1282
|
+
return best.s;
|
|
1097
1283
|
}
|
|
1098
1284
|
};
|
|
1099
1285
|
|
|
1100
|
-
// ../server/dist/log.js
|
|
1101
|
-
function log(event, fields = {}) {
|
|
1102
|
-
const line = JSON.stringify({ event, ...fields });
|
|
1103
|
-
process.stderr.write(`${line}
|
|
1104
|
-
`);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
1286
|
// ../server/dist/bridge.js
|
|
1287
|
+
function normalizeOrigin(origin) {
|
|
1288
|
+
try {
|
|
1289
|
+
return new URL(origin).origin;
|
|
1290
|
+
} catch {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function tokensMatch(expected, received) {
|
|
1295
|
+
if (received === void 0)
|
|
1296
|
+
return false;
|
|
1297
|
+
const expectedBytes = Buffer.from(expected);
|
|
1298
|
+
const receivedBytes = Buffer.from(received);
|
|
1299
|
+
return expectedBytes.length === receivedBytes.length && timingSafeEqual(expectedBytes, receivedBytes);
|
|
1300
|
+
}
|
|
1108
1301
|
function rawToString(raw) {
|
|
1109
1302
|
if (typeof raw === "string")
|
|
1110
1303
|
return raw;
|
|
@@ -1120,31 +1313,132 @@ var Bridge = class {
|
|
|
1120
1313
|
ready;
|
|
1121
1314
|
#wss;
|
|
1122
1315
|
#clock;
|
|
1316
|
+
#token;
|
|
1317
|
+
#allowedOrigins;
|
|
1318
|
+
#maxMessagesPerSecond;
|
|
1319
|
+
#maxSessions;
|
|
1320
|
+
#maxPendingConnections;
|
|
1321
|
+
#helloTimeoutMs;
|
|
1322
|
+
#pendingConnections = 0;
|
|
1123
1323
|
constructor(options) {
|
|
1324
|
+
const host = options.host ?? "127.0.0.1";
|
|
1325
|
+
if ((options.token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
|
|
1326
|
+
throw new Error(`Iris pairing token exceeds ${String(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH)} characters`);
|
|
1327
|
+
}
|
|
1328
|
+
if (!isLoopbackHostname(host) && (options.token === void 0 || options.token.length === 0)) {
|
|
1329
|
+
throw new Error("a pairing token is required when the Iris bridge binds beyond localhost");
|
|
1330
|
+
}
|
|
1124
1331
|
this.#clock = options.clock ?? (() => Date.now());
|
|
1125
|
-
this.#
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
this
|
|
1131
|
-
|
|
1132
|
-
|
|
1332
|
+
this.#token = options.token !== void 0 && options.token.length > 0 ? options.token : void 0;
|
|
1333
|
+
this.#allowedOrigins = new Set((options.allowedOrigins ?? []).map(normalizeOrigin).filter((origin) => origin !== null));
|
|
1334
|
+
this.#maxMessagesPerSecond = options.maxMessagesPerSecond ?? TRANSPORT_LIMITS.MAX_MESSAGES_PER_SECOND;
|
|
1335
|
+
this.#maxSessions = options.maxSessions ?? TRANSPORT_LIMITS.MAX_SESSIONS;
|
|
1336
|
+
this.#maxPendingConnections = options.maxPendingConnections ?? TRANSPORT_LIMITS.MAX_PENDING_CONNECTIONS;
|
|
1337
|
+
this.#helloTimeoutMs = options.helloTimeoutMs ?? TRANSPORT_LIMITS.HELLO_TIMEOUT_MS;
|
|
1338
|
+
if (options.server !== void 0) {
|
|
1339
|
+
const srv = options.server;
|
|
1340
|
+
this.#wss = new WebSocketServer({
|
|
1341
|
+
server: srv,
|
|
1342
|
+
path: IRIS_WS_PATH,
|
|
1343
|
+
maxPayload: TRANSPORT_LIMITS.MAX_MESSAGE_BYTES,
|
|
1344
|
+
verifyClient: ({ origin }, done) => {
|
|
1345
|
+
const allowed = this.#originAllowed(origin);
|
|
1346
|
+
if (!allowed)
|
|
1347
|
+
log("origin_rejected", { origin: origin ?? "missing" });
|
|
1348
|
+
done(allowed, 403, "Forbidden");
|
|
1349
|
+
}
|
|
1133
1350
|
});
|
|
1134
|
-
|
|
1351
|
+
this.ready = new Promise((resolve) => {
|
|
1352
|
+
if (srv.listening) {
|
|
1353
|
+
resolve(srv.address().port);
|
|
1354
|
+
} else {
|
|
1355
|
+
srv.once("listening", () => {
|
|
1356
|
+
resolve(srv.address().port);
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
} else {
|
|
1361
|
+
this.#wss = new WebSocketServer({
|
|
1362
|
+
port: options.port,
|
|
1363
|
+
host,
|
|
1364
|
+
path: IRIS_WS_PATH,
|
|
1365
|
+
maxPayload: TRANSPORT_LIMITS.MAX_MESSAGE_BYTES,
|
|
1366
|
+
verifyClient: ({ origin }, done) => {
|
|
1367
|
+
const allowed = this.#originAllowed(origin);
|
|
1368
|
+
if (!allowed)
|
|
1369
|
+
log("origin_rejected", { origin: origin ?? "missing" });
|
|
1370
|
+
done(allowed, 403, "Forbidden");
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
this.ready = new Promise((resolve) => {
|
|
1374
|
+
this.#wss.on("listening", () => {
|
|
1375
|
+
resolve(this.#wss.address().port);
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1135
1379
|
this.#wss.on("connection", (socket) => {
|
|
1136
1380
|
this.#onConnection(socket);
|
|
1137
1381
|
});
|
|
1138
1382
|
}
|
|
1139
1383
|
#onConnection(socket) {
|
|
1384
|
+
if (this.#pendingConnections >= this.#maxPendingConnections) {
|
|
1385
|
+
socket.close(1013, "too many pending handshakes");
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
this.#pendingConnections += 1;
|
|
1389
|
+
let awaitingHello = true;
|
|
1140
1390
|
let session;
|
|
1391
|
+
let messageWindowStartedAt = this.#clock();
|
|
1392
|
+
let messagesInWindow = 0;
|
|
1393
|
+
const releasePending = () => {
|
|
1394
|
+
if (!awaitingHello)
|
|
1395
|
+
return;
|
|
1396
|
+
awaitingHello = false;
|
|
1397
|
+
this.#pendingConnections -= 1;
|
|
1398
|
+
};
|
|
1399
|
+
const helloTimer = setTimeout(() => {
|
|
1400
|
+
if (!awaitingHello)
|
|
1401
|
+
return;
|
|
1402
|
+
releasePending();
|
|
1403
|
+
socket.close(1008, "hello timeout");
|
|
1404
|
+
}, this.#helloTimeoutMs);
|
|
1141
1405
|
socket.on("message", (raw) => {
|
|
1406
|
+
const now = this.#clock();
|
|
1407
|
+
if (now - messageWindowStartedAt >= 1e3) {
|
|
1408
|
+
messageWindowStartedAt = now;
|
|
1409
|
+
messagesInWindow = 0;
|
|
1410
|
+
}
|
|
1411
|
+
messagesInWindow += 1;
|
|
1412
|
+
if (messagesInWindow > this.#maxMessagesPerSecond) {
|
|
1413
|
+
log("message_rate_exceeded", {});
|
|
1414
|
+
socket.close(1008, "message rate exceeded");
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1142
1417
|
const parsed = this.#parse(rawToString(raw));
|
|
1143
|
-
if (parsed === null)
|
|
1418
|
+
if (parsed === null) {
|
|
1419
|
+
socket.close(1008, "invalid message");
|
|
1144
1420
|
return;
|
|
1421
|
+
}
|
|
1145
1422
|
if (parsed.kind === MessageKind.HELLO) {
|
|
1423
|
+
if (session !== void 0) {
|
|
1424
|
+
socket.close(1008, "hello already received");
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (this.#token !== void 0 && !tokensMatch(this.#token, parsed.token)) {
|
|
1428
|
+
log("authentication_failed", {});
|
|
1429
|
+
socket.close(1008, "authentication failed");
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const existing = this.sessions.get(parsed.sessionId);
|
|
1433
|
+
if (existing === void 0 && this.sessions.count() >= this.#maxSessions) {
|
|
1434
|
+
socket.close(1013, "session limit reached");
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
clearTimeout(helloTimer);
|
|
1438
|
+
releasePending();
|
|
1146
1439
|
session = new Session(parsed, socket, this.#clock);
|
|
1147
|
-
this.sessions.add(session);
|
|
1440
|
+
const replaced = this.sessions.add(session);
|
|
1441
|
+
replaced?.disconnect("session replaced by a newer connection");
|
|
1148
1442
|
log("session_connected", { sessionId: session.id, url: session.url });
|
|
1149
1443
|
return;
|
|
1150
1444
|
}
|
|
@@ -1158,15 +1452,28 @@ var Bridge = class {
|
|
|
1158
1452
|
}
|
|
1159
1453
|
});
|
|
1160
1454
|
socket.on("close", () => {
|
|
1455
|
+
clearTimeout(helloTimer);
|
|
1456
|
+
releasePending();
|
|
1161
1457
|
if (session !== void 0) {
|
|
1162
|
-
this.sessions.remove(session
|
|
1163
|
-
|
|
1458
|
+
if (this.sessions.remove(session)) {
|
|
1459
|
+
log("session_disconnected", { sessionId: session.id });
|
|
1460
|
+
}
|
|
1164
1461
|
}
|
|
1165
1462
|
});
|
|
1166
1463
|
socket.on("error", (err) => {
|
|
1167
1464
|
log("socket_error", { error: err.message });
|
|
1168
1465
|
});
|
|
1169
1466
|
}
|
|
1467
|
+
#originAllowed(origin) {
|
|
1468
|
+
if (origin === void 0)
|
|
1469
|
+
return true;
|
|
1470
|
+
const normalized = normalizeOrigin(origin);
|
|
1471
|
+
if (normalized === null)
|
|
1472
|
+
return false;
|
|
1473
|
+
if (this.#allowedOrigins.has(normalized))
|
|
1474
|
+
return true;
|
|
1475
|
+
return isLoopbackHostname(new URL(normalized).hostname);
|
|
1476
|
+
}
|
|
1170
1477
|
#parse(text) {
|
|
1171
1478
|
let json;
|
|
1172
1479
|
try {
|
|
@@ -1183,6 +1490,8 @@ var Bridge = class {
|
|
|
1183
1490
|
}
|
|
1184
1491
|
close() {
|
|
1185
1492
|
return new Promise((resolve) => {
|
|
1493
|
+
for (const client of this.#wss.clients)
|
|
1494
|
+
client.terminate();
|
|
1186
1495
|
this.#wss.close(() => {
|
|
1187
1496
|
resolve();
|
|
1188
1497
|
});
|
|
@@ -1297,6 +1606,7 @@ var IrisTool = {
|
|
|
1297
1606
|
STATE: "iris_state",
|
|
1298
1607
|
CAPABILITIES: "iris_capabilities",
|
|
1299
1608
|
CONTRACT_SAVE: "iris_contract_save",
|
|
1609
|
+
DOMAIN: "iris_domain",
|
|
1300
1610
|
FLOW_SAVE: "iris_flow_save",
|
|
1301
1611
|
FLOW_LIST: "iris_flow_list",
|
|
1302
1612
|
FLOW_LOAD: "iris_flow_load",
|
|
@@ -1330,151 +1640,526 @@ var IrisTool = {
|
|
|
1330
1640
|
/** Navigate the connected browser tab to a URL. */
|
|
1331
1641
|
NAVIGATE: "iris_navigate",
|
|
1332
1642
|
/** Reload the connected browser tab (soft or hard). */
|
|
1333
|
-
REFRESH: "iris_refresh"
|
|
1643
|
+
REFRESH: "iris_refresh",
|
|
1644
|
+
/** Report running version, latest available, changelog, and breaking changes. */
|
|
1645
|
+
VERSION_INFO: "iris_version_info",
|
|
1646
|
+
/** Install the latest server version and restart (Claude Code reconnects automatically). */
|
|
1647
|
+
APPLY_UPDATE: "iris_apply_update",
|
|
1648
|
+
/** Restore the previous server version and restart. */
|
|
1649
|
+
ROLLBACK: "iris_rollback"
|
|
1334
1650
|
};
|
|
1335
1651
|
|
|
1336
|
-
// ../server/dist/
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
baselines: join(root, IrisDir.BASELINES_SUBDIR),
|
|
1344
|
-
project: join(root, IrisDir.PROJECT_FILE),
|
|
1345
|
-
visual: join(root, IrisDir.VISUAL_SUBDIR)
|
|
1346
|
-
};
|
|
1347
|
-
}
|
|
1348
|
-
function visualPath(root, name) {
|
|
1349
|
-
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.png`);
|
|
1350
|
-
}
|
|
1351
|
-
function visualDiffPath(root, name) {
|
|
1352
|
-
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.diff.png`);
|
|
1353
|
-
}
|
|
1354
|
-
function flowPath(root, name) {
|
|
1355
|
-
return join(root, IrisDir.FLOWS_SUBDIR, `${name}.json`);
|
|
1356
|
-
}
|
|
1357
|
-
function isValidFlowName(name) {
|
|
1358
|
-
return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
|
|
1359
|
-
}
|
|
1360
|
-
function baselinePath(root, name) {
|
|
1361
|
-
return join(root, IrisDir.BASELINES_SUBDIR, `${name}.json`);
|
|
1362
|
-
}
|
|
1363
|
-
async function ensureIrisDir(fs, root) {
|
|
1364
|
-
const p = irisDirPaths(root);
|
|
1365
|
-
await fs.mkdir(p.root);
|
|
1366
|
-
await fs.mkdir(p.flows);
|
|
1367
|
-
await fs.mkdir(p.baselines);
|
|
1368
|
-
}
|
|
1369
|
-
var JSON_INDENT = 2;
|
|
1370
|
-
function stableSerialize(capabilities, generatedAt) {
|
|
1371
|
-
const envelope = {
|
|
1372
|
-
version: CONTRACT_FILE_VERSION,
|
|
1373
|
-
generatedAt,
|
|
1374
|
-
capabilities: {
|
|
1375
|
-
testids: [...capabilities.testids].sort(),
|
|
1376
|
-
signals: [...capabilities.signals].sort(),
|
|
1377
|
-
stores: [...capabilities.stores].sort(),
|
|
1378
|
-
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)
|
|
1652
|
+
// ../server/dist/tools/tools-helpers.js
|
|
1653
|
+
function parseInteractive(tree) {
|
|
1654
|
+
const items = [];
|
|
1655
|
+
for (const line of tree.split("\n")) {
|
|
1656
|
+
const match = /\(ref=(e\d+)\)/.exec(line);
|
|
1657
|
+
if (match !== null) {
|
|
1658
|
+
items.push({ ref: match[1] ?? "", desc: line.replace(/\s*\(ref=e\d+\)/, "").trim() });
|
|
1379
1659
|
}
|
|
1380
|
-
};
|
|
1381
|
-
return `${JSON.stringify(envelope, null, JSON_INDENT)}
|
|
1382
|
-
`;
|
|
1383
|
-
}
|
|
1384
|
-
async function writeContract(fs, root, capabilities, now) {
|
|
1385
|
-
await ensureIrisDir(fs, root);
|
|
1386
|
-
await fs.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
|
|
1387
|
-
}
|
|
1388
|
-
async function readContract(fs, root) {
|
|
1389
|
-
const path = irisDirPaths(root).contract;
|
|
1390
|
-
if (!await fs.exists(path))
|
|
1391
|
-
return { ok: false, reason: ContractReadError.MISSING };
|
|
1392
|
-
let text;
|
|
1393
|
-
try {
|
|
1394
|
-
text = await fs.readFile(path);
|
|
1395
|
-
} catch (error) {
|
|
1396
|
-
return {
|
|
1397
|
-
ok: false,
|
|
1398
|
-
reason: fs.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1401
|
-
let parsed;
|
|
1402
|
-
try {
|
|
1403
|
-
parsed = JSON.parse(text);
|
|
1404
|
-
} catch {
|
|
1405
|
-
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
1406
1660
|
}
|
|
1407
|
-
|
|
1408
|
-
if (!result.success)
|
|
1409
|
-
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
1410
|
-
return { ok: true, capabilities: result.data.capabilities, generatedAt: result.data.generatedAt };
|
|
1661
|
+
return items;
|
|
1411
1662
|
}
|
|
1412
|
-
|
|
1413
|
-
// ../server/dist/flows/flows.js
|
|
1414
1663
|
function asString(value) {
|
|
1415
1664
|
return typeof value === "string" ? value : void 0;
|
|
1416
1665
|
}
|
|
1666
|
+
function asNumber(value) {
|
|
1667
|
+
return typeof value === "number" ? value : void 0;
|
|
1668
|
+
}
|
|
1417
1669
|
function asRecord(value) {
|
|
1418
1670
|
return typeof value === "object" && value !== null ? value : {};
|
|
1419
1671
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1672
|
+
|
|
1673
|
+
// ../server/dist/flows/replay.js
|
|
1674
|
+
function asString2(value) {
|
|
1675
|
+
return typeof value === "string" ? value : void 0;
|
|
1422
1676
|
}
|
|
1423
|
-
function
|
|
1424
|
-
|
|
1425
|
-
const by = asString(sub["by"]);
|
|
1426
|
-
const value = asString(sub["value"]);
|
|
1427
|
-
const action = asString(sub["action"]);
|
|
1428
|
-
const args = asRecord(sub["args"]);
|
|
1429
|
-
if (by === QueryBy.TESTID && value !== void 0) {
|
|
1430
|
-
return buildStep(IrisTool.ACT, { kind: AnchorKind.TESTID, value }, action, args, false);
|
|
1431
|
-
}
|
|
1432
|
-
return buildStep(IrisTool.ACT, degradedAnchor(), action, args, true);
|
|
1677
|
+
function asRecord2(value) {
|
|
1678
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
1433
1679
|
}
|
|
1434
|
-
function
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
return step;
|
|
1680
|
+
function replayActionArgs(value, confirmDangerous = false) {
|
|
1681
|
+
const args = { ...asRecord2(value) };
|
|
1682
|
+
delete args[DANGEROUS_ACTION_CONFIRM_ARG];
|
|
1683
|
+
if (confirmDangerous)
|
|
1684
|
+
args[DANGEROUS_ACTION_CONFIRM_ARG] = true;
|
|
1685
|
+
return args;
|
|
1441
1686
|
}
|
|
1442
|
-
function
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
out2.expect = step.expect;
|
|
1453
|
-
return out2;
|
|
1687
|
+
function compileActStep(args, res) {
|
|
1688
|
+
const testid = asString2(asRecord2(res)["testid"]);
|
|
1689
|
+
const action = asString2(args["action"]) ?? "";
|
|
1690
|
+
const actArgs = replayActionArgs(args["args"]);
|
|
1691
|
+
if (testid !== void 0) {
|
|
1692
|
+
return {
|
|
1693
|
+
tool: IrisTool.ACT,
|
|
1694
|
+
stable: true,
|
|
1695
|
+
args: { by: QueryBy.TESTID, value: testid, action, args: actArgs }
|
|
1696
|
+
};
|
|
1454
1697
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
if (step.expect !== void 0)
|
|
1461
|
-
out.expect = step.expect;
|
|
1462
|
-
return out;
|
|
1698
|
+
return {
|
|
1699
|
+
tool: IrisTool.ACT,
|
|
1700
|
+
stable: false,
|
|
1701
|
+
args: { ref: asString2(args["ref"]) ?? "", action, args: actArgs }
|
|
1702
|
+
};
|
|
1463
1703
|
}
|
|
1464
|
-
function
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1704
|
+
function compileSequenceStep(args, res) {
|
|
1705
|
+
const inputSteps = Array.isArray(args["steps"]) ? args["steps"] : [];
|
|
1706
|
+
const resolved = Array.isArray(asRecord2(res)["steps"]) ? asRecord2(res)["steps"] : [];
|
|
1707
|
+
let stable = inputSteps.length > 0;
|
|
1708
|
+
const subSteps = inputSteps.map((raw, i) => {
|
|
1709
|
+
const step = asRecord2(raw);
|
|
1710
|
+
const action = asString2(step["action"]) ?? "";
|
|
1711
|
+
const stepArgs = replayActionArgs(step["args"]);
|
|
1712
|
+
const testid = asString2(asRecord2(resolved[i])["testid"]);
|
|
1713
|
+
if (testid !== void 0) {
|
|
1714
|
+
return { by: QueryBy.TESTID, value: testid, action, args: stepArgs };
|
|
1715
|
+
}
|
|
1716
|
+
stable = false;
|
|
1717
|
+
return { ref: asString2(step["ref"]) ?? "", action, args: stepArgs };
|
|
1470
1718
|
});
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1719
|
+
return { tool: IrisTool.ACT_SEQUENCE, stable, args: { steps: subSteps } };
|
|
1720
|
+
}
|
|
1721
|
+
async function resolveRef(session, step) {
|
|
1722
|
+
const by = asString2(step.by);
|
|
1723
|
+
const value = asString2(step.value);
|
|
1724
|
+
if (by === QueryBy.TESTID && value !== void 0) {
|
|
1725
|
+
const result = await session.command(IrisCommand.QUERY, { by, value });
|
|
1726
|
+
if (!result.ok)
|
|
1727
|
+
throw new Error(result.error ?? "query failed");
|
|
1728
|
+
const elements = Array.isArray(asRecord2(result.result)["elements"]) ? asRecord2(result.result)["elements"] : [];
|
|
1729
|
+
const ref2 = asString2(asRecord2(elements[0])["ref"]);
|
|
1730
|
+
if (ref2 === void 0)
|
|
1731
|
+
throw new Error(`testid '${value}' did not resolve in current page`);
|
|
1732
|
+
return elements.length > 1 ? { ref: ref2, note: `ambiguous testid '${value}', used first match` } : { ref: ref2 };
|
|
1474
1733
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1734
|
+
const ref = asString2(step.ref);
|
|
1735
|
+
if (ref === void 0 || ref.length === 0)
|
|
1736
|
+
throw new Error("step has no testid or ref to resolve");
|
|
1737
|
+
return { ref, note: "replayed by stale ref (not portable across sessions)" };
|
|
1738
|
+
}
|
|
1739
|
+
async function replayProgram(session, program, confirmDangerous = false) {
|
|
1740
|
+
const results = [];
|
|
1741
|
+
for (const step of program.steps) {
|
|
1742
|
+
try {
|
|
1743
|
+
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
1744
|
+
const subs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
1745
|
+
const notes = [];
|
|
1746
|
+
const liveSteps = [];
|
|
1747
|
+
for (const raw of subs) {
|
|
1748
|
+
const sub = asRecord2(raw);
|
|
1749
|
+
const { ref, note } = await resolveRef(session, sub);
|
|
1750
|
+
if (note !== void 0)
|
|
1751
|
+
notes.push(note);
|
|
1752
|
+
liveSteps.push({
|
|
1753
|
+
ref,
|
|
1754
|
+
action: asString2(sub["action"]) ?? "",
|
|
1755
|
+
args: replayActionArgs(sub["args"], confirmDangerous)
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
const r = await session.command(IrisCommand.ACT_SEQUENCE, { steps: liveSteps });
|
|
1759
|
+
results.push(buildResult(step.tool, r.ok, r.error, notes));
|
|
1760
|
+
if (!r.ok)
|
|
1761
|
+
break;
|
|
1762
|
+
} else {
|
|
1763
|
+
const { ref, note } = await resolveRef(session, step.args);
|
|
1764
|
+
const r = await session.command(IrisCommand.ACT, {
|
|
1765
|
+
ref,
|
|
1766
|
+
action: asString2(step.args["action"]) ?? "",
|
|
1767
|
+
args: replayActionArgs(step.args["args"], confirmDangerous)
|
|
1768
|
+
});
|
|
1769
|
+
results.push(buildResult(step.tool, r.ok, r.error, note !== void 0 ? [note] : []));
|
|
1770
|
+
if (!r.ok)
|
|
1771
|
+
break;
|
|
1772
|
+
}
|
|
1773
|
+
} catch (e) {
|
|
1774
|
+
results.push({
|
|
1775
|
+
tool: step.tool,
|
|
1776
|
+
ok: false,
|
|
1777
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1778
|
+
});
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return results;
|
|
1783
|
+
}
|
|
1784
|
+
function buildResult(tool, ok, error, notes) {
|
|
1785
|
+
const base = { tool, ok };
|
|
1786
|
+
if (!ok)
|
|
1787
|
+
base.error = error ?? "command failed";
|
|
1788
|
+
if (notes.length > 0)
|
|
1789
|
+
base.note = notes.join("; ");
|
|
1790
|
+
return base;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// ../server/dist/flows/flow-replay.js
|
|
1794
|
+
function editDistance(a, b) {
|
|
1795
|
+
const s = a.toLowerCase();
|
|
1796
|
+
const t = b.toLowerCase();
|
|
1797
|
+
const rows = s.length + 1;
|
|
1798
|
+
const cols = t.length + 1;
|
|
1799
|
+
const prev = new Array(cols);
|
|
1800
|
+
const curr = new Array(cols);
|
|
1801
|
+
for (let j = 0; j < cols; j++)
|
|
1802
|
+
prev[j] = j;
|
|
1803
|
+
for (let i = 1; i < rows; i++) {
|
|
1804
|
+
curr[0] = i;
|
|
1805
|
+
for (let j = 1; j < cols; j++) {
|
|
1806
|
+
const cost = s[i - 1] === t[j - 1] ? 0 : 1;
|
|
1807
|
+
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
1808
|
+
}
|
|
1809
|
+
for (let j = 0; j < cols; j++)
|
|
1810
|
+
prev[j] = curr[j] ?? 0;
|
|
1811
|
+
}
|
|
1812
|
+
return prev[cols - 1] ?? 0;
|
|
1813
|
+
}
|
|
1814
|
+
function nearestTestid(missing, present) {
|
|
1815
|
+
let best = null;
|
|
1816
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1817
|
+
for (const candidate of present) {
|
|
1818
|
+
const distance = editDistance(missing, candidate);
|
|
1819
|
+
if (distance < bestDistance || distance === bestDistance && best !== null && candidate.length < best.length || distance === bestDistance && best !== null && candidate.length === best.length && candidate < best) {
|
|
1820
|
+
best = candidate;
|
|
1821
|
+
bestDistance = distance;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return best;
|
|
1825
|
+
}
|
|
1826
|
+
function readQuery(result) {
|
|
1827
|
+
if (!result.ok)
|
|
1828
|
+
return { refs: [] };
|
|
1829
|
+
const payload = asRecord(result.result);
|
|
1830
|
+
const elements = Array.isArray(payload["elements"]) ? payload["elements"] : [];
|
|
1831
|
+
const refs = elements.map((e) => asString(asRecord(e)["ref"]) ?? "").filter((r) => r.length > 0);
|
|
1832
|
+
const rawHint = payload["hint"];
|
|
1833
|
+
if (typeof rawHint === "object" && rawHint !== null) {
|
|
1834
|
+
const hint = asRecord(rawHint);
|
|
1835
|
+
const present = Array.isArray(hint["presentTestids"]) ? hint["presentTestids"].filter((t) => typeof t === "string") : [];
|
|
1836
|
+
return {
|
|
1837
|
+
refs,
|
|
1838
|
+
hint: {
|
|
1839
|
+
route: asString(hint["route"]) ?? "",
|
|
1840
|
+
presentTestids: present,
|
|
1841
|
+
presentRegions: [],
|
|
1842
|
+
knownEmptyState: hint["knownEmptyState"] === true
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
return { refs };
|
|
1847
|
+
}
|
|
1848
|
+
function nearestIsAmbiguous(missing, present) {
|
|
1849
|
+
if (present.length < 2)
|
|
1850
|
+
return false;
|
|
1851
|
+
let min = Number.POSITIVE_INFINITY;
|
|
1852
|
+
let count = 0;
|
|
1853
|
+
for (const candidate of present) {
|
|
1854
|
+
const distance = editDistance(missing, candidate);
|
|
1855
|
+
if (distance < min) {
|
|
1856
|
+
min = distance;
|
|
1857
|
+
count = 1;
|
|
1858
|
+
} else if (distance === min) {
|
|
1859
|
+
count += 1;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return count >= 2;
|
|
1863
|
+
}
|
|
1864
|
+
function testidDrift(value, hint) {
|
|
1865
|
+
const present = hint?.presentTestids ?? [];
|
|
1866
|
+
const drift = {
|
|
1867
|
+
reasonKind: DriftReason.TESTID_NOT_FOUND,
|
|
1868
|
+
reason: `testid "${value}" not found`,
|
|
1869
|
+
anchor: value,
|
|
1870
|
+
nearest: nearestTestid(value, present)
|
|
1871
|
+
};
|
|
1872
|
+
if (nearestIsAmbiguous(value, present))
|
|
1873
|
+
drift.ambiguous = true;
|
|
1874
|
+
return drift;
|
|
1875
|
+
}
|
|
1876
|
+
function anchorLabel(anchor) {
|
|
1877
|
+
if (anchor.kind === AnchorKind.TESTID)
|
|
1878
|
+
return anchor.value;
|
|
1879
|
+
if (anchor.kind === AnchorKind.SIGNAL)
|
|
1880
|
+
return anchor.name;
|
|
1881
|
+
return anchor.name ?? anchor.role;
|
|
1882
|
+
}
|
|
1883
|
+
async function runTestidStep(session, step, index, value, dynamic, confirmDangerous) {
|
|
1884
|
+
const queryResult = await session.command(IrisCommand.QUERY, { by: QueryBy.TESTID, value });
|
|
1885
|
+
const { refs, hint } = readQuery(queryResult);
|
|
1886
|
+
if (refs.length === 0) {
|
|
1887
|
+
return {
|
|
1888
|
+
step: index,
|
|
1889
|
+
tool: step.tool,
|
|
1890
|
+
anchor: value,
|
|
1891
|
+
ok: false,
|
|
1892
|
+
drift: testidDrift(value, hint)
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
const ref = refs[0] ?? "";
|
|
1896
|
+
const note = refs.length > 1 ? `ambiguous testid '${value}', used first match` : void 0;
|
|
1897
|
+
const act = await session.command(IrisCommand.ACT, {
|
|
1898
|
+
ref,
|
|
1899
|
+
action: step.action ?? "",
|
|
1900
|
+
args: replayActionArgs(step.args, confirmDangerous)
|
|
1901
|
+
});
|
|
1902
|
+
const result = { step: index, tool: step.tool, anchor: value, ok: act.ok };
|
|
1903
|
+
if (!act.ok) {
|
|
1904
|
+
result.error = act.error ?? "command failed";
|
|
1905
|
+
if (note !== void 0)
|
|
1906
|
+
result.note = note;
|
|
1907
|
+
return result;
|
|
1908
|
+
}
|
|
1909
|
+
const expectTestid = step.expect?.element?.testid;
|
|
1910
|
+
if (expectTestid !== void 0 && !dynamic.has(expectTestid)) {
|
|
1911
|
+
const expectQuery = await session.command(IrisCommand.QUERY, {
|
|
1912
|
+
by: QueryBy.TESTID,
|
|
1913
|
+
value: expectTestid
|
|
1914
|
+
});
|
|
1915
|
+
const expectRefs = readQuery(expectQuery);
|
|
1916
|
+
if (expectRefs.refs.length === 0) {
|
|
1917
|
+
return {
|
|
1918
|
+
step: index,
|
|
1919
|
+
tool: step.tool,
|
|
1920
|
+
anchor: expectTestid,
|
|
1921
|
+
ok: false,
|
|
1922
|
+
drift: testidDrift(expectTestid, expectRefs.hint)
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (note !== void 0)
|
|
1927
|
+
result.note = note;
|
|
1928
|
+
return result;
|
|
1929
|
+
}
|
|
1930
|
+
async function runSignalStep(session, step, index, name, waitForSignal, signalTimeoutMs) {
|
|
1931
|
+
const verdict = await waitForSignal(session, { kind: "signal", name }, signalTimeoutMs);
|
|
1932
|
+
if (verdict.pass)
|
|
1933
|
+
return { step: index, tool: step.tool, anchor: name, ok: true };
|
|
1934
|
+
return {
|
|
1935
|
+
step: index,
|
|
1936
|
+
tool: step.tool,
|
|
1937
|
+
anchor: name,
|
|
1938
|
+
ok: false,
|
|
1939
|
+
drift: {
|
|
1940
|
+
reasonKind: DriftReason.SIGNAL_NOT_OBSERVED,
|
|
1941
|
+
reason: `signal "${name}" not observed`,
|
|
1942
|
+
anchor: name,
|
|
1943
|
+
nearest: null
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
async function replayFlow(session, flow, waitForSignal, signalTimeoutMs, confirmDangerous = false) {
|
|
1948
|
+
const results = [];
|
|
1949
|
+
const dynamic = new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
1950
|
+
let index = 0;
|
|
1951
|
+
for (const step of flow.steps) {
|
|
1952
|
+
const label = anchorLabel(step.anchor);
|
|
1953
|
+
let result;
|
|
1954
|
+
if (step.anchor.kind === AnchorKind.SIGNAL) {
|
|
1955
|
+
result = await runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs);
|
|
1956
|
+
} else {
|
|
1957
|
+
result = await runTestidStep(session, step, index, label, dynamic, confirmDangerous);
|
|
1958
|
+
}
|
|
1959
|
+
results.push(result);
|
|
1960
|
+
if (result.drift !== void 0 || !result.ok)
|
|
1961
|
+
break;
|
|
1962
|
+
index += 1;
|
|
1963
|
+
}
|
|
1964
|
+
return results;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// ../server/dist/flows/heal.js
|
|
1968
|
+
function confidenceFor(from, to) {
|
|
1969
|
+
if (from === to)
|
|
1970
|
+
return 1;
|
|
1971
|
+
const span = Math.max(from.length, to.length);
|
|
1972
|
+
if (span === 0)
|
|
1973
|
+
return 1;
|
|
1974
|
+
const raw = 1 - editDistance(from, to) / span;
|
|
1975
|
+
if (raw >= 1)
|
|
1976
|
+
return 1;
|
|
1977
|
+
if (raw <= 0)
|
|
1978
|
+
return Number.EPSILON;
|
|
1979
|
+
return raw;
|
|
1980
|
+
}
|
|
1981
|
+
function applyHealChanges(flow, changes) {
|
|
1982
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
1983
|
+
for (const change of changes)
|
|
1984
|
+
byStep.set(change.step, change);
|
|
1985
|
+
const applied = [];
|
|
1986
|
+
const steps = flow.steps.map((step, index) => {
|
|
1987
|
+
const change = byStep.get(index);
|
|
1988
|
+
if (change === void 0 || step.anchor.kind !== AnchorKind.TESTID || step.anchor.value !== change.from) {
|
|
1989
|
+
return step;
|
|
1990
|
+
}
|
|
1991
|
+
applied.push(change);
|
|
1992
|
+
return { ...step, anchor: { kind: AnchorKind.TESTID, value: change.to } };
|
|
1993
|
+
});
|
|
1994
|
+
return { flow: { ...flow, steps }, applied };
|
|
1995
|
+
}
|
|
1996
|
+
function proposeRebindWith(drift, step, minConfidence) {
|
|
1997
|
+
if (drift.reasonKind !== DriftReason.TESTID_NOT_FOUND)
|
|
1998
|
+
return void 0;
|
|
1999
|
+
if (drift.ambiguous === true)
|
|
2000
|
+
return void 0;
|
|
2001
|
+
const to = drift.nearest;
|
|
2002
|
+
if (to === null)
|
|
2003
|
+
return void 0;
|
|
2004
|
+
const confidence = confidenceFor(drift.anchor, to);
|
|
2005
|
+
if (confidence < minConfidence)
|
|
2006
|
+
return void 0;
|
|
2007
|
+
return { step, from: drift.anchor, to, confidence };
|
|
2008
|
+
}
|
|
2009
|
+
function collectProposals(steps, minConfidence = HEAL_CONFIDENCE_MIN) {
|
|
2010
|
+
const proposals = [];
|
|
2011
|
+
for (const step of steps) {
|
|
2012
|
+
if (step.drift === void 0)
|
|
2013
|
+
continue;
|
|
2014
|
+
const proposal = proposeRebindWith(step.drift, step.step, minConfidence);
|
|
2015
|
+
if (proposal !== void 0)
|
|
2016
|
+
proposals.push(proposal);
|
|
2017
|
+
}
|
|
2018
|
+
return proposals;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// ../server/dist/project/iris-dir.js
|
|
2022
|
+
import { join } from "path";
|
|
2023
|
+
function irisDirPaths(root) {
|
|
2024
|
+
return {
|
|
2025
|
+
root,
|
|
2026
|
+
contract: join(root, IrisDir.CONTRACT_FILE),
|
|
2027
|
+
flows: join(root, IrisDir.FLOWS_SUBDIR),
|
|
2028
|
+
baselines: join(root, IrisDir.BASELINES_SUBDIR),
|
|
2029
|
+
project: join(root, IrisDir.PROJECT_FILE),
|
|
2030
|
+
visual: join(root, IrisDir.VISUAL_SUBDIR)
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
function visualPath(root, name) {
|
|
2034
|
+
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.png`);
|
|
2035
|
+
}
|
|
2036
|
+
function visualDiffPath(root, name) {
|
|
2037
|
+
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.diff.png`);
|
|
2038
|
+
}
|
|
2039
|
+
function flowPath(root, name) {
|
|
2040
|
+
return join(root, IrisDir.FLOWS_SUBDIR, `${name}.json`);
|
|
2041
|
+
}
|
|
2042
|
+
function isValidFlowName(name) {
|
|
2043
|
+
return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
|
|
2044
|
+
}
|
|
2045
|
+
function baselinePath(root, name) {
|
|
2046
|
+
return join(root, IrisDir.BASELINES_SUBDIR, `${name}.json`);
|
|
2047
|
+
}
|
|
2048
|
+
async function ensureIrisDir(fs2, root) {
|
|
2049
|
+
const p = irisDirPaths(root);
|
|
2050
|
+
await fs2.mkdir(p.root);
|
|
2051
|
+
await fs2.mkdir(p.flows);
|
|
2052
|
+
await fs2.mkdir(p.baselines);
|
|
2053
|
+
}
|
|
2054
|
+
var JSON_INDENT = 2;
|
|
2055
|
+
function stableSerialize(capabilities, generatedAt) {
|
|
2056
|
+
const envelope = {
|
|
2057
|
+
version: CONTRACT_FILE_VERSION,
|
|
2058
|
+
generatedAt,
|
|
2059
|
+
capabilities: {
|
|
2060
|
+
testids: [...capabilities.testids].sort(),
|
|
2061
|
+
signals: [...capabilities.signals].sort(),
|
|
2062
|
+
stores: [...capabilities.stores].sort(),
|
|
2063
|
+
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)
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
return `${JSON.stringify(envelope, null, JSON_INDENT)}
|
|
2067
|
+
`;
|
|
2068
|
+
}
|
|
2069
|
+
async function writeContract(fs2, root, capabilities, now) {
|
|
2070
|
+
await ensureIrisDir(fs2, root);
|
|
2071
|
+
await fs2.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
|
|
2072
|
+
}
|
|
2073
|
+
async function readContract(fs2, root) {
|
|
2074
|
+
const path = irisDirPaths(root).contract;
|
|
2075
|
+
if (!await fs2.exists(path))
|
|
2076
|
+
return { ok: false, reason: ContractReadError.MISSING };
|
|
2077
|
+
let text;
|
|
2078
|
+
try {
|
|
2079
|
+
text = await fs2.readFile(path);
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
return {
|
|
2082
|
+
ok: false,
|
|
2083
|
+
reason: fs2.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
let parsed;
|
|
2087
|
+
try {
|
|
2088
|
+
parsed = JSON.parse(text);
|
|
2089
|
+
} catch {
|
|
2090
|
+
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
2091
|
+
}
|
|
2092
|
+
const result = ContractFileSchema.safeParse(parsed);
|
|
2093
|
+
if (!result.success)
|
|
2094
|
+
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
2095
|
+
return { ok: true, capabilities: result.data.capabilities, generatedAt: result.data.generatedAt };
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// ../server/dist/flows/flows.js
|
|
2099
|
+
function asString3(value) {
|
|
2100
|
+
return typeof value === "string" ? value : void 0;
|
|
2101
|
+
}
|
|
2102
|
+
function asRecord3(value) {
|
|
2103
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
2104
|
+
}
|
|
2105
|
+
function degradedAnchor() {
|
|
2106
|
+
return { kind: AnchorKind.ROLE, role: DEGRADED_ANCHOR_ROLE };
|
|
2107
|
+
}
|
|
2108
|
+
function subStepToFlowStep(raw) {
|
|
2109
|
+
const sub = asRecord3(raw);
|
|
2110
|
+
const by = asString3(sub["by"]);
|
|
2111
|
+
const value = asString3(sub["value"]);
|
|
2112
|
+
const action = asString3(sub["action"]);
|
|
2113
|
+
const args = asRecord3(sub["args"]);
|
|
2114
|
+
if (by === QueryBy.TESTID && value !== void 0) {
|
|
2115
|
+
return buildStep(IrisTool.ACT, { kind: AnchorKind.TESTID, value }, action, args, false);
|
|
2116
|
+
}
|
|
2117
|
+
return buildStep(IrisTool.ACT, degradedAnchor(), action, args, true);
|
|
2118
|
+
}
|
|
2119
|
+
function buildStep(tool, anchor, action, args, degraded) {
|
|
2120
|
+
const step = { tool, anchor, args };
|
|
2121
|
+
if (action !== void 0)
|
|
2122
|
+
step.action = action;
|
|
2123
|
+
if (degraded)
|
|
2124
|
+
step.degraded = true;
|
|
2125
|
+
return step;
|
|
2126
|
+
}
|
|
2127
|
+
function recordedStepToFlowStep(step) {
|
|
2128
|
+
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
2129
|
+
const rawSubs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
2130
|
+
const subs = rawSubs.map(subStepToFlowStep);
|
|
2131
|
+
const degraded = subs.some((s) => s.degraded === true);
|
|
2132
|
+
const anchor = subs[0]?.anchor ?? degradedAnchor();
|
|
2133
|
+
const out2 = { tool: IrisTool.ACT_SEQUENCE, anchor, steps: subs };
|
|
2134
|
+
if (degraded)
|
|
2135
|
+
out2.degraded = true;
|
|
2136
|
+
if (step.expect !== void 0)
|
|
2137
|
+
out2.expect = step.expect;
|
|
2138
|
+
return out2;
|
|
2139
|
+
}
|
|
2140
|
+
const by = asString3(step.args["by"]);
|
|
2141
|
+
const value = asString3(step.args["value"]);
|
|
2142
|
+
const action = asString3(step.args["action"]);
|
|
2143
|
+
const args = asRecord3(step.args["args"]);
|
|
2144
|
+
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);
|
|
2145
|
+
if (step.expect !== void 0)
|
|
2146
|
+
out.expect = step.expect;
|
|
2147
|
+
return out;
|
|
2148
|
+
}
|
|
2149
|
+
function withAnnotations(flow, ann) {
|
|
2150
|
+
if (ann === void 0)
|
|
2151
|
+
return flow;
|
|
2152
|
+
const steps = flow.steps.map((step, i) => {
|
|
2153
|
+
const expect = ann.stepExpect.get(i);
|
|
2154
|
+
return expect === void 0 ? step : { ...step, expect };
|
|
2155
|
+
});
|
|
2156
|
+
const out = { ...flow, steps };
|
|
2157
|
+
if (ann.dynamic.length > 0) {
|
|
2158
|
+
out.dynamic = ann.dynamic.map((value) => ({ kind: AnchorKind.TESTID, value }));
|
|
2159
|
+
}
|
|
2160
|
+
if (ann.success !== void 0)
|
|
2161
|
+
out.success = ann.success;
|
|
2162
|
+
return out;
|
|
1478
2163
|
}
|
|
1479
2164
|
var JSON_INDENT2 = 2;
|
|
1480
2165
|
var FLOW_SUFFIX = ".json";
|
|
@@ -1482,8 +2167,8 @@ var FlowStore = class {
|
|
|
1482
2167
|
#fs;
|
|
1483
2168
|
#root;
|
|
1484
2169
|
#clock;
|
|
1485
|
-
constructor(
|
|
1486
|
-
this.#fs =
|
|
2170
|
+
constructor(fs2, root, clock) {
|
|
2171
|
+
this.#fs = fs2;
|
|
1487
2172
|
this.#root = root;
|
|
1488
2173
|
this.#clock = clock;
|
|
1489
2174
|
}
|
|
@@ -1569,19 +2254,7 @@ var FlowStore = class {
|
|
|
1569
2254
|
if (!loaded.ok)
|
|
1570
2255
|
return { ok: false, code: loaded.code };
|
|
1571
2256
|
const flow = loaded.value;
|
|
1572
|
-
const
|
|
1573
|
-
for (const change of changes)
|
|
1574
|
-
byStep.set(change.step, change);
|
|
1575
|
-
const applied = [];
|
|
1576
|
-
const steps = flow.steps.map((step, index) => {
|
|
1577
|
-
const change = byStep.get(index);
|
|
1578
|
-
if (change === void 0 || step.anchor.kind !== AnchorKind.TESTID || step.anchor.value !== change.from) {
|
|
1579
|
-
return step;
|
|
1580
|
-
}
|
|
1581
|
-
applied.push(change);
|
|
1582
|
-
return { ...step, anchor: { kind: AnchorKind.TESTID, value: change.to } };
|
|
1583
|
-
});
|
|
1584
|
-
const next = { ...flow, steps };
|
|
2257
|
+
const { flow: next, applied } = applyHealChanges(flow, changes);
|
|
1585
2258
|
await this.#fs.writeFile(flowPath(this.#root, name), this.#serialize(next));
|
|
1586
2259
|
return { ok: true, value: { name, changed: applied } };
|
|
1587
2260
|
}
|
|
@@ -1629,8 +2302,8 @@ var ProjectStore = class {
|
|
|
1629
2302
|
#fs;
|
|
1630
2303
|
#root;
|
|
1631
2304
|
#clock;
|
|
1632
|
-
constructor(
|
|
1633
|
-
this.#fs =
|
|
2305
|
+
constructor(fs2, root, clock) {
|
|
2306
|
+
this.#fs = fs2;
|
|
1634
2307
|
this.#root = root;
|
|
1635
2308
|
this.#clock = clock;
|
|
1636
2309
|
}
|
|
@@ -1809,10 +2482,10 @@ function createNodeFileSystem() {
|
|
|
1809
2482
|
|
|
1810
2483
|
// ../server/dist/mcp.js
|
|
1811
2484
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1812
|
-
import { z as
|
|
2485
|
+
import { z as z17 } from "zod";
|
|
1813
2486
|
|
|
1814
2487
|
// ../server/dist/tools/tools.js
|
|
1815
|
-
import { z as
|
|
2488
|
+
import { z as z16 } from "zod";
|
|
1816
2489
|
|
|
1817
2490
|
// ../server/dist/input/real-input.js
|
|
1818
2491
|
var DriveError = class extends Error {
|
|
@@ -1873,8 +2546,10 @@ async function performGesture(page, action, box, args, sleep) {
|
|
|
1873
2546
|
}
|
|
1874
2547
|
return { performed: false, center };
|
|
1875
2548
|
}
|
|
2549
|
+
var HIDE_IRIS_CHROME_CSS = "[data-iris-overlay]{display:none !important}";
|
|
2550
|
+
var SCREENSHOT_DETERMINISM = { style: HIDE_IRIS_CHROME_CSS, animations: "disabled" };
|
|
1876
2551
|
async function capturePage(page, opts) {
|
|
1877
|
-
const buf = await page.screenshot(opts.clip !== void 0 ? { clip: opts.clip } : opts.fullPage === true ? { fullPage: true } : {});
|
|
2552
|
+
const buf = await page.screenshot(opts.clip !== void 0 ? { ...SCREENSHOT_DETERMINISM, clip: opts.clip } : opts.fullPage === true ? { ...SCREENSHOT_DETERMINISM, fullPage: true } : { ...SCREENSHOT_DETERMINISM });
|
|
1878
2553
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
1879
2554
|
}
|
|
1880
2555
|
var nodeSleep = (ms) => new Promise((resolve) => {
|
|
@@ -2109,6 +2784,7 @@ var PredicateSchema = z3.lazy(() => z3.discriminatedUnion("kind", [
|
|
|
2109
2784
|
name: z3.string().optional(),
|
|
2110
2785
|
dataMatches: z3.record(z3.unknown()).optional()
|
|
2111
2786
|
}),
|
|
2787
|
+
z3.object({ kind: z3.literal("settled"), quietMs: z3.number().positive().optional() }),
|
|
2112
2788
|
z3.object({ kind: z3.literal("allOf"), predicates: z3.array(PredicateSchema) }),
|
|
2113
2789
|
z3.object({ kind: z3.literal("anyOf"), predicates: z3.array(PredicateSchema) }),
|
|
2114
2790
|
z3.object({ kind: z3.literal("not"), predicate: PredicateSchema })
|
|
@@ -2305,6 +2981,39 @@ function evalSignal(events, p) {
|
|
|
2305
2981
|
evidence: sameName.length > 0 ? { nearMiss: sameName } : void 0
|
|
2306
2982
|
};
|
|
2307
2983
|
}
|
|
2984
|
+
var SETTLE_ACTIVITY = /* @__PURE__ */ new Set([
|
|
2985
|
+
EventType.NET_REQUEST,
|
|
2986
|
+
EventType.DOM_ADDED,
|
|
2987
|
+
EventType.DOM_REMOVED,
|
|
2988
|
+
EventType.DOM_ATTR
|
|
2989
|
+
]);
|
|
2990
|
+
var DEFAULT_QUIET_MS = 500;
|
|
2991
|
+
function evalSettled(events, p, now) {
|
|
2992
|
+
const quietMs = p.quietMs ?? DEFAULT_QUIET_MS;
|
|
2993
|
+
let lastT = -1;
|
|
2994
|
+
let lastType;
|
|
2995
|
+
for (const e of events) {
|
|
2996
|
+
if (SETTLE_ACTIVITY.has(e.type) && e.t > lastT) {
|
|
2997
|
+
lastT = e.t;
|
|
2998
|
+
lastType = e.type;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
if (lastT < 0) {
|
|
3002
|
+
return {
|
|
3003
|
+
pass: true,
|
|
3004
|
+
evidence: { settled: true, quietForMs: null, note: "no activity to settle" }
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
const quietForMs = now - lastT;
|
|
3008
|
+
if (quietForMs >= quietMs) {
|
|
3009
|
+
return { pass: true, evidence: { settled: true, quietForMs, lastActivity: lastType } };
|
|
3010
|
+
}
|
|
3011
|
+
return {
|
|
3012
|
+
pass: false,
|
|
3013
|
+
failureReason: `not settled: last activity (${String(lastType)}) ${String(quietForMs)}ms ago, need ${String(quietMs)}ms quiet`,
|
|
3014
|
+
evidence: { quietForMs, lastActivity: lastType }
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
2308
3017
|
async function evaluatePredicate(session, predicate, since = 0) {
|
|
2309
3018
|
const events = session.eventsSince(since);
|
|
2310
3019
|
switch (predicate.kind) {
|
|
@@ -2322,6 +3031,8 @@ async function evaluatePredicate(session, predicate, since = 0) {
|
|
|
2322
3031
|
return evalAnimation(events, predicate);
|
|
2323
3032
|
case "signal":
|
|
2324
3033
|
return evalSignal(events, predicate);
|
|
3034
|
+
case "settled":
|
|
3035
|
+
return evalSettled(events, predicate, session.elapsed());
|
|
2325
3036
|
case "allOf": {
|
|
2326
3037
|
const results = await Promise.all(predicate.predicates.map((p) => evaluatePredicate(session, p, since)));
|
|
2327
3038
|
const failed = results.find((r) => !r.pass);
|
|
@@ -2347,6 +3058,10 @@ async function evaluatePredicate(session, predicate, since = 0) {
|
|
|
2347
3058
|
function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
2348
3059
|
return new Promise((resolve) => {
|
|
2349
3060
|
let done = false;
|
|
3061
|
+
const failed = (error) => ({
|
|
3062
|
+
pass: false,
|
|
3063
|
+
failureReason: error instanceof Error ? error.message : String(error)
|
|
3064
|
+
});
|
|
2350
3065
|
const finish = (result) => {
|
|
2351
3066
|
if (done)
|
|
2352
3067
|
return;
|
|
@@ -2360,6 +3075,8 @@ function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
2360
3075
|
void evaluatePredicate(session, predicate, since).then((r) => {
|
|
2361
3076
|
if (r.pass)
|
|
2362
3077
|
finish(r);
|
|
3078
|
+
}).catch((error) => {
|
|
3079
|
+
finish(failed(error));
|
|
2363
3080
|
});
|
|
2364
3081
|
};
|
|
2365
3082
|
const unsub = session.onEvent(() => {
|
|
@@ -2373,141 +3090,30 @@ function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
2373
3090
|
evidence: r.evidence,
|
|
2374
3091
|
failureReason: r.failureReason ?? "timed out waiting for predicate"
|
|
2375
3092
|
});
|
|
3093
|
+
}).catch((error) => {
|
|
3094
|
+
finish(failed(error));
|
|
2376
3095
|
});
|
|
2377
3096
|
}, timeoutMs);
|
|
2378
3097
|
check();
|
|
2379
3098
|
});
|
|
2380
3099
|
}
|
|
2381
3100
|
|
|
2382
|
-
// ../server/dist/flows/replay.js
|
|
2383
|
-
function asString2(value) {
|
|
2384
|
-
return typeof value === "string" ? value : void 0;
|
|
2385
|
-
}
|
|
2386
|
-
function asRecord2(value) {
|
|
2387
|
-
return typeof value === "object" && value !== null ? value : {};
|
|
2388
|
-
}
|
|
2389
|
-
function compileActStep(args, res) {
|
|
2390
|
-
const testid = asString2(asRecord2(res)["testid"]);
|
|
2391
|
-
const action = asString2(args["action"]) ?? "";
|
|
2392
|
-
const actArgs = asRecord2(args["args"]);
|
|
2393
|
-
if (testid !== void 0) {
|
|
2394
|
-
return {
|
|
2395
|
-
tool: IrisTool.ACT,
|
|
2396
|
-
stable: true,
|
|
2397
|
-
args: { by: QueryBy.TESTID, value: testid, action, args: actArgs }
|
|
2398
|
-
};
|
|
2399
|
-
}
|
|
2400
|
-
return {
|
|
2401
|
-
tool: IrisTool.ACT,
|
|
2402
|
-
stable: false,
|
|
2403
|
-
args: { ref: asString2(args["ref"]) ?? "", action, args: actArgs }
|
|
2404
|
-
};
|
|
2405
|
-
}
|
|
2406
|
-
function compileSequenceStep(args, res) {
|
|
2407
|
-
const inputSteps = Array.isArray(args["steps"]) ? args["steps"] : [];
|
|
2408
|
-
const resolved = Array.isArray(asRecord2(res)["steps"]) ? asRecord2(res)["steps"] : [];
|
|
2409
|
-
let stable = inputSteps.length > 0;
|
|
2410
|
-
const subSteps = inputSteps.map((raw, i) => {
|
|
2411
|
-
const step = asRecord2(raw);
|
|
2412
|
-
const action = asString2(step["action"]) ?? "";
|
|
2413
|
-
const stepArgs = asRecord2(step["args"]);
|
|
2414
|
-
const testid = asString2(asRecord2(resolved[i])["testid"]);
|
|
2415
|
-
if (testid !== void 0) {
|
|
2416
|
-
return { by: QueryBy.TESTID, value: testid, action, args: stepArgs };
|
|
2417
|
-
}
|
|
2418
|
-
stable = false;
|
|
2419
|
-
return { ref: asString2(step["ref"]) ?? "", action, args: stepArgs };
|
|
2420
|
-
});
|
|
2421
|
-
return { tool: IrisTool.ACT_SEQUENCE, stable, args: { steps: subSteps } };
|
|
2422
|
-
}
|
|
2423
|
-
async function resolveRef(session, step) {
|
|
2424
|
-
const by = asString2(step.by);
|
|
2425
|
-
const value = asString2(step.value);
|
|
2426
|
-
if (by === QueryBy.TESTID && value !== void 0) {
|
|
2427
|
-
const result = await session.command(IrisCommand.QUERY, { by, value });
|
|
2428
|
-
if (!result.ok)
|
|
2429
|
-
throw new Error(result.error ?? "query failed");
|
|
2430
|
-
const elements = Array.isArray(asRecord2(result.result)["elements"]) ? asRecord2(result.result)["elements"] : [];
|
|
2431
|
-
const ref2 = asString2(asRecord2(elements[0])["ref"]);
|
|
2432
|
-
if (ref2 === void 0)
|
|
2433
|
-
throw new Error(`testid '${value}' did not resolve in current page`);
|
|
2434
|
-
return elements.length > 1 ? { ref: ref2, note: `ambiguous testid '${value}', used first match` } : { ref: ref2 };
|
|
2435
|
-
}
|
|
2436
|
-
const ref = asString2(step.ref);
|
|
2437
|
-
if (ref === void 0 || ref.length === 0)
|
|
2438
|
-
throw new Error("step has no testid or ref to resolve");
|
|
2439
|
-
return { ref, note: "replayed by stale ref (not portable across sessions)" };
|
|
2440
|
-
}
|
|
2441
|
-
async function replayProgram(session, program) {
|
|
2442
|
-
const results = [];
|
|
2443
|
-
for (const step of program.steps) {
|
|
2444
|
-
try {
|
|
2445
|
-
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
2446
|
-
const subs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
2447
|
-
const notes = [];
|
|
2448
|
-
const liveSteps = [];
|
|
2449
|
-
for (const raw of subs) {
|
|
2450
|
-
const sub = asRecord2(raw);
|
|
2451
|
-
const { ref, note } = await resolveRef(session, sub);
|
|
2452
|
-
if (note !== void 0)
|
|
2453
|
-
notes.push(note);
|
|
2454
|
-
liveSteps.push({
|
|
2455
|
-
ref,
|
|
2456
|
-
action: asString2(sub["action"]) ?? "",
|
|
2457
|
-
args: asRecord2(sub["args"])
|
|
2458
|
-
});
|
|
2459
|
-
}
|
|
2460
|
-
const r = await session.command(IrisCommand.ACT_SEQUENCE, { steps: liveSteps });
|
|
2461
|
-
results.push(buildResult(step.tool, r.ok, r.error, notes));
|
|
2462
|
-
if (!r.ok)
|
|
2463
|
-
break;
|
|
2464
|
-
} else {
|
|
2465
|
-
const { ref, note } = await resolveRef(session, step.args);
|
|
2466
|
-
const r = await session.command(IrisCommand.ACT, {
|
|
2467
|
-
ref,
|
|
2468
|
-
action: asString2(step.args["action"]) ?? "",
|
|
2469
|
-
args: asRecord2(step.args["args"])
|
|
2470
|
-
});
|
|
2471
|
-
results.push(buildResult(step.tool, r.ok, r.error, note !== void 0 ? [note] : []));
|
|
2472
|
-
if (!r.ok)
|
|
2473
|
-
break;
|
|
2474
|
-
}
|
|
2475
|
-
} catch (e) {
|
|
2476
|
-
results.push({
|
|
2477
|
-
tool: step.tool,
|
|
2478
|
-
ok: false,
|
|
2479
|
-
error: e instanceof Error ? e.message : String(e)
|
|
2480
|
-
});
|
|
2481
|
-
break;
|
|
2482
|
-
}
|
|
2483
|
-
}
|
|
2484
|
-
return results;
|
|
2485
|
-
}
|
|
2486
|
-
function buildResult(tool, ok, error, notes) {
|
|
2487
|
-
const base = { tool, ok };
|
|
2488
|
-
if (!ok)
|
|
2489
|
-
base.error = error ?? "command failed";
|
|
2490
|
-
if (notes.length > 0)
|
|
2491
|
-
base.note = notes.join("; ");
|
|
2492
|
-
return base;
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
3101
|
// ../server/dist/events/event-filters.js
|
|
2496
|
-
function
|
|
3102
|
+
function asString4(value) {
|
|
2497
3103
|
return typeof value === "string" ? value : void 0;
|
|
2498
3104
|
}
|
|
2499
|
-
function
|
|
3105
|
+
function asNumber2(value) {
|
|
2500
3106
|
return typeof value === "number" ? value : void 0;
|
|
2501
3107
|
}
|
|
2502
3108
|
function matchNet(e, method, urlContains, status) {
|
|
2503
3109
|
const d = e.data;
|
|
2504
|
-
if (method !== void 0 &&
|
|
3110
|
+
if (method !== void 0 && asString4(d["method"])?.toUpperCase() !== method.toUpperCase()) {
|
|
2505
3111
|
return false;
|
|
2506
3112
|
}
|
|
2507
|
-
if (urlContains !== void 0 && !(
|
|
3113
|
+
if (urlContains !== void 0 && !(asString4(d["url"]) ?? "").includes(urlContains)) {
|
|
2508
3114
|
return false;
|
|
2509
3115
|
}
|
|
2510
|
-
if (status !== void 0 &&
|
|
3116
|
+
if (status !== void 0 && asNumber2(d["status"]) !== status)
|
|
2511
3117
|
return false;
|
|
2512
3118
|
return true;
|
|
2513
3119
|
}
|
|
@@ -2524,8 +3130,8 @@ function matchConsole(e, level) {
|
|
|
2524
3130
|
var HINT_SAMPLE_MAX = 5;
|
|
2525
3131
|
function netEmptyHint(allNet) {
|
|
2526
3132
|
const present = allNet.slice(-HINT_SAMPLE_MAX).reverse().map((e) => {
|
|
2527
|
-
const status =
|
|
2528
|
-
const base = { method:
|
|
3133
|
+
const status = asNumber2(e.data["status"]);
|
|
3134
|
+
const base = { method: asString4(e.data["method"]) ?? "", url: asString4(e.data["url"]) ?? "" };
|
|
2529
3135
|
return status === void 0 ? base : { ...base, status };
|
|
2530
3136
|
});
|
|
2531
3137
|
return { totalInWindow: allNet.length, present };
|
|
@@ -2556,6 +3162,8 @@ function refuseIfThrottled(session, refuse) {
|
|
|
2556
3162
|
}
|
|
2557
3163
|
|
|
2558
3164
|
// ../server/dist/session/output-budget.js
|
|
3165
|
+
var LARGE_TIMELINE_EVENTS = 80;
|
|
3166
|
+
var LARGE_TIMELINE_BYTES = 8e3;
|
|
2559
3167
|
function applyEventBudget(events, maxEvents) {
|
|
2560
3168
|
if (maxEvents === void 0 || maxEvents < 0 || events.length <= maxEvents) {
|
|
2561
3169
|
return { events, droppedOldest: 0 };
|
|
@@ -2565,9 +3173,93 @@ function applyEventBudget(events, maxEvents) {
|
|
|
2565
3173
|
droppedOldest: events.length - maxEvents
|
|
2566
3174
|
};
|
|
2567
3175
|
}
|
|
2568
|
-
function costHint(payload, events, droppedOldest = 0) {
|
|
2569
|
-
const
|
|
2570
|
-
|
|
3176
|
+
function costHint(payload, events, droppedOldest = 0) {
|
|
3177
|
+
const json = JSON.stringify(payload) ?? "";
|
|
3178
|
+
const bytes = json.length;
|
|
3179
|
+
const base = droppedOldest > 0 ? { events, bytes, droppedOldest } : { events, bytes };
|
|
3180
|
+
if (events >= LARGE_TIMELINE_EVENTS || bytes >= LARGE_TIMELINE_BYTES) {
|
|
3181
|
+
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`;
|
|
3182
|
+
}
|
|
3183
|
+
return base;
|
|
3184
|
+
}
|
|
3185
|
+
var CHARS_PER_TOKEN = 4;
|
|
3186
|
+
function estimateTokens(text) {
|
|
3187
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
3188
|
+
}
|
|
3189
|
+
function sizeCost(payload) {
|
|
3190
|
+
const json = JSON.stringify(payload) ?? "";
|
|
3191
|
+
return { bytes: json.length, tokens: estimateTokens(json) };
|
|
3192
|
+
}
|
|
3193
|
+
function withSizeCost(result) {
|
|
3194
|
+
if (typeof result !== "object" || result === null)
|
|
3195
|
+
return result;
|
|
3196
|
+
return { ...result, cost: sizeCost(result) };
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
// ../server/dist/tools/snapshot-delta.js
|
|
3200
|
+
var SnapshotDeltaMode = {
|
|
3201
|
+
FULL: "full",
|
|
3202
|
+
DELTA: "delta",
|
|
3203
|
+
UNCHANGED: "unchanged"
|
|
3204
|
+
};
|
|
3205
|
+
function snapshotDelta(prevTree, nextTree) {
|
|
3206
|
+
if (prevTree === void 0)
|
|
3207
|
+
return { mode: SnapshotDeltaMode.FULL };
|
|
3208
|
+
const { added, removed } = diffLines(normalizeLines(prevTree), normalizeLines(nextTree));
|
|
3209
|
+
if (added.length === 0 && removed.length === 0)
|
|
3210
|
+
return { mode: SnapshotDeltaMode.UNCHANGED };
|
|
3211
|
+
return {
|
|
3212
|
+
mode: SnapshotDeltaMode.DELTA,
|
|
3213
|
+
delta: { added, removed, addedCount: added.length, removedCount: removed.length }
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
var DEFAULT_MAX_ENTRIES = 50;
|
|
3217
|
+
var SnapshotCache = class {
|
|
3218
|
+
#map = /* @__PURE__ */ new Map();
|
|
3219
|
+
#max;
|
|
3220
|
+
constructor(max = DEFAULT_MAX_ENTRIES) {
|
|
3221
|
+
this.#max = max;
|
|
3222
|
+
}
|
|
3223
|
+
/** Last tree for this key IF the route still matches; undefined when absent or route changed. */
|
|
3224
|
+
recall(key, route) {
|
|
3225
|
+
const entry = this.#map.get(key);
|
|
3226
|
+
return entry !== void 0 && entry.route === route ? entry.tree : void 0;
|
|
3227
|
+
}
|
|
3228
|
+
remember(key, route, tree) {
|
|
3229
|
+
if (this.#map.size >= this.#max && !this.#map.has(key)) {
|
|
3230
|
+
const oldest = this.#map.keys().next().value;
|
|
3231
|
+
if (oldest !== void 0)
|
|
3232
|
+
this.#map.delete(oldest);
|
|
3233
|
+
}
|
|
3234
|
+
this.#map.set(key, { route, tree });
|
|
3235
|
+
}
|
|
3236
|
+
};
|
|
3237
|
+
function snapshotCacheKey(sessionId, scope, mode) {
|
|
3238
|
+
return `${sessionId}\0${scope}\0${mode}`;
|
|
3239
|
+
}
|
|
3240
|
+
function applySnapshotDelta(raw, opts, cache) {
|
|
3241
|
+
if (typeof raw !== "object" || raw === null)
|
|
3242
|
+
return raw;
|
|
3243
|
+
const r = raw;
|
|
3244
|
+
if (typeof r["tree"] !== "string")
|
|
3245
|
+
return raw;
|
|
3246
|
+
const tree = r["tree"];
|
|
3247
|
+
const status = typeof r["status"] === "object" && r["status"] !== null ? r["status"] : {};
|
|
3248
|
+
const route = typeof status["route"] === "string" ? status["route"] : "";
|
|
3249
|
+
const key = snapshotCacheKey(opts.sessionId, opts.scope, opts.mode);
|
|
3250
|
+
if (!opts.diff) {
|
|
3251
|
+
cache.remember(key, route, tree);
|
|
3252
|
+
return raw;
|
|
3253
|
+
}
|
|
3254
|
+
const prev = cache.recall(key, route);
|
|
3255
|
+
cache.remember(key, route, tree);
|
|
3256
|
+
const decision = snapshotDelta(prev, tree);
|
|
3257
|
+
if (decision.mode === SnapshotDeltaMode.FULL)
|
|
3258
|
+
return raw;
|
|
3259
|
+
if (decision.mode === SnapshotDeltaMode.UNCHANGED) {
|
|
3260
|
+
return { mode: SnapshotDeltaMode.UNCHANGED, status: r["status"] };
|
|
3261
|
+
}
|
|
3262
|
+
return { mode: SnapshotDeltaMode.DELTA, delta: decision.delta, status: r["status"] };
|
|
2571
3263
|
}
|
|
2572
3264
|
|
|
2573
3265
|
// ../server/dist/session/state-select.js
|
|
@@ -2618,25 +3310,55 @@ function capDepth(value, maxDepth) {
|
|
|
2618
3310
|
return value;
|
|
2619
3311
|
}
|
|
2620
3312
|
|
|
2621
|
-
// ../server/dist/tools/
|
|
2622
|
-
function
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
3313
|
+
// ../server/dist/tools/query-paginate.js
|
|
3314
|
+
function paginateQueryResult(result, limit, countOnly) {
|
|
3315
|
+
if (typeof result !== "object" || result === null)
|
|
3316
|
+
return result;
|
|
3317
|
+
const record = result;
|
|
3318
|
+
const elements = record["elements"];
|
|
3319
|
+
if (!Array.isArray(elements))
|
|
3320
|
+
return result;
|
|
3321
|
+
const total = elements.length;
|
|
3322
|
+
if (countOnly) {
|
|
3323
|
+
const { elements: _dropped, ...rest } = record;
|
|
3324
|
+
return { ...rest, count: total };
|
|
2629
3325
|
}
|
|
2630
|
-
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
return
|
|
3326
|
+
if (limit !== void 0 && limit >= 0 && total > limit) {
|
|
3327
|
+
return { ...record, elements: elements.slice(0, limit), total, truncated: true };
|
|
3328
|
+
}
|
|
3329
|
+
return result;
|
|
2634
3330
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
3331
|
+
|
|
3332
|
+
// ../server/dist/tools/assert-grade.js
|
|
3333
|
+
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.";
|
|
3334
|
+
function walk(predicate) {
|
|
3335
|
+
switch (predicate.kind) {
|
|
3336
|
+
case "signal":
|
|
3337
|
+
case "net":
|
|
3338
|
+
return { consequence: true, presence: false };
|
|
3339
|
+
case "element":
|
|
3340
|
+
case "text":
|
|
3341
|
+
return { consequence: false, presence: true };
|
|
3342
|
+
case "route":
|
|
3343
|
+
case "console":
|
|
3344
|
+
case "animation":
|
|
3345
|
+
case "settled":
|
|
3346
|
+
return { consequence: false, presence: false };
|
|
3347
|
+
case "allOf":
|
|
3348
|
+
case "anyOf": {
|
|
3349
|
+
const subs = predicate.predicates.map(walk);
|
|
3350
|
+
return {
|
|
3351
|
+
consequence: subs.some((s) => s.consequence),
|
|
3352
|
+
presence: subs.some((s) => s.presence)
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
case "not":
|
|
3356
|
+
return walk(predicate.predicate);
|
|
3357
|
+
}
|
|
2637
3358
|
}
|
|
2638
|
-
function
|
|
2639
|
-
|
|
3359
|
+
function isPresenceOnlyAssertion(predicate) {
|
|
3360
|
+
const kinds = walk(predicate);
|
|
3361
|
+
return kinds.presence && !kinds.consequence;
|
|
2640
3362
|
}
|
|
2641
3363
|
|
|
2642
3364
|
// ../server/dist/tools/contract-tools.js
|
|
@@ -2670,7 +3392,7 @@ var CONTRACT_TOOLS = [
|
|
|
2670
3392
|
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");
|
|
2671
3393
|
return { ...r.capabilities, source: "disk", generatedAt: r.generatedAt };
|
|
2672
3394
|
}
|
|
2673
|
-
const caps = await commandOrThrow(deps,
|
|
3395
|
+
const caps = await commandOrThrow(deps, asString(args["sessionId"]), IrisCommand.CAPABILITIES, {});
|
|
2674
3396
|
return { ...caps, source: "live" };
|
|
2675
3397
|
}
|
|
2676
3398
|
},
|
|
@@ -2685,7 +3407,7 @@ var CONTRACT_TOOLS = [
|
|
|
2685
3407
|
signalCount: z4.number()
|
|
2686
3408
|
},
|
|
2687
3409
|
handler: async (deps, args) => {
|
|
2688
|
-
const res = await commandOrThrow(deps,
|
|
3410
|
+
const res = await commandOrThrow(deps, asString(args["sessionId"]), IrisCommand.CAPABILITIES, {});
|
|
2689
3411
|
const caps = CapabilitiesSchema.parse(res);
|
|
2690
3412
|
await writeContract(deps.fs, deps.irisRoot, caps, deps.now);
|
|
2691
3413
|
return {
|
|
@@ -2698,253 +3420,399 @@ var CONTRACT_TOOLS = [
|
|
|
2698
3420
|
}
|
|
2699
3421
|
];
|
|
2700
3422
|
|
|
2701
|
-
// ../server/dist/
|
|
3423
|
+
// ../server/dist/domain/domain-tools.js
|
|
2702
3424
|
import { z as z5 } from "zod";
|
|
2703
|
-
|
|
2704
|
-
|
|
3425
|
+
|
|
3426
|
+
// ../server/dist/flows/flow-classify.js
|
|
3427
|
+
var FlowAssertionGrade = {
|
|
3428
|
+
/** At least one step (or the success end-condition) asserts a signal/network consequence. */
|
|
3429
|
+
ASSERTED: "asserted",
|
|
3430
|
+
/** Only element-presence checks — a healed-but-wrong locator could still pass. */
|
|
3431
|
+
PRESENCE_ONLY: "presence-only",
|
|
3432
|
+
/** Performs actions but asserts nothing observable — passes even if the feature is broken. */
|
|
3433
|
+
ASSERTION_FREE: "assertion-free"
|
|
2705
3434
|
};
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
throw new Error(result.error ?? `command '${name}' failed`);
|
|
2711
|
-
return result.result;
|
|
3435
|
+
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.";
|
|
3436
|
+
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).";
|
|
3437
|
+
function expectIsConsequence(e) {
|
|
3438
|
+
return e !== void 0 && (e.signal !== void 0 || e.net !== void 0);
|
|
2712
3439
|
}
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
ok: z5.boolean(),
|
|
2723
|
-
url: z5.string().optional(),
|
|
2724
|
-
reason: z5.string().optional()
|
|
2725
|
-
},
|
|
2726
|
-
handler: async (deps, args) => {
|
|
2727
|
-
const url = asString4(args["url"]);
|
|
2728
|
-
if (url === void 0 || url.length === 0)
|
|
2729
|
-
return { ok: false, reason: "url required" };
|
|
2730
|
-
await commandOrThrow2(deps, asString4(args["sessionId"]), IrisCommand.NAVIGATE, { url });
|
|
2731
|
-
return { ok: true, url };
|
|
2732
|
-
}
|
|
2733
|
-
},
|
|
2734
|
-
{
|
|
2735
|
-
name: IrisTool.REFRESH,
|
|
2736
|
-
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.",
|
|
2737
|
-
inputSchema: {
|
|
2738
|
-
hard: z5.boolean().optional().describe("Set true to bypass the browser cache. Default: false (normal reload)."),
|
|
2739
|
-
...sessionIdShape2
|
|
2740
|
-
},
|
|
2741
|
-
outputSchema: {
|
|
2742
|
-
ok: z5.boolean()
|
|
2743
|
-
},
|
|
2744
|
-
handler: async (deps, args) => {
|
|
2745
|
-
await commandOrThrow2(deps, asString4(args["sessionId"]), IrisCommand.REFRESH, {
|
|
2746
|
-
hard: args["hard"] === true
|
|
2747
|
-
});
|
|
2748
|
-
return { ok: true };
|
|
2749
|
-
}
|
|
3440
|
+
function expectIsWeak(e) {
|
|
3441
|
+
return e !== void 0 && e.element !== void 0 && e.signal === void 0 && e.net === void 0;
|
|
3442
|
+
}
|
|
3443
|
+
function flattenSteps(steps) {
|
|
3444
|
+
const out = [];
|
|
3445
|
+
for (const s of steps) {
|
|
3446
|
+
out.push(s);
|
|
3447
|
+
if (s.steps !== void 0)
|
|
3448
|
+
out.push(...flattenSteps(s.steps));
|
|
2750
3449
|
}
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
3450
|
+
return out;
|
|
3451
|
+
}
|
|
3452
|
+
function classifyFlowAssertions(flow) {
|
|
3453
|
+
const all = flattenSteps(flow.steps);
|
|
3454
|
+
let consequenceSteps = 0;
|
|
3455
|
+
let weakSteps = 0;
|
|
3456
|
+
for (const s of all) {
|
|
3457
|
+
if (expectIsConsequence(s.expect))
|
|
3458
|
+
consequenceSteps++;
|
|
3459
|
+
else if (expectIsWeak(s.expect))
|
|
3460
|
+
weakSteps++;
|
|
3461
|
+
}
|
|
3462
|
+
const successIsConsequence = expectIsConsequence(flow.success);
|
|
3463
|
+
const successIsWeak = expectIsWeak(flow.success);
|
|
3464
|
+
const hasConsequenceAssertion = consequenceSteps > 0 || successIsConsequence;
|
|
3465
|
+
const hasAnyAssertion = hasConsequenceAssertion || weakSteps > 0 || successIsWeak;
|
|
3466
|
+
let grade;
|
|
3467
|
+
let warning;
|
|
3468
|
+
if (hasConsequenceAssertion) {
|
|
3469
|
+
grade = FlowAssertionGrade.ASSERTED;
|
|
3470
|
+
} else if (hasAnyAssertion) {
|
|
3471
|
+
grade = FlowAssertionGrade.PRESENCE_ONLY;
|
|
3472
|
+
warning = PRESENCE_ONLY_WARNING;
|
|
3473
|
+
} else {
|
|
3474
|
+
grade = FlowAssertionGrade.ASSERTION_FREE;
|
|
3475
|
+
warning = ASSERTION_FREE_WARNING;
|
|
3476
|
+
}
|
|
3477
|
+
return {
|
|
3478
|
+
grade,
|
|
3479
|
+
hasConsequenceAssertion,
|
|
3480
|
+
totalSteps: all.length,
|
|
3481
|
+
consequenceSteps,
|
|
3482
|
+
weakSteps,
|
|
3483
|
+
successIsConsequence,
|
|
3484
|
+
...warning !== void 0 ? { warning } : {}
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
2755
3487
|
|
|
2756
|
-
// ../server/dist/flows/flow-
|
|
2757
|
-
function
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
3488
|
+
// ../server/dist/flows/flow-success.js
|
|
3489
|
+
function dynamicTestids(flow) {
|
|
3490
|
+
return new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
3491
|
+
}
|
|
3492
|
+
function successLabel(success) {
|
|
3493
|
+
if (success.signal !== void 0)
|
|
3494
|
+
return success.signal;
|
|
3495
|
+
if (success.net !== void 0)
|
|
3496
|
+
return success.net.urlContains ?? success.net.method ?? "net";
|
|
3497
|
+
return success.element?.testid ?? success.element?.name ?? success.element?.role ?? "success";
|
|
3498
|
+
}
|
|
3499
|
+
function successToPredicate(success, dynamic) {
|
|
3500
|
+
const parts = [];
|
|
3501
|
+
if (success.signal !== void 0) {
|
|
3502
|
+
parts.push(success.signalData !== void 0 ? { kind: "signal", name: success.signal, dataMatches: success.signalData } : { kind: "signal", name: success.signal });
|
|
3503
|
+
}
|
|
3504
|
+
if (success.net !== void 0) {
|
|
3505
|
+
const net = { kind: "net" };
|
|
3506
|
+
if (success.net.method !== void 0)
|
|
3507
|
+
net.method = success.net.method;
|
|
3508
|
+
if (success.net.urlContains !== void 0)
|
|
3509
|
+
net.urlContains = success.net.urlContains;
|
|
3510
|
+
if (success.net.status !== void 0)
|
|
3511
|
+
net.status = success.net.status;
|
|
3512
|
+
parts.push(net);
|
|
3513
|
+
}
|
|
3514
|
+
const element = success.element;
|
|
3515
|
+
if (element !== void 0) {
|
|
3516
|
+
const testid = element.testid;
|
|
3517
|
+
if (testid === void 0 || !dynamic.has(testid)) {
|
|
3518
|
+
const query = {};
|
|
3519
|
+
if (testid !== void 0)
|
|
3520
|
+
query["testid"] = testid;
|
|
3521
|
+
if (element.role !== void 0)
|
|
3522
|
+
query["role"] = element.role;
|
|
3523
|
+
if (element.name !== void 0)
|
|
3524
|
+
query["name"] = element.name;
|
|
3525
|
+
if (Object.keys(query).length > 0)
|
|
3526
|
+
parts.push({ kind: "element", query });
|
|
2771
3527
|
}
|
|
2772
|
-
for (let j = 0; j < cols; j++)
|
|
2773
|
-
prev[j] = curr[j] ?? 0;
|
|
2774
3528
|
}
|
|
2775
|
-
|
|
3529
|
+
const [first] = parts;
|
|
3530
|
+
if (parts.length === 0)
|
|
3531
|
+
return void 0;
|
|
3532
|
+
if (parts.length === 1 && first !== void 0)
|
|
3533
|
+
return first;
|
|
3534
|
+
return { kind: "allOf", predicates: parts };
|
|
3535
|
+
}
|
|
3536
|
+
async function assertSuccess(session, success, dynamic, waitForSignal, timeoutMs, since = 0) {
|
|
3537
|
+
if (success === void 0)
|
|
3538
|
+
return { pass: true };
|
|
3539
|
+
const predicate = successToPredicate(success, dynamic);
|
|
3540
|
+
if (predicate === void 0)
|
|
3541
|
+
return { pass: true };
|
|
3542
|
+
return waitForSignal(session, predicate, timeoutMs, since);
|
|
2776
3543
|
}
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
3544
|
+
|
|
3545
|
+
// ../server/dist/domain/flow-risk.js
|
|
3546
|
+
var RiskLevel = {
|
|
3547
|
+
HIGH: "high",
|
|
3548
|
+
MEDIUM: "medium",
|
|
3549
|
+
LOW: "low",
|
|
3550
|
+
UNKNOWN: "unknown"
|
|
3551
|
+
};
|
|
3552
|
+
var RANK = {
|
|
3553
|
+
[RiskLevel.HIGH]: 3,
|
|
3554
|
+
[RiskLevel.MEDIUM]: 2,
|
|
3555
|
+
[RiskLevel.UNKNOWN]: 1,
|
|
3556
|
+
[RiskLevel.LOW]: 0
|
|
3557
|
+
};
|
|
3558
|
+
function latestRun(name, runs) {
|
|
3559
|
+
let best;
|
|
3560
|
+
for (const run of runs) {
|
|
3561
|
+
if (run.name === name && (best === void 0 || run.at > best.at))
|
|
3562
|
+
best = run;
|
|
2786
3563
|
}
|
|
2787
3564
|
return best;
|
|
2788
3565
|
}
|
|
2789
|
-
function
|
|
2790
|
-
if (
|
|
2791
|
-
return {
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
3566
|
+
function runRisk(run) {
|
|
3567
|
+
if (run === void 0)
|
|
3568
|
+
return { level: RiskLevel.UNKNOWN, reason: "never run" };
|
|
3569
|
+
if (run.status === RunStatus.ERROR || run.status === RunStatus.FAIL) {
|
|
3570
|
+
return { level: RiskLevel.HIGH, reason: "last run failed" };
|
|
3571
|
+
}
|
|
3572
|
+
if (run.status === RunStatus.DRIFT)
|
|
3573
|
+
return { level: RiskLevel.HIGH, reason: "last run drifted" };
|
|
3574
|
+
const errors = (run.evidence?.consoleErrors ?? 0) + (run.evidence?.networkErrors ?? 0);
|
|
3575
|
+
if (errors > 0) {
|
|
2799
3576
|
return {
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
route: asString4(hint["route"]) ?? "",
|
|
2803
|
-
presentTestids: present,
|
|
2804
|
-
presentRegions: [],
|
|
2805
|
-
knownEmptyState: hint["knownEmptyState"] === true
|
|
2806
|
-
}
|
|
3577
|
+
level: RiskLevel.MEDIUM,
|
|
3578
|
+
reason: `last run passed but logged ${String(errors)} error(s)`
|
|
2807
3579
|
};
|
|
2808
3580
|
}
|
|
2809
|
-
return {
|
|
2810
|
-
}
|
|
2811
|
-
function testidDrift(value, hint) {
|
|
2812
|
-
return {
|
|
2813
|
-
reasonKind: DriftReason.TESTID_NOT_FOUND,
|
|
2814
|
-
reason: `testid "${value}" not found`,
|
|
2815
|
-
anchor: value,
|
|
2816
|
-
nearest: nearestTestid(value, hint?.presentTestids ?? [])
|
|
2817
|
-
};
|
|
2818
|
-
}
|
|
2819
|
-
function anchorLabel(anchor) {
|
|
2820
|
-
if (anchor.kind === AnchorKind.TESTID)
|
|
2821
|
-
return anchor.value;
|
|
2822
|
-
if (anchor.kind === AnchorKind.SIGNAL)
|
|
2823
|
-
return anchor.name;
|
|
2824
|
-
return anchor.name ?? anchor.role;
|
|
3581
|
+
return { level: RiskLevel.LOW, reason: "last run passed clean" };
|
|
2825
3582
|
}
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
const { refs, hint } = readQuery(queryResult);
|
|
2829
|
-
if (refs.length === 0) {
|
|
3583
|
+
function gradeRisk(grade) {
|
|
3584
|
+
if (grade === FlowAssertionGrade.ASSERTION_FREE) {
|
|
2830
3585
|
return {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
anchor: value,
|
|
2834
|
-
ok: false,
|
|
2835
|
-
drift: testidDrift(value, hint)
|
|
3586
|
+
level: RiskLevel.MEDIUM,
|
|
3587
|
+
reason: "asserts no consequence \u2014 a green run proves little"
|
|
2836
3588
|
};
|
|
2837
3589
|
}
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
const act = await session.command(IrisCommand.ACT, {
|
|
2841
|
-
ref,
|
|
2842
|
-
action: step.action ?? "",
|
|
2843
|
-
args: step.args ?? {}
|
|
2844
|
-
});
|
|
2845
|
-
const result = { step: index, tool: step.tool, anchor: value, ok: act.ok };
|
|
2846
|
-
if (!act.ok) {
|
|
2847
|
-
result.error = act.error ?? "command failed";
|
|
2848
|
-
if (note !== void 0)
|
|
2849
|
-
result.note = note;
|
|
2850
|
-
return result;
|
|
2851
|
-
}
|
|
2852
|
-
const expectTestid = step.expect?.element?.testid;
|
|
2853
|
-
if (expectTestid !== void 0 && !dynamic.has(expectTestid)) {
|
|
2854
|
-
const expectQuery = await session.command(IrisCommand.QUERY, {
|
|
2855
|
-
by: QueryBy.TESTID,
|
|
2856
|
-
value: expectTestid
|
|
2857
|
-
});
|
|
2858
|
-
const expectRefs = readQuery(expectQuery);
|
|
2859
|
-
if (expectRefs.refs.length === 0) {
|
|
2860
|
-
return {
|
|
2861
|
-
step: index,
|
|
2862
|
-
tool: step.tool,
|
|
2863
|
-
anchor: expectTestid,
|
|
2864
|
-
ok: false,
|
|
2865
|
-
drift: testidDrift(expectTestid, expectRefs.hint)
|
|
2866
|
-
};
|
|
2867
|
-
}
|
|
3590
|
+
if (grade === FlowAssertionGrade.PRESENCE_ONLY) {
|
|
3591
|
+
return { level: RiskLevel.LOW, reason: "presence-only assertion" };
|
|
2868
3592
|
}
|
|
2869
|
-
|
|
2870
|
-
result.note = note;
|
|
2871
|
-
return result;
|
|
3593
|
+
return { level: RiskLevel.LOW, reason: "asserts a consequence" };
|
|
2872
3594
|
}
|
|
2873
|
-
|
|
2874
|
-
const
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
return {
|
|
2878
|
-
step: index,
|
|
2879
|
-
tool: step.tool,
|
|
2880
|
-
anchor: name,
|
|
2881
|
-
ok: false,
|
|
2882
|
-
drift: {
|
|
2883
|
-
reasonKind: DriftReason.SIGNAL_NOT_OBSERVED,
|
|
2884
|
-
reason: `signal "${name}" not observed`,
|
|
2885
|
-
anchor: name,
|
|
2886
|
-
nearest: null
|
|
2887
|
-
}
|
|
2888
|
-
};
|
|
3595
|
+
function flowRisk(grade, run) {
|
|
3596
|
+
const r = runRisk(run);
|
|
3597
|
+
const g = gradeRisk(grade);
|
|
3598
|
+
const top = RANK[r.level] >= RANK[g.level] ? r : g;
|
|
3599
|
+
return run === void 0 ? { level: top.level, reason: top.reason } : { level: top.level, reason: top.reason, lastStatus: run.status };
|
|
2889
3600
|
}
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
const dynamic = new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
2893
|
-
let index = 0;
|
|
2894
|
-
for (const step of flow.steps) {
|
|
2895
|
-
const label = anchorLabel(step.anchor);
|
|
2896
|
-
let result;
|
|
2897
|
-
if (step.anchor.kind === AnchorKind.SIGNAL) {
|
|
2898
|
-
result = await runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs);
|
|
2899
|
-
} else {
|
|
2900
|
-
result = await runTestidStep(session, step, index, label, dynamic);
|
|
2901
|
-
}
|
|
2902
|
-
results.push(result);
|
|
2903
|
-
if (result.drift !== void 0 || !result.ok)
|
|
2904
|
-
break;
|
|
2905
|
-
index += 1;
|
|
2906
|
-
}
|
|
2907
|
-
return results;
|
|
3601
|
+
function rankByRisk(entries) {
|
|
3602
|
+
return [...entries].sort((a, b) => RANK[b.risk.level] - RANK[a.risk.level] || a.name.localeCompare(b.name)).map((e) => e.name);
|
|
2908
3603
|
}
|
|
2909
3604
|
|
|
2910
|
-
// ../server/dist/
|
|
2911
|
-
function
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
return 1;
|
|
2920
|
-
if (raw <= 0)
|
|
2921
|
-
return Number.EPSILON;
|
|
2922
|
-
return raw;
|
|
3605
|
+
// ../server/dist/domain/domain-model.js
|
|
3606
|
+
function flatten(steps) {
|
|
3607
|
+
const out = [];
|
|
3608
|
+
for (const s of steps) {
|
|
3609
|
+
out.push(s);
|
|
3610
|
+
if (s.steps !== void 0)
|
|
3611
|
+
out.push(...flatten(s.steps));
|
|
3612
|
+
}
|
|
3613
|
+
return out;
|
|
2923
3614
|
}
|
|
2924
|
-
function
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3615
|
+
function flowSignals(flow) {
|
|
3616
|
+
const set = /* @__PURE__ */ new Set();
|
|
3617
|
+
for (const step of flatten(flow.steps)) {
|
|
3618
|
+
if (step.anchor.kind === AnchorKind.SIGNAL)
|
|
3619
|
+
set.add(step.anchor.name);
|
|
3620
|
+
if (step.expect?.signal !== void 0)
|
|
3621
|
+
set.add(step.expect.signal);
|
|
3622
|
+
}
|
|
3623
|
+
if (flow.success?.signal !== void 0)
|
|
3624
|
+
set.add(flow.success.signal);
|
|
3625
|
+
return [...set];
|
|
3626
|
+
}
|
|
3627
|
+
function flowTestids(flow) {
|
|
3628
|
+
const set = /* @__PURE__ */ new Set();
|
|
3629
|
+
for (const step of flatten(flow.steps)) {
|
|
3630
|
+
if (step.anchor.kind === AnchorKind.TESTID)
|
|
3631
|
+
set.add(step.anchor.value);
|
|
3632
|
+
if (step.expect?.element?.testid !== void 0)
|
|
3633
|
+
set.add(step.expect.element.testid);
|
|
3634
|
+
}
|
|
3635
|
+
if (flow.success?.element?.testid !== void 0)
|
|
3636
|
+
set.add(flow.success.element.testid);
|
|
3637
|
+
return [...set];
|
|
3638
|
+
}
|
|
3639
|
+
var EMPTY_CONTRACT = { testids: [], signals: [], stores: [], flows: [] };
|
|
3640
|
+
function buildDomainModel(flows, contract, runs = []) {
|
|
3641
|
+
const caps = contract ?? EMPTY_CONTRACT;
|
|
3642
|
+
const hasHistory = runs.length > 0;
|
|
3643
|
+
const flowSummaries = flows.map((flow) => {
|
|
3644
|
+
const c = classifyFlowAssertions(flow);
|
|
3645
|
+
const summary = {
|
|
3646
|
+
name: flow.name,
|
|
3647
|
+
steps: c.totalSteps,
|
|
3648
|
+
grade: c.grade,
|
|
3649
|
+
asserts: c.hasConsequenceAssertion,
|
|
3650
|
+
signals: flowSignals(flow),
|
|
3651
|
+
testids: flowTestids(flow)
|
|
3652
|
+
};
|
|
3653
|
+
if (flow.success !== void 0)
|
|
3654
|
+
summary.mustHold = successLabel(flow.success);
|
|
3655
|
+
if (c.warning !== void 0)
|
|
3656
|
+
summary.warning = c.warning;
|
|
3657
|
+
if (hasHistory)
|
|
3658
|
+
summary.risk = flowRisk(c.grade, latestRun(flow.name, runs));
|
|
3659
|
+
return summary;
|
|
3660
|
+
});
|
|
3661
|
+
const testedSignals = new Set(flowSummaries.flatMap((f) => f.signals));
|
|
3662
|
+
const testedTestids = new Set(flowSummaries.flatMap((f) => f.testids));
|
|
3663
|
+
const coverage = {
|
|
3664
|
+
asserted: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.ASSERTED).length,
|
|
3665
|
+
presenceOnly: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.PRESENCE_ONLY).length,
|
|
3666
|
+
assertionFree: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.ASSERTION_FREE).length
|
|
3667
|
+
};
|
|
3668
|
+
const gaps = {
|
|
3669
|
+
unassertedFlows: flowSummaries.filter((f) => !f.asserts).map((f) => f.name),
|
|
3670
|
+
declaredUntestedSignals: caps.signals.filter((s) => !testedSignals.has(s)),
|
|
3671
|
+
declaredUntestedTestids: caps.testids.filter((t) => !testedTestids.has(t))
|
|
3672
|
+
};
|
|
3673
|
+
const riskRanked = hasHistory ? rankByRisk(flowSummaries.filter((f) => f.risk !== void 0).map((f) => ({ name: f.name, risk: f.risk }))) : [];
|
|
3674
|
+
const top = riskRanked[0];
|
|
3675
|
+
const topFlow = top === void 0 ? void 0 : flowSummaries.find((f) => f.name === top);
|
|
3676
|
+
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;
|
|
3677
|
+
return {
|
|
3678
|
+
flowCount: flows.length,
|
|
3679
|
+
flows: flowSummaries,
|
|
3680
|
+
declared: { testids: caps.testids.length, signals: caps.signals, stores: caps.stores },
|
|
3681
|
+
coverage,
|
|
3682
|
+
gaps,
|
|
3683
|
+
riskRanked,
|
|
3684
|
+
summary: buildSummary(flows.length, coverage, gaps, topRisk)
|
|
3685
|
+
};
|
|
2934
3686
|
}
|
|
2935
|
-
function
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
if (step.drift === void 0)
|
|
2939
|
-
continue;
|
|
2940
|
-
const proposal = proposeRebindWith(step.drift, step.step, minConfidence);
|
|
2941
|
-
if (proposal !== void 0)
|
|
2942
|
-
proposals.push(proposal);
|
|
3687
|
+
function buildSummary(flowCount, coverage, gaps, topRisk) {
|
|
3688
|
+
if (flowCount === 0) {
|
|
3689
|
+
return "No saved flows yet \u2014 record the critical journeys (iris_record_start) so the agent learns the app.";
|
|
2943
3690
|
}
|
|
2944
|
-
|
|
3691
|
+
const parts = [
|
|
3692
|
+
`${String(flowCount)} flow${flowCount === 1 ? "" : "s"}: ${String(coverage.asserted)} asserted, ${String(coverage.presenceOnly)} presence-only, ${String(coverage.assertionFree)} assertion-free`
|
|
3693
|
+
];
|
|
3694
|
+
if (topRisk !== void 0) {
|
|
3695
|
+
parts.push(`test first: ${topRisk.name} (${topRisk.reason})`);
|
|
3696
|
+
}
|
|
3697
|
+
if (gaps.declaredUntestedSignals.length > 0) {
|
|
3698
|
+
parts.push(`${String(gaps.declaredUntestedSignals.length)} declared signal(s) no flow asserts (${gaps.declaredUntestedSignals.join(", ")})`);
|
|
3699
|
+
}
|
|
3700
|
+
if (gaps.unassertedFlows.length > 0) {
|
|
3701
|
+
parts.push(`${String(gaps.unassertedFlows.length)} flow(s) assert no consequence`);
|
|
3702
|
+
}
|
|
3703
|
+
return parts.join(". ") + ".";
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
// ../server/dist/domain/domain-tools.js
|
|
3707
|
+
var DOMAIN_TOOLS = [
|
|
3708
|
+
{
|
|
3709
|
+
name: IrisTool.DOMAIN,
|
|
3710
|
+
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).",
|
|
3711
|
+
inputSchema: {},
|
|
3712
|
+
outputSchema: {
|
|
3713
|
+
flowCount: z5.number(),
|
|
3714
|
+
flows: z5.array(z5.object({
|
|
3715
|
+
name: z5.string(),
|
|
3716
|
+
steps: z5.number(),
|
|
3717
|
+
grade: z5.string(),
|
|
3718
|
+
asserts: z5.boolean(),
|
|
3719
|
+
mustHold: z5.string().optional().describe("The success consequence that must hold for this flow (what it actually tests)."),
|
|
3720
|
+
warning: z5.string().optional(),
|
|
3721
|
+
signals: z5.array(z5.string()),
|
|
3722
|
+
testids: z5.array(z5.string())
|
|
3723
|
+
})),
|
|
3724
|
+
declared: z5.object({
|
|
3725
|
+
testids: z5.number(),
|
|
3726
|
+
signals: z5.array(z5.string()),
|
|
3727
|
+
stores: z5.array(z5.string())
|
|
3728
|
+
}),
|
|
3729
|
+
coverage: z5.object({
|
|
3730
|
+
asserted: z5.number(),
|
|
3731
|
+
presenceOnly: z5.number(),
|
|
3732
|
+
assertionFree: z5.number()
|
|
3733
|
+
}),
|
|
3734
|
+
gaps: z5.object({
|
|
3735
|
+
unassertedFlows: z5.array(z5.string()),
|
|
3736
|
+
declaredUntestedSignals: z5.array(z5.string()),
|
|
3737
|
+
declaredUntestedTestids: z5.array(z5.string())
|
|
3738
|
+
}),
|
|
3739
|
+
riskRanked: z5.array(z5.string()).describe("Flow names worst-risk first (run history + assertion quality). Test these first."),
|
|
3740
|
+
summary: z5.string()
|
|
3741
|
+
},
|
|
3742
|
+
handler: async (deps) => {
|
|
3743
|
+
const names = await deps.flows.list();
|
|
3744
|
+
const flows = [];
|
|
3745
|
+
for (const name of names) {
|
|
3746
|
+
const loaded = await deps.flows.load(name);
|
|
3747
|
+
if (loaded.ok)
|
|
3748
|
+
flows.push(loaded.value);
|
|
3749
|
+
}
|
|
3750
|
+
const contract = await readContract(deps.fs, deps.irisRoot);
|
|
3751
|
+
const project = await deps.project.read();
|
|
3752
|
+
const runs = project.ok ? project.file.runs : [];
|
|
3753
|
+
return buildDomainModel(flows, contract.ok ? contract.capabilities : null, runs);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
];
|
|
3757
|
+
|
|
3758
|
+
// ../server/dist/tools/browser-tools.js
|
|
3759
|
+
import { z as z6 } from "zod";
|
|
3760
|
+
var sessionIdShape2 = {
|
|
3761
|
+
sessionId: z6.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3762
|
+
};
|
|
3763
|
+
async function commandOrThrow2(deps, sessionId, name, args) {
|
|
3764
|
+
const session = deps.sessions.resolve(sessionId);
|
|
3765
|
+
const result = await session.command(name, args);
|
|
3766
|
+
if (!result.ok)
|
|
3767
|
+
throw new Error(result.error ?? `command '${name}' failed`);
|
|
3768
|
+
return result.result;
|
|
2945
3769
|
}
|
|
3770
|
+
var BROWSER_TOOLS = [
|
|
3771
|
+
{
|
|
3772
|
+
name: IrisTool.NAVIGATE,
|
|
3773
|
+
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.",
|
|
3774
|
+
inputSchema: {
|
|
3775
|
+
url: z6.string().describe("The URL to navigate to."),
|
|
3776
|
+
...sessionIdShape2
|
|
3777
|
+
},
|
|
3778
|
+
outputSchema: {
|
|
3779
|
+
ok: z6.boolean(),
|
|
3780
|
+
url: z6.string().optional(),
|
|
3781
|
+
reason: z6.string().optional()
|
|
3782
|
+
},
|
|
3783
|
+
handler: async (deps, args) => {
|
|
3784
|
+
const url = asString(args["url"]);
|
|
3785
|
+
if (url === void 0 || url.length === 0)
|
|
3786
|
+
return { ok: false, reason: "url required" };
|
|
3787
|
+
const result = await commandOrThrow2(deps, asString(args["sessionId"]), IrisCommand.NAVIGATE, { url });
|
|
3788
|
+
return {
|
|
3789
|
+
ok: result.ok === true,
|
|
3790
|
+
...typeof result.url === "string" ? { url: result.url } : {},
|
|
3791
|
+
...typeof result.reason === "string" ? { reason: result.reason } : {}
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
},
|
|
3795
|
+
{
|
|
3796
|
+
name: IrisTool.REFRESH,
|
|
3797
|
+
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.",
|
|
3798
|
+
inputSchema: {
|
|
3799
|
+
hard: z6.boolean().optional().describe("Set true to bypass the browser cache. Default: false (normal reload)."),
|
|
3800
|
+
...sessionIdShape2
|
|
3801
|
+
},
|
|
3802
|
+
outputSchema: {
|
|
3803
|
+
ok: z6.boolean()
|
|
3804
|
+
},
|
|
3805
|
+
handler: async (deps, args) => {
|
|
3806
|
+
await commandOrThrow2(deps, asString(args["sessionId"]), IrisCommand.REFRESH, {
|
|
3807
|
+
hard: args["hard"] === true
|
|
3808
|
+
});
|
|
3809
|
+
return { ok: true };
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
];
|
|
2946
3813
|
|
|
2947
3814
|
// ../server/dist/flows/flow-tools.js
|
|
3815
|
+
import { z as z7 } from "zod";
|
|
2948
3816
|
function latestRecordedFlow(events) {
|
|
2949
3817
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
2950
3818
|
const event = events[i];
|
|
@@ -2990,18 +3858,26 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs) {
|
|
|
2990
3858
|
var FLOW_TOOLS = [
|
|
2991
3859
|
{
|
|
2992
3860
|
name: IrisTool.FLOW_SAVE,
|
|
2993
|
-
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
|
|
3861
|
+
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).',
|
|
2994
3862
|
inputSchema: {
|
|
2995
|
-
flowName:
|
|
3863
|
+
flowName: z7.string().describe("Name for the flow file (saved to .iris/flows/<flowName>.json). Use again in iris_flow_load/iris_flow_replay.")
|
|
2996
3864
|
},
|
|
2997
3865
|
outputSchema: {
|
|
2998
|
-
saved:
|
|
2999
|
-
path:
|
|
3000
|
-
stepCount:
|
|
3001
|
-
degraded:
|
|
3866
|
+
saved: z7.boolean(),
|
|
3867
|
+
path: z7.string(),
|
|
3868
|
+
stepCount: z7.number().optional(),
|
|
3869
|
+
degraded: z7.number().optional(),
|
|
3870
|
+
assertions: z7.object({
|
|
3871
|
+
grade: z7.string().describe("asserted | presence-only | assertion-free"),
|
|
3872
|
+
hasConsequenceAssertion: z7.boolean(),
|
|
3873
|
+
totalSteps: z7.number(),
|
|
3874
|
+
consequenceSteps: z7.number(),
|
|
3875
|
+
weakSteps: z7.number(),
|
|
3876
|
+
warning: z7.string().optional()
|
|
3877
|
+
}).optional()
|
|
3002
3878
|
},
|
|
3003
3879
|
handler: (deps, args) => {
|
|
3004
|
-
const name =
|
|
3880
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3005
3881
|
const program = deps.recordings.getCompiled(name);
|
|
3006
3882
|
if (program === void 0) {
|
|
3007
3883
|
return Promise.resolve({
|
|
@@ -3015,10 +3891,12 @@ var FLOW_TOOLS = [
|
|
|
3015
3891
|
dynamic: deps.annotations.dynamic(name),
|
|
3016
3892
|
...success !== void 0 ? { success } : {}
|
|
3017
3893
|
};
|
|
3018
|
-
return deps.flows.save(program, annotations).then((res) => {
|
|
3019
|
-
if (res.ok)
|
|
3020
|
-
|
|
3021
|
-
|
|
3894
|
+
return deps.flows.save(program, annotations).then(async (res) => {
|
|
3895
|
+
if (!res.ok)
|
|
3896
|
+
return { error: flowErrorMessage(res.code), code: res.code };
|
|
3897
|
+
deps.annotations.clear(name);
|
|
3898
|
+
const loaded = await deps.flows.load(res.value.name);
|
|
3899
|
+
return loaded.ok ? { ...res.value, assertions: classifyFlowAssertions(loaded.value) } : res.value;
|
|
3022
3900
|
});
|
|
3023
3901
|
}
|
|
3024
3902
|
},
|
|
@@ -3027,22 +3905,27 @@ var FLOW_TOOLS = [
|
|
|
3027
3905
|
description: "List saved flow names under .iris/flows (a fresh agent learns the demonstrated journeys without a browser).",
|
|
3028
3906
|
inputSchema: {},
|
|
3029
3907
|
outputSchema: {
|
|
3030
|
-
flows:
|
|
3908
|
+
flows: z7.array(z7.object({ name: z7.string(), path: z7.string(), createdAt: z7.number().optional() }))
|
|
3031
3909
|
},
|
|
3032
|
-
|
|
3910
|
+
// Return {name, path} objects to MATCH the declared outputSchema. Returning bare name strings
|
|
3911
|
+
// (the prior bug) made schema-validating MCP clients reject the result ("expected object,
|
|
3912
|
+
// received string") — caught driving the live demo.
|
|
3913
|
+
handler: (deps) => deps.flows.list().then((names) => ({
|
|
3914
|
+
flows: names.map((name) => ({ name, path: flowPath(deps.irisRoot, name) }))
|
|
3915
|
+
}))
|
|
3033
3916
|
},
|
|
3034
3917
|
{
|
|
3035
3918
|
name: IrisTool.FLOW_LOAD,
|
|
3036
3919
|
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 }.",
|
|
3037
3920
|
inputSchema: {
|
|
3038
|
-
flowName:
|
|
3921
|
+
flowName: z7.string().describe("Flow file name (without .json extension) from iris_flow_list.")
|
|
3039
3922
|
},
|
|
3040
3923
|
outputSchema: {
|
|
3041
|
-
flowName:
|
|
3042
|
-
steps:
|
|
3043
|
-
createdAt:
|
|
3924
|
+
flowName: z7.string(),
|
|
3925
|
+
steps: z7.array(z7.unknown()),
|
|
3926
|
+
createdAt: z7.number().optional()
|
|
3044
3927
|
},
|
|
3045
|
-
handler: (deps, args) => deps.flows.load(
|
|
3928
|
+
handler: (deps, args) => deps.flows.load(asString(args["flowName"]) ?? "").then((res) => {
|
|
3046
3929
|
if (!res.ok)
|
|
3047
3930
|
return { error: flowErrorMessage(res.code), code: res.code };
|
|
3048
3931
|
const { name, ...rest } = res.value;
|
|
@@ -3051,20 +3934,21 @@ var FLOW_TOOLS = [
|
|
|
3051
3934
|
},
|
|
3052
3935
|
{
|
|
3053
3936
|
name: IrisTool.FLOW_REPLAY,
|
|
3054
|
-
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:[...] };
|
|
3937
|
+
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).`,
|
|
3055
3938
|
inputSchema: {
|
|
3056
|
-
flowName:
|
|
3057
|
-
|
|
3939
|
+
flowName: z7.string().describe("Flow file name (without .json extension) from iris_flow_list."),
|
|
3940
|
+
confirmDangerous: z7.boolean().optional().describe("Set true to allow destructive controls during this replay only."),
|
|
3941
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3058
3942
|
},
|
|
3059
3943
|
outputSchema: {
|
|
3060
|
-
status:
|
|
3061
|
-
steps:
|
|
3062
|
-
proposals:
|
|
3063
|
-
error:
|
|
3944
|
+
status: z7.string().describe("ok | drift | error"),
|
|
3945
|
+
steps: z7.array(z7.unknown()),
|
|
3946
|
+
proposals: z7.array(z7.unknown()).optional(),
|
|
3947
|
+
error: z7.object({ code: z7.string(), message: z7.string() }).optional()
|
|
3064
3948
|
},
|
|
3065
3949
|
handler: async (deps, args) => {
|
|
3066
3950
|
const startedAt = deps.now();
|
|
3067
|
-
const name =
|
|
3951
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3068
3952
|
const loaded = await deps.flows.load(name);
|
|
3069
3953
|
if (!loaded.ok) {
|
|
3070
3954
|
await recordReplayRun(deps, name, ReplayStatus.ERROR, 0, deps.now() - startedAt);
|
|
@@ -3075,12 +3959,35 @@ var FLOW_TOOLS = [
|
|
|
3075
3959
|
error: { code: loaded.code, message: flowErrorMessage(loaded.code) }
|
|
3076
3960
|
};
|
|
3077
3961
|
}
|
|
3078
|
-
const session = deps.sessions.resolve(
|
|
3079
|
-
const
|
|
3962
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
3963
|
+
const replayFloor = session.elapsed();
|
|
3964
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
3965
|
+
const stepsClean = steps.length > 0 && steps.every((s) => s.ok && s.drift === void 0);
|
|
3966
|
+
if (stepsClean && loaded.value.success !== void 0) {
|
|
3967
|
+
const verdict = await assertSuccess(session, loaded.value.success, dynamicTestids(loaded.value), waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, replayFloor);
|
|
3968
|
+
const row = {
|
|
3969
|
+
step: steps.length,
|
|
3970
|
+
tool: "success",
|
|
3971
|
+
anchor: successLabel(loaded.value.success),
|
|
3972
|
+
ok: verdict.pass,
|
|
3973
|
+
...verdict.pass ? {} : { error: verdict.failureReason ?? "flow.success not satisfied" }
|
|
3974
|
+
};
|
|
3975
|
+
steps.push(row);
|
|
3976
|
+
}
|
|
3080
3977
|
const driftSteps = steps.filter((s) => s.drift !== void 0).length;
|
|
3081
3978
|
const allOk = steps.every((s) => s.ok);
|
|
3082
|
-
const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.
|
|
3979
|
+
const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.ERROR;
|
|
3083
3980
|
await recordReplayRun(deps, name, status, driftSteps, deps.now() - startedAt);
|
|
3981
|
+
const failed = steps.find((step) => !step.ok && step.drift === void 0);
|
|
3982
|
+
if (failed !== void 0) {
|
|
3983
|
+
const message = failed.error ?? "flow action failed";
|
|
3984
|
+
return {
|
|
3985
|
+
name,
|
|
3986
|
+
status,
|
|
3987
|
+
steps,
|
|
3988
|
+
error: { code: ReplayStatus.ERROR, message }
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3084
3991
|
return { name, status, steps };
|
|
3085
3992
|
}
|
|
3086
3993
|
},
|
|
@@ -3088,20 +3995,20 @@ var FLOW_TOOLS = [
|
|
|
3088
3995
|
name: IrisTool.FLOW_SAVE_RECORDED,
|
|
3089
3996
|
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).",
|
|
3090
3997
|
inputSchema: {
|
|
3091
|
-
flowName:
|
|
3998
|
+
flowName: z7.string().optional().describe("Override the flow name embedded in the recorded flow. Omit to use the recorder-assigned name."),
|
|
3092
3999
|
...{
|
|
3093
|
-
sessionId:
|
|
4000
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3094
4001
|
}
|
|
3095
4002
|
},
|
|
3096
4003
|
outputSchema: {
|
|
3097
|
-
flowName:
|
|
3098
|
-
stepCount:
|
|
3099
|
-
degraded:
|
|
3100
|
-
error:
|
|
3101
|
-
code:
|
|
4004
|
+
flowName: z7.string().optional(),
|
|
4005
|
+
stepCount: z7.number().optional(),
|
|
4006
|
+
degraded: z7.number().optional(),
|
|
4007
|
+
error: z7.string().optional(),
|
|
4008
|
+
code: z7.string().optional()
|
|
3102
4009
|
},
|
|
3103
4010
|
handler: async (deps, args) => {
|
|
3104
|
-
const session = deps.sessions.resolve(
|
|
4011
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
3105
4012
|
const recorded = latestRecordedFlow(session.eventsSince(0));
|
|
3106
4013
|
if (recorded === void 0) {
|
|
3107
4014
|
return {
|
|
@@ -3109,7 +4016,7 @@ var FLOW_TOOLS = [
|
|
|
3109
4016
|
code: RecordedSaveError.NO_RECORDED_FLOW
|
|
3110
4017
|
};
|
|
3111
4018
|
}
|
|
3112
|
-
const override =
|
|
4019
|
+
const override = asString(args["flowName"]);
|
|
3113
4020
|
const flow = override !== void 0 ? { ...recorded.flow, name: override } : recorded.flow;
|
|
3114
4021
|
const res = await deps.flows.saveFlow(flow);
|
|
3115
4022
|
if (!res.ok)
|
|
@@ -3120,35 +4027,38 @@ var FLOW_TOOLS = [
|
|
|
3120
4027
|
},
|
|
3121
4028
|
{
|
|
3122
4029
|
name: IrisTool.FLOW_HEAL,
|
|
3123
|
-
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 }.",
|
|
4030
|
+
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 }.",
|
|
3124
4031
|
inputSchema: {
|
|
3125
|
-
flowName:
|
|
3126
|
-
apply:
|
|
3127
|
-
|
|
4032
|
+
flowName: z7.string().describe("Flow file name to heal (from iris_flow_list)."),
|
|
4033
|
+
apply: z7.boolean().optional(),
|
|
4034
|
+
confirmDangerous: z7.boolean().optional().describe("Set true to allow destructive controls during this heal replay only."),
|
|
4035
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3128
4036
|
},
|
|
3129
4037
|
outputSchema: {
|
|
3130
|
-
flowName:
|
|
3131
|
-
status:
|
|
3132
|
-
applied:
|
|
3133
|
-
proposals:
|
|
3134
|
-
changed:
|
|
3135
|
-
message:
|
|
3136
|
-
error:
|
|
4038
|
+
flowName: z7.string(),
|
|
4039
|
+
status: z7.string(),
|
|
4040
|
+
applied: z7.boolean(),
|
|
4041
|
+
proposals: z7.array(z7.unknown()),
|
|
4042
|
+
changed: z7.array(z7.unknown()),
|
|
4043
|
+
message: z7.string(),
|
|
4044
|
+
error: z7.object({ code: z7.string(), message: z7.string() }).optional()
|
|
3137
4045
|
},
|
|
3138
4046
|
handler: (deps, args) => healFlow(deps, args).then(({ name, ...rest }) => ({ flowName: name, ...rest }))
|
|
3139
4047
|
}
|
|
3140
4048
|
];
|
|
3141
4049
|
var HEAL_MESSAGES = {
|
|
3142
4050
|
NOTHING: "nothing to heal \u2014 every anchor resolved on replay",
|
|
3143
|
-
HEALED: "rewrote drifted testid anchors to their nearest surviving match",
|
|
4051
|
+
HEALED: "rewrote drifted testid anchors to their nearest surviving match and re-verified the flow's success consequence still fires",
|
|
3144
4052
|
DRIFT_DRY: "confident rebind(s) proposed \u2014 re-run with apply:true to write them to disk",
|
|
3145
|
-
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
|
|
4053
|
+
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`,
|
|
4054
|
+
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.",
|
|
4055
|
+
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"
|
|
3146
4056
|
};
|
|
3147
4057
|
function toChange(proposal) {
|
|
3148
4058
|
return { step: proposal.step, from: proposal.from, to: proposal.to };
|
|
3149
4059
|
}
|
|
3150
4060
|
async function healFlow(deps, args) {
|
|
3151
|
-
const name =
|
|
4061
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3152
4062
|
const apply = args["apply"] === true;
|
|
3153
4063
|
const loaded = await deps.flows.load(name);
|
|
3154
4064
|
if (!loaded.ok) {
|
|
@@ -3162,9 +4072,22 @@ async function healFlow(deps, args) {
|
|
|
3162
4072
|
error: { code: loaded.code, message: flowErrorMessage(loaded.code) }
|
|
3163
4073
|
};
|
|
3164
4074
|
}
|
|
3165
|
-
const session = deps.sessions.resolve(
|
|
3166
|
-
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS);
|
|
4075
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4076
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
3167
4077
|
const drifted = steps.some((s) => s.drift !== void 0);
|
|
4078
|
+
const failed = steps.find((s) => !s.ok && s.drift === void 0);
|
|
4079
|
+
if (failed !== void 0) {
|
|
4080
|
+
const message = failed.error ?? "flow replay failed before an anchor could be healed";
|
|
4081
|
+
return {
|
|
4082
|
+
name,
|
|
4083
|
+
status: HealStatus.ERROR,
|
|
4084
|
+
applied: false,
|
|
4085
|
+
proposals: [],
|
|
4086
|
+
changed: [],
|
|
4087
|
+
message,
|
|
4088
|
+
error: { code: ReplayStatus.ERROR, message }
|
|
4089
|
+
};
|
|
4090
|
+
}
|
|
3168
4091
|
if (!drifted) {
|
|
3169
4092
|
return {
|
|
3170
4093
|
name,
|
|
@@ -3196,6 +4119,23 @@ async function healFlow(deps, args) {
|
|
|
3196
4119
|
message: HEAL_MESSAGES.DRIFT_DRY
|
|
3197
4120
|
};
|
|
3198
4121
|
}
|
|
4122
|
+
const { flow: healed } = applyHealChanges(loaded.value, proposals.map(toChange));
|
|
4123
|
+
if (healed.success !== void 0) {
|
|
4124
|
+
const verifyFloor = session.elapsed();
|
|
4125
|
+
const verifySteps = await replayFlow(session, healed, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
4126
|
+
const verifyClean = verifySteps.length > 0 && verifySteps.every((s) => s.ok && s.drift === void 0);
|
|
4127
|
+
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" };
|
|
4128
|
+
if (!verdict.pass) {
|
|
4129
|
+
return {
|
|
4130
|
+
name,
|
|
4131
|
+
status: HealStatus.CONSEQUENCE_BROKEN,
|
|
4132
|
+
applied: false,
|
|
4133
|
+
proposals,
|
|
4134
|
+
changed: [],
|
|
4135
|
+
message: `${HEAL_MESSAGES.CONSEQUENCE_BROKEN} (${successLabel(healed.success)}: ${verdict.failureReason ?? "not satisfied"})`
|
|
4136
|
+
};
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
3199
4139
|
const written = await deps.flows.heal(name, proposals.map(toChange));
|
|
3200
4140
|
if (!written.ok) {
|
|
3201
4141
|
return {
|
|
@@ -3214,14 +4154,14 @@ async function healFlow(deps, args) {
|
|
|
3214
4154
|
applied: written.value.changed.length > 0,
|
|
3215
4155
|
proposals,
|
|
3216
4156
|
changed: written.value.changed,
|
|
3217
|
-
message: HEAL_MESSAGES.HEALED
|
|
4157
|
+
message: loaded.value.success !== void 0 ? HEAL_MESSAGES.HEALED : HEAL_MESSAGES.HEALED_UNVERIFIED
|
|
3218
4158
|
};
|
|
3219
4159
|
}
|
|
3220
4160
|
|
|
3221
4161
|
// ../server/dist/project/project-tools.js
|
|
3222
|
-
import { z as
|
|
4162
|
+
import { z as z8 } from "zod";
|
|
3223
4163
|
var sessionIdShape3 = {
|
|
3224
|
-
sessionId:
|
|
4164
|
+
sessionId: z8.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3225
4165
|
};
|
|
3226
4166
|
var REGRESSION_STATUSES = /* @__PURE__ */ new Set([
|
|
3227
4167
|
RunStatus.FAIL,
|
|
@@ -3262,12 +4202,12 @@ var PROJECT_TOOLS = [
|
|
|
3262
4202
|
name: IrisTool.PROJECT,
|
|
3263
4203
|
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.',
|
|
3264
4204
|
inputSchema: {
|
|
3265
|
-
name:
|
|
4205
|
+
name: z8.string().optional().describe("Filter runs by this name. Omit to return all runs."),
|
|
3266
4206
|
...sessionIdShape3
|
|
3267
4207
|
},
|
|
3268
4208
|
outputSchema: {
|
|
3269
|
-
runs:
|
|
3270
|
-
diff:
|
|
4209
|
+
runs: z8.array(z8.unknown()),
|
|
4210
|
+
diff: z8.unknown().optional()
|
|
3271
4211
|
},
|
|
3272
4212
|
handler: async (deps, args) => {
|
|
3273
4213
|
const read = await deps.project.read();
|
|
@@ -3277,7 +4217,7 @@ var PROJECT_TOOLS = [
|
|
|
3277
4217
|
reason: read.reason
|
|
3278
4218
|
};
|
|
3279
4219
|
}
|
|
3280
|
-
const name =
|
|
4220
|
+
const name = asString(args["name"]);
|
|
3281
4221
|
if (name === void 0) {
|
|
3282
4222
|
return { runs: read.file.runs, learned: read.file.learned };
|
|
3283
4223
|
}
|
|
@@ -3295,22 +4235,22 @@ var PROJECT_TOOLS = [
|
|
|
3295
4235
|
name: IrisTool.RUN_RECORD,
|
|
3296
4236
|
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 }.",
|
|
3297
4237
|
inputSchema: {
|
|
3298
|
-
name:
|
|
3299
|
-
status:
|
|
3300
|
-
kind:
|
|
3301
|
-
summary:
|
|
4238
|
+
name: z8.string().describe("Run name for grouping in iris_project history."),
|
|
4239
|
+
status: z8.nativeEnum(RunStatus).describe("Outcome: pass | fail | drift | error"),
|
|
4240
|
+
kind: z8.nativeEnum(RunKind).optional(),
|
|
4241
|
+
summary: z8.string().optional().describe("One-line human summary of what this run covered."),
|
|
3302
4242
|
...sessionIdShape3
|
|
3303
4243
|
},
|
|
3304
4244
|
outputSchema: {
|
|
3305
|
-
recorded:
|
|
3306
|
-
runName:
|
|
3307
|
-
status:
|
|
4245
|
+
recorded: z8.boolean(),
|
|
4246
|
+
runName: z8.string(),
|
|
4247
|
+
status: z8.string()
|
|
3308
4248
|
},
|
|
3309
4249
|
handler: async (deps, args) => {
|
|
3310
|
-
const name =
|
|
4250
|
+
const name = asString(args["name"]) ?? "";
|
|
3311
4251
|
const status = args["status"];
|
|
3312
4252
|
const kindArg = args["kind"];
|
|
3313
|
-
const summary =
|
|
4253
|
+
const summary = asString(args["summary"]);
|
|
3314
4254
|
await deps.project.recordRun({
|
|
3315
4255
|
kind: typeof kindArg === "string" ? kindArg : RunKind.MANUAL,
|
|
3316
4256
|
name,
|
|
@@ -3323,7 +4263,7 @@ var PROJECT_TOOLS = [
|
|
|
3323
4263
|
];
|
|
3324
4264
|
|
|
3325
4265
|
// ../server/dist/visual/visual-tools.js
|
|
3326
|
-
import { z as
|
|
4266
|
+
import { z as z9 } from "zod";
|
|
3327
4267
|
|
|
3328
4268
|
// ../server/dist/visual/visual-diff.js
|
|
3329
4269
|
async function loadDeps() {
|
|
@@ -3421,8 +4361,8 @@ async function diffPng(baselineBytes, currentBytes, opts = {}) {
|
|
|
3421
4361
|
var VisualStore = class {
|
|
3422
4362
|
#fs;
|
|
3423
4363
|
#root;
|
|
3424
|
-
constructor(
|
|
3425
|
-
this.#fs =
|
|
4364
|
+
constructor(fs2, root) {
|
|
4365
|
+
this.#fs = fs2;
|
|
3426
4366
|
this.#root = root;
|
|
3427
4367
|
}
|
|
3428
4368
|
/** The absolute baseline path for `name` (for echoing back to the agent). */
|
|
@@ -3474,13 +4414,13 @@ var VisualStore = class {
|
|
|
3474
4414
|
|
|
3475
4415
|
// ../server/dist/visual/visual-tools.js
|
|
3476
4416
|
var sessionIdShape4 = {
|
|
3477
|
-
sessionId:
|
|
4417
|
+
sessionId: z9.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3478
4418
|
};
|
|
3479
|
-
var rectShape =
|
|
3480
|
-
x:
|
|
3481
|
-
y:
|
|
3482
|
-
width:
|
|
3483
|
-
height:
|
|
4419
|
+
var rectShape = z9.object({
|
|
4420
|
+
x: z9.number(),
|
|
4421
|
+
y: z9.number(),
|
|
4422
|
+
width: z9.number(),
|
|
4423
|
+
height: z9.number()
|
|
3484
4424
|
});
|
|
3485
4425
|
function screenshotProvider(deps) {
|
|
3486
4426
|
const p = deps.realInput;
|
|
@@ -3492,11 +4432,11 @@ var noProvider = {
|
|
|
3492
4432
|
recommendation: VISUAL_NO_PROVIDER_RECOMMENDATION
|
|
3493
4433
|
};
|
|
3494
4434
|
function asBox(value) {
|
|
3495
|
-
const b =
|
|
3496
|
-
const x =
|
|
3497
|
-
const y =
|
|
3498
|
-
const w =
|
|
3499
|
-
const h =
|
|
4435
|
+
const b = asRecord(asRecord(value)["box"]);
|
|
4436
|
+
const x = asNumber(b["x"]);
|
|
4437
|
+
const y = asNumber(b["y"]);
|
|
4438
|
+
const w = asNumber(b["width"]);
|
|
4439
|
+
const h = asNumber(b["height"]);
|
|
3500
4440
|
if (x === void 0 || y === void 0 || w === void 0 || h === void 0)
|
|
3501
4441
|
return void 0;
|
|
3502
4442
|
if (w <= 0 || h <= 0)
|
|
@@ -3506,12 +4446,12 @@ function asBox(value) {
|
|
|
3506
4446
|
async function buildOpts(deps, sessionId, args) {
|
|
3507
4447
|
const clipArg = args["clip"];
|
|
3508
4448
|
if (clipArg !== void 0) {
|
|
3509
|
-
const c =
|
|
4449
|
+
const c = asRecord(clipArg);
|
|
3510
4450
|
const box = asBox({ box: c });
|
|
3511
4451
|
if (box !== void 0)
|
|
3512
4452
|
return { clip: box };
|
|
3513
4453
|
}
|
|
3514
|
-
const ref =
|
|
4454
|
+
const ref = asString(args["ref"]);
|
|
3515
4455
|
if (ref !== void 0) {
|
|
3516
4456
|
const session = deps.sessions.resolve(sessionId);
|
|
3517
4457
|
const res = await session.command(IrisCommand.INSPECT, { ref });
|
|
@@ -3537,31 +4477,31 @@ var VISUAL_TOOLS = [
|
|
|
3537
4477
|
name: IrisTool.SCREENSHOT,
|
|
3538
4478
|
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.",
|
|
3539
4479
|
inputSchema: {
|
|
3540
|
-
name:
|
|
3541
|
-
fullPage:
|
|
3542
|
-
ref:
|
|
4480
|
+
name: z9.string().describe("Baseline name \u2014 saved as .iris/visual/<name>.png. Use the same name in iris_visual_diff to compare."),
|
|
4481
|
+
fullPage: z9.boolean().optional().describe("Capture the full scroll height. Default: viewport only."),
|
|
4482
|
+
ref: z9.string().optional().describe("Element ref to screenshot (scopes to element bounding box). Omit for full page."),
|
|
3543
4483
|
clip: rectShape.optional().describe("Explicit { x, y, width, height } clip rectangle in page coordinates."),
|
|
3544
4484
|
...sessionIdShape4
|
|
3545
4485
|
},
|
|
3546
4486
|
outputSchema: {
|
|
3547
|
-
ok:
|
|
3548
|
-
saved:
|
|
3549
|
-
name:
|
|
3550
|
-
path:
|
|
3551
|
-
bytes:
|
|
3552
|
-
reason:
|
|
3553
|
-
recommendation:
|
|
4487
|
+
ok: z9.boolean(),
|
|
4488
|
+
saved: z9.boolean().optional(),
|
|
4489
|
+
name: z9.string().optional(),
|
|
4490
|
+
path: z9.string().optional(),
|
|
4491
|
+
bytes: z9.number().optional(),
|
|
4492
|
+
reason: z9.string().optional(),
|
|
4493
|
+
recommendation: z9.string().optional()
|
|
3554
4494
|
},
|
|
3555
4495
|
handler: async (deps, args) => {
|
|
3556
4496
|
const provider = screenshotProvider(deps);
|
|
3557
4497
|
if (provider === void 0)
|
|
3558
4498
|
return noProvider;
|
|
3559
|
-
const sessionId =
|
|
4499
|
+
const sessionId = asString(args["sessionId"]);
|
|
3560
4500
|
const session = deps.sessions.resolve(sessionId);
|
|
3561
4501
|
const png = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
|
|
3562
4502
|
if (png === void 0)
|
|
3563
4503
|
return { ok: false, reason: VisualReason.CAPTURE_FAILED };
|
|
3564
|
-
const name =
|
|
4504
|
+
const name = asString(args["name"]) ?? "default";
|
|
3565
4505
|
const store = new VisualStore(deps.fs, deps.irisRoot);
|
|
3566
4506
|
const path = await store.saveBaseline(name, png);
|
|
3567
4507
|
return { ok: true, saved: true, name, path, bytes: png.length };
|
|
@@ -3571,39 +4511,39 @@ var VISUAL_TOOLS = [
|
|
|
3571
4511
|
name: IrisTool.VISUAL_DIFF,
|
|
3572
4512
|
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).",
|
|
3573
4513
|
inputSchema: {
|
|
3574
|
-
baseline:
|
|
3575
|
-
fullPage:
|
|
3576
|
-
ref:
|
|
4514
|
+
baseline: z9.string().describe("Baseline screenshot name (from iris_screenshot). Used to compare with the current screenshot."),
|
|
4515
|
+
fullPage: z9.boolean().optional(),
|
|
4516
|
+
ref: z9.string().optional(),
|
|
3577
4517
|
clip: rectShape.optional(),
|
|
3578
|
-
masks:
|
|
3579
|
-
maxRatio:
|
|
3580
|
-
threshold:
|
|
4518
|
+
masks: z9.array(rectShape).optional(),
|
|
4519
|
+
maxRatio: z9.number().optional(),
|
|
4520
|
+
threshold: z9.number().optional().describe("Pixel difference threshold (0\u20131). Default: 0.01."),
|
|
3581
4521
|
...sessionIdShape4
|
|
3582
4522
|
},
|
|
3583
4523
|
outputSchema: {
|
|
3584
|
-
ok:
|
|
3585
|
-
match:
|
|
3586
|
-
diffPct:
|
|
3587
|
-
diffPath:
|
|
3588
|
-
reason:
|
|
4524
|
+
ok: z9.boolean(),
|
|
4525
|
+
match: z9.boolean().optional(),
|
|
4526
|
+
diffPct: z9.number().optional(),
|
|
4527
|
+
diffPath: z9.string().optional(),
|
|
4528
|
+
reason: z9.string().optional()
|
|
3589
4529
|
},
|
|
3590
4530
|
handler: async (deps, args) => {
|
|
3591
4531
|
const provider = screenshotProvider(deps);
|
|
3592
4532
|
if (provider === void 0)
|
|
3593
4533
|
return noProvider;
|
|
3594
|
-
const baseline =
|
|
4534
|
+
const baseline = asString(args["baseline"]) ?? "";
|
|
3595
4535
|
const store = new VisualStore(deps.fs, deps.irisRoot);
|
|
3596
4536
|
const baselineBytes = await store.readBaseline(baseline);
|
|
3597
4537
|
if (baselineBytes === void 0)
|
|
3598
4538
|
return { ok: false, reason: VisualReason.BASELINE_MISSING };
|
|
3599
|
-
const sessionId =
|
|
4539
|
+
const sessionId = asString(args["sessionId"]);
|
|
3600
4540
|
const session = deps.sessions.resolve(sessionId);
|
|
3601
4541
|
const current = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
|
|
3602
4542
|
if (current === void 0)
|
|
3603
4543
|
return { ok: false, reason: VisualReason.CAPTURE_FAILED };
|
|
3604
4544
|
const masks = rectsFrom(args["masks"]);
|
|
3605
|
-
const threshold =
|
|
3606
|
-
const maxRatio =
|
|
4545
|
+
const threshold = asNumber(args["threshold"]);
|
|
4546
|
+
const maxRatio = asNumber(args["maxRatio"]);
|
|
3607
4547
|
const result = await diffPng(baselineBytes, current, {
|
|
3608
4548
|
...threshold !== void 0 ? { threshold } : {},
|
|
3609
4549
|
...maxRatio !== void 0 ? { maxRatio } : {},
|
|
@@ -3620,7 +4560,7 @@ var VISUAL_TOOLS = [
|
|
|
3620
4560
|
];
|
|
3621
4561
|
|
|
3622
4562
|
// ../server/dist/crawl/crawl-tools.js
|
|
3623
|
-
import { z as
|
|
4563
|
+
import { z as z10 } from "zod";
|
|
3624
4564
|
|
|
3625
4565
|
// ../server/dist/crawl/crawl.js
|
|
3626
4566
|
function isActivity(e) {
|
|
@@ -3633,7 +4573,7 @@ function failedRequests(events, floor) {
|
|
|
3633
4573
|
return events.filter((e) => {
|
|
3634
4574
|
if (e.type !== EventType.NET_REQUEST)
|
|
3635
4575
|
return false;
|
|
3636
|
-
const status =
|
|
4576
|
+
const status = asNumber(e.data["status"]);
|
|
3637
4577
|
return status !== void 0 && status >= floor;
|
|
3638
4578
|
});
|
|
3639
4579
|
}
|
|
@@ -3663,7 +4603,7 @@ async function crawl(session, opts, sleep) {
|
|
|
3663
4603
|
const act = await session.command(IrisCommand.ACT, {
|
|
3664
4604
|
ref: item.ref,
|
|
3665
4605
|
action: ActionType.CLICK,
|
|
3666
|
-
args: {}
|
|
4606
|
+
args: opts.confirmDangerous === true ? { [DANGEROUS_ACTION_CONFIRM_ARG]: true } : {}
|
|
3667
4607
|
});
|
|
3668
4608
|
await sleep(settleMs);
|
|
3669
4609
|
const events = session.eventsSince(since);
|
|
@@ -3674,14 +4614,14 @@ async function crawl(session, opts, sleep) {
|
|
|
3674
4614
|
kind: CrawlAnomalyKind.CONSOLE_ERROR,
|
|
3675
4615
|
ref: item.ref,
|
|
3676
4616
|
desc: item.desc,
|
|
3677
|
-
detail:
|
|
4617
|
+
detail: asString(e.data["message"]) ?? e.type
|
|
3678
4618
|
});
|
|
3679
4619
|
}
|
|
3680
4620
|
for (const e of failedRequests(events, CRAWL_DEFAULTS.FAILED_STATUS)) {
|
|
3681
4621
|
counts.failedRequests += 1;
|
|
3682
|
-
const method =
|
|
3683
|
-
const url =
|
|
3684
|
-
const status =
|
|
4622
|
+
const method = asString(e.data["method"]) ?? "";
|
|
4623
|
+
const url = asString(e.data["url"]) ?? "";
|
|
4624
|
+
const status = asNumber(e.data["status"]);
|
|
3685
4625
|
anomalies.push({
|
|
3686
4626
|
kind: CrawlAnomalyKind.FAILED_REQUEST,
|
|
3687
4627
|
ref: item.ref,
|
|
@@ -3689,7 +4629,7 @@ async function crawl(session, opts, sleep) {
|
|
|
3689
4629
|
detail: `${method} ${url} \u2192 ${status ?? ""}`.trim()
|
|
3690
4630
|
});
|
|
3691
4631
|
}
|
|
3692
|
-
const dispatched =
|
|
4632
|
+
const dispatched = asRecord(act.result)["dispatched"] !== false && act.ok;
|
|
3693
4633
|
if (dispatched && errs.length === 0 && !events.some(isActivity)) {
|
|
3694
4634
|
counts.deadControls += 1;
|
|
3695
4635
|
anomalies.push({
|
|
@@ -3717,32 +4657,34 @@ var CRAWL_TOOLS = [
|
|
|
3717
4657
|
name: IrisTool.CRAWL,
|
|
3718
4658
|
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 }.",
|
|
3719
4659
|
inputSchema: {
|
|
3720
|
-
maxSteps:
|
|
3721
|
-
settleMs:
|
|
3722
|
-
scope:
|
|
3723
|
-
|
|
4660
|
+
maxSteps: z10.number().optional().describe("Maximum number of controls to click. Default: 25."),
|
|
4661
|
+
settleMs: z10.number().optional().describe("Milliseconds to wait after each click for the app to react. Default: 500."),
|
|
4662
|
+
scope: z10.string().optional().describe("CSS selector or element ref to restrict crawling to a subtree."),
|
|
4663
|
+
confirmDangerous: z10.boolean().optional().describe("Set true to allow controls classified as destructive. Default false; those controls are blocked by the browser."),
|
|
4664
|
+
sessionId: z10.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3724
4665
|
},
|
|
3725
4666
|
outputSchema: {
|
|
3726
|
-
interactiveFound:
|
|
3727
|
-
stepsRun:
|
|
3728
|
-
anomalies:
|
|
3729
|
-
kind:
|
|
3730
|
-
ref:
|
|
3731
|
-
desc:
|
|
3732
|
-
detail:
|
|
4667
|
+
interactiveFound: z10.number(),
|
|
4668
|
+
stepsRun: z10.number(),
|
|
4669
|
+
anomalies: z10.array(z10.object({
|
|
4670
|
+
kind: z10.string(),
|
|
4671
|
+
ref: z10.string(),
|
|
4672
|
+
desc: z10.string(),
|
|
4673
|
+
detail: z10.string().optional()
|
|
3733
4674
|
})),
|
|
3734
|
-
counts:
|
|
3735
|
-
truncated:
|
|
4675
|
+
counts: z10.record(z10.number()),
|
|
4676
|
+
truncated: z10.boolean()
|
|
3736
4677
|
},
|
|
3737
4678
|
handler: (deps, args) => {
|
|
3738
|
-
const session = deps.sessions.resolve(
|
|
3739
|
-
const maxSteps =
|
|
3740
|
-
const settleMs =
|
|
3741
|
-
const scope =
|
|
4679
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4680
|
+
const maxSteps = asNumber(args["maxSteps"]);
|
|
4681
|
+
const settleMs = asNumber(args["settleMs"]);
|
|
4682
|
+
const scope = asString(args["scope"]);
|
|
3742
4683
|
const opts = {
|
|
3743
4684
|
...maxSteps !== void 0 ? { maxSteps } : {},
|
|
3744
4685
|
...settleMs !== void 0 ? { settleMs } : {},
|
|
3745
|
-
...scope !== void 0 ? { scope } : {}
|
|
4686
|
+
...scope !== void 0 ? { scope } : {},
|
|
4687
|
+
...args["confirmDangerous"] === true ? { confirmDangerous: true } : {}
|
|
3746
4688
|
};
|
|
3747
4689
|
return crawl(session, opts, nodeSleep2);
|
|
3748
4690
|
}
|
|
@@ -3750,7 +4692,7 @@ var CRAWL_TOOLS = [
|
|
|
3750
4692
|
];
|
|
3751
4693
|
|
|
3752
4694
|
// ../server/dist/input/scroll-tools.js
|
|
3753
|
-
import { z as
|
|
4695
|
+
import { z as z11 } from "zod";
|
|
3754
4696
|
|
|
3755
4697
|
// ../server/dist/input/scroll-find.js
|
|
3756
4698
|
async function queryFirst(session, q) {
|
|
@@ -3759,9 +4701,9 @@ async function queryFirst(session, q) {
|
|
|
3759
4701
|
value: q.value,
|
|
3760
4702
|
...q.name !== void 0 ? { name: q.name } : {}
|
|
3761
4703
|
});
|
|
3762
|
-
const elements =
|
|
4704
|
+
const elements = asRecord(res.result)["elements"];
|
|
3763
4705
|
if (Array.isArray(elements) && elements.length > 0)
|
|
3764
|
-
return
|
|
4706
|
+
return asRecord(elements[0]);
|
|
3765
4707
|
return void 0;
|
|
3766
4708
|
}
|
|
3767
4709
|
async function scrollToFind(session, q, opts = {}) {
|
|
@@ -3780,7 +4722,7 @@ async function scrollToFind(session, q, opts = {}) {
|
|
|
3780
4722
|
const hit = await queryFirst(session, q);
|
|
3781
4723
|
if (hit !== void 0)
|
|
3782
4724
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
3783
|
-
const data =
|
|
4725
|
+
const data = asRecord(sr.result);
|
|
3784
4726
|
if (data["atEnd"] === true || data["scrolled"] === false) {
|
|
3785
4727
|
return { found: false, scrolls, exhausted: true };
|
|
3786
4728
|
}
|
|
@@ -3788,7 +4730,7 @@ async function scrollToFind(session, q, opts = {}) {
|
|
|
3788
4730
|
for (let i = 0; i < max; i += 1) {
|
|
3789
4731
|
const sr = await session.command(IrisCommand.SCROLL, q.container !== void 0 ? { ref: q.container } : {});
|
|
3790
4732
|
scrolls += 1;
|
|
3791
|
-
const data =
|
|
4733
|
+
const data = asRecord(sr.result);
|
|
3792
4734
|
const hit = await queryFirst(session, q);
|
|
3793
4735
|
if (hit !== void 0)
|
|
3794
4736
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
@@ -3805,58 +4747,58 @@ var SCROLL_TOOLS = [
|
|
|
3805
4747
|
name: IrisTool.SCROLL_TO,
|
|
3806
4748
|
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 }.",
|
|
3807
4749
|
inputSchema: {
|
|
3808
|
-
by:
|
|
3809
|
-
value:
|
|
3810
|
-
name:
|
|
3811
|
-
container:
|
|
3812
|
-
maxScrolls:
|
|
3813
|
-
targetIndex:
|
|
3814
|
-
totalCount:
|
|
3815
|
-
sessionId:
|
|
4750
|
+
by: z11.string().describe("Query strategy for finding the target: role | text | testid | label | placeholder | alt"),
|
|
4751
|
+
value: z11.string().describe("Query value for the selected strategy (the element to scroll into view)."),
|
|
4752
|
+
name: z11.string().optional().describe("Optional accessible name filter when using by=role."),
|
|
4753
|
+
container: z11.string().optional().describe("Element ref for the scrollable container. Omit to scroll the document."),
|
|
4754
|
+
maxScrolls: z11.number().optional().describe("Maximum number of scroll steps before giving up. Default: 20."),
|
|
4755
|
+
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."),
|
|
4756
|
+
totalCount: z11.number().optional().describe("Total item count in the virtualized list. Required for bisection with targetIndex."),
|
|
4757
|
+
sessionId: z11.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3816
4758
|
},
|
|
3817
4759
|
outputSchema: {
|
|
3818
|
-
found:
|
|
3819
|
-
element:
|
|
3820
|
-
scrolls:
|
|
3821
|
-
exhausted:
|
|
4760
|
+
found: z11.boolean(),
|
|
4761
|
+
element: z11.object({ ref: z11.string(), role: z11.string(), name: z11.string() }).optional(),
|
|
4762
|
+
scrolls: z11.number(),
|
|
4763
|
+
exhausted: z11.boolean()
|
|
3822
4764
|
},
|
|
3823
4765
|
handler: (deps, args) => {
|
|
3824
|
-
const session = deps.sessions.resolve(
|
|
3825
|
-
const name =
|
|
3826
|
-
const container =
|
|
3827
|
-
const targetIndex =
|
|
3828
|
-
const totalCount =
|
|
4766
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4767
|
+
const name = asString(args["name"]);
|
|
4768
|
+
const container = asString(args["container"]);
|
|
4769
|
+
const targetIndex = asNumber(args["targetIndex"]);
|
|
4770
|
+
const totalCount = asNumber(args["totalCount"]);
|
|
3829
4771
|
const q = {
|
|
3830
|
-
by:
|
|
3831
|
-
value:
|
|
4772
|
+
by: asString(args["by"]) ?? "",
|
|
4773
|
+
value: asString(args["value"]) ?? "",
|
|
3832
4774
|
...name !== void 0 ? { name } : {},
|
|
3833
4775
|
...container !== void 0 ? { container } : {},
|
|
3834
4776
|
...targetIndex !== void 0 ? { targetIndex } : {},
|
|
3835
4777
|
...totalCount !== void 0 ? { totalCount } : {}
|
|
3836
4778
|
};
|
|
3837
|
-
const maxScrolls =
|
|
4779
|
+
const maxScrolls = asNumber(args["maxScrolls"]);
|
|
3838
4780
|
return scrollToFind(session, q, maxScrolls !== void 0 ? { maxScrolls } : {});
|
|
3839
4781
|
}
|
|
3840
4782
|
}
|
|
3841
4783
|
];
|
|
3842
4784
|
|
|
3843
4785
|
// ../server/dist/session/session-tools.js
|
|
3844
|
-
import { z as
|
|
4786
|
+
import { z as z12 } from "zod";
|
|
3845
4787
|
var SESSION_TOOLS = [
|
|
3846
4788
|
{
|
|
3847
4789
|
name: IrisTool.SESSION,
|
|
3848
4790
|
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 }.",
|
|
3849
4791
|
inputSchema: {
|
|
3850
|
-
idleEndMs:
|
|
3851
|
-
sessionId:
|
|
4792
|
+
idleEndMs: z12.number().optional().describe("Idle window in milliseconds after which the presenter session auto-ends. Default: 300000 (5 min). Raise for slow apps."),
|
|
4793
|
+
sessionId: z12.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3852
4794
|
},
|
|
3853
4795
|
outputSchema: {
|
|
3854
|
-
applied:
|
|
3855
|
-
idleEndMs:
|
|
4796
|
+
applied: z12.boolean(),
|
|
4797
|
+
idleEndMs: z12.number().optional()
|
|
3856
4798
|
},
|
|
3857
4799
|
handler: async (deps, args) => {
|
|
3858
|
-
const session = deps.sessions.resolve(
|
|
3859
|
-
const idleEndMs =
|
|
4800
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4801
|
+
const idleEndMs = asNumber(args["idleEndMs"]);
|
|
3860
4802
|
if (idleEndMs !== void 0)
|
|
3861
4803
|
session.setIdleEndMs(idleEndMs);
|
|
3862
4804
|
const res = await session.command(IrisCommand.SESSION_CONFIG, idleEndMs !== void 0 ? { idleEndMs } : {});
|
|
@@ -3868,7 +4810,7 @@ var SESSION_TOOLS = [
|
|
|
3868
4810
|
];
|
|
3869
4811
|
|
|
3870
4812
|
// ../server/dist/flows/annotate-tools.js
|
|
3871
|
-
import { z as
|
|
4813
|
+
import { z as z13 } from "zod";
|
|
3872
4814
|
|
|
3873
4815
|
// ../server/dist/flows/annotate.js
|
|
3874
4816
|
function compileAnnotation(a, stepCount) {
|
|
@@ -3945,23 +4887,23 @@ var ANNOTATE_TOOLS = [
|
|
|
3945
4887
|
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.",
|
|
3946
4888
|
inputSchema: {
|
|
3947
4889
|
// `flow` selects the recording; `name`/`signal`/`testid`/`dataMatches` are the annotation fields.
|
|
3948
|
-
flow:
|
|
3949
|
-
kind:
|
|
3950
|
-
name:
|
|
3951
|
-
testid:
|
|
3952
|
-
signal:
|
|
3953
|
-
dataMatches:
|
|
3954
|
-
sessionId:
|
|
3955
|
-
annotation:
|
|
4890
|
+
flow: z13.string().optional().describe("Named recording to annotate. Defaults to 'default'."),
|
|
4891
|
+
kind: z13.string().describe("Annotation kind: assert-signal | assert-visible | mark-dynamic | success-state."),
|
|
4892
|
+
name: z13.string().optional().describe("Signal name for assert-signal annotations."),
|
|
4893
|
+
testid: z13.string().optional().describe("data-testid value for assert-visible / mark-dynamic / success-state annotations."),
|
|
4894
|
+
signal: z13.string().optional().describe("Signal name for success-state annotations."),
|
|
4895
|
+
dataMatches: z13.record(z13.unknown()).optional().describe("Key/value pairs the signal payload must match (assert-signal only)."),
|
|
4896
|
+
sessionId: z13.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open."),
|
|
4897
|
+
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.")
|
|
3956
4898
|
},
|
|
3957
4899
|
outputSchema: {
|
|
3958
|
-
ok:
|
|
3959
|
-
target:
|
|
3960
|
-
compiled:
|
|
3961
|
-
code:
|
|
4900
|
+
ok: z13.boolean(),
|
|
4901
|
+
target: z13.string().optional(),
|
|
4902
|
+
compiled: z13.string().optional(),
|
|
4903
|
+
code: z13.string().optional()
|
|
3962
4904
|
},
|
|
3963
4905
|
handler: (deps, args) => {
|
|
3964
|
-
const name =
|
|
4906
|
+
const name = asString(args["flow"]) ?? DEFAULT_RECORDING;
|
|
3965
4907
|
const parsed = AnnotationSchema.safeParse(args);
|
|
3966
4908
|
if (!parsed.success) {
|
|
3967
4909
|
return Promise.resolve({ ok: false, code: AnnotationErrorCode.UNKNOWN_KIND });
|
|
@@ -3990,19 +4932,19 @@ var ANNOTATE_TOOLS = [
|
|
|
3990
4932
|
];
|
|
3991
4933
|
|
|
3992
4934
|
// ../server/dist/session/live-control-tools.js
|
|
3993
|
-
import { z as
|
|
4935
|
+
import { z as z14 } from "zod";
|
|
3994
4936
|
var sessionIdShape5 = {
|
|
3995
|
-
sessionId:
|
|
4937
|
+
sessionId: z14.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3996
4938
|
};
|
|
3997
4939
|
var LIVE_CONTROL_TOOLS = [
|
|
3998
4940
|
{
|
|
3999
4941
|
name: IrisTool.END_SESSION,
|
|
4000
4942
|
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.',
|
|
4001
|
-
inputSchema: { summary:
|
|
4002
|
-
outputSchema: { ended:
|
|
4943
|
+
inputSchema: { summary: z14.string().optional(), ...sessionIdShape5 },
|
|
4944
|
+
outputSchema: { ended: z14.boolean(), sessionId: z14.string() },
|
|
4003
4945
|
handler: (deps, args) => {
|
|
4004
|
-
const session = deps.sessions.resolve(
|
|
4005
|
-
session.setState(SessionState.ENDED,
|
|
4946
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4947
|
+
session.setState(SessionState.ENDED, asString(args["summary"]));
|
|
4006
4948
|
return Promise.resolve({ ended: true, sessionId: session.id });
|
|
4007
4949
|
}
|
|
4008
4950
|
},
|
|
@@ -4010,9 +4952,9 @@ var LIVE_CONTROL_TOOLS = [
|
|
|
4010
4952
|
name: IrisTool.RESUME,
|
|
4011
4953
|
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.',
|
|
4012
4954
|
inputSchema: { ...sessionIdShape5 },
|
|
4013
|
-
outputSchema: { ok:
|
|
4955
|
+
outputSchema: { ok: z14.boolean() },
|
|
4014
4956
|
handler: (deps, args) => {
|
|
4015
|
-
const session = deps.sessions.resolve(
|
|
4957
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4016
4958
|
session.setState(SessionState.ACTIVE);
|
|
4017
4959
|
return Promise.resolve({ ok: true });
|
|
4018
4960
|
}
|
|
@@ -4021,9 +4963,9 @@ var LIVE_CONTROL_TOOLS = [
|
|
|
4021
4963
|
name: IrisTool.MESSAGES,
|
|
4022
4964
|
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.",
|
|
4023
4965
|
inputSchema: { ...sessionIdShape5 },
|
|
4024
|
-
outputSchema: { messages:
|
|
4966
|
+
outputSchema: { messages: z14.array(z14.unknown()) },
|
|
4025
4967
|
handler: (deps, args) => {
|
|
4026
|
-
const session = deps.sessions.resolve(
|
|
4968
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4027
4969
|
return Promise.resolve({ messages: session.drainInbox() });
|
|
4028
4970
|
}
|
|
4029
4971
|
}
|
|
@@ -4048,6 +4990,266 @@ function withControl(session, result) {
|
|
|
4048
4990
|
return control === void 0 ? result : { ...result, control };
|
|
4049
4991
|
}
|
|
4050
4992
|
|
|
4993
|
+
// ../server/dist/update/update-tools.js
|
|
4994
|
+
import { z as z15 } from "zod";
|
|
4995
|
+
|
|
4996
|
+
// ../server/dist/update/update-checker.js
|
|
4997
|
+
import * as fs from "fs";
|
|
4998
|
+
import * as https from "https";
|
|
4999
|
+
import { join as join2 } from "path";
|
|
5000
|
+
import { homedir } from "os";
|
|
5001
|
+
var IRIS_HOME = join2(homedir(), ".iris");
|
|
5002
|
+
var MANIFEST_PATH = join2(IRIS_HOME, "update-manifest.json");
|
|
5003
|
+
var NPM_REGISTRY = "https://registry.npmjs.org/@syrin/iris/latest";
|
|
5004
|
+
function loadManifest() {
|
|
5005
|
+
if (!fs.existsSync(MANIFEST_PATH))
|
|
5006
|
+
return null;
|
|
5007
|
+
try {
|
|
5008
|
+
const raw = fs.readFileSync(MANIFEST_PATH, "utf8");
|
|
5009
|
+
return JSON.parse(raw);
|
|
5010
|
+
} catch {
|
|
5011
|
+
return null;
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
function saveManifest(manifest) {
|
|
5015
|
+
fs.mkdirSync(IRIS_HOME, { recursive: true });
|
|
5016
|
+
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2), "utf8");
|
|
5017
|
+
}
|
|
5018
|
+
function isCacheFresh(manifest) {
|
|
5019
|
+
const checked = new Date(manifest.lastChecked).getTime();
|
|
5020
|
+
return Date.now() - checked < UpdateCheckIntervalMs;
|
|
5021
|
+
}
|
|
5022
|
+
function fetchNpmInfo() {
|
|
5023
|
+
return new Promise((resolve, reject) => {
|
|
5024
|
+
const req = https.get(NPM_REGISTRY, (res) => {
|
|
5025
|
+
let body = "";
|
|
5026
|
+
res.setEncoding("utf8");
|
|
5027
|
+
res.on("data", (chunk) => {
|
|
5028
|
+
body += chunk;
|
|
5029
|
+
});
|
|
5030
|
+
res.on("end", () => {
|
|
5031
|
+
try {
|
|
5032
|
+
resolve(JSON.parse(body));
|
|
5033
|
+
} catch (err) {
|
|
5034
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
5035
|
+
}
|
|
5036
|
+
});
|
|
5037
|
+
res.on("error", reject);
|
|
5038
|
+
});
|
|
5039
|
+
req.setTimeout(5e3, () => {
|
|
5040
|
+
req.destroy();
|
|
5041
|
+
reject(new Error("npm registry request timed out"));
|
|
5042
|
+
});
|
|
5043
|
+
req.on("error", reject);
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
async function checkForUpdate(currentVersion) {
|
|
5047
|
+
const cached = loadManifest();
|
|
5048
|
+
if (cached !== null && cached.currentVersion === currentVersion && isCacheFresh(cached)) {
|
|
5049
|
+
return cached;
|
|
5050
|
+
}
|
|
5051
|
+
try {
|
|
5052
|
+
const info = await fetchNpmInfo();
|
|
5053
|
+
const updateAvailable = info.version !== currentVersion;
|
|
5054
|
+
const manifest = {
|
|
5055
|
+
currentVersion,
|
|
5056
|
+
latestVersion: info.version,
|
|
5057
|
+
updateAvailable,
|
|
5058
|
+
lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5059
|
+
...info.iris?.changelog !== void 0 ? { changelog: info.iris.changelog } : {},
|
|
5060
|
+
...info.iris?.breakingChanges !== void 0 ? { breakingChanges: info.iris.breakingChanges } : {},
|
|
5061
|
+
...cached?.previousVersion !== void 0 ? { previousVersion: cached.previousVersion } : {}
|
|
5062
|
+
};
|
|
5063
|
+
saveManifest(manifest);
|
|
5064
|
+
return manifest;
|
|
5065
|
+
} catch (err) {
|
|
5066
|
+
log("iris_update_check_failed", {
|
|
5067
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5068
|
+
});
|
|
5069
|
+
if (cached !== null)
|
|
5070
|
+
return { ...cached, currentVersion };
|
|
5071
|
+
return {
|
|
5072
|
+
currentVersion,
|
|
5073
|
+
updateAvailable: false,
|
|
5074
|
+
lastChecked: (/* @__PURE__ */ new Date()).toISOString()
|
|
5075
|
+
};
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
// ../server/dist/update/updater.js
|
|
5080
|
+
import { execFile } from "child_process";
|
|
5081
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5082
|
+
import { platform } from "os";
|
|
5083
|
+
import { dirname, join as join3 } from "path";
|
|
5084
|
+
var NPM_BIN = platform() === "win32" ? "npm.cmd" : "npm";
|
|
5085
|
+
var NPM_TIMEOUT_MS = 12e4;
|
|
5086
|
+
var ExecutionKind = {
|
|
5087
|
+
/** Launched via `npx @syrin/iris` — npm re-resolves the package on restart. */
|
|
5088
|
+
NPX: "npx",
|
|
5089
|
+
/** Installed globally via `npm install -g`. */
|
|
5090
|
+
GLOBAL: "global",
|
|
5091
|
+
/** Installed as a local project dependency. */
|
|
5092
|
+
LOCAL: "local"
|
|
5093
|
+
};
|
|
5094
|
+
function detectExecutionKind() {
|
|
5095
|
+
const script = process.argv[1] ?? "";
|
|
5096
|
+
if (script.includes("/_npx/") || script.includes("\\_npx\\"))
|
|
5097
|
+
return ExecutionKind.NPX;
|
|
5098
|
+
if (script.includes("/node_modules/") || script.includes("\\node_modules\\")) {
|
|
5099
|
+
return ExecutionKind.LOCAL;
|
|
5100
|
+
}
|
|
5101
|
+
return ExecutionKind.GLOBAL;
|
|
5102
|
+
}
|
|
5103
|
+
function findLocalProjectRoot() {
|
|
5104
|
+
let dir = process.cwd();
|
|
5105
|
+
for (; ; ) {
|
|
5106
|
+
if (existsSync2(join3(dir, "package.json")))
|
|
5107
|
+
return dir;
|
|
5108
|
+
const parent = dirname(dir);
|
|
5109
|
+
if (parent === dir)
|
|
5110
|
+
return null;
|
|
5111
|
+
dir = parent;
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
function runNpm(args, opts = {}) {
|
|
5115
|
+
return new Promise((resolve, reject) => {
|
|
5116
|
+
execFile(NPM_BIN, args, { timeout: NPM_TIMEOUT_MS, ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {} }, (err, _stdout, stderr) => {
|
|
5117
|
+
if (err !== null) {
|
|
5118
|
+
reject(new Error(`npm ${args.join(" ")} failed: ${stderr !== "" ? stderr : err.message}`));
|
|
5119
|
+
} else {
|
|
5120
|
+
resolve();
|
|
5121
|
+
}
|
|
5122
|
+
});
|
|
5123
|
+
});
|
|
5124
|
+
}
|
|
5125
|
+
async function installVersion(version, kind) {
|
|
5126
|
+
const pkg = `@syrin/iris@${version}`;
|
|
5127
|
+
if (kind === ExecutionKind.NPX) {
|
|
5128
|
+
log("iris_update_npx_strategy", {
|
|
5129
|
+
note: "Running via npx \u2014 exiting so Claude Code restarts and npx fetches the new version"
|
|
5130
|
+
});
|
|
5131
|
+
return;
|
|
5132
|
+
}
|
|
5133
|
+
if (kind === ExecutionKind.LOCAL) {
|
|
5134
|
+
const root = findLocalProjectRoot();
|
|
5135
|
+
if (root !== null) {
|
|
5136
|
+
await runNpm(["install", pkg], { cwd: root });
|
|
5137
|
+
return;
|
|
5138
|
+
}
|
|
5139
|
+
log("iris_update_local_no_root", { fallback: "global" });
|
|
5140
|
+
}
|
|
5141
|
+
await runNpm(["install", "-g", pkg]);
|
|
5142
|
+
}
|
|
5143
|
+
async function installVersionRollback(version, kind) {
|
|
5144
|
+
if (kind === ExecutionKind.NPX) {
|
|
5145
|
+
log("iris_rollback_npx_strategy", {
|
|
5146
|
+
note: "Running via npx \u2014 update your .mcp.json args to pin the version you want to restore"
|
|
5147
|
+
});
|
|
5148
|
+
return;
|
|
5149
|
+
}
|
|
5150
|
+
await installVersion(version, kind);
|
|
5151
|
+
}
|
|
5152
|
+
async function applyUpdate(targetVersion) {
|
|
5153
|
+
const manifest = loadManifest();
|
|
5154
|
+
if (manifest !== null) {
|
|
5155
|
+
saveManifest({ ...manifest, previousVersion: manifest.currentVersion });
|
|
5156
|
+
}
|
|
5157
|
+
const kind = detectExecutionKind();
|
|
5158
|
+
log("iris_update_applying", { version: targetVersion, executionKind: kind });
|
|
5159
|
+
await installVersion(targetVersion, kind);
|
|
5160
|
+
log("iris_update_applied", { version: targetVersion, executionKind: kind });
|
|
5161
|
+
process.exit(0);
|
|
5162
|
+
}
|
|
5163
|
+
async function rollback() {
|
|
5164
|
+
const manifest = loadManifest();
|
|
5165
|
+
if (manifest === null || manifest.previousVersion === void 0) {
|
|
5166
|
+
throw new Error("No previous version available for rollback");
|
|
5167
|
+
}
|
|
5168
|
+
const prev = manifest.previousVersion;
|
|
5169
|
+
const kind = detectExecutionKind();
|
|
5170
|
+
log("iris_rollback_applying", { version: prev, executionKind: kind });
|
|
5171
|
+
await installVersionRollback(prev, kind);
|
|
5172
|
+
log("iris_rollback_applied", { version: prev, executionKind: kind });
|
|
5173
|
+
process.exit(0);
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5176
|
+
// ../server/dist/server-version.js
|
|
5177
|
+
import { createRequire } from "module";
|
|
5178
|
+
var _pkg = createRequire(import.meta.url)("../package.json");
|
|
5179
|
+
var SERVER_VERSION = _pkg.version;
|
|
5180
|
+
|
|
5181
|
+
// ../server/dist/update/update-tools.js
|
|
5182
|
+
var UPDATE_TOOLS = [
|
|
5183
|
+
{
|
|
5184
|
+
name: IrisTool.VERSION_INFO,
|
|
5185
|
+
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.",
|
|
5186
|
+
inputSchema: {},
|
|
5187
|
+
outputSchema: {
|
|
5188
|
+
currentVersion: z15.string().describe("The Iris server version currently running."),
|
|
5189
|
+
latestVersion: z15.string().optional().describe("Latest published version on npm."),
|
|
5190
|
+
updateAvailable: z15.boolean().describe("True when a newer version is available to install."),
|
|
5191
|
+
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).'),
|
|
5192
|
+
changelog: z15.string().optional().describe("Release notes for the latest version."),
|
|
5193
|
+
breakingChanges: z15.array(z15.string()).optional().describe("Breaking changes in the latest version that may affect your scripts."),
|
|
5194
|
+
rollbackAvailable: z15.boolean().describe("True when a previous version is stored and can be restored."),
|
|
5195
|
+
previousVersion: z15.string().optional().describe("The version that would be restored on rollback.")
|
|
5196
|
+
},
|
|
5197
|
+
handler: async (_deps) => {
|
|
5198
|
+
const manifest = await checkForUpdate(SERVER_VERSION);
|
|
5199
|
+
return {
|
|
5200
|
+
currentVersion: manifest.currentVersion,
|
|
5201
|
+
...manifest.latestVersion !== void 0 ? { latestVersion: manifest.latestVersion } : {},
|
|
5202
|
+
updateAvailable: manifest.updateAvailable,
|
|
5203
|
+
executionKind: detectExecutionKind(),
|
|
5204
|
+
...manifest.changelog !== void 0 ? { changelog: manifest.changelog } : {},
|
|
5205
|
+
...manifest.breakingChanges !== void 0 ? { breakingChanges: manifest.breakingChanges } : {},
|
|
5206
|
+
rollbackAvailable: manifest.previousVersion !== void 0,
|
|
5207
|
+
...manifest.previousVersion !== void 0 ? { previousVersion: manifest.previousVersion } : {}
|
|
5208
|
+
};
|
|
5209
|
+
}
|
|
5210
|
+
},
|
|
5211
|
+
{
|
|
5212
|
+
name: IrisTool.APPLY_UPDATE,
|
|
5213
|
+
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.',
|
|
5214
|
+
inputSchema: {
|
|
5215
|
+
confirm: z15.boolean().describe("Set to true to confirm the update should be applied. Required to prevent accidental upgrades.")
|
|
5216
|
+
},
|
|
5217
|
+
outputSchema: {
|
|
5218
|
+
ok: z15.boolean(),
|
|
5219
|
+
message: z15.string().optional()
|
|
5220
|
+
},
|
|
5221
|
+
handler: async (_deps, args) => {
|
|
5222
|
+
if (args["confirm"] !== true) {
|
|
5223
|
+
return { ok: false, message: "Set confirm:true to apply the update" };
|
|
5224
|
+
}
|
|
5225
|
+
const manifest = await checkForUpdate(SERVER_VERSION);
|
|
5226
|
+
if (!manifest.updateAvailable || manifest.latestVersion === void 0) {
|
|
5227
|
+
return { ok: false, message: "No update available \u2014 already on the latest version" };
|
|
5228
|
+
}
|
|
5229
|
+
await applyUpdate(manifest.latestVersion);
|
|
5230
|
+
return { ok: true };
|
|
5231
|
+
}
|
|
5232
|
+
},
|
|
5233
|
+
{
|
|
5234
|
+
name: IrisTool.ROLLBACK,
|
|
5235
|
+
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.",
|
|
5236
|
+
inputSchema: {
|
|
5237
|
+
confirm: z15.boolean().describe("Set to true to confirm the rollback. Required to prevent accidental downgrades.")
|
|
5238
|
+
},
|
|
5239
|
+
outputSchema: {
|
|
5240
|
+
ok: z15.boolean(),
|
|
5241
|
+
message: z15.string().optional()
|
|
5242
|
+
},
|
|
5243
|
+
handler: async (_deps, args) => {
|
|
5244
|
+
if (args["confirm"] !== true) {
|
|
5245
|
+
return { ok: false, message: "Set confirm:true to apply the rollback" };
|
|
5246
|
+
}
|
|
5247
|
+
await rollback();
|
|
5248
|
+
return { ok: true };
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
];
|
|
5252
|
+
|
|
4051
5253
|
// ../server/dist/tools/tools.js
|
|
4052
5254
|
async function snapshotTree(deps, sessionId) {
|
|
4053
5255
|
const session = deps.sessions.resolve(sessionId);
|
|
@@ -4058,7 +5260,7 @@ async function snapshotTree(deps, sessionId) {
|
|
|
4058
5260
|
return { lines: normalizeLines(snap.tree ?? ""), route: snap.status?.route ?? "" };
|
|
4059
5261
|
}
|
|
4060
5262
|
var sessionIdShape6 = {
|
|
4061
|
-
sessionId:
|
|
5263
|
+
sessionId: z16.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open \u2014 Iris resolves it automatically.")
|
|
4062
5264
|
};
|
|
4063
5265
|
async function commandOrThrow3(deps, sessionId, name, args) {
|
|
4064
5266
|
const session = deps.sessions.resolve(sessionId);
|
|
@@ -4068,11 +5270,11 @@ async function commandOrThrow3(deps, sessionId, name, args) {
|
|
|
4068
5270
|
return result.result;
|
|
4069
5271
|
}
|
|
4070
5272
|
function asBox2(value) {
|
|
4071
|
-
const b =
|
|
4072
|
-
const x =
|
|
4073
|
-
const y =
|
|
4074
|
-
const w =
|
|
4075
|
-
const h =
|
|
5273
|
+
const b = asRecord(asRecord(value)["box"]);
|
|
5274
|
+
const x = asNumber(b["x"]);
|
|
5275
|
+
const y = asNumber(b["y"]);
|
|
5276
|
+
const w = asNumber(b["width"]);
|
|
5277
|
+
const h = asNumber(b["height"]);
|
|
4076
5278
|
if (x === void 0 || y === void 0 || w === void 0 || h === void 0)
|
|
4077
5279
|
return void 0;
|
|
4078
5280
|
if (w <= 0 || h <= 0)
|
|
@@ -4088,29 +5290,51 @@ async function tryRealInput(deps, session, ref, action, args) {
|
|
|
4088
5290
|
return synthetic();
|
|
4089
5291
|
if (!isPointerAction(action))
|
|
4090
5292
|
return synthetic(InputModeReason.NOT_POINTER);
|
|
4091
|
-
const inner =
|
|
5293
|
+
const inner = asRecord(args["args"]);
|
|
4092
5294
|
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && inner["native"] !== true) {
|
|
4093
5295
|
return synthetic(InputModeReason.SYNTHETIC_CLICK_PREFERRED);
|
|
4094
5296
|
}
|
|
4095
5297
|
if (!await provider.isAvailableFor(session.url))
|
|
4096
5298
|
return synthetic(InputModeReason.PAGE_NOT_CORRELATED);
|
|
4097
|
-
const
|
|
5299
|
+
const inspected = await commandOrThrow3(deps, session.id, IrisCommand.INSPECT, { ref });
|
|
5300
|
+
const confirmed = inner[DANGEROUS_ACTION_CONFIRM_ARG] === true;
|
|
5301
|
+
const dangerousDescriptorText = (value2) => {
|
|
5302
|
+
const descriptor = asRecord(value2);
|
|
5303
|
+
return [
|
|
5304
|
+
asString(descriptor["name"]) ?? "",
|
|
5305
|
+
asString(descriptor["text"]) ?? "",
|
|
5306
|
+
asString(descriptor["value"]) ?? "",
|
|
5307
|
+
asString(descriptor["href"]) ?? "",
|
|
5308
|
+
asString(descriptor["formAction"]) ?? "",
|
|
5309
|
+
asString(descriptor["formText"]) ?? ""
|
|
5310
|
+
].join(" ");
|
|
5311
|
+
};
|
|
5312
|
+
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && !confirmed && isDangerousActionText(dangerousDescriptorText(inspected))) {
|
|
5313
|
+
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
5314
|
+
}
|
|
5315
|
+
const box = asBox2(inspected);
|
|
4098
5316
|
if (box === void 0)
|
|
4099
5317
|
return synthetic(InputModeReason.ELEMENT_NOT_LOCATABLE);
|
|
4100
5318
|
let toBox;
|
|
4101
5319
|
if (action === ActionType.DRAG) {
|
|
4102
|
-
const toRef =
|
|
5320
|
+
const toRef = asString(inner["toRef"]);
|
|
4103
5321
|
if (toRef === void 0)
|
|
4104
5322
|
return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
|
|
4105
|
-
|
|
5323
|
+
const targetInspected = await commandOrThrow3(deps, session.id, IrisCommand.INSPECT, {
|
|
5324
|
+
ref: toRef
|
|
5325
|
+
});
|
|
5326
|
+
if (!confirmed && isDangerousActionText(`${dangerousDescriptorText(inspected)} ${dangerousDescriptorText(targetInspected)}`)) {
|
|
5327
|
+
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
5328
|
+
}
|
|
5329
|
+
toBox = asBox2(targetInspected);
|
|
4106
5330
|
if (toBox === void 0)
|
|
4107
5331
|
return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
|
|
4108
5332
|
}
|
|
4109
5333
|
const performArgs = {};
|
|
4110
|
-
const value =
|
|
5334
|
+
const value = asString(inner["value"]);
|
|
4111
5335
|
if (value !== void 0)
|
|
4112
5336
|
performArgs.value = value;
|
|
4113
|
-
const text =
|
|
5337
|
+
const text = asString(inner["text"]);
|
|
4114
5338
|
if (text !== void 0)
|
|
4115
5339
|
performArgs.text = text;
|
|
4116
5340
|
if (toBox !== void 0)
|
|
@@ -4129,23 +5353,24 @@ async function tryRealInput(deps, session, ref, action, args) {
|
|
|
4129
5353
|
};
|
|
4130
5354
|
}
|
|
4131
5355
|
}
|
|
5356
|
+
var SNAPSHOT_CACHE = new SnapshotCache();
|
|
4132
5357
|
var TOOLS = [
|
|
4133
5358
|
{
|
|
4134
5359
|
name: IrisTool.SESSIONS,
|
|
4135
5360
|
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.",
|
|
4136
5361
|
inputSchema: {},
|
|
4137
5362
|
outputSchema: {
|
|
4138
|
-
sessions:
|
|
4139
|
-
sessionId:
|
|
4140
|
-
url:
|
|
4141
|
-
title:
|
|
4142
|
-
lastSeenMs:
|
|
4143
|
-
throttled:
|
|
4144
|
-
focused:
|
|
4145
|
-
hidden:
|
|
4146
|
-
realInputAvailable:
|
|
4147
|
-
stale:
|
|
4148
|
-
recommendation:
|
|
5363
|
+
sessions: z16.array(z16.object({
|
|
5364
|
+
sessionId: z16.string(),
|
|
5365
|
+
url: z16.string(),
|
|
5366
|
+
title: z16.string().optional(),
|
|
5367
|
+
lastSeenMs: z16.number(),
|
|
5368
|
+
throttled: z16.boolean(),
|
|
5369
|
+
focused: z16.boolean(),
|
|
5370
|
+
hidden: z16.boolean(),
|
|
5371
|
+
realInputAvailable: z16.boolean().optional(),
|
|
5372
|
+
stale: z16.boolean().optional(),
|
|
5373
|
+
recommendation: z16.string().optional()
|
|
4149
5374
|
})).describe("Connected browser sessions with health state.")
|
|
4150
5375
|
},
|
|
4151
5376
|
handler: async (deps) => {
|
|
@@ -4159,71 +5384,95 @@ var TOOLS = [
|
|
|
4159
5384
|
},
|
|
4160
5385
|
{
|
|
4161
5386
|
name: IrisTool.SNAPSHOT,
|
|
4162
|
-
description: "Semantic accessibility snapshot of the page or a subtree. mode: full|interactive|status. Use to see what is on screen right now.",
|
|
5387
|
+
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.",
|
|
4163
5388
|
inputSchema: {
|
|
4164
|
-
scope:
|
|
4165
|
-
mode:
|
|
5389
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the snapshot to a subtree. Omit to snapshot the whole page."),
|
|
5390
|
+
mode: z16.nativeEnum(SnapshotMode).optional().describe("full = all elements; interactive = only clickable/focusable elements; status = only route + title. Default: full."),
|
|
5391
|
+
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."),
|
|
4166
5392
|
...sessionIdShape6
|
|
4167
5393
|
},
|
|
4168
5394
|
outputSchema: {
|
|
4169
|
-
tree:
|
|
4170
|
-
status:
|
|
5395
|
+
tree: z16.string().optional().describe("Indented ARIA tree of every element on the page (or the scoped subtree)."),
|
|
5396
|
+
status: z16.object({ route: z16.string(), title: z16.string().optional() }).optional(),
|
|
5397
|
+
mode: z16.string().optional().describe("delta | unchanged when diff:true returned a change set."),
|
|
5398
|
+
delta: z16.object({
|
|
5399
|
+
added: z16.array(z16.string()),
|
|
5400
|
+
removed: z16.array(z16.string()),
|
|
5401
|
+
addedCount: z16.number(),
|
|
5402
|
+
removedCount: z16.number()
|
|
5403
|
+
}).optional().describe("Only present on a diff:true call that found changes."),
|
|
5404
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional().describe("Estimated size of this result \u2014 re-scope if large.")
|
|
4171
5405
|
},
|
|
4172
|
-
handler: (deps, args) =>
|
|
4173
|
-
|
|
4174
|
-
mode
|
|
4175
|
-
|
|
5406
|
+
handler: (deps, args) => {
|
|
5407
|
+
const sessionId = asString(args["sessionId"]);
|
|
5408
|
+
const mode = asString(args["mode"]) ?? SnapshotMode.FULL;
|
|
5409
|
+
return commandOrThrow3(deps, sessionId, IrisCommand.SNAPSHOT, {
|
|
5410
|
+
scope: args["scope"],
|
|
5411
|
+
mode
|
|
5412
|
+
}).then((raw) => withSizeCost(applySnapshotDelta(raw, {
|
|
5413
|
+
sessionId: sessionId ?? "default",
|
|
5414
|
+
scope: asString(args["scope"]) ?? "",
|
|
5415
|
+
mode,
|
|
5416
|
+
diff: args["diff"] === true
|
|
5417
|
+
}, SNAPSHOT_CACHE)));
|
|
5418
|
+
}
|
|
4176
5419
|
},
|
|
4177
5420
|
{
|
|
4178
5421
|
name: IrisTool.QUERY,
|
|
4179
|
-
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.",
|
|
5422
|
+
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.",
|
|
4180
5423
|
inputSchema: {
|
|
4181
|
-
by:
|
|
4182
|
-
value:
|
|
4183
|
-
name:
|
|
4184
|
-
scope:
|
|
5424
|
+
by: z16.string().describe("Query strategy: role | text | label | placeholder | testid | alt"),
|
|
5425
|
+
value: z16.string().describe("Query value for the selected strategy (e.g. by=role value=button, or by=testid value=submit-btn)."),
|
|
5426
|
+
name: z16.string().optional().describe("Accessible name filter \u2014 narrows results when `by` is role and the page has many elements of that role."),
|
|
5427
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the search to a subtree."),
|
|
5428
|
+
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."),
|
|
5429
|
+
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.'),
|
|
4185
5430
|
...sessionIdShape6
|
|
4186
5431
|
},
|
|
4187
5432
|
outputSchema: {
|
|
4188
|
-
elements:
|
|
4189
|
-
ref:
|
|
4190
|
-
role:
|
|
4191
|
-
name:
|
|
4192
|
-
value:
|
|
4193
|
-
states:
|
|
4194
|
-
visible:
|
|
4195
|
-
})),
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
5433
|
+
elements: z16.array(z16.object({
|
|
5434
|
+
ref: z16.string(),
|
|
5435
|
+
role: z16.string(),
|
|
5436
|
+
name: z16.string(),
|
|
5437
|
+
value: z16.string().optional(),
|
|
5438
|
+
states: z16.array(z16.string()),
|
|
5439
|
+
visible: z16.boolean()
|
|
5440
|
+
})).optional(),
|
|
5441
|
+
count: z16.number().optional().describe("Match count \u2014 present when count_only is set."),
|
|
5442
|
+
total: z16.number().optional().describe("Total matches before `limit` truncation \u2014 present only when truncated."),
|
|
5443
|
+
truncated: z16.boolean().optional().describe("True when `limit` dropped some matches."),
|
|
5444
|
+
hint: z16.object({
|
|
5445
|
+
route: z16.string(),
|
|
5446
|
+
presentTestids: z16.array(z16.string()),
|
|
5447
|
+
knownEmptyState: z16.boolean()
|
|
5448
|
+
}).optional().describe("Present only on zero matches \u2014 tells you what IS on the page so you can diagnose the miss."),
|
|
5449
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional().describe("Estimated size of this result \u2014 narrow with `name`/`scope`/`limit` if large.")
|
|
4201
5450
|
},
|
|
4202
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5451
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.QUERY, {
|
|
4203
5452
|
by: args["by"],
|
|
4204
5453
|
value: args["value"],
|
|
4205
5454
|
name: args["name"],
|
|
4206
5455
|
scope: args["scope"]
|
|
4207
|
-
})
|
|
5456
|
+
}).then((result) => withSizeCost(paginateQueryResult(result, asNumber(args["limit"]), args["count_only"] === true)))
|
|
4208
5457
|
},
|
|
4209
5458
|
{
|
|
4210
5459
|
name: IrisTool.INSPECT,
|
|
4211
5460
|
description: "Deep info on one element by ref: full a11y props, visibility, box, and (with @syrin/iris-react) component stack + source file.",
|
|
4212
5461
|
inputSchema: {
|
|
4213
|
-
ref:
|
|
5462
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
|
|
4214
5463
|
...sessionIdShape6
|
|
4215
5464
|
},
|
|
4216
5465
|
outputSchema: {
|
|
4217
|
-
ref:
|
|
4218
|
-
role:
|
|
4219
|
-
name:
|
|
4220
|
-
value:
|
|
4221
|
-
states:
|
|
4222
|
-
visible:
|
|
4223
|
-
box:
|
|
4224
|
-
component:
|
|
5466
|
+
ref: z16.string(),
|
|
5467
|
+
role: z16.string(),
|
|
5468
|
+
name: z16.string(),
|
|
5469
|
+
value: z16.string().optional(),
|
|
5470
|
+
states: z16.array(z16.string()),
|
|
5471
|
+
visible: z16.boolean(),
|
|
5472
|
+
box: z16.object({ x: z16.number(), y: z16.number(), width: z16.number(), height: z16.number() }).optional(),
|
|
5473
|
+
component: z16.object({ name: z16.string().optional(), sourceFile: z16.string().optional() }).optional()
|
|
4225
5474
|
},
|
|
4226
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5475
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.INSPECT, {
|
|
4227
5476
|
ref: args["ref"]
|
|
4228
5477
|
})
|
|
4229
5478
|
},
|
|
@@ -4231,30 +5480,30 @@ var TOOLS = [
|
|
|
4231
5480
|
name: IrisTool.ACT,
|
|
4232
5481
|
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.',
|
|
4233
5482
|
inputSchema: {
|
|
4234
|
-
ref:
|
|
4235
|
-
action:
|
|
4236
|
-
args:
|
|
4237
|
-
refuseWhenThrottled:
|
|
5483
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
|
|
5484
|
+
action: z16.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
|
|
5485
|
+
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."),
|
|
5486
|
+
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)."),
|
|
4238
5487
|
...sessionIdShape6
|
|
4239
5488
|
},
|
|
4240
5489
|
outputSchema: {
|
|
4241
|
-
since:
|
|
4242
|
-
dispatched:
|
|
4243
|
-
settled:
|
|
4244
|
-
inputMode:
|
|
4245
|
-
result:
|
|
4246
|
-
session:
|
|
5490
|
+
since: z16.number().describe("Cursor \u2014 pass to iris_observe/iris_wait_for/iris_assert to scope reaction queries to this act."),
|
|
5491
|
+
dispatched: z16.boolean(),
|
|
5492
|
+
settled: z16.boolean().nullable(),
|
|
5493
|
+
inputMode: z16.string(),
|
|
5494
|
+
result: z16.unknown().optional(),
|
|
5495
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4247
5496
|
},
|
|
4248
5497
|
handler: async (deps, args) => {
|
|
4249
|
-
const session = deps.sessions.resolve(
|
|
5498
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4250
5499
|
const paused = pausedShortCircuit(session);
|
|
4251
5500
|
if (paused !== void 0)
|
|
4252
5501
|
return paused;
|
|
4253
5502
|
refuseIfThrottled(session, args["refuseWhenThrottled"]);
|
|
4254
5503
|
const since = session.elapsed();
|
|
4255
5504
|
session.markActCursor(since);
|
|
4256
|
-
const ref =
|
|
4257
|
-
const action =
|
|
5505
|
+
const ref = asString(args["ref"]) ?? "";
|
|
5506
|
+
const action = asString(args["action"]) ?? "";
|
|
4258
5507
|
const real = await tryRealInput(deps, session, ref, action, args);
|
|
4259
5508
|
if (real.result !== void 0) {
|
|
4260
5509
|
if (deps.recordings.active().length > 0) {
|
|
@@ -4280,7 +5529,7 @@ var TOOLS = [
|
|
|
4280
5529
|
if (deps.recordings.active().length > 0) {
|
|
4281
5530
|
deps.recordings.capture(compileActStep(args, result.result));
|
|
4282
5531
|
}
|
|
4283
|
-
const r =
|
|
5532
|
+
const r = asRecord(result.result);
|
|
4284
5533
|
return withControl(session, {
|
|
4285
5534
|
since,
|
|
4286
5535
|
inputMode: InputMode.SYNTHETIC,
|
|
@@ -4299,17 +5548,17 @@ var TOOLS = [
|
|
|
4299
5548
|
name: IrisTool.ACT_SEQUENCE,
|
|
4300
5549
|
description: "Run multiple actions in order (fill -> fill -> submit) in one round-trip. Returns per-step effects[] (see iris_act).",
|
|
4301
5550
|
inputSchema: {
|
|
4302
|
-
steps:
|
|
5551
|
+
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."),
|
|
4303
5552
|
...sessionIdShape6
|
|
4304
5553
|
},
|
|
4305
5554
|
outputSchema: {
|
|
4306
|
-
since:
|
|
4307
|
-
dispatched:
|
|
4308
|
-
result:
|
|
4309
|
-
session:
|
|
5555
|
+
since: z16.number(),
|
|
5556
|
+
dispatched: z16.boolean(),
|
|
5557
|
+
result: z16.unknown().optional(),
|
|
5558
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4310
5559
|
},
|
|
4311
5560
|
handler: async (deps, args) => {
|
|
4312
|
-
const session = deps.sessions.resolve(
|
|
5561
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4313
5562
|
const paused = pausedShortCircuit(session);
|
|
4314
5563
|
if (paused !== void 0)
|
|
4315
5564
|
return paused;
|
|
@@ -4321,7 +5570,7 @@ var TOOLS = [
|
|
|
4321
5570
|
if (deps.recordings.active().length > 0) {
|
|
4322
5571
|
deps.recordings.capture(compileSequenceStep(args, result.result));
|
|
4323
5572
|
}
|
|
4324
|
-
const r =
|
|
5573
|
+
const r = asRecord(result.result);
|
|
4325
5574
|
return withControl(session, {
|
|
4326
5575
|
since,
|
|
4327
5576
|
dispatched: r["count"] !== void 0,
|
|
@@ -4332,34 +5581,34 @@ var TOOLS = [
|
|
|
4332
5581
|
},
|
|
4333
5582
|
{
|
|
4334
5583
|
name: IrisTool.ACT_AND_WAIT,
|
|
4335
|
-
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.",
|
|
5584
|
+
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.",
|
|
4336
5585
|
inputSchema: {
|
|
4337
|
-
ref:
|
|
4338
|
-
action:
|
|
4339
|
-
args:
|
|
4340
|
-
until: PredicateSchema.describe(
|
|
4341
|
-
timeout_ms:
|
|
4342
|
-
refuseWhenThrottled:
|
|
5586
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query."),
|
|
5587
|
+
action: z16.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
|
|
5588
|
+
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."),
|
|
5589
|
+
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" }] }.'),
|
|
5590
|
+
timeout_ms: z16.number().optional().describe("Maximum wait time in milliseconds. 0 = evaluate once without waiting. Default: 4000."),
|
|
5591
|
+
refuseWhenThrottled: z16.boolean().optional().describe("Throw if the tab is throttled. Default: false."),
|
|
4343
5592
|
...sessionIdShape6
|
|
4344
5593
|
},
|
|
4345
5594
|
outputSchema: {
|
|
4346
|
-
effect:
|
|
4347
|
-
verdict:
|
|
4348
|
-
pass:
|
|
4349
|
-
evidence:
|
|
4350
|
-
failureReason:
|
|
5595
|
+
effect: z16.unknown().describe("The iris_act result (dispatched, settled, inputMode, etc.)."),
|
|
5596
|
+
verdict: z16.object({
|
|
5597
|
+
pass: z16.boolean(),
|
|
5598
|
+
evidence: z16.unknown().optional(),
|
|
5599
|
+
failureReason: z16.string().optional()
|
|
4351
5600
|
}),
|
|
4352
|
-
trace:
|
|
4353
|
-
session:
|
|
5601
|
+
trace: z16.unknown().describe("Reaction report (same shape as iris_observe summary)."),
|
|
5602
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4354
5603
|
},
|
|
4355
5604
|
handler: async (deps, args) => {
|
|
4356
|
-
const session = deps.sessions.resolve(
|
|
5605
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4357
5606
|
const paused = pausedShortCircuit(session);
|
|
4358
5607
|
if (paused !== void 0)
|
|
4359
5608
|
return paused;
|
|
4360
5609
|
refuseIfThrottled(session, args["refuseWhenThrottled"]);
|
|
4361
|
-
const until = PredicateSchema.parse(args["until"]);
|
|
4362
|
-
const timeout =
|
|
5610
|
+
const until = args["until"] !== void 0 ? PredicateSchema.parse(args["until"]) : { kind: "settled" };
|
|
5611
|
+
const timeout = asNumber(args["timeout_ms"]) ?? 4e3;
|
|
4363
5612
|
const since = session.elapsed();
|
|
4364
5613
|
session.markActCursor(since);
|
|
4365
5614
|
const actResult = await session.command(IrisCommand.ACT, {
|
|
@@ -4383,40 +5632,41 @@ var TOOLS = [
|
|
|
4383
5632
|
name: IrisTool.OBSERVE,
|
|
4384
5633
|
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.",
|
|
4385
5634
|
inputSchema: {
|
|
4386
|
-
window_ms:
|
|
4387
|
-
since:
|
|
4388
|
-
filters:
|
|
4389
|
-
max_events:
|
|
5635
|
+
window_ms: z16.number().optional().describe("Time window to look back. Default: 2000ms. Ignored when `since` is provided."),
|
|
5636
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act or iris_observe call. Scopes the event window to exactly that span."),
|
|
5637
|
+
filters: z16.array(z16.string()).optional().describe("Event type allowlist: dom | net | route | console | animation | signal. Omit to return all types."),
|
|
5638
|
+
max_events: z16.number().optional().describe("Cap the timeline to the most recent N events. Older events are counted in cost.droppedOldest."),
|
|
4390
5639
|
...sessionIdShape6
|
|
4391
5640
|
},
|
|
4392
5641
|
outputSchema: {
|
|
4393
|
-
events:
|
|
4394
|
-
summary:
|
|
4395
|
-
total:
|
|
4396
|
-
network:
|
|
4397
|
-
domAdded:
|
|
4398
|
-
domRemoved:
|
|
4399
|
-
domChanged:
|
|
4400
|
-
routeChanges:
|
|
4401
|
-
consoleErrors:
|
|
4402
|
-
animations:
|
|
4403
|
-
signals:
|
|
5642
|
+
events: z16.array(z16.unknown()),
|
|
5643
|
+
summary: z16.object({
|
|
5644
|
+
total: z16.number(),
|
|
5645
|
+
network: z16.number(),
|
|
5646
|
+
domAdded: z16.number(),
|
|
5647
|
+
domRemoved: z16.number(),
|
|
5648
|
+
domChanged: z16.number(),
|
|
5649
|
+
routeChanges: z16.number(),
|
|
5650
|
+
consoleErrors: z16.number(),
|
|
5651
|
+
animations: z16.number(),
|
|
5652
|
+
signals: z16.number()
|
|
4404
5653
|
}),
|
|
4405
|
-
cost:
|
|
4406
|
-
events:
|
|
4407
|
-
bytes:
|
|
4408
|
-
droppedOldest:
|
|
5654
|
+
cost: z16.object({
|
|
5655
|
+
events: z16.number(),
|
|
5656
|
+
bytes: z16.number(),
|
|
5657
|
+
droppedOldest: z16.number().optional(),
|
|
5658
|
+
recommendation: z16.string().optional().describe("Present when the timeline is large \u2014 scope your next call (filters/max_events).")
|
|
4409
5659
|
}),
|
|
4410
|
-
session:
|
|
5660
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4411
5661
|
},
|
|
4412
5662
|
handler: (deps, args) => {
|
|
4413
|
-
const session = deps.sessions.resolve(
|
|
4414
|
-
const since =
|
|
4415
|
-
const windowMs =
|
|
5663
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5664
|
+
const since = asNumber(args["since"]);
|
|
5665
|
+
const windowMs = asNumber(args["window_ms"]) ?? 2e3;
|
|
4416
5666
|
const events = since !== void 0 ? session.eventsSince(since) : session.eventsInWindow(windowMs);
|
|
4417
5667
|
const filters = Array.isArray(args["filters"]) ? args["filters"] : void 0;
|
|
4418
5668
|
const filtered = filters === void 0 ? events : events.filter((e) => filters.includes(e.type));
|
|
4419
|
-
const { events: budgeted, droppedOldest } = applyEventBudget(filtered,
|
|
5669
|
+
const { events: budgeted, droppedOldest } = applyEventBudget(filtered, asNumber(args["max_events"]));
|
|
4420
5670
|
const report = buildReactionReport(budgeted, windowMs);
|
|
4421
5671
|
return Promise.resolve(withControl(session, {
|
|
4422
5672
|
...report,
|
|
@@ -4429,99 +5679,113 @@ var TOOLS = [
|
|
|
4429
5679
|
name: IrisTool.WAIT_FOR,
|
|
4430
5680
|
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.",
|
|
4431
5681
|
inputSchema: {
|
|
4432
|
-
predicate: PredicateSchema.describe(
|
|
4433
|
-
timeout_ms:
|
|
4434
|
-
since:
|
|
5682
|
+
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.'),
|
|
5683
|
+
timeout_ms: z16.number().optional().describe("Maximum wait in milliseconds. Default: 4000."),
|
|
5684
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the wait to events after that act."),
|
|
4435
5685
|
...sessionIdShape6
|
|
4436
5686
|
},
|
|
4437
5687
|
outputSchema: {
|
|
4438
|
-
pass:
|
|
4439
|
-
evidence:
|
|
4440
|
-
failureReason:
|
|
4441
|
-
session:
|
|
5688
|
+
pass: z16.boolean(),
|
|
5689
|
+
evidence: z16.unknown().optional(),
|
|
5690
|
+
failureReason: z16.string().optional(),
|
|
5691
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4442
5692
|
},
|
|
4443
5693
|
handler: async (deps, args) => {
|
|
4444
|
-
const session = deps.sessions.resolve(
|
|
5694
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4445
5695
|
const predicate = PredicateSchema.parse(args["predicate"]);
|
|
4446
|
-
const since =
|
|
4447
|
-
const verdict = await waitForPredicate(session, predicate,
|
|
5696
|
+
const since = asNumber(args["since"]) ?? session.lastActCursor() ?? 0;
|
|
5697
|
+
const verdict = await waitForPredicate(session, predicate, asNumber(args["timeout_ms"]) ?? 4e3, since);
|
|
4448
5698
|
return withControl(session, { ...verdict, ...healthEnvelope(session) });
|
|
4449
5699
|
}
|
|
4450
5700
|
},
|
|
4451
5701
|
{
|
|
4452
5702
|
name: IrisTool.ASSERT,
|
|
4453
|
-
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.",
|
|
5703
|
+
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.",
|
|
4454
5704
|
inputSchema: {
|
|
4455
5705
|
predicate: PredicateSchema.describe("Predicate to evaluate: { signal }, { net }, { element } or a combination."),
|
|
4456
|
-
timeout_ms:
|
|
4457
|
-
since:
|
|
5706
|
+
timeout_ms: z16.number().optional().describe("If > 0, wait up to this many milliseconds before failing. Default: 0 (evaluate once)."),
|
|
5707
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the assertion to events after that act."),
|
|
4458
5708
|
...sessionIdShape6
|
|
4459
5709
|
},
|
|
4460
5710
|
outputSchema: {
|
|
4461
|
-
pass:
|
|
4462
|
-
evidence:
|
|
4463
|
-
failureReason:
|
|
4464
|
-
|
|
5711
|
+
pass: z16.boolean(),
|
|
5712
|
+
evidence: z16.unknown().optional(),
|
|
5713
|
+
failureReason: z16.string().optional(),
|
|
5714
|
+
advice: z16.string().optional().describe("Present on a PASSING presence-only assertion \u2014 nudges toward a consequence."),
|
|
5715
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4465
5716
|
},
|
|
4466
5717
|
handler: async (deps, args) => {
|
|
4467
|
-
const session = deps.sessions.resolve(
|
|
5718
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4468
5719
|
const predicate = PredicateSchema.parse(args["predicate"]);
|
|
4469
|
-
const timeout =
|
|
4470
|
-
const since =
|
|
5720
|
+
const timeout = asNumber(args["timeout_ms"]) ?? 0;
|
|
5721
|
+
const since = asNumber(args["since"]) ?? session.lastActCursor() ?? 0;
|
|
4471
5722
|
const verdict = timeout > 0 ? await waitForPredicate(session, predicate, timeout, since) : await evaluatePredicate(session, predicate, since);
|
|
4472
|
-
|
|
5723
|
+
const advice = verdict.pass && isPresenceOnlyAssertion(predicate) ? { advice: PRESENCE_ONLY_ADVICE } : {};
|
|
5724
|
+
return withControl(session, { ...verdict, ...advice, ...healthEnvelope(session) });
|
|
4473
5725
|
}
|
|
4474
5726
|
},
|
|
4475
5727
|
{
|
|
4476
5728
|
name: IrisTool.NETWORK,
|
|
4477
5729
|
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.',
|
|
4478
5730
|
inputSchema: {
|
|
4479
|
-
since:
|
|
4480
|
-
method:
|
|
4481
|
-
urlContains:
|
|
4482
|
-
status:
|
|
5731
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to requests fired after that act."),
|
|
5732
|
+
method: z16.string().optional().describe("HTTP method filter: GET | POST | PUT | DELETE | PATCH etc."),
|
|
5733
|
+
urlContains: z16.string().optional().describe("Substring that the request URL must contain."),
|
|
5734
|
+
status: z16.number().optional().describe("HTTP status code filter (e.g. 200, 404, 500)."),
|
|
5735
|
+
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."),
|
|
4483
5736
|
...sessionIdShape6
|
|
4484
5737
|
},
|
|
4485
5738
|
outputSchema: {
|
|
4486
|
-
calls:
|
|
4487
|
-
|
|
5739
|
+
calls: z16.array(z16.unknown()),
|
|
5740
|
+
total: z16.number().optional().describe("Total matches before `limit` \u2014 present only when capped."),
|
|
5741
|
+
droppedOldest: z16.number().optional().describe("How many older matches `limit` dropped."),
|
|
5742
|
+
hint: z16.object({ totalInWindow: z16.number(), present: z16.array(z16.string()) }).optional(),
|
|
5743
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional()
|
|
4488
5744
|
},
|
|
4489
5745
|
handler: (deps, args) => {
|
|
4490
|
-
const session = deps.sessions.resolve(
|
|
4491
|
-
const since =
|
|
4492
|
-
const method =
|
|
4493
|
-
const urlContains =
|
|
4494
|
-
const status =
|
|
5746
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5747
|
+
const since = asNumber(args["since"]) ?? 0;
|
|
5748
|
+
const method = asString(args["method"]);
|
|
5749
|
+
const urlContains = asString(args["urlContains"]);
|
|
5750
|
+
const status = asNumber(args["status"]);
|
|
5751
|
+
const limit = asNumber(args["limit"]);
|
|
4495
5752
|
const allNet = session.eventsSince(since).filter((e) => e.type === EventType.NET_REQUEST);
|
|
4496
|
-
const
|
|
4497
|
-
if (
|
|
4498
|
-
return Promise.resolve({ calls, hint: netEmptyHint(allNet) });
|
|
5753
|
+
const matched = allNet.filter((e) => matchNet(e, method, urlContains, status));
|
|
5754
|
+
if (matched.length === 0 && allNet.length > 0) {
|
|
5755
|
+
return Promise.resolve(withSizeCost({ calls: matched, hint: netEmptyHint(allNet) }));
|
|
4499
5756
|
}
|
|
4500
|
-
|
|
5757
|
+
const { events: calls, droppedOldest } = applyEventBudget(matched, limit);
|
|
5758
|
+
return Promise.resolve(withSizeCost(droppedOldest > 0 ? { calls, total: matched.length, droppedOldest } : { calls }));
|
|
4501
5759
|
}
|
|
4502
5760
|
},
|
|
4503
5761
|
{
|
|
4504
5762
|
name: IrisTool.CONSOLE,
|
|
4505
5763
|
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.',
|
|
4506
5764
|
inputSchema: {
|
|
4507
|
-
level:
|
|
4508
|
-
since:
|
|
5765
|
+
level: z16.string().optional().describe("Log level filter: error | warn | info | log. Omit to return all levels."),
|
|
5766
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to log entries after that act."),
|
|
5767
|
+
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."),
|
|
4509
5768
|
...sessionIdShape6
|
|
4510
5769
|
},
|
|
4511
5770
|
outputSchema: {
|
|
4512
|
-
logs:
|
|
4513
|
-
|
|
5771
|
+
logs: z16.array(z16.unknown()),
|
|
5772
|
+
total: z16.number().optional().describe("Total matches before `limit` \u2014 present only when capped."),
|
|
5773
|
+
droppedOldest: z16.number().optional().describe("How many older matches `limit` dropped."),
|
|
5774
|
+
hint: z16.object({ totalInWindow: z16.number(), byLevel: z16.record(z16.number()) }).optional(),
|
|
5775
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional()
|
|
4514
5776
|
},
|
|
4515
5777
|
handler: (deps, args) => {
|
|
4516
|
-
const session = deps.sessions.resolve(
|
|
4517
|
-
const since =
|
|
4518
|
-
const level =
|
|
5778
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5779
|
+
const since = asNumber(args["since"]) ?? 0;
|
|
5780
|
+
const level = asString(args["level"]);
|
|
5781
|
+
const limit = asNumber(args["limit"]);
|
|
4519
5782
|
const allConsole = session.eventsSince(since).filter(isConsoleEvent);
|
|
4520
|
-
const
|
|
4521
|
-
if (
|
|
4522
|
-
return Promise.resolve({ logs, hint: consoleEmptyHint(allConsole) });
|
|
5783
|
+
const matched = allConsole.filter((e) => matchConsole(e, level));
|
|
5784
|
+
if (matched.length === 0 && allConsole.length > 0) {
|
|
5785
|
+
return Promise.resolve(withSizeCost({ logs: matched, hint: consoleEmptyHint(allConsole) }));
|
|
4523
5786
|
}
|
|
4524
|
-
|
|
5787
|
+
const { events: logs, droppedOldest } = applyEventBudget(matched, limit);
|
|
5788
|
+
return Promise.resolve(withSizeCost(droppedOldest > 0 ? { logs, total: matched.length, droppedOldest } : { logs }));
|
|
4525
5789
|
}
|
|
4526
5790
|
},
|
|
4527
5791
|
{
|
|
@@ -4529,24 +5793,24 @@ var TOOLS = [
|
|
|
4529
5793
|
description: "Currently running + recently completed animations with targets/timing.",
|
|
4530
5794
|
inputSchema: { ...sessionIdShape6 },
|
|
4531
5795
|
outputSchema: {
|
|
4532
|
-
animations:
|
|
5796
|
+
animations: z16.array(z16.unknown())
|
|
4533
5797
|
},
|
|
4534
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5798
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.ANIMATIONS, {})
|
|
4535
5799
|
},
|
|
4536
5800
|
{
|
|
4537
5801
|
name: IrisTool.BASELINE_SAVE,
|
|
4538
5802
|
description: "Snapshot the current semantic state under a name, to diff against later (regression detection).",
|
|
4539
5803
|
inputSchema: {
|
|
4540
|
-
name:
|
|
5804
|
+
name: z16.string().describe('Label for this baseline snapshot (e.g. "dashboard-initial"). Use the same name in iris_diff to compare.'),
|
|
4541
5805
|
...sessionIdShape6
|
|
4542
5806
|
},
|
|
4543
5807
|
outputSchema: {
|
|
4544
|
-
baseline:
|
|
4545
|
-
lineCount:
|
|
5808
|
+
baseline: z16.string().describe("Saved baseline name \u2014 pass to iris_diff to compare."),
|
|
5809
|
+
lineCount: z16.number()
|
|
4546
5810
|
},
|
|
4547
5811
|
handler: async (deps, args) => {
|
|
4548
|
-
const name =
|
|
4549
|
-
const { lines, route } = await snapshotTree(deps,
|
|
5812
|
+
const name = asString(args["name"]) ?? "default";
|
|
5813
|
+
const { lines, route } = await snapshotTree(deps, asString(args["sessionId"]));
|
|
4550
5814
|
deps.baselines.save({ name, lines, route });
|
|
4551
5815
|
return { baseline: name, lineCount: lines.length };
|
|
4552
5816
|
}
|
|
@@ -4556,7 +5820,7 @@ var TOOLS = [
|
|
|
4556
5820
|
description: "List saved baseline names.",
|
|
4557
5821
|
inputSchema: {},
|
|
4558
5822
|
outputSchema: {
|
|
4559
|
-
baselines:
|
|
5823
|
+
baselines: z16.array(z16.string())
|
|
4560
5824
|
},
|
|
4561
5825
|
handler: (deps) => Promise.resolve({ baselines: deps.baselines.list() })
|
|
4562
5826
|
},
|
|
@@ -4564,23 +5828,23 @@ var TOOLS = [
|
|
|
4564
5828
|
name: IrisTool.DIFF,
|
|
4565
5829
|
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?".',
|
|
4566
5830
|
inputSchema: {
|
|
4567
|
-
baseline:
|
|
5831
|
+
baseline: z16.string().describe("Baseline name to compare against. Call iris_baseline_list to get available names; names are created by iris_baseline_save."),
|
|
4568
5832
|
...sessionIdShape6
|
|
4569
5833
|
},
|
|
4570
5834
|
outputSchema: {
|
|
4571
|
-
baseline:
|
|
4572
|
-
removed:
|
|
4573
|
-
added:
|
|
4574
|
-
consoleErrors:
|
|
4575
|
-
routeChanged:
|
|
5835
|
+
baseline: z16.string(),
|
|
5836
|
+
removed: z16.array(z16.string()),
|
|
5837
|
+
added: z16.array(z16.string()),
|
|
5838
|
+
consoleErrors: z16.number(),
|
|
5839
|
+
routeChanged: z16.boolean()
|
|
4576
5840
|
},
|
|
4577
5841
|
handler: async (deps, args) => {
|
|
4578
|
-
const name =
|
|
5842
|
+
const name = asString(args["baseline"]) ?? "default";
|
|
4579
5843
|
const base = deps.baselines.get(name);
|
|
4580
5844
|
if (base === void 0)
|
|
4581
5845
|
throw new Error(`no baseline named '${name}'`);
|
|
4582
|
-
const session = deps.sessions.resolve(
|
|
4583
|
-
const { lines, route } = await snapshotTree(deps,
|
|
5846
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5847
|
+
const { lines, route } = await snapshotTree(deps, asString(args["sessionId"]));
|
|
4584
5848
|
const { removed, added } = diffLines(base.lines, lines);
|
|
4585
5849
|
const consoleErrors = session.eventsSince(0).filter((e) => e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT).length;
|
|
4586
5850
|
return { baseline: name, removed, added, consoleErrors, routeChanged: base.route !== route };
|
|
@@ -4590,16 +5854,16 @@ var TOOLS = [
|
|
|
4590
5854
|
name: IrisTool.RECORD_START,
|
|
4591
5855
|
description: "Start recording the event timeline under a name (for replay / a flow report).",
|
|
4592
5856
|
inputSchema: {
|
|
4593
|
-
recordingName:
|
|
5857
|
+
recordingName: z16.string().describe("Identifier for this recording. Pass the same name to iris_record_stop and iris_replay."),
|
|
4594
5858
|
...sessionIdShape6
|
|
4595
5859
|
},
|
|
4596
5860
|
outputSchema: {
|
|
4597
|
-
recordingName:
|
|
4598
|
-
since:
|
|
5861
|
+
recordingName: z16.string(),
|
|
5862
|
+
since: z16.number()
|
|
4599
5863
|
},
|
|
4600
5864
|
handler: (deps, args) => {
|
|
4601
|
-
const session = deps.sessions.resolve(
|
|
4602
|
-
const name =
|
|
5865
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5866
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
4603
5867
|
const cursor = session.elapsed();
|
|
4604
5868
|
deps.recordings.start(name, cursor);
|
|
4605
5869
|
return Promise.resolve({ recordingName: name, since: cursor });
|
|
@@ -4609,17 +5873,17 @@ var TOOLS = [
|
|
|
4609
5873
|
name: IrisTool.RECORD_STOP,
|
|
4610
5874
|
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.",
|
|
4611
5875
|
inputSchema: {
|
|
4612
|
-
recordingName:
|
|
5876
|
+
recordingName: z16.string().describe("Identifier of an active recording started with iris_record_start."),
|
|
4613
5877
|
...sessionIdShape6
|
|
4614
5878
|
},
|
|
4615
5879
|
outputSchema: {
|
|
4616
|
-
recordingName:
|
|
4617
|
-
program:
|
|
4618
|
-
warning:
|
|
5880
|
+
recordingName: z16.string(),
|
|
5881
|
+
program: z16.unknown(),
|
|
5882
|
+
warning: z16.string().optional()
|
|
4619
5883
|
},
|
|
4620
5884
|
handler: (deps, args) => {
|
|
4621
|
-
const session = deps.sessions.resolve(
|
|
4622
|
-
const name =
|
|
5885
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5886
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
4623
5887
|
const rec = deps.recordings.stop(name);
|
|
4624
5888
|
if (rec === void 0)
|
|
4625
5889
|
throw new Error(`no active recording named '${name}'`);
|
|
@@ -4645,29 +5909,30 @@ var TOOLS = [
|
|
|
4645
5909
|
},
|
|
4646
5910
|
{
|
|
4647
5911
|
name: IrisTool.REPLAY,
|
|
4648
|
-
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?}] }.",
|
|
5912
|
+
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?}] }.",
|
|
4649
5913
|
inputSchema: {
|
|
4650
|
-
recordingName:
|
|
5914
|
+
recordingName: z16.string().describe("Name of a compiled recording (from iris_record_stop) to re-execute."),
|
|
5915
|
+
confirmDangerous: z16.boolean().optional().describe("Set true to allow destructive controls during this replay only."),
|
|
4651
5916
|
...sessionIdShape6
|
|
4652
5917
|
},
|
|
4653
5918
|
outputSchema: {
|
|
4654
|
-
recordingName:
|
|
4655
|
-
ok:
|
|
4656
|
-
steps:
|
|
4657
|
-
tool:
|
|
4658
|
-
ok:
|
|
4659
|
-
error:
|
|
4660
|
-
note:
|
|
5919
|
+
recordingName: z16.string(),
|
|
5920
|
+
ok: z16.boolean(),
|
|
5921
|
+
steps: z16.array(z16.object({
|
|
5922
|
+
tool: z16.string(),
|
|
5923
|
+
ok: z16.boolean(),
|
|
5924
|
+
error: z16.string().optional(),
|
|
5925
|
+
note: z16.string().optional()
|
|
4661
5926
|
}))
|
|
4662
5927
|
},
|
|
4663
5928
|
handler: async (deps, args) => {
|
|
4664
|
-
const name =
|
|
5929
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
4665
5930
|
const program = deps.recordings.getCompiled(name);
|
|
4666
5931
|
if (program === void 0)
|
|
4667
5932
|
throw new Error(`no compiled recording named '${name}'`);
|
|
4668
|
-
const session = deps.sessions.resolve(
|
|
5933
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4669
5934
|
const since = session.elapsed();
|
|
4670
|
-
const steps = await replayProgram(session, program);
|
|
5935
|
+
const steps = await replayProgram(session, program, args["confirmDangerous"] === true);
|
|
4671
5936
|
return { recordingName: name, since, steps, ok: steps.every((s) => s.ok) };
|
|
4672
5937
|
}
|
|
4673
5938
|
},
|
|
@@ -4675,32 +5940,33 @@ var TOOLS = [
|
|
|
4675
5940
|
name: IrisTool.NARRATE,
|
|
4676
5941
|
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.",
|
|
4677
5942
|
inputSchema: {
|
|
4678
|
-
text:
|
|
4679
|
-
level:
|
|
5943
|
+
text: z16.string().describe("Short sentence describing your next action, shown on the presenter HUD for the developer watching."),
|
|
5944
|
+
level: z16.string().optional().describe("Display severity: info | warn | error. Default: info."),
|
|
4680
5945
|
...sessionIdShape6
|
|
4681
5946
|
},
|
|
4682
|
-
outputSchema: {
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
5947
|
+
outputSchema: { ok: z16.boolean() },
|
|
5948
|
+
handler: async (deps, args) => {
|
|
5949
|
+
const result = await commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.NARRATE, {
|
|
5950
|
+
text: args["text"],
|
|
5951
|
+
level: args["level"]
|
|
5952
|
+
});
|
|
5953
|
+
return { ok: true, ...result };
|
|
5954
|
+
}
|
|
4689
5955
|
},
|
|
4690
5956
|
{
|
|
4691
5957
|
name: IrisTool.CLOCK,
|
|
4692
5958
|
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.",
|
|
4693
5959
|
inputSchema: {
|
|
4694
|
-
freeze:
|
|
4695
|
-
advanceMs:
|
|
4696
|
-
reset:
|
|
5960
|
+
freeze: z16.boolean().optional().describe("Freeze the fake clock. Time stops advancing until advanceMs or reset."),
|
|
5961
|
+
advanceMs: z16.number().optional().describe("Fast-forward time by this many milliseconds \u2014 triggers debounces, toasts, auto-dismiss timers."),
|
|
5962
|
+
reset: z16.boolean().optional().describe("Restore the real clock."),
|
|
4697
5963
|
...sessionIdShape6
|
|
4698
5964
|
},
|
|
4699
5965
|
outputSchema: {
|
|
4700
|
-
ok:
|
|
4701
|
-
elapsed:
|
|
5966
|
+
ok: z16.boolean().optional(),
|
|
5967
|
+
elapsed: z16.number().optional()
|
|
4702
5968
|
},
|
|
4703
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5969
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.CLOCK, {
|
|
4704
5970
|
freeze: args["freeze"],
|
|
4705
5971
|
advanceMs: args["advanceMs"],
|
|
4706
5972
|
reset: args["reset"]
|
|
@@ -4710,27 +5976,27 @@ var TOOLS = [
|
|
|
4710
5976
|
name: IrisTool.STATE,
|
|
4711
5977
|
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? }.",
|
|
4712
5978
|
inputSchema: {
|
|
4713
|
-
ref:
|
|
4714
|
-
store:
|
|
4715
|
-
path:
|
|
4716
|
-
depth:
|
|
5979
|
+
ref: z16.string().optional().describe("Element ref \u2014 attempts a best-effort read of the nearest React component's hook state."),
|
|
5980
|
+
store: z16.string().optional().describe("Registered store name (e.g. 'workspace'). Omit to read all stores."),
|
|
5981
|
+
path: z16.string().optional().describe("Dot-path into the store (e.g. 'captionCache.v3'). Numeric array indices are supported."),
|
|
5982
|
+
depth: z16.number().optional().describe("Collapse anything deeper than N levels to a size marker \u2014 avoids huge outputs for large stores."),
|
|
4717
5983
|
...sessionIdShape6
|
|
4718
5984
|
},
|
|
4719
5985
|
outputSchema: {
|
|
4720
|
-
stores:
|
|
4721
|
-
storeNames:
|
|
4722
|
-
found:
|
|
4723
|
-
value:
|
|
4724
|
-
component:
|
|
5986
|
+
stores: z16.record(z16.unknown()).optional(),
|
|
5987
|
+
storeNames: z16.array(z16.string()).optional(),
|
|
5988
|
+
found: z16.boolean().optional(),
|
|
5989
|
+
value: z16.unknown().optional(),
|
|
5990
|
+
component: z16.object({ ok: z16.boolean(), reason: z16.string().optional(), state: z16.unknown().optional() }).optional()
|
|
4725
5991
|
},
|
|
4726
5992
|
handler: async (deps, args) => {
|
|
4727
|
-
const store =
|
|
4728
|
-
const result = await commandOrThrow3(deps,
|
|
5993
|
+
const store = asString(args["store"]);
|
|
5994
|
+
const result = await commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.STATE_READ, {
|
|
4729
5995
|
ref: args["ref"],
|
|
4730
5996
|
store
|
|
4731
5997
|
});
|
|
4732
|
-
const path =
|
|
4733
|
-
const depth =
|
|
5998
|
+
const path = asString(args["path"]);
|
|
5999
|
+
const depth = asNumber(args["depth"]);
|
|
4734
6000
|
if (path === void 0 && depth === void 0)
|
|
4735
6001
|
return result;
|
|
4736
6002
|
const root = result;
|
|
@@ -4750,16 +6016,16 @@ var TOOLS = [
|
|
|
4750
6016
|
name: IrisTool.EXPLORE,
|
|
4751
6017
|
description: "Autonomous-exploration helper: list interactive elements (with refs) + current console-error count, so the agent can drive the app and report anomalies.",
|
|
4752
6018
|
inputSchema: {
|
|
4753
|
-
scope:
|
|
6019
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the interactive element list to a subtree."),
|
|
4754
6020
|
...sessionIdShape6
|
|
4755
6021
|
},
|
|
4756
6022
|
outputSchema: {
|
|
4757
|
-
interactive:
|
|
4758
|
-
consoleErrors:
|
|
4759
|
-
hint:
|
|
6023
|
+
interactive: z16.array(z16.unknown()),
|
|
6024
|
+
consoleErrors: z16.number(),
|
|
6025
|
+
hint: z16.string()
|
|
4760
6026
|
},
|
|
4761
6027
|
handler: async (deps, args) => {
|
|
4762
|
-
const session = deps.sessions.resolve(
|
|
6028
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4763
6029
|
const result = await session.command(IrisCommand.SNAPSHOT, {
|
|
4764
6030
|
mode: SnapshotMode.INTERACTIVE,
|
|
4765
6031
|
scope: args["scope"]
|
|
@@ -4777,6 +6043,7 @@ var TOOLS = [
|
|
|
4777
6043
|
},
|
|
4778
6044
|
// iris_capabilities (live | fromDisk) + iris_contract_save. See contract-tools.ts.
|
|
4779
6045
|
...CONTRACT_TOOLS,
|
|
6046
|
+
...DOMAIN_TOOLS,
|
|
4780
6047
|
// iris_flow_save / iris_flow_list / iris_flow_load. See flow-tools.ts.
|
|
4781
6048
|
...FLOW_TOOLS,
|
|
4782
6049
|
// iris_project (read history + diff-vs-last) / iris_run_record. See project-tools.ts.
|
|
@@ -4794,7 +6061,9 @@ var TOOLS = [
|
|
|
4794
6061
|
// Live-control: iris_end_session / iris_resume / iris_messages. See live-control-tools.ts.
|
|
4795
6062
|
...LIVE_CONTROL_TOOLS,
|
|
4796
6063
|
// iris_navigate / iris_refresh — browser navigation tools. See browser-tools.ts.
|
|
4797
|
-
...BROWSER_TOOLS
|
|
6064
|
+
...BROWSER_TOOLS,
|
|
6065
|
+
// iris_version_info / iris_apply_update / iris_rollback — update lifecycle tools.
|
|
6066
|
+
...UPDATE_TOOLS
|
|
4798
6067
|
];
|
|
4799
6068
|
|
|
4800
6069
|
// ../server/dist/tools/profiles.js
|
|
@@ -4928,7 +6197,7 @@ async function runTool(tool, deps, args) {
|
|
|
4928
6197
|
return result;
|
|
4929
6198
|
if (!isPlainObject(result) || "session" in result)
|
|
4930
6199
|
return result;
|
|
4931
|
-
const session = deps.sessions.resolve(
|
|
6200
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4932
6201
|
const envelope = { ...healthEnvelope(session) };
|
|
4933
6202
|
const lease = session.takeSessionLease();
|
|
4934
6203
|
if (lease !== void 0)
|
|
@@ -4940,7 +6209,7 @@ async function runTool(tool, deps, args) {
|
|
|
4940
6209
|
}
|
|
4941
6210
|
|
|
4942
6211
|
// ../server/dist/mcp.js
|
|
4943
|
-
var SERVER_INFO = { name: "iris", version:
|
|
6212
|
+
var SERVER_INFO = { name: "iris", version: SERVER_VERSION };
|
|
4944
6213
|
var ENCODING_ENV = "IRIS_ENCODING";
|
|
4945
6214
|
var TOON_VALUE = "toon";
|
|
4946
6215
|
function encodeResult(result, useToon) {
|
|
@@ -5054,10 +6323,61 @@ function createToolInvoker(deps) {
|
|
|
5054
6323
|
};
|
|
5055
6324
|
}
|
|
5056
6325
|
|
|
6326
|
+
// ../server/dist/daemon.js
|
|
6327
|
+
import { join as join4 } from "path";
|
|
6328
|
+
import { homedir as homedir2 } from "os";
|
|
6329
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, unlinkSync, openSync } from "fs";
|
|
6330
|
+
import { spawn } from "child_process";
|
|
6331
|
+
var IRIS_HOME2 = join4(homedir2(), ".iris");
|
|
6332
|
+
function pidPath(port) {
|
|
6333
|
+
return join4(IRIS_HOME2, `daemon-${port}.pid`);
|
|
6334
|
+
}
|
|
6335
|
+
function logPath(port) {
|
|
6336
|
+
return join4(IRIS_HOME2, `daemon-${port}.log`);
|
|
6337
|
+
}
|
|
6338
|
+
function readPid(port) {
|
|
6339
|
+
const path = pidPath(port);
|
|
6340
|
+
if (!existsSync3(path))
|
|
6341
|
+
return null;
|
|
6342
|
+
const n = parseInt(readFileSync2(path, "utf8").trim(), 10);
|
|
6343
|
+
return isNaN(n) ? null : n;
|
|
6344
|
+
}
|
|
6345
|
+
function isAlive(pid) {
|
|
6346
|
+
try {
|
|
6347
|
+
process.kill(pid, 0);
|
|
6348
|
+
return true;
|
|
6349
|
+
} catch {
|
|
6350
|
+
return false;
|
|
6351
|
+
}
|
|
6352
|
+
}
|
|
6353
|
+
function writePid(port) {
|
|
6354
|
+
mkdirSync2(IRIS_HOME2, { recursive: true });
|
|
6355
|
+
writeFileSync2(pidPath(port), String(process.pid), "utf8");
|
|
6356
|
+
}
|
|
6357
|
+
function removePid(port) {
|
|
6358
|
+
const path = pidPath(port);
|
|
6359
|
+
if (existsSync3(path))
|
|
6360
|
+
unlinkSync(path);
|
|
6361
|
+
}
|
|
6362
|
+
function isRunning(port) {
|
|
6363
|
+
const pid = readPid(port);
|
|
6364
|
+
return pid !== null && isAlive(pid);
|
|
6365
|
+
}
|
|
6366
|
+
|
|
5057
6367
|
// ../server/dist/index.js
|
|
5058
6368
|
async function start(options = {}) {
|
|
5059
6369
|
const port = options.port ?? IRIS_DEFAULT_PORT;
|
|
5060
|
-
const
|
|
6370
|
+
const envToken = process.env["IRIS_TOKEN"];
|
|
6371
|
+
const envOrigins = process.env["IRIS_ALLOWED_ORIGINS"];
|
|
6372
|
+
const host = options.host ?? process.env["IRIS_HOST"];
|
|
6373
|
+
const token = options.token ?? (envToken !== void 0 && envToken.length > 0 ? envToken : void 0);
|
|
6374
|
+
const allowedOrigins = options.allowedOrigins ?? envOrigins?.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
|
|
6375
|
+
const bridge = new Bridge({
|
|
6376
|
+
port,
|
|
6377
|
+
...host === void 0 ? {} : { host },
|
|
6378
|
+
...token === void 0 ? {} : { token },
|
|
6379
|
+
...allowedOrigins === void 0 ? {} : { allowedOrigins }
|
|
6380
|
+
});
|
|
5061
6381
|
const reaper = new SessionReaper(bridge.sessions);
|
|
5062
6382
|
reaper.start();
|
|
5063
6383
|
const baselines = new BaselineStore();
|
|
@@ -5086,11 +6406,11 @@ async function start(options = {}) {
|
|
|
5086
6406
|
}
|
|
5087
6407
|
}
|
|
5088
6408
|
if (options.mcp !== false) {
|
|
5089
|
-
const
|
|
5090
|
-
const irisRoot = options.irisRoot ??
|
|
6409
|
+
const fs2 = createNodeFileSystem();
|
|
6410
|
+
const irisRoot = options.irisRoot ?? join5(process.cwd(), IrisDir.ROOT);
|
|
5091
6411
|
const now = options.now ?? (() => Date.now());
|
|
5092
|
-
const flows = new FlowStore(
|
|
5093
|
-
const project = new ProjectStore(
|
|
6412
|
+
const flows = new FlowStore(fs2, irisRoot, { now });
|
|
6413
|
+
const project = new ProjectStore(fs2, irisRoot, { now });
|
|
5094
6414
|
const annotations = new AnnotationStore();
|
|
5095
6415
|
const deps = {
|
|
5096
6416
|
sessions: bridge.sessions,
|
|
@@ -5099,7 +6419,7 @@ async function start(options = {}) {
|
|
|
5099
6419
|
annotations,
|
|
5100
6420
|
flows,
|
|
5101
6421
|
project,
|
|
5102
|
-
fs,
|
|
6422
|
+
fs: fs2,
|
|
5103
6423
|
irisRoot,
|
|
5104
6424
|
now
|
|
5105
6425
|
};
|
|
@@ -5121,6 +6441,71 @@ async function start(options = {}) {
|
|
|
5121
6441
|
}
|
|
5122
6442
|
};
|
|
5123
6443
|
}
|
|
6444
|
+
async function startDaemon(options = {}) {
|
|
6445
|
+
const port = options.port ?? IRIS_DEFAULT_PORT;
|
|
6446
|
+
const shared = createSharedServer();
|
|
6447
|
+
const bridge = new Bridge({ port, server: shared.httpServer });
|
|
6448
|
+
const reaper = new SessionReaper(bridge.sessions);
|
|
6449
|
+
reaper.start();
|
|
6450
|
+
let owned;
|
|
6451
|
+
let realInput;
|
|
6452
|
+
const driveUrl = options.driveUrl;
|
|
6453
|
+
if (driveUrl !== void 0 && driveUrl.length > 0) {
|
|
6454
|
+
const headless = options.headless ?? true;
|
|
6455
|
+
const factory = options.realInputFactory ?? ((opts) => new LaunchedRealInputProvider({ driveUrl: opts.driveUrl, headless: opts.headless }));
|
|
6456
|
+
const launched = factory({ driveUrl, headless });
|
|
6457
|
+
try {
|
|
6458
|
+
await launched.navigate();
|
|
6459
|
+
} catch (error) {
|
|
6460
|
+
await shared.close();
|
|
6461
|
+
throw error;
|
|
6462
|
+
}
|
|
6463
|
+
owned = launched;
|
|
6464
|
+
realInput = launched;
|
|
6465
|
+
} else {
|
|
6466
|
+
const cdpUrl = options.cdpUrl ?? process.env["IRIS_CDP_URL"];
|
|
6467
|
+
if (cdpUrl !== void 0 && cdpUrl.length > 0) {
|
|
6468
|
+
const cdp = new CdpRealInputProvider({ cdpUrl });
|
|
6469
|
+
owned = cdp;
|
|
6470
|
+
realInput = cdp;
|
|
6471
|
+
}
|
|
6472
|
+
}
|
|
6473
|
+
const fs2 = createNodeFileSystem();
|
|
6474
|
+
const irisRoot = options.irisRoot ?? join5(process.cwd(), IrisDir.ROOT);
|
|
6475
|
+
const now = options.now ?? (() => Date.now());
|
|
6476
|
+
const flows = new FlowStore(fs2, irisRoot, { now });
|
|
6477
|
+
const project = new ProjectStore(fs2, irisRoot, { now });
|
|
6478
|
+
const annotations = new AnnotationStore();
|
|
6479
|
+
const deps = {
|
|
6480
|
+
sessions: bridge.sessions,
|
|
6481
|
+
baselines: new BaselineStore(),
|
|
6482
|
+
recordings: new RecordingStore(),
|
|
6483
|
+
annotations,
|
|
6484
|
+
flows,
|
|
6485
|
+
project,
|
|
6486
|
+
fs: fs2,
|
|
6487
|
+
irisRoot,
|
|
6488
|
+
now
|
|
6489
|
+
};
|
|
6490
|
+
const profile = resolveToolProfile(options.toolProfile);
|
|
6491
|
+
const effectiveDeps = realInput !== void 0 ? { ...deps, realInput } : deps;
|
|
6492
|
+
shared.attachMcp(() => createMcpServer(effectiveDeps, profile));
|
|
6493
|
+
await new Promise((resolve) => {
|
|
6494
|
+
shared.httpServer.once("listening", resolve);
|
|
6495
|
+
shared.httpServer.listen(port, "127.0.0.1");
|
|
6496
|
+
});
|
|
6497
|
+
log("mcp_daemon_started", { port });
|
|
6498
|
+
return {
|
|
6499
|
+
bridge,
|
|
6500
|
+
...realInput !== void 0 ? { realInput } : {},
|
|
6501
|
+
close: async () => {
|
|
6502
|
+
reaper.stop();
|
|
6503
|
+
await owned?.dispose();
|
|
6504
|
+
await bridge.close();
|
|
6505
|
+
await shared.close();
|
|
6506
|
+
}
|
|
6507
|
+
};
|
|
6508
|
+
}
|
|
5124
6509
|
export {
|
|
5125
6510
|
AnnotationStore,
|
|
5126
6511
|
BaselineStore,
|
|
@@ -5128,9 +6513,12 @@ export {
|
|
|
5128
6513
|
CORE_TOOL_NAMES,
|
|
5129
6514
|
CdpRealInputProvider,
|
|
5130
6515
|
DriveError,
|
|
6516
|
+
FlowAssertionGrade,
|
|
5131
6517
|
FlowStore,
|
|
5132
6518
|
IrisTool,
|
|
5133
6519
|
LaunchedRealInputProvider,
|
|
6520
|
+
MCP_MESSAGE_PATH,
|
|
6521
|
+
MCP_SSE_PATH,
|
|
5134
6522
|
PredicateSchema,
|
|
5135
6523
|
ProjectStore,
|
|
5136
6524
|
RecordingStore,
|
|
@@ -5144,31 +6532,44 @@ export {
|
|
|
5144
6532
|
TOOL_PROFILE_ENV,
|
|
5145
6533
|
UNKNOWN_TOOL_ERROR,
|
|
5146
6534
|
VisualStore,
|
|
6535
|
+
assertSuccess,
|
|
5147
6536
|
baselinePath,
|
|
5148
6537
|
boxCenter,
|
|
6538
|
+
buildDomainModel,
|
|
5149
6539
|
buildReactionReport,
|
|
5150
6540
|
buildSessionRecommendation,
|
|
6541
|
+
classifyFlowAssertions,
|
|
5151
6542
|
crawl,
|
|
5152
6543
|
createNodeFileSystem,
|
|
5153
6544
|
createToolInvoker,
|
|
5154
6545
|
diffLines,
|
|
5155
6546
|
diffPng,
|
|
6547
|
+
dynamicTestids,
|
|
5156
6548
|
ensureIrisDir,
|
|
5157
6549
|
evaluatePredicate,
|
|
5158
6550
|
filterTools,
|
|
5159
6551
|
flowPath,
|
|
5160
6552
|
irisDirPaths,
|
|
6553
|
+
isAlive,
|
|
5161
6554
|
isPointerAction,
|
|
6555
|
+
isRunning,
|
|
6556
|
+
logPath,
|
|
5162
6557
|
nearestTestid,
|
|
5163
6558
|
normalizeLines,
|
|
5164
6559
|
performGesture,
|
|
5165
6560
|
readContract,
|
|
6561
|
+
readPid,
|
|
5166
6562
|
recordedStepToFlowStep,
|
|
6563
|
+
removePid,
|
|
5167
6564
|
replayFlow,
|
|
5168
6565
|
resolveToolProfile,
|
|
5169
6566
|
runTool,
|
|
5170
6567
|
scrollToFind,
|
|
5171
6568
|
start,
|
|
6569
|
+
startDaemon,
|
|
6570
|
+
successLabel,
|
|
6571
|
+
successToPredicate,
|
|
5172
6572
|
waitForPredicate,
|
|
5173
|
-
writeContract
|
|
6573
|
+
writeContract,
|
|
6574
|
+
writePid
|
|
5174
6575
|
};
|