@opencrater/sdk 0.8.48 → 0.8.50

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.

Potentially problematic release.


This version of @opencrater/sdk might be problematic. Click here for more details.

Files changed (2) hide show
  1. package/dist/paint.js +147 -12
  2. package/package.json +1 -1
package/dist/paint.js CHANGED
@@ -298,16 +298,29 @@ var WIN_PUMP_PS = [
298
298
  // parent chain, attach to each ancestor, and keep the first console that is
299
299
  // NOT the 9001-row hidden buffer — that is the one with a window on screen.
300
300
  "$self=Csbi (OpenConout); if($self){ L ('pump: SELF bufH='+$self.sz.Y+' winRows='+($self.win.B-$self.win.T+1)+' curY='+$self.cur.Y) }",
301
- "$par=@{}; try{ Get-CimInstance Win32_Process -ErrorAction Stop | ForEach-Object { $par[[int]$_.ProcessId]=[int]$_.ParentProcessId } }catch{ L 'pump: WMI walk failed' }",
302
- "$chain=New-Object System.Collections.ArrayList; $p=$PID; for($i=0;$i -lt 12;$i++){ if(-not $par.ContainsKey($p)){break}; $pp=$par[$p]; if($pp -le 4){break}; $nm='?'; try{ $nm=(Get-Process -Id $pp -ErrorAction Stop).ProcessName }catch{}; [void]$chain.Add(@{id=$pp;name=$nm}); $p=$pp }",
303
- "L ('pump: ancestors = ' + (($chain | ForEach-Object { $_.name+'('+$_.id+')' }) -join ' -> '))",
304
301
  "$how='self'; $winId=0",
305
- "foreach($c in $chain){ [void][OCP.N]::FreeConsole(); if([OCP.N]::AttachConsole([uint32]$c.id)){ $b=Csbi (OpenConout); if($b){ L ('pump: '+$c.name+'('+$c.id+') bufH='+$b.sz.Y+' winRows='+($b.win.B-$b.win.T+1)+' curY='+$b.cur.Y); if($winId -eq 0 -and $b.sz.Y -lt 1000){ $winId=$c.id; $how=('attach:'+$c.name) } } } }",
302
+ // Given an explicit console-owner PID (the escaped painter passes it its
303
+ // own parent chain is WmiPrvSE, so the walk below would never find Windows
304
+ // Terminal), attach to it directly and skip discovery.
305
+ "$given=[int]($env:OPENCRATER_WIN_CONSOLE_PID)",
306
+ "if($given -gt 0){ [void][OCP.N]::FreeConsole(); if([OCP.N]::AttachConsole([uint32]$given)){ $winId=$given; $how='given' } L ('pump: given console pid '+$given+' attached='+($winId -eq $given)) }",
307
+ "if($winId -eq 0){",
308
+ " $par=@{}; try{ Get-CimInstance Win32_Process -ErrorAction Stop | ForEach-Object { $par[[int]$_.ProcessId]=[int]$_.ParentProcessId } }catch{ L 'pump: WMI walk failed' }",
309
+ " $chain=New-Object System.Collections.ArrayList; $p=$PID; for($i=0;$i -lt 12;$i++){ if(-not $par.ContainsKey($p)){break}; $pp=$par[$p]; if($pp -le 4){break}; $nm='?'; try{ $nm=(Get-Process -Id $pp -ErrorAction Stop).ProcessName }catch{}; [void]$chain.Add(@{id=$pp;name=$nm}); $p=$pp }",
310
+ " L ('pump: ancestors = ' + (($chain | ForEach-Object { $_.name+'('+$_.id+')' }) -join ' -> '))",
311
+ " foreach($c in $chain){ [void][OCP.N]::FreeConsole(); if([OCP.N]::AttachConsole([uint32]$c.id)){ $b=Csbi (OpenConout); if($b){ L ('pump: '+$c.name+'('+$c.id+') bufH='+$b.sz.Y+' winRows='+($b.win.B-$b.win.T+1)+' curY='+$b.cur.Y); if($winId -eq 0 -and $b.sz.Y -lt 1000){ $winId=$c.id; $how=('attach:'+$c.name) } } } }",
312
+ "}",
306
313
  "[void][OCP.N]::FreeConsole()",
307
314
  "if($winId -ne 0){ [void][OCP.N]::AttachConsole([uint32]$winId) } else { [void][OCP.N]::AttachConsole(0xFFFFFFFF); $how='attach-parent' }",
308
315
  "$h=OpenConout",
309
316
  "if($h -eq [IntPtr]-1){ L 'pump: NO CONSOLE after attach walk'; exit 1 }",
310
317
  "$fb=Csbi $h; if($fb){ L ('pump: FINAL via '+$how+' bufH='+$fb.sz.Y+' winRows='+($fb.win.B-$fb.win.T+1)+' curY='+$fb.cur.Y) } else { L ('pump: FINAL via '+$how) }",
318
+ // Report the REAL visible-console size back to the painter (over stdout, a
319
+ // pipe — the card itself goes to CONOUT$ via WriteFile, so this channel is
320
+ // free). The painter blocks on this line, then anchors the card to the true
321
+ // right edge. win.R/L/B/T are the on-screen window rect (not the scrollback
322
+ // buffer), so this is exactly what `mode con` SHOULD have returned.
323
+ "if($fb){ $cols=$fb.win.R-$fb.win.L+1; $rows=$fb.win.B-$fb.win.T+1; [Console]::Out.WriteLine('OCWIN '+$winId+' '+$cols+' '+$rows); [Console]::Out.Flush() }",
311
324
  "$m=[uint32]0;[void][OCP.N]::GetConsoleMode($h,[ref]$m);[void][OCP.N]::SetConsoleMode($h,($m -bor 4));[void][OCP.N]::SetConsoleOutputCP(65001)",
312
325
  // Stream frames straight to the console as they arrive. The painter drives
313
326
  // the timing — it repaints the overlay (live countdown, position defence)
@@ -337,7 +350,7 @@ function spawnWindowsConsoleSink() {
337
350
  }
338
351
  pdebug("spawning pump:", exe);
339
352
  const child = child_process.spawn(exe, ["-NoProfile", "-NonInteractive", "-EncodedCommand", b64], {
340
- stdio: ["pipe", errStdio, errStdio],
353
+ stdio: ["pipe", "pipe", errStdio],
341
354
  windowsHide: false
342
355
  });
343
356
  child.on("error", (e) => pdebug("pump spawn error:", exe, "-", e.message));
@@ -353,7 +366,32 @@ function spawnWindowsConsoleSink() {
353
366
  child.on("exit", () => {
354
367
  exited = true;
355
368
  });
369
+ const size = new Promise((resolve) => {
370
+ let acc = "";
371
+ let settled = false;
372
+ const done = (v) => {
373
+ if (!settled) {
374
+ settled = true;
375
+ resolve(v);
376
+ }
377
+ };
378
+ const out = child.stdout;
379
+ if (!out) return done(null);
380
+ out.setEncoding("utf8");
381
+ out.on("data", (chunk) => {
382
+ acc += chunk;
383
+ const m = acc.match(/OCWIN\s+(\d+)\s+(\d+)\s+(\d+)/);
384
+ if (m) {
385
+ pdebug("pump reported console pid", m[1], "size", m[2] + "x" + m[3]);
386
+ done({ pid: Number(m[1]), cols: Number(m[2]), rows: Number(m[3]) });
387
+ }
388
+ });
389
+ out.on("error", () => done(null));
390
+ out.on("end", () => done(null));
391
+ setTimeout(() => done(null), 2500);
392
+ });
356
393
  return {
394
+ size,
357
395
  write: (d) => {
358
396
  try {
359
397
  stdin.write(d);
@@ -389,6 +427,50 @@ function spawnWindowsConsoleSink() {
389
427
  return null;
390
428
  }
391
429
  }
430
+ function wmiSpawnDetached(cmdLine) {
431
+ try {
432
+ const esc = cmdLine.replace(/'/g, "''");
433
+ const ps = `$ErrorActionPreference='Stop';try{$r=Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{CommandLine='${esc}'};if($r.ReturnValue -eq 0){[Console]::Out.Write('OCPID '+$r.ProcessId)}else{[Console]::Out.Write('OCERR rv='+$r.ReturnValue)}}catch{[Console]::Out.Write('OCERR '+$_.Exception.Message)}`;
434
+ const b64 = Buffer.from(ps, "utf16le").toString("base64");
435
+ const res = child_process.spawnSync(
436
+ powershellPath(),
437
+ ["-NoProfile", "-NonInteractive", "-EncodedCommand", b64],
438
+ { timeout: 5e3, encoding: "utf8" }
439
+ );
440
+ const m = (res.stdout ?? "").match(/OCPID\s+(\d+)/);
441
+ if (m) return Number(m[1]);
442
+ pdebug("wmi spawn failed:", (res.stdout ?? "").trim(), "|", (res.stderr ?? "").trim());
443
+ return 0;
444
+ } catch (err) {
445
+ pdebug("wmi spawn threw", err.message);
446
+ return 0;
447
+ }
448
+ }
449
+ function escapeToPersistentPainter(payload, consolePid) {
450
+ try {
451
+ const dir = path3__namespace.join(os.homedir(), ".config", "opencrater");
452
+ fs2.mkdirSync(dir, { recursive: true });
453
+ const file = path3__namespace.join(dir, `paint-${process.pid}-${Date.now()}.json`);
454
+ fs2.writeFileSync(file, JSON.stringify(payload), "utf8");
455
+ const q = (s) => `"${s}"`;
456
+ const parts = [
457
+ q(process.execPath),
458
+ q(__filename),
459
+ "--persist",
460
+ "--payload-file",
461
+ q(file),
462
+ "--console-pid",
463
+ String(consolePid)
464
+ ];
465
+ if (DEBUG_ON) parts.push("--debug");
466
+ const pid = wmiSpawnDetached(parts.join(" "));
467
+ pdebug("escape: wmi persistent painter pid", pid);
468
+ return pid > 0;
469
+ } catch (err) {
470
+ pdebug("escape failed", err.message);
471
+ return false;
472
+ }
473
+ }
392
474
  function windowsVtOk(env) {
393
475
  if (env["WT_SESSION"]) return true;
394
476
  if (env["TERM_PROGRAM"]) return true;
@@ -728,9 +810,10 @@ function releaseLock() {
728
810
  } catch {
729
811
  }
730
812
  }
813
+ var DEBUG_ON = process.env["OPENCRATER_DEBUG"] === "1";
731
814
  function pdebug(...args) {
732
815
  try {
733
- if (process.env["OPENCRATER_DEBUG"] !== "1") return;
816
+ if (!DEBUG_ON) return;
734
817
  const line = `[opencrater-paint] ${args.map((a) => String(a)).join(" ")}`;
735
818
  console.error(line);
736
819
  try {
@@ -745,16 +828,38 @@ function pdebug(...args) {
745
828
  }
746
829
  }
747
830
  function main() {
748
- const raw = process.env["OPENCRATER_PAINT"];
831
+ const argv = process.argv.slice(2);
832
+ const argVal = (flag) => {
833
+ const i = argv.indexOf(flag);
834
+ return i >= 0 ? argv[i + 1] : void 0;
835
+ };
836
+ if (argv.includes("--debug")) {
837
+ DEBUG_ON = true;
838
+ process.env["OPENCRATER_DEBUG"] = "1";
839
+ }
840
+ const persist = argv.includes("--persist") || process.env["OPENCRATER_WIN_PERSIST"] === "1";
841
+ const consolePidArg = Number(argVal("--console-pid") ?? "0");
842
+ if (consolePidArg > 0) {
843
+ process.env["OPENCRATER_WIN_CONSOLE_PID"] = String(consolePidArg);
844
+ }
845
+ const payloadFile = argVal("--payload-file");
846
+ let raw = process.env["OPENCRATER_PAINT"];
847
+ if (payloadFile) {
848
+ try {
849
+ raw = fs2.readFileSync(payloadFile, "utf8");
850
+ } catch (err) {
851
+ pdebug("payload-file unreadable:", err.message);
852
+ }
853
+ }
749
854
  if (!raw) {
750
- pdebug("no OPENCRATER_PAINT payload in env \u2014 nothing to draw");
855
+ pdebug("no paint payload (env or --payload-file) \u2014 nothing to draw");
751
856
  return;
752
857
  }
753
858
  let payload;
754
859
  try {
755
860
  payload = JSON.parse(raw);
756
861
  } catch (err) {
757
- pdebug("OPENCRATER_PAINT payload is not valid JSON:", err.message);
862
+ pdebug("paint payload is not valid JSON:", err.message);
758
863
  return;
759
864
  }
760
865
  const ttyPath = process.env["OPENCRATER_TTY"] ?? "/dev/tty";
@@ -807,13 +912,13 @@ function main() {
807
912
  }
808
913
  const mediaRowCount = isVisualMedia ? Math.max(8, Math.min(16, Math.floor((termRows - 14) / 2) * 2)) : MEDIA_ROWS;
809
914
  const effectiveRows = sideMedia ? 5 : mediaRowCount;
810
- const width = Math.min(maxWidth, cols - 4);
915
+ let width = Math.min(maxWidth, cols - 4);
811
916
  if (width < 24) {
812
917
  sink.close();
813
918
  return;
814
919
  }
815
- const inner = width - 4;
816
- const col = Math.max(1, cols - width - 1);
920
+ let inner = width - 4;
921
+ let col = Math.max(1, cols - width - 1);
817
922
  const startRow = 2;
818
923
  const painterStartedAt = Date.now();
819
924
  const dim = color ? `${ESC}[2m` : "";
@@ -1319,12 +1424,42 @@ function main() {
1319
1424
  };
1320
1425
  if (IS_WINDOWS) {
1321
1426
  void (async () => {
1427
+ const real = sink.size ? await sink.size : null;
1428
+ if (real && real.cols >= 24) {
1429
+ cols = real.cols;
1430
+ width = Math.min(maxWidth, cols - 4);
1431
+ inner = width - 4;
1432
+ col = Math.max(1, cols - width - 1);
1433
+ pdebug("windows geometry from pump:", "cols", cols, "width", width, "col", col, "ownerPid", real.pid);
1434
+ }
1322
1435
  if (format === "image" || format === "logo" || format === "gif" || format === "video") {
1323
1436
  await upgradeVisual();
1324
1437
  } else if (format === "audio") {
1325
1438
  await upgradeAudio();
1326
1439
  }
1327
1440
  if (payload.audioUrl && format !== "audio") void upgradeAudio(payload.audioUrl);
1441
+ if (persist) {
1442
+ const duration2 = Math.min(Math.max(payload.durationMs ?? DEFAULT_DURATION_MS, 3e3), 12e4);
1443
+ deadlineAt = Date.now() + duration2;
1444
+ if (payloadFile) {
1445
+ try {
1446
+ fs2.unlinkSync(payloadFile);
1447
+ } catch {
1448
+ }
1449
+ }
1450
+ const framesNow = ansiFrames;
1451
+ const animated = !!(framesNow && framesNow.length > 1);
1452
+ paint();
1453
+ interval = setInterval(paint, animated ? ANIM_MS : REPAINT_MS);
1454
+ setTimeout(() => cleanup(), duration2);
1455
+ return;
1456
+ }
1457
+ const ownerPid = real?.pid ?? 0;
1458
+ if (ownerPid > 0 && escapeToPersistentPainter(payload, ownerPid)) {
1459
+ sink.close();
1460
+ process.exit(0);
1461
+ }
1462
+ pdebug("no job-escape (ownerPid", ownerPid, ") \u2014 bounded toast fallback");
1328
1463
  const envHold = Number(process.env["OPENCRATER_WIN_HOLD_MS"]);
1329
1464
  const holdMs = envHold > 0 ? envHold : Math.min(payload.durationMs ?? DEFAULT_DURATION_MS, 5e3);
1330
1465
  deadlineAt = Date.now() + holdMs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencrater/sdk",
3
- "version": "0.8.48",
3
+ "version": "0.8.50",
4
4
  "description": "OpenCrater SDK — sponsor cards for CLI tools and MCP servers. Fail-silent, zero dependencies.",
5
5
  "keywords": [
6
6
  "opencrater",