summrise-agent 1.2.469 → 1.2.471

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/bin/summrise.js CHANGED
@@ -206,6 +206,34 @@ function apiPost(pathname, body) {
206
206
  throw new Error("gateway bad response: " + out.slice(0, 120));
207
207
  }
208
208
  }
209
+ // ── `sh()` IS `cmd.exe /d /s /c "<string>"`, AND DOUBLE QUOTES DO NOT STOP `%` ──
210
+ //
211
+ // Quoting a value stops cmd from reading `&`, `|`, `>` and `^` as OPERATORS (that is
212
+ // the `psArgv` incident below). It does NOT stop PERCENT EXPANSION: cmd expands
213
+ // `%NAME%` inside a quoted region exactly as it does outside one, so an interpolated
214
+ // PATH that contains a `%` is still a variable reference rather than data. `%` is
215
+ // legal in an NTFS name, and the install/data dirs here are chosen by the operator,
216
+ // so this is reachable rather than theoretical.
217
+ //
218
+ // MEASURED on d1 (2026-09-25) through this exact `spawnSync(cmd, {shell:true})` path;
219
+ // each arrow is the child's own stdout:
220
+ // `echo [%CMDCMDLINE%]` -> [C:\WINDOWS\system32\cmd.exe /d /s /c "echo [%CMDCMDLINE%]"]
221
+ // `echo ["C:\%ProgramFiles%\Summrise"]` -> ["C:\C:\Program Files\Summrise"] (rewritten)
222
+ // shell `powershell ... "Write-Output '%ProgramFiles%'"` -> C:\Program Files (value lost)
223
+ // argv `powershell ... Write-Output '%ProgramFiles%'` -> %ProgramFiles% (verbatim)
224
+ //
225
+ // AND THE BATCH-FILE ESCAPE IS THE WRONG FIX: cmd's `%%` -> `%` rule belongs to batch
226
+ // FILES. On a `/d /s /c` command line that same measurement prints `[100%%]` for
227
+ // `echo [100%%]` — the doubling is NOT collapsed, so "escaping" a value with `%%`
228
+ // would put a DOUBLED sign into the path, which is worse than the defect. What
229
+ // remains is argv (no cmd in the path at all — `ps()` below hands PowerShell its
230
+ // script directly) or a value that provably cannot contain `%`. A site that keeps a
231
+ // cmd line anyway says which of those it is in a `cmd-% <kind>: <reason>` comment,
232
+ // and test/cli.test.mjs fails any interpolating site that does not.
233
+ //
234
+ // `rmdir`, `taskkill`, `sc`, `schtasks` and `reg` cannot take argv (the first is a
235
+ // cmd BUILTIN — there is no rmdir.exe), so a path handed to one of those must reach
236
+ // cmd as a `%NAME%` reference rather than as text.
209
237
  function sh(cmd, opts = {}) {
210
238
  return (0, child_process_1.spawnSync)(cmd, { shell: true, stdio: "inherit", ...opts });
211
239
  }
@@ -1055,7 +1083,9 @@ function ensureElectron() {
1055
1083
  if (!zip)
1056
1084
  return null;
1057
1085
  fs.mkdirSync(dist, { recursive: true });
1058
- sh(`powershell -NoProfile -Command "Expand-Archive -Force -Path '${(0, exports.psq)(zip)}' -DestinationPath '${(0, exports.psq)(dist)}'"`);
1086
+ // argv via `ps()`, not a cmd string: BOTH values are paths, and cmd expands `%NAME%`
1087
+ // even inside the double quotes the old form carried (see `sh()`).
1088
+ ps(`Expand-Archive -Force -Path '${(0, exports.psq)(zip)}' -DestinationPath '${(0, exports.psq)(dist)}'`);
1059
1089
  if (!fs.existsSync(path.join(dist, "electron.exe")))
1060
1090
  return null;
1061
1091
  try {
@@ -1736,15 +1766,20 @@ const commands = {
1736
1766
  sh("reg delete HKLM\\SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application\\Cloudflared /f >NUL 2>&1");
1737
1767
  // 4. Stale update-busy marker (a crashed update would lock updates).
1738
1768
  const BUSY = path.join(process.env.ProgramData || "C:\\ProgramData", "SummriseAgent", "update-busy");
1739
- sh(`powershell -NoProfile -Command "Remove-Item -Force -ErrorAction SilentlyContinue '${(0, exports.psq)(BUSY)}'"`);
1769
+ // `ps()` (argv) rather than `sh()`: BUSY is a PATH under %ProgramData%, and cmd
1770
+ // expands `%NAME%` in a value even when the value is quoted (see `sh()`).
1771
+ ps(`Remove-Item -Force -ErrorAction SilentlyContinue '${(0, exports.psq)(BUSY)}'`);
1740
1772
  // 5. Refresh the BOXED playwright bundle: delete the old tree first so a
1741
1773
  // removed package/version never leaves stale files behind.
1742
1774
  // round-163: kill the runner/bridge node processes FIRST — a running
1743
1775
  // node.exe holds its image file locked, the Remove-Item/Expand-Archive
1744
1776
  // pair silently skipped it, and the device was left with a playwright
1745
1777
  // dir WITHOUT node.exe (bridge could never spawn again; observed d1).
1746
- sh(`powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'node.exe' -and $_.CommandLine -like '*${(0, exports.psq)(DIR)}*playwright*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"`);
1747
- sh(`powershell -NoProfile -Command "Remove-Item -Recurse -Force -ErrorAction SilentlyContinue '${(0, exports.psq)(PW_DIR)}'"`);
1778
+ // argv (`ps()`), because DIR is the install dir: an operator-chosen PATH, so cmd
1779
+ // could find a `%NAME%` in it — and this script's `|` and `{ }` are only data
1780
+ // because no shell parses the line at all.
1781
+ ps(`Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'node.exe' -and $_.CommandLine -like '*${(0, exports.psq)(DIR)}*playwright*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`);
1782
+ ps(`Remove-Item -Recurse -Force -ErrorAction SilentlyContinue '${(0, exports.psq)(PW_DIR)}'`);
1748
1783
  // 6. Legacy install dirs from retired installers (C:\summrise-agent /
1749
1784
  // D:\summrise-agent). If the registry now points at a DIFFERENT dir and a
1750
1785
  // legacy dir exists, it is a residue of the old channel — remove it
@@ -1753,6 +1788,9 @@ const commands = {
1753
1788
  for (const legacy of ["C:\\summrise-agent", "D:\\summrise-agent"]) {
1754
1789
  if (legacy !== DIR && fs.existsSync(legacy)) {
1755
1790
  console.log("setup: removing legacy install dir", legacy);
1791
+ // cmd-% literal: `legacy` is one of the two STRING LITERALS in the array on the
1792
+ // `for` line above, so this value cannot carry a `%` — reviewed, left on the cmd
1793
+ // line (the pin re-reads that array and checks it is literals all the way down).
1756
1794
  sh(`powershell -NoProfile -Command "Remove-Item -Recurse -Force -ErrorAction SilentlyContinue '${(0, exports.psq)(legacy)}'"`);
1757
1795
  }
1758
1796
  }
@@ -1761,6 +1799,12 @@ const commands = {
1761
1799
  // the retired orphans. Repair-only (helper checks link existence).
1762
1800
  // Backslash-escape the .lnk Arguments double quotes for -Command.
1763
1801
  console.log("setup: reconciling desktop shortcut (retired-exe repair)...");
1802
+ // cmd-% residual: THE ONE SITE THIS ROUND DID NOT FIX, and it is not "safe" — both
1803
+ // values are paths (SCRIPTS_DIR, DESK_DIR). It is left because the same line depends
1804
+ // on the cmd-level `\"` escaping above, which is its own defect (cmd does not unescape
1805
+ // `\"`: the quoted region ends at the first `"` and the call works by accident), and
1806
+ // argv-ing this call means deleting that `.replace()` in the same change. Named rather
1807
+ // than silently exempted: the pin allows exactly ONE such site and fails a second.
1764
1808
  sh(`powershell -NoProfile -Command "${deskShortcutRepairPs((0, exports.psq)(SCRIPTS_DIR), (0, exports.psq)(DESK_DIR), "Write-Host").join("; ").replace(/"/g, '\\"')}"`);
1765
1809
  // C1: write the registry single source of truth (InstallDir; DataDir
1766
1810
  // defaults to %ProgramData%\Summrise). Everything else reads it back.
@@ -1825,13 +1869,15 @@ const commands = {
1825
1869
  if (PW_ZIP) {
1826
1870
  const pwDir = PW_DIR;
1827
1871
  fs.mkdirSync(pwDir, { recursive: true });
1828
- sh(`powershell -NoProfile -Command "Expand-Archive -Force -Path '${(0, exports.psq)(PW_ZIP)}' -DestinationPath '${(0, exports.psq)(COMPONENTS_DIR)}'"`);
1872
+ // argv (`ps()`): PW_ZIP and COMPONENTS_DIR are paths, and cmd expands `%NAME%`
1873
+ // inside quotes (see `sh()`).
1874
+ ps(`Expand-Archive -Force -Path '${(0, exports.psq)(PW_ZIP)}' -DestinationPath '${(0, exports.psq)(COMPONENTS_DIR)}'`);
1829
1875
  // round-163: the whole point of the bundle is node.exe — VERIFY it
1830
1876
  // landed (a silently-missing copy killed the bridge forever on d1).
1831
1877
  // One retry, then fail loudly: a half-staged bundle is worse than none.
1832
1878
  if (!fs.existsSync(path.join(pwDir, "node.exe"))) {
1833
1879
  console.log("setup: node.exe missing after expand -- retrying once");
1834
- sh(`powershell -NoProfile -Command "Expand-Archive -Force -Path '${(0, exports.psq)(PW_ZIP)}' -DestinationPath '${(0, exports.psq)(COMPONENTS_DIR)}'"`);
1880
+ ps(`Expand-Archive -Force -Path '${(0, exports.psq)(PW_ZIP)}' -DestinationPath '${(0, exports.psq)(COMPONENTS_DIR)}'`);
1835
1881
  }
1836
1882
  // "node_modules verified" was in the message while only node.exe was checked —
1837
1883
  // and node_modules is where the entry point the agent actually runs lives, so a
@@ -1932,16 +1978,38 @@ const commands = {
1932
1978
  // filter. `pre` is a PATH (npm's global prefix), so it is DOUBLE-QUOTED for the
1933
1979
  // cmd layer: unquoted, a prefix like `C:\Program Files\nodejs` was split at its
1934
1980
  // space and npm installed the CLI somewhere else while reporting success.
1981
+ //
1982
+ // QUOTING IS NOT ENOUGH FOR `pre`, because cmd expands `%NAME%` inside quotes —
1983
+ // so the path does not appear in this command line at all. `%SUMMRISE_NPM_PREFIX%`
1984
+ // does, and the value travels in the environment below. MEASURED on d1, through
1985
+ // npm.cmd ITSELF (the target here is a `.cmd` BATCH shim, and a batch file re-parses
1986
+ // its own arguments, so the /c line was not the only parser to clear):
1987
+ // `npm config get prefix --prefix "%SUMMRISE_PROBE2%"` -> C:\%ProgramFiles%\Summrise
1988
+ // `npm config get prefix --prefix "C:\%ProgramFiles%\Summrise"` -> C:\C:\Program Files\Summrise
1989
+ // The reference is expanded ONCE and what it produced is not re-scanned, so a prefix
1990
+ // literally named `C:\%ProgramFiles%\Summrise` arrives as exactly that, while the raw
1991
+ // interpolation of the same value does not; the quotes still hold an `&` inside one
1992
+ // argument. The pin requires every `%NAME%` on a cmd line to be defined in the same
1993
+ // call's `env`.
1994
+ //
1995
+ // cmd-% version: `${selfVer}` is package.json's own version — a semver, read from
1996
+ // the file the tests read too, so it cannot carry a `%` (and it must be quoted for
1997
+ // the cmd layer regardless, or npm reads an empty spec).
1935
1998
  const inst = (0, child_process_1.spawnSync)("npm", [
1936
1999
  "i",
1937
2000
  "-g",
1938
2001
  `"summrise-agent@${selfVer}"`,
1939
2002
  "--prefix",
1940
- `"${pre}"`,
2003
+ `"%SUMMRISE_NPM_PREFIX%"`,
1941
2004
  "--registry=https://registry.npmjs.org/",
1942
2005
  "--no-audit",
1943
2006
  "--no-fund",
1944
- ], { encoding: "utf8", shell: true, timeout: 300000 });
2007
+ ], {
2008
+ encoding: "utf8",
2009
+ shell: true,
2010
+ timeout: 300000,
2011
+ env: { ...process.env, SUMMRISE_NPM_PREFIX: pre },
2012
+ });
1945
2013
  console.log(inst.status === 0
1946
2014
  ? `setup: ${selfVer} installed globally -- \`summrise status\` works from any shell`
1947
2015
  : `setup: WARNING -- could not install the CLI globally; run: npm i -g summrise-agent@${selfVer}`);
@@ -1967,13 +2035,16 @@ const commands = {
1967
2035
  // regardless". Worse, the `catch` below could not fire for this — `sh()` RETURNS, it does not
1968
2036
  // throw — so the only thing it ever caught was `writeFileSync`.
1969
2037
  //
1970
- // ONE CALL, and its status is READ. DOUBLE quotes in the command: a cmd-layer argument, because the
1971
- // single-quoted version this shipped with reached PowerShell with the quotes included and died on
1972
- // the device with "unsupported path format" for a path that was perfectly valid.
2038
+ // ONE CALL, and its status is READ. The path is an ARGV ELEMENT now, and that is the fix for
2039
+ // both quoting attempts that came before it: the single-quoted version this shipped with
2040
+ // reached PowerShell with the quotes INCLUDED and died on the device with "unsupported path
2041
+ // format" for a path that was perfectly valid, and the double-quoted version that replaced it
2042
+ // still handed a PATH to cmd — where `%NAME%` expands inside the quotes (see `sh()`). argv
2043
+ // needs no quotes at all: PowerShell receives the path verbatim.
1973
2044
  //
1974
2045
  // A WARNING, NOT FATAL, unlike the agent task: headless installs have no SummriseDesktop, and the
1975
2046
  // registration script itself exits quietly when electron is already alive.
1976
- const reg302 = sh(`powershell -NoProfile -ExecutionPolicy Bypass -File "${reg}"`, { stdio: "pipe" });
2047
+ const reg302 = (0, child_process_1.spawnSync)("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", reg], { stdio: "pipe" });
1977
2048
  if (reg302 && reg302.status === 0) {
1978
2049
  console.log("setup: SummriseDesktop registered (logon + a 5-minute watchdog) and started");
1979
2050
  }
@@ -2224,11 +2295,14 @@ const commands = {
2224
2295
  // The register script is WRITTEN here (a machine that never ran setup still gets a working
2225
2296
  // task, from the same builder setup uses) but NOT run: desktop-start.ps1 runs it only when
2226
2297
  // the task is genuinely missing.
2227
- // ONE STRING, NOT an args array. With `shell: true` Node concatenates the arguments and warns
2228
- // about it (DEP0190: "the arguments are not escaped, only concatenated") — a warning the
2229
- // operator saw on the device the first time this command worked. The path is double-quoted,
2230
- // which is what -File accepts at the cmd layer; nothing here needs an argument list.
2231
- const r = (0, child_process_1.spawnSync)(`powershell -NoProfile -ExecutionPolicy Bypass -File "${start}"`, { encoding: "utf8", shell: true });
2298
+ // ARGV, NO SHELL — the path is the reason. `shell: true` JOINS an args array into one cmd.exe
2299
+ // line and warns about it (DEP0190: "the arguments are not escaped, only concatenated" — the
2300
+ // warning the operator saw on the device the first time this command worked), and that line is
2301
+ // then re-parsed a second time: `%NAME%` expands even inside the double quotes the old
2302
+ // one-string form carried (see `sh()`), and this path lives under the install dir, which the
2303
+ // operator chooses. Passed as argv, the path reaches PowerShell VERBATIM as one element and
2304
+ // DEP0190 goes with the shell instead of being worked around.
2305
+ const r = (0, child_process_1.spawnSync)("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", start], { encoding: "utf8" });
2232
2306
  const out = String(r.stdout || "").trim();
2233
2307
  if (out.endsWith("already-running")) {
2234
2308
  console.log("desktop: the Summrise shell is ALREADY RUNNING (look for the window, or the tray icon)");
@@ -3123,8 +3197,15 @@ const commands = {
3123
3197
  sh("sc stop Cloudflared 2>NUL");
3124
3198
  sh("sc delete Cloudflared 2>NUL");
3125
3199
  sh("reg delete HKLM\\SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application\\Cloudflared /f 2>NUL");
3126
- // program dir + registry
3127
- sh(`rmdir /s /q "${DIR}"`);
3200
+ // program dir + registry.
3201
+ //
3202
+ // NOT `rmdir /s /q "<DIR>"`: `rmdir` is a cmd BUILTIN — there is no rmdir.exe to hand argv
3203
+ // to — so a PATH given to it is text that cmd re-parses, and `%NAME%` expands even inside
3204
+ // those quotes (see `sh()`). DIR is the install dir, i.e. operator-chosen. `ps()` runs the
3205
+ // same deletion (Remove-Item -Recurse -Force, the form the legacy loop below already uses
3206
+ // because it also copes with locked/read-only files) with the path as argv. The VERIFY below
3207
+ // is what decides, which is why silence on failure is acceptable here.
3208
+ ps(`Remove-Item -LiteralPath '${(0, exports.psq)(DIR)}' -Recurse -Force -ErrorAction SilentlyContinue`);
3128
3209
  // Legacy install dirs from retired installers (C:\summrise-agent /
3129
3210
  // D:\summrise-agent) — uninstall must leave NO residue anywhere.
3130
3211
  for (const legacy of ["C:\\summrise-agent", "D:\\summrise-agent"]) {
@@ -3132,6 +3213,9 @@ const commands = {
3132
3213
  console.log("uninstall: removing legacy install dir", legacy);
3133
3214
  // PowerShell Remove-Item -Recurse -Force handles locked/read-only
3134
3215
  // files better than rmdir; retry once after a short wait.
3216
+ //
3217
+ // cmd-% literal: `legacy` is one of the two STRING LITERALS in the array on the `for`
3218
+ // line above, so this value cannot carry a `%` — the pin re-reads that array.
3135
3219
  sh(`powershell -NoProfile -Command "Remove-Item -LiteralPath '${(0, exports.psq)(legacy)}' -Recurse -Force -ErrorAction SilentlyContinue; Start-Sleep -Milliseconds 500; Remove-Item -LiteralPath '${(0, exports.psq)(legacy)}' -Recurse -Force -ErrorAction SilentlyContinue"`);
3136
3220
  if (fs.existsSync(legacy)) {
3137
3221
  console.log("uninstall: WARNING -- legacy dir still present:", legacy);
@@ -3167,8 +3251,10 @@ const commands = {
3167
3251
  console.log("uninstall: program dir + registry removed");
3168
3252
  }
3169
3253
  if (purge) {
3170
- sh(`rmdir /s /q "${DATA}"`);
3171
- // A failed rmdir used to print "data dir purged" anyway.
3254
+ // Same as the program dir above: `rmdir` is a builtin, so its path would be text cmd
3255
+ // re-parses (`%NAME%` expands inside quotes), and DATA is a path the operator can remap.
3256
+ ps(`Remove-Item -LiteralPath '${(0, exports.psq)(DATA)}' -Recurse -Force -ErrorAction SilentlyContinue`);
3257
+ // A failed removal used to print "data dir purged" anyway.
3172
3258
  if (fs.existsSync(DATA)) {
3173
3259
  console.error(`uninstall: FAILED to purge the data dir -- ${DATA} is still present`);
3174
3260
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "summrise-agent",
3
- "version": "1.2.469",
3
+ "version": "1.2.471",
4
4
  "description": "Summrise Agent — Windows device MCP server (terminal/SSH/serial) + panel. Self-hosted npm distribution.",
5
5
  "//build": "唯一构建路径:在本目录执行 npm run build。它是自包含的:经 npm exec 按需拉取 typescript@5,无需预装任何 node_modules(干净检出可跑;旧脚本引用 ../summrise-desktop-electron/node_modules/.bin/tsc,干净检出必 ENOENT)。必须用 npm exec --package … -- tsc 的形式:npx 会吃掉传给 tsc 的 -p/--outDir 等参数(round-435:npx 写法从未真正跑通过)。先编 electron TS(--noCheck 纯 emit,与 release.yml 一致)同步两处 src/*.js,再编 src/summrise.ts 到 bin/summrise.js 并校验 round-298 标记(弱 oracle,只证某次构建发生,新鲜性由 publish-release.sh 的 tsc 重编 + cmp 门保证)。",
6
6
  "bin": {
Binary file