@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/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,496 @@ 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
+ // [core] Extended default allowlist (added 4b3d18d1 + this commit). All
3380
+ // non-destructive, broadly-used dev binaries. Per-arg safety is still
3381
+ // enforced by BLOCKED_ARG_PATTERNS + bash-kill-guard.ts.
3382
+ // --- Archives & compression ---
3383
+ "7z",
3384
+ "7za",
3385
+ "bzip2",
3386
+ "gzip",
3387
+ "xz",
3388
+ "unzip",
3389
+ "zip",
3390
+ "gtar",
3391
+ "bsdtar",
3392
+ "star",
3393
+ "pax",
3394
+ "cpio",
3395
+ // --- Android / mobile dev ---
3396
+ "adb",
3397
+ "fastboot",
3398
+ "sdkmanager",
3399
+ // --- DevOps / config mgmt ---
3400
+ "ansible",
3401
+ "ansible-playbook",
3402
+ "ansible-vault",
3403
+ "ansible-lint",
3404
+ "ansible-galaxy",
3405
+ "molecule",
3406
+ // --- Cloud CLIs ---
3407
+ "aws",
3408
+ "aws-vault",
3409
+ "awslocal",
3410
+ "az",
3411
+ "azcopy",
3412
+ "gcloud",
3413
+ "gsutil",
3414
+ "doctl",
3415
+ "linode-cli",
3416
+ // --- Native / C / C++ / linker tools ---
3417
+ "clang++",
3418
+ "clang-format",
3419
+ "clang-tidy",
3420
+ "clangd",
3421
+ "lld",
3422
+ "lldb",
3423
+ "ctest",
3424
+ "gmake",
3425
+ "meson",
3426
+ "conan",
3427
+ "vcpkg",
3428
+ "cl",
3429
+ "rc",
3430
+ "mt",
3431
+ "dumpbin",
3432
+ "dotnet-format",
3433
+ // --- Image / media / binary tools ---
3434
+ "convert",
3435
+ "ffmpeg",
3436
+ "ffprobe",
3437
+ "magick",
3438
+ "gs",
3439
+ "exiftool",
3440
+ // --- HTTP / fetch ---
3441
+ "wget2",
3442
+ "aria2c",
3443
+ "axel",
3444
+ "httpie",
3445
+ "hey",
3446
+ "ab",
3447
+ "wrk",
3448
+ "http",
3449
+ // --- Diff / patch / merge ---
3450
+ "diff",
3451
+ "diff3",
3452
+ "patch",
3453
+ "meld",
3454
+ "kdiff3",
3455
+ "kompare",
3456
+ // --- Encoding / file inspection ---
3457
+ "dos2unix",
3458
+ "unix2dos",
3459
+ "iconv",
3460
+ "file",
3461
+ "stat",
3462
+ "xxd",
3463
+ "hexdump",
3464
+ "od",
3465
+ "base64",
3466
+ // --- SSH / crypto / signing ---
3467
+ "ssh",
3468
+ "ssh-add",
3469
+ "ssh-keygen",
3470
+ "ssh-keyscan",
3471
+ "scp",
3472
+ "sftp",
3473
+ "rsync",
3474
+ "gpg",
3475
+ "gpg2",
3476
+ "gpg-agent",
3477
+ "openssl",
3478
+ "step",
3479
+ "keytool",
3480
+ // --- Search ---
3481
+ "egrep",
3482
+ "fgrep",
3483
+ "ag",
3484
+ "ack",
3485
+ "sift",
3486
+ "ugrep",
3487
+ "fd",
3488
+ "fdfind",
3489
+ "jq",
3490
+ "yq",
3491
+ "xq",
3492
+ "fx",
3493
+ "gron",
3494
+ // --- K8s / container ecosystem ---
3495
+ "kubectl.exe",
3496
+ "kubeadm",
3497
+ "kubelet",
3498
+ "helm",
3499
+ "k9s",
3500
+ "kustomize",
3501
+ "skaffold",
3502
+ "tilt",
3503
+ "minikube",
3504
+ "kind",
3505
+ "k3d",
3506
+ "k3s",
3507
+ "docker-compose",
3508
+ "buildah",
3509
+ "skopeo",
3510
+ "nerdctl",
3511
+ "ctr",
3512
+ "ctr.exe",
3513
+ // --- Databases ---
3514
+ "sqlite3",
3515
+ "sqlite",
3516
+ "psql",
3517
+ "pg_dump",
3518
+ "pg_restore",
3519
+ "mysql",
3520
+ "mysqladmin",
3521
+ "mysqldump",
3522
+ "mariadb",
3523
+ "mariadb-dump",
3524
+ "redis-cli",
3525
+ "redis-server",
3526
+ "memcached",
3527
+ "etcdctl",
3528
+ "consul",
3529
+ "vault",
3530
+ "nomad",
3531
+ "mongosh",
3532
+ "mongo",
3533
+ "mongoexport",
3534
+ "mongoimport",
3535
+ "mongodump",
3536
+ "mongorestore",
3537
+ // --- Windows extended (read-mostly ops) ---
3538
+ "taskkill",
3539
+ "gpupdate",
3540
+ "gpresult",
3541
+ "hostname",
3542
+ "whoami",
3543
+ "who",
3544
+ "net",
3545
+ "net1",
3546
+ // --- VCS ecosystem ---
3547
+ "glab",
3548
+ "hub",
3549
+ "tea",
3550
+ "git-lfs",
3551
+ "tig",
3552
+ "lazygit",
3553
+ // --- POSIX text utilities (extended; duplicates of pwd/ls/cat/head/tail/wc/
3554
+ // grep/find/echo/awk/mkdir/cp/mv/rm/touch from the base list above are
3555
+ // omitted — the Set is de-duplicated at runtime but a clean literal is
3556
+ // easier to maintain) ---
3557
+ "gawk",
3558
+ "tr",
3559
+ "cut",
3560
+ "paste",
3561
+ "join",
3562
+ "comm",
3563
+ "expand",
3564
+ "unexpand",
3565
+ "fold",
3566
+ "fmt",
3567
+ "nl",
3568
+ "pr",
3569
+ "column",
3570
+ "tsort",
3571
+ "tty",
3572
+ "ul",
3573
+ "units",
3574
+ "factor",
3575
+ "seq",
3576
+ "shuf",
3577
+ "look",
3578
+ "yes",
3579
+ "true",
3580
+ "false",
3581
+ "test",
3582
+ "[",
3583
+ "printf",
3584
+ "env",
3585
+ "tree",
3586
+ "locate",
3587
+ "which",
3588
+ "whereis",
3589
+ "type",
3590
+ "hash",
3591
+ "pushd",
3592
+ "popd",
3593
+ "dirs",
3594
+ "history",
3595
+ "fc",
3596
+ "jobs",
3597
+ "bg",
3598
+ "fg",
3599
+ "wait",
3600
+ "ulimit",
3601
+ "umask",
3602
+ "nice",
3603
+ "nohup",
3604
+ "timeout",
3605
+ "time",
3606
+ "trap",
3607
+ "exit",
3608
+ "return",
3609
+ "source",
3610
+ ".",
3611
+ "alias",
3612
+ "unalias",
3613
+ "set",
3614
+ "unset",
3615
+ "export",
3616
+ "readonly",
3617
+ "typeset",
3618
+ "declare",
3619
+ "local",
3620
+ "eval",
3621
+ "exec",
3622
+ // --- Process / system inspection ---
3623
+ "htop",
3624
+ "top",
3625
+ "atop",
3626
+ "glances",
3627
+ "iotop",
3628
+ "nethogs",
3629
+ "iftop",
3630
+ "lsof",
3631
+ "strace",
3632
+ "ltrace",
3633
+ "sysstat",
3634
+ "vmstat",
3635
+ "iostat",
3636
+ "mpstat",
3637
+ "sar",
3638
+ "free",
3639
+ "df",
3640
+ "du",
3641
+ "mount",
3642
+ "umount",
3643
+ "lsblk",
3644
+ "blkid",
3645
+ "kill",
3646
+ "killall",
3647
+ "pkill",
3648
+ "pgrep",
3649
+ "pidof",
3650
+ "ps",
3651
+ "ps.exe",
3652
+ // --- Network inspection ---
3653
+ "ip",
3654
+ "ss",
3655
+ "route",
3656
+ "arp",
3657
+ "arping",
3658
+ "ping",
3659
+ "ping6",
3660
+ "hping3",
3661
+ "mtr",
3662
+ "tracepath",
3663
+ "tcpdump",
3664
+ "nmap",
3665
+ "netcat",
3666
+ "nc",
3667
+ "ncat",
3668
+ "socat",
3669
+ // --- Sync / backup ---
3670
+ "rclone",
3671
+ "restic",
3672
+ "borg",
3673
+ "duplicati",
3674
+ "duplicacy",
3675
+ "syncthing",
3676
+ "syncthing-cli",
3677
+ // --- Permissions / users / ACLs (POSIX) ---
3678
+ "useradd",
3679
+ "userdel",
3680
+ "usermod",
3681
+ "groupadd",
3682
+ "groupdel",
3683
+ "groupmod",
3684
+ "chown",
3685
+ "chmod",
3686
+ "chgrp",
3687
+ "getfacl",
3688
+ "setfacl",
3689
+ "setcap",
3690
+ "getcap",
3691
+ // --- Crypto / cert mgmt (extended) ---
3692
+ "certbot",
3693
+ "mkcert",
3694
+ "jarsigner",
3695
+ // --- Editors ---
3696
+ "subl",
3697
+ "code",
3698
+ "code-insiders",
3699
+ "cursor",
3700
+ "atom",
3701
+ "nano",
3702
+ "vim",
3703
+ "nvim",
3704
+ "vi",
3705
+ "emacs",
3706
+ "helix",
3707
+ "hx",
3708
+ "micro",
3709
+ "jed",
3710
+ "ed",
3711
+ "ex",
3712
+ "mg",
3713
+ // --- Terminal multiplexers ---
3714
+ "asciinema",
3715
+ "script",
3716
+ "scriptreplay",
3717
+ "expect",
3718
+ "screen",
3719
+ "tmux",
3720
+ "byobu",
3721
+ "dtach",
3722
+ "abduco",
3723
+ // --- Calculators / REPLs / scientific ---
3724
+ "bc",
3725
+ "dc",
3726
+ "calc",
3727
+ "qalc",
3728
+ "genius",
3729
+ "octave",
3730
+ "R",
3731
+ "Rscript",
3732
+ "julia",
3733
+ "irb",
3734
+ "pry",
3735
+ "ghci",
3736
+ "stack",
3737
+ "cabal",
3738
+ "ghc",
3739
+ // --- PHP / Lua / Perl / Ruby ecosystem (extended) ---
3740
+ "php8",
3741
+ "php7",
3742
+ "phpcs",
3743
+ "phpcbf",
3744
+ "phpmd",
3745
+ "phpstan",
3746
+ "psalm",
3747
+ "lua",
3748
+ "lua5.1",
3749
+ "lua5.2",
3750
+ "lua5.3",
3751
+ "lua5.4",
3752
+ "luarocks",
3753
+ "perl",
3754
+ "cpan",
3755
+ "prove",
3756
+ "plackup",
3757
+ "rake",
3758
+ "rspec",
3759
+ "jekyll",
3760
+ "node-gyp",
3761
+ "node-pre-gyp",
3762
+ // --- JS / TS toolchain (extended) ---
3763
+ "electron",
3764
+ "electron-builder",
3765
+ "electron-forge",
3766
+ "vite-preview",
3767
+ "swc",
3768
+ "swc-cli",
3769
+ "swcpack",
3770
+ "mocha",
3771
+ "chai",
3772
+ "jasmine",
3773
+ "puppeteer",
3774
+ "lighthouse",
3775
+ // --- Linters / formatters (extended) ---
3776
+ "tslint",
3777
+ "stylelint",
3778
+ "htmlhint",
3779
+ "jshint",
3780
+ "jslint",
3781
+ "jscs",
3782
+ // --- Document conversion ---
3783
+ "pandoc",
3784
+ "weasyprint",
3785
+ "wkhtmltopdf",
3786
+ "wkhtmltoimage",
3787
+ "prince",
3788
+ "mdp",
3789
+ "markdown",
3790
+ "multimarkdown",
3791
+ "cmark",
3792
+ "cmark-gfm",
3793
+ // --- Office / spreadsheet ---
3794
+ "soffice",
3795
+ "libreoffice",
3796
+ "unoconv",
3797
+ "abiword",
3798
+ "gnumeric",
3799
+ // --- Spell / grammar ---
3800
+ "aspell",
3801
+ "hunspell",
3802
+ "enchant",
3803
+ "languagetool",
3804
+ // --- Source-highlight / diff tools ---
3805
+ "delta",
3806
+ "bat",
3807
+ "ccat",
3808
+ "hl",
3809
+ "highlight",
3810
+ "source-highlight",
3811
+ "ansifilter",
3812
+ // --- Job schedulers ---
3813
+ "pueue",
3814
+ "task-spooler",
3815
+ "ts",
3816
+ "at",
3817
+ "atd",
3818
+ "anacron",
3819
+ "fcron",
3820
+ "cronie",
3821
+ "systemd-run",
3822
+ "systemd-cat",
3823
+ // --- Plotting / visualization ---
3824
+ "gnuplot",
3825
+ "gnuplot-nox",
3826
+ "veusz",
3827
+ "scidavis",
3828
+ "grace",
3829
+ "xmgrace",
3830
+ "labplot",
3831
+ // --- Security / recon (network + web) ---
3832
+ "masscan",
3833
+ "zmap",
3834
+ "rustscan",
3835
+ "amass",
3836
+ "subfinder",
3837
+ "httpx",
3838
+ "nuclei",
3839
+ "naabu",
3840
+ "katana",
3841
+ "dnsx",
3842
+ "assetfinder",
3843
+ "findomain",
3844
+ "gau",
3845
+ "waybackurls",
3846
+ "httprobe",
3847
+ "meg",
3848
+ "subjack",
3849
+ "sublert",
3850
+ "chaos"
3340
3851
  ]);
3341
3852
  var allowedCommands = new Set(DEFAULT_ALLOWED_COMMANDS);
3342
3853
  var normalizeCmd = (c) => c.trim();
@@ -3559,17 +4070,18 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
3559
4070
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
3560
4071
  const resolved = resolveWin32Command(cmd);
3561
4072
  const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
3562
- const spawnCmd = needsShell ? cmd : resolved;
3563
- if (needsShell) assertSafeWin32ShellArgs(args);
4073
+ const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
4074
+ const spawnCmd = shim?.command ?? resolved;
4075
+ const spawnArgs = shim?.args ?? args;
3564
4076
  let child;
3565
4077
  try {
3566
- child = spawn(spawnCmd, args, {
4078
+ child = spawn(spawnCmd, spawnArgs, {
3567
4079
  cwd,
3568
4080
  env: buildChildEnv(sessionId),
3569
4081
  stdio: ["ignore", "pipe", "pipe"],
3570
4082
  windowsHide: true,
3571
4083
  ...isWin2 ? {} : { signal },
3572
- ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
4084
+ ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
3573
4085
  });
3574
4086
  } catch (err) {
3575
4087
  spool.finalize();
@@ -3664,6 +4176,7 @@ TD.addRule("stripDangerousElements", {
3664
4176
  });
3665
4177
  var MAX_BYTES2 = 131072;
3666
4178
  var TIMEOUT_MS = 2e4;
4179
+ var nativeGlobalFetch = globalThis.fetch;
3667
4180
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
3668
4181
  if (ALLOW_PRIVATE && !process.env["CI"]) {
3669
4182
  console.warn(
@@ -3713,6 +4226,9 @@ function getPinnedDispatcher() {
3713
4226
  }
3714
4227
  return pinnedAgent;
3715
4228
  }
4229
+ function dispatcherFetch() {
4230
+ return globalThis.fetch === nativeGlobalFetch ? fetch : globalThis.fetch;
4231
+ }
3716
4232
  var _beforeExitRegistered = false;
3717
4233
  if (!_beforeExitRegistered) {
3718
4234
  _beforeExitRegistered = true;
@@ -3748,7 +4264,7 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
3748
4264
  headers,
3749
4265
  dispatcher: getPinnedDispatcher()
3750
4266
  };
3751
- const res = await fetch(currentUrl, init);
4267
+ const res = await dispatcherFetch()(currentUrl, init);
3752
4268
  if (res.status < 300 || res.status > 399) {
3753
4269
  return res;
3754
4270
  }
@@ -3776,7 +4292,7 @@ var fetchTool = {
3776
4292
  category: "Network",
3777
4293
  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
4294
  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",
4295
+ permission: "auto",
3780
4296
  mutating: false,
3781
4297
  capabilities: ["net.outbound"],
3782
4298
  icon: "web",
@@ -4017,7 +4533,7 @@ var searchTool = {
4017
4533
  category: "Search",
4018
4534
  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
4535
  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",
4536
+ permission: "auto",
4021
4537
  mutating: false,
4022
4538
  capabilities: ["net.outbound"],
4023
4539
  icon: "search",
@@ -4080,15 +4596,15 @@ var searchTool = {
4080
4596
  };
4081
4597
  yield {
4082
4598
  type: "partial_output",
4083
- text: `${results.length} cached results from ${source}`,
4084
- data: { count: results.length, cached: true }
4599
+ text: `${results.length} cached results from ${entry.source}`,
4600
+ data: { count: results.length, cached: true, source: entry.source }
4085
4601
  };
4086
4602
  yield {
4087
4603
  type: "final",
4088
4604
  output: {
4089
4605
  query: input.query,
4090
4606
  results: results.slice(0, num),
4091
- source,
4607
+ source: entry.source,
4092
4608
  truncated: results.length >= num,
4093
4609
  cached: true
4094
4610
  }
@@ -4102,6 +4618,7 @@ var searchTool = {
4102
4618
  data: { source, query: input.query, cached: false }
4103
4619
  };
4104
4620
  let rawResults;
4621
+ let effectiveSource = source;
4105
4622
  switch (source) {
4106
4623
  case "duckduckgo":
4107
4624
  rawResults = await duckduckgoSearch(input.query, num, opts.signal);
@@ -4118,24 +4635,24 @@ var searchTool = {
4118
4635
  field: "source"
4119
4636
  });
4120
4637
  }
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
- }
4638
+ let ranked = rankSearchResults(rawResults, input.query);
4639
+ if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
4640
+ yield {
4641
+ type: "log",
4642
+ text: `${source} returned no relevant static results; falling back to duckduckgo`,
4643
+ data: { source, fallback: "duckduckgo", query: input.query }
4644
+ };
4645
+ rawResults = await duckduckgoSearch(input.query, num, opts.signal);
4646
+ ranked = rankSearchResults(rawResults, input.query);
4647
+ effectiveSource = "duckduckgo";
4130
4648
  }
4131
- const ranked = scoreResults(deduped, input.query);
4132
4649
  const finalResults = ranked.slice(0, num);
4133
- cache.set(cacheKey, { results: ranked, timestamp: Date.now() });
4650
+ cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
4134
4651
  pruneStaleCacheEntries();
4135
4652
  yield {
4136
4653
  type: "partial_output",
4137
- text: `${finalResults.length} results from ${source}`,
4138
- data: { count: finalResults.length, cached: false }
4654
+ text: `${finalResults.length} results from ${effectiveSource}`,
4655
+ data: { count: finalResults.length, cached: false, source: effectiveSource }
4139
4656
  };
4140
4657
  yield {
4141
4658
  type: "final",
@@ -4146,7 +4663,7 @@ var searchTool = {
4146
4663
  url: r.url,
4147
4664
  snippet: r.snippet
4148
4665
  })),
4149
- source,
4666
+ source: effectiveSource,
4150
4667
  truncated: finalResults.length >= num,
4151
4668
  cached: false
4152
4669
  }
@@ -4159,6 +4676,19 @@ function pruneStaleCacheEntries() {
4159
4676
  if (entry.timestamp < cutoff) cache.delete(key);
4160
4677
  }
4161
4678
  }
4679
+ function rankSearchResults(results, query) {
4680
+ const seenUrls = /* @__PURE__ */ new Set();
4681
+ const deduped = [];
4682
+ for (const r of results) {
4683
+ const noQuery = r.url.split("?")[0] ?? r.url;
4684
+ const normalized = noQuery.split("#")[0] ?? r.url;
4685
+ if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
4686
+ seenUrls.add(normalized);
4687
+ deduped.push(r);
4688
+ }
4689
+ }
4690
+ return scoreResults(deduped, query);
4691
+ }
4162
4692
  function scoreResults(results, query) {
4163
4693
  const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
4164
4694
  return results.map((r) => {
@@ -4172,6 +4702,15 @@ function scoreResults(results, query) {
4172
4702
  return { ...r, score };
4173
4703
  }).sort((a, b) => b.score - a.score);
4174
4704
  }
4705
+ function shouldFallbackToDuckDuckGo(results, query) {
4706
+ if (results.length === 0) return true;
4707
+ const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length >= 3);
4708
+ if (terms.length === 0) return false;
4709
+ return !results.some((r) => {
4710
+ const haystack = `${r.title} ${r.url} ${r.snippet}`.toLowerCase();
4711
+ return terms.some((term) => haystack.includes(term));
4712
+ });
4713
+ }
4175
4714
  async function duckduckgoSearch(query, num, signal) {
4176
4715
  const encoded = encodeURIComponent(query);
4177
4716
  const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
@@ -4196,14 +4735,19 @@ function takeFrom(iter, max) {
4196
4735
  }
4197
4736
  function parseDuckDuckGo(html, num) {
4198
4737
  const results = [];
4199
- const snippetRegex = /<a class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi;
4200
- const snippet2Regex = /<a class="result-snippet"[^>]*>([^<]+)<\/a>/gi;
4738
+ const linkRegex = /<a\b([^>]*\bclass=(["'])[^"']*\bresult-link\b[^"']*\2[^>]*)>([\s\S]*?)<\/a>/gi;
4739
+ const snippetRegex = /<([a-z0-9]+)\b([^>]*\bclass=(["'])[^"']*\bresult-snippet\b[^"']*\3[^>]*)>([\s\S]*?)<\/\1>/gi;
4201
4740
  const linkMatches = takeFrom(
4202
- [...html.matchAll(snippetRegex)].filter((m) => m[1] && m[2]).map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),
4741
+ [...html.matchAll(linkRegex)].map((m) => {
4742
+ const attrs = expectDefined(m[1]);
4743
+ const href = getHtmlAttr(attrs, "href");
4744
+ const title = stripTags(expectDefined(m[3]));
4745
+ return href && title ? { url: normalizeDuckDuckGoUrl(href), title } : void 0;
4746
+ }).filter((m) => m !== void 0),
4203
4747
  num
4204
4748
  );
4205
4749
  const snippetMatches = takeFrom(
4206
- [...html.matchAll(snippet2Regex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),
4750
+ [...html.matchAll(snippetRegex)].filter((m) => m[4]).map((m) => stripTags(expectDefined(m[4]))),
4207
4751
  num
4208
4752
  );
4209
4753
  for (let i = 0; i < linkMatches.length && i < num; i++) {
@@ -4219,6 +4763,24 @@ function parseDuckDuckGo(html, num) {
4219
4763
  }
4220
4764
  return results;
4221
4765
  }
4766
+ function getHtmlAttr(attrs, name) {
4767
+ const quoted = new RegExp(`\\b${name}\\s*=\\s*(["'])(.*?)\\1`, "i").exec(attrs);
4768
+ if (quoted?.[2]) return decodeHtmlEntities(quoted[2]);
4769
+ const unquoted = new RegExp(`\\b${name}\\s*=\\s*([^\\s>]+)`, "i").exec(attrs);
4770
+ return unquoted?.[1] ? decodeHtmlEntities(unquoted[1]) : void 0;
4771
+ }
4772
+ function normalizeDuckDuckGoUrl(raw) {
4773
+ if (raw.startsWith("//")) return `https:${raw}`;
4774
+ if (!raw.startsWith("/")) return raw;
4775
+ if (!raw.startsWith("/l/")) return raw;
4776
+ try {
4777
+ const url = new URL(raw, "https://duckduckgo.com");
4778
+ const uddg = url.searchParams.get("uddg");
4779
+ return uddg?.startsWith("http") ? uddg : url.toString();
4780
+ } catch {
4781
+ return raw;
4782
+ }
4783
+ }
4222
4784
  async function googleSearch(query, num, signal) {
4223
4785
  const encoded = encodeURIComponent(query);
4224
4786
  const url = `https://www.google.com/search?q=${encoded}&hl=en`;
@@ -4260,29 +4822,53 @@ async function bingSearch(query, num, signal) {
4260
4822
  }
4261
4823
  function parseBingResults(html, num) {
4262
4824
  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
- );
4825
+ 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]));
4826
+ const candidates = blocks.length > 0 ? blocks : [html];
4827
+ const entries = takeFrom(candidates.flatMap((block) => {
4828
+ const titleMatch = /<h2[^>]*>\s*<a\b([^>]*)>([\s\S]*?)<\/a>\s*<\/h2>/i.exec(block);
4829
+ if (!titleMatch) return [];
4830
+ const href = getHtmlAttr(expectDefined(titleMatch[1]), "href");
4831
+ const title = stripTags(expectDefined(titleMatch[2]));
4832
+ if (!href || !title) return [];
4833
+ 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);
4834
+ const snippet = snippetMatch ? stripTags(expectDefined(snippetMatch.at(-1))) : "";
4835
+ return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
4836
+ }), num);
4273
4837
  for (let i = 0; i < entries.length; i++) {
4274
4838
  const entry = entries[i];
4275
4839
  if (entry) {
4276
4840
  results.push({
4277
4841
  title: entry.title ?? "",
4278
4842
  url: entry.url ?? "",
4279
- snippet: snippets[i] ?? "",
4843
+ snippet: entry.snippet ?? "",
4280
4844
  score: 1
4281
4845
  });
4282
4846
  }
4283
4847
  }
4284
4848
  return results;
4285
4849
  }
4850
+ function normalizeBingUrl(raw) {
4851
+ try {
4852
+ const url = new URL(raw);
4853
+ if (url.hostname.endsWith("bing.com") && url.pathname.startsWith("/ck/")) {
4854
+ const encoded = url.searchParams.get("u");
4855
+ const decoded = decodeBingTarget(encoded);
4856
+ if (decoded?.startsWith("http")) return decoded;
4857
+ }
4858
+ } catch {
4859
+ }
4860
+ return raw;
4861
+ }
4862
+ function decodeBingTarget(encoded) {
4863
+ if (!encoded) return void 0;
4864
+ const payload = encoded.startsWith("a1") ? encoded.slice(2) : encoded;
4865
+ try {
4866
+ const padded = payload.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(payload.length / 4) * 4, "=");
4867
+ return Buffer.from(padded, "base64").toString("utf8");
4868
+ } catch {
4869
+ return void 0;
4870
+ }
4871
+ }
4286
4872
  async function fetchWithTimeout(url, signal, timeoutMs) {
4287
4873
  const controller = new AbortController();
4288
4874
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -4310,7 +4896,10 @@ function anySignal(...signals) {
4310
4896
  return AbortSignal.any(signals);
4311
4897
  }
4312
4898
  function stripTags(html) {
4313
- return html.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
4899
+ return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
4900
+ }
4901
+ function decodeHtmlEntities(text) {
4902
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
4314
4903
  }
4315
4904
  var todoTool = {
4316
4905
  name: "todo",
@@ -5880,15 +6469,16 @@ async function* spawnStream(opts) {
5880
6469
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
5881
6470
  const resolved = resolveWin32Command(opts.cmd);
5882
6471
  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, {
6472
+ const shim = needsShell ? buildWin32CmdShimInvocation(resolved, opts.args) : null;
6473
+ const cmd = shim?.command ?? resolved;
6474
+ const args = shim?.args ?? opts.args;
6475
+ const child = spawn(cmd, args, {
5886
6476
  cwd: opts.cwd,
5887
6477
  env: buildChildEnv(),
5888
6478
  stdio: ["ignore", "pipe", "pipe"],
5889
6479
  windowsHide: true,
5890
6480
  ...isWin3 ? {} : { signal: opts.signal },
5891
- ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {}
6481
+ ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
5892
6482
  });
5893
6483
  const registry = getProcessRegistry();
5894
6484
  const pid = child.pid;
@@ -6135,7 +6725,7 @@ var formatTool = {
6135
6725
  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
6726
  permission: "confirm",
6137
6727
  mutating: true,
6138
- capabilities: ["fs.write", "shell.exec"],
6728
+ capabilities: ["fs.write", "shell.restricted"],
6139
6729
  icon: "code",
6140
6730
  timeoutMs: 6e4,
6141
6731
  inputSchema: {
@@ -6788,9 +7378,10 @@ function runOutdated(manager, args, cwd, signal) {
6788
7378
  const MAX = 1e5;
6789
7379
  const resolved = resolveWin32Command(manager);
6790
7380
  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 } : {} });
7381
+ const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
7382
+ const spawnCmd = shim?.command ?? resolved;
7383
+ const spawnArgs = shim?.args ?? args;
7384
+ const child = spawn(spawnCmd, spawnArgs, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {} });
6794
7385
  child.stdout?.on("data", (c) => {
6795
7386
  if (stdout.length < MAX) stdout += c.toString();
6796
7387
  });
@@ -7395,9 +7986,9 @@ var designTool = {
7395
7986
  category: "Design",
7396
7987
  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
7988
  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: [],
7989
+ permission: "confirm",
7990
+ mutating: true,
7991
+ capabilities: ["fs.write"],
7401
7992
  timeoutMs: 15e3,
7402
7993
  inputSchema: {
7403
7994
  type: "object",
@@ -7998,7 +8589,7 @@ function rememberTool(memory) {
7998
8589
  category: "Session",
7999
8590
  description: "Persist facts, conventions, decisions, and preferences into long-term memory. Memories survive restarts and are scored for relevance in future sessions.",
8000
8591
  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",
8592
+ permission: "confirm",
8002
8593
  mutating: true,
8003
8594
  timeoutMs: 2e3,
8004
8595
  capabilities: ["memory.write"],