@wrongstack/tools 0.276.3 → 0.277.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pack.js CHANGED
@@ -15,7 +15,7 @@ import * as ts from 'typescript';
15
15
  import { toErrorMessage as toErrorMessage$1 } from '@wrongstack/core/utils/error';
16
16
  import * as dns from 'node:dns/promises';
17
17
  import * as net from 'node:net';
18
- import { Agent } from 'undici';
18
+ import { Agent, fetch } from 'undici';
19
19
  import TurndownService from 'turndown';
20
20
  import { randomUUID } from 'node:crypto';
21
21
 
@@ -714,16 +714,28 @@ function resolvePowerShell(cmd) {
714
714
  }
715
715
  return resolved;
716
716
  }
717
- var WIN32_SHELL_META = /[&|<>\r\n\0]/;
717
+ var WIN32_SHELL_META = /[&|<>"\r\n\0]/;
718
718
  function assertSafeWin32ShellArgs(args) {
719
- for (const a of args) {
720
- if (typeof a === "string" && WIN32_SHELL_META.test(a)) {
719
+ for (const arg of args) {
720
+ if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
721
721
  throw new Error(
722
- "win32 shell spawn: argument contains a shell metacharacter (one of & | < > or a newline) that could enable command injection through the .cmd/.bat wrapper \u2014 refusing to run. Offending argument: " + JSON.stringify(a)
722
+ 'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
723
723
  );
724
724
  }
725
725
  }
726
726
  }
727
+ function buildWin32CmdShimInvocation(command, args = []) {
728
+ assertSafeWin32ShellArgs([command, ...args]);
729
+ const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
730
+ return {
731
+ command: process.env["COMSPEC"] ?? "cmd.exe",
732
+ args: ["/d", "/c", line],
733
+ windowsVerbatimArguments: true
734
+ };
735
+ }
736
+ function quoteWin32CmdArg(arg) {
737
+ return `"${arg}"`;
738
+ }
727
739
 
728
740
  // src/_spawn-stream.ts
729
741
  var isWin = process.platform === "win32";
@@ -738,15 +750,16 @@ async function* spawnStream(opts) {
738
750
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
739
751
  const resolved = resolveWin32Command(opts.cmd);
740
752
  const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
741
- const cmd = needsShell ? opts.cmd : resolved;
742
- if (needsShell) assertSafeWin32ShellArgs(opts.args);
743
- const child = spawn(cmd, opts.args, {
753
+ const shim = needsShell ? buildWin32CmdShimInvocation(resolved, opts.args) : null;
754
+ const cmd = shim?.command ?? resolved;
755
+ const args = shim?.args ?? opts.args;
756
+ const child = spawn(cmd, args, {
744
757
  cwd: opts.cwd,
745
758
  env: buildChildEnv(),
746
759
  stdio: ["ignore", "pipe", "pipe"],
747
760
  windowsHide: true,
748
761
  ...isWin ? {} : { signal: opts.signal },
749
- ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
762
+ ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
750
763
  });
751
764
  const registry = getProcessRegistry();
752
765
  const pid = child.pid;
@@ -5408,9 +5421,9 @@ var designTool = {
5408
5421
  category: "Design",
5409
5422
  description: 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: "list" (menu), "use" (load+pin a kit for a stack), "foundations" (baseline), "set" (override kit colors/tokens), "materialize" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), "verify" (scan UI files for off-palette colors).',
5410
5423
  usageHint: 'Flow: `design {action:"use", kit:"minimal-clarity", stack:"web"}` \u2192 optionally `design {action:"set", set:{primary:"oklch(62% 0.2 25)"}}` \u2192 `design {action:"materialize"}` to write tokens to disk \u2192 implement against them \u2192 `design {action:"verify"}`.',
5411
- permission: "auto",
5412
- mutating: false,
5413
- capabilities: [],
5424
+ permission: "confirm",
5425
+ mutating: true,
5426
+ capabilities: ["fs.write"],
5414
5427
  timeoutMs: 15e3,
5415
5428
  inputSchema: {
5416
5429
  type: "object",
@@ -6146,10 +6159,20 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
6146
6159
  // C / C++ / native build
6147
6160
  "make",
6148
6161
  "cmake",
6162
+ "ninja",
6163
+ "clang",
6164
+ "clang-cl",
6165
+ "gcc",
6166
+ "g++",
6167
+ "link",
6168
+ "msbuild",
6149
6169
  // containers / orchestration
6150
6170
  "docker",
6151
6171
  "podman",
6152
6172
  "kubectl",
6173
+ // network (read-only intent; destructive ops still blocked by BLOCKED_ARG_PATTERNS)
6174
+ "curl",
6175
+ "wget",
6153
6176
  // common POSIX file/text utilities
6154
6177
  "pwd",
6155
6178
  "ls",
@@ -6169,7 +6192,496 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
6169
6192
  "cp",
6170
6193
  "mv",
6171
6194
  "rm",
6172
- "touch"
6195
+ "touch",
6196
+ "tar",
6197
+ // Windows-native tooling (win32). All non-destructive binaries; per-arg
6198
+ // safety is still enforced by BLOCKED_ARG_PATTERNS + the destructive-ops
6199
+ // gate in bash-kill-guard.ts. `gh` is included even though it lives at
6200
+ // "C:\Program Files\GitHub CLI\gh.exe" — resolveWin32Command handles the
6201
+ // space in the path.
6202
+ "gh",
6203
+ "where",
6204
+ "tasklist",
6205
+ "systeminfo",
6206
+ "wmic",
6207
+ "sc",
6208
+ "netstat",
6209
+ "ipconfig",
6210
+ "nslookup",
6211
+ "tracert",
6212
+ "pathping",
6213
+ // [core] Extended default allowlist (added 4b3d18d1 + this commit). All
6214
+ // non-destructive, broadly-used dev binaries. Per-arg safety is still
6215
+ // enforced by BLOCKED_ARG_PATTERNS + bash-kill-guard.ts.
6216
+ // --- Archives & compression ---
6217
+ "7z",
6218
+ "7za",
6219
+ "bzip2",
6220
+ "gzip",
6221
+ "xz",
6222
+ "unzip",
6223
+ "zip",
6224
+ "gtar",
6225
+ "bsdtar",
6226
+ "star",
6227
+ "pax",
6228
+ "cpio",
6229
+ // --- Android / mobile dev ---
6230
+ "adb",
6231
+ "fastboot",
6232
+ "sdkmanager",
6233
+ // --- DevOps / config mgmt ---
6234
+ "ansible",
6235
+ "ansible-playbook",
6236
+ "ansible-vault",
6237
+ "ansible-lint",
6238
+ "ansible-galaxy",
6239
+ "molecule",
6240
+ // --- Cloud CLIs ---
6241
+ "aws",
6242
+ "aws-vault",
6243
+ "awslocal",
6244
+ "az",
6245
+ "azcopy",
6246
+ "gcloud",
6247
+ "gsutil",
6248
+ "doctl",
6249
+ "linode-cli",
6250
+ // --- Native / C / C++ / linker tools ---
6251
+ "clang++",
6252
+ "clang-format",
6253
+ "clang-tidy",
6254
+ "clangd",
6255
+ "lld",
6256
+ "lldb",
6257
+ "ctest",
6258
+ "gmake",
6259
+ "meson",
6260
+ "conan",
6261
+ "vcpkg",
6262
+ "cl",
6263
+ "rc",
6264
+ "mt",
6265
+ "dumpbin",
6266
+ "dotnet-format",
6267
+ // --- Image / media / binary tools ---
6268
+ "convert",
6269
+ "ffmpeg",
6270
+ "ffprobe",
6271
+ "magick",
6272
+ "gs",
6273
+ "exiftool",
6274
+ // --- HTTP / fetch ---
6275
+ "wget2",
6276
+ "aria2c",
6277
+ "axel",
6278
+ "httpie",
6279
+ "hey",
6280
+ "ab",
6281
+ "wrk",
6282
+ "http",
6283
+ // --- Diff / patch / merge ---
6284
+ "diff",
6285
+ "diff3",
6286
+ "patch",
6287
+ "meld",
6288
+ "kdiff3",
6289
+ "kompare",
6290
+ // --- Encoding / file inspection ---
6291
+ "dos2unix",
6292
+ "unix2dos",
6293
+ "iconv",
6294
+ "file",
6295
+ "stat",
6296
+ "xxd",
6297
+ "hexdump",
6298
+ "od",
6299
+ "base64",
6300
+ // --- SSH / crypto / signing ---
6301
+ "ssh",
6302
+ "ssh-add",
6303
+ "ssh-keygen",
6304
+ "ssh-keyscan",
6305
+ "scp",
6306
+ "sftp",
6307
+ "rsync",
6308
+ "gpg",
6309
+ "gpg2",
6310
+ "gpg-agent",
6311
+ "openssl",
6312
+ "step",
6313
+ "keytool",
6314
+ // --- Search ---
6315
+ "egrep",
6316
+ "fgrep",
6317
+ "ag",
6318
+ "ack",
6319
+ "sift",
6320
+ "ugrep",
6321
+ "fd",
6322
+ "fdfind",
6323
+ "jq",
6324
+ "yq",
6325
+ "xq",
6326
+ "fx",
6327
+ "gron",
6328
+ // --- K8s / container ecosystem ---
6329
+ "kubectl.exe",
6330
+ "kubeadm",
6331
+ "kubelet",
6332
+ "helm",
6333
+ "k9s",
6334
+ "kustomize",
6335
+ "skaffold",
6336
+ "tilt",
6337
+ "minikube",
6338
+ "kind",
6339
+ "k3d",
6340
+ "k3s",
6341
+ "docker-compose",
6342
+ "buildah",
6343
+ "skopeo",
6344
+ "nerdctl",
6345
+ "ctr",
6346
+ "ctr.exe",
6347
+ // --- Databases ---
6348
+ "sqlite3",
6349
+ "sqlite",
6350
+ "psql",
6351
+ "pg_dump",
6352
+ "pg_restore",
6353
+ "mysql",
6354
+ "mysqladmin",
6355
+ "mysqldump",
6356
+ "mariadb",
6357
+ "mariadb-dump",
6358
+ "redis-cli",
6359
+ "redis-server",
6360
+ "memcached",
6361
+ "etcdctl",
6362
+ "consul",
6363
+ "vault",
6364
+ "nomad",
6365
+ "mongosh",
6366
+ "mongo",
6367
+ "mongoexport",
6368
+ "mongoimport",
6369
+ "mongodump",
6370
+ "mongorestore",
6371
+ // --- Windows extended (read-mostly ops) ---
6372
+ "taskkill",
6373
+ "gpupdate",
6374
+ "gpresult",
6375
+ "hostname",
6376
+ "whoami",
6377
+ "who",
6378
+ "net",
6379
+ "net1",
6380
+ // --- VCS ecosystem ---
6381
+ "glab",
6382
+ "hub",
6383
+ "tea",
6384
+ "git-lfs",
6385
+ "tig",
6386
+ "lazygit",
6387
+ // --- POSIX text utilities (extended; duplicates of pwd/ls/cat/head/tail/wc/
6388
+ // grep/find/echo/awk/mkdir/cp/mv/rm/touch from the base list above are
6389
+ // omitted — the Set is de-duplicated at runtime but a clean literal is
6390
+ // easier to maintain) ---
6391
+ "gawk",
6392
+ "tr",
6393
+ "cut",
6394
+ "paste",
6395
+ "join",
6396
+ "comm",
6397
+ "expand",
6398
+ "unexpand",
6399
+ "fold",
6400
+ "fmt",
6401
+ "nl",
6402
+ "pr",
6403
+ "column",
6404
+ "tsort",
6405
+ "tty",
6406
+ "ul",
6407
+ "units",
6408
+ "factor",
6409
+ "seq",
6410
+ "shuf",
6411
+ "look",
6412
+ "yes",
6413
+ "true",
6414
+ "false",
6415
+ "test",
6416
+ "[",
6417
+ "printf",
6418
+ "env",
6419
+ "tree",
6420
+ "locate",
6421
+ "which",
6422
+ "whereis",
6423
+ "type",
6424
+ "hash",
6425
+ "pushd",
6426
+ "popd",
6427
+ "dirs",
6428
+ "history",
6429
+ "fc",
6430
+ "jobs",
6431
+ "bg",
6432
+ "fg",
6433
+ "wait",
6434
+ "ulimit",
6435
+ "umask",
6436
+ "nice",
6437
+ "nohup",
6438
+ "timeout",
6439
+ "time",
6440
+ "trap",
6441
+ "exit",
6442
+ "return",
6443
+ "source",
6444
+ ".",
6445
+ "alias",
6446
+ "unalias",
6447
+ "set",
6448
+ "unset",
6449
+ "export",
6450
+ "readonly",
6451
+ "typeset",
6452
+ "declare",
6453
+ "local",
6454
+ "eval",
6455
+ "exec",
6456
+ // --- Process / system inspection ---
6457
+ "htop",
6458
+ "top",
6459
+ "atop",
6460
+ "glances",
6461
+ "iotop",
6462
+ "nethogs",
6463
+ "iftop",
6464
+ "lsof",
6465
+ "strace",
6466
+ "ltrace",
6467
+ "sysstat",
6468
+ "vmstat",
6469
+ "iostat",
6470
+ "mpstat",
6471
+ "sar",
6472
+ "free",
6473
+ "df",
6474
+ "du",
6475
+ "mount",
6476
+ "umount",
6477
+ "lsblk",
6478
+ "blkid",
6479
+ "kill",
6480
+ "killall",
6481
+ "pkill",
6482
+ "pgrep",
6483
+ "pidof",
6484
+ "ps",
6485
+ "ps.exe",
6486
+ // --- Network inspection ---
6487
+ "ip",
6488
+ "ss",
6489
+ "route",
6490
+ "arp",
6491
+ "arping",
6492
+ "ping",
6493
+ "ping6",
6494
+ "hping3",
6495
+ "mtr",
6496
+ "tracepath",
6497
+ "tcpdump",
6498
+ "nmap",
6499
+ "netcat",
6500
+ "nc",
6501
+ "ncat",
6502
+ "socat",
6503
+ // --- Sync / backup ---
6504
+ "rclone",
6505
+ "restic",
6506
+ "borg",
6507
+ "duplicati",
6508
+ "duplicacy",
6509
+ "syncthing",
6510
+ "syncthing-cli",
6511
+ // --- Permissions / users / ACLs (POSIX) ---
6512
+ "useradd",
6513
+ "userdel",
6514
+ "usermod",
6515
+ "groupadd",
6516
+ "groupdel",
6517
+ "groupmod",
6518
+ "chown",
6519
+ "chmod",
6520
+ "chgrp",
6521
+ "getfacl",
6522
+ "setfacl",
6523
+ "setcap",
6524
+ "getcap",
6525
+ // --- Crypto / cert mgmt (extended) ---
6526
+ "certbot",
6527
+ "mkcert",
6528
+ "jarsigner",
6529
+ // --- Editors ---
6530
+ "subl",
6531
+ "code",
6532
+ "code-insiders",
6533
+ "cursor",
6534
+ "atom",
6535
+ "nano",
6536
+ "vim",
6537
+ "nvim",
6538
+ "vi",
6539
+ "emacs",
6540
+ "helix",
6541
+ "hx",
6542
+ "micro",
6543
+ "jed",
6544
+ "ed",
6545
+ "ex",
6546
+ "mg",
6547
+ // --- Terminal multiplexers ---
6548
+ "asciinema",
6549
+ "script",
6550
+ "scriptreplay",
6551
+ "expect",
6552
+ "screen",
6553
+ "tmux",
6554
+ "byobu",
6555
+ "dtach",
6556
+ "abduco",
6557
+ // --- Calculators / REPLs / scientific ---
6558
+ "bc",
6559
+ "dc",
6560
+ "calc",
6561
+ "qalc",
6562
+ "genius",
6563
+ "octave",
6564
+ "R",
6565
+ "Rscript",
6566
+ "julia",
6567
+ "irb",
6568
+ "pry",
6569
+ "ghci",
6570
+ "stack",
6571
+ "cabal",
6572
+ "ghc",
6573
+ // --- PHP / Lua / Perl / Ruby ecosystem (extended) ---
6574
+ "php8",
6575
+ "php7",
6576
+ "phpcs",
6577
+ "phpcbf",
6578
+ "phpmd",
6579
+ "phpstan",
6580
+ "psalm",
6581
+ "lua",
6582
+ "lua5.1",
6583
+ "lua5.2",
6584
+ "lua5.3",
6585
+ "lua5.4",
6586
+ "luarocks",
6587
+ "perl",
6588
+ "cpan",
6589
+ "prove",
6590
+ "plackup",
6591
+ "rake",
6592
+ "rspec",
6593
+ "jekyll",
6594
+ "node-gyp",
6595
+ "node-pre-gyp",
6596
+ // --- JS / TS toolchain (extended) ---
6597
+ "electron",
6598
+ "electron-builder",
6599
+ "electron-forge",
6600
+ "vite-preview",
6601
+ "swc",
6602
+ "swc-cli",
6603
+ "swcpack",
6604
+ "mocha",
6605
+ "chai",
6606
+ "jasmine",
6607
+ "puppeteer",
6608
+ "lighthouse",
6609
+ // --- Linters / formatters (extended) ---
6610
+ "tslint",
6611
+ "stylelint",
6612
+ "htmlhint",
6613
+ "jshint",
6614
+ "jslint",
6615
+ "jscs",
6616
+ // --- Document conversion ---
6617
+ "pandoc",
6618
+ "weasyprint",
6619
+ "wkhtmltopdf",
6620
+ "wkhtmltoimage",
6621
+ "prince",
6622
+ "mdp",
6623
+ "markdown",
6624
+ "multimarkdown",
6625
+ "cmark",
6626
+ "cmark-gfm",
6627
+ // --- Office / spreadsheet ---
6628
+ "soffice",
6629
+ "libreoffice",
6630
+ "unoconv",
6631
+ "abiword",
6632
+ "gnumeric",
6633
+ // --- Spell / grammar ---
6634
+ "aspell",
6635
+ "hunspell",
6636
+ "enchant",
6637
+ "languagetool",
6638
+ // --- Source-highlight / diff tools ---
6639
+ "delta",
6640
+ "bat",
6641
+ "ccat",
6642
+ "hl",
6643
+ "highlight",
6644
+ "source-highlight",
6645
+ "ansifilter",
6646
+ // --- Job schedulers ---
6647
+ "pueue",
6648
+ "task-spooler",
6649
+ "ts",
6650
+ "at",
6651
+ "atd",
6652
+ "anacron",
6653
+ "fcron",
6654
+ "cronie",
6655
+ "systemd-run",
6656
+ "systemd-cat",
6657
+ // --- Plotting / visualization ---
6658
+ "gnuplot",
6659
+ "gnuplot-nox",
6660
+ "veusz",
6661
+ "scidavis",
6662
+ "grace",
6663
+ "xmgrace",
6664
+ "labplot",
6665
+ // --- Security / recon (network + web) ---
6666
+ "masscan",
6667
+ "zmap",
6668
+ "rustscan",
6669
+ "amass",
6670
+ "subfinder",
6671
+ "httpx",
6672
+ "nuclei",
6673
+ "naabu",
6674
+ "katana",
6675
+ "dnsx",
6676
+ "assetfinder",
6677
+ "findomain",
6678
+ "gau",
6679
+ "waybackurls",
6680
+ "httprobe",
6681
+ "meg",
6682
+ "subjack",
6683
+ "sublert",
6684
+ "chaos"
6173
6685
  ]);
6174
6686
  var allowedCommands = new Set(DEFAULT_ALLOWED_COMMANDS);
6175
6687
  var normalizeCmd = (c) => c.trim();
@@ -6377,17 +6889,18 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
6377
6889
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
6378
6890
  const resolved = resolveWin32Command(cmd);
6379
6891
  const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
6380
- const spawnCmd = needsShell ? cmd : resolved;
6381
- if (needsShell) assertSafeWin32ShellArgs(args);
6892
+ const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
6893
+ const spawnCmd = shim?.command ?? resolved;
6894
+ const spawnArgs = shim?.args ?? args;
6382
6895
  let child;
6383
6896
  try {
6384
- child = spawn(spawnCmd, args, {
6897
+ child = spawn(spawnCmd, spawnArgs, {
6385
6898
  cwd,
6386
6899
  env: buildChildEnv(sessionId),
6387
6900
  stdio: ["ignore", "pipe", "pipe"],
6388
6901
  windowsHide: true,
6389
6902
  ...isWin3 ? {} : { signal },
6390
- ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
6903
+ ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
6391
6904
  });
6392
6905
  } catch (err) {
6393
6906
  spool.finalize();
@@ -6482,6 +6995,7 @@ TD.addRule("stripDangerousElements", {
6482
6995
  });
6483
6996
  var MAX_BYTES = 131072;
6484
6997
  var TIMEOUT_MS = 2e4;
6998
+ var nativeGlobalFetch = globalThis.fetch;
6485
6999
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
6486
7000
  if (ALLOW_PRIVATE && !process.env["CI"]) {
6487
7001
  console.warn(
@@ -6531,6 +7045,9 @@ function getPinnedDispatcher() {
6531
7045
  }
6532
7046
  return pinnedAgent;
6533
7047
  }
7048
+ function dispatcherFetch() {
7049
+ return globalThis.fetch === nativeGlobalFetch ? fetch : globalThis.fetch;
7050
+ }
6534
7051
  var _beforeExitRegistered = false;
6535
7052
  if (!_beforeExitRegistered) {
6536
7053
  _beforeExitRegistered = true;
@@ -6566,7 +7083,7 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
6566
7083
  headers,
6567
7084
  dispatcher: getPinnedDispatcher()
6568
7085
  };
6569
- const res = await fetch(currentUrl, init);
7086
+ const res = await dispatcherFetch()(currentUrl, init);
6570
7087
  if (res.status < 300 || res.status > 399) {
6571
7088
  return res;
6572
7089
  }
@@ -6594,7 +7111,7 @@ var fetchTool = {
6594
7111
  category: "Network",
6595
7112
  description: "Fetch a URL and return its content. HTML pages are automatically converted to clean markdown. This tool has strong SSRF protections (private IPs, localhost, and cloud metadata endpoints are blocked by default).",
6596
7113
  usageHint: "Use this when you need external information (documentation, API responses, web pages, etc.).\n\nSecurity notes:\n- Only HTTPS is allowed by default.\n- Internal/private networks are blocked unless explicitly enabled via environment variable.\n- Redirects are followed but re-validated at each hop.\n- Output is capped (128KB by default) to avoid flooding context.\nPrefer this over raw `bash curl` or `bash wget`.",
6597
- permission: "confirm",
7114
+ permission: "auto",
6598
7115
  mutating: false,
6599
7116
  capabilities: ["net.outbound"],
6600
7117
  icon: "web",
@@ -6834,7 +7351,7 @@ var formatTool = {
6834
7351
  usageHint: "RUN REGULARLY:\n\n- Use on changed files before committing.\n- `check: true` verifies formatting without making changes (useful in CI-like flows).\nThis project has very consistent formatting expectations. Always ensure your changes are formatted.",
6835
7352
  permission: "confirm",
6836
7353
  mutating: true,
6837
- capabilities: ["fs.write", "shell.exec"],
7354
+ capabilities: ["fs.write", "shell.restricted"],
6838
7355
  icon: "code",
6839
7356
  timeoutMs: 6e4,
6840
7357
  inputSchema: {
@@ -8739,9 +9256,10 @@ function runOutdated(manager, args, cwd, signal) {
8739
9256
  const MAX = 1e5;
8740
9257
  const resolved = resolveWin32Command(manager);
8741
9258
  const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
8742
- const spawnCmd = needsShell ? manager : resolved;
8743
- if (needsShell) assertSafeWin32ShellArgs(args);
8744
- const child = spawn(spawnCmd, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
9259
+ const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
9260
+ const spawnCmd = shim?.command ?? resolved;
9261
+ const spawnArgs = shim?.args ?? args;
9262
+ const child = spawn(spawnCmd, spawnArgs, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {} });
8745
9263
  child.stdout?.on("data", (c) => {
8746
9264
  if (stdout.length < MAX) stdout += c.toString();
8747
9265
  });
@@ -9782,7 +10300,7 @@ var searchTool = {
9782
10300
  category: "Search",
9783
10301
  description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.",
9784
10302
  usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- Supports duckduckgo (default), google, and bing sources.\n- Set `skip_cache: true` to force a fresh search.\n- This is often better than the model trying to recall outdated knowledge.",
9785
- permission: "confirm",
10303
+ permission: "auto",
9786
10304
  mutating: false,
9787
10305
  capabilities: ["net.outbound"],
9788
10306
  icon: "search",
@@ -9845,15 +10363,15 @@ var searchTool = {
9845
10363
  };
9846
10364
  yield {
9847
10365
  type: "partial_output",
9848
- text: `${results.length} cached results from ${source}`,
9849
- data: { count: results.length, cached: true }
10366
+ text: `${results.length} cached results from ${entry.source}`,
10367
+ data: { count: results.length, cached: true, source: entry.source }
9850
10368
  };
9851
10369
  yield {
9852
10370
  type: "final",
9853
10371
  output: {
9854
10372
  query: input.query,
9855
10373
  results: results.slice(0, num),
9856
- source,
10374
+ source: entry.source,
9857
10375
  truncated: results.length >= num,
9858
10376
  cached: true
9859
10377
  }
@@ -9867,6 +10385,7 @@ var searchTool = {
9867
10385
  data: { source, query: input.query, cached: false }
9868
10386
  };
9869
10387
  let rawResults;
10388
+ let effectiveSource = source;
9870
10389
  switch (source) {
9871
10390
  case "duckduckgo":
9872
10391
  rawResults = await duckduckgoSearch(input.query, num, opts.signal);
@@ -9883,24 +10402,24 @@ var searchTool = {
9883
10402
  field: "source"
9884
10403
  });
9885
10404
  }
9886
- const seenUrls = /* @__PURE__ */ new Set();
9887
- const deduped = [];
9888
- for (const r of rawResults) {
9889
- const noQuery = r.url.split("?")[0] ?? r.url;
9890
- const normalized = noQuery.split("#")[0] ?? r.url;
9891
- if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
9892
- seenUrls.add(normalized);
9893
- deduped.push(r);
9894
- }
10405
+ let ranked = rankSearchResults(rawResults, input.query);
10406
+ if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
10407
+ yield {
10408
+ type: "log",
10409
+ text: `${source} returned no relevant static results; falling back to duckduckgo`,
10410
+ data: { source, fallback: "duckduckgo", query: input.query }
10411
+ };
10412
+ rawResults = await duckduckgoSearch(input.query, num, opts.signal);
10413
+ ranked = rankSearchResults(rawResults, input.query);
10414
+ effectiveSource = "duckduckgo";
9895
10415
  }
9896
- const ranked = scoreResults(deduped, input.query);
9897
10416
  const finalResults = ranked.slice(0, num);
9898
- cache.set(cacheKey, { results: ranked, timestamp: Date.now() });
10417
+ cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
9899
10418
  pruneStaleCacheEntries();
9900
10419
  yield {
9901
10420
  type: "partial_output",
9902
- text: `${finalResults.length} results from ${source}`,
9903
- data: { count: finalResults.length, cached: false }
10421
+ text: `${finalResults.length} results from ${effectiveSource}`,
10422
+ data: { count: finalResults.length, cached: false, source: effectiveSource }
9904
10423
  };
9905
10424
  yield {
9906
10425
  type: "final",
@@ -9911,7 +10430,7 @@ var searchTool = {
9911
10430
  url: r.url,
9912
10431
  snippet: r.snippet
9913
10432
  })),
9914
- source,
10433
+ source: effectiveSource,
9915
10434
  truncated: finalResults.length >= num,
9916
10435
  cached: false
9917
10436
  }
@@ -9924,6 +10443,19 @@ function pruneStaleCacheEntries() {
9924
10443
  if (entry.timestamp < cutoff) cache.delete(key);
9925
10444
  }
9926
10445
  }
10446
+ function rankSearchResults(results, query) {
10447
+ const seenUrls = /* @__PURE__ */ new Set();
10448
+ const deduped = [];
10449
+ for (const r of results) {
10450
+ const noQuery = r.url.split("?")[0] ?? r.url;
10451
+ const normalized = noQuery.split("#")[0] ?? r.url;
10452
+ if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
10453
+ seenUrls.add(normalized);
10454
+ deduped.push(r);
10455
+ }
10456
+ }
10457
+ return scoreResults(deduped, query);
10458
+ }
9927
10459
  function scoreResults(results, query) {
9928
10460
  const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
9929
10461
  return results.map((r) => {
@@ -9937,6 +10469,15 @@ function scoreResults(results, query) {
9937
10469
  return { ...r, score };
9938
10470
  }).sort((a, b) => b.score - a.score);
9939
10471
  }
10472
+ function shouldFallbackToDuckDuckGo(results, query) {
10473
+ if (results.length === 0) return true;
10474
+ const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length >= 3);
10475
+ if (terms.length === 0) return false;
10476
+ return !results.some((r) => {
10477
+ const haystack = `${r.title} ${r.url} ${r.snippet}`.toLowerCase();
10478
+ return terms.some((term) => haystack.includes(term));
10479
+ });
10480
+ }
9940
10481
  async function duckduckgoSearch(query, num, signal) {
9941
10482
  const encoded = encodeURIComponent(query);
9942
10483
  const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
@@ -9961,14 +10502,19 @@ function takeFrom(iter, max) {
9961
10502
  }
9962
10503
  function parseDuckDuckGo(html, num) {
9963
10504
  const results = [];
9964
- const snippetRegex = /<a class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi;
9965
- const snippet2Regex = /<a class="result-snippet"[^>]*>([^<]+)<\/a>/gi;
10505
+ const linkRegex = /<a\b([^>]*\bclass=(["'])[^"']*\bresult-link\b[^"']*\2[^>]*)>([\s\S]*?)<\/a>/gi;
10506
+ const snippetRegex = /<([a-z0-9]+)\b([^>]*\bclass=(["'])[^"']*\bresult-snippet\b[^"']*\3[^>]*)>([\s\S]*?)<\/\1>/gi;
9966
10507
  const linkMatches = takeFrom(
9967
- [...html.matchAll(snippetRegex)].filter((m) => m[1] && m[2]).map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),
10508
+ [...html.matchAll(linkRegex)].map((m) => {
10509
+ const attrs = expectDefined(m[1]);
10510
+ const href = getHtmlAttr(attrs, "href");
10511
+ const title = stripTags(expectDefined(m[3]));
10512
+ return href && title ? { url: normalizeDuckDuckGoUrl(href), title } : void 0;
10513
+ }).filter((m) => m !== void 0),
9968
10514
  num
9969
10515
  );
9970
10516
  const snippetMatches = takeFrom(
9971
- [...html.matchAll(snippet2Regex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),
10517
+ [...html.matchAll(snippetRegex)].filter((m) => m[4]).map((m) => stripTags(expectDefined(m[4]))),
9972
10518
  num
9973
10519
  );
9974
10520
  for (let i = 0; i < linkMatches.length && i < num; i++) {
@@ -9984,6 +10530,24 @@ function parseDuckDuckGo(html, num) {
9984
10530
  }
9985
10531
  return results;
9986
10532
  }
10533
+ function getHtmlAttr(attrs, name) {
10534
+ const quoted = new RegExp(`\\b${name}\\s*=\\s*(["'])(.*?)\\1`, "i").exec(attrs);
10535
+ if (quoted?.[2]) return decodeHtmlEntities(quoted[2]);
10536
+ const unquoted = new RegExp(`\\b${name}\\s*=\\s*([^\\s>]+)`, "i").exec(attrs);
10537
+ return unquoted?.[1] ? decodeHtmlEntities(unquoted[1]) : void 0;
10538
+ }
10539
+ function normalizeDuckDuckGoUrl(raw) {
10540
+ if (raw.startsWith("//")) return `https:${raw}`;
10541
+ if (!raw.startsWith("/")) return raw;
10542
+ if (!raw.startsWith("/l/")) return raw;
10543
+ try {
10544
+ const url = new URL(raw, "https://duckduckgo.com");
10545
+ const uddg = url.searchParams.get("uddg");
10546
+ return uddg?.startsWith("http") ? uddg : url.toString();
10547
+ } catch {
10548
+ return raw;
10549
+ }
10550
+ }
9987
10551
  async function googleSearch(query, num, signal) {
9988
10552
  const encoded = encodeURIComponent(query);
9989
10553
  const url = `https://www.google.com/search?q=${encoded}&hl=en`;
@@ -10025,29 +10589,53 @@ async function bingSearch(query, num, signal) {
10025
10589
  }
10026
10590
  function parseBingResults(html, num) {
10027
10591
  const results = [];
10028
- const titleRegex = /<h2[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>\s*<\/h2>/gi;
10029
- const snippetRegex = /<p[^>]*class="[^"]*b_paractl[^"]*"[^>]*>([^<]+)<\/p>/gi;
10030
- const entries = takeFrom(
10031
- [...html.matchAll(titleRegex)].filter((m) => m[1] && m[2]).map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),
10032
- num
10033
- );
10034
- const snippets = takeFrom(
10035
- [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),
10036
- num
10037
- );
10592
+ const blocks = [...html.matchAll(/<li\b[^>]*class=(["'])[^"']*\bb_algo\b[^"']*\1[^>]*>([\s\S]*?)(?=<li\b[^>]*class=(["'])[^"']*\bb_algo\b[^"']*\3|<\/ol>)/gi)].map((m) => expectDefined(m[2]));
10593
+ const candidates = blocks.length > 0 ? blocks : [html];
10594
+ const entries = takeFrom(candidates.flatMap((block) => {
10595
+ const titleMatch = /<h2[^>]*>\s*<a\b([^>]*)>([\s\S]*?)<\/a>\s*<\/h2>/i.exec(block);
10596
+ if (!titleMatch) return [];
10597
+ const href = getHtmlAttr(expectDefined(titleMatch[1]), "href");
10598
+ const title = stripTags(expectDefined(titleMatch[2]));
10599
+ if (!href || !title) return [];
10600
+ const snippetMatch = /<p\b[^>]*class=(["'])[^"']*\b(?:b_paractl|b_lineclamp\d*)\b[^"']*\1[^>]*>([\s\S]*?)<\/p>/i.exec(block) ?? /<p\b[^>]*>([\s\S]*?)<\/p>/i.exec(block);
10601
+ const snippet = snippetMatch ? stripTags(expectDefined(snippetMatch.at(-1))) : "";
10602
+ return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
10603
+ }), num);
10038
10604
  for (let i = 0; i < entries.length; i++) {
10039
10605
  const entry = entries[i];
10040
10606
  if (entry) {
10041
10607
  results.push({
10042
10608
  title: entry.title ?? "",
10043
10609
  url: entry.url ?? "",
10044
- snippet: snippets[i] ?? "",
10610
+ snippet: entry.snippet ?? "",
10045
10611
  score: 1
10046
10612
  });
10047
10613
  }
10048
10614
  }
10049
10615
  return results;
10050
10616
  }
10617
+ function normalizeBingUrl(raw) {
10618
+ try {
10619
+ const url = new URL(raw);
10620
+ if (url.hostname.endsWith("bing.com") && url.pathname.startsWith("/ck/")) {
10621
+ const encoded = url.searchParams.get("u");
10622
+ const decoded = decodeBingTarget(encoded);
10623
+ if (decoded?.startsWith("http")) return decoded;
10624
+ }
10625
+ } catch {
10626
+ }
10627
+ return raw;
10628
+ }
10629
+ function decodeBingTarget(encoded) {
10630
+ if (!encoded) return void 0;
10631
+ const payload = encoded.startsWith("a1") ? encoded.slice(2) : encoded;
10632
+ try {
10633
+ const padded = payload.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payload.length / 4) * 4, "=");
10634
+ return Buffer.from(padded, "base64").toString("utf8");
10635
+ } catch {
10636
+ return void 0;
10637
+ }
10638
+ }
10051
10639
  async function fetchWithTimeout(url, signal, timeoutMs) {
10052
10640
  const controller = new AbortController();
10053
10641
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -10075,7 +10663,10 @@ function anySignal(...signals) {
10075
10663
  return AbortSignal.any(signals);
10076
10664
  }
10077
10665
  function stripTags(html) {
10078
- return html.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
10666
+ return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
10667
+ }
10668
+ function decodeHtmlEntities(text) {
10669
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
10079
10670
  }
10080
10671
  var setWorkingDirTool = {
10081
10672
  name: "set_working_dir",