@wrongstack/tools 0.276.4 → 0.277.1

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/index.js CHANGED
@@ -10,7 +10,7 @@ import { statSync, mkdirSync, createWriteStream } from 'node:fs';
10
10
  import { toErrorMessage as toErrorMessage$1 } from '@wrongstack/core/utils/error';
11
11
  import * as dns from 'node:dns/promises';
12
12
  import * as net from 'node:net';
13
- import { Agent } from 'undici';
13
+ import { Agent, fetch } from 'undici';
14
14
  import TurndownService from 'turndown';
15
15
  import { toErrorMessage as toErrorMessage$2 } from '@wrongstack/core/utils';
16
16
  import { randomUUID } from 'node:crypto';
@@ -2762,16 +2762,28 @@ function resolvePowerShell(cmd) {
2762
2762
  }
2763
2763
  return resolved;
2764
2764
  }
2765
- var WIN32_SHELL_META = /[&|<>\r\n\0]/;
2765
+ var WIN32_SHELL_META = /[&|<>"\r\n\0]/;
2766
2766
  function assertSafeWin32ShellArgs(args) {
2767
- for (const a of args) {
2768
- if (typeof a === "string" && WIN32_SHELL_META.test(a)) {
2767
+ for (const arg of args) {
2768
+ if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
2769
2769
  throw new Error(
2770
- "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)
2770
+ '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)
2771
2771
  );
2772
2772
  }
2773
2773
  }
2774
2774
  }
2775
+ function buildWin32CmdShimInvocation(command, args = []) {
2776
+ assertSafeWin32ShellArgs([command, ...args]);
2777
+ const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
2778
+ return {
2779
+ command: process.env["COMSPEC"] ?? "cmd.exe",
2780
+ args: ["/d", "/c", line],
2781
+ windowsVerbatimArguments: true
2782
+ };
2783
+ }
2784
+ function quoteWin32CmdArg(arg) {
2785
+ return `"${arg}"`;
2786
+ }
2775
2787
 
2776
2788
  // src/bash.ts
2777
2789
  var MAX_OUTPUT = 32768;
@@ -3313,10 +3325,20 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
3313
3325
  // C / C++ / native build
3314
3326
  "make",
3315
3327
  "cmake",
3328
+ "ninja",
3329
+ "clang",
3330
+ "clang-cl",
3331
+ "gcc",
3332
+ "g++",
3333
+ "link",
3334
+ "msbuild",
3316
3335
  // containers / orchestration
3317
3336
  "docker",
3318
3337
  "podman",
3319
3338
  "kubectl",
3339
+ // network (read-only intent; destructive ops still blocked by BLOCKED_ARG_PATTERNS)
3340
+ "curl",
3341
+ "wget",
3320
3342
  // common POSIX file/text utilities
3321
3343
  "pwd",
3322
3344
  "ls",
@@ -3336,7 +3358,509 @@ var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
3336
3358
  "cp",
3337
3359
  "mv",
3338
3360
  "rm",
3339
- "touch"
3361
+ "touch",
3362
+ "tar",
3363
+ // Windows-native tooling (win32). All non-destructive binaries; per-arg
3364
+ // safety is still enforced by BLOCKED_ARG_PATTERNS + the destructive-ops
3365
+ // gate in bash-kill-guard.ts. `gh` is included even though it lives at
3366
+ // "C:\Program Files\GitHub CLI\gh.exe" — resolveWin32Command handles the
3367
+ // space in the path.
3368
+ "gh",
3369
+ "where",
3370
+ "tasklist",
3371
+ "systeminfo",
3372
+ "wmic",
3373
+ "sc",
3374
+ "netstat",
3375
+ "ipconfig",
3376
+ "nslookup",
3377
+ "tracert",
3378
+ "pathping",
3379
+ // Windows shell interpreters. Without these, `exec` cannot run cmd builtins
3380
+ // (`dir`, `type`, `copy`, …) or any PowerShell cmdlet on Windows — a major
3381
+ // gap since those are the platform's primary shells. Arbitrary execution
3382
+ // through `cmd /c …` / `powershell -c …` is NOT a new hole: the permission
3383
+ // policy reconstructs the full command line (shellCommandLineFromInput) and
3384
+ // still runs it through the YOLO destructive classifier, so a genuinely
3385
+ // destructive `cmd /c del /s /q C:\…` continues to require confirmation.
3386
+ "cmd",
3387
+ "cmd.exe",
3388
+ "powershell",
3389
+ "powershell.exe",
3390
+ "pwsh",
3391
+ "pwsh.exe",
3392
+ // [core] Extended default allowlist (added 4b3d18d1 + this commit). All
3393
+ // non-destructive, broadly-used dev binaries. Per-arg safety is still
3394
+ // enforced by BLOCKED_ARG_PATTERNS + bash-kill-guard.ts.
3395
+ // --- Archives & compression ---
3396
+ "7z",
3397
+ "7za",
3398
+ "bzip2",
3399
+ "gzip",
3400
+ "xz",
3401
+ "unzip",
3402
+ "zip",
3403
+ "gtar",
3404
+ "bsdtar",
3405
+ "star",
3406
+ "pax",
3407
+ "cpio",
3408
+ // --- Android / mobile dev ---
3409
+ "adb",
3410
+ "fastboot",
3411
+ "sdkmanager",
3412
+ // --- DevOps / config mgmt ---
3413
+ "ansible",
3414
+ "ansible-playbook",
3415
+ "ansible-vault",
3416
+ "ansible-lint",
3417
+ "ansible-galaxy",
3418
+ "molecule",
3419
+ // --- Cloud CLIs ---
3420
+ "aws",
3421
+ "aws-vault",
3422
+ "awslocal",
3423
+ "az",
3424
+ "azcopy",
3425
+ "gcloud",
3426
+ "gsutil",
3427
+ "doctl",
3428
+ "linode-cli",
3429
+ // --- Native / C / C++ / linker tools ---
3430
+ "clang++",
3431
+ "clang-format",
3432
+ "clang-tidy",
3433
+ "clangd",
3434
+ "lld",
3435
+ "lldb",
3436
+ "ctest",
3437
+ "gmake",
3438
+ "meson",
3439
+ "conan",
3440
+ "vcpkg",
3441
+ "cl",
3442
+ "rc",
3443
+ "mt",
3444
+ "dumpbin",
3445
+ "dotnet-format",
3446
+ // --- Image / media / binary tools ---
3447
+ "convert",
3448
+ "ffmpeg",
3449
+ "ffprobe",
3450
+ "magick",
3451
+ "gs",
3452
+ "exiftool",
3453
+ // --- HTTP / fetch ---
3454
+ "wget2",
3455
+ "aria2c",
3456
+ "axel",
3457
+ "httpie",
3458
+ "hey",
3459
+ "ab",
3460
+ "wrk",
3461
+ "http",
3462
+ // --- Diff / patch / merge ---
3463
+ "diff",
3464
+ "diff3",
3465
+ "patch",
3466
+ "meld",
3467
+ "kdiff3",
3468
+ "kompare",
3469
+ // --- Encoding / file inspection ---
3470
+ "dos2unix",
3471
+ "unix2dos",
3472
+ "iconv",
3473
+ "file",
3474
+ "stat",
3475
+ "xxd",
3476
+ "hexdump",
3477
+ "od",
3478
+ "base64",
3479
+ // --- SSH / crypto / signing ---
3480
+ "ssh",
3481
+ "ssh-add",
3482
+ "ssh-keygen",
3483
+ "ssh-keyscan",
3484
+ "scp",
3485
+ "sftp",
3486
+ "rsync",
3487
+ "gpg",
3488
+ "gpg2",
3489
+ "gpg-agent",
3490
+ "openssl",
3491
+ "step",
3492
+ "keytool",
3493
+ // --- Search ---
3494
+ "egrep",
3495
+ "fgrep",
3496
+ "ag",
3497
+ "ack",
3498
+ "sift",
3499
+ "ugrep",
3500
+ "fd",
3501
+ "fdfind",
3502
+ "jq",
3503
+ "yq",
3504
+ "xq",
3505
+ "fx",
3506
+ "gron",
3507
+ // --- K8s / container ecosystem ---
3508
+ "kubectl.exe",
3509
+ "kubeadm",
3510
+ "kubelet",
3511
+ "helm",
3512
+ "k9s",
3513
+ "kustomize",
3514
+ "skaffold",
3515
+ "tilt",
3516
+ "minikube",
3517
+ "kind",
3518
+ "k3d",
3519
+ "k3s",
3520
+ "docker-compose",
3521
+ "buildah",
3522
+ "skopeo",
3523
+ "nerdctl",
3524
+ "ctr",
3525
+ "ctr.exe",
3526
+ // --- Databases ---
3527
+ "sqlite3",
3528
+ "sqlite",
3529
+ "psql",
3530
+ "pg_dump",
3531
+ "pg_restore",
3532
+ "mysql",
3533
+ "mysqladmin",
3534
+ "mysqldump",
3535
+ "mariadb",
3536
+ "mariadb-dump",
3537
+ "redis-cli",
3538
+ "redis-server",
3539
+ "memcached",
3540
+ "etcdctl",
3541
+ "consul",
3542
+ "vault",
3543
+ "nomad",
3544
+ "mongosh",
3545
+ "mongo",
3546
+ "mongoexport",
3547
+ "mongoimport",
3548
+ "mongodump",
3549
+ "mongorestore",
3550
+ // --- Windows extended (read-mostly ops) ---
3551
+ "taskkill",
3552
+ "gpupdate",
3553
+ "gpresult",
3554
+ "hostname",
3555
+ "whoami",
3556
+ "who",
3557
+ "net",
3558
+ "net1",
3559
+ // --- VCS ecosystem ---
3560
+ "glab",
3561
+ "hub",
3562
+ "tea",
3563
+ "git-lfs",
3564
+ "tig",
3565
+ "lazygit",
3566
+ // --- POSIX text utilities (extended; duplicates of pwd/ls/cat/head/tail/wc/
3567
+ // grep/find/echo/awk/mkdir/cp/mv/rm/touch from the base list above are
3568
+ // omitted — the Set is de-duplicated at runtime but a clean literal is
3569
+ // easier to maintain) ---
3570
+ "gawk",
3571
+ "tr",
3572
+ "cut",
3573
+ "paste",
3574
+ "join",
3575
+ "comm",
3576
+ "expand",
3577
+ "unexpand",
3578
+ "fold",
3579
+ "fmt",
3580
+ "nl",
3581
+ "pr",
3582
+ "column",
3583
+ "tsort",
3584
+ "tty",
3585
+ "ul",
3586
+ "units",
3587
+ "factor",
3588
+ "seq",
3589
+ "shuf",
3590
+ "look",
3591
+ "yes",
3592
+ "true",
3593
+ "false",
3594
+ "test",
3595
+ "[",
3596
+ "printf",
3597
+ "env",
3598
+ "tree",
3599
+ "locate",
3600
+ "which",
3601
+ "whereis",
3602
+ "type",
3603
+ "hash",
3604
+ "pushd",
3605
+ "popd",
3606
+ "dirs",
3607
+ "history",
3608
+ "fc",
3609
+ "jobs",
3610
+ "bg",
3611
+ "fg",
3612
+ "wait",
3613
+ "ulimit",
3614
+ "umask",
3615
+ "nice",
3616
+ "nohup",
3617
+ "timeout",
3618
+ "time",
3619
+ "trap",
3620
+ "exit",
3621
+ "return",
3622
+ "source",
3623
+ ".",
3624
+ "alias",
3625
+ "unalias",
3626
+ "set",
3627
+ "unset",
3628
+ "export",
3629
+ "readonly",
3630
+ "typeset",
3631
+ "declare",
3632
+ "local",
3633
+ "eval",
3634
+ "exec",
3635
+ // --- Process / system inspection ---
3636
+ "htop",
3637
+ "top",
3638
+ "atop",
3639
+ "glances",
3640
+ "iotop",
3641
+ "nethogs",
3642
+ "iftop",
3643
+ "lsof",
3644
+ "strace",
3645
+ "ltrace",
3646
+ "sysstat",
3647
+ "vmstat",
3648
+ "iostat",
3649
+ "mpstat",
3650
+ "sar",
3651
+ "free",
3652
+ "df",
3653
+ "du",
3654
+ "mount",
3655
+ "umount",
3656
+ "lsblk",
3657
+ "blkid",
3658
+ "kill",
3659
+ "killall",
3660
+ "pkill",
3661
+ "pgrep",
3662
+ "pidof",
3663
+ "ps",
3664
+ "ps.exe",
3665
+ // --- Network inspection ---
3666
+ "ip",
3667
+ "ss",
3668
+ "route",
3669
+ "arp",
3670
+ "arping",
3671
+ "ping",
3672
+ "ping6",
3673
+ "hping3",
3674
+ "mtr",
3675
+ "tracepath",
3676
+ "tcpdump",
3677
+ "nmap",
3678
+ "netcat",
3679
+ "nc",
3680
+ "ncat",
3681
+ "socat",
3682
+ // --- Sync / backup ---
3683
+ "rclone",
3684
+ "restic",
3685
+ "borg",
3686
+ "duplicati",
3687
+ "duplicacy",
3688
+ "syncthing",
3689
+ "syncthing-cli",
3690
+ // --- Permissions / users / ACLs (POSIX) ---
3691
+ "useradd",
3692
+ "userdel",
3693
+ "usermod",
3694
+ "groupadd",
3695
+ "groupdel",
3696
+ "groupmod",
3697
+ "chown",
3698
+ "chmod",
3699
+ "chgrp",
3700
+ "getfacl",
3701
+ "setfacl",
3702
+ "setcap",
3703
+ "getcap",
3704
+ // --- Crypto / cert mgmt (extended) ---
3705
+ "certbot",
3706
+ "mkcert",
3707
+ "jarsigner",
3708
+ // --- Editors ---
3709
+ "subl",
3710
+ "code",
3711
+ "code-insiders",
3712
+ "cursor",
3713
+ "atom",
3714
+ "nano",
3715
+ "vim",
3716
+ "nvim",
3717
+ "vi",
3718
+ "emacs",
3719
+ "helix",
3720
+ "hx",
3721
+ "micro",
3722
+ "jed",
3723
+ "ed",
3724
+ "ex",
3725
+ "mg",
3726
+ // --- Terminal multiplexers ---
3727
+ "asciinema",
3728
+ "script",
3729
+ "scriptreplay",
3730
+ "expect",
3731
+ "screen",
3732
+ "tmux",
3733
+ "byobu",
3734
+ "dtach",
3735
+ "abduco",
3736
+ // --- Calculators / REPLs / scientific ---
3737
+ "bc",
3738
+ "dc",
3739
+ "calc",
3740
+ "qalc",
3741
+ "genius",
3742
+ "octave",
3743
+ "R",
3744
+ "Rscript",
3745
+ "julia",
3746
+ "irb",
3747
+ "pry",
3748
+ "ghci",
3749
+ "stack",
3750
+ "cabal",
3751
+ "ghc",
3752
+ // --- PHP / Lua / Perl / Ruby ecosystem (extended) ---
3753
+ "php8",
3754
+ "php7",
3755
+ "phpcs",
3756
+ "phpcbf",
3757
+ "phpmd",
3758
+ "phpstan",
3759
+ "psalm",
3760
+ "lua",
3761
+ "lua5.1",
3762
+ "lua5.2",
3763
+ "lua5.3",
3764
+ "lua5.4",
3765
+ "luarocks",
3766
+ "perl",
3767
+ "cpan",
3768
+ "prove",
3769
+ "plackup",
3770
+ "rake",
3771
+ "rspec",
3772
+ "jekyll",
3773
+ "node-gyp",
3774
+ "node-pre-gyp",
3775
+ // --- JS / TS toolchain (extended) ---
3776
+ "electron",
3777
+ "electron-builder",
3778
+ "electron-forge",
3779
+ "vite-preview",
3780
+ "swc",
3781
+ "swc-cli",
3782
+ "swcpack",
3783
+ "mocha",
3784
+ "chai",
3785
+ "jasmine",
3786
+ "puppeteer",
3787
+ "lighthouse",
3788
+ // --- Linters / formatters (extended) ---
3789
+ "tslint",
3790
+ "stylelint",
3791
+ "htmlhint",
3792
+ "jshint",
3793
+ "jslint",
3794
+ "jscs",
3795
+ // --- Document conversion ---
3796
+ "pandoc",
3797
+ "weasyprint",
3798
+ "wkhtmltopdf",
3799
+ "wkhtmltoimage",
3800
+ "prince",
3801
+ "mdp",
3802
+ "markdown",
3803
+ "multimarkdown",
3804
+ "cmark",
3805
+ "cmark-gfm",
3806
+ // --- Office / spreadsheet ---
3807
+ "soffice",
3808
+ "libreoffice",
3809
+ "unoconv",
3810
+ "abiword",
3811
+ "gnumeric",
3812
+ // --- Spell / grammar ---
3813
+ "aspell",
3814
+ "hunspell",
3815
+ "enchant",
3816
+ "languagetool",
3817
+ // --- Source-highlight / diff tools ---
3818
+ "delta",
3819
+ "bat",
3820
+ "ccat",
3821
+ "hl",
3822
+ "highlight",
3823
+ "source-highlight",
3824
+ "ansifilter",
3825
+ // --- Job schedulers ---
3826
+ "pueue",
3827
+ "task-spooler",
3828
+ "ts",
3829
+ "at",
3830
+ "atd",
3831
+ "anacron",
3832
+ "fcron",
3833
+ "cronie",
3834
+ "systemd-run",
3835
+ "systemd-cat",
3836
+ // --- Plotting / visualization ---
3837
+ "gnuplot",
3838
+ "gnuplot-nox",
3839
+ "veusz",
3840
+ "scidavis",
3841
+ "grace",
3842
+ "xmgrace",
3843
+ "labplot",
3844
+ // --- Security / recon (network + web) ---
3845
+ "masscan",
3846
+ "zmap",
3847
+ "rustscan",
3848
+ "amass",
3849
+ "subfinder",
3850
+ "httpx",
3851
+ "nuclei",
3852
+ "naabu",
3853
+ "katana",
3854
+ "dnsx",
3855
+ "assetfinder",
3856
+ "findomain",
3857
+ "gau",
3858
+ "waybackurls",
3859
+ "httprobe",
3860
+ "meg",
3861
+ "subjack",
3862
+ "sublert",
3863
+ "chaos"
3340
3864
  ]);
3341
3865
  var allowedCommands = new Set(DEFAULT_ALLOWED_COMMANDS);
3342
3866
  var normalizeCmd = (c) => c.trim();
@@ -3559,17 +4083,18 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3559
4083
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
3560
4084
  const resolved = resolveWin32Command(cmd);
3561
4085
  const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
3562
- const spawnCmd = needsShell ? cmd : resolved;
3563
- if (needsShell) assertSafeWin32ShellArgs(args);
4086
+ const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
4087
+ const spawnCmd = shim?.command ?? resolved;
4088
+ const spawnArgs = shim?.args ?? args;
3564
4089
  let child;
3565
4090
  try {
3566
- child = spawn(spawnCmd, args, {
4091
+ child = spawn(spawnCmd, spawnArgs, {
3567
4092
  cwd,
3568
4093
  env: buildChildEnv(sessionId),
3569
4094
  stdio: ["ignore", "pipe", "pipe"],
3570
4095
  windowsHide: true,
3571
4096
  ...isWin2 ? {} : { signal },
3572
- ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
4097
+ ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
3573
4098
  });
3574
4099
  } catch (err) {
3575
4100
  spool.finalize();
@@ -3664,6 +4189,7 @@ TD.addRule("stripDangerousElements", {
3664
4189
  });
3665
4190
  var MAX_BYTES2 = 131072;
3666
4191
  var TIMEOUT_MS = 2e4;
4192
+ var nativeGlobalFetch = globalThis.fetch;
3667
4193
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
3668
4194
  if (ALLOW_PRIVATE && !process.env["CI"]) {
3669
4195
  console.warn(
@@ -3713,6 +4239,9 @@ function getPinnedDispatcher() {
3713
4239
  }
3714
4240
  return pinnedAgent;
3715
4241
  }
4242
+ function dispatcherFetch() {
4243
+ return globalThis.fetch === nativeGlobalFetch ? fetch : globalThis.fetch;
4244
+ }
3716
4245
  var _beforeExitRegistered = false;
3717
4246
  if (!_beforeExitRegistered) {
3718
4247
  _beforeExitRegistered = true;
@@ -3748,7 +4277,7 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
3748
4277
  headers,
3749
4278
  dispatcher: getPinnedDispatcher()
3750
4279
  };
3751
- const res = await fetch(currentUrl, init);
4280
+ const res = await dispatcherFetch()(currentUrl, init);
3752
4281
  if (res.status < 300 || res.status > 399) {
3753
4282
  return res;
3754
4283
  }
@@ -3776,7 +4305,7 @@ var fetchTool = {
3776
4305
  category: "Network",
3777
4306
  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).",
3778
4307
  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`.",
3779
- permission: "confirm",
4308
+ permission: "auto",
3780
4309
  mutating: false,
3781
4310
  capabilities: ["net.outbound"],
3782
4311
  icon: "web",
@@ -4017,7 +4546,7 @@ var searchTool = {
4017
4546
  category: "Search",
4018
4547
  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.",
4019
4548
  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.",
4020
- permission: "confirm",
4549
+ permission: "auto",
4021
4550
  mutating: false,
4022
4551
  capabilities: ["net.outbound"],
4023
4552
  icon: "search",
@@ -4080,15 +4609,15 @@ var searchTool = {
4080
4609
  };
4081
4610
  yield {
4082
4611
  type: "partial_output",
4083
- text: `${results.length} cached results from ${source}`,
4084
- data: { count: results.length, cached: true }
4612
+ text: `${results.length} cached results from ${entry.source}`,
4613
+ data: { count: results.length, cached: true, source: entry.source }
4085
4614
  };
4086
4615
  yield {
4087
4616
  type: "final",
4088
4617
  output: {
4089
4618
  query: input.query,
4090
4619
  results: results.slice(0, num),
4091
- source,
4620
+ source: entry.source,
4092
4621
  truncated: results.length >= num,
4093
4622
  cached: true
4094
4623
  }
@@ -4102,6 +4631,7 @@ var searchTool = {
4102
4631
  data: { source, query: input.query, cached: false }
4103
4632
  };
4104
4633
  let rawResults;
4634
+ let effectiveSource = source;
4105
4635
  switch (source) {
4106
4636
  case "duckduckgo":
4107
4637
  rawResults = await duckduckgoSearch(input.query, num, opts.signal);
@@ -4118,24 +4648,24 @@ var searchTool = {
4118
4648
  field: "source"
4119
4649
  });
4120
4650
  }
4121
- const seenUrls = /* @__PURE__ */ new Set();
4122
- const deduped = [];
4123
- for (const r of rawResults) {
4124
- const noQuery = r.url.split("?")[0] ?? r.url;
4125
- const normalized = noQuery.split("#")[0] ?? r.url;
4126
- if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
4127
- seenUrls.add(normalized);
4128
- deduped.push(r);
4129
- }
4651
+ let ranked = rankSearchResults(rawResults, input.query);
4652
+ if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
4653
+ yield {
4654
+ type: "log",
4655
+ text: `${source} returned no relevant static results; falling back to duckduckgo`,
4656
+ data: { source, fallback: "duckduckgo", query: input.query }
4657
+ };
4658
+ rawResults = await duckduckgoSearch(input.query, num, opts.signal);
4659
+ ranked = rankSearchResults(rawResults, input.query);
4660
+ effectiveSource = "duckduckgo";
4130
4661
  }
4131
- const ranked = scoreResults(deduped, input.query);
4132
4662
  const finalResults = ranked.slice(0, num);
4133
- cache.set(cacheKey, { results: ranked, timestamp: Date.now() });
4663
+ cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
4134
4664
  pruneStaleCacheEntries();
4135
4665
  yield {
4136
4666
  type: "partial_output",
4137
- text: `${finalResults.length} results from ${source}`,
4138
- data: { count: finalResults.length, cached: false }
4667
+ text: `${finalResults.length} results from ${effectiveSource}`,
4668
+ data: { count: finalResults.length, cached: false, source: effectiveSource }
4139
4669
  };
4140
4670
  yield {
4141
4671
  type: "final",
@@ -4146,7 +4676,7 @@ var searchTool = {
4146
4676
  url: r.url,
4147
4677
  snippet: r.snippet
4148
4678
  })),
4149
- source,
4679
+ source: effectiveSource,
4150
4680
  truncated: finalResults.length >= num,
4151
4681
  cached: false
4152
4682
  }
@@ -4159,6 +4689,19 @@ function pruneStaleCacheEntries() {
4159
4689
  if (entry.timestamp < cutoff) cache.delete(key);
4160
4690
  }
4161
4691
  }
4692
+ function rankSearchResults(results, query) {
4693
+ const seenUrls = /* @__PURE__ */ new Set();
4694
+ const deduped = [];
4695
+ for (const r of results) {
4696
+ const noQuery = r.url.split("?")[0] ?? r.url;
4697
+ const normalized = noQuery.split("#")[0] ?? r.url;
4698
+ if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
4699
+ seenUrls.add(normalized);
4700
+ deduped.push(r);
4701
+ }
4702
+ }
4703
+ return scoreResults(deduped, query);
4704
+ }
4162
4705
  function scoreResults(results, query) {
4163
4706
  const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
4164
4707
  return results.map((r) => {
@@ -4172,6 +4715,15 @@ function scoreResults(results, query) {
4172
4715
  return { ...r, score };
4173
4716
  }).sort((a, b) => b.score - a.score);
4174
4717
  }
4718
+ function shouldFallbackToDuckDuckGo(results, query) {
4719
+ if (results.length === 0) return true;
4720
+ const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length >= 3);
4721
+ if (terms.length === 0) return false;
4722
+ return !results.some((r) => {
4723
+ const haystack = `${r.title} ${r.url} ${r.snippet}`.toLowerCase();
4724
+ return terms.some((term) => haystack.includes(term));
4725
+ });
4726
+ }
4175
4727
  async function duckduckgoSearch(query, num, signal) {
4176
4728
  const encoded = encodeURIComponent(query);
4177
4729
  const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
@@ -4196,14 +4748,19 @@ function takeFrom(iter, max) {
4196
4748
  }
4197
4749
  function parseDuckDuckGo(html, num) {
4198
4750
  const results = [];
4199
- const snippetRegex = /<a class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi;
4200
- const snippet2Regex = /<a class="result-snippet"[^>]*>([^<]+)<\/a>/gi;
4751
+ const linkRegex = /<a\b([^>]*\bclass=(["'])[^"']*\bresult-link\b[^"']*\2[^>]*)>([\s\S]*?)<\/a>/gi;
4752
+ const snippetRegex = /<([a-z0-9]+)\b([^>]*\bclass=(["'])[^"']*\bresult-snippet\b[^"']*\3[^>]*)>([\s\S]*?)<\/\1>/gi;
4201
4753
  const linkMatches = takeFrom(
4202
- [...html.matchAll(snippetRegex)].filter((m) => m[1] && m[2]).map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),
4754
+ [...html.matchAll(linkRegex)].map((m) => {
4755
+ const attrs = expectDefined(m[1]);
4756
+ const href = getHtmlAttr(attrs, "href");
4757
+ const title = stripTags(expectDefined(m[3]));
4758
+ return href && title ? { url: normalizeDuckDuckGoUrl(href), title } : void 0;
4759
+ }).filter((m) => m !== void 0),
4203
4760
  num
4204
4761
  );
4205
4762
  const snippetMatches = takeFrom(
4206
- [...html.matchAll(snippet2Regex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),
4763
+ [...html.matchAll(snippetRegex)].filter((m) => m[4]).map((m) => stripTags(expectDefined(m[4]))),
4207
4764
  num
4208
4765
  );
4209
4766
  for (let i = 0; i < linkMatches.length && i < num; i++) {
@@ -4219,6 +4776,24 @@ function parseDuckDuckGo(html, num) {
4219
4776
  }
4220
4777
  return results;
4221
4778
  }
4779
+ function getHtmlAttr(attrs, name) {
4780
+ const quoted = new RegExp(`\\b${name}\\s*=\\s*(["'])(.*?)\\1`, "i").exec(attrs);
4781
+ if (quoted?.[2]) return decodeHtmlEntities(quoted[2]);
4782
+ const unquoted = new RegExp(`\\b${name}\\s*=\\s*([^\\s>]+)`, "i").exec(attrs);
4783
+ return unquoted?.[1] ? decodeHtmlEntities(unquoted[1]) : void 0;
4784
+ }
4785
+ function normalizeDuckDuckGoUrl(raw) {
4786
+ if (raw.startsWith("//")) return `https:${raw}`;
4787
+ if (!raw.startsWith("/")) return raw;
4788
+ if (!raw.startsWith("/l/")) return raw;
4789
+ try {
4790
+ const url = new URL(raw, "https://duckduckgo.com");
4791
+ const uddg = url.searchParams.get("uddg");
4792
+ return uddg?.startsWith("http") ? uddg : url.toString();
4793
+ } catch {
4794
+ return raw;
4795
+ }
4796
+ }
4222
4797
  async function googleSearch(query, num, signal) {
4223
4798
  const encoded = encodeURIComponent(query);
4224
4799
  const url = `https://www.google.com/search?q=${encoded}&hl=en`;
@@ -4260,29 +4835,53 @@ async function bingSearch(query, num, signal) {
4260
4835
  }
4261
4836
  function parseBingResults(html, num) {
4262
4837
  const results = [];
4263
- const titleRegex = /<h2[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>\s*<\/h2>/gi;
4264
- const snippetRegex = /<p[^>]*class="[^"]*b_paractl[^"]*"[^>]*>([^<]+)<\/p>/gi;
4265
- const entries = takeFrom(
4266
- [...html.matchAll(titleRegex)].filter((m) => m[1] && m[2]).map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),
4267
- num
4268
- );
4269
- const snippets = takeFrom(
4270
- [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),
4271
- num
4272
- );
4838
+ 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]));
4839
+ const candidates = blocks.length > 0 ? blocks : [html];
4840
+ const entries = takeFrom(candidates.flatMap((block) => {
4841
+ const titleMatch = /<h2[^>]*>\s*<a\b([^>]*)>([\s\S]*?)<\/a>\s*<\/h2>/i.exec(block);
4842
+ if (!titleMatch) return [];
4843
+ const href = getHtmlAttr(expectDefined(titleMatch[1]), "href");
4844
+ const title = stripTags(expectDefined(titleMatch[2]));
4845
+ if (!href || !title) return [];
4846
+ 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);
4847
+ const snippet = snippetMatch ? stripTags(expectDefined(snippetMatch.at(-1))) : "";
4848
+ return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
4849
+ }), num);
4273
4850
  for (let i = 0; i < entries.length; i++) {
4274
4851
  const entry = entries[i];
4275
4852
  if (entry) {
4276
4853
  results.push({
4277
4854
  title: entry.title ?? "",
4278
4855
  url: entry.url ?? "",
4279
- snippet: snippets[i] ?? "",
4856
+ snippet: entry.snippet ?? "",
4280
4857
  score: 1
4281
4858
  });
4282
4859
  }
4283
4860
  }
4284
4861
  return results;
4285
4862
  }
4863
+ function normalizeBingUrl(raw) {
4864
+ try {
4865
+ const url = new URL(raw);
4866
+ if (url.hostname.endsWith("bing.com") && url.pathname.startsWith("/ck/")) {
4867
+ const encoded = url.searchParams.get("u");
4868
+ const decoded = decodeBingTarget(encoded);
4869
+ if (decoded?.startsWith("http")) return decoded;
4870
+ }
4871
+ } catch {
4872
+ }
4873
+ return raw;
4874
+ }
4875
+ function decodeBingTarget(encoded) {
4876
+ if (!encoded) return void 0;
4877
+ const payload = encoded.startsWith("a1") ? encoded.slice(2) : encoded;
4878
+ try {
4879
+ const padded = payload.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payload.length / 4) * 4, "=");
4880
+ return Buffer.from(padded, "base64").toString("utf8");
4881
+ } catch {
4882
+ return void 0;
4883
+ }
4884
+ }
4286
4885
  async function fetchWithTimeout(url, signal, timeoutMs) {
4287
4886
  const controller = new AbortController();
4288
4887
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -4310,7 +4909,10 @@ function anySignal(...signals) {
4310
4909
  return AbortSignal.any(signals);
4311
4910
  }
4312
4911
  function stripTags(html) {
4313
- return html.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
4912
+ return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
4913
+ }
4914
+ function decodeHtmlEntities(text) {
4915
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
4314
4916
  }
4315
4917
  var todoTool = {
4316
4918
  name: "todo",
@@ -5880,15 +6482,16 @@ async function* spawnStream(opts) {
5880
6482
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
5881
6483
  const resolved = resolveWin32Command(opts.cmd);
5882
6484
  const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
5883
- const cmd = needsShell ? opts.cmd : resolved;
5884
- if (needsShell) assertSafeWin32ShellArgs(opts.args);
5885
- const child = spawn(cmd, opts.args, {
6485
+ const shim = needsShell ? buildWin32CmdShimInvocation(resolved, opts.args) : null;
6486
+ const cmd = shim?.command ?? resolved;
6487
+ const args = shim?.args ?? opts.args;
6488
+ const child = spawn(cmd, args, {
5886
6489
  cwd: opts.cwd,
5887
6490
  env: buildChildEnv(),
5888
6491
  stdio: ["ignore", "pipe", "pipe"],
5889
6492
  windowsHide: true,
5890
6493
  ...isWin3 ? {} : { signal: opts.signal },
5891
- ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
6494
+ ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
5892
6495
  });
5893
6496
  const registry = getProcessRegistry();
5894
6497
  const pid = child.pid;
@@ -6135,7 +6738,7 @@ var formatTool = {
6135
6738
  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.",
6136
6739
  permission: "confirm",
6137
6740
  mutating: true,
6138
- capabilities: ["fs.write", "shell.exec"],
6741
+ capabilities: ["fs.write", "shell.restricted"],
6139
6742
  icon: "code",
6140
6743
  timeoutMs: 6e4,
6141
6744
  inputSchema: {
@@ -6788,9 +7391,10 @@ function runOutdated(manager, args, cwd, signal) {
6788
7391
  const MAX = 1e5;
6789
7392
  const resolved = resolveWin32Command(manager);
6790
7393
  const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
6791
- const spawnCmd = needsShell ? manager : resolved;
6792
- if (needsShell) assertSafeWin32ShellArgs(args);
6793
- const child = spawn(spawnCmd, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
7394
+ const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
7395
+ const spawnCmd = shim?.command ?? resolved;
7396
+ const spawnArgs = shim?.args ?? args;
7397
+ const child = spawn(spawnCmd, spawnArgs, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {} });
6794
7398
  child.stdout?.on("data", (c) => {
6795
7399
  if (stdout.length < MAX) stdout += c.toString();
6796
7400
  });
@@ -7395,9 +7999,9 @@ var designTool = {
7395
7999
  category: "Design",
7396
8000
  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).',
7397
8001
  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"}`.',
7398
- permission: "auto",
7399
- mutating: false,
7400
- capabilities: [],
8002
+ permission: "confirm",
8003
+ mutating: true,
8004
+ capabilities: ["fs.write"],
7401
8005
  timeoutMs: 15e3,
7402
8006
  inputSchema: {
7403
8007
  type: "object",
@@ -7998,7 +8602,7 @@ function rememberTool(memory) {
7998
8602
  category: "Session",
7999
8603
  description: "Persist facts, conventions, decisions, and preferences into long-term memory. Memories survive restarts and are scored for relevance in future sessions.",
8000
8604
  usageHint: "Persist facts, conventions, decisions, and preferences into long-term memory.\n\nWHEN TO USE:\n- Project conventions discovered during a task (build tool, lint rules, code style)\n- Architecture decisions made (chose X over Y, decided to use pattern Z)\n- User preferences expressed (prefers short names, always uses pnpm)\n- Anti-patterns identified (never do X, avoid pattern Y)\n- File/location references useful across sessions\n\nWHEN NOT TO USE:\n- Temporary task state or progress \u2192 use `todo`\n- One-off debugging notes\n- Information already obvious from the codebase\n\nAlways include `type` and `priority`. Use 1-3 `tags` for grouping.\nBetter to remember a fact now than rediscover it next session.",
8001
- permission: "auto",
8605
+ permission: "confirm",
8002
8606
  mutating: true,
8003
8607
  timeoutMs: 2e3,
8004
8608
  capabilities: ["memory.write"],