@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/cli.js CHANGED
@@ -48,6 +48,7 @@ var CRAWL_DEFAULTS = {
48
48
  /** HTTP status at/above which a response counts as a failed request. */
49
49
  FAILED_STATUS: 400
50
50
  };
51
+ var UpdateCheckIntervalMs = 24 * 60 * 60 * 1e3;
51
52
  var CONTRACT_FILE_VERSION = 1;
52
53
  var FROM_DISK_ARG = "fromDisk";
53
54
  var ContractReadError = {
@@ -651,10 +652,97 @@ var AnnotationSchema = z2.discriminatedUnion("kind", [
651
652
  ]);
652
653
 
653
654
  // ../server/dist/index.js
654
- import { join as join2 } from "path";
655
+ import { join as join5 } from "path";
655
656
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
656
657
 
658
+ // ../server/dist/http-server.js
659
+ import * as http from "http";
660
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
661
+
662
+ // ../server/dist/log.js
663
+ function log(event, fields = {}) {
664
+ const line = JSON.stringify({ event, ...fields });
665
+ process.stderr.write(`${line}
666
+ `);
667
+ }
668
+
669
+ // ../server/dist/http-server.js
670
+ var MCP_SSE_PATH = "/mcp/sse";
671
+ var MCP_MESSAGE_PATH = "/mcp/message";
672
+ function createSharedServer() {
673
+ let mcpFactory;
674
+ const transports = /* @__PURE__ */ new Map();
675
+ const httpServer = http.createServer((req, res) => {
676
+ const url = req.url ?? "/";
677
+ if (req.method === "GET" && url === MCP_SSE_PATH) {
678
+ if (mcpFactory === void 0) {
679
+ res.writeHead(503, { "Content-Type": "text/plain" });
680
+ res.end("MCP server not ready");
681
+ return;
682
+ }
683
+ const mcpServer = mcpFactory();
684
+ const transport = new SSEServerTransport(MCP_MESSAGE_PATH, res);
685
+ const sid = transport.sessionId;
686
+ transports.set(sid, transport);
687
+ res.on("close", () => {
688
+ transports.delete(sid);
689
+ transport.close().catch(() => void 0);
690
+ mcpServer.close().catch(() => void 0);
691
+ log("mcp_client_disconnected", { sessionId: sid });
692
+ });
693
+ mcpServer.connect(transport).then(() => {
694
+ log("mcp_client_connected", { sessionId: sid });
695
+ }).catch((err) => {
696
+ const message = err instanceof Error ? err.message : String(err);
697
+ log("mcp_connect_error", { error: message });
698
+ });
699
+ return;
700
+ }
701
+ if (req.method === "POST" && url.startsWith(MCP_MESSAGE_PATH)) {
702
+ const parsed = new URL(url, "http://localhost");
703
+ const sessionId = parsed.searchParams.get("sessionId");
704
+ if (sessionId === null) {
705
+ res.writeHead(400, { "Content-Type": "text/plain" });
706
+ res.end("missing sessionId");
707
+ return;
708
+ }
709
+ const transport = transports.get(sessionId);
710
+ if (transport === void 0) {
711
+ res.writeHead(404, { "Content-Type": "text/plain" });
712
+ res.end("session not found");
713
+ return;
714
+ }
715
+ transport.handlePostMessage(req, res).catch((err) => {
716
+ const message = err instanceof Error ? err.message : String(err);
717
+ log("mcp_message_error", { error: message });
718
+ });
719
+ return;
720
+ }
721
+ res.writeHead(404, { "Content-Type": "text/plain" });
722
+ res.end("not found");
723
+ });
724
+ function attachMcp(factory) {
725
+ mcpFactory = factory;
726
+ }
727
+ async function close() {
728
+ for (const transport of transports.values()) {
729
+ await transport.close();
730
+ }
731
+ transports.clear();
732
+ await new Promise((resolve, reject) => {
733
+ httpServer.close((err) => {
734
+ if (err !== void 0 && err !== null)
735
+ reject(err);
736
+ else
737
+ resolve();
738
+ });
739
+ });
740
+ }
741
+ return { httpServer, attachMcp, close };
742
+ }
743
+
657
744
  // ../server/dist/bridge.js
745
+ import * as http2 from "http";
658
746
  import { WebSocketServer } from "ws";
659
747
 
660
748
  // ../server/dist/events/ring-buffer.js
@@ -1076,7 +1164,16 @@ var SessionManager = class {
1076
1164
  }
1077
1165
  /**
1078
1166
  * Resolve the target session. With an explicit id, returns it. With none and exactly
1079
- * one connected, returns that. Otherwise throws a clear, agent-readable error.
1167
+ * one connected, returns that.
1168
+ *
1169
+ * With none and multiple connected, applies smart auto-selection:
1170
+ * 1. Prefer non-throttled sessions (not hidden + recently heard from).
1171
+ * 2. Within each tier, prefer lowest lastSeenMs (most recently active SDK heartbeat).
1172
+ * 3. If two or more non-throttled sessions are within 1 s of each other, throw —
1173
+ * genuinely ambiguous, agent must specify sessionId.
1174
+ * 4. If ALL sessions are throttled (e.g. user is working in their editor on another
1175
+ * desktop), skip the gap check and pick the freshest heartbeat. This lets the agent
1176
+ * keep working in the background without requiring sessionId every time.
1080
1177
  */
1081
1178
  resolve(sessionId) {
1082
1179
  if (sessionId !== void 0) {
@@ -1090,25 +1187,33 @@ var SessionManager = class {
1090
1187
  if (this.#sessions.size === 0) {
1091
1188
  throw new Error("no browser session connected \u2014 is your app running with @syrin/iris-browser enabled?");
1092
1189
  }
1093
- if (this.#sessions.size > 1) {
1094
- const ids = [...this.#sessions.keys()].join(", ");
1095
- throw new Error(`multiple sessions connected (${ids}); pass sessionId to target one`);
1190
+ const all = [...this.#sessions.values()];
1191
+ if (all.length === 1) {
1192
+ const [only] = all;
1193
+ if (only === void 0)
1194
+ throw new Error("session lookup failed");
1195
+ only.markAgentActivity();
1196
+ return only;
1096
1197
  }
1097
- const [only] = this.#sessions.values();
1098
- if (only === void 0)
1198
+ const scored = all.map((s) => ({ s, score: s.throttled() ? 1 : 0, ms: s.lastSeenMs() }));
1199
+ const bestScore = Math.min(...scored.map((x) => x.score));
1200
+ const candidates = scored.filter((x) => x.score === bestScore);
1201
+ candidates.sort((a, b) => a.ms - b.ms);
1202
+ const [best, runnerUp] = candidates;
1203
+ if (best === void 0)
1099
1204
  throw new Error("session lookup failed");
1100
- only.markAgentActivity();
1101
- return only;
1205
+ const allThrottled = bestScore === 1;
1206
+ const RECENCY_GAP_MS = allThrottled ? 0 : 1e3;
1207
+ const clearWinner = runnerUp === void 0 || best.ms + RECENCY_GAP_MS < runnerUp.ms;
1208
+ if (!clearWinner) {
1209
+ const detail = all.map((s) => `${s.id} (${s.throttled() ? "throttled" : "active"}, lastSeenMs=${s.lastSeenMs()})`).join(", ");
1210
+ throw new Error(`multiple sessions connected \u2014 pass sessionId to target one: ${detail}`);
1211
+ }
1212
+ best.s.markAgentActivity();
1213
+ return best.s;
1102
1214
  }
1103
1215
  };
1104
1216
 
1105
- // ../server/dist/log.js
1106
- function log(event, fields = {}) {
1107
- const line = JSON.stringify({ event, ...fields });
1108
- process.stderr.write(`${line}
1109
- `);
1110
- }
1111
-
1112
1217
  // ../server/dist/bridge.js
1113
1218
  function rawToString(raw) {
1114
1219
  if (typeof raw === "string")
@@ -1127,16 +1232,30 @@ var Bridge = class {
1127
1232
  #clock;
1128
1233
  constructor(options) {
1129
1234
  this.#clock = options.clock ?? (() => Date.now());
1130
- this.#wss = new WebSocketServer({
1131
- port: options.port,
1132
- host: options.host ?? "127.0.0.1",
1133
- path: IRIS_WS_PATH
1134
- });
1135
- this.ready = new Promise((resolve) => {
1136
- this.#wss.on("listening", () => {
1137
- resolve(this.#wss.address().port);
1235
+ if (options.server !== void 0) {
1236
+ const srv = options.server;
1237
+ this.#wss = new WebSocketServer({ server: srv, path: IRIS_WS_PATH });
1238
+ this.ready = new Promise((resolve) => {
1239
+ if (srv.listening) {
1240
+ resolve(srv.address().port);
1241
+ } else {
1242
+ srv.once("listening", () => {
1243
+ resolve(srv.address().port);
1244
+ });
1245
+ }
1138
1246
  });
1139
- });
1247
+ } else {
1248
+ this.#wss = new WebSocketServer({
1249
+ port: options.port,
1250
+ host: options.host ?? "127.0.0.1",
1251
+ path: IRIS_WS_PATH
1252
+ });
1253
+ this.ready = new Promise((resolve) => {
1254
+ this.#wss.on("listening", () => {
1255
+ resolve(this.#wss.address().port);
1256
+ });
1257
+ });
1258
+ }
1140
1259
  this.#wss.on("connection", (socket) => {
1141
1260
  this.#onConnection(socket);
1142
1261
  });
@@ -1335,7 +1454,13 @@ var IrisTool = {
1335
1454
  /** Navigate the connected browser tab to a URL. */
1336
1455
  NAVIGATE: "iris_navigate",
1337
1456
  /** Reload the connected browser tab (soft or hard). */
1338
- REFRESH: "iris_refresh"
1457
+ REFRESH: "iris_refresh",
1458
+ /** Report running version, latest available, changelog, and breaking changes. */
1459
+ VERSION_INFO: "iris_version_info",
1460
+ /** Install the latest server version and restart (Claude Code reconnects automatically). */
1461
+ APPLY_UPDATE: "iris_apply_update",
1462
+ /** Restore the previous server version and restart. */
1463
+ ROLLBACK: "iris_rollback"
1339
1464
  };
1340
1465
 
1341
1466
  // ../server/dist/project/iris-dir.js
@@ -1362,11 +1487,11 @@ function flowPath(root, name) {
1362
1487
  function isValidFlowName(name) {
1363
1488
  return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
1364
1489
  }
1365
- async function ensureIrisDir(fs, root) {
1490
+ async function ensureIrisDir(fs2, root) {
1366
1491
  const p = irisDirPaths(root);
1367
- await fs.mkdir(p.root);
1368
- await fs.mkdir(p.flows);
1369
- await fs.mkdir(p.baselines);
1492
+ await fs2.mkdir(p.root);
1493
+ await fs2.mkdir(p.flows);
1494
+ await fs2.mkdir(p.baselines);
1370
1495
  }
1371
1496
  var JSON_INDENT = 2;
1372
1497
  function stableSerialize(capabilities, generatedAt) {
@@ -1383,21 +1508,21 @@ function stableSerialize(capabilities, generatedAt) {
1383
1508
  return `${JSON.stringify(envelope, null, JSON_INDENT)}
1384
1509
  `;
1385
1510
  }
1386
- async function writeContract(fs, root, capabilities, now) {
1387
- await ensureIrisDir(fs, root);
1388
- await fs.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
1511
+ async function writeContract(fs2, root, capabilities, now) {
1512
+ await ensureIrisDir(fs2, root);
1513
+ await fs2.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
1389
1514
  }
1390
- async function readContract(fs, root) {
1515
+ async function readContract(fs2, root) {
1391
1516
  const path = irisDirPaths(root).contract;
1392
- if (!await fs.exists(path))
1517
+ if (!await fs2.exists(path))
1393
1518
  return { ok: false, reason: ContractReadError.MISSING };
1394
1519
  let text;
1395
1520
  try {
1396
- text = await fs.readFile(path);
1521
+ text = await fs2.readFile(path);
1397
1522
  } catch (error) {
1398
1523
  return {
1399
1524
  ok: false,
1400
- reason: fs.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
1525
+ reason: fs2.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
1401
1526
  };
1402
1527
  }
1403
1528
  let parsed;
@@ -1484,8 +1609,8 @@ var FlowStore = class {
1484
1609
  #fs;
1485
1610
  #root;
1486
1611
  #clock;
1487
- constructor(fs, root, clock) {
1488
- this.#fs = fs;
1612
+ constructor(fs2, root, clock) {
1613
+ this.#fs = fs2;
1489
1614
  this.#root = root;
1490
1615
  this.#clock = clock;
1491
1616
  }
@@ -1631,8 +1756,8 @@ var ProjectStore = class {
1631
1756
  #fs;
1632
1757
  #root;
1633
1758
  #clock;
1634
- constructor(fs, root, clock) {
1635
- this.#fs = fs;
1759
+ constructor(fs2, root, clock) {
1760
+ this.#fs = fs2;
1636
1761
  this.#root = root;
1637
1762
  this.#clock = clock;
1638
1763
  }
@@ -1811,10 +1936,10 @@ function createNodeFileSystem() {
1811
1936
 
1812
1937
  // ../server/dist/mcp.js
1813
1938
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1814
- import { z as z15 } from "zod";
1939
+ import { z as z16 } from "zod";
1815
1940
 
1816
1941
  // ../server/dist/tools/tools.js
1817
- import { z as z14 } from "zod";
1942
+ import { z as z15 } from "zod";
1818
1943
 
1819
1944
  // ../server/dist/input/real-input.js
1820
1945
  var DriveError = class extends Error {
@@ -3423,8 +3548,8 @@ async function diffPng(baselineBytes, currentBytes, opts = {}) {
3423
3548
  var VisualStore = class {
3424
3549
  #fs;
3425
3550
  #root;
3426
- constructor(fs, root) {
3427
- this.#fs = fs;
3551
+ constructor(fs2, root) {
3552
+ this.#fs = fs2;
3428
3553
  this.#root = root;
3429
3554
  }
3430
3555
  /** The absolute baseline path for `name` (for echoing back to the agent). */
@@ -4050,6 +4175,266 @@ function withControl(session, result) {
4050
4175
  return control === void 0 ? result : { ...result, control };
4051
4176
  }
4052
4177
 
4178
+ // ../server/dist/update/update-tools.js
4179
+ import { z as z14 } from "zod";
4180
+
4181
+ // ../server/dist/update/update-checker.js
4182
+ import * as fs from "fs";
4183
+ import * as https from "https";
4184
+ import { join as join2 } from "path";
4185
+ import { homedir } from "os";
4186
+ var IRIS_HOME = join2(homedir(), ".iris");
4187
+ var MANIFEST_PATH = join2(IRIS_HOME, "update-manifest.json");
4188
+ var NPM_REGISTRY = "https://registry.npmjs.org/@syrin/iris/latest";
4189
+ function loadManifest() {
4190
+ if (!fs.existsSync(MANIFEST_PATH))
4191
+ return null;
4192
+ try {
4193
+ const raw = fs.readFileSync(MANIFEST_PATH, "utf8");
4194
+ return JSON.parse(raw);
4195
+ } catch {
4196
+ return null;
4197
+ }
4198
+ }
4199
+ function saveManifest(manifest) {
4200
+ fs.mkdirSync(IRIS_HOME, { recursive: true });
4201
+ fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2), "utf8");
4202
+ }
4203
+ function isCacheFresh(manifest) {
4204
+ const checked = new Date(manifest.lastChecked).getTime();
4205
+ return Date.now() - checked < UpdateCheckIntervalMs;
4206
+ }
4207
+ function fetchNpmInfo() {
4208
+ return new Promise((resolve, reject) => {
4209
+ const req = https.get(NPM_REGISTRY, (res) => {
4210
+ let body = "";
4211
+ res.setEncoding("utf8");
4212
+ res.on("data", (chunk) => {
4213
+ body += chunk;
4214
+ });
4215
+ res.on("end", () => {
4216
+ try {
4217
+ resolve(JSON.parse(body));
4218
+ } catch (err) {
4219
+ reject(err instanceof Error ? err : new Error(String(err)));
4220
+ }
4221
+ });
4222
+ res.on("error", reject);
4223
+ });
4224
+ req.setTimeout(5e3, () => {
4225
+ req.destroy();
4226
+ reject(new Error("npm registry request timed out"));
4227
+ });
4228
+ req.on("error", reject);
4229
+ });
4230
+ }
4231
+ async function checkForUpdate(currentVersion) {
4232
+ const cached = loadManifest();
4233
+ if (cached !== null && cached.currentVersion === currentVersion && isCacheFresh(cached)) {
4234
+ return cached;
4235
+ }
4236
+ try {
4237
+ const info = await fetchNpmInfo();
4238
+ const updateAvailable = info.version !== currentVersion;
4239
+ const manifest = {
4240
+ currentVersion,
4241
+ latestVersion: info.version,
4242
+ updateAvailable,
4243
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString(),
4244
+ ...info.iris?.changelog !== void 0 ? { changelog: info.iris.changelog } : {},
4245
+ ...info.iris?.breakingChanges !== void 0 ? { breakingChanges: info.iris.breakingChanges } : {},
4246
+ ...cached?.previousVersion !== void 0 ? { previousVersion: cached.previousVersion } : {}
4247
+ };
4248
+ saveManifest(manifest);
4249
+ return manifest;
4250
+ } catch (err) {
4251
+ log("iris_update_check_failed", {
4252
+ error: err instanceof Error ? err.message : String(err)
4253
+ });
4254
+ if (cached !== null)
4255
+ return { ...cached, currentVersion };
4256
+ return {
4257
+ currentVersion,
4258
+ updateAvailable: false,
4259
+ lastChecked: (/* @__PURE__ */ new Date()).toISOString()
4260
+ };
4261
+ }
4262
+ }
4263
+
4264
+ // ../server/dist/update/updater.js
4265
+ import { execFile } from "child_process";
4266
+ import { existsSync as existsSync2 } from "fs";
4267
+ import { platform } from "os";
4268
+ import { dirname, join as join3 } from "path";
4269
+ var NPM_BIN = platform() === "win32" ? "npm.cmd" : "npm";
4270
+ var NPM_TIMEOUT_MS = 12e4;
4271
+ var ExecutionKind = {
4272
+ /** Launched via `npx @syrin/iris` — npm re-resolves the package on restart. */
4273
+ NPX: "npx",
4274
+ /** Installed globally via `npm install -g`. */
4275
+ GLOBAL: "global",
4276
+ /** Installed as a local project dependency. */
4277
+ LOCAL: "local"
4278
+ };
4279
+ function detectExecutionKind() {
4280
+ const script = process.argv[1] ?? "";
4281
+ if (script.includes("/_npx/") || script.includes("\\_npx\\"))
4282
+ return ExecutionKind.NPX;
4283
+ if (script.includes("/node_modules/") || script.includes("\\node_modules\\")) {
4284
+ return ExecutionKind.LOCAL;
4285
+ }
4286
+ return ExecutionKind.GLOBAL;
4287
+ }
4288
+ function findLocalProjectRoot() {
4289
+ let dir = process.cwd();
4290
+ for (; ; ) {
4291
+ if (existsSync2(join3(dir, "package.json")))
4292
+ return dir;
4293
+ const parent = dirname(dir);
4294
+ if (parent === dir)
4295
+ return null;
4296
+ dir = parent;
4297
+ }
4298
+ }
4299
+ function runNpm(args, opts = {}) {
4300
+ return new Promise((resolve, reject) => {
4301
+ execFile(NPM_BIN, args, { timeout: NPM_TIMEOUT_MS, ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {} }, (err, _stdout, stderr) => {
4302
+ if (err !== null) {
4303
+ reject(new Error(`npm ${args.join(" ")} failed: ${stderr !== "" ? stderr : err.message}`));
4304
+ } else {
4305
+ resolve();
4306
+ }
4307
+ });
4308
+ });
4309
+ }
4310
+ async function installVersion(version, kind) {
4311
+ const pkg = `@syrin/iris@${version}`;
4312
+ if (kind === ExecutionKind.NPX) {
4313
+ log("iris_update_npx_strategy", {
4314
+ note: "Running via npx \u2014 exiting so Claude Code restarts and npx fetches the new version"
4315
+ });
4316
+ return;
4317
+ }
4318
+ if (kind === ExecutionKind.LOCAL) {
4319
+ const root = findLocalProjectRoot();
4320
+ if (root !== null) {
4321
+ await runNpm(["install", pkg], { cwd: root });
4322
+ return;
4323
+ }
4324
+ log("iris_update_local_no_root", { fallback: "global" });
4325
+ }
4326
+ await runNpm(["install", "-g", pkg]);
4327
+ }
4328
+ async function installVersionRollback(version, kind) {
4329
+ if (kind === ExecutionKind.NPX) {
4330
+ log("iris_rollback_npx_strategy", {
4331
+ note: "Running via npx \u2014 update your .mcp.json args to pin the version you want to restore"
4332
+ });
4333
+ return;
4334
+ }
4335
+ await installVersion(version, kind);
4336
+ }
4337
+ async function applyUpdate(targetVersion) {
4338
+ const manifest = loadManifest();
4339
+ if (manifest !== null) {
4340
+ saveManifest({ ...manifest, previousVersion: manifest.currentVersion });
4341
+ }
4342
+ const kind = detectExecutionKind();
4343
+ log("iris_update_applying", { version: targetVersion, executionKind: kind });
4344
+ await installVersion(targetVersion, kind);
4345
+ log("iris_update_applied", { version: targetVersion, executionKind: kind });
4346
+ process.exit(0);
4347
+ }
4348
+ async function rollback() {
4349
+ const manifest = loadManifest();
4350
+ if (manifest === null || manifest.previousVersion === void 0) {
4351
+ throw new Error("No previous version available for rollback");
4352
+ }
4353
+ const prev = manifest.previousVersion;
4354
+ const kind = detectExecutionKind();
4355
+ log("iris_rollback_applying", { version: prev, executionKind: kind });
4356
+ await installVersionRollback(prev, kind);
4357
+ log("iris_rollback_applied", { version: prev, executionKind: kind });
4358
+ process.exit(0);
4359
+ }
4360
+
4361
+ // ../server/dist/server-version.js
4362
+ import { createRequire } from "module";
4363
+ var _pkg = createRequire(import.meta.url)("../package.json");
4364
+ var SERVER_VERSION = _pkg.version;
4365
+
4366
+ // ../server/dist/update/update-tools.js
4367
+ var UPDATE_TOOLS = [
4368
+ {
4369
+ name: IrisTool.VERSION_INFO,
4370
+ description: "Returns the running Iris version, latest available version, release changelog, and any breaking changes. Call this at the start of a session or when unexpected tool behavior suggests a version mismatch.",
4371
+ inputSchema: {},
4372
+ outputSchema: {
4373
+ currentVersion: z14.string().describe("The Iris server version currently running."),
4374
+ latestVersion: z14.string().optional().describe("Latest published version on npm."),
4375
+ updateAvailable: z14.boolean().describe("True when a newer version is available to install."),
4376
+ 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).'),
4377
+ changelog: z14.string().optional().describe("Release notes for the latest version."),
4378
+ breakingChanges: z14.array(z14.string()).optional().describe("Breaking changes in the latest version that may affect your scripts."),
4379
+ rollbackAvailable: z14.boolean().describe("True when a previous version is stored and can be restored."),
4380
+ previousVersion: z14.string().optional().describe("The version that would be restored on rollback.")
4381
+ },
4382
+ handler: async (_deps) => {
4383
+ const manifest = await checkForUpdate(SERVER_VERSION);
4384
+ return {
4385
+ currentVersion: manifest.currentVersion,
4386
+ ...manifest.latestVersion !== void 0 ? { latestVersion: manifest.latestVersion } : {},
4387
+ updateAvailable: manifest.updateAvailable,
4388
+ executionKind: detectExecutionKind(),
4389
+ ...manifest.changelog !== void 0 ? { changelog: manifest.changelog } : {},
4390
+ ...manifest.breakingChanges !== void 0 ? { breakingChanges: manifest.breakingChanges } : {},
4391
+ rollbackAvailable: manifest.previousVersion !== void 0,
4392
+ ...manifest.previousVersion !== void 0 ? { previousVersion: manifest.previousVersion } : {}
4393
+ };
4394
+ }
4395
+ },
4396
+ {
4397
+ name: IrisTool.APPLY_UPDATE,
4398
+ description: 'Install the latest Iris server version and restart. Strategy depends on how iris was launched (check executionKind from iris_version_info): "global" and "local" installs run npm install then exit; "npx" just exits \u2014 Claude Code restarts and npx re-resolves the latest version from npm automatically. The MCP connection briefly drops during restart.',
4399
+ inputSchema: {
4400
+ confirm: z14.boolean().describe("Set to true to confirm the update should be applied. Required to prevent accidental upgrades.")
4401
+ },
4402
+ outputSchema: {
4403
+ ok: z14.boolean(),
4404
+ message: z14.string().optional()
4405
+ },
4406
+ handler: async (_deps, args) => {
4407
+ if (args["confirm"] !== true) {
4408
+ return { ok: false, message: "Set confirm:true to apply the update" };
4409
+ }
4410
+ const manifest = await checkForUpdate(SERVER_VERSION);
4411
+ if (!manifest.updateAvailable || manifest.latestVersion === void 0) {
4412
+ return { ok: false, message: "No update available \u2014 already on the latest version" };
4413
+ }
4414
+ await applyUpdate(manifest.latestVersion);
4415
+ return { ok: true };
4416
+ }
4417
+ },
4418
+ {
4419
+ name: IrisTool.ROLLBACK,
4420
+ description: "Restore the previous Iris server version and restart. Use when an update introduced a regression. The MCP connection will briefly drop \u2014 Claude Code restarts the process automatically with the restored binary.",
4421
+ inputSchema: {
4422
+ confirm: z14.boolean().describe("Set to true to confirm the rollback. Required to prevent accidental downgrades.")
4423
+ },
4424
+ outputSchema: {
4425
+ ok: z14.boolean(),
4426
+ message: z14.string().optional()
4427
+ },
4428
+ handler: async (_deps, args) => {
4429
+ if (args["confirm"] !== true) {
4430
+ return { ok: false, message: "Set confirm:true to apply the rollback" };
4431
+ }
4432
+ await rollback();
4433
+ return { ok: true };
4434
+ }
4435
+ }
4436
+ ];
4437
+
4053
4438
  // ../server/dist/tools/tools.js
4054
4439
  async function snapshotTree(deps, sessionId) {
4055
4440
  const session = deps.sessions.resolve(sessionId);
@@ -4060,7 +4445,7 @@ async function snapshotTree(deps, sessionId) {
4060
4445
  return { lines: normalizeLines(snap.tree ?? ""), route: snap.status?.route ?? "" };
4061
4446
  }
4062
4447
  var sessionIdShape6 = {
4063
- sessionId: z14.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open \u2014 Iris resolves it automatically.")
4448
+ sessionId: z15.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open \u2014 Iris resolves it automatically.")
4064
4449
  };
4065
4450
  async function commandOrThrow3(deps, sessionId, name, args) {
4066
4451
  const session = deps.sessions.resolve(sessionId);
@@ -4137,17 +4522,17 @@ var TOOLS = [
4137
4522
  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.",
4138
4523
  inputSchema: {},
4139
4524
  outputSchema: {
4140
- sessions: z14.array(z14.object({
4141
- sessionId: z14.string(),
4142
- url: z14.string(),
4143
- title: z14.string().optional(),
4144
- lastSeenMs: z14.number(),
4145
- throttled: z14.boolean(),
4146
- focused: z14.boolean(),
4147
- hidden: z14.boolean(),
4148
- realInputAvailable: z14.boolean().optional(),
4149
- stale: z14.boolean().optional(),
4150
- recommendation: z14.string().optional()
4525
+ sessions: z15.array(z15.object({
4526
+ sessionId: z15.string(),
4527
+ url: z15.string(),
4528
+ title: z15.string().optional(),
4529
+ lastSeenMs: z15.number(),
4530
+ throttled: z15.boolean(),
4531
+ focused: z15.boolean(),
4532
+ hidden: z15.boolean(),
4533
+ realInputAvailable: z15.boolean().optional(),
4534
+ stale: z15.boolean().optional(),
4535
+ recommendation: z15.string().optional()
4151
4536
  })).describe("Connected browser sessions with health state.")
4152
4537
  },
4153
4538
  handler: async (deps) => {
@@ -4163,13 +4548,13 @@ var TOOLS = [
4163
4548
  name: IrisTool.SNAPSHOT,
4164
4549
  description: "Semantic accessibility snapshot of the page or a subtree. mode: full|interactive|status. Use to see what is on screen right now.",
4165
4550
  inputSchema: {
4166
- scope: z14.string().optional().describe("CSS selector or element ref to restrict the snapshot to a subtree. Omit to snapshot the whole page."),
4167
- mode: z14.nativeEnum(SnapshotMode).optional().describe("full = all elements; interactive = only clickable/focusable elements; status = only route + title. Default: full."),
4551
+ scope: z15.string().optional().describe("CSS selector or element ref to restrict the snapshot to a subtree. Omit to snapshot the whole page."),
4552
+ mode: z15.nativeEnum(SnapshotMode).optional().describe("full = all elements; interactive = only clickable/focusable elements; status = only route + title. Default: full."),
4168
4553
  ...sessionIdShape6
4169
4554
  },
4170
4555
  outputSchema: {
4171
- tree: z14.string().optional().describe("Indented ARIA tree of every element on the page (or the scoped subtree)."),
4172
- status: z14.object({ route: z14.string(), title: z14.string().optional() }).optional()
4556
+ tree: z15.string().optional().describe("Indented ARIA tree of every element on the page (or the scoped subtree)."),
4557
+ status: z15.object({ route: z15.string(), title: z15.string().optional() }).optional()
4173
4558
  },
4174
4559
  handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.SNAPSHOT, {
4175
4560
  scope: args["scope"],
@@ -4180,25 +4565,25 @@ var TOOLS = [
4180
4565
  name: IrisTool.QUERY,
4181
4566
  description: "Find elements by Testing-Library semantics. Pass `by` (role|text|label|placeholder|testid|alt) and `value` (the query string). Returns matching refs + descriptors + visibility. On zero matches, also returns hint:{ route, presentTestids[], knownEmptyState } so you can distinguish an empty state from a missing element WITHOUT taking a snapshot.",
4182
4567
  inputSchema: {
4183
- by: z14.string().describe("Query strategy: role | text | label | placeholder | testid | alt"),
4184
- value: z14.string().describe("Query value for the selected strategy (e.g. by=role value=button, or by=testid value=submit-btn)."),
4185
- name: z14.string().optional().describe("Accessible name filter \u2014 narrows results when `by` is role and the page has many elements of that role."),
4186
- scope: z14.string().optional().describe("CSS selector or element ref to restrict the search to a subtree."),
4568
+ by: z15.string().describe("Query strategy: role | text | label | placeholder | testid | alt"),
4569
+ value: z15.string().describe("Query value for the selected strategy (e.g. by=role value=button, or by=testid value=submit-btn)."),
4570
+ name: z15.string().optional().describe("Accessible name filter \u2014 narrows results when `by` is role and the page has many elements of that role."),
4571
+ scope: z15.string().optional().describe("CSS selector or element ref to restrict the search to a subtree."),
4187
4572
  ...sessionIdShape6
4188
4573
  },
4189
4574
  outputSchema: {
4190
- elements: z14.array(z14.object({
4191
- ref: z14.string(),
4192
- role: z14.string(),
4193
- name: z14.string(),
4194
- value: z14.string().optional(),
4195
- states: z14.array(z14.string()),
4196
- visible: z14.boolean()
4575
+ elements: z15.array(z15.object({
4576
+ ref: z15.string(),
4577
+ role: z15.string(),
4578
+ name: z15.string(),
4579
+ value: z15.string().optional(),
4580
+ states: z15.array(z15.string()),
4581
+ visible: z15.boolean()
4197
4582
  })),
4198
- hint: z14.object({
4199
- route: z14.string(),
4200
- presentTestids: z14.array(z14.string()),
4201
- knownEmptyState: z14.boolean()
4583
+ hint: z15.object({
4584
+ route: z15.string(),
4585
+ presentTestids: z15.array(z15.string()),
4586
+ knownEmptyState: z15.boolean()
4202
4587
  }).optional().describe("Present only on zero matches \u2014 tells you what IS on the page so you can diagnose the miss.")
4203
4588
  },
4204
4589
  handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.QUERY, {
@@ -4212,18 +4597,18 @@ var TOOLS = [
4212
4597
  name: IrisTool.INSPECT,
4213
4598
  description: "Deep info on one element by ref: full a11y props, visibility, box, and (with @syrin/iris-react) component stack + source file.",
4214
4599
  inputSchema: {
4215
- ref: z14.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
4600
+ ref: z15.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
4216
4601
  ...sessionIdShape6
4217
4602
  },
4218
4603
  outputSchema: {
4219
- ref: z14.string(),
4220
- role: z14.string(),
4221
- name: z14.string(),
4222
- value: z14.string().optional(),
4223
- states: z14.array(z14.string()),
4224
- visible: z14.boolean(),
4225
- box: z14.object({ x: z14.number(), y: z14.number(), width: z14.number(), height: z14.number() }).optional(),
4226
- component: z14.object({ name: z14.string(), sourceFile: z14.string().optional() }).optional()
4604
+ ref: z15.string(),
4605
+ role: z15.string(),
4606
+ name: z15.string(),
4607
+ value: z15.string().optional(),
4608
+ states: z15.array(z15.string()),
4609
+ visible: z15.boolean(),
4610
+ box: z15.object({ x: z15.number(), y: z15.number(), width: z15.number(), height: z15.number() }).optional(),
4611
+ component: z15.object({ name: z15.string().optional(), sourceFile: z15.string().optional() }).optional()
4227
4612
  },
4228
4613
  handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.INSPECT, {
4229
4614
  ref: args["ref"]
@@ -4233,19 +4618,19 @@ var TOOLS = [
4233
4618
  name: IrisTool.ACT,
4234
4619
  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.',
4235
4620
  inputSchema: {
4236
- ref: z14.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
4237
- action: z14.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
4238
- 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."),
4239
- 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)."),
4621
+ ref: z15.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
4622
+ action: z15.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
4623
+ 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."),
4624
+ 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)."),
4240
4625
  ...sessionIdShape6
4241
4626
  },
4242
4627
  outputSchema: {
4243
- since: z14.number().describe("Cursor \u2014 pass to iris_observe/iris_wait_for/iris_assert to scope reaction queries to this act."),
4244
- dispatched: z14.boolean(),
4245
- settled: z14.boolean().nullable(),
4246
- inputMode: z14.string(),
4247
- result: z14.unknown().optional(),
4248
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4628
+ since: z15.number().describe("Cursor \u2014 pass to iris_observe/iris_wait_for/iris_assert to scope reaction queries to this act."),
4629
+ dispatched: z15.boolean(),
4630
+ settled: z15.boolean().nullable(),
4631
+ inputMode: z15.string(),
4632
+ result: z15.unknown().optional(),
4633
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4249
4634
  },
4250
4635
  handler: async (deps, args) => {
4251
4636
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4301,14 +4686,14 @@ var TOOLS = [
4301
4686
  name: IrisTool.ACT_SEQUENCE,
4302
4687
  description: "Run multiple actions in order (fill -> fill -> submit) in one round-trip. Returns per-step effects[] (see iris_act).",
4303
4688
  inputSchema: {
4304
- steps: z14.array(z14.record(z14.unknown())).describe("Ordered list of { ref, action, args? } objects. Each step is equivalent to one iris_act call."),
4689
+ steps: z15.array(z15.record(z15.unknown())).describe("Ordered list of { ref, action, args? } objects. Each step is equivalent to one iris_act call."),
4305
4690
  ...sessionIdShape6
4306
4691
  },
4307
4692
  outputSchema: {
4308
- since: z14.number(),
4309
- dispatched: z14.boolean(),
4310
- result: z14.unknown().optional(),
4311
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4693
+ since: z15.number(),
4694
+ dispatched: z15.boolean(),
4695
+ result: z15.unknown().optional(),
4696
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4312
4697
  },
4313
4698
  handler: async (deps, args) => {
4314
4699
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4336,23 +4721,23 @@ var TOOLS = [
4336
4721
  name: IrisTool.ACT_AND_WAIT,
4337
4722
  description: "Act on a ref, then wait for a predicate to hold \u2014 one hop for the act->observe->assert loop. Returns { effect } (the action result), { verdict } (predicate pass/evidence/near-miss), and { trace } (the reaction report of everything the app did after the action). timeout_ms 0 evaluates the predicate once without waiting.",
4338
4723
  inputSchema: {
4339
- ref: z14.string().describe("Element ref from iris_snapshot or iris_query."),
4340
- action: z14.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
4341
- args: z14.record(z14.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press."),
4724
+ ref: z15.string().describe("Element ref from iris_snapshot or iris_query."),
4725
+ action: z15.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
4726
+ args: z15.record(z15.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press."),
4342
4727
  until: PredicateSchema.describe("Predicate to wait for after the action completes. Same shape accepted by iris_assert."),
4343
- timeout_ms: z14.number().optional().describe("Maximum wait time in milliseconds. 0 = evaluate once without waiting. Default: 4000."),
4344
- refuseWhenThrottled: z14.boolean().optional().describe("Throw if the tab is throttled. Default: false."),
4728
+ timeout_ms: z15.number().optional().describe("Maximum wait time in milliseconds. 0 = evaluate once without waiting. Default: 4000."),
4729
+ refuseWhenThrottled: z15.boolean().optional().describe("Throw if the tab is throttled. Default: false."),
4345
4730
  ...sessionIdShape6
4346
4731
  },
4347
4732
  outputSchema: {
4348
- effect: z14.unknown().describe("The iris_act result (dispatched, settled, inputMode, etc.)."),
4349
- verdict: z14.object({
4350
- pass: z14.boolean(),
4351
- evidence: z14.unknown().optional(),
4352
- failureReason: z14.string().optional()
4733
+ effect: z15.unknown().describe("The iris_act result (dispatched, settled, inputMode, etc.)."),
4734
+ verdict: z15.object({
4735
+ pass: z15.boolean(),
4736
+ evidence: z15.unknown().optional(),
4737
+ failureReason: z15.string().optional()
4353
4738
  }),
4354
- trace: z14.unknown().describe("Reaction report (same shape as iris_observe summary)."),
4355
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4739
+ trace: z15.unknown().describe("Reaction report (same shape as iris_observe summary)."),
4740
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4356
4741
  },
4357
4742
  handler: async (deps, args) => {
4358
4743
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4385,31 +4770,31 @@ var TOOLS = [
4385
4770
  name: IrisTool.OBSERVE,
4386
4771
  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.",
4387
4772
  inputSchema: {
4388
- window_ms: z14.number().optional().describe("Time window to look back. Default: 2000ms. Ignored when `since` is provided."),
4389
- since: z14.number().optional().describe("Cursor from a prior iris_act or iris_observe call. Scopes the event window to exactly that span."),
4390
- filters: z14.array(z14.string()).optional().describe("Event type allowlist: dom | net | route | console | animation | signal. Omit to return all types."),
4391
- max_events: z14.number().optional().describe("Cap the timeline to the most recent N events. Older events are counted in cost.droppedOldest."),
4773
+ window_ms: z15.number().optional().describe("Time window to look back. Default: 2000ms. Ignored when `since` is provided."),
4774
+ since: z15.number().optional().describe("Cursor from a prior iris_act or iris_observe call. Scopes the event window to exactly that span."),
4775
+ filters: z15.array(z15.string()).optional().describe("Event type allowlist: dom | net | route | console | animation | signal. Omit to return all types."),
4776
+ max_events: z15.number().optional().describe("Cap the timeline to the most recent N events. Older events are counted in cost.droppedOldest."),
4392
4777
  ...sessionIdShape6
4393
4778
  },
4394
4779
  outputSchema: {
4395
- events: z14.array(z14.unknown()),
4396
- summary: z14.object({
4397
- total: z14.number(),
4398
- network: z14.number(),
4399
- domAdded: z14.number(),
4400
- domRemoved: z14.number(),
4401
- domChanged: z14.number(),
4402
- routeChanges: z14.number(),
4403
- consoleErrors: z14.number(),
4404
- animations: z14.number(),
4405
- signals: z14.number()
4780
+ events: z15.array(z15.unknown()),
4781
+ summary: z15.object({
4782
+ total: z15.number(),
4783
+ network: z15.number(),
4784
+ domAdded: z15.number(),
4785
+ domRemoved: z15.number(),
4786
+ domChanged: z15.number(),
4787
+ routeChanges: z15.number(),
4788
+ consoleErrors: z15.number(),
4789
+ animations: z15.number(),
4790
+ signals: z15.number()
4406
4791
  }),
4407
- cost: z14.object({
4408
- events: z14.number(),
4409
- bytes: z14.number(),
4410
- droppedOldest: z14.number().optional()
4792
+ cost: z15.object({
4793
+ events: z15.number(),
4794
+ bytes: z15.number(),
4795
+ droppedOldest: z15.number().optional()
4411
4796
  }),
4412
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4797
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4413
4798
  },
4414
4799
  handler: (deps, args) => {
4415
4800
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4432,15 +4817,15 @@ var TOOLS = [
4432
4817
  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.",
4433
4818
  inputSchema: {
4434
4819
  predicate: PredicateSchema.describe("Predicate to wait for: { signal }, { net }, { element } or a combination."),
4435
- timeout_ms: z14.number().optional().describe("Maximum wait in milliseconds. Default: 4000."),
4436
- since: z14.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the wait to events after that act."),
4820
+ timeout_ms: z15.number().optional().describe("Maximum wait in milliseconds. Default: 4000."),
4821
+ since: z15.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the wait to events after that act."),
4437
4822
  ...sessionIdShape6
4438
4823
  },
4439
4824
  outputSchema: {
4440
- pass: z14.boolean(),
4441
- evidence: z14.unknown().optional(),
4442
- failureReason: z14.string().optional(),
4443
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4825
+ pass: z15.boolean(),
4826
+ evidence: z15.unknown().optional(),
4827
+ failureReason: z15.string().optional(),
4828
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4444
4829
  },
4445
4830
  handler: async (deps, args) => {
4446
4831
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4455,15 +4840,15 @@ var TOOLS = [
4455
4840
  description: "Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.",
4456
4841
  inputSchema: {
4457
4842
  predicate: PredicateSchema.describe("Predicate to evaluate: { signal }, { net }, { element } or a combination."),
4458
- timeout_ms: z14.number().optional().describe("If > 0, wait up to this many milliseconds before failing. Default: 0 (evaluate once)."),
4459
- since: z14.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the assertion to events after that act."),
4843
+ timeout_ms: z15.number().optional().describe("If > 0, wait up to this many milliseconds before failing. Default: 0 (evaluate once)."),
4844
+ since: z15.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the assertion to events after that act."),
4460
4845
  ...sessionIdShape6
4461
4846
  },
4462
4847
  outputSchema: {
4463
- pass: z14.boolean(),
4464
- evidence: z14.unknown().optional(),
4465
- failureReason: z14.string().optional(),
4466
- session: z14.object({ lastSeenMs: z14.number(), throttled: z14.boolean(), focused: z14.boolean() }).optional()
4848
+ pass: z15.boolean(),
4849
+ evidence: z15.unknown().optional(),
4850
+ failureReason: z15.string().optional(),
4851
+ session: z15.object({ lastSeenMs: z15.number(), throttled: z15.boolean(), focused: z15.boolean() }).optional()
4467
4852
  },
4468
4853
  handler: async (deps, args) => {
4469
4854
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4478,15 +4863,15 @@ var TOOLS = [
4478
4863
  name: IrisTool.NETWORK,
4479
4864
  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.',
4480
4865
  inputSchema: {
4481
- since: z14.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to requests fired after that act."),
4482
- method: z14.string().optional().describe("HTTP method filter: GET | POST | PUT | DELETE | PATCH etc."),
4483
- urlContains: z14.string().optional().describe("Substring that the request URL must contain."),
4484
- status: z14.number().optional().describe("HTTP status code filter (e.g. 200, 404, 500)."),
4866
+ since: z15.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to requests fired after that act."),
4867
+ method: z15.string().optional().describe("HTTP method filter: GET | POST | PUT | DELETE | PATCH etc."),
4868
+ urlContains: z15.string().optional().describe("Substring that the request URL must contain."),
4869
+ status: z15.number().optional().describe("HTTP status code filter (e.g. 200, 404, 500)."),
4485
4870
  ...sessionIdShape6
4486
4871
  },
4487
4872
  outputSchema: {
4488
- calls: z14.array(z14.unknown()),
4489
- hint: z14.object({ totalInWindow: z14.number(), present: z14.array(z14.string()) }).optional()
4873
+ calls: z15.array(z15.unknown()),
4874
+ hint: z15.object({ totalInWindow: z15.number(), present: z15.array(z15.string()) }).optional()
4490
4875
  },
4491
4876
  handler: (deps, args) => {
4492
4877
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4506,13 +4891,13 @@ var TOOLS = [
4506
4891
  name: IrisTool.CONSOLE,
4507
4892
  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.',
4508
4893
  inputSchema: {
4509
- level: z14.string().optional().describe("Log level filter: error | warn | info | log. Omit to return all levels."),
4510
- since: z14.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to log entries after that act."),
4894
+ level: z15.string().optional().describe("Log level filter: error | warn | info | log. Omit to return all levels."),
4895
+ since: z15.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to log entries after that act."),
4511
4896
  ...sessionIdShape6
4512
4897
  },
4513
4898
  outputSchema: {
4514
- logs: z14.array(z14.unknown()),
4515
- hint: z14.object({ totalInWindow: z14.number(), byLevel: z14.record(z14.number()) }).optional()
4899
+ logs: z15.array(z15.unknown()),
4900
+ hint: z15.object({ totalInWindow: z15.number(), byLevel: z15.record(z15.number()) }).optional()
4516
4901
  },
4517
4902
  handler: (deps, args) => {
4518
4903
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4531,7 +4916,7 @@ var TOOLS = [
4531
4916
  description: "Currently running + recently completed animations with targets/timing.",
4532
4917
  inputSchema: { ...sessionIdShape6 },
4533
4918
  outputSchema: {
4534
- animations: z14.array(z14.unknown())
4919
+ animations: z15.array(z15.unknown())
4535
4920
  },
4536
4921
  handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.ANIMATIONS, {})
4537
4922
  },
@@ -4539,12 +4924,12 @@ var TOOLS = [
4539
4924
  name: IrisTool.BASELINE_SAVE,
4540
4925
  description: "Snapshot the current semantic state under a name, to diff against later (regression detection).",
4541
4926
  inputSchema: {
4542
- name: z14.string().describe('Label for this baseline snapshot (e.g. "dashboard-initial"). Use the same name in iris_diff to compare.'),
4927
+ name: z15.string().describe('Label for this baseline snapshot (e.g. "dashboard-initial"). Use the same name in iris_diff to compare.'),
4543
4928
  ...sessionIdShape6
4544
4929
  },
4545
4930
  outputSchema: {
4546
- baseline: z14.string().describe("Saved baseline name \u2014 pass to iris_diff to compare."),
4547
- lineCount: z14.number()
4931
+ baseline: z15.string().describe("Saved baseline name \u2014 pass to iris_diff to compare."),
4932
+ lineCount: z15.number()
4548
4933
  },
4549
4934
  handler: async (deps, args) => {
4550
4935
  const name = asString4(args["name"]) ?? "default";
@@ -4558,7 +4943,7 @@ var TOOLS = [
4558
4943
  description: "List saved baseline names.",
4559
4944
  inputSchema: {},
4560
4945
  outputSchema: {
4561
- baselines: z14.array(z14.string())
4946
+ baselines: z15.array(z15.string())
4562
4947
  },
4563
4948
  handler: (deps) => Promise.resolve({ baselines: deps.baselines.list() })
4564
4949
  },
@@ -4566,15 +4951,15 @@ var TOOLS = [
4566
4951
  name: IrisTool.DIFF,
4567
4952
  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?".',
4568
4953
  inputSchema: {
4569
- baseline: z14.string().describe("Baseline name to compare against. Call iris_baseline_list to get available names; names are created by iris_baseline_save."),
4954
+ baseline: z15.string().describe("Baseline name to compare against. Call iris_baseline_list to get available names; names are created by iris_baseline_save."),
4570
4955
  ...sessionIdShape6
4571
4956
  },
4572
4957
  outputSchema: {
4573
- baseline: z14.string(),
4574
- removed: z14.array(z14.string()),
4575
- added: z14.array(z14.string()),
4576
- consoleErrors: z14.number(),
4577
- routeChanged: z14.boolean()
4958
+ baseline: z15.string(),
4959
+ removed: z15.array(z15.string()),
4960
+ added: z15.array(z15.string()),
4961
+ consoleErrors: z15.number(),
4962
+ routeChanged: z15.boolean()
4578
4963
  },
4579
4964
  handler: async (deps, args) => {
4580
4965
  const name = asString4(args["baseline"]) ?? "default";
@@ -4592,12 +4977,12 @@ var TOOLS = [
4592
4977
  name: IrisTool.RECORD_START,
4593
4978
  description: "Start recording the event timeline under a name (for replay / a flow report).",
4594
4979
  inputSchema: {
4595
- recordingName: z14.string().describe("Identifier for this recording. Pass the same name to iris_record_stop and iris_replay."),
4980
+ recordingName: z15.string().describe("Identifier for this recording. Pass the same name to iris_record_stop and iris_replay."),
4596
4981
  ...sessionIdShape6
4597
4982
  },
4598
4983
  outputSchema: {
4599
- recordingName: z14.string(),
4600
- since: z14.number()
4984
+ recordingName: z15.string(),
4985
+ since: z15.number()
4601
4986
  },
4602
4987
  handler: (deps, args) => {
4603
4988
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4611,13 +4996,13 @@ var TOOLS = [
4611
4996
  name: IrisTool.RECORD_STOP,
4612
4997
  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.",
4613
4998
  inputSchema: {
4614
- recordingName: z14.string().describe("Identifier of an active recording started with iris_record_start."),
4999
+ recordingName: z15.string().describe("Identifier of an active recording started with iris_record_start."),
4615
5000
  ...sessionIdShape6
4616
5001
  },
4617
5002
  outputSchema: {
4618
- recordingName: z14.string(),
4619
- program: z14.unknown(),
4620
- warning: z14.string().optional()
5003
+ recordingName: z15.string(),
5004
+ program: z15.unknown(),
5005
+ warning: z15.string().optional()
4621
5006
  },
4622
5007
  handler: (deps, args) => {
4623
5008
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4649,17 +5034,17 @@ var TOOLS = [
4649
5034
  name: IrisTool.REPLAY,
4650
5035
  description: "Re-execute a previously recorded program by recordingName. Re-resolves each step to its element by testid (falling back to the stored ref for unstable steps) and runs the actions in order against the live session. Stops at the first failure. Returns { ok, steps:[{tool,ok,error?,note?}] }.",
4651
5036
  inputSchema: {
4652
- recordingName: z14.string().describe("Name of a compiled recording (from iris_record_stop) to re-execute."),
5037
+ recordingName: z15.string().describe("Name of a compiled recording (from iris_record_stop) to re-execute."),
4653
5038
  ...sessionIdShape6
4654
5039
  },
4655
5040
  outputSchema: {
4656
- recordingName: z14.string(),
4657
- ok: z14.boolean(),
4658
- steps: z14.array(z14.object({
4659
- tool: z14.string(),
4660
- ok: z14.boolean(),
4661
- error: z14.string().optional(),
4662
- note: z14.string().optional()
5041
+ recordingName: z15.string(),
5042
+ ok: z15.boolean(),
5043
+ steps: z15.array(z15.object({
5044
+ tool: z15.string(),
5045
+ ok: z15.boolean(),
5046
+ error: z15.string().optional(),
5047
+ note: z15.string().optional()
4663
5048
  }))
4664
5049
  },
4665
5050
  handler: async (deps, args) => {
@@ -4677,30 +5062,31 @@ var TOOLS = [
4677
5062
  name: IrisTool.NARRATE,
4678
5063
  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.",
4679
5064
  inputSchema: {
4680
- text: z14.string().describe("Short sentence describing your next action, shown on the presenter HUD for the developer watching."),
4681
- level: z14.string().optional().describe("Display severity: info | warn | error. Default: info."),
5065
+ text: z15.string().describe("Short sentence describing your next action, shown on the presenter HUD for the developer watching."),
5066
+ level: z15.string().optional().describe("Display severity: info | warn | error. Default: info."),
4682
5067
  ...sessionIdShape6
4683
5068
  },
4684
- outputSchema: {
4685
- ok: z14.boolean()
4686
- },
4687
- handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.NARRATE, {
4688
- text: args["text"],
4689
- level: args["level"]
4690
- })
5069
+ outputSchema: { ok: z15.boolean() },
5070
+ handler: async (deps, args) => {
5071
+ const result = await commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.NARRATE, {
5072
+ text: args["text"],
5073
+ level: args["level"]
5074
+ });
5075
+ return { ok: true, ...result };
5076
+ }
4691
5077
  },
4692
5078
  {
4693
5079
  name: IrisTool.CLOCK,
4694
5080
  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.",
4695
5081
  inputSchema: {
4696
- freeze: z14.boolean().optional().describe("Freeze the fake clock. Time stops advancing until advanceMs or reset."),
4697
- advanceMs: z14.number().optional().describe("Fast-forward time by this many milliseconds \u2014 triggers debounces, toasts, auto-dismiss timers."),
4698
- reset: z14.boolean().optional().describe("Restore the real clock."),
5082
+ freeze: z15.boolean().optional().describe("Freeze the fake clock. Time stops advancing until advanceMs or reset."),
5083
+ advanceMs: z15.number().optional().describe("Fast-forward time by this many milliseconds \u2014 triggers debounces, toasts, auto-dismiss timers."),
5084
+ reset: z15.boolean().optional().describe("Restore the real clock."),
4699
5085
  ...sessionIdShape6
4700
5086
  },
4701
5087
  outputSchema: {
4702
- ok: z14.boolean(),
4703
- elapsed: z14.number().optional()
5088
+ ok: z15.boolean().optional(),
5089
+ elapsed: z15.number().optional()
4704
5090
  },
4705
5091
  handler: (deps, args) => commandOrThrow3(deps, asString4(args["sessionId"]), IrisCommand.CLOCK, {
4706
5092
  freeze: args["freeze"],
@@ -4712,18 +5098,18 @@ var TOOLS = [
4712
5098
  name: IrisTool.STATE,
4713
5099
  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? }.",
4714
5100
  inputSchema: {
4715
- ref: z14.string().optional().describe("Element ref \u2014 attempts a best-effort read of the nearest React component's hook state."),
4716
- store: z14.string().optional().describe("Registered store name (e.g. 'workspace'). Omit to read all stores."),
4717
- path: z14.string().optional().describe("Dot-path into the store (e.g. 'captionCache.v3'). Numeric array indices are supported."),
4718
- depth: z14.number().optional().describe("Collapse anything deeper than N levels to a size marker \u2014 avoids huge outputs for large stores."),
5101
+ ref: z15.string().optional().describe("Element ref \u2014 attempts a best-effort read of the nearest React component's hook state."),
5102
+ store: z15.string().optional().describe("Registered store name (e.g. 'workspace'). Omit to read all stores."),
5103
+ path: z15.string().optional().describe("Dot-path into the store (e.g. 'captionCache.v3'). Numeric array indices are supported."),
5104
+ depth: z15.number().optional().describe("Collapse anything deeper than N levels to a size marker \u2014 avoids huge outputs for large stores."),
4719
5105
  ...sessionIdShape6
4720
5106
  },
4721
5107
  outputSchema: {
4722
- stores: z14.record(z14.unknown()).optional(),
4723
- storeNames: z14.array(z14.string()).optional(),
4724
- found: z14.boolean().optional(),
4725
- value: z14.unknown().optional(),
4726
- component: z14.object({ ok: z14.boolean(), reason: z14.string().optional(), state: z14.unknown().optional() }).optional()
5108
+ stores: z15.record(z15.unknown()).optional(),
5109
+ storeNames: z15.array(z15.string()).optional(),
5110
+ found: z15.boolean().optional(),
5111
+ value: z15.unknown().optional(),
5112
+ component: z15.object({ ok: z15.boolean(), reason: z15.string().optional(), state: z15.unknown().optional() }).optional()
4727
5113
  },
4728
5114
  handler: async (deps, args) => {
4729
5115
  const store = asString4(args["store"]);
@@ -4752,13 +5138,13 @@ var TOOLS = [
4752
5138
  name: IrisTool.EXPLORE,
4753
5139
  description: "Autonomous-exploration helper: list interactive elements (with refs) + current console-error count, so the agent can drive the app and report anomalies.",
4754
5140
  inputSchema: {
4755
- scope: z14.string().optional().describe("CSS selector or element ref to restrict the interactive element list to a subtree."),
5141
+ scope: z15.string().optional().describe("CSS selector or element ref to restrict the interactive element list to a subtree."),
4756
5142
  ...sessionIdShape6
4757
5143
  },
4758
5144
  outputSchema: {
4759
- interactive: z14.array(z14.unknown()),
4760
- consoleErrors: z14.number(),
4761
- hint: z14.string()
5145
+ interactive: z15.array(z15.unknown()),
5146
+ consoleErrors: z15.number(),
5147
+ hint: z15.string()
4762
5148
  },
4763
5149
  handler: async (deps, args) => {
4764
5150
  const session = deps.sessions.resolve(asString4(args["sessionId"]));
@@ -4796,7 +5182,9 @@ var TOOLS = [
4796
5182
  // Live-control: iris_end_session / iris_resume / iris_messages. See live-control-tools.ts.
4797
5183
  ...LIVE_CONTROL_TOOLS,
4798
5184
  // iris_navigate / iris_refresh — browser navigation tools. See browser-tools.ts.
4799
- ...BROWSER_TOOLS
5185
+ ...BROWSER_TOOLS,
5186
+ // iris_version_info / iris_apply_update / iris_rollback — update lifecycle tools.
5187
+ ...UPDATE_TOOLS
4800
5188
  ];
4801
5189
 
4802
5190
  // ../server/dist/tools/profiles.js
@@ -4942,7 +5330,7 @@ async function runTool(tool, deps, args) {
4942
5330
  }
4943
5331
 
4944
5332
  // ../server/dist/mcp.js
4945
- var SERVER_INFO = { name: "iris", version: "0.3.10" };
5333
+ var SERVER_INFO = { name: "iris", version: SERVER_VERSION };
4946
5334
  var ENCODING_ENV = "IRIS_ENCODING";
4947
5335
  var TOON_VALUE = "toon";
4948
5336
  function encodeResult(result, useToon) {
@@ -5043,6 +5431,55 @@ var SessionReaper = class {
5043
5431
  }
5044
5432
  };
5045
5433
 
5434
+ // ../server/dist/daemon.js
5435
+ import { join as join4 } from "path";
5436
+ import { homedir as homedir2 } from "os";
5437
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, unlinkSync, openSync } from "fs";
5438
+ import { spawn } from "child_process";
5439
+ var IRIS_HOME2 = join4(homedir2(), ".iris");
5440
+ function pidPath(port) {
5441
+ return join4(IRIS_HOME2, `daemon-${port}.pid`);
5442
+ }
5443
+ function logPath(port) {
5444
+ return join4(IRIS_HOME2, `daemon-${port}.log`);
5445
+ }
5446
+ function readPid(port) {
5447
+ const path = pidPath(port);
5448
+ if (!existsSync3(path))
5449
+ return null;
5450
+ const n = parseInt(readFileSync2(path, "utf8").trim(), 10);
5451
+ return isNaN(n) ? null : n;
5452
+ }
5453
+ function isAlive(pid) {
5454
+ try {
5455
+ process.kill(pid, 0);
5456
+ return true;
5457
+ } catch {
5458
+ return false;
5459
+ }
5460
+ }
5461
+ function removePid(port) {
5462
+ const path = pidPath(port);
5463
+ if (existsSync3(path))
5464
+ unlinkSync(path);
5465
+ }
5466
+ function isRunning(port) {
5467
+ const pid = readPid(port);
5468
+ return pid !== null && isAlive(pid);
5469
+ }
5470
+ function spawnDaemon(nodeExec, scriptPath, args, port) {
5471
+ mkdirSync2(IRIS_HOME2, { recursive: true });
5472
+ const fd = openSync(logPath(port), "a");
5473
+ const child = spawn(nodeExec, [scriptPath, ...args], {
5474
+ detached: true,
5475
+ stdio: ["ignore", fd, fd]
5476
+ });
5477
+ if (child.pid !== void 0) {
5478
+ writeFileSync2(pidPath(port), String(child.pid), "utf8");
5479
+ }
5480
+ child.unref();
5481
+ }
5482
+
5046
5483
  // ../server/dist/index.js
5047
5484
  async function start(options = {}) {
5048
5485
  const port = options.port ?? IRIS_DEFAULT_PORT;
@@ -5075,11 +5512,11 @@ async function start(options = {}) {
5075
5512
  }
5076
5513
  }
5077
5514
  if (options.mcp !== false) {
5078
- const fs = createNodeFileSystem();
5079
- const irisRoot = options.irisRoot ?? join2(process.cwd(), IrisDir.ROOT);
5515
+ const fs2 = createNodeFileSystem();
5516
+ const irisRoot = options.irisRoot ?? join5(process.cwd(), IrisDir.ROOT);
5080
5517
  const now = options.now ?? (() => Date.now());
5081
- const flows = new FlowStore(fs, irisRoot, { now });
5082
- const project = new ProjectStore(fs, irisRoot, { now });
5518
+ const flows = new FlowStore(fs2, irisRoot, { now });
5519
+ const project = new ProjectStore(fs2, irisRoot, { now });
5083
5520
  const annotations = new AnnotationStore();
5084
5521
  const deps = {
5085
5522
  sessions: bridge.sessions,
@@ -5088,7 +5525,7 @@ async function start(options = {}) {
5088
5525
  annotations,
5089
5526
  flows,
5090
5527
  project,
5091
- fs,
5528
+ fs: fs2,
5092
5529
  irisRoot,
5093
5530
  now
5094
5531
  };
@@ -5110,42 +5547,455 @@ async function start(options = {}) {
5110
5547
  }
5111
5548
  };
5112
5549
  }
5550
+ async function startDaemon(options = {}) {
5551
+ const port = options.port ?? IRIS_DEFAULT_PORT;
5552
+ const shared = createSharedServer();
5553
+ const bridge = new Bridge({ port, server: shared.httpServer });
5554
+ const reaper = new SessionReaper(bridge.sessions);
5555
+ reaper.start();
5556
+ let owned;
5557
+ let realInput;
5558
+ const driveUrl = options.driveUrl;
5559
+ if (driveUrl !== void 0 && driveUrl.length > 0) {
5560
+ const headless = options.headless ?? true;
5561
+ const factory = options.realInputFactory ?? ((opts) => new LaunchedRealInputProvider({ driveUrl: opts.driveUrl, headless: opts.headless }));
5562
+ const launched = factory({ driveUrl, headless });
5563
+ try {
5564
+ await launched.navigate();
5565
+ } catch (error) {
5566
+ await shared.close();
5567
+ throw error;
5568
+ }
5569
+ owned = launched;
5570
+ realInput = launched;
5571
+ } else {
5572
+ const cdpUrl = options.cdpUrl ?? process.env["IRIS_CDP_URL"];
5573
+ if (cdpUrl !== void 0 && cdpUrl.length > 0) {
5574
+ const cdp = new CdpRealInputProvider({ cdpUrl });
5575
+ owned = cdp;
5576
+ realInput = cdp;
5577
+ }
5578
+ }
5579
+ const fs2 = createNodeFileSystem();
5580
+ const irisRoot = options.irisRoot ?? join5(process.cwd(), IrisDir.ROOT);
5581
+ const now = options.now ?? (() => Date.now());
5582
+ const flows = new FlowStore(fs2, irisRoot, { now });
5583
+ const project = new ProjectStore(fs2, irisRoot, { now });
5584
+ const annotations = new AnnotationStore();
5585
+ const deps = {
5586
+ sessions: bridge.sessions,
5587
+ baselines: new BaselineStore(),
5588
+ recordings: new RecordingStore(),
5589
+ annotations,
5590
+ flows,
5591
+ project,
5592
+ fs: fs2,
5593
+ irisRoot,
5594
+ now
5595
+ };
5596
+ const profile = resolveToolProfile(options.toolProfile);
5597
+ const effectiveDeps = realInput !== void 0 ? { ...deps, realInput } : deps;
5598
+ shared.attachMcp(() => createMcpServer(effectiveDeps, profile));
5599
+ await new Promise((resolve) => {
5600
+ shared.httpServer.once("listening", resolve);
5601
+ shared.httpServer.listen(port, "127.0.0.1");
5602
+ });
5603
+ log("mcp_daemon_started", { port });
5604
+ return {
5605
+ bridge,
5606
+ ...realInput !== void 0 ? { realInput } : {},
5607
+ close: async () => {
5608
+ reaper.stop();
5609
+ await owned?.dispose();
5610
+ await bridge.close();
5611
+ await shared.close();
5612
+ }
5613
+ };
5614
+ }
5615
+
5616
+ // ../server/dist/mcp-proxy.js
5617
+ import * as http3 from "http";
5618
+ import * as net from "net";
5619
+ var DAEMON_READY_TIMEOUT_MS = 1e4;
5620
+ var DAEMON_POLL_INTERVAL_MS = 100;
5621
+ function delay(ms) {
5622
+ return new Promise((resolve) => setTimeout(resolve, ms));
5623
+ }
5624
+ function probeDaemon(port) {
5625
+ return new Promise((resolve) => {
5626
+ const socket = new net.Socket();
5627
+ socket.setTimeout(500);
5628
+ socket.on("connect", () => {
5629
+ socket.destroy();
5630
+ resolve(true);
5631
+ });
5632
+ socket.on("error", () => resolve(false));
5633
+ socket.on("timeout", () => {
5634
+ socket.destroy();
5635
+ resolve(false);
5636
+ });
5637
+ socket.connect(port, "127.0.0.1");
5638
+ });
5639
+ }
5640
+ async function waitForDaemon(port) {
5641
+ const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
5642
+ while (Date.now() < deadline) {
5643
+ const reachable = await probeDaemon(port);
5644
+ if (reachable)
5645
+ return;
5646
+ await delay(DAEMON_POLL_INTERVAL_MS);
5647
+ }
5648
+ throw new Error(`iris daemon did not become ready on port ${port} within ${DAEMON_READY_TIMEOUT_MS}ms`);
5649
+ }
5650
+ function postToSession(url, body) {
5651
+ return new Promise((resolve) => {
5652
+ const parsed = new URL(url);
5653
+ const bodyBuf = Buffer.from(body, "utf8");
5654
+ const options = {
5655
+ host: parsed.hostname,
5656
+ port: parsed.port !== "" ? parseInt(parsed.port, 10) : 80,
5657
+ path: `${parsed.pathname}${parsed.search}`,
5658
+ method: "POST",
5659
+ headers: {
5660
+ "Content-Type": "application/json",
5661
+ "Content-Length": bodyBuf.byteLength
5662
+ }
5663
+ };
5664
+ const req = http3.request(options, (res) => {
5665
+ res.resume();
5666
+ resolve();
5667
+ });
5668
+ req.on("error", (err) => {
5669
+ log("iris_mcp_proxy_post_error", { error: err.message });
5670
+ resolve();
5671
+ });
5672
+ req.write(bodyBuf);
5673
+ req.end();
5674
+ });
5675
+ }
5676
+ function buildSessionUrl(rawData, port) {
5677
+ return rawData.startsWith("/") ? `http://127.0.0.1:${port}${rawData}` : rawData;
5678
+ }
5679
+ function startMcpProxy(port) {
5680
+ return new Promise((_resolve, reject) => {
5681
+ let postUrl = null;
5682
+ const stdinQueue = [];
5683
+ const req = http3.get({ host: "127.0.0.1", port, path: MCP_SSE_PATH }, (res) => {
5684
+ res.setEncoding("utf8");
5685
+ let sseBuffer = "";
5686
+ let currentEvent = "";
5687
+ let currentData = "";
5688
+ res.on("data", (chunk) => {
5689
+ sseBuffer += chunk;
5690
+ const normalised = sseBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
5691
+ const lines = normalised.split("\n");
5692
+ sseBuffer = lines.pop() ?? "";
5693
+ for (const line of lines) {
5694
+ if (line === "") {
5695
+ if (currentData !== "") {
5696
+ onSseEvent(currentEvent !== "" ? currentEvent : "message", currentData, port);
5697
+ }
5698
+ currentEvent = "";
5699
+ currentData = "";
5700
+ } else if (line.startsWith("event:")) {
5701
+ currentEvent = line.slice(6).trim();
5702
+ } else if (line.startsWith("data:")) {
5703
+ const val = line.slice(5).trim();
5704
+ currentData = currentData !== "" ? `${currentData}
5705
+ ${val}` : val;
5706
+ }
5707
+ }
5708
+ });
5709
+ res.on("end", () => {
5710
+ log("iris_mcp_proxy_sse_ended", { port });
5711
+ process.exit(0);
5712
+ });
5713
+ res.on("error", (err) => {
5714
+ log("iris_mcp_proxy_sse_error", { error: err.message });
5715
+ process.exit(1);
5716
+ });
5717
+ });
5718
+ req.on("error", (err) => reject(err));
5719
+ function onSseEvent(event, data, p) {
5720
+ if (event === "endpoint") {
5721
+ const url = buildSessionUrl(data, p);
5722
+ postUrl = url;
5723
+ for (const queued of stdinQueue.splice(0)) {
5724
+ void postToSession(url, queued);
5725
+ }
5726
+ return;
5727
+ }
5728
+ if (event === "message") {
5729
+ process.stdout.write(`${data}
5730
+ `);
5731
+ }
5732
+ }
5733
+ process.stdin.setEncoding("utf8");
5734
+ let stdinBuffer = "";
5735
+ process.stdin.on("data", (chunk) => {
5736
+ stdinBuffer += chunk;
5737
+ const lines = stdinBuffer.split("\n");
5738
+ stdinBuffer = lines.pop() ?? "";
5739
+ for (const line of lines) {
5740
+ const trimmed = line.trim();
5741
+ if (trimmed === "")
5742
+ continue;
5743
+ if (postUrl === null) {
5744
+ stdinQueue.push(trimmed);
5745
+ } else {
5746
+ void postToSession(postUrl, trimmed);
5747
+ }
5748
+ }
5749
+ });
5750
+ process.stdin.on("end", () => process.exit(0));
5751
+ });
5752
+ }
5113
5753
 
5114
5754
  // ../server/dist/cli.js
5115
- var DRIVE_USAGE = "usage: iris drive <url> [--headed]";
5116
- var HEADED_FLAG = "--headed";
5755
+ var CLI_USAGE = `usage:
5756
+ iris serve [--port N] [--drive <url>] [--headed]
5757
+ iris stop [--port N] [--quiet]
5758
+ iris status [--port N]
5759
+ iris drive <url> [--headed] (foreground mode \u2014 for debugging)
5760
+ iris mcp [--port N] [--drive <url>] [--headed] (MCP stdio proxy \u2014 auto-starts daemon if needed)`;
5761
+ var SERVE_COMMAND = "serve";
5762
+ var STOP_COMMAND = "stop";
5763
+ var STATUS_COMMAND = "status";
5117
5764
  var DRIVE_COMMAND = "drive";
5118
- function parseDriveArgs(argv, port) {
5119
- if (argv.length === 0 || argv[0] !== DRIVE_COMMAND) {
5120
- return { kind: "serve", port };
5765
+ var MCP_COMMAND = "mcp";
5766
+ var DAEMON_INNER_COMMAND = "_daemon";
5767
+ var HEADED_FLAG = "--headed";
5768
+ var PORT_FLAG = "--port";
5769
+ var DRIVE_FLAG = "--drive";
5770
+ var QUIET_FLAG = "--quiet";
5771
+ function parseServeFlags(args, defaultPort) {
5772
+ let port = defaultPort;
5773
+ let driveUrl;
5774
+ let headless = true;
5775
+ let i = 0;
5776
+ while (i < args.length) {
5777
+ const arg = args[i];
5778
+ if (arg === PORT_FLAG) {
5779
+ i++;
5780
+ const n = args[i];
5781
+ if (n === void 0)
5782
+ return { kind: "error", message: CLI_USAGE };
5783
+ const parsed = parseInt(n, 10);
5784
+ if (isNaN(parsed))
5785
+ return { kind: "error", message: CLI_USAGE };
5786
+ port = parsed;
5787
+ } else if (arg === DRIVE_FLAG) {
5788
+ i++;
5789
+ driveUrl = args[i];
5790
+ if (driveUrl === void 0)
5791
+ return { kind: "error", message: CLI_USAGE };
5792
+ } else if (arg === HEADED_FLAG) {
5793
+ headless = false;
5794
+ } else {
5795
+ return { kind: "error", message: CLI_USAGE };
5796
+ }
5797
+ i++;
5121
5798
  }
5122
- const rest = argv.slice(1);
5799
+ return { kind: "ok", port, headless, ...driveUrl !== void 0 ? { driveUrl } : {} };
5800
+ }
5801
+ function parsePortFlag(args, defaultPort) {
5802
+ const idx = args.indexOf(PORT_FLAG);
5803
+ if (idx === -1)
5804
+ return defaultPort;
5805
+ const n = args[idx + 1];
5806
+ if (n === void 0)
5807
+ return defaultPort;
5808
+ const parsed = parseInt(n, 10);
5809
+ return isNaN(parsed) ? defaultPort : parsed;
5810
+ }
5811
+ function parseDriveSuffix(args, port) {
5123
5812
  let headless = true;
5124
5813
  let driveUrl;
5125
- for (const arg of rest) {
5814
+ for (const arg of args) {
5126
5815
  if (arg === HEADED_FLAG) {
5127
5816
  headless = false;
5128
5817
  } else if (arg.startsWith("--")) {
5129
- return { kind: "error", message: DRIVE_USAGE };
5818
+ return { kind: "error", message: CLI_USAGE };
5130
5819
  } else if (driveUrl === void 0) {
5131
5820
  driveUrl = arg;
5132
5821
  } else {
5133
- return { kind: "error", message: DRIVE_USAGE };
5822
+ return { kind: "error", message: CLI_USAGE };
5134
5823
  }
5135
5824
  }
5136
5825
  if (driveUrl === void 0)
5137
- return { kind: "error", message: DRIVE_USAGE };
5138
- return { kind: "drive", driveUrl, headless, port };
5826
+ return { kind: "error", message: CLI_USAGE };
5827
+ return { kind: "ok", port, driveUrl, headless };
5828
+ }
5829
+ function parseCliArgs(argv, defaultPort) {
5830
+ if (argv.length === 0)
5831
+ return { kind: "serve", port: defaultPort, headless: true };
5832
+ const [cmd, ...rest] = argv;
5833
+ switch (cmd) {
5834
+ case SERVE_COMMAND: {
5835
+ const r = parseServeFlags(rest, defaultPort);
5836
+ if (r.kind === "error")
5837
+ return r;
5838
+ return {
5839
+ kind: "serve",
5840
+ port: r.port,
5841
+ headless: r.headless,
5842
+ ...r.driveUrl !== void 0 ? { driveUrl: r.driveUrl } : {}
5843
+ };
5844
+ }
5845
+ case STOP_COMMAND: {
5846
+ const port = parsePortFlag(rest, defaultPort);
5847
+ const quiet = rest.includes(QUIET_FLAG);
5848
+ return { kind: "stop", port, quiet };
5849
+ }
5850
+ case STATUS_COMMAND: {
5851
+ const port = parsePortFlag(rest, defaultPort);
5852
+ return { kind: "status", port };
5853
+ }
5854
+ case DRIVE_COMMAND: {
5855
+ const r = parseDriveSuffix(rest, defaultPort);
5856
+ if (r.kind === "error")
5857
+ return r;
5858
+ return { kind: "drive", port: r.port, driveUrl: r.driveUrl, headless: r.headless };
5859
+ }
5860
+ case DAEMON_INNER_COMMAND: {
5861
+ const r = parseServeFlags(rest, defaultPort);
5862
+ if (r.kind === "error")
5863
+ return r;
5864
+ return {
5865
+ kind: "_daemon",
5866
+ port: r.port,
5867
+ headless: r.headless,
5868
+ ...r.driveUrl !== void 0 ? { driveUrl: r.driveUrl } : {}
5869
+ };
5870
+ }
5871
+ case MCP_COMMAND: {
5872
+ const r = parseServeFlags(rest, defaultPort);
5873
+ if (r.kind === "error")
5874
+ return r;
5875
+ return {
5876
+ kind: "mcp",
5877
+ port: r.port,
5878
+ headless: r.headless,
5879
+ ...r.driveUrl !== void 0 ? { driveUrl: r.driveUrl } : {}
5880
+ };
5881
+ }
5882
+ default:
5883
+ return { kind: "error", message: CLI_USAGE };
5884
+ }
5139
5885
  }
5140
- function main() {
5141
- const portEnv = process.env["IRIS_PORT"];
5142
- const port = portEnv === void 0 ? IRIS_DEFAULT_PORT : Number.parseInt(portEnv, 10);
5143
- const parsed = parseDriveArgs(process.argv.slice(2), port);
5144
- if (parsed.kind === "error") {
5145
- log("iris_usage_error", { message: parsed.message });
5886
+ function handleServe(parsed) {
5887
+ if (isRunning(parsed.port)) {
5888
+ log("iris_daemon_already_running", { port: parsed.port });
5889
+ return;
5890
+ }
5891
+ const scriptPath = process.argv[1];
5892
+ if (scriptPath === void 0) {
5893
+ log("iris_serve_no_script", {});
5146
5894
  process.exit(1);
5895
+ return;
5896
+ }
5897
+ const daemonArgs = [DAEMON_INNER_COMMAND, PORT_FLAG, String(parsed.port)];
5898
+ if (parsed.driveUrl !== void 0) {
5899
+ daemonArgs.push(DRIVE_FLAG, parsed.driveUrl);
5900
+ if (!parsed.headless)
5901
+ daemonArgs.push(HEADED_FLAG);
5902
+ }
5903
+ spawnDaemon(process.execPath, scriptPath, daemonArgs, parsed.port);
5904
+ log("iris_daemon_spawned", { port: parsed.port });
5905
+ }
5906
+ function handleStop(port, quiet) {
5907
+ const pid = readPid(port);
5908
+ if (pid === null || !isAlive(pid)) {
5909
+ removePid(port);
5910
+ if (!quiet)
5911
+ log("iris_daemon_not_running", { port });
5912
+ return;
5913
+ }
5914
+ process.kill(pid, "SIGTERM");
5915
+ const started = Date.now();
5916
+ const poll = setInterval(() => {
5917
+ if (!isAlive(pid)) {
5918
+ clearInterval(poll);
5919
+ removePid(port);
5920
+ if (!quiet)
5921
+ log("iris_daemon_stopped", { port, pid });
5922
+ return;
5923
+ }
5924
+ if (Date.now() - started > 5e3) {
5925
+ clearInterval(poll);
5926
+ if (!quiet)
5927
+ log("iris_daemon_stop_timeout", { port, pid });
5928
+ process.exit(1);
5929
+ }
5930
+ }, 100);
5931
+ }
5932
+ function handleStatus(port) {
5933
+ const pid = readPid(port);
5934
+ if (pid === null || !isAlive(pid)) {
5935
+ log("iris_status", { port, running: false });
5936
+ return;
5147
5937
  }
5148
- const options = parsed.kind === "drive" ? { port: parsed.port, driveUrl: parsed.driveUrl, headless: parsed.headless } : { port: parsed.port };
5938
+ log("iris_status", { port, running: true, pid });
5939
+ }
5940
+ function handleDaemonInner(parsed) {
5941
+ const options = {
5942
+ port: parsed.port,
5943
+ ...parsed.driveUrl !== void 0 ? { driveUrl: parsed.driveUrl, headless: parsed.headless } : {}
5944
+ };
5945
+ startDaemon(options).then((server) => {
5946
+ log("iris_daemon_ready", { port: parsed.port, pid: process.pid });
5947
+ const shutdown = () => {
5948
+ server.close().then(() => {
5949
+ removePid(parsed.port);
5950
+ process.exit(0);
5951
+ }).catch((err) => {
5952
+ const message = err instanceof Error ? err.message : String(err);
5953
+ log("iris_daemon_close_error", { error: message });
5954
+ removePid(parsed.port);
5955
+ process.exit(1);
5956
+ });
5957
+ };
5958
+ process.on("SIGTERM", shutdown);
5959
+ process.on("SIGINT", shutdown);
5960
+ }).catch((error) => {
5961
+ const message = error instanceof Error ? error.message : String(error);
5962
+ log("iris_daemon_start_failed", { error: message });
5963
+ removePid(parsed.port);
5964
+ process.exit(1);
5965
+ });
5966
+ }
5967
+ function handleMcp(opts) {
5968
+ const { port, driveUrl, headless } = opts;
5969
+ probeDaemon(port).then((listening) => {
5970
+ if (!listening) {
5971
+ const scriptPath = process.argv[1];
5972
+ if (scriptPath === void 0) {
5973
+ log("iris_mcp_no_script", {});
5974
+ process.exit(1);
5975
+ return;
5976
+ }
5977
+ const daemonArgs = [DAEMON_INNER_COMMAND, PORT_FLAG, String(port)];
5978
+ if (driveUrl !== void 0) {
5979
+ daemonArgs.push(DRIVE_FLAG, driveUrl);
5980
+ if (!headless)
5981
+ daemonArgs.push(HEADED_FLAG);
5982
+ }
5983
+ spawnDaemon(process.execPath, scriptPath, daemonArgs, port);
5984
+ log("iris_mcp_daemon_started", { port, ...driveUrl !== void 0 ? { driveUrl } : {} });
5985
+ }
5986
+ return waitForDaemon(port).then(() => startMcpProxy(port));
5987
+ }).catch((err) => {
5988
+ const message = err instanceof Error ? err.message : String(err);
5989
+ log("iris_mcp_proxy_error", { error: message });
5990
+ process.exit(1);
5991
+ });
5992
+ }
5993
+ function handleLegacyDrive(parsed) {
5994
+ const options = {
5995
+ port: parsed.port,
5996
+ driveUrl: parsed.driveUrl,
5997
+ headless: parsed.headless
5998
+ };
5149
5999
  start(options).then(() => {
5150
6000
  log("iris_started", { port: parsed.port });
5151
6001
  }).catch((error) => {
@@ -5154,7 +6004,35 @@ function main() {
5154
6004
  process.exit(1);
5155
6005
  });
5156
6006
  }
5157
- var invokedPath = process.argv[1];
5158
- if (invokedPath !== void 0 && import.meta.url === pathToFileURL(invokedPath).href) {
6007
+ function main() {
6008
+ const portEnv = process.env["IRIS_PORT"];
6009
+ const defaultPort = portEnv === void 0 ? IRIS_DEFAULT_PORT : parseInt(portEnv, 10);
6010
+ const parsed = parseCliArgs(process.argv.slice(2), defaultPort);
6011
+ switch (parsed.kind) {
6012
+ case "error":
6013
+ log("iris_usage_error", { message: parsed.message });
6014
+ process.exit(1);
6015
+ break;
6016
+ case "serve":
6017
+ handleServe(parsed);
6018
+ break;
6019
+ case "stop":
6020
+ handleStop(parsed.port, parsed.quiet);
6021
+ break;
6022
+ case "status":
6023
+ handleStatus(parsed.port);
6024
+ break;
6025
+ case "drive":
6026
+ handleLegacyDrive(parsed);
6027
+ break;
6028
+ case "mcp":
6029
+ handleMcp(parsed);
6030
+ break;
6031
+ case "_daemon":
6032
+ handleDaemonInner(parsed);
6033
+ break;
6034
+ }
6035
+ }
6036
+ if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href) {
5159
6037
  main();
5160
6038
  }