aipeek 0.2.4 → 0.2.6
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/{chunk-3NVB3GGE.cjs → chunk-5ZZYOETF.cjs} +99 -131
- package/dist/{chunk-72ZKZ42D.js → chunk-XA2LT6I4.js} +92 -124
- package/dist/index.cjs +2 -2
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +8 -2
- package/dist/plugin.js +9 -3
- package/package.json +2 -1
- package/src/client/client-patch.ts +35 -3
- package/src/client/client.ts +7 -17
- package/src/core/action.ts +54 -1
- package/src/core/compact.ts +4 -30
- package/src/core/detail.ts +10 -26
- package/src/core/util.ts +53 -0
- package/src/server/plugin.ts +39 -93
|
@@ -67,6 +67,47 @@ function check(raw) {
|
|
|
67
67
|
function truncate(s, max) {
|
|
68
68
|
return s.length > max ? `${s.slice(0, max)}\u2026` : s;
|
|
69
69
|
}
|
|
70
|
+
function formatValue(v, seen = /* @__PURE__ */ new Set()) {
|
|
71
|
+
if (v === null || v === void 0)
|
|
72
|
+
return String(v);
|
|
73
|
+
const t = typeof v;
|
|
74
|
+
if (t === "string")
|
|
75
|
+
return v;
|
|
76
|
+
if (t === "number" || t === "boolean" || t === "bigint")
|
|
77
|
+
return String(v);
|
|
78
|
+
if (t === "symbol")
|
|
79
|
+
return v.toString();
|
|
80
|
+
if (t === "function")
|
|
81
|
+
return `[Function: ${v.name || "anonymous"}]`;
|
|
82
|
+
const obj = v;
|
|
83
|
+
if (seen.has(obj))
|
|
84
|
+
return "[Circular]";
|
|
85
|
+
if (v instanceof Error)
|
|
86
|
+
return v.stack || `${v.name}: ${v.message}`;
|
|
87
|
+
seen.add(obj);
|
|
88
|
+
if (v instanceof Map) {
|
|
89
|
+
const items = [...v.entries()].slice(0, 15).map(([k, val]) => `${formatValue(k, seen)} => ${formatValue(val, seen)}`);
|
|
90
|
+
return `Map(${v.size}) {${items.join(", ")}${v.size > 15 ? ", \u2026" : ""}}`;
|
|
91
|
+
}
|
|
92
|
+
if (v instanceof Set) {
|
|
93
|
+
const items = [...v.values()].slice(0, 15).map((val) => formatValue(val, seen));
|
|
94
|
+
return `Set(${v.size}) {${items.join(", ")}${v.size > 15 ? ", \u2026" : ""}}`;
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(v)) {
|
|
97
|
+
const items = v.slice(0, 30).map((val) => formatValue(val, seen));
|
|
98
|
+
return `[${items.join(", ")}${v.length > 30 ? ", \u2026" : ""}]`;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return JSON.stringify(v);
|
|
102
|
+
} catch {
|
|
103
|
+
const entries = Object.entries(v).slice(0, 15);
|
|
104
|
+
const parts = entries.map(([k, val]) => `${k}: ${formatValue(val, seen)}`);
|
|
105
|
+
return `{${parts.join(", ")}}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function appStackFrames(stack, max) {
|
|
109
|
+
return stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ") && !l.includes("node_modules") && !l.includes("<anonymous>")).slice(0, max);
|
|
110
|
+
}
|
|
70
111
|
function compactUrl(url, search) {
|
|
71
112
|
try {
|
|
72
113
|
const u = new URL(url);
|
|
@@ -283,17 +324,12 @@ function compactErrors(errors) {
|
|
|
283
324
|
for (const err of seen.values()) {
|
|
284
325
|
lines.push(err.message);
|
|
285
326
|
if (err.stack) {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
lines.push(` at ${frame}`);
|
|
289
|
-
}
|
|
327
|
+
for (const frame of appStackFrames(err.stack, 5))
|
|
328
|
+
lines.push(` ${frame}`);
|
|
290
329
|
}
|
|
291
330
|
}
|
|
292
331
|
return lines.join("\n");
|
|
293
332
|
}
|
|
294
|
-
function filterStack(stack) {
|
|
295
|
-
return stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ")).map((l) => l.slice(3)).filter((l) => !l.includes("node_modules") && !l.includes("<anonymous>")).slice(0, 5);
|
|
296
|
-
}
|
|
297
333
|
function compactState(state) {
|
|
298
334
|
if (!state || !Object.keys(state).length)
|
|
299
335
|
return "";
|
|
@@ -302,7 +338,7 @@ function compactState(state) {
|
|
|
302
338
|
lines.push(`${name}:`);
|
|
303
339
|
if (typeof value === "object" && value !== null) {
|
|
304
340
|
for (const [k, v] of Object.entries(value)) {
|
|
305
|
-
lines.push(` ${k}: ${formatValue(v)}`);
|
|
341
|
+
lines.push(` ${k}: ${truncate(formatValue(v), 120)}`);
|
|
306
342
|
}
|
|
307
343
|
} else {
|
|
308
344
|
lines.push(` ${String(value)}`);
|
|
@@ -310,19 +346,6 @@ function compactState(state) {
|
|
|
310
346
|
}
|
|
311
347
|
return lines.join("\n");
|
|
312
348
|
}
|
|
313
|
-
function formatValue(v) {
|
|
314
|
-
if (v === null || v === void 0)
|
|
315
|
-
return String(v);
|
|
316
|
-
if (typeof v === "string")
|
|
317
|
-
return v;
|
|
318
|
-
if (typeof v === "number" || typeof v === "boolean")
|
|
319
|
-
return String(v);
|
|
320
|
-
if (typeof v === "object") {
|
|
321
|
-
const s = JSON.stringify(v);
|
|
322
|
-
return s.length > 120 ? `${s.slice(0, 120)}\u2026` : s;
|
|
323
|
-
}
|
|
324
|
-
return String(v);
|
|
325
|
-
}
|
|
326
349
|
function compact(raw) {
|
|
327
350
|
return {
|
|
328
351
|
url: raw.url,
|
|
@@ -459,12 +482,10 @@ function detailError(errors, index, full) {
|
|
|
459
482
|
}
|
|
460
483
|
const lines = [err.message];
|
|
461
484
|
if (err.stack) {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (totalApp > 3)
|
|
467
|
-
lines.push(` ... ${totalApp - 3} more app frames`);
|
|
485
|
+
const all = appStackFrames(err.stack, Infinity);
|
|
486
|
+
lines.push(...all.slice(0, 3));
|
|
487
|
+
if (all.length > 3)
|
|
488
|
+
lines.push(` ... ${all.length - 3} more app frames`);
|
|
468
489
|
}
|
|
469
490
|
if (err.line != null)
|
|
470
491
|
lines.push(`location: ${err.source || ""}:${err.line}:${err.column ?? 0}`);
|
|
@@ -482,9 +503,9 @@ function detailState(state, name, full) {
|
|
|
482
503
|
const value = state[name];
|
|
483
504
|
if (full) {
|
|
484
505
|
try {
|
|
485
|
-
return JSON.stringify(value, null, 2);
|
|
506
|
+
return JSON.stringify(value, null, 2) ?? formatValue(value);
|
|
486
507
|
} catch {
|
|
487
|
-
return
|
|
508
|
+
return formatValue(value);
|
|
488
509
|
}
|
|
489
510
|
}
|
|
490
511
|
if (typeof value !== "object" || value === null)
|
|
@@ -502,20 +523,9 @@ function isArraySentinel(v) {
|
|
|
502
523
|
return digits.length > 0 && [...digits].every((c) => c >= "0" && c <= "9");
|
|
503
524
|
}
|
|
504
525
|
function formatSummaryValue(v) {
|
|
505
|
-
if (v ===
|
|
506
|
-
return
|
|
507
|
-
|
|
508
|
-
if (isArraySentinel(v))
|
|
509
|
-
return v;
|
|
510
|
-
return v.length > 80 ? `${v.slice(0, 80)}\u2026` : v;
|
|
511
|
-
}
|
|
512
|
-
if (typeof v === "number" || typeof v === "boolean")
|
|
513
|
-
return String(v);
|
|
514
|
-
if (typeof v === "object") {
|
|
515
|
-
const s = JSON.stringify(v);
|
|
516
|
-
return s.length > 80 ? `${s.slice(0, 80)}\u2026` : s;
|
|
517
|
-
}
|
|
518
|
-
return String(v);
|
|
526
|
+
if (typeof v === "string" && isArraySentinel(v))
|
|
527
|
+
return v;
|
|
528
|
+
return truncate(formatValue(v), 80);
|
|
519
529
|
}
|
|
520
530
|
function jsonSchema(sample) {
|
|
521
531
|
try {
|
|
@@ -749,6 +759,10 @@ function readBody(req) {
|
|
|
749
759
|
req.on("end", () => resolve2(s));
|
|
750
760
|
});
|
|
751
761
|
}
|
|
762
|
+
function send(res, status, body) {
|
|
763
|
+
res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
|
|
764
|
+
res.end(body);
|
|
765
|
+
}
|
|
752
766
|
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
753
767
|
var clientDir = existsSync(resolve(__dirname2, "../client")) ? resolve(__dirname2, "../client") : resolve(__dirname2, "../src/client");
|
|
754
768
|
var clientPath = resolve(clientDir, "client.ts");
|
|
@@ -826,61 +840,29 @@ JS in the page and returns the result \u2014 for anything the typed endpoints ca
|
|
|
826
840
|
aipeek auto-detects errors after HMR and prints them to the terminal \u2014 watch for \`[aipeek]\` messages.
|
|
827
841
|
`;
|
|
828
842
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const i = t.indexOf("localhost:");
|
|
832
|
-
if (i === -1)
|
|
833
|
-
return t;
|
|
834
|
-
let j = i + "localhost:".length;
|
|
835
|
-
while (j < t.length && t[j] >= "0" && t[j] <= "9") j++;
|
|
836
|
-
return `${t.slice(0, i)}localhost:PORT${t.slice(j)}`;
|
|
837
|
-
}
|
|
838
|
-
function stripBlocks(content, snippet) {
|
|
839
|
-
const known = new Set(snippet.split("\n").map(norm).filter((l) => l.length > 3));
|
|
840
|
-
const lines = content.split("\n");
|
|
841
|
-
const keep = [];
|
|
842
|
-
let inside = false;
|
|
843
|
-
let buf = [];
|
|
844
|
-
let hits = 0;
|
|
845
|
-
const flush = () => {
|
|
846
|
-
if (buf.length && hits / buf.length <= 0.5)
|
|
847
|
-
keep.push(...buf);
|
|
848
|
-
buf = [];
|
|
849
|
-
hits = 0;
|
|
850
|
-
inside = false;
|
|
851
|
-
};
|
|
852
|
-
for (const line of lines) {
|
|
853
|
-
const isKnown = known.has(norm(line));
|
|
854
|
-
if (!inside) {
|
|
855
|
-
if (isKnown) {
|
|
856
|
-
inside = true;
|
|
857
|
-
buf = [line];
|
|
858
|
-
hits = 1;
|
|
859
|
-
} else {
|
|
860
|
-
keep.push(line);
|
|
861
|
-
}
|
|
862
|
-
continue;
|
|
863
|
-
}
|
|
864
|
-
buf.push(line);
|
|
865
|
-
if (isKnown)
|
|
866
|
-
hits++;
|
|
867
|
-
else if (buf.slice(-3).every((l) => !known.has(norm(l))))
|
|
868
|
-
flush();
|
|
869
|
-
}
|
|
870
|
-
flush();
|
|
871
|
-
return keep.join("\n");
|
|
872
|
-
}
|
|
843
|
+
var START_TAG = "<!-- AIPEEK:START -->";
|
|
844
|
+
var END_TAG = "<!-- AIPEEK:END -->";
|
|
873
845
|
function injectClaudeMd(root, port) {
|
|
874
846
|
const path = resolve(root, "CLAUDE.md");
|
|
875
|
-
const
|
|
847
|
+
const block = `${START_TAG}
|
|
848
|
+
${aipeekSnippet(port).trim()}
|
|
849
|
+
${END_TAG}
|
|
850
|
+
`;
|
|
876
851
|
try {
|
|
877
852
|
if (!existsSync(path)) {
|
|
878
|
-
writeFileSync(path,
|
|
853
|
+
writeFileSync(path, block);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const content = readFileSync(path, "utf-8");
|
|
857
|
+
const si = content.indexOf(START_TAG);
|
|
858
|
+
const ei = content.indexOf(END_TAG);
|
|
859
|
+
if (si !== -1 && ei !== -1) {
|
|
860
|
+
writeFileSync(path, content.slice(0, si) + block.trimEnd() + content.slice(ei + END_TAG.length));
|
|
879
861
|
return;
|
|
880
862
|
}
|
|
881
|
-
const
|
|
882
|
-
writeFileSync(path, `${
|
|
883
|
-
${
|
|
863
|
+
const sep = content.endsWith("\n") ? "" : "\n";
|
|
864
|
+
writeFileSync(path, `${content}${sep}
|
|
865
|
+
${block}`);
|
|
884
866
|
} catch {
|
|
885
867
|
}
|
|
886
868
|
}
|
|
@@ -1043,13 +1025,11 @@ function aipeekPlugin() {
|
|
|
1043
1025
|
if (!code && req.method === "POST")
|
|
1044
1026
|
code = await readBody(req);
|
|
1045
1027
|
if (!code) {
|
|
1046
|
-
res
|
|
1047
|
-
res.end("eval needs ?code= or a POST body");
|
|
1028
|
+
send(res, 400, "eval needs ?code= or a POST body");
|
|
1048
1029
|
return;
|
|
1049
1030
|
}
|
|
1050
1031
|
const r = await evalInClient(code);
|
|
1051
|
-
res
|
|
1052
|
-
res.end(r.ok ? r.value ?? "undefined" : `error: ${r.error}`);
|
|
1032
|
+
send(res, r.ok ? 200 : 422, r.ok ? r.value ?? "undefined" : `error: ${r.error}`);
|
|
1053
1033
|
return;
|
|
1054
1034
|
}
|
|
1055
1035
|
if (parts[0] === "dom") {
|
|
@@ -1057,14 +1037,12 @@ function aipeekPlugin() {
|
|
|
1057
1037
|
url.searchParams.get("scope") || void 0,
|
|
1058
1038
|
url.searchParams.get("sel") || void 0
|
|
1059
1039
|
);
|
|
1060
|
-
res
|
|
1061
|
-
res.end(dom || "(empty)");
|
|
1040
|
+
send(res, 200, dom || "(empty)");
|
|
1062
1041
|
return;
|
|
1063
1042
|
}
|
|
1064
1043
|
if (parts[0] === "screen") {
|
|
1065
1044
|
const screen = await collectScreenFromClient();
|
|
1066
|
-
res
|
|
1067
|
-
res.end(screen || "(empty)");
|
|
1045
|
+
send(res, 200, screen || "(empty)");
|
|
1068
1046
|
return;
|
|
1069
1047
|
}
|
|
1070
1048
|
if (parts[0] === "chain") {
|
|
@@ -1075,8 +1053,7 @@ function aipeekPlugin() {
|
|
|
1075
1053
|
if (!Array.isArray(steps))
|
|
1076
1054
|
throw new Error("body must be a JSON array");
|
|
1077
1055
|
} catch (e) {
|
|
1078
|
-
res
|
|
1079
|
-
res.end(`invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
|
|
1056
|
+
send(res, 400, `invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
|
|
1080
1057
|
return;
|
|
1081
1058
|
}
|
|
1082
1059
|
lastRaw = null;
|
|
@@ -1102,8 +1079,7 @@ function aipeekPlugin() {
|
|
|
1102
1079
|
break;
|
|
1103
1080
|
}
|
|
1104
1081
|
}
|
|
1105
|
-
res
|
|
1106
|
-
res.end(lastUi ? `${lines.join("\n")}
|
|
1082
|
+
send(res, allOk ? 200 : 422, lastUi ? `${lines.join("\n")}
|
|
1107
1083
|
|
|
1108
1084
|
--- ui after ---
|
|
1109
1085
|
${lastUi}` : lines.join("\n"));
|
|
@@ -1121,8 +1097,7 @@ ${lastUi}` : lines.join("\n"));
|
|
|
1121
1097
|
};
|
|
1122
1098
|
const check2 = resolveAction(parts[0], args);
|
|
1123
1099
|
if (!check2.valid) {
|
|
1124
|
-
res
|
|
1125
|
-
res.end(check2.error);
|
|
1100
|
+
send(res, 400, check2.error ?? "invalid action");
|
|
1126
1101
|
return;
|
|
1127
1102
|
}
|
|
1128
1103
|
const result = await sendAction(parts[0], args);
|
|
@@ -1133,15 +1108,13 @@ ${lastUi}` : lines.join("\n"));
|
|
|
1133
1108
|
const name = q.get("out") || `shot-${result.dataUrl.length}.png`;
|
|
1134
1109
|
const file = resolve(dir, name);
|
|
1135
1110
|
writeFileSync(file, Buffer.from(result.dataUrl.split(",")[1], "base64"));
|
|
1136
|
-
res
|
|
1137
|
-
res.end(`saved: ${file}`);
|
|
1111
|
+
send(res, 200, `saved: ${file}`);
|
|
1138
1112
|
return;
|
|
1139
1113
|
}
|
|
1140
|
-
res.writeHead(result.ok ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1141
1114
|
const head = result.ok ? result.detail || "ok" : `${result.error}${result.detail ? `
|
|
1142
1115
|
|
|
1143
1116
|
clickable: ${result.detail}` : ""}`;
|
|
1144
|
-
res.
|
|
1117
|
+
send(res, result.ok ? 200 : 422, result.ui ? `${head}
|
|
1145
1118
|
|
|
1146
1119
|
--- ui after ---
|
|
1147
1120
|
${result.ui}` : head);
|
|
@@ -1152,8 +1125,7 @@ ${result.ui}` : head);
|
|
|
1152
1125
|
lastRaw = raw2;
|
|
1153
1126
|
const result = check(raw2);
|
|
1154
1127
|
const output = emitCheck(result);
|
|
1155
|
-
res
|
|
1156
|
-
res.end(output);
|
|
1128
|
+
send(res, result.pass ? 200 : 417, output);
|
|
1157
1129
|
return;
|
|
1158
1130
|
}
|
|
1159
1131
|
if (parts.length >= 1) {
|
|
@@ -1161,29 +1133,22 @@ ${result.ui}` : head);
|
|
|
1161
1133
|
lastRaw = await collectFromClient();
|
|
1162
1134
|
const result = detail(lastRaw, parts[0], parts[1], full);
|
|
1163
1135
|
if (result !== null) {
|
|
1164
|
-
res
|
|
1165
|
-
res.end(result);
|
|
1136
|
+
send(res, 200, result);
|
|
1166
1137
|
return;
|
|
1167
1138
|
}
|
|
1168
|
-
res
|
|
1169
|
-
res.end(`not found: ${parts.join("/")}`);
|
|
1139
|
+
send(res, 404, `not found: ${parts.join("/")}`);
|
|
1170
1140
|
return;
|
|
1171
1141
|
}
|
|
1172
1142
|
const raw = await collectFromClient();
|
|
1173
1143
|
lastRaw = raw;
|
|
1174
1144
|
if (full) {
|
|
1175
1145
|
const compacted = compact(raw);
|
|
1176
|
-
|
|
1177
|
-
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1178
|
-
res.end(output);
|
|
1146
|
+
send(res, 200, emit(compacted));
|
|
1179
1147
|
} else {
|
|
1180
|
-
|
|
1181
|
-
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1182
|
-
res.end(output);
|
|
1148
|
+
send(res, 200, emitSummary(raw));
|
|
1183
1149
|
}
|
|
1184
1150
|
} catch (err) {
|
|
1185
|
-
res
|
|
1186
|
-
res.end(err instanceof Error ? err.message : "unknown error");
|
|
1151
|
+
send(res, 504, err instanceof Error ? err.message : "unknown error");
|
|
1187
1152
|
}
|
|
1188
1153
|
});
|
|
1189
1154
|
}
|
|
@@ -1196,5 +1161,8 @@ export {
|
|
|
1196
1161
|
emitSummary,
|
|
1197
1162
|
emitCheck,
|
|
1198
1163
|
emitDiff,
|
|
1164
|
+
START_TAG,
|
|
1165
|
+
END_TAG,
|
|
1166
|
+
injectClaudeMd,
|
|
1199
1167
|
aipeekPlugin
|
|
1200
1168
|
};
|
package/dist/index.cjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
var
|
|
8
|
+
var _chunk5ZZYOETFcjs = require('./chunk-5ZZYOETF.cjs');
|
|
9
9
|
require('./chunk-Z2Y65YOY.cjs');
|
|
10
10
|
|
|
11
11
|
|
|
@@ -14,4 +14,4 @@ require('./chunk-Z2Y65YOY.cjs');
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
exports.aipeekPlugin =
|
|
17
|
+
exports.aipeekPlugin = _chunk5ZZYOETFcjs.aipeekPlugin; exports.check = _chunk5ZZYOETFcjs.check; exports.diffState = _chunk5ZZYOETFcjs.diffState; exports.emitCheck = _chunk5ZZYOETFcjs.emitCheck; exports.emitDiff = _chunk5ZZYOETFcjs.emitDiff; exports.emitSummary = _chunk5ZZYOETFcjs.emitSummary;
|
package/dist/index.js
CHANGED
package/dist/plugin.cjs
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
var _chunk5ZZYOETFcjs = require('./chunk-5ZZYOETF.cjs');
|
|
4
7
|
require('./chunk-Z2Y65YOY.cjs');
|
|
5
8
|
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
exports.END_TAG = _chunk5ZZYOETFcjs.END_TAG; exports.START_TAG = _chunk5ZZYOETFcjs.START_TAG; exports.aipeekPlugin = _chunk5ZZYOETFcjs.aipeekPlugin; exports.injectClaudeMd = _chunk5ZZYOETFcjs.injectClaudeMd;
|
package/dist/plugin.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aipeek",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Gives AI a peek into your running browser app — UI tree, console, network, errors, state",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"jsdom": "^29.1.1",
|
|
45
|
+
"teact": "file:../teact",
|
|
45
46
|
"tsup": "^8.4.0",
|
|
46
47
|
"typescript": "~5.7.2",
|
|
47
48
|
"vitest": "^3.1.2"
|
|
@@ -39,7 +39,7 @@ for (const level of ['log', 'info', 'warn', 'error', 'debug'] as const) {
|
|
|
39
39
|
console[level] = (...args: unknown[]) => {
|
|
40
40
|
pushBounded(consoleLogs, {
|
|
41
41
|
level,
|
|
42
|
-
text: args.map(a => typeof a === 'string' ? a :
|
|
42
|
+
text: args.map(a => typeof a === 'string' ? a : formatValue(a)).join(' '),
|
|
43
43
|
timestamp: Date.now(),
|
|
44
44
|
}, MAX_CONSOLE)
|
|
45
45
|
orig.apply(console, args)
|
|
@@ -302,11 +302,43 @@ function sampleOf(v: unknown, d: number): unknown {
|
|
|
302
302
|
return null
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
|
|
305
|
+
// client-patch 是自包含内联脚本(esbuild transformSync,无模块解析)——不能 import core/util。
|
|
306
|
+
// 这是 core/util.ts formatValue 的镜像;两处契约由各自单测守,client 侧靠运行时验证。
|
|
307
|
+
function formatValue(v: unknown, seen: Set<object> = new Set()): string {
|
|
308
|
+
if (v === null || v === undefined)
|
|
309
|
+
return String(v)
|
|
310
|
+
const t = typeof v
|
|
311
|
+
if (t === 'string')
|
|
312
|
+
return v as string
|
|
313
|
+
if (t === 'number' || t === 'boolean' || t === 'bigint')
|
|
314
|
+
return String(v)
|
|
315
|
+
if (t === 'symbol')
|
|
316
|
+
return (v as symbol).toString()
|
|
317
|
+
if (t === 'function')
|
|
318
|
+
return `[Function: ${(v as { name?: string }).name || 'anonymous'}]`
|
|
319
|
+
const obj = v as object
|
|
320
|
+
if (seen.has(obj))
|
|
321
|
+
return '[Circular]'
|
|
322
|
+
if (v instanceof Error)
|
|
323
|
+
return v.stack || `${v.name}: ${v.message}`
|
|
324
|
+
seen.add(obj)
|
|
325
|
+
if (v instanceof Map) {
|
|
326
|
+
const items = [...v.entries()].slice(0, 15).map(([k, val]) => `${formatValue(k, seen)} => ${formatValue(val, seen)}`)
|
|
327
|
+
return `Map(${v.size}) {${items.join(', ')}${v.size > 15 ? ', …' : ''}}`
|
|
328
|
+
}
|
|
329
|
+
if (v instanceof Set) {
|
|
330
|
+
const items = [...v.values()].slice(0, 15).map(val => formatValue(val, seen))
|
|
331
|
+
return `Set(${v.size}) {${items.join(', ')}${v.size > 15 ? ', …' : ''}}`
|
|
332
|
+
}
|
|
333
|
+
if (Array.isArray(v)) {
|
|
334
|
+
const items = v.slice(0, 30).map(val => formatValue(val, seen))
|
|
335
|
+
return `[${items.join(', ')}${v.length > 30 ? ', …' : ''}]`
|
|
336
|
+
}
|
|
306
337
|
try {
|
|
307
338
|
return JSON.stringify(v)
|
|
308
339
|
}
|
|
309
340
|
catch {
|
|
310
|
-
|
|
341
|
+
const entries = Object.entries(v as Record<string, unknown>).slice(0, 15)
|
|
342
|
+
return `{${entries.map(([k, val]) => `${k}: ${formatValue(val, seen)}`).join(', ')}}`
|
|
311
343
|
}
|
|
312
344
|
}
|
package/src/client/client.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ActionArgs, ActionType } from '../core/action'
|
|
7
7
|
import type { ErrorEntry, LogEntry, NetworkRequest } from '../core/types'
|
|
8
|
-
import { INTERACTIVE, performAction } from '../core/action'
|
|
8
|
+
import { INTERACTIVE, performAction, runEval, withDialogGuard } from '../core/action'
|
|
9
9
|
|
|
10
10
|
declare global {
|
|
11
11
|
interface Window { __AIPEEK_STORES__?: Record<string, unknown> }
|
|
@@ -643,7 +643,10 @@ if (import.meta.hot) {
|
|
|
643
643
|
import.meta.hot.on('aipeek:action', async (msg: { id: number, type: ActionType, args: ActionArgs, requireVisible?: boolean }) => {
|
|
644
644
|
if (skip(msg))
|
|
645
645
|
return
|
|
646
|
-
|
|
646
|
+
// Guard against native alert/confirm/prompt freezing the probe (see withDialogGuard).
|
|
647
|
+
const { result, dialogs } = await withDialogGuard(() => performAction(msg.type, msg.args))
|
|
648
|
+
if (dialogs.length)
|
|
649
|
+
result.detail = `${result.detail ?? ''} [auto-dismissed ${dialogs.join('; ')}]`.trim()
|
|
647
650
|
// For mutating actions, settle the DOM then ship both the full UI tree and
|
|
648
651
|
// the compact screen projection — the caller skips a round-trip to /ui, and
|
|
649
652
|
// /chain uses the per-step screen so an interaction's every transition shows.
|
|
@@ -654,24 +657,11 @@ if (import.meta.hot) {
|
|
|
654
657
|
import.meta.hot!.send('aipeek:result', { id: msg.id, ...result })
|
|
655
658
|
})
|
|
656
659
|
|
|
657
|
-
// eval: run server-supplied code in the page
|
|
658
|
-
// code can `await` and use `return`; non-string results are JSON-stringified.
|
|
660
|
+
// eval: run server-supplied code in the page with auto-return (see runEval).
|
|
659
661
|
import.meta.hot.on('aipeek:eval', async (msg: { id: number, code: string, requireVisible?: boolean }) => {
|
|
660
662
|
if (skip(msg))
|
|
661
663
|
return
|
|
662
|
-
|
|
663
|
-
let value: string | undefined
|
|
664
|
-
let error: string | undefined
|
|
665
|
-
try {
|
|
666
|
-
// eslint-disable-next-line no-new-func
|
|
667
|
-
const fn = new Function(`return (async () => { ${msg.code} })()`)
|
|
668
|
-
const result = await fn()
|
|
669
|
-
value = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
670
|
-
}
|
|
671
|
-
catch (e) {
|
|
672
|
-
ok = false
|
|
673
|
-
error = e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e)
|
|
674
|
-
}
|
|
664
|
+
const { ok, value, error } = await runEval(msg.code)
|
|
675
665
|
import.meta.hot!.send('aipeek:eval-result', { id: msg.id, ok, value, error })
|
|
676
666
|
})
|
|
677
667
|
|
package/src/core/action.ts
CHANGED
|
@@ -57,7 +57,7 @@ export function resolveAction(type: string, args: ActionArgs): { valid: boolean,
|
|
|
57
57
|
|
|
58
58
|
// --- Browser-side execution (client.ts only) ---
|
|
59
59
|
|
|
60
|
-
export const INTERACTIVE = 'a, button, input, textarea, select, [role], [onclick], [tabindex], [contenteditable]'
|
|
60
|
+
export const INTERACTIVE = 'a, button, input, textarea, select, [role], [onclick], [tabindex], [contenteditable], [aria-label]'
|
|
61
61
|
|
|
62
62
|
function visibleText(el: Element): string {
|
|
63
63
|
const aria = el.getAttribute('aria-label')
|
|
@@ -114,6 +114,59 @@ export async function performAction(type: ActionType, args: ActionArgs): Promise
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
export interface EvalResult { ok: boolean, value?: string, error?: string }
|
|
118
|
+
|
|
119
|
+
// Run server-supplied JS in the page with auto-return (Chrome-console / Node-REPL
|
|
120
|
+
// ergonomics): try the whole code as a single expression first so `qsa(...).length`
|
|
121
|
+
// or `1+1` yield a value without an explicit `return`. If that fails to *compile*
|
|
122
|
+
// (multi-statement, contains `return`, etc.) fall back to a plain statement block.
|
|
123
|
+
// A compile error only swaps the wrapper; a runtime throw surfaces as the error.
|
|
124
|
+
// undefined results are dropped (no value); objects are JSON-stringified.
|
|
125
|
+
export async function runEval(code: string): Promise<EvalResult> {
|
|
126
|
+
try {
|
|
127
|
+
let fn: () => Promise<unknown>
|
|
128
|
+
try {
|
|
129
|
+
// eslint-disable-next-line no-new-func
|
|
130
|
+
fn = new Function(`return (async () => (${code}))()`) as () => Promise<unknown>
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// eslint-disable-next-line no-new-func
|
|
134
|
+
fn = new Function(`return (async () => { ${code} })()`) as () => Promise<unknown>
|
|
135
|
+
}
|
|
136
|
+
const result = await fn()
|
|
137
|
+
const value = result === undefined ? undefined : typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
138
|
+
return { ok: true, value }
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
return { ok: false, error: e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e) }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Native alert/confirm/prompt are *synchronous* and freeze the whole JS thread until
|
|
146
|
+
// a human dismisses them — which deadlocks the probe (it runs on that same thread, so
|
|
147
|
+
// the HMR channel can never answer and every curl times out). A click that hits a
|
|
148
|
+
// `copy-to-clipboard` fallback or a `confirm("delete?")` would hang aipeek forever.
|
|
149
|
+
// So we stub them for the duration of `fn`: auto-answer (confirm→true, prompt→default,
|
|
150
|
+
// alert→noop) and return what was suppressed so the caller can report it. Always
|
|
151
|
+
// restored in finally — the page's own dialogs work again after the action settles.
|
|
152
|
+
export async function withDialogGuard<T>(fn: () => Promise<T>): Promise<{ result: T, dialogs: string[] }> {
|
|
153
|
+
const realAlert = window.alert
|
|
154
|
+
const realConfirm = window.confirm
|
|
155
|
+
const realPrompt = window.prompt
|
|
156
|
+
const dialogs: string[] = []
|
|
157
|
+
window.alert = (m?: unknown) => { dialogs.push(`alert: ${String(m ?? '')}`.slice(0, 80)) }
|
|
158
|
+
window.confirm = (m?: unknown) => { dialogs.push(`confirm→true: ${String(m ?? '')}`.slice(0, 80)); return true }
|
|
159
|
+
window.prompt = (m?: unknown, d?: string) => { dialogs.push(`prompt→default: ${String(m ?? '')}`.slice(0, 80)); return d ?? '' }
|
|
160
|
+
try {
|
|
161
|
+
return { result: await fn(), dialogs }
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
window.alert = realAlert
|
|
165
|
+
window.confirm = realConfirm
|
|
166
|
+
window.prompt = realPrompt
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
117
170
|
// Click like a human, not like el.click(). A real click is a *position* the browser
|
|
118
171
|
// hit-tests, then a full event sequence at that point — hover, pointerdown/mousedown,
|
|
119
172
|
// browser-decided focus, pointerup/mouseup, click. Two things matter that el.click()
|