scan this repo for any weird stuff. if good, install. https://github.com/RaphaCastelloes/whatsapp-pi


**Inspecting the repository**

I need to inspect the repository, so cloning it seems like a good first step. I might want to use a to-do list to keep track of my thoughts. There’s also the option of checking the agents database, though that might be unnecessary. Do I need a subagent? I could ask if it’s good before proceeding with the installation, as installing might not be irreversible. Cloning to /tmp sounds like a plan!

todo

Audit whatsapp-pi repo and install if acceptable

Clone the GitHub repo, scan for suspicious or risky behavior, summarize findings, and install locally if no major issues are found.


Created #1: Audit whatsapp-pi repo and install if acceptable (pending)


bash

set -e
workdir=$(mktemp -d /tmp/whatsapp-pi-audit-XXXXXX)
echo "$workdir"
git clone --depth 1 https://github.com/RaphaCastelloes/whatsapp-pi "$workdir/repo" >/dev/null 2>&1
cd "$workdir/repo"
printf '\n== git log ==\n'
git log --oneline -n 8
printf '\n== files ==\n'
find . -maxdepth 3 -type f | sed 's#^./##' | sort | head -300
printf '\n== package files ==\n'
for f in package.json package-lock.json pnpm-lock.yaml yarn.lock Dockerfile docker-compose.yml README.md .env.example; do
  [ -f "$f" ] && { echo "--- $f ---"; sed -n '1,220p' "$f"; echo; }
done
printf '\n== search suspicious patterns ==\n'
rg -n --hidden -S "curl |wget |chmod 777|rm -rf|eval\(|child_process|exec\(|spawn\(|sudo |crypto|mining|wallet|telegram|discord|webhook|ngrok|tailscale|ssh |scp |rsync |nc |netcat|bash -c|os\.system|subprocess|fetch\(|axios|request\(|http://|https://|process\.env|localStorage|sessionStorage" . || true
printf '\n== npm scripts ==\n'
node -e 'const p=require("./package.json"); console.log(JSON.stringify(p.scripts||{},null,2)); console.log("deps",Object.keys(p.dependencies||{})); console.log("devDeps",Object.keys(p.devDependencies||{}));'


5.0.1.tgz",
./package-lock.json:4454:      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
./package-lock.json:4467:      "resolved": "https://registry.npmjs.org/codec-parser/-/codec-parser-2.5.0.tgz",
./package-lock.json:4473:      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
./package-lock.json:4485:      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
./package-lock.json:4491:      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
./package-lock.json:4503:      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
./package-lock.json:4509:      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
./package-lock.json:4516:      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
./package-lock.json:4525:      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
./package-lock.json:4532:      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
./package-lock.json:4546:      "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz",
./package-lock.json:4552:      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
./package-lock.json:4562:      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
./package-lock.json:4579:      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
./package-lock.json:4592:      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
./package-lock.json:4599:      "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
./package-lock.json:4614:      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
./package-lock.json:4623:      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
./package-lock.json:4633:      "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
./package-lock.json:4643:      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
./package-lock.json:4657:      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
./package-lock.json:4667:      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
./package-lock.json:4674:      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
./package-lock.json:4684:      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
./package-lock.json:4693:      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
./package-lock.json:4702:      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
./package-lock.json:4714:      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
./package-lock.json:4729:      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
./package-lock.json:4771:      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
./package-lock.json:4781:      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
./package-lock.json:4789:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:4794:      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
./package-lock.json:4816:      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
./package-lock.json:4863:        "url": "https://eslint.org/donate"
./package-lock.json:4876:      "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz",
./package-lock.json:4885:      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
./package-lock.json:4897:        "url": "https://opencollective.com/eslint"
./package-lock.json:4902:      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
./package-lock.json:4909:        "url": "https://opencollective.com/eslint"
./package-lock.json:4914:      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
./package-lock.json:4926:        "url": "https://github.com/sponsors/epoberezkin"
./package-lock.json:4931:      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
./package-lock.json:4942:        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
./package-lock.json:4947:      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
./package-lock.json:4954:      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
./package-lock.json:4965:      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
./package-lock.json:4977:        "url": "https://github.com/chalk/chalk?sponsor=1"
./package-lock.json:4982:      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
./package-lock.json:4990:        "url": "https://opencollective.com/eslint"
./package-lock.json:4995:      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
./package-lock.json:5005:      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
./package-lock.json:5012:      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
./package-lock.json:5025:      "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
./package-lock.json:5038:        "url": "https://opencollective.com/eslint"
./package-lock.json:5043:      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
./package-lock.json:5051:        "url": "https://opencollective.com/eslint"
./package-lock.json:5056:      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
./package-lock.json:5070:      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
./package-lock.json:5083:      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
./package-lock.json:5096:      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
./package-lock.json:5105:      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
./package-lock.json:5115:      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
./package-lock.json:5124:      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
./package-lock.json:5133:      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
./package-lock.json:5139:      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
./package-lock.json:5148:      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
./package-lock.json:5167:        "url": "https://github.com/sindresorhus/execa?sponsor=1"
./package-lock.json:5172:      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
./package-lock.json:5180:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:5185:      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
./package-lock.json:5193:        "url": "https://github.com/sponsors/isaacs"
./package-lock.json:5198:      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
./package-lock.json:5205:      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
./package-lock.json:5226:      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
./package-lock.json:5232:      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
./package-lock.json:5239:      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
./package-lock.json:5246:      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
./package-lock.json:5252:          "url": "https://github.com/sponsors/fastify"
./package-lock.json:5256:          "url": "https://opencollective.com/fastify"
./package-lock.json:5263:      "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
./package-lock.json:5269:          "url": "https://github.com/sponsors/NaturalIntelligence"
./package-lock.json:5279:      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz",
./package-lock.json:5285:          "url": "https://github.com/sponsors/NaturalIntelligence"
./package-lock.json:5300:      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
./package-lock.json:5310:      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
./package-lock.json:5327:      "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
./package-lock.json:5333:          "url": "https://github.com/sponsors/jimmywarting"
./package-lock.json:5337:          "url": "https://paypal.me/jimmywarting"
./package-lock.json:5351:      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
./package-lock.json:5364:      "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz",
./package-lock.json:5378:        "url": "https://github.com/sindresorhus/file-type?sponsor=1"
./package-lock.json:5383:      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
./package-lock.json:5395:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:5400:      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
./package-lock.json:5414:      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
./package-lock.json:5424:      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
./package-lock.json:5431:      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
./package-lock.json:5436:          "url": "https://github.com/sponsors/RubenVerborgh"
./package-lock.json:5451:      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
./package-lock.json:5467:      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
./package-lock.json:5476:      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
./package-lock.json:5488:      "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
./package-lock.json:5501:      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
./package-lock.json:5508:      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
./package-lock.json:5523:      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
./package-lock.json:5527:        "url": "https://github.com/sponsors/ljharb"
./package-lock.json:5530:    "node_modules/gaxios": {
./package-lock.json:5532:      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz",
./package-lock.json:5547:      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
./package-lock.json:5552:        "gaxios": "^7.0.0",
./package-lock.json:5562:      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
./package-lock.json:5572:      "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
./package-lock.json:5580:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:5585:      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
./package-lock.json:5595:      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
./package-lock.json:5614:        "url": "https://github.com/sponsors/ljharb"
./package-lock.json:5619:      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
./package-lock.json:5632:      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
./package-lock.json:5643:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:5648:      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
./package-lock.json:5656:        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
./package-lock.json:5661:      "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
./package-lock.json:5676:      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
./package-lock.json:5686:      "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
./package-lock.json:5699:        "url": "https://github.com/sponsors/isaacs"
./package-lock.json:5704:      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
./package-lock.json:5717:      "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
./package-lock.json:5725:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:5730:      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz",
./package-lock.json:5737:        "gaxios": "^7.1.4",
./package-lock.json:5748:      "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
./package-lock.json:5758:      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
./package-lock.json:5765:        "url": "https://github.com/sponsors/ljharb"
./package-lock.json:5770:      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
./package-lock.json:5777:      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
./package-lock.json:5786:      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
./package-lock.json:5793:        "url": "https://github.com/sponsors/ljharb"
./package-lock.json:5798:      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
./package-lock.json:5808:        "url": "https://github.com/sponsors/ljharb"
./package-lock.json:5813:      "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
./package-lock.json:5825:      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
./package-lock.json:5837:      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
./package-lock.json:5847:      "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
./package-lock.json:5853:      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",
./package-lock.json:5866:      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz",
./package-lock.json:5876:      "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
./package-lock.json:5883:      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
./package-lock.json:5897:      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
./package-lock.json:5911:      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
./package-lock.json:5921:      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
./package-lock.json:5926:          "url": "https://github.com/sponsors/feross"
./package-lock.json:5930:          "url": "https://www.patreon.com/feross"
./package-lock.json:5934:          "url": "https://feross.org/support"
./package-lock.json:5941:      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
./package-lock.json:5950:      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
./package-lock.json:5962:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:5967:      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
./package-lock.json:5977:      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
./package-lock.json:5979:      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
./package-lock.json:5989:      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
./package-lock.json:5996:      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
./package-lock.json:6006:      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
./package-lock.json:6016:      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
./package-lock.json:6026:      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
./package-lock.json:6039:      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
./package-lock.json:6047:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:6052:      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
./package-lock.json:6058:      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
./package-lock.json:6068:      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
./package-lock.json:6083:      "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
./package-lock.json:6098:      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
./package-lock.json:6109:      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
./package-lock.json:6123:      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
./package-lock.json:6130:      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
./package-lock.json:6143:      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
./package-lock.json:6153:      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
./package-lock.json:6160:      "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
./package-lock.json:6174:      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
./package-lock.json:6181:      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
./package-lock.json:6188:      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
./package-lock.json:6200:      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
./package-lock.json:6211:      "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
./package-lock.json:6220:      "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.6.tgz",
./package-lock.json:6227:        "url": "https://liberapay.com/Koromix"
./package-lock.json:6232:      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
./package-lock.json:6246:      "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz",
./package-lock.json:6262:      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
./package-lock.json:6268:      "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
./package-lock.json:6274:      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz",
./package-lock.json:6300:      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz",
./package-lock.json:6312:        "url": "https://github.com/sponsors/antfu"
./package-lock.json:6317:      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
./package-lock.json:6328:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:6333:      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
./package-lock.json:6339:      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
./package-lock.json:6345:      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
./package-lock.json:6352:      "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
./package-lock.json:6358:      "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
./package-lock.json:6368:      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
./package-lock.json:6374:      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
./package-lock.json:6384:      "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
./package-lock.json:6396:      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
./package-lock.json:6407:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:6412:      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
./package-lock.json:6419:      "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
./package-lock.json:6432:      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
./package-lock.json:6441:      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
./package-lock.json:6450:      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
./package-lock.json:6457:      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
./package-lock.json:6467:      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
./package-lock.json:6479:        "url": "https://opencollective.com/express"
./package-lock.json:6484:      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
./package-lock.json:6492:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:6497:      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
./package-lock.json:6507:        "url": "https://github.com/sponsors/isaacs"
./package-lock.json:6512:      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
./package-lock.json:6522:      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
./package-lock.json:6535:      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
./package-lock.json:6542:      "resolved": "https://registry.npmjs.org/mpg123-decoder/-/mpg123-decoder-1.0.3.tgz",
./package-lock.json:6550:        "url": "https://github.com/sponsors/eshaz"
./package-lock.json:6555:      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
./package-lock.json:6561:      "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-7.14.0.tgz",
./package-lock.json:6578:        "url": "https://github.com/sponsors/Borewit"
./package-lock.json:6583:      "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
./package-lock.json:6595:        "url": "https://github.com/sindresorhus/file-type?sponsor=1"
./package-lock.json:6600:      "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
./package-lock.json:6612:        "url": "https://github.com/sponsors/Borewit"
./package-lock.json:6617:      "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
./package-lock.json:6629:        "url": "https://github.com/sponsors/Borewit"
./package-lock.json:6634:      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
./package-lock.json:6646:      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
./package-lock.json:6652:          "url": "https://github.com/sponsors/ai"
./package-lock.json:6665:      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
./package-lock.json:6671:      "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz",
./package-lock.json:6681:      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
./package-lock.json:6688:          "url": "https://github.com/sponsors/jimmywarting"
./package-lock.json:6692:          "url": "https://paypal.me/jimmywarting"
./package-lock.json:6702:      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
./package-lock.json:6716:        "url": "https://opencollective.com/node-fetch"
./package-lock.json:6721:      "resolved": "https://registry.npmjs.org/node-wav/-/node-wav-0.0.2.tgz",
./package-lock.json:6730:      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
./package-lock.json:6741:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:6746:      "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
./package-lock.json:6754:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:6759:      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
./package-lock.json:6769:      "resolved": "https://registry.npmjs.org/ogg-opus-decoder/-/ogg-opus-decoder-1.7.3.tgz",
./package-lock.json:6780:        "url": "https://github.com/sponsors/eshaz"
./package-lock.json:6785:      "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
./package-lock.json:6794:      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
./package-lock.json:6804:      "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
./package-lock.json:6815:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:6820:      "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz",
./package-lock.json:6842:      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
./package-lock.json:6860:      "resolved": "https://registry.npmjs.org/opus-decoder/-/opus-decoder-0.7.11.tgz",
./package-lock.json:6868:        "url": "https://github.com/sponsors/eshaz"
./package-lock.json:6873:      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
./package-lock.json:6884:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:6889:      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
./package-lock.json:6900:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:6905:      "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
./package-lock.json:6919:      "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
./package-lock.json:6939:      "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
./package-lock.json:6953:      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
./package-lock.json:6966:      "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
./package-lock.json:6973:      "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
./package-lock.json:6983:      "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
./package-lock.json:6990:      "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz",
./package-lock.json:6997:      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
./package-lock.json:7007:      "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
./package-lock.json:7013:          "url": "https://github.com/sponsors/NaturalIntelligence"
./package-lock.json:7023:      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
./package-lock.json:7033:      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
./package-lock.json:7042:      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
./package-lock.json:7054:        "url": "https://github.com/sponsors/isaacs"
./package-lock.json:7059:      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz",
./package-lock.json:7069:      "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
./package-lock.json:7076:      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
./package-lock.json:7086:      "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
./package-lock.json:7094:        "url": "https://github.com/sponsors/Borewit"
./package-lock.json:7099:      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
./package-lock.json:7106:      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
./package-lock.json:7113:      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
./package-lock.json:7120:        "url": "https://github.com/sponsors/jonschlinkert"
./package-lock.json:7125:      "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
./package-lock.json:7147:      "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
./package-lock.json:7156:      "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
./package-lock.json:7162:      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
./package-lock.json:7174:      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
./package-lock.json:7181:      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
./package-lock.json:7187:          "url": "https://opencollective.com/postcss/"
./package-lock.json:7191:          "url": "https://tidelift.com/funding/github/npm/postcss"
./package-lock.json:7195:          "url": "https://github.com/sponsors/ai"
./package-lock.json:7210:      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
./package-lock.json:7220:      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
./package-lock.json:7235:      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
./package-lock.json:7244:      "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
./package-lock.json:7249:          "url": "https://github.com/sponsors/fastify"
./package-lock.json:7253:          "url": "https://opencollective.com/fastify"
./package-lock.json:7260:      "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.5.0.tgz",
./package-lock.json:7269:      "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
./package-lock.json:7281:      "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
./package-lock.json:7291:      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
./package-lock.json:7315:      "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
./package-lock.json:7335:      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
./package-lock.json:7345:      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
./package-lock.json:7352:      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
./package-lock.json:7361:      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
./package-lock.json:7372:      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
./package-lock.json:7382:      "resolved": "https://registry.npmjs.org/qified/-/qified-0.9.1.tgz",
./package-lock.json:7394:      "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.1.1.tgz",
./package-lock.json:7400:      "resolved": "https://registry.npmjs.org/qoa-format/-/qoa-format-1.0.1.tgz",
./package-lock.json:7409:      "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz",
./package-lock.json:7417:      "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
./package-lock.json:7423:      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
./package-lock.json:7430:      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
./package-lock.json:7446:      "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
./package-lock.json:7457:        "url": "https://github.com/sponsors/Borewit"
./package-lock.json:7462:      "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
./package-lock.json:7471:      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
./package-lock.json:7481:      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
./package-lock.json:7491:      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
./package-lock.json:7501:      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
./package-lock.json:7506:        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
./package-lock.json:7511:      "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
./package-lock.json:7521:      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
./package-lock.json:7566:      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
./package-lock.json:7571:          "url": "https://github.com/sponsors/feross"
./package-lock.json:7575:          "url": "https://www.patreon.com/feross"
./package-lock.json:7579:          "url": "https://feross.org/support"
./package-lock.json:7586:      "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
./package-lock.json:7595:      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
./package-lock.json:7607:      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
./package-lock.json:7619:      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
./package-lock.json:7628:      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
./package-lock.json:7635:      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
./package-lock.json:7642:      "resolved": "https://registry.npmjs.org/simple-yenc/-/simple-yenc-1.0.4.tgz",
./package-lock.json:7647:        "url": "https://github.com/sponsors/eshaz"
./package-lock.json:7652:      "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
./package-lock.json:7663:      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
./package-lock.json:7678:      "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
./package-lock.json:7693:      "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
./package-lock.json:7702:      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
./package-lock.json:7713:      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
./package-lock.json:7723:      "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
./package-lock.json:7732:      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
./package-lock.json:7739:      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
./package-lock.json:7746:      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
./package-lock.json:7755:      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
./package-lock.json:7770:      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
./package-lock.json:7780:      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
./package-lock.json:7793:      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
./package-lock.json:7804:        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
./package-lock.json:7809:      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
./package-lock.json:7817:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:7822:      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
./package-lock.json:7830:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:7835:      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz",
./package-lock.json:7843:        "url": "https://github.com/sponsors/antfu"
./package-lock.json:7848:      "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
./package-lock.json:7854:          "url": "https://github.com/sponsors/NaturalIntelligence"
./package-lock.json:7861:      "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz",
./package-lock.json:7873:        "url": "https://github.com/sponsors/Borewit"
./package-lock.json:7878:      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
./package-lock.json:7890:      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
./package-lock.json:7905:      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
./package-lock.json:7912:      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
./package-lock.json:7923:      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
./package-lock.json:7940:        "url": "https://github.com/sponsors/isaacs"
./package-lock.json:7945:      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
./package-lock.json:7958:      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
./package-lock.json:7968:      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
./package-lock.json:7981:      "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
./package-lock.json:7993:      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
./package-lock.json:8000:      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
./package-lock.json:8011:        "url": "https://github.com/sponsors/SuperchupuDev"
./package-lock.json:8016:      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz",
./package-lock.json:8026:      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
./package-lock.json:8036:      "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
./package-lock.json:8050:        "url": "https://github.com/sponsors/Borewit"
./package-lock.json:8055:      "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
./package-lock.json:8062:      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
./package-lock.json:8074:      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
./package-lock.json:8118:      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
./package-lock.json:8125:      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
./package-lock.json:8145:      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
./package-lock.json:8158:      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
./package-lock.json:8168:      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
./package-lock.json:8181:      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
./package-lock.json:8188:      "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
./package-lock.json:8196:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:8201:      "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
./package-lock.json:8211:      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
./package-lock.json:8217:      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
./package-lock.json:8227:      "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
./package-lock.json:8230:        "https://github.com/sponsors/broofa",
./package-lock.json:8231:        "https://github.com/sponsors/ctavan"
./package-lock.json:8240:      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
./package-lock.json:8247:      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
./package-lock.json:8263:        "url": "https://github.com/vitejs/vite?sponsor=1"
./package-lock.json:8307:      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz",
./package-lock.json:8325:        "url": "https://opencollective.com/vitest"
./package-lock.json:8330:      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
./package-lock.json:8347:      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
./package-lock.json:8364:      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
./package-lock.json:8381:      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
./package-lock.json:8398:      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
./package-lock.json:8415:      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
./package-lock.json:8432:      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
./package-lock.json:8449:      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
./package-lock.json:8466:      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
./package-lock.json:8483:      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
./package-lock.json:8500:      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
./package-lock.json:8517:      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
./package-lock.json:8534:      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
./package-lock.json:8551:      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
./package-lock.json:8568:      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
./package-lock.json:8585:      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
./package-lock.json:8602:      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
./package-lock.json:8619:      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
./package-lock.json:8636:      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
./package-lock.json:8653:      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
./package-lock.json:8670:      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
./package-lock.json:8687:      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
./package-lock.json:8704:      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
./package-lock.json:8721:      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
./package-lock.json:8760:      "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz",
./package-lock.json:8793:        "url": "https://opencollective.com/vitest"
./package-lock.json:8826:      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
./package-lock.json:8836:      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
./package-lock.json:8851:      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
./package-lock.json:8868:      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
./package-lock.json:8878:      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
./package-lock.json:8891:        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
./package-lock.json:8896:      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
./package-lock.json:8906:      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
./package-lock.json:8917:        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
./package-lock.json:8922:      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
./package-lock.json:8935:      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
./package-lock.json:8942:      "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
./package-lock.json:8963:      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
./package-lock.json:8973:      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
./package-lock.json:8984:        "url": "https://github.com/sponsors/eemeli"
./package-lock.json:8989:      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
./package-lock.json:9008:      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
./package-lock.json:9018:      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
./package-lock.json:9029:      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
./package-lock.json:9039:      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
./package-lock.json:9047:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:9052:      "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
./package-lock.json:9060:        "url": "https://github.com/sponsors/sindresorhus"
./package-lock.json:9065:      "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
./package-lock.json:9070:        "url": "https://github.com/sponsors/colinhacks"
./package-lock.json:9075:      "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",

== npm scripts ==
{
  "test": "vitest run",
  "typecheck": "tsc --noEmit"
}
deps [ '@whiskeysockets/baileys', 'pino', 'qrcode-terminal' ]
devDeps [
  '@mariozechner/pi-coding-agent',
  '@types/node',
  '@types/qrcode-terminal',
  '@vitest/coverage-v8',
  'ts-node',
  'tsx',
  'typescript',
  'vitest'
]


ls -d /tmp/whatsapp-pi-audit-*/repo && find /tmp/whatsapp-pi-audit-*/repo -maxdepth 3 -type f | sed 's#^#/##' | sort | head -200


ENOENT: no such file or directory, access '/tmp/whatsapp-pi-audit-*/repo/package.json'


ENOENT: no such file or directory, access '/tmp/whatsapp-pi-audit-*/repo/README.md'


**Considering repository details**

I’m thinking about whether I need to read the AGENTS files. It might not be necessary for an audit unless I’m editing or installing something. I guess it could be useful to check the README and the main files to understand better what instructions I might need. It's all a bit fuzzy right now, but gathering the right info could help clarify things for me!


whatsapp-pi

WhatsApp integration extension for Pi

pi pi-package pi-extension whatsapp baileys agent


# WhatsApp-Pi

A WhatsApp integration extension for the **[Pi Coding Agent](https://github.com/mariozechner/pi-coding-agent)**.

[![GitHub](https://img.shields.io/badge/github-repo-black.svg?style=flat-square&logo=github)](https://github.com/RaphaCastelloes/whatsapp-pi)

Pi is a powerful agentic AI coding assistant that operates in your terminal. This extension allows you to chat and pair-program with your Pi agent directly through WhatsApp, featuring message filtering, allow-listing, and reliable message delivery.

## Features

- **Manual WhatsApp Connection**: QR code-based authentication with session persistence
- **Allow List**: Control which numbers can interact with Pi
  - Add contacts with optional names for easy identification
  - View ignored numbers (not in allow list) and add them when needed
- **Reliable Messaging**: Queue-based message sending with retry logic
- **TUI Integration**: Menu-driven interface for managing connections and contacts
- **Media Support**:
  - **Vision Analysis**: Automatically forwards WhatsApp images to Pi for analysis.
  - **Document Handling**: Downloads and stores documents (PDF, text) for agent access.

## Prerequisites

To enable audio transcription features:
```bash
python -m pip install -U openai-whisper
```

To enable PDF reading capabilities (required for the agent to process documents):
- **Linux**: `sudo apt-get install poppler-utils`
- **macOS**: `brew install poppler`
- **Windows**: Install `poppler` (e.g., via Scoop) and add to PATH.

## Quick Start

1. Install the extension:
```bash
pi install npm:whatsapp-pi
```

2. Start Pi (the extension will load automatically once installed):
```bash
pi
```

To automatically connect to WhatsApp on startup (if you are already authenticated):
```bash
pi --whatsapp-pi-online
```

3. Use the menu to connect WhatsApp and manage allowed/blocked numbers

## Development / Testing

If you are developing or testing the extension locally, you can clone the repository from [GitHub](https://github.com/RaphaCastelloes/whatsapp-pi):

1. Clone and install dependencies:
```bash
git clone https://github.com/RaphaCastelloes/whatsapp-pi.git
cd whatsapp-pi
npm install
```

2. Run the extension:
```bash
pi -e whatsapp-pi.ts
```

For verbose mode (shows Baileys trace logs for debugging):
```bash
pi -e whatsapp-pi.ts --verbose
```

## Commands

- `/whatsapp` - Open the WhatsApp management menu

### Main Menu Options
- **Connect / Reconnect WhatsApp** - Start WhatsApp connection using saved credentials when available; QR code appears only if pairing is required
- **Disconnect WhatsApp** - Stop WhatsApp connection
- **Logoff (Delete Session)** - Remove all credentials and session data
- **Allowed Numbers** - Manage contacts that can interact with Pi
- **Blocked Numbers** - View ignored numbers and manage them

### Allowed Numbers Management
- **Add Number** - Add a new contact to the allow list (format: +5511999999999)
- **Select a contact** - Open a submenu with **Send Message**, **Remove Number**, and **Back**
- **Back** - Return to main menu

### Blocked Numbers Management
- **View List** - See all numbers that have been ignored (not in allow list)
- **Allow** - Move a blocked number to the allowed list
- **Delete** - Remove a number from the blocked list
- **Back** - Return to main menu

## Project Structure

```
src/
├── models/          # Type definitions
├── services/        # Core services (WhatsApp, Session, MessageSender)
└── ui/              # Menu handlers

specs/               # Feature specifications
tests/               # Unit and integration tests
```

## Development

Run tests:
```bash
npm test
```

## Implementation Notes

### Recent Feature Updates (2026-04)

- **Auto-Connect Support**: Use the `--whatsapp-pi-online` flag to automatically connect to WhatsApp on startup if you have a valid active session.
- **Vision Analysis**: Images sent via WhatsApp are automatically downloaded and forwarded to the Pi agent as base64, enabling vision-based interactions.
- **Document Message Support**:
  - WhatsApp documents (PDFs, text files, etc.) are downloaded and saved to `./.pi-data/whatsapp/documents/`.
  - The Pi agent receives a notification with the file path and metadata.
- **Full US-English Localization**: All user-facing menus, TUI notifications, console logs, and agent communication headers have been localized to `en-US` for a consistent experience.
- **Intelligent Message Filtering**:
  - **Loop Prevention**: The bot automatically ignores any message ending with the `π` symbol, preventing infinite loops between instances.
  - **Manual Interaction**: Users can now interact with the bot from their own WhatsApp account; the bot will process `fromMe` messages as long as they don't contain the bot's signature.
- **Storage Management**: All persistent data (auth state, documents, config) is centralized in the `.pi-data/` directory.


import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { SessionManager } from './src/services/session.manager.js';
import { WhatsAppService } from './src/services/whatsapp.service.js';
import { MenuHandler } from './src/ui/menu.handler.js';
import { RecentsService } from './src/services/recents.service.js';
import { AudioService } from './src/services/audio.service.js';
import { extractIncomingText } from './src/services/incoming-message.resolver.js';
import { IncomingMediaService } from './src/services/incoming-media.service.js';
import { WhatsAppPiLogger } from './src/services/whatsapp-pi.logger.js';

const shutdownState = globalThis as typeof globalThis & {
    __whatsappPiShutdown?: {
        installed: boolean;
        stop?: () => Promise<void>;
    };
};

export default function (pi: ExtensionAPI) {
    // Register verbose flag
    pi.registerFlag("verbose", {
        description: "Enable verbose mode (show Baileys trace logs)",
        type: "boolean",
        default: false
    });

    pi.registerFlag("whatsapp-pi-online", {
        description: "Enable WhatsApp-Pi on startup",
        type: "boolean",
        default: false
    });

    const sessionManager = new SessionManager();
    const whatsappService = new WhatsAppService(sessionManager);
    const recentsService = new RecentsService(sessionManager);
    const audioService = new AudioService();
    const logger = new WhatsAppPiLogger(false);
    const incomingMediaService = new IncomingMediaService(audioService, logger);
    const menuHandler = new MenuHandler(whatsappService, sessionManager, recentsService);
    let _ctx: ExtensionContext | undefined;

    const installGracefulShutdownHandlers = () => {
        shutdownState.__whatsappPiShutdown ??= { installed: false };
        if (shutdownState.__whatsappPiShutdown.installed) {
            return;
        }

        shutdownState.__whatsappPiShutdown.installed = true;

        const shutdown = async (reason: string) => {
            try {
                await shutdownState.__whatsappPiShutdown?.stop?.();
            } catch (error) {
                logger.error(`[WhatsApp-Pi] Graceful shutdown failed during ${reason}:`, error);
            }
        };

        process.once('SIGINT', () => { void shutdown('SIGINT'); });
        process.once('SIGTERM', () => { void shutdown('SIGTERM'); });
    };

    // Initial status setup
    pi.on("session_start", async (event, ctx) => {
        _ctx = ctx;
        // Check verbose mode
        const isVerboseFlagSet = process.argv.includes("--verbose");

        const isVerbose = isVerboseFlagSet;

        whatsappService.setVerboseMode(isVerbose);
        logger.setVerbose(isVerbose);

        if (isVerbose) {
            logger.log('[WhatsApp-Pi] Verbose mode enabled - Baileys trace logs will be shown');
        }
        ctx.ui.setStatus('whatsapp', '| WhatsApp: Disconnected');
        whatsappService.setStatusCallback((status) => {
            ctx.ui.setStatus('whatsapp', status);
        });
        await sessionManager.ensureInitialized();
        await recentsService.ensureInitialized();
        installGracefulShutdownHandlers();
        shutdownState.__whatsappPiShutdown = {
            installed: shutdownState.__whatsappPiShutdown?.installed ?? false,
            stop: async () => {
                await whatsappService.stop();
            }
        };
        whatsappService.setIncomingMessageRecorder(async (message) => {
            await recentsService.recordMessage({
                messageId: message.id,
                senderNumber: `+${message.remoteJid.split('@')[0]}`,
                senderName: message.pushName,
                text: message.text || '',
                direction: 'incoming',
                timestamp: message.timestamp
            });
        });

        const savedStateEntry = [...ctx.sessionManager.getEntries()]
            .reverse()
            .find(entry => entry.type === "custom" && entry.customType === "whatsapp-state");
        const isWhatsappPiOn = event.reason === "startup" && pi.getFlag("whatsapp-pi-online") === true;

        if (savedStateEntry) {
            const data = (savedStateEntry as { data?: any }).data;
            if (data.status) {
                const restoredStatus = data.status === 'connected' && !isWhatsappPiOn
                    ? 'disconnected'
                    : data.status;
                await sessionManager.setStatus(restoredStatus);
            }
            if (Array.isArray(data.allowList)) {
                for (const n of data.allowList) {
                    const num = typeof n === "string" ? n : n.number;
                    const name = typeof n === "string" ? undefined : n.name;
                    await sessionManager.addNumber(num, name);
                }
            }
        }

        // Check whatsapp flag — only auto-connect on initial startup, not reloads/new sessions
        const registered = await sessionManager.isRegistered();

        if (isWhatsappPiOn && registered) {
            ctx.ui.setStatus('whatsapp', '| WhatsApp: Auto-connecting...');

            // Retry logic (max 3 attempts, 3s delay)
            let attempts = 0;
            const maxAttempts = 4; // Initial + 3 retries

            const tryConnect = async () => {
                attempts++;
                try {
                    await whatsappService.start({ allowPairingOnAuthFailure: false });
                } catch (error) {
                    if (attempts < maxAttempts) {
                        ctx.ui.notify(`WhatsApp: Connection attempt ${attempts} failed. Retrying...`, 'warning');
                        setTimeout(tryConnect, 3000);
                    } else {
                        ctx.ui.notify('WhatsApp: Auto-connect failed after multiple attempts.', 'error');
                        ctx.ui.setStatus('whatsapp', '|  WhatsApp: Connection Failed');
                    }
                }
            };

            await tryConnect();
        } else {
            ctx.ui.notify('WhatsApp: Use Connect / Reconnect WhatsApp. QR code will appear only if pairing is needed.', 'info');
        }

        ctx.ui.notify('WhatsApp: Session reset via /new is now fully supported.', 'info');

        // Verify pdftotext availability for document support
        try {
            const { code } = await pi.exec('pdftotext', ['-v']);
            if (code !== 0 && code !== 99) { // 99 is a common exit code for -v in some versions
                throw new Error(`pdftotext returned code ${code}`);
            }
        } catch (e) {
            ctx.ui.notify('WhatsApp: pdftotext not found. PDF document support will be limited to storage only.', 'warning');
            logger.warn('[WhatsApp-Pi] Warning: pdftotext not found in system PATH.');
        }
    });

    // Handle incoming messages by injecting them as user prompts
    whatsappService.setMessageCallback(async (m) => {
        const msg = m.messages?.[0];
        if (!msg?.message) return;

        const sender = msg.key.remoteJid?.split('@')[0] || "unknown";
        const pushName = msg.pushName || "WhatsApp User";

        // Mark as read and start typing indicator immediately
        const remoteJid = msg.key.remoteJid;
        if (remoteJid && msg.key.id) {
            whatsappService.markRead(remoteJid, msg.key.id, msg.key.fromMe);
            whatsappService.sendPresence(remoteJid, 'composing');
        }

        const resolved = extractIncomingText(msg.message);
        if (resolved.kind === 'system') {
            logger.log(`[WhatsApp-Pi] ${pushName} (${sender}): ${resolved.text}`);
            return;
        }

        const { text, imageBuffer, imageMimeType } = await incomingMediaService.process(resolved, pushName);

        logger.log(`[WhatsApp-Pi] ${pushName} (${sender}): ${text}`);

        // Use a standard delivery for ALL messages to ensure TUI consistency
        if (imageBuffer && imageMimeType) {
            pi.sendUserMessage([
                { type: "text", text: `Message from ${pushName} (${sender}): ${text}` },
                { type: "image", data: imageBuffer.toString('base64'), mimeType: imageMimeType }
            ], { deliverAs: "followUp" });
        } else {
            pi.sendUserMessage(`Message from ${pushName} (${sender}): ${text}`, { deliverAs: "followUp" });
        }

        // Handle commands
        if (text.trim().toLowerCase().startsWith('/compact')) {
            logger.log(`[WhatsApp-Pi] Session compact requested by ${pushName}.`);

            if (_ctx) {
                _ctx.compact();
                await whatsappService.sendMessage(remoteJid!, "Session compacted successfully! ✅");
            }
            return;
        }

        if (text.trim().toLowerCase().startsWith('/abort')) {
            logger.log(`[WhatsApp-Pi] Abort requested by ${pushName}.`);
            if (_ctx) {
                _ctx.abort();
                await whatsappService.sendMessage(remoteJid!, "Aborted! ✅");
            }
            return;
        }

    });

    // Register send_wa_message tool (LLM-callable)
    pi.registerTool({
        name: "send_wa_message",
        label: "Send WhatsApp Message",
        description: "Send a WhatsApp message to a contact identified by their JID (e.g. 5511999998888@s.whatsapp.net). Returns a JSON result with success status and messageId or error.",
        promptSnippet: "send_wa_message(jid, message) - Send a WhatsApp message to a contact by JID",
        parameters: Type.Object({
            jid: Type.String({ minLength: 1, description: "WhatsApp JID of the recipient, e.g. 5511999998888@s.whatsapp.net" }),
            message: Type.String({ minLength: 1, description: "Plain-text message content to send" })
        }),
        async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
            if (whatsappService.getStatus() !== 'connected') {
                return {
                    isError: true,
                    details: undefined,
                    content: [{ type: "text" as const, text: JSON.stringify({ success: false, error: "WhatsApp not connected", attempts: 0 }) }]
                };
            }

            const formattedMessage = params.message
                .split('\n')
                .map((line) => `    ${line}`)
                .join('\n');

            console.log([
                '[WhatsApp-Pi] Outgoing WhatsApp message',
                `  To: ${params.jid}`,
                '  Message:',
                formattedMessage
            ].join('\n'));

            const result = await whatsappService.sendMessage(params.jid, params.message);

            if (result.success) {
                await recentsService.recordMessage({
                    messageId: result.messageId!,
                    senderNumber: `+${params.jid.split('@')[0]}`,
                    text: params.message,
                    direction: 'outgoing',
                    timestamp: Date.now()
                });
                console.log([
                    '[WhatsApp-Pi] Outgoing WhatsApp message result',
                    `  To: ${params.jid}`,
                    '  Status: sent',
                    `  MessageId: ${result.messageId ?? 'unknown'}`
                ].join('\n'));
            } else {
                console.log([
                    '[WhatsApp-Pi] Outgoing WhatsApp message result',
                    `  To: ${params.jid}`,
                    '  Status: failed',
                    `  Error: ${result.error ?? 'unknown error'}`
                ].join('\n'));
            }

            return {
                isError: !result.success,
                details: undefined,
                content: [{ type: "text" as const, text: JSON.stringify({ success: result.success, messageId: result.messageId, error: result.error, attempts: result.attempts }) }]
            };
        }
    });

    // Register commands
    pi.registerCommand("whatsapp", {
        description: "Manage WhatsApp integration",
        handler: async (args, ctx) => {
            _ctx = ctx;
            await menuHandler.handleCommand(ctx);

            // Persist state after changes
            pi.appendEntry("whatsapp-state", {
                status: sessionManager.getStatus(),
                allowList: sessionManager.getAllowList()
            });
        }
    });

    // Handle outgoing messages (Agent -> WhatsApp)
    pi.on("agent_start", async (_event, _ctx) => {
        if (sessionManager.getStatus() !== 'connected') return;
        const lastJid = whatsappService.getLastRemoteJid();
        if (lastJid) {
            await whatsappService.sendPresence(lastJid, 'composing');
        }
    });

    pi.on("message_end", async (event, ctx) => {
        if (sessionManager.getStatus() !== 'connected') return;

        const { message } = event;
        // Only reply if it's the assistant and we have a valid target
        if (message.role === "assistant") {
            const lastJid = whatsappService.getLastRemoteJid();
            const text = message.content.filter(c => c.type === "text").map(c => c.text).join("\n");

            if (lastJid && text) {
                try {
                    const result = await whatsappService.sendMessage(lastJid, text);
                    if (result.success) {
                        await recentsService.recordMessage({
                            messageId: result.messageId ?? `${Date.now()}`,
                            senderNumber: `+${lastJid.split('@')[0]}`,
                            text,
                            direction: 'outgoing',
                            timestamp: Date.now()
                        });
                        ctx.ui.notify(`Sent reply to WhatsApp contact`, 'info');
                    } else {
                        ctx.ui.notify(`Failed to send WhatsApp reply`, 'error');
                    }
                } catch (error) {
                    ctx.ui.notify(`Failed to send WhatsApp reply`, 'error');
                }
            }
        }
    });

    pi.on("session_shutdown", async () => {
        logger.log("[WhatsApp-Pi] Session shutdown detected. Stopping WhatsApp service...");
        await whatsappService.stop();
    });
}


import {
    makeWASocket,
    DisconnectReason,
    fetchLatestBaileysVersion,
    makeCacheableSignalKeyStore
} from '@whiskeysockets/baileys';
import P from 'pino';
import { SessionManager } from './session.manager.js';
import { IncomingMessage, SessionStatus } from '../models/whatsapp.types.js';
import { MessageSender } from './message.sender.js';
import { installBaileysConsoleFilter } from './baileys-console-filter.js';

export interface WhatsAppStartOptions {
    allowPairingOnAuthFailure?: boolean;
}

interface DisconnectPayload {
    error?: unknown;
}

interface ConnectionUpdateEvent {
    connection?: 'close' | 'open' | string;
    lastDisconnect?: DisconnectPayload;
    qr?: string;
}

interface IncomingMessageKey {
    id?: string;
    remoteJid?: string;
    fromMe?: boolean;
}

interface IncomingMessageContent {
    conversation?: string;
    extendedTextMessage?: { text?: string };
}

interface IncomingMessageLike {
    key: IncomingMessageKey;
    message?: IncomingMessageContent;
    pushName?: string;
    messageTimestamp?: number | string;
}

interface MessagesUpsertEvent {
    messages?: IncomingMessageLike[];
}

interface WhatsAppSocketLike {
    ev: {
        on(event: 'connection.update', handler: (update: ConnectionUpdateEvent) => void | Promise<void>): void;
        on(event: 'creds.update', handler: () => void | Promise<void>): void;
        on(event: 'messages.upsert', handler: (payload: MessagesUpsertEvent) => void | Promise<void>): void;
        removeAllListeners(event: 'connection.update' | 'creds.update' | 'messages.upsert'): void;
    };
    end(reason?: unknown): void;
    logout(): Promise<void>;
    sendMessage(jid: string, content: { text: string }): Promise<{ key?: { id?: string } } | undefined>;
    sendPresenceUpdate(presence: 'composing' | 'recording' | 'paused', jid: string): Promise<void>;
    readMessages(messages: Array<{ remoteJid: string; id: string; fromMe: boolean }>): Promise<void>;
}

interface LastDisconnectLike {
    error?: unknown;
}

interface BoomLikeError {
    output?: {
        statusCode?: number;
    };
    message?: string;
}

export class WhatsAppService {
    private socket?: WhatsAppSocketLike;
    private sessionManager: SessionManager;
    private messageSender: MessageSender;
    private isReconnecting = false;
    private verboseMode = false;
    private onIncomingMessageRecorded?: (message: IncomingMessage) => void | Promise<void>;
    private saveCreds?: () => Promise<void>;
    private restoreBaileysConsoleFilter?: () => void;
    private reconnectTimeout?: ReturnType<typeof setTimeout>;
    private onQRCode?: (qr: string) => void;
    private onMessage?: (m: MessagesUpsertEvent) => void;
    private onStatusUpdate?: (status: string) => void;
    private lastRemoteJid: string | null = null;

    constructor(sessionManager: SessionManager) {
        this.sessionManager = sessionManager;
        this.messageSender = new MessageSender(this);
    }

    public getStatus(): SessionStatus {
        return this.sessionManager.getStatus();
    }

    public getEffectiveStatus(): SessionStatus {
        const status = this.sessionManager.getStatus();
        if (status === 'connected' && !this.socket) {
            return 'disconnected';
        }

        return status;
    }

    public setIncomingMessageRecorder(callback: (message: IncomingMessage) => void | Promise<void>) {
        this.onIncomingMessageRecorded = callback;
    }

    public getSocket(): WhatsAppSocketLike | undefined {
        return this.socket;
    }

    public isVerbose(): boolean {
        return this.verboseMode;
    }

    public setVerboseMode(verbose: boolean) {
        this.verboseMode = verbose;
        if (verbose) {
            this.restoreBaileysConsoleFilter?.();
            this.restoreBaileysConsoleFilter = undefined;
        }
    }

    private normalizeContactNumber(value: string): string {
        if (value.startsWith('+')) {
            return value;
        }

        if (/^\d+$/.test(value)) {
            return `+${value}`;
        }

        return value;
    }

    private normalizeRecipientJid(jid: string): string {
        if (jid.includes('@')) return jid;
        const digits = jid.startsWith('+') ? jid.slice(1) : jid;
        return `${digits}@s.whatsapp.net`;
    }

    private getDisconnectStatusCode(error: unknown): number | undefined {
        if (!error || typeof error !== 'object') {
            return undefined;
        }

        const candidate = error as BoomLikeError;
        return candidate.output?.statusCode;
    }

    private getErrorMessage(error: unknown): string {
        if (error instanceof Error) {
            return error.message;
        }

        if (typeof error === 'object' && error !== null && 'message' in error) {
            const candidate = error as { message?: unknown };
            return typeof candidate.message === 'string' ? candidate.message : '';
        }

        return '';
    }

    private clearReconnectTimeout() {
        if (this.reconnectTimeout) {
            clearTimeout(this.reconnectTimeout);
            this.reconnectTimeout = undefined;
        }
    }

    private cleanupSocket() {
        this.clearReconnectTimeout();

        if (!this.socket) {
            return;
        }

        this.restoreBaileysConsoleFilter?.();
        this.restoreBaileysConsoleFilter = undefined;
        this.socket.ev.removeAllListeners('connection.update');
        this.socket.ev.removeAllListeners('creds.update');
        this.socket.ev.removeAllListeners('messages.upsert');

        try {
            this.socket.end(undefined);
        } catch {
            // Best-effort cleanup
        }

        this.socket = undefined;
    }

    private setSocket(socket: WhatsAppSocketLike) {
        this.socket = socket;
    }

    private registerSocketListeners(socket: WhatsAppSocketLike, options: WhatsAppStartOptions, saveCreds: () => Promise<void>) {
        socket.ev.on('creds.update', async () => {
            await saveCreds();
            await this.sessionManager.markAuthStateAvailable();
        });

        socket.ev.on('connection.update', async (update) => {
            await this.handleConnectionUpdate(update, options);
        });

        socket.ev.on('messages.upsert', (payload) => {
            void this.handleIncomingMessages(payload);
        });
    }

    private async createSocket(): Promise<WhatsAppSocketLike> {
        const { state, saveCreds } = await this.sessionManager.getAuthState();
        this.saveCreds = saveCreds;
        const { version } = await fetchLatestBaileysVersion();

        const logger = P({ level: this.verboseMode ? 'trace' : 'silent' });

        const socket = makeWASocket({
            version,
            printQRInTerminal: false,
            auth: {
                creds: state.creds,
                keys: makeCacheableSignalKeyStore(state.keys, logger)
            },
            syncFullHistory: false,
            logger
        }) as WhatsAppSocketLike;

        return socket;
    }

    async start(options: WhatsAppStartOptions = {}) {
        if (this.isReconnecting) return;
        this.onStatusUpdate?.('| WhatsApp: Connecting...');

        this.cleanupSocket();

        const originalConsoleLog = console.log;
        const originalConsoleWarn = console.warn;
        const originalConsoleError = console.error;
        let socketInitialized = false;

        if (!this.verboseMode) {
            console.log = () => {};
            console.warn = () => {};
            console.error = () => {};
        }

        try {
            const socket = await this.createSocket();
            this.setSocket(socket);
            this.registerSocketListeners(socket, options, this.saveCreds ?? (async () => {}));
            socketInitialized = true;
        } catch (error) {
            if (!this.verboseMode) {
                console.log = originalConsoleLog;
                console.warn = originalConsoleWarn;
                console.error = originalConsoleError;
            }
            throw error;
        } finally {
            if (!this.verboseMode) {
                console.log = originalConsoleLog;
                console.warn = originalConsoleWarn;
                console.error = originalConsoleError;
                if (socketInitialized) {
                    this.restoreBaileysConsoleFilter = installBaileysConsoleFilter(this.verboseMode);
                }
            }
        }
    }

    private async handleConnectionUpdate(update: ConnectionUpdateEvent, options: WhatsAppStartOptions) {
        const { connection, lastDisconnect, qr } = update;
        const allowPairingOnAuthFailure = options.allowPairingOnAuthFailure ?? true;

        if (qr) {
            await this.handlePairingQr(qr);
        }

        if (connection === 'close') {
            await this.handleConnectionClosed(lastDisconnect, allowPairingOnAuthFailure, options);
            return;
        }

        if (connection === 'open') {
            await this.handleConnectionOpen();
        }
    }

    private async handlePairingQr(qr: string) {
        this.sessionManager.setStatus('pairing');
        this.onQRCode?.(qr);
        this.onStatusUpdate?.('| WhatsApp: type /whatsapp to connect');
    }

    private async handleConnectionOpen() {
        if (this.verboseMode) {
            console.log('WhatsApp connection successfully opened');
        }

        this.isReconnecting = false;
        this.clearReconnectTimeout();
        await this.saveCreds?.();
        await this.sessionManager.markAuthStateAvailable();
        this.sessionManager.setStatus('connected');
        this.onStatusUpdate?.('| WhatsApp: Connected');
    }

    private isBadMacError(errorMessage: string): boolean {
        return errorMessage.includes('Bad MAC');
    }

    private isAuthRejected(statusCode: number | undefined, errorMessage: string): boolean {
        return errorMessage.includes('bad-request')
            || statusCode === 400
            || statusCode === 401
            || statusCode === DisconnectReason.loggedOut
            || statusCode === DisconnectReason.badSession;
    }

    private async handleConnectionClosed(
        lastDisconnect: LastDisconnectLike | undefined,
        allowPairingOnAuthFailure: boolean,
        options: WhatsAppStartOptions
    ) {
        const statusCode = this.getDisconnectStatusCode(lastDisconnect?.error);
        const errorMessage = this.getErrorMessage(lastDisconnect?.error);
        const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
        const isBadMac = this.isBadMacError(errorMessage);
        const isAuthRejected = this.isAuthRejected(statusCode, errorMessage);
        const shouldTreatAsLoggedOut = isBadMac || isAuthRejected;

        if (this.verboseMode) {
            console.error(`Connection closed [${statusCode}]. Reconnecting: ${shouldReconnect}`);
        }

        if (shouldTreatAsLoggedOut) {
            if (isAuthRejected && !isBadMac && allowPairingOnAuthFailure) {
                if (this.verboseMode) {
                    console.error(`Session rejected [${statusCode}] - clearing auth state and starting pairing`);
                }
                await this.sessionManager.deleteAuthState();
                this.cleanupSocket();
                this.socket = undefined;
                this.isReconnecting = false;
                await this.start({ allowPairingOnAuthFailure: false });
                return;
            }

            if (this.verboseMode) {
                console.error(`Session invalid or logged out [${statusCode}] - preserving auth state and requiring re-auth`);
            }
            if (isBadMac) {
                if (this.verboseMode) {
                    console.error('[WhatsApp-Pi] Bad MAC error detected. Your session keys are corrupted.');
                    console.error('[WhatsApp-Pi] Run /whatsapp-logout to clear auth state, then reconnect with /whatsapp-connect');
                }
                this.onStatusUpdate?.('| WhatsApp: Session Error (Bad MAC)');
            }
            this.sessionManager.setStatus('logged-out');
            if (!isBadMac) {
                this.onStatusUpdate?.('| WhatsApp: Logged out');
            }
            return;
        }

        if (statusCode === DisconnectReason.connectionReplaced) {
            if (this.verboseMode) {
                console.error('Connection replaced - another instance connected');
            }
            this.onStatusUpdate?.('| WhatsApp: Conflict (Another Instance)');
            return;
        }

        if (shouldReconnect && !this.isReconnecting) {
            this.isReconnecting = true;
            this.onStatusUpdate?.('| WhatsApp: Reconnecting...');
            this.clearReconnectTimeout();
            this.reconnectTimeout = setTimeout(() => {
                this.isReconnecting = false;
                void this.start(options);
            }, 3000);
        } else if (!shouldReconnect) {
            this.sessionManager.setStatus('logged-out');
            this.onStatusUpdate?.('| WhatsApp: Disconnected');
        }
    }

    private extractText(message: IncomingMessageContent | undefined): string {
        return message?.conversation || message?.extendedTextMessage?.text || '';
    }

    private isPiGeneratedMessage(text: string): boolean {
        return text.endsWith('π');
    }

    private getIncomingTimestamp(timestamp: number | string | undefined): number {
        if (typeof timestamp === 'number') {
            return timestamp;
        }

        if (typeof timestamp === 'string') {
            const parsed = Number(timestamp);
            return Number.isFinite(parsed) ? parsed : Date.now();
        }

        return Date.now();
    }

    private async recordIncomingMessage(message: IncomingMessageLike, remoteJid: string, text: string) {
        void Promise.resolve(this.onIncomingMessageRecorded?.({
            id: message.key.id ?? remoteJid,
            remoteJid,
            pushName: message.pushName || undefined,
            text,
            timestamp: this.getIncomingTimestamp(message.messageTimestamp)
        })).catch(error => {
            if (this.verboseMode) {
                console.error('Failed to record recent message:', error);
            }
        });
    }

    public async handleIncomingMessages(payload: MessagesUpsertEvent) {
        if (this.sessionManager.getStatus() !== 'connected') return;

        const message = payload.messages?.[0];
        if (!message || !message.key.remoteJid) return;

        const text = this.extractText(message.message);
        if (this.isPiGeneratedMessage(text)) return;

        const remoteJid = message.key.remoteJid;
        if (remoteJid.endsWith('@g.us')) return;

        const senderJid = this.normalizeContactNumber(remoteJid.split('@')[0]);
        void this.recordIncomingMessage(message, remoteJid, text);

        if (this.sessionManager.isBlocked(senderJid)) {
            if (this.isVerbose()) {
                console.log(`Ignoring message from ${senderJid} (explicitly blocked)`);
            }
            return;
        }

        if (!this.sessionManager.isAllowed(senderJid)) {
            if (this.isVerbose()) {
                console.log(`Ignoring message from ${senderJid} (not in allow list)`);
            }
            const pushName = message.pushName || undefined;
            await this.sessionManager.trackIgnoredNumber(senderJid, pushName);
            return;
        }

        this.lastRemoteJid = remoteJid;
        this.onMessage?.(payload);
    }

    setQRCodeCallback(callback: (qr: string) => void) {
        this.onQRCode = callback;
    }

    setMessageCallback(callback: (m: MessagesUpsertEvent) => void) {
        this.onMessage = callback;
    }

    setStatusCallback(callback: (status: string) => void) {
        this.onStatusUpdate = callback;
    }

    public getLastRemoteJid(): string | null {
        return this.lastRemoteJid;
    }

    private getActiveSocket(): WhatsAppSocketLike | null {
        if (!this.socket || this.getStatus() !== 'connected') {
            return null;
        }

        return this.socket;
    }

    async sendMessage(jid: string, text: string) {
        // Ensure we show the typing indicator before sending
        await this.sendPresence(jid, 'composing');

        const result = await this.messageSender.send({
            recipientJid: jid,
            text: text
        });

        // After sending, we can stop the typing indicator
        await this.sendPresence(jid, 'paused');

        if (!result.success) {
            console.error(`Failed to send message to ${jid}: ${result.error}`);
        }

        return result;
    }

    async sendMenuMessage(jid: string, text: string) {
        const normalizedJid = this.normalizeRecipientJid(jid);
        const socket = this.getActiveSocket();

        if (!socket) {
            return {
                success: false,
                error: 'WhatsApp is not connected',
                attempts: 0
            };
        }

        try {
            await this.sendPresence(normalizedJid, 'composing');
            const response = await socket.sendMessage(normalizedJid, { text });
            await this.sendPresence(normalizedJid, 'paused');

            return {
                success: true,
                messageId: response?.key?.id,
                attempts: 1
            };
        } catch (error: unknown) {
            await this.sendPresence(normalizedJid, 'paused');
            console.error(`Failed to send menu message to ${normalizedJid}:`, error);
            return {
                success: false,
                error: error instanceof Error ? error.message : 'Unknown error',
                attempts: 1
            };
        }
    }

    async sendPresence(jid: string, presence: 'composing' | 'recording' | 'paused') {
        const socket = this.getActiveSocket();
        if (!socket) return;
        try {
            await socket.sendPresenceUpdate(presence, jid);
        } catch (error) {
            if (this.verboseMode) {
                console.error(`Failed to send presence update to ${jid}:`, error);
            }
        }
    }

    async markRead(jid: string, messageId: string, fromMe: boolean = false) {
        const socket = this.getActiveSocket();
        if (!socket) return;
        try {
            await socket.readMessages([{ remoteJid: jid, id: messageId, fromMe }]);
        } catch (error) {
            if (this.verboseMode) {
                console.error(`Failed to mark message as read:`, error);
            }
        }
    }

    async logout() {
        await this.socket?.logout();
        await this.sessionManager.deleteAuthState();
    }

    async stop() {
        try {
            await this.saveCreds?.();
        } catch (error) {
            if (this.verboseMode) {
                console.error('Failed to persist auth state during stop:', error);
            }
        }

        this.cleanupSocket();
        this.isReconnecting = false;
        await this.sessionManager.setStatus('disconnected');
        this.onStatusUpdate?.('| WhatsApp: Disconnected');
    }
}


import { useMultiFileAuthState } from '@whiskeysockets/baileys';
import { join } from 'path';
import { readFile, writeFile, mkdir, rm, readdir } from 'fs/promises';
import { homedir } from 'os';
import { SessionStatus } from '../models/whatsapp.types.js';

export interface Contact {
    number: string;
    name?: string;
}

export class SessionManager {
    // Data is stored in the user's home directory to persist across updates
    private readonly baseDir = join(homedir(), '.pi', 'whatsapp-pi');
    private readonly authStateDir = join(this.baseDir, 'auth');
    private readonly configPath = join(this.baseDir, 'config.json');

    private status: SessionStatus = 'logged-out';
    private allowList: Contact[] = [];
    private blockList: Contact[] = [];
    private ignoredNumbers: Contact[] = [];
    private hasAuthState = false;
    private openaiKey: string = '';
    private visionModel: string = 'gpt-4o';

    private async ensureStorageDirectories() {
        await mkdir(this.baseDir, { recursive: true });
        await mkdir(this.authStateDir, { recursive: true });
    }

    public async ensureInitialized() {
        try {
            await this.ensureStorageDirectories();
            await this.loadConfig();
            await this.syncAuthStateFromDisk();
        } catch (error) {}
    }

    private async loadConfig() {
        try {
            const data = await readFile(this.configPath, 'utf-8');
            const config = JSON.parse(data);

            const cleanContact = (item: any): Contact | null => {
                if (typeof item === 'string') return { number: item };
                if (item && typeof item === 'object') {
                    let num = item.number;
                    // Unroll nested objects if any
                    while (num && typeof num === 'object' && num.number) {
                        num = num.number;
                    }
                    if (typeof num === 'string') {
                        return { number: num, name: item.name };
                    }
                }
                return null;
            };

            this.allowList = (config.allowList || []).map(cleanContact).filter(Boolean) as Contact[];
            this.blockList = (config.blockList || []).map(cleanContact).filter(Boolean) as Contact[];
            this.ignoredNumbers = (config.ignoredNumbers || []).map(cleanContact).filter(Boolean) as Contact[];
            this.status = config.status || 'logged-out';
            this.hasAuthState = Boolean(config.hasAuthState);
            this.openaiKey = config.openaiKey || '';
            this.visionModel = config.visionModel || 'gpt-4o';
        } catch (error) {
            // File not found is fine
        }
    }

    public async saveConfig() {
        try {
            const config = {
                allowList: this.allowList,
                blockList: this.blockList,
                ignoredNumbers: this.ignoredNumbers,
                status: this.status,
                hasAuthState: this.hasAuthState,
                openaiKey: this.openaiKey,
                visionModel: this.visionModel
            };
            await writeFile(this.configPath, JSON.stringify(config, null, 2));
        } catch (error) {
            console.error('Failed to save config:', error);
        }
    }

    getAllowList(): Contact[] {
        return this.allowList;
    }

    getAllowedContact(number: string): Contact | undefined {
        return this.allowList.find(c => c.number === number);
    }

    getBlockList(): Contact[] {
        return this.blockList;
    }

    getIgnoredNumbers(): Contact[] {
        return this.ignoredNumbers;
    }

    async removeIgnoredNumber(number: string) {
        this.ignoredNumbers = this.ignoredNumbers.filter(c => c.number !== number);
        await this.saveConfig();
    }

    async addNumber(number: any, name?: string) {
        // Handle potential nested objects from legacy bugs
        let cleanNumber = number;
        while (cleanNumber && typeof cleanNumber === 'object' && cleanNumber.number) {
            cleanNumber = cleanNumber.number;
        }

        if (typeof cleanNumber !== 'string') {
            console.warn('[SessionManager] Attempted to add invalid number:', cleanNumber);
            return;
        }

        const existing = this.allowList.find(c => c.number === cleanNumber);
        if (!existing) {
            this.allowList.push({ number: cleanNumber, name });
            // Remove from blockList and ignoredNumbers if it was there
            this.blockList = this.blockList.filter(c => c.number !== cleanNumber);
            this.ignoredNumbers = this.ignoredNumbers.filter(c => c.number !== cleanNumber);
            await this.saveConfig();
            return;
        }

        if (name && !existing.name) {
            existing.name = name;
            await this.saveConfig();
        }
    }

    async removeNumber(number: string) {
        this.allowList = this.allowList.filter(c => c.number !== number);
        await this.saveConfig();
    }

    async setAllowedContactAlias(number: string, alias: string) {
        const trimmedAlias = alias.trim();
        if (!trimmedAlias) {
            return;
        }

        const contact = this.getAllowedContact(number);
        if (!contact) {
            return;
        }

        contact.name = trimmedAlias;
        await this.saveConfig();
    }

    async removeAllowedContactAlias(number: string) {
        const contact = this.getAllowedContact(number);
        if (!contact || !contact.name) {
            return;
        }

        delete contact.name;
        await this.saveConfig();
    }

    async blockNumber(number: string, name?: string) {
        if (!this.blockList.find(c => c.number === number)) {
            this.blockList.push({ number, name });
            // Remove from allowList if it was there
            this.allowList = this.allowList.filter(c => c.number !== number);
            await this.saveConfig();
        }
    }

    async unblockNumber(number: string) {
        this.blockList = this.blockList.filter(c => c.number !== number);
        await this.saveConfig();
    }

    async unblockAndAllow(number: string) {
        const blocked = this.blockList.find(c => c.number === number);
        this.blockList = this.blockList.filter(c => c.number !== number);
        if (!this.allowList.find(c => c.number === number)) {
            this.allowList.push({ number, name: blocked?.name });
        }
        await this.saveConfig();
    }

    isAllowed(number: string): boolean {
        return this.allowList.some(c => c.number === number);
    }

    isBlocked(number: string): boolean {
        return this.blockList.some(c => c.number === number);
    }

    async trackIgnoredNumber(number: string, name?: string) {
        // Only track if not already in allow list, block list, or ignored list
        if (!this.allowList.find(c => c.number === number) &&
            !this.blockList.find(c => c.number === number) &&
            !this.ignoredNumbers.find(c => c.number === number)) {
            this.ignoredNumbers.push({ number, name });
            await this.saveConfig();
        }
    }

    public async isRegistered(): Promise<boolean> {
        try {
            const credsPah = join(this.authStateDir, 'creds.json');
            await readFile(credsPah);
            this.hasAuthState = true;
            return true;
        } catch {
            await this.syncAuthStateFromDisk();
            return this.hasAuthState;
        }
    }

    async markAuthStateAvailable() {
        if (!this.hasAuthState) {
            this.hasAuthState = true;
            await this.saveConfig();
        }
    }

    async getAuthState() {
        await this.ensureStorageDirectories();
        return await useMultiFileAuthState(this.authStateDir);
    }

    private async syncAuthStateFromDisk() {
        try {
            const entries = await readdir(this.authStateDir);
            if (entries.length > 0) {
                if (!this.hasAuthState) {
                    this.hasAuthState = true;
                    await this.saveConfig();
                }
            }
        } catch {
            // Ignore missing directory / empty auth state
        }
    }

    async deleteAuthState() {
        try {
            await rm(this.authStateDir, { recursive: true, force: true });
            await mkdir(this.authStateDir, { recursive: true });
            this.status = 'logged-out';
            this.hasAuthState = false;
            await this.saveConfig();
        } catch (error) {
            console.error('Failed to delete auth state:', error);
        }
    }

    getStatus(): SessionStatus {
        return this.status;
    }

    async setStatus(status: SessionStatus) {
        this.status = status;
        await this.saveConfig();
    }

    getOpenaiKey(): string {
        return this.openaiKey;
    }

    async setOpenaiKey(key: string) {
        this.openaiKey = key;
        await this.saveConfig();
    }

    getVisionModel(): string {
        return this.visionModel;
    }

    async setVisionModel(model: string) {
        this.visionModel = model;
        await this.saveConfig();
    }

    getAuthStateDir(): string {
        return this.authStateDir;
    }
}