@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 +1112 -234
- package/dist/index.js +5 -0
- package/dist/server.d.ts +10 -1
- package/dist/server.js +718 -215
- package/dist/test.js +542 -222
- package/package.json +9 -9
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
|
|
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.
|
|
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
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
|
1098
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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(
|
|
1490
|
+
async function ensureIrisDir(fs2, root) {
|
|
1366
1491
|
const p = irisDirPaths(root);
|
|
1367
|
-
await
|
|
1368
|
-
await
|
|
1369
|
-
await
|
|
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(
|
|
1387
|
-
await ensureIrisDir(
|
|
1388
|
-
await
|
|
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(
|
|
1515
|
+
async function readContract(fs2, root) {
|
|
1391
1516
|
const path = irisDirPaths(root).contract;
|
|
1392
|
-
if (!await
|
|
1517
|
+
if (!await fs2.exists(path))
|
|
1393
1518
|
return { ok: false, reason: ContractReadError.MISSING };
|
|
1394
1519
|
let text;
|
|
1395
1520
|
try {
|
|
1396
|
-
text = await
|
|
1521
|
+
text = await fs2.readFile(path);
|
|
1397
1522
|
} catch (error) {
|
|
1398
1523
|
return {
|
|
1399
1524
|
ok: false,
|
|
1400
|
-
reason:
|
|
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(
|
|
1488
|
-
this.#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(
|
|
1635
|
-
this.#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
|
|
1939
|
+
import { z as z16 } from "zod";
|
|
1815
1940
|
|
|
1816
1941
|
// ../server/dist/tools/tools.js
|
|
1817
|
-
import { z as
|
|
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(
|
|
3427
|
-
this.#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:
|
|
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:
|
|
4141
|
-
sessionId:
|
|
4142
|
-
url:
|
|
4143
|
-
title:
|
|
4144
|
-
lastSeenMs:
|
|
4145
|
-
throttled:
|
|
4146
|
-
focused:
|
|
4147
|
-
hidden:
|
|
4148
|
-
realInputAvailable:
|
|
4149
|
-
stale:
|
|
4150
|
-
recommendation:
|
|
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:
|
|
4167
|
-
mode:
|
|
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:
|
|
4172
|
-
status:
|
|
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:
|
|
4184
|
-
value:
|
|
4185
|
-
name:
|
|
4186
|
-
scope:
|
|
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:
|
|
4191
|
-
ref:
|
|
4192
|
-
role:
|
|
4193
|
-
name:
|
|
4194
|
-
value:
|
|
4195
|
-
states:
|
|
4196
|
-
visible:
|
|
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:
|
|
4199
|
-
route:
|
|
4200
|
-
presentTestids:
|
|
4201
|
-
knownEmptyState:
|
|
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:
|
|
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:
|
|
4220
|
-
role:
|
|
4221
|
-
name:
|
|
4222
|
-
value:
|
|
4223
|
-
states:
|
|
4224
|
-
visible:
|
|
4225
|
-
box:
|
|
4226
|
-
component:
|
|
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:
|
|
4237
|
-
action:
|
|
4238
|
-
args:
|
|
4239
|
-
refuseWhenThrottled:
|
|
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:
|
|
4244
|
-
dispatched:
|
|
4245
|
-
settled:
|
|
4246
|
-
inputMode:
|
|
4247
|
-
result:
|
|
4248
|
-
session:
|
|
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:
|
|
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:
|
|
4309
|
-
dispatched:
|
|
4310
|
-
result:
|
|
4311
|
-
session:
|
|
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:
|
|
4340
|
-
action:
|
|
4341
|
-
args:
|
|
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:
|
|
4344
|
-
refuseWhenThrottled:
|
|
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:
|
|
4349
|
-
verdict:
|
|
4350
|
-
pass:
|
|
4351
|
-
evidence:
|
|
4352
|
-
failureReason:
|
|
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:
|
|
4355
|
-
session:
|
|
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:
|
|
4389
|
-
since:
|
|
4390
|
-
filters:
|
|
4391
|
-
max_events:
|
|
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:
|
|
4396
|
-
summary:
|
|
4397
|
-
total:
|
|
4398
|
-
network:
|
|
4399
|
-
domAdded:
|
|
4400
|
-
domRemoved:
|
|
4401
|
-
domChanged:
|
|
4402
|
-
routeChanges:
|
|
4403
|
-
consoleErrors:
|
|
4404
|
-
animations:
|
|
4405
|
-
signals:
|
|
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:
|
|
4408
|
-
events:
|
|
4409
|
-
bytes:
|
|
4410
|
-
droppedOldest:
|
|
4792
|
+
cost: z15.object({
|
|
4793
|
+
events: z15.number(),
|
|
4794
|
+
bytes: z15.number(),
|
|
4795
|
+
droppedOldest: z15.number().optional()
|
|
4411
4796
|
}),
|
|
4412
|
-
session:
|
|
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:
|
|
4436
|
-
since:
|
|
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:
|
|
4441
|
-
evidence:
|
|
4442
|
-
failureReason:
|
|
4443
|
-
session:
|
|
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:
|
|
4459
|
-
since:
|
|
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:
|
|
4464
|
-
evidence:
|
|
4465
|
-
failureReason:
|
|
4466
|
-
session:
|
|
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:
|
|
4482
|
-
method:
|
|
4483
|
-
urlContains:
|
|
4484
|
-
status:
|
|
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:
|
|
4489
|
-
hint:
|
|
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:
|
|
4510
|
-
since:
|
|
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:
|
|
4515
|
-
hint:
|
|
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:
|
|
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:
|
|
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:
|
|
4547
|
-
lineCount:
|
|
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:
|
|
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:
|
|
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:
|
|
4574
|
-
removed:
|
|
4575
|
-
added:
|
|
4576
|
-
consoleErrors:
|
|
4577
|
-
routeChanged:
|
|
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:
|
|
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:
|
|
4600
|
-
since:
|
|
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:
|
|
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:
|
|
4619
|
-
program:
|
|
4620
|
-
warning:
|
|
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:
|
|
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:
|
|
4657
|
-
ok:
|
|
4658
|
-
steps:
|
|
4659
|
-
tool:
|
|
4660
|
-
ok:
|
|
4661
|
-
error:
|
|
4662
|
-
note:
|
|
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:
|
|
4681
|
-
level:
|
|
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
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
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:
|
|
4697
|
-
advanceMs:
|
|
4698
|
-
reset:
|
|
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:
|
|
4703
|
-
elapsed:
|
|
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:
|
|
4716
|
-
store:
|
|
4717
|
-
path:
|
|
4718
|
-
depth:
|
|
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:
|
|
4723
|
-
storeNames:
|
|
4724
|
-
found:
|
|
4725
|
-
value:
|
|
4726
|
-
component:
|
|
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:
|
|
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:
|
|
4760
|
-
consoleErrors:
|
|
4761
|
-
hint:
|
|
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:
|
|
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
|
|
5079
|
-
const irisRoot = options.irisRoot ??
|
|
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(
|
|
5082
|
-
const project = new ProjectStore(
|
|
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
|
|
5116
|
-
|
|
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
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
5822
|
+
return { kind: "error", message: CLI_USAGE };
|
|
5134
5823
|
}
|
|
5135
5824
|
}
|
|
5136
5825
|
if (driveUrl === void 0)
|
|
5137
|
-
return { kind: "error", message:
|
|
5138
|
-
return { kind: "
|
|
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
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5158
|
-
|
|
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
|
}
|