aipeek 0.2.5 → 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.
@@ -71,13 +71,54 @@ function check(raw) {
71
71
  function truncate(s, max) {
72
72
  return s.length > max ? `${s.slice(0, max)}\u2026` : s;
73
73
  }
74
+ function formatValue(v, seen = /* @__PURE__ */ new Set()) {
75
+ if (v === null || v === void 0)
76
+ return String(v);
77
+ const t = typeof v;
78
+ if (t === "string")
79
+ return v;
80
+ if (t === "number" || t === "boolean" || t === "bigint")
81
+ return String(v);
82
+ if (t === "symbol")
83
+ return v.toString();
84
+ if (t === "function")
85
+ return `[Function: ${v.name || "anonymous"}]`;
86
+ const obj = v;
87
+ if (seen.has(obj))
88
+ return "[Circular]";
89
+ if (v instanceof Error)
90
+ return v.stack || `${v.name}: ${v.message}`;
91
+ seen.add(obj);
92
+ if (v instanceof Map) {
93
+ const items = [...v.entries()].slice(0, 15).map(([k, val]) => `${formatValue(k, seen)} => ${formatValue(val, seen)}`);
94
+ return `Map(${v.size}) {${items.join(", ")}${v.size > 15 ? ", \u2026" : ""}}`;
95
+ }
96
+ if (v instanceof Set) {
97
+ const items = [...v.values()].slice(0, 15).map((val) => formatValue(val, seen));
98
+ return `Set(${v.size}) {${items.join(", ")}${v.size > 15 ? ", \u2026" : ""}}`;
99
+ }
100
+ if (Array.isArray(v)) {
101
+ const items = v.slice(0, 30).map((val) => formatValue(val, seen));
102
+ return `[${items.join(", ")}${v.length > 30 ? ", \u2026" : ""}]`;
103
+ }
104
+ try {
105
+ return JSON.stringify(v);
106
+ } catch (e2) {
107
+ const entries = Object.entries(v).slice(0, 15);
108
+ const parts = entries.map(([k, val]) => `${k}: ${formatValue(val, seen)}`);
109
+ return `{${parts.join(", ")}}`;
110
+ }
111
+ }
112
+ function appStackFrames(stack, max) {
113
+ return stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ") && !l.includes("node_modules") && !l.includes("<anonymous>")).slice(0, max);
114
+ }
74
115
  function compactUrl(url, search) {
75
116
  try {
76
117
  const u = new URL(url);
77
118
  if (search && u.search)
78
119
  return `${u.pathname}?${truncate(u.search.slice(1), search)}`;
79
120
  return u.pathname;
80
- } catch (e2) {
121
+ } catch (e3) {
81
122
  return truncate(url, 80);
82
123
  }
83
124
  }
@@ -249,7 +290,7 @@ function isApiUrl(url) {
249
290
  try {
250
291
  const u = new URL(url);
251
292
  return u.pathname.startsWith("/api") || u.pathname.includes("/graphql");
252
- } catch (e3) {
293
+ } catch (e4) {
253
294
  return false;
254
295
  }
255
296
  }
@@ -287,17 +328,12 @@ function compactErrors(errors) {
287
328
  for (const err of seen.values()) {
288
329
  lines.push(err.message);
289
330
  if (err.stack) {
290
- const frames = filterStack(err.stack);
291
- for (const frame of frames) {
292
- lines.push(` at ${frame}`);
293
- }
331
+ for (const frame of appStackFrames(err.stack, 5))
332
+ lines.push(` ${frame}`);
294
333
  }
295
334
  }
296
335
  return lines.join("\n");
297
336
  }
298
- function filterStack(stack) {
299
- 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);
300
- }
301
337
  function compactState(state) {
302
338
  if (!state || !Object.keys(state).length)
303
339
  return "";
@@ -306,7 +342,7 @@ function compactState(state) {
306
342
  lines.push(`${name}:`);
307
343
  if (typeof value === "object" && value !== null) {
308
344
  for (const [k, v] of Object.entries(value)) {
309
- lines.push(` ${k}: ${formatValue(v)}`);
345
+ lines.push(` ${k}: ${truncate(formatValue(v), 120)}`);
310
346
  }
311
347
  } else {
312
348
  lines.push(` ${String(value)}`);
@@ -314,19 +350,6 @@ function compactState(state) {
314
350
  }
315
351
  return lines.join("\n");
316
352
  }
317
- function formatValue(v) {
318
- if (v === null || v === void 0)
319
- return String(v);
320
- if (typeof v === "string")
321
- return v;
322
- if (typeof v === "number" || typeof v === "boolean")
323
- return String(v);
324
- if (typeof v === "object") {
325
- const s = JSON.stringify(v);
326
- return s.length > 120 ? `${s.slice(0, 120)}\u2026` : s;
327
- }
328
- return String(v);
329
- }
330
353
  function compact(raw) {
331
354
  return {
332
355
  url: raw.url,
@@ -463,12 +486,10 @@ function detailError(errors, index, full) {
463
486
  }
464
487
  const lines = [err.message];
465
488
  if (err.stack) {
466
- const appFrames = err.stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ") && !l.includes("node_modules")).slice(0, 3);
467
- if (appFrames.length)
468
- lines.push(...appFrames);
469
- const totalApp = err.stack.split("\n").filter((l) => l.trim().startsWith("at ") && !l.includes("node_modules")).length;
470
- if (totalApp > 3)
471
- lines.push(` ... ${totalApp - 3} more app frames`);
489
+ const all = appStackFrames(err.stack, Infinity);
490
+ lines.push(...all.slice(0, 3));
491
+ if (all.length > 3)
492
+ lines.push(` ... ${all.length - 3} more app frames`);
472
493
  }
473
494
  if (err.line != null)
474
495
  lines.push(`location: ${err.source || ""}:${err.line}:${_nullishCoalesce(err.column, () => ( 0))}`);
@@ -486,9 +507,9 @@ function detailState(state, name, full) {
486
507
  const value = state[name];
487
508
  if (full) {
488
509
  try {
489
- return JSON.stringify(value, null, 2);
490
- } catch (e4) {
491
- return String(value);
510
+ return _nullishCoalesce(JSON.stringify(value, null, 2), () => ( formatValue(value)));
511
+ } catch (e5) {
512
+ return formatValue(value);
492
513
  }
493
514
  }
494
515
  if (typeof value !== "object" || value === null)
@@ -506,25 +527,14 @@ function isArraySentinel(v) {
506
527
  return digits.length > 0 && [...digits].every((c) => c >= "0" && c <= "9");
507
528
  }
508
529
  function formatSummaryValue(v) {
509
- if (v === null || v === void 0)
510
- return String(v);
511
- if (typeof v === "string") {
512
- if (isArraySentinel(v))
513
- return v;
514
- return v.length > 80 ? `${v.slice(0, 80)}\u2026` : v;
515
- }
516
- if (typeof v === "number" || typeof v === "boolean")
517
- return String(v);
518
- if (typeof v === "object") {
519
- const s = JSON.stringify(v);
520
- return s.length > 80 ? `${s.slice(0, 80)}\u2026` : s;
521
- }
522
- return String(v);
530
+ if (typeof v === "string" && isArraySentinel(v))
531
+ return v;
532
+ return truncate(formatValue(v), 80);
523
533
  }
524
534
  function jsonSchema(sample) {
525
535
  try {
526
536
  return schemaOf(JSON.parse(sample), 0);
527
- } catch (e5) {
537
+ } catch (e6) {
528
538
  return null;
529
539
  }
530
540
  }
@@ -753,6 +763,10 @@ function readBody(req) {
753
763
  req.on("end", () => resolve2(s));
754
764
  });
755
765
  }
766
+ function send(res, status, body) {
767
+ res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
768
+ res.end(body);
769
+ }
756
770
  var __dirname = _path.dirname.call(void 0, _url.fileURLToPath.call(void 0, _chunkZ2Y65YOYcjs.importMetaUrl));
757
771
  var clientDir = _fs.existsSync.call(void 0, _path.resolve.call(void 0, __dirname, "../client")) ? _path.resolve.call(void 0, __dirname, "../client") : _path.resolve.call(void 0, __dirname, "../src/client");
758
772
  var clientPath = _path.resolve.call(void 0, clientDir, "client.ts");
@@ -853,7 +867,7 @@ ${END_TAG}
853
867
  const sep = content.endsWith("\n") ? "" : "\n";
854
868
  _fs.writeFileSync.call(void 0, path, `${content}${sep}
855
869
  ${block}`);
856
- } catch (e6) {
870
+ } catch (e7) {
857
871
  }
858
872
  }
859
873
  function aipeekPlugin() {
@@ -1001,7 +1015,7 @@ function aipeekPlugin() {
1001
1015
  if (msg)
1002
1016
  server.config.logger.warn(msg);
1003
1017
  }
1004
- } catch (e7) {
1018
+ } catch (e8) {
1005
1019
  }
1006
1020
  }, 500);
1007
1021
  });
@@ -1015,13 +1029,11 @@ function aipeekPlugin() {
1015
1029
  if (!code && req.method === "POST")
1016
1030
  code = await readBody(req);
1017
1031
  if (!code) {
1018
- res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1019
- res.end("eval needs ?code= or a POST body");
1032
+ send(res, 400, "eval needs ?code= or a POST body");
1020
1033
  return;
1021
1034
  }
1022
1035
  const r = await evalInClient(code);
1023
- res.writeHead(r.ok ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1024
- res.end(r.ok ? _nullishCoalesce(r.value, () => ( "undefined")) : `error: ${r.error}`);
1036
+ send(res, r.ok ? 200 : 422, r.ok ? _nullishCoalesce(r.value, () => ( "undefined")) : `error: ${r.error}`);
1025
1037
  return;
1026
1038
  }
1027
1039
  if (parts[0] === "dom") {
@@ -1029,14 +1041,12 @@ function aipeekPlugin() {
1029
1041
  url.searchParams.get("scope") || void 0,
1030
1042
  url.searchParams.get("sel") || void 0
1031
1043
  );
1032
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1033
- res.end(dom || "(empty)");
1044
+ send(res, 200, dom || "(empty)");
1034
1045
  return;
1035
1046
  }
1036
1047
  if (parts[0] === "screen") {
1037
1048
  const screen = await collectScreenFromClient();
1038
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1039
- res.end(screen || "(empty)");
1049
+ send(res, 200, screen || "(empty)");
1040
1050
  return;
1041
1051
  }
1042
1052
  if (parts[0] === "chain") {
@@ -1047,8 +1057,7 @@ function aipeekPlugin() {
1047
1057
  if (!Array.isArray(steps))
1048
1058
  throw new Error("body must be a JSON array");
1049
1059
  } catch (e) {
1050
- res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1051
- res.end(`invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
1060
+ send(res, 400, `invalid chain body: ${e instanceof Error ? e.message : String(e)}`);
1052
1061
  return;
1053
1062
  }
1054
1063
  lastRaw = null;
@@ -1074,8 +1083,7 @@ function aipeekPlugin() {
1074
1083
  break;
1075
1084
  }
1076
1085
  }
1077
- res.writeHead(allOk ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1078
- res.end(lastUi ? `${lines.join("\n")}
1086
+ send(res, allOk ? 200 : 422, lastUi ? `${lines.join("\n")}
1079
1087
 
1080
1088
  --- ui after ---
1081
1089
  ${lastUi}` : lines.join("\n"));
@@ -1093,8 +1101,7 @@ ${lastUi}` : lines.join("\n"));
1093
1101
  };
1094
1102
  const check2 = resolveAction(parts[0], args);
1095
1103
  if (!check2.valid) {
1096
- res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1097
- res.end(check2.error);
1104
+ send(res, 400, _nullishCoalesce(check2.error, () => ( "invalid action")));
1098
1105
  return;
1099
1106
  }
1100
1107
  const result = await sendAction(parts[0], args);
@@ -1105,15 +1112,13 @@ ${lastUi}` : lines.join("\n"));
1105
1112
  const name = q.get("out") || `shot-${result.dataUrl.length}.png`;
1106
1113
  const file = _path.resolve.call(void 0, dir, name);
1107
1114
  _fs.writeFileSync.call(void 0, file, _buffer.Buffer.from(result.dataUrl.split(",")[1], "base64"));
1108
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1109
- res.end(`saved: ${file}`);
1115
+ send(res, 200, `saved: ${file}`);
1110
1116
  return;
1111
1117
  }
1112
- res.writeHead(result.ok ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1113
1118
  const head = result.ok ? result.detail || "ok" : `${result.error}${result.detail ? `
1114
1119
 
1115
1120
  clickable: ${result.detail}` : ""}`;
1116
- res.end(result.ui ? `${head}
1121
+ send(res, result.ok ? 200 : 422, result.ui ? `${head}
1117
1122
 
1118
1123
  --- ui after ---
1119
1124
  ${result.ui}` : head);
@@ -1124,8 +1129,7 @@ ${result.ui}` : head);
1124
1129
  lastRaw = raw2;
1125
1130
  const result = check(raw2);
1126
1131
  const output = emitCheck(result);
1127
- res.writeHead(result.pass ? 200 : 417, { "Content-Type": "text/plain; charset=utf-8" });
1128
- res.end(output);
1132
+ send(res, result.pass ? 200 : 417, output);
1129
1133
  return;
1130
1134
  }
1131
1135
  if (parts.length >= 1) {
@@ -1133,29 +1137,22 @@ ${result.ui}` : head);
1133
1137
  lastRaw = await collectFromClient();
1134
1138
  const result = detail(lastRaw, parts[0], parts[1], full);
1135
1139
  if (result !== null) {
1136
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1137
- res.end(result);
1140
+ send(res, 200, result);
1138
1141
  return;
1139
1142
  }
1140
- res.writeHead(404, { "Content-Type": "text/plain" });
1141
- res.end(`not found: ${parts.join("/")}`);
1143
+ send(res, 404, `not found: ${parts.join("/")}`);
1142
1144
  return;
1143
1145
  }
1144
1146
  const raw = await collectFromClient();
1145
1147
  lastRaw = raw;
1146
1148
  if (full) {
1147
1149
  const compacted = compact(raw);
1148
- const output = emit(compacted);
1149
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1150
- res.end(output);
1150
+ send(res, 200, emit(compacted));
1151
1151
  } else {
1152
- const output = emitSummary(raw);
1153
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1154
- res.end(output);
1152
+ send(res, 200, emitSummary(raw));
1155
1153
  }
1156
1154
  } catch (err) {
1157
- res.writeHead(504, { "Content-Type": "text/plain" });
1158
- res.end(err instanceof Error ? err.message : "unknown error");
1155
+ send(res, 504, err instanceof Error ? err.message : "unknown error");
1159
1156
  }
1160
1157
  });
1161
1158
  }
@@ -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 frames = filterStack(err.stack);
287
- for (const frame of frames) {
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 appFrames = err.stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ") && !l.includes("node_modules")).slice(0, 3);
463
- if (appFrames.length)
464
- lines.push(...appFrames);
465
- const totalApp = err.stack.split("\n").filter((l) => l.trim().startsWith("at ") && !l.includes("node_modules")).length;
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 String(value);
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 === null || v === void 0)
506
- return String(v);
507
- if (typeof v === "string") {
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");
@@ -1011,13 +1025,11 @@ function aipeekPlugin() {
1011
1025
  if (!code && req.method === "POST")
1012
1026
  code = await readBody(req);
1013
1027
  if (!code) {
1014
- res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1015
- res.end("eval needs ?code= or a POST body");
1028
+ send(res, 400, "eval needs ?code= or a POST body");
1016
1029
  return;
1017
1030
  }
1018
1031
  const r = await evalInClient(code);
1019
- res.writeHead(r.ok ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1020
- 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}`);
1021
1033
  return;
1022
1034
  }
1023
1035
  if (parts[0] === "dom") {
@@ -1025,14 +1037,12 @@ function aipeekPlugin() {
1025
1037
  url.searchParams.get("scope") || void 0,
1026
1038
  url.searchParams.get("sel") || void 0
1027
1039
  );
1028
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1029
- res.end(dom || "(empty)");
1040
+ send(res, 200, dom || "(empty)");
1030
1041
  return;
1031
1042
  }
1032
1043
  if (parts[0] === "screen") {
1033
1044
  const screen = await collectScreenFromClient();
1034
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1035
- res.end(screen || "(empty)");
1045
+ send(res, 200, screen || "(empty)");
1036
1046
  return;
1037
1047
  }
1038
1048
  if (parts[0] === "chain") {
@@ -1043,8 +1053,7 @@ function aipeekPlugin() {
1043
1053
  if (!Array.isArray(steps))
1044
1054
  throw new Error("body must be a JSON array");
1045
1055
  } catch (e) {
1046
- res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1047
- 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)}`);
1048
1057
  return;
1049
1058
  }
1050
1059
  lastRaw = null;
@@ -1070,8 +1079,7 @@ function aipeekPlugin() {
1070
1079
  break;
1071
1080
  }
1072
1081
  }
1073
- res.writeHead(allOk ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1074
- res.end(lastUi ? `${lines.join("\n")}
1082
+ send(res, allOk ? 200 : 422, lastUi ? `${lines.join("\n")}
1075
1083
 
1076
1084
  --- ui after ---
1077
1085
  ${lastUi}` : lines.join("\n"));
@@ -1089,8 +1097,7 @@ ${lastUi}` : lines.join("\n"));
1089
1097
  };
1090
1098
  const check2 = resolveAction(parts[0], args);
1091
1099
  if (!check2.valid) {
1092
- res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
1093
- res.end(check2.error);
1100
+ send(res, 400, check2.error ?? "invalid action");
1094
1101
  return;
1095
1102
  }
1096
1103
  const result = await sendAction(parts[0], args);
@@ -1101,15 +1108,13 @@ ${lastUi}` : lines.join("\n"));
1101
1108
  const name = q.get("out") || `shot-${result.dataUrl.length}.png`;
1102
1109
  const file = resolve(dir, name);
1103
1110
  writeFileSync(file, Buffer.from(result.dataUrl.split(",")[1], "base64"));
1104
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1105
- res.end(`saved: ${file}`);
1111
+ send(res, 200, `saved: ${file}`);
1106
1112
  return;
1107
1113
  }
1108
- res.writeHead(result.ok ? 200 : 422, { "Content-Type": "text/plain; charset=utf-8" });
1109
1114
  const head = result.ok ? result.detail || "ok" : `${result.error}${result.detail ? `
1110
1115
 
1111
1116
  clickable: ${result.detail}` : ""}`;
1112
- res.end(result.ui ? `${head}
1117
+ send(res, result.ok ? 200 : 422, result.ui ? `${head}
1113
1118
 
1114
1119
  --- ui after ---
1115
1120
  ${result.ui}` : head);
@@ -1120,8 +1125,7 @@ ${result.ui}` : head);
1120
1125
  lastRaw = raw2;
1121
1126
  const result = check(raw2);
1122
1127
  const output = emitCheck(result);
1123
- res.writeHead(result.pass ? 200 : 417, { "Content-Type": "text/plain; charset=utf-8" });
1124
- res.end(output);
1128
+ send(res, result.pass ? 200 : 417, output);
1125
1129
  return;
1126
1130
  }
1127
1131
  if (parts.length >= 1) {
@@ -1129,29 +1133,22 @@ ${result.ui}` : head);
1129
1133
  lastRaw = await collectFromClient();
1130
1134
  const result = detail(lastRaw, parts[0], parts[1], full);
1131
1135
  if (result !== null) {
1132
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1133
- res.end(result);
1136
+ send(res, 200, result);
1134
1137
  return;
1135
1138
  }
1136
- res.writeHead(404, { "Content-Type": "text/plain" });
1137
- res.end(`not found: ${parts.join("/")}`);
1139
+ send(res, 404, `not found: ${parts.join("/")}`);
1138
1140
  return;
1139
1141
  }
1140
1142
  const raw = await collectFromClient();
1141
1143
  lastRaw = raw;
1142
1144
  if (full) {
1143
1145
  const compacted = compact(raw);
1144
- const output = emit(compacted);
1145
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1146
- res.end(output);
1146
+ send(res, 200, emit(compacted));
1147
1147
  } else {
1148
- const output = emitSummary(raw);
1149
- res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1150
- res.end(output);
1148
+ send(res, 200, emitSummary(raw));
1151
1149
  }
1152
1150
  } catch (err) {
1153
- res.writeHead(504, { "Content-Type": "text/plain" });
1154
- res.end(err instanceof Error ? err.message : "unknown error");
1151
+ send(res, 504, err instanceof Error ? err.message : "unknown error");
1155
1152
  }
1156
1153
  });
1157
1154
  }
package/dist/index.cjs CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
 
7
7
 
8
- var _chunk6EZKMGRDcjs = require('./chunk-6EZKMGRD.cjs');
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 = _chunk6EZKMGRDcjs.aipeekPlugin; exports.check = _chunk6EZKMGRDcjs.check; exports.diffState = _chunk6EZKMGRDcjs.diffState; exports.emitCheck = _chunk6EZKMGRDcjs.emitCheck; exports.emitDiff = _chunk6EZKMGRDcjs.emitDiff; exports.emitSummary = _chunk6EZKMGRDcjs.emitSummary;
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
@@ -5,7 +5,7 @@ import {
5
5
  emitCheck,
6
6
  emitDiff,
7
7
  emitSummary
8
- } from "./chunk-X3HAXWFJ.js";
8
+ } from "./chunk-XA2LT6I4.js";
9
9
  export {
10
10
  aipeekPlugin,
11
11
  check,
package/dist/plugin.cjs CHANGED
@@ -3,11 +3,11 @@
3
3
 
4
4
 
5
5
 
6
- var _chunk6EZKMGRDcjs = require('./chunk-6EZKMGRD.cjs');
6
+ var _chunk5ZZYOETFcjs = require('./chunk-5ZZYOETF.cjs');
7
7
  require('./chunk-Z2Y65YOY.cjs');
8
8
 
9
9
 
10
10
 
11
11
 
12
12
 
13
- exports.END_TAG = _chunk6EZKMGRDcjs.END_TAG; exports.START_TAG = _chunk6EZKMGRDcjs.START_TAG; exports.aipeekPlugin = _chunk6EZKMGRDcjs.aipeekPlugin; exports.injectClaudeMd = _chunk6EZKMGRDcjs.injectClaudeMd;
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
@@ -3,7 +3,7 @@ import {
3
3
  START_TAG,
4
4
  aipeekPlugin,
5
5
  injectClaudeMd
6
- } from "./chunk-X3HAXWFJ.js";
6
+ } from "./chunk-XA2LT6I4.js";
7
7
  export {
8
8
  END_TAG,
9
9
  START_TAG,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aipeek",
3
- "version": "0.2.5",
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 : tryStringify(a)).join(' '),
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
- function tryStringify(v: unknown): string {
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
- return String(v)
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
  }
@@ -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
- const result = await performAction(msg.type, msg.args)
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. Wrapped in an async IIFE so the
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
- let ok = true
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
 
@@ -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()
@@ -1,5 +1,5 @@
1
1
  import type { CompactState, ErrorEntry, LogEntry, NetworkRequest, RawState } from './types'
2
- import { compactUrl, truncate } from './util'
2
+ import { appStackFrames, compactUrl, formatValue, truncate } from './util'
3
3
 
4
4
  const SLOW_THRESHOLD = 1000
5
5
 
@@ -262,26 +262,14 @@ export function compactErrors(errors: ErrorEntry[]): string {
262
262
  for (const err of seen.values()) {
263
263
  lines.push(err.message)
264
264
  if (err.stack) {
265
- const frames = filterStack(err.stack)
266
- for (const frame of frames) {
267
- lines.push(` at ${frame}`)
268
- }
265
+ for (const frame of appStackFrames(err.stack, 5))
266
+ lines.push(` ${frame}`)
269
267
  }
270
268
  }
271
269
 
272
270
  return lines.join('\n')
273
271
  }
274
272
 
275
- function filterStack(stack: string): string[] {
276
- return stack
277
- .split('\n')
278
- .map(l => l.trim())
279
- .filter(l => l.startsWith('at '))
280
- .map(l => l.slice(3))
281
- .filter(l => !l.includes('node_modules') && !l.includes('<anonymous>'))
282
- .slice(0, 5)
283
- }
284
-
285
273
  // --- State ---
286
274
 
287
275
  export function compactState(state: Record<string, unknown>): string {
@@ -293,7 +281,7 @@ export function compactState(state: Record<string, unknown>): string {
293
281
  lines.push(`${name}:`)
294
282
  if (typeof value === 'object' && value !== null) {
295
283
  for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
296
- lines.push(` ${k}: ${formatValue(v)}`)
284
+ lines.push(` ${k}: ${truncate(formatValue(v), 120)}`)
297
285
  }
298
286
  }
299
287
  else {
@@ -303,20 +291,6 @@ export function compactState(state: Record<string, unknown>): string {
303
291
  return lines.join('\n')
304
292
  }
305
293
 
306
- function formatValue(v: unknown): string {
307
- if (v === null || v === undefined)
308
- return String(v)
309
- if (typeof v === 'string')
310
- return v
311
- if (typeof v === 'number' || typeof v === 'boolean')
312
- return String(v)
313
- if (typeof v === 'object') {
314
- const s = JSON.stringify(v)
315
- return s.length > 120 ? `${s.slice(0, 120)}…` : s
316
- }
317
- return String(v)
318
- }
319
-
320
294
  // --- Main ---
321
295
 
322
296
  export function compact(raw: RawState): CompactState {
@@ -1,6 +1,6 @@
1
1
  import type { ErrorEntry, LogEntry, NetworkRequest, RawState } from './types'
2
2
  import { compactUI } from './compact'
3
- import { truncate } from './util'
3
+ import { appStackFrames, formatValue, truncate } from './util'
4
4
 
5
5
  export function detail(raw: RawState, section: string, index: string | undefined, full: boolean): string | null {
6
6
  switch (section) {
@@ -130,15 +130,10 @@ function detailError(errors: ErrorEntry[], index: string | undefined, full: bool
130
130
 
131
131
  const lines = [err.message]
132
132
  if (err.stack) {
133
- const appFrames = err.stack.split('\n')
134
- .map(l => l.trim())
135
- .filter(l => l.startsWith('at ') && !l.includes('node_modules'))
136
- .slice(0, 3)
137
- if (appFrames.length)
138
- lines.push(...appFrames)
139
- const totalApp = err.stack.split('\n').filter(l => l.trim().startsWith('at ') && !l.includes('node_modules')).length
140
- if (totalApp > 3)
141
- lines.push(` ... ${totalApp - 3} more app frames`)
133
+ const all = appStackFrames(err.stack, Infinity)
134
+ lines.push(...all.slice(0, 3))
135
+ if (all.length > 3)
136
+ lines.push(` ... ${all.length - 3} more app frames`)
142
137
  }
143
138
  if (err.line != null)
144
139
  lines.push(`location: ${err.source || ''}:${err.line}:${err.column ?? 0}`)
@@ -159,10 +154,10 @@ function detailState(state: Record<string, unknown>, name: string | undefined, f
159
154
  const value = state[name]
160
155
  if (full) {
161
156
  try {
162
- return JSON.stringify(value, null, 2)
157
+ return JSON.stringify(value, null, 2) ?? formatValue(value)
163
158
  }
164
159
  catch {
165
- return String(value)
160
+ return formatValue(value) // 循环引用 / Error / Map → 不再 {}
166
161
  }
167
162
  }
168
163
  if (typeof value !== 'object' || value === null)
@@ -182,20 +177,9 @@ function isArraySentinel(v: string): boolean {
182
177
  }
183
178
 
184
179
  function formatSummaryValue(v: unknown): string {
185
- if (v === null || v === undefined)
186
- return String(v)
187
- if (typeof v === 'string') {
188
- if (isArraySentinel(v)) // "Array(N)" from boundedSnapshot — leave untruncated
189
- return v
190
- return v.length > 80 ? `${v.slice(0, 80)}…` : v
191
- }
192
- if (typeof v === 'number' || typeof v === 'boolean')
193
- return String(v)
194
- if (typeof v === 'object') {
195
- const s = JSON.stringify(v)
196
- return s.length > 80 ? `${s.slice(0, 80)}…` : s
197
- }
198
- return String(v)
180
+ if (typeof v === 'string' && isArraySentinel(v)) // "Array(N)" from boundedSnapshot — leave untruncated
181
+ return v
182
+ return truncate(formatValue(v), 80)
199
183
  }
200
184
 
201
185
  // --- JSON schema fallback ---
package/src/core/util.ts CHANGED
@@ -2,6 +2,59 @@ export function truncate(s: string, max: number): string {
2
2
  return s.length > max ? `${s.slice(0, max)}…` : s
3
3
  }
4
4
 
5
+ // unknown → 人类可读字符串。穷举 JS 类型,让 JSON.stringify 的「漏网类型 → {}」失败模式不可能出现。
6
+ // 返回未截断字符串——截断由调用方按各自 max 处理。
7
+ export function formatValue(v: unknown, seen: Set<object> = new Set()): string {
8
+ if (v === null || v === undefined)
9
+ return String(v)
10
+ const t = typeof v
11
+ if (t === 'string')
12
+ return v as string
13
+ if (t === 'number' || t === 'boolean' || t === 'bigint')
14
+ return String(v)
15
+ if (t === 'symbol')
16
+ return (v as symbol).toString()
17
+ if (t === 'function')
18
+ return `[Function: ${(v as { name?: string }).name || 'anonymous'}]`
19
+ // 此后 v 是 object
20
+ const obj = v as object
21
+ if (seen.has(obj))
22
+ return '[Circular]'
23
+ if (v instanceof Error)
24
+ return v.stack || `${v.name}: ${v.message}`
25
+ seen.add(obj)
26
+ if (v instanceof Map) {
27
+ const items = [...v.entries()].slice(0, 15).map(([k, val]) => `${formatValue(k, seen)} => ${formatValue(val, seen)}`)
28
+ return `Map(${v.size}) {${items.join(', ')}${v.size > 15 ? ', …' : ''}}`
29
+ }
30
+ if (v instanceof Set) {
31
+ const items = [...v.values()].slice(0, 15).map(val => formatValue(val, seen))
32
+ return `Set(${v.size}) {${items.join(', ')}${v.size > 15 ? ', …' : ''}}`
33
+ }
34
+ if (Array.isArray(v)) {
35
+ const items = v.slice(0, 30).map(val => formatValue(val, seen))
36
+ return `[${items.join(', ')}${v.length > 30 ? ', …' : ''}]`
37
+ }
38
+ try {
39
+ return JSON.stringify(v)
40
+ }
41
+ catch {
42
+ // 循环引用 / getter 抛错 → 手动遍历可枚举键,循环处标 [Circular]
43
+ const entries = Object.entries(v as Record<string, unknown>).slice(0, 15)
44
+ const parts = entries.map(([k, val]) => `${k}: ${formatValue(val, seen)}`)
45
+ return `{${parts.join(', ')}}`
46
+ }
47
+ }
48
+
49
+ // stack → 应用栈帧(去 node_modules / <anonymous>),保留 `at ` 前缀。max 与溢出提示由调用方决定。
50
+ export function appStackFrames(stack: string, max: number): string[] {
51
+ return stack
52
+ .split('\n')
53
+ .map(l => l.trim())
54
+ .filter(l => l.startsWith('at ') && !l.includes('node_modules') && !l.includes('<anonymous>'))
55
+ .slice(0, max)
56
+ }
57
+
5
58
  // pathname only; pass `search` to append a truncated query string
6
59
  export function compactUrl(url: string, search?: number): string {
7
60
  try {
@@ -21,6 +21,12 @@ function readBody(req: { on: (e: string, cb: (c: unknown) => void) => void }): P
21
21
  })
22
22
  }
23
23
 
24
+ // 所有端点都回文本响应——writeHead(Content-Type) + end 二连写了 15 遍(且有 2 处漏 charset)。收敛成一处。
25
+ function send(res: { writeHead: (s: number, h: Record<string, string>) => void, end: (b: string) => void }, status: number, body: string) {
26
+ res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' })
27
+ res.end(body)
28
+ }
29
+
24
30
  const __dirname = dirname(fileURLToPath(import.meta.url))
25
31
  // client/core sources are read at runtime (client.ts served via Vite transform,
26
32
  // client-patch.ts compiled by esbuild). Two layouts: source consumption
@@ -331,13 +337,11 @@ export function aipeekPlugin(): Plugin {
331
337
  if (!code && req.method === 'POST')
332
338
  code = await readBody(req)
333
339
  if (!code) {
334
- res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
335
- res.end('eval needs ?code= or a POST body')
340
+ send(res, 400, 'eval needs ?code= or a POST body')
336
341
  return
337
342
  }
338
343
  const r = await evalInClient(code)
339
- res.writeHead(r.ok ? 200 : 422, { 'Content-Type': 'text/plain; charset=utf-8' })
340
- res.end(r.ok ? (r.value ?? 'undefined') : `error: ${r.error}`)
344
+ send(res, r.ok ? 200 : 422, r.ok ? (r.value ?? 'undefined') : `error: ${r.error}`)
341
345
  return
342
346
  }
343
347
 
@@ -347,16 +351,14 @@ export function aipeekPlugin(): Plugin {
347
351
  url.searchParams.get('scope') || undefined,
348
352
  url.searchParams.get('sel') || undefined,
349
353
  )
350
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
351
- res.end(dom || '(empty)')
354
+ send(res, 200, dom || '(empty)')
352
355
  return
353
356
  }
354
357
 
355
358
  // /__aipeek/screen — state-machine projection {view, modal, focus, knobs}
356
359
  if (parts[0] === 'screen') {
357
360
  const screen = await collectScreenFromClient()
358
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
359
- res.end(screen || '(empty)')
361
+ send(res, 200, screen || '(empty)')
360
362
  return
361
363
  }
362
364
 
@@ -372,8 +374,7 @@ export function aipeekPlugin(): Plugin {
372
374
  throw new Error('body must be a JSON array')
373
375
  }
374
376
  catch (e) {
375
- res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
376
- res.end(`invalid chain body: ${e instanceof Error ? e.message : String(e)}`)
377
+ send(res, 400, `invalid chain body: ${e instanceof Error ? e.message : String(e)}`)
377
378
  return
378
379
  }
379
380
  lastRaw = null
@@ -402,8 +403,7 @@ export function aipeekPlugin(): Plugin {
402
403
  break
403
404
  }
404
405
  }
405
- res.writeHead(allOk ? 200 : 422, { 'Content-Type': 'text/plain; charset=utf-8' })
406
- res.end(lastUi ? `${lines.join('\n')}\n\n--- ui after ---\n${lastUi}` : lines.join('\n'))
406
+ send(res, allOk ? 200 : 422, lastUi ? `${lines.join('\n')}\n\n--- ui after ---\n${lastUi}` : lines.join('\n'))
407
407
  return
408
408
  }
409
409
 
@@ -420,8 +420,7 @@ export function aipeekPlugin(): Plugin {
420
420
  }
421
421
  const check = resolveAction(parts[0], args)
422
422
  if (!check.valid) {
423
- res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
424
- res.end(check.error)
423
+ send(res, 400, check.error ?? 'invalid action')
425
424
  return
426
425
  }
427
426
  const result = await sendAction(parts[0], args)
@@ -432,13 +431,11 @@ export function aipeekPlugin(): Plugin {
432
431
  const name = q.get('out') || `shot-${result.dataUrl.length}.png`
433
432
  const file = resolve(dir, name)
434
433
  writeFileSync(file, Buffer.from(result.dataUrl.split(',')[1], 'base64'))
435
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
436
- res.end(`saved: ${file}`)
434
+ send(res, 200, `saved: ${file}`)
437
435
  return
438
436
  }
439
- res.writeHead(result.ok ? 200 : 422, { 'Content-Type': 'text/plain; charset=utf-8' })
440
437
  const head = result.ok ? (result.detail || 'ok') : `${result.error}${result.detail ? `\n\nclickable: ${result.detail}` : ''}`
441
- res.end(result.ui ? `${head}\n\n--- ui after ---\n${result.ui}` : head)
438
+ send(res, result.ok ? 200 : 422, result.ui ? `${head}\n\n--- ui after ---\n${result.ui}` : head)
442
439
  return
443
440
  }
444
441
 
@@ -448,8 +445,7 @@ export function aipeekPlugin(): Plugin {
448
445
  lastRaw = raw
449
446
  const result = check(raw)
450
447
  const output = emitCheck(result)
451
- res.writeHead(result.pass ? 200 : 417, { 'Content-Type': 'text/plain; charset=utf-8' })
452
- res.end(output)
448
+ send(res, result.pass ? 200 : 417, output)
453
449
  return
454
450
  }
455
451
 
@@ -459,12 +455,10 @@ export function aipeekPlugin(): Plugin {
459
455
  lastRaw = await collectFromClient()
460
456
  const result = detail(lastRaw, parts[0], parts[1], full)
461
457
  if (result !== null) {
462
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
463
- res.end(result)
458
+ send(res, 200, result)
464
459
  return
465
460
  }
466
- res.writeHead(404, { 'Content-Type': 'text/plain' })
467
- res.end(`not found: ${parts.join('/')}`)
461
+ send(res, 404, `not found: ${parts.join('/')}`)
468
462
  return
469
463
  }
470
464
 
@@ -473,19 +467,14 @@ export function aipeekPlugin(): Plugin {
473
467
  lastRaw = raw
474
468
  if (full) {
475
469
  const compacted = compact(raw)
476
- const output = emit(compacted)
477
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
478
- res.end(output)
470
+ send(res, 200, emit(compacted))
479
471
  }
480
472
  else {
481
- const output = emitSummary(raw)
482
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
483
- res.end(output)
473
+ send(res, 200, emitSummary(raw))
484
474
  }
485
475
  }
486
476
  catch (err) {
487
- res.writeHead(504, { 'Content-Type': 'text/plain' })
488
- res.end(err instanceof Error ? err.message : 'unknown error')
477
+ send(res, 504, err instanceof Error ? err.message : 'unknown error')
489
478
  }
490
479
  })
491
480
  },