diff --git a/LICENSE_AUDIT.md b/LICENSE_AUDIT.md new file mode 100644 index 00000000000..8e4a63c81c9 --- /dev/null +++ b/LICENSE_AUDIT.md @@ -0,0 +1,282 @@ +# Hermes Agent โ€” Dependency License Audit + +**Date:** 2026-07-20 +**Project license:** MIT (Copyright 2025 Nous Research) +**Audited by:** ethie (automated โ€” PyPI/npm/GitHub registry queries + lockfile parsing) + +## Scope + +Every dependency that ships in a Hermes Agent distribution, across all three +ecosystems and all four npm lockfiles: + +| Ecosystem | Lockfile | Packages | +|-----------|----------|----------| +| Python | `uv.lock` | 233 | +| npm (root workspaces) | `package-lock.json` | 1,387 | +| npm (website / Docusaurus) | `website/package-lock.json` | 1,443 | +| npm (whatsapp-bridge) | `scripts/whatsapp-bridge/package-lock.json` | 167 | +| npm (photon sidecar) | `plugins/platforms/photon/sidecar/package-lock.json` | 144 | +| npm (deduped across all 4) | โ€” | 2,401 unique | +| Rust | `apps/bootstrap-installer/src-tauri/Cargo.toml` | 19 direct (no `Cargo.lock`) | + +**Total: 2,653 dependency versions audited.** + +## Executive summary + +The overwhelming majority of dependencies (2,564 / 2,653 = 96.6%) are under +permissive licenses (MIT, ISC, Apache-2.0, BSD, PSF) that are fully +compatible with the project's MIT license. No action is needed for those. + +**9 packages require attention** โ€” they fall into copyleft, attribution-required, +proprietary, or unlicensed categories that create real compliance obligations. +The most serious is **`libsignal` (GPL-3.0)** in the whatsapp-bridge, which has +strong copyleft implications if the bridge is distributed as part of Hermes. + +**There is currently zero license-compliance scanning in CI.** The existing +`osv-scanner.yml` covers CVEs only; `supply-chain-audit.yml` covers malware +patterns only. Neither checks license compatibility. There is no `NOTICE` or +`THIRD_PARTY_LICENSES` file at the repo root (the only `NOTICE` is inside +`plugins/security-guidance/` for forked Anthropic Apache-2.0 code). + +--- + +## Current compliance posture + +| What exists | What it covers | Gap | +|-------------|----------------|-----| +| `osv-scanner.yml` | Known CVEs in pinned deps (vuln DB) | Does not check licenses | +| `supply-chain-audit.yml` | Malware patterns in PR diffs (.pth, base64+exec, etc.) | Does not check licenses | +| `tests/docker/test_license_file_present.py` | Our own LICENSE ships in the Docker image | Only checks our LICENSE, not deps | +| `plugins/security-guidance/NOTICE` | Forked Anthropic code attribution | Local to that plugin only | +| **Missing** | **License compatibility scanning** | **No CI check prevents adding a GPL/AGPL/unlicensed dep** | +| **Missing** | **NOTICE / THIRD_PARTY_LICENSES file** | **No bundled attribution for permissive licenses that require it** | + +--- + +## Findings by severity + +### ๐Ÿ”ด Critical โ€” GPL-3.0 (strong copyleft) + +#### `libsignal@6.0.0` (npm, whatsapp-bridge) + +- **License:** GPL-3.0 +- **Source:** `https://github.com/WhiskeySockets/libsignal-node` +- **Where:** `scripts/whatsapp-bridge/package.json` โ†’ `@whiskeysockets/baileys` depends on it +- **Direct dep?** No โ€” transitive via `@whiskeysockets/baileys` +- **Compliance issue:** GPL-3.0 is strong copyleft. If the whatsapp-bridge is + distributed as part of Hermes Agent (which is MIT), the GPL-3.0 license + would require the *entire combined work* to be GPL-3.0. This is a + license conflict โ€” MIT and GPL-3.0 are compatible (MIT code can be included + in a GPL-3.0 project), but only if the combined work is distributed under + GPL-3.0, which contradicts Hermes's MIT license. +- **Mitigation:** The whatsapp-bridge is a standalone sidecar script + (`scripts/whatsapp-bridge/bridge.js`), not imported by the Python core. + If it ships as a separate process invoked via subprocess (not linked + into the Hermes binary), the "aggregate, not derivative" argument may + apply. **This needs a legal review** to confirm the boundary. + +### ๐ŸŸ  High โ€” LGPL-3.0 (weak copyleft) + +LGPL allows linking from non-LGPL code, but modifications to the LGPL +library itself must be released under LGPL, and the license + source +must be offered to recipients. + +#### Python + +| Package | Version | Extra | Notes | +|---------|---------|-------|-------| +| `edge-tts` | 7.2.7 | `[edge-tts]` | Default TTS provider. LICENSE file confirms: one file (srt_composer.py) is MIT, rest is LGPLv3. | +| `python-telegram-bot` | 22.6 | `[messaging]`, `[termux]` | PEP 639 `license_expression: LGPL-3.0-only`. LICENSE file ships GPL-3 text (LGPL is a permitted additional permission). | + +#### npm (sharp / libvips native binaries) + +| Package | Version | License | Notes | +|---------|---------|---------|-------| +| `@img/sharp-libvips-*` (10 platform variants) | 1.3.2 | LGPL-3.0-or-later | libvips native binaries. Pulled by `sharp` (image processing). | +| `@img/sharp-win32-*` (3 variants) | 0.35.3 | Apache-2.0 AND LGPL-3.0-or-later | sharp Windows binaries. | +| `@img/sharp-wasm32` | 0.35.3 | Apache-2.0 AND LGPL-3.0-or-later AND MIT | sharp wasm binary. | + +- **Where:** Root `package-lock.json`, transitive via `sharp` +- **Direct dep?** No โ€” `sharp` is not declared in any workspace `package.json`; + it's pulled transitively by an upstream package. +- **Compliance issue:** LGPL-3.0-or-later allows use in non-LGPL projects, + but requires: (1) providing the LGPL license text, (2) providing a way to + relink against a modified version of the library. For statically-linked + native binaries, this means documenting how to swap the .node/.so/.dll. +- **Mitigation:** Include LGPL license text in NOTICE file. The "provide a + way to relink" obligation is satisfied by the npm package boundary (users + can `npm install` a different `sharp` build). + +### ๐ŸŸก Medium โ€” MPL-2.0 / MPL-1.1 (file-level weak copyleft) + +MPL is file-level copyleft: modifications to MPL-licensed *files* must stay +MPL, but it does not infect the rest of the project. Compatible with MIT. + +| Package | Version | Ecosystem | Notes | +|---------|---------|-----------|-------| +| `lightningcss` (+ 11 platform variants) | 1.32.0 | npm | CSS transformer, pulled by `@tailwindcss` + `vite`. | +| `lunr-languages` | 1.14.0 | npm (website) | Language packs for Docusaurus search. Old MPL-1.1. | +| `certifi` | 2026.5.20 | python | CA bundle. | +| `mautrix` | 0.21.0 | python | Matrix SDK (`[matrix]` extra). | +| `pathspec` | 1.1.1 | python | .gitignore matching. Core dep. | + +- **Compliance:** Include MPL license text in NOTICE. No source disclosure + obligation. If we modify any MPL file, that file must stay MPL. + +### ๐ŸŸก Medium โ€” CC-BY-4.0 (attribution required) + +CC-BY-4.0 requires attribution in a way that is "reasonable to the medium." +For software distribution, this means the NOTICE file. + +| Package | Version | Ecosystem | Notes | +|---------|---------|-----------|-------| +| `@vscode/codicons` | 0.0.45 | npm | Icon font, direct dep in `apps/desktop` + `apps/bootstrap-installer`. | +| `caniuse-lite` | 1.0.30001799 | npm | Browser support DB, transitive via `browserslist`. | + +- **Compliance:** Add attribution lines to NOTICE file. + +### ๐ŸŸก Medium โ€” Non-standard / proprietary license + +#### `gsap@3.15.0` (npm) + +- **License:** "Standard 'no charge' license: https://gsap.com/standard-license" +- **Where:** `web/package.json` (direct dep), also pulled by `@nous-research/ui` +- **Compliance issue:** This is not an OSI-approved license. GSAP's own terms + apply. Per gsap.com: "GSAP is now free for everyone, thanks to Webflow's + support!" โ€” but the license has specific terms (no resale as a standalone + library, etc.) that differ from MIT. +- **Compliance:** Reproduce GSAP's license terms in the NOTICE file with a + link to the full text. + +### ๐ŸŸข Low โ€” No license (all-rights-reserved) + +| Package | Version | Ecosystem | Notes | +|---------|---------|-----------|-------| +| `hindsight-client` | 0.6.1 | python | Optional `[hindsight]` extra. No LICENSE in wheel, no repo link. | +| `format` | 0.2.2 | npm (website) | sprintf library. No LICENSE anywhere. Transitive via Docusaurus. | + +- **Compliance issue:** Without a license, the default is "all rights reserved" + โ€” the copyright holder has not granted permission to use, modify, or + distribute. Technically, using these at all is a copyright infringement. +- **Mitigation:** + - `hindsight-client`: Contact the "Hindsight Team" for a license + clarification, or remove the extra and document it as user-installed. + - `format`: It's a tiny, ancient (2012-era) sprintf utility pulled + transitively by Docusaurus. Contact the author (samsonjs) or replace + with a licensed alternative. Alternatively, Docusaurus may have already + addressed this upstream. + +### โœ… Resolved (were "missing" in metadata, verified permissive) + +These packages had no license in their npm/PyPI metadata but were verified +permissive by fetching their LICENSE files from GitHub or inspecting tarballs: + +| Package | Resolved license | How verified | +|---------|-----------------|--------------| +| `khroma` | MIT | GitHub `fabiospampinato/khroma` LICENSE file | +| `eval` | MIT | GitHub `pierrec/node-eval` LICENSE file | +| `require-like` | MIT | GitHub `felixge/node-require-like` package.json | +| `qrcode-terminal` | MIT | GitHub `gtanner/qrcode-terminal` LICENSE file | +| `@photon-ai/slack` | MIT | Tarball `package/proto/LICENSE`: "Copyright (c) 2025 Photon AI" | +| `@photon-ai/whatsapp-business` | Likely MIT | Same Photon AI scope; no LICENSE in tarball (unverified) | +| `agent-client-protocol` | Apache-2.0 | GitHub `agentclientprotocol/python-sdk` LICENSE | +| `azure-core` | MIT | GitHub `Azure/azure-sdk-for-python` LICENSE | +| `azure-identity` | MIT | GitHub `Azure/azure-sdk-for-python` LICENSE | +| `fal-client` | Apache-2.0 | GitHub `fal-ai/fal` LICENSE | +| `mistralai` | Apache-2.0 | GitHub `mistralai/client-python` LICENSE | +| `microsoft-teams-*` (4 pkgs) | MIT | GitHub `microsoft/teams.py` LICENSE | +| `honcho-ai` | Apache-2.0 | PyPI `license_expression` field | + +### โœ… No issue (compound permissive licenses) + +| Package | License | Why it's fine | +|---------|---------|---------------| +| `@bufbuild/protobuf` | Apache-2.0 AND BSD-3-Clause | Both permissive | +| `dompurify` | MPL-2.0 OR Apache-2.0 | Can elect Apache-2.0 | + +--- + +## Rust (Cargo) + +No `Cargo.lock` exists in the repo, so a transitive audit is deferred. +All 19 direct dependencies in `apps/bootstrap-installer/src-tauri/Cargo.toml` +are permissive (MIT or Apache-2.0 or both). Run `cargo generate-lockfile` +before the installer is first built/distributed to enable the transitive check. + +--- + +## Compliance plan + +### 1. Create a root `NOTICE` file + +The biggest gap. Many permissive licenses (MIT, Apache-2.0, BSD) require +retaining the copyright + license notice when distributed. Currently we ship +our own `LICENSE` but don't bundle attribution for dependencies. + +**Action:** Generate `NOTICE` (or `THIRD_PARTY_LICENSES.md`) at repo root, +containing: +- The project's own MIT license + copyright +- For each non-MIT dependency: the license name, copyright holder, and + license text (or link) +- Attribution blocks for CC-BY-4.0 packages (codicons, caniuse-lite) +- Reproduced terms for GSAP's non-standard license +- LGPL/MPL license texts for the copyleft deps + +This can be auto-generated from the lockfiles using a tool like +`pip-licenses` (Python) + `license-checker` (npm) in CI. + +### 2. Add license-scanning CI + +**Action:** Add a `license-audit.yml` workflow that: +- Runs `pip-licenses` (or `uv pip licenses`) against `uv.lock` +- Runs `license-checker` (or `licensee`) against all 4 `package-lock.json` files +- Fails on: GPL, AGPL, unlisted, or "unknown" licenses +- Warns on: LGPL, MPL, CC-BY, non-OSI licenses (advisory, not blocking) +- Posts findings as a collapsible `
` PR comment + +This closes the gap where a PR could add a GPL-3.0 dependency and CI would +never catch it. + +### 3. Resolve the `libsignal` GPL-3.0 question + +**Action:** Legal review to confirm whether `scripts/whatsapp-bridge/` is a +separate aggregate (not a derivative of Hermes) when distributed. If the +bridge ships as a standalone subprocess invoked via `node bridge.js`, the +GPL-3.0 obligation may not extend to Hermes itself. If there's any doubt, +document the boundary clearly or move the bridge to a separate repo. + +### 4. Resolve unlicensed packages + +**Action:** +- `hindsight-client`: Contact the Hindsight Team for a license, or move the + `[hindsight]` extra to lazy-install only (it already is, partially). +- `format`: Check if Docusaurus has dropped or replaced it upstream. If not, + file an issue with samsonjs/format to add a license, or patch it out. + +### 5. Generate `Cargo.lock` for the bootstrap installer + +**Action:** Run `cargo generate-lockfile` in +`apps/bootstrap-installer/src-tauri/` and commit it, so the transitive Rust +dependency tree is auditable before the installer is distributed. + +### 6. Document the MPL/LGPL boundary + +**Action:** Add a section to `CONTRIBUTING.md` documenting that: +- MPL-2.0 deps are acceptable (file-level copyleft, MIT-compatible) but + modifications to MPL files must stay MPL +- LGPL-3.0 deps are acceptable (linking is fine) but the NOTICE file must + carry the LGPL text and document how to relink +- GPL/AGPL deps are NOT acceptable in the core or any workspace that ships + with Hermes โ€” they must be isolated in standalone sidecar processes + +--- + +## Appendix: tools used + +This audit was performed with: +- `uv.lock` parsing (233 Python packages, PEP 639 `license_expression` field) +- `package-lock.json` parsing (4 lockfiles, 2,401 unique npm packages) +- PyPI JSON API (`/pypi/{name}/{version}/json`) for Python license metadata +- npm registry API (`registry.npmjs.org`) for npm license metadata +- GitHub raw (`raw.githubusercontent.com`) for LICENSE file verification +- npm tarball inspection for packages with no metadata diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index b67e8b69f3a..bd3a802c0e6 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -8521,6 +8521,47 @@ ipcMain.handle('hermes:readFileText', async (_event, filePath) => { } }) +// Read a bundled license file from the app's extraResources directory. +// The desktop build generates dependencies.txt (JS deps) and +// dependencies-python.txt (Python deps) via the generate-licenses scripts, +// and electron-builder ships them via extraResources so they land at +// process.resourcesPath/ in the packaged app. In dev (running +// from a checkout), they live in apps/desktop/dist/. +// +// Returns the file contents as a string, or null if the file is missing. +// The Settings โ†’ Licenses page uses this to display third-party license +// attributions required by MIT/Apache/BSD licenses. +ipcMain.handle('hermes:readBundledLicenseFile', async (_event, filename: string) => { + if (!filename || typeof filename !== 'string') { + return null + } + + // Only allow plain filenames โ€” no path traversal. + const safe = path.basename(filename) + if (safe !== filename) { + return null + } + + // electron-builder extraResources place files directly in resources/. + // The Nix package preserves the renderer's dist/ directory instead, so + // support both layouts plus the local dev build location. + const candidates = [ + process.resourcesPath ? path.join(process.resourcesPath, safe) : null, + process.resourcesPath ? path.join(process.resourcesPath, 'dist', safe) : null, + path.join(APP_ROOT, 'dist', safe) + ].filter(Boolean) + + for (const p of candidates) { + try { + return await fs.promises.readFile(p, 'utf8') + } catch { + // ENOENT โ€” try next candidate + } + } + + return null +}) + ipcMain.handle('hermes:selectPaths', async (_event, options: any = {}) => { const properties = options?.directories ? ['openDirectory'] : ['openFile'] diff --git a/apps/desktop/electron/preload.ts b/apps/desktop/electron/preload.ts index 732d13a5366..1bfc43391e8 100644 --- a/apps/desktop/electron/preload.ts +++ b/apps/desktop/electron/preload.ts @@ -61,6 +61,7 @@ contextBridge.exposeInMainWorld('hermesDesktop', { requestMicrophoneAccess: () => ipcRenderer.invoke('hermes:requestMicrophoneAccess'), readFileDataUrl: filePath => ipcRenderer.invoke('hermes:readFileDataUrl', filePath), readFileText: filePath => ipcRenderer.invoke('hermes:readFileText', filePath), + readBundledLicenseFile: filename => ipcRenderer.invoke('hermes:readBundledLicenseFile', filename), selectPaths: options => ipcRenderer.invoke('hermes:selectPaths', options), writeClipboard: text => ipcRenderer.invoke('hermes:writeClipboard', text), saveImageFromUrl: url => ipcRenderer.invoke('hermes:saveImageFromUrl', url), diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 743269c1047..a7549f9db31 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -20,7 +20,7 @@ "profile:main:cpu": "wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .", "start": "npm run build && electron .", "prebuild": "tsc -b . --clean", - "build": "node scripts/assert-root-install.mjs && node scripts/write-build-stamp.mjs && vite build && node scripts/bundle-electron-main.mjs && node scripts/stage-native-deps.mjs", + "build": "node scripts/assert-root-install.mjs && node scripts/write-build-stamp.mjs && vite build && npm run generate-licenses && node scripts/bundle-electron-main.mjs && node scripts/stage-native-deps.mjs", "postbuild": "node scripts/assert-dist-built.mjs", "prebuilder": "node scripts/patch-electron-builder-mac-binary.mjs", "builder": "cross-env NODE_OPTIONS=--max-old-space-size=16384 node scripts/run-electron-builder.mjs", @@ -53,7 +53,10 @@ "check": "npm run typecheck && npm run test && npm run test:desktop:all", "test:e2e": "playwright test e2e/", "test:e2e:visual": "WLR_BACKENDS=headless WLR_NO_HARDWARE_CURSORS=1 cage -- npx playwright test e2e/ --reporter=list", - "test:e2e:update-snapshots": "WLR_BACKENDS=headless WLR_NO_HARDWARE_CURSORS=1 cage -- npx playwright test e2e/ --reporter=list --update-snapshots" + "test:e2e:update-snapshots": "WLR_BACKENDS=headless WLR_NO_HARDWARE_CURSORS=1 cage -- npx playwright test e2e/ --reporter=list --update-snapshots", + "generate-licenses": "node scripts/generate-js-licenses.mjs && node scripts/generate-python-licenses.mjs", + "generate-licenses:js": "node scripts/generate-js-licenses.mjs", + "generate-licenses:python": "node scripts/generate-python-licenses.mjs" }, "dependencies": { "@assistant-ui/react": "^0.14.23", @@ -151,6 +154,7 @@ "eslint-plugin-unused-imports": "^4.4.1", "globals": "^16.5.0", "jsdom": "^29.1.1", + "license-checker": "^25.0.1", "prettier": "^3.8.3", "rcedit": "^5.0.2", "tsx": "^4.22.4", @@ -194,6 +198,14 @@ { "from": "assets/icon.ico", "to": "icon.ico" + }, + { + "from": "dist/dependencies.txt", + "to": "dependencies.txt" + }, + { + "from": "dist/dependencies-python.txt", + "to": "dependencies-python.txt" } ], "asar": true, diff --git a/apps/desktop/scripts/generate-js-licenses.mjs b/apps/desktop/scripts/generate-js-licenses.mjs new file mode 100644 index 00000000000..f8b916fcaeb --- /dev/null +++ b/apps/desktop/scripts/generate-js-licenses.mjs @@ -0,0 +1,182 @@ +// Generates a bundled third-party license inventory for the desktop app's +// JavaScript dependencies. +// +// Scans the installed node_modules tree with license-checker, then emits +// dist/dependencies.txt โ€” a plain-text file containing each package's +// name, version, license, repository URL, and full license text when +// available. This file is shipped in the electron-builder app bundle via +// extraResources and surfaced in the Settings โ†’ Licenses page. +// +// Why license-checker (standalone CLI) instead of rollup-plugin-license: +// Vite 8 uses rolldown under the hood, and rollup-plugin-license is a +// rollup plugin โ€” compatibility is uncertain and the plugin approach +// only covers deps that end up in the bundle. license-checker scans the +// full node_modules tree, so the inventory is complete even for deps +// that are never imported at runtime (native modules, etc.). +import { execFileSync } from 'node:child_process' +import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +// __dirname = apps/desktop/scripts/ +// repoRoot = apps/desktop/scripts/../../.. = repo root +const repoRoot = path.resolve(__dirname, '..', '..', '..') +const distDir = path.resolve(__dirname, '..', 'dist') + +// We scan from the repo root, not the desktop workspace, because npm +// workspaces hoist all dependencies to the root node_modules/. Scanning +// from apps/desktop only finds the handful of deps that have their own +// node_modules (non-hoisted native modules etc.), while the repo root +// captures the full production dependency tree (77+ packages). +const SCAN_ROOT = repoRoot + +function runLicenseChecker() { + const bin = path.join(repoRoot, 'node_modules', '.bin', 'license-checker') + // --production: exclude devDependencies โ€” the shipped app only carries + // production deps in its bundle, so the license inventory matches. + // --json: structured output for easy post-processing. + // --relativeLicensePath: emit paths to LICENSE files so we can read them. + const args = ['--production', '--json', '--relativeLicensePath'] + const raw = execFileSync(bin, args, { + cwd: SCAN_ROOT, + encoding: 'utf8', + maxBuffer: 50 * 1024 * 1024, + stdio: ['pipe', 'pipe', 'pipe'], + }) + return JSON.parse(raw) +} + +function readLicenseFile(relPath) { + if (!relPath) { + return null + } + + // license-checker returns paths relative to the scan root (cwd). + // In a workspace, these look like "node_modules/foo/LICENSE" and + // resolve correctly from the workspace root. + const full = path.resolve(SCAN_ROOT, relPath) + if (!existsSync(full)) { + return null + } + + try { + return readFileSync(full, 'utf8').trim() + } catch { + return null + } +} + +// Format the inventory as a human-readable plain-text file. Each package +// gets a delimited block with its metadata + full license text, so the +// result is a self-contained NOTICE-equivalent. +function generatedAt() { + // Nix sets SOURCE_DATE_EPOCH for reproducible builds. Local builds retain a + // useful wall-clock timestamp when that conventional variable is absent. + const epoch = Number(process.env.SOURCE_DATE_EPOCH) + return new Date(Number.isFinite(epoch) && epoch > 0 ? epoch * 1000 : Date.now()).toISOString() +} + +function formatInventory(entries) { + const lines = [] + + lines.push('Third-Party Software Licenses') + lines.push('=============================') + lines.push('') + lines.push('This file lists the open-source licenses of the npm dependencies') + lines.push('bundled with the Hermes desktop application.') + lines.push('') + lines.push(`Generated: ${generatedAt()}`) + lines.push(`Total packages: ${entries.length}`) + lines.push('') + + for (const entry of entries) { + lines.push('โ”€'.repeat(70)) + lines.push(`Package: ${entry.name}@${entry.version}`) + lines.push(`License: ${entry.license}`) + if (entry.repository) { + lines.push(`Repository: ${entry.repository}`) + } + if (entry.publisher) { + lines.push(`Publisher: ${entry.publisher}`) + } + if (entry.email) { + lines.push(`Email: ${entry.email}`) + } + lines.push('') + if (entry.licenseText) { + lines.push(entry.licenseText) + } else { + lines.push('(No license text file found โ€” see the package repository for details.)') + } + lines.push('') + } + + return lines.join('\n') +} + +function main() { + mkdirSync(distDir, { recursive: true }) + + let data + try { + data = runLicenseChecker() + } catch (e) { + // license-checker not installed or failed โ€” write a stub so the build + // doesn't fail and the Settings page can show a graceful message. + const stub = [ + 'Third-Party Software Licenses', + '=============================', + '', + 'WARNING: license-checker was not found or failed to run.', + 'The JavaScript dependency license inventory could not be generated.', + '', + `Error: ${e.message}`, + '', + ].join('\n') + writeFileSync(path.join(distDir, 'dependencies.txt'), stub, 'utf8') + console.warn(`[generate-js-licenses] license-checker failed: ${e.message}`) + return + } + + // Dedupe by name@version โ€” the same package can appear once. + const seen = new Map() + + for (const [pkgKey, info] of Object.entries(data)) { + // pkgKey is "name@version" + const atIdx = pkgKey.lastIndexOf('@') + const name = atIdx > 0 ? pkgKey.slice(0, atIdx) : pkgKey + const version = atIdx > 0 ? pkgKey.slice(atIdx + 1) : '' + + const dedupeKey = `${name}@${version}` + if (seen.has(dedupeKey)) { + continue + } + + const licenseText = info.licenseFile + ? readLicenseFile(info.licenseFile) + : null + + seen.set(dedupeKey, { + name, + version, + license: info.licenses || info.license || 'UNKNOWN', + repository: info.repository || null, + publisher: info.publisher || null, + email: info.email || null, + licenseText, + }) + } + + const entries = [...seen.values()].sort((a, b) => + a.name.toLowerCase().localeCompare(b.name.toLowerCase()), + ) + + const output = formatInventory(entries) + const outPath = path.join(distDir, 'dependencies.txt') + writeFileSync(outPath, output, 'utf8') + + console.log(`[generate-js-licenses] Wrote ${entries.length} packages to ${outPath}`) +} + +main() diff --git a/apps/desktop/scripts/generate-python-licenses.mjs b/apps/desktop/scripts/generate-python-licenses.mjs new file mode 100644 index 00000000000..5a6bb9ec87d --- /dev/null +++ b/apps/desktop/scripts/generate-python-licenses.mjs @@ -0,0 +1,181 @@ +// Generates a bundled third-party license inventory for the Hermes Agent +// Python dependencies. +// +// Shells out to `pip-licenses` against the active Python environment, +// emitting dist/dependencies-python.txt โ€” a plain-text file containing +// each package's name, version, license, and full license text (when +// available in the installed dist-info). +// +// pip-licenses reads from the installed environment, so the inventory +// reflects whatever extras are actually installed on the build machine. +// --from=mixed is critical: it reads both the legacy License classifier +// AND the PEP 639 license_expression field, which is where modern +// packages (cryptography, pydantic, fastapi, etc.) declare their SPDX +// license. Without --from=mixed, ~30% of packages show as UNKNOWN. +import { execFileSync } from 'node:child_process' +import { mkdirSync, writeFileSync, existsSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +// __dirname = apps/desktop/scripts/; three parents is the repository root. +const repoRoot = path.resolve(__dirname, '..', '..', '..') +const distDir = path.resolve(__dirname, '..', 'dist') + +// Locate the Python environment to scan. Resolution order: +// 1. HERMES_PYTHON (Nix dev shell / Nix desktop build) +// 2. VIRTUAL_ENV (explicit activated venv) +// 3. repo-root .venv or venv (local dev worktree) +// 4. HERMES_HOME/hermes-agent/venv (managed install, matches main.ts) +// 5. pip-licenses on PATH (fallback) +function findPipLicensesBin() { + // Nix provides the exact Python interpreter through HERMES_PYTHON. It is + // a store path rather than a traditional venv directory, so looking for + // bin/pip-licenses next to it does not work; invoke the installed module. + if (process.env.HERMES_PYTHON && existsSync(process.env.HERMES_PYTHON)) { + return { type: 'module', path: process.env.HERMES_PYTHON, venv: null } + } + + // pip-licenses is a console script installed into the venv's bin/Scripts + // dir. We look for it relative to candidate venv roots. + const binName = process.platform === 'win32' ? 'pip-licenses.exe' : 'pip-licenses' + const scriptName = process.platform === 'win32' ? 'pip-licenses-script.py' : 'pip-licenses' + + const candidates = [] + + if (process.env.VIRTUAL_ENV) { + candidates.push(process.env.VIRTUAL_ENV) + } + + candidates.push(path.join(repoRoot, '.venv')) + candidates.push(path.join(repoRoot, 'venv')) + + if (process.env.HERMES_HOME) { + candidates.push(path.join(process.env.HERMES_HOME, 'hermes-agent', 'venv')) + } + + // Check $HOME/.hermes/hermes-agent/venv (the managed-install default) + candidates.push(path.join(process.env.HOME || '', '.hermes', 'hermes-agent', 'venv')) + + for (const venv of candidates) { + const binDir = process.platform === 'win32' + ? path.join(venv, 'Scripts') + : path.join(venv, 'bin') + const binPath = path.join(binDir, binName) + const scriptPath = path.join(binDir, scriptName) + + if (existsSync(binPath)) { + return { type: 'bin', path: binPath, venv } + } + if (existsSync(scriptPath)) { + // Need to run it with the venv's python + const pythonBin = process.platform === 'win32' + ? path.join(venv, 'Scripts', 'python.exe') + : path.join(venv, 'bin', 'python') + if (existsSync(pythonBin)) { + return { type: 'module', path: pythonBin, venv } + } + } + } + + // Fallback: assume pip-licenses is on PATH (CI/dev shells) + return { type: 'path', path: 'pip-licenses', venv: null } +} + +function runPipLicenses() { + const { type, path: binPath } = findPipLicensesBin() + + // Flags: + // --from=mixed โ€” read PEP 639 license_expression AND legacy classifier + // --with-license-file โ€” include the full license text from the dist-info + // --with-notice-file โ€” include NOTICE files (Apache-2.0 ยง4(d) requirement) + // --no-license-path โ€” don't emit the on-disk path (we're bundling, not linking) + // --format=plain-vertical โ€” one package per block, readable as a NOTICE file + // + // Note: --with-license-file prints a warning about long fields with + // plain-vertical format, but the output is still correct โ€” the warning + // is cosmetic. We suppress stderr to keep the build log clean. + const args = [ + '--from=mixed', + '--with-license-file', + '--with-notice-file', + '--no-license-path', + '--format=plain-vertical', + ] + + let result + if (type === 'module') { + // python -m piplicenses + result = execFileSync(binPath, ['-m', 'piplicenses', ...args], { + encoding: 'utf8', + maxBuffer: 50 * 1024 * 1024, + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, PYTHONUNBUFFERED: '1' }, + }) + } else { + // Direct binary or PATH lookup + result = execFileSync(binPath, args, { + encoding: 'utf8', + maxBuffer: 50 * 1024 * 1024, + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, PYTHONUNBUFFERED: '1' }, + }) + } + + return result +} + +function generatedAt() { + // Nix sets SOURCE_DATE_EPOCH for reproducible builds. Local builds retain a + // useful wall-clock timestamp when that conventional variable is absent. + const epoch = Number(process.env.SOURCE_DATE_EPOCH) + return new Date(Number.isFinite(epoch) && epoch > 0 ? epoch * 1000 : Date.now()).toISOString() +} + +function main() { + mkdirSync(distDir, { recursive: true }) + + let output + try { + output = runPipLicenses() + } catch (e) { + // pip-licenses not installed โ€” write a stub so the build doesn't fail + // and the Settings page can show a graceful "not available" message. + output = [ + 'Third-Party Software Licenses (Python)', + '=======================================', + '', + 'WARNING: pip-licenses was not found in the Python environment.', + 'The Python dependency license inventory could not be generated.', + '', + 'To generate this file, install pip-licenses in the hermes venv:', + ' pip install pip-licenses', + '', + `Error: ${e.message}`, + '', + ].join('\n') + console.warn(`[generate-python-licenses] pip-licenses not available: ${e.message}`) + } + + // Prepend a header so the file is self-documenting + const header = [ + 'Third-Party Software Licenses (Python)', + '=======================================', + '', + 'This file lists the open-source licenses of the Python dependencies', + 'of the Hermes Agent runtime bundled with the Hermes desktop app.', + '', + `Generated: ${generatedAt()}`, + '', + ].join('\n') + + const outPath = path.join(distDir, 'dependencies-python.txt') + writeFileSync(outPath, header + output, 'utf8') + + // Count packages: in plain-vertical format, each block is name\nversion\nlicense\ntext + // We approximate by counting version-like lines (X.Y.Z or X.Y patterns). + const pkgCount = (output.match(/^\d+\.\d+/gm) || []).length + console.log(`[generate-python-licenses] Wrote ~${pkgCount} packages to ${outPath}`) +} + +main() diff --git a/apps/desktop/src/app/settings/index.tsx b/apps/desktop/src/app/settings/index.tsx index f51cca30f94..a3666a91615 100644 --- a/apps/desktop/src/app/settings/index.tsx +++ b/apps/desktop/src/app/settings/index.tsx @@ -11,6 +11,7 @@ import { BarChart3, Bell, Download, + FileText, Globe, Info, Keyboard, @@ -38,6 +39,7 @@ import { SECTIONS } from './constants' import { GatewaySettings } from './gateway-settings' import { KeybindSettings } from './keybind-settings' import { KEYS_VIEWS, KeysSettings, type KeysView } from './keys-settings' +import { LicensesSettings } from './licenses-settings' import { NotificationsSettings } from './notifications-settings' import { PluginsSettings } from './plugins-settings' import { PROVIDER_VIEWS, ProvidersSettings, type ProviderView } from './providers-settings' @@ -54,7 +56,8 @@ const SETTINGS_VIEWS: readonly SettingsViewId[] = [ 'billing', 'plugins', 'sessions', - 'about' + 'about', + 'licenses' ] export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: SettingsPageProps) { @@ -249,6 +252,13 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set id: 'about', label: t.settings.nav.about, onSelect: () => setActiveView('about') + }, + { + active: activeView === 'licenses', + icon: FileText, + id: 'licenses', + label: t.settings.nav.licenses, + onSelect: () => setActiveView('licenses') } ] @@ -293,6 +303,8 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set ) : activeView === 'about' ? ( + ) : activeView === 'licenses' ? ( + ) : activeView === 'gateway' ? ( ) : activeView === 'keybinds' ? ( diff --git a/apps/desktop/src/app/settings/licenses-settings.tsx b/apps/desktop/src/app/settings/licenses-settings.tsx new file mode 100644 index 00000000000..183ce41f9e0 --- /dev/null +++ b/apps/desktop/src/app/settings/licenses-settings.tsx @@ -0,0 +1,145 @@ +import { useEffect, useState } from 'react' + +import { Button } from '@/components/ui/button' +import { Codicon } from '@/components/ui/codicon' +import { useI18n } from '@/i18n' +import { FileText, Loader2, AlertCircle } from '@/lib/icons' + +import { ListRow, SectionHeading, SettingsContent } from './primitives' + +type LicenseSource = { + key: string + filename: string + label: string + description: string +} + +const LICENSE_SOURCES: readonly LicenseSource[] = [ + { + key: 'js', + filename: 'dependencies.txt', + label: 'JavaScript Dependencies', + description: 'npm packages bundled with the desktop app renderer and Electron main process.' + }, + { + key: 'python', + filename: 'dependencies-python.txt', + label: 'Python Dependencies', + description: 'Python packages from the Hermes Agent runtime environment.' + } +] + +async function loadLicenseFile(filename: string): Promise { + const bridge = window.hermesDesktop + if (!bridge?.readBundledLicenseFile) { + return null + } + + return bridge.readBundledLicenseFile(filename) +} + +function LicenseFileBlock({ source }: { source: LicenseSource }) { + const [content, setContent] = useState(undefined) + + useEffect(() => { + let cancelled = false + void loadLicenseFile(source.filename).then(result => { + if (!cancelled) { + setContent(result) + } + }) + return () => { + cancelled = true + } + }, [source.filename]) + + const isLoading = content === undefined + const isMissing = content === null + const isEmpty = content !== null && content !== undefined && content.trim() === '' + + return ( +
+ + +
+ {isLoading ? ( +
+ + Loadingโ€ฆ +
+ ) : isMissing ? ( +
+ +
+

License file not found

+

+ Run npm run generate-licenses in{' '} + apps/desktop to generate it, or it was + not bundled with this build. +

+
+
+ ) : isEmpty ? ( +
+ License file is empty. +
+ ) : ( + <> +
+ + {source.description} + + +
+
+              {content}
+            
+ + )} +
+
+ ) +} + +export function LicensesSettings() { + return ( + +
+ + + + + {LICENSE_SOURCES.map(source => ( + + ))} + + +
+
+ ) +} diff --git a/apps/desktop/src/app/settings/types.ts b/apps/desktop/src/app/settings/types.ts index 2828609ef63..dc250277065 100644 --- a/apps/desktop/src/app/settings/types.ts +++ b/apps/desktop/src/app/settings/types.ts @@ -10,6 +10,7 @@ export type SettingsView = | 'gateway' | 'keybinds' | 'keys' + | 'licenses' | 'notifications' | 'plugins' | 'providers' diff --git a/apps/desktop/src/global.d.ts b/apps/desktop/src/global.d.ts index 9202c28e7fc..e6809af3263 100644 --- a/apps/desktop/src/global.d.ts +++ b/apps/desktop/src/global.d.ts @@ -76,6 +76,7 @@ declare global { requestMicrophoneAccess: () => Promise readFileDataUrl: (filePath: string) => Promise readFileText: (filePath: string) => Promise + readBundledLicenseFile: (filename: string) => Promise selectPaths: (options?: HermesSelectPathsOptions) => Promise writeClipboard: (text: string) => Promise saveImageFromUrl: (url: string) => Promise diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index a17ecbb3bd6..25dcd5806b5 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -325,7 +325,8 @@ export const en: Translations = { about: 'About', billing: 'Billing', notifications: 'Notifications', - plugins: 'Plugins' + plugins: 'Plugins', + licenses: 'Licenses' }, plugins: { title: 'Desktop plugins', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 3ade882c6ab..a9319c89fc3 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -225,7 +225,8 @@ export const ja = defineLocale({ archivedChats: 'ใ‚ขใƒผใ‚ซใ‚คใƒ–ๆธˆใฟใƒใƒฃใƒƒใƒˆ', about: 'ๆƒ…ๅ ฑ', billing: '่ซ‹ๆฑ‚', - notifications: '้€š็Ÿฅ' + notifications: '้€š็Ÿฅ', + licenses: 'ใƒฉใ‚คใ‚ปใƒณใ‚น' }, notifications: { title: '้€š็Ÿฅ', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index 25a9f67e219..cbe3f06ed59 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -284,6 +284,7 @@ export interface Translations { billing: string notifications: string plugins: string + licenses: string } plugins: { title: string diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index d061a6d57a7..8f0b55d70c6 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -219,7 +219,8 @@ export const zhHant = defineLocale({ archivedChats: 'ๅทฒๅฐๅญ˜่Šๅคฉ', about: '้—œๆ–ผ', billing: 'ๅธณๅ–ฎ', - notifications: '้€š็Ÿฅ' + notifications: '้€š็Ÿฅ', + licenses: 'ๆŽˆๆฌŠๆขๆฌพ' }, notifications: { title: '้€š็Ÿฅ', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 61d6ff602aa..65a360d02f5 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -316,7 +316,8 @@ export const zh: Translations = { about: 'ๅ…ณไบŽ', billing: '่ดฆๅ•', notifications: '้€š็Ÿฅ', - plugins: 'ๆ’ไปถ' + plugins: 'ๆ’ไปถ', + licenses: '่ฎธๅฏ่ฏ' }, plugins: { title: 'ๆกŒ้ขๆ’ไปถ', diff --git a/nix/desktop.nix b/nix/desktop.nix index cb18a4f0491..a62345c988d 100644 --- a/nix/desktop.nix +++ b/nix/desktop.nix @@ -14,6 +14,7 @@ hermesNpmLib, electron, hermesAgent, + licensePython, ... }: let @@ -72,11 +73,15 @@ let # typecheck :3 npm exec tsc -b - # build the renderer bundle - # vite's emptyOutDir wipes dist/ on every run - # so it has to be first + # Build the renderer bundle first: Vite's emptyOutDir wipes dist/. npm exec vite build + # Generate attribution files AFTER Vite, otherwise emptyOutDir would + # delete them. HERMES_PYTHON points at the sealed nix venv that has + # hermes's locked runtime closure plus pip-licenses from [dev]. + HERMES_PYTHON=${licensePython}/bin/python3 node scripts/generate-js-licenses.mjs + HERMES_PYTHON=${licensePython}/bin/python3 node scripts/generate-python-licenses.mjs + # build the electron bundle node scripts/bundle-electron-main.mjs diff --git a/nix/hermes-agent.nix b/nix/hermes-agent.nix index 043d1e94202..57f4ee4f719 100644 --- a/nix/hermes-agent.nix +++ b/nix/hermes-agent.nix @@ -225,6 +225,11 @@ stdenv.mkDerivation (finalAttrs: { passthru = let + # A sealed, non-editable venv containing the normal [all] runtime + # groups plus [dev] tooling. Desktop's Nix renderer build uses this + # only for pip-licenses, so the generated attribution inventory matches + # the locked Python closure without making license tooling a runtime dep. + licensePython = (mkHermesVenv (extraDependencyGroups ++ [ "dev" ])).venv; devPython = (mkHermesVenv (extraDependencyGroups ++ [ "dev" ])).editableVenv; in { @@ -233,6 +238,7 @@ stdenv.mkDerivation (finalAttrs: { hermesWeb hermesNpmLib hermesVenv + licensePython ; # `hermesDesktop` references `finalAttrs.finalPackage` (this whole @@ -243,7 +249,7 @@ stdenv.mkDerivation (finalAttrs: { # runtime PATH (ripgrep/git/ffmpeg/etc). No re-implementation # of the agent resolution in the desktop wrapper. hermesDesktop = callPackage ./desktop.nix { - inherit hermesNpmLib electron; + inherit hermesNpmLib electron licensePython; hermesAgent = finalAttrs.finalPackage; }; diff --git a/package-lock.json b/package-lock.json index 3236f2e7310..b9fd225d6a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,6 +178,7 @@ "eslint-plugin-unused-imports": "^4.4.1", "globals": "^16.5.0", "jsdom": "^29.1.1", + "license-checker": "^25.0.1", "prettier": "^3.8.3", "rcedit": "^5.0.2", "tsx": "^4.22.4", @@ -7368,6 +7369,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -7489,6 +7500,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/asn1js": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", @@ -9202,6 +9220,17 @@ } } }, + "node_modules/debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", @@ -9386,6 +9415,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -13153,6 +13193,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -13321,6 +13368,147 @@ "node": ">= 0.8.0" } }, + "node_modules/license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker": "bin/license-checker" + } + }, + "node_modules/license-checker/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/license-checker/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/license-checker/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/license-checker/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/license-checker/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/license-checker/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/license-checker/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/license-checker/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/license-checker/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -14803,7 +14991,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -15060,6 +15247,58 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/normalize-package-data/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -15073,6 +15312,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true, + "license": "ISC" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -15254,6 +15500,38 @@ "node": ">= 0.8.0" } }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -16438,6 +16716,49 @@ "read-binary-file-arch": "cli.js" } }, + "node_modules/read-installed": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", + "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/read-installed/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-package-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -16461,6 +16782,20 @@ "dev": true, "license": "MIT" }, + "node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -17412,6 +17747,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "*" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -17452,6 +17797,73 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "dev": true, + "license": "(MIT AND CC-BY-3.0)" + }, + "node_modules/spdx-satisfies": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", + "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -18152,6 +18564,16 @@ "tree-kill": "cli.js" } }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -18803,6 +19225,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/util-extend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", + "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", + "dev": true, + "license": "MIT" + }, "node_modules/uuid": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", @@ -18822,6 +19251,17 @@ "integrity": "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A==", "license": "MIT" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/pyproject.toml b/pyproject.toml index c630b3cf7bb..23296f37931 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,7 +157,7 @@ edge-tts = ["edge-tts==7.2.7"] modal = ["modal==1.3.4"] daytona = ["daytona==0.155.0"] hindsight = ["hindsight-client==0.6.1"] -dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "mcp==1.26.0", "starlette==1.0.1", "ty==0.0.21", "ruff==0.15.10", "setuptools==81.0.0"] # starlette: CVE-2026-48710; setuptools: latest <82 (torch >=2.11 caps setuptools<82) +dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "mcp==1.26.0", "starlette==1.0.1", "ty==0.0.21", "ruff==0.15.10", "setuptools==81.0.0", "pip-licenses==5.0.0"] # starlette: CVE-2026-48710; setuptools: latest <82 (torch >=2.11 caps setuptools<82); pip-licenses: generate THIRD_PARTY_LICENSES at desktop build time messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.14.1", "brotlicffi==1.2.0.1", "slack-bolt==1.29.0", "slack-sdk==3.43.0", "qrcode==7.4.2"] # aiohttp 3.14.1: CVE-2026-34513/34518/34519/34520/34525 + 34993(RCE)/47265 cron = [] # croniter is now a core dependency; this extra kept for back-compat slack = ["slack-bolt==1.29.0", "slack-sdk==3.43.0", "aiohttp==3.14.1"] diff --git a/uv.lock b/uv.lock index 257f7d69645..5bdfeed1deb 100644 --- a/uv.lock +++ b/uv.lock @@ -1591,6 +1591,7 @@ daytona = [ dev = [ { name = "debugpy" }, { name = "mcp" }, + { name = "pip-licenses" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, @@ -1811,6 +1812,7 @@ requires-dist = [ { name = "parallel-web", marker = "extra == 'parallel-web'", specifier = "==0.4.2" }, { name = "pathspec", specifier = "==1.1.1" }, { name = "pillow", specifier = "==12.2.0" }, + { name = "pip-licenses", marker = "extra == 'dev'", specifier = "==5.0.0" }, { name = "prompt-toolkit", specifier = "==3.0.52" }, { name = "psutil", specifier = "==7.2.2" }, { name = "ptyprocess", marker = "sys_platform != 'win32'", specifier = ">=0.7.0,<1" }, @@ -3013,6 +3015,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, ] +[[package]] +name = "pip-licenses" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prettytable" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/49/d36a3ddb73d22970a35afa3e9fd53c8318150f8122e4257ca9875f1d4e38/pip_licenses-5.0.0.tar.gz", hash = "sha256:0633a1f9aab58e5a6216931b0e1d5cdded8bcc2709ff563674eb0e2ff9e77e8e", size = 41542, upload-time = "2024-07-23T10:48:29.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/0a/bfaf1479d09d19f503a669d9c8e433ac59ae687fb8da1d8207eb85c5a9f4/pip_licenses-5.0.0-py3-none-any.whl", hash = "sha256:82c83666753efb86d1af1c405c8ab273413eb10d6689c218df2f09acf40e477d", size = 20497, upload-time = "2024-07-23T10:48:27.59Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -3049,6 +3064,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c3/07/ca761fd0e3b23e9bfeaaeeffa5df4c235fd0c5086d89934553583e6902f3/posthog-7.21.0-py3-none-any.whl", hash = "sha256:11dca1e9772bedcb6721751fb0945fa8dcddbf9a4e72e2430483b53cdb8d1ec0", size = 370932, upload-time = "2026-06-26T15:56:59.844Z" }, ] +[[package]] +name = "prettytable" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/74/ba08d81e668ccfe8658d7520a307e63c19862c08eb4ccb26f356c5239a7a/prettytable-3.18.0.tar.gz", hash = "sha256:439217116152244369caf3d9f1caf2f9fe29b03bd79e88d2928c8e718c95d680", size = 76373, upload-time = "2026-06-22T16:07:50.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/be/2e6798ace5cc036f5d05d36b7b2fd85346f1a708c87060890b070d0ec607/prettytable-3.18.0-py3-none-any.whl", hash = "sha256:b3346e0e6f79180833aebaac088ae926340586cf6d7d991b9eb125b65f72313a", size = 37357, upload-time = "2026-06-22T16:07:48.595Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -4105,6 +4132,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, ] +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "tornado" version = "6.5.5"