@syrin/iris 0.4.0 → 0.5.0

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/test.js CHANGED
@@ -191,12 +191,12 @@ ${body}
191
191
  </${JUnit.SUITE}>
192
192
  `;
193
193
  }
194
- async function writeJUnit(fs, path, results, opts) {
195
- await fs.writeFile(path, toJUnitXml(results, opts));
194
+ async function writeJUnit(fs2, path, results, opts) {
195
+ await fs2.writeFile(path, toJUnitXml(results, opts));
196
196
  }
197
197
 
198
198
  // ../test/dist/boot.js
199
- import { join as join3 } from "path";
199
+ import { join as join6 } from "path";
200
200
 
201
201
  // ../protocol/dist/constants.js
202
202
  var IRIS_DEFAULT_PORT = 4400;
@@ -243,6 +243,7 @@ var CRAWL_DEFAULTS = {
243
243
  /** HTTP status at/above which a response counts as a failed request. */
244
244
  FAILED_STATUS: 400
245
245
  };
246
+ var UpdateCheckIntervalMs = 24 * 60 * 60 * 1e3;
246
247
  var CONTRACT_FILE_VERSION = 1;
247
248
  var FROM_DISK_ARG = "fromDisk";
248
249
  var ContractReadError = {
@@ -846,10 +847,22 @@ var AnnotationSchema = z2.discriminatedUnion("kind", [
846
847
  ]);
847
848
 
848
849
  // ../server/dist/index.js
849
- import { join as join2 } from "path";
850
+ import { join as join5 } from "path";
850
851
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
851
852
 
853
+ // ../server/dist/http-server.js
854
+ import * as http from "http";
855
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
856
+
857
+ // ../server/dist/log.js
858
+ function log(event, fields = {}) {
859
+ const line = JSON.stringify({ event, ...fields });
860
+ process.stderr.write(`${line}
861
+ `);
862
+ }
863
+
852
864
  // ../server/dist/bridge.js
865
+ import * as http2 from "http";
853
866
  import { WebSocketServer } from "ws";
854
867
 
855
868
  // ../server/dist/events/ring-buffer.js
@@ -1271,7 +1284,16 @@ var SessionManager = class {
1271
1284
  }
1272
1285
  /**
1273
1286
  * Resolve the target session. With an explicit id, returns it. With none and exactly
1274
- * one connected, returns that. Otherwise throws a clear, agent-readable error.
1287
+ * one connected, returns that.
1288
+ *
1289
+ * With none and multiple connected, applies smart auto-selection:
1290
+ * 1. Prefer non-throttled sessions (not hidden + recently heard from).
1291
+ * 2. Within each tier, prefer lowest lastSeenMs (most recently active SDK heartbeat).
1292
+ * 3. If two or more non-throttled sessions are within 1 s of each other, throw —
1293
+ * genuinely ambiguous, agent must specify sessionId.
1294
+ * 4. If ALL sessions are throttled (e.g. user is working in their editor on another
1295
+ * desktop), skip the gap check and pick the freshest heartbeat. This lets the agent
1296
+ * keep working in the background without requiring sessionId every time.
1275
1297
  */
1276
1298
  resolve(sessionId) {
1277
1299
  if (sessionId !== void 0) {
@@ -1285,25 +1307,33 @@ var SessionManager = class {
1285
1307
  if (this.#sessions.size === 0) {
1286
1308
  throw new Error("no browser session connected \u2014 is your app running with @syrin/iris-browser enabled?");
1287
1309
  }
1288
- if (this.#sessions.size > 1) {
1289
- const ids = [...this.#sessions.keys()].join(", ");
1290
- throw new Error(`multiple sessions connected (${ids}); pass sessionId to target one`);
1310
+ const all = [...this.#sessions.values()];
1311
+ if (all.length === 1) {
1312
+ const [only] = all;
1313
+ if (only === void 0)
1314
+ throw new Error("session lookup failed");
1315
+ only.markAgentActivity();
1316
+ return only;
1291
1317
  }
1292
- const [only] = this.#sessions.values();
1293
- if (only === void 0)
1318
+ const scored = all.map((s) => ({ s, score: s.throttled() ? 1 : 0, ms: s.lastSeenMs() }));
1319
+ const bestScore = Math.min(...scored.map((x) => x.score));
1320
+ const candidates = scored.filter((x) => x.score === bestScore);
1321
+ candidates.sort((a, b) => a.ms - b.ms);
1322
+ const [best, runnerUp] = candidates;
1323
+ if (best === void 0)
1294
1324
  throw new Error("session lookup failed");
1295
- only.markAgentActivity();
1296
- return only;
1325
+ const allThrottled = bestScore === 1;
1326
+ const RECENCY_GAP_MS = allThrottled ? 0 : 1e3;
1327
+ const clearWinner = runnerUp === void 0 || best.ms + RECENCY_GAP_MS < runnerUp.ms;
1328
+ if (!clearWinner) {
1329
+ const detail = all.map((s) => `${s.id} (${s.throttled() ? "throttled" : "active"}, lastSeenMs=${s.lastSeenMs()})`).join(", ");
1330
+ throw new Error(`multiple sessions connected \u2014 pass sessionId to target one: ${detail}`);
1331
+ }
1332
+ best.s.markAgentActivity();
1333
+ return best.s;
1297
1334
  }
1298
1335
  };
1299
1336
 
1300
- // ../server/dist/log.js
1301
- function log(event, fields = {}) {
1302
- const line = JSON.stringify({ event, ...fields });
1303
- process.stderr.write(`${line}
1304
- `);
1305
- }
1306
-
1307
1337
  // ../server/dist/bridge.js
1308
1338
  function rawToString(raw) {
1309
1339
  if (typeof raw === "string")
@@ -1322,16 +1352,30 @@ var Bridge = class {
1322
1352
  #clock;
1323
1353
  constructor(options) {
1324
1354
  this.#clock = options.clock ?? (() => Date.now());
1325
- this.#wss = new WebSocketServer({
1326
- port: options.port,
1327
- host: options.host ?? "127.0.0.1",
1328
- path: IRIS_WS_PATH
1329
- });
1330
- this.ready = new Promise((resolve) => {
1331
- this.#wss.on("listening", () => {
1332
- resolve(this.#wss.address().port);
1355
+ if (options.server !== void 0) {
1356
+ const srv = options.server;
1357
+ this.#wss = new WebSocketServer({ server: srv, path: IRIS_WS_PATH });
1358
+ this.ready = new Promise((resolve) => {
1359
+ if (srv.listening) {
1360
+ resolve(srv.address().port);
1361
+ } else {
1362
+ srv.once("listening", () => {
1363
+ resolve(srv.address().port);
1364
+ });
1365
+ }
1333
1366
  });
1334
- });
1367
+ } else {
1368
+ this.#wss = new WebSocketServer({
1369
+ port: options.port,
1370
+ host: options.host ?? "127.0.0.1",
1371
+ path: IRIS_WS_PATH
1372
+ });
1373
+ this.ready = new Promise((resolve) => {
1374
+ this.#wss.on("listening", () => {
1375
+ resolve(this.#wss.address().port);
1376
+ });
1377
+ });
1378
+ }
1335
1379
  this.#wss.on("connection", (socket) => {
1336
1380
  this.#onConnection(socket);
1337
1381
  });
@@ -1530,7 +1574,13 @@ var IrisTool = {
1530
1574
  /** Navigate the connected browser tab to a URL. */
1531
1575
  NAVIGATE: "iris_navigate",
1532
1576
  /** Reload the connected browser tab (soft or hard). */
1533
- REFRESH: "iris_refresh"
1577
+ REFRESH: "iris_refresh",
1578
+ /** Report running version, latest available, changelog, and breaking changes. */
1579
+ VERSION_INFO: "iris_version_info",
1580
+ /** Install the latest server version and restart (Claude Code reconnects automatically). */
1581
+ APPLY_UPDATE: "iris_apply_update",
1582
+ /** Restore the previous server version and restart. */
1583
+ ROLLBACK: "iris_rollback"
1534
1584
  };
1535
1585
 
1536
1586
  // ../server/dist/project/iris-dir.js
@@ -1557,11 +1607,11 @@ function flowPath(root, name) {
1557
1607
  function isValidFlowName(name) {
1558
1608
  return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
1559
1609
  }
1560
- async function ensureIrisDir(fs, root) {
1610
+ async function ensureIrisDir(fs2, root) {
1561
1611
  const p = irisDirPaths(root);
1562
- await fs.mkdir(p.root);
1563
- await fs.mkdir(p.flows);
1564
- await fs.mkdir(p.baselines);
1612
+ await fs2.mkdir(p.root);
1613
+ await fs2.mkdir(p.flows);
1614
+ await fs2.mkdir(p.baselines);
1565
1615
  }
1566
1616
  var JSON_INDENT = 2;
1567
1617
  function stableSerialize(capabilities, generatedAt) {
@@ -1578,21 +1628,21 @@ function stableSerialize(capabilities, generatedAt) {
1578
1628
  return `${JSON.stringify(envelope, null, JSON_INDENT)}
1579
1629
  `;
1580
1630
  }
1581
- async function writeContract(fs, root, capabilities, now) {
1582
- await ensureIrisDir(fs, root);
1583
- await fs.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
1631
+ async function writeContract(fs2, root, capabilities, now) {
1632
+ await ensureIrisDir(fs2, root);
1633
+ await fs2.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
1584
1634
  }
1585
- async function readContract(fs, root) {
1635
+ async function readContract(fs2, root) {
1586
1636
  const path = irisDirPaths(root).contract;
1587
- if (!await fs.exists(path))
1637
+ if (!await fs2.exists(path))
1588
1638
  return { ok: false, reason: ContractReadError.MISSING };
1589
1639
  let text;
1590
1640
  try {
1591
- text = await fs.readFile(path);
1641
+ text = await fs2.readFile(path);
1592
1642
  } catch (error) {
1593
1643
  return {
1594
1644
  ok: false,
1595
- reason: fs.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
1645
+ reason: fs2.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
1596
1646
  };
1597
1647
  }
1598
1648
  let parsed;
@@ -1679,8 +1729,8 @@ var FlowStore = class {
1679
1729
  #fs;
1680
1730
  #root;
1681
1731
  #clock;
1682
- constructor(fs, root, clock) {
1683
- this.#fs = fs;
1732
+ constructor(fs2, root, clock) {
1733
+ this.#fs = fs2;
1684
1734
  this.#root = root;
1685
1735
  this.#clock = clock;
1686
1736
  }
@@ -1826,8 +1876,8 @@ var ProjectStore = class {
1826
1876
  #fs;
1827
1877
  #root;
1828
1878
  #clock;
1829
- constructor(fs, root, clock) {
1830
- this.#fs = fs;
1879
+ constructor(fs2, root, clock) {
1880
+ this.#fs = fs2;
1831
1881
  this.#root = root;
1832
1882
  this.#clock = clock;
1833
1883
  }
@@ -2006,10 +2056,10 @@ function createNodeFileSystem() {
2006
2056
 
2007
2057
  // ../server/dist/mcp.js
2008
2058
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2009
- import { z as z15 } from "zod";
2059
+ import { z as z16 } from "zod";
2010
2060
 
2011
2061
  // ../server/dist/tools/tools.js
2012
- import { z as z14 } from "zod";
2062
+ import { z as z15 } from "zod";
2013
2063
 
2014
2064
  // ../server/dist/input/real-input.js
2015
2065
  var DriveError = class extends Error {
@@ -3618,8 +3668,8 @@ async function diffPng(baselineBytes, currentBytes, opts = {}) {
3618
3668
  var VisualStore = class {
3619
3669
  #fs;
3620
3670
  #root;
3621
- constructor(fs, root) {
3622
- this.#fs = fs;
3671
+ constructor(fs2, root) {
3672
+ this.#fs = fs2;
3623
3673
  this.#root = root;
3624
3674
  }
3625
3675
  /** The absolute baseline path for `name` (for echoing back to the agent). */
@@ -4245,6 +4295,266 @@ function withControl(session, result) {
4245
4295
  return control === void 0 ? result : { ...result, control };
4246
4296
  }
4247
4297
 
4298
+ // ../server/dist/update/update-tools.js
4299
+ import { z as z14 } from "zod";
4300
+
4301
+ // ../server/dist/update/update-checker.js
4302
+ import * as fs from "fs";
4303
+ import * as https from "https";
4304
+ import { join as join2 } from "path";
4305
+ import { homedir } from "os";
4306
+ var IRIS_HOME = join2(homedir(), ".iris");
4307
+ var MANIFEST_PATH = join2(IRIS_HOME, "update-manifest.json");
4308
+ var NPM_REGISTRY = "https://registry.npmjs.org/@syrin/iris/latest";
4309
+ function loadManifest() {
4310
+ if (!fs.existsSync(MANIFEST_PATH))
4311
+ return null;
4312
+ try {
4313
+ const raw = fs.readFileSync(MANIFEST_PATH, "utf8");
4314
+ return JSON.parse(raw);
4315
+ } catch {
4316
+ return null;
4317
+ }
4318
+ }
4319
+ function saveManifest(manifest) {
4320
+ fs.mkdirSync(IRIS_HOME, { recursive: true });
4321
+ fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2), "utf8");
4322
+ }
4323
+ function isCacheFresh(manifest) {
4324
+ const checked = new Date(manifest.lastChecked).getTime();
4325
+ return Date.now() - checked < UpdateCheckIntervalMs;
4326
+ }
4327
+ function fetchNpmInfo() {
4328
+ return new Promise((resolve, reject) => {
4329
+ const req = https.get(NPM_REGISTRY, (res) => {
4330
+ let body = "";
4331
+ res.setEncoding("utf8");
4332
+ res.on("data", (chunk) => {
4333
+ body += chunk;
4334
+ });
4335
+ res.on("end", () => {
4336
+ try {
4337
+ resolve(JSON.parse(body));
4338
+ } catch (err) {
4339
+ reject(err instanceof Error ? err : new Error(String(err)));
4340
+ }
4341
+ });
4342
+ res.on("error", reject);
4343
+ });
4344
+ req.setTimeout(5e3, () => {
4345
+ req.destroy();
4346
+ reject(new Error("npm registry request timed out"));
4347
+ });
4348
+ req.on("error", reject);
4349
+ });
4350
+ }
4351
+ async function checkForUpdate(currentVersion) {
4352
+ const cached = loadManifest();
4353
+ if (cached !== null && cached.currentVersion === currentVersion && isCacheFresh(cached)) {
4354
+ return cached;
4355
+ }
4356
+ try {
4357
+ const info = await fetchNpmInfo();
4358
+ const updateAvailable = info.version !== currentVersion;
4359
+ const manifest = {
4360
+ currentVersion,
4361
+ latestVersion: info.version,
4362
+ updateAvailable,
4363
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
4364
+ ...info.iris?.changelog !== void 0 ? { changelog: info.iris.changelog } : {},
4365
+ ...info.iris?.breakingChanges !== void 0 ? { breakingChanges: info.iris.breakingChanges } : {},
4366
+ ...cached?.previousVersion !== void 0 ? { previousVersion: cached.previousVersion } : {}
4367
+ };
4368
+ saveManifest(manifest);
4369
+ return manifest;
4370
+ } catch (err) {
4371
+ log("iris_update_check_failed", {
4372
+ error: err instanceof Error ? err.message : String(err)
4373
+ });
4374
+ if (cached !== null)
4375
+ return { ...cached, currentVersion };
4376
+ return {
4377
+ currentVersion,
4378
+ updateAvailable: false,
4379
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString()
4380
+ };
4381
+ }
4382
+ }
4383
+
4384
+ // ../server/dist/update/updater.js
4385
+ import { execFile } from "child_process";
4386
+ import { existsSync as existsSync2 } from "fs";
4387
+ import { platform } from "os";
4388
+ import { dirname, join as join3 } from "path";
4389
+ var NPM_BIN = platform() === "win32" ? "npm.cmd" : "npm";
4390
+ var NPM_TIMEOUT_MS = 12e4;
4391
+ var ExecutionKind = {
4392
+ /** Launched via `npx @syrin/iris` — npm re-resolves the package on restart. */
4393
+ NPX: "npx",
4394
+ /** Installed globally via `npm install -g`. */
4395
+ GLOBAL: "global",
4396
+ /** Installed as a local project dependency. */
4397
+ LOCAL: "local"
4398
+ };
4399
+ function detectExecutionKind() {
4400
+ const script = process.argv[1] ?? "";
4401
+ if (script.includes("/_npx/") || script.includes("\\_npx\\"))
4402
+ return ExecutionKind.NPX;
4403
+ if (script.includes("/node_modules/") || script.includes("\\node_modules\\")) {
4404
+ return ExecutionKind.LOCAL;
4405
+ }
4406
+ return ExecutionKind.GLOBAL;
4407
+ }
4408
+ function findLocalProjectRoot() {
4409
+ let dir = process.cwd();
4410
+ for (; ; ) {
4411
+ if (existsSync2(join3(dir, "package.json")))
4412
+ return dir;
4413
+ const parent = dirname(dir);
4414
+ if (parent === dir)
4415
+ return null;
4416
+ dir = parent;
4417
+ }
4418
+ }
4419
+ function runNpm(args, opts = {}) {
4420
+ return new Promise((resolve, reject) => {
4421
+ execFile(NPM_BIN, args, { timeout: NPM_TIMEOUT_MS, ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {} }, (err, _stdout, stderr) => {
4422
+ if (err !== null) {
4423
+ reject(new Error(`npm ${args.join(" ")} failed: ${stderr !== "" ? stderr : err.message}`));
4424
+ } else {
4425
+ resolve();
4426
+ }
4427
+ });
4428
+ });
4429
+ }
4430
+ async function installVersion(version, kind) {
4431
+ const pkg = `@syrin/iris@${version}`;
4432
+ if (kind === ExecutionKind.NPX) {
4433
+ log("iris_update_npx_strategy", {
4434
+ note: "Running via npx \u2014 exiting so Claude Code restarts and npx fetches the new version"
4435
+ });
4436
+ return;
4437
+ }
4438
+ if (kind === ExecutionKind.LOCAL) {
4439
+ const root = findLocalProjectRoot();
4440
+ if (root !== null) {
4441
+ await runNpm(["install", pkg], { cwd: root });
4442
+ return;
4443
+ }
4444
+ log("iris_update_local_no_root", { fallback: "global" });
4445
+ }
4446
+ await runNpm(["install", "-g", pkg]);
4447
+ }
4448
+ async function installVersionRollback(version, kind) {
4449
+ if (kind === ExecutionKind.NPX) {
4450
+ log("iris_rollback_npx_strategy", {
4451
+ note: "Running via npx \u2014 update your .mcp.json args to pin the version you want to restore"
4452
+ });
4453
+ return;
4454
+ }
4455
+ await installVersion(version, kind);
4456
+ }
4457
+ async function applyUpdate(targetVersion) {
4458
+ const manifest = loadManifest();
4459
+ if (manifest !== null) {
4460
+ saveManifest({ ...manifest, previousVersion: manifest.currentVersion });
4461
+ }
4462
+ const kind = detectExecutionKind();
4463
+ log("iris_update_applying", { version: targetVersion, executionKind: kind });
4464
+ await installVersion(targetVersion, kind);
4465
+ log("iris_update_applied", { version: targetVersion, executionKind: kind });
4466
+ process.exit(0);
4467
+ }
4468
+ async function rollback() {
4469
+ const manifest = loadManifest();
4470
+ if (manifest === null || manifest.previousVersion === void 0) {
4471
+ throw new Error("No previous version available for rollback");
4472
+ }
4473
+ const prev = manifest.previousVersion;
4474
+ const kind = detectExecutionKind();
4475
+ log("iris_rollback_applying", { version: prev, executionKind: kind });
4476
+ await installVersionRollback(prev, kind);
4477
+ log("iris_rollback_applied", { version: prev, executionKind: kind });
4478
+ process.exit(0);
4479
+ }
4480
+
4481
+ // ../server/dist/server-version.js
4482
+ import { createRequire } from "module";
4483
+ var _pkg = createRequire(import.meta.url)("../package.json");
4484
+ var SERVER_VERSION = _pkg.version;
4485
+
4486
+ // ../server/dist/update/update-tools.js
4487
+ var UPDATE_TOOLS = [
4488
+ {
4489
+ name: IrisTool.VERSION_INFO,
4490
+ 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.",
4491
+ inputSchema: {},
4492
+ outputSchema: {
4493
+ currentVersion: z14.string().describe("The Iris server version currently running."),
4494
+ latestVersion: z14.string().optional().describe("Latest published version on npm."),
4495
+ updateAvailable: z14.boolean().describe("True when a newer version is available to install."),
4496
+ executionKind: z14.string().describe('How iris was launched: "npx" (no install needed \u2014 restart applies update), "global" (npm install -g), or "local" (project node_modules).'),
4497
+ changelog: z14.string().optional().describe("Release notes for the latest version."),
4498
+ breakingChanges: z14.array(z14.string()).optional().describe("Breaking changes in the latest version that may affect your scripts."),
4499
+ rollbackAvailable: z14.boolean().describe("True when a previous version is stored and can be restored."),
4500
+ previousVersion: z14.string().optional().describe("The version that would be restored on rollback.")
4501
+ },
4502
+ handler: async (_deps) => {
4503
+ const manifest = await checkForUpdate(SERVER_VERSION);
4504
+ return {
4505
+ currentVersion: manifest.currentVersion,
4506
+ ...manifest.latestVersion !== void 0 ? { latestVersion: manifest.latestVersion } : {},
4507
+ updateAvailable: manifest.updateAvailable,
4508
+ executionKind: detectExecutionKind(),
4509
+ ...manifest.changelog !== void 0 ? { changelog: manifest.changelog } : {},
4510
+ ...manifest.breakingChanges !== void 0 ? { breakingChanges: manifest.breakingChanges } : {},
4511
+ rollbackAvailable: manifest.previousVersion !== void 0,
4512
+ ...manifest.previousVersion !== void 0 ? { previousVersion: manifest.previousVersion } : {}
4513
+ };
4514
+ }
4515
+ },
4516
+ {
4517
+ name: IrisTool.APPLY_UPDATE,
4518
+ 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.',
4519
+ inputSchema: {
4520
+ confirm: z14.boolean().describe("Set to true to confirm the update should be applied. Required to prevent accidental upgrades.")
4521
+ },
4522
+ outputSchema: {
4523
+ ok: z14.boolean(),
4524
+ message: z14.string().optional()
4525
+ },
4526
+ handler: async (_deps, args) => {
4527
+ if (args["confirm"] !== true) {
4528
+ return { ok: false, message: "Set confirm:true to apply the update" };
4529
+ }
4530
+ const manifest = await checkForUpdate(SERVER_VERSION);
4531
+ if (!manifest.updateAvailable || manifest.latestVersion === void 0) {
4532
+ return { ok: false, message: "No update available \u2014 already on the latest version" };
4533
+ }
4534
+ await applyUpdate(manifest.latestVersion);
4535
+ return { ok: true };
4536
+ }
4537
+ },
4538
+ {
4539
+ name: IrisTool.ROLLBACK,
4540
+ 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.",
4541
+ inputSchema: {
4542
+ confirm: z14.boolean().describe("Set to true to confirm the rollback. Required to prevent accidental downgrades.")
4543
+ },
4544
+ outputSchema: {
4545
+ ok: z14.boolean(),
4546
+ message: z14.string().optional()
4547
+ },
4548
+ handler: async (_deps, args) => {
4549
+ if (args["confirm"] !== true) {
4550
+ return { ok: false, message: "Set confirm:true to apply the rollback" };
4551
+ }
4552
+ await rollback();
4553
+ return { ok: true };
4554
+ }
4555
+ }
4556
+ ];
4557
+
4248
4558
  // ../server/dist/tools/tools.js
4249
4559
  async function snapshotTree(deps, sessionId) {
4250
4560
  const session = deps.sessions.resolve(sessionId);
@@ -4255,7 +4565,7 @@ async function snapshotTree(deps, sessionId) {
4255
4565
  return { lines: normalizeLines(snap.tree ?? ""), route: snap.status?.route ?? "" };
4256
4566
  }
4257
4567
  var sessionIdShape6 = {
4258
- sessionId: z14.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open \u2014 Iris resolves it automatically.")
4568
+ sessionId: z15.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open \u2014 Iris resolves it automatically.")
4259
4569
  };
4260
4570
  async function commandOrThrow3(deps, sessionId, name, args) {
4261
4571
  const session = deps.sessions.resolve(sessionId);
@@ -4332,17 +4642,17 @@ var TOOLS = [
4332
4642
  description: "List connected browser sessions (tab url/title, sessionId, last-seen, health: hidden/focused/throttled, and `realInputAvailable` \u2014 true when native CDP/launched real input is driving this tab), plus a `recommendation` pointing to `iris drive` when a tab is hidden/throttled and may be un-scriptable from here.",
4333
4643
  inputSchema: {},
4334
4644
  outputSchema: {
4335
- sessions: z14.array(z14.object({
4336
- sessionId: z14.string(),
4337
- url: z14.string(),
4338
- title: z14.string().optional(),
4339
- lastSeenMs: z14.number(),
4340
- throttled: z14.boolean(),
4341
- focused: z14.boolean(),
4342
- hidden: z14.boolean(),
4343
- realInputAvailable: z14.boolean().optional(),
4344
- stale: z14.boolean().optional(),
4345
- recommendation: z14.string().optional()
4645
+ sessions: z15.array(z15.object({
4646
+ sessionId: z15.string(),
4647
+ url: z15.string(),
4648
+ title: z15.string().optional(),
4649
+ lastSeenMs: z15.number(),
4650
+ throttled: z15.boolean(),
4651
+ focused: z15.boolean(),
4652
+ hidden: z15.boolean(),
4653
+ realInputAvailable: z15.boolean().optional(),
4654
+ stale: z15.boolean().optional(),
4655
+ recommendation: z15.string().optional()
4346
4656
  })).describe("Connected browser sessions with health state.")
4347
4657
  },
4348
4658
  handler: async (deps) => {
@@ -4358,13 +4668,13 @@ var TOOLS = [
4358
4668
  name: IrisTool.SNAPSHOT,
4359
4669
  description: "Semantic accessibility snapshot of the page or a subtree. mode: full|interactive|status. Use to see what is on screen right now.",
4360
4670
  inputSchema: {
4361
- scope: z14.string().optional().describe("CSS selector or element ref to restrict the snapshot to a subtree. Omit to snapshot the whole page."),
4362
- mode: z14.nativeEnum(SnapshotMode).optional().describe("full = all elements; interactive = only clickable/focusable elements; status = only route + title. Default: full."),
4671
+ scope: z15.string().optional().describe("CSS selector or element ref to restrict the snapshot to a subtree. Omit to snapshot the whole page."),
4672
+ mode: z15.nativeEnum(SnapshotMode).optional().describe("full = all elements; interactive = only clickable/focusable elements; status = only route + title. Default: full."),
4363
4673
  ...sessionIdShape6
4364
4674
  },
4365
4675
  outputSchema: {
4366
- tree: z14.string().optional().describe("Indented ARIA tree of every element on the page (or the scoped subtree)."),
4367
- status: z14.object({ route: z14.string(), title: z14.string().optional() }).optional()
4676
+ tree: z15.string().optional().describe("Indented ARIA tree of every element on the page (or the scoped subtree)."),
4677
+ status: z15.object({ route: z15.string(), title: z15.string().optional() }).optional()
4368
4678
  },
4369
4679
  handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.SNAPSHOT, {
4370
4680
  scope: args["scope"],
@@ -4375,25 +4685,25 @@ var TOOLS = [
4375
4685
  name: IrisTool.QUERY,
4376
4686
  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.",
4377
4687
  inputSchema: {
4378
- by: z14.string().describe("Query strategy: role | text | label | placeholder | testid | alt"),
4379
- value: z14.string().describe("Query value for the selected strategy (e.g. by=role value=button, or by=testid value=submit-btn)."),
4380
- name: z14.string().optional().describe("Accessible name filter \u2014 narrows results when `by` is role and the page has many elements of that role."),
4381
- scope: z14.string().optional().describe("CSS selector or element ref to restrict the search to a subtree."),
4688
+ by: z15.string().describe("Query strategy: role | text | label | placeholder | testid | alt"),
4689
+ value: z15.string().describe("Query value for the selected strategy (e.g. by=role value=button, or by=testid value=submit-btn)."),
4690
+ name: z15.string().optional().describe("Accessible name filter \u2014 narrows results when `by` is role and the page has many elements of that role."),
4691
+ scope: z15.string().optional().describe("CSS selector or element ref to restrict the search to a subtree."),
4382
4692
  ...sessionIdShape6
4383
4693
  },
4384
4694
  outputSchema: {
4385
- elements: z14.array(z14.object({
4386
- ref: z14.string(),
4387
- role: z14.string(),
4388
- name: z14.string(),
4389
- value: z14.string().optional(),
4390
- states: z14.array(z14.string()),
4391
- visible: z14.boolean()
4695
+ elements: z15.array(z15.object({
4696
+ ref: z15.string(),
4697
+ role: z15.string(),
4698
+ name: z15.string(),
4699
+ value: z15.string().optional(),
4700
+ states: z15.array(z15.string()),
4701
+ visible: z15.boolean()
4392
4702
  })),
4393
- hint: z14.object({
4394
- route: z14.string(),
4395
- presentTestids: z14.array(z14.string()),
4396
- knownEmptyState: z14.boolean()
4703
+ hint: z15.object({
4704
+ route: z15.string(),
4705
+ presentTestids: z15.array(z15.string()),
4706
+ knownEmptyState: z15.boolean()
4397
4707
  }).optional().describe("Present only on zero matches \u2014 tells you what IS on the page so you can diagnose the miss.")
4398
4708
  },
4399
4709
  handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.QUERY, {
@@ -4407,18 +4717,18 @@ var TOOLS = [
4407
4717
  name: IrisTool.INSPECT,
4408
4718
  description: "Deep info on one element by ref: full a11y props, visibility, box, and (with @syrin/iris-react) component stack + source file.",
4409
4719
  inputSchema: {
4410
- ref: z14.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
4720
+ ref: z15.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
4411
4721
  ...sessionIdShape6
4412
4722
  },
4413
4723
  outputSchema: {
4414
- ref: z14.string(),
4415
- role: z14.string(),
4416
- name: z14.string(),
4417
- value: z14.string().optional(),
4418
- states: z14.array(z14.string()),
4419
- visible: z14.boolean(),
4420
- box: z14.object({ x: z14.number(), y: z14.number(), width: z14.number(), height: z14.number() }).optional(),
4421
- component: z14.object({ name: z14.string(), sourceFile: z14.string().optional() }).optional()
4724
+ ref: z15.string(),
4725
+ role: z15.string(),
4726
+ name: z15.string(),
4727
+ value: z15.string().optional(),
4728
+ states: z15.array(z15.string()),
4729
+ visible: z15.boolean(),
4730
+ box: z15.object({ x: z15.number(), y: z15.number(), width: z15.number(), height: z15.number() }).optional(),
4731
+ component: z15.object({ name: z15.string().optional(), sourceFile: z15.string().optional() }).optional()
4422
4732
  },
4423
4733
  handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.INSPECT, {
4424
4734
  ref: args["ref"]
@@ -4428,19 +4738,19 @@ var TOOLS = [
4428
4738
  name: IrisTool.ACT,
4429
4739
  description: 'Execute one action against a ref: click|dblclick|hover|focus|fill|type|clear|select|check|uncheck|submit|press|scrollIntoView. Returns immediately with a `since` cursor \u2014 observe the reaction with iris_observe. Carries effect:{dispatched,targetMatched,visible,enabled,focusMoved,valueChanged,domMutatedWithin,occluded,occludedBy,scrolledIntoView} to tell "action missed" from "app didn\'t react"; dispatched=landed, settled=a real frame flushed, and a settle timeout never fails the tool. occluded=true means the click point is covered by another element (a real user could not click it) \u2014 synthetic dispatch still delivered the event; scrolledIntoView=true means an off-viewport target was scrolled in first. inputMode is "real" (native CDP, no synthetic effect block) or "synthetic"; clicks default to the occlusion-honest synthetic path even when CDP is configured \u2014 pass args.native:true to force a trusted native click (file pickers, clipboard). inputModeReason explains any real\u2192synthetic choice so it is never silent. Full model (real-input, throttled tabs, `iris drive`): docs/usage.md \xA718.',
4430
4740
  inputSchema: {
4431
- ref: z14.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
4432
- action: z14.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
4433
- args: z14.record(z14.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press, { native: true } to force a trusted native click."),
4434
- refuseWhenThrottled: z14.boolean().optional().describe("Throw instead of silently sending synthetic events when the tab is throttled/backgrounded. Default: false (synthetic events are still sent)."),
4741
+ ref: z15.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
4742
+ action: z15.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
4743
+ args: z15.record(z15.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press, { native: true } to force a trusted native click."),
4744
+ refuseWhenThrottled: z15.boolean().optional().describe("Throw instead of silently sending synthetic events when the tab is throttled/backgrounded. Default: false (synthetic events are still sent)."),
4435
4745
  ...sessionIdShape6
4436
4746
  },
4437
4747
  outputSchema: {
4438
- since: z14.number().describe("Cursor \u2014 pass to iris_observe/iris_wait_for/iris_assert to scope reaction queries to this act."),
4439
- dispatched: z14.boolean(),
4440
- settled: z14.boolean().nullable(),
4441
- inputMode: z14.string(),
4442
- result: z14.unknown().optional(),
4443
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4748
+ since: z15.number().describe("Cursor \u2014 pass to iris_observe/iris_wait_for/iris_assert to scope reaction queries to this act."),
4749
+ dispatched: z15.boolean(),
4750
+ settled: z15.boolean().nullable(),
4751
+ inputMode: z15.string(),
4752
+ result: z15.unknown().optional(),
4753
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4444
4754
  },
4445
4755
  handler: async (deps, args) => {
4446
4756
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4496,14 +4806,14 @@ var TOOLS = [
4496
4806
  name: IrisTool.ACT_SEQUENCE,
4497
4807
  description: "Run multiple actions in order (fill -> fill -> submit) in one round-trip. Returns per-step effects[] (see iris_act).",
4498
4808
  inputSchema: {
4499
- steps: z14.array(z14.record(z14.unknown())).describe("Ordered list of { ref, action, args? } objects. Each step is equivalent to one iris_act call."),
4809
+ steps: z15.array(z15.record(z15.unknown())).describe("Ordered list of { ref, action, args? } objects. Each step is equivalent to one iris_act call."),
4500
4810
  ...sessionIdShape6
4501
4811
  },
4502
4812
  outputSchema: {
4503
- since: z14.number(),
4504
- dispatched: z14.boolean(),
4505
- result: z14.unknown().optional(),
4506
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4813
+ since: z15.number(),
4814
+ dispatched: z15.boolean(),
4815
+ result: z15.unknown().optional(),
4816
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4507
4817
  },
4508
4818
  handler: async (deps, args) => {
4509
4819
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4531,23 +4841,23 @@ var TOOLS = [
4531
4841
  name: IrisTool.ACT_AND_WAIT,
4532
4842
  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.",
4533
4843
  inputSchema: {
4534
- ref: z14.string().describe("Element ref from iris_snapshot or iris_query."),
4535
- action: z14.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
4536
- args: z14.record(z14.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press."),
4844
+ ref: z15.string().describe("Element ref from iris_snapshot or iris_query."),
4845
+ action: z15.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
4846
+ args: z15.record(z15.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press."),
4537
4847
  until: PredicateSchema.describe("Predicate to wait for after the action completes. Same shape accepted by iris_assert."),
4538
- timeout_ms: z14.number().optional().describe("Maximum wait time in milliseconds. 0 = evaluate once without waiting. Default: 4000."),
4539
- refuseWhenThrottled: z14.boolean().optional().describe("Throw if the tab is throttled. Default: false."),
4848
+ timeout_ms: z15.number().optional().describe("Maximum wait time in milliseconds. 0 = evaluate once without waiting. Default: 4000."),
4849
+ refuseWhenThrottled: z15.boolean().optional().describe("Throw if the tab is throttled. Default: false."),
4540
4850
  ...sessionIdShape6
4541
4851
  },
4542
4852
  outputSchema: {
4543
- effect: z14.unknown().describe("The iris_act result (dispatched, settled, inputMode, etc.)."),
4544
- verdict: z14.object({
4545
- pass: z14.boolean(),
4546
- evidence: z14.unknown().optional(),
4547
- failureReason: z14.string().optional()
4853
+ effect: z15.unknown().describe("The iris_act result (dispatched, settled, inputMode, etc.)."),
4854
+ verdict: z15.object({
4855
+ pass: z15.boolean(),
4856
+ evidence: z15.unknown().optional(),
4857
+ failureReason: z15.string().optional()
4548
4858
  }),
4549
- trace: z14.unknown().describe("Reaction report (same shape as iris_observe summary)."),
4550
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4859
+ trace: z15.unknown().describe("Reaction report (same shape as iris_observe summary)."),
4860
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4551
4861
  },
4552
4862
  handler: async (deps, args) => {
4553
4863
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4580,31 +4890,31 @@ var TOOLS = [
4580
4890
  name: IrisTool.OBSERVE,
4581
4891
  description: "Return the timeline of everything the app did in a window (DOM/network/route/console/animation/signal), with a summary. Use after an action. Pass `max_events` to cap the timeline to the most recent N (older events are dropped and counted in cost.droppedOldest). Every result carries a `cost:{events,bytes}` hint so you can self-budget your next call.",
4582
4892
  inputSchema: {
4583
- window_ms: z14.number().optional().describe("Time window to look back. Default: 2000ms. Ignored when `since` is provided."),
4584
- since: z14.number().optional().describe("Cursor from a prior iris_act or iris_observe call. Scopes the event window to exactly that span."),
4585
- filters: z14.array(z14.string()).optional().describe("Event type allowlist: dom | net | route | console | animation | signal. Omit to return all types."),
4586
- max_events: z14.number().optional().describe("Cap the timeline to the most recent N events. Older events are counted in cost.droppedOldest."),
4893
+ window_ms: z15.number().optional().describe("Time window to look back. Default: 2000ms. Ignored when `since` is provided."),
4894
+ since: z15.number().optional().describe("Cursor from a prior iris_act or iris_observe call. Scopes the event window to exactly that span."),
4895
+ filters: z15.array(z15.string()).optional().describe("Event type allowlist: dom | net | route | console | animation | signal. Omit to return all types."),
4896
+ max_events: z15.number().optional().describe("Cap the timeline to the most recent N events. Older events are counted in cost.droppedOldest."),
4587
4897
  ...sessionIdShape6
4588
4898
  },
4589
4899
  outputSchema: {
4590
- events: z14.array(z14.unknown()),
4591
- summary: z14.object({
4592
- total: z14.number(),
4593
- network: z14.number(),
4594
- domAdded: z14.number(),
4595
- domRemoved: z14.number(),
4596
- domChanged: z14.number(),
4597
- routeChanges: z14.number(),
4598
- consoleErrors: z14.number(),
4599
- animations: z14.number(),
4600
- signals: z14.number()
4900
+ events: z15.array(z15.unknown()),
4901
+ summary: z15.object({
4902
+ total: z15.number(),
4903
+ network: z15.number(),
4904
+ domAdded: z15.number(),
4905
+ domRemoved: z15.number(),
4906
+ domChanged: z15.number(),
4907
+ routeChanges: z15.number(),
4908
+ consoleErrors: z15.number(),
4909
+ animations: z15.number(),
4910
+ signals: z15.number()
4601
4911
  }),
4602
- cost: z14.object({
4603
- events: z14.number(),
4604
- bytes: z14.number(),
4605
- droppedOldest: z14.number().optional()
4912
+ cost: z15.object({
4913
+ events: z15.number(),
4914
+ bytes: z15.number(),
4915
+ droppedOldest: z15.number().optional()
4606
4916
  }),
4607
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4917
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4608
4918
  },
4609
4919
  handler: (deps, args) => {
4610
4920
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4627,15 +4937,15 @@ var TOOLS = [
4627
4937
  description: "Block until a predicate is satisfied (or already true in the recent buffer), else time out. Returns matching evidence or a near-miss diagnosis. By default it only counts events since your last act, so a signal buffered BEFORE the action can never fake a pass; pass `since` (an observe/act cursor) to widen or narrow that window explicitly.",
4628
4938
  inputSchema: {
4629
4939
  predicate: PredicateSchema.describe("Predicate to wait for: { signal }, { net }, { element } or a combination."),
4630
- timeout_ms: z14.number().optional().describe("Maximum wait in milliseconds. Default: 4000."),
4631
- since: z14.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the wait to events after that act."),
4940
+ timeout_ms: z15.number().optional().describe("Maximum wait in milliseconds. Default: 4000."),
4941
+ since: z15.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the wait to events after that act."),
4632
4942
  ...sessionIdShape6
4633
4943
  },
4634
4944
  outputSchema: {
4635
- pass: z14.boolean(),
4636
- evidence: z14.unknown().optional(),
4637
- failureReason: z14.string().optional(),
4638
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4945
+ pass: z15.boolean(),
4946
+ evidence: z15.unknown().optional(),
4947
+ failureReason: z15.string().optional(),
4948
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4639
4949
  },
4640
4950
  handler: async (deps, args) => {
4641
4951
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4650,15 +4960,15 @@ var TOOLS = [
4650
4960
  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.",
4651
4961
  inputSchema: {
4652
4962
  predicate: PredicateSchema.describe("Predicate to evaluate: { signal }, { net }, { element } or a combination."),
4653
- timeout_ms: z14.number().optional().describe("If > 0, wait up to this many milliseconds before failing. Default: 0 (evaluate once)."),
4654
- since: z14.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the assertion to events after that act."),
4963
+ timeout_ms: z15.number().optional().describe("If > 0, wait up to this many milliseconds before failing. Default: 0 (evaluate once)."),
4964
+ since: z15.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the assertion to events after that act."),
4655
4965
  ...sessionIdShape6
4656
4966
  },
4657
4967
  outputSchema: {
4658
- pass: z14.boolean(),
4659
- evidence: z14.unknown().optional(),
4660
- failureReason: z14.string().optional(),
4661
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4968
+ pass: z15.boolean(),
4969
+ evidence: z15.unknown().optional(),
4970
+ failureReason: z15.string().optional(),
4971
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4662
4972
  },
4663
4973
  handler: async (deps, args) => {
4664
4974
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4673,15 +4983,15 @@ var TOOLS = [
4673
4983
  name: IrisTool.NETWORK,
4674
4984
  description: 'Filtered list of network calls. Fast path for "did POST /x return 200?". A zero-match filter returns a `hint` { totalInWindow, present[] } of the calls that DID fire, so a miss is diagnosable.',
4675
4985
  inputSchema: {
4676
- since: z14.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to requests fired after that act."),
4677
- method: z14.string().optional().describe("HTTP method filter: GET | POST | PUT | DELETE | PATCH etc."),
4678
- urlContains: z14.string().optional().describe("Substring that the request URL must contain."),
4679
- status: z14.number().optional().describe("HTTP status code filter (e.g. 200, 404, 500)."),
4986
+ since: z15.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to requests fired after that act."),
4987
+ method: z15.string().optional().describe("HTTP method filter: GET | POST | PUT | DELETE | PATCH etc."),
4988
+ urlContains: z15.string().optional().describe("Substring that the request URL must contain."),
4989
+ status: z15.number().optional().describe("HTTP status code filter (e.g. 200, 404, 500)."),
4680
4990
  ...sessionIdShape6
4681
4991
  },
4682
4992
  outputSchema: {
4683
- calls: z14.array(z14.unknown()),
4684
- hint: z14.object({ totalInWindow: z14.number(), present: z14.array(z14.string()) }).optional()
4993
+ calls: z15.array(z15.unknown()),
4994
+ hint: z15.object({ totalInWindow: z15.number(), present: z15.array(z15.string()) }).optional()
4685
4995
  },
4686
4996
  handler: (deps, args) => {
4687
4997
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4701,13 +5011,13 @@ var TOOLS = [
4701
5011
  name: IrisTool.CONSOLE,
4702
5012
  description: 'Console/error log. Fast path for "were there any errors during this flow?". When a level filter matches nothing, returns a `hint` { totalInWindow, byLevel } so 0 errors is distinguishable from a silent page.',
4703
5013
  inputSchema: {
4704
- level: z14.string().optional().describe("Log level filter: error | warn | info | log. Omit to return all levels."),
4705
- since: z14.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to log entries after that act."),
5014
+ level: z15.string().optional().describe("Log level filter: error | warn | info | log. Omit to return all levels."),
5015
+ since: z15.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to log entries after that act."),
4706
5016
  ...sessionIdShape6
4707
5017
  },
4708
5018
  outputSchema: {
4709
- logs: z14.array(z14.unknown()),
4710
- hint: z14.object({ totalInWindow: z14.number(), byLevel: z14.record(z14.number()) }).optional()
5019
+ logs: z15.array(z15.unknown()),
5020
+ hint: z15.object({ totalInWindow: z15.number(), byLevel: z15.record(z15.number()) }).optional()
4711
5021
  },
4712
5022
  handler: (deps, args) => {
4713
5023
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4726,7 +5036,7 @@ var TOOLS = [
4726
5036
  description: "Currently running + recently completed animations with targets/timing.",
4727
5037
  inputSchema: { ...sessionIdShape6 },
4728
5038
  outputSchema: {
4729
- animations: z14.array(z14.unknown())
5039
+ animations: z15.array(z15.unknown())
4730
5040
  },
4731
5041
  handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.ANIMATIONS, {})
4732
5042
  },
@@ -4734,12 +5044,12 @@ var TOOLS = [
4734
5044
  name: IrisTool.BASELINE_SAVE,
4735
5045
  description: "Snapshot the current semantic state under a name, to diff against later (regression detection).",
4736
5046
  inputSchema: {
4737
- name: z14.string().describe('Label for this baseline snapshot (e.g. "dashboard-initial"). Use the same name in iris_diff to compare.'),
5047
+ name: z15.string().describe('Label for this baseline snapshot (e.g. "dashboard-initial"). Use the same name in iris_diff to compare.'),
4738
5048
  ...sessionIdShape6
4739
5049
  },
4740
5050
  outputSchema: {
4741
- baseline: z14.string().describe("Saved baseline name \u2014 pass to iris_diff to compare."),
4742
- lineCount: z14.number()
5051
+ baseline: z15.string().describe("Saved baseline name \u2014 pass to iris_diff to compare."),
5052
+ lineCount: z15.number()
4743
5053
  },
4744
5054
  handler: async (deps, args) => {
4745
5055
  const name = asString4(args["name"]) ?? "default";
@@ -4753,7 +5063,7 @@ var TOOLS = [
4753
5063
  description: "List saved baseline names.",
4754
5064
  inputSchema: {},
4755
5065
  outputSchema: {
4756
- baselines: z14.array(z14.string())
5066
+ baselines: z15.array(z15.string())
4757
5067
  },
4758
5068
  handler: (deps) => Promise.resolve({ baselines: deps.baselines.list() })
4759
5069
  },
@@ -4761,15 +5071,15 @@ var TOOLS = [
4761
5071
  name: IrisTool.DIFF,
4762
5072
  description: 'Diff current semantic state vs a saved baseline: REMOVED/ADDED elements + console-error count. Call iris_baseline_list to list saved baselines, iris_baseline_save to create one. Pass `baseline` (name from iris_baseline_list). Answers "did anything silently go missing/break?".',
4763
5073
  inputSchema: {
4764
- baseline: z14.string().describe("Baseline name to compare against. Call iris_baseline_list to get available names; names are created by iris_baseline_save."),
5074
+ baseline: z15.string().describe("Baseline name to compare against. Call iris_baseline_list to get available names; names are created by iris_baseline_save."),
4765
5075
  ...sessionIdShape6
4766
5076
  },
4767
5077
  outputSchema: {
4768
- baseline: z14.string(),
4769
- removed: z14.array(z14.string()),
4770
- added: z14.array(z14.string()),
4771
- consoleErrors: z14.number(),
4772
- routeChanged: z14.boolean()
5078
+ baseline: z15.string(),
5079
+ removed: z15.array(z15.string()),
5080
+ added: z15.array(z15.string()),
5081
+ consoleErrors: z15.number(),
5082
+ routeChanged: z15.boolean()
4773
5083
  },
4774
5084
  handler: async (deps, args) => {
4775
5085
  const name = asString4(args["baseline"]) ?? "default";
@@ -4787,12 +5097,12 @@ var TOOLS = [
4787
5097
  name: IrisTool.RECORD_START,
4788
5098
  description: "Start recording the event timeline under a name (for replay / a flow report).",
4789
5099
  inputSchema: {
4790
- recordingName: z14.string().describe("Identifier for this recording. Pass the same name to iris_record_stop and iris_replay."),
5100
+ recordingName: z15.string().describe("Identifier for this recording. Pass the same name to iris_record_stop and iris_replay."),
4791
5101
  ...sessionIdShape6
4792
5102
  },
4793
5103
  outputSchema: {
4794
- recordingName: z14.string(),
4795
- since: z14.number()
5104
+ recordingName: z15.string(),
5105
+ since: z15.number()
4796
5106
  },
4797
5107
  handler: (deps, args) => {
4798
5108
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4806,13 +5116,13 @@ var TOOLS = [
4806
5116
  name: IrisTool.RECORD_STOP,
4807
5117
  description: "Stop the recording identified by `recordingName` and return both the reaction report for the span and a compiled, replayable { program: { version, steps:[{tool,args,stable}] } } of the agent acts captured during it.",
4808
5118
  inputSchema: {
4809
- recordingName: z14.string().describe("Identifier of an active recording started with iris_record_start."),
5119
+ recordingName: z15.string().describe("Identifier of an active recording started with iris_record_start."),
4810
5120
  ...sessionIdShape6
4811
5121
  },
4812
5122
  outputSchema: {
4813
- recordingName: z14.string(),
4814
- program: z14.unknown(),
4815
- warning: z14.string().optional()
5123
+ recordingName: z15.string(),
5124
+ program: z15.unknown(),
5125
+ warning: z15.string().optional()
4816
5126
  },
4817
5127
  handler: (deps, args) => {
4818
5128
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4844,17 +5154,17 @@ var TOOLS = [
4844
5154
  name: IrisTool.REPLAY,
4845
5155
  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?}] }.",
4846
5156
  inputSchema: {
4847
- recordingName: z14.string().describe("Name of a compiled recording (from iris_record_stop) to re-execute."),
5157
+ recordingName: z15.string().describe("Name of a compiled recording (from iris_record_stop) to re-execute."),
4848
5158
  ...sessionIdShape6
4849
5159
  },
4850
5160
  outputSchema: {
4851
- recordingName: z14.string(),
4852
- ok: z14.boolean(),
4853
- steps: z14.array(z14.object({
4854
- tool: z14.string(),
4855
- ok: z14.boolean(),
4856
- error: z14.string().optional(),
4857
- note: z14.string().optional()
5161
+ recordingName: z15.string(),
5162
+ ok: z15.boolean(),
5163
+ steps: z15.array(z15.object({
5164
+ tool: z15.string(),
5165
+ ok: z15.boolean(),
5166
+ error: z15.string().optional(),
5167
+ note: z15.string().optional()
4858
5168
  }))
4859
5169
  },
4860
5170
  handler: async (deps, args) => {
@@ -4872,30 +5182,31 @@ var TOOLS = [
4872
5182
  name: IrisTool.NARRATE,
4873
5183
  description: "Narrate your intent on the page (presenter HUD) so the human watching sees what you are about to do and why. Use a short sentence before a meaningful action.",
4874
5184
  inputSchema: {
4875
- text: z14.string().describe("Short sentence describing your next action, shown on the presenter HUD for the developer watching."),
4876
- level: z14.string().optional().describe("Display severity: info | warn | error. Default: info."),
5185
+ text: z15.string().describe("Short sentence describing your next action, shown on the presenter HUD for the developer watching."),
5186
+ level: z15.string().optional().describe("Display severity: info | warn | error. Default: info."),
4877
5187
  ...sessionIdShape6
4878
5188
  },
4879
- outputSchema: {
4880
- ok: z14.boolean()
4881
- },
4882
- handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.NARRATE, {
4883
- text: args["text"],
4884
- level: args["level"]
4885
- })
5189
+ outputSchema: { ok: z15.boolean() },
5190
+ handler: async (deps, args) => {
5191
+ const result = await commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.NARRATE, {
5192
+ text: args["text"],
5193
+ level: args["level"]
5194
+ });
5195
+ return { ok: true, ...result };
5196
+ }
4886
5197
  },
4887
5198
  {
4888
5199
  name: IrisTool.CLOCK,
4889
5200
  description: "Control a fake clock: { freeze:true } to freeze time, { advanceMs:N } to fast-forward timers (toasts, debounces, auto-dismiss), { reset:true } to restore. Lets you test time-gated UI deterministically.",
4890
5201
  inputSchema: {
4891
- freeze: z14.boolean().optional().describe("Freeze the fake clock. Time stops advancing until advanceMs or reset."),
4892
- advanceMs: z14.number().optional().describe("Fast-forward time by this many milliseconds \u2014 triggers debounces, toasts, auto-dismiss timers."),
4893
- reset: z14.boolean().optional().describe("Restore the real clock."),
5202
+ freeze: z15.boolean().optional().describe("Freeze the fake clock. Time stops advancing until advanceMs or reset."),
5203
+ advanceMs: z15.number().optional().describe("Fast-forward time by this many milliseconds \u2014 triggers debounces, toasts, auto-dismiss timers."),
5204
+ reset: z15.boolean().optional().describe("Restore the real clock."),
4894
5205
  ...sessionIdShape6
4895
5206
  },
4896
5207
  outputSchema: {
4897
- ok: z14.boolean(),
4898
- elapsed: z14.number().optional()
5208
+ ok: z15.boolean().optional(),
5209
+ elapsed: z15.number().optional()
4899
5210
  },
4900
5211
  handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.CLOCK, {
4901
5212
  freeze: args["freeze"],
@@ -4907,18 +5218,18 @@ var TOOLS = [
4907
5218
  name: IrisTool.STATE,
4908
5219
  description: "Read live framework state without the app pre-broadcasting it. PREFERRED/RELIABLE: `store` reads a registered store (e.g. 'workspace'); omit `store` to read all stores. To avoid paying for a huge store, scope the read: `path` extracts a dot-path sub-tree (e.g. 'captionCache.v3', with numeric array indices), and `depth` collapses anything deeper than N levels to a size marker. A wrong `path` returns { found:false, availableKeys } so it is diagnosable. `ref` attempts a best-effort read of the nearest React component's hook state and is BOUNDED \u2014 on failure it returns component: { ok: false, reason: 'component-state-unavailable' }. Without path/depth: returns { stores, storeNames, component? }.",
4909
5220
  inputSchema: {
4910
- ref: z14.string().optional().describe("Element ref \u2014 attempts a best-effort read of the nearest React component's hook state."),
4911
- store: z14.string().optional().describe("Registered store name (e.g. 'workspace'). Omit to read all stores."),
4912
- path: z14.string().optional().describe("Dot-path into the store (e.g. 'captionCache.v3'). Numeric array indices are supported."),
4913
- depth: z14.number().optional().describe("Collapse anything deeper than N levels to a size marker \u2014 avoids huge outputs for large stores."),
5221
+ ref: z15.string().optional().describe("Element ref \u2014 attempts a best-effort read of the nearest React component's hook state."),
5222
+ store: z15.string().optional().describe("Registered store name (e.g. 'workspace'). Omit to read all stores."),
5223
+ path: z15.string().optional().describe("Dot-path into the store (e.g. 'captionCache.v3'). Numeric array indices are supported."),
5224
+ depth: z15.number().optional().describe("Collapse anything deeper than N levels to a size marker \u2014 avoids huge outputs for large stores."),
4914
5225
  ...sessionIdShape6
4915
5226
  },
4916
5227
  outputSchema: {
4917
- stores: z14.record(z14.unknown()).optional(),
4918
- storeNames: z14.array(z14.string()).optional(),
4919
- found: z14.boolean().optional(),
4920
- value: z14.unknown().optional(),
4921
- component: z14.object({ ok: z14.boolean(), reason: z14.string().optional(), state: z14.unknown().optional() }).optional()
5228
+ stores: z15.record(z15.unknown()).optional(),
5229
+ storeNames: z15.array(z15.string()).optional(),
5230
+ found: z15.boolean().optional(),
5231
+ value: z15.unknown().optional(),
5232
+ component: z15.object({ ok: z15.boolean(), reason: z15.string().optional(), state: z15.unknown().optional() }).optional()
4922
5233
  },
4923
5234
  handler: async (deps, args) => {
4924
5235
  const store = asString4(args["store"]);
@@ -4947,13 +5258,13 @@ var TOOLS = [
4947
5258
  name: IrisTool.EXPLORE,
4948
5259
  description: "Autonomous-exploration helper: list interactive elements (with refs) + current console-error count, so the agent can drive the app and report anomalies.",
4949
5260
  inputSchema: {
4950
- scope: z14.string().optional().describe("CSS selector or element ref to restrict the interactive element list to a subtree."),
5261
+ scope: z15.string().optional().describe("CSS selector or element ref to restrict the interactive element list to a subtree."),
4951
5262
  ...sessionIdShape6
4952
5263
  },
4953
5264
  outputSchema: {
4954
- interactive: z14.array(z14.unknown()),
4955
- consoleErrors: z14.number(),
4956
- hint: z14.string()
5265
+ interactive: z15.array(z15.unknown()),
5266
+ consoleErrors: z15.number(),
5267
+ hint: z15.string()
4957
5268
  },
4958
5269
  handler: async (deps, args) => {
4959
5270
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4991,7 +5302,9 @@ var TOOLS = [
4991
5302
  // Live-control: iris_end_session / iris_resume / iris_messages. See live-control-tools.ts.
4992
5303
  ...LIVE_CONTROL_TOOLS,
4993
5304
  // iris_navigate / iris_refresh — browser navigation tools. See browser-tools.ts.
4994
- ...BROWSER_TOOLS
5305
+ ...BROWSER_TOOLS,
5306
+ // iris_version_info / iris_apply_update / iris_rollback — update lifecycle tools.
5307
+ ...UPDATE_TOOLS
4995
5308
  ];
4996
5309
 
4997
5310
  // ../server/dist/tools/profiles.js
@@ -5137,7 +5450,7 @@ async function runTool(tool, deps, args) {
5137
5450
  }
5138
5451
 
5139
5452
  // ../server/dist/mcp.js
5140
- var SERVER_INFO = { name: "iris", version: "0.3.10" };
5453
+ var SERVER_INFO = { name: "iris", version: SERVER_VERSION };
5141
5454
  var ENCODING_ENV = "IRIS_ENCODING";
5142
5455
  var TOON_VALUE = "toon";
5143
5456
  function encodeResult(result, useToon) {
@@ -5251,6 +5564,13 @@ function createToolInvoker(deps) {
5251
5564
  };
5252
5565
  }
5253
5566
 
5567
+ // ../server/dist/daemon.js
5568
+ import { join as join4 } from "path";
5569
+ import { homedir as homedir2 } from "os";
5570
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, unlinkSync, openSync } from "fs";
5571
+ import { spawn } from "child_process";
5572
+ var IRIS_HOME2 = join4(homedir2(), ".iris");
5573
+
5254
5574
  // ../server/dist/index.js
5255
5575
  async function start(options = {}) {
5256
5576
  const port = options.port ?? IRIS_DEFAULT_PORT;
@@ -5283,11 +5603,11 @@ async function start(options = {}) {
5283
5603
  }
5284
5604
  }
5285
5605
  if (options.mcp !== false) {
5286
- const fs = createNodeFileSystem();
5287
- const irisRoot = options.irisRoot ?? join2(process.cwd(), IrisDir.ROOT);
5606
+ const fs2 = createNodeFileSystem();
5607
+ const irisRoot = options.irisRoot ?? join5(process.cwd(), IrisDir.ROOT);
5288
5608
  const now = options.now ?? (() => Date.now());
5289
- const flows = new FlowStore(fs, irisRoot, { now });
5290
- const project = new ProjectStore(fs, irisRoot, { now });
5609
+ const flows = new FlowStore(fs2, irisRoot, { now });
5610
+ const project = new ProjectStore(fs2, irisRoot, { now });
5291
5611
  const annotations = new AnnotationStore();
5292
5612
  const deps = {
5293
5613
  sessions: bridge.sessions,
@@ -5296,7 +5616,7 @@ async function start(options = {}) {
5296
5616
  annotations,
5297
5617
  flows,
5298
5618
  project,
5299
- fs,
5619
+ fs: fs2,
5300
5620
  irisRoot,
5301
5621
  now
5302
5622
  };
@@ -5321,17 +5641,17 @@ async function start(options = {}) {
5321
5641
 
5322
5642
  // ../test/dist/boot.js
5323
5643
  function defaultBuildDeps(server, opts) {
5324
- const fs = createNodeFileSystem();
5325
- const irisRoot = opts.irisRoot ?? join3(process.cwd(), IrisDir.ROOT);
5644
+ const fs2 = createNodeFileSystem();
5645
+ const irisRoot = opts.irisRoot ?? join6(process.cwd(), IrisDir.ROOT);
5326
5646
  const now = opts.now ?? (() => Date.now());
5327
5647
  const base = {
5328
5648
  sessions: server.bridge.sessions,
5329
5649
  baselines: new BaselineStore(),
5330
5650
  recordings: new RecordingStore(),
5331
- flows: new FlowStore(fs, irisRoot, { now }),
5332
- project: new ProjectStore(fs, irisRoot, { now }),
5651
+ flows: new FlowStore(fs2, irisRoot, { now }),
5652
+ project: new ProjectStore(fs2, irisRoot, { now }),
5333
5653
  annotations: new AnnotationStore(),
5334
- fs,
5654
+ fs: fs2,
5335
5655
  irisRoot,
5336
5656
  now
5337
5657
  };